diff --git a/.ci/macos.entitlements b/.ci/macos.entitlements
new file mode 100644
index 000000000..2bff1d681
--- /dev/null
+++ b/.ci/macos.entitlements
@@ -0,0 +1,17 @@
+
+
+
+
+ com.apple.security.app-sandbox
+
+
+ com.apple.security.cs.disable-library-validation
+
+
+ com.apple.security.cs.disable-executable-page-protection
+
+
+ com.apple.security.cs.allow-unsigned-executable-memory
+
+
+
diff --git a/.ci/release_template.md b/.ci/release_template.md
index 75671f90e..6e53a3ee1 100644
--- a/.ci/release_template.md
+++ b/.ci/release_template.md
@@ -4,23 +4,30 @@
git push -d origin --REPLACE-WITH-BETA-LIST--
-->
-
+
-Pre-compiled binaries we serve:
- - Windows 10+
- - Windows 7+
- - macOS 13+ ("Ventura") / Apple M
- - macOS 13+ ("Ventura") / Intel
- - Ubuntu 24.04 LTS ("Noble Numbat")
- - Ubuntu 22.04 LTS ("Jammy Jellyfish")
- - Ubuntu 20.04 LTS ("Focal Fossa")
- - Debian 12 ("Bookworm")
- - Debian 11 ("Bullseye")
- - Fedora 41
- - Fedora 40
-We are also packaged in Arch Linux's official "extra" repository, courtesy of @FFY00
-General Linux support is available via a flatpak package (Flathub)
+Available pre-compiled binaries for installation:
+
+ Windows
+ • Windows 10+
+ • Windows 7+
+
+ macOS
+ • macOS 15+ Sequoia Apple M
+ • macOS 14+ Sonoma Apple M
+ • macOS 13+ Ventura Intel
+
+ Linux
+ • Ubuntu 24.04 LTS Noble Numbat
+ • Ubuntu 22.04 LTS Jammy Jellyfish
+ • Ubuntu 20.04 LTS Focal Fossa
+ • Debian 12 Bookworm
+ • Debian 11 Bullseye
+ • Fedora 41
+ • Fedora 40
+
+We are also packaged in Arch Linux's official extra repository, courtesy of @FFY00.
+General Linux support is available via a flatpak package at Flathub!
@@ -28,22 +35,24 @@ include different targets -->
We're pleased to announce the newest official release: --REPLACE-WITH-RELEASE-TITLE--
-We hope you enjoy the changes made and we have listed all changes, with their corresponding tickets, since the last version of Cockatrice was released for your convenience.
+We hope you enjoy the changes made! All improvements with their corresponding tickets since the last version of Cockatrice are listed in the changelog below.
-If you ever encounter a bug, have a suggestion or idea, or feel a need for a developer to look into something, please feel free to [open a ticket](https://github.com/Cockatrice/Cockatrice/issues). ([How to create a GitHub Ticket for Cockatrice](https://github.com/Cockatrice/Cockatrice/wiki/How-to-Create-a-GitHub-Ticket-Regarding-Cockatrice))
+If you ever encounter a bug, have a suggestion or idea, or feel a need for a developer to look into something, please feel free to [open a ticket](https://github.com/Cockatrice/Cockatrice/issues). ([How to create a Ticket for Cockatrice](https://github.com/Cockatrice/Cockatrice/wiki/How-to-Create-a-GitHub-Ticket-Regarding-Cockatrice))
-For any information relating to Cockatrice, please take a look at our official site: **https://cockatrice.github.io**
+For basic information related to the app and getting started, please take a look at our official site: **https://cockatrice.github.io**
-If you'd like to help contribute to Cockatrice in any way, check out our [README](https://github.com/Cockatrice/Cockatrice#get-involved-). We're always available to answer questions you may have on how the program works and how you can provide a meaningful contribution.
+If you'd like to help and contribute to Cockatrice in any way, check out our [README](https://github.com/Cockatrice/Cockatrice#get-involved-).
+We're always available to answer questions you may have on how the program works and how you can provide a meaningful contribution.
## Upgrading Cockatrice
-- Run the internal software updater: Help → Check for Client Updates
+Run the internal software updater: Help → Check for Client Updates
Don't forget to update your card database right after! (Help → Check for Card Updates...)
@@ -60,14 +69,14 @@ Remove empty headers when done.
-->
-### ⚠️ Important:
+### 🔖 Highlights:
### ✨ New Features:
-### 🐛 Fixed Bugs / Resolved issues:
+### 🐛 Fixed Bugs / Resolved Issues:
-📘 Show all changes (--REPLACE-WITH-COMMIT-COUNT-- commits)
+Show all changes (--REPLACE-WITH-COMMIT-COUNT-- commits)
### User Interface
@@ -88,5 +97,6 @@ Remove empty headers when done.
## Special Thanks
-We continue to find it amazing that so many people contribute their time, knowledge, code, testing and more to the project. We'd like to thank the entire Cockatrice community for their efforts.
+It's amazing that so many people contribute their time, knowledge, code, testing and more to the project.
+We'd like to thank the entire Cockatrice community for their efforts! 🙏
diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md
index 2db045819..10ffa3493 100644
--- a/.github/CONTRIBUTING.md
+++ b/.github/CONTRIBUTING.md
@@ -7,32 +7,33 @@
# Contributing to Cockatrice #
-First off, thanks for taking the time to contribute to our project! 🎉 ❤ ️✨
-The following is a set of guidelines for contributing to Cockatrice. These are
-mostly guidelines, not rules. Use your best judgment, and feel free to propose
-changes to this document in a pull request.
+First off, thanks for taking the time and considering to lend a helping hand to our project! 🎉 ❤ ️✨
+
+> [!NOTE]
+> The following is a set of guidelines for contributing to Cockatrice.
+> These are mostly guidelines, not rules. Use your best judgment, and feel free to
+> propose changes to this document in a pull request.
+>
+> [](
+> https://discord.gg/3Z9yzmA)
+> If you'd like to ask questions, get advice, or just want to say "Hi",
+> the Cockatrice Development Team uses [Discord](https://discord.gg/ZASRzKu)
+> for communications and you can reach out in the `#dev` channel.
# Recommended Setups #
-For those developers who like the Linux or MacOS environment, many of our
+For those developers on **Linux** or **macOS** environment, many of our
developers like working with a nifty program called [CLion](
-https://www.jetbrains.com/clion/). The program's a great asset and one of the
-best tools you'll find on these systems, but you're welcomed to use any IDE
-you most enjoy.
+https://www.jetbrains.com/clion/). The program is a great asset and one of the
+best tools you'll find on these systems.
-Developers who like Windows development tend to find [Visual Studio](
+Developers on **Windows** systems tend to find [Visual Studio](
https://www.visualstudio.com/) the best tool for the job.
-[](https://discord.gg/ZASRzKu)
-[](https://gitter.im/Cockatrice/Cockatrice)
-
-If you'd like to ask questions, get advice, or just want to say hi,
-the Cockatrice Development Team uses [Discord](https://discord.gg/ZASRzKu)
-for communications in the #dev channel. If you're not into Discord, we also
-have a [Gitter](https://gitter.im/Cockatrice/Cockatrice) channel available,
-albeit slightly less active.
+But you're welcomed to use any IDE you enjoy most of course!
# Code Style Guide #
@@ -54,7 +55,7 @@ The message will look like this:
*** Then commit and push those changes to this branch. ***
*** Check our CONTRIBUTING.md file for more details. ***
*** ***
-*** Thank you ❤️ ***
+*** Thank you ❤️ ***
*** ***
***********************************************************
```
@@ -64,7 +65,7 @@ information on our formatting guidelines.
### Compatibility ###
-Cockatrice is currently compiled on all platforms using C++11.
+Cockatrice is currently compiled on all platforms using C++20.
You'll notice C++03 code throughout the codebase. Please feel free
to help convert it over!
@@ -78,11 +79,12 @@ or other appropriate conversion.
### Formatting ###
The handy tool `clang-format` can format your code for you, it is available for
-almost any environment. A special `.clang-format` configuration file is
+almost any environment. A special [`.clang-format`](
+https://github.com/Cockatrice/Cockatrice/blob/master/.clang-format) configuration file is
included in the project and is used to format your code.
We've also included a bash script, `format.sh`, that will use clang-format to
-format all files in your pr in one go. Use `./format.sh --help` to show a full
+format all files in your PR in one go. Use `./format.sh --help` to show a full
help page.
To run clang-format on a single source file simply use the command
@@ -90,10 +92,10 @@ To run clang-format on a single source file simply use the command
clang-format with a specific version number appended,
`find /usr/bin -name clang-format*` should find it for you)
-See [the clang-format documentation](
+See the [clang-format documentation](
https://clang.llvm.org/docs/ClangFormat.html) for more information about the tool.
-#### Header files ####
+#### Header Files ####
Use header files with the extension `.h` and source files with the extension
`.cpp`.
@@ -168,10 +170,10 @@ braces around single line statements is preferred.
See the following example:
```c++
int main()
-{ // function or class: own line
- if (someCondition) { // control statement: same line
- doSomething(); // single line statement, braces preferred
- } else if (someOtherCondition1) { // else goes on the same line as a closing brace
+{ // function or class: own line
+ if (someCondition) { // control statement: same line
+ doSomething(); // single line statement, braces preferred
+ } else if (someOtherCondition1) { // else goes on the same line as a closing brace
for (int i = 0; i < 100; i++) {
doSomethingElse();
}
@@ -234,7 +236,7 @@ mutating objects.)
When pointers can't be avoided, try to use a smart pointer of some sort, such
as `QScopedPointer`, or, less preferably, `QSharedPointer`.
-### Database migrations ###
+### Database Migrations ###
The servatrice database's schema can be found at `servatrice/servatrice.sql`.
Everytime the schema gets modified, some other steps are due:
@@ -255,7 +257,7 @@ Ensure that the migration produces the expected effects; e.g. if you add a
new column, make sure the migration places it in the same order as
servatrice.sql.
-### Protocol buffer ###
+### Protocol Buffer ###
Cockatrice and Servatrice exchange data using binary messages. The syntax of
these messages is defined in the `proto` files in the `common/pb` folder. These
@@ -268,6 +270,7 @@ new clients incompatible to the old server and vice versa.
You can find more information on how we use Protobuf on [our wiki!](
https://github.com/Cockatrice/Cockatrice/wiki/Client-server-protocol)
+
# Reviewing Pull Requests #
After you have finished your changes to the project you should put them on a
@@ -286,6 +289,7 @@ all changes have been approved your pull request will be squashed into a single
commit and merged into the master branch by a team member. Your change will then
be included in the next release 👍
+
# Translations #
Basic workflow for translations:
@@ -294,16 +298,16 @@ Basic workflow for translations:
3. Maintainer verifies and merges the change;
4. Transifex picks up the new files from GitHub automatically;
5. Translators translate the new untranslated strings on Transifex;
- 6. Before a release, a maintainer fetches the updated translations from Transifex.
+ 6. Before a release, a maintainer fetches the newest translations from Transifex.
### Using Translations (for developers) ###
All user interface strings inside Cockatrice's source code must be written
in English (US).
Translations to other languages are managed using [Transifex](
-https://www.transifex.com/projects/p/cockatrice/).
+https://transifex.com/cockatrice/cockatrice/).
-Adding a new string to translate is as easy as adding the string in the
+Adding a new string for translation is as easy as adding the string in the
`tr("")` function, the string will be picked up as translatable automatically
and translated as needed.
For example, setting the text of a label in the way that the string
@@ -315,9 +319,9 @@ nameLabel.setText(tr("My name is:"));
To translate a string that would have plural forms you can add the amount to
the tr() call, also you can add an extra string as a hint for translators:
```c++
-QString message = tr("Everyone draws %n cards", "pop up message", amount);
+QString message = tr("Everyone draws %n cards", "english hint for translators", amount);
```
-See [QT's wiki on translations](
+See [Qt's wiki on translations](
https://doc.qt.io/qt-5/i18n-source-translation.html#handling-plurals)
If you're about to propose a change that adds or modifies any translatable
@@ -325,7 +329,7 @@ string in the code, you don't need to take care of adding the new strings to
the translation files.
We have an automated process to update our language source files on a schedule
and provide the translators on Transifex with the new contents.
-Maintainers can also manually trigger this on demand.
+Maintainers can also manually trigger this workflow on demand via GitHub Actions.
### Maintaining Translations (for maintainers) ###
@@ -389,27 +393,27 @@ Now you are ready to commit your changes and open a PR.
Once the changes get merged, Transifex will pick up the modified files
-automatically (checked every few hours) and update their online editor where
+automatically (checked every few hours) and update the web editor where
translators will be able to translate the new strings right in the browser.
### Releasing Translations (for maintainers) ###
-Before rushing out a new release, a maintainer should fetch the most up to date
+Before publishing a new release, a maintainer should fetch the most up to date
translations from Transifex and commit them into the Cockatrice source code.
-This can be done manually from the Transifex web interface, but it's quite time
-consuming.
-
-As an alternative, you can install the Transifex CLI:
-
- http://docs.transifex.com/developer/client/
+We utilize the official GitHub integration to push all languages that are 100%
+translated from Transifex to our GitHub repo automatically.
+On top, it runs on a quarterly schedule to update changes for incomplete languages.
+A synchronisation/update can also be triggered manually from the Transifex web interface
+and a translation treshold can be set.
+As an alternative, you can install the [Transifex CLI](https://developers.transifex.com/docs/cli).
You'll then be able to use a git-like cli command to push and pull translations
from Transifex to the source code and vice versa.
### Adding Translations (for translators) ###
-As a translator you can help translate the new strings on [Transifex](
-https://www.transifex.com/projects/p/cockatrice/).
+As a translator, you can help to translate new strings on [Transifex](
+https://www.transifex.com/projects/p/cockatrice/) to your native language.
Please have a look at the specific [FAQ for translators](
https://github.com/Cockatrice/Cockatrice/wiki/Translation-FAQ).
@@ -419,9 +423,9 @@ https://github.com/Cockatrice/Cockatrice/wiki/Translation-FAQ).
### Publishing A New Release ###
We use [GitHub Releases](https://github.com/Cockatrice/Cockatrice/releases) to
-publish new stable versions and betas.
-Whenever a git tag is pushed to the repository github will create a draft
-release and upload binaries automatically.
+publish new stable versions and beta releases.
+Whenever a git tag is pushed to the repository, GitHub will create a draft
+release and upload binaries from our CI automatically.
To create a tag, simply do the following:
```bash
@@ -433,18 +437,16 @@ git push $UPSTREAM $TAG_NAME
```
You should define the variables as such:
-```
-`$UPSTREAM` - the remote for git@github.com:Cockatrice/Cockatrice.git
-`$TAG_NAME` should be formatted as:
- - `YYYY-MM-DD-Release-MAJ.MIN.PATCH` for **stable releases**
- - `YYYY-MM-DD-Development-MAJ.MIN.PATCH-beta.X` for **beta releases**
- With *MAJ.MIN.PATCH* being the NEXT release version!
-```
+ - `$UPSTREAM`: the remote for `git@github.com:Cockatrice/Cockatrice.git`
+ - `$TAG_NAME` should be formatted as:
+ - `YYYY-MM-DD-Release-MAJ.MIN.PATCH` for **stable releases**
+ - `YYYY-MM-DD-Development-MAJ.MIN.PATCH-beta.X` for **beta releases**
+ With MAJ.MIN.PATCH being the NEXT release version!
This will cause a tagged release to be established on the GitHub repository,
-with the binaries being added to the release whenever they are ready.
-The release is initially a draft, where the path notes can be edited and after
-all is good and ready it can be published on GitHub.
+with the binaries being added to the release whenever they are done building in CI.
+The release is initially a draft, where the release notes can be edited and after
+all is checked and ready, it can be published as GitHub release.
If you use a SemVer tag including "beta", the release that will be created at
GitHub will be marked as a "Pre-release" automatically.
The target of the `.../latest` URL will not be changed in that case, it always
@@ -457,7 +459,7 @@ revoke the tag by doing the following:
git push --delete upstream $TAG_NAME
git tag -d $TAG_NAME
```
-You can also do this on GitHub, you'll also want to delete the new release.
+You can also do this on GitHub, you'll also want to delete the false release.
In the first lines of [CMakeLists.txt](
https://github.com/Cockatrice/Cockatrice/blob/master/CMakeLists.txt)
@@ -468,25 +470,24 @@ coming from the tag title, it's good practice to increment the ones at CMake
after every full release to stress that master is ahead of the last stable
release.
The preferred flow of operation is:
- * Just before a release, make sure the version number in CMakeLists.txt is set
- to the same release version you are about to tag.
- * This is also the time to change the pretty name in CMakeLists.txt called
- GIT_TAG_RELEASENAME and commit and push these changes.
- * Tag the release following the previously described syntax in order to get it
- correctly built and deployed by CI.
- * Wait for the configure step to create the release and update the patch
- notes.
- * Check on the github actions page for build progress which should be the top
- listed [here](
+ - Just before a release, make sure the version number in CMakeLists.txt is set
+ to the same release version you are about to tag.
+ - This is also the time to change the pretty name in CMakeLists.txt called
+ `GIT_TAG_RELEASENAME` and commit and push these changes.
+ - Tag the release following the previously described syntax in order to get it
+ correctly built and deployed by CI.
+ - Wait for the configuration step to create the release and update the patch
+ notes.
+ - Check on the GitHub Actions page for build progress which should be the top
+ listed [here](
https://github.com/Cockatrice/Cockatrice/actions?query=event%3Apush+-branch%3Amaster
).
- * When the build has been completed you can verify all uploaded files on the
- release are in order and hit the publish button.
- * After the release is complete, update the CMake version number again to the
- next targeted beta version, typically increasing `PROJECT_VERSION_PATCH` by
- one.
+ - When the build has been completed, you can verify if all uploaded files on the
+ draft release are included and hit the publish button.
+ - After the release is complete, update the CMake version number again to the
+ next targeted beta version, typically increasing `PROJECT_VERSION_PATCH` by
+ one.
When releasing a new stable version, all previous beta releases (and tags)
-should be deleted. This is needed for Cockatrice to update users of the "Beta"
-release channel correctly to the latest stable version as well.
+should be deleted as well.
This can be done the same way as revoking tags, mentioned above.
diff --git a/.github/workflows/desktop-build.yml b/.github/workflows/desktop-build.yml
index 99c05deee..fbb882aa2 100644
--- a/.github/workflows/desktop-build.yml
+++ b/.github/workflows/desktop-build.yml
@@ -217,10 +217,18 @@ jobs:
core_count: 3
make_package: 1
- - target: 14
+ - target: 15
soc: Apple
- os: macos-14
- xcode: "15.4"
+ os: macos-15
+ xcode: "16.2"
+ type: Release
+ core_count: 3
+ make_package: 1
+
+ - target: 15
+ soc: Apple
+ os: macos-15
+ xcode: "16.2"
type: Debug
core_count: 3
@@ -246,7 +254,7 @@ jobs:
brew update
brew install protobuf qt --force-bottle
- - name: Build on Xcode ${{matrix.xcode}}
+ - name: Build & Sign on Xcode ${{matrix.xcode}}
shell: bash
id: build
env:
@@ -254,16 +262,75 @@ jobs:
MAKE_TEST: 1
MAKE_PACKAGE: '${{matrix.make_package}}'
PACKAGE_SUFFIX: '-macOS${{matrix.target}}_${{matrix.soc}}'
+ MACOS_CERTIFICATE: ${{ secrets.PROD_MACOS_CERTIFICATE }}
+ MACOS_CERTIFICATE_PWD: ${{ secrets.PROD_MACOS_CERTIFICATE_PWD }}
+ MACOS_CERTIFICATE_NAME: ${{ secrets.PROD_MACOS_CERTIFICATE_NAME }}
+ MACOS_CI_KEYCHAIN_PWD: ${{ secrets.PROD_MACOS_CI_KEYCHAIN_PWD }}
# macOS runner have 3 cores usually - only the macos-13 image has 4:
# https://docs.github.com/en/actions/using-github-hosted-runners/using-github-hosted-runners/about-github-hosted-runners#standard-github-hosted-runners-for-public-repositories
# https://github.com/actions/runner-images?tab=readme-ov-file#available-images
- run: .ci/compile.sh --server --parallel ${{matrix.core_count}}
+ run: |
+ if [[ -n "$MACOS_CERTIFICATE_NAME" ]]
+ then
+ echo $MACOS_CERTIFICATE | base64 --decode > certificate.p12
+ security create-keychain -p "$MACOS_CI_KEYCHAIN_PWD" build.keychain
+ security default-keychain -s build.keychain
+ security set-keychain-settings -t 3600 -l build.keychain
+ security unlock-keychain -p "$MACOS_CI_KEYCHAIN_PWD" build.keychain
+ security import certificate.p12 -k build.keychain -P "$MACOS_CERTIFICATE_PWD" -T /usr/bin/codesign
+ security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$MACOS_CI_KEYCHAIN_PWD" build.keychain
+ fi
+ .ci/compile.sh --server --parallel ${{matrix.core_count}}
+
+ - name: Sign app bundle
+ if: matrix.make_package
+ env:
+ MACOS_CERTIFICATE_NAME: ${{ secrets.PROD_MACOS_CERTIFICATE_NAME }}
+ MACOS_CI_KEYCHAIN_PWD: ${{ secrets.PROD_MACOS_CI_KEYCHAIN_PWD }}
+ run: |
+ if [[ -n "$MACOS_CERTIFICATE_NAME" ]]
+ then
+ security unlock-keychain -p "$MACOS_CI_KEYCHAIN_PWD" build.keychain
+ /usr/bin/codesign --sign="$MACOS_CERTIFICATE_NAME" --entitlements=".ci/macos.entitlements" --options=runtime --force --deep --timestamp --verbose ${{steps.build.outputs.path}}
+ fi
+
+ - name: Notarize app bundle
+ if: matrix.make_package
+ env:
+ MACOS_NOTARIZATION_APPLE_ID: ${{ secrets.PROD_MACOS_NOTARIZATION_APPLE_ID }}
+ MACOS_NOTARIZATION_TEAM_ID: ${{ secrets.PROD_MACOS_NOTARIZATION_TEAM_ID }}
+ MACOS_NOTARIZATION_PWD: ${{ secrets.PROD_MACOS_NOTARIZATION_PWD }}
+ run: |
+ if [[ -n "$MACOS_NOTARIZATION_APPLE_ID" ]]
+ then
+ # Store the notarization credentials so that we can prevent a UI password dialog from blocking the CI
+ echo "Create keychain profile"
+ xcrun notarytool store-credentials "notarytool-profile" --apple-id "$MACOS_NOTARIZATION_APPLE_ID" --team-id "$MACOS_NOTARIZATION_TEAM_ID" --password "$MACOS_NOTARIZATION_PWD"
+
+ # We can't notarize an app bundle directly, but we need to compress it as an archive.
+ # Therefore, we create a zip file containing our app bundle, so that we can send it to the
+ # notarization service
+ echo "Creating temp notarization archive"
+ ditto -c -k --keepParent ${{steps.build.outputs.path}} "notarization.zip"
+
+ # Here we send the notarization request to the Apple's Notarization service, waiting for the result.
+ # This typically takes a few seconds inside a CI environment, but it might take more depending on the App
+ # characteristics. Visit the Notarization docs for more information and strategies on how to optimize it if
+ # you're curious
+ echo "Notarize app"
+ xcrun notarytool submit "notarization.zip" --keychain-profile "notarytool-profile" --wait
+
+ # Finally, we need to "attach the staple" to our executable, which will allow our app to be
+ # validated by macOS even when an internet connection is not available.
+ echo "Attach staple"
+ xcrun stapler staple ${{steps.build.outputs.path}}
+ fi
- name: Upload artifact
if: matrix.make_package
uses: actions/upload-artifact@v4
with:
- name: macOS${{matrix.target}}${{ matrix.soc == 'Intel' && '_Intel' || '' }}${{ matrix.type == 'Debug' && '_Debug' || '' }}-dmg
+ name: macOS${{matrix.target}}${{ matrix.soc == 'Intel' && '_Intel' || '' }}${{ matrix.type == 'Debug' && '_Debug' || '' }}-package
path: ${{steps.build.outputs.path}}
if-no-files-found: error
@@ -287,7 +354,7 @@ jobs:
qt_arch: msvc2019_64
- target: 10
- qt_version: 6.5.*
+ qt_version: 6.6.*
qt_arch: msvc2019_64
qt_modules: "qtimageformats qtmultimedia qtwebsockets"
@@ -347,6 +414,15 @@ jobs:
path: ${{steps.build.outputs.path}}
if-no-files-found: error
+ - name: Upload pdb database
+ uses: actions/upload-artifact@v4
+ with:
+ name: Windows${{matrix.target}}-debug-pdbs
+ path: |
+ build/cockatrice/Release/*.pdb
+ build/servatrice/Release/*.pdb
+ if-no-files-found: error
+
- name: Upload to release
if: matrix.package != 'skip' && needs.configure.outputs.tag != null
shell: bash
diff --git a/.gitignore b/.gitignore
index eedbba91d..f147c1458 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,10 +6,11 @@ mysql.cnf
.DS_Store
.idea/
*.aps
-cmake-build-debug*
+cmake-build*
preferences
compile_commands.json
.vs/
.vscode/
.cache
.gdb_history
+cockatrice/resources/config/qtlogging.ini
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 6fdf938ed..29ee84798 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -74,16 +74,16 @@ endif()
# A project name is needed for CPack
# Version can be overriden by git tags, see cmake/getversion.cmake
-project("Cockatrice" VERSION 2.10.0)
+project("Cockatrice" VERSION 2.11.0)
# Set release name if not provided via env/cmake var
if(NOT DEFINED GIT_TAG_RELEASENAME)
- set(GIT_TAG_RELEASENAME "Rings of the Wild")
+ set(GIT_TAG_RELEASENAME "Omenpath")
endif()
-# Use c++17 for all targets
+# Use c++20 for all targets
set(CMAKE_CXX_STANDARD
- 17
+ 20
CACHE STRING "C++ ISO Standard"
)
set(CMAKE_CXX_STANDARD_REQUIRED True)
@@ -140,10 +140,17 @@ endif()
# Define proper compilation flags
if(MSVC)
- # Visual Studio: Maximum optimization, disable warning C4251, establish C++17 compatibility
- set(CMAKE_CXX_FLAGS_RELEASE "/Ox /MD /wd4251 /Zc:__cplusplus /std:c++17 /permissive- /W4")
- # Generate complete debugging information
- #set(CMAKE_CXX_FLAGS_DEBUG "/Zi")
+ # Disable Warning C4251, C++20 compatibility, Multi-threaded Builds, Warn Detection, Unwind Semantics, Debug Symbols
+ set(CMAKE_CXX_FLAGS "/wd4251 /Zc:__cplusplus /std:c++20 /permissive- /W4 /MP /EHsc /Zi")
+ # Visual Studio: Maximum Optimization, Multi-threaded DLL
+ set(CMAKE_CXX_FLAGS_RELEASE "/Ox /MD")
+ # Visual Studio: No Optimization, Multi-threaded Debug DLL
+ set(CMAKE_CXX_FLAGS_DEBUG "/Od /MDd")
+
+ # Generate PDB, even when in release (So developers can better analyze crash logs)
+ set(CMAKE_EXE_LINKER_FLAGS_RELEASE "${CMAKE_EXE_LINKER_FLAGS_RELEASE} /DEBUG /OPT:REF /OPT:ICF")
+
+ add_compile_definitions(_SILENCE_STDEXT_ARR_ITERS_DEPRECATION_WARNING)
elseif(CMAKE_COMPILER_IS_GNUCXX)
# linux/gcc, bsd/gcc, windows/mingw
include(CheckCXXCompilerFlag)
@@ -156,7 +163,7 @@ elseif(CMAKE_COMPILER_IS_GNUCXX)
endif()
if(APPLE)
- set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=gnu++17")
+ set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=gnu++20")
endif()
set(ADDITIONAL_DEBUG_FLAGS
@@ -264,6 +271,7 @@ if(UNIX)
set(CPACK_PACKAGE_ICON "${CMAKE_CURRENT_SOURCE_DIR}/cockatrice/resources/appicon.icns")
set(CPACK_DMG_DS_STORE_SETUP_SCRIPT "${CMAKE_CURRENT_SOURCE_DIR}/cmake/CMakeDMGSetup.script")
set(CPACK_DMG_BACKGROUND_IMAGE "${CMAKE_CURRENT_SOURCE_DIR}/cmake/dmgBackground.tif")
+ set(CPACK_PRE_BUILD_SCRIPTS "${CMAKE_CURRENT_SOURCE_DIR}/cmake/SignMacApplications.cmake")
else()
# linux
if(CPACK_GENERATOR STREQUAL "RPM")
diff --git a/Dockerfile b/Dockerfile
index 047c9ecf6..330c51b4a 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,19 +1,20 @@
-FROM ubuntu:bionic
-MAINTAINER Zach Halpern
+FROM ubuntu:24.04
+
+ARG DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y\
- build-essential\
- cmake\
- git\
- libprotobuf-dev\
- libqt5sql5-mysql\
- libmysqlclient-dev\
- libqt5websockets5-dev\
- protobuf-compiler\
- qt5-default\
- qtbase5-dev\
- qttools5-dev-tools\
- qttools5-dev
+ build-essential \
+ cmake \
+ file \
+ g++ \
+ git \
+ libmariadb-dev-compat \
+ libprotobuf-dev \
+ libqt6sql6-mysql \
+ qt6-websockets-dev \
+ protobuf-compiler \
+ qt6-tools-dev \
+ qt6-tools-dev-tools
COPY . /home/servatrice/code/
WORKDIR /home/servatrice/code
@@ -25,7 +26,6 @@ RUN cmake .. -DWITH_SERVER=1 -DWITH_CLIENT=0 -DWITH_ORACLE=0 -DWITH_DBCONVERTER=
WORKDIR /home/servatrice
-EXPOSE 4747
+EXPOSE 4748
ENTRYPOINT [ "servatrice", "--log-to-console" ]
-
diff --git a/README.md b/README.md
index 53b79f356..789134996 100644
--- a/README.md
+++ b/README.md
@@ -133,6 +133,7 @@ The following flags (with their non-default values) can be passed to `cmake`:
# Run
+
Cockatrice is the game client
Oracle fetches card data
Servatrice is the server
diff --git a/cmake/FindQtRuntime.cmake b/cmake/FindQtRuntime.cmake
index 6db821dd7..3025934f4 100644
--- a/cmake/FindQtRuntime.cmake
+++ b/cmake/FindQtRuntime.cmake
@@ -23,6 +23,7 @@ if(WITH_CLIENT)
Svg
WebSockets
Widgets
+ Xml
)
endif()
if(WITH_ORACLE)
diff --git a/cmake/NSIS.template.in b/cmake/NSIS.template.in
index dc4412ff3..fbdde0b86 100644
--- a/cmake/NSIS.template.in
+++ b/cmake/NSIS.template.in
@@ -5,7 +5,7 @@ OutFile "@CPACK_TOPLEVEL_DIRECTORY@/@CPACK_OUTPUT_FILE_NAME@"
!define INST_DIR "@CPACK_TEMPORARY_DIRECTORY@"
-RequestExecutionlevel highest
+RequestExecutionlevel admin
SetCompressor LZMA
Var NormalDestDir
@@ -235,6 +235,13 @@ ${If} $PortableMode = 0
WriteUninstaller "$INSTDIR\uninstall.exe"
${GetSize} "$INSTDIR" "/S=0K" $0 $1 $2
IntFmt $0 "0x%08X" $0
+
+ ; Enable Windows User-Mode Dumps
+ ; https://learn.microsoft.com/en-us/windows/win32/wer/collecting-user-mode-dumps
+ WriteRegExpandStr HKLM "Software\Microsoft\Windows\Windows Error Reporting\LocalDumps\cockatrice.exe" "DumpFolder" "%LOCALAPPDATA%\CrashDumps\Cockatrice"
+ WriteRegDWORD HKLM "Software\Microsoft\Windows\Windows Error Reporting\LocalDumps\cockatrice.exe" "DumpCount" "5"
+ WriteRegDWORD HKLM "Software\Microsoft\Windows\Windows Error Reporting\LocalDumps\cockatrice.exe" "DumpType" "2"
+
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Cockatrice" "DisplayIcon" "$INSTDIR\cockatrice.exe"
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Cockatrice" "DisplayName" "Cockatrice"
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Cockatrice" "DisplayVersion" "@CPACK_PACKAGE_VERSION_MAJOR@.@CPACK_PACKAGE_VERSION_MINOR@.@CPACK_PACKAGE_VERSION_PATCH@"
diff --git a/cmake/SignMacApplications.cmake b/cmake/SignMacApplications.cmake
new file mode 100644
index 000000000..b91e53725
--- /dev/null
+++ b/cmake/SignMacApplications.cmake
@@ -0,0 +1,27 @@
+# This script re-signs all apps after CPack packages them. This is necessary because CPack modifies
+# the library references used by Cockatrice to App relative paths, invalidating the code signature.
+string(LENGTH "$ENV{MACOS_CERTIFICATE_NAME}" MACOS_CERTIFICATE_NAME_LEN)
+
+if(APPLE AND MACOS_CERTIFICATE_NAME_LEN GREATER 0)
+ set(APPLICATIONS "cockatrice" "servatrice" "oracle" "dbconverter")
+ foreach(app_name IN LISTS APPLICATIONS)
+ set(FULL_APP_PATH "${CPACK_TEMPORARY_INSTALL_DIRECTORY}/${app_name}.app")
+
+ message(STATUS "Signing Interior Dynamically Loaded Libraries for ${app_name}.app")
+ execute_process(COMMAND "find" "${FULL_APP_PATH}" "-name" "*.dylib" OUTPUT_VARIABLE INTERIOR_DLLS)
+ string(REPLACE "\n" ";" INTERIOR_DLLS_LIST ${INTERIOR_DLLS})
+
+ foreach(INTERIOR_DLL IN LISTS INTERIOR_DLLS_LIST)
+ execute_process(
+ COMMAND "codesign" "--sign" "$ENV{MACOS_CERTIFICATE_NAME}" "--entitlements" "../.ci/macos.entitlements"
+ "--options" "runtime" "--force" "--deep" "--timestamp" "--verbose" "${INTERIOR_DLL}"
+ )
+ endforeach()
+
+ message(STATUS "Signing Exterior Applications ${app_name}.app")
+ execute_process(
+ COMMAND "codesign" "--sign" "$ENV{MACOS_CERTIFICATE_NAME}" "--entitlements" "../.ci/macos.entitlements"
+ "--options" "runtime" "--force" "--deep" "--timestamp" "--verbose" "${FULL_APP_PATH}"
+ )
+ endforeach()
+endif()
diff --git a/cockatrice/CMakeLists.txt b/cockatrice/CMakeLists.txt
index e2fc703e9..9b50c9469 100644
--- a/cockatrice/CMakeLists.txt
+++ b/cockatrice/CMakeLists.txt
@@ -4,145 +4,15 @@
project(Cockatrice VERSION "${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}.${PROJECT_VERSION_PATCH}")
-set(cockatrice_SOURCES
- src/game/cards/abstract_card_drag_item.cpp
- src/game/cards/abstract_card_item.cpp
- src/client/game_logic/abstract_client.cpp
- src/game/board/abstract_counter.cpp
- src/game/board/abstract_graphics_item.cpp
- src/game/board/arrow_item.cpp
- src/game/board/arrow_target.cpp
- src/game/cards/card_database.cpp
- src/game/cards/card_database_manager.cpp
- src/game/cards/card_database_model.cpp
- src/game/cards/card_database_parser/card_database_parser.cpp
- src/game/cards/card_database_parser/cockatrice_xml_3.cpp
- src/game/cards/card_database_parser/cockatrice_xml_4.cpp
- src/game/cards/card_drag_item.cpp
- src/game/filters/filter_card.cpp
- src/client/ui/widgets/cards/card_info_frame_widget.cpp
- src/client/ui/widgets/cards/card_info_picture_widget.cpp
- src/client/ui/widgets/cards/card_info_text_widget.cpp
- src/client/ui/widgets/cards/card_info_display_widget.cpp
- src/game/cards/card_item.cpp
- src/game/cards/card_list.cpp
- src/game/zones/card_zone.cpp
- src/server/chat_view/chat_view.cpp
- src/game/board/counter_general.cpp
- src/deck/custom_line_edit.cpp
- src/deck/deck_loader.cpp
- src/deck/deck_list_model.cpp
- src/deck/deck_stats_interface.cpp
- src/deck/deck_view.cpp
- src/dialogs/dlg_connect.cpp
- src/dialogs/dlg_create_token.cpp
- src/dialogs/dlg_create_game.cpp
- src/dialogs/dlg_edit_avatar.cpp
- src/dialogs/dlg_edit_password.cpp
- src/dialogs/dlg_edit_tokens.cpp
- src/dialogs/dlg_edit_user.cpp
- src/dialogs/dlg_filter_games.cpp
- src/dialogs/dlg_forgot_password_challenge.cpp
- src/dialogs/dlg_forgot_password_request.cpp
- src/dialogs/dlg_forgot_password_reset.cpp
- src/dialogs/dlg_load_deck_from_clipboard.cpp
- src/dialogs/dlg_load_remote_deck.cpp
- src/dialogs/dlg_manage_sets.cpp
- src/dialogs/dlg_register.cpp
- src/dialogs/dlg_roll_dice.cpp
- src/dialogs/dlg_settings.cpp
- src/dialogs/dlg_tip_of_the_day.cpp
- src/dialogs/dlg_update.cpp
- src/dialogs/dlg_view_log.cpp
- src/game/filters/filter_string.cpp
- src/game/filters/filter_builder.cpp
- src/game/filters/filter_tree.cpp
- src/game/filters/filter_tree_model.cpp
- src/client/ui/layouts/flow_layout.cpp
- src/client/ui/layouts/horizontal_flow_layout.cpp
- src/client/ui/layouts/vertical_flow_layout.cpp
- src/client/ui/widgets/general/layout_containers/flow_widget.cpp
- src/game/game_scene.cpp
- src/game/game_selector.cpp
- src/game/games_model.cpp
- src/game/game_view.cpp
- src/client/get_text_with_max.cpp
- src/game/hand_counter.cpp
- src/server/handle_public_servers.cpp
- src/game/zones/hand_zone.cpp
- src/client/game_logic/key_signals.cpp
- src/client/ui/line_edit_completer.cpp
- src/server/local_client.cpp
- src/server/local_server.cpp
- src/server/local_server_interface.cpp
- src/utility/logger.cpp
- src/client/ui/widgets/cards/card_info_picture_enlarged_widget.cpp
- src/client/ui/widgets/cards/card_info_picture_with_text_overlay_widget.cpp
- src/client/ui/widgets/general/display/labeled_input.cpp
- src/main.cpp
- src/server/message_log_widget.cpp
- src/client/ui/layouts/overlap_layout.cpp
- src/client/ui/widgets/general/layout_containers/overlap_widget.cpp
- src/client/ui/widgets/general/layout_containers/overlap_control_widget.cpp
- src/server/pending_command.cpp
- src/game/phase.cpp
- src/client/ui/phases_toolbar.cpp
- src/client/ui/picture_loader.cpp
- src/game/zones/pile_zone.cpp
- src/client/ui/pixel_map_generator.cpp
- src/game/player/player.cpp
- src/game/player/player_list_widget.cpp
- src/game/player/player_target.cpp
- src/client/network/release_channel.cpp
- src/server/remote/remote_client.cpp
- src/server/remote/remote_decklist_tree_widget.cpp
- src/server/remote/remote_replay_list_tree_widget.cpp
- src/client/network/replay_timeline_widget.cpp
- src/game/zones/select_zone.cpp
- src/utility/sequence_edit.cpp
- src/client/network/sets_model.cpp
- src/settings/card_database_settings.cpp
- src/settings/download_settings.cpp
- src/settings/game_filters_settings.cpp
- src/settings/layouts_settings.cpp
- src/settings/message_settings.cpp
- src/settings/servers_settings.cpp
- src/settings/settings_manager.cpp
- src/settings/cache_settings.cpp
- src/settings/shortcuts_settings.cpp
- src/client/sound_engine.cpp
- src/client/network/spoiler_background_updater.cpp
- src/game/zones/stack_zone.cpp
- src/client/tabs/tab.cpp
- src/client/tabs/tab_account.cpp
- src/client/tabs/tab_admin.cpp
- src/client/tabs/tab_deck_editor.cpp
- src/client/tabs/tab_deck_storage.cpp
- src/client/tabs/tab_game.cpp
- src/client/tabs/tab_logs.cpp
- src/client/tabs/tab_message.cpp
- src/client/tabs/tab_replays.cpp
- src/client/tabs/tab_room.cpp
- src/client/tabs/tab_server.cpp
- src/client/tabs/tab_supervisor.cpp
- src/game/zones/table_zone.cpp
- src/client/tapped_out_interface.cpp
- src/client/ui/theme_manager.cpp
- src/client/ui/tip_of_the_day.cpp
- src/client/translate_counter_name.cpp
- src/client/update_downloader.cpp
- src/server/user/user_context_menu.cpp
- src/server/user/user_info_connection.cpp
- src/server/user/user_info_box.cpp
- src/server/user/user_list.cpp
- src/client/ui/window_main.cpp
- src/game/zones/view_zone_widget.cpp
- src/game/zones/view_zone.cpp
- ${VERSION_STRING_CPP}
-)
+file(GLOB_RECURSE cockatrice_CPP_FILES CONFIGURE_DEPENDS ${CMAKE_SOURCE_DIR}/cockatrice/src/*.cpp)
+
+set(cockatrice_SOURCES ${cockatrice_CPP_FILES} ${VERSION_STRING_CPP})
add_subdirectory(sounds)
add_subdirectory(themes)
+configure_file(
+ ${CMAKE_SOURCE_DIR}/cockatrice/resources/config/qtlogging.ini ${CMAKE_BINARY_DIR}/cockatrice/qtlogging.ini COPYONLY
+)
set(cockatrice_RESOURCES cockatrice.qrc)
@@ -263,7 +133,7 @@ if(APPLE)
set(plugin_dest_dir cockatrice.app/Contents/Plugins)
set(qtconf_dest_dir cockatrice.app/Contents/Resources)
- # Qt plugins: audio (Qt5), iconengines, imageformats, platforms, printsupport (Qt5), styles, tls (Qt6)
+ # Qt plugins: audio (Qt5), iconengines, imageformats, multimedia (Qt6), platforms, printsupport (Qt5), styles, tls (Qt6)
install(
DIRECTORY "${QT_PLUGINS_DIR}/"
DESTINATION ${plugin_dest_dir}
@@ -274,12 +144,20 @@ if(APPLE)
PATTERN "audio/*.dylib"
PATTERN "iconengines/*.dylib"
PATTERN "imageformats/*.dylib"
+ PATTERN "multimedia/*.dylib"
PATTERN "platforms/*.dylib"
PATTERN "printsupport/*.dylib"
PATTERN "styles/*.dylib"
PATTERN "tls/*.dylib"
)
+ install(
+ DIRECTORY "${CMAKE_BINARY_DIR}/cockatrice/"
+ DESTINATION ${qtconf_dest_dir}/
+ FILES_MATCHING
+ PATTERN "*.ini"
+ )
+
install(
CODE "
file(WRITE \"\${CMAKE_INSTALL_PREFIX}/${qtconf_dest_dir}/qt.conf\" \"[Paths]
@@ -314,7 +192,14 @@ if(WIN32)
PATTERN "*.dll"
)
- # Qt plugins: audio (Qt5), iconengines, imageformats, platforms, printsupport (Qt5), styles, tls (Qt6)
+ install(
+ DIRECTORY "${CMAKE_BINARY_DIR}/cockatrice/"
+ DESTINATION ./
+ FILES_MATCHING
+ PATTERN "*.ini"
+ )
+
+ # Qt plugins: audio (Qt5), iconengines, imageformats, multimedia (Qt6) platforms, printsupport (Qt5), styles, tls (Qt6)
install(
DIRECTORY "${QT_PLUGINS_DIR}/"
DESTINATION ${plugin_dest_dir}
@@ -326,6 +211,7 @@ if(WIN32)
PATTERN "imageformats/*.dll"
PATTERN "mediaservice/dsengine.dll"
PATTERN "mediaservice/wmfengine.dll"
+ PATTERN "multimedia/*.dll"
PATTERN "platforms/qdirect2d.dll"
PATTERN "platforms/qminimal.dll"
PATTERN "platforms/qoffscreen.dll"
diff --git a/cockatrice/cockatrice.qrc b/cockatrice/cockatrice.qrc
index 2d147bcf6..38c40561f 100644
--- a/cockatrice/cockatrice.qrc
+++ b/cockatrice/cockatrice.qrc
@@ -1,5 +1,5 @@
-
+
resources/cardback.svg
resources/cockatrice.svg
resources/hand.svg
@@ -13,9 +13,12 @@
resources/icons/arrow_top_green.svg
resources/icons/arrow_up_green.svg
resources/icons/clearsearch.svg
+ resources/icons/cogwheel.svg
resources/icons/conceded.svg
resources/icons/decrement.svg
resources/icons/delete.svg
+ resources/icons/dropdown_collapsed.svg
+ resources/icons/dropdown_expanded.svg
resources/icons/forgot_password.svg
resources/icons/increment.svg
resources/icons/info.svg
@@ -24,16 +27,25 @@
resources/icons/pencil.svg
resources/icons/player.svg
resources/icons/ready_start.svg
+ resources/icons/reload.svg
resources/icons/remove_row.svg
+ resources/icons/rename.svg
resources/icons/scales.svg
resources/icons/search.svg
resources/icons/settings.svg
resources/icons/spectator.svg
+ resources/icons/swap.svg
resources/icons/sync.svg
resources/icons/tab_changed.svg
resources/icons/update.png
resources/icons/view.svg
+ resources/icons/mana/B.svg
+ resources/icons/mana/G.svg
+ resources/icons/mana/R.svg
+ resources/icons/mana/U.svg
+ resources/icons/mana/W.svg
+
resources/config/general.svg
resources/config/appearance.svg
resources/config/interface.svg
@@ -41,6 +53,8 @@
resources/config/deckeditor.svg
resources/config/shorcuts.svg
resources/config/sound.svg
+ resources/config/debug.ini
+ resources/config/qtlogging.ini
resources/counters/w.svg
resources/counters/w_highlight.svg
@@ -326,39 +340,42 @@
resources/replay/fastforward.svg
resources/replay/pause.svg
- resources/userlevels/normal.svg
- resources/userlevels/registered.svg
- resources/userlevels/registered_buddy.svg
- resources/userlevels/registered_vip.svg
- resources/userlevels/registered_vip_buddy.svg
- resources/userlevels/registered_donator.svg
- resources/userlevels/registered_donator_buddy.svg
-
- resources/userlevels/moderator.svg
- resources/userlevels/moderator_buddy.svg
- resources/userlevels/moderator_vip.svg
- resources/userlevels/moderator_vip_buddy.svg
-
- resources/userlevels/admin.svg
- resources/userlevels/admin_buddy.svg
- resources/userlevels/admin_vip.svg
- resources/userlevels/admin_vip_buddy.svg
+ resources/usericons/pawn_single.svg
+ resources/usericons/pawn_double.svg
+ resources/usericons/pawn_donator_single.svg
+ resources/usericons/pawn_donator_double.svg
+ resources/usericons/pawn_judge_single.svg
+ resources/usericons/pawn_judge_double.svg
+ resources/usericons/pawn_vip_single.svg
+ resources/usericons/pawn_vip_double.svg
+ resources/usericons/star_single.svg
+ resources/usericons/star_double.svg
resources/tips/images/accounts_tab.png
+ resources/tips/images/add_card.png
resources/tips/images/arrows.png
+ resources/tips/images/card_select.png
resources/tips/images/cockatrice_register.png
resources/tips/images/cockatrice_wiki.png
resources/tips/images/coin_flip.png
resources/tips/images/counter_expression.png
resources/tips/images/discord.png
+ resources/tips/images/edhrec.png
+ resources/tips/images/expand_card_view.png
resources/tips/images/face_down.png
resources/tips/images/filter_games.png
resources/tips/images/github_logo.png
+ resources/tips/images/highlight_cards.png
+ resources/tips/images/pawns.png
resources/tips/images/setpt.png
resources/tips/images/shortcuts.png
+ resources/tips/images/syntax_help.png
resources/tips/images/themes.png
resources/tips/images/tip_of_the_day.png
+ resources/tips/images/token.png
+ resources/tips/images/updates.png
+ resources/tips/images/visual_deck_tags.png
resources/tips/tips_of_the_day.xml
resources/help/search.md
diff --git a/cockatrice/cockatrice_en@source.ts b/cockatrice/cockatrice_en@source.ts
index 7418f9dcf..9574f33a4 100644
--- a/cockatrice/cockatrice_en@source.ts
+++ b/cockatrice/cockatrice_en@source.ts
@@ -4,7 +4,7 @@
AbstractCounter
-
+
&Set counter...
@@ -12,95 +12,247 @@
AbstractCounterDialog
-
+
Set counter
-
+
New value for counter '%1':
+
+ AbstractDlgDeckTextEdit
+
+
+ &Refresh
+
+
+
+
+ Parse Set Name and Number (if available)
+
+
+
+
+ AbstractTabDeckEditor
+
+
+ Open in new tab
+
+
+
+
+ Are you sure?
+
+
+
+
+ The decklist has been modified.
+Do you want to save the changes?
+
+
+
+
+
+
+
+
+
+
+ Error
+
+
+
+
+ Could not open deck at %1
+
+
+
+
+ Could not save remote deck
+
+
+
+
+
+ The deck could not be saved.
+Please check that the directory is writable and try again.
+
+
+
+
+ Save deck
+
+
+
+
+ The deck could not be saved.
+
+
+
+
+ There are no cards in your deck to be exported
+
+
+
+
+ No deck was selected to be exported.
+
+
+
+
+ AdminNotesDialog
+
+
+ Update Notes
+
+
+
+
+ Admin Notes for %1
+
+
+
+
+ AllZonesCardAmountWidget
+
+
+ Mainboard
+
+
+
+
+ Sideboard
+
+
+
AppearanceSettingsPage
-
+
Error
-
+
Could not create themes directory at '%1'.
-
+
Theme settings
-
+
Current theme:
-
+
Open themes folder
-
+
+ Menu settings
+
+
+
+
+ Show keyboard shortcuts in right-click menus
+
+
+
+
Card rendering
-
+
Display card names on cards having a picture
-
+
+ Auto-Rotate cards with sideways layout
+
+
+
+
+ Override all card art with personal set preference (Pre-ProviderID change behavior) [Requires Client restart]
+
+
+
+
+ Bump sets that the deck contains cards from to the top in the printing selector
+
+
+
+
Scale cards on mouse over
-
+
+ Use rounded card corners
+
+
+
+
Minimum overlap percentage of cards on the stack and in vertical hand
-
+
+ Maximum initial height for card view window:
+
+
+
+
+
+ rows
+
+
+
+
+ Maximum expanded height for card view window:
+
+
+
+
Hand layout
-
+
Display hand horizontally (wastes space)
-
+
Enable left justification
-
+
Table grid layout
-
+
Invert vertical coordinate
-
+
Minimum player count for multi-column layout:
-
+
Maximum font size for information displayed on cards:
@@ -108,111 +260,111 @@
BanDialog
-
+
ban &user name
-
+
ban &IP address
-
+
ban client I&D
-
+
Ban type
-
+
&permanent ban
-
+
&temporary ban
-
+
&Days:
-
+
&Hours:
-
+
&Minutes:
-
+
Duration of the ban
-
+
Please enter the reason for the ban.
This is only saved for moderators and cannot be seen by the banned person.
-
+
Please enter the reason for the ban that will be visible to the banned person.
-
+
Redact all messages from this user in all rooms
-
+
&OK
-
+
&Cancel
-
+
Ban user from server
-
-
-
-
+
+
+
+
Error
-
+
You have to select a name-based, IP-based, clientId based, or some combination of the three to place a ban.
-
+
You must have a value in the name ban when selecting the name ban checkbox.
-
+
You must have a value in the ip ban when selecting the ip ban checkbox.
-
+
You must have a value in the clientid ban when selecting the clientid ban checkbox.
@@ -220,22 +372,22 @@ This is only saved for moderators and cannot be seen by the banned person.
BetaReleaseChannel
-
- Beta Releases
+
+ Beta
-
+
No reply received from the release update server.
-
+
Invalid reply received from the release update server.
-
+
No reply received from the file update server.
@@ -243,32 +395,32 @@ This is only saved for moderators and cannot be seen by the banned person.
CardDatabaseModel
-
+
Name
-
+
Sets
-
+
Mana cost
-
+
Card type
-
+
P/T
-
+
Color(s)
@@ -361,37 +513,65 @@ This is only saved for moderators and cannot be seen by the banned person.
- CardFrame
+ CardInfoFrameWidget
-
+
Image
-
+
Description
-
+
Both
+
+
+ View transformation
+
+
- CardInfoText
+ CardInfoPictureWidget
-
+
+ View related cards
+
+
+
+
+ Add card to deck
+
+
+
+
+ Mainboard
+
+
+
+
+ Sideboard
+
+
+
+
+ CardInfoTextWidget
+
+
Name:
-
+
Related cards:
-
+
Unknown card:
@@ -399,287 +579,555 @@ This is only saved for moderators and cannot be seen by the banned person.
CardItem
-
+
&Move to
-
+
&Power / toughness
+
+ CardSizeWidget
+
+
+ Card Size
+
+
+
CardZone
-
+
their hand
nominative
-
+
%1's hand
nominative
-
+
their library
look at zone
-
+
%1's library
look at zone
-
+
of their library
top cards of zone,
-
+
of %1's library
top cards of zone
-
+
their library
reveal zone
-
+
%1's library
reveal zone
-
+
their library
shuffle
-
+
%1's library
shuffle
-
+
their library
nominative
-
+
%1's library
nominative
-
+
their graveyard
nominative
-
+
%1's graveyard
nominative
-
+
their exile
nominative
-
+
%1's exile
nominative
-
+
their sideboard
look at zone
-
+
%1's sideboard
look at zone
-
+
their sideboard
nominative
-
+
%1's sideboard
nominative
+
+ DeckEditorCardInfoDockWidget
+
+
+ Card Info
+
+
+
+
+ DeckEditorDatabaseDisplayWidget
+
+
+ Search by card name (or search expressions)
+
+
+
+
+ Add to Deck
+
+
+
+
+ Add to Sideboard
+
+
+
+
+ Select Printing
+
+
+
+
+ Show on EDHREC (Commander)
+
+
+
+
+ Show on EDHREC (Card)
+
+
+
+
+ Show Related cards
+
+
+
+
+ Add card to &maindeck
+
+
+
+
+ Add card to &sideboard
+
+
+
+
+ DeckEditorDeckDockWidget
+
+
+ Banner Card
+
+
+
+
+ Select Printing
+
+
+
+
+ Deck
+
+
+
+
+ Deck &name:
+
+
+
+
+ &Comments:
+
+
+
+
+ Hash:
+
+
+
+
+ &Increment number
+
+
+
+
+ &Decrement number
+
+
+
+
+ &Remove row
+
+
+
+
+ Swap card to/from sideboard
+
+
+
+
+ DeckEditorFilterDockWidget
+
+
+ Filters
+
+
+
+
+ &Clear all filters
+
+
+
+
+ Delete selected
+
+
+
+
+ DeckEditorMenu
+
+
+ &Deck Editor
+
+
+
+
+ &New deck
+
+
+
+
+ &Load deck...
+
+
+
+
+ Load recent deck...
+
+
+
+
+ Clear
+
+
+
+
+ &Save deck
+
+
+
+
+ Save deck &as...
+
+
+
+
+ Load deck from cl&ipboard...
+
+
+
+
+ Edit deck in clipboard
+
+
+
+
+
+ Annotated
+
+
+
+
+
+ Not Annotated
+
+
+
+
+ Save deck to clipboard
+
+
+
+
+ Annotated (No set info)
+
+
+
+
+ Not Annotated (No set info)
+
+
+
+
+ &Print deck...
+
+
+
+
+ &Send deck to online service
+
+
+
+
+ Create decklist (decklist.org)
+
+
+
+
+ Create decklist (decklist.xyz)
+
+
+
+
+ Analyze deck (deckstats.net)
+
+
+
+
+ Analyze deck (tappedout.net)
+
+
+
+
+ &Close
+
+
+
+
+ DeckEditorPrintingSelectorDockWidget
+
+
+ Printing Selector
+
+
+
DeckEditorSettingsPage
-
-
+
+
Update Spoilers
-
-
+
+
Success
-
+
Download URLs have been reset.
-
+
Downloaded card pictures have been reset.
-
+
Error
-
+
One or more downloaded card pictures could not be cleared.
-
+
Add URL
-
-
+
+
URL:
-
-
+
+
Edit URL
-
+
+ Network Cache Size:
+
+
+
+
+ Redirect Cache TTL:
+
+
+
+
+ How long cached redirects for urls are valid for.
+
+
+
+
+ Picture Cache Size:
+
+
+
+
Add New URL
-
+
Remove URL
-
+
+ Day(s)
+
+
+
+
Updating...
-
+
Choose path
-
+
URL Download Priority
-
+
Spoilers
-
+
Download Spoilers Automatically
-
+
Spoiler Location:
-
+
Last Change
-
+
Spoilers download automatically on launch
-
+
Press the button to manually update without relaunching
-
+
Do not close settings until manual update is complete
-
+
Download card pictures on the fly
-
+
How to add a custom URL
-
+
Delete Downloaded Images
-
+
Reset Download URLs
-
- Downloaded images directory size:
-
-
-
-
+
On-disk cache for downloaded pictures
-
- Picture cache size:
-
-
-
-
+
In-memory cache for pictures not currently on screen
@@ -687,16 +1135,218 @@ This is only saved for moderators and cannot be seen by the banned person.
DeckListModel
-
+
+ Count
+
+
+
+
+ Set
+
+
+
+
Number
-
+
+ Provider ID
+
+
+
+
Card
+
+ DeckLoader
+
+
+ Common deck formats (%1)
+
+
+
+
+ All files (*.*)
+
+
+
+
+ DeckPreviewColorIdentityFilterWidget
+
+
+ Mode: Exact Match
+
+
+
+
+ Mode: Includes
+
+
+
+
+ DeckPreviewDeckTagsDisplayWidget
+
+
+ Edit tags ...
+
+
+
+
+ DeckPreviewTagDialog
+
+
+ Deck Tags Manager
+
+
+
+
+ Manage your deck tags. Check or uncheck tags as needed, or add new ones:
+
+
+
+
+ Add a new tag (e.g., Aggro️)
+
+
+
+
+ Add Tag
+
+
+
+
+ Filter tags...
+
+
+
+
+ OK
+
+
+
+
+ Cancel
+
+
+
+
+ Invalid Input
+
+
+
+
+ Tag name cannot be empty!
+
+
+
+
+ Duplicate Tag
+
+
+
+
+ This tag already exists.
+
+
+
+
+ DeckPreviewWidget
+
+
+ Banner Card
+
+
+
+
+ Open in deck editor
+
+
+
+
+ Edit Tags
+
+
+
+
+ Rename Deck
+
+
+
+
+ Save Deck to Clipboard
+
+
+
+
+ Annotated
+
+
+
+
+ Annotated (No set info)
+
+
+
+
+ Not Annotated
+
+
+
+
+ Not Annotated (No set info)
+
+
+
+
+ Rename File
+
+
+
+
+ Delete File
+
+
+
+
+ Set Banner Card
+
+
+
+
+
+ New name:
+
+
+
+
+
+ Error
+
+
+
+
+ Rename failed
+
+
+
+
+ Delete file
+
+
+
+
+ Are you sure you want to delete the selected file?
+
+
+
+
+ Delete failed
+
+
+
DeckStatsInterface
@@ -714,181 +1364,217 @@ This is only saved for moderators and cannot be seen by the banned person.
DeckViewContainer
-
+
Load deck...
-
+
Load remote deck...
-
+
+ Unload deck
+ Unload deck...
+
+
+
+
Ready to start
-
+
+ Force start
+
+
+
+
Sideboard unlocked
-
+
Sideboard locked
-
- Load deck
-
-
-
-
+
Error
-
+
The selected file could not be loaded.
+
+ DialogConvertDeckToCodFormat
+
+
+ Deck Format Conversion
+
+
+
+
+ You tried to add a tag to a .txt format deck.
+ Tags can only be added to .cod format decks.
+ Do you want to convert the deck to the .cod format?
+
+
+
+
+ Remember and automatically apply choice in the future
+
+
+
DlgConnect
-
+
Downloading...
-
+
Known Hosts
-
+
+ Delete the currently selected saved server
+
+
+
+
Refresh the server list with known public servers
-
+
New Host
-
+
Name:
-
+
&Host:
-
+
&Port:
-
+
Player &name:
-
+
P&assword:
-
+
&Save password
-
+
A&uto connect
-
+
Automatically connect to the most recent login when Cockatrice opens
-
+
If you have any trouble connecting or registering then contact the server staff for help!
-
-
+
+
Webpage
-
+
Reset Password
-
+
+ Forgot password?
+
+
+
+
&Connect
-
+
Server
-
+
Login
-
+
Server Contact
-
+
Connect to Server
-
+
Server URL
-
+
Communication Port
-
+
Unique Server Name
-
+
Connection Warning
-
+
You need to name your new connection profile.
-
+
Connect Warning
-
+
The player name can't be empty.
@@ -971,27 +1657,37 @@ This is only saved for moderators and cannot be seen by the banned person.
-
+
+ Starting life total:
+
+
+
+
+ Game setup options
+
+
+
+
&Clear
-
+
Create game
-
+
Game information
-
+
Error
-
+
Server error.
@@ -999,92 +1695,92 @@ This is only saved for moderators and cannot be seen by the banned person.
DlgCreateToken
-
+
&Name:
-
+
Token
-
+
C&olor:
-
+
white
-
+
blue
-
+
black
-
+
red
-
+
green
-
+
multicolor
-
+
colorless
-
+
&P/T:
-
+
&Annotation:
-
+
&Destroy token when it leaves the table
-
+
Token data
-
+
Show &all tokens
-
+
Show tokens from this &deck
-
+
Choose token from list
-
+
Create token
@@ -1129,6 +1825,24 @@ To remove your current avatar, confirm without choosing a new image.
+
+ DlgEditDeckInClipboard
+
+
+ Edit deck in clipboard
+
+
+
+
+ Error
+
+
+
+
+ Invalid deck list.
+
+
+
DlgEditPassword
@@ -1171,93 +1885,93 @@ To remove your current avatar, confirm without choosing a new image.
DlgEditTokens
-
+
&Name:
-
+
C&olor:
-
+
white
-
+
blue
-
+
black
-
+
red
-
+
green
-
+
multicolor
-
+
colorless
-
+
&P/T:
-
+
&Annotation:
-
+
Token data
-
-
+
+
Add token
-
+
Remove token
-
+
Edit custom tokens
-
+
Please enter the name of the token:
-
+
Error
-
+
The chosen name conflicts with an existing card or token.
Make sure to enable the 'Token' set in the "Manage sets" dialog to display them correctly.
@@ -1294,132 +2008,132 @@ Make sure to enable the 'Token' set in the "Manage sets" dia
DlgFilterGames
-
+
no limit
-
+
5 minutes
-
+
10 minutes
-
+
30 minutes
-
+
1 hour
-
+
2 hours
-
- Show '&buddies only' games
+
+ Hide 'buddies only' games
-
- Show &full games
+
+ Hide full games
-
- Show games &that have started
+
+ Hide games that have started
-
- Show &password protected games
+
+ Hide password protected games
-
- Hide '&ignored user' games
+
+ Hide 'ignored user' games
-
+
&Newer than:
-
+
Game &description:
-
+
&Creator name:
-
+
General
-
+
&Game types
-
+
at &least:
-
+
at &most:
-
+
Maximum player count
-
+
Restrictions
-
+
Show games only if &spectators can watch
-
+
Show spectator password p&rotected games
-
+
Show only if spectators can ch&at
-
+
Show only if spectators can see &hands
-
+
Spectators
-
+
Filter games
@@ -1603,26 +2317,27 @@ Make sure to enable the 'Token' set in the "Manage sets" dia
- DlgLoadDeckFromClipboard
+ DlgLoadDeck
-
- &Refresh
+
+ Load Deck
+
+
+ DlgLoadDeckFromClipboard
-
+
Load deck from clipboard
-
-
+
Error
-
-
+
Invalid deck list.
@@ -1630,11 +2345,49 @@ Make sure to enable the 'Token' set in the "Manage sets" dia
DlgLoadRemoteDeck
-
+
Load deck
+
+ DlgMoveTopCardsUntil
+
+
+ Card name (or search expressions):
+
+
+
+
+ Number of hits:
+
+
+
+
+ Auto play hits
+
+
+
+
+ Put top cards on stack until...
+
+
+
+
+ No cards matching the search expression exists in the card database. Proceed anyways?
+
+
+
+
+ Cockatrice
+
+
+
+
+ Invalid filter
+
+
+
DlgRegister
@@ -1748,12 +2501,12 @@ Your email will be used to verify your account.
DlgSettings
-
+
Unknown Error loading card database
-
+
Your card database is invalid.
Cockatrice may not function correctly with an invalid database
@@ -1764,7 +2517,7 @@ Would you like to change your database location setting?
-
+
Your card database version is too old.
This can cause problems loading card information or images
@@ -1775,7 +2528,7 @@ Would you like to change your database location setting?
-
+
Your card database did not finish loading
Please file a ticket at https://github.com/Cockatrice/Cockatrice/issues with your cards.xml attached
@@ -1784,21 +2537,21 @@ Would you like to change your database location setting?
-
+
File Error loading your card database.
Would you like to change your database location setting?
-
+
Your card database was loaded but contains no cards.
Would you like to change your database location setting?
-
+
Unknown card database load status
Please file a ticket at https://github.com/Cockatrice/Cockatrice/issues
@@ -1807,59 +2560,59 @@ Would you like to change your database location setting?
-
-
-
+
+
+
Error
-
+
The path to your deck directory is invalid. Would you like to go back and set the correct path?
-
+
The path to your card pictures directory is invalid. Would you like to go back and set the correct path?
-
+
Settings
-
+
General
-
+
Appearance
-
+
User Interface
-
+
Card Sources
-
+
Chat
-
+
Sound
-
+
Shortcuts
@@ -1885,164 +2638,168 @@ Would you like to change your database location setting?
DlgUpdate
-
+
Current release channel
-
+
Reinstall
-
+
Cancel Download
-
+
Open Download Page
-
+
Check for Client Updates
-
-
-
-
+
+
+
+
Error
-
+
Cockatrice was not built with SSL support, therefore you cannot download updates automatically!
Please visit the download page to update manually.
-
- Downloading update...
+
+ Downloading update: %1
+ Downloading update...
-
+
Checking for updates...
-
+
Finished checking for updates
-
+
No Update Available
-
+
Cockatrice is up to date!
-
+
You are already running the latest version available in the chosen release channel.
-
+
Current version
-
+
Selected release channel
-
-
+
+
Update Available
-
-
-
- A new version of Cockatrice is available!
-
-
-
-
-
- New version
-
-
- Released
+ A new version of Cockatrice is available!
+
+
+
+
+
+ New version
- Changelog
+ Released
+
+ Changelog
+
+
+
+
Do you want to update now?
-
- Unfortunately there are no download packages available for your operating system.
-You may have to build from source yourself.
+
+ Unfortunately, the automatic updater failed to find a compatible download.
+You may have to manually download the new version.
+ Unfortunately there are no download packages available for your operating system.
+You may have to build from source yourself.
-
- Please check the download page manually and visit the wiki for instructions on compiling.
+
+ Please check the <a href="%1">releases page</a> on our Github and download the build for your system.
+ Please check the download page manually and visit the wiki for instructions on compiling.
-
-
-
+
+
+
Update Error
-
+
An error occurred while checking for updates:
-
+
An error occurred while downloading an update:
-
+
Installing...
-
+
Cockatrice is unable to open the installer.
-
+
Try to update manually by closing Cockatrice and running the installer.
-
+
Download location
@@ -2060,6 +2817,14 @@ You may have to build from source yourself.
+
+ EdhrecCommanderResponseCommanderDetailsDisplayWidget
+
+
+ Salt:
+
+
+
FilterBuilder
@@ -2071,105 +2836,120 @@ You may have to build from source yourself.
GameSelector
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
Error
-
+
Please join the appropriate room first.
-
+
Wrong password.
-
+
Spectators are not allowed in this game.
-
+
The game is already full.
-
+
The game does not exist any more.
-
+
This game is only open to registered users.
-
+
This game is only open to its creator's buddies.
-
+
You are being ignored by the creator of this game.
-
+
+ Join Game
+
+
+
+
+ Spectate Game
+
+
+
+
+ Game Information
+
+
+
+
Join game
-
+
Password:
-
+
Please join the respective room first.
-
+
&Filter games
-
+
C&lear filter
-
+
C&reate
-
+
&Join
-
+
J&oin as spectator
-
+
Games shown: %1 / %2
-
+
Games
@@ -2177,12 +2957,12 @@ You may have to build from source yourself.
GamesModel
-
+
>1 day
-
+
%1%2 hr
short age in hours
@@ -2191,12 +2971,12 @@ You may have to build from source yourself.
-
+
new
-
+
%1%2 min
short age in minutes
@@ -2205,78 +2985,78 @@ You may have to build from source yourself.
-
+
password
-
+
buddies only
-
+
reg. users only
-
-
+
+
can chat
-
+
see hands
-
+
can see hands
-
+
not allowed
-
+
Room
-
+
Age
-
+
Description
-
+
Creator
-
+
Type
-
+
Restrictions
-
+
Players
-
+
Spectators
@@ -2284,92 +3064,102 @@ You may have to build from source yourself.
GeneralSettingsPage
-
+
Reset all paths
-
+
All paths have been reset
-
-
+
+
Choose path
-
+
Personal settings
-
+
Language:
-
+
Paths (editing disabled in portable mode)
-
+
Paths
-
+
+ How to help with translations
+
+
+
+
Decks directory:
-
+
Replays directory:
-
+
Pictures directory:
-
+
Card database:
-
+
Custom database directory:
-
+
Token database:
-
+
Update channel
-
+
+ Check for client updates on startup
+
+
+
+
Notify if a feature supported by the server is missing in my client
-
+
Automatically run Oracle when running a new version of Cockatrice
-
+
Show tips on startup
@@ -2377,60 +3167,60 @@ You may have to build from source yourself.
MainWindow
-
-
+
+
The server has reached its maximum user capacity, please check back later.
-
+
There are too many concurrent connections from your address.
-
+
Banned by moderator
-
+
Expected end time: %1
-
+
This ban lasts indefinitely.
-
+
Scheduled server shutdown.
-
-
+
+
Invalid username.
-
+
You have been logged out due to logging in at another location.
-
+
Connection closed
-
+
The server has terminated your connection.
Reason: %1
-
+
The server is going to be restarted in %n minute(s).
All running games will be lost.
Reason for shutdown: %1
@@ -2440,576 +3230,586 @@ Reason for shutdown: %1
-
+
Scheduled server shutdown
-
-
+
+
Success
-
+
Registration accepted.
Will now login.
-
+
Account activation accepted.
Will now login.
-
+
Number of players
-
+
Please enter the number of players.
-
-
+
+
Player %1
-
+
Load replay
-
+
About Cockatrice
-
+
Version
-
+
Cockatrice Webpage
-
+
Project Manager:
-
+
Past Project Managers:
-
+
Developers:
-
+
Our Developers
-
+
Help Develop!
-
+
Translators:
-
+
Our Translators
-
+
Help Translate!
-
+
Support:
-
+
Report an Issue
-
+
Troubleshooting
-
+
F.A.Q.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
+
+
+
+
+
Error
-
+
Server timeout
-
+
Failed Login
-
+
Your client seems to be missing features this server requires for connection.
-
+
To update your client, go to 'Help -> Check for Client Updates'.
-
+
Incorrect username or password. Please check your authentication information and try again.
-
+
There is already an active session using this user name.
Please close that session first and re-login.
-
-
+
+
You are banned until %1.
-
-
+
+
You are banned indefinitely.
-
+
This server requires user registration. Do you want to register now?
-
+
This server requires client IDs. Your client is either failing to generate an ID or you are running a modified client.
Please close and reopen your client to try again.
-
+
An internal error has occurred, please close and reopen Cockatrice before trying again.
If the error persists, ensure you are running the latest version of the software and if needed contact the software developers.
-
+
Account activation
-
+
Your account has not been activated yet.
You need to provide the activation token received in the activation email.
-
+
Server Full
-
+
Unknown login error: %1
-
-
+
+
This usually means that your client version is out of date, and the server sent a reply your client doesn't understand.
-
+
Your username must respect these rules:
-
+
is %1 - %2 characters long
-
+
can %1 contain lowercase characters
-
-
-
-
+
+
+
+
NOT
-
+
can %1 contain uppercase characters
-
+
can %1 contain numeric characters
-
+
can contain the following punctuation: %1
-
+
first character can %1 be a punctuation mark
-
+
no unacceptable language as specified by these server rules:
note that the following lines will not be translated
-
+
can not contain any of the following words: %1
-
+
can not match any of the following expressions: %1
-
+
You may only use A-Z, a-z, 0-9, _, ., and - in your username.
-
-
-
-
-
-
+
+
+
+
+
+
Registration denied
-
+
Registration is currently disabled on this server
-
+
There is already an existing account with the same user name.
-
+
It's mandatory to specify a valid email address when registering.
-
+
It appears you are attempting to register a new account on this server yet you already have an account registered with the email provided. This server restricts the number of accounts a user can register per address. Please contact the server operator for further assistance or to obtain your credential information.
-
+
Password too short.
-
+
Registration failed for a technical problem on the server.
-
+
Unknown registration error: %1
-
+
Account activation failed
-
+
Socket error: %1
-
+
You are trying to connect to an obsolete server. Please downgrade your Cockatrice version or connect to a suitable server.
Local version is %1, remote version is %2.
-
+
Your Cockatrice client is obsolete. Please update your Cockatrice version.
Local version is %1, remote version is %2.
-
+
Connecting to %1...
-
+
Registering to %1 as %2...
-
+
Disconnected
-
+
Connected, logging in at %1
-
-
-
+
+
+
Requesting forgotten password to %1 as %2...
-
+
&Connect...
-
+
&Disconnect
-
+
Start &local game...
-
+
&Watch replay...
-
- &Deck editor
-
-
-
-
+
&Full screen
-
+
&Register to server...
-
+
&Restore password...
-
+
&Settings...
-
+
&Exit
-
+
A&ctions
-
+
&Cockatrice
-
+
C&ard Database
-
+
&Manage sets...
-
+
Edit custom &tokens...
-
+
Open custom image folder
-
+
Open custom sets folder
-
+
Add custom sets/cards
-
+
+ Reload card database
+
+
+
+
+ Tabs
+
+
+
+
&Help
-
+
&About Cockatrice
-
+
&Tip of the Day
-
+
Check for Client Updates
-
+
Check for Card Updates...
-
+
View &Debug Log
-
+
+ Open Settings Folder
+
+
+
+
Show/Hide
-
+
New Version
-
+
Congratulations on updating to Cockatrice %1!
Oracle will now launch to update your card database.
-
+
Cockatrice installed
-
+
Congratulations on installing Cockatrice %1!
Oracle will now launch to install the initial card database.
-
+
Card database
-
+
Cockatrice is unable to load the card database.
Do you want to update your card database now?
If unsure or first time user, choose "Yes"
-
-
+
+
Yes
-
-
+
+
No
-
+
Open settings
-
+
New sets found
-
+
%n new set(s) found in the card database
Set code(s): %1
Do you want to enable it/them?
@@ -3019,75 +3819,76 @@ Do you want to enable it/them?
-
+
View sets
-
+
Welcome
-
+
Hi! It seems like you're running this version of Cockatrice for the first time.
All the sets in the card database have been enabled.
Read more about changing the set order or disabling specific sets and consequent effects in the "Manage Sets" dialog.
-
-
+
+
Information
-
+
A card database update is already running.
-
+
Unable to run the card database updater:
-
- failed to start.
+
+ Failed to start. The file might be missing, or permissions might be incorrect.
-
- crashed.
+
+ The process crashed some time after starting successfully.
-
- timed out.
+
+ Timed out. The process took too long to respond. The last waitFor...() function timed out.
-
- write error.
+
+ An error occurred when attempting to write to the process. For example, the process may not be running, or it may have closed its input channel.
-
- read error.
+
+ An error occurred when attempting to read from the process. For example, the process may not be running.
-
- unknown error.
+
+ Unknown error occurred.
-
- The card database updater exited with an error: %1
+
+ The card database updater exited with an error:
+%1
-
+
This server supports additional features that your client doesn't have.
This is most likely not a problem, but this message might mean there is a new version of Cockatrice available or this server is running a custom or pre-release version.
@@ -3095,54 +3896,54 @@ To update your client, go to Help -> Check for Updates.
-
-
-
-
-
+
+
+
+
+
Load sets/cards
-
+
Selected file cannot be found.
-
+
You can only import XML databases at this time.
-
+
The new sets/cards have been added successfully.
Cockatrice will now reload the card database.
-
+
Sets/cards failed to import.
-
-
-
+
+
+
Reset Password
-
+
Your password has been reset successfully, you can now log in using the new credentials.
-
+
Failed to reset user account password, please contact the server operator to reset your password.
-
+
Activation request received, please check your email for an activation token.
@@ -3419,7 +4220,8 @@ Cockatrice will now reload the card database.
- %1 is looking at the top %3 card(s) %2.
+ %1 is looking at the %4 %3 card(s) %2.
+ %1 is looking at the top %3 card(s) %2.
top card for singular, top %3 cards for plural
@@ -3427,57 +4229,67 @@ Cockatrice will now reload the card database.
-
+
+ bottom
+
+
+
+
+ top
+
+
+
+
%1 turns %2 face-down.
-
+
%1 turns %2 face-up.
-
+
The game has been closed.
-
+
The game has started.
-
+
%1 has joined the game.
-
+
%1 is now watching the game.
-
+
You have been kicked out of the game.
-
+
%1 has left the game (%2).
-
+
%1 is not watching the game any more (%2).
-
+
%1 is not ready to start the game any more.
-
+
%1 shuffles their deck and draws a new hand of %2 card(s).
@@ -3485,28 +4297,28 @@ Cockatrice will now reload the card database.
-
+
%1 shuffles their deck and draws a new hand.
-
+
You are watching a replay of game #%1.
-
+
%1 is ready to start the game.
-
+
cards
an unknown amount of cards
-
+
%1 card(s)
a card for singular, %1 cards for plural
@@ -3515,92 +4327,92 @@ Cockatrice will now reload the card database.
-
+
%1 lends %2 to %3.
-
+
%1 reveals %2 to %3.
-
+
%1 reveals %2.
-
+
%1 randomly reveals %2%3 to %4.
-
+
%1 randomly reveals %2%3.
-
+
%1 peeks at face down card #%2.
-
+
%1 peeks at face down card #%2: %3.
-
+
%1 reveals %2%3 to %4.
-
+
%1 reveals %2%3.
-
+
%1 reversed turn order, now it's %2.
-
+
reversed
-
+
normal
-
+
Heads
-
+
Tails
-
+
%1 flipped a coin. It landed as %2.
-
+
%1 rolls a %2 with a %3-sided die.
-
+
%1 flips %2 coins. There are %3 heads and %4 tails.
-
+
%1 rolls a %2-sided dice %3 times: %4.
@@ -3729,17 +4541,17 @@ Cockatrice will now reload the card database.
-
+
%1 unattaches %2.
-
+
%1 undoes their last draw.
-
+
%1 undoes their last draw (%2).
@@ -3747,110 +4559,110 @@ Cockatrice will now reload the card database.
MessagesSettingsPage
-
+
Word1 Word2 Word3
-
+
Add New Message
-
+
Edit Message
-
+
Remove Message
-
+
Add message
-
-
+
+
Message:
-
+
Edit message
-
+
Chat settings
-
+
Custom alert words
-
+
Enable chat mentions
-
+
Enable mention completer
-
+
In-game message macros
-
+
How to use in-game message macros
-
+
Ignore chat room messages sent by unregistered users
-
+
Ignore private messages sent by unregistered users
-
-
+
+
Invert text color
-
+
Enable desktop notifications for private messages
-
+
Enable desktop notification for mentions
-
+
Enable room message history on join
-
-
+
+
(Color is hexadecimal)
-
+
Separate words with a space, alphanumeric characters only
@@ -3908,6 +4720,24 @@ Cockatrice will now reload the card database.
+
+ OverlapControlWidget
+
+
+ Cards to overlap:
+
+
+
+
+ Overlap percentage:
+
+
+
+
+ Overlap direction:
+
+
+
Phase
@@ -3974,57 +4804,57 @@ Cockatrice will now reload the card database.
PhasesToolbar
-
+
Untap step
-
+
Upkeep step
-
+
Draw step
-
+
First main phase
-
+
Beginning of combat step
-
+
Declare attackers step
-
+
Declare blockers step
-
+
Combat damage step
-
+
End of combat step
-
+
Second main phase
-
+
End of turn step
@@ -4032,7 +4862,7 @@ Cockatrice will now reload the card database.
PictureLoader
-
+
en
code for scryfall's language property, not available for all languages
@@ -4041,647 +4871,781 @@ Cockatrice will now reload the card database.
Player
-
+
Reveal top cards of library
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
Number of cards: (max. %1)
-
+
&View graveyard
-
+
&View exile
-
+
Player "%1"
-
-
-
-
+
+
+
+
&Graveyard
-
-
-
-
+
+
+
+
&Exile
-
+
&Move hand to...
-
-
-
+
+
+
&Top of library
-
-
-
+
+
+
&Bottom of library
-
+
&Move graveyard to...
-
-
-
-
+
+
+
+
&Hand
-
+
&Move exile to...
-
+
&View library
-
+
&View hand
-
+
View &top cards of library...
-
+
Reveal &library to...
-
+
&Always reveal top card
-
+
&View sideboard
-
+
&Draw card
-
+
D&raw cards...
-
+
&Undo last draw
-
+
Take &mulligan
-
- &Shuffle
-
-
-
-
+
Play top card &face down
-
+
Move top card to grave&yard
-
+
Move top card to e&xile
-
+
Move top cards to &graveyard...
-
+
Move top cards to &exile...
-
+
Put top card on &bottom
-
+
+ View bottom cards of library...
+
+
+
+
Lend library to...
-
+
+ Shuffle
+
+
+
+
Put top cards on stack &until...
Take top cards &until...
-
+
+ Shuffle top cards...
+
+
+
+
+ Shuffle bottom cards...
+
+
+
+
&Reveal hand to...
-
+
Reveal r&andom card to...
-
+
Reveal random card to...
-
+
&Sideboard
-
+
&Library
-
+
&Counters
-
+
&Untap all permanents
-
+
R&oll die...
-
+
&Create token...
-
+
C&reate another token
-
+
Cr&eate predefined token
-
-
+
+
&All players
-
+
S&ay
-
+
+ &Select All
+
+
+
+
+ S&elect Row
+
+
+
+
+ S&elect Column
+
+
+
+
&Play
-
+
&Hide
-
+
Play &Face Down
-
+
&Tap / Untap
Turn sideways or back again
-
+
Toggle &normal untapping
-
+
T&urn Over
Turn face up/face down
-
+
&Peek at card face
-
+
&Clone
-
+
Attac&h to card...
-
+
Unattac&h
-
+
&Draw arrow...
-
+
&Increase power
-
+
&Decrease power
-
+
I&ncrease toughness
-
+
D&ecrease toughness
-
+
In&crease power and toughness
-
+
Dec&rease power and toughness
-
+
Increase power and decrease toughness
-
+
Decrease power and increase toughness
-
+
Set &power and toughness...
-
+
Reset p&ower and toughness
-
+
&Set annotation...
-
+
Red
-
+
Yellow
-
+
Green
-
+
&Add counter (%1)
-
+
&Remove counter (%1)
-
+
&Set counters (%1)...
-
+
&Top of library in random order
-
+
X cards from the top of library...
-
+
&Bottom of library in random order
-
+
View top cards of library
-
+
+ View bottom cards of library
+
+
+
+
+ Shuffle top cards of library
+
+
+
+
+ Shuffle bottom cards of library
+
+
+
+
Which position should this card be placed:
-
+
(max. %1)
-
+
Draw hand
-
+
0 and lower are in comparison to current hand size
-
+
Draw cards
-
-
+
+
Number:
-
+
Move top cards to grave
-
+
Reveal &top cards to...
-
+
&Top of library...
-
+
&Bottom of library...
-
+
&Always look at top card
-
+
&Open deck in deck editor
-
+
&Play top card
-
+
&Draw bottom card
-
+
D&raw bottom cards...
-
+
&Play bottom card
-
+
Play bottom card &face down
-
+
Move bottom card to grave&yard
-
+
Move bottom card to e&xile
-
+
Move bottom cards to &graveyard...
-
+
Move bottom cards to &exile...
-
+
Put bottom card on &top
-
+
Selec&ted cards
-
+
Move top cards to exile
-
+
Move bottom cards to grave
-
+
Move bottom cards to exile
-
+
Draw bottom cards
-
-
+
+
C&reate another %1 token
-
+
Create tokens
-
+
Place card X cards from top of library
-
+
Change power/toughness
-
+
Change stats to:
-
+
Set annotation
-
+
Please enter the new annotation:
-
+
Set counters
-
+
Re&veal to...
-
+
View related cards
-
+
Token:
-
+
All tokens
+
+ PrintingSelector
+
+
+ Display Navigation Buttons
+
+
+
+
+ <b>Warning:</b> You appear to be using custom card art, which has known bugs when also using the printing selector in this version of Cockatrice.
+
+
+
+
+ PrintingSelectorCardOverlayWidget
+
+
+ Preference
+
+
+
+
+ Pin Printing
+
+
+
+
+ Unpin Printing
+
+
+
+
+ Show Related cards
+
+
+
+
+ PrintingSelectorCardSearchWidget
+
+
+ Search by set name or set code
+
+
+
+
+ PrintingSelectorCardSelectionWidget
+
+
+ Previous Card in Deck
+
+
+
+
+ Next Card in Deck
+
+
+
+
+ PrintingSelectorCardSortingWidget
+
+
+ Alphabetical
+
+
+
+
+ Preference
+
+
+
+
+ Release Date
+
+
+
+
+
+ Descending
+
+
+
+
+ Ascending
+
+
+
QMenuBar
-
+
Services
-
+
Hide %1
-
+
Hide Others
-
+
Show All
-
+
Preferences...
-
+
Quit %1
-
+
About %1
@@ -4689,38 +5653,32 @@ Cockatrice will now reload the card database.
QObject
-
+
Cockatrice card database (*.xml)
-
-
+
All files (*.*)
-
+
Cockatrice replays (*.cor)
-
- Common deck formats (*.cod *.dec *.dek *.txt *.mwDeck)
-
-
-
-
+
Maindeck
-
+
Sideboard
-
+
Tokens
@@ -4728,92 +5686,92 @@ Cockatrice will now reload the card database.
QPlatformTheme
-
+
OK
-
+
Save
-
+
Save All
-
+
Open
-
+
&Yes
-
+
Yes to &All
-
+
&No
-
+
N&o to All
-
+
Abort
-
+
Retry
-
+
Ignore
-
+
Close
-
+
Cancel
-
+
Discard
-
+
Help
-
+
Apply
-
+
Reset
-
+
Restore Defaults
@@ -4821,17 +5779,17 @@ Cockatrice will now reload the card database.
RemoteDeckList_TreeModel
-
+
Name
-
+
ID
-
+
Upload time
@@ -4839,32 +5797,32 @@ Cockatrice will now reload the card database.
RemoteReplayList_TreeModel
-
+
ID
-
+
Name
-
+
Players
-
+
Keep
-
+
Time started
-
+
Duration (sec)
@@ -4910,32 +5868,32 @@ Cockatrice will now reload the card database.
SequenceEdit
-
+
Choose an action from the table
-
+
Hit the key/combination of keys you want to set for this action
-
- Shortcut already in use
-
-
-
-
+
Invalid key
-
+
+ Shortcut already in use by:
+
+
+
+
Clear
-
+
Restore default
@@ -4971,72 +5929,80 @@ Cockatrice will now reload the card database.
ShortcutSettingsPage
-
-
+
+
Restore all default shortcuts
-
+
Do you really want to restore all default shortcuts?
-
+
Clear all default shortcuts
-
+
Do you really want to clear all shortcuts?
-
- Action
-
-
-
-
- Shortcut
-
-
-
-
+
Section:
-
+
Action:
-
+
Shortcut:
-
+
How to set custom shortcuts
-
+
Clear all shortcuts
+
+
+ Search by shortcut name
+
+
+
+
+ ShortcutTreeView
+
+
+ Action
+
+
+
+
+ Shortcut
+
+
ShortcutsSettings
-
+
Your configuration file contained invalid shortcuts.
Please check your shortcut settings!
-
+
The following shortcuts have been set to default:
@@ -5045,17 +6011,17 @@ Please check your shortcut settings!
ShutdownDialog
-
+
&Reason for shutdown:
-
+
&Time until shutdown (minutes):
-
+
Shut down server
@@ -5063,27 +6029,27 @@ Please check your shortcut settings!
SoundSettingsPage
-
+
Enable &sounds
-
+
Current sounds theme:
-
+
Test system sound engine
-
+
Sound settings
-
+
Master volume
@@ -5091,48 +6057,48 @@ Please check your shortcut settings!
SpoilerBackgroundUpdater
-
+
Spoilers season has ended
-
+
Deleting spoiler.xml. Please run Oracle
-
-
+
+
Spoilers download failed
-
+
No internet connection
-
+
Error
-
+
Spoilers already up to date
-
+
No new spoilers added
-
+
Spoilers have been updated!
-
+
Last change:
@@ -5140,635 +6106,593 @@ Please check your shortcut settings!
StableReleaseChannel
-
- Stable Releases
+
+ Default
-
+
No reply received from the release update server.
-
+
Invalid reply received from the release update server.
-
+
No reply received from the tag update server.
-
+
Invalid reply received from the tag update server.
+
+ TabAccount
+
+
+ Add to Buddy List
+
+
+
+
+ Add to Ignore List
+
+
+
+
+ Account
+
+
+
TabAdmin
-
+
Administration
-
+
Update server &message
-
+
&Shut down server
-
+
&Reload configuration
-
+
Server administration functions
-
+
+ Server moderator functions
+
+
+
+
+ Replay ID
+
+
+
+
+ Grant Replay Access
+
+
+
+
+ Username to Activate
+
+
+
+
+ Force Activate User
+
+
+
+
&Unlock functions
-
+
&Lock functions
+
+
+
+ Success
+
+
+
+
+ Replay access granted
+
+
+
+
+
+
+
+
+ Error
+
+
+
+
+ Unable to grant replay access. Replay ID invalid
+
+
+
+
+ Unable to grant replay access. Internal error
+
+
+
+
+ User successfully activated
+
+
+
+
+ Unable to activate user. Username invalid
+
+
+
+
+ Unable to activate user. User already active
+
+
+
+
+ Unable to activate user. Internal error
+
+
TabDeckEditor
-
- Search by card name (or search expressions)
- Search by card name
-
-
-
-
- Add to Deck
-
-
-
-
- Add to Sideboard
-
-
-
-
- Show Related cards
-
-
-
-
- &Clear all filters
-
-
-
-
- Delete selected
-
-
-
-
- Deck &name:
-
-
-
-
- &Comments:
-
-
-
-
- Hash:
-
-
-
-
- &New deck
-
-
-
-
- &Load deck...
-
-
-
-
- &Save deck
-
-
-
-
- Save deck &as...
-
-
-
-
- Load deck from cl&ipboard...
-
-
-
-
- Save deck to clipboard
-
-
-
-
- Annotated
-
-
-
-
- Not Annotated
-
-
-
-
- &Print deck...
-
-
-
-
- &Send deck to online service
-
-
-
-
- Create decklist (decklist.org)
-
-
-
-
- Analyze deck (deckstats.net)
-
-
-
-
- Analyze deck (tappedout.net)
-
-
-
-
- &Close
-
-
-
-
- Add card to &maindeck
-
-
-
-
- Add card to &sideboard
-
-
-
-
- &Remove row
-
-
-
-
- &Increment number
-
-
-
-
- &Decrement number
-
-
-
-
- &Deck Editor
-
-
-
-
-
+
Card Info
-
-
+
Deck
-
-
+
Filters
-
+
&View
-
-
-
+
+ Printing
+
+
+
+
+
+
+
Visible
-
-
-
+
+
+
+
Floating
-
+
Reset layout
-
+
Deck: %1
-
-
- Are you sure?
-
-
-
-
- The decklist has been modified.
-Do you want to save the changes?
-
-
-
-
- Load deck
-
-
-
-
-
-
-
-
-
- Error
-
-
-
-
- The deck could not be saved.
-
-
-
-
- Could not save remote deck
-
-
-
-
-
- The deck could not be saved.
-Please check that the directory is writable and try again.
-
-
-
-
- Save deck
-
-
-
-
- There are no cards in your deck to be exported
-
-
-
-
- No deck was selected to be saved.
-
-
TabDeckStorage
-
- Deck storage
-
-
-
-
+
Local file system
-
+
Server deck storage
-
-
+
+
Open in deck editor
-
+
+ Rename deck or folder
+
+
+
+
Upload deck
-
+
Download deck
-
-
+
+
+
+
New folder
-
-
+
+
Delete
-
-
+
+ Open decks folder
+
+
+
+
+ Rename local folder
+
+
+
+
+ Rename local file
+
+
+
+
+ New name:
+
+
+
+
+
+
+
Error
-
+
+ Rename failed
+
+
+
+
+
Invalid deck file
-
+
Enter deck name
-
+
This decklist does not have a name.
Please enter a name:
-
+
Unnamed deck
-
+
Failed to upload deck to server
-
+
Delete local file
-
-
-
- Are you sure you want to delete "%1"?
+
+ Are you sure you want to delete the selected files?
-
+
+ Delete remote decks
+
+
+
+
+ Are you sure you want to delete the selected decks?
+
+
+
+
+
Name of new folder:
-
- Delete remote folder
+
+ Deck Storage
+
+
+
+
+ TabDeckStorageVisual
+
+
+ Visual Deck Storage
-
- Delete remote deck
+
+ Error
+
+
+
+
+ Could not open deck at %1
+
+
+
+
+ TabEdhRec
+
+
+ EDHREC:
TabGame
-
-
-
+
+
+
Replay
-
-
+
+
Game
-
-
+
+
Player List
-
-
+
+
Card Info
-
-
+
+
Messages
-
-
+
+
Replay Timeline
-
+
&Phases
-
+
&Game
-
+
Next &phase
-
+
Next phase with &action
-
+
Next &turn
-
+
Reverse turn order
-
+
&Remove all local arrows
-
+
Rotate View Cl&ockwise
-
+
Rotate View Co&unterclockwise
-
+
Game &information
-
+
+ Un&concede
+
+
+
+
&Concede
-
+
&Leave game
-
+
C&lose replay
-
+
&Focus Chat
-
+
&Say:
-
+
&View
-
-
-
-
+
+
+
+
Visible
-
-
-
-
+
+
+
+
Floating
-
+
Reset layout
-
+
Concede
-
+
Are you sure you want to concede this game?
-
+
Unconcede
-
+
You have already conceded. Do you want to return to this game?
-
+
Leave game
-
+
Are you sure you want to leave this game?
-
+
You are flooding the game. Please wait a couple of seconds.
-
+
A player has joined game #%1
-
+
%1 has joined the game
-
+
kicked by game host or moderator
-
+
player left the game
-
+
player disconnected from server
-
+
reason unknown
-
+
You have been kicked out of the game.
@@ -5781,152 +6705,152 @@ Please enter a name:
-
-
-
+
+
+
Time;SenderName;SenderIP;Message;TargetID;TargetName
-
+
Room Logs
-
+
Game Logs
-
+
Chat Logs
-
-
+
+
Error
-
+
You must select at least one filter.
-
+
You have to select a valid number of days to locate.
-
+
Username:
-
+
IP Address:
-
+
Game Name:
-
+
GameID:
-
+
Message:
-
+
Main Room
-
+
Game Room
-
+
Private Chat
-
+
Past X Days:
-
+
Today
-
+
Last Hour
-
+
Maximum Results:
-
+
At least one filter is required.
The more information you put in, the more specific your results will be.
-
+
Get User Logs
-
+
Clear Filters
-
+
Filters
-
+
Log Locations
-
+
Date Range
-
+
Maximum Results
-
-
+
+
Message History
-
+
Failed to collect message history information.
-
+
There are no messages for the selected filters.
@@ -5949,22 +6873,22 @@ The more information you put in, the more specific your results will be.
-
+
This user is ignoring you, they cannot see your messages in main chat and you cannot join their games.
-
+
Private message from
-
+
%1 has left the server.
-
+
%1 has joined the server.
@@ -5972,117 +6896,153 @@ The more information you put in, the more specific your results will be.
TabReplays
-
- Game replays
-
-
-
-
+
Local file system
-
+
Server replay storage
-
-
+
+
Watch replay
-
-
+
+ Rename
+
+
+
+
+
+ New folder
+
+
+
+
+
Delete
-
+
+ Open replays folder
+
+
+
+
Download replay
-
+
Toggle expiration lock
-
+
+ Rename local folder
+
+
+
+
+ Rename local file
+
+
+
+
+ New name:
+
+
+
+
+ Error
+
+
+
+
+ Rename failed
+
+
+
+
+ Name of new folder:
+
+
+
+
Delete local file
-
- Are you sure you want to delete "%1"?
+
+ Are you sure you want to delete the selected files?
-
- Downloading Replays
+
+ Are you sure you want to delete the selected replays?
-
- Folder download is not yet supported. Please download replays individually.
-
-
-
-
+
Delete remote replay
-
- Are you sure you want to delete the replay of game %1?
+
+ Game Replays
TabRoom
-
+
&Say:
-
+
Chat
-
+
&Room
-
+
&Leave room
-
+
&Clear chat
-
+
Chat Settings...
-
+
mentioned you.
-
+
Click to view
-
+
You are flooding the chat. Please wait a couple of seconds.
@@ -6095,30 +7055,30 @@ The more information you put in, the more specific your results will be.
-
-
-
-
+
+
+
+
Error
-
+
Failed to join the server room: it doesn't exist on the server.
-
+
The server thinks you are in the server room but your client is unable to display it. Try restarting your client.
-
+
You do not have the required permission to join this server room.
-
+
Failed to join the server room due to an unknown error: %1.
@@ -6126,32 +7086,72 @@ The more information you put in, the more specific your results will be.
TabSupervisor
-
+
+ Deck Editor
+
+
+
+
+ &Visual Deck Storage
+
+
+
+
+ Server
+
+
+
+
+ Account
+
+
+
+
+ Deck Storage
+
+
+
+
+ Game Replays
+
+
+
+
+ Administration
+
+
+
+
+ Logs
+
+
+
+
Are you sure?
-
+
There are still open games. Are you sure you want to quit?
-
+
Click to view
-
+
Your buddy %1 has signed on!
-
+
Unknown Event
-
+
The server has sent you a message that your client does not understand.
This message might mean there is a new version of Cockatrice available or this server is running a custom or pre-release version.
@@ -6159,61 +7159,43 @@ To update your client, go to Help -> Check for Updates.
-
+
Idle Timeout
-
+
You are about to be logged out due to inactivity.
-
+
Promotion
-
+
You have been promoted. Please log out and back in for changes to take effect.
-
+
Warned
-
+
You have received a warning due to %1.
Please refrain from engaging in this activity or further actions may be taken against you. If you have any questions, please private message a moderator.
-
+
You have received the following message from the server.
(custom messages like these could be untranslated)
-
- TabUserLists
-
-
- Account
-
-
-
-
- Add to Buddy List
-
-
-
-
- Add to Ignore List
-
-
-
TappedOutInterface
@@ -6297,173 +7279,184 @@ Please refrain from engaging in this activity or further actions may be taken ag
UserContextMenu
-
+
User &details
-
+
Private &chat
-
+
Show this user's &games
-
+
Add to &buddy list
-
+
Remove from &buddy list
-
+
Add to &ignore list
-
+
Remove from &ignore list
-
+
Kick from &game
-
+
Warn user
-
+
View user's war&n history
-
+
Ban from &server
-
+
View user's &ban history
-
+
&Promote user to moderator
-
+
Dem&ote user from moderator
-
+
Promote user to &judge
-
+
Demote user from judge
-
+
+ View admin notes
+
+
+
+
%1's games
-
-
-
+
+
+
Ban History
-
+
Ban Time;Moderator;Ban Length;Ban Reason;Visible Reason
-
+
User has never been banned.
-
+
Failed to collect ban information.
-
-
-
+
+
+
Warning History
-
+
Warning Time;Moderator;User Name;Reason
-
+
User has never been warned.
-
+
Failed to collect warning information.
-
-
+
+ Failed to get admin notes.
+
+
+
+
+
Success
-
+
Successfully promoted user.
-
+
Successfully demoted user.
-
-
+
+
+
Failed
-
+
Failed to promote user.
-
+
Failed to demote user.
-
+
Copy hash to clipboard
-
+
Remove this user's messages
@@ -6471,109 +7464,109 @@ Please refrain from engaging in this activity or further actions may be taken ag
UserInfoBox
-
+
Location:
-
+
Account Age:
-
+
Edit
-
+
Change password
-
+
Change avatar
-
+
Administrator
-
+
Moderator
-
+
Registered user
-
-
+
+
Unregistered user
-
+
Judge
-
+
Unknown
-
+
The entered password does not match your account.
-
-
-
+
+
+
Information
-
+
User information updated.
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
Error
-
+
User Information
-
+
Real Name:
-
+
User Level:
-
+
%n Year(s),
amount of years (only shown if more than 0)
@@ -6582,7 +7575,7 @@ Please refrain from engaging in this activity or further actions may be taken ag
-
+
%10%n Day(s) %20
amount of years (if more than 0), amount of days, date in local short format
@@ -6591,59 +7584,59 @@ Please refrain from engaging in this activity or further actions may be taken ag
-
+
Enter Password
-
+
Password verification is required in order to change your email address
-
-
-
+
+
+
An error occurred while trying to update your user information.
-
+
This server does not permit you to update your user informations.
-
+
Password changed.
-
+
This server does not permit you to change your password.
-
+
The new password is too short.
-
+
The old password is incorrect.
-
+
Avatar updated.
-
+
This server does not permit you to update your avatar.
-
+
An error occured while trying to updater your avatar.
@@ -6651,124 +7644,251 @@ Please refrain from engaging in this activity or further actions may be taken ag
UserInterfaceSettingsPage
-
+
General interface settings
-
+
&Double-click cards to play them (instead of single-click)
-
+
+ &Clicking plays all selected cards (instead of just the clicked card)
+
+
+
+
&Play all nonlands onto the stack (not the battlefield) by default
-
+
+ Close card view window when last card is removed
+
+
+
+
Annotate card text on tokens
-
+
Use tear-off menus, allowing right click menus to persist on screen
-
+
Notifications settings
-
+
Enable notifications in taskbar
-
+
Notify in the taskbar for game events while you are spectating
-
+
Notify in the taskbar when users in your buddy list connect
-
+
Animation settings
-
+
&Tap/untap animation
+
+
+ Deck editor/storage settings
+
+
+
+
+ Open deck in new tab by default
+
+
+
+
+ Prompt before converting .txt decks to .cod format
+
+
+
+
+ Always convert if not prompted
+
+
+
+
+ Use visual deck storage in game lobby
+
+
+
+
+ Replay settings
+
+
+
+
+ Buffer time for backwards skip via shortcut:
+
+
- UserList
+ UserListWidget
-
+
Users connected to server: %1
-
+
Users in this room: %1
-
+
Buddies online: %1 / %2
-
+
Ignored users online: %1 / %2
+
+ VisualDeckStorageFolderDisplayWidget
+
+
+ Deck Storage
+
+
+
+
+ VisualDeckStorageSearchWidget
+
+
+ Search by filename
+
+
+
+
+ VisualDeckStorageSortWidget
+
+
+ Sort Alphabetically (Deck Name)
+
+
+
+
+ Sort Alphabetically (Filename)
+
+
+
+
+ Sort by Last Modified
+
+
+
+
+ Sort by Last Loaded
+
+
+
+
+ VisualDeckStorageWidget
+
+
+ Loading database ...
+
+
+
+
+ Show Folders
+
+
+
+
+ Show Tag Filter
+
+
+
+
+ Show Tags On Deck Previews
+
+
+
+
+ Show Banner Card Selection Option
+
+
+
+
+ Include Folder Names in Search
+
+
+
+
+ Draw unused Color Identities
+
+
+
+
+ Unused Color Identities Opacity
+
+
+
WarningDialog
-
+
Which warning would you like to send?
-
+
Redact all messages from this user in all rooms
-
+
&OK
-
+
&Cancel
-
+
Warn user for misconduct
-
-
+
+
Error
-
+
User name to send a warning to can not be blank, please specify a user to warn.
-
+
Warning to use can not be blank, please select a valid warning to send.
@@ -6776,128 +7896,133 @@ Please refrain from engaging in this activity or further actions may be taken ag
WndSets
-
+
Move selected set to the top
-
+
Move selected set up
-
+
Move selected set down
-
+
Move selected set to the bottom
-
+
Search by set name, code, or type
-
+
Default order
-
+
Restore original art priority order
-
+
Enable all sets
-
+
Disable all sets
-
+
Enable selected set(s)
-
+
Disable selected set(s)
-
+
Deck Editor
-
+
Use CTRL+A to select all sets in the view.
-
+
Only cards in enabled sets will appear in the card list of the deck editor.
-
+
Image priority is decided in the following order:
-
+
first the CUSTOM Folder (%1), then the Enabled Sets in this dialog (Top to Bottom)
%1 is a link to the wiki
-
+
Card Art
-
+
How to use custom card art
-
+
Hints
-
+
Note
-
+
Sorting by column allows you to find a set while not changing set priority.
-
+
To enable ordering again, click the column header until this message disappears.
-
+
Use the current sorting as the set priority instead
-
+
Sorts the set priority using the same column
-
+
+ Include online-only (Arena) cards [requires restart]
+
+
+
+
Manage sets
@@ -6905,22 +8030,67 @@ Please refrain from engaging in this activity or further actions may be taken ag
ZoneViewWidget
-
- sort by name
+
+ Ungrouped
-
- sort by type
+
+ Group by Type
-
+
+ Group by Mana Value
+
+
+
+
+ Group by Color
+
+
+
+
+ Unsorted
+
+
+
+
+ Sort by Name
+
+
+
+
+ Sort by Type
+
+
+
+
+ Sort by Mana Cost
+
+
+
+
+ Sort by Colors
+
+
+
+
+ Sort by P/T
+
+
+
+
+ Sort by Set
+
+
+
+
shuffle when closing
-
+
pile view
@@ -6928,7 +8098,7 @@ Please refrain from engaging in this activity or further actions may be taken ag
i18n
-
+
English
@@ -6936,12 +8106,12 @@ Please refrain from engaging in this activity or further actions may be taken ag
main
-
+
Connect on startup
-
+
Debug to file
@@ -6949,786 +8119,908 @@ Please refrain from engaging in this activity or further actions may be taken ag
shortcutsTab
-
+
Main Window
-
-
+
+
Deck Editor
-
+
Game Lobby
-
+
Card Counters
-
+
Player Counters
-
+
Power and Toughness
-
+
Game Phases
-
+
Playing Area
-
+
Move Selected Card
-
+
View
-
+
Move Top Card
-
+
Move Bottom Card
-
+
Gameplay
-
+
Drawing
-
+
Chat Room
-
+
Game Window
-
+
Load Deck from Clipboard
-
+
+
Replays
-
+
+ Tabs
+
+
+
+
Check for Card Updates...
-
+
Connect...
-
+
Disconnect
-
+
Exit
-
+
Full screen
-
+
Register...
-
+
Settings...
-
+
Start a Local Game...
-
+
Watch Replay...
-
- Analyze Deck
+
+ Analyze Deck (deckstats.net)
+ Analyze Deck
-
- Clear All Filters
-
-
-
-
- Clear Selected Filter
-
-
-
-
- Close
+
+ Analyze Deck (tappedout.net)
- Remove Card
+ Clear All Filters
+ Clear Selected Filter
+
+
+
+
+ Close
+
+
+
+
+ Remove Card
+
+
+
+
Manage Sets...
-
+
Edit Custom Tokens...
-
- Export Deck
-
-
-
-
- Add Card
-
-
-
-
- Load Deck...
-
-
-
-
- Load Deck from Clipboard...
+
+ Export Deck (decklist.org)
- New Deck
+ Export Deck (decklist.xyz)
-
- Open Custom Pictures Folder
+
+ Add Card
-
- Print Deck...
+
+ Load Deck...
+ Load Deck from Clipboard...
+
+
+
+
+ Edit Deck in Clipboard, Annotated
+
+
+
+
+ Edit Deck in Clipboard
+
+
+
+
+ New Deck
+
+
+
+
+ Open Custom Pictures Folder
+
+
+
+
+ Print Deck...
+
+
+
+
Delete Card
-
-
+
+
Reset Layout
-
+
Save Deck
-
+
Save Deck as...
-
+
Save Deck to Clipboard, Annotated
-
- Save Deck to Clipboard
-
-
-
-
- Load Local Deck...
-
-
-
-
- Load Remote Deck...
-
-
-
-
- Set Ready to Start
-
-
-
-
- Toggle Sideboard Lock
-
-
-
-
-
- Add Green Counter
-
-
-
-
-
- Remove Green Counter
-
-
-
-
-
- Set Green Counters...
+
+ Save Deck to Clipboard, Annotated (No Set Info)
+ Save Deck to Clipboard
+
+
+
+
+ Save Deck to Clipboard (No Set Info)
+
+
+
+
+ Load Local Deck...
+
+
+
+
+ Load Remote Deck...
+
+
+
+
+ Set Ready to Start
+
+
+
+
+ Toggle Sideboard Lock
+
+
+
+
+
+ Add Green Counter
+
+
+
+
+
+ Remove Green Counter
+
+
+
+
+
+ Set Green Counters...
+
+
+
+
Add Yellow Counter
-
+
Remove Yellow Counter
-
+
Set Yellow Counters...
-
-
+
+
Add Red Counter
-
-
+
+
Remove Red Counter
-
-
+
+
Set Red Counters...
-
+
Add Life Counter
-
+
Remove Life Counter
-
+
Set Life Counters...
-
+
Add White Counter
-
+
Remove White Counter
-
+
Set White Counters...
-
+
Add Blue Counter
-
+
Remove Blue Counter
-
+
Set Blue Counters...
-
+
Add Black Counter
-
+
Remove Black Counter
-
+
Set Black Counters...
-
+
Add Colorless Counter
-
+
Remove Colorless Counter
-
+
Set Colorless Counters...
-
+
Add Other Counter
-
+
Remove Other Counter
-
+
Set Other Counters...
-
+
Add Power (+1/+0)
-
+
Remove Power (-1/-0)
-
+
Move Toughness to Power (+1/-1)
-
+
Add Toughness (+0/+1)
-
+
Remove Toughness (-0/-1)
-
+
Move Power to Toughness (-1/+1)
-
+
Add Power and Toughness (+1/+1)
-
+
Remove Power and Toughness (-1/-1)
-
+
Set Power and Toughness...
-
+
Reset Power and Toughness
-
+
Untap
-
+
Upkeep
-
+
Draw
-
+
First Main Phase
-
+
Start Combat
-
+
Attack
-
+
Block
-
+
Damage
-
+
End Combat
-
+
Second Main Phase
-
+
End
-
+
Next Phase
-
+
Next Phase Action
-
+
Next Turn
-
+
+ Hide Card in Reveal Window
+
+
+
+
Tap / Untap Card
-
+
Untap All
-
+
Toggle Untap
-
+
Turn Card Over
-
+
Peek Card
-
+
Play Card
-
+
Attach Card...
-
+
Unattach Card
-
+
Clone Card
-
+
Create Token...
-
+
Create All Related Tokens
-
+
Create Another Token
-
+
Set Annotation...
-
-
+
+ Select All Cards in Zone
+
+
+
+
+ Select All Cards in Row
+
+
+
+
+ Select All Cards in Column
+
+
+
+
+
Bottom of Library
-
-
-
-
+
+
+
+
Exile
-
-
-
-
+
+
+
+
Graveyard
-
-
+
+
Hand
-
-
+
+
Top of Library
-
-
-
+
+
+
Battlefield, Face Down
-
+
Battlefield
-
+
Library
-
+
Sideboard
-
+
Top Cards of Library
-
+
+ Bottom Cards of Library
+
+
+
+
Close Recent View
-
-
+
+
Stack
-
-
+
+
Graveyard (Multiple)
-
-
+
+
Exile (Multiple)
-
+
Stack Until Found
-
+
Draw Bottom Card
-
+
Draw Multiple Cards from Bottom...
-
+
Draw Arrow...
-
+
Remove Local Arrows
-
+
Leave Game
-
+
Concede
-
+
Roll Dice...
-
+
Shuffle Library
-
+
+ Shuffle Top Cards of Library
+
+
+
+
+ Shuffle Bottom Cards of Library
+
+
+
+
Mulligan
-
+
Draw a Card
-
+
Draw Multiple Cards...
-
+
Undo Draw
-
+
Always Reveal Top Card
-
+
Always Look At Top Card
-
+
Rotate View Clockwise
-
+
Rotate View Counterclockwise
-
+
Unfocus Text Box
-
+
Focus Chat
-
+
Clear Chat
-
+
Refresh
-
+
+ Skip Forward
+
+
+
+
+ Skip Backward
+
+
+
+
+ Skip Forward by a lot
+
+
+
+
+ Skip Backward by a lot
+
+
+
+
Play/Pause
-
+
Toggle Fast Forward
+
+
+ Visual Deck Storage
+
+
+
+
+ Deck Storage
+
+
+
+
+ Server
+
+
+
+
+ Account
+
+
+
+
+ Administration
+
+
+
+
+ Logs
+
+
diff --git a/cockatrice/resources/config/debug.ini b/cockatrice/resources/config/debug.ini
new file mode 100644
index 000000000..a1d203c7a
--- /dev/null
+++ b/cockatrice/resources/config/debug.ini
@@ -0,0 +1,11 @@
+[debug]
+showCardId=false
+
+[localgame]
+onStartup=false
+playerCount=1
+;deck\Player 1=path/to/deck
+;deck\Player 2=path/to/deck
+
+; Fun Fact: You can assign a deck to your username and it will auto load and ready when you join a server game
+;deck\Your Username Here=path/to/deck
diff --git a/cockatrice/resources/config/qtlogging.ini b/cockatrice/resources/config/qtlogging.ini
new file mode 100644
index 000000000..5404c96fb
--- /dev/null
+++ b/cockatrice/resources/config/qtlogging.ini
@@ -0,0 +1,62 @@
+[Rules]
+# The default log level is info
+*.debug = false
+
+# Uncomment a rule to disable logging for that category,
+# or set .debug = true for that category to see debug level logs
+
+# main = false
+# qt_translator = false
+# window_main.* = false
+# release_channel = false
+# spoiler_background_updater = false
+# theme_manager = false
+# sound_engine = false
+# tapped_out_interface = false
+
+# tab_game = false
+# tab_message = false
+# tab_supervisor = false
+
+# dlg_edit_avatar = false
+# dlg_settings = false
+# dlg_tip_of_the_day = false
+# dlg_update = false
+
+# settings_cache = false
+# servers_settings = false
+# shortcuts_settings = false
+
+# remote_client = false
+
+# player = false
+# game_scene = false
+# game_scene.player_addition_removal = false
+# card_zone = false
+# view_zone = false
+
+# user_info_connection = false
+
+# picture_loader = false
+# picture_loader.worker = false
+# picture_loader.card_back_cache_fail = false
+# picture_loader.picture_to_load = false
+# deck_loader = false
+# card_database = false
+# card_database.loading = false
+# card_database.loading.success_or_failure = false
+# cockatrice_xml.* = false
+# cockatrice_xml.xml_3_parser = false
+# cockatrice_xml.xml_4_parser = false
+# card_info = false
+# card_list = false
+
+#flow_layout = false
+#flow_widget = false
+#flow_widget.size = false
+
+# card_info_picture_widget = false
+
+# pixel_map_generator = false
+
+# filter_string = false
diff --git a/cockatrice/resources/icons/cogwheel.svg b/cockatrice/resources/icons/cogwheel.svg
new file mode 100644
index 000000000..8c7aad617
--- /dev/null
+++ b/cockatrice/resources/icons/cogwheel.svg
@@ -0,0 +1,29 @@
+
+
+
\ No newline at end of file
diff --git a/cockatrice/resources/icons/dropdown_collapsed.svg b/cockatrice/resources/icons/dropdown_collapsed.svg
new file mode 100644
index 000000000..52e833e65
--- /dev/null
+++ b/cockatrice/resources/icons/dropdown_collapsed.svg
@@ -0,0 +1,10 @@
+
+
+
+
+ ]>
+
diff --git a/cockatrice/resources/icons/dropdown_expanded.svg b/cockatrice/resources/icons/dropdown_expanded.svg
new file mode 100644
index 000000000..fa3ee205c
--- /dev/null
+++ b/cockatrice/resources/icons/dropdown_expanded.svg
@@ -0,0 +1,10 @@
+
+
+
+
+ ]>
+
diff --git a/cockatrice/resources/icons/mana/B.svg b/cockatrice/resources/icons/mana/B.svg
new file mode 100644
index 000000000..d7e360054
--- /dev/null
+++ b/cockatrice/resources/icons/mana/B.svg
@@ -0,0 +1,47 @@
+
+
diff --git a/cockatrice/resources/icons/mana/G.svg b/cockatrice/resources/icons/mana/G.svg
new file mode 100644
index 000000000..3521f9d5a
--- /dev/null
+++ b/cockatrice/resources/icons/mana/G.svg
@@ -0,0 +1,47 @@
+
+
diff --git a/cockatrice/resources/icons/mana/R.svg b/cockatrice/resources/icons/mana/R.svg
new file mode 100644
index 000000000..1f3f606eb
--- /dev/null
+++ b/cockatrice/resources/icons/mana/R.svg
@@ -0,0 +1,47 @@
+
+
diff --git a/cockatrice/resources/icons/mana/U.svg b/cockatrice/resources/icons/mana/U.svg
new file mode 100644
index 000000000..b9b9f6069
--- /dev/null
+++ b/cockatrice/resources/icons/mana/U.svg
@@ -0,0 +1,48 @@
+
+
diff --git a/cockatrice/resources/icons/mana/W.svg b/cockatrice/resources/icons/mana/W.svg
new file mode 100644
index 000000000..3b8a6e06b
--- /dev/null
+++ b/cockatrice/resources/icons/mana/W.svg
@@ -0,0 +1,47 @@
+
+
diff --git a/cockatrice/resources/icons/reload.svg b/cockatrice/resources/icons/reload.svg
new file mode 100644
index 000000000..102995876
--- /dev/null
+++ b/cockatrice/resources/icons/reload.svg
@@ -0,0 +1,7 @@
+
+
+
\ No newline at end of file
diff --git a/cockatrice/resources/icons/rename.svg b/cockatrice/resources/icons/rename.svg
new file mode 100644
index 000000000..c68a00246
--- /dev/null
+++ b/cockatrice/resources/icons/rename.svg
@@ -0,0 +1,10 @@
+
+
diff --git a/cockatrice/resources/icons/swap.svg b/cockatrice/resources/icons/swap.svg
new file mode 100644
index 000000000..a18ca2f06
--- /dev/null
+++ b/cockatrice/resources/icons/swap.svg
@@ -0,0 +1,12 @@
+
+
+
+
\ No newline at end of file
diff --git a/cockatrice/resources/tips/images/add_card.png b/cockatrice/resources/tips/images/add_card.png
new file mode 100644
index 000000000..731dc305a
Binary files /dev/null and b/cockatrice/resources/tips/images/add_card.png differ
diff --git a/cockatrice/resources/tips/images/card_select.png b/cockatrice/resources/tips/images/card_select.png
new file mode 100644
index 000000000..1d2cc8f91
Binary files /dev/null and b/cockatrice/resources/tips/images/card_select.png differ
diff --git a/cockatrice/resources/tips/images/edhrec.png b/cockatrice/resources/tips/images/edhrec.png
new file mode 100644
index 000000000..8f17e6d49
Binary files /dev/null and b/cockatrice/resources/tips/images/edhrec.png differ
diff --git a/cockatrice/resources/tips/images/expand_card_view.png b/cockatrice/resources/tips/images/expand_card_view.png
new file mode 100644
index 000000000..0f348cf7a
Binary files /dev/null and b/cockatrice/resources/tips/images/expand_card_view.png differ
diff --git a/cockatrice/resources/tips/images/filter_games.png b/cockatrice/resources/tips/images/filter_games.png
index de4eb7fdb..28cc7eacc 100644
Binary files a/cockatrice/resources/tips/images/filter_games.png and b/cockatrice/resources/tips/images/filter_games.png differ
diff --git a/cockatrice/resources/tips/images/highlight_cards.png b/cockatrice/resources/tips/images/highlight_cards.png
new file mode 100644
index 000000000..8ffeeb3b0
Binary files /dev/null and b/cockatrice/resources/tips/images/highlight_cards.png differ
diff --git a/cockatrice/resources/tips/images/pawns.png b/cockatrice/resources/tips/images/pawns.png
new file mode 100644
index 000000000..300a388bc
Binary files /dev/null and b/cockatrice/resources/tips/images/pawns.png differ
diff --git a/cockatrice/resources/tips/images/setpt.png b/cockatrice/resources/tips/images/setpt.png
index 974c0224a..ad3ce3831 100644
Binary files a/cockatrice/resources/tips/images/setpt.png and b/cockatrice/resources/tips/images/setpt.png differ
diff --git a/cockatrice/resources/tips/images/shortcuts.png b/cockatrice/resources/tips/images/shortcuts.png
index 2034fdfd8..f64f9ad43 100644
Binary files a/cockatrice/resources/tips/images/shortcuts.png and b/cockatrice/resources/tips/images/shortcuts.png differ
diff --git a/cockatrice/resources/tips/images/syntax_help.png b/cockatrice/resources/tips/images/syntax_help.png
new file mode 100644
index 000000000..460b6d29a
Binary files /dev/null and b/cockatrice/resources/tips/images/syntax_help.png differ
diff --git a/cockatrice/resources/tips/images/token.png b/cockatrice/resources/tips/images/token.png
new file mode 100644
index 000000000..3f4fb14c1
Binary files /dev/null and b/cockatrice/resources/tips/images/token.png differ
diff --git a/cockatrice/resources/tips/images/updates.png b/cockatrice/resources/tips/images/updates.png
new file mode 100644
index 000000000..b79e80f35
Binary files /dev/null and b/cockatrice/resources/tips/images/updates.png differ
diff --git a/cockatrice/resources/tips/images/visual_deck_tags.png b/cockatrice/resources/tips/images/visual_deck_tags.png
new file mode 100644
index 000000000..903fa2012
Binary files /dev/null and b/cockatrice/resources/tips/images/visual_deck_tags.png differ
diff --git a/cockatrice/resources/tips/tips_of_the_day.xml b/cockatrice/resources/tips/tips_of_the_day.xml
index 32afea374..cf1c419c7 100644
--- a/cockatrice/resources/tips/tips_of_the_day.xml
+++ b/cockatrice/resources/tips/tips_of_the_day.xml
@@ -1,99 +1,222 @@
-
- Tip of the Day
- Tip of the Day is a new feature to Cockatrice that allows users to get information about the newest features of the program and some of the most commonly asked questions!
- tip_of_the_day.png
- 2018-03-01
-
-
- Suggesting New Tips
- You can suggest new Tips of the Day by reaching out to the development team on <a href="https://discord.gg/3Z9yzmA">Discord</a>!
- discord.png
- 2023-10-18
-
-
- Reporting Bugs
- If you encounter a bug while using Cockatrice, you can report the bug to the development team via <a href="https://github.com/cockatrice/cockatrice/issues">GitHub<a>
- github_logo.png
- 2018-03-01
-
-
- FAQ/Troubleshooting Wiki
- You can find answers to the most common questions and some helpful Cockatrice toubleshooting over on the <a href="https://github.com/cockatrice/cockatrice/wiki">GitHub wiki<a>
- cockatrice_wiki.png
- 2018-03-01
-
-
- Register for a Server
- Click on either Cockatrice (Windows) or Actions (Mac) and then Register to server... When the dialogue appears, fill out the desired server information.
- cockatrice_register.png
- 2018-03-01
-
-
- Drawing Arrows
- You can draw arrows of different color by holding a combination of keys!
- Right Click: Red Arrow
- Shift + Right Click: Green Arrow
- Alt + Right Click: Blue Arrow
- Cmd + Right Click: Yellow Arrow
-
- arrows.png
- 2018-03-01
-
-
- Filtering Games
- Don't see all the active games? Want to see a smaller selection? Use the Game Filters to change your horizon
- filter_games.png
- 2018-03-01
-
-
- Upload Custom Avatar
- Want to show off your hippo avatar? Need to update your password? Check out the Accounts Tab for more info!
- accounts_tab.png
- 2018-03-01
-
-
- Common Shortcuts
- You can find a full list of shortcuts <a href="https://github.com/Cockatrice/Cockatrice/wiki/Custom-Keyboard-Shortcuts">on the wiki</a>, but a short list:
- <br>Roll a die: CTRL + I
- <br>Mulligan: CTRL + M
- <br>Draw a card: CTRL + D
- <br>Undo a draw: CTRL + SHIFT + D
- <br>View Sideboard: CTRL + F3
- <br>Change Life: CTRL + L
- <br>All shortcuts can be customized via Settings->Shortcuts!
-
- shortcuts.png
- 2018-03-01
-
-
- Changing Themes
- Did you know Cockatrice has custom themes? You can either <a href="https://github.com/Cockatrice/Cockatrice/wiki/Themes">create one yourself</a> or use one of the several pre-loaded ones! Go to Settings->Appearance and try them out!
- themes.png
- 2018-03-01
-
-
- Flip of the Coin
- You can flip a coin instead of rolling a die by rolling a 2 sided die instead!
- coin_flip.png
- 2018-03-01
-
-
- Face Down Cards
- You can hold Shift while dragging or clicking on a card to have it enter play face down
- face_down.png
- 2018-03-01
-
-
- Counter expressions
- When setting a counter value, you can type a math expression in the box and the counter will be set to the result.<br>The "x" variable contains the current counter value.
- counter_expression.png
- 2019-02-02
-
-
- Power and Toughness
- You can add and subtract to a creature's stats.<br>With a card selected, set the power and toughness ( default: ctrl + p ) and enter +3/-1 to increase power by three while decreasing toughness by one.<br>You can also reset it to the original value ( default: ctrl + alt + 0 ).
- setpt.png
- 2019-03-02
-
+
+ Tip of the Day
+ Tip of the Day is a feature to Cockatrice that allows users to get information about the newest features of the program and some of the most commonly asked questions!
+Check back in with major updates for new tips to be added or old tips to be updated as features are added or expanded upon!
+
+ tip_of_the_day.png
+ 2025-02-10
+
+
+ Join the Community
+ You can join the community to find games, interact with other players, suggest new 'Tips of the Day' and provide user feedback to the development or support teams on <a href="https://discord.gg/3Z9yzmA">Discord</a>!
+ discord.png
+ 2025-02-09
+
+
+ Reporting Bugs and Requesting Features
+ If you encounter a bug while using Cockatrice, you can report the bug to and request the feature from the development team via <a href="https://github.com/cockatrice/cockatrice/issues">GitHub<a>
+ github_logo.png
+ 2025-02-09
+
+
+ FAQ/Troubleshooting Wiki
+ You can find answers to the most common questions and some helpful Cockatrice troubleshooting over on the <a href="https://github.com/cockatrice/cockatrice/wiki">GitHub wiki<a>
+ cockatrice_wiki.png
+ 2018-03-01
+
+
+ Register for a Server
+ Click on either Cockatrice (Windows) or Actions (Mac) and then Register to server... When the dialogue appears, fill out the desired server information.
+ cockatrice_register.png
+ 2018-03-01
+
+
+ Filtering Games
+ Don't see an open game or want to see a smaller selection? Use the Game Filters to change your horizon!
+ filter_games.png
+ 2025-02-09
+
+
+ Common Shortcuts
+ You can find a full list of default shortcuts <a href="https://github.com/Cockatrice/Cockatrice/wiki/Custom-Keyboard-Shortcuts">on the wiki</a> or in Settings -> Shortcuts, but a short list:
+<ul>
+<li>Roll a die: CMD/CTRL + I</li>
+<li>Mulligan: CMD/CTRL + M</li>
+<li>Draw 1 / X card(s): CMD/CTRL + D / E</li>
+<li>Undo a draw: CMD/CTRL + SHIFT + D</li>
+<li>View Library / Sideboard: F3 / CMD/CTRL + F3</li>
+<li>Change Life: CMD/CTRL + L</li>
+<li>Show Card Info: Middle Mouse Click</li>
+</ul>
+All shortcuts can be customized via Cockatrice -> Settings -> Shortcuts!
+
+ shortcuts.png
+ 2025-02-09
+
+
+ Power and Toughness
+ You can add and subtract to a creature's stats.
+With a card selected, with the following keybindings:
+
+<ul>
+<li>Set P/T to any value: CTRL + P</li>
+<li>+1 or -1 to both to P/T: CMD/CTRL + ALT + '+' or '-'</li>
+<li>+1 or -1 to Power Only: CMD/CTRL + '+' or '-'</li>
+<li>+1 or -1 to Toughness Only: ALT + '+' or '-'</li>
+<li>Set P/T to Default: CMD/CTRL + ALT + 0</li>
+</ul>
+All shortcuts can be customized via Cockatrice -> Settings -> Shortcuts!
+
+ setpt.png
+ 2025-02-09
+
+
+ Drawing Arrows
+ You can draw arrows of different color by holding a combination of keys!
+
+<ul>
+<li>Right Click: Red Arrow</li>
+<li>SHIFT + Right Click: Green Arrow</li>
+<li>ALT + Right Click: Blue Arrow</li>
+<li>CMD/CTRL + Right Click: Yellow Arrow</li>
+</ul>
+
+ arrows.png
+ 2018-03-01
+
+
+ General Etiquette
+ When playing on Cockatrice there are general "rules of engagement" for a better play experience for all players.
+The following are some of the expectations a player should reasonably expect for how to operate smooth and enjoyable games:
+<ul>
+<li>Have a "Rule 0" conversation pre-game with your fellow players on the expectations of the game including deck strength, house rules (e.g. no take backs and "may is not must"), etc.</li>
+<li>When rolling your die for turn order, it is often best to create a token with the text of your roll (using CTRL + T and typing the number into "Name") and then tap this token when created to show that you have resolved your mulligans and are ready to start the game.</li>
+<li>Use the phases and steps trackers on the left of the client to show others where in the turn you are and announce when moving between them. (Double Click for the action of this button)</li>
+<li>When your turn is over leave it on your end step and DO NOT HIT PASS. Allow other players to respond if they can, and then if not, let the next player pass to themselves.</li>
+<li>Please treat others kindly and respect as per <a href="https://cockatrice.us/terms.php">our Terms of Service</a>.</li>
+</ul>
+
+ token.png
+ 2025-02-10
+
+
+ Flip of the Coin
+ You can flip a coin instead of rolling a die by rolling a 2 sided die instead!
+ coin_flip.png
+ 2018-03-01
+
+
+ Upload Custom Avatar
+ Want to show off your hippo avatar? Need to update your password? Check out the Accounts Tab for more info!
+ accounts_tab.png
+ 2018-03-01
+
+
+ Player Icon Key
+ Besides your name is a player icon, this is a key for what they mean:
+<ul>
+<li>Flag: Country of Origin Player Selected</li>
+<li>Purple Heart: Donator (<a href="https://cockatrice.us/donate">Help support us and donate here!</a>)</li>
+<li>Gold Star: VIP (Special Admin Given Role)</li>
+<li>Brown Gavel (Hammer): Judge / Rule's Lawyer</li>
+<li>Black/White Pawn: Moderator (Support for Client Issues)</li>
+<li>Red: Administrator (Sever Operators)</li>
+</ul>
+
+ pawns.png
+ 2025-02-09
+
+
+ Changing Themes
+ Did you know Cockatrice has custom themes? You can either <a href="https://github.com/Cockatrice/Cockatrice/wiki/Themes">create one yourself</a> or use one of the several preloaded ones! Go to Settings->Appearance and try them out!
+ themes.png
+ 2018-03-01
+
+
+ Face Down Cards
+ You can hold Shift while dragging or clicking on a card to have it enter play face down.
+You can also hold CTRL + SHIFT and click and drag from your library to move bottom card face down as well!
+
+ face_down.png
+ 2025-02-09
+
+
+ Counter expressions
+ When setting a counter value, you can type a math expression in the box and the counter will be set to the result.<br>The "x" variable contains the current counter value.
+ counter_expression.png
+ 2019-02-02
+
+
+ Select Multiple Cards
+ You can click and drag in any zone in order to highlight all cards within the created box.
+You can also hold CMD/CTRL and clik or click and drag to maintain other previously selected cards while adding others!
+You can move, alter and attach multiple highlighted cards at the same time!
+Other useful multi-select keybindings:
+<ul>
+<li>Select All Cards in Zone: CMD/CTRL + A</li>
+<li>Select All Cards in Column: CMD/CTRL + SHIFT + C</li>
+<li>Select All Cards in Row: CMD/CTRL + SHIFT + X</li>
+</ul>
+
+ highlight_cards.png
+ 2025-02-09
+
+
+ Auto Cascade and Search Parameters
+ You can use the default keybind of CMD/CTRL + SHIFT + Y in order to perform auto cascade or similar effects from your library using Scryfall search syntax.
+If you are unfamiliar with the syntax you may find it by opening a deck in deck editor and clicking the "i" next to the search bar in order to pull up a list of syntax commands.
+This same syntax can be used in the deck editor search bar as well to help you find the best cards for your decks!
+
+ syntax_help.png
+ 2025-02-09
+
+
+ Visual Deck Storage and Tags
+ You can now view your saved decks visually with integrated folder,tags and filter support!
+Pro Tip: If you want to use emoji's press WinKey + '.' or CTRL + CMD + SPACE on Windows or Mac respectively!
+
+ visual_deck_tags.png
+ 2025-02-09
+
+
+ Card Selector And Card Preferences
+ Cockatrice's deck editor now has the ability for players to bling out their decks by selecting the arts for cards on a per-card basis!
+You can also have multiple printings of the same card in your deck if you so choose!
+Not only that, other players will see the arts you have chosen by default!
+If you want to disable this feature for other players: Settings -> Appearance -> Select "Override all card art with personal set preference"
+
+ card_select.png
+ 2025-02-10
+
+
+ EDHREC Integration
+ We now have in-client integration for EDHREC, which allows you to right-click on any card name in the deck editor in order to bring up the contextual menu for the card.
+ edhrec.png
+ 2025-02-10
+
+
+ Add Cards to Deck
+ You can now right-click anywhere on a card in any card info view in order to add it to any decks you have open in your tabs.
+ add_card.png
+ 2025-02-10
+
+
+ Update Client and Card Database
+ You can update your client by going to Help and selecting whichever you wish to update.
+Client Update: Updates the client (if available) for new features, fixes and changes.
+Card Update: Updates card sources for spoilers and new card printings.
+Updating your card sources can often fix issues of cards not working or displaying properly, if not, go to Cockatrice -> Settings -> Card Sources -> Update Spoilers
+
+ updates.png
+ 2025-02-10
+
+
+ Expand Card View Window
+ Double click on the title bar of the card view window to expand it! Double click again to shrink it back down.
+ You can configure the initial and expanded heights of the window in the settings, under "Appearance".
+
+ expand_card_view.png
+ 2025-02-27
+
diff --git a/cockatrice/resources/userlevels/registered.svg b/cockatrice/resources/usericons/pawn_donator_double.svg
similarity index 74%
rename from cockatrice/resources/userlevels/registered.svg
rename to cockatrice/resources/usericons/pawn_donator_double.svg
index a7f8a72cf..f0dba08b2 100644
--- a/cockatrice/resources/userlevels/registered.svg
+++ b/cockatrice/resources/usericons/pawn_donator_double.svg
@@ -2,20 +2,20 @@
diff --git a/cockatrice/resources/usericons/pawn_donator_single.svg b/cockatrice/resources/usericons/pawn_donator_single.svg
new file mode 100644
index 000000000..9e7d63240
--- /dev/null
+++ b/cockatrice/resources/usericons/pawn_donator_single.svg
@@ -0,0 +1,229 @@
+
+
+
+
diff --git a/cockatrice/resources/userlevels/normal.svg b/cockatrice/resources/usericons/pawn_double.svg
similarity index 72%
rename from cockatrice/resources/userlevels/normal.svg
rename to cockatrice/resources/usericons/pawn_double.svg
index 172d8620c..b9f72d385 100644
--- a/cockatrice/resources/userlevels/normal.svg
+++ b/cockatrice/resources/usericons/pawn_double.svg
@@ -2,20 +2,20 @@
diff --git a/cockatrice/resources/userlevels/registered_vip.svg b/cockatrice/resources/usericons/pawn_judge_double.svg
similarity index 81%
rename from cockatrice/resources/userlevels/registered_vip.svg
rename to cockatrice/resources/usericons/pawn_judge_double.svg
index 48982fa2f..e929f6490 100644
--- a/cockatrice/resources/userlevels/registered_vip.svg
+++ b/cockatrice/resources/usericons/pawn_judge_double.svg
@@ -2,33 +2,22 @@
diff --git a/cockatrice/resources/usericons/pawn_judge_single.svg b/cockatrice/resources/usericons/pawn_judge_single.svg
new file mode 100644
index 000000000..10246d42d
--- /dev/null
+++ b/cockatrice/resources/usericons/pawn_judge_single.svg
@@ -0,0 +1,212 @@
+
+
+
+
diff --git a/cockatrice/resources/userlevels/registered_donator.svg b/cockatrice/resources/usericons/pawn_single.svg
similarity index 78%
rename from cockatrice/resources/userlevels/registered_donator.svg
rename to cockatrice/resources/usericons/pawn_single.svg
index 1ec430e68..2f0c94ae8 100644
--- a/cockatrice/resources/userlevels/registered_donator.svg
+++ b/cockatrice/resources/usericons/pawn_single.svg
@@ -2,20 +2,20 @@
diff --git a/cockatrice/resources/userlevels/moderator_vip.svg b/cockatrice/resources/usericons/pawn_vip_double.svg
similarity index 96%
rename from cockatrice/resources/userlevels/moderator_vip.svg
rename to cockatrice/resources/usericons/pawn_vip_double.svg
index 6d93983e3..0d002a791 100644
--- a/cockatrice/resources/userlevels/moderator_vip.svg
+++ b/cockatrice/resources/usericons/pawn_vip_double.svg
@@ -337,19 +337,19 @@
id="layer1"
transform="translate(0,-952.36218)">
+ id="right" />
+ id="left" />
diff --git a/cockatrice/resources/userlevels/registered_buddy.svg b/cockatrice/resources/usericons/star_single.svg
similarity index 97%
rename from cockatrice/resources/userlevels/registered_buddy.svg
rename to cockatrice/resources/usericons/star_single.svg
index 2d2ca600b..acc5e4da6 100644
--- a/cockatrice/resources/userlevels/registered_buddy.svg
+++ b/cockatrice/resources/usericons/star_single.svg
@@ -109,7 +109,7 @@
inkscape:connector-curvature="0" />
diff --git a/cockatrice/resources/userlevels/admin.svg b/cockatrice/resources/userlevels/admin.svg
deleted file mode 100644
index 5675cc0f8..000000000
--- a/cockatrice/resources/userlevels/admin.svg
+++ /dev/null
@@ -1,301 +0,0 @@
-
-
-
-
diff --git a/cockatrice/resources/userlevels/admin_buddy.svg b/cockatrice/resources/userlevels/admin_buddy.svg
deleted file mode 100644
index 5ff5c87aa..000000000
--- a/cockatrice/resources/userlevels/admin_buddy.svg
+++ /dev/null
@@ -1,136 +0,0 @@
-
-
diff --git a/cockatrice/resources/userlevels/admin_vip_buddy.svg b/cockatrice/resources/userlevels/admin_vip_buddy.svg
deleted file mode 100644
index 5ff5c87aa..000000000
--- a/cockatrice/resources/userlevels/admin_vip_buddy.svg
+++ /dev/null
@@ -1,136 +0,0 @@
-
-
diff --git a/cockatrice/resources/userlevels/moderator.svg b/cockatrice/resources/userlevels/moderator.svg
deleted file mode 100644
index 37ac156b9..000000000
--- a/cockatrice/resources/userlevels/moderator.svg
+++ /dev/null
@@ -1,294 +0,0 @@
-
-
-
-
diff --git a/cockatrice/resources/userlevels/moderator_vip_buddy.svg b/cockatrice/resources/userlevels/moderator_vip_buddy.svg
deleted file mode 100644
index 73a3efb04..000000000
--- a/cockatrice/resources/userlevels/moderator_vip_buddy.svg
+++ /dev/null
@@ -1,131 +0,0 @@
-
-
diff --git a/cockatrice/resources/userlevels/registered_donator_buddy.svg b/cockatrice/resources/userlevels/registered_donator_buddy.svg
deleted file mode 100644
index 476d55088..000000000
--- a/cockatrice/resources/userlevels/registered_donator_buddy.svg
+++ /dev/null
@@ -1,137 +0,0 @@
-
-
diff --git a/cockatrice/resources/userlevels/registered_vip_buddy.svg b/cockatrice/resources/userlevels/registered_vip_buddy.svg
deleted file mode 100644
index 27757b934..000000000
--- a/cockatrice/resources/userlevels/registered_vip_buddy.svg
+++ /dev/null
@@ -1,127 +0,0 @@
-
-
diff --git a/cockatrice/src/client/game_logic/abstract_client.cpp b/cockatrice/src/client/game_logic/abstract_client.cpp
index c186ae323..bc227bf68 100644
--- a/cockatrice/src/client/game_logic/abstract_client.cpp
+++ b/cockatrice/src/client/game_logic/abstract_client.cpp
@@ -52,7 +52,7 @@ AbstractClient::AbstractClient(QObject *parent)
FeatureSet features;
features.initalizeFeatureList(clientFeatures);
- connect(this, SIGNAL(sigQueuePendingCommand(PendingCommand *)), this, SLOT(queuePendingCommand(PendingCommand *)));
+ connect(this, &AbstractClient::sigQueuePendingCommand, this, &AbstractClient::queuePendingCommand);
}
AbstractClient::~AbstractClient()
diff --git a/cockatrice/src/client/game_logic/abstract_client.h b/cockatrice/src/client/game_logic/abstract_client.h
index 97b9c0f2d..5e4948f5e 100644
--- a/cockatrice/src/client/game_logic/abstract_client.h
+++ b/cockatrice/src/client/game_logic/abstract_client.h
@@ -48,6 +48,7 @@ class AbstractClient : public QObject
Q_OBJECT
signals:
void statusChanged(ClientStatus _status);
+ void maxPingTime(int seconds, int maxSeconds);
// Room events
void roomEventReceived(const RoomEvent &event);
@@ -97,8 +98,8 @@ protected:
virtual void sendCommandContainer(const CommandContainer &cont) = 0;
public:
- AbstractClient(QObject *parent = nullptr);
- ~AbstractClient();
+ explicit AbstractClient(QObject *parent = nullptr);
+ ~AbstractClient() override;
ClientStatus getStatus() const
{
diff --git a/cockatrice/src/client/game_logic/key_signals.h b/cockatrice/src/client/game_logic/key_signals.h
index e0808fb0c..32e656c76 100644
--- a/cockatrice/src/client/game_logic/key_signals.h
+++ b/cockatrice/src/client/game_logic/key_signals.h
@@ -23,7 +23,7 @@ signals:
void onCtrlC();
protected:
- virtual bool eventFilter(QObject *, QEvent *event);
+ bool eventFilter(QObject *, QEvent *event) override;
};
#endif
diff --git a/cockatrice/src/client/menus/deck_editor/deck_editor_menu.cpp b/cockatrice/src/client/menus/deck_editor/deck_editor_menu.cpp
new file mode 100644
index 000000000..2d8c613a5
--- /dev/null
+++ b/cockatrice/src/client/menus/deck_editor/deck_editor_menu.cpp
@@ -0,0 +1,201 @@
+#include "deck_editor_menu.h"
+
+#include "../../../settings/cache_settings.h"
+#include "../../../settings/shortcuts_settings.h"
+
+DeckEditorMenu::DeckEditorMenu(QWidget *parent, AbstractTabDeckEditor *_deckEditor)
+ : QMenu(parent), deckEditor(_deckEditor)
+{
+ aNewDeck = new QAction(QString(), this);
+ connect(aNewDeck, SIGNAL(triggered()), deckEditor, SLOT(actNewDeck()));
+
+ aLoadDeck = new QAction(QString(), this);
+ connect(aLoadDeck, SIGNAL(triggered()), deckEditor, SLOT(actLoadDeck()));
+
+ loadRecentDeckMenu = new QMenu(this);
+ connect(&SettingsCache::instance().recents(), &RecentsSettings::recentlyOpenedDeckPathsChanged, this,
+ &DeckEditorMenu::updateRecentlyOpened);
+
+ aClearRecents = new QAction(QString(), this);
+ connect(aClearRecents, &QAction::triggered, this, &DeckEditorMenu::actClearRecents);
+
+ updateRecentlyOpened();
+
+ aSaveDeck = new QAction(QString(), this);
+ connect(aSaveDeck, SIGNAL(triggered()), deckEditor, SLOT(actSaveDeck()));
+
+ aSaveDeckAs = new QAction(QString(), this);
+ connect(aSaveDeckAs, SIGNAL(triggered()), deckEditor, SLOT(actSaveDeckAs()));
+
+ aLoadDeckFromClipboard = new QAction(QString(), this);
+ connect(aLoadDeckFromClipboard, SIGNAL(triggered()), deckEditor, SLOT(actLoadDeckFromClipboard()));
+
+ aEditDeckInClipboard = new QAction(QString(), this);
+ connect(aEditDeckInClipboard, SIGNAL(triggered()), deckEditor, SLOT(actEditDeckInClipboard()));
+
+ aEditDeckInClipboardRaw = new QAction(QString(), this);
+ connect(aEditDeckInClipboardRaw, SIGNAL(triggered()), deckEditor, SLOT(actEditDeckInClipboardRaw()));
+
+ aSaveDeckToClipboard = new QAction(QString(), this);
+ connect(aSaveDeckToClipboard, SIGNAL(triggered()), deckEditor, SLOT(actSaveDeckToClipboard()));
+
+ aSaveDeckToClipboardNoSetInfo = new QAction(QString(), this);
+ connect(aSaveDeckToClipboardNoSetInfo, SIGNAL(triggered()), deckEditor, SLOT(actSaveDeckToClipboardNoSetInfo()));
+
+ aSaveDeckToClipboardRaw = new QAction(QString(), this);
+ connect(aSaveDeckToClipboardRaw, SIGNAL(triggered()), deckEditor, SLOT(actSaveDeckToClipboardRaw()));
+
+ aSaveDeckToClipboardRawNoSetInfo = new QAction(QString(), this);
+ connect(aSaveDeckToClipboardRawNoSetInfo, SIGNAL(triggered()), deckEditor,
+ SLOT(actSaveDeckToClipboardRawNoSetInfo()));
+
+ aPrintDeck = new QAction(QString(), this);
+ connect(aPrintDeck, SIGNAL(triggered()), deckEditor, SLOT(actPrintDeck()));
+
+ aExportDeckDecklist = new QAction(QString(), this);
+ connect(aExportDeckDecklist, SIGNAL(triggered()), deckEditor, SLOT(actExportDeckDecklist()));
+
+ aExportDeckDecklistXyz = new QAction(QString(), this);
+ connect(aExportDeckDecklistXyz, SIGNAL(triggered()), deckEditor, SLOT(actExportDeckDecklistXyz()));
+
+ aAnalyzeDeckDeckstats = new QAction(QString(), this);
+ connect(aAnalyzeDeckDeckstats, SIGNAL(triggered()), deckEditor, SLOT(actAnalyzeDeckDeckstats()));
+
+ aAnalyzeDeckTappedout = new QAction(QString(), this);
+ connect(aAnalyzeDeckTappedout, SIGNAL(triggered()), deckEditor, SLOT(actAnalyzeDeckTappedout()));
+
+ analyzeDeckMenu = new QMenu(this);
+ analyzeDeckMenu->addAction(aExportDeckDecklist);
+ analyzeDeckMenu->addAction(aExportDeckDecklistXyz);
+ analyzeDeckMenu->addSeparator();
+ analyzeDeckMenu->addAction(aAnalyzeDeckDeckstats);
+ analyzeDeckMenu->addAction(aAnalyzeDeckTappedout);
+
+ aClose = new QAction(QString(), this);
+ connect(aClose, &QAction::triggered, deckEditor, &AbstractTabDeckEditor::closeRequest);
+
+ editDeckInClipboardMenu = new QMenu(this);
+ editDeckInClipboardMenu->addAction(aEditDeckInClipboard);
+ editDeckInClipboardMenu->addAction(aEditDeckInClipboardRaw);
+
+ saveDeckToClipboardMenu = new QMenu(this);
+ saveDeckToClipboardMenu->addAction(aSaveDeckToClipboard);
+ saveDeckToClipboardMenu->addAction(aSaveDeckToClipboardNoSetInfo);
+ saveDeckToClipboardMenu->addAction(aSaveDeckToClipboardRaw);
+ saveDeckToClipboardMenu->addAction(aSaveDeckToClipboardRawNoSetInfo);
+
+ addAction(aNewDeck);
+ addAction(aLoadDeck);
+ addMenu(loadRecentDeckMenu);
+ addAction(aSaveDeck);
+ addAction(aSaveDeckAs);
+ addSeparator();
+ addAction(aLoadDeckFromClipboard);
+ addMenu(editDeckInClipboardMenu);
+ addMenu(saveDeckToClipboardMenu);
+ addSeparator();
+ addAction(aPrintDeck);
+ addMenu(analyzeDeckMenu);
+ addSeparator();
+ addAction(deckEditor->filterDockWidget->aClearFilterOne);
+ addAction(deckEditor->filterDockWidget->aClearFilterAll);
+ addSeparator();
+ addAction(aClose);
+
+ retranslateUi();
+ connect(&SettingsCache::instance().shortcuts(), SIGNAL(shortCutChanged()), this, SLOT(refreshShortcuts()));
+ refreshShortcuts();
+}
+
+void DeckEditorMenu::setSaveStatus(bool newStatus)
+{
+ aSaveDeck->setEnabled(newStatus);
+ aSaveDeckAs->setEnabled(newStatus);
+ aSaveDeckToClipboard->setEnabled(newStatus);
+ aSaveDeckToClipboardNoSetInfo->setEnabled(newStatus);
+ aSaveDeckToClipboardRaw->setEnabled(newStatus);
+ aSaveDeckToClipboardRawNoSetInfo->setEnabled(newStatus);
+ saveDeckToClipboardMenu->setEnabled(newStatus);
+ aPrintDeck->setEnabled(newStatus);
+ analyzeDeckMenu->setEnabled(newStatus);
+}
+
+void DeckEditorMenu::updateRecentlyOpened()
+{
+ loadRecentDeckMenu->clear();
+ for (const auto &deckPath : SettingsCache::instance().recents().getRecentlyOpenedDeckPaths()) {
+ QAction *aRecentlyOpenedDeck = new QAction(deckPath, this);
+ loadRecentDeckMenu->addAction(aRecentlyOpenedDeck);
+ connect(aRecentlyOpenedDeck, &QAction::triggered, deckEditor,
+ [=, this] { deckEditor->actOpenRecent(aRecentlyOpenedDeck->text()); });
+ }
+ loadRecentDeckMenu->addSeparator();
+ loadRecentDeckMenu->addAction(aClearRecents);
+ aClearRecents->setEnabled(SettingsCache::instance().recents().getRecentlyOpenedDeckPaths().length() > 0);
+}
+
+void DeckEditorMenu::actClearRecents()
+{
+ SettingsCache::instance().recents().clearRecentlyOpenedDeckPaths();
+}
+
+void DeckEditorMenu::retranslateUi()
+{
+ setTitle(tr("&Deck Editor"));
+ aNewDeck->setText(tr("&New deck"));
+ aLoadDeck->setText(tr("&Load deck..."));
+ loadRecentDeckMenu->setTitle(tr("Load recent deck..."));
+ aClearRecents->setText(tr("Clear"));
+ aSaveDeck->setText(tr("&Save deck"));
+ aSaveDeckAs->setText(tr("Save deck &as..."));
+
+ aLoadDeckFromClipboard->setText(tr("Load deck from cl&ipboard..."));
+
+ editDeckInClipboardMenu->setTitle(tr("Edit deck in clipboard"));
+ aEditDeckInClipboard->setText(tr("Annotated"));
+ aEditDeckInClipboardRaw->setText(tr("Not Annotated"));
+
+ saveDeckToClipboardMenu->setTitle(tr("Save deck to clipboard"));
+ aSaveDeckToClipboard->setText(tr("Annotated"));
+ aSaveDeckToClipboardNoSetInfo->setText(tr("Annotated (No set info)"));
+ aSaveDeckToClipboardRaw->setText(tr("Not Annotated"));
+ aSaveDeckToClipboardRawNoSetInfo->setText(tr("Not Annotated (No set info)"));
+
+ aPrintDeck->setText(tr("&Print deck..."));
+
+ analyzeDeckMenu->setTitle(tr("&Send deck to online service"));
+ aExportDeckDecklist->setText(tr("Create decklist (decklist.org)"));
+ aExportDeckDecklistXyz->setText(tr("Create decklist (decklist.xyz)"));
+ aAnalyzeDeckDeckstats->setText(tr("Analyze deck (deckstats.net)"));
+ aAnalyzeDeckTappedout->setText(tr("Analyze deck (tappedout.net)"));
+
+ aClose->setText(tr("&Close"));
+}
+
+void DeckEditorMenu::refreshShortcuts()
+{
+ ShortcutsSettings &shortcuts = SettingsCache::instance().shortcuts();
+ aNewDeck->setShortcuts(shortcuts.getShortcut("TabDeckEditor/aNewDeck"));
+ aLoadDeck->setShortcuts(shortcuts.getShortcut("TabDeckEditor/aLoadDeck"));
+ aSaveDeck->setShortcuts(shortcuts.getShortcut("TabDeckEditor/aSaveDeck"));
+ aSaveDeckAs->setShortcuts(shortcuts.getShortcut("TabDeckEditor/aSaveDeckAs"));
+ aLoadDeckFromClipboard->setShortcuts(shortcuts.getShortcut("TabDeckEditor/aLoadDeckFromClipboard"));
+ aEditDeckInClipboard->setShortcuts(shortcuts.getShortcut("TabDeckEditor/aEditDeckInClipboard"));
+ aEditDeckInClipboardRaw->setShortcuts(shortcuts.getShortcut("TabDeckEditor/aEditDeckInClipboardRaw"));
+ aPrintDeck->setShortcuts(shortcuts.getShortcut("TabDeckEditor/aPrintDeck"));
+
+ aExportDeckDecklist->setShortcuts(shortcuts.getShortcut("TabDeckEditor/aExportDeckDecklist"));
+ aExportDeckDecklistXyz->setShortcuts(shortcuts.getShortcut("TabDeckEditor/aExportDeckDecklistXyz"));
+ aAnalyzeDeckDeckstats->setShortcuts(shortcuts.getShortcut("TabDeckEditor/aAnalyzeDeck"));
+ aAnalyzeDeckTappedout->setShortcuts(shortcuts.getShortcut("TabDeckEditor/aAnalyzeDeckTappedout"));
+
+ aClose->setShortcuts(shortcuts.getShortcut("TabDeckEditor/aClose"));
+
+ aSaveDeckToClipboard->setShortcuts(shortcuts.getShortcut("TabDeckEditor/aSaveDeckToClipboard"));
+ aSaveDeckToClipboardNoSetInfo->setShortcuts(shortcuts.getShortcut("TabDeckEditor/aSaveDeckToClipboardNoSetInfo"));
+ aSaveDeckToClipboardRaw->setShortcuts(shortcuts.getShortcut("TabDeckEditor/aSaveDeckToClipboardRaw"));
+ aSaveDeckToClipboardRawNoSetInfo->setShortcuts(
+ shortcuts.getShortcut("TabDeckEditor/aSaveDeckToClipboardRawNoSetInfo"));
+
+ aClose->setShortcuts(shortcuts.getShortcut("TabDeckEditor/aClose"));
+}
diff --git a/cockatrice/src/client/menus/deck_editor/deck_editor_menu.h b/cockatrice/src/client/menus/deck_editor/deck_editor_menu.h
new file mode 100644
index 000000000..a713b009f
--- /dev/null
+++ b/cockatrice/src/client/menus/deck_editor/deck_editor_menu.h
@@ -0,0 +1,32 @@
+#ifndef DECK_EDITOR_MENU_H
+#define DECK_EDITOR_MENU_H
+
+#include "../../tabs/abstract_tab_deck_editor.h"
+
+#include
+
+class AbstractTabDeckEditor;
+class DeckEditorMenu : public QMenu
+{
+ Q_OBJECT
+public:
+ explicit DeckEditorMenu(QWidget *parent, AbstractTabDeckEditor *deckEditor);
+
+ AbstractTabDeckEditor *deckEditor;
+
+ QAction *aNewDeck, *aLoadDeck, *aClearRecents, *aSaveDeck, *aSaveDeckAs, *aLoadDeckFromClipboard,
+ *aEditDeckInClipboard, *aEditDeckInClipboardRaw, *aSaveDeckToClipboard, *aSaveDeckToClipboardNoSetInfo,
+ *aSaveDeckToClipboardRaw, *aSaveDeckToClipboardRawNoSetInfo, *aPrintDeck, *aExportDeckDecklist,
+ *aExportDeckDecklistXyz, *aAnalyzeDeckDeckstats, *aAnalyzeDeckTappedout, *aClose;
+ QMenu *loadRecentDeckMenu, *analyzeDeckMenu, *editDeckInClipboardMenu, *saveDeckToClipboardMenu;
+
+ void setSaveStatus(bool newStatus);
+
+public slots:
+ void updateRecentlyOpened();
+ void actClearRecents();
+ void retranslateUi();
+ void refreshShortcuts();
+};
+
+#endif
diff --git a/cockatrice/src/client/network/client_update_checker.cpp b/cockatrice/src/client/network/client_update_checker.cpp
new file mode 100644
index 000000000..23c52d08d
--- /dev/null
+++ b/cockatrice/src/client/network/client_update_checker.cpp
@@ -0,0 +1,35 @@
+#include "client_update_checker.h"
+
+#include "../../settings/cache_settings.h"
+#include "release_channel.h"
+
+ClientUpdateChecker::ClientUpdateChecker(QObject *parent) : QObject(parent)
+{
+}
+
+void ClientUpdateChecker::check()
+{
+ auto releaseChannel = SettingsCache::instance().getUpdateReleaseChannel();
+
+ finishedCheckConnection =
+ connect(releaseChannel, &ReleaseChannel::finishedCheck, this, &ClientUpdateChecker::actFinishedCheck);
+ errorConnection = connect(releaseChannel, &ReleaseChannel::error, this, &ClientUpdateChecker::actError);
+
+ releaseChannel->checkForUpdates();
+}
+
+void ClientUpdateChecker::actFinishedCheck(bool needToUpdate, bool isCompatible, Release *release)
+{
+ disconnect(finishedCheckConnection);
+ disconnect(errorConnection);
+
+ emit finishedCheck(needToUpdate, isCompatible, release);
+}
+
+void ClientUpdateChecker::actError(const QString &errorString)
+{
+ disconnect(finishedCheckConnection);
+ disconnect(errorConnection);
+
+ emit error(errorString);
+}
\ No newline at end of file
diff --git a/cockatrice/src/client/network/client_update_checker.h b/cockatrice/src/client/network/client_update_checker.h
new file mode 100644
index 000000000..96a7213b1
--- /dev/null
+++ b/cockatrice/src/client/network/client_update_checker.h
@@ -0,0 +1,45 @@
+#ifndef CLIENT_UPDATE_CHECKER_H
+#define CLIENT_UPDATE_CHECKER_H
+#include
+
+class Release;
+
+/**
+ * We use a singleton instance of UpdateChannel, which can cause interference and feedback loops when multiple objects
+ * connect to it.
+ *
+ * This class encapsulates the usage of that UpdateChannel to ensure that the check only happens once per connection and
+ * the connection is destroyed after it's been used.
+ */
+class ClientUpdateChecker : public QObject
+{
+ Q_OBJECT
+
+ QMetaObject::Connection finishedCheckConnection;
+ QMetaObject::Connection errorConnection;
+
+ void actFinishedCheck(bool needToUpdate, bool isCompatible, Release *release);
+ void actError(const QString &errorString);
+
+public:
+ explicit ClientUpdateChecker(QObject *parent = nullptr);
+ /**
+ * Actually performs the check, using the currently selected update channel in the settings.
+ * Any resulting signals will only be sent once.
+ * This method should only be called ONCE per instance.
+ */
+ void check();
+
+signals:
+ /**
+ * Forwarded from UpdateChannel::finishedCheck
+ */
+ void finishedCheck(bool needToUpdate, bool isCompatible, Release *release);
+
+ /**
+ * Forwarded from UpdateChannel::error
+ */
+ void error(const QString &errorString);
+};
+
+#endif // CLIENT_UPDATE_CHECKER_H
diff --git a/cockatrice/src/client/network/release_channel.cpp b/cockatrice/src/client/network/release_channel.cpp
index b467aa4a4..9edfe7caf 100644
--- a/cockatrice/src/client/network/release_channel.cpp
+++ b/cockatrice/src/client/network/release_channel.cpp
@@ -11,6 +11,11 @@
#include
#include
+#if defined(Q_OS_MACOS)
+#include
+#include
+#endif
+
#define STABLERELEASE_URL "https://api.github.com/repos/Cockatrice/Cockatrice/releases/latest"
#define STABLEMANUALDOWNLOAD_URL "https://github.com/Cockatrice/Cockatrice/releases/latest"
#define STABLETAG_URL "https://api.github.com/repos/Cockatrice/Cockatrice/git/refs/tags/"
@@ -21,11 +26,8 @@
#define GIT_SHORT_HASH_LEN 7
-int ReleaseChannel::sharedIndex = 0;
-
ReleaseChannel::ReleaseChannel() : netMan(new QNetworkAccessManager(this)), response(nullptr), lastRelease(nullptr)
{
- index = sharedIndex++;
}
ReleaseChannel::~ReleaseChannel()
@@ -36,31 +38,42 @@ ReleaseChannel::~ReleaseChannel()
void ReleaseChannel::checkForUpdates()
{
QString releaseChannelUrl = getReleaseChannelUrl();
- qDebug() << "Searching for updates on the channel: " << releaseChannelUrl;
+ qCInfo(ReleaseChannelLog) << "Searching for updates on the channel: " << releaseChannelUrl;
response = netMan->get(QNetworkRequest(releaseChannelUrl));
- connect(response, SIGNAL(finished()), this, SLOT(releaseListFinished()));
+ connect(response, &QNetworkReply::finished, this, &ReleaseChannel::releaseListFinished);
}
// Different release channel checking functions for different operating systems
-#if defined(Q_OS_MACOS)
bool ReleaseChannel::downloadMatchesCurrentOS(const QString &fileName)
{
- static QRegularExpression version_regex("macOS-(\\d+)\\.(\\d+)");
+#if defined(Q_OS_MACOS)
+ static QRegularExpression version_regex("macOS(\\d+)");
auto match = version_regex.match(fileName);
if (!match.hasMatch()) {
return false;
}
+ auto getSystemVersion = [] {
+ // QSysInfo does not go through translation layers
+ // We need to use sysctl to reliably detect the underlying architecture
+ char arch[255];
+ size_t len = sizeof(arch);
+ if (sysctlbyname("machdep.cpu.brand_string", arch, &len, nullptr, 0) == 0) {
+ // Intel mac is only supported on macOS 13 versions
+ if (QString::fromUtf8(arch).contains("Intel")) {
+ return 13;
+ }
+ }
+
+ return QSysInfo::productVersion().split(".")[0].toInt();
+ };
+
// older(smaller) releases are compatible with a newer or the same system version
- int sys_maj = QSysInfo::productVersion().split(".")[0].toInt();
- int sys_min = QSysInfo::productVersion().split(".")[1].toInt();
+ int sys_maj = getSystemVersion();
int rel_maj = match.captured(1).toInt();
- int rel_min = match.captured(2).toInt();
- return rel_maj < sys_maj || (rel_maj == sys_maj && rel_min <= sys_min);
-}
+ return rel_maj == sys_maj;
+
#elif defined(Q_OS_WIN)
-bool ReleaseChannel::downloadMatchesCurrentOS(const QString &fileName)
-{
#if Q_PROCESSOR_WORDSIZE == 4
return fileName.contains("32bit");
#elif Q_PROCESSOR_WORDSIZE == 8
@@ -71,16 +84,15 @@ bool ReleaseChannel::downloadMatchesCurrentOS(const QString &fileName)
return fileName.contains("Win10");
}
#else
+ Q_UNUSED(fileName);
+ return false;
+#endif
+
+#else // If the OS doesn't fit one of the above #defines, then it will never match
+ Q_UNUSED(fileName);
return false;
#endif
}
-#else
-bool ReleaseChannel::downloadMatchesCurrentOS(const QString &)
-{
- // If the OS doesn't fit one of the above #defines, then it will never match
- return false;
-}
-#endif
QString StableReleaseChannel::getManualDownloadUrl() const
{
@@ -89,7 +101,7 @@ QString StableReleaseChannel::getManualDownloadUrl() const
QString StableReleaseChannel::getName() const
{
- return tr("Stable Releases");
+ return tr("Default");
}
QString StableReleaseChannel::getReleaseChannelUrl() const
@@ -104,7 +116,7 @@ void StableReleaseChannel::releaseListFinished()
QJsonDocument jsonResponse = QJsonDocument::fromJson(reply->readAll(), &parseError);
reply->deleteLater();
if (parseError.error != QJsonParseError::NoError) {
- qWarning() << "No reply received from the release update server.";
+ qCWarning(ReleaseChannelLog) << "No reply received from the release update server.";
emit error(tr("No reply received from the release update server."));
return;
}
@@ -112,7 +124,7 @@ void StableReleaseChannel::releaseListFinished()
QVariantMap resultMap = jsonResponse.toVariant().toMap();
if (!(resultMap.contains("name") && resultMap.contains("html_url") && resultMap.contains("tag_name") &&
resultMap.contains("published_at"))) {
- qWarning() << "Invalid received from the release update server.";
+ qCWarning(ReleaseChannelLog) << "Invalid received from the release update server:" << resultMap;
emit error(tr("Invalid reply received from the release update server."));
return;
}
@@ -126,39 +138,31 @@ void StableReleaseChannel::releaseListFinished()
if (resultMap.contains("assets")) {
auto rawAssets = resultMap["assets"].toList();
- // [(name, url)]
- QVector> assets;
- std::transform(rawAssets.begin(), rawAssets.end(), std::back_inserter(assets), [](QVariant _asset) {
- QVariantMap asset = _asset.toMap();
+ for (const auto &rawAsset : rawAssets) {
+ QVariantMap asset = rawAsset.toMap();
QString name = asset["name"].toString();
QString url = asset["browser_download_url"].toString();
- return std::make_pair(name, url);
- });
- auto _releaseAsset = std::find_if(assets.begin(), assets.end(), [](std::pair nameAndUrl) {
- return downloadMatchesCurrentOS(nameAndUrl.first);
- });
-
- if (_releaseAsset != assets.end()) {
- std::pair releaseAsset = *_releaseAsset;
- auto releaseUrl = releaseAsset.second;
- lastRelease->setDownloadUrl(releaseUrl);
+ if (downloadMatchesCurrentOS(name)) {
+ lastRelease->setDownloadUrl(url);
+ break;
+ }
}
}
QString shortHash = lastRelease->getCommitHash().left(GIT_SHORT_HASH_LEN);
QString myHash = QString(VERSION_COMMIT);
- qDebug() << "Current hash=" << myHash << "update hash=" << shortHash;
+ qCInfo(ReleaseChannelLog) << "Current hash=" << myHash << "update hash=" << shortHash;
- qDebug() << "Got reply from release server, name=" << lastRelease->getName()
- << "desc=" << lastRelease->getDescriptionUrl() << "date=" << lastRelease->getPublishDate()
- << "url=" << lastRelease->getDownloadUrl();
+ qCInfo(ReleaseChannelLog) << "Got reply from release server, name=" << lastRelease->getName()
+ << "desc=" << lastRelease->getDescriptionUrl() << "date=" << lastRelease->getPublishDate()
+ << "url=" << lastRelease->getDownloadUrl();
const QString &tagName = resultMap["tag_name"].toString();
QString url = QString(STABLETAG_URL) + tagName;
- qDebug() << "Searching for commit hash corresponding to stable channel tag: " << tagName;
+ qCInfo(ReleaseChannelLog) << "Searching for commit hash corresponding to stable channel tag: " << tagName;
response = netMan->get(QNetworkRequest(url));
- connect(response, SIGNAL(finished()), this, SLOT(tagListFinished()));
+ connect(response, &QNetworkReply::finished, this, &StableReleaseChannel::tagListFinished);
}
void StableReleaseChannel::tagListFinished()
@@ -168,24 +172,24 @@ void StableReleaseChannel::tagListFinished()
QJsonDocument jsonResponse = QJsonDocument::fromJson(reply->readAll(), &parseError);
reply->deleteLater();
if (parseError.error != QJsonParseError::NoError) {
- qWarning() << "No reply received from the tag update server.";
+ qCWarning(ReleaseChannelLog) << "No reply received from the tag update server.";
emit error(tr("No reply received from the tag update server."));
return;
}
QVariantMap resultMap = jsonResponse.toVariant().toMap();
if (!(resultMap.contains("object") && resultMap["object"].toMap().contains("sha"))) {
- qWarning() << "Invalid received from the tag update server.";
+ qCWarning(ReleaseChannelLog) << "Invalid received from the tag update server.";
emit error(tr("Invalid reply received from the tag update server."));
return;
}
lastRelease->setCommitHash(resultMap["object"].toMap()["sha"].toString());
- qDebug() << "Got reply from tag server, commit=" << lastRelease->getCommitHash();
+ qCInfo(ReleaseChannelLog) << "Got reply from tag server, commit=" << lastRelease->getCommitHash();
QString shortHash = lastRelease->getCommitHash().left(GIT_SHORT_HASH_LEN);
QString myHash = QString(VERSION_COMMIT);
- qDebug() << "Current hash=" << myHash << "update hash=" << shortHash;
+ qCInfo(ReleaseChannelLog) << "Current hash=" << myHash << "update hash=" << shortHash;
const bool needToUpdate = (QString::compare(shortHash, myHash, Qt::CaseInsensitive) != 0);
emit finishedCheck(needToUpdate, lastRelease->isCompatibleVersionFound(), lastRelease);
@@ -203,7 +207,7 @@ QString BetaReleaseChannel::getManualDownloadUrl() const
QString BetaReleaseChannel::getName() const
{
- return tr("Beta Releases");
+ return tr("Beta");
}
QString BetaReleaseChannel::getReleaseChannelUrl() const
@@ -228,7 +232,7 @@ void BetaReleaseChannel::releaseListFinished()
QVariantMap resultMap = array.at(0).toObject().toVariantMap();
if (array.empty() || resultMap.empty()) {
- qWarning() << "No reply received from the release update server:" << QString(jsonData);
+ qCWarning(ReleaseChannelLog) << "No reply received from the release update server:" << QString(jsonData);
emit error(tr("No reply received from the release update server."));
return;
}
@@ -237,7 +241,7 @@ void BetaReleaseChannel::releaseListFinished()
if (!resultMap.contains("assets") || !resultMap.contains("author") || !resultMap.contains("tag_name") ||
!resultMap.contains("target_commitish") || !resultMap.contains("assets_url") ||
!resultMap.contains("published_at")) {
- qWarning() << "Invalid received from the release update server:" << resultMap;
+ qCWarning(ReleaseChannelLog) << "Invalid received from the release update server:" << resultMap;
emit error(tr("Invalid reply received from the release update server."));
return;
}
@@ -252,15 +256,15 @@ void BetaReleaseChannel::releaseListFinished()
lastRelease->setName(QString("%1 (%2)").arg(resultMap["tag_name"].toString()).arg(shortHash));
lastRelease->setDescriptionUrl(QString(BETARELEASE_CHANGESURL).arg(VERSION_COMMIT, shortHash));
- qDebug() << "Got reply from release server, size=" << resultMap.size() << "name=" << lastRelease->getName()
- << "desc=" << lastRelease->getDescriptionUrl() << "commit=" << lastRelease->getCommitHash()
- << "date=" << lastRelease->getPublishDate();
+ qCInfo(ReleaseChannelLog) << "Got reply from release server, size=" << resultMap.size()
+ << "name=" << lastRelease->getName() << "desc=" << lastRelease->getDescriptionUrl()
+ << "commit=" << lastRelease->getCommitHash() << "date=" << lastRelease->getPublishDate();
QString betaBuildDownloadUrl = resultMap["assets_url"].toString();
- qDebug() << "Searching for a corresponding file on the beta channel: " << betaBuildDownloadUrl;
+ qCInfo(ReleaseChannelLog) << "Searching for a corresponding file on the beta channel: " << betaBuildDownloadUrl;
response = netMan->get(QNetworkRequest(betaBuildDownloadUrl));
- connect(response, SIGNAL(finished()), this, SLOT(fileListFinished()));
+ connect(response, &QNetworkReply::finished, this, &BetaReleaseChannel::fileListFinished);
}
void BetaReleaseChannel::fileListFinished()
@@ -270,7 +274,7 @@ void BetaReleaseChannel::fileListFinished()
QJsonDocument jsonResponse = QJsonDocument::fromJson(reply->readAll(), &parseError);
reply->deleteLater();
if (parseError.error != QJsonParseError::NoError) {
- qWarning() << "No reply received from the file update server.";
+ qCWarning(ReleaseChannelLog) << "No reply received from the file update server.";
emit error(tr("No reply received from the file update server."));
return;
}
@@ -278,7 +282,7 @@ void BetaReleaseChannel::fileListFinished()
QVariantList resultList = jsonResponse.toVariant().toList();
QString shortHash = lastRelease->getCommitHash().left(GIT_SHORT_HASH_LEN);
QString myHash = QString(VERSION_COMMIT);
- qDebug() << "Current hash=" << myHash << "update hash=" << shortHash;
+ qCInfo(ReleaseChannelLog) << "Current hash=" << myHash << "update hash=" << shortHash;
bool needToUpdate = (QString::compare(shortHash, myHash, Qt::CaseInsensitive) != 0);
bool compatibleVersion = false;
@@ -295,7 +299,7 @@ void BetaReleaseChannel::fileListFinished()
if (downloadMatchesCurrentOS(*url)) {
compatibleVersion = true;
lastRelease->setDownloadUrl(*url);
- qDebug() << "Found compatible version url=" << *url;
+ qCInfo(ReleaseChannelLog) << "Found compatible version url=" << *url;
break;
}
}
diff --git a/cockatrice/src/client/network/release_channel.h b/cockatrice/src/client/network/release_channel.h
index f9507b858..0fb459969 100644
--- a/cockatrice/src/client/network/release_channel.h
+++ b/cockatrice/src/client/network/release_channel.h
@@ -2,11 +2,14 @@
#define RELEASECHANNEL_H
#include
+#include
#include
#include
#include
#include
+inline Q_LOGGING_CATEGORY(ReleaseChannelLog, "release_channel");
+
class QNetworkReply;
class QNetworkAccessManager;
@@ -82,9 +85,6 @@ public:
~ReleaseChannel() override;
protected:
- // shared by all instances
- static int sharedIndex;
- int index;
QNetworkAccessManager *netMan;
QNetworkReply *response;
Release *lastRelease;
@@ -94,10 +94,6 @@ protected:
virtual QString getReleaseChannelUrl() const = 0;
public:
- int getIndex() const
- {
- return index;
- }
Release *getLastRelease()
{
return lastRelease;
diff --git a/cockatrice/src/client/network/replay_timeline_widget.cpp b/cockatrice/src/client/network/replay_timeline_widget.cpp
index 83b1abda0..919ee3d97 100644
--- a/cockatrice/src/client/network/replay_timeline_widget.cpp
+++ b/cockatrice/src/client/network/replay_timeline_widget.cpp
@@ -6,10 +6,11 @@
#include
ReplayTimelineWidget::ReplayTimelineWidget(QWidget *parent)
- : QWidget(parent), maxBinValue(1), maxTime(1), timeScaleFactor(1.0), currentTime(0), currentEvent(0)
+ : QWidget(parent), maxBinValue(1), maxTime(1), timeScaleFactor(1.0), currentVisualTime(0), currentProcessedTime(0),
+ currentEvent(0)
{
replayTimer = new QTimer(this);
- connect(replayTimer, SIGNAL(timeout()), this, SLOT(replayTimerTimeout()));
+ connect(replayTimer, &QTimer::timeout, this, &ReplayTimelineWidget::replayTimerTimeout);
rewindBufferingTimer = new QTimer(this);
rewindBufferingTimer->setSingleShot(true);
@@ -58,7 +59,7 @@ void ReplayTimelineWidget::paintEvent(QPaintEvent * /* event */)
painter.fillPath(path, Qt::black);
const QColor barColor = QColor::fromHsv(120, 255, 255, 100);
- quint64 w = (quint64)(width() - 1) * (quint64)currentTime / maxTime;
+ quint64 w = (quint64)(width() - 1) * (quint64)currentVisualTime / maxTime;
painter.fillRect(0, 0, static_cast(w), height() - 1, barColor);
}
@@ -85,8 +86,8 @@ void ReplayTimelineWidget::skipToTime(int newTime, bool doRewindBuffering)
newTime -= newTime % TIMER_INTERVAL_MS; // Time should always be a multiple of the interval
- const bool isBackwardsSkip = newTime < currentTime;
- currentTime = newTime;
+ const bool isBackwardsSkip = newTime < currentProcessedTime;
+ currentVisualTime = newTime;
if (isBackwardsSkip) {
handleBackwardsSkip(doRewindBuffering);
@@ -106,20 +107,13 @@ void ReplayTimelineWidget::handleBackwardsSkip(bool doRewindBuffering)
// The rewind only happens once the timer runs out.
// If another backwards skip happens, the timer will just get reset instead of rewinding.
rewindBufferingTimer->stop();
- rewindBufferingTimer->start(calcRewindBufferingTimeout());
+ rewindBufferingTimer->start(SettingsCache::instance().getRewindBufferingMs());
} else {
// otherwise, process the rewind immediately
processRewind();
}
}
-/// The timeout scales based on the current event number, up to a limit
-int ReplayTimelineWidget::calcRewindBufferingTimeout() const
-{
- int extraTime = currentEvent / 100;
- return std::min(BASE_REWIND_BUFFERING_TIMEOUT_MS + extraTime, MAX_REWIND_BUFFERING_TIMEOUT_MS);
-}
-
void ReplayTimelineWidget::processRewind()
{
// stop any queued-up rewinds
@@ -143,25 +137,31 @@ QSize ReplayTimelineWidget::minimumSizeHint() const
void ReplayTimelineWidget::replayTimerTimeout()
{
- currentTime += TIMER_INTERVAL_MS;
+ currentVisualTime += TIMER_INTERVAL_MS;
processNewEvents(NORMAL_PLAYBACK);
- if (!(currentTime % 1000))
+ if (!(currentVisualTime % 1000))
update();
}
/// Processes all unprocessed events up to the current time.
void ReplayTimelineWidget::processNewEvents(PlaybackMode playbackMode)
{
- while ((currentEvent < replayTimeline.size()) && (replayTimeline[currentEvent] < currentTime)) {
+ currentProcessedTime = currentVisualTime;
+
+ while ((currentEvent < replayTimeline.size()) && (replayTimeline[currentEvent] < currentProcessedTime)) {
Player::EventProcessingOptions options;
// backwards skip => always skip reveal windows
// forwards skip => skip reveal windows that don't happen within a big skip of the target
- if (playbackMode == BACKWARD_SKIP || currentTime - replayTimeline[currentEvent] > BIG_SKIP_MS)
+ if (playbackMode == BACKWARD_SKIP || currentProcessedTime - replayTimeline[currentEvent] > BIG_SKIP_MS)
options |= Player::EventProcessingOption::SKIP_REVEAL_WINDOW;
+ // backwards skip => always skip tap animation
+ if (playbackMode == BACKWARD_SKIP)
+ options |= Player::EventProcessingOption::SKIP_TAP_ANIMATION;
+
emit processNextEvent(options);
++currentEvent;
}
@@ -189,5 +189,5 @@ void ReplayTimelineWidget::stopReplay()
void ReplayTimelineWidget::skipByAmount(int amount)
{
- skipToTime(currentTime + amount, true);
+ skipToTime(currentVisualTime + amount, amount < 0);
}
\ No newline at end of file
diff --git a/cockatrice/src/client/network/replay_timeline_widget.h b/cockatrice/src/client/network/replay_timeline_widget.h
index b4a0559a4..9676e7b47 100644
--- a/cockatrice/src/client/network/replay_timeline_widget.h
+++ b/cockatrice/src/client/network/replay_timeline_widget.h
@@ -28,8 +28,6 @@ private:
static constexpr int TIMER_INTERVAL_MS = 200;
static constexpr int BIN_LENGTH = 5000;
- static constexpr int BASE_REWIND_BUFFERING_TIMEOUT_MS = 180;
- static constexpr int MAX_REWIND_BUFFERING_TIMEOUT_MS = 280;
QTimer *replayTimer;
QTimer *rewindBufferingTimer;
@@ -37,12 +35,13 @@ private:
QList histogram;
int maxBinValue, maxTime;
qreal timeScaleFactor;
- int currentTime;
+ int currentVisualTime; // time currently displayed by the timeline
+ int currentProcessedTime; // time that events are currently processed up to. Could differ from visual time due to
+ // rewind buffering
int currentEvent;
void skipToTime(int newTime, bool doRewindBuffering);
void handleBackwardsSkip(bool doRewindBuffering);
- int calcRewindBufferingTimeout() const;
void processRewind();
void processNewEvents(PlaybackMode playbackMode);
private slots:
diff --git a/cockatrice/src/client/network/sets_model.cpp b/cockatrice/src/client/network/sets_model.cpp
index de3521772..a86b710b5 100644
--- a/cockatrice/src/client/network/sets_model.cpp
+++ b/cockatrice/src/client/network/sets_model.cpp
@@ -195,6 +195,13 @@ void SetsModel::swapRows(int oldRow, int newRow)
emit dataChanged(index(0, 0), index(rowCount() - 1, columnCount() - 1));
}
+void SetsModel::restoreOriginalOrder()
+{
+ int numRows = rowCount();
+ sets.defaultSort();
+ emit dataChanged(index(0, 0), index(numRows - 1, columnCount() - 1));
+}
+
void SetsModel::sort(int column, Qt::SortOrder order)
{
QMultiMap setMap;
diff --git a/cockatrice/src/client/network/sets_model.h b/cockatrice/src/client/network/sets_model.h
index 20b5adbcb..0bfbe57e0 100644
--- a/cockatrice/src/client/network/sets_model.h
+++ b/cockatrice/src/client/network/sets_model.h
@@ -49,44 +49,47 @@ public:
LongNameCol,
ShortNameCol,
SetTypeCol,
- ReleaseDateCol
+ ReleaseDateCol,
+ PriorityCol
};
enum Role
{
SortRole = Qt::UserRole
};
- SetsModel(CardDatabase *_db, QObject *parent = nullptr);
- ~SetsModel();
- int rowCount(const QModelIndex &parent = QModelIndex()) const;
- int columnCount(const QModelIndex &parent = QModelIndex()) const
+ explicit SetsModel(CardDatabase *_db, QObject *parent = nullptr);
+ ~SetsModel() override;
+ int rowCount(const QModelIndex &parent = QModelIndex()) const override;
+ int columnCount(const QModelIndex &parent = QModelIndex()) const override
{
Q_UNUSED(parent);
return NUM_COLS;
}
- QVariant data(const QModelIndex &index, int role) const;
- bool setData(const QModelIndex &index, const QVariant &value, int role);
- QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const;
- Qt::ItemFlags flags(const QModelIndex &index) const;
- Qt::DropActions supportedDropActions() const;
+ QVariant data(const QModelIndex &index, int role) const override;
+ bool setData(const QModelIndex &index, const QVariant &value, int role) override;
+ QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;
+ Qt::ItemFlags flags(const QModelIndex &index) const override;
+ Qt::DropActions supportedDropActions() const override;
- QMimeData *mimeData(const QModelIndexList &indexes) const;
- bool dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent);
- QStringList mimeTypes() const;
+ QMimeData *mimeData(const QModelIndexList &indexes) const override;
+ bool
+ dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) override;
+ QStringList mimeTypes() const override;
void swapRows(int oldRow, int newRow);
void toggleRow(int row, bool enable);
void toggleRow(int row);
void toggleAll(bool);
- void sort(int column, Qt::SortOrder order = Qt::AscendingOrder);
+ void sort(int column, Qt::SortOrder order = Qt::AscendingOrder) override;
void save(CardDatabase *db);
void restore(CardDatabase *db);
+ void restoreOriginalOrder();
};
class SetsDisplayModel : public QSortFilterProxyModel
{
Q_OBJECT
public:
- SetsDisplayModel(QObject *parent = NULL);
+ explicit SetsDisplayModel(QObject *parent = nullptr);
protected:
bool lessThan(const QModelIndex &left, const QModelIndex &right) const override;
diff --git a/cockatrice/src/client/network/spoiler_background_updater.cpp b/cockatrice/src/client/network/spoiler_background_updater.cpp
index e60eeb96a..a8a3a4b81 100644
--- a/cockatrice/src/client/network/spoiler_background_updater.cpp
+++ b/cockatrice/src/client/network/spoiler_background_updater.cpp
@@ -28,7 +28,7 @@ SpoilerBackgroundUpdater::SpoilerBackgroundUpdater(QObject *apParent) : QObject(
// File exists means we're in spoiler season
startSpoilerDownloadProcess(SPOILERS_STATUS_URL, false);
} else {
- qDebug() << "Spoilers Disabled";
+ qCInfo(SpoilerBackgroundUpdaterLog) << "Spoilers Disabled";
}
}
@@ -45,10 +45,10 @@ void SpoilerBackgroundUpdater::downloadFromURL(QUrl url, bool saveResults)
if (saveResults) {
// This will write out to the file (used for spoiler.xml)
- connect(reply, SIGNAL(finished()), this, SLOT(actDownloadFinishedSpoilersFile()));
+ connect(reply, &QNetworkReply::finished, this, &SpoilerBackgroundUpdater::actDownloadFinishedSpoilersFile);
} else {
// This will check the status (used to see if we're in spoiler season or not)
- connect(reply, SIGNAL(finished()), this, SLOT(actCheckIfSpoilerSeasonEnabled()));
+ connect(reply, &QNetworkReply::finished, this, &SpoilerBackgroundUpdater::actCheckIfSpoilerSeasonEnabled);
}
}
@@ -67,7 +67,7 @@ void SpoilerBackgroundUpdater::actDownloadFinishedSpoilersFile()
reply->deleteLater();
emit spoilerCheckerDone();
} else {
- qDebug() << "Error downloading spoilers file" << errorCode;
+ qCWarning(SpoilerBackgroundUpdaterLog) << "Error downloading spoilers file" << errorCode;
emit spoilerCheckerDone();
}
}
@@ -81,11 +81,11 @@ bool SpoilerBackgroundUpdater::deleteSpoilerFile()
// Delete the spoiler.xml file
if (file.exists() && file.remove()) {
- qDebug() << "Deleting spoiler.xml";
+ qCInfo(SpoilerBackgroundUpdaterLog) << "Deleting spoiler.xml";
return true;
}
- qDebug() << "Error: Spoiler.xml not found or not deleted";
+ qCInfo(SpoilerBackgroundUpdaterLog) << "Error: Spoiler.xml not found or not deleted";
return false;
}
@@ -101,24 +101,24 @@ void SpoilerBackgroundUpdater::actCheckIfSpoilerSeasonEnabled()
trayIcon->showMessage(tr("Spoilers season has ended"), tr("Deleting spoiler.xml. Please run Oracle"));
}
- qDebug() << "Spoiler Season Offline";
+ qCInfo(SpoilerBackgroundUpdaterLog) << "Spoiler Season Offline";
emit spoilerCheckerDone();
} else if (errorCode == QNetworkReply::NoError) {
- qDebug() << "Spoiler Service Online";
+ qCInfo(SpoilerBackgroundUpdaterLog) << "Spoiler Service Online";
startSpoilerDownloadProcess(SPOILERS_URL, true);
} else if (errorCode == QNetworkReply::HostNotFoundError) {
if (trayIcon) {
trayIcon->showMessage(tr("Spoilers download failed"), tr("No internet connection"));
}
- qDebug() << "Spoiler download failed due to no internet connection";
+ qCWarning(SpoilerBackgroundUpdaterLog) << "Spoiler download failed due to no internet connection";
emit spoilerCheckerDone();
} else {
if (trayIcon) {
trayIcon->showMessage(tr("Spoilers download failed"), tr("Error") + " " + (short)errorCode);
}
- qDebug() << "Spoiler download failed with reason" << errorCode;
+ qCWarning(SpoilerBackgroundUpdaterLog) << "Spoiler download failed with reason" << errorCode;
emit spoilerCheckerDone();
}
}
@@ -139,19 +139,19 @@ bool SpoilerBackgroundUpdater::saveDownloadedFile(QByteArray data)
trayIcon->showMessage(tr("Spoilers already up to date"), tr("No new spoilers added"));
}
- qDebug() << "Spoilers Up to Date";
+ qCInfo(SpoilerBackgroundUpdaterLog) << "Spoilers Up to Date";
return false;
}
QFile file(fileName);
if (!file.open(QIODevice::WriteOnly)) {
- qDebug() << "Spoiler Service Error: File open (w) failed for" << fileName;
+ qCWarning(SpoilerBackgroundUpdaterLog) << "Spoiler Service Error: File open (w) failed for" << fileName;
file.close();
return false;
}
if (file.write(data) == -1) {
- qDebug() << "Spoiler Service Error: File write (w) failed for" << fileName;
+ qCWarning(SpoilerBackgroundUpdaterLog) << "Spoiler Service Error: File write (w) failed for" << fileName;
file.close();
return false;
}
@@ -159,7 +159,7 @@ bool SpoilerBackgroundUpdater::saveDownloadedFile(QByteArray data)
file.close();
// Data written, so reload the card database
- qDebug() << "Spoiler Service Data Written";
+ qCInfo(SpoilerBackgroundUpdaterLog) << "Spoiler Service Data Written";
const auto reloadOk = QtConcurrent::run([] { CardDatabaseManager::getInstance()->loadCardDatabases(); });
// If the user has notifications enabled, let them know
@@ -167,7 +167,7 @@ bool SpoilerBackgroundUpdater::saveDownloadedFile(QByteArray data)
if (trayIcon) {
QList lines = data.split('\n');
- foreach (QByteArray line, lines) {
+ for (const QByteArray &line : lines) {
if (line.contains("Created At:")) {
QString timeStamp = QString(line).replace("Created At:", "").trimmed();
timeStamp.chop(6); // Remove " (UTC)"
@@ -202,12 +202,12 @@ QByteArray SpoilerBackgroundUpdater::getHash(const QString fileName)
QCryptographicHash hash(QCryptographicHash::Algorithm::Md5);
hash.addData(bytes);
- qDebug() << "File Hash =" << hash.result();
+ qCInfo(SpoilerBackgroundUpdaterLog) << "File Hash =" << hash.result();
file.close();
return hash.result();
} else {
- qDebug() << "getHash ReadOnly failed!";
+ qCWarning(SpoilerBackgroundUpdaterLog) << "getHash ReadOnly failed!";
file.close();
return QByteArray();
}
@@ -221,7 +221,7 @@ QByteArray SpoilerBackgroundUpdater::getHash(QByteArray data)
QCryptographicHash hash(QCryptographicHash::Algorithm::Md5);
hash.addData(bytes);
- qDebug() << "Data Hash =" << hash.result();
+ qCInfo(SpoilerBackgroundUpdaterLog) << "Data Hash =" << hash.result();
return hash.result();
}
diff --git a/cockatrice/src/client/network/spoiler_background_updater.h b/cockatrice/src/client/network/spoiler_background_updater.h
index 7d7a2892c..a29488bf2 100644
--- a/cockatrice/src/client/network/spoiler_background_updater.h
+++ b/cockatrice/src/client/network/spoiler_background_updater.h
@@ -2,9 +2,12 @@
#define COCKATRICE_SPOILER_DOWNLOADER_H
#include
+#include
#include
#include
+inline Q_LOGGING_CATEGORY(SpoilerBackgroundUpdaterLog, "spoiler_background_updater");
+
class SpoilerBackgroundUpdater : public QObject
{
Q_OBJECT
diff --git a/cockatrice/src/client/sound_engine.cpp b/cockatrice/src/client/sound_engine.cpp
index 35cc6d5c7..fef2df093 100644
--- a/cockatrice/src/client/sound_engine.cpp
+++ b/cockatrice/src/client/sound_engine.cpp
@@ -12,11 +12,11 @@
#define DEFAULT_THEME_NAME "Default"
#define TEST_SOUND_FILENAME "player_join"
-SoundEngine::SoundEngine(QObject *parent) : QObject(parent), player(nullptr)
+SoundEngine::SoundEngine(QObject *parent) : QObject(parent), audioOutput(nullptr), player(nullptr)
{
ensureThemeDirectoryExists();
- connect(&SettingsCache::instance(), SIGNAL(soundThemeChanged()), this, SLOT(themeChangedSlot()));
- connect(&SettingsCache::instance(), SIGNAL(soundEnabledChanged()), this, SLOT(soundEnabledChanged()));
+ connect(&SettingsCache::instance(), &SettingsCache::soundThemeChanged, this, &SoundEngine::themeChangedSlot);
+ connect(&SettingsCache::instance(), &SettingsCache::soundEnabledChanged, this, &SoundEngine::soundEnabledChanged);
soundEnabledChanged();
themeChangedSlot();
@@ -28,26 +28,34 @@ SoundEngine::~SoundEngine()
player->deleteLater();
player = nullptr;
}
+ if (audioOutput) {
+ audioOutput->deleteLater();
+ audioOutput = nullptr;
+ }
}
void SoundEngine::soundEnabledChanged()
{
if (SettingsCache::instance().getSoundEnabled()) {
- qDebug() << "SoundEngine: enabling sound with" << audioData.size() << "sounds";
+ qCInfo(SoundEngineLog) << "SoundEngine: enabling sound with" << audioData.size() << "sounds";
if (!player) {
player = new QMediaPlayer;
#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
- auto qAudioOutput = new QAudioOutput;
- player->setAudioOutput(qAudioOutput);
+ audioOutput = new QAudioOutput(player);
+ player->setAudioOutput(audioOutput);
#endif
}
} else {
- qDebug() << "SoundEngine: disabling sound";
+ qCInfo(SoundEngineLog) << "SoundEngine: disabling sound";
if (player) {
player->stop();
player->deleteLater();
player = nullptr;
}
+ if (audioOutput) {
+ audioOutput->deleteLater();
+ audioOutput = nullptr;
+ }
}
}
@@ -82,7 +90,7 @@ void SoundEngine::ensureThemeDirectoryExists()
{
if (SettingsCache::instance().getSoundThemeName().isEmpty() ||
!getAvailableThemes().contains(SettingsCache::instance().getSoundThemeName())) {
- qDebug() << "Sounds theme name not set, setting default value";
+ qCInfo(SoundEngineLog) << "Sounds theme name not set, setting default value";
SettingsCache::instance().setSoundThemeName(DEFAULT_THEME_NAME);
}
}
@@ -123,7 +131,7 @@ QStringMap &SoundEngine::getAvailableThemes()
void SoundEngine::themeChangedSlot()
{
QString themeName = SettingsCache::instance().getSoundThemeName();
- qDebug() << "Sound theme changed:" << themeName;
+ qCInfo(SoundEngineLog) << "Sound theme changed:" << themeName;
QDir dir = getAvailableThemes().value(themeName);
diff --git a/cockatrice/src/client/sound_engine.h b/cockatrice/src/client/sound_engine.h
index 11304874a..851ca3928 100644
--- a/cockatrice/src/client/sound_engine.h
+++ b/cockatrice/src/client/sound_engine.h
@@ -1,12 +1,15 @@
#ifndef SOUNDENGINE_H
#define SOUNDENGINE_H
+#include
+#include
#include
#include
#include
#include
-class QAudioOutput;
+inline Q_LOGGING_CATEGORY(SoundEngineLog, "sound_engine");
+
class QBuffer;
typedef QMap QStringMap;
@@ -23,6 +26,7 @@ public:
private:
QStringMap availableThemes;
QMap audioData;
+ QAudioOutput *audioOutput;
QMediaPlayer *player;
protected:
diff --git a/cockatrice/src/client/tabs/abstract_tab_deck_editor.cpp b/cockatrice/src/client/tabs/abstract_tab_deck_editor.cpp
new file mode 100644
index 000000000..fe2148293
--- /dev/null
+++ b/cockatrice/src/client/tabs/abstract_tab_deck_editor.cpp
@@ -0,0 +1,569 @@
+#include "abstract_tab_deck_editor.h"
+
+#include "../../client/game_logic/abstract_client.h"
+#include "../../client/tapped_out_interface.h"
+#include "../../client/ui/widgets/cards/card_info_frame_widget.h"
+#include "../../deck/deck_stats_interface.h"
+#include "../../dialogs/dlg_load_deck.h"
+#include "../../dialogs/dlg_load_deck_from_clipboard.h"
+#include "../../game/cards/card_database_manager.h"
+#include "../../game/cards/card_database_model.h"
+#include "../../server/pending_command.h"
+#include "../../settings/cache_settings.h"
+#include "../ui/picture_loader/picture_loader.h"
+#include "../ui/pixel_map_generator.h"
+#include "pb/command_deck_upload.pb.h"
+#include "pb/response.pb.h"
+#include "tab_supervisor.h"
+#include "trice_limits.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+AbstractTabDeckEditor::AbstractTabDeckEditor(TabSupervisor *_tabSupervisor) : Tab(_tabSupervisor)
+{
+ setDockOptions(QMainWindow::AnimatedDocks | QMainWindow::AllowNestedDocks | QMainWindow::AllowTabbedDocks);
+
+ databaseDisplayDockWidget = new DeckEditorDatabaseDisplayWidget(this);
+ deckDockWidget = new DeckEditorDeckDockWidget(this);
+ cardInfoDockWidget = new DeckEditorCardInfoDockWidget(this);
+ filterDockWidget = new DeckEditorFilterDockWidget(this);
+ printingSelectorDockWidget = new DeckEditorPrintingSelectorDockWidget(this);
+
+ connect(deckDockWidget, &DeckEditorDeckDockWidget::deckChanged, this, &AbstractTabDeckEditor::onDeckChanged);
+ connect(deckDockWidget, &DeckEditorDeckDockWidget::cardChanged, this, &AbstractTabDeckEditor::updateCard);
+ connect(this, &AbstractTabDeckEditor::decrementCard, deckDockWidget, &DeckEditorDeckDockWidget::actDecrementCard);
+ connect(databaseDisplayDockWidget, &DeckEditorDatabaseDisplayWidget::cardChanged, this,
+ &AbstractTabDeckEditor::updateCard);
+ connect(databaseDisplayDockWidget, &DeckEditorDatabaseDisplayWidget::addCardToMainDeck, this,
+ &AbstractTabDeckEditor::actAddCard);
+ connect(databaseDisplayDockWidget, &DeckEditorDatabaseDisplayWidget::addCardToSideboard, this,
+ &AbstractTabDeckEditor::actAddCardToSideboard);
+ connect(databaseDisplayDockWidget, &DeckEditorDatabaseDisplayWidget::decrementCardFromMainDeck, this,
+ &AbstractTabDeckEditor::actDecrementCard);
+ connect(databaseDisplayDockWidget, &DeckEditorDatabaseDisplayWidget::decrementCardFromSideboard, this,
+ &AbstractTabDeckEditor::actDecrementCardFromSideboard);
+
+ connect(filterDockWidget, &DeckEditorFilterDockWidget::clearAllDatabaseFilters, databaseDisplayDockWidget,
+ &DeckEditorDatabaseDisplayWidget::clearAllDatabaseFilters);
+
+ connect(&SettingsCache::instance().shortcuts(), SIGNAL(shortCutChanged()), this, SLOT(refreshShortcuts()));
+}
+
+void AbstractTabDeckEditor::updateCard(CardInfoPtr _card)
+{
+ cardInfoDockWidget->updateCard(_card);
+ printingSelectorDockWidget->printingSelector->setCard(_card, DECK_ZONE_MAIN);
+}
+
+void AbstractTabDeckEditor::onDeckChanged()
+{
+ setModified(!isBlankNewDeck());
+ deckMenu->setSaveStatus(!isBlankNewDeck());
+}
+
+void AbstractTabDeckEditor::addCardHelper(const CardInfoPtr info, QString zoneName)
+{
+ if (!info)
+ return;
+ if (info->getIsToken())
+ zoneName = DECK_ZONE_TOKENS;
+
+ QModelIndex newCardIndex = deckDockWidget->deckModel->addPreferredPrintingCard(info->getName(), zoneName, false);
+ // recursiveExpand(newCardIndex);
+ deckDockWidget->deckView->clearSelection();
+ deckDockWidget->deckView->setCurrentIndex(newCardIndex);
+ setModified(true);
+ databaseDisplayDockWidget->searchEdit->setSelection(0, databaseDisplayDockWidget->searchEdit->text().length());
+}
+
+void AbstractTabDeckEditor::actAddCard(CardInfoPtr info)
+{
+ if (QApplication::keyboardModifiers() & Qt::ControlModifier)
+ actAddCardToSideboard(info);
+ else
+ addCardHelper(info, DECK_ZONE_MAIN);
+ deckMenu->setSaveStatus(true);
+}
+
+void AbstractTabDeckEditor::actAddCardToSideboard(CardInfoPtr info)
+{
+ addCardHelper(info, DECK_ZONE_SIDE);
+ deckMenu->setSaveStatus(true);
+}
+
+void AbstractTabDeckEditor::actDecrementCard(CardInfoPtr info)
+{
+ emit decrementCard(info, DECK_ZONE_MAIN);
+}
+
+void AbstractTabDeckEditor::actDecrementCardFromSideboard(CardInfoPtr info)
+{
+ emit decrementCard(info, DECK_ZONE_SIDE);
+}
+
+void AbstractTabDeckEditor::actSwapCard(CardInfoPtr info, QString zoneName)
+{
+ QString providerId = CardDatabaseManager::getInstance()->getSetInfoForCard(info).getProperty("uuid");
+ QString collectorNumber = CardDatabaseManager::getInstance()->getSetInfoForCard(info).getProperty("num");
+ deckDockWidget->swapCard(
+ deckDockWidget->deckModel->findCard(info->getName(), zoneName, providerId, collectorNumber));
+}
+
+/**
+ * Opens the deck in this tab.
+ * @param deck The deck. Takes ownership of the object
+ */
+void AbstractTabDeckEditor::openDeck(DeckLoader *deck)
+{
+ setDeck(deck);
+
+ if (!deck->getLastFileName().isEmpty()) {
+ SettingsCache::instance().recents().updateRecentlyOpenedDeckPaths(deck->getLastFileName());
+ }
+}
+
+/**
+ * Sets the currently active deck for this tab
+ * @param _deck The deck. Takes ownership of the object
+ */
+void AbstractTabDeckEditor::setDeck(DeckLoader *_deck)
+{
+ deckDockWidget->setDeck(_deck);
+ PictureLoader::cacheCardPixmaps(
+ CardDatabaseManager::getInstance()->getCardsByNameAndProviderId(getDeckList()->getCardListWithProviderId()));
+ setModified(false);
+
+ // If they load a deck, make the deck list appear
+ aDeckDockVisible->setChecked(true);
+ deckDockWidget->setVisible(aDeckDockVisible->isChecked());
+}
+
+DeckLoader *AbstractTabDeckEditor::getDeckList() const
+{
+ return deckDockWidget->getDeckList();
+}
+
+void AbstractTabDeckEditor::setModified(bool _modified)
+{
+ modified = _modified;
+ emit tabTextChanged(this, getTabText());
+}
+
+/**
+ * @brief Returns true if this tab is a blank newly opened tab, as if it was just created with the `New Deck` action.
+ */
+bool AbstractTabDeckEditor::isBlankNewDeck() const
+{
+ DeckLoader *deck = getDeckList();
+ return !modified && deck->hasNotBeenLoaded();
+}
+
+void AbstractTabDeckEditor::actNewDeck()
+{
+ auto deckOpenLocation = confirmOpen(false);
+
+ if (deckOpenLocation == CANCELLED) {
+ return;
+ }
+
+ if (deckOpenLocation == NEW_TAB) {
+ emit openDeckEditor(nullptr);
+ return;
+ }
+
+ cleanDeckAndResetModified();
+}
+
+void AbstractTabDeckEditor::cleanDeckAndResetModified()
+{
+ deckMenu->setSaveStatus(false);
+ deckDockWidget->cleanDeck();
+ setModified(false);
+}
+
+/**
+ * @brief Displays the save confirmation dialogue that is shown before loading a deck, if required. Takes into
+ * account the `openDeckInNewTab` settting.
+ *
+ * @param openInSameTabIfBlank Open the deck in the same tab instead of a new tab if the current tab is completely
+ * blank. Only relevant when the `openDeckInNewTab` setting is enabled.
+ *
+ * @returns An enum that indicates if and where to load the deck
+ */
+AbstractTabDeckEditor::DeckOpenLocation AbstractTabDeckEditor::confirmOpen(const bool openInSameTabIfBlank)
+{
+ // handle `openDeckInNewTab` setting
+ if (SettingsCache::instance().getOpenDeckInNewTab()) {
+ if (openInSameTabIfBlank && isBlankNewDeck()) {
+ return SAME_TAB;
+ } else {
+ return NEW_TAB;
+ }
+ }
+
+ // early return if deck is unmodified
+ if (!modified) {
+ return SAME_TAB;
+ }
+
+ // do the save confirmation dialogue
+ tabSupervisor->setCurrentWidget(this);
+
+ QMessageBox *msgBox = createSaveConfirmationWindow();
+ QPushButton *newTabButton = msgBox->addButton(tr("Open in new tab"), QMessageBox::ApplyRole);
+
+ int ret = msgBox->exec();
+
+ // `exec()` returns an opaque value if a non-standard button was clicked.
+ // Directly check if newTabButton was clicked before switching over the standard buttons.
+ if (msgBox->clickedButton() == newTabButton) {
+ return NEW_TAB;
+ }
+
+ switch (ret) {
+ case QMessageBox::Save:
+ return actSaveDeck() ? SAME_TAB : CANCELLED;
+ case QMessageBox::Discard:
+ return SAME_TAB;
+ default:
+ return CANCELLED;
+ }
+}
+
+/**
+ * @brief Creates the base save confirmation dialogue box.
+ *
+ * @returns A QMessageBox that can be further modified
+ */
+QMessageBox *AbstractTabDeckEditor::createSaveConfirmationWindow()
+{
+ QMessageBox *msgBox = new QMessageBox(this);
+ msgBox->setIcon(QMessageBox::Warning);
+ msgBox->setWindowTitle(tr("Are you sure?"));
+ msgBox->setText(tr("The decklist has been modified.\nDo you want to save the changes?"));
+ msgBox->setStandardButtons(QMessageBox::Save | QMessageBox::Discard | QMessageBox::Cancel);
+ return msgBox;
+}
+
+void AbstractTabDeckEditor::actLoadDeck()
+{
+ auto deckOpenLocation = confirmOpen();
+
+ if (deckOpenLocation == CANCELLED) {
+ return;
+ }
+
+ DlgLoadDeck dialog(this);
+ if (!dialog.exec())
+ return;
+
+ QString fileName = dialog.selectedFiles().at(0);
+ openDeckFromFile(fileName, deckOpenLocation);
+ deckDockWidget->updateBannerCardComboBox();
+}
+
+void AbstractTabDeckEditor::actOpenRecent(const QString &fileName)
+{
+ auto deckOpenLocation = confirmOpen();
+
+ if (deckOpenLocation == CANCELLED) {
+ return;
+ }
+
+ openDeckFromFile(fileName, deckOpenLocation);
+}
+
+/**
+ * Actually opens the deck from file
+ * @param fileName The path of the deck to open
+ * @param deckOpenLocation Which tab to open the deck
+ */
+void AbstractTabDeckEditor::openDeckFromFile(const QString &fileName, DeckOpenLocation deckOpenLocation)
+{
+ DeckLoader::FileFormat fmt = DeckLoader::getFormatFromName(fileName);
+
+ auto *l = new DeckLoader;
+ if (l->loadFromFile(fileName, fmt, true)) {
+ if (deckOpenLocation == NEW_TAB) {
+ emit openDeckEditor(l);
+ l->deleteLater();
+ } else {
+ deckMenu->setSaveStatus(false);
+ openDeck(l);
+ }
+ } else {
+ l->deleteLater();
+ QMessageBox::critical(this, tr("Error"), tr("Could not open deck at %1").arg(fileName));
+ }
+ deckMenu->setSaveStatus(true);
+}
+
+bool AbstractTabDeckEditor::actSaveDeck()
+{
+ DeckLoader *const deck = getDeckList();
+ if (deck->getLastRemoteDeckId() != -1) {
+ QString deckString = deck->writeToString_Native();
+ if (deckString.length() > MAX_FILE_LENGTH) {
+ QMessageBox::critical(this, tr("Error"), tr("Could not save remote deck"));
+ return false;
+ }
+
+ Command_DeckUpload cmd;
+ cmd.set_deck_id(static_cast(deck->getLastRemoteDeckId()));
+ cmd.set_deck_list(deckString.toStdString());
+
+ PendingCommand *pend = AbstractClient::prepareSessionCommand(cmd);
+ connect(pend, SIGNAL(finished(Response, CommandContainer, QVariant)), this,
+ SLOT(saveDeckRemoteFinished(Response)));
+ tabSupervisor->getClient()->sendCommand(pend);
+
+ return true;
+ } else if (deck->getLastFileName().isEmpty())
+ return actSaveDeckAs();
+ else if (deck->saveToFile(deck->getLastFileName(), deck->getLastFileFormat())) {
+ setModified(false);
+ return true;
+ }
+ QMessageBox::critical(
+ this, tr("Error"),
+ tr("The deck could not be saved.\nPlease check that the directory is writable and try again."));
+ return false;
+}
+
+bool AbstractTabDeckEditor::actSaveDeckAs()
+{
+ QFileDialog dialog(this, tr("Save deck"));
+ dialog.setDirectory(SettingsCache::instance().getDeckPath());
+ dialog.setAcceptMode(QFileDialog::AcceptSave);
+ dialog.setDefaultSuffix("cod");
+ dialog.setNameFilters(DeckLoader::FILE_NAME_FILTERS);
+ dialog.selectFile(getDeckList()->getName().trimmed());
+ if (!dialog.exec())
+ return false;
+
+ QString fileName = dialog.selectedFiles().at(0);
+ DeckLoader::FileFormat fmt = DeckLoader::getFormatFromName(fileName);
+
+ if (!getDeckList()->saveToFile(fileName, fmt)) {
+ QMessageBox::critical(
+ this, tr("Error"),
+ tr("The deck could not be saved.\nPlease check that the directory is writable and try again."));
+ return false;
+ }
+ setModified(false);
+
+ SettingsCache::instance().recents().updateRecentlyOpenedDeckPaths(fileName);
+
+ return true;
+}
+
+void AbstractTabDeckEditor::saveDeckRemoteFinished(const Response &response)
+{
+ if (response.response_code() != Response::RespOk)
+ QMessageBox::critical(this, tr("Error"), tr("The deck could not be saved."));
+ else
+ setModified(false);
+}
+
+void AbstractTabDeckEditor::actLoadDeckFromClipboard()
+{
+ auto deckOpenLocation = confirmOpen();
+
+ if (deckOpenLocation == CANCELLED) {
+ return;
+ }
+
+ DlgLoadDeckFromClipboard dlg(this);
+ if (!dlg.exec())
+ return;
+
+ if (deckOpenLocation == NEW_TAB) {
+ emit openDeckEditor(dlg.getDeckList());
+ } else {
+ setDeck(dlg.getDeckList());
+ setModified(true);
+ }
+
+ deckMenu->setSaveStatus(true);
+}
+
+void AbstractTabDeckEditor::editDeckInClipboard(bool annotated)
+{
+ DlgEditDeckInClipboard dlg(*getDeckList(), annotated, this);
+ if (!dlg.exec())
+ return;
+
+ setDeck(dlg.getDeckList());
+ setModified(true);
+
+ deckMenu->setSaveStatus(true);
+}
+
+void AbstractTabDeckEditor::actEditDeckInClipboard()
+{
+ editDeckInClipboard(true);
+}
+
+void AbstractTabDeckEditor::actEditDeckInClipboardRaw()
+{
+ editDeckInClipboard(false);
+}
+
+void AbstractTabDeckEditor::actSaveDeckToClipboard()
+{
+ getDeckList()->saveToClipboard(true, true);
+}
+
+void AbstractTabDeckEditor::actSaveDeckToClipboardNoSetInfo()
+{
+ getDeckList()->saveToClipboard(true, false);
+}
+
+void AbstractTabDeckEditor::actSaveDeckToClipboardRaw()
+{
+ getDeckList()->saveToClipboard(false, true);
+}
+
+void AbstractTabDeckEditor::actSaveDeckToClipboardRawNoSetInfo()
+{
+ getDeckList()->saveToClipboard(false, false);
+}
+
+void AbstractTabDeckEditor::actPrintDeck()
+{
+ auto *dlg = new QPrintPreviewDialog(this);
+ connect(dlg, SIGNAL(paintRequested(QPrinter *)), deckDockWidget->deckModel, SLOT(printDeckList(QPrinter *)));
+ dlg->exec();
+}
+
+void AbstractTabDeckEditor::exportToDecklistWebsite(DeckLoader::DecklistWebsite website)
+{
+ // check if deck is not null
+ if (DeckLoader *const deck = getDeckList()) {
+ // Get the decklist url string from the deck loader class.
+ QString decklistUrlString = deck->exportDeckToDecklist(website);
+ // Check to make sure the string isn't empty.
+ if (QString::compare(decklistUrlString, "", Qt::CaseInsensitive) == 0) {
+ // Show an error if the deck is empty, and return.
+ QMessageBox::critical(this, tr("Error"), tr("There are no cards in your deck to be exported"));
+ return;
+ }
+ // Encode the string recieved from the model to make sure all characters are encoded.
+ // first we put it into a qurl object
+ QUrl decklistUrl = QUrl(decklistUrlString);
+ // we get the correctly encoded url.
+ decklistUrlString = decklistUrl.toEncoded();
+ // We open the url in the user's default browser
+ QDesktopServices::openUrl(decklistUrlString);
+ } else {
+ // if there's no deck loader object, return an error
+ QMessageBox::critical(this, tr("Error"), tr("No deck was selected to be exported."));
+ }
+}
+
+/**
+ * Exports the deck to www.decklist.org (the old website)
+ */
+void AbstractTabDeckEditor::actExportDeckDecklist()
+{
+ exportToDecklistWebsite(DeckLoader::DecklistOrg);
+}
+
+/**
+ * Exports the deck to www.decklist.xyz (the new website)
+ */
+void AbstractTabDeckEditor::actExportDeckDecklistXyz()
+{
+ exportToDecklistWebsite(DeckLoader::DecklistXyz);
+}
+
+void AbstractTabDeckEditor::actAnalyzeDeckDeckstats()
+{
+ auto *interface = new DeckStatsInterface(*databaseDisplayDockWidget->databaseModel->getDatabase(),
+ this); // it deletes itself when done
+ interface->analyzeDeck(getDeckList());
+}
+
+void AbstractTabDeckEditor::actAnalyzeDeckTappedout()
+{
+ auto *interface = new TappedOutInterface(*databaseDisplayDockWidget->databaseModel->getDatabase(),
+ this); // it deletes itself when done
+ interface->analyzeDeck(getDeckList());
+}
+
+void AbstractTabDeckEditor::filterTreeChanged(FilterTree *filterTree)
+{
+ databaseDisplayDockWidget->setFilterTree(filterTree);
+}
+
+// Method uses to sync docks state with menu items state
+bool AbstractTabDeckEditor::eventFilter(QObject *o, QEvent *e)
+{
+ if (e->type() == QEvent::Close) {
+ if (o == cardInfoDockWidget) {
+ aCardInfoDockVisible->setChecked(false);
+ aCardInfoDockFloating->setEnabled(false);
+ } else if (o == deckDockWidget) {
+ aDeckDockVisible->setChecked(false);
+ aDeckDockFloating->setEnabled(false);
+ } else if (o == filterDockWidget) {
+ aFilterDockVisible->setChecked(false);
+ aFilterDockFloating->setEnabled(false);
+ } else if (o == printingSelectorDockWidget) {
+ aPrintingSelectorDockVisible->setChecked(false);
+ aPrintingSelectorDockFloating->setEnabled(false);
+ }
+ }
+ if (o == this && e->type() == QEvent::Hide) {
+ LayoutsSettings &layouts = SettingsCache::instance().layouts();
+ layouts.setDeckEditorLayoutState(saveState());
+ layouts.setDeckEditorGeometry(saveGeometry());
+ layouts.setDeckEditorCardSize(cardInfoDockWidget->size());
+ layouts.setDeckEditorFilterSize(filterDockWidget->size());
+ layouts.setDeckEditorDeckSize(deckDockWidget->size());
+ layouts.setDeckEditorPrintingSelectorSize(printingSelectorDockWidget->size());
+ }
+ return false;
+}
+
+bool AbstractTabDeckEditor::confirmClose()
+{
+ if (modified) {
+ tabSupervisor->setCurrentWidget(this);
+ int ret = createSaveConfirmationWindow()->exec();
+ if (ret == QMessageBox::Save)
+ return actSaveDeck();
+ else if (ret == QMessageBox::Cancel)
+ return false;
+ }
+ return true;
+}
+
+void AbstractTabDeckEditor::closeRequest(bool forced)
+{
+ if (!forced && !confirmClose()) {
+ return;
+ }
+
+ emit deckEditorClosing(this);
+ close();
+}
\ No newline at end of file
diff --git a/cockatrice/src/client/tabs/abstract_tab_deck_editor.h b/cockatrice/src/client/tabs/abstract_tab_deck_editor.h
new file mode 100644
index 000000000..0a5f97f56
--- /dev/null
+++ b/cockatrice/src/client/tabs/abstract_tab_deck_editor.h
@@ -0,0 +1,155 @@
+#ifndef TAB_GENERIC_DECK_EDITOR_H
+#define TAB_GENERIC_DECK_EDITOR_H
+
+#include "../../game/cards/card_database.h"
+#include "../menus/deck_editor/deck_editor_menu.h"
+#include "../ui/widgets/deck_editor/deck_editor_card_info_dock_widget.h"
+#include "../ui/widgets/deck_editor/deck_editor_database_display_widget.h"
+#include "../ui/widgets/deck_editor/deck_editor_deck_dock_widget.h"
+#include "../ui/widgets/deck_editor/deck_editor_filter_dock_widget.h"
+#include "../ui/widgets/deck_editor/deck_editor_printing_selector_dock_widget.h"
+#include "../ui/widgets/visual_deck_storage/deck_preview/deck_preview_deck_tags_display_widget.h"
+#include "tab.h"
+
+class CardDatabaseModel;
+class CardDatabaseDisplayModel;
+
+class CardInfoFrameWidget;
+class DeckLoader;
+class DeckEditorMenu;
+class DeckEditorCardInfoDockWidget;
+class DeckEditorDatabaseDisplayWidget;
+class DeckEditorDeckDockWidget;
+class DeckEditorFilterDockWidget;
+class DeckEditorPrintingSelectorDockWidget;
+class DeckPreviewDeckTagsDisplayWidget;
+class Response;
+class FilterTreeModel;
+class FilterBuilder;
+
+class QTreeView;
+class QTextEdit;
+class QLabel;
+class QComboBox;
+class QGroupBox;
+class QMessageBox;
+class QHBoxLayout;
+class QVBoxLayout;
+class QPushButton;
+class QDockWidget;
+class QMenu;
+class QAction;
+
+class AbstractTabDeckEditor : public Tab
+{
+ Q_OBJECT
+
+public:
+ explicit AbstractTabDeckEditor(TabSupervisor *_tabSupervisor);
+
+ // UI and Navigation
+ virtual void createMenus() = 0;
+ [[nodiscard]] virtual QString getTabText() const override = 0;
+ bool confirmClose();
+ virtual void retranslateUi() override = 0;
+
+ // Deck Management
+ void openDeck(DeckLoader *deck);
+ DeckLoader *getDeckList() const;
+ void setModified(bool _windowModified);
+
+ // UI Elements
+ DeckEditorMenu *deckMenu;
+ DeckEditorDatabaseDisplayWidget *databaseDisplayDockWidget;
+ DeckEditorCardInfoDockWidget *cardInfoDockWidget;
+ DeckEditorDeckDockWidget *deckDockWidget;
+ DeckEditorFilterDockWidget *filterDockWidget;
+ DeckEditorPrintingSelectorDockWidget *printingSelectorDockWidget;
+
+public slots:
+ void onDeckChanged();
+ void updateCard(CardInfoPtr _card);
+ void actAddCard(CardInfoPtr info);
+ void actAddCardToSideboard(CardInfoPtr info);
+ void actDecrementCard(CardInfoPtr info);
+ void actDecrementCardFromSideboard(CardInfoPtr info);
+ void actOpenRecent(const QString &fileName);
+ void filterTreeChanged(FilterTree *filterTree);
+ void closeRequest(bool forced = false) override;
+ virtual void showPrintingSelector() = 0;
+ virtual void dockTopLevelChanged(bool topLevel) = 0;
+
+signals:
+ void openDeckEditor(const DeckLoader *deckLoader);
+ void deckEditorClosing(AbstractTabDeckEditor *tab);
+ void decrementCard(CardInfoPtr card, QString zoneName);
+
+protected slots:
+ // Deck Operations
+ virtual void actNewDeck();
+ void cleanDeckAndResetModified();
+ virtual void actLoadDeck();
+ bool actSaveDeck();
+ bool actSaveDeckAs();
+ virtual void actLoadDeckFromClipboard();
+ void actEditDeckInClipboard();
+ void actEditDeckInClipboardRaw();
+ void actSaveDeckToClipboard();
+ void actSaveDeckToClipboardNoSetInfo();
+ void actSaveDeckToClipboardRaw();
+ void actSaveDeckToClipboardRawNoSetInfo();
+ void actPrintDeck();
+ void actExportDeckDecklist();
+ void actExportDeckDecklistXyz();
+ void actAnalyzeDeckDeckstats();
+ void actAnalyzeDeckTappedout();
+
+ // Remote Save
+ void saveDeckRemoteFinished(const Response &r);
+
+ // UI Layout Management
+ virtual void loadLayout() = 0;
+ virtual void restartLayout() = 0;
+ virtual void freeDocksSize() = 0;
+ virtual void refreshShortcuts() = 0;
+
+ bool eventFilter(QObject *o, QEvent *e) override;
+ virtual void dockVisibleTriggered() = 0;
+ virtual void dockFloatingTriggered() = 0;
+
+private:
+ virtual void setDeck(DeckLoader *_deck);
+ void editDeckInClipboard(bool annotated);
+ void exportToDecklistWebsite(DeckLoader::DecklistWebsite website);
+
+protected:
+ /**
+ * @brief Enum for selecting deck open location
+ */
+ enum DeckOpenLocation
+ {
+ CANCELLED,
+ SAME_TAB,
+ NEW_TAB
+ };
+
+ DeckOpenLocation confirmOpen(bool openInSameTabIfBlank = true);
+ QMessageBox *createSaveConfirmationWindow();
+ bool isBlankNewDeck() const;
+
+ // Helper functions for card actions
+ void addCardHelper(CardInfoPtr info, QString zoneName);
+ void actSwapCard(CardInfoPtr info, QString zoneName);
+ virtual void openDeckFromFile(const QString &fileName, DeckOpenLocation deckOpenLocation);
+
+ // UI Menu Elements
+ QMenu *viewMenu, *cardInfoDockMenu, *deckDockMenu, *filterDockMenu, *printingSelectorDockMenu;
+
+ QAction *aResetLayout;
+ QAction *aCardInfoDockVisible, *aCardInfoDockFloating, *aDeckDockVisible, *aDeckDockFloating;
+ QAction *aFilterDockVisible, *aFilterDockFloating, *aPrintingSelectorDockVisible, *aPrintingSelectorDockFloating;
+
+ bool modified = false;
+};
+
+#endif // TAB_GENERIC_DECK_EDITOR_H
diff --git a/cockatrice/src/client/tabs/api/edhrec/api_response/edhrec_commander_api_response.cpp b/cockatrice/src/client/tabs/api/edhrec/api_response/edhrec_commander_api_response.cpp
new file mode 100644
index 000000000..da2a9c052
--- /dev/null
+++ b/cockatrice/src/client/tabs/api/edhrec/api_response/edhrec_commander_api_response.cpp
@@ -0,0 +1,45 @@
+#include "edhrec_commander_api_response.h"
+
+#include
+#include
+
+void EdhrecCommanderApiResponse::fromJson(const QJsonObject &json)
+{
+ // Parse the collapsed DeckStatistics
+ deckStats.fromJson(json);
+
+ // Parse Archidekt section
+ QJsonArray archidektJson = json.value("archidekt").toArray();
+ archidekt.fromJson(archidektJson);
+
+ // Parse other fields
+ similar = json.value("similar").toObject();
+ header = json.value("header").toString();
+ panels = json.value("panels").toObject();
+ description = json.value("description").toString();
+ QJsonObject containerJson = json.value("container").toObject();
+ container.fromJson(containerJson);
+}
+
+void EdhrecCommanderApiResponse::debugPrint() const
+{
+ qDebug() << "Deck Statistics:";
+ qDebug() << " Creature:" << deckStats.creature;
+ qDebug() << " Instant:" << deckStats.instant;
+ qDebug() << " Sorcery:" << deckStats.sorcery;
+ qDebug() << " Artifact:" << deckStats.artifact;
+ qDebug() << " Enchantment:" << deckStats.enchantment;
+ qDebug() << " Battle:" << deckStats.battle;
+ qDebug() << " Planeswalker:" << deckStats.planeswalker;
+ qDebug() << " Land:" << deckStats.land;
+ qDebug() << " Basic:" << deckStats.basic;
+ qDebug() << " Nonbasic:" << deckStats.nonbasic;
+
+ archidekt.debugPrint();
+
+ qDebug() << "Similar:" << similar;
+ qDebug() << "Header:" << header;
+ qDebug() << "Panels:" << panels;
+ qDebug() << "Description:" << description;
+ container.debugPrint();
+}
diff --git a/cockatrice/src/client/tabs/api/edhrec/api_response/edhrec_commander_api_response.h b/cockatrice/src/client/tabs/api/edhrec/api_response/edhrec_commander_api_response.h
new file mode 100644
index 000000000..91cf3afbc
--- /dev/null
+++ b/cockatrice/src/client/tabs/api/edhrec/api_response/edhrec_commander_api_response.h
@@ -0,0 +1,28 @@
+#ifndef DECKDATA_H
+#define DECKDATA_H
+
+#include "edhrec_commander_api_response_archidekt_links.h"
+#include "edhrec_commander_api_response_average_deck_statistics.h"
+#include "edhrec_commander_api_response_card_container.h"
+
+#include
+#include
+#include
+
+// Represents the main structure of the JSON
+class EdhrecCommanderApiResponse
+{
+public:
+ EdhrecCommanderApiResponseAverageDeckStatistics deckStats;
+ EdhrecCommanderApiResponseArchidektLinks archidekt;
+ QJsonObject similar;
+ QString header;
+ QJsonObject panels;
+ QString description;
+ EdhrecCommanderApiResponseCardContainer container;
+
+ void fromJson(const QJsonObject &json);
+ void debugPrint() const;
+};
+
+#endif // DECKDATA_H
diff --git a/cockatrice/src/client/tabs/api/edhrec/api_response/edhrec_commander_api_response_archidekt_links.cpp b/cockatrice/src/client/tabs/api/edhrec/api_response/edhrec_commander_api_response_archidekt_links.cpp
new file mode 100644
index 000000000..aac540f90
--- /dev/null
+++ b/cockatrice/src/client/tabs/api/edhrec/api_response/edhrec_commander_api_response_archidekt_links.cpp
@@ -0,0 +1,43 @@
+#include "edhrec_commander_api_response_archidekt_links.h"
+
+#include
+#include
+#include
+#include
+
+void EdhrecCommanderApiResponseArchidektLink::fromJson(const QJsonObject &json)
+{
+ c = json.value("c").toString();
+ f = json.value("f").toInt(0);
+ q = json.value("q").toInt(0);
+ u = json.value("u").toString();
+}
+
+void EdhrecCommanderApiResponseArchidektLink::debugPrint() const
+{
+ qDebug() << " C:" << c;
+ qDebug() << " F:" << f;
+ qDebug() << " Q:" << q;
+ qDebug() << " U:" << u;
+}
+
+void EdhrecCommanderApiResponseArchidektLinks::fromJson(const QJsonArray &json)
+{
+ entries.clear();
+ for (const QJsonValue &value : json) {
+ if (value.isObject()) {
+ QJsonObject entryJson = value.toObject();
+ EdhrecCommanderApiResponseArchidektLink entry;
+ entry.fromJson(entryJson);
+ entries.append(entry);
+ }
+ }
+}
+
+void EdhrecCommanderApiResponseArchidektLinks::debugPrint() const
+{
+ qDebug() << "Archidekt Entries:";
+ for (const auto &entry : entries) {
+ entry.debugPrint();
+ }
+}
diff --git a/cockatrice/src/client/tabs/api/edhrec/api_response/edhrec_commander_api_response_archidekt_links.h b/cockatrice/src/client/tabs/api/edhrec/api_response/edhrec_commander_api_response_archidekt_links.h
new file mode 100644
index 000000000..537dbb1fc
--- /dev/null
+++ b/cockatrice/src/client/tabs/api/edhrec/api_response/edhrec_commander_api_response_archidekt_links.h
@@ -0,0 +1,32 @@
+#ifndef ARCHIDEKTENTRY_H
+#define ARCHIDEKTENTRY_H
+
+#include
+#include
+#include
+#include
+
+// Represents a single Archidekt entry
+class EdhrecCommanderApiResponseArchidektLink
+{
+public:
+ QString c;
+ int f = 0;
+ int q = 0;
+ QString u;
+
+ void fromJson(const QJsonObject &json);
+ void debugPrint() const;
+};
+
+// Represents the Archidekt section as a list of entries
+class EdhrecCommanderApiResponseArchidektLinks
+{
+public:
+ QVector entries;
+
+ void fromJson(const QJsonArray &json);
+ void debugPrint() const;
+};
+
+#endif // ARCHIDEKTENTRY_H
diff --git a/cockatrice/src/client/tabs/api/edhrec/api_response/edhrec_commander_api_response_average_deck_statistics.cpp b/cockatrice/src/client/tabs/api/edhrec/api_response/edhrec_commander_api_response_average_deck_statistics.cpp
new file mode 100644
index 000000000..74cce5322
--- /dev/null
+++ b/cockatrice/src/client/tabs/api/edhrec/api_response/edhrec_commander_api_response_average_deck_statistics.cpp
@@ -0,0 +1,15 @@
+#include "edhrec_commander_api_response_average_deck_statistics.h"
+
+void EdhrecCommanderApiResponseAverageDeckStatistics::fromJson(const QJsonObject &json)
+{
+ creature = json.value("creature").toInt(0);
+ instant = json.value("instant").toInt(0);
+ sorcery = json.value("sorcery").toInt(0);
+ artifact = json.value("artifact").toInt(0);
+ enchantment = json.value("enchantment").toInt(0);
+ battle = json.value("battle").toInt(0);
+ planeswalker = json.value("planeswalker").toInt(0);
+ land = json.value("land").toInt(0);
+ basic = json.value("basic").toInt(0);
+ nonbasic = json.value("nonbasic").toInt(0);
+}
\ No newline at end of file
diff --git a/cockatrice/src/client/tabs/api/edhrec/api_response/edhrec_commander_api_response_average_deck_statistics.h b/cockatrice/src/client/tabs/api/edhrec/api_response/edhrec_commander_api_response_average_deck_statistics.h
new file mode 100644
index 000000000..1dd01abe2
--- /dev/null
+++ b/cockatrice/src/client/tabs/api/edhrec/api_response/edhrec_commander_api_response_average_deck_statistics.h
@@ -0,0 +1,22 @@
+#ifndef AVERAGE_DECK_STATISTICS_H
+#define AVERAGE_DECK_STATISTICS_H
+
+#include
+
+// Represents the typical deck statistics (collapsed section)
+struct EdhrecCommanderApiResponseAverageDeckStatistics
+{
+ int creature = 0;
+ int instant = 0;
+ int sorcery = 0;
+ int artifact = 0;
+ int enchantment = 0;
+ int battle = 0;
+ int planeswalker = 0;
+ int land = 0;
+ int basic = 0;
+ int nonbasic = 0;
+
+ void fromJson(const QJsonObject &json);
+};
+#endif // AVERAGE_DECK_STATISTICS_H
diff --git a/cockatrice/src/client/tabs/api/edhrec/api_response/edhrec_commander_api_response_card_container.cpp b/cockatrice/src/client/tabs/api/edhrec/api_response/edhrec_commander_api_response_card_container.cpp
new file mode 100644
index 000000000..f8ffab787
--- /dev/null
+++ b/cockatrice/src/client/tabs/api/edhrec/api_response/edhrec_commander_api_response_card_container.cpp
@@ -0,0 +1,49 @@
+#include "edhrec_commander_api_response_card_container.h"
+
+#include
+#include
+#include
+
+void EdhrecCommanderApiResponseCardContainer::fromJson(const QJsonObject &json)
+{
+ // Parse breadcrumb
+ QJsonArray breadcrumbArray = json.value("breadcrumb").toArray();
+ for (const QJsonValue &breadcrumbValue : breadcrumbArray) {
+ breadcrumb.push_back(breadcrumbValue.toObject());
+ }
+
+ description = json.value("description").toString();
+ QJsonObject jsonDict = json.value("json_dict").toObject();
+ card.fromJson(jsonDict.value("card").toObject());
+ QJsonArray cardlistsArray = jsonDict.value("cardlists").toArray();
+
+ for (const QJsonValue &cardlistValue : cardlistsArray) {
+ QJsonObject cardlistObj = cardlistValue.toObject();
+ QJsonArray cardviewsArray = cardlistObj.value("cardviews").toArray();
+ EdhrecCommanderApiResponseCardList cardView;
+ cardView.fromJson(cardlistValue.toObject());
+ cardlists.push_back(cardView);
+ }
+
+ keywords = json.value("keywords").toString();
+ title = json.value("title").toString();
+}
+
+void EdhrecCommanderApiResponseCardContainer::debugPrint() const
+{
+ qDebug() << "Breadcrumb:";
+ for (const auto &breadcrumbEntry : breadcrumb) {
+ qDebug() << breadcrumbEntry;
+ }
+
+ qDebug() << "Description:" << description;
+ card.debugPrint();
+
+ qDebug() << "Cardlists:";
+ for (const auto &cardlist : cardlists) {
+ cardlist.debugPrint();
+ }
+
+ qDebug() << "Keywords:" << keywords;
+ qDebug() << "Title:" << title;
+}
diff --git a/cockatrice/src/client/tabs/api/edhrec/api_response/edhrec_commander_api_response_card_container.h b/cockatrice/src/client/tabs/api/edhrec/api_response/edhrec_commander_api_response_card_container.h
new file mode 100644
index 000000000..d2ea05874
--- /dev/null
+++ b/cockatrice/src/client/tabs/api/edhrec/api_response/edhrec_commander_api_response_card_container.h
@@ -0,0 +1,60 @@
+#ifndef CONTAINER_ENTRY_H
+#define CONTAINER_ENTRY_H
+
+#include "edhrec_commander_api_response_card_list.h"
+#include "edhrec_commander_api_response_commander_details.h"
+
+#include
+#include
+#include
+#include
+#include
+
+class EdhrecCommanderApiResponseCardContainer
+{
+public:
+ // Constructor
+ EdhrecCommanderApiResponseCardContainer() = default;
+
+ // Parse deck-related data from JSON
+ void fromJson(const QJsonObject &json);
+
+ // Debug method for logging
+ void debugPrint() const;
+
+ // Getter methods for deck container
+ const QString &getDescription() const
+ {
+ return description;
+ }
+ const QVector &getBreadcrumb() const
+ {
+ return breadcrumb;
+ }
+ const EdhrecCommanderApiResponseCommanderDetails &getCommanderDetails() const
+ {
+ return card;
+ }
+ const QVector &getCardlists() const
+ {
+ return cardlists;
+ }
+ const QString &getKeywords() const
+ {
+ return keywords;
+ }
+ const QString &getTitle() const
+ {
+ return title;
+ }
+
+private:
+ QString description;
+ QVector breadcrumb;
+ EdhrecCommanderApiResponseCommanderDetails card;
+ QVector cardlists;
+ QString keywords;
+ QString title;
+};
+
+#endif // CONTAINER_ENTRY_H
diff --git a/cockatrice/src/client/tabs/api/edhrec/api_response/edhrec_commander_api_response_card_details.cpp b/cockatrice/src/client/tabs/api/edhrec/api_response/edhrec_commander_api_response_card_details.cpp
new file mode 100644
index 000000000..2f2791de1
--- /dev/null
+++ b/cockatrice/src/client/tabs/api/edhrec/api_response/edhrec_commander_api_response_card_details.cpp
@@ -0,0 +1,36 @@
+#include "edhrec_commander_api_response_card_details.h"
+
+#include
+
+EdhrecCommanderApiResponseCardDetails::EdhrecCommanderApiResponseCardDetails()
+ : synergy(0.0), inclusion(0), numDecks(0), potentialDecks(0)
+{
+}
+
+void EdhrecCommanderApiResponseCardDetails::fromJson(const QJsonObject &json)
+{
+ // Parse the fields from the JSON object
+ name = json.value("name").toString();
+ sanitized = json.value("sanitized").toString();
+ sanitizedWo = json.value("sanitized_wo").toString();
+ url = json.value("url").toString();
+ synergy = json.value("synergy").toDouble(0.0);
+ inclusion = json.value("inclusion").toInt(0);
+ label = json.value("label").toString();
+ numDecks = json.value("num_decks").toInt(0);
+ potentialDecks = json.value("potential_decks").toInt(0);
+}
+
+void EdhrecCommanderApiResponseCardDetails::debugPrint() const
+{
+ // Print out all the fields for debugging
+ qDebug() << "Name:" << name;
+ qDebug() << "Sanitized:" << sanitized;
+ qDebug() << "Sanitized Wo:" << sanitizedWo;
+ qDebug() << "URL:" << url;
+ qDebug() << "Synergy:" << synergy;
+ qDebug() << "Inclusion:" << inclusion;
+ qDebug() << "Label:" << label;
+ qDebug() << "Num Decks:" << numDecks;
+ qDebug() << "Potential Decks:" << potentialDecks;
+}
diff --git a/cockatrice/src/client/tabs/api/edhrec/api_response/edhrec_commander_api_response_card_details.h b/cockatrice/src/client/tabs/api/edhrec/api_response/edhrec_commander_api_response_card_details.h
new file mode 100644
index 000000000..82d85824b
--- /dev/null
+++ b/cockatrice/src/client/tabs/api/edhrec/api_response/edhrec_commander_api_response_card_details.h
@@ -0,0 +1,29 @@
+#ifndef CARD_VIEW_H
+#define CARD_VIEW_H
+
+#include
+#include
+
+class EdhrecCommanderApiResponseCardDetails
+{
+public:
+ QString name;
+ QString sanitized;
+ QString sanitizedWo;
+ QString url;
+ double synergy;
+ int inclusion;
+ QString label;
+ int numDecks;
+ int potentialDecks;
+
+ EdhrecCommanderApiResponseCardDetails();
+
+ // Method to populate the object from a JSON object
+ void fromJson(const QJsonObject &json);
+
+ // Debug method to print out the data
+ void debugPrint() const;
+};
+
+#endif // CARD_VIEW_H
diff --git a/cockatrice/src/client/tabs/api/edhrec/api_response/edhrec_commander_api_response_card_list.cpp b/cockatrice/src/client/tabs/api/edhrec/api_response/edhrec_commander_api_response_card_list.cpp
new file mode 100644
index 000000000..5d3eebedd
--- /dev/null
+++ b/cockatrice/src/client/tabs/api/edhrec/api_response/edhrec_commander_api_response_card_list.cpp
@@ -0,0 +1,33 @@
+#include "edhrec_commander_api_response_card_list.h"
+
+#include
+
+EdhrecCommanderApiResponseCardList::EdhrecCommanderApiResponseCardList()
+{
+}
+
+void EdhrecCommanderApiResponseCardList::fromJson(const QJsonObject &json)
+{
+ // Parse the header from the JSON object
+ header = json.value("header").toString();
+
+ // Parse the cardviews array and populate cardViews
+ QJsonArray cardviewsArray = json.value("cardviews").toArray();
+ for (const QJsonValue &value : cardviewsArray) {
+ QJsonObject cardviewObj = value.toObject();
+ EdhrecCommanderApiResponseCardDetails cardView;
+ cardView.fromJson(cardviewObj);
+ cardViews.append(cardView);
+ }
+}
+
+void EdhrecCommanderApiResponseCardList::debugPrint() const
+{
+ // Print out the header
+ qDebug() << "Header:" << header;
+
+ // Print out all the CardView objects
+ for (const EdhrecCommanderApiResponseCardDetails &cardView : cardViews) {
+ cardView.debugPrint();
+ }
+}
diff --git a/cockatrice/src/client/tabs/api/edhrec/api_response/edhrec_commander_api_response_card_list.h b/cockatrice/src/client/tabs/api/edhrec/api_response/edhrec_commander_api_response_card_list.h
new file mode 100644
index 000000000..e0ddc3466
--- /dev/null
+++ b/cockatrice/src/client/tabs/api/edhrec/api_response/edhrec_commander_api_response_card_list.h
@@ -0,0 +1,27 @@
+#ifndef CARD_LIST_H
+#define CARD_LIST_H
+
+#include "edhrec_commander_api_response_card_details.h"
+
+#include
+#include
+#include
+#include
+
+class EdhrecCommanderApiResponseCardList
+{
+public:
+ QString header;
+ QList cardViews;
+
+ // Default constructor
+ EdhrecCommanderApiResponseCardList();
+
+ // Method to populate the object from a JSON object
+ void fromJson(const QJsonObject &json);
+
+ // Debug method to print out the data
+ void debugPrint() const;
+};
+
+#endif // CARD_LIST_H
diff --git a/cockatrice/src/client/tabs/api/edhrec/api_response/edhrec_commander_api_response_card_prices.cpp b/cockatrice/src/client/tabs/api/edhrec/api_response/edhrec_commander_api_response_card_prices.cpp
new file mode 100644
index 000000000..6454e5500
--- /dev/null
+++ b/cockatrice/src/client/tabs/api/edhrec/api_response/edhrec_commander_api_response_card_prices.cpp
@@ -0,0 +1,31 @@
+#include "edhrec_commander_api_response_card_prices.h"
+
+#include
+
+void CardPrices::fromJson(const QJsonObject &json)
+{
+ // Parse prices from various sources
+ cardhoarder = json.value("cardhoarder").toObject();
+ cardkingdom = json.value("cardkingdom").toObject();
+ cardmarket = json.value("cardmarket").toObject();
+ face2face = json.value("face2face").toObject();
+ manapool = json.value("manapool").toObject();
+ mtgstocks = json.value("mtgstocks").toObject();
+ scg = json.value("scg").toObject();
+ tcgl = json.value("tcgl").toObject();
+ tcgplayer = json.value("tcgplayer").toObject();
+}
+
+void CardPrices::debugPrint() const
+{
+ qDebug() << "Card Prices:";
+ qDebug() << "Cardhoarder:" << cardhoarder;
+ qDebug() << "Cardkingdom:" << cardkingdom;
+ qDebug() << "Cardmarket:" << cardmarket;
+ qDebug() << "Face2Face:" << face2face;
+ qDebug() << "Manapool:" << manapool;
+ qDebug() << "Mtgstocks:" << mtgstocks;
+ qDebug() << "SCG:" << scg;
+ qDebug() << "TCGL:" << tcgl;
+ qDebug() << "Tcgplayer:" << tcgplayer;
+}
diff --git a/cockatrice/src/client/tabs/api/edhrec/api_response/edhrec_commander_api_response_card_prices.h b/cockatrice/src/client/tabs/api/edhrec/api_response/edhrec_commander_api_response_card_prices.h
new file mode 100644
index 000000000..32e017a92
--- /dev/null
+++ b/cockatrice/src/client/tabs/api/edhrec/api_response/edhrec_commander_api_response_card_prices.h
@@ -0,0 +1,66 @@
+#ifndef EDHREC_COMMANDER_API_RESPONSE_CARD_PRICES_H
+#define EDHREC_COMMANDER_API_RESPONSE_CARD_PRICES_H
+
+#include
+
+class CardPrices
+{
+public:
+ // Constructor
+ CardPrices() = default;
+
+ // Parse prices from JSON
+ void fromJson(const QJsonObject &json);
+ void debugPrint() const;
+
+ // Getter methods for card prices
+ const QJsonObject &getCardhoarder() const
+ {
+ return cardhoarder;
+ }
+ const QJsonObject &getCardkingdom() const
+ {
+ return cardkingdom;
+ }
+ const QJsonObject &getCardmarket() const
+ {
+ return cardmarket;
+ }
+ const QJsonObject &getFace2face() const
+ {
+ return face2face;
+ }
+ const QJsonObject &getManapool() const
+ {
+ return manapool;
+ }
+ const QJsonObject &getMtgstocks() const
+ {
+ return mtgstocks;
+ }
+ const QJsonObject &getScg() const
+ {
+ return scg;
+ }
+ const QJsonObject &getTcgl() const
+ {
+ return tcgl;
+ }
+ const QJsonObject &getTcgplayer() const
+ {
+ return tcgplayer;
+ }
+
+private:
+ QJsonObject cardhoarder;
+ QJsonObject cardkingdom;
+ QJsonObject cardmarket;
+ QJsonObject face2face;
+ QJsonObject manapool;
+ QJsonObject mtgstocks;
+ QJsonObject scg;
+ QJsonObject tcgl;
+ QJsonObject tcgplayer;
+};
+
+#endif // EDHREC_COMMANDER_API_RESPONSE_CARD_PRICES_H
diff --git a/cockatrice/src/client/tabs/api/edhrec/api_response/edhrec_commander_api_response_commander_details.cpp b/cockatrice/src/client/tabs/api/edhrec/api_response/edhrec_commander_api_response_commander_details.cpp
new file mode 100644
index 000000000..7b220603e
--- /dev/null
+++ b/cockatrice/src/client/tabs/api/edhrec/api_response/edhrec_commander_api_response_commander_details.cpp
@@ -0,0 +1,90 @@
+#include "edhrec_commander_api_response_commander_details.h"
+
+#include
+
+void EdhrecCommanderApiResponseCommanderDetails::fromJson(const QJsonObject &json)
+{
+ // Parse card-related data
+ aetherhubUri = json.value("aetherhub_uri").toString();
+ archidektUri = json.value("archidekt_uri").toString();
+ cmc = json.value("cmc").toInt(0);
+ colorIdentity = json.value("color_identity").toArray();
+ combos = json.value("combos").toBool(false);
+ deckstatsUri = json.value("deckstats_uri").toString();
+
+ // Parse image URIs
+ QJsonArray imageUrisArray = json.value("image_uris").toArray();
+ for (const QJsonValue &imageValue : imageUrisArray) {
+ QJsonObject imageObject = imageValue.toObject();
+ imageUris.push_back(imageObject.value("normal").toString());
+ imageUris.push_back(imageObject.value("art_crop").toString());
+ }
+
+ inclusion = json.value("inclusion").toInt(0);
+ isCommander = json.value("is_commander").toBool(false);
+ label = json.value("label").toString();
+ layout = json.value("layout").toString();
+ legalCommander = json.value("legal_commander").toBool(false);
+ moxfieldUri = json.value("moxfield_uri").toString();
+ mtggoldfishUri = json.value("mtggoldfish_uri").toString();
+ name = json.value("name").toString();
+ names = json.value("names").toArray();
+ numDecks = json.value("num_decks").toInt(0);
+ potentialDecks = json.value("potential_decks").toInt(0);
+ precon = json.value("precon").toString();
+
+ // Parse prices
+ prices.fromJson(json.value("prices").toObject());
+
+ primaryType = json.value("primary_type").toString();
+ rarity = json.value("rarity").toString();
+ salt = json.value("salt").toDouble(0.0);
+ sanitized = json.value("sanitized").toString();
+ sanitizedWo = json.value("sanitized_wo").toString();
+ scryfallUri = json.value("scryfall_uri").toString();
+ spellbookUri = json.value("spellbook_uri").toString();
+ type = json.value("type").toString();
+ url = json.value("url").toString();
+}
+
+void EdhrecCommanderApiResponseCommanderDetails::debugPrint() const
+{
+ qDebug() << "Card Data:";
+ qDebug() << "Aetherhub URI:" << aetherhubUri;
+ qDebug() << "Archidekt URI:" << archidektUri;
+ qDebug() << "CMC:" << cmc;
+ qDebug() << "Color Identity:" << colorIdentity;
+ qDebug() << "Combos:" << combos;
+ qDebug() << "Deckstats URI:" << deckstatsUri;
+
+ qDebug() << "Image URIs:";
+ for (const auto &uri : imageUris) {
+ qDebug() << uri;
+ }
+
+ qDebug() << "Inclusion:" << inclusion;
+ qDebug() << "Is Commander:" << isCommander;
+ qDebug() << "Label:" << label;
+ qDebug() << "Layout:" << layout;
+ qDebug() << "Legal Commander:" << legalCommander;
+ qDebug() << "Moxfield URI:" << moxfieldUri;
+ qDebug() << "MTGGoldfish URI:" << mtggoldfishUri;
+ qDebug() << "Name:" << name;
+ qDebug() << "Names:" << names;
+ qDebug() << "Number of Decks:" << numDecks;
+ qDebug() << "Potential Decks:" << potentialDecks;
+ qDebug() << "Precon:" << precon;
+
+ // Print the prices using the debugPrint method from CardPrices
+ prices.debugPrint();
+
+ qDebug() << "Primary Type:" << primaryType;
+ qDebug() << "Rarity:" << rarity;
+ qDebug() << "Salt:" << salt;
+ qDebug() << "Sanitized:" << sanitized;
+ qDebug() << "Sanitized WO:" << sanitizedWo;
+ qDebug() << "Scryfall URI:" << scryfallUri;
+ qDebug() << "Spellbook URI:" << spellbookUri;
+ qDebug() << "Type:" << type;
+ qDebug() << "URL:" << url;
+}
\ No newline at end of file
diff --git a/cockatrice/src/client/tabs/api/edhrec/api_response/edhrec_commander_api_response_commander_details.h b/cockatrice/src/client/tabs/api/edhrec/api_response/edhrec_commander_api_response_commander_details.h
new file mode 100644
index 000000000..78ccfc23a
--- /dev/null
+++ b/cockatrice/src/client/tabs/api/edhrec/api_response/edhrec_commander_api_response_commander_details.h
@@ -0,0 +1,173 @@
+#ifndef EDHREC_COMMANDER_API_RESPONSE_COMMANDER_DETAILS_H
+#define EDHREC_COMMANDER_API_RESPONSE_COMMANDER_DETAILS_H
+
+#include "edhrec_commander_api_response_card_prices.h"
+
+#include
+#include
+#include
+#include
+
+class EdhrecCommanderApiResponseCommanderDetails
+{
+public:
+ // Constructor
+ EdhrecCommanderApiResponseCommanderDetails() = default;
+
+ // Parse card-related data from JSON
+ void fromJson(const QJsonObject &json);
+
+ // Debug method for logging
+ void debugPrint() const;
+
+ // Getters for the card data
+ const QString &getAetherhubUri() const
+ {
+ return aetherhubUri;
+ }
+ const QString &getArchidektUri() const
+ {
+ return archidektUri;
+ }
+ int getCmc() const
+ {
+ return cmc;
+ }
+ const QJsonArray &getColorIdentity() const
+ {
+ return colorIdentity;
+ }
+ bool isCombos() const
+ {
+ return combos;
+ }
+ const QString &getDeckstatsUri() const
+ {
+ return deckstatsUri;
+ }
+ const QVector &getImageUris() const
+ {
+ return imageUris;
+ }
+ int getInclusion() const
+ {
+ return inclusion;
+ }
+ bool getIsCommander() const
+ {
+ return isCommander;
+ }
+ const QString &getLabel() const
+ {
+ return label;
+ }
+ const QString &getLayout() const
+ {
+ return layout;
+ }
+ bool getLegalCommander() const
+ {
+ return legalCommander;
+ }
+ const QString &getMoxfieldUri() const
+ {
+ return moxfieldUri;
+ }
+ const QString &getMtggoldfishUri() const
+ {
+ return mtggoldfishUri;
+ }
+ const QString &getName() const
+ {
+ return name;
+ }
+ const QJsonArray &getNames() const
+ {
+ return names;
+ }
+ int getNumDecks() const
+ {
+ return numDecks;
+ }
+ int getPotentialDecks() const
+ {
+ return potentialDecks;
+ }
+ const QString &getPrecon() const
+ {
+ return precon;
+ }
+ const CardPrices &getPrices() const
+ {
+ return prices;
+ }
+ const QString &getPrimaryType() const
+ {
+ return primaryType;
+ }
+ const QString &getRarity() const
+ {
+ return rarity;
+ }
+ double getSalt() const
+ {
+ return salt;
+ }
+ const QString &getSanitized() const
+ {
+ return sanitized;
+ }
+ const QString &getSanitizedWo() const
+ {
+ return sanitizedWo;
+ }
+ const QString &getScryfallUri() const
+ {
+ return scryfallUri;
+ }
+ const QString &getSpellbookUri() const
+ {
+ return spellbookUri;
+ }
+ const QString &getType() const
+ {
+ return type;
+ }
+ const QString &getUrl() const
+ {
+ return url;
+ }
+
+private:
+ QString aetherhubUri;
+ QString archidektUri;
+ int cmc = 0;
+ QJsonArray colorIdentity;
+ bool combos = false;
+ QString deckstatsUri;
+ QVector imageUris;
+ int inclusion = 0;
+ bool isCommander = false;
+ QString label;
+ QString layout;
+ bool legalCommander = false;
+ QString moxfieldUri;
+ QString mtggoldfishUri;
+ QString name;
+ QJsonArray names;
+ int numDecks = 0;
+ int potentialDecks = 0;
+ QString precon;
+ CardPrices prices;
+ QString primaryType;
+ QString rarity;
+ double salt = 0.0;
+ QString sanitized;
+ QString sanitizedWo;
+ QString scryfallUri;
+ QString spellbookUri;
+ QString type;
+ QString url;
+};
+
+#endif // EDHREC_COMMANDER_API_RESPONSE_COMMANDER_DETAILS_H
diff --git a/cockatrice/src/client/tabs/api/edhrec/edhrec_commander_api_response_card_details_display_widget.cpp b/cockatrice/src/client/tabs/api/edhrec/edhrec_commander_api_response_card_details_display_widget.cpp
new file mode 100644
index 000000000..87853b9aa
--- /dev/null
+++ b/cockatrice/src/client/tabs/api/edhrec/edhrec_commander_api_response_card_details_display_widget.cpp
@@ -0,0 +1,41 @@
+#include "edhrec_commander_api_response_card_details_display_widget.h"
+
+#include "../../../../game/cards/card_database_manager.h"
+
+EdhrecCommanderApiResponseCardDetailsDisplayWidget::EdhrecCommanderApiResponseCardDetailsDisplayWidget(
+ QWidget *parent,
+ const EdhrecCommanderApiResponseCardDetails &_toDisplay)
+ : QWidget(parent), toDisplay(_toDisplay)
+{
+ layout = new QVBoxLayout(this);
+ setLayout(layout);
+
+ cardPictureWidget = new CardInfoPictureWidget(this);
+ cardPictureWidget->setCard(CardDatabaseManager::getInstance()->getCard(toDisplay.name));
+
+ label = new QLabel(this);
+ label->setText(toDisplay.name + "\n" + toDisplay.label);
+ label->setAlignment(Qt::AlignHCenter);
+
+ // Set label color based on inclusion rate
+ int inclusionRate = (toDisplay.numDecks * 100) / toDisplay.potentialDecks;
+
+ QColor labelColor;
+ if (inclusionRate <= 30) {
+ labelColor = QColor(255, 0, 0); // Red
+ } else if (inclusionRate <= 60) {
+ int red = 255 - ((inclusionRate - 30) * 2);
+ int green = (inclusionRate - 30) * 4; // Adjust green to make the transition smoother
+ labelColor = QColor(red, green, 0); // purple-ish
+ } else if (inclusionRate <= 90) {
+ int green = (inclusionRate - 60) * 5; // Increase green
+ labelColor = QColor(100, green, 100); // Green shades
+ } else {
+ labelColor = QColor(100, 200, 100); // Dark Green
+ }
+
+ label->setStyleSheet(QString("color: %1").arg(labelColor.name()));
+
+ layout->addWidget(cardPictureWidget);
+ layout->addWidget(label);
+}
diff --git a/cockatrice/src/client/tabs/api/edhrec/edhrec_commander_api_response_card_details_display_widget.h b/cockatrice/src/client/tabs/api/edhrec/edhrec_commander_api_response_card_details_display_widget.h
new file mode 100644
index 000000000..1cd76989c
--- /dev/null
+++ b/cockatrice/src/client/tabs/api/edhrec/edhrec_commander_api_response_card_details_display_widget.h
@@ -0,0 +1,26 @@
+#ifndef EDHREC_COMMANDER_API_RESPONSE_CARD_DETAILS_DISPLAY_WIDGET_H
+#define EDHREC_COMMANDER_API_RESPONSE_CARD_DETAILS_DISPLAY_WIDGET_H
+
+#include "../../../ui/widgets/cards/card_info_picture_widget.h"
+#include "api_response/edhrec_commander_api_response_card_details.h"
+
+#include
+#include
+#include
+
+class EdhrecCommanderApiResponseCardDetailsDisplayWidget : public QWidget
+{
+ Q_OBJECT
+public:
+ explicit EdhrecCommanderApiResponseCardDetailsDisplayWidget(
+ QWidget *parent,
+ const EdhrecCommanderApiResponseCardDetails &_toDisplay);
+
+private:
+ EdhrecCommanderApiResponseCardDetails toDisplay;
+ QVBoxLayout *layout;
+ CardInfoPictureWidget *cardPictureWidget;
+ QLabel *label;
+};
+
+#endif // EDHREC_COMMANDER_API_RESPONSE_CARD_DETAILS_DISPLAY_WIDGET_H
diff --git a/cockatrice/src/client/tabs/api/edhrec/edhrec_commander_api_response_card_list_display_widget.cpp b/cockatrice/src/client/tabs/api/edhrec/edhrec_commander_api_response_card_list_display_widget.cpp
new file mode 100644
index 000000000..0d1f1e8f3
--- /dev/null
+++ b/cockatrice/src/client/tabs/api/edhrec/edhrec_commander_api_response_card_list_display_widget.cpp
@@ -0,0 +1,34 @@
+#include "edhrec_commander_api_response_card_list_display_widget.h"
+
+#include "../../../ui/widgets/general/display/banner_widget.h"
+#include "edhrec_commander_api_response_card_details_display_widget.h"
+
+#include
+
+EdhrecCommanderApiResponseCardListDisplayWidget::EdhrecCommanderApiResponseCardListDisplayWidget(
+ QWidget *parent,
+ EdhrecCommanderApiResponseCardList toDisplay)
+ : QWidget(parent)
+{
+ layout = new QVBoxLayout(this);
+ setLayout(layout);
+
+ header = new BannerWidget(this, toDisplay.header);
+
+ flowWidget = new FlowWidget(this, Qt::Horizontal, Qt::ScrollBarAlwaysOff, Qt::ScrollBarAlwaysOff);
+ header->setBuddy(flowWidget);
+
+ foreach (EdhrecCommanderApiResponseCardDetails card_detail, toDisplay.cardViews) {
+ auto widget = new EdhrecCommanderApiResponseCardDetailsDisplayWidget(flowWidget, card_detail);
+ flowWidget->addWidget(widget);
+ }
+
+ layout->addWidget(header);
+ layout->addWidget(flowWidget);
+}
+
+void EdhrecCommanderApiResponseCardListDisplayWidget::resizeEvent(QResizeEvent *event)
+{
+ QWidget::resizeEvent(event);
+ qDebug() << event->size();
+}
diff --git a/cockatrice/src/client/tabs/api/edhrec/edhrec_commander_api_response_card_list_display_widget.h b/cockatrice/src/client/tabs/api/edhrec/edhrec_commander_api_response_card_list_display_widget.h
new file mode 100644
index 000000000..9df667929
--- /dev/null
+++ b/cockatrice/src/client/tabs/api/edhrec/edhrec_commander_api_response_card_list_display_widget.h
@@ -0,0 +1,30 @@
+#ifndef EDHREC_COMMANDER_API_RESPONSE_CARD_LIST_DISPLAY_WIDGET_H
+#define EDHREC_COMMANDER_API_RESPONSE_CARD_LIST_DISPLAY_WIDGET_H
+
+#include "../../../ui/widgets/general/display/banner_widget.h"
+#include "../../../ui/widgets/general/layout_containers/flow_widget.h"
+#include "api_response/edhrec_commander_api_response_card_list.h"
+
+#include
+#include
+#include
+
+class EdhrecCommanderApiResponseCardListDisplayWidget : public QWidget
+{
+ Q_OBJECT
+public:
+ explicit EdhrecCommanderApiResponseCardListDisplayWidget(QWidget *parent,
+ EdhrecCommanderApiResponseCardList toDisplay);
+ void resizeEvent(QResizeEvent *event) override;
+ [[nodiscard]] QString getBannerText() const
+ {
+ return header->getText();
+ };
+
+private:
+ QVBoxLayout *layout;
+ BannerWidget *header;
+ FlowWidget *flowWidget;
+};
+
+#endif // EDHREC_COMMANDER_API_RESPONSE_CARD_LIST_DISPLAY_WIDGET_H
diff --git a/cockatrice/src/client/tabs/api/edhrec/edhrec_commander_api_response_commander_details_display_widget.cpp b/cockatrice/src/client/tabs/api/edhrec/edhrec_commander_api_response_commander_details_display_widget.cpp
new file mode 100644
index 000000000..58b77df73
--- /dev/null
+++ b/cockatrice/src/client/tabs/api/edhrec/edhrec_commander_api_response_commander_details_display_widget.cpp
@@ -0,0 +1,36 @@
+#include "edhrec_commander_api_response_commander_details_display_widget.h"
+
+#include "../../../../game/cards/card_database_manager.h"
+#include "../../../ui/widgets/cards/card_info_picture_widget.h"
+
+#include
+
+EdhrecCommanderResponseCommanderDetailsDisplayWidget::EdhrecCommanderResponseCommanderDetailsDisplayWidget(
+ QWidget *parent,
+ const EdhrecCommanderApiResponseCommanderDetails &_commanderDetails)
+ : QWidget(parent), commanderDetails(_commanderDetails)
+{
+ layout = new QVBoxLayout(this);
+ setLayout(layout);
+
+ commanderPicture = new CardInfoPictureWidget(this);
+ commanderPicture->setCard(CardDatabaseManager::getInstance()->getCard(commanderDetails.getName()));
+
+ commanderDetails.debugPrint();
+
+ label = new QLabel(this);
+ label->setAlignment(Qt::AlignCenter);
+ salt = new QLabel(this);
+ salt->setAlignment(Qt::AlignCenter);
+
+ layout->addWidget(commanderPicture);
+ layout->addWidget(label);
+ layout->addWidget(salt);
+ retranslateUi();
+}
+
+void EdhrecCommanderResponseCommanderDetailsDisplayWidget::retranslateUi()
+{
+ label->setText(commanderDetails.getLabel());
+ salt->setText(tr("Salt: ") + QString::number(commanderDetails.getSalt()));
+}
\ No newline at end of file
diff --git a/cockatrice/src/client/tabs/api/edhrec/edhrec_commander_api_response_commander_details_display_widget.h b/cockatrice/src/client/tabs/api/edhrec/edhrec_commander_api_response_commander_details_display_widget.h
new file mode 100644
index 000000000..85970d503
--- /dev/null
+++ b/cockatrice/src/client/tabs/api/edhrec/edhrec_commander_api_response_commander_details_display_widget.h
@@ -0,0 +1,28 @@
+#ifndef EDHREC_COMMANDER_API_RESPONSE_COMMANDER_DETAILS_DISPLAY_WIDGET_H
+#define EDHREC_COMMANDER_API_RESPONSE_COMMANDER_DETAILS_DISPLAY_WIDGET_H
+
+#include "../../../ui/widgets/cards/card_info_picture_widget.h"
+#include "api_response/edhrec_commander_api_response_commander_details.h"
+
+#include
+#include
+#include
+
+class EdhrecCommanderResponseCommanderDetailsDisplayWidget : public QWidget
+{
+ Q_OBJECT
+public:
+ explicit EdhrecCommanderResponseCommanderDetailsDisplayWidget(
+ QWidget *parent,
+ const EdhrecCommanderApiResponseCommanderDetails &_commanderDetails);
+ void retranslateUi();
+
+private:
+ QLabel *label;
+ QLabel *salt;
+ QVBoxLayout *layout;
+ CardInfoPictureWidget *commanderPicture;
+ EdhrecCommanderApiResponseCommanderDetails commanderDetails;
+};
+
+#endif // EDHREC_COMMANDER_API_RESPONSE_COMMANDER_DETAILS_DISPLAY_WIDGET_H
diff --git a/cockatrice/src/client/tabs/api/edhrec/edhrec_commander_api_response_display_widget.cpp b/cockatrice/src/client/tabs/api/edhrec/edhrec_commander_api_response_display_widget.cpp
new file mode 100644
index 000000000..3cab65bf8
--- /dev/null
+++ b/cockatrice/src/client/tabs/api/edhrec/edhrec_commander_api_response_display_widget.cpp
@@ -0,0 +1,102 @@
+#include "edhrec_commander_api_response_display_widget.h"
+
+#include "../../../ui/widgets/cards/card_info_picture_widget.h"
+#include "api_response/edhrec_commander_api_response.h"
+#include "edhrec_commander_api_response_card_list_display_widget.h"
+#include "edhrec_commander_api_response_commander_details_display_widget.h"
+
+#include
+#include
+#include
+#include
+#include
+
+EdhrecCommanderApiResponseDisplayWidget::EdhrecCommanderApiResponseDisplayWidget(QWidget *parent,
+ EdhrecCommanderApiResponse response)
+ : QWidget(parent)
+{
+ layout = new QHBoxLayout(this);
+ setLayout(layout);
+
+ cardDisplayLayout = new QVBoxLayout(this);
+
+ // Create a QSplitter to hold the ListView and ScrollArea holding CardListdisplayWidgets side by side
+ auto splitter = new QSplitter(this);
+ splitter->setOrientation(Qt::Horizontal);
+
+ auto listView = new QListView(splitter);
+ listView->setMinimumWidth(50);
+ listView->setMaximumWidth(150);
+ auto listModel = new QStringListModel(this);
+ QStringList widgetNames;
+
+ // Add commander details
+ auto commanderPicture =
+ new EdhrecCommanderResponseCommanderDetailsDisplayWidget(this, response.container.getCommanderDetails());
+ cardDisplayLayout->addWidget(commanderPicture);
+ widgetNames.append("Commander Details");
+
+ // Add card list widgets
+ auto edhrec_commander_api_response_card_lists = response.container.getCardlists();
+ for (const EdhrecCommanderApiResponseCardList &card_list : edhrec_commander_api_response_card_lists) {
+ auto cardListDisplayWidget = new EdhrecCommanderApiResponseCardListDisplayWidget(this, card_list);
+ cardDisplayLayout->addWidget(cardListDisplayWidget);
+ widgetNames.append(cardListDisplayWidget->getBannerText());
+ }
+
+ // Create a QScrollArea to hold the card display widgets
+ scrollArea = new QScrollArea(splitter);
+ scrollArea->setWidgetResizable(true);
+ scrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
+ scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
+
+ // Set the cardDisplayLayout inside the scroll area
+ auto scrollWidget = new QWidget(scrollArea);
+ scrollWidget->setLayout(cardDisplayLayout);
+ connect(splitter, &QSplitter::splitterMoved, this, &EdhrecCommanderApiResponseDisplayWidget::onSplitterChange);
+ scrollArea->setWidget(scrollWidget);
+
+ // Configure the list view
+ listModel->setStringList(widgetNames);
+ listView->setModel(listModel);
+ listView->setEditTriggers(QAbstractItemView::NoEditTriggers);
+
+ // Connect the list view to ensure the corresponding widget is visible
+ connect(listView, &QListView::clicked, this, [this](const QModelIndex &index) {
+ int widgetIndex = index.row();
+ qDebug() << "clicked: " << widgetIndex;
+ auto targetWidget = cardDisplayLayout->itemAt(widgetIndex)->widget();
+ if (targetWidget) {
+ qDebug() << "Found targetWidget" << targetWidget;
+ // Attempt to cast the parent to QScrollArea
+ auto scrollArea = qobject_cast(this->scrollArea); // Use the scroll area instance
+ if (scrollArea) {
+ qDebug() << "ScrollArea" << scrollArea;
+ scrollArea->ensureWidgetVisible(targetWidget);
+ }
+ }
+ });
+
+ // Add splitter to the main layout
+ splitter->addWidget(listView);
+ splitter->addWidget(scrollArea);
+
+ layout->addWidget(splitter);
+}
+
+void EdhrecCommanderApiResponseDisplayWidget::onSplitterChange()
+{
+ scrollArea->widget()->resize(scrollArea->size());
+}
+
+void EdhrecCommanderApiResponseDisplayWidget::resizeEvent(QResizeEvent *event)
+{
+ QWidget::resizeEvent(event);
+ qDebug() << event->size();
+ layout->invalidate();
+ layout->activate();
+ layout->update();
+ if (scrollArea && scrollArea->widget()) {
+ scrollArea->widget()->resize(event->size());
+ }
+}
diff --git a/cockatrice/src/client/tabs/api/edhrec/edhrec_commander_api_response_display_widget.h b/cockatrice/src/client/tabs/api/edhrec/edhrec_commander_api_response_display_widget.h
new file mode 100644
index 000000000..d1e95e3d6
--- /dev/null
+++ b/cockatrice/src/client/tabs/api/edhrec/edhrec_commander_api_response_display_widget.h
@@ -0,0 +1,27 @@
+#ifndef EDHREC_COMMANDER_API_RESPONSE_DISPLAY_WIDGET_H
+#define EDHREC_COMMANDER_API_RESPONSE_DISPLAY_WIDGET_H
+
+#include "api_response/edhrec_commander_api_response.h"
+
+#include
+#include
+#include
+
+class EdhrecCommanderApiResponseDisplayWidget : public QWidget
+{
+ Q_OBJECT
+
+public:
+ explicit EdhrecCommanderApiResponseDisplayWidget(QWidget *parent, EdhrecCommanderApiResponse response);
+ void resizeEvent(QResizeEvent *event) override;
+
+public slots:
+ void onSplitterChange();
+
+private:
+ QHBoxLayout *layout;
+ QVBoxLayout *cardDisplayLayout;
+ QScrollArea *scrollArea;
+};
+
+#endif // EDHREC_COMMANDER_API_RESPONSE_DISPLAY_WIDGET_H
diff --git a/cockatrice/src/client/tabs/api/edhrec/tab_edhrec.cpp b/cockatrice/src/client/tabs/api/edhrec/tab_edhrec.cpp
new file mode 100644
index 000000000..18662aaab
--- /dev/null
+++ b/cockatrice/src/client/tabs/api/edhrec/tab_edhrec.cpp
@@ -0,0 +1,112 @@
+#include "tab_edhrec.h"
+
+#include "api_response/edhrec_commander_api_response.h"
+#include "edhrec_commander_api_response_display_widget.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+TabEdhRec::TabEdhRec(TabSupervisor *_tabSupervisor) : Tab(_tabSupervisor)
+{
+ networkManager = new QNetworkAccessManager(this);
+#if (QT_VERSION >= QT_VERSION_CHECK(5, 15, 0))
+ networkManager->setTransferTimeout(); // Use Qt's default timeout
+#endif
+
+ networkManager->setRedirectPolicy(QNetworkRequest::ManualRedirectPolicy);
+ connect(networkManager, SIGNAL(finished(QNetworkReply *)), this, SLOT(processApiJson(QNetworkReply *)));
+}
+
+void TabEdhRec::retranslateUi()
+{
+}
+
+void TabEdhRec::setCard(CardInfoPtr _cardToQuery, bool isCommander)
+{
+ cardToQuery = _cardToQuery;
+
+ if (!cardToQuery) {
+ qDebug() << "Invalid card information provided.";
+ return;
+ }
+
+ QString cardName = cardToQuery->getName();
+ QString formattedName = cardName.toLower().replace(" ", "-").remove(QRegularExpression("[^a-z0-9\\-]"));
+
+ QString url;
+ if (isCommander) {
+ url = QString("https://json.edhrec.com/pages/commanders/%1.json").arg(formattedName);
+ } else {
+ url = QString("https://json.edhrec.com/pages/cards/%1.json").arg(formattedName);
+ }
+
+ QNetworkRequest request{QUrl(url)};
+
+ networkManager->get(request);
+}
+
+void TabEdhRec::processApiJson(QNetworkReply *reply)
+{
+ if (reply->error() != QNetworkReply::NoError) {
+ qDebug() << "Network error occurred:" << reply->errorString();
+ reply->deleteLater();
+ return;
+ }
+
+ QByteArray responseData = reply->readAll();
+ QJsonDocument jsonDoc = QJsonDocument::fromJson(responseData);
+
+ if (!jsonDoc.isObject()) {
+ qDebug() << "Invalid JSON response received.";
+ reply->deleteLater();
+ return;
+ }
+
+ QJsonObject jsonObj = jsonDoc.object();
+
+ // qDebug() << jsonObj;
+
+ EdhrecCommanderApiResponse deckData;
+ deckData.fromJson(jsonObj);
+
+ displayWidget = new EdhrecCommanderApiResponseDisplayWidget(this, deckData);
+ // flowWidget->addWidget(displayWidget);
+ setCentralWidget(displayWidget);
+
+ reply->deleteLater();
+ update();
+}
+
+void TabEdhRec::prettyPrintJson(const QJsonValue &value, int indentLevel)
+{
+ const QString indent(indentLevel * 2, ' '); // Adjust spacing as needed for pretty printing
+
+ if (value.isObject()) {
+ QJsonObject obj = value.toObject();
+ for (auto it = obj.begin(); it != obj.end(); ++it) {
+ qDebug().noquote() << indent + it.key() + ":";
+ prettyPrintJson(it.value(), indentLevel + 1);
+ }
+ } else if (value.isArray()) {
+ QJsonArray array = value.toArray();
+ for (int i = 0; i < array.size(); ++i) {
+ qDebug().noquote() << indent + QString("[%1]:").arg(i);
+ prettyPrintJson(array[i], indentLevel + 1);
+ }
+ } else if (value.isString()) {
+ qDebug().noquote() << indent + "\"" + value.toString() + "\"";
+ } else if (value.isDouble()) {
+ qDebug().noquote() << indent + QString::number(value.toDouble());
+ } else if (value.isBool()) {
+ qDebug().noquote() << indent + (value.toBool() ? "true" : "false");
+ } else if (value.isNull()) {
+ qDebug().noquote() << indent + "null";
+ }
+}
diff --git a/cockatrice/src/client/tabs/api/edhrec/tab_edhrec.h b/cockatrice/src/client/tabs/api/edhrec/tab_edhrec.h
new file mode 100644
index 000000000..c15f22b26
--- /dev/null
+++ b/cockatrice/src/client/tabs/api/edhrec/tab_edhrec.h
@@ -0,0 +1,37 @@
+#ifndef TAB_EDHREC_H
+#define TAB_EDHREC_H
+
+#include "../../../../game/cards/card_database.h"
+#include "../../../ui/widgets/general/layout_containers/flow_widget.h"
+#include "../../tab.h"
+#include "edhrec_commander_api_response_display_widget.h"
+
+#include
+#include
+
+class TabEdhRec : public Tab
+{
+ Q_OBJECT
+public:
+ explicit TabEdhRec(TabSupervisor *_tabSupervisor);
+
+ void retranslateUi() override;
+ QString getTabText() const override
+ {
+ auto cardName = cardToQuery.isNull() ? QString() : cardToQuery->getName();
+ return tr("EDHREC: ") + cardName;
+ }
+
+ QNetworkAccessManager *networkManager;
+
+public slots:
+ void processApiJson(QNetworkReply *reply);
+ void prettyPrintJson(const QJsonValue &value, int indentLevel);
+ void setCard(CardInfoPtr _cardToQuery, bool isCommander = false);
+
+private:
+ CardInfoPtr cardToQuery;
+ EdhrecCommanderApiResponseDisplayWidget *displayWidget;
+};
+
+#endif // TAB_EDHREC_H
diff --git a/cockatrice/src/client/tabs/tab.cpp b/cockatrice/src/client/tabs/tab.cpp
index 0ffc5ecdf..d31b86bc9 100644
--- a/cockatrice/src/client/tabs/tab.cpp
+++ b/cockatrice/src/client/tabs/tab.cpp
@@ -1,25 +1,29 @@
#include "tab.h"
#include "../ui/widgets/cards/card_info_display_widget.h"
+#include "./tab_supervisor.h"
#include
+#include
#include
#include
-Tab::Tab(TabSupervisor *_tabSupervisor, QWidget *parent)
- : QMainWindow(parent), tabSupervisor(_tabSupervisor), contentsChanged(false), infoPopup(0)
+Tab::Tab(TabSupervisor *_tabSupervisor)
+ : QMainWindow(_tabSupervisor), tabSupervisor(_tabSupervisor), contentsChanged(false), infoPopup(0)
{
setAttribute(Qt::WA_DeleteOnClose);
}
-void Tab::showCardInfoPopup(const QPoint &pos, const QString &cardName)
+void Tab::showCardInfoPopup(const QPoint &pos, const QString &cardName, const QString &providerId)
{
if (infoPopup) {
infoPopup->deleteLater();
}
currentCardName = cardName;
- infoPopup = new CardInfoDisplayWidget(
- cardName, 0, Qt::Widget | Qt::FramelessWindowHint | Qt::X11BypassWindowManagerHint | Qt::WindowStaysOnTopHint);
+ currentProviderId = providerId;
+ infoPopup = new CardInfoDisplayWidget(cardName, providerId, nullptr,
+ Qt::Widget | Qt::FramelessWindowHint | Qt::X11BypassWindowManagerHint |
+ Qt::WindowStaysOnTopHint);
infoPopup->setAttribute(Qt::WA_TransparentForMouseEvents);
auto screenRect = qApp->primaryScreen()->geometry();
@@ -39,3 +43,17 @@ void Tab::deleteCardInfoPopup(const QString &cardName)
}
}
}
+
+/**
+ * Overrides the closeEvent in order to emit a close signal
+ */
+void Tab::closeEvent(QCloseEvent *event)
+{
+ emit closed();
+ event->accept();
+}
+
+void Tab::closeRequest(bool /*forced*/)
+{
+ close();
+}
\ No newline at end of file
diff --git a/cockatrice/src/client/tabs/tab.h b/cockatrice/src/client/tabs/tab.h
index 295c1e28f..f82f9a7b2 100644
--- a/cockatrice/src/client/tabs/tab.h
+++ b/cockatrice/src/client/tabs/tab.h
@@ -13,6 +13,12 @@ class Tab : public QMainWindow
signals:
void userEvent(bool globalEvent = true);
void tabTextChanged(Tab *tab, const QString &newTabText);
+ /**
+ * Emitted when the tab is closed (because Qt doesn't provide a built-in close signal)
+ * This signal is emitted from this class's overridden Tab::closeEvent method.
+ * Make sure any subclasses that override closeEvent still emit this signal from there.
+ */
+ void closed();
protected:
TabSupervisor *tabSupervisor;
@@ -21,17 +27,18 @@ protected:
tabMenus.append(menu);
}
protected slots:
- void showCardInfoPopup(const QPoint &pos, const QString &cardName);
+ void showCardInfoPopup(const QPoint &pos, const QString &cardName, const QString &providerId);
void deleteCardInfoPopup(const QString &cardName);
+ void closeEvent(QCloseEvent *event) override;
private:
- QString currentCardName;
+ QString currentCardName, currentProviderId;
bool contentsChanged;
CardInfoDisplayWidget *infoPopup;
QList tabMenus;
public:
- Tab(TabSupervisor *_tabSupervisor, QWidget *parent = nullptr);
+ explicit Tab(TabSupervisor *_tabSupervisor);
const QList &getTabMenus() const
{
return tabMenus;
@@ -50,9 +57,13 @@ public:
}
virtual QString getTabText() const = 0;
virtual void retranslateUi() = 0;
- virtual void closeRequest()
- {
- }
+ /**
+ * Sends a request to close the tab.
+ * Signals for cleanup should be emitted from this method instead of the destructor.
+ *
+ * @param forced whether this close request was initiated by the user or forced by the server.
+ */
+ virtual void closeRequest(bool forced = false);
virtual void tabActivated()
{
}
diff --git a/cockatrice/src/client/tabs/tab_account.cpp b/cockatrice/src/client/tabs/tab_account.cpp
index fab79bea1..78517a82d 100644
--- a/cockatrice/src/client/tabs/tab_account.cpp
+++ b/cockatrice/src/client/tabs/tab_account.cpp
@@ -3,7 +3,8 @@
#include "../../deck/custom_line_edit.h"
#include "../../server/pending_command.h"
#include "../../server/user/user_info_box.h"
-#include "../../server/user/user_list.h"
+#include "../../server/user/user_list_manager.h"
+#include "../../server/user/user_list_widget.h"
#include "../game_logic/abstract_client.h"
#include "../sound_engine.h"
#include "pb/event_add_to_list.pb.h"
@@ -12,125 +13,118 @@
#include "pb/event_user_left.pb.h"
#include "pb/response_list_users.pb.h"
#include "pb/session_commands.pb.h"
+#include "tab_supervisor.h"
#include "trice_limits.h"
-#include
#include
#include
-TabUserLists::TabUserLists(TabSupervisor *_tabSupervisor,
- AbstractClient *_client,
- const ServerInfo_User &userInfo,
- QWidget *parent)
- : Tab(_tabSupervisor, parent), client(_client)
+TabAccount::TabAccount(TabSupervisor *_tabSupervisor, AbstractClient *_client, const ServerInfo_User &userInfo)
+ : Tab(_tabSupervisor), client(_client)
{
- allUsersList = new UserList(_tabSupervisor, client, UserList::AllUsersList);
- buddyList = new UserList(_tabSupervisor, client, UserList::BuddyList);
- ignoreList = new UserList(_tabSupervisor, client, UserList::IgnoreList);
+ allUsersList = new UserListWidget(_tabSupervisor, client, UserListWidget::AllUsersList);
+ buddyList = new UserListWidget(_tabSupervisor, client, UserListWidget::BuddyList);
+ ignoreList = new UserListWidget(_tabSupervisor, client, UserListWidget::IgnoreList);
userInfoBox = new UserInfoBox(client, true);
userInfoBox->updateInfo(userInfo);
- connect(allUsersList, SIGNAL(openMessageDialog(const QString &, bool)), this,
- SIGNAL(openMessageDialog(const QString &, bool)));
- connect(buddyList, SIGNAL(openMessageDialog(const QString &, bool)), this,
- SIGNAL(openMessageDialog(const QString &, bool)));
- connect(ignoreList, SIGNAL(openMessageDialog(const QString &, bool)), this,
- SIGNAL(openMessageDialog(const QString &, bool)));
+ connect(allUsersList, &UserListWidget::openMessageDialog, this, &TabAccount::openMessageDialog);
+ connect(buddyList, &UserListWidget::openMessageDialog, this, &TabAccount::openMessageDialog);
+ connect(ignoreList, &UserListWidget::openMessageDialog, this, &TabAccount::openMessageDialog);
- connect(client, SIGNAL(userJoinedEventReceived(const Event_UserJoined &)), this,
- SLOT(processUserJoinedEvent(const Event_UserJoined &)));
- connect(client, SIGNAL(userLeftEventReceived(const Event_UserLeft &)), this,
- SLOT(processUserLeftEvent(const Event_UserLeft &)));
- connect(client, SIGNAL(buddyListReceived(const QList &)), this,
- SLOT(buddyListReceived(const QList &)));
- connect(client, SIGNAL(ignoreListReceived(const QList &)), this,
- SLOT(ignoreListReceived(const QList &)));
- connect(client, SIGNAL(addToListEventReceived(const Event_AddToList &)), this,
- SLOT(processAddToListEvent(const Event_AddToList &)));
- connect(client, SIGNAL(removeFromListEventReceived(const Event_RemoveFromList &)), this,
- SLOT(processRemoveFromListEvent(const Event_RemoveFromList &)));
+ connect(client, &AbstractClient::userJoinedEventReceived, this, &TabAccount::processUserJoinedEvent);
+ connect(client, &AbstractClient::userLeftEventReceived, this, &TabAccount::processUserLeftEvent);
+ connect(client, &AbstractClient::buddyListReceived, this, &TabAccount::buddyListReceived);
+ connect(client, &AbstractClient::ignoreListReceived, this, &TabAccount::ignoreListReceived);
+ connect(client, &AbstractClient::addToListEventReceived, this, &TabAccount::processAddToListEvent);
+ connect(client, &AbstractClient::removeFromListEventReceived, this, &TabAccount::processRemoveFromListEvent);
- PendingCommand *pend = client->prepareSessionCommand(Command_ListUsers());
- connect(pend, SIGNAL(finished(Response, CommandContainer, QVariant)), this,
- SLOT(processListUsersResponse(const Response &)));
+ // Attempt to populate the tab with the cache
+ buddyListReceived(tabSupervisor->getUserListManager()->getBuddyList().values());
+ ignoreListReceived(tabSupervisor->getUserListManager()->getIgnoreList().values());
+
+ PendingCommand *pend = AbstractClient::prepareSessionCommand(Command_ListUsers());
+ connect(pend, &PendingCommand::finished, this, &TabAccount::processListUsersResponse);
client->sendCommand(pend);
- QVBoxLayout *vbox = new QVBoxLayout;
+ auto *vbox = new QVBoxLayout;
vbox->addWidget(userInfoBox);
vbox->addWidget(allUsersList);
- QHBoxLayout *addToBuddyList = new QHBoxLayout;
+ auto *addToBuddyList = new QHBoxLayout;
addBuddyEdit = new LineEditUnfocusable;
addBuddyEdit->setMaxLength(MAX_NAME_LENGTH);
addBuddyEdit->setPlaceholderText(tr("Add to Buddy List"));
- connect(addBuddyEdit, SIGNAL(returnPressed()), this, SLOT(addToBuddyList()));
- QPushButton *addBuddyButton = new QPushButton("Add");
- connect(addBuddyButton, SIGNAL(clicked()), this, SLOT(addToBuddyList()));
+ connect(addBuddyEdit, &LineEditUnfocusable::returnPressed, this, &TabAccount::addToBuddyList);
+ auto *addBuddyButton = new QPushButton("Add");
+ connect(addBuddyButton, &QPushButton::clicked, this, &TabAccount::addToBuddyList);
addToBuddyList->addWidget(addBuddyEdit);
addToBuddyList->addWidget(addBuddyButton);
- QHBoxLayout *addToIgnoreList = new QHBoxLayout;
+ auto *addToIgnoreList = new QHBoxLayout;
addIgnoreEdit = new LineEditUnfocusable;
addIgnoreEdit->setMaxLength(MAX_NAME_LENGTH);
addIgnoreEdit->setPlaceholderText(tr("Add to Ignore List"));
- connect(addIgnoreEdit, SIGNAL(returnPressed()), this, SLOT(addToIgnoreList()));
- QPushButton *addIgnoreButton = new QPushButton("Add");
- connect(addIgnoreButton, SIGNAL(clicked()), this, SLOT(addToIgnoreList()));
+ connect(addIgnoreEdit, &LineEditUnfocusable::returnPressed, this, &TabAccount::addToIgnoreList);
+ auto *addIgnoreButton = new QPushButton("Add");
+ connect(addIgnoreButton, &QPushButton::clicked, this, &TabAccount::addToIgnoreList);
addToIgnoreList->addWidget(addIgnoreEdit);
addToIgnoreList->addWidget(addIgnoreButton);
- QVBoxLayout *buddyPanel = new QVBoxLayout;
+ auto *buddyPanel = new QVBoxLayout;
buddyPanel->addWidget(buddyList);
buddyPanel->addLayout(addToBuddyList);
- QVBoxLayout *ignorePanel = new QVBoxLayout;
+ auto *ignorePanel = new QVBoxLayout;
ignorePanel->addWidget(ignoreList);
ignorePanel->addLayout(addToIgnoreList);
- QHBoxLayout *mainLayout = new QHBoxLayout;
+ auto *mainLayout = new QHBoxLayout;
mainLayout->addLayout(buddyPanel);
mainLayout->addLayout(ignorePanel);
mainLayout->addLayout(vbox);
retranslateUi();
- QWidget *mainWidget = new QWidget(this);
+ auto *mainWidget = new QWidget(this);
mainWidget->setLayout(mainLayout);
setCentralWidget(mainWidget);
}
-void TabUserLists::addToBuddyList()
+void TabAccount::addToBuddyList()
{
- QString userName = addBuddyEdit->text();
- if (userName.length() < 1)
+ const QString &userName = addBuddyEdit->text();
+ if (userName.isEmpty()) {
return;
+ }
- std::string listName = "buddy";
+ const std::string listName = "buddy";
addToList(listName, userName);
addBuddyEdit->clear();
}
-void TabUserLists::addToIgnoreList()
+void TabAccount::addToIgnoreList()
{
- QString userName = addIgnoreEdit->text();
- if (userName.length() < 1)
+ const QString &userName = addIgnoreEdit->text();
+ if (userName.isEmpty()) {
return;
+ }
- std::string listName = "ignore";
+ const std::string listName = "ignore";
addToList(listName, userName);
addIgnoreEdit->clear();
}
-void TabUserLists::addToList(const std::string &listName, const QString &userName)
+void TabAccount::addToList(const std::string &listName, const QString &userName)
{
Command_AddToList cmd;
cmd.set_list(listName);
cmd.set_user_name(userName.toStdString());
- client->sendCommand(client->prepareSessionCommand(cmd));
+ client->sendCommand(AbstractClient::prepareSessionCommand(cmd));
}
-void TabUserLists::retranslateUi()
+void TabAccount::retranslateUi()
{
allUsersList->retranslateUi();
buddyList->retranslateUi();
@@ -138,14 +132,12 @@ void TabUserLists::retranslateUi()
userInfoBox->retranslateUi();
}
-void TabUserLists::processListUsersResponse(const Response &response)
+void TabAccount::processListUsersResponse(const Response &response)
{
const Response_ListUsers &resp = response.GetExtension(Response_ListUsers::ext);
-
- const int userListSize = resp.user_list_size();
- for (int i = 0; i < userListSize; ++i) {
+ for (int i = 0; i < resp.user_list_size(); ++i) {
const ServerInfo_User &info = resp.user_list(i);
- const QString userName = QString::fromStdString(info.name());
+ const QString &userName = QString::fromStdString(info.name());
allUsersList->processUserInfo(info, true);
ignoreList->setUserOnline(userName, true);
buddyList->setUserOnline(userName, true);
@@ -156,10 +148,10 @@ void TabUserLists::processListUsersResponse(const Response &response)
buddyList->sortItems();
}
-void TabUserLists::processUserJoinedEvent(const Event_UserJoined &event)
+void TabAccount::processUserJoinedEvent(const Event_UserJoined &event)
{
const ServerInfo_User &info = event.user_info();
- const QString userName = QString::fromStdString(info.name());
+ const QString &userName = QString::fromStdString(info.name());
allUsersList->processUserInfo(info, true);
ignoreList->setUserOnline(userName, true);
@@ -169,18 +161,20 @@ void TabUserLists::processUserJoinedEvent(const Event_UserJoined &event)
ignoreList->sortItems();
buddyList->sortItems();
- if (buddyList->getUsers().keys().contains(userName))
+ if (buddyList->getUsers().keys().contains(userName)) {
soundEngine->playSound("buddy_join");
+ }
emit userJoined(info);
}
-void TabUserLists::processUserLeftEvent(const Event_UserLeft &event)
+void TabAccount::processUserLeftEvent(const Event_UserLeft &event)
{
- QString userName = QString::fromStdString(event.name());
+ const QString &userName = QString::fromStdString(event.name());
- if (buddyList->getUsers().keys().contains(userName))
+ if (buddyList->getUsers().keys().contains(userName)) {
soundEngine->playSound("buddy_leave");
+ }
if (allUsersList->deleteUser(userName)) {
ignoreList->setUserOnline(userName, false);
@@ -192,47 +186,54 @@ void TabUserLists::processUserLeftEvent(const Event_UserLeft &event)
}
}
-void TabUserLists::buddyListReceived(const QList &_buddyList)
+void TabAccount::buddyListReceived(const QList &_buddyList)
{
- for (int i = 0; i < _buddyList.size(); ++i)
- buddyList->processUserInfo(_buddyList[i], false);
+ for (const auto &user : _buddyList) {
+ buddyList->processUserInfo(user, false);
+ }
buddyList->sortItems();
}
-void TabUserLists::ignoreListReceived(const QList &_ignoreList)
+void TabAccount::ignoreListReceived(const QList &_ignoreList)
{
- for (int i = 0; i < _ignoreList.size(); ++i)
- ignoreList->processUserInfo(_ignoreList[i], false);
+ for (const auto &user : _ignoreList) {
+ ignoreList->processUserInfo(user, false);
+ }
ignoreList->sortItems();
}
-void TabUserLists::processAddToListEvent(const Event_AddToList &event)
+void TabAccount::processAddToListEvent(const Event_AddToList &event)
{
const ServerInfo_User &info = event.user_info();
- bool online = allUsersList->getUsers().contains(QString::fromStdString(info.name()));
- QString list = QString::fromStdString(event.list_name());
- UserList *userList = 0;
- if (list == "buddy")
+ const bool online = allUsersList->getUsers().contains(QString::fromStdString(info.name()));
+ const QString &list = QString::fromStdString(event.list_name());
+
+ UserListWidget *userList;
+ if (list == "buddy") {
userList = buddyList;
- else if (list == "ignore")
+ } else if (list == "ignore") {
userList = ignoreList;
- if (!userList)
+ } else {
return;
+ }
userList->processUserInfo(info, online);
userList->sortItems();
}
-void TabUserLists::processRemoveFromListEvent(const Event_RemoveFromList &event)
+void TabAccount::processRemoveFromListEvent(const Event_RemoveFromList &event)
{
- QString list = QString::fromStdString(event.list_name());
- QString user = QString::fromStdString(event.user_name());
- UserList *userList = 0;
- if (list == "buddy")
+ const auto &list = QString::fromStdString(event.list_name());
+ const auto &user = QString::fromStdString(event.user_name());
+
+ UserListWidget *userList;
+ if (list == "buddy") {
userList = buddyList;
- else if (list == "ignore")
+ } else if (list == "ignore") {
userList = ignoreList;
- if (!userList)
+ } else {
return;
+ }
+
userList->deleteUser(user);
}
diff --git a/cockatrice/src/client/tabs/tab_account.h b/cockatrice/src/client/tabs/tab_account.h
index ba6eb78ad..5824a7527 100644
--- a/cockatrice/src/client/tabs/tab_account.h
+++ b/cockatrice/src/client/tabs/tab_account.h
@@ -5,25 +5,25 @@
#include "tab.h"
class AbstractClient;
-class UserList;
-class UserInfoBox;
-class LineEditUnfocusable;
-
+class Event_AddToList;
class Event_ListRooms;
+class Event_RemoveFromList;
class Event_UserJoined;
class Event_UserLeft;
+class LineEditUnfocusable;
class Response;
class ServerInfo_User;
-class Event_AddToList;
-class Event_RemoveFromList;
+class UserInfoBox;
+class UserListWidget;
-class TabUserLists : public Tab
+class TabAccount : public Tab
{
Q_OBJECT
signals:
void openMessageDialog(const QString &userName, bool focus);
void userLeft(const QString &userName);
void userJoined(const ServerInfo_User &userInfo);
+
private slots:
void processListUsersResponse(const Response &response);
void processUserJoinedEvent(const Event_UserJoined &event);
@@ -37,36 +37,21 @@ private slots:
private:
AbstractClient *client;
- UserList *allUsersList;
- UserList *buddyList;
- UserList *ignoreList;
+ UserListWidget *allUsersList;
+ UserListWidget *buddyList;
+ UserListWidget *ignoreList;
UserInfoBox *userInfoBox;
LineEditUnfocusable *addBuddyEdit;
LineEditUnfocusable *addIgnoreEdit;
void addToList(const std::string &listName, const QString &userName);
public:
- TabUserLists(TabSupervisor *_tabSupervisor,
- AbstractClient *_client,
- const ServerInfo_User &userInfo,
- QWidget *parent = nullptr);
- void retranslateUi();
- QString getTabText() const
+ explicit TabAccount(TabSupervisor *_tabSupervisor, AbstractClient *_client, const ServerInfo_User &userInfo);
+ void retranslateUi() override;
+ [[nodiscard]] QString getTabText() const override
{
return tr("Account");
}
- const UserList *getAllUsersList() const
- {
- return allUsersList;
- }
- const UserList *getBuddyList() const
- {
- return buddyList;
- }
- const UserList *getIgnoreList() const
- {
- return ignoreList;
- }
};
#endif
diff --git a/cockatrice/src/client/tabs/tab_admin.cpp b/cockatrice/src/client/tabs/tab_admin.cpp
index 07031c1cc..5a8ace0c6 100644
--- a/cockatrice/src/client/tabs/tab_admin.cpp
+++ b/cockatrice/src/client/tabs/tab_admin.cpp
@@ -1,19 +1,20 @@
#include "tab_admin.h"
+#include "../../server/pending_command.h"
#include "../game_logic/abstract_client.h"
#include "pb/admin_commands.pb.h"
+#include "pb/event_replay_added.pb.h"
+#include "pb/moderator_commands.pb.h"
#include "trice_limits.h"
#include
#include
#include
-#include
#include
#include
#include
#include
#include
-#include
ShutdownDialog::ShutdownDialog(QWidget *parent) : QDialog(parent)
{
@@ -29,8 +30,8 @@ ShutdownDialog::ShutdownDialog(QWidget *parent) : QDialog(parent)
minutesEdit->setMaximum(999);
QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
- connect(buttonBox, SIGNAL(accepted()), this, SLOT(accept()));
- connect(buttonBox, SIGNAL(rejected()), this, SLOT(reject()));
+ connect(buttonBox, &QDialogButtonBox::accepted, this, &ShutdownDialog::accept);
+ connect(buttonBox, &QDialogButtonBox::rejected, this, &ShutdownDialog::reject);
QGridLayout *mainLayout = new QGridLayout;
mainLayout->addWidget(reasonLabel, 0, 0);
@@ -53,34 +54,66 @@ int ShutdownDialog::getMinutes() const
return minutesEdit->value();
}
-TabAdmin::TabAdmin(TabSupervisor *_tabSupervisor, AbstractClient *_client, bool _fullAdmin, QWidget *parent)
- : Tab(_tabSupervisor, parent), locked(true), client(_client), fullAdmin(_fullAdmin)
+TabAdmin::TabAdmin(TabSupervisor *_tabSupervisor, AbstractClient *_client, bool _fullAdmin)
+ : Tab(_tabSupervisor), locked(true), client(_client), fullAdmin(_fullAdmin)
{
updateServerMessageButton = new QPushButton;
- connect(updateServerMessageButton, SIGNAL(clicked()), this, SLOT(actUpdateServerMessage()));
+ connect(updateServerMessageButton, &QPushButton::clicked, this, &TabAdmin::actUpdateServerMessage);
shutdownServerButton = new QPushButton;
- connect(shutdownServerButton, SIGNAL(clicked()), this, SLOT(actShutdownServer()));
+ connect(shutdownServerButton, &QPushButton::clicked, this, &TabAdmin::actShutdownServer);
reloadConfigButton = new QPushButton;
- connect(reloadConfigButton, SIGNAL(clicked()), this, SLOT(actReloadConfig()));
+ connect(reloadConfigButton, &QPushButton::clicked, this, &TabAdmin::actReloadConfig);
- QVBoxLayout *vbox = new QVBoxLayout;
- vbox->addWidget(updateServerMessageButton);
- vbox->addWidget(shutdownServerButton);
- vbox->addWidget(reloadConfigButton);
- vbox->addStretch();
+ grantReplayAccessButton = new QPushButton;
+ grantReplayAccessButton->setEnabled(false);
+ connect(grantReplayAccessButton, &QPushButton::clicked, this, &TabAdmin::actGrantReplayAccess);
+ replayIdToGrant = new QLineEdit;
+ replayIdToGrant->setMaximumWidth(500);
+ replayIdToGrant->setValidator(new QIntValidator(0, INT_MAX, this));
+ connect(replayIdToGrant, &QLineEdit::textChanged, this,
+ [=, this]() { grantReplayAccessButton->setEnabled(!replayIdToGrant->text().isEmpty()); });
+ auto *grandReplayAccessLayout = new QGridLayout(this);
+ grandReplayAccessLayout->addWidget(replayIdToGrant, 0, 0);
+ grandReplayAccessLayout->addWidget(grantReplayAccessButton, 0, 1);
+
+ activateUserButton = new QPushButton;
+ activateUserButton->setEnabled(false);
+ connect(activateUserButton, &QPushButton::clicked, this, &TabAdmin::actForceActivateUser);
+ userToActivate = new QLineEdit;
+ userToActivate->setMaximumWidth(500);
+ connect(userToActivate, &QLineEdit::textChanged, this,
+ [=, this]() { activateUserButton->setEnabled(!userToActivate->text().isEmpty()); });
+ auto *activateUserLayout = new QGridLayout(this);
+ activateUserLayout->addWidget(userToActivate, 0, 0);
+ activateUserLayout->addWidget(activateUserButton, 0, 1);
+
+ auto *adminVBox = new QVBoxLayout;
+ adminVBox->addWidget(updateServerMessageButton);
+ adminVBox->addWidget(shutdownServerButton);
+ adminVBox->addWidget(reloadConfigButton);
adminGroupBox = new QGroupBox;
- adminGroupBox->setLayout(vbox);
+ adminGroupBox->setLayout(adminVBox);
adminGroupBox->setEnabled(false);
+ auto *moderatorVBox = new QVBoxLayout;
+ moderatorVBox->addLayout(grandReplayAccessLayout);
+ moderatorVBox->addLayout(activateUserLayout);
+
+ moderatorGroupBox = new QGroupBox;
+ moderatorGroupBox->setLayout(moderatorVBox);
+ moderatorGroupBox->setEnabled(false);
+
unlockButton = new QPushButton;
- connect(unlockButton, SIGNAL(clicked()), this, SLOT(actUnlock()));
+ connect(unlockButton, &QPushButton::clicked, this, &TabAdmin::actUnlock);
lockButton = new QPushButton;
lockButton->setEnabled(false);
- connect(lockButton, SIGNAL(clicked()), this, SLOT(actLock()));
+ connect(lockButton, &QPushButton::clicked, this, &TabAdmin::actLock);
QVBoxLayout *mainLayout = new QVBoxLayout;
mainLayout->addWidget(adminGroupBox);
+ mainLayout->addWidget(moderatorGroupBox);
+ mainLayout->addStretch();
mainLayout->addWidget(unlockButton);
mainLayout->addWidget(lockButton);
@@ -99,6 +132,13 @@ void TabAdmin::retranslateUi()
shutdownServerButton->setText(tr("&Shut down server"));
reloadConfigButton->setText(tr("&Reload configuration"));
adminGroupBox->setTitle(tr("Server administration functions"));
+ moderatorGroupBox->setTitle(tr("Server moderator functions"));
+
+ replayIdToGrant->setPlaceholderText(tr("Replay ID"));
+ grantReplayAccessButton->setText(tr("Grant Replay Access"));
+
+ userToActivate->setPlaceholderText(tr("Username to Activate"));
+ activateUserButton->setText(tr("Force Activate User"));
unlockButton->setText(tr("&Unlock functions"));
lockButton->setText(tr("&Lock functions"));
@@ -117,7 +157,7 @@ void TabAdmin::actShutdownServer()
cmd.set_reason(dlg.getReason().toStdString());
cmd.set_minutes(dlg.getMinutes());
- client->sendCommand(client->prepareAdminCommand(cmd));
+ client->sendCommand(AbstractClient::prepareAdminCommand(cmd));
}
}
@@ -127,22 +167,98 @@ void TabAdmin::actReloadConfig()
client->sendCommand(client->prepareAdminCommand(cmd));
}
+void TabAdmin::actGrantReplayAccess()
+{
+ if (!replayIdToGrant) {
+ return;
+ }
+
+ Command_GrantReplayAccess cmd;
+ cmd.set_replay_id(replayIdToGrant->text().toUInt());
+ cmd.set_moderator_name(client->getUserName().toStdString());
+
+ auto *pend = client->prepareModeratorCommand(cmd);
+ connect(pend, &PendingCommand::finished, this, &TabAdmin::grantReplayAccessProcessResponse);
+ client->sendCommand(pend);
+}
+
+void TabAdmin::actForceActivateUser()
+{
+ if (!userToActivate) {
+ return;
+ }
+
+ Command_ForceActivateUser cmd;
+ cmd.set_username_to_activate(userToActivate->text().trimmed().toStdString());
+ cmd.set_moderator_name(client->getUserName().toStdString());
+
+ auto *pend = client->prepareModeratorCommand(cmd);
+ connect(pend,
+ QOverload::of(&PendingCommand::finished),
+ this, &TabAdmin::activateUserProcessResponse);
+ client->sendCommand(pend);
+}
+
+void TabAdmin::grantReplayAccessProcessResponse(const Response &response)
+{
+ auto *event = new Event_ReplayAdded();
+
+ switch (response.response_code()) {
+ case Response::RespOk:
+ client->replayAddedEventReceived(*event);
+ QMessageBox::information(this, tr("Success"), tr("Replay access granted"));
+ break;
+ case Response::RespContextError:
+ QMessageBox::critical(this, tr("Error"), tr("Unable to grant replay access. Replay ID invalid"));
+ break;
+ default:
+ QMessageBox::critical(this, tr("Error"), tr("Unable to grant replay access. Internal error"));
+ break;
+ }
+}
+
+void TabAdmin::activateUserProcessResponse(const Response &response)
+{
+ switch (response.response_code()) {
+ case Response::RespActivationAccepted:
+ QMessageBox::information(this, tr("Success"), tr("User successfully activated"));
+ break;
+ case Response::RespNameNotFound:
+ QMessageBox::critical(this, tr("Error"), tr("Unable to activate user. Username invalid"));
+ break;
+ case Response::RespActivationFailed:
+ QMessageBox::critical(this, tr("Error"), tr("Unable to activate user. User already active"));
+ break;
+ default:
+ QMessageBox::critical(this, tr("Error"), tr("Unable to activate user. Internal error"));
+ break;
+ }
+}
+
void TabAdmin::actUnlock()
{
- if (fullAdmin)
+ if (fullAdmin) {
adminGroupBox->setEnabled(true);
+ }
+
+ moderatorGroupBox->setEnabled(true);
lockButton->setEnabled(true);
unlockButton->setEnabled(false);
locked = false;
+
emit adminLockChanged(false);
}
void TabAdmin::actLock()
{
- if (fullAdmin)
+ if (fullAdmin) {
adminGroupBox->setEnabled(false);
+ }
+
+ moderatorGroupBox->setEnabled(false);
lockButton->setEnabled(false);
unlockButton->setEnabled(true);
locked = true;
+
emit adminLockChanged(true);
}
diff --git a/cockatrice/src/client/tabs/tab_admin.h b/cockatrice/src/client/tabs/tab_admin.h
index 650e881a9..e219aa627 100644
--- a/cockatrice/src/client/tabs/tab_admin.h
+++ b/cockatrice/src/client/tabs/tab_admin.h
@@ -1,6 +1,8 @@
#ifndef TAB_ADMIN_H
#define TAB_ADMIN_H
+#include "pb/commands.pb.h"
+#include "pb/response.pb.h"
#include "tab.h"
#include
@@ -20,7 +22,7 @@ private:
QSpinBox *minutesEdit;
public:
- ShutdownDialog(QWidget *parent = nullptr);
+ explicit ShutdownDialog(QWidget *parent = nullptr);
QString getReason() const;
int getMinutes() const;
};
@@ -32,23 +34,29 @@ private:
bool locked;
AbstractClient *client;
bool fullAdmin;
- QPushButton *updateServerMessageButton, *shutdownServerButton, *reloadConfigButton;
- QGroupBox *adminGroupBox;
+ QPushButton *updateServerMessageButton, *shutdownServerButton, *reloadConfigButton, *grantReplayAccessButton,
+ *activateUserButton;
+ QGroupBox *adminGroupBox, *moderatorGroupBox;
QPushButton *unlockButton, *lockButton;
+ QLineEdit *replayIdToGrant, *userToActivate;
signals:
void adminLockChanged(bool lock);
private slots:
void actUpdateServerMessage();
void actShutdownServer();
void actReloadConfig();
+ void actGrantReplayAccess();
+ void actForceActivateUser();
+ void grantReplayAccessProcessResponse(const Response &response);
+ void activateUserProcessResponse(const Response &response);
void actUnlock();
void actLock();
public:
- TabAdmin(TabSupervisor *_tabSupervisor, AbstractClient *_client, bool _fullAdmin, QWidget *parent = nullptr);
- void retranslateUi();
- QString getTabText() const
+ TabAdmin(TabSupervisor *_tabSupervisor, AbstractClient *_client, bool _fullAdmin);
+ void retranslateUi() override;
+ QString getTabText() const override
{
return tr("Administration");
}
diff --git a/cockatrice/src/client/tabs/tab_deck_editor.cpp b/cockatrice/src/client/tabs/tab_deck_editor.cpp
index 1e2284186..dbe38a215 100644
--- a/cockatrice/src/client/tabs/tab_deck_editor.cpp
+++ b/cockatrice/src/client/tabs/tab_deck_editor.cpp
@@ -3,345 +3,59 @@
#include "../../client/game_logic/abstract_client.h"
#include "../../client/tapped_out_interface.h"
#include "../../client/ui/widgets/cards/card_info_frame_widget.h"
-#include "../../deck/deck_list_model.h"
-#include "../../deck/deck_stats_interface.h"
+#include "../../dialogs/dlg_load_deck.h"
#include "../../dialogs/dlg_load_deck_from_clipboard.h"
#include "../../game/cards/card_database_manager.h"
#include "../../game/cards/card_database_model.h"
#include "../../game/filters/filter_builder.h"
#include "../../game/filters/filter_tree_model.h"
-#include "../../main.h"
#include "../../server/pending_command.h"
#include "../../settings/cache_settings.h"
-#include "../ui/picture_loader.h"
+#include "../menus/deck_editor/deck_editor_menu.h"
#include "../ui/pixel_map_generator.h"
-#include "pb/command_deck_upload.pb.h"
-#include "pb/response.pb.h"
+#include "../ui/widgets/deck_editor/deck_editor_filter_dock_widget.h"
#include "tab_supervisor.h"
#include "trice_limits.h"
#include
#include
-#include
#include
-#include
+#include
+#include
#include
#include
#include
#include
#include
#include
-#include
#include
#include
#include
#include
-#include
-#include
#include
-#include
#include
-#include
#include
-#include
#include
#include
-#include
#include
-void SearchLineEdit::keyPressEvent(QKeyEvent *event)
+TabDeckEditor::TabDeckEditor(TabSupervisor *_tabSupervisor) : AbstractTabDeckEditor(_tabSupervisor)
{
- // List of key events that must be handled by the card list instead of the search box
- static const QVector forwardToTreeView = {Qt::Key_Up, Qt::Key_Down, Qt::Key_PageDown, Qt::Key_PageUp};
- // forward only if the search text is empty
- static const QVector forwardWhenEmpty = {Qt::Key_Home, Qt::Key_End};
- Qt::Key key = static_cast(event->key());
- if (treeView) {
- if (forwardToTreeView.contains(key))
- QCoreApplication::sendEvent(treeView, event);
- if (text().isEmpty() && forwardWhenEmpty.contains(key))
- QCoreApplication::sendEvent(treeView, event);
- }
- LineEditUnfocusable::keyPressEvent(event);
-}
+ setObjectName("TabDeckEditor");
-void TabDeckEditor::createDeckDock()
-{
- deckModel = new DeckListModel(this);
- deckModel->setObjectName("deckModel");
- connect(deckModel, SIGNAL(deckHashChanged()), this, SLOT(updateHash()));
- deckView = new QTreeView();
- deckView->setObjectName("deckView");
- deckView->setModel(deckModel);
- deckView->setUniformRowHeights(true);
- deckView->setSortingEnabled(true);
- deckView->sortByColumn(1, Qt::AscendingOrder);
- deckView->header()->setSectionResizeMode(QHeaderView::ResizeToContents);
- deckView->installEventFilter(&deckViewKeySignals);
- connect(deckView->selectionModel(), SIGNAL(currentRowChanged(const QModelIndex &, const QModelIndex &)), this,
- SLOT(updateCardInfoRight(const QModelIndex &, const QModelIndex &)));
- connect(deckView, SIGNAL(doubleClicked(const QModelIndex &)), this, SLOT(actSwapCard()));
- connect(&deckViewKeySignals, SIGNAL(onShiftS()), this, SLOT(actSwapCard()));
- connect(&deckViewKeySignals, SIGNAL(onEnter()), this, SLOT(actIncrement()));
- connect(&deckViewKeySignals, SIGNAL(onCtrlAltEqual()), this, SLOT(actIncrement()));
- connect(&deckViewKeySignals, SIGNAL(onCtrlAltMinus()), this, SLOT(actDecrement()));
- connect(&deckViewKeySignals, SIGNAL(onShiftRight()), this, SLOT(actIncrement()));
- connect(&deckViewKeySignals, SIGNAL(onShiftLeft()), this, SLOT(actDecrement()));
- connect(&deckViewKeySignals, SIGNAL(onDelete()), this, SLOT(actRemoveCard()));
+ TabDeckEditor::createMenus();
- nameLabel = new QLabel();
- nameLabel->setObjectName("nameLabel");
- nameEdit = new LineEditUnfocusable;
- nameEdit->setMaxLength(MAX_NAME_LENGTH);
- nameEdit->setObjectName("nameEdit");
- nameLabel->setBuddy(nameEdit);
- connect(nameEdit, SIGNAL(textChanged(const QString &)), this, SLOT(updateName(const QString &)));
- commentsLabel = new QLabel();
- commentsLabel->setObjectName("commentsLabel");
- commentsEdit = new QTextEdit;
- commentsEdit->setAcceptRichText(false);
- commentsEdit->setMinimumHeight(nameEdit->minimumSizeHint().height());
- commentsEdit->setObjectName("commentsEdit");
- commentsLabel->setBuddy(commentsEdit);
- connect(commentsEdit, SIGNAL(textChanged()), this, SLOT(updateComments()));
+ installEventFilter(this);
- aIncrement = new QAction(QString(), this);
- aIncrement->setIcon(QPixmap("theme:icons/increment"));
- connect(aIncrement, SIGNAL(triggered()), this, SLOT(actIncrement()));
- auto *tbIncrement = new QToolButton(this);
- tbIncrement->setDefaultAction(aIncrement);
+ TabDeckEditor::retranslateUi();
+ TabDeckEditor::refreshShortcuts();
- aDecrement = new QAction(QString(), this);
- aDecrement->setIcon(QPixmap("theme:icons/decrement"));
- connect(aDecrement, SIGNAL(triggered()), this, SLOT(actDecrement()));
- auto *tbDecrement = new QToolButton(this);
- tbDecrement->setDefaultAction(aDecrement);
-
- aRemoveCard = new QAction(QString(), this);
- aRemoveCard->setIcon(QPixmap("theme:icons/remove_row"));
- connect(aRemoveCard, SIGNAL(triggered()), this, SLOT(actRemoveCard()));
- auto *tbRemoveCard = new QToolButton(this);
- tbRemoveCard->setDefaultAction(aRemoveCard);
-
- auto *upperLayout = new QGridLayout;
- upperLayout->setObjectName("upperLayout");
- upperLayout->addWidget(nameLabel, 0, 0);
- upperLayout->addWidget(nameEdit, 0, 1);
-
- upperLayout->addWidget(commentsLabel, 1, 0);
- upperLayout->addWidget(commentsEdit, 1, 1);
-
- hashLabel1 = new QLabel();
- hashLabel1->setObjectName("hashLabel1");
- auto *hashSizePolicy = new QSizePolicy();
- hashSizePolicy->setHorizontalPolicy(QSizePolicy::Fixed);
- hashLabel1->setSizePolicy(*hashSizePolicy);
- hashLabel = new LineEditUnfocusable;
- hashLabel->setObjectName("hashLabel");
- hashLabel->setReadOnly(true);
- hashLabel->setFrame(false);
-
- auto *lowerLayout = new QGridLayout;
- lowerLayout->setObjectName("lowerLayout");
- lowerLayout->addWidget(hashLabel1, 0, 0);
- lowerLayout->addWidget(hashLabel, 0, 1);
- lowerLayout->addWidget(tbIncrement, 0, 2);
- lowerLayout->addWidget(tbDecrement, 0, 3);
- lowerLayout->addWidget(tbRemoveCard, 0, 4);
- lowerLayout->addWidget(deckView, 1, 0, 1, 5);
-
- // Create widgets for both layouts to make splitter work correctly
- auto *topWidget = new QWidget;
- topWidget->setLayout(upperLayout);
- auto *bottomWidget = new QWidget;
- bottomWidget->setLayout(lowerLayout);
-
- auto *split = new QSplitter;
- split->setObjectName("deckSplitter");
- split->setOrientation(Qt::Vertical);
- split->setChildrenCollapsible(true);
- split->addWidget(topWidget);
- split->addWidget(bottomWidget);
- split->setStretchFactor(0, 1);
- split->setStretchFactor(1, 4);
-
- auto *rightFrame = new QVBoxLayout;
- rightFrame->setObjectName("rightFrame");
- rightFrame->addWidget(split);
-
- deckDock = new QDockWidget(this);
- deckDock->setObjectName("deckDock");
-
- deckDock->setMinimumSize(QSize(200, 41));
- deckDock->setAllowedAreas(Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea);
- deckDock->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetFloatable |
- QDockWidget::DockWidgetMovable);
- auto *deckDockContents = new QWidget();
- deckDockContents->setObjectName("deckDockContents");
- deckDockContents->setLayout(rightFrame);
- deckDock->setWidget(deckDockContents);
-
- deckDock->installEventFilter(this);
- connect(deckDock, SIGNAL(topLevelChanged(bool)), this, SLOT(dockTopLevelChanged(bool)));
-}
-
-void TabDeckEditor::createCardInfoDock()
-{
- cardInfo = new CardInfoFrameWidget();
- cardInfo->setObjectName("cardInfo");
- auto *cardInfoFrame = new QVBoxLayout;
- cardInfoFrame->setObjectName("cardInfoFrame");
- cardInfoFrame->addWidget(cardInfo);
-
- cardInfoDock = new QDockWidget(this);
- cardInfoDock->setObjectName("cardInfoDock");
-
- cardInfoDock->setMinimumSize(QSize(200, 41));
- cardInfoDock->setAllowedAreas(Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea);
- cardInfoDock->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetFloatable |
- QDockWidget::DockWidgetMovable);
- auto *cardInfoDockContents = new QWidget();
- cardInfoDockContents->setObjectName("cardInfoDockContents");
- cardInfoDockContents->setLayout(cardInfoFrame);
- cardInfoDock->setWidget(cardInfoDockContents);
-
- cardInfoDock->installEventFilter(this);
- connect(cardInfoDock, SIGNAL(topLevelChanged(bool)), this, SLOT(dockTopLevelChanged(bool)));
-}
-
-void TabDeckEditor::createFiltersDock()
-{
- filterModel = new FilterTreeModel();
- filterModel->setObjectName("filterModel");
- databaseDisplayModel->setFilterTree(filterModel->filterTree());
- databaseDisplayModel->setObjectName("databaseDisplayModel");
- filterView = new QTreeView;
- filterView->setObjectName("filterView");
- filterView->setModel(filterModel);
- filterView->setUniformRowHeights(true);
- filterView->setHeaderHidden(true);
- filterView->setContextMenuPolicy(Qt::CustomContextMenu);
- filterView->installEventFilter(&filterViewKeySignals);
- connect(filterModel, SIGNAL(layoutChanged()), filterView, SLOT(expandAll()));
- connect(filterView, SIGNAL(customContextMenuRequested(const QPoint &)), this,
- SLOT(filterViewCustomContextMenu(const QPoint &)));
- connect(&filterViewKeySignals, SIGNAL(onDelete()), this, SLOT(actClearFilterOne()));
-
- auto *filterBuilder = new FilterBuilder;
- filterBuilder->setObjectName("filterBuilder");
- connect(filterBuilder, SIGNAL(add(const CardFilter *)), filterModel, SLOT(addFilter(const CardFilter *)));
-
- auto *filterDelOne = new QToolButton();
- filterDelOne->setObjectName("filterDelOne");
- filterDelOne->setDefaultAction(aClearFilterOne);
- filterDelOne->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
-
- auto *filterDelAll = new QToolButton();
- filterDelAll->setObjectName("filterDelAll");
- filterDelAll->setDefaultAction(aClearFilterAll);
- filterDelAll->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
-
- auto *filterLayout = new QGridLayout;
- filterLayout->setObjectName("filterLayout");
- filterLayout->setContentsMargins(0, 0, 0, 0);
- filterLayout->addWidget(filterBuilder, 0, 0, 1, 3);
- filterLayout->addWidget(filterView, 1, 0, 1, 3);
- filterLayout->addWidget(filterDelOne, 2, 0, 1, 1);
- filterLayout->addWidget(filterDelAll, 2, 2, 1, 1);
-
- filterBox = new QWidget();
- filterBox->setObjectName("filterBox");
- filterBox->setLayout(filterLayout);
-
- auto *filterFrame = new QVBoxLayout;
- filterFrame->setObjectName("filterFrame");
- filterFrame->addWidget(filterBox);
-
- filterDock = new QDockWidget(this);
- filterDock->setObjectName("filterDock");
-
- filterDock->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetFloatable |
- QDockWidget::DockWidgetMovable);
- auto *filterDockContents = new QWidget(this);
- filterDockContents->setObjectName("filterDockContents");
- filterDockContents->setLayout(filterFrame);
- filterDock->setWidget(filterDockContents);
-
- filterDock->installEventFilter(this);
- connect(filterDock, SIGNAL(topLevelChanged(bool)), this, SLOT(dockTopLevelChanged(bool)));
+ TabDeckEditor::loadLayout();
}
void TabDeckEditor::createMenus()
{
- aNewDeck = new QAction(QString(), this);
- connect(aNewDeck, SIGNAL(triggered()), this, SLOT(actNewDeck()));
-
- aLoadDeck = new QAction(QString(), this);
- connect(aLoadDeck, SIGNAL(triggered()), this, SLOT(actLoadDeck()));
-
- aSaveDeck = new QAction(QString(), this);
- connect(aSaveDeck, SIGNAL(triggered()), this, SLOT(actSaveDeck()));
-
- aSaveDeckAs = new QAction(QString(), this);
- connect(aSaveDeckAs, SIGNAL(triggered()), this, SLOT(actSaveDeckAs()));
-
- aLoadDeckFromClipboard = new QAction(QString(), this);
- connect(aLoadDeckFromClipboard, SIGNAL(triggered()), this, SLOT(actLoadDeckFromClipboard()));
-
- aSaveDeckToClipboard = new QAction(QString(), this);
- connect(aSaveDeckToClipboard, SIGNAL(triggered()), this, SLOT(actSaveDeckToClipboard()));
-
- aSaveDeckToClipboardRaw = new QAction(QString(), this);
- connect(aSaveDeckToClipboardRaw, SIGNAL(triggered()), this, SLOT(actSaveDeckToClipboardRaw()));
-
- aPrintDeck = new QAction(QString(), this);
- connect(aPrintDeck, SIGNAL(triggered()), this, SLOT(actPrintDeck()));
-
- aExportDeckDecklist = new QAction(QString(), this);
- connect(aExportDeckDecklist, SIGNAL(triggered()), this, SLOT(actExportDeckDecklist()));
-
- aAnalyzeDeckDeckstats = new QAction(QString(), this);
- connect(aAnalyzeDeckDeckstats, SIGNAL(triggered()), this, SLOT(actAnalyzeDeckDeckstats()));
-
- aAnalyzeDeckTappedout = new QAction(QString(), this);
- connect(aAnalyzeDeckTappedout, SIGNAL(triggered()), this, SLOT(actAnalyzeDeckTappedout()));
-
- analyzeDeckMenu = new QMenu(this);
- analyzeDeckMenu->addAction(aExportDeckDecklist);
- analyzeDeckMenu->addAction(aAnalyzeDeckDeckstats);
- analyzeDeckMenu->addAction(aAnalyzeDeckTappedout);
-
- aClose = new QAction(QString(), this);
- connect(aClose, SIGNAL(triggered()), this, SLOT(closeRequest()));
-
- aClearFilterAll = new QAction(QString(), this);
- aClearFilterAll->setIcon(QPixmap("theme:icons/clearsearch"));
- connect(aClearFilterAll, SIGNAL(triggered()), this, SLOT(actClearFilterAll()));
-
- aClearFilterOne = new QAction(QString(), this);
- aClearFilterOne->setIcon(QPixmap("theme:icons/decrement"));
- connect(aClearFilterOne, SIGNAL(triggered()), this, SLOT(actClearFilterOne()));
-
- saveDeckToClipboardMenu = new QMenu(this);
- saveDeckToClipboardMenu->addAction(aSaveDeckToClipboard);
- saveDeckToClipboardMenu->addAction(aSaveDeckToClipboardRaw);
-
- deckMenu = new QMenu(this);
- deckMenu->addAction(aNewDeck);
- deckMenu->addAction(aLoadDeck);
- deckMenu->addAction(aSaveDeck);
- deckMenu->addAction(aSaveDeckAs);
- deckMenu->addSeparator();
- deckMenu->addAction(aLoadDeckFromClipboard);
- deckMenu->addMenu(saveDeckToClipboardMenu);
- deckMenu->addSeparator();
- deckMenu->addAction(aPrintDeck);
- deckMenu->addMenu(analyzeDeckMenu);
- deckMenu->addSeparator();
- deckMenu->addAction(aClearFilterOne);
- deckMenu->addAction(aClearFilterAll);
- deckMenu->addSeparator();
- deckMenu->addAction(aClose);
+ deckMenu = new DeckEditorMenu(this, this);
addTabMenu(deckMenu);
viewMenu = new QMenu(this);
@@ -349,6 +63,7 @@ void TabDeckEditor::createMenus()
cardInfoDockMenu = viewMenu->addMenu(QString());
deckDockMenu = viewMenu->addMenu(QString());
filterDockMenu = viewMenu->addMenu(QString());
+ printingSelectorDockMenu = viewMenu->addMenu(QString());
aCardInfoDockVisible = cardInfoDockMenu->addAction(QString());
aCardInfoDockVisible->setCheckable(true);
@@ -371,315 +86,46 @@ void TabDeckEditor::createMenus()
aFilterDockFloating->setCheckable(true);
connect(aFilterDockFloating, SIGNAL(triggered()), this, SLOT(dockFloatingTriggered()));
+ aPrintingSelectorDockVisible = printingSelectorDockMenu->addAction(QString());
+ aPrintingSelectorDockVisible->setCheckable(true);
+ connect(aPrintingSelectorDockVisible, SIGNAL(triggered()), this, SLOT(dockVisibleTriggered()));
+ aPrintingSelectorDockFloating = printingSelectorDockMenu->addAction(QString());
+ aPrintingSelectorDockFloating->setCheckable(true);
+ connect(aPrintingSelectorDockFloating, SIGNAL(triggered()), this, SLOT(dockFloatingTriggered()));
+
viewMenu->addSeparator();
aResetLayout = viewMenu->addAction(QString());
connect(aResetLayout, SIGNAL(triggered()), this, SLOT(restartLayout()));
viewMenu->addAction(aResetLayout);
- setSaveStatus(false);
+ deckMenu->setSaveStatus(false);
addTabMenu(viewMenu);
}
-void TabDeckEditor::createCentralFrame()
+QString TabDeckEditor::getTabText() const
{
- searchEdit = new SearchLineEdit;
- searchEdit->setObjectName("searchEdit");
- searchEdit->setPlaceholderText(tr("Search by card name (or search expressions)"));
- searchEdit->setClearButtonEnabled(true);
- searchEdit->addAction(loadColorAdjustedPixmap("theme:icons/search"), QLineEdit::LeadingPosition);
- auto help = searchEdit->addAction(QPixmap("theme:icons/info"), QLineEdit::TrailingPosition);
- searchEdit->installEventFilter(&searchKeySignals);
-
- setFocusProxy(searchEdit);
- setFocusPolicy(Qt::ClickFocus);
-
- searchKeySignals.setObjectName("searchKeySignals");
- connect(searchEdit, SIGNAL(textChanged(const QString &)), this, SLOT(updateSearch(const QString &)));
- connect(&searchKeySignals, SIGNAL(onEnter()), this, SLOT(actAddCard()));
- connect(&searchKeySignals, SIGNAL(onCtrlAltEqual()), this, SLOT(actAddCard()));
- connect(&searchKeySignals, SIGNAL(onCtrlAltRBracket()), this, SLOT(actAddCardToSideboard()));
- connect(&searchKeySignals, SIGNAL(onCtrlAltMinus()), this, SLOT(actDecrementCard()));
- connect(&searchKeySignals, SIGNAL(onCtrlAltLBracket()), this, SLOT(actDecrementCardFromSideboard()));
- connect(&searchKeySignals, SIGNAL(onCtrlAltEnter()), this, SLOT(actAddCardToSideboard()));
- connect(&searchKeySignals, SIGNAL(onCtrlEnter()), this, SLOT(actAddCardToSideboard()));
- connect(&searchKeySignals, SIGNAL(onCtrlC()), this, SLOT(copyDatabaseCellContents()));
- connect(help, &QAction::triggered, this, &TabDeckEditor::showSearchSyntaxHelp);
-
- databaseModel = new CardDatabaseModel(CardDatabaseManager::getInstance(), true, this);
- databaseModel->setObjectName("databaseModel");
- databaseDisplayModel = new CardDatabaseDisplayModel(this);
- databaseDisplayModel->setSourceModel(databaseModel);
- databaseDisplayModel->setFilterKeyColumn(0);
-
- databaseView = new QTreeView();
- databaseView->setObjectName("databaseView");
- databaseView->setFocusProxy(searchEdit);
- databaseView->setUniformRowHeights(true);
- databaseView->setRootIsDecorated(false);
- databaseView->setAlternatingRowColors(true);
- databaseView->setSortingEnabled(true);
- databaseView->sortByColumn(0, Qt::AscendingOrder);
- databaseView->setModel(databaseDisplayModel);
- databaseView->setContextMenuPolicy(Qt::CustomContextMenu);
- connect(databaseView, SIGNAL(customContextMenuRequested(QPoint)), this, SLOT(databaseCustomMenu(QPoint)));
- connect(databaseView->selectionModel(), SIGNAL(currentRowChanged(const QModelIndex &, const QModelIndex &)), this,
- SLOT(updateCardInfoLeft(const QModelIndex &, const QModelIndex &)));
- connect(databaseView, SIGNAL(doubleClicked(const QModelIndex &)), this, SLOT(actAddCard()));
-
- QByteArray dbHeaderState = SettingsCache::instance().layouts().getDeckEditorDbHeaderState();
- if (dbHeaderState.isNull()) {
- // first run
- databaseView->setColumnWidth(0, 200);
- } else {
- databaseView->header()->restoreState(dbHeaderState);
- }
- connect(databaseView->header(), SIGNAL(geometriesChanged()), this, SLOT(saveDbHeaderState()));
-
- searchEdit->setTreeView(databaseView);
-
- aAddCard = new QAction(QString(), this);
- aAddCard->setIcon(QPixmap("theme:icons/arrow_right_green"));
- connect(aAddCard, SIGNAL(triggered()), this, SLOT(actAddCard()));
- auto *tbAddCard = new QToolButton(this);
- tbAddCard->setDefaultAction(aAddCard);
-
- aAddCardToSideboard = new QAction(QString(), this);
- aAddCardToSideboard->setIcon(QPixmap("theme:icons/arrow_right_blue"));
- connect(aAddCardToSideboard, SIGNAL(triggered()), this, SLOT(actAddCardToSideboard()));
- auto *tbAddCardToSideboard = new QToolButton(this);
- tbAddCardToSideboard->setDefaultAction(aAddCardToSideboard);
-
- searchLayout = new QHBoxLayout;
- searchLayout->setObjectName("searchLayout");
- searchLayout->addWidget(searchEdit);
- searchLayout->addWidget(tbAddCard);
- searchLayout->addWidget(tbAddCardToSideboard);
-
- centralFrame = new QVBoxLayout;
- centralFrame->setObjectName("centralFrame");
- centralFrame->addLayout(searchLayout);
- centralFrame->addWidget(databaseView);
-
- centralWidget = new QWidget(this);
- centralWidget->setObjectName("centralWidget");
- centralWidget->setLayout(centralFrame);
- setCentralWidget(centralWidget);
- setDockOptions(QMainWindow::AnimatedDocks | QMainWindow::AllowNestedDocks | QMainWindow::AllowTabbedDocks);
-}
-
-void TabDeckEditor::databaseCustomMenu(QPoint point)
-{
- QMenu menu;
- const CardInfoPtr info = currentCardInfo();
-
- // add to deck and sideboard options
- QAction *addToDeck, *addToSideboard;
- addToDeck = menu.addAction(tr("Add to Deck"));
- addToSideboard = menu.addAction(tr("Add to Sideboard"));
- connect(addToDeck, SIGNAL(triggered()), this, SLOT(actAddCard()));
- connect(addToSideboard, SIGNAL(triggered()), this, SLOT(actAddCardToSideboard()));
-
- // filling out the related cards submenu
- auto *relatedMenu = new QMenu(tr("Show Related cards"));
- menu.addMenu(relatedMenu);
- auto relatedCards = info->getAllRelatedCards();
- if (relatedCards.isEmpty()) {
- relatedMenu->setDisabled(true);
- } else {
- for (const CardRelation *rel : relatedCards) {
- const QString &relatedCardName = rel->getName();
- QAction *relatedCard = relatedMenu->addAction(relatedCardName);
- connect(relatedCard, &QAction::triggered, cardInfo,
- [this, relatedCardName] { cardInfo->setCard(relatedCardName); });
- }
- }
- menu.exec(databaseView->mapToGlobal(point));
-}
-
-void TabDeckEditor::restartLayout()
-{
- deckDock->setVisible(true);
- cardInfoDock->setVisible(true);
- filterDock->setVisible(true);
-
- deckDock->setFloating(false);
- cardInfoDock->setFloating(false);
- filterDock->setFloating(false);
-
- aCardInfoDockVisible->setChecked(true);
- aDeckDockVisible->setChecked(true);
- aFilterDockVisible->setChecked(true);
-
- aCardInfoDockFloating->setChecked(false);
- aDeckDockFloating->setChecked(false);
- aFilterDockFloating->setChecked(false);
-
- addDockWidget(static_cast(2), deckDock);
- addDockWidget(static_cast(2), cardInfoDock);
- addDockWidget(static_cast(2), filterDock);
-
- splitDockWidget(cardInfoDock, deckDock, Qt::Horizontal);
- splitDockWidget(cardInfoDock, filterDock, Qt::Vertical);
-
- deckDock->setMinimumWidth(360);
- deckDock->setMaximumWidth(360);
-
- cardInfoDock->setMinimumSize(250, 360);
- cardInfoDock->setMaximumSize(250, 360);
- QTimer::singleShot(100, this, SLOT(freeDocksSize()));
-}
-
-void TabDeckEditor::freeDocksSize()
-{
- deckDock->setMinimumSize(100, 100);
- deckDock->setMaximumSize(5000, 5000);
-
- cardInfoDock->setMinimumSize(100, 100);
- cardInfoDock->setMaximumSize(5000, 5000);
-
- filterDock->setMinimumSize(100, 100);
- filterDock->setMaximumSize(5000, 5000);
-}
-
-void TabDeckEditor::refreshShortcuts()
-{
- ShortcutsSettings &shortcuts = SettingsCache::instance().shortcuts();
- aNewDeck->setShortcuts(shortcuts.getShortcut("TabDeckEditor/aNewDeck"));
- aLoadDeck->setShortcuts(shortcuts.getShortcut("TabDeckEditor/aLoadDeck"));
- aSaveDeck->setShortcuts(shortcuts.getShortcut("TabDeckEditor/aSaveDeck"));
- aExportDeckDecklist->setShortcuts(shortcuts.getShortcut("TabDeckEditor/aExportDeckDecklist"));
- aSaveDeckAs->setShortcuts(shortcuts.getShortcut("TabDeckEditor/aSaveDeckAs"));
- aLoadDeckFromClipboard->setShortcuts(shortcuts.getShortcut("TabDeckEditor/aLoadDeckFromClipboard"));
- aPrintDeck->setShortcuts(shortcuts.getShortcut("TabDeckEditor/aPrintDeck"));
- aAnalyzeDeckDeckstats->setShortcuts(shortcuts.getShortcut("TabDeckEditor/aAnalyzeDeck"));
- aClose->setShortcuts(shortcuts.getShortcut("TabDeckEditor/aClose"));
- aResetLayout->setShortcuts(shortcuts.getShortcut("TabDeckEditor/aResetLayout"));
- aClearFilterAll->setShortcuts(shortcuts.getShortcut("TabDeckEditor/aClearFilterAll"));
- aClearFilterOne->setShortcuts(shortcuts.getShortcut("TabDeckEditor/aClearFilterOne"));
-
- aSaveDeckToClipboard->setShortcuts(shortcuts.getShortcut("TabDeckEditor/aSaveDeckToClipboard"));
- aSaveDeckToClipboardRaw->setShortcuts(shortcuts.getShortcut("TabDeckEditor/aSaveDeckToClipboardRaw"));
-
- aClearFilterOne->setShortcuts(shortcuts.getShortcut("TabDeckEditor/aClearFilterOne"));
- aClose->setShortcuts(shortcuts.getShortcut("TabDeckEditor/aClose"));
- aRemoveCard->setShortcuts(shortcuts.getShortcut("TabDeckEditor/aRemoveCard"));
- aIncrement->setShortcuts(shortcuts.getShortcut("TabDeckEditor/aIncrement"));
- aDecrement->setShortcuts(shortcuts.getShortcut("TabDeckEditor/aDecrement"));
-}
-
-void TabDeckEditor::loadLayout()
-{
- LayoutsSettings &layouts = SettingsCache::instance().layouts();
- auto &layoutState = layouts.getDeckEditorLayoutState();
- if (layoutState.isNull()) {
- restartLayout();
- } else {
- restoreState(layoutState);
- restoreGeometry(layouts.getDeckEditorGeometry());
- }
-
- aCardInfoDockVisible->setChecked(cardInfoDock->isVisible());
- aFilterDockVisible->setChecked(filterDock->isVisible());
- aDeckDockVisible->setChecked(deckDock->isVisible());
-
- aCardInfoDockFloating->setEnabled(aCardInfoDockVisible->isChecked());
- aDeckDockFloating->setEnabled(aDeckDockVisible->isChecked());
- aFilterDockFloating->setEnabled(aFilterDockVisible->isChecked());
-
- aCardInfoDockFloating->setChecked(cardInfoDock->isFloating());
- aFilterDockFloating->setChecked(filterDock->isFloating());
- aDeckDockFloating->setChecked(deckDock->isFloating());
-
- cardInfoDock->setMinimumSize(layouts.getDeckEditorCardSize());
- cardInfoDock->setMaximumSize(layouts.getDeckEditorCardSize());
-
- filterDock->setMinimumSize(layouts.getDeckEditorFilterSize());
- filterDock->setMaximumSize(layouts.getDeckEditorFilterSize());
-
- deckDock->setMinimumSize(layouts.getDeckEditorDeckSize());
- deckDock->setMaximumSize(layouts.getDeckEditorDeckSize());
-
- QTimer::singleShot(100, this, SLOT(freeDocksSize()));
-}
-
-TabDeckEditor::TabDeckEditor(TabSupervisor *_tabSupervisor, QWidget *parent)
- : Tab(_tabSupervisor, parent), modified(false)
-{
- setObjectName("TabDeckEditor");
-
- createMenus();
-
- createCentralFrame();
-
- createDeckDock();
- createCardInfoDock();
- createFiltersDock();
-
- this->installEventFilter(this);
-
- retranslateUi();
- connect(&SettingsCache::instance().shortcuts(), SIGNAL(shortCutChanged()), this, SLOT(refreshShortcuts()));
- refreshShortcuts();
-
- loadLayout();
-}
-
-TabDeckEditor::~TabDeckEditor()
-{
- emit deckEditorClosing(this);
+ QString result = tr("Deck: %1").arg(deckDockWidget->getSimpleDeckName());
+ if (modified)
+ result.prepend("* ");
+ return result;
}
void TabDeckEditor::retranslateUi()
{
- cardInfo->retranslateUi();
+ deckMenu->retranslateUi();
- aClearFilterAll->setText(tr("&Clear all filters"));
- aClearFilterOne->setText(tr("Delete selected"));
-
- nameLabel->setText(tr("Deck &name:"));
- commentsLabel->setText(tr("&Comments:"));
- hashLabel1->setText(tr("Hash:"));
-
- aNewDeck->setText(tr("&New deck"));
- aLoadDeck->setText(tr("&Load deck..."));
- aSaveDeck->setText(tr("&Save deck"));
- aSaveDeckAs->setText(tr("Save deck &as..."));
- aLoadDeckFromClipboard->setText(tr("Load deck from cl&ipboard..."));
-
- saveDeckToClipboardMenu->setTitle(tr("Save deck to clipboard"));
- aSaveDeckToClipboard->setText(tr("Annotated"));
- aSaveDeckToClipboardRaw->setText(tr("Not Annotated"));
-
- aPrintDeck->setText(tr("&Print deck..."));
-
- analyzeDeckMenu->setTitle(tr("&Send deck to online service"));
- aExportDeckDecklist->setText(tr("Create decklist (decklist.org)"));
- aAnalyzeDeckDeckstats->setText(tr("Analyze deck (deckstats.net)"));
- aAnalyzeDeckTappedout->setText(tr("Analyze deck (tappedout.net)"));
-
- aClose->setText(tr("&Close"));
-
- aAddCard->setText(tr("Add card to &maindeck"));
- aAddCardToSideboard->setText(tr("Add card to &sideboard"));
-
- aRemoveCard->setText(tr("&Remove row"));
-
- aIncrement->setText(tr("&Increment number"));
-
- aDecrement->setText(tr("&Decrement number"));
-
- deckMenu->setTitle(tr("&Deck Editor"));
-
- cardInfoDock->setWindowTitle(tr("Card Info"));
- deckDock->setWindowTitle(tr("Deck"));
- filterDock->setWindowTitle(tr("Filters"));
+ cardInfoDockWidget->retranslateUi();
+ deckDockWidget->retranslateUi();
+ filterDockWidget->retranslateUi();
+ printingSelectorDockWidget->retranslateUi();
viewMenu->setTitle(tr("&View"));
cardInfoDockMenu->setTitle(tr("Card Info"));
deckDockMenu->setTitle(tr("Deck"));
filterDockMenu->setTitle(tr("Filters"));
+ printingSelectorDockMenu->setTitle(tr("Printing"));
aCardInfoDockVisible->setText(tr("Visible"));
aCardInfoDockFloating->setText(tr("Floating"));
@@ -690,636 +136,175 @@ void TabDeckEditor::retranslateUi()
aFilterDockVisible->setText(tr("Visible"));
aFilterDockFloating->setText(tr("Floating"));
+ aPrintingSelectorDockVisible->setText(tr("Visible"));
+ aPrintingSelectorDockFloating->setText(tr("Floating"));
+
aResetLayout->setText(tr("Reset layout"));
}
-QString TabDeckEditor::getTabText() const
+void TabDeckEditor::refreshShortcuts()
{
- QString result = tr("Deck: %1").arg(nameEdit->text().simplified());
- if (modified)
- result.prepend("* ");
- return result;
+ ShortcutsSettings &shortcuts = SettingsCache::instance().shortcuts();
+ aResetLayout->setShortcuts(shortcuts.getShortcut("TabDeckEditor/aResetLayout"));
}
-void TabDeckEditor::updateName(const QString &name)
+void TabDeckEditor::showPrintingSelector()
{
- deckModel->getDeckList()->setName(name);
- setModified(true);
- setSaveStatus(true);
+ printingSelectorDockWidget->printingSelector->setCard(cardInfoDockWidget->cardInfo->getInfo(), DECK_ZONE_MAIN);
+ printingSelectorDockWidget->printingSelector->updateDisplay();
+ aPrintingSelectorDockVisible->setChecked(true);
+ printingSelectorDockWidget->setVisible(true);
}
-void TabDeckEditor::updateComments()
+void TabDeckEditor::loadLayout()
{
- deckModel->getDeckList()->setComments(commentsEdit->toPlainText());
- setModified(true);
- setSaveStatus(true);
-}
+ LayoutsSettings &layouts = SettingsCache::instance().layouts();
-void TabDeckEditor::updateCardInfoLeft(const QModelIndex ¤t, const QModelIndex & /*previous*/)
-{
- cardInfo->setCard(current.sibling(current.row(), 0).data().toString());
-}
+ setCentralWidget(databaseDisplayDockWidget);
-void TabDeckEditor::updateCardInfoRight(const QModelIndex ¤t, const QModelIndex & /*previous*/)
-{
- if (!current.isValid())
- return;
- if (!current.model()->hasChildren(current.sibling(current.row(), 0))) {
- cardInfo->setCard(current.sibling(current.row(), 1).data().toString(),
- current.sibling(current.row(), 4).data().toString());
- }
-}
-
-void TabDeckEditor::updateSearch(const QString &search)
-{
- databaseDisplayModel->setStringFilter(search);
- QModelIndexList sel = databaseView->selectionModel()->selectedRows();
- if (sel.isEmpty() && databaseDisplayModel->rowCount())
- databaseView->selectionModel()->setCurrentIndex(databaseDisplayModel->index(0, 0),
- QItemSelectionModel::SelectCurrent | QItemSelectionModel::Rows);
-}
-
-void TabDeckEditor::updateHash()
-{
- hashLabel->setText(deckModel->getDeckList()->getDeckHash());
-}
-
-bool TabDeckEditor::confirmClose()
-{
- if (modified) {
- tabSupervisor->setCurrentWidget(this);
- int ret = createSaveConfirmationWindow()->exec();
- if (ret == QMessageBox::Save)
- return actSaveDeck();
- else if (ret == QMessageBox::Cancel)
- return false;
- }
- return true;
-}
-
-void TabDeckEditor::closeRequest()
-{
- if (confirmClose())
- deleteLater();
-}
-
-void TabDeckEditor::actNewDeck()
-{
- auto deckOpenLocation = confirmOpen(false);
-
- if (deckOpenLocation == CANCELLED) {
- return;
- }
-
- if (deckOpenLocation == NEW_TAB) {
- emit openDeckEditor(nullptr);
- return;
- }
-
- deckModel->cleanList();
- nameEdit->setText(QString());
- commentsEdit->setText(QString());
- hashLabel->setText(QString());
- setModified(false);
- setSaveStatus(false);
-}
-
-void TabDeckEditor::actLoadDeck()
-{
- auto deckOpenLocation = confirmOpen();
-
- if (deckOpenLocation == CANCELLED) {
- return;
- }
-
- QFileDialog dialog(this, tr("Load deck"));
- dialog.setDirectory(SettingsCache::instance().getDeckPath());
- dialog.setNameFilters(DeckLoader::fileNameFilters);
- if (!dialog.exec())
- return;
-
- QString fileName = dialog.selectedFiles().at(0);
- DeckLoader::FileFormat fmt = DeckLoader::getFormatFromName(fileName);
-
- auto *l = new DeckLoader;
- if (l->loadFromFile(fileName, fmt)) {
- if (deckOpenLocation == NEW_TAB) {
- emit openDeckEditor(l);
- } else {
- setSaveStatus(false);
- setDeck(l);
- }
- } else
- delete l;
- setSaveStatus(true);
-}
-
-void TabDeckEditor::saveDeckRemoteFinished(const Response &response)
-{
- if (response.response_code() != Response::RespOk)
- QMessageBox::critical(this, tr("Error"), tr("The deck could not be saved."));
- else
- setModified(false);
-}
-
-bool TabDeckEditor::actSaveDeck()
-{
- DeckLoader *const deck = deckModel->getDeckList();
- if (deck->getLastRemoteDeckId() != -1) {
- QString deckString = deck->writeToString_Native();
- if (deckString.length() > MAX_FILE_LENGTH) {
- QMessageBox::critical(this, tr("Error"), tr("Could not save remote deck"));
- return false;
- }
-
- Command_DeckUpload cmd;
- cmd.set_deck_id(static_cast(deck->getLastRemoteDeckId()));
- cmd.set_deck_list(deckString.toStdString());
-
- PendingCommand *pend = AbstractClient::prepareSessionCommand(cmd);
- connect(pend, SIGNAL(finished(Response, CommandContainer, QVariant)), this,
- SLOT(saveDeckRemoteFinished(Response)));
- tabSupervisor->getClient()->sendCommand(pend);
-
- return true;
- } else if (deck->getLastFileName().isEmpty())
- return actSaveDeckAs();
- else if (deck->saveToFile(deck->getLastFileName(), deck->getLastFileFormat())) {
- setModified(false);
- return true;
- }
- QMessageBox::critical(
- this, tr("Error"),
- tr("The deck could not be saved.\nPlease check that the directory is writable and try again."));
- return false;
-}
-
-bool TabDeckEditor::actSaveDeckAs()
-{
- QFileDialog dialog(this, tr("Save deck"));
- dialog.setDirectory(SettingsCache::instance().getDeckPath());
- dialog.setAcceptMode(QFileDialog::AcceptSave);
- dialog.setDefaultSuffix("cod");
- dialog.setNameFilters(DeckLoader::fileNameFilters);
- dialog.selectFile(deckModel->getDeckList()->getName().trimmed() + ".cod");
- if (!dialog.exec())
- return false;
-
- QString fileName = dialog.selectedFiles().at(0);
- DeckLoader::FileFormat fmt = DeckLoader::getFormatFromName(fileName);
-
- if (!deckModel->getDeckList()->saveToFile(fileName, fmt)) {
- QMessageBox::critical(
- this, tr("Error"),
- tr("The deck could not be saved.\nPlease check that the directory is writable and try again."));
- return false;
- }
- setModified(false);
- return true;
-}
-
-void TabDeckEditor::actLoadDeckFromClipboard()
-{
- auto deckOpenLocation = confirmOpen();
-
- if (deckOpenLocation == CANCELLED) {
- return;
- }
-
- DlgLoadDeckFromClipboard dlg(this);
- if (!dlg.exec())
- return;
-
- if (deckOpenLocation == NEW_TAB) {
- emit openDeckEditor(dlg.getDeckList());
+ auto &layoutState = layouts.getDeckEditorLayoutState();
+ if (layoutState.isNull()) {
+ restartLayout();
} else {
- setDeck(dlg.getDeckList());
- setModified(true);
+ restoreState(layoutState);
+ restoreGeometry(layouts.getDeckEditorGeometry());
}
- setSaveStatus(true);
+ aCardInfoDockVisible->setChecked(!cardInfoDockWidget->isHidden());
+ aFilterDockVisible->setChecked(!filterDockWidget->isHidden());
+ aDeckDockVisible->setChecked(!deckDockWidget->isHidden());
+ aPrintingSelectorDockVisible->setChecked(!printingSelectorDockWidget->isHidden());
+
+ aCardInfoDockFloating->setEnabled(aCardInfoDockVisible->isChecked());
+ aDeckDockFloating->setEnabled(aDeckDockVisible->isChecked());
+ aFilterDockFloating->setEnabled(aFilterDockVisible->isChecked());
+ aPrintingSelectorDockFloating->setEnabled(aPrintingSelectorDockVisible->isChecked());
+
+ aCardInfoDockFloating->setChecked(cardInfoDockWidget->isFloating());
+ aFilterDockFloating->setChecked(filterDockWidget->isFloating());
+ aDeckDockFloating->setChecked(deckDockWidget->isFloating());
+ aPrintingSelectorDockFloating->setChecked(printingSelectorDockWidget->isFloating());
+
+ cardInfoDockWidget->setMinimumSize(layouts.getDeckEditorCardSize());
+ cardInfoDockWidget->setMaximumSize(layouts.getDeckEditorCardSize());
+
+ filterDockWidget->setMinimumSize(layouts.getDeckEditorFilterSize());
+ filterDockWidget->setMaximumSize(layouts.getDeckEditorFilterSize());
+
+ deckDockWidget->setMinimumSize(layouts.getDeckEditorDeckSize());
+ deckDockWidget->setMaximumSize(layouts.getDeckEditorDeckSize());
+
+ printingSelectorDockWidget->setMinimumSize(layouts.getDeckEditorPrintingSelectorSize());
+ printingSelectorDockWidget->setMaximumSize(layouts.getDeckEditorPrintingSelectorSize());
+
+ databaseDisplayDockWidget->setMinimumSize(100, 100);
+ databaseDisplayDockWidget->setMaximumSize(1400, 5000);
+
+ QTimer::singleShot(100, this, SLOT(freeDocksSize()));
}
-void TabDeckEditor::actSaveDeckToClipboard()
+void TabDeckEditor::restartLayout()
{
- QString buffer;
- QTextStream stream(&buffer);
- deckModel->getDeckList()->saveToStream_Plain(stream);
- QApplication::clipboard()->setText(buffer, QClipboard::Clipboard);
- QApplication::clipboard()->setText(buffer, QClipboard::Selection);
-}
+ deckDockWidget->setVisible(true);
+ cardInfoDockWidget->setVisible(true);
+ filterDockWidget->setVisible(true);
+ printingSelectorDockWidget->setVisible(false);
-void TabDeckEditor::actSaveDeckToClipboardRaw()
-{
- QString buffer;
- QTextStream stream(&buffer);
- deckModel->getDeckList()->saveToStream_Plain(stream, false);
- QApplication::clipboard()->setText(buffer, QClipboard::Clipboard);
- QApplication::clipboard()->setText(buffer, QClipboard::Selection);
-}
+ deckDockWidget->setFloating(false);
+ cardInfoDockWidget->setFloating(false);
+ filterDockWidget->setFloating(false);
+ printingSelectorDockWidget->setFloating(false);
-void TabDeckEditor::actPrintDeck()
-{
- auto *dlg = new QPrintPreviewDialog(this);
- connect(dlg, SIGNAL(paintRequested(QPrinter *)), deckModel, SLOT(printDeckList(QPrinter *)));
- dlg->exec();
-}
-
-// Action called when export deck to decklist menu item is pressed.
-void TabDeckEditor::actExportDeckDecklist()
-{
- // Get the decklist class for the deck.
- DeckLoader *const deck = deckModel->getDeckList();
- // create a string to load the decklist url into.
- QString decklistUrlString;
- // check if deck is not null
- if (deck) {
- // Get the decklist url string from the deck loader class.
- decklistUrlString = deck->exportDeckToDecklist();
- // Check to make sure the string isn't empty.
- if (QString::compare(decklistUrlString, "", Qt::CaseInsensitive) == 0) {
- // Show an error if the deck is empty, and return.
- QMessageBox::critical(this, tr("Error"), tr("There are no cards in your deck to be exported"));
- return;
- }
- // Encode the string recieved from the model to make sure all characters are encoded.
- // first we put it into a qurl object
- QUrl decklistUrl = QUrl(decklistUrlString);
- // we get the correctly encoded url.
- decklistUrlString = decklistUrl.toEncoded();
- // We open the url in the user's default browser
- QDesktopServices::openUrl(decklistUrlString);
- } else {
- // if there's no deck loader object, return an error
- QMessageBox::critical(this, tr("Error"), tr("No deck was selected to be saved."));
- }
-}
-
-void TabDeckEditor::actAnalyzeDeckDeckstats()
-{
- auto *interface = new DeckStatsInterface(*databaseModel->getDatabase(),
- this); // it deletes itself when done
- interface->analyzeDeck(deckModel->getDeckList());
-}
-
-void TabDeckEditor::actAnalyzeDeckTappedout()
-{
- auto *interface = new TappedOutInterface(*databaseModel->getDatabase(),
- this); // it deletes itself when done
- interface->analyzeDeck(deckModel->getDeckList());
-}
-
-void TabDeckEditor::actClearFilterAll()
-{
- databaseDisplayModel->clearFilterAll();
- searchEdit->setText("");
-}
-
-void TabDeckEditor::actClearFilterOne()
-{
- QModelIndexList selIndexes = filterView->selectionModel()->selectedIndexes();
- for (QModelIndex idx : selIndexes) {
- filterModel->removeRow(idx.row(), idx.parent());
- }
-}
-
-void TabDeckEditor::recursiveExpand(const QModelIndex &index)
-{
- if (index.parent().isValid())
- recursiveExpand(index.parent());
- deckView->expand(index);
-}
-
-/**
- * @brief Displays the save confirmation dialogue that is shown before loading a deck, if required. Takes into
- * account the `openDeckInNewTab` settting.
- *
- * @param openInSameTabIfBlank Open the deck in the same tab instead of a new tab if the current tab is completely
- * blank. Only relevant when the `openDeckInNewTab` setting is enabled.
- *
- * @returns An enum that indicates if and where to load the deck
- */
-TabDeckEditor::DeckOpenLocation TabDeckEditor::confirmOpen(const bool openInSameTabIfBlank)
-{
- // handle `openDeckInNewTab` setting
- if (SettingsCache::instance().getOpenDeckInNewTab()) {
- if (openInSameTabIfBlank && isBlankNewDeck()) {
- return SAME_TAB;
- } else {
- return NEW_TAB;
- }
- }
-
- // early return if deck is unmodified
- if (!modified) {
- return SAME_TAB;
- }
-
- // do the save confirmation dialogue
- tabSupervisor->setCurrentWidget(this);
-
- QMessageBox *msgBox = createSaveConfirmationWindow();
- QPushButton *newTabButton = msgBox->addButton(tr("Open in new tab"), QMessageBox::ApplyRole);
-
- int ret = msgBox->exec();
-
- // `exec()` returns an opaque value if a non-standard button was clicked.
- // Directly check if newTabButton was clicked before switching over the standard buttons.
- if (msgBox->clickedButton() == newTabButton) {
- return NEW_TAB;
- }
-
- switch (ret) {
- case QMessageBox::Save:
- return actSaveDeck() ? SAME_TAB : CANCELLED;
- case QMessageBox::Discard:
- return SAME_TAB;
- default:
- return CANCELLED;
- }
-}
-
-/**
- * @brief Creates the base save confirmation dialogue box.
- *
- * @returns A QMessageBox that can be further modified
- */
-QMessageBox *TabDeckEditor::createSaveConfirmationWindow()
-{
- QMessageBox *msgBox = new QMessageBox(this);
- msgBox->setIcon(QMessageBox::Warning);
- msgBox->setWindowTitle(tr("Are you sure?"));
- msgBox->setText(tr("The decklist has been modified.\nDo you want to save the changes?"));
- msgBox->setStandardButtons(QMessageBox::Save | QMessageBox::Discard | QMessageBox::Cancel);
- return msgBox;
-}
-
-/**
- * @brief Returns true if this tab is a blank newly opened tab, as if it was just created with the `New Deck` action.
- */
-bool TabDeckEditor::isBlankNewDeck() const
-{
- DeckLoader *const deck = deckModel->getDeckList();
- return !modified && deck->getLastFileName().isEmpty() && deck->getLastRemoteDeckId() == -1;
-}
-
-CardInfoPtr TabDeckEditor::currentCardInfo() const
-{
- const QModelIndex currentIndex = databaseView->selectionModel()->currentIndex();
- if (!currentIndex.isValid()) {
- return {};
- }
-
- const QString cardName = currentIndex.sibling(currentIndex.row(), 0).data().toString();
-
- return CardDatabaseManager::getInstance()->getCard(cardName);
-}
-
-void TabDeckEditor::addCardHelper(QString zoneName)
-{
- const CardInfoPtr info = currentCardInfo();
- if (!info)
- return;
- if (info->getIsToken())
- zoneName = DECK_ZONE_TOKENS;
-
- QModelIndex newCardIndex = deckModel->addPreferredPrintingCard(info->getName(), zoneName, false);
- recursiveExpand(newCardIndex);
- deckView->setCurrentIndex(newCardIndex);
- setModified(true);
- searchEdit->setSelection(0, searchEdit->text().length());
-}
-
-void TabDeckEditor::actSwapCard()
-{
- const QModelIndex currentIndex = deckView->selectionModel()->currentIndex();
- if (!currentIndex.isValid())
- return;
- const QString cardName = currentIndex.sibling(currentIndex.row(), 1).data().toString();
- const QString cardProviderID = currentIndex.sibling(currentIndex.row(), 2).data().toString();
- const QModelIndex gparent = currentIndex.parent().parent();
-
- if (!gparent.isValid())
- return;
-
- const QString zoneName = gparent.sibling(gparent.row(), 1).data(Qt::EditRole).toString();
- actDecrement();
- const QString otherZoneName = zoneName == DECK_ZONE_MAIN ? DECK_ZONE_SIDE : DECK_ZONE_MAIN;
-
- // Third argument (true) says create the card no matter what, even if not in DB
- QModelIndex newCardIndex = deckModel->addCard(
- cardName, CardDatabaseManager::getInstance()->getSpecificSetForCard(cardName, cardProviderID), otherZoneName,
- true);
- recursiveExpand(newCardIndex);
-
- setModified(true);
- setSaveStatus(true);
-}
-
-void TabDeckEditor::actAddCard()
-{
- if (QApplication::keyboardModifiers() & Qt::ControlModifier)
- actAddCardToSideboard();
- else
- addCardHelper(DECK_ZONE_MAIN);
- setSaveStatus(true);
-}
-
-void TabDeckEditor::actAddCardToSideboard()
-{
- addCardHelper(DECK_ZONE_SIDE);
- setSaveStatus(true);
-}
-
-void TabDeckEditor::actRemoveCard()
-{
- const QModelIndex ¤tIndex = deckView->selectionModel()->currentIndex();
- if (!currentIndex.isValid() || deckModel->hasChildren(currentIndex))
- return;
- deckModel->removeRow(currentIndex.row(), currentIndex.parent());
-
- DeckLoader *const deck = deckModel->getDeckList();
- setSaveStatus(!deck->isEmpty());
- setModified(true);
-}
-
-void TabDeckEditor::offsetCountAtIndex(const QModelIndex &idx, int offset)
-{
- if (!idx.isValid() || offset == 0)
- return;
-
- const QModelIndex numberIndex = idx.sibling(idx.row(), 0);
- const int count = deckModel->data(numberIndex, Qt::EditRole).toInt();
- const int new_count = count + offset;
- deckView->setCurrentIndex(numberIndex);
- if (new_count <= 0)
- deckModel->removeRow(idx.row(), idx.parent());
- else
- deckModel->setData(numberIndex, new_count, Qt::EditRole);
- setModified(true);
-}
-
-void TabDeckEditor::decrementCardHelper(QString zoneName)
-{
- const CardInfoPtr info = currentCardInfo();
- QModelIndex idx;
-
- if (!info)
- return;
- if (info->getIsToken())
- zoneName = DECK_ZONE_TOKENS;
-
- idx = deckModel->findCard(info->getName(), zoneName);
- offsetCountAtIndex(idx, -1);
-}
-
-void TabDeckEditor::actDecrementCard()
-{
- decrementCardHelper(DECK_ZONE_MAIN);
-}
-
-void TabDeckEditor::actDecrementCardFromSideboard()
-{
- decrementCardHelper(DECK_ZONE_SIDE);
-}
-
-void TabDeckEditor::copyDatabaseCellContents()
-{
- auto _data = databaseView->selectionModel()->currentIndex().data();
- QApplication::clipboard()->setText(_data.toString());
-}
-
-void TabDeckEditor::actIncrement()
-{
- const QModelIndex ¤tIndex = deckView->selectionModel()->currentIndex();
- offsetCountAtIndex(currentIndex, 1);
-}
-
-void TabDeckEditor::actDecrement()
-{
- const QModelIndex ¤tIndex = deckView->selectionModel()->currentIndex();
- offsetCountAtIndex(currentIndex, -1);
-}
-
-void TabDeckEditor::setDeck(DeckLoader *_deck)
-{
- deckModel->setDeckList(_deck);
-
- nameEdit->setText(deckModel->getDeckList()->getName());
- commentsEdit->setText(deckModel->getDeckList()->getComments());
- updateHash();
- deckModel->sort(deckView->header()->sortIndicatorSection(), deckView->header()->sortIndicatorOrder());
- deckView->expandAll();
- setModified(false);
-
- PictureLoader::cacheCardPixmaps(
- CardDatabaseManager::getInstance()->getCards(deckModel->getDeckList()->getCardList()));
- deckView->expandAll();
- setModified(false);
-
- // If they load a deck, make the deck list appear
+ aCardInfoDockVisible->setChecked(true);
aDeckDockVisible->setChecked(true);
- deckDock->setVisible(aDeckDockVisible->isChecked());
+ aFilterDockVisible->setChecked(true);
+ aPrintingSelectorDockVisible->setChecked(false);
+
+ aCardInfoDockFloating->setChecked(false);
+ aDeckDockFloating->setChecked(false);
+ aFilterDockFloating->setChecked(false);
+ aPrintingSelectorDockFloating->setChecked(false);
+
+ setCentralWidget(databaseDisplayDockWidget);
+ addDockWidget(static_cast(2), deckDockWidget);
+ addDockWidget(static_cast(2), cardInfoDockWidget);
+ addDockWidget(static_cast(2), filterDockWidget);
+ addDockWidget(static_cast(2), printingSelectorDockWidget);
+
+ splitDockWidget(cardInfoDockWidget, printingSelectorDockWidget, Qt::Horizontal);
+ splitDockWidget(printingSelectorDockWidget, deckDockWidget, Qt::Horizontal);
+ splitDockWidget(cardInfoDockWidget, printingSelectorDockWidget, Qt::Horizontal);
+ splitDockWidget(cardInfoDockWidget, filterDockWidget, Qt::Vertical);
+
+ QTimer::singleShot(100, this, SLOT(freeDocksSize()));
}
-void TabDeckEditor::setModified(bool _modified)
+void TabDeckEditor::freeDocksSize()
{
- modified = _modified;
- emit tabTextChanged(this, getTabText());
-}
+ deckDockWidget->setMinimumSize(100, 100);
+ deckDockWidget->setMaximumSize(5000, 5000);
-void TabDeckEditor::filterViewCustomContextMenu(const QPoint &point)
-{
- QMenu menu;
- QAction *action;
- QModelIndex idx;
+ cardInfoDockWidget->setMinimumSize(100, 100);
+ cardInfoDockWidget->setMaximumSize(5000, 5000);
- idx = filterView->indexAt(point);
- if (!idx.isValid())
- return;
+ filterDockWidget->setMinimumSize(100, 100);
+ filterDockWidget->setMaximumSize(5000, 5000);
- action = menu.addAction(QString("delete"));
- action->setData(point);
- connect(&menu, SIGNAL(triggered(QAction *)), this, SLOT(filterRemove(QAction *)));
- menu.exec(filterView->mapToGlobal(point));
-}
+ printingSelectorDockWidget->setMinimumSize(525, 100);
+ printingSelectorDockWidget->setMaximumSize(5000, 5000);
-void TabDeckEditor::filterRemove(QAction *action)
-{
- QPoint point;
- QModelIndex idx;
-
- point = action->data().toPoint();
- idx = filterView->indexAt(point);
- if (!idx.isValid())
- return;
-
- filterModel->removeRow(idx.row(), idx.parent());
-}
-
-// Method uses to sync docks state with menu items state
-bool TabDeckEditor::eventFilter(QObject *o, QEvent *e)
-{
- if (e->type() == QEvent::Close) {
- if (o == cardInfoDock) {
- aCardInfoDockVisible->setChecked(false);
- aCardInfoDockFloating->setEnabled(false);
- } else if (o == deckDock) {
- aDeckDockVisible->setChecked(false);
- aDeckDockFloating->setEnabled(false);
- } else if (o == filterDock) {
- aFilterDockVisible->setChecked(false);
- aFilterDockFloating->setEnabled(false);
- }
- }
- if (o == this && e->type() == QEvent::Hide) {
- LayoutsSettings &layouts = SettingsCache::instance().layouts();
- layouts.setDeckEditorLayoutState(saveState());
- layouts.setDeckEditorGeometry(saveGeometry());
- layouts.setDeckEditorCardSize(cardInfoDock->size());
- layouts.setDeckEditorFilterSize(filterDock->size());
- layouts.setDeckEditorDeckSize(deckDock->size());
- }
- return false;
+ databaseDisplayDockWidget->setMinimumSize(100, 100);
+ databaseDisplayDockWidget->setMaximumSize(1400, 5000);
}
void TabDeckEditor::dockVisibleTriggered()
{
QObject *o = sender();
if (o == aCardInfoDockVisible) {
- cardInfoDock->setVisible(aCardInfoDockVisible->isChecked());
+ cardInfoDockWidget->setHidden(!aCardInfoDockVisible->isChecked());
aCardInfoDockFloating->setEnabled(aCardInfoDockVisible->isChecked());
return;
}
if (o == aDeckDockVisible) {
- deckDock->setVisible(aDeckDockVisible->isChecked());
+ deckDockWidget->setHidden(!aDeckDockVisible->isChecked());
aDeckDockFloating->setEnabled(aDeckDockVisible->isChecked());
return;
}
if (o == aFilterDockVisible) {
- filterDock->setVisible(aFilterDockVisible->isChecked());
+ filterDockWidget->setHidden(!aFilterDockVisible->isChecked());
aFilterDockFloating->setEnabled(aFilterDockVisible->isChecked());
return;
}
+
+ if (o == aPrintingSelectorDockVisible) {
+ printingSelectorDockWidget->setHidden(!aPrintingSelectorDockVisible->isChecked());
+ aPrintingSelectorDockFloating->setEnabled(aPrintingSelectorDockVisible->isChecked());
+ return;
+ }
}
void TabDeckEditor::dockFloatingTriggered()
{
QObject *o = sender();
if (o == aCardInfoDockFloating) {
- cardInfoDock->setFloating(aCardInfoDockFloating->isChecked());
+ cardInfoDockWidget->setFloating(aCardInfoDockFloating->isChecked());
return;
}
if (o == aDeckDockFloating) {
- deckDock->setFloating(aDeckDockFloating->isChecked());
+ deckDockWidget->setFloating(aDeckDockFloating->isChecked());
return;
}
if (o == aFilterDockFloating) {
- filterDock->setFloating(aFilterDockFloating->isChecked());
+ filterDockWidget->setFloating(aFilterDockFloating->isChecked());
+ return;
+ }
+
+ if (o == aPrintingSelectorDockFloating) {
+ printingSelectorDockWidget->setFloating(aPrintingSelectorDockFloating->isChecked());
return;
}
}
@@ -1327,70 +312,53 @@ void TabDeckEditor::dockFloatingTriggered()
void TabDeckEditor::dockTopLevelChanged(bool topLevel)
{
QObject *o = sender();
- if (o == cardInfoDock) {
+ if (o == cardInfoDockWidget) {
aCardInfoDockFloating->setChecked(topLevel);
return;
}
- if (o == deckDock) {
+ if (o == deckDockWidget) {
aDeckDockFloating->setChecked(topLevel);
return;
}
- if (o == filterDock) {
+ if (o == filterDockWidget) {
aFilterDockFloating->setChecked(topLevel);
return;
}
-}
-void TabDeckEditor::saveDbHeaderState()
-{
- SettingsCache::instance().layouts().setDeckEditorDbHeaderState(databaseView->header()->saveState());
-}
-
-void TabDeckEditor::setSaveStatus(bool newStatus)
-{
- aSaveDeck->setEnabled(newStatus);
- aSaveDeckAs->setEnabled(newStatus);
- aSaveDeckToClipboard->setEnabled(newStatus);
- aSaveDeckToClipboardRaw->setEnabled(newStatus);
- saveDeckToClipboardMenu->setEnabled(newStatus);
- aPrintDeck->setEnabled(newStatus);
- analyzeDeckMenu->setEnabled(newStatus);
-}
-
-void TabDeckEditor::showSearchSyntaxHelp()
-{
-
- QFile file("theme:help/search.md");
-
- if (!file.open(QFile::ReadOnly | QFile::Text)) {
+ if (o == printingSelectorDockWidget) {
+ aPrintingSelectorDockFloating->setChecked(topLevel);
return;
}
-
- QTextStream in(&file);
- QString text = in.readAll();
- file.close();
-
- // Poor Markdown Converter
- auto opts = QRegularExpression::MultilineOption;
- text = text.replace(QRegularExpression("^(###)(.*)", opts), "\\2
")
- .replace(QRegularExpression("^(##)(.*)", opts), "\\2
")
- .replace(QRegularExpression("^(#)(.*)", opts), "\\2
")
- .replace(QRegularExpression("^------*", opts), "
")
- .replace(QRegularExpression(R"(\[([^[]+)\]\(([^\)]+)\))", opts), R"(\1)");
-
- auto browser = new QTextBrowser;
- browser->setParent(this, Qt::Window | Qt::WindowTitleHint | Qt::WindowSystemMenuHint | Qt::WindowMinMaxButtonsHint |
- Qt::WindowCloseButtonHint | Qt::WindowFullscreenButtonHint);
- browser->setWindowTitle("Search Help");
- browser->setReadOnly(true);
- browser->setMinimumSize({500, 600});
-
- QString sheet = QString("a { text-decoration: underline; color: rgb(71,158,252) };");
- browser->document()->setDefaultStyleSheet(sheet);
-
- browser->setHtml(text);
- connect(browser, &QTextBrowser::anchorClicked, [=](const QUrl &link) { searchEdit->setText(link.fragment()); });
- browser->show();
}
+
+// Method uses to sync docks state with menu items state
+bool TabDeckEditor::eventFilter(QObject *o, QEvent *e)
+{
+ if (e->type() == QEvent::Close) {
+ if (o == cardInfoDockWidget) {
+ aCardInfoDockVisible->setChecked(false);
+ aCardInfoDockFloating->setEnabled(false);
+ } else if (o == deckDockWidget) {
+ aDeckDockVisible->setChecked(false);
+ aDeckDockFloating->setEnabled(false);
+ } else if (o == filterDockWidget) {
+ aFilterDockVisible->setChecked(false);
+ aFilterDockFloating->setEnabled(false);
+ } else if (o == printingSelectorDockWidget) {
+ aPrintingSelectorDockVisible->setChecked(false);
+ aPrintingSelectorDockFloating->setEnabled(false);
+ }
+ }
+ if (o == this && e->type() == QEvent::Hide) {
+ LayoutsSettings &layouts = SettingsCache::instance().layouts();
+ layouts.setDeckEditorLayoutState(saveState());
+ layouts.setDeckEditorGeometry(saveGeometry());
+ layouts.setDeckEditorCardSize(cardInfoDockWidget->size());
+ layouts.setDeckEditorFilterSize(filterDockWidget->size());
+ layouts.setDeckEditorDeckSize(deckDockWidget->size());
+ layouts.setDeckEditorPrintingSelectorSize(printingSelectorDockWidget->size());
+ }
+ return false;
+}
\ No newline at end of file
diff --git a/cockatrice/src/client/tabs/tab_deck_editor.h b/cockatrice/src/client/tabs/tab_deck_editor.h
index 6aff1cc31..d9b10de7d 100644
--- a/cockatrice/src/client/tabs/tab_deck_editor.h
+++ b/cockatrice/src/client/tabs/tab_deck_editor.h
@@ -1,186 +1,41 @@
#ifndef WINDOW_DECKEDITOR_H
#define WINDOW_DECKEDITOR_H
-#include "../../deck/custom_line_edit.h"
#include "../../game/cards/card_database.h"
#include "../game_logic/key_signals.h"
-#include "tab.h"
-
-#include
-#include
+#include "../ui/widgets/visual_deck_storage/deck_preview/deck_preview_deck_tags_display_widget.h"
+#include "abstract_tab_deck_editor.h"
class CardDatabaseModel;
class CardDatabaseDisplayModel;
class DeckListModel;
-class QTreeView;
-class CardInfoFrameWidget;
-class QTextEdit;
class QLabel;
class DeckLoader;
-class Response;
-class FilterTreeModel;
-class FilterBuilder;
-class QGroupBox;
-class QMessageBox;
-class QHBoxLayout;
-class QVBoxLayout;
-class QPushButton;
-class QDockWidget;
-class SearchLineEdit : public LineEditUnfocusable
-{
-private:
- QTreeView *treeView;
-
-protected:
- void keyPressEvent(QKeyEvent *event) override;
-
-public:
- SearchLineEdit() : LineEditUnfocusable(), treeView(nullptr)
- {
- }
- void setTreeView(QTreeView *_treeView)
- {
- treeView = _treeView;
- }
-};
-
-class TabDeckEditor : public Tab
+class TabDeckEditor : public AbstractTabDeckEditor
{
Q_OBJECT
-private slots:
- void updateName(const QString &name);
- void updateComments();
- void updateHash();
- void updateCardInfoLeft(const QModelIndex ¤t, const QModelIndex &previous);
- void updateCardInfoRight(const QModelIndex ¤t, const QModelIndex &previous);
- void updateSearch(const QString &search);
- void databaseCustomMenu(QPoint point);
- void actNewDeck();
- void actLoadDeck();
- bool actSaveDeck();
- bool actSaveDeckAs();
- void actLoadDeckFromClipboard();
- void actSaveDeckToClipboard();
- void actSaveDeckToClipboardRaw();
- void actPrintDeck();
- void actExportDeckDecklist();
- void actAnalyzeDeckDeckstats();
- void actAnalyzeDeckTappedout();
-
- void actClearFilterAll();
- void actClearFilterOne();
-
- void actSwapCard();
- void actAddCard();
- void actAddCardToSideboard();
- void actRemoveCard();
- void actIncrement();
- void actDecrement();
- void actDecrementCard();
- void actDecrementCardFromSideboard();
- void copyDatabaseCellContents();
-
- void saveDeckRemoteFinished(const Response &r);
- void filterViewCustomContextMenu(const QPoint &point);
- void filterRemove(QAction *action);
-
- void loadLayout();
- void restartLayout();
- void freeDocksSize();
- void refreshShortcuts();
+protected slots:
+ void loadLayout() override;
+ void restartLayout() override;
+ void freeDocksSize() override;
+ void refreshShortcuts() override;
bool eventFilter(QObject *o, QEvent *e) override;
- void dockVisibleTriggered();
- void dockFloatingTriggered();
- void dockTopLevelChanged(bool topLevel);
- void saveDbHeaderState();
- void setSaveStatus(bool newStatus);
- void showSearchSyntaxHelp();
-
-private:
- /**
- * @brief Which tab to open the new deck in
- */
- enum DeckOpenLocation
- {
- CANCELLED,
- SAME_TAB,
- NEW_TAB
- };
-
- DeckOpenLocation confirmOpen(const bool openInSameTabIfBlank = true);
- QMessageBox *createSaveConfirmationWindow();
-
- bool isBlankNewDeck() const;
- CardInfoPtr currentCardInfo() const;
- void addCardHelper(QString zoneName);
- void offsetCountAtIndex(const QModelIndex &idx, int offset);
- void decrementCardHelper(QString zoneName);
- void recursiveExpand(const QModelIndex &index);
-
- CardDatabaseModel *databaseModel;
- CardDatabaseDisplayModel *databaseDisplayModel;
- DeckListModel *deckModel;
- QTreeView *databaseView;
-
- QTreeView *deckView;
- KeySignals deckViewKeySignals;
- CardInfoFrameWidget *cardInfo;
- SearchLineEdit *searchEdit;
- KeySignals searchKeySignals;
-
- QLabel *nameLabel;
- LineEditUnfocusable *nameEdit;
- QLabel *commentsLabel;
- QTextEdit *commentsEdit;
- QLabel *hashLabel1;
- LineEditUnfocusable *hashLabel;
- FilterTreeModel *filterModel;
- QTreeView *filterView;
- KeySignals filterViewKeySignals;
- QWidget *filterBox;
-
- QMenu *deckMenu, *viewMenu, *cardInfoDockMenu, *deckDockMenu, *filterDockMenu, *analyzeDeckMenu,
- *saveDeckToClipboardMenu;
- QAction *aNewDeck, *aLoadDeck, *aSaveDeck, *aSaveDeckAs, *aLoadDeckFromClipboard, *aSaveDeckToClipboard,
- *aSaveDeckToClipboardRaw, *aPrintDeck, *aExportDeckDecklist, *aAnalyzeDeckDeckstats, *aAnalyzeDeckTappedout,
- *aClose;
- QAction *aClearFilterAll, *aClearFilterOne;
- QAction *aAddCard, *aAddCardToSideboard, *aRemoveCard, *aIncrement, *aDecrement;
- QAction *aResetLayout;
- QAction *aCardInfoDockVisible, *aCardInfoDockFloating, *aDeckDockVisible, *aDeckDockFloating, *aFilterDockVisible,
- *aFilterDockFloating;
-
- bool modified;
- QVBoxLayout *centralFrame;
- QHBoxLayout *searchLayout;
- QDockWidget *cardInfoDock;
- QDockWidget *deckDock;
- QDockWidget *filterDock;
- QWidget *centralWidget;
+ void dockVisibleTriggered() override;
+ void dockFloatingTriggered() override;
+ void dockTopLevelChanged(bool topLevel) override;
public:
- explicit TabDeckEditor(TabSupervisor *_tabSupervisor, QWidget *parent = nullptr);
- ~TabDeckEditor() override;
+ explicit TabDeckEditor(TabSupervisor *_tabSupervisor);
void retranslateUi() override;
QString getTabText() const override;
- void setDeck(DeckLoader *_deckLoader);
- void setModified(bool _windowModified);
- bool confirmClose();
- void createDeckDock();
- void createCardInfoDock();
- void createFiltersDock();
- void createMenus();
- void createCentralFrame();
+ void createMenus() override;
public slots:
- void closeRequest() override;
-signals:
- void openDeckEditor(const DeckLoader *deckLoader);
- void deckEditorClosing(TabDeckEditor *tab);
+ void showPrintingSelector() override;
};
#endif
diff --git a/cockatrice/src/client/tabs/tab_deck_storage.cpp b/cockatrice/src/client/tabs/tab_deck_storage.cpp
index 8ab91e2be..f696f607c 100644
--- a/cockatrice/src/client/tabs/tab_deck_storage.cpp
+++ b/cockatrice/src/client/tabs/tab_deck_storage.cpp
@@ -4,7 +4,6 @@
#include "../../server/pending_command.h"
#include "../../server/remote/remote_decklist_tree_widget.h"
#include "../../settings/cache_settings.h"
-#include "../game_logic/abstract_client.h"
#include "../get_text_with_max.h"
#include "decklist.h"
#include "pb/command_deck_del.pb.h"
@@ -19,6 +18,7 @@
#include
#include
#include
+#include
#include
#include
#include
@@ -27,9 +27,12 @@
#include
#include
#include
+#include
#include
-TabDeckStorage::TabDeckStorage(TabSupervisor *_tabSupervisor, AbstractClient *_client)
+TabDeckStorage::TabDeckStorage(TabSupervisor *_tabSupervisor,
+ AbstractClient *_client,
+ const ServerInfo_User *currentUserInfo)
: Tab(_tabSupervisor), client(_client)
{
localDirModel = new QFileSystemModel(this);
@@ -41,16 +44,33 @@ TabDeckStorage::TabDeckStorage(TabSupervisor *_tabSupervisor, AbstractClient *_c
localDirView->setColumnHidden(1, true);
localDirView->setRootIndex(localDirModel->index(localDirModel->rootPath(), 0));
localDirView->setSortingEnabled(true);
+ localDirView->setSelectionMode(QAbstractItemView::ExtendedSelection);
localDirView->header()->setSectionResizeMode(QHeaderView::ResizeToContents);
localDirView->header()->setSortIndicator(0, Qt::AscendingOrder);
- leftToolBar = new QToolBar;
+ connect(localDirView, &QTreeView::doubleClicked, this, &TabDeckStorage::actLocalDoubleClick);
+
+ // Left side layout
+ /* put an invisible dummy QToolBar in the leftmost column so that the main toolbar is centered.
+ * Really ugly workaround, but I couldn't figure out the proper way to make it centered */
+ QToolBar *dummyToolBar = new QToolBar(this);
+ QSizePolicy sizePolicy = dummyToolBar->sizePolicy();
+ sizePolicy.setRetainSizeWhenHidden(true);
+ dummyToolBar->setSizePolicy(sizePolicy);
+ dummyToolBar->setVisible(false);
+
+ leftToolBar = new QToolBar(this);
leftToolBar->setOrientation(Qt::Horizontal);
leftToolBar->setIconSize(QSize(32, 32));
- QHBoxLayout *leftToolBarLayout = new QHBoxLayout;
- leftToolBarLayout->addStretch();
- leftToolBarLayout->addWidget(leftToolBar);
- leftToolBarLayout->addStretch();
+
+ QToolBar *leftRightmostToolBar = new QToolBar(this);
+ leftRightmostToolBar->setOrientation(Qt::Horizontal);
+ leftRightmostToolBar->setIconSize(QSize(32, 32));
+
+ QGridLayout *leftToolBarLayout = new QGridLayout;
+ leftToolBarLayout->addWidget(dummyToolBar, 0, 0, Qt::AlignLeft);
+ leftToolBarLayout->addWidget(leftToolBar, 0, 1, Qt::AlignHCenter);
+ leftToolBarLayout->addWidget(leftRightmostToolBar, 0, 2, Qt::AlignRight);
QVBoxLayout *leftVbox = new QVBoxLayout;
leftVbox->addWidget(localDirView);
@@ -58,6 +78,7 @@ TabDeckStorage::TabDeckStorage(TabSupervisor *_tabSupervisor, AbstractClient *_c
leftGroupBox = new QGroupBox;
leftGroupBox->setLayout(leftVbox);
+ // Right side layout
rightToolBar = new QToolBar;
rightToolBar->setOrientation(Qt::Horizontal);
rightToolBar->setIconSize(QSize(32, 32));
@@ -68,25 +89,41 @@ TabDeckStorage::TabDeckStorage(TabSupervisor *_tabSupervisor, AbstractClient *_c
serverDirView = new RemoteDeckList_TreeWidget(client);
+ connect(serverDirView, &QTreeView::doubleClicked, this, &TabDeckStorage::actRemoteDoubleClick);
+
QVBoxLayout *rightVbox = new QVBoxLayout;
rightVbox->addWidget(serverDirView);
rightVbox->addLayout(rightToolBarLayout);
rightGroupBox = new QGroupBox;
rightGroupBox->setLayout(rightVbox);
+ // combine layouts
QHBoxLayout *hbox = new QHBoxLayout;
hbox->addWidget(leftGroupBox);
hbox->addWidget(rightGroupBox);
+ // Left side actions
aOpenLocalDeck = new QAction(this);
aOpenLocalDeck->setIcon(QPixmap("theme:icons/pencil"));
connect(aOpenLocalDeck, SIGNAL(triggered()), this, SLOT(actOpenLocalDeck()));
+ aRenameLocal = new QAction(this);
+ aRenameLocal->setIcon(QPixmap("theme:icons/rename"));
+ connect(aRenameLocal, &QAction::triggered, this, &TabDeckStorage::actRenameLocal);
aUpload = new QAction(this);
aUpload->setIcon(QPixmap("theme:icons/arrow_right_green"));
connect(aUpload, SIGNAL(triggered()), this, SLOT(actUpload()));
+ aNewLocalFolder = new QAction(this);
+ aNewLocalFolder->setIcon(qApp->style()->standardIcon(QStyle::SP_FileDialogNewFolder));
+ connect(aNewLocalFolder, &QAction::triggered, this, &TabDeckStorage::actNewLocalFolder);
aDeleteLocalDeck = new QAction(this);
aDeleteLocalDeck->setIcon(QPixmap("theme:icons/remove_row"));
connect(aDeleteLocalDeck, SIGNAL(triggered()), this, SLOT(actDeleteLocalDeck()));
+
+ aOpenDecksFolder = new QAction(this);
+ aOpenDecksFolder->setIcon(qApp->style()->standardIcon(QStyle::SP_DirOpenIcon));
+ connect(aOpenDecksFolder, &QAction::triggered, this, &TabDeckStorage::actOpenDecksFolder);
+
+ // Right side actions
aOpenRemoteDeck = new QAction(this);
aOpenRemoteDeck->setIcon(QPixmap("theme:icons/pencil"));
connect(aOpenRemoteDeck, SIGNAL(triggered()), this, SLOT(actOpenRemoteDeck()));
@@ -100,9 +137,15 @@ TabDeckStorage::TabDeckStorage(TabSupervisor *_tabSupervisor, AbstractClient *_c
aDeleteRemoteDeck->setIcon(QPixmap("theme:icons/remove_row"));
connect(aDeleteRemoteDeck, SIGNAL(triggered()), this, SLOT(actDeleteRemoteDeck()));
+ // Add actions to toolbars
leftToolBar->addAction(aOpenLocalDeck);
+ leftToolBar->addAction(aRenameLocal);
leftToolBar->addAction(aUpload);
+ leftToolBar->addAction(aNewLocalFolder);
leftToolBar->addAction(aDeleteLocalDeck);
+
+ leftRightmostToolBar->addAction(aOpenDecksFolder);
+
rightToolBar->addAction(aOpenRemoteDeck);
rightToolBar->addAction(aDownload);
rightToolBar->addAction(aNewFolder);
@@ -113,6 +156,10 @@ TabDeckStorage::TabDeckStorage(TabSupervisor *_tabSupervisor, AbstractClient *_c
QWidget *mainWidget = new QWidget(this);
mainWidget->setLayout(hbox);
setCentralWidget(mainWidget);
+
+ connect(client, &AbstractClient::userInfoChanged, this, &TabDeckStorage::handleConnected);
+ connect(client, &AbstractClient::statusChanged, this, &TabDeckStorage::handleConnectionChanged);
+ setRemoteEnabled(currentUserInfo && currentUserInfo->user_level() & ServerInfo_User::IsRegistered);
}
void TabDeckStorage::retranslateUi()
@@ -121,12 +168,15 @@ void TabDeckStorage::retranslateUi()
rightGroupBox->setTitle(tr("Server deck storage"));
aOpenLocalDeck->setText(tr("Open in deck editor"));
+ aRenameLocal->setText(tr("Rename deck or folder"));
aUpload->setText(tr("Upload deck"));
aOpenRemoteDeck->setText(tr("Open in deck editor"));
aDownload->setText(tr("Download deck"));
+ aNewLocalFolder->setText(tr("New folder"));
aNewFolder->setText(tr("New folder"));
aDeleteLocalDeck->setText(tr("Delete"));
aDeleteRemoteDeck->setText(tr("Delete"));
+ aOpenDecksFolder->setText(tr("Open decks folder"));
}
QString TabDeckStorage::getTargetPath() const
@@ -147,43 +197,119 @@ QString TabDeckStorage::getTargetPath() const
}
}
+void TabDeckStorage::handleConnected(const ServerInfo_User &userInfo)
+{
+ setRemoteEnabled(userInfo.user_level() & ServerInfo_User::IsRegistered);
+}
+
+/**
+ * This is only responsible for handling the disconnect. The connect is already handled elsewhere
+ */
+void TabDeckStorage::handleConnectionChanged(ClientStatus status)
+{
+ if (status == StatusDisconnected) {
+ setRemoteEnabled(false);
+ }
+}
+
+void TabDeckStorage::setRemoteEnabled(bool enabled)
+{
+ aUpload->setEnabled(enabled);
+ aOpenRemoteDeck->setEnabled(enabled);
+ aDownload->setEnabled(enabled);
+ aNewFolder->setEnabled(enabled);
+ aDeleteRemoteDeck->setEnabled(enabled);
+
+ if (enabled) {
+ serverDirView->refreshTree();
+ } else {
+ serverDirView->clearTree();
+ }
+}
+
+void TabDeckStorage::actLocalDoubleClick(const QModelIndex &curLeft)
+{
+ if (!localDirModel->isDir(curLeft)) {
+ actOpenLocalDeck();
+ }
+}
+
void TabDeckStorage::actOpenLocalDeck()
{
- QModelIndex curLeft = localDirView->selectionModel()->currentIndex();
- if (localDirModel->isDir(curLeft))
- return;
- QString filePath = localDirModel->filePath(curLeft);
+ QModelIndexList curLefts = localDirView->selectionModel()->selectedRows();
+ for (const auto &curLeft : curLefts) {
+ if (localDirModel->isDir(curLeft))
+ continue;
+ QString filePath = localDirModel->filePath(curLeft);
- DeckLoader deckLoader;
- if (!deckLoader.loadFromFile(filePath, DeckLoader::CockatriceFormat))
- return;
+ DeckLoader deckLoader;
+ if (!deckLoader.loadFromFile(filePath, DeckLoader::CockatriceFormat, true))
+ continue;
- emit openDeckEditor(&deckLoader);
+ emit openDeckEditor(&deckLoader);
+ }
+}
+
+void TabDeckStorage::actRenameLocal()
+{
+ QModelIndexList curLefts = localDirView->selectionModel()->selectedRows();
+ for (const auto &curLeft : curLefts) {
+ const QFileInfo info = localDirModel->fileInfo(curLeft);
+
+ const QString oldName = info.baseName();
+ const QString title = info.isDir() ? tr("Rename local folder") : tr("Rename local file");
+
+ bool ok;
+ QString newName = QInputDialog::getText(this, title, tr("New name:"), QLineEdit::Normal, oldName, &ok);
+ if (!ok) { // terminate all remaining selections if user cancels
+ return;
+ }
+ if (newName.isEmpty() || oldName == newName) {
+ continue;
+ }
+
+ QString newFileName = newName;
+ if (!info.suffix().isEmpty()) {
+ newFileName += "." + info.suffix();
+ }
+ const QString newFilePath = QFileInfo(info.dir(), newFileName).filePath();
+
+ if (!QFile::rename(info.filePath(), newFilePath)) {
+ QMessageBox::critical(this, tr("Error"), tr("Rename failed"));
+ }
+ }
}
void TabDeckStorage::actUpload()
{
- QModelIndex curLeft = localDirView->selectionModel()->currentIndex();
- if (localDirModel->isDir(curLeft))
+ QModelIndexList curLefts = localDirView->selectionModel()->selectedRows();
+ if (curLefts.isEmpty()) {
return;
+ }
+
QString targetPath = getTargetPath();
if (targetPath.length() > MAX_NAME_LENGTH) {
qCritical() << "target path to upload to is too long" << targetPath;
return;
}
- QString filePath = localDirModel->filePath(curLeft);
+ for (const auto &curLeft : curLefts) {
+ if (localDirModel->isDir(curLeft)) {
+ continue;
+ }
+
+ QString filePath = localDirModel->filePath(curLeft);
+ uploadDeck(filePath, targetPath);
+ }
+}
+
+void TabDeckStorage::uploadDeck(const QString &filePath, const QString &targetPath)
+{
QFile deckFile(filePath);
QFileInfo deckFileInfo(deckFile);
- QString deckString;
DeckLoader deck;
- bool error = !deck.loadFromFile(filePath, DeckLoader::CockatriceFormat);
- if (!error) {
- deckString = deck.writeToString_Native();
- error = deckString.length() > MAX_FILE_LENGTH;
- }
- if (error) {
+ if (!deck.loadFromFile(filePath, DeckLoader::CockatriceFormat)) {
QMessageBox::critical(this, tr("Error"), tr("Invalid deck file"));
return;
}
@@ -202,6 +328,12 @@ void TabDeckStorage::actUpload()
deck.setName(deck.getName().left(MAX_NAME_LENGTH));
}
+ QString deckString = deck.writeToString_Native();
+ if (deckString.length() > MAX_FILE_LENGTH) {
+ QMessageBox::critical(this, tr("Error"), tr("Invalid deck file"));
+ return;
+ }
+
Command_DeckUpload cmd;
cmd.set_path(targetPath.toStdString());
cmd.set_deck_list(deckString.toStdString());
@@ -226,34 +358,73 @@ void TabDeckStorage::uploadFinished(const Response &r, const CommandContainer &c
serverDirView->addFileToTree(resp.new_file(), serverDirView->getNodeByPath(QString::fromStdString(cmd.path())));
}
-void TabDeckStorage::actDeleteLocalDeck()
+void TabDeckStorage::actNewLocalFolder()
{
QModelIndex curLeft = localDirView->selectionModel()->currentIndex();
- if (localDirModel->isDir(curLeft))
+
+ QModelIndex dirIndex;
+ if (curLeft.isValid() && !localDirModel->isDir(curLeft)) {
+ dirIndex = curLeft.parent();
+ } else {
+ dirIndex = curLeft;
+ }
+
+ bool ok;
+ QString folderName =
+ QInputDialog::getText(this, tr("New folder"), tr("Name of new folder:"), QLineEdit::Normal, "", &ok);
+ if (!ok || folderName.isEmpty())
return;
- if (QMessageBox::warning(this, tr("Delete local file"),
- tr("Are you sure you want to delete \"%1\"?").arg(localDirModel->fileName(curLeft)),
+ localDirModel->mkdir(dirIndex, folderName);
+}
+
+void TabDeckStorage::actDeleteLocalDeck()
+{
+ const QModelIndexList curLefts = localDirView->selectionModel()->selectedRows();
+
+ if (curLefts.isEmpty()) {
+ return;
+ }
+
+ if (QMessageBox::warning(this, tr("Delete local file"), tr("Are you sure you want to delete the selected files?"),
QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes)
return;
- localDirModel->remove(curLeft);
+ for (const auto &curLeft : curLefts) {
+ if (curLeft.isValid()) {
+ localDirModel->remove(curLeft);
+ }
+ }
+}
+
+void TabDeckStorage::actOpenDecksFolder()
+{
+ QString dir = localDirModel->rootPath();
+ QDesktopServices::openUrl(QUrl::fromLocalFile(dir));
+}
+
+void TabDeckStorage::actRemoteDoubleClick(const QModelIndex &curRight)
+{
+ if (dynamic_cast(serverDirView->getNode(curRight))) {
+ actOpenRemoteDeck();
+ }
}
void TabDeckStorage::actOpenRemoteDeck()
{
- RemoteDeckList_TreeModel::FileNode *curRight =
- dynamic_cast(serverDirView->getCurrentItem());
- if (!curRight)
- return;
+ for (const auto &curRight : serverDirView->getCurrentSelection()) {
+ RemoteDeckList_TreeModel::FileNode *node = dynamic_cast(curRight);
+ if (!node)
+ continue;
- Command_DeckDownload cmd;
- cmd.set_deck_id(curRight->getId());
+ Command_DeckDownload cmd;
+ cmd.set_deck_id(node->getId());
- PendingCommand *pend = client->prepareSessionCommand(cmd);
- connect(pend, SIGNAL(finished(Response, CommandContainer, QVariant)), this,
- SLOT(openRemoteDeckFinished(Response, CommandContainer)));
- client->sendCommand(pend);
+ PendingCommand *pend = client->prepareSessionCommand(cmd);
+ connect(pend, SIGNAL(finished(Response, CommandContainer, QVariant)), this,
+ SLOT(openRemoteDeckFinished(Response, CommandContainer)));
+ client->sendCommand(pend);
+ }
}
void TabDeckStorage::openRemoteDeckFinished(const Response &r, const CommandContainer &commandContainer)
@@ -273,30 +444,46 @@ void TabDeckStorage::openRemoteDeckFinished(const Response &r, const CommandCont
void TabDeckStorage::actDownload()
{
- QString filePath;
QModelIndex curLeft = localDirView->selectionModel()->currentIndex();
- if (!curLeft.isValid())
- filePath = localDirModel->rootPath();
- else {
- while (!localDirModel->isDir(curLeft))
- curLeft = curLeft.parent();
- filePath = localDirModel->filePath(curLeft);
+ while (!localDirModel->isDir(curLeft)) {
+ curLeft = curLeft.parent();
}
- RemoteDeckList_TreeModel::FileNode *curRight =
- dynamic_cast(serverDirView->getCurrentItem());
- if (!curRight)
- return;
- filePath += QString("/deck_%1.cod").arg(curRight->getId());
+ for (const auto curRight : serverDirView->selectionModel()->selectedRows()) {
+ downloadNodeAtIndex(curLeft, curRight);
+ }
+}
- Command_DeckDownload cmd;
- cmd.set_deck_id(curRight->getId());
+void TabDeckStorage::downloadNodeAtIndex(const QModelIndex &curLeft, const QModelIndex &curRight)
+{
+ auto node = serverDirView->getNode(curRight);
+ if (const auto dirNode = dynamic_cast(node)) {
+ // node at index is a folder
+ const QString name = dirNode->getName();
- PendingCommand *pend = client->prepareSessionCommand(cmd);
- pend->setExtraData(filePath);
- connect(pend, SIGNAL(finished(Response, CommandContainer, QVariant)), this,
- SLOT(downloadFinished(Response, CommandContainer, QVariant)));
- client->sendCommand(pend);
+ const auto dirIndex = curLeft.isValid() ? curLeft : localDirModel->index(localDirModel->rootPath());
+ const auto newDirIndex = localDirModel->mkdir(dirIndex, name);
+
+ int rows = serverDirView->model()->rowCount(curRight);
+ for (int i = 0; i < rows; i++) {
+ const auto childIndex = serverDirView->model()->index(i, 0, curRight);
+ downloadNodeAtIndex(newDirIndex, childIndex);
+ }
+ } else if (const auto fileNode = dynamic_cast(node)) {
+ // node at index is a deck
+ const QString dirPath = curLeft.isValid() ? localDirModel->filePath(curLeft) : localDirModel->rootPath();
+ const QString filePath = dirPath + QString("/deck_%1.cod").arg(fileNode->getId());
+
+ Command_DeckDownload cmd;
+ cmd.set_deck_id(fileNode->getId());
+
+ PendingCommand *pend = client->prepareSessionCommand(cmd);
+ pend->setExtraData(filePath);
+ connect(pend, SIGNAL(finished(Response, CommandContainer, QVariant)), this,
+ SLOT(downloadFinished(Response, CommandContainer, QVariant)));
+ client->sendCommand(pend);
+ }
+ // node at index is invalid
}
void TabDeckStorage::downloadFinished(const Response &r,
@@ -352,12 +539,30 @@ void TabDeckStorage::newFolderFinished(const Response &response, const CommandCo
void TabDeckStorage::actDeleteRemoteDeck()
{
- PendingCommand *pend;
- RemoteDeckList_TreeModel::Node *curRight = serverDirView->getCurrentItem();
- if (!curRight)
+ auto curRights = serverDirView->getCurrentSelection();
+
+ if (curRights.isEmpty()) {
return;
- RemoteDeckList_TreeModel::DirectoryNode *dir = dynamic_cast(curRight);
- if (dir) {
+ }
+
+ if (QMessageBox::warning(this, tr("Delete remote decks"), tr("Are you sure you want to delete the selected decks?"),
+ QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes) {
+ return;
+ }
+
+ for (const auto &curRight : curRights) {
+ deleteRemoteDeck(curRight);
+ }
+}
+
+void TabDeckStorage::deleteRemoteDeck(const RemoteDeckList_TreeModel::Node *curRight)
+{
+ if (!curRight) {
+ return;
+ }
+
+ PendingCommand *pend;
+ if (const auto *dir = dynamic_cast(curRight)) {
QString targetPath = dir->getPath();
if (targetPath.isEmpty())
return;
@@ -365,22 +570,13 @@ void TabDeckStorage::actDeleteRemoteDeck()
qCritical() << "target path to delete is too long" << targetPath;
return;
}
- if (QMessageBox::warning(this, tr("Delete remote folder"),
- tr("Are you sure you want to delete \"%1\"?").arg(targetPath),
- QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes)
- return;
Command_DeckDelDir cmd;
cmd.set_path(targetPath.toStdString());
pend = client->prepareSessionCommand(cmd);
connect(pend, SIGNAL(finished(Response, CommandContainer, QVariant)), this,
SLOT(deleteFolderFinished(Response, CommandContainer)));
} else {
- RemoteDeckList_TreeModel::FileNode *deckNode = dynamic_cast(curRight);
- if (QMessageBox::warning(this, tr("Delete remote deck"),
- tr("Are you sure you want to delete \"%1\"?").arg(deckNode->getName()),
- QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes)
- return;
-
+ const auto *deckNode = dynamic_cast(curRight);
Command_DeckDel cmd;
cmd.set_deck_id(deckNode->getId());
pend = client->prepareSessionCommand(cmd);
diff --git a/cockatrice/src/client/tabs/tab_deck_storage.h b/cockatrice/src/client/tabs/tab_deck_storage.h
index 2c7d1e726..e207a1187 100644
--- a/cockatrice/src/client/tabs/tab_deck_storage.h
+++ b/cockatrice/src/client/tabs/tab_deck_storage.h
@@ -1,8 +1,11 @@
#ifndef TAB_DECK_STORAGE_H
#define TAB_DECK_STORAGE_H
+#include "../../server/remote/remote_decklist_tree_widget.h"
+#include "../game_logic/abstract_client.h"
#include "tab.h"
+class ServerInfo_User;
class AbstractClient;
class QTreeView;
class QFileSystemModel;
@@ -10,7 +13,6 @@ class QToolBar;
class QTreeWidget;
class QTreeWidgetItem;
class QGroupBox;
-class RemoteDeckList_TreeWidget;
class CommandContainer;
class Response;
class DeckLoader;
@@ -26,16 +28,36 @@ private:
RemoteDeckList_TreeWidget *serverDirView;
QGroupBox *leftGroupBox, *rightGroupBox;
- QAction *aOpenLocalDeck, *aUpload, *aDeleteLocalDeck, *aOpenRemoteDeck, *aDownload, *aNewFolder, *aDeleteRemoteDeck;
+ QAction *aOpenLocalDeck, *aRenameLocal, *aUpload, *aNewLocalFolder, *aDeleteLocalDeck;
+ QAction *aOpenDecksFolder;
+ QAction *aOpenRemoteDeck, *aDownload, *aNewFolder, *aDeleteRemoteDeck;
QString getTargetPath() const;
+
+ void setRemoteEnabled(bool enabled);
+
+ void uploadDeck(const QString &filePath, const QString &targetPath);
+ void deleteRemoteDeck(const RemoteDeckList_TreeModel::Node *node);
+
+ void downloadNodeAtIndex(const QModelIndex &curLeft, const QModelIndex &curRight);
+
private slots:
+ void handleConnected(const ServerInfo_User &userInfo);
+ void handleConnectionChanged(ClientStatus status);
+
+ void actLocalDoubleClick(const QModelIndex &curLeft);
void actOpenLocalDeck();
+ void actRenameLocal();
+
void actUpload();
void uploadFinished(const Response &r, const CommandContainer &commandContainer);
+ void actNewLocalFolder();
void actDeleteLocalDeck();
+ void actOpenDecksFolder();
+
+ void actRemoteDoubleClick(const QModelIndex &curRight);
void actOpenRemoteDeck();
void openRemoteDeckFinished(const Response &r, const CommandContainer &commandContainer);
@@ -50,11 +72,11 @@ private slots:
void deleteDeckFinished(const Response &response, const CommandContainer &commandContainer);
public:
- TabDeckStorage(TabSupervisor *_tabSupervisor, AbstractClient *_client);
- void retranslateUi();
- QString getTabText() const
+ TabDeckStorage(TabSupervisor *_tabSupervisor, AbstractClient *_client, const ServerInfo_User *currentUserInfo);
+ void retranslateUi() override;
+ QString getTabText() const override
{
- return tr("Deck storage");
+ return tr("Deck Storage");
}
signals:
void openDeckEditor(const DeckLoader *deckLoader);
diff --git a/cockatrice/src/client/tabs/tab_game.cpp b/cockatrice/src/client/tabs/tab_game.cpp
index a37e05567..8c018f644 100644
--- a/cockatrice/src/client/tabs/tab_game.cpp
+++ b/cockatrice/src/client/tabs/tab_game.cpp
@@ -1,43 +1,36 @@
#include "tab_game.h"
#include "../../client/ui/widgets/cards/card_info_frame_widget.h"
-#include "../../deck/deck_loader.h"
-#include "../../deck/deck_view.h"
#include "../../dialogs/dlg_create_game.h"
-#include "../../dialogs/dlg_load_remote_deck.h"
-#include "../../dialogs/dlg_manage_sets.h"
#include "../../game/board/arrow_item.h"
#include "../../game/cards/card_database.h"
#include "../../game/cards/card_database_manager.h"
#include "../../game/cards/card_item.h"
+#include "../../game/deckview/deck_view_container.h"
#include "../../game/game_scene.h"
#include "../../game/game_view.h"
#include "../../game/player/player.h"
#include "../../game/player/player_list_widget.h"
-#include "../../game/zones/view_zone.h"
-#include "../../game/zones/view_zone_widget.h"
+#include "../../game/zones/card_zone.h"
#include "../../main.h"
#include "../../server/message_log_widget.h"
#include "../../server/pending_command.h"
+#include "../../server/user/user_list_manager.h"
#include "../../settings/cache_settings.h"
#include "../game_logic/abstract_client.h"
#include "../network/replay_timeline_widget.h"
#include "../ui/line_edit_completer.h"
#include "../ui/phases_toolbar.h"
-#include "../ui/picture_loader.h"
+#include "../ui/picture_loader/picture_loader.h"
#include "../ui/window_main.h"
#include "get_pb_extension.h"
#include "pb/command_concede.pb.h"
-#include "pb/command_deck_select.pb.h"
#include "pb/command_delete_arrow.pb.h"
#include "pb/command_game_say.pb.h"
#include "pb/command_leave_game.pb.h"
#include "pb/command_next_turn.pb.h"
-#include "pb/command_ready_start.pb.h"
#include "pb/command_reverse_turn.pb.h"
#include "pb/command_set_active_phase.pb.h"
-#include "pb/command_set_sideboard_lock.pb.h"
-#include "pb/command_set_sideboard_plan.pb.h"
#include "pb/context_connection_state_changed.pb.h"
#include "pb/context_deck_select.pb.h"
#include "pb/context_ping_changed.pb.h"
@@ -55,7 +48,6 @@
#include "pb/event_set_active_player.pb.h"
#include "pb/game_event_container.pb.h"
#include "pb/game_replay.pb.h"
-#include "pb/response_deck_download.pb.h"
#include "tab_supervisor.h"
#include "trice_limits.h"
@@ -73,110 +65,237 @@
#include
#include
#include
-#include
-ToggleButton::ToggleButton(QWidget *parent) : QPushButton(parent), state(false)
+TabGame::TabGame(TabSupervisor *_tabSupervisor, GameReplay *_replay)
+ : Tab(_tabSupervisor), secondsElapsed(0), hostId(-1), localPlayerId(-1),
+ isLocalGame(_tabSupervisor->getIsLocalGame()), spectator(true), judge(false), gameStateKnown(false),
+ resuming(false), currentPhase(-1), activeCard(nullptr), gameClosed(false), replay(_replay), currentReplayStep(0),
+ sayLabel(nullptr), sayEdit(nullptr)
{
-}
+ // THIS CTOR IS USED ON REPLAY
+ gameInfo.CopyFrom(replay->game_info());
+ gameInfo.set_spectators_omniscient(true);
-void ToggleButton::paintEvent(QPaintEvent *event)
-{
- QPushButton::paintEvent(event);
+ // Create list: event number -> time [ms]
+ // Distribute simultaneous events evenly across 1 second.
+ unsigned int lastEventTimestamp = 0;
+ const int eventCount = replay->event_list_size();
+ for (int i = 0; i < eventCount; ++i) {
+ int j = i + 1;
+ while ((j < eventCount) && (replay->event_list(j).seconds_elapsed() == lastEventTimestamp))
+ ++j;
- QPainter painter(this);
- QPen pen;
- pen.setWidth(3);
- pen.setJoinStyle(Qt::MiterJoin);
- pen.setColor(state ? Qt::green : Qt::red);
- painter.setPen(pen);
- painter.drawRect(QRect(1, 1, width() - 3, height() - 3));
-}
+ const int numberEventsThisSecond = j - i;
+ for (int k = 0; k < numberEventsThisSecond; ++k)
+ replayTimeline.append(replay->event_list(i + k).seconds_elapsed() * 1000 +
+ (int)((qreal)k / (qreal)numberEventsThisSecond * 1000));
-void ToggleButton::setState(bool _state)
-{
- state = _state;
- emit stateChanged();
- update();
-}
-
-DeckViewContainer::DeckViewContainer(int _playerId, TabGame *parent)
- : QWidget(nullptr), parentGame(parent), playerId(_playerId)
-{
- loadLocalButton = new QPushButton;
- loadRemoteButton = new QPushButton;
- readyStartButton = new ToggleButton;
- readyStartButton->setEnabled(false);
- sideboardLockButton = new ToggleButton;
- sideboardLockButton->setEnabled(false);
-
- connect(loadLocalButton, SIGNAL(clicked()), this, SLOT(loadLocalDeck()));
- connect(readyStartButton, SIGNAL(clicked()), this, SLOT(readyStart()));
- connect(sideboardLockButton, SIGNAL(clicked()), this, SLOT(sideboardLockButtonClicked()));
- connect(sideboardLockButton, SIGNAL(stateChanged()), this, SLOT(updateSideboardLockButtonText()));
-
- if (parentGame->getIsLocalGame()) {
- loadRemoteButton->setEnabled(false);
- } else {
- connect(loadRemoteButton, SIGNAL(clicked()), this, SLOT(loadRemoteDeck()));
+ if (j < eventCount)
+ lastEventTimestamp = replay->event_list(j).seconds_elapsed();
+ i += numberEventsThisSecond - 1;
}
- auto *buttonHBox = new QHBoxLayout;
- buttonHBox->addWidget(loadLocalButton);
- buttonHBox->addWidget(loadRemoteButton);
- buttonHBox->addWidget(readyStartButton);
- buttonHBox->addWidget(sideboardLockButton);
- buttonHBox->setContentsMargins(0, 0, 0, 0);
- buttonHBox->addStretch();
- deckView = new DeckView;
- connect(deckView, SIGNAL(newCardAdded(AbstractCardItem *)), this, SIGNAL(newCardAdded(AbstractCardItem *)));
- connect(deckView, SIGNAL(sideboardPlanChanged()), this, SLOT(sideboardPlanChanged()));
+ createCardInfoDock(true);
+ createPlayerListDock(true);
+ createMessageDock(true);
+ createPlayAreaWidget(true);
+ createDeckViewContainerWidget(true);
+ createReplayDock();
- auto *deckViewLayout = new QVBoxLayout;
- deckViewLayout->addLayout(buttonHBox);
- deckViewLayout->addWidget(deckView);
- deckViewLayout->setContentsMargins(0, 0, 0, 0);
- setLayout(deckViewLayout);
+ addDockWidget(Qt::RightDockWidgetArea, cardInfoDock);
+ addDockWidget(Qt::RightDockWidgetArea, playerListDock);
+ addDockWidget(Qt::RightDockWidgetArea, messageLayoutDock);
+ addDockWidget(Qt::BottomDockWidgetArea, replayDock);
+ mainWidget = new QStackedWidget(this);
+ mainWidget->addWidget(deckViewContainerWidget);
+ mainWidget->addWidget(gamePlayAreaWidget);
+ setCentralWidget(mainWidget);
+
+ createReplayMenuItems();
+ createViewMenuItems();
retranslateUi();
connect(&SettingsCache::instance().shortcuts(), SIGNAL(shortCutChanged()), this, SLOT(refreshShortcuts()));
refreshShortcuts();
+ messageLog->logReplayStarted(gameInfo.game_id());
+
+ QTimer::singleShot(0, this, SLOT(loadLayout()));
}
-void DeckViewContainer::retranslateUi()
+TabGame::TabGame(TabSupervisor *_tabSupervisor,
+ QList &_clients,
+ const Event_GameJoined &event,
+ const QMap &_roomGameTypes)
+ : Tab(_tabSupervisor), userListProxy(_tabSupervisor->getUserListManager()), clients(_clients),
+ gameInfo(event.game_info()), roomGameTypes(_roomGameTypes), hostId(event.host_id()),
+ localPlayerId(event.player_id()), isLocalGame(_tabSupervisor->getIsLocalGame()), spectator(event.spectator()),
+ judge(event.judge()), gameStateKnown(false), resuming(event.resuming()), currentPhase(-1), activeCard(nullptr),
+ gameClosed(false), replay(nullptr), replayPlayButton(nullptr), replayFastForwardButton(nullptr),
+ aReplaySkipForward(nullptr), aReplaySkipBackward(nullptr), aReplaySkipForwardBig(nullptr),
+ aReplaySkipBackwardBig(nullptr), replayDock(nullptr)
{
- loadLocalButton->setText(tr("Load deck..."));
- loadRemoteButton->setText(tr("Load remote deck..."));
- readyStartButton->setText(tr("Ready to start"));
- updateSideboardLockButtonText();
+ // THIS CTOR IS USED ON GAMES
+ gameInfo.set_started(false);
+
+ createCardInfoDock();
+ createPlayerListDock();
+ createMessageDock();
+ createPlayAreaWidget();
+ createDeckViewContainerWidget();
+
+ addDockWidget(Qt::RightDockWidgetArea, cardInfoDock);
+ addDockWidget(Qt::RightDockWidgetArea, playerListDock);
+ addDockWidget(Qt::RightDockWidgetArea, messageLayoutDock);
+
+ mainWidget = new QStackedWidget(this);
+ mainWidget->addWidget(deckViewContainerWidget);
+ mainWidget->addWidget(gamePlayAreaWidget);
+ mainWidget->setContentsMargins(0, 0, 0, 0);
+ setCentralWidget(mainWidget);
+
+ createMenuItems();
+ createViewMenuItems();
+ retranslateUi();
+ connect(&SettingsCache::instance().shortcuts(), SIGNAL(shortCutChanged()), this, SLOT(refreshShortcuts()));
+ refreshShortcuts();
+
+ // append game to rooms game list for others to see
+ for (int i = gameInfo.game_types_size() - 1; i >= 0; i--)
+ gameTypes.append(roomGameTypes.find(gameInfo.game_types(i)).value());
+
+ QTimer::singleShot(0, this, SLOT(loadLayout()));
}
-void DeckViewContainer::setButtonsVisible(bool _visible)
+void TabGame::addMentionTag(const QString &value)
{
- loadLocalButton->setVisible(_visible);
- loadRemoteButton->setVisible(_visible);
- readyStartButton->setVisible(_visible);
- sideboardLockButton->setVisible(_visible);
+ sayEdit->insert(value + " ");
+ sayEdit->setFocus();
}
-void DeckViewContainer::updateSideboardLockButtonText()
+void TabGame::linkCardToChat(const QString &cardName)
{
- if (sideboardLockButton->getState()) {
- sideboardLockButton->setText(tr("Sideboard unlocked"));
- } else {
- sideboardLockButton->setText(tr("Sideboard locked"));
+ sayEdit->insert("[[" + cardName + "]] ");
+ sayEdit->setFocus();
+}
+
+void TabGame::emitUserEvent()
+{
+ bool globalEvent = !spectator || SettingsCache::instance().getSpectatorNotificationsEnabled();
+ emit userEvent(globalEvent);
+ updatePlayerListDockTitle();
+}
+
+TabGame::~TabGame()
+{
+ delete replay;
+}
+
+void TabGame::updatePlayerListDockTitle()
+{
+ QString tabText = " | " + (replay ? tr("Replay") : tr("Game")) + " #" + QString::number(gameInfo.game_id());
+ QString userCountInfo = QString(" %1/%2").arg(players.size()).arg(gameInfo.max_players());
+ playerListDock->setWindowTitle(tr("Player List") + userCountInfo +
+ (playerListDock->isWindow() ? tabText : QString()));
+}
+
+bool TabGame::isMainPlayerConceded() const
+{
+ Player *player = players.value(localPlayerId, nullptr);
+ return player && player->getConceded();
+}
+
+void TabGame::retranslateUi()
+{
+ QString tabText = " | " + (replay ? tr("Replay") : tr("Game")) + " #" + QString::number(gameInfo.game_id());
+
+ updatePlayerListDockTitle();
+ cardInfoDock->setWindowTitle(tr("Card Info") + (cardInfoDock->isWindow() ? tabText : QString()));
+ messageLayoutDock->setWindowTitle(tr("Messages") + (messageLayoutDock->isWindow() ? tabText : QString()));
+ if (replayDock)
+ replayDock->setWindowTitle(tr("Replay Timeline") + (replayDock->isWindow() ? tabText : QString()));
+
+ if (phasesMenu) {
+ for (int i = 0; i < phaseActions.size(); ++i)
+ phaseActions[i]->setText(phasesToolbar->getLongPhaseName(i));
+ phasesMenu->setTitle(tr("&Phases"));
}
- // setting text on a button removes its shortcut
- ShortcutsSettings &shortcuts = SettingsCache::instance().shortcuts();
- sideboardLockButton->setShortcut(shortcuts.getSingleShortcut("DeckViewContainer/sideboardLockButton"));
-}
-void DeckViewContainer::refreshShortcuts()
-{
- ShortcutsSettings &shortcuts = SettingsCache::instance().shortcuts();
- loadLocalButton->setShortcut(shortcuts.getSingleShortcut("DeckViewContainer/loadLocalButton"));
- loadRemoteButton->setShortcut(shortcuts.getSingleShortcut("DeckViewContainer/loadRemoteButton"));
- readyStartButton->setShortcut(shortcuts.getSingleShortcut("DeckViewContainer/readyStartButton"));
- sideboardLockButton->setShortcut(shortcuts.getSingleShortcut("DeckViewContainer/sideboardLockButton"));
+ gameMenu->setTitle(tr("&Game"));
+ if (aNextPhase) {
+ aNextPhase->setText(tr("Next &phase"));
+ }
+ if (aNextPhaseAction) {
+ aNextPhaseAction->setText(tr("Next phase with &action"));
+ }
+ if (aNextTurn) {
+ aNextTurn->setText(tr("Next &turn"));
+ }
+ if (aReverseTurn) {
+ aReverseTurn->setText(tr("Reverse turn order"));
+ }
+ if (aRemoveLocalArrows) {
+ aRemoveLocalArrows->setText(tr("&Remove all local arrows"));
+ }
+ if (aRotateViewCW) {
+ aRotateViewCW->setText(tr("Rotate View Cl&ockwise"));
+ }
+ if (aRotateViewCCW) {
+ aRotateViewCCW->setText(tr("Rotate View Co&unterclockwise"));
+ }
+ if (aGameInfo)
+ aGameInfo->setText(tr("Game &information"));
+ if (aConcede) {
+ if (isMainPlayerConceded()) {
+ aConcede->setText(tr("Un&concede"));
+ } else {
+ aConcede->setText(tr("&Concede"));
+ }
+ }
+ if (aLeaveGame) {
+ aLeaveGame->setText(tr("&Leave game"));
+ }
+ if (aCloseReplay) {
+ aCloseReplay->setText(tr("C&lose replay"));
+ }
+ if (aFocusChat) {
+ aFocusChat->setText(tr("&Focus Chat"));
+ }
+ if (sayLabel) {
+ sayLabel->setText(tr("&Say:"));
+ }
+
+ viewMenu->setTitle(tr("&View"));
+ cardInfoDockMenu->setTitle(tr("Card Info"));
+ messageLayoutDockMenu->setTitle(tr("Messages"));
+ playerListDockMenu->setTitle(tr("Player List"));
+
+ aCardInfoDockVisible->setText(tr("Visible"));
+ aCardInfoDockFloating->setText(tr("Floating"));
+
+ aMessageLayoutDockVisible->setText(tr("Visible"));
+ aMessageLayoutDockFloating->setText(tr("Floating"));
+
+ aPlayerListDockVisible->setText(tr("Visible"));
+ aPlayerListDockFloating->setText(tr("Floating"));
+
+ if (replayDock) {
+ replayDockMenu->setTitle(tr("Replay Timeline"));
+ aReplayDockVisible->setText(tr("Visible"));
+ aReplayDockFloating->setText(tr("Floating"));
+ }
+
+ aResetLayout->setText(tr("Reset layout"));
+
+ cardInfoFrameWidget->retranslateUi();
+
+ QMapIterator i(players);
+ while (i.hasNext())
+ i.next().value()->retranslateUi();
+ QMapIterator j(deckViewContainers);
+ while (j.hasNext())
+ j.next().value()->retranslateUi();
+
+ scene->retranslateUi();
}
void TabGame::refreshShortcuts()
@@ -278,341 +397,15 @@ void TabGame::refreshShortcuts()
}
}
-void DeckViewContainer::loadLocalDeck()
+void TabGame::closeRequest(bool forced)
{
- QFileDialog dialog(this, tr("Load deck"));
- dialog.setDirectory(SettingsCache::instance().getDeckPath());
- dialog.setNameFilters(DeckLoader::fileNameFilters);
- if (!dialog.exec())
- return;
-
- QString fileName = dialog.selectedFiles().at(0);
- DeckLoader::FileFormat fmt = DeckLoader::getFormatFromName(fileName);
- QString deckString;
- DeckLoader deck;
-
- bool error = !deck.loadFromFile(fileName, fmt);
- if (!error) {
- deckString = deck.writeToString_Native();
- error = deckString.length() > MAX_FILE_LENGTH;
- }
- if (error) {
- QMessageBox::critical(this, tr("Error"), tr("The selected file could not be loaded."));
+ if (!forced && !leaveGame()) {
return;
}
- Command_DeckSelect cmd;
- cmd.set_deck(deckString.toStdString());
- PendingCommand *pend = parentGame->prepareGameCommand(cmd);
- connect(pend, SIGNAL(finished(Response, CommandContainer, QVariant)), this,
- SLOT(deckSelectFinished(const Response &)));
- parentGame->sendGameCommand(pend, playerId);
-}
-
-void DeckViewContainer::loadRemoteDeck()
-{
- DlgLoadRemoteDeck dlg(parentGame->getClientForPlayer(playerId), this);
- if (dlg.exec()) {
- Command_DeckSelect cmd;
- cmd.set_deck_id(dlg.getDeckId());
- PendingCommand *pend = parentGame->prepareGameCommand(cmd);
- connect(pend, SIGNAL(finished(Response, CommandContainer, QVariant)), this,
- SLOT(deckSelectFinished(const Response &)));
- parentGame->sendGameCommand(pend, playerId);
- }
-}
-
-void DeckViewContainer::deckSelectFinished(const Response &r)
-{
- const Response_DeckDownload &resp = r.GetExtension(Response_DeckDownload::ext);
- DeckLoader newDeck(QString::fromStdString(resp.deck()));
- // TODO CHANGE THIS TO BE SELECTED BY UUID
- PictureLoader::cacheCardPixmaps(CardDatabaseManager::getInstance()->getCards(newDeck.getCardList()));
- setDeck(newDeck);
-}
-
-void DeckViewContainer::readyStart()
-{
- Command_ReadyStart cmd;
- cmd.set_ready(!readyStartButton->getState());
- parentGame->sendGameCommand(cmd, playerId);
-}
-
-void DeckViewContainer::sideboardLockButtonClicked()
-{
- Command_SetSideboardLock cmd;
- cmd.set_locked(sideboardLockButton->getState());
-
- parentGame->sendGameCommand(cmd, playerId);
-}
-
-void DeckViewContainer::sideboardPlanChanged()
-{
- Command_SetSideboardPlan cmd;
- const QList &newPlan = deckView->getSideboardPlan();
- for (const auto &i : newPlan)
- cmd.add_move_list()->CopyFrom(i);
- parentGame->sendGameCommand(cmd, playerId);
-}
-
-void DeckViewContainer::setReadyStart(bool ready)
-{
- readyStartButton->setState(ready);
- deckView->setLocked(ready || !sideboardLockButton->getState());
- sideboardLockButton->setEnabled(!readyStartButton->getState() && readyStartButton->isEnabled());
-}
-
-void DeckViewContainer::setSideboardLocked(bool locked)
-{
- sideboardLockButton->setState(!locked);
- deckView->setLocked(readyStartButton->getState() || !sideboardLockButton->getState());
- if (locked)
- deckView->resetSideboardPlan();
-}
-
-void DeckViewContainer::setDeck(const DeckLoader &deck)
-{
- deckView->setDeck(deck);
- readyStartButton->setEnabled(true);
- sideboardLockButton->setState(false);
- sideboardLockButton->setEnabled(true);
-}
-
-TabGame::TabGame(TabSupervisor *_tabSupervisor, GameReplay *_replay)
- : Tab(_tabSupervisor), secondsElapsed(0), hostId(-1), localPlayerId(-1),
- isLocalGame(_tabSupervisor->getIsLocalGame()), spectator(true), judge(false), gameStateKnown(false),
- resuming(false), currentPhase(-1), activeCard(nullptr), gameClosed(false), replay(_replay), currentReplayStep(0),
- sayLabel(nullptr), sayEdit(nullptr)
-{
- // THIS CTOR IS USED ON REPLAY
- gameInfo.CopyFrom(replay->game_info());
- gameInfo.set_spectators_omniscient(true);
-
- // Create list: event number -> time [ms]
- // Distribute simultaneous events evenly across 1 second.
- unsigned int lastEventTimestamp = 0;
- const int eventCount = replay->event_list_size();
- for (int i = 0; i < eventCount; ++i) {
- int j = i + 1;
- while ((j < eventCount) && (replay->event_list(j).seconds_elapsed() == lastEventTimestamp))
- ++j;
-
- const int numberEventsThisSecond = j - i;
- for (int k = 0; k < numberEventsThisSecond; ++k)
- replayTimeline.append(replay->event_list(i + k).seconds_elapsed() * 1000 +
- (int)((qreal)k / (qreal)numberEventsThisSecond * 1000));
-
- if (j < eventCount)
- lastEventTimestamp = replay->event_list(j).seconds_elapsed();
- i += numberEventsThisSecond - 1;
- }
-
- createCardInfoDock(true);
- createPlayerListDock(true);
- createMessageDock(true);
- createPlayAreaWidget(true);
- createDeckViewContainerWidget(true);
- createReplayDock();
-
- addDockWidget(Qt::RightDockWidgetArea, cardInfoDock);
- addDockWidget(Qt::RightDockWidgetArea, playerListDock);
- addDockWidget(Qt::RightDockWidgetArea, messageLayoutDock);
- addDockWidget(Qt::BottomDockWidgetArea, replayDock);
-
- mainWidget = new QStackedWidget(this);
- mainWidget->addWidget(deckViewContainerWidget);
- mainWidget->addWidget(gamePlayAreaWidget);
- setCentralWidget(mainWidget);
-
- createReplayMenuItems();
- createViewMenuItems();
- retranslateUi();
- connect(&SettingsCache::instance().shortcuts(), SIGNAL(shortCutChanged()), this, SLOT(refreshShortcuts()));
- refreshShortcuts();
- messageLog->logReplayStarted(gameInfo.game_id());
-
- this->installEventFilter(this);
- QTimer::singleShot(0, this, SLOT(loadLayout()));
-}
-
-TabGame::TabGame(TabSupervisor *_tabSupervisor,
- QList &_clients,
- const Event_GameJoined &event,
- const QMap &_roomGameTypes)
- : Tab(_tabSupervisor), clients(_clients), gameInfo(event.game_info()), roomGameTypes(_roomGameTypes),
- hostId(event.host_id()), localPlayerId(event.player_id()), isLocalGame(_tabSupervisor->getIsLocalGame()),
- spectator(event.spectator()), judge(event.judge()), gameStateKnown(false), resuming(event.resuming()),
- currentPhase(-1), activeCard(nullptr), gameClosed(false), replay(nullptr), replayPlayButton(nullptr),
- replayFastForwardButton(nullptr), aReplaySkipForward(nullptr), aReplaySkipBackward(nullptr),
- aReplaySkipForwardBig(nullptr), aReplaySkipBackwardBig(nullptr), replayDock(nullptr)
-{
- // THIS CTOR IS USED ON GAMES
- gameInfo.set_started(false);
-
- createCardInfoDock();
- createPlayerListDock();
- createMessageDock();
- createPlayAreaWidget();
- createDeckViewContainerWidget();
-
- addDockWidget(Qt::RightDockWidgetArea, cardInfoDock);
- addDockWidget(Qt::RightDockWidgetArea, playerListDock);
- addDockWidget(Qt::RightDockWidgetArea, messageLayoutDock);
-
- mainWidget = new QStackedWidget(this);
- mainWidget->addWidget(deckViewContainerWidget);
- mainWidget->addWidget(gamePlayAreaWidget);
- mainWidget->setContentsMargins(0, 0, 0, 0);
- setCentralWidget(mainWidget);
-
- createMenuItems();
- createViewMenuItems();
- retranslateUi();
- connect(&SettingsCache::instance().shortcuts(), SIGNAL(shortCutChanged()), this, SLOT(refreshShortcuts()));
- refreshShortcuts();
-
- // append game to rooms game list for others to see
- for (int i = gameInfo.game_types_size() - 1; i >= 0; i--)
- gameTypes.append(roomGameTypes.find(gameInfo.game_types(i)).value());
-
- this->installEventFilter(this);
- QTimer::singleShot(0, this, SLOT(loadLayout()));
-}
-
-void TabGame::addMentionTag(const QString &value)
-{
- sayEdit->insert(value + " ");
- sayEdit->setFocus();
-}
-
-void TabGame::linkCardToChat(const QString &cardName)
-{
- sayEdit->insert("[[" + cardName + "]] ");
- sayEdit->setFocus();
-}
-
-void TabGame::emitUserEvent()
-{
- bool globalEvent = !spectator || SettingsCache::instance().getSpectatorNotificationsEnabled();
- emit userEvent(globalEvent);
- updatePlayerListDockTitle();
-}
-
-TabGame::~TabGame()
-{
- delete replay;
-
- QMapIterator i(players);
- while (i.hasNext()) {
- delete i.next().value();
- }
-
- players.clear();
-
emit gameClosing(this);
-}
-void TabGame::updatePlayerListDockTitle()
-{
- QString tabText = " | " + (replay ? tr("Replay") : tr("Game")) + " #" + QString::number(gameInfo.game_id());
- QString userCountInfo = QString(" %1/%2").arg(players.size()).arg(gameInfo.max_players());
- playerListDock->setWindowTitle(tr("Player List") + userCountInfo +
- (playerListDock->isWindow() ? tabText : QString()));
-}
-
-void TabGame::retranslateUi()
-{
- QString tabText = " | " + (replay ? tr("Replay") : tr("Game")) + " #" + QString::number(gameInfo.game_id());
-
- updatePlayerListDockTitle();
- cardInfoDock->setWindowTitle(tr("Card Info") + (cardInfoDock->isWindow() ? tabText : QString()));
- messageLayoutDock->setWindowTitle(tr("Messages") + (messageLayoutDock->isWindow() ? tabText : QString()));
- if (replayDock)
- replayDock->setWindowTitle(tr("Replay Timeline") + (replayDock->isWindow() ? tabText : QString()));
-
- if (phasesMenu) {
- for (int i = 0; i < phaseActions.size(); ++i)
- phaseActions[i]->setText(phasesToolbar->getLongPhaseName(i));
- phasesMenu->setTitle(tr("&Phases"));
- }
-
- gameMenu->setTitle(tr("&Game"));
- if (aNextPhase) {
- aNextPhase->setText(tr("Next &phase"));
- }
- if (aNextPhaseAction) {
- aNextPhaseAction->setText(tr("Next phase with &action"));
- }
- if (aNextTurn) {
- aNextTurn->setText(tr("Next &turn"));
- }
- if (aReverseTurn) {
- aReverseTurn->setText(tr("Reverse turn order"));
- }
- if (aRemoveLocalArrows) {
- aRemoveLocalArrows->setText(tr("&Remove all local arrows"));
- }
- if (aRotateViewCW) {
- aRotateViewCW->setText(tr("Rotate View Cl&ockwise"));
- }
- if (aRotateViewCCW) {
- aRotateViewCCW->setText(tr("Rotate View Co&unterclockwise"));
- }
- if (aGameInfo)
- aGameInfo->setText(tr("Game &information"));
- if (aConcede) {
- aConcede->setText(tr("&Concede"));
- }
- if (aLeaveGame) {
- aLeaveGame->setText(tr("&Leave game"));
- }
- if (aCloseReplay) {
- aCloseReplay->setText(tr("C&lose replay"));
- }
- if (aFocusChat) {
- aFocusChat->setText(tr("&Focus Chat"));
- }
- if (sayLabel) {
- sayLabel->setText(tr("&Say:"));
- }
-
- viewMenu->setTitle(tr("&View"));
- cardInfoDockMenu->setTitle(tr("Card Info"));
- messageLayoutDockMenu->setTitle(tr("Messages"));
- playerListDockMenu->setTitle(tr("Player List"));
-
- aCardInfoDockVisible->setText(tr("Visible"));
- aCardInfoDockFloating->setText(tr("Floating"));
-
- aMessageLayoutDockVisible->setText(tr("Visible"));
- aMessageLayoutDockFloating->setText(tr("Floating"));
-
- aPlayerListDockVisible->setText(tr("Visible"));
- aPlayerListDockFloating->setText(tr("Floating"));
-
- if (replayDock) {
- replayDockMenu->setTitle(tr("Replay Timeline"));
- aReplayDockVisible->setText(tr("Visible"));
- aReplayDockFloating->setText(tr("Floating"));
- }
-
- aResetLayout->setText(tr("Reset layout"));
-
- cardInfoFrameWidget->retranslateUi();
-
- QMapIterator i(players);
- while (i.hasNext())
- i.next().value()->retranslateUi();
- QMapIterator