Pre-compiled binaries we serve:
- - Windows 7+
- Windows 10+
- - macOS 10.15+ ("Catalina")
- - macOS 13+ ("Ventura")
- - Ubuntu 18.04 LTS ("Bionic Beaver")
- - Ubuntu 20.04 LTS ("Focal Fossa")
- - Ubuntu 22.04 LTS ("Jammy Jellyfish")
+ - Windows 7+
+ - macOS 14+ ("Sonoma") / Apple M
+ - macOS 13+ ("Ventura") / Intel
- Ubuntu 24.04 LTS ("Noble Numbat")
- - Debian 11 ("Bullseye")
+ - Ubuntu 22.04 LTS ("Jammy Jellyfish")
+ - Ubuntu 20.04 LTS ("Focal Fossa")
- Debian 12 ("Bookworm")
- - Fedora 39
+ - Debian 11 ("Bullseye")
+ - Fedora 41
- Fedora 40
-We are also packaged in Arch Linux's official community repository, courtesy of @FFY00
-General Linux support is available via a flatpak package (Flathub)
+
+We are also packaged in Arch Linux's official "extra" repository, courtesy of @FFY00
+General Linux support is available via a flatpak package (Flathub)
@@ -29,22 +29,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...)
@@ -61,14 +63,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
@@ -89,5 +91,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/workflows/desktop-build.yml b/.github/workflows/desktop-build.yml
index 0300ab4a4..99c05deee 100644
--- a/.github/workflows/desktop-build.yml
+++ b/.github/workflows/desktop-build.yml
@@ -37,13 +37,13 @@ jobs:
shell: bash
run: |
tag_regex='^refs/tags/'
- if [[ $GITHUB_EVENT_NAME == pull-request ]]; then # pull request
+ if [[ $GITHUB_EVENT_NAME == pull-request ]]; then # pull request
sha="${{github.event.pull_request.head.sha}}"
- elif [[ $GITHUB_REF =~ $tag_regex ]]; then # release
+ elif [[ $GITHUB_REF =~ $tag_regex ]]; then # release
sha="$GITHUB_SHA"
tag="${GITHUB_REF/refs\/tags\//}"
echo "tag=$tag" >>"$GITHUB_OUTPUT"
- else # push to branch
+ else # push to branch
sha="$GITHUB_SHA"
fi
echo "sha=$sha" >>"$GITHUB_OUTPUT"
@@ -85,46 +85,52 @@ jobs:
strategy:
fail-fast: false
matrix:
- # these names correspond to the files in .ci/$distro
+ # These names correspond to the files in ".ci/$distro$version"
include:
- - distro: ArchLinux
- package: skip # we are packaged in arch already
+ - distro: Arch
+ package: skip # We are packaged in Arch already
allow-failure: yes
- - distro: Debian11
+ - distro: Debian
+ version: 11
+ package: DEB
+ test: skip # Running tests on all distros is superfluous
+
+ - distro: Debian
+ version: 12
package: DEB
- - distro: Debian12
- package: DEB
+ - distro: Fedora
+ version: 40
+ package: RPM
+ test: skip # Running tests on all distros is superfluous
- - distro: Fedora39
+ - distro: Fedora
+ version: 41
package: RPM
- - distro: Fedora40
- package: RPM
+ - distro: Ubuntu
+ version: 20.04
+ package: DEB
+ test: skip # Ubuntu 20.04 has a broken Qt for debug builds
- - distro: UbuntuBionic
+ - distro: Ubuntu
+ version: 22.04
+ package: DEB
+ test: skip # Running tests on all distros is superfluous
+
+ - distro: Ubuntu
+ version: 24.04
package: DEB
- - distro: UbuntuFocal
- package: DEB
- test: skip # UbuntuFocal has a broken qt for debug builds
-
- - distro: UbuntuJammy
- package: DEB
- test: skip # running tests on all distros is superfluous
-
- - distro: UbuntuNoble
- package: DEB
-
- name: ${{matrix.distro}}
+ name: ${{matrix.distro}} ${{matrix.version}}
needs: configure
runs-on: ubuntu-latest
continue-on-error: ${{matrix.allow-failure == 'yes'}}
env:
- NAME: ${{matrix.distro}}
- CACHE: /tmp/${{matrix.distro}}-cache # ${{runner.temp}} does not work?
- # cache size over the entire repo is 10Gi link:
+ NAME: ${{matrix.distro}}${{matrix.version}}
+ CACHE: /tmp/${{matrix.distro}}${{matrix.version}}-cache # ${{runner.temp}} does not work?
+ # Cache size over the entire repo is 10Gi:
# https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#usage-limits-and-eviction-policy
CCACHE_SIZE: 200M
@@ -132,7 +138,7 @@ jobs:
- name: Checkout
uses: actions/checkout@v4
- - name: Get cache timestamp
+ - name: Generate cache timestamp
id: cache_timestamp
shell: bash
run: echo "timestamp=$(date -u '+%Y%m%d%H%M%S')" >>"$GITHUB_OUTPUT"
@@ -143,19 +149,17 @@ jobs:
timestamp: ${{steps.cache_timestamp.outputs.timestamp}}
with:
path: ${{env.CACHE}}
- key: docker-${{matrix.distro}}-cache-${{env.timestamp}}
+ key: docker-${{matrix.distro}}${{matrix.version}}-cache-${{env.timestamp}}
restore-keys: |
- docker-${{matrix.distro}}-cache-
+ docker-${{matrix.distro}}${{matrix.version}}-cache-
- - name: Build ${{matrix.distro}} Docker image
+ - name: Build ${{matrix.distro}} ${{matrix.version}} Docker image
shell: bash
run: source .ci/docker.sh --build
- name: Build debug and test
if: matrix.test != 'skip'
shell: bash
- env:
- distro: '${{matrix.distro}}'
run: |
source .ci/docker.sh
RUN --server --debug --test --ccache "$CCACHE_SIZE" --parallel 4
@@ -166,8 +170,7 @@ jobs:
shell: bash
env:
BUILD_DIR: build
- SUFFIX: '-${{matrix.distro}}'
- distro: '${{matrix.distro}}'
+ SUFFIX: '-${{matrix.distro}}${{matrix.version}}'
type: '${{matrix.package}}'
run: |
source .ci/docker.sh
@@ -179,7 +182,7 @@ jobs:
if: matrix.package != 'skip'
uses: actions/upload-artifact@v4
with:
- name: ${{matrix.distro}}-package
+ name: ${{matrix.distro}}${{matrix.version}}-package
path: ${{steps.build.outputs.path}}
if-no-files-found: error
@@ -198,24 +201,30 @@ jobs:
fail-fast: false
matrix:
include:
- - target: 12_Monterey_and_13_Ventura
- os: macos-12
- xcode: "14.0.1"
+ - target: 13
+ soc: Intel
+ os: macos-13
+ xcode: "14.3.1"
type: Release
+ core_count: 4
make_package: 1
- - target: 14_Sonoma
+ - target: 14
+ soc: Apple
os: macos-14
xcode: "15.4"
type: Release
+ core_count: 3
make_package: 1
- - target: 14_Sonoma_Debug
+ - target: 14
+ soc: Apple
os: macos-14
xcode: "15.4"
type: Debug
+ core_count: 3
- name: macOS${{matrix.target}}
+ name: macOS ${{matrix.target}}${{ matrix.soc == 'Intel' && ' Intel' || '' }}${{ matrix.type == 'Debug' && ' Debug' || '' }}
needs: configure
runs-on: ${{matrix.os}}
continue-on-error: ${{matrix.allow-failure == 'yes'}}
@@ -229,8 +238,8 @@ jobs:
- name: Install dependencies using Homebrew
shell: bash
- # cmake cannot find the mysql connector
- # neither of these works: mariadb-connector-c mysql-connector-c++
+ # CMake cannot find the MySQL connector
+ # Neither of these works: mariadb-connector-c mysql-connector-c++
env:
HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK: 1
run: |
@@ -244,16 +253,17 @@ jobs:
BUILDTYPE: '${{matrix.type}}'
MAKE_TEST: 1
MAKE_PACKAGE: '${{matrix.make_package}}'
- PACKAGE_SUFFIX: '-macOS-${{matrix.target}}'
- # macOS runner actually have only 3 cores
- # See https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners/about-github-hosted-runners#supported-runners-and-hardware-resources
- run: .ci/compile.sh --server --parallel 3
+ PACKAGE_SUFFIX: '-macOS${{matrix.target}}_${{matrix.soc}}'
+ # 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}}
- name: Upload artifact
if: matrix.make_package
uses: actions/upload-artifact@v4
with:
- name: macOS-${{matrix.target}}-dmg
+ name: macOS${{matrix.target}}${{ matrix.soc == 'Intel' && '_Intel' || '' }}${{ matrix.type == 'Debug' && '_Debug' || '' }}-dmg
path: ${{steps.build.outputs.path}}
if-no-files-found: error
diff --git a/.github/workflows/translations-pull.yml b/.github/workflows/translations-pull.yml
index 6dcea5886..3aad12998 100644
--- a/.github/workflows/translations-pull.yml
+++ b/.github/workflows/translations-pull.yml
@@ -32,7 +32,7 @@ jobs:
- name: Create pull request
if: github.event_name != 'pull_request'
id: create_pr
- uses: peter-evans/create-pull-request@v6
+ uses: peter-evans/create-pull-request@v7
with:
add-paths: |
cockatrice/translations/*.ts
@@ -64,7 +64,7 @@ jobs:
env:
STATUS: ${{ steps.create_pr.outputs.pull-request-operation }}
run: |
- if [[ "$STATUS" == "" ]]; then
+ if [[ "$STATUS" == "none" ]]; then
echo "PR #${{ steps.create_pr.outputs.pull-request-number }} unchanged!" >> $GITHUB_STEP_SUMMARY
else
echo "PR #${{ steps.create_pr.outputs.pull-request-number }} $STATUS!" >> $GITHUB_STEP_SUMMARY
diff --git a/.github/workflows/translations-push.yml b/.github/workflows/translations-push.yml
index 3003ec6e4..b0c2f62af 100644
--- a/.github/workflows/translations-push.yml
+++ b/.github/workflows/translations-push.yml
@@ -56,7 +56,7 @@ jobs:
- name: Create pull request
if: github.event_name != 'pull_request'
id: create_pr
- uses: peter-evans/create-pull-request@v6
+ uses: peter-evans/create-pull-request@v7
with:
add-paths: |
cockatrice/cockatrice_en@source.ts
@@ -79,7 +79,7 @@ jobs:
env:
STATUS: ${{ steps.create_pr.outputs.pull-request-operation }}
run: |
- if [[ "$STATUS" == "" ]]; then
+ if [[ "$STATUS" == "none" ]]; then
echo "PR #${{ steps.create_pr.outputs.pull-request-number }} unchanged!" >> $GITHUB_STEP_SUMMARY
else
echo "PR #${{ steps.create_pr.outputs.pull-request-number }} $STATUS!" >> $GITHUB_STEP_SUMMARY
diff --git a/CMakeLists.txt b/CMakeLists.txt
index fae8c394d..6fdf938ed 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -74,7 +74,7 @@ endif()
# A project name is needed for CPack
# Version can be overriden by git tags, see cmake/getversion.cmake
-project("Cockatrice" VERSION 2.9.1)
+project("Cockatrice" VERSION 2.10.0)
# Set release name if not provided via env/cmake var
if(NOT DEFINED GIT_TAG_RELEASENAME)
diff --git a/README.md b/README.md
index bbdf5764d..86c669763 100644
--- a/README.md
+++ b/README.md
@@ -42,7 +42,7 @@ Downloads are available for full releases and the current beta version in develo
# Get Involved [](https://discord.gg/3Z9yzmA)
-Join our [Discord community](https://discord.gg/3Z9yzmA) to connect with the project or fellow users of the app. The Cockatrice developers are also available on [Gitter](https://gitter.im/Cockatrice/Cockatrice). Come here to talk about the application, features, or just to hang out.
+Join our [Discord community](https://discord.gg/3Z9yzmA) to connect with the project, contributors or fellow users of the app. Come here to talk about the application, features, or just to hang out.
For support regarding specific servers, please contact that server's admin or forum for support rather than asking here.
To contribute code to the project, please review [the guidelines](https://github.com/Cockatrice/Cockatrice/blob/master/.github/CONTRIBUTING.md).
diff --git a/cmake/getversion.cmake b/cmake/getversion.cmake
index 5bec3548d..cb27ebd87 100644
--- a/cmake/getversion.cmake
+++ b/cmake/getversion.cmake
@@ -19,7 +19,7 @@ function(get_commit_id)
PARENT_SCOPE
)
set(PROJECT_VERSION_LABEL
- "custom(${GIT_COM_ID})"
+ "custom-${GIT_COM_ID}"
PARENT_SCOPE
)
endfunction()
diff --git a/cockatrice/CMakeLists.txt b/cockatrice/CMakeLists.txt
index 731c55369..e2fc703e9 100644
--- a/cockatrice/CMakeLists.txt
+++ b/cockatrice/CMakeLists.txt
@@ -5,128 +5,139 @@
project(Cockatrice VERSION "${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}.${PROJECT_VERSION_PATCH}")
set(cockatrice_SOURCES
- src/abstractcarddragitem.cpp
- src/abstractcarditem.cpp
- src/abstractclient.cpp
- src/abstractcounter.cpp
- src/abstractgraphicsitem.cpp
- src/arrowitem.cpp
- src/arrowtarget.cpp
- src/carddatabase.cpp
- src/carddatabasemodel.cpp
- src/carddbparser/carddatabaseparser.cpp
- src/carddbparser/cockatricexml3.cpp
- src/carddbparser/cockatricexml4.cpp
- src/carddragitem.cpp
- src/cardfilter.cpp
- src/cardframe.cpp
- src/cardinfopicture.cpp
- src/cardinfotext.cpp
- src/cardinfowidget.cpp
- src/carditem.cpp
- src/cardlist.cpp
- src/cardzone.cpp
- src/chatview/chatview.cpp
- src/counter_general.cpp
- src/customlineedit.cpp
- src/deck_loader.cpp
- src/decklistmodel.cpp
- src/deckstats_interface.cpp
- src/deckview.cpp
- src/dlg_connect.cpp
- src/dlg_create_token.cpp
- src/dlg_creategame.cpp
- src/dlg_edit_avatar.cpp
- src/dlg_edit_password.cpp
- src/dlg_edit_tokens.cpp
- src/dlg_edit_user.cpp
- src/dlg_filter_games.cpp
- src/dlg_forgotpasswordchallenge.cpp
- src/dlg_forgotpasswordrequest.cpp
- src/dlg_forgotpasswordreset.cpp
- src/dlg_load_deck_from_clipboard.cpp
- src/dlg_load_remote_deck.cpp
- src/dlg_manage_sets.cpp
- src/dlg_register.cpp
- src/dlg_roll_dice.cpp
- src/dlg_settings.cpp
- src/dlg_tip_of_the_day.cpp
- src/dlg_update.cpp
- src/dlg_viewlog.cpp
- src/filter_string.cpp
- src/filterbuilder.cpp
- src/filtertree.cpp
- src/filtertreemodel.cpp
- src/gamescene.cpp
- src/gameselector.cpp
- src/gamesmodel.cpp
- src/gameview.cpp
- src/gettextwithmax.cpp
- src/handcounter.cpp
- src/handle_public_servers.cpp
- src/handzone.cpp
- src/keysignals.cpp
- src/lineeditcompleter.cpp
- src/localclient.cpp
- src/localserver.cpp
- src/localserverinterface.cpp
- src/logger.cpp
+ 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/messagelogwidget.cpp
- src/pending_command.cpp
- src/phase.cpp
- src/phasestoolbar.cpp
- src/pictureloader.cpp
- src/pilezone.cpp
- src/pixmapgenerator.cpp
- src/player.cpp
- src/playerlistwidget.cpp
- src/playertarget.cpp
- src/releasechannel.cpp
- src/remoteclient.cpp
- src/remotedecklist_treewidget.cpp
- src/remotereplaylist_treewidget.cpp
- src/replay_timeline_widget.cpp
- src/selectzone.cpp
- src/sequenceEdit/sequenceedit.cpp
- src/setsmodel.cpp
- src/settings/carddatabasesettings.cpp
- src/settings/downloadsettings.cpp
- src/settings/gamefilterssettings.cpp
- src/settings/layoutssettings.cpp
- src/settings/messagesettings.cpp
- src/settings/serverssettings.cpp
- src/settings/settingsmanager.cpp
- src/settingscache.cpp
- src/shortcutssettings.cpp
- src/soundengine.cpp
- src/spoilerbackgroundupdater.cpp
- src/stackzone.cpp
- src/tab.cpp
- src/tab_account.cpp
- src/tab_admin.cpp
- src/tab_deck_editor.cpp
- src/tab_deck_storage.cpp
- src/tab_game.cpp
- src/tab_logs.cpp
- src/tab_message.cpp
- src/tab_replays.cpp
- src/tab_room.cpp
- src/tab_server.cpp
- src/tab_supervisor.cpp
- src/tablezone.cpp
- src/tappedout_interface.cpp
- src/thememanager.cpp
- src/tip_of_the_day.cpp
- src/translatecountername.cpp
- src/update_downloader.cpp
- src/user_context_menu.cpp
- src/userconnection_information.cpp
- src/userinfobox.cpp
- src/userlist.cpp
- src/window_main.cpp
- src/zoneviewwidget.cpp
- src/zoneviewzone.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}
)
diff --git a/cockatrice/cockatrice_en@source.ts b/cockatrice/cockatrice_en@source.ts
index 10c52c566..7418f9dcf 100644
--- a/cockatrice/cockatrice_en@source.ts
+++ b/cockatrice/cockatrice_en@source.ts
@@ -4,7 +4,7 @@
AbstractCounter
-
+ &Set counter...
@@ -12,12 +12,12 @@
AbstractCounterDialog
-
+ Set counter
-
+ New value for counter '%1':
@@ -25,82 +25,82 @@
AppearanceSettingsPage
-
+ Error
-
+ Could not create themes directory at '%1'.
-
+ Theme settings
-
+ Current theme:
-
+ Open themes folder
-
+ Card rendering
-
+ Display card names on cards having a picture
-
+ Scale cards on mouse over
-
+ Minimum overlap percentage of cards on the stack and in vertical hand
-
+ 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 +108,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 +220,22 @@ This is only saved for moderators and cannot be seen by the banned person.
BetaReleaseChannel
-
+ Beta Releases
-
+ 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 +243,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)
@@ -276,86 +276,86 @@ This is only saved for moderators and cannot be seen by the banned person.
CardFilter
-
+ ANDLogical conjunction operator used in card filter
-
+ ORLogical disjunction operator used in card filter
-
+ AND NOTNegated logical conjunction operator used in card filter
-
+ OR NOTNegated logical disjunction operator used in card filter
-
+ Name
-
+ Type
-
+ Color
-
+ Text
-
+ Set
-
+ Mana Cost
-
+ Mana Value
-
+ Rarity
-
+ Power
-
+ Toughness
-
+ Loyalty
-
+ Format
@@ -363,17 +363,17 @@ This is only saved for moderators and cannot be seen by the banned person.
CardFrame
-
+ Image
-
+ Description
-
+ Both
@@ -381,17 +381,17 @@ This is only saved for moderators and cannot be seen by the banned person.
CardInfoText
-
+ Name:
-
+ Related cards:
-
+ Unknown card:
@@ -399,12 +399,12 @@ This is only saved for moderators and cannot be seen by the banned person.
CardItem
-
+ &Move to
-
+ &Power / toughness
@@ -412,121 +412,121 @@ This is only saved for moderators and cannot be seen by the banned person.
CardZone
-
+ their handnominative
-
+ %1's handnominative
-
+ their librarylook at zone
-
+ %1's librarylook at zone
-
+ of their librarytop cards of zone,
-
+ of %1's librarytop cards of zone
-
+ their libraryreveal zone
-
+ %1's libraryreveal zone
-
+ their libraryshuffle
-
+ %1's libraryshuffle
-
+ their librarynominative
-
+ %1's librarynominative
-
+ their graveyardnominative
-
+ %1's graveyardnominative
-
+ their exilenominative
-
+ %1's exilenominative
-
+ their sideboardlook at zone
-
+ %1's sideboardlook at zone
-
+ their sideboardnominative
-
+ %1's sideboardnominative
@@ -535,151 +535,151 @@ This is only saved for moderators and cannot be seen by the banned person.
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
-
+ Add New URL
-
+ Remove URL
-
+ 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,12 +687,12 @@ This is only saved for moderators and cannot be seen by the banned person.
DeckListModel
-
+ Number
-
+ Card
@@ -700,13 +700,13 @@ This is only saved for moderators and cannot be seen by the banned person.
DeckStatsInterface
-
-
+
+ Error
-
+ The reply from the server could not be parsed.
@@ -714,42 +714,42 @@ This is only saved for moderators and cannot be seen by the banned person.
DeckViewContainer
-
+ Load deck...
-
+ Load remote deck...
-
+ Ready to start
-
+ Sideboard unlocked
-
+ Sideboard locked
-
+ Load deck
-
+ Error
-
+ The selected file could not be loaded.
@@ -757,138 +757,138 @@ This is only saved for moderators and cannot be seen by the banned person.
DlgConnect
-
+ Downloading...
-
+ Known Hosts
-
+ 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
-
+ &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.
@@ -896,102 +896,102 @@ This is only saved for moderators and cannot be seen by the banned person.
DlgCreateGame
-
+ Re&member settings
-
+ &Description:
-
+ P&layers:
-
+ General
-
+ Game type
-
+ &Password:
-
+ Only &buddies can join
-
+ Only ®istered users can join
-
+ Joining restrictions
-
+ &Spectators can watch
-
+ Spectators &need a password to watch
-
+ Spectators can &chat
-
+ Spectators can see &hands
-
+ Create game as spectator
-
+ Spectators
-
+ &Clear
-
+ Create game
-
+ Game information
-
+ Error
-
+ Server error.
@@ -999,92 +999,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
@@ -1092,39 +1092,39 @@ This is only saved for moderators and cannot be seen by the banned person.
DlgEditAvatar
-
-
+
+ No image chosen.
-
+ To change your avatar, choose a new image.
To remove your current avatar, confirm without choosing a new image.
-
+ Browse...
-
+ Change avatar
-
+ Open Image
-
+ Image Files (*.png *.jpg *.bmp)
-
+ Invalid image chosen.
@@ -1132,38 +1132,38 @@ To remove your current avatar, confirm without choosing a new image.
DlgEditPassword
-
+ Old password:
-
+ New password:
-
+ Confirm new password:
-
+ Change password
-
-
+
+ Error
-
+ Your password is too short.
-
+ The new passwords don't match.
@@ -1171,93 +1171,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.
@@ -1266,27 +1266,27 @@ Make sure to enable the 'Token' set in the "Manage sets" dia
DlgEditUser
-
+ Email:
-
+ Country:
-
+ Undefined
-
+ Real name:
-
+ Edit user profile
@@ -1294,132 +1294,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
-
+ Show &full games
-
+ Show games &that have started
-
+ Show &password protected 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
@@ -1427,52 +1427,52 @@ Make sure to enable the 'Token' set in the "Manage sets" dia
DlgForgotPasswordChallenge
-
+ Reset Password Challenge Warning
-
+ A problem has occurred. Please try to request a new password again.
-
+ Enter the information of the server and the account you'd like to request a new password for.
-
+ &Host:
-
+ &Port:
-
+ Player &name:
-
+ Email:
-
+ Reset Password Challenge
-
+ Reset Password Challenge Error
-
+ The email address can't be empty.
@@ -1480,37 +1480,37 @@ Make sure to enable the 'Token' set in the "Manage sets" dia
DlgForgotPasswordRequest
-
+ Enter the information of the server you'd like to request a new password for.
-
+ &Host:
-
+ &Port:
-
+ Player &name:
-
+ Reset Password Request
-
+ Reset Password Error
-
+ The player name can't be empty.
@@ -1518,86 +1518,86 @@ Make sure to enable the 'Token' set in the "Manage sets" dia
DlgForgotPasswordReset
-
+ Reset Password Warning
-
+ A problem has occurred. Please try to request a new password again.
-
+ Enter the received token and the new password in order to set your new password.
-
+ &Host:
-
+ &Port:
-
+ Player &name:
-
+ Token:
-
-
+
+ New Password:
-
+ Reset Password
-
+ The player name can't be empty.
-
-
-
-
+
+
+
+ Reset Password Error
-
+ The token can't be empty.
-
+ The new password can't be empty.
-
+ Error
-
+ Your password is too short.
-
+ The passwords do not match.
@@ -1605,24 +1605,24 @@ Make sure to enable the 'Token' set in the "Manage sets" dia
DlgLoadDeckFromClipboard
-
+ &Refresh
-
+ Load deck from clipboard
-
-
+
+ Error
-
-
+
+ Invalid deck list.
@@ -1630,7 +1630,7 @@ Make sure to enable the 'Token' set in the "Manage sets" dia
DlgLoadRemoteDeck
-
+ Load deck
@@ -1638,91 +1638,91 @@ Make sure to enable the 'Token' set in the "Manage sets" dia
DlgRegister
-
+ Enter your information and the information of the server you'd like to register to.
Your email will be used to verify your account.
-
+ &Host:
-
+ &Port:
-
+ Player &name:
-
+ P&assword:
-
+ Password (again):
-
+ Email:
-
+ Email (again):
-
+ Country:
-
+ Undefined
-
+ Real name:
-
+ Register to server
-
-
-
-
+
+
+
+ Registration Warning
-
+ Your password is too short.
-
+ Your passwords do not match, please try again.
-
+ Your email addresses do not match, please try again.
-
+ The player name can't be empty.
@@ -1730,17 +1730,17 @@ Your email will be used to verify your account.
DlgRollDice
-
+ Number of sides:
-
+ Number of dice:
-
+ Roll Dice
@@ -1748,12 +1748,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 +1764,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 +1775,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 +1784,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 +1807,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
@@ -1867,17 +1867,17 @@ Would you like to change your database location setting?
DlgTipOfTheDay
-
+ Next
-
+ Previous
-
+ Tip of the Day
@@ -1885,164 +1885,164 @@ 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...
-
+ 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
-
-
+
+ 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.
-
+ 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
@@ -2050,12 +2050,12 @@ You may have to build from source yourself.
DlgViewLog
-
+ Clear log when closing
-
+ Debug Log
@@ -2063,7 +2063,7 @@ You may have to build from source yourself.
FilterBuilder
-
+ Type your filter here
@@ -2071,105 +2071,105 @@ 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
-
+ 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 +2177,12 @@ You may have to build from source yourself.
GamesModel
-
+ >1 day
-
+ %1%2 hrshort age in hours
@@ -2191,12 +2191,12 @@ You may have to build from source yourself.
-
+ new
-
+ %1%2 minshort age in minutes
@@ -2205,78 +2205,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 +2284,92 @@ 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
-
+ Decks directory:
-
+ Replays directory:
-
+ Pictures directory:
-
+ Card database:
-
+ Custom database directory:
-
+ Token database:
-
+ Update channel
-
+ 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 +2377,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 +2440,576 @@ 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
-
+ &Help
-
+ &About Cockatrice
-
+ &Tip of the Day
-
+ Check for Client Updates
-
+ Check for Card Updates...
-
+ View &Debug Log
-
+ 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 +3019,75 @@ 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.
-
+ crashed.
-
+ timed out.
-
+ write error.
-
+ read error.
-
+ unknown error.
-
+ 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 +3095,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.
@@ -3150,262 +3150,262 @@ Cockatrice will now reload the card database.
MessageLogWidget
-
+ from play
-
+ from their graveyard
-
+ from exile
-
+ from their hand
-
+ the top card of %1's library
-
+ the top card of their library
-
+ from the top of %1's library
-
+ from the top of their library
-
+ the bottom card of %1's library
-
+ the bottom card of their library
-
+ from the bottom of %1's library
-
+ from the bottom of their library
-
+ from %1's library
-
+ from their library
-
+ from sideboard
-
+ from the stack
-
+ %1 is now keeping the top card %2 revealed.
-
+ %1 is not revealing the top card %2 any longer.
-
+ %1 can now look at top card %2 at any time.
-
+ %1 no longer can look at top card %2 at any time.
-
+ %1 attaches %2 to %3's %4.
-
+ %1 has conceded the game.
-
+ %1 has unconceded the game.
-
+ %1 has restored connection to the game.
-
+ %1 has lost connection to the game.
-
+ %1 points from their %2 to themselves.
-
+ %1 points from their %2 to %3.
-
+ %1 points from %2's %3 to themselves.
-
+ %1 points from %2's %3 to %4.
-
+ %1 points from their %2 to their %3.
-
+ %1 points from their %2 to %3's %4.
-
+ %1 points from %2's %3 to their own %4.
-
+ %1 points from %2's %3 to %4's %5.
-
+ %1 creates token: %2%3.
-
+ %1 has loaded a deck (%2).
-
+ %1 has loaded a deck with %2 sideboard cards (%3).
-
+ %1 destroys %2.
-
+ a card
-
+ %1 gives %2 control over %3.
-
+ %1 puts %2 into play%3 face down.
-
+ %1 puts %2 into play%3.
-
+ %1 puts %2%3 into their graveyard.
-
+ %1 exiles %2%3.
-
+ %1 moves %2%3 to their hand.
-
+ %1 puts %2%3 into their library.
-
+ %1 puts %2%3 onto the bottom of their library.
-
+ %1 puts %2%3 on top of their library.
-
+ %1 puts %2%3 into their library %4 cards from the top.
-
+ %1 moves %2%3 to sideboard.
-
+ %1 plays %2%3.
-
+ %1 tries to draw from an empty library
-
+ %1 draws %2 card(s).
@@ -3413,12 +3413,12 @@ Cockatrice will now reload the card database.
-
+ %1 is looking at %2.
-
+ %1 is looking at the top %3 card(s) %2.top card for singular, top %3 cards for plural
@@ -3427,57 +3427,57 @@ Cockatrice will now reload the card database.
-
+ %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 +3485,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.
-
+ cardsan unknown amount of cards
-
+ %1 card(s)a card for singular, %1 cards for plural
@@ -3515,117 +3515,117 @@ 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.
-
+ %1's turn.
-
+ %1 sets annotation of %2 to %3.
-
+ %1 places %2 %3 on %4 (now %5).
-
+ %1 removes %2 %3 from %4 (now %5).
-
+ red counter(s)
@@ -3633,7 +3633,7 @@ Cockatrice will now reload the card database.
-
+ yellow counter(s)
@@ -3641,7 +3641,7 @@ Cockatrice will now reload the card database.
-
+ green counter(s)
@@ -3649,97 +3649,97 @@ Cockatrice will now reload the card database.
-
+ %1 sets counter %2 to %3 (%4%5).
-
+ %1 sets %2 to not untap normally.
-
+ %1 sets %2 to untap normally.
-
+ %1 removes the PT of %2.
-
+ %1 changes the PT of %2 from nothing to %4.
-
+ %1 changes the PT of %2 from %3 to %4.
-
+ %1 has locked their sideboard.
-
+ %1 has unlocked their sideboard.
-
+ %1 taps their permanents.
-
+ %1 untaps their permanents.
-
+ %1 taps %2.
-
+ %1 untaps %2.
-
+ %1 shuffles %2.
-
+ %1 shuffles the bottom %3 cards of %2.
-
+ %1 shuffles the top %3 cards of %2.
-
+ %1 shuffles cards %3 - %4 of %2.
-
+ %1 unattaches %2.
-
+ %1 undoes their last draw.
-
+ %1 undoes their last draw (%2).
@@ -3747,110 +3747,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
@@ -3858,52 +3858,52 @@ Cockatrice will now reload the card database.
Mtg
-
+ Card Type
-
+ Mana Value
-
+ Color(s)
-
+ Loyalty
-
+ Main Card Type
-
+ Mana Cost
-
+ P/T
-
+ Side
-
+ Layout
-
+ Color Identity
@@ -3911,62 +3911,62 @@ Cockatrice will now reload the card database.
Phase
-
+ Unknown Phase
-
+ Untap
-
+ Upkeep
-
+ Draw
-
+ First Main
-
+ Beginning of Combat
-
+ Declare Attackers
-
+ Declare Blockers
-
+ Combat Damage
-
+ End of Combat
-
+ Second Main
-
+ End/Cleanup
@@ -3974,57 +3974,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 +4032,7 @@ Cockatrice will now reload the card database.
PictureLoader
-
+ encode for scryfall's language property, not available for all languages
@@ -4041,609 +4041,609 @@ 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
-
+ Lend library to...
-
+ Put top cards on stack &until...Take top cards &until...
-
+ &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
-
+ &Play
-
+ &Hide
-
+ Play &Face Down
-
+ &Tap / UntapTurn sideways or back again
-
+ Toggle &normal untapping
-
+ T&urn OverTurn 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
-
+ 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
@@ -4651,37 +4651,37 @@ Cockatrice will now reload the card database.
QMenuBar
-
+ Services
-
+ Hide %1
-
+ Hide Others
-
+ Show All
-
+ Preferences...
-
+ Quit %1
-
+ About %1
@@ -4689,23 +4689,23 @@ 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)
@@ -4728,92 +4728,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 +4821,17 @@ Cockatrice will now reload the card database.
RemoteDeckList_TreeModel
-
+ Name
-
+ ID
-
+ Upload time
@@ -4839,32 +4839,32 @@ Cockatrice will now reload the card database.
RemoteReplayList_TreeModel
-
+ ID
-
+ Name
-
+ Players
-
+ Keep
-
+ Time started
-
+ Duration (sec)
@@ -4872,37 +4872,37 @@ Cockatrice will now reload the card database.
RoomSelector
-
+ Rooms
-
+ Joi&n
-
+ Room
-
+ Description
-
+ Permissions
-
+ Players
-
+ Games
@@ -4910,32 +4910,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
-
+ Clear
-
+ Restore default
@@ -4943,27 +4943,27 @@ Cockatrice will now reload the card database.
SetsModel
-
+ Enabled
-
+ Set type
-
+ Set code
-
+ Long name
-
+ Release date
@@ -4971,58 +4971,58 @@ 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
@@ -5030,13 +5030,13 @@ Cockatrice will now reload the card database.
ShortcutsSettings
-
+ Your configuration file contained invalid shortcuts.
Please check your shortcut settings!
-
+ The following shortcuts have been set to default:
@@ -5045,17 +5045,17 @@ Please check your shortcut settings!
ShutdownDialog
-
+ &Reason for shutdown:
-
+ &Time until shutdown (minutes):
-
+ Shut down server
@@ -5063,27 +5063,27 @@ Please check your shortcut settings!
SoundSettingsPage
-
+ Enable &sounds
-
+ Current sounds theme:
-
+ Test system sound engine
-
+ Sound settings
-
+ Master volume
@@ -5091,48 +5091,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,27 +5140,27 @@ Please check your shortcut settings!
StableReleaseChannel
-
+ Stable Releases
-
+ 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.
@@ -5168,37 +5168,37 @@ Please check your shortcut settings!
TabAdmin
-
+ Administration
-
+ Update server &message
-
+ &Shut down server
-
+ &Reload configuration
-
+ Server administration functions
-
+ &Unlock functions
-
+ &Lock functions
@@ -5206,253 +5206,253 @@ Please check your shortcut settings!
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
-
-
-
+
+
+ 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.
@@ -5460,104 +5460,104 @@ Please check that the directory is writable and try again.
TabDeckStorage
-
+ Deck storage
-
+ Local file system
-
+ Server deck storage
-
-
+
+ Open in deck editor
-
+ Upload deck
-
+ Download deck
-
-
+
+ New folder
-
-
+
+ Delete
-
-
+
+ Error
-
+ 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"?
-
+ Name of new folder:
-
+ Delete remote folder
-
+ Delete remote deck
@@ -5565,210 +5565,210 @@ Please enter a name:
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
-
+ &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.
@@ -5776,157 +5776,157 @@ Please enter a name:
TabLog
-
+ Logs
-
-
-
+
+
+ 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.
@@ -5934,37 +5934,37 @@ The more information you put in, the more specific your results will be.
TabMessage
-
+ Private &chat
-
+ &Leave
-
+ %1 - Private chat
-
+ 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,69 +5972,69 @@ The more information you put in, the more specific your results will be.
TabReplays
-
+ Game replays
-
+ Local file system
-
+ Server replay storage
-
-
+
+ Watch replay
-
-
+
+ Delete
-
+ Download replay
-
+ Toggle expiration lock
-
+ Delete local file
-
+ Are you sure you want to delete "%1"?
-
+ Downloading 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?
@@ -6042,47 +6042,47 @@ The more information you put in, the more specific your results will be.
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.
@@ -6090,35 +6090,35 @@ The more information you put in, the more specific your results will be.
TabServer
-
+ Server
-
-
-
-
+
+
+
+ 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 +6126,32 @@ The more information you put in, the more specific your results will be.
TabSupervisor
-
+ 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,38 +6159,38 @@ 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)
@@ -6199,17 +6199,17 @@ Please refrain from engaging in this activity or further actions may be taken ag
TabUserLists
-
+ Account
-
+ Add to Buddy List
-
+ Add to Ignore List
@@ -6217,13 +6217,13 @@ Please refrain from engaging in this activity or further actions may be taken ag
TappedOutInterface
-
-
+
+ Error
-
+ Unable to analyze the deck.
@@ -6231,13 +6231,13 @@ Please refrain from engaging in this activity or further actions may be taken ag
TipsOfTheDay
-
+ File does not exist.
-
+ Failed to open file.
@@ -6246,42 +6246,42 @@ Please refrain from engaging in this activity or further actions may be taken ag
TranslateCounterName
-
+ Life
-
+ White
-
+ Blue
-
+ Black
-
+ Red
-
+ Green
-
+ Colorless
-
+ Other
@@ -6289,7 +6289,7 @@ Please refrain from engaging in this activity or further actions may be taken ag
UpdateDownloader
-
+ Could not open the file for reading.
@@ -6297,173 +6297,173 @@ 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
-
+ %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.
-
-
+
+ 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 +6471,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 +6582,7 @@ Please refrain from engaging in this activity or further actions may be taken ag
-
+ %10%n Day(s) %20amount of years (if more than 0), amount of days, date in local short format
@@ -6591,59 +6591,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,57 +6651,57 @@ 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)
-
+ &Play all nonlands onto the stack (not the battlefield) by default
-
+ 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
@@ -6709,22 +6709,22 @@ Please refrain from engaging in this activity or further actions may be taken ag
UserList
-
+ Users connected to server: %1
-
+ Users in this room: %1
-
+ Buddies online: %1 / %2
-
+ Ignored users online: %1 / %2
@@ -6732,43 +6732,43 @@ Please refrain from engaging in this activity or further actions may be taken ag
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 +6776,128 @@ 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
-
+ Manage sets
@@ -6905,22 +6905,22 @@ Please refrain from engaging in this activity or further actions may be taken ag
ZoneViewWidget
-
+ sort by name
-
+ sort by type
-
+ shuffle when closing
-
+ pile view
@@ -6928,7 +6928,7 @@ Please refrain from engaging in this activity or further actions may be taken ag
i18n
-
+ English
@@ -6949,771 +6949,786 @@ 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
+
+
+
+ Check for Card Updates...
-
+ Connect...
-
+ Disconnect
-
+ Exit
-
+ Full screen
-
+ Register...
-
+ Settings...
-
+ Start a Local Game...
-
+ Watch Replay...
-
+ Analyze Deck
-
+ Clear All Filters
-
+ Clear Selected Filter
-
+ Close
-
+ Remove Card
-
+ Manage Sets...
-
+ Edit Custom Tokens...
-
+ Export Deck
-
+ Add Card
-
+ Load Deck...
-
+ Load Deck from 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...
-
+ 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
-
+ 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...
-
-
+
+ Bottom of Library
-
-
-
-
+
+
+
+ Exile
-
-
-
-
+
+
+
+ Graveyard
-
-
+
+ Hand
-
-
+
+ Top of Library
-
-
-
+
+
+ Battlefield, Face Down
-
+ Battlefield
-
+ Library
-
+ Sideboard
-
+ Top 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
-
+ 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
+
+
+ Play/Pause
+
+
+
+
+ Toggle Fast Forward
+
+
diff --git a/cockatrice/src/cardinfopicture.cpp b/cockatrice/src/cardinfopicture.cpp
deleted file mode 100644
index 1e712c3aa..000000000
--- a/cockatrice/src/cardinfopicture.cpp
+++ /dev/null
@@ -1,66 +0,0 @@
-#include "cardinfopicture.h"
-
-#include "carditem.h"
-#include "main.h"
-#include "pictureloader.h"
-
-#include
-#include
-
-CardInfoPicture::CardInfoPicture(QWidget *parent) : QWidget(parent), info(nullptr), pixmapDirty(true)
-{
- setMinimumHeight(100);
-}
-
-void CardInfoPicture::setCard(CardInfoPtr card)
-{
- if (info) {
- disconnect(info.data(), nullptr, this, nullptr);
- }
-
- info = card;
-
- if (info) {
- connect(info.data(), SIGNAL(pixmapUpdated()), this, SLOT(updatePixmap()));
- }
-
- updatePixmap();
-}
-
-void CardInfoPicture::resizeEvent(QResizeEvent *)
-{
- updatePixmap();
-}
-
-void CardInfoPicture::updatePixmap()
-{
- pixmapDirty = true;
- update();
-}
-
-void CardInfoPicture::loadPixmap()
-{
- if (info)
- PictureLoader::getPixmap(resizedPixmap, info, size());
- else
- PictureLoader::getCardBackPixmap(resizedPixmap, size());
-}
-
-void CardInfoPicture::paintEvent(QPaintEvent *)
-{
- if (width() == 0 || height() == 0)
- return;
-
- if (pixmapDirty)
- loadPixmap();
-
- QSize scaledSize = resizedPixmap.size().scaled(size(), Qt::KeepAspectRatio);
- QPoint topLeft{(width() - scaledSize.width()) / 2, (height() - scaledSize.height()) / 2};
- qreal radius = 0.05 * scaledSize.width();
-
- QStylePainter painter(this);
- QPainterPath shape;
- shape.addRoundedRect(QRect(topLeft, scaledSize), radius, radius);
- painter.setClipPath(shape);
- painter.drawItemPixmap(QRect(topLeft, scaledSize), Qt::AlignCenter, resizedPixmap);
-}
diff --git a/cockatrice/src/cardinfopicture.h b/cockatrice/src/cardinfopicture.h
deleted file mode 100644
index f81f889af..000000000
--- a/cockatrice/src/cardinfopicture.h
+++ /dev/null
@@ -1,31 +0,0 @@
-#ifndef CARDINFOPICTURE_H
-#define CARDINFOPICTURE_H
-
-#include "carddatabase.h"
-
-#include
-
-class AbstractCardItem;
-
-class CardInfoPicture : public QWidget
-{
- Q_OBJECT
-
-private:
- CardInfoPtr info;
- QPixmap resizedPixmap;
- bool pixmapDirty;
-
-public:
- CardInfoPicture(QWidget *parent = nullptr);
-
-protected:
- void resizeEvent(QResizeEvent *event);
- void paintEvent(QPaintEvent *);
- void loadPixmap();
-public slots:
- void setCard(CardInfoPtr card);
- void updatePixmap();
-};
-
-#endif
diff --git a/cockatrice/src/cardlist.cpp b/cockatrice/src/cardlist.cpp
deleted file mode 100644
index d0347df54..000000000
--- a/cockatrice/src/cardlist.cpp
+++ /dev/null
@@ -1,63 +0,0 @@
-#include "cardlist.h"
-
-#include "carddatabase.h"
-#include "carditem.h"
-
-#include
-
-CardList::CardList(bool _contentsKnown) : QList(), contentsKnown(_contentsKnown)
-{
-}
-
-CardItem *CardList::findCard(const int id, const bool remove, int *position)
-{
- if (!contentsKnown) {
- if (empty())
- return 0;
- CardItem *temp = at(0);
- if (remove)
- removeAt(0);
- if (position)
- *position = id;
- return temp;
- } else
- for (int i = 0; i < size(); i++) {
- CardItem *temp = at(i);
- if (temp->getId() == id) {
- if (remove)
- removeAt(i);
- if (position)
- *position = i;
- return temp;
- }
- }
- return 0;
-}
-
-class CardList::compareFunctor
-{
-private:
- int flags;
-
-public:
- explicit compareFunctor(int _flags) : flags(_flags)
- {
- }
- inline bool operator()(CardItem *a, CardItem *b) const
- {
- if (flags & SortByType) {
- QString t1 = a->getInfo() ? a->getInfo()->getMainCardType() : QString();
- QString t2 = b->getInfo() ? b->getInfo()->getMainCardType() : QString();
- if ((t1 == t2) && (flags & SortByName))
- return a->getName() < b->getName();
- return t1 < t2;
- } else
- return a->getName() < b->getName();
- }
-};
-
-void CardList::sort(int flags)
-{
- compareFunctor cf(flags);
- std::sort(begin(), end(), cf);
-}
diff --git a/cockatrice/src/cardlist.h b/cockatrice/src/cardlist.h
deleted file mode 100644
index 1835ca96e..000000000
--- a/cockatrice/src/cardlist.h
+++ /dev/null
@@ -1,31 +0,0 @@
-#ifndef CARDLIST_H
-#define CARDLIST_H
-
-#include
-
-class CardItem;
-
-class CardList : public QList
-{
-private:
- class compareFunctor;
-
-protected:
- bool contentsKnown;
-
-public:
- enum SortFlags
- {
- SortByName = 1,
- SortByType = 2
- };
- CardList(bool _contentsKnown);
- CardItem *findCard(const int id, const bool remove, int *position = NULL);
- bool getContentsKnown() const
- {
- return contentsKnown;
- }
- void sort(int flags = SortByName);
-};
-
-#endif
diff --git a/cockatrice/src/abstractclient.cpp b/cockatrice/src/client/game_logic/abstract_client.cpp
similarity index 99%
rename from cockatrice/src/abstractclient.cpp
rename to cockatrice/src/client/game_logic/abstract_client.cpp
index 5f578c353..c186ae323 100644
--- a/cockatrice/src/abstractclient.cpp
+++ b/cockatrice/src/client/game_logic/abstract_client.cpp
@@ -1,5 +1,6 @@
-#include "abstractclient.h"
+#include "abstract_client.h"
+#include "../../server/pending_command.h"
#include "featureset.h"
#include "get_pb_extension.h"
#include "pb/commands.pb.h"
@@ -17,7 +18,6 @@
#include "pb/event_user_left.pb.h"
#include "pb/event_user_message.pb.h"
#include "pb/server_message.pb.h"
-#include "pending_command.h"
#include
diff --git a/cockatrice/src/abstractclient.h b/cockatrice/src/client/game_logic/abstract_client.h
similarity index 100%
rename from cockatrice/src/abstractclient.h
rename to cockatrice/src/client/game_logic/abstract_client.h
diff --git a/cockatrice/src/keysignals.cpp b/cockatrice/src/client/game_logic/key_signals.cpp
similarity index 98%
rename from cockatrice/src/keysignals.cpp
rename to cockatrice/src/client/game_logic/key_signals.cpp
index c8f75aab5..b4a38fad5 100644
--- a/cockatrice/src/keysignals.cpp
+++ b/cockatrice/src/client/game_logic/key_signals.cpp
@@ -1,4 +1,4 @@
-#include "keysignals.h"
+#include "key_signals.h"
#include
diff --git a/cockatrice/src/keysignals.h b/cockatrice/src/client/game_logic/key_signals.h
similarity index 100%
rename from cockatrice/src/keysignals.h
rename to cockatrice/src/client/game_logic/key_signals.h
diff --git a/cockatrice/src/gettextwithmax.cpp b/cockatrice/src/client/get_text_with_max.cpp
similarity index 96%
rename from cockatrice/src/gettextwithmax.cpp
rename to cockatrice/src/client/get_text_with_max.cpp
index 200661bba..73745be02 100644
--- a/cockatrice/src/gettextwithmax.cpp
+++ b/cockatrice/src/client/get_text_with_max.cpp
@@ -1,4 +1,4 @@
-#include "gettextwithmax.h"
+#include "get_text_with_max.h"
QString getTextWithMax(QWidget *parent,
const QString &title,
diff --git a/cockatrice/src/gettextwithmax.h b/cockatrice/src/client/get_text_with_max.h
similarity index 100%
rename from cockatrice/src/gettextwithmax.h
rename to cockatrice/src/client/get_text_with_max.h
diff --git a/cockatrice/src/releasechannel.cpp b/cockatrice/src/client/network/release_channel.cpp
similarity index 99%
rename from cockatrice/src/releasechannel.cpp
rename to cockatrice/src/client/network/release_channel.cpp
index 31a268185..b467aa4a4 100644
--- a/cockatrice/src/releasechannel.cpp
+++ b/cockatrice/src/client/network/release_channel.cpp
@@ -1,4 +1,4 @@
-#include "releasechannel.h"
+#include "release_channel.h"
#include "version_string.h"
diff --git a/cockatrice/src/releasechannel.h b/cockatrice/src/client/network/release_channel.h
similarity index 100%
rename from cockatrice/src/releasechannel.h
rename to cockatrice/src/client/network/release_channel.h
diff --git a/cockatrice/src/client/network/replay_timeline_widget.cpp b/cockatrice/src/client/network/replay_timeline_widget.cpp
new file mode 100644
index 000000000..deb77d623
--- /dev/null
+++ b/cockatrice/src/client/network/replay_timeline_widget.cpp
@@ -0,0 +1,197 @@
+#include "replay_timeline_widget.h"
+
+#include
+#include
+#include
+#include
+
+ReplayTimelineWidget::ReplayTimelineWidget(QWidget *parent)
+ : QWidget(parent), maxBinValue(1), maxTime(1), timeScaleFactor(1.0), currentTime(0), currentEvent(0)
+{
+ replayTimer = new QTimer(this);
+ connect(replayTimer, SIGNAL(timeout()), this, SLOT(replayTimerTimeout()));
+
+ rewindBufferingTimer = new QTimer(this);
+ rewindBufferingTimer->setSingleShot(true);
+ connect(rewindBufferingTimer, &QTimer::timeout, this, &ReplayTimelineWidget::processRewind);
+}
+
+void ReplayTimelineWidget::setTimeline(const QList &_replayTimeline)
+{
+ replayTimeline = _replayTimeline;
+ histogram.clear();
+ int binEndTime = BIN_LENGTH - 1;
+ int binValue = 0;
+ for (int i : replayTimeline) {
+ if (i > binEndTime) {
+ histogram.append(binValue);
+ if (binValue > maxBinValue)
+ maxBinValue = binValue;
+ while (i > binEndTime + BIN_LENGTH) {
+ histogram.append(0);
+ binEndTime += BIN_LENGTH;
+ }
+ binValue = 1;
+ binEndTime += BIN_LENGTH;
+ } else
+ ++binValue;
+ }
+ histogram.append(binValue);
+ if (!replayTimeline.isEmpty())
+ maxTime = replayTimeline.last();
+
+ update();
+}
+
+void ReplayTimelineWidget::paintEvent(QPaintEvent * /* event */)
+{
+ QPainter painter(this);
+ painter.drawRect(0, 0, width() - 1, height() - 1);
+
+ qreal binWidth = (qreal)width() / histogram.size();
+ QPainterPath path;
+ path.moveTo(0, height() - 1);
+ for (int i = 0; i < histogram.size(); ++i)
+ path.lineTo(qRound(i * binWidth), (height() - 1) * (1.0 - (qreal)histogram[i] / maxBinValue));
+ path.lineTo(width() - 1, height() - 1);
+ path.lineTo(0, height() - 1);
+ painter.fillPath(path, Qt::black);
+
+ const QColor barColor = QColor::fromHsv(120, 255, 255, 100);
+ quint64 w = (quint64)(width() - 1) * (quint64)currentTime / maxTime;
+ painter.fillRect(0, 0, static_cast(w), height() - 1, barColor);
+}
+
+void ReplayTimelineWidget::mousePressEvent(QMouseEvent *event)
+{
+#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
+ int newTime = static_cast((qint64)maxTime * (qint64)event->position().x() / width());
+#else
+ int newTime = static_cast((qint64)maxTime * (qint64)event->x() / width());
+#endif
+ // don't buffer rewinds from clicks, since clicks usually don't happen fast enough to require buffering
+ skipToTime(newTime, false);
+}
+
+void ReplayTimelineWidget::skipToTime(int newTime, bool doRewindBuffering)
+{
+ // check boundary conditions
+ if (newTime < 0) {
+ newTime = 0;
+ }
+ if (newTime > maxTime) {
+ newTime = maxTime;
+ }
+
+ newTime -= newTime % TIMER_INTERVAL_MS; // Time should always be a multiple of the interval
+
+ const bool isBackwardsSkip = newTime < currentTime;
+ currentTime = newTime;
+
+ if (isBackwardsSkip) {
+ handleBackwardsSkip(doRewindBuffering);
+ } else {
+ processNewEvents(FORWARD_SKIP);
+ }
+
+ update();
+}
+
+/// @param doRewindBuffering When true, if multiple backward skips are made in quick succession, only a single rewind
+/// is processed at the end. When false, the backwards skip will always cause an immediate rewind
+void ReplayTimelineWidget::handleBackwardsSkip(bool doRewindBuffering)
+{
+ if (doRewindBuffering) {
+ // We use a one-shot timer to implement the rewind buffering.
+ // 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());
+ } 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
+ rewindBufferingTimer->stop();
+
+ // process the rewind
+ currentEvent = 0;
+ emit rewound();
+ processNewEvents(BACKWARD_SKIP);
+}
+
+QSize ReplayTimelineWidget::sizeHint() const
+{
+ return {-1, 50};
+}
+
+QSize ReplayTimelineWidget::minimumSizeHint() const
+{
+ return {400, 50};
+}
+
+void ReplayTimelineWidget::replayTimerTimeout()
+{
+ currentTime += TIMER_INTERVAL_MS;
+
+ processNewEvents(NORMAL_PLAYBACK);
+
+ if (!(currentTime % 1000))
+ update();
+}
+
+/// Processes all unprocessed events up to the current time.
+void ReplayTimelineWidget::processNewEvents(PlaybackMode playbackMode)
+{
+ while ((currentEvent < replayTimeline.size()) && (replayTimeline[currentEvent] < currentTime)) {
+ 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)
+ 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;
+ }
+ if (currentEvent == replayTimeline.size()) {
+ emit replayFinished();
+ replayTimer->stop();
+ }
+}
+
+void ReplayTimelineWidget::setTimeScaleFactor(qreal _timeScaleFactor)
+{
+ timeScaleFactor = _timeScaleFactor;
+ replayTimer->setInterval(static_cast(TIMER_INTERVAL_MS / timeScaleFactor));
+}
+
+void ReplayTimelineWidget::startReplay()
+{
+ replayTimer->start(static_cast(TIMER_INTERVAL_MS / timeScaleFactor));
+}
+
+void ReplayTimelineWidget::stopReplay()
+{
+ replayTimer->stop();
+}
+
+void ReplayTimelineWidget::skipByAmount(int amount)
+{
+ skipToTime(currentTime + amount, true);
+}
\ No newline at end of file
diff --git a/cockatrice/src/replay_timeline_widget.h b/cockatrice/src/client/network/replay_timeline_widget.h
similarity index 52%
rename from cockatrice/src/replay_timeline_widget.h
rename to cockatrice/src/client/network/replay_timeline_widget.h
index 2805b89e4..b4a0559a4 100644
--- a/cockatrice/src/replay_timeline_widget.h
+++ b/cockatrice/src/client/network/replay_timeline_widget.h
@@ -1,6 +1,8 @@
#ifndef REPLAY_TIMELINE_WIDGET
#define REPLAY_TIMELINE_WIDGET
+#include "../../game/player/player.h"
+
#include
#include
#include
@@ -12,23 +14,45 @@ class ReplayTimelineWidget : public QWidget
{
Q_OBJECT
signals:
- void processNextEvent();
+ void processNextEvent(Player::EventProcessingOptions options);
void replayFinished();
void rewound();
private:
+ enum PlaybackMode
+ {
+ NORMAL_PLAYBACK,
+ FORWARD_SKIP,
+ BACKWARD_SKIP
+ };
+
+ 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;
QList replayTimeline;
QList histogram;
- static const int binLength;
int maxBinValue, maxTime;
qreal timeScaleFactor;
int currentTime;
int currentEvent;
+
+ void skipToTime(int newTime, bool doRewindBuffering);
+ void handleBackwardsSkip(bool doRewindBuffering);
+ int calcRewindBufferingTimeout() const;
+ void processRewind();
+ void processNewEvents(PlaybackMode playbackMode);
private slots:
void replayTimerTimeout();
public:
+ static constexpr int SMALL_SKIP_MS = 1000;
+ static constexpr int BIG_SKIP_MS = 10000;
+ static constexpr qreal FAST_FORWARD_SCALE_FACTOR = 10.0;
+
explicit ReplayTimelineWidget(QWidget *parent = nullptr);
void setTimeline(const QList &_replayTimeline);
QSize sizeHint() const override;
@@ -41,6 +65,7 @@ public:
public slots:
void startReplay();
void stopReplay();
+ void skipByAmount(int amount); // use a negative amount to skip backwards
protected:
void paintEvent(QPaintEvent *event) override;
diff --git a/cockatrice/src/setsmodel.cpp b/cockatrice/src/client/network/sets_model.cpp
similarity index 99%
rename from cockatrice/src/setsmodel.cpp
rename to cockatrice/src/client/network/sets_model.cpp
index b4a684176..de3521772 100644
--- a/cockatrice/src/setsmodel.cpp
+++ b/cockatrice/src/client/network/sets_model.cpp
@@ -1,4 +1,4 @@
-#include "setsmodel.h"
+#include "sets_model.h"
#include
diff --git a/cockatrice/src/setsmodel.h b/cockatrice/src/client/network/sets_model.h
similarity index 98%
rename from cockatrice/src/setsmodel.h
rename to cockatrice/src/client/network/sets_model.h
index b40a53906..20b5adbcb 100644
--- a/cockatrice/src/setsmodel.h
+++ b/cockatrice/src/client/network/sets_model.h
@@ -1,7 +1,7 @@
#ifndef SETSMODEL_H
#define SETSMODEL_H
-#include "carddatabase.h"
+#include "../../game/cards/card_database.h"
#include
#include
diff --git a/cockatrice/src/spoilerbackgroundupdater.cpp b/cockatrice/src/client/network/spoiler_background_updater.cpp
similarity index 93%
rename from cockatrice/src/spoilerbackgroundupdater.cpp
rename to cockatrice/src/client/network/spoiler_background_updater.cpp
index 7fb55f167..e60eeb96a 100644
--- a/cockatrice/src/spoilerbackgroundupdater.cpp
+++ b/cockatrice/src/client/network/spoiler_background_updater.cpp
@@ -1,9 +1,10 @@
-#include "spoilerbackgroundupdater.h"
+#include "spoiler_background_updater.h"
-#include "carddatabase.h"
-#include "main.h"
-#include "settingscache.h"
-#include "window_main.h"
+#include "../../game/cards/card_database.h"
+#include "../../game/cards/card_database_manager.h"
+#include "../../main.h"
+#include "../../settings/cache_settings.h"
+#include "../ui/window_main.h"
#include
#include
@@ -159,7 +160,7 @@ bool SpoilerBackgroundUpdater::saveDownloadedFile(QByteArray data)
// Data written, so reload the card database
qDebug() << "Spoiler Service Data Written";
- const auto reloadOk = QtConcurrent::run([] { db->loadCardDatabases(); });
+ const auto reloadOk = QtConcurrent::run([] { CardDatabaseManager::getInstance()->loadCardDatabases(); });
// If the user has notifications enabled, let them know
// when the database was last updated
@@ -172,7 +173,11 @@ bool SpoilerBackgroundUpdater::saveDownloadedFile(QByteArray data)
timeStamp.chop(6); // Remove " (UTC)"
auto utcTime = QLocale().toDateTime(timeStamp, "ddd, MMM dd yyyy, hh:mm:ss");
+#if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0)
+ utcTime.setTimeZone(QTimeZone::UTC);
+#else
utcTime.setTimeSpec(Qt::UTC);
+#endif
QString localTime = utcTime.toLocalTime().toString("MMM d, hh:mm");
diff --git a/cockatrice/src/spoilerbackgroundupdater.h b/cockatrice/src/client/network/spoiler_background_updater.h
similarity index 100%
rename from cockatrice/src/spoilerbackgroundupdater.h
rename to cockatrice/src/client/network/spoiler_background_updater.h
diff --git a/cockatrice/src/soundengine.cpp b/cockatrice/src/client/sound_engine.cpp
similarity index 98%
rename from cockatrice/src/soundengine.cpp
rename to cockatrice/src/client/sound_engine.cpp
index f2918cf52..35cc6d5c7 100644
--- a/cockatrice/src/soundengine.cpp
+++ b/cockatrice/src/client/sound_engine.cpp
@@ -1,6 +1,6 @@
-#include "soundengine.h"
+#include "sound_engine.h"
-#include "settingscache.h"
+#include "../settings/cache_settings.h"
#include
#include
diff --git a/cockatrice/src/soundengine.h b/cockatrice/src/client/sound_engine.h
similarity index 100%
rename from cockatrice/src/soundengine.h
rename to cockatrice/src/client/sound_engine.h
diff --git a/cockatrice/src/tab.cpp b/cockatrice/src/client/tabs/tab.cpp
similarity index 92%
rename from cockatrice/src/tab.cpp
rename to cockatrice/src/client/tabs/tab.cpp
index 06cf377c8..0ffc5ecdf 100644
--- a/cockatrice/src/tab.cpp
+++ b/cockatrice/src/client/tabs/tab.cpp
@@ -1,6 +1,6 @@
#include "tab.h"
-#include "cardinfowidget.h"
+#include "../ui/widgets/cards/card_info_display_widget.h"
#include
#include
@@ -18,7 +18,7 @@ void Tab::showCardInfoPopup(const QPoint &pos, const QString &cardName)
infoPopup->deleteLater();
}
currentCardName = cardName;
- infoPopup = new CardInfoWidget(
+ infoPopup = new CardInfoDisplayWidget(
cardName, 0, Qt::Widget | Qt::FramelessWindowHint | Qt::X11BypassWindowManagerHint | Qt::WindowStaysOnTopHint);
infoPopup->setAttribute(Qt::WA_TransparentForMouseEvents);
diff --git a/cockatrice/src/tab.h b/cockatrice/src/client/tabs/tab.h
similarity index 94%
rename from cockatrice/src/tab.h
rename to cockatrice/src/client/tabs/tab.h
index 53af3f40a..295c1e28f 100644
--- a/cockatrice/src/tab.h
+++ b/cockatrice/src/client/tabs/tab.h
@@ -5,7 +5,7 @@
class QMenu;
class TabSupervisor;
-class CardInfoWidget;
+class CardInfoDisplayWidget;
class Tab : public QMainWindow
{
@@ -27,7 +27,7 @@ protected slots:
private:
QString currentCardName;
bool contentsChanged;
- CardInfoWidget *infoPopup;
+ CardInfoDisplayWidget *infoPopup;
QList tabMenus;
public:
diff --git a/cockatrice/src/tab_account.cpp b/cockatrice/src/client/tabs/tab_account.cpp
similarity index 97%
rename from cockatrice/src/tab_account.cpp
rename to cockatrice/src/client/tabs/tab_account.cpp
index f289f7da0..fab79bea1 100644
--- a/cockatrice/src/tab_account.cpp
+++ b/cockatrice/src/client/tabs/tab_account.cpp
@@ -1,18 +1,18 @@
#include "tab_account.h"
-#include "abstractclient.h"
-#include "customlineedit.h"
+#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 "../game_logic/abstract_client.h"
+#include "../sound_engine.h"
#include "pb/event_add_to_list.pb.h"
#include "pb/event_remove_from_list.pb.h"
#include "pb/event_user_joined.pb.h"
#include "pb/event_user_left.pb.h"
#include "pb/response_list_users.pb.h"
#include "pb/session_commands.pb.h"
-#include "pending_command.h"
-#include "soundengine.h"
#include "trice_limits.h"
-#include "userinfobox.h"
-#include "userlist.h"
#include
#include
diff --git a/cockatrice/src/tab_account.h b/cockatrice/src/client/tabs/tab_account.h
similarity index 100%
rename from cockatrice/src/tab_account.h
rename to cockatrice/src/client/tabs/tab_account.h
diff --git a/cockatrice/src/tab_admin.cpp b/cockatrice/src/client/tabs/tab_admin.cpp
similarity index 99%
rename from cockatrice/src/tab_admin.cpp
rename to cockatrice/src/client/tabs/tab_admin.cpp
index 5610fa542..07031c1cc 100644
--- a/cockatrice/src/tab_admin.cpp
+++ b/cockatrice/src/client/tabs/tab_admin.cpp
@@ -1,6 +1,6 @@
#include "tab_admin.h"
-#include "abstractclient.h"
+#include "../game_logic/abstract_client.h"
#include "pb/admin_commands.pb.h"
#include "trice_limits.h"
diff --git a/cockatrice/src/tab_admin.h b/cockatrice/src/client/tabs/tab_admin.h
similarity index 100%
rename from cockatrice/src/tab_admin.h
rename to cockatrice/src/client/tabs/tab_admin.h
diff --git a/cockatrice/src/tab_deck_editor.cpp b/cockatrice/src/client/tabs/tab_deck_editor.cpp
similarity index 89%
rename from cockatrice/src/tab_deck_editor.cpp
rename to cockatrice/src/client/tabs/tab_deck_editor.cpp
index ab859cae9..1e2284186 100644
--- a/cockatrice/src/tab_deck_editor.cpp
+++ b/cockatrice/src/client/tabs/tab_deck_editor.cpp
@@ -1,22 +1,23 @@
#include "tab_deck_editor.h"
-#include "abstractclient.h"
-#include "carddatabasemodel.h"
-#include "cardframe.h"
-#include "decklistmodel.h"
-#include "deckstats_interface.h"
-#include "dlg_load_deck_from_clipboard.h"
-#include "filterbuilder.h"
-#include "filtertreemodel.h"
-#include "main.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_list_model.h"
+#include "../../deck/deck_stats_interface.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 "../ui/pixel_map_generator.h"
#include "pb/command_deck_upload.pb.h"
#include "pb/response.pb.h"
-#include "pending_command.h"
-#include "pictureloader.h"
-#include "pixmapgenerator.h"
-#include "settingscache.h"
#include "tab_supervisor.h"
-#include "tappedout_interface.h"
#include "trice_limits.h"
#include
@@ -50,8 +51,17 @@
void SearchLineEdit::keyPressEvent(QKeyEvent *event)
{
- if (treeView && ((event->key() == Qt::Key_Up) || (event->key() == Qt::Key_Down)))
- QCoreApplication::sendEvent(treeView, event);
+ // 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);
}
@@ -177,7 +187,7 @@ void TabDeckEditor::createDeckDock()
void TabDeckEditor::createCardInfoDock()
{
- cardInfo = new CardFrame();
+ cardInfo = new CardInfoFrameWidget();
cardInfo->setObjectName("cardInfo");
auto *cardInfoFrame = new QVBoxLayout;
cardInfoFrame->setObjectName("cardInfoFrame");
@@ -397,7 +407,7 @@ void TabDeckEditor::createCentralFrame()
connect(&searchKeySignals, SIGNAL(onCtrlC()), this, SLOT(copyDatabaseCellContents()));
connect(help, &QAction::triggered, this, &TabDeckEditor::showSearchSyntaxHelp);
- databaseModel = new CardDatabaseModel(db, true, this);
+ databaseModel = new CardDatabaseModel(CardDatabaseManager::getInstance(), true, this);
databaseModel->setObjectName("databaseModel");
databaseDisplayModel = new CardDatabaseDisplayModel(this);
databaseDisplayModel->setSourceModel(databaseModel);
@@ -714,8 +724,10 @@ void TabDeckEditor::updateCardInfoRight(const QModelIndex ¤t, const QModel
{
if (!current.isValid())
return;
- if (!current.model()->hasChildren(current.sibling(current.row(), 0)))
- cardInfo->setCard(current.sibling(current.row(), 1).data().toString());
+ 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)
@@ -736,9 +748,7 @@ bool TabDeckEditor::confirmClose()
{
if (modified) {
tabSupervisor->setCurrentWidget(this);
- QMessageBox::StandardButton ret = QMessageBox::warning(
- this, tr("Are you sure?"), tr("The decklist has been modified.\nDo you want to save the changes?"),
- QMessageBox::Save | QMessageBox::Discard | QMessageBox::Cancel);
+ int ret = createSaveConfirmationWindow()->exec();
if (ret == QMessageBox::Save)
return actSaveDeck();
else if (ret == QMessageBox::Cancel)
@@ -755,8 +765,16 @@ void TabDeckEditor::closeRequest()
void TabDeckEditor::actNewDeck()
{
- if (!confirmClose())
+ auto deckOpenLocation = confirmOpen(false);
+
+ if (deckOpenLocation == CANCELLED) {
return;
+ }
+
+ if (deckOpenLocation == NEW_TAB) {
+ emit openDeckEditor(nullptr);
+ return;
+ }
deckModel->cleanList();
nameEdit->setText(QString());
@@ -768,8 +786,11 @@ void TabDeckEditor::actNewDeck()
void TabDeckEditor::actLoadDeck()
{
- if (!confirmClose())
+ auto deckOpenLocation = confirmOpen();
+
+ if (deckOpenLocation == CANCELLED) {
return;
+ }
QFileDialog dialog(this, tr("Load deck"));
dialog.setDirectory(SettingsCache::instance().getDeckPath());
@@ -782,8 +803,12 @@ void TabDeckEditor::actLoadDeck()
auto *l = new DeckLoader;
if (l->loadFromFile(fileName, fmt)) {
- setSaveStatus(false);
- setDeck(l);
+ if (deckOpenLocation == NEW_TAB) {
+ emit openDeckEditor(l);
+ } else {
+ setSaveStatus(false);
+ setDeck(l);
+ }
} else
delete l;
setSaveStatus(true);
@@ -855,15 +880,23 @@ bool TabDeckEditor::actSaveDeckAs()
void TabDeckEditor::actLoadDeckFromClipboard()
{
- if (!confirmClose())
+ auto deckOpenLocation = confirmOpen();
+
+ if (deckOpenLocation == CANCELLED) {
return;
+ }
DlgLoadDeckFromClipboard dlg(this);
if (!dlg.exec())
return;
- setDeck(dlg.getDeckList());
- setModified(true);
+ if (deckOpenLocation == NEW_TAB) {
+ emit openDeckEditor(dlg.getDeckList());
+ } else {
+ setDeck(dlg.getDeckList());
+ setModified(true);
+ }
+
setSaveStatus(true);
}
@@ -957,6 +990,79 @@ void TabDeckEditor::recursiveExpand(const QModelIndex &index)
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();
@@ -966,7 +1072,7 @@ CardInfoPtr TabDeckEditor::currentCardInfo() const
const QString cardName = currentIndex.sibling(currentIndex.row(), 0).data().toString();
- return db->getCard(cardName);
+ return CardDatabaseManager::getInstance()->getCard(cardName);
}
void TabDeckEditor::addCardHelper(QString zoneName)
@@ -977,7 +1083,7 @@ void TabDeckEditor::addCardHelper(QString zoneName)
if (info->getIsToken())
zoneName = DECK_ZONE_TOKENS;
- QModelIndex newCardIndex = deckModel->addCard(info->getName(), zoneName);
+ QModelIndex newCardIndex = deckModel->addPreferredPrintingCard(info->getName(), zoneName, false);
recursiveExpand(newCardIndex);
deckView->setCurrentIndex(newCardIndex);
setModified(true);
@@ -990,6 +1096,7 @@ void TabDeckEditor::actSwapCard()
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())
@@ -999,8 +1106,10 @@ void TabDeckEditor::actSwapCard()
actDecrement();
const QString otherZoneName = zoneName == DECK_ZONE_MAIN ? DECK_ZONE_SIDE : DECK_ZONE_MAIN;
- // Third argument (true) says create the card no mater what, even if not in DB
- QModelIndex newCardIndex = deckModel->addCard(cardName, otherZoneName, true);
+ // 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);
@@ -1103,7 +1212,8 @@ void TabDeckEditor::setDeck(DeckLoader *_deck)
deckView->expandAll();
setModified(false);
- PictureLoader::cacheCardPixmaps(db->getCards(deckModel->getDeckList()->getCardList()));
+ PictureLoader::cacheCardPixmaps(
+ CardDatabaseManager::getInstance()->getCards(deckModel->getDeckList()->getCardList()));
deckView->expandAll();
setModified(false);
diff --git a/cockatrice/src/tab_deck_editor.h b/cockatrice/src/client/tabs/tab_deck_editor.h
similarity index 88%
rename from cockatrice/src/tab_deck_editor.h
rename to cockatrice/src/client/tabs/tab_deck_editor.h
index 2eb643178..6aff1cc31 100644
--- a/cockatrice/src/tab_deck_editor.h
+++ b/cockatrice/src/client/tabs/tab_deck_editor.h
@@ -1,9 +1,9 @@
#ifndef WINDOW_DECKEDITOR_H
#define WINDOW_DECKEDITOR_H
-#include "carddatabase.h"
-#include "customlineedit.h"
-#include "keysignals.h"
+#include "../../deck/custom_line_edit.h"
+#include "../../game/cards/card_database.h"
+#include "../game_logic/key_signals.h"
#include "tab.h"
#include
@@ -14,7 +14,7 @@ class CardDatabaseDisplayModel;
class DeckListModel;
class QTreeView;
-class CardFrame;
+class CardInfoFrameWidget;
class QTextEdit;
class QLabel;
class DeckLoader;
@@ -22,6 +22,7 @@ class Response;
class FilterTreeModel;
class FilterBuilder;
class QGroupBox;
+class QMessageBox;
class QHBoxLayout;
class QVBoxLayout;
class QPushButton;
@@ -100,6 +101,20 @@ private slots:
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);
@@ -113,7 +128,7 @@ private:
QTreeView *deckView;
KeySignals deckViewKeySignals;
- CardFrame *cardInfo;
+ CardInfoFrameWidget *cardInfo;
SearchLineEdit *searchEdit;
KeySignals searchKeySignals;
@@ -164,6 +179,7 @@ public:
public slots:
void closeRequest() override;
signals:
+ void openDeckEditor(const DeckLoader *deckLoader);
void deckEditorClosing(TabDeckEditor *tab);
};
diff --git a/cockatrice/src/tab_deck_storage.cpp b/cockatrice/src/client/tabs/tab_deck_storage.cpp
similarity index 98%
rename from cockatrice/src/tab_deck_storage.cpp
rename to cockatrice/src/client/tabs/tab_deck_storage.cpp
index fb52e3a62..8ab91e2be 100644
--- a/cockatrice/src/tab_deck_storage.cpp
+++ b/cockatrice/src/client/tabs/tab_deck_storage.cpp
@@ -1,9 +1,12 @@
#include "tab_deck_storage.h"
-#include "abstractclient.h"
-#include "deck_loader.h"
+#include "../../deck/deck_loader.h"
+#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 "gettextwithmax.h"
#include "pb/command_deck_del.pb.h"
#include "pb/command_deck_del_dir.pb.h"
#include "pb/command_deck_download.pb.h"
@@ -12,9 +15,6 @@
#include "pb/response.pb.h"
#include "pb/response_deck_download.pb.h"
#include "pb/response_deck_upload.pb.h"
-#include "pending_command.h"
-#include "remotedecklist_treewidget.h"
-#include "settingscache.h"
#include
#include
diff --git a/cockatrice/src/tab_deck_storage.h b/cockatrice/src/client/tabs/tab_deck_storage.h
similarity index 100%
rename from cockatrice/src/tab_deck_storage.h
rename to cockatrice/src/client/tabs/tab_deck_storage.h
diff --git a/cockatrice/src/tab_game.cpp b/cockatrice/src/client/tabs/tab_game.cpp
similarity index 92%
rename from cockatrice/src/tab_game.cpp
rename to cockatrice/src/client/tabs/tab_game.cpp
index aa2b59922..a37e05567 100644
--- a/cockatrice/src/tab_game.cpp
+++ b/cockatrice/src/client/tabs/tab_game.cpp
@@ -1,21 +1,32 @@
#include "tab_game.h"
-#include "abstractclient.h"
-#include "arrowitem.h"
-#include "carddatabase.h"
-#include "cardframe.h"
-#include "carditem.h"
-#include "deck_loader.h"
-#include "deckview.h"
-#include "dlg_creategame.h"
-#include "dlg_load_remote_deck.h"
-#include "dlg_manage_sets.h"
-#include "gamescene.h"
-#include "gameview.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/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 "../../main.h"
+#include "../../server/message_log_widget.h"
+#include "../../server/pending_command.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/window_main.h"
#include "get_pb_extension.h"
-#include "lineeditcompleter.h"
-#include "main.h"
-#include "messagelogwidget.h"
#include "pb/command_concede.pb.h"
#include "pb/command_deck_select.pb.h"
#include "pb/command_delete_arrow.pb.h"
@@ -45,18 +56,8 @@
#include "pb/game_event_container.pb.h"
#include "pb/game_replay.pb.h"
#include "pb/response_deck_download.pb.h"
-#include "pending_command.h"
-#include "phasestoolbar.h"
-#include "pictureloader.h"
-#include "player.h"
-#include "playerlistwidget.h"
-#include "replay_timeline_widget.h"
-#include "settingscache.h"
#include "tab_supervisor.h"
#include "trice_limits.h"
-#include "window_main.h"
-#include "zoneviewwidget.h"
-#include "zoneviewzone.h"
#include
#include
@@ -255,7 +256,25 @@ void TabGame::refreshShortcuts()
aResetLayout->setShortcuts(shortcuts.getShortcut("Player/aResetLayout"));
}
if (aFocusChat) {
- aFocusChat->setShortcuts(shortcuts.getShortcut("tab_game/aFocusChat"));
+ aFocusChat->setShortcuts(shortcuts.getShortcut("Player/aFocusChat"));
+ }
+ if (aReplaySkipForward) {
+ aReplaySkipForward->setShortcuts(shortcuts.getShortcut("Replays/aSkipForward"));
+ }
+ if (aReplaySkipBackward) {
+ aReplaySkipBackward->setShortcuts(shortcuts.getShortcut("Replays/aSkipBackward"));
+ }
+ if (aReplaySkipForwardBig) {
+ aReplaySkipForwardBig->setShortcuts(shortcuts.getShortcut("Replays/aSkipForwardBig"));
+ }
+ if (aReplaySkipBackwardBig) {
+ aReplaySkipBackwardBig->setShortcuts(shortcuts.getShortcut("Replays/aSkipBackwardBig"));
+ }
+ if (replayPlayButton) {
+ replayPlayButton->setShortcut(shortcuts.getSingleShortcut("Replays/playButton"));
+ }
+ if (replayFastForwardButton) {
+ replayFastForwardButton->setShortcut(shortcuts.getSingleShortcut("Replays/fastForwardButton"));
}
}
@@ -307,7 +326,8 @@ void DeckViewContainer::deckSelectFinished(const Response &r)
{
const Response_DeckDownload &resp = r.GetExtension(Response_DeckDownload::ext);
DeckLoader newDeck(QString::fromStdString(resp.deck()));
- PictureLoader::cacheCardPixmaps(db->getCards(newDeck.getCardList()));
+ // TODO CHANGE THIS TO BE SELECTED BY UUID
+ PictureLoader::cacheCardPixmaps(CardDatabaseManager::getInstance()->getCards(newDeck.getCardList()));
setDeck(newDeck);
}
@@ -422,7 +442,9 @@ TabGame::TabGame(TabSupervisor *_tabSupervisor,
: 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), replayDock(nullptr)
+ 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);
@@ -576,7 +598,7 @@ void TabGame::retranslateUi()
aResetLayout->setText(tr("Reset layout"));
- cardInfo->retranslateUi();
+ cardInfoFrameWidget->retranslateUi();
QMapIterator i(players);
while (i.hasNext())
@@ -593,39 +615,40 @@ void TabGame::closeRequest()
actLeaveGame();
}
-void TabGame::replayNextEvent()
+void TabGame::replayNextEvent(Player::EventProcessingOptions options)
{
- processGameEventContainer(replay->event_list(timelineWidget->getCurrentEvent()), nullptr);
+ processGameEventContainer(replay->event_list(timelineWidget->getCurrentEvent()), nullptr, options);
}
void TabGame::replayFinished()
{
- replayStartButton->setEnabled(true);
- replayPauseButton->setEnabled(false);
- replayFastForwardButton->setEnabled(false);
+ replayPlayButton->setChecked(false);
}
-void TabGame::replayStartButtonClicked()
+void TabGame::replayPlayButtonToggled(bool checked)
{
- replayStartButton->setEnabled(false);
- replayPauseButton->setEnabled(true);
- replayFastForwardButton->setEnabled(true);
-
- timelineWidget->startReplay();
-}
-
-void TabGame::replayPauseButtonClicked()
-{
- replayStartButton->setEnabled(true);
- replayPauseButton->setEnabled(false);
- replayFastForwardButton->setEnabled(false);
-
- timelineWidget->stopReplay();
+ if (checked) { // start replay
+ timelineWidget->startReplay();
+ } else { // pause replay
+ timelineWidget->stopReplay();
+ }
}
void TabGame::replayFastForwardButtonToggled(bool checked)
{
- timelineWidget->setTimeScaleFactor(checked ? 10.0 : 1.0);
+ timelineWidget->setTimeScaleFactor(checked ? ReplayTimelineWidget::FAST_FORWARD_SCALE_FACTOR : 1.0);
+}
+
+/**
+ * @brief Handles everything that needs to be reset when doing a replay rewind.
+ */
+void TabGame::replayRewind()
+{
+ // reset chat log
+ messageLog->clearChat();
+
+ // reset phase markers
+ setActivePhase(-1);
}
void TabGame::incrementGameTime()
@@ -839,7 +862,9 @@ Player *TabGame::addPlayer(int playerId, const ServerInfo_User &info)
return newPlayer;
}
-void TabGame::processGameEventContainer(const GameEventContainer &cont, AbstractClient *client)
+void TabGame::processGameEventContainer(const GameEventContainer &cont,
+ AbstractClient *client,
+ Player::EventProcessingOptions options)
{
const GameEventContext &context = cont.context();
messageLog->containerProcessingStarted(context);
@@ -914,7 +939,7 @@ void TabGame::processGameEventContainer(const GameEventContainer &cont, Abstract
qDebug() << "unhandled game event: invalid player id";
break;
}
- player->processGameEvent(eventType, event, context);
+ player->processGameEvent(eventType, event, context, options);
emitUserEvent();
}
}
@@ -1087,7 +1112,8 @@ void TabGame::eventGameStateChanged(const Event_GameStateChanged &event,
DeckViewContainer *deckViewContainer = deckViewContainers.value(playerId);
if (playerInfo.has_deck_list()) {
DeckLoader newDeck(QString::fromStdString(playerInfo.deck_list()));
- PictureLoader::cacheCardPixmaps(db->getCards(newDeck.getCardList()));
+ PictureLoader::cacheCardPixmaps(
+ CardDatabaseManager::getInstance()->getCards(newDeck.getCardList()));
deckViewContainer->setDeck(newDeck);
player->setDeck(newDeck);
}
@@ -1354,7 +1380,7 @@ void TabGame::eventSetActivePhase(const Event_SetActivePhase &event,
void TabGame::newCardAdded(AbstractCardItem *card)
{
- connect(card, SIGNAL(hovered(AbstractCardItem *)), cardInfo, SLOT(setCard(AbstractCardItem *)));
+ connect(card, SIGNAL(hovered(AbstractCardItem *)), cardInfoFrameWidget, SLOT(setCard(AbstractCardItem *)));
connect(card, SIGNAL(showCardInfoPopup(QPoint, QString)), this, SLOT(showCardInfoPopup(QPoint, QString)));
connect(card, SIGNAL(deleteCardInfoPopup(QString)), this, SLOT(deleteCardInfoPopup(QString)));
connect(card, SIGNAL(cardShiftClicked(QString)), this, SLOT(linkCardToChat(QString)));
@@ -1704,32 +1730,55 @@ void TabGame::createPlayAreaWidget(bool bReplay)
void TabGame::createReplayDock()
{
+ // timeline widget
timelineWidget = new ReplayTimelineWidget;
timelineWidget->setTimeline(replayTimeline);
- connect(timelineWidget, SIGNAL(processNextEvent()), this, SLOT(replayNextEvent()));
+ connect(timelineWidget, SIGNAL(processNextEvent(Player::EventProcessingOptions)), this,
+ SLOT(replayNextEvent(Player::EventProcessingOptions)));
connect(timelineWidget, SIGNAL(replayFinished()), this, SLOT(replayFinished()));
- connect(timelineWidget, &ReplayTimelineWidget::rewound, messageLog, &ChatView::clearChat);
+ connect(timelineWidget, &ReplayTimelineWidget::rewound, this, &TabGame::replayRewind);
+
+ // timeline skip shortcuts
+ aReplaySkipForward = new QAction(timelineWidget);
+ timelineWidget->addAction(aReplaySkipForward);
+ connect(aReplaySkipForward, &QAction::triggered, this,
+ [=]() { timelineWidget->skipByAmount(ReplayTimelineWidget::SMALL_SKIP_MS); });
+
+ aReplaySkipBackward = new QAction(timelineWidget);
+ timelineWidget->addAction(aReplaySkipBackward);
+ connect(aReplaySkipBackward, &QAction::triggered, this,
+ [=]() { timelineWidget->skipByAmount(-ReplayTimelineWidget::SMALL_SKIP_MS); });
+
+ aReplaySkipForwardBig = new QAction(timelineWidget);
+ timelineWidget->addAction(aReplaySkipForwardBig);
+ connect(aReplaySkipForwardBig, &QAction::triggered, this,
+ [=]() { timelineWidget->skipByAmount(ReplayTimelineWidget::BIG_SKIP_MS); });
+
+ aReplaySkipBackwardBig = new QAction(timelineWidget);
+ timelineWidget->addAction(aReplaySkipBackwardBig);
+ connect(aReplaySkipBackwardBig, &QAction::triggered, this,
+ [=]() { timelineWidget->skipByAmount(-ReplayTimelineWidget::BIG_SKIP_MS); });
+
+ // buttons
+ replayPlayButton = new QToolButton;
+ replayPlayButton->setIconSize(QSize(32, 32));
+ QIcon playButtonIcon = QIcon();
+ playButtonIcon.addPixmap(QPixmap("theme:replay/start"), QIcon::Normal, QIcon::Off);
+ playButtonIcon.addPixmap(QPixmap("theme:replay/pause"), QIcon::Normal, QIcon::On);
+ replayPlayButton->setIcon(playButtonIcon);
+ replayPlayButton->setCheckable(true);
+ connect(replayPlayButton, SIGNAL(toggled(bool)), this, SLOT(replayPlayButtonToggled(bool)));
- replayStartButton = new QToolButton;
- replayStartButton->setIconSize(QSize(32, 32));
- replayStartButton->setIcon(QPixmap("theme:replay/start"));
- connect(replayStartButton, SIGNAL(clicked()), this, SLOT(replayStartButtonClicked()));
- replayPauseButton = new QToolButton;
- replayPauseButton->setIconSize(QSize(32, 32));
- replayPauseButton->setEnabled(false);
- replayPauseButton->setIcon(QPixmap("theme:replay/pause"));
- connect(replayPauseButton, SIGNAL(clicked()), this, SLOT(replayPauseButtonClicked()));
replayFastForwardButton = new QToolButton;
replayFastForwardButton->setIconSize(QSize(32, 32));
- replayFastForwardButton->setEnabled(false);
replayFastForwardButton->setIcon(QPixmap("theme:replay/fastforward"));
replayFastForwardButton->setCheckable(true);
connect(replayFastForwardButton, SIGNAL(toggled(bool)), this, SLOT(replayFastForwardButtonToggled(bool)));
+ // putting everything together
replayControlLayout = new QHBoxLayout;
replayControlLayout->addWidget(timelineWidget, 10);
- replayControlLayout->addWidget(replayStartButton);
- replayControlLayout->addWidget(replayPauseButton);
+ replayControlLayout->addWidget(replayPlayButton);
replayControlLayout->addWidget(replayFastForwardButton);
replayControlWidget = new QWidget();
@@ -1760,18 +1809,18 @@ void TabGame::createDeckViewContainerWidget(bool bReplay)
void TabGame::viewCardInfo(const QString &cardName)
{
- cardInfo->setCard(cardName);
+ cardInfoFrameWidget->setCard(cardName);
}
void TabGame::createCardInfoDock(bool bReplay)
{
Q_UNUSED(bReplay);
- cardInfo = new CardFrame();
+ cardInfoFrameWidget = new CardInfoFrameWidget();
cardHInfoLayout = new QHBoxLayout;
cardVInfoLayout = new QVBoxLayout;
cardVInfoLayout->setContentsMargins(0, 0, 0, 0);
- cardVInfoLayout->addWidget(cardInfo);
+ cardVInfoLayout->addWidget(cardInfoFrameWidget);
cardVInfoLayout->addLayout(cardHInfoLayout);
cardBoxLayoutWidget = new QWidget;
@@ -1813,7 +1862,7 @@ void TabGame::createPlayerListDock(bool bReplay)
void TabGame::createMessageDock(bool bReplay)
{
messageLog = new MessageLogWidget(tabSupervisor, tabSupervisor, this);
- connect(messageLog, SIGNAL(cardNameHovered(QString)), cardInfo, SLOT(setCard(QString)));
+ connect(messageLog, SIGNAL(cardNameHovered(QString)), cardInfoFrameWidget, SLOT(setCard(QString)));
connect(messageLog, SIGNAL(showCardInfoPopup(QPoint, QString)), this, SLOT(showCardInfoPopup(QPoint, QString)));
connect(messageLog, SIGNAL(deleteCardInfoPopup(QString)), this, SLOT(deleteCardInfoPopup(QString)));
diff --git a/cockatrice/src/tab_game.h b/cockatrice/src/client/tabs/tab_game.h
similarity index 94%
rename from cockatrice/src/tab_game.h
rename to cockatrice/src/client/tabs/tab_game.h
index 0c35262cb..0d9d1d0db 100644
--- a/cockatrice/src/tab_game.h
+++ b/cockatrice/src/client/tabs/tab_game.h
@@ -1,10 +1,11 @@
#ifndef TAB_GAME_H
#define TAB_GAME_H
+#include "../../client/tearoff_menu.h"
+#include "../../game/player/player.h"
#include "pb/event_leave.pb.h"
#include "pb/serverinfo_game.pb.h"
#include "tab.h"
-#include "tearoffmenu.h"
#include
#include
@@ -15,7 +16,7 @@ class CardDatabase;
class GameView;
class DeckView;
class GameScene;
-class CardFrame;
+class CardInfoFrameWidget;
class MessageLogWidget;
class QTimer;
class QSplitter;
@@ -47,7 +48,6 @@ class Event_Ping;
class Event_GameSay;
class Event_Kicked;
class Event_ReverseTurn;
-class Player;
class CardZone;
class AbstractCardItem;
class CardItem;
@@ -146,9 +146,10 @@ private:
int currentReplayStep;
QList replayTimeline;
ReplayTimelineWidget *timelineWidget;
- QToolButton *replayStartButton, *replayPauseButton, *replayFastForwardButton;
+ QToolButton *replayPlayButton, *replayFastForwardButton;
+ QAction *aReplaySkipForward, *aReplaySkipBackward, *aReplaySkipForwardBig, *aReplaySkipBackwardBig;
- CardFrame *cardInfo;
+ CardInfoFrameWidget *cardInfoFrameWidget;
PlayerListWidget *playerListWidget;
QLabel *timeElapsedLabel;
MessageLogWidget *messageLog;
@@ -218,11 +219,11 @@ signals:
void openDeckEditor(const DeckLoader *deck);
void notIdle();
private slots:
- void replayNextEvent();
+ void replayNextEvent(Player::EventProcessingOptions options);
void replayFinished();
- void replayStartButtonClicked();
- void replayPauseButtonClicked();
+ void replayPlayButtonToggled(bool checked);
void replayFastForwardButtonToggled(bool checked);
+ void replayRewind();
void incrementGameTime();
void adminLockChanged(bool lock);
@@ -304,7 +305,9 @@ public:
return activeCard;
}
- void processGameEventContainer(const GameEventContainer &cont, AbstractClient *client);
+ void processGameEventContainer(const GameEventContainer &cont,
+ AbstractClient *client,
+ Player::EventProcessingOptions options);
PendingCommand *prepareGameCommand(const ::google::protobuf::Message &cmd);
PendingCommand *prepareGameCommand(const QList &cmdList);
public slots:
diff --git a/cockatrice/src/tab_logs.cpp b/cockatrice/src/client/tabs/tab_logs.cpp
similarity index 98%
rename from cockatrice/src/tab_logs.cpp
rename to cockatrice/src/client/tabs/tab_logs.cpp
index dd4ee9c50..b9d06dee2 100644
--- a/cockatrice/src/tab_logs.cpp
+++ b/cockatrice/src/client/tabs/tab_logs.cpp
@@ -1,11 +1,11 @@
#include "tab_logs.h"
-#include "abstractclient.h"
-#include "customlineedit.h"
-#include "dlg_manage_sets.h"
+#include "../../deck/custom_line_edit.h"
+#include "../../dialogs/dlg_manage_sets.h"
+#include "../../server/pending_command.h"
+#include "../game_logic/abstract_client.h"
#include "pb/moderator_commands.pb.h"
#include "pb/response_viewlog_history.pb.h"
-#include "pending_command.h"
#include "trice_limits.h"
#include
diff --git a/cockatrice/src/tab_logs.h b/cockatrice/src/client/tabs/tab_logs.h
similarity index 100%
rename from cockatrice/src/tab_logs.h
rename to cockatrice/src/client/tabs/tab_logs.h
diff --git a/cockatrice/src/tab_message.cpp b/cockatrice/src/client/tabs/tab_message.cpp
similarity index 95%
rename from cockatrice/src/tab_message.cpp
rename to cockatrice/src/client/tabs/tab_message.cpp
index 8c7dac2b5..21fc0e265 100644
--- a/cockatrice/src/tab_message.cpp
+++ b/cockatrice/src/client/tabs/tab_message.cpp
@@ -1,15 +1,15 @@
#include "tab_message.h"
-#include "abstractclient.h"
-#include "chatview/chatview.h"
-#include "customlineedit.h"
-#include "main.h"
+#include "../../deck/custom_line_edit.h"
+#include "../../main.h"
+#include "../../server/chat_view/chat_view.h"
+#include "../../server/pending_command.h"
+#include "../../settings/cache_settings.h"
+#include "../game_logic/abstract_client.h"
+#include "../sound_engine.h"
#include "pb/event_user_message.pb.h"
#include "pb/serverinfo_user.pb.h"
#include "pb/session_commands.pb.h"
-#include "pending_command.h"
-#include "settingscache.h"
-#include "soundengine.h"
#include "trice_limits.h"
#include
diff --git a/cockatrice/src/tab_message.h b/cockatrice/src/client/tabs/tab_message.h
similarity index 100%
rename from cockatrice/src/tab_message.h
rename to cockatrice/src/client/tabs/tab_message.h
diff --git a/cockatrice/src/tab_replays.cpp b/cockatrice/src/client/tabs/tab_replays.cpp
similarity index 97%
rename from cockatrice/src/tab_replays.cpp
rename to cockatrice/src/client/tabs/tab_replays.cpp
index fc70cd4a8..3a3655d79 100644
--- a/cockatrice/src/tab_replays.cpp
+++ b/cockatrice/src/client/tabs/tab_replays.cpp
@@ -1,6 +1,9 @@
#include "tab_replays.h"
-#include "abstractclient.h"
+#include "../../server/pending_command.h"
+#include "../../server/remote/remote_replay_list_tree_widget.h"
+#include "../../settings/cache_settings.h"
+#include "../game_logic/abstract_client.h"
#include "pb/command_replay_delete_match.pb.h"
#include "pb/command_replay_download.pb.h"
#include "pb/command_replay_modify_match.pb.h"
@@ -8,9 +11,6 @@
#include "pb/game_replay.pb.h"
#include "pb/response.pb.h"
#include "pb/response_replay_download.pb.h"
-#include "pending_command.h"
-#include "remotereplaylist_treewidget.h"
-#include "settingscache.h"
#include "tab_game.h"
#include
@@ -146,6 +146,9 @@ void TabReplays::actOpenLocalReplay()
void TabReplays::actDeleteLocalReplay()
{
QModelIndex curLeft = localDirView->selectionModel()->currentIndex();
+ if (!curLeft.isValid())
+ return;
+
if (QMessageBox::warning(this, tr("Delete local file"),
tr("Are you sure you want to delete \"%1\"?").arg(localDirModel->fileName(curLeft)),
QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes)
diff --git a/cockatrice/src/tab_replays.h b/cockatrice/src/client/tabs/tab_replays.h
similarity index 100%
rename from cockatrice/src/tab_replays.h
rename to cockatrice/src/client/tabs/tab_replays.h
diff --git a/cockatrice/src/tab_room.cpp b/cockatrice/src/client/tabs/tab_room.cpp
similarity index 97%
rename from cockatrice/src/tab_room.cpp
rename to cockatrice/src/client/tabs/tab_room.cpp
index fc260d175..034a236ce 100644
--- a/cockatrice/src/tab_room.cpp
+++ b/cockatrice/src/client/tabs/tab_room.cpp
@@ -1,11 +1,14 @@
#include "tab_room.h"
-#include "abstractclient.h"
-#include "chatview/chatview.h"
-#include "dlg_settings.h"
-#include "gameselector.h"
+#include "../../client/game_logic/abstract_client.h"
+#include "../../dialogs/dlg_settings.h"
+#include "../../game/game_selector.h"
+#include "../../main.h"
+#include "../../server/chat_view/chat_view.h"
+#include "../../server/pending_command.h"
+#include "../../server/user/user_list.h"
+#include "../../settings/cache_settings.h"
#include "get_pb_extension.h"
-#include "main.h"
#include "pb/event_join_room.pb.h"
#include "pb/event_leave_room.pb.h"
#include "pb/event_list_games.pb.h"
@@ -13,12 +16,9 @@
#include "pb/event_room_say.pb.h"
#include "pb/room_commands.pb.h"
#include "pb/serverinfo_room.pb.h"
-#include "pending_command.h"
-#include "settingscache.h"
#include "tab_account.h"
#include "tab_supervisor.h"
#include "trice_limits.h"
-#include "userlist.h"
#include
#include
diff --git a/cockatrice/src/tab_room.h b/cockatrice/src/client/tabs/tab_room.h
similarity index 98%
rename from cockatrice/src/tab_room.h
rename to cockatrice/src/client/tabs/tab_room.h
index d9e6d9246..a01943742 100644
--- a/cockatrice/src/tab_room.h
+++ b/cockatrice/src/client/tabs/tab_room.h
@@ -1,7 +1,7 @@
#ifndef TAB_ROOM_H
#define TAB_ROOM_H
-#include "lineeditcompleter.h"
+#include "../ui/line_edit_completer.h"
#include "tab.h"
#include
diff --git a/cockatrice/src/tab_server.cpp b/cockatrice/src/client/tabs/tab_server.cpp
similarity index 98%
rename from cockatrice/src/tab_server.cpp
rename to cockatrice/src/client/tabs/tab_server.cpp
index 03919f364..e0906e8d4 100644
--- a/cockatrice/src/tab_server.cpp
+++ b/cockatrice/src/client/tabs/tab_server.cpp
@@ -1,13 +1,13 @@
#include "tab_server.h"
-#include "abstractclient.h"
+#include "../../client/game_logic/abstract_client.h"
+#include "../../server/pending_command.h"
+#include "../../server/user/user_list.h"
#include "pb/event_list_rooms.pb.h"
#include "pb/event_server_message.pb.h"
#include "pb/response_join_room.pb.h"
#include "pb/session_commands.pb.h"
-#include "pending_command.h"
#include "tab_supervisor.h"
-#include "userlist.h"
#include
#include
diff --git a/cockatrice/src/tab_server.h b/cockatrice/src/client/tabs/tab_server.h
similarity index 100%
rename from cockatrice/src/tab_server.h
rename to cockatrice/src/client/tabs/tab_server.h
diff --git a/cockatrice/src/tab_supervisor.cpp b/cockatrice/src/client/tabs/tab_supervisor.cpp
similarity index 98%
rename from cockatrice/src/tab_supervisor.cpp
rename to cockatrice/src/client/tabs/tab_supervisor.cpp
index 5649b046c..9ef7ee385 100644
--- a/cockatrice/src/tab_supervisor.cpp
+++ b/cockatrice/src/client/tabs/tab_supervisor.cpp
@@ -1,7 +1,10 @@
#include "tab_supervisor.h"
-#include "abstractclient.h"
-#include "main.h"
+#include "../../client/game_logic/abstract_client.h"
+#include "../../main.h"
+#include "../../server/user/user_list.h"
+#include "../../settings/cache_settings.h"
+#include "../ui/pixel_map_generator.h"
#include "pb/event_game_joined.pb.h"
#include "pb/event_notify_user.pb.h"
#include "pb/event_user_message.pb.h"
@@ -11,8 +14,6 @@
#include "pb/room_event.pb.h"
#include "pb/serverinfo_room.pb.h"
#include "pb/serverinfo_user.pb.h"
-#include "pixmapgenerator.h"
-#include "settingscache.h"
#include "tab_account.h"
#include "tab_admin.h"
#include "tab_deck_editor.h"
@@ -23,7 +24,6 @@
#include "tab_replays.h"
#include "tab_room.h"
#include "tab_server.h"
-#include "userlist.h"
#include
#include
@@ -496,6 +496,7 @@ TabDeckEditor *TabSupervisor::addDeckEditorTab(const DeckLoader *deckToOpen)
if (deckToOpen)
tab->setDeck(new DeckLoader(*deckToOpen));
connect(tab, SIGNAL(deckEditorClosing(TabDeckEditor *)), this, SLOT(deckEditorClosed(TabDeckEditor *)));
+ connect(tab, SIGNAL(openDeckEditor(const DeckLoader *)), this, SLOT(addDeckEditorTab(const DeckLoader *)));
int tabIndex = myAddTab(tab);
addCloseButtonToTab(tab, tabIndex);
deckEditorTabs.append(tab);
@@ -541,7 +542,7 @@ void TabSupervisor::processGameEventContainer(const GameEventContainer &cont)
{
TabGame *tab = gameTabs.value(cont.game_id());
if (tab)
- tab->processGameEventContainer(cont, qobject_cast(sender()));
+ tab->processGameEventContainer(cont, qobject_cast(sender()), {});
else
qDebug() << "gameEvent: invalid gameId";
}
diff --git a/cockatrice/src/tab_supervisor.h b/cockatrice/src/client/tabs/tab_supervisor.h
similarity index 97%
rename from cockatrice/src/tab_supervisor.h
rename to cockatrice/src/client/tabs/tab_supervisor.h
index 1e4c0f949..7b43881d3 100644
--- a/cockatrice/src/tab_supervisor.h
+++ b/cockatrice/src/client/tabs/tab_supervisor.h
@@ -1,8 +1,8 @@
#ifndef TAB_SUPERVISOR_H
#define TAB_SUPERVISOR_H
-#include "chatview/userlistProxy.h"
-#include "deck_loader.h"
+#include "../../deck/deck_loader.h"
+#include "../../server/chat_view/user_list_proxy.h"
#include
#include
diff --git a/cockatrice/src/tappedout_interface.cpp b/cockatrice/src/client/tapped_out_interface.cpp
similarity index 99%
rename from cockatrice/src/tappedout_interface.cpp
rename to cockatrice/src/client/tapped_out_interface.cpp
index aad61e250..c0cbab095 100644
--- a/cockatrice/src/tappedout_interface.cpp
+++ b/cockatrice/src/client/tapped_out_interface.cpp
@@ -1,4 +1,4 @@
-#include "tappedout_interface.h"
+#include "tapped_out_interface.h"
#include "decklist.h"
diff --git a/cockatrice/src/tappedout_interface.h b/cockatrice/src/client/tapped_out_interface.h
similarity index 95%
rename from cockatrice/src/tappedout_interface.h
rename to cockatrice/src/client/tapped_out_interface.h
index 670e00030..239a3ee18 100644
--- a/cockatrice/src/tappedout_interface.h
+++ b/cockatrice/src/client/tapped_out_interface.h
@@ -1,7 +1,7 @@
#ifndef TAPPEDOUT_INTERFACE_H
#define TAPPEDOUT_INTERFACE_H
-#include "carddatabase.h"
+#include "../game/cards/card_database.h"
#include "decklist.h"
#include
diff --git a/cockatrice/src/tearoffmenu.h b/cockatrice/src/client/tearoff_menu.h
similarity index 95%
rename from cockatrice/src/tearoffmenu.h
rename to cockatrice/src/client/tearoff_menu.h
index 14a3a26e2..20ee30e10 100644
--- a/cockatrice/src/tearoffmenu.h
+++ b/cockatrice/src/client/tearoff_menu.h
@@ -1,6 +1,6 @@
#pragma once
-#include "settingscache.h"
+#include "../settings/cache_settings.h"
#include
diff --git a/cockatrice/src/translatecountername.cpp b/cockatrice/src/client/translate_counter_name.cpp
similarity index 94%
rename from cockatrice/src/translatecountername.cpp
rename to cockatrice/src/client/translate_counter_name.cpp
index 0f2813883..4f1c97657 100644
--- a/cockatrice/src/translatecountername.cpp
+++ b/cockatrice/src/client/translate_counter_name.cpp
@@ -1,4 +1,4 @@
-#include "translatecountername.h"
+#include "translate_counter_name.h"
const QMap TranslateCounterName::translated = {
{"life", QT_TRANSLATE_NOOP("TranslateCounterName", "Life")},
diff --git a/cockatrice/src/translatecountername.h b/cockatrice/src/client/translate_counter_name.h
similarity index 100%
rename from cockatrice/src/translatecountername.h
rename to cockatrice/src/client/translate_counter_name.h
diff --git a/cockatrice/src/translation.h b/cockatrice/src/client/translation.h
similarity index 100%
rename from cockatrice/src/translation.h
rename to cockatrice/src/client/translation.h
diff --git a/cockatrice/src/client/ui/layouts/flow_layout.cpp b/cockatrice/src/client/ui/layouts/flow_layout.cpp
new file mode 100644
index 000000000..f1a776c47
--- /dev/null
+++ b/cockatrice/src/client/ui/layouts/flow_layout.cpp
@@ -0,0 +1,337 @@
+/**
+ * @file flow_layout.cpp
+ * @brief Implementation of the FlowLayout class, a custom layout for dynamically organizing widgets
+ * in rows within the constraints of available width or parent scroll areas.
+ */
+
+#include "flow_layout.h"
+
+#include "../widgets/general/layout_containers/flow_widget.h"
+
+#include
+#include
+#include
+#include
+
+/**
+ * @brief Constructs a FlowLayout instance with the specified parent widget, margin, and spacing values.
+ * @param parent The parent widget for this layout.
+ * @param margin The layout margin.
+ * @param hSpacing The horizontal spacing between items.
+ * @param vSpacing The vertical spacing between items.
+ */
+FlowLayout::FlowLayout(QWidget *parent, const int margin, const int hSpacing, const int vSpacing)
+ : QLayout(parent), horizontalMargin(hSpacing), verticalMargin(vSpacing)
+{
+ setContentsMargins(margin, margin, margin, margin);
+}
+
+/**
+ * @brief Destructor for FlowLayout, which cleans up all items in the layout.
+ */
+FlowLayout::~FlowLayout()
+{
+ QLayoutItem *item;
+ while ((item = FlowLayout::takeAt(0))) {
+ delete item;
+ }
+}
+
+/**
+ * @brief Indicates the layout's support for expansion in both horizontal and vertical directions.
+ * @return The orientations (Qt::Horizontal | Qt::Vertical) this layout can expand to fill.
+ */
+Qt::Orientations FlowLayout::expandingDirections() const
+{
+ return Qt::Horizontal | Qt::Vertical;
+}
+
+/**
+ * @brief Indicates that this layout's height depends on its width.
+ * @return True, as the layout adjusts its height to fit the specified width.
+ */
+bool FlowLayout::hasHeightForWidth() const
+{
+ return true;
+}
+
+/**
+ * @brief Calculates the required height to display all items within the specified width.
+ * @param width The available width for arranging items.
+ * @return The total height needed to fit all items in rows constrained by the specified width.
+ */
+int FlowLayout::heightForWidth(const int width) const
+{
+ int height = 0;
+ int rowWidth = 0;
+ int rowHeight = 0;
+
+ for (const QLayoutItem *item : items) {
+ if (item == nullptr || item->isEmpty()) {
+ continue;
+ }
+
+ int itemWidth = item->sizeHint().width() + horizontalSpacing();
+ if (rowWidth + itemWidth > width) { // Start a new row if the row width exceeds available width
+ height += rowHeight + verticalSpacing();
+ rowWidth = itemWidth;
+ rowHeight = item->sizeHint().height() + verticalSpacing();
+ } else {
+ rowWidth += itemWidth;
+ rowHeight = qMax(rowHeight, item->sizeHint().height());
+ }
+ }
+ height += rowHeight; // Add the final row's height
+ return height;
+}
+
+/**
+ * @brief Arranges layout items in rows within the specified rectangle bounds.
+ * @param rect The area within which to position layout items.
+ */
+void FlowLayout::setGeometry(const QRect &rect)
+{
+ QLayout::setGeometry(rect); // Sets the geometry of the layout based on the given rectangle.
+
+ int left, top, right, bottom;
+ getContentsMargins(&left, &top, &right, &bottom); // Retrieves the layout's content margins.
+
+ // Adjust the rectangle to exclude margins.
+ const QRect adjustedRect = rect.adjusted(+left, +top, -right, -bottom);
+
+ // Calculate the available width for items, considering either the adjusted rectangle's width
+ // or the parent scroll area width, if applicable.
+ const int availableWidth = qMax(adjustedRect.width(), getParentScrollAreaWidth());
+
+ // Arrange all rows of items within the available width and get the total height used.
+ const int totalHeight = layoutAllRows(adjustedRect.x(), adjustedRect.y(), availableWidth);
+
+ // If the layout's parent is a QWidget, update its minimum size to ensure it can accommodate
+ // the arranged items' dimensions.
+ if (QWidget *parentWidgetPtr = parentWidget()) {
+ parentWidgetPtr->setMinimumSize(availableWidth, totalHeight);
+ }
+}
+
+/**
+ * @brief Arranges items in rows based on the available width.
+ * Items are added to a row until the row's width exceeds `availableWidth`.
+ * Then, a new row is started.
+ * @param originX The starting x-coordinate for the row layout.
+ * @param originY The starting y-coordinate for the row layout.
+ * @param availableWidth The available width to lay out items.
+ * @return The y-coordinate of the final row's end position.
+ */
+int FlowLayout::layoutAllRows(const int originX, const int originY, const int availableWidth)
+{
+ QVector rowItems; // Temporary storage for items in the current row.
+ int currentXPosition = originX; // Tracks the x-coordinate for placing items in the current row.
+ int currentYPosition = originY; // Tracks the y-coordinate, updated after each row.
+
+ int rowHeight = 0; // Tracks the maximum height of items in the current row.
+
+ // Iterate through all layout items to arrange them.
+ for (QLayoutItem *item : items) {
+ if (item == nullptr || item->isEmpty()) {
+ continue;
+ }
+
+ QSize itemSize = item->sizeHint(); // The suggested size for the item.
+ const int itemWidth = itemSize.width() + horizontalSpacing();
+
+ // Check if the item fits in the current row's remaining width.
+ if (currentXPosition + itemWidth > availableWidth) {
+ // If not, layout the current row and start a new row.
+ layoutSingleRow(rowItems, originX, currentYPosition);
+ rowItems.clear(); // Clear the temporary storage for the new row.
+ currentXPosition = originX; // Reset x-position to the start of the new row.
+ currentYPosition += rowHeight + verticalSpacing(); // Move y-position down for the new row.
+ rowHeight = 0; // Reset row height for the new row.
+ }
+
+ // Add the item to the current row.
+ rowItems.append(item);
+ rowHeight = qMax(rowHeight, itemSize.height()); // Update the row height to the tallest item.
+ currentXPosition += itemSize.width() + horizontalSpacing(); // Move x-position for the next item.
+ }
+
+ // Layout the final row if there are remaining items.
+ layoutSingleRow(rowItems, originX, currentYPosition);
+
+ currentYPosition += rowHeight; // Add the final row's height
+ return currentYPosition;
+}
+
+/**
+ * @brief Helper function for arranging a single row of items within specified bounds.
+ * @param rowItems Items to be arranged in the row.
+ * @param x The x-coordinate for starting the row.
+ * @param y The y-coordinate for starting the row.
+ */
+void FlowLayout::layoutSingleRow(const QVector &rowItems, int x, const int y)
+{
+ // Iterate through each item in the row and position it.
+ for (QLayoutItem *item : rowItems) {
+ if (item == nullptr || item->isEmpty()) {
+ continue;
+ }
+
+ QSize itemMaxSize = item->widget()->maximumSize(); // Get the item's maximum allowable size.
+ // Constrain the item's width and height to its size hint or maximum size.
+ int itemWidth = qMin(item->sizeHint().width(), itemMaxSize.width());
+ int itemHeight = qMin(item->sizeHint().height(), itemMaxSize.height());
+ // Set the item's geometry based on the calculated size and position.
+ item->setGeometry(QRect(QPoint(x, y), QSize(itemWidth, itemHeight)));
+ // Move the x-position for the next item, including horizontal spacing.
+ x += itemWidth + horizontalSpacing();
+ }
+}
+
+/**
+ * @brief Returns the preferred size for this layout.
+ * @return The maximum of all item size hints as a QSize.
+ */
+QSize FlowLayout::sizeHint() const
+{
+ QSize size;
+ for (const QLayoutItem *item : items) {
+ if (item != nullptr && !item->isEmpty()) {
+ size = size.expandedTo(item->sizeHint());
+ }
+ }
+ return size.isValid() ? size : QSize(0, 0);
+}
+
+/**
+ * @brief Returns the minimum size required to display all layout items.
+ * @return The minimum QSize needed by the layout.
+ */
+QSize FlowLayout::minimumSize() const
+{
+ QSize size;
+ for (const QLayoutItem *item : items) {
+ if (item != nullptr && !item->isEmpty()) {
+ size = size.expandedTo(item->minimumSize());
+ }
+ }
+
+ size.setWidth(qMin(size.width(), getParentScrollAreaWidth()));
+ size.setHeight(qMin(size.height(), getParentScrollAreaHeight()));
+
+ return size.isValid() ? size : QSize(0, 0);
+}
+
+/**
+ * @brief Adds a new item to the layout.
+ * @param item The layout item to add.
+ */
+void FlowLayout::addItem(QLayoutItem *item)
+{
+ if (item != nullptr) {
+ items.append(item);
+ }
+}
+
+/**
+ * @brief Retrieves the count of items in the layout.
+ * @return The number of layout items.
+ */
+int FlowLayout::count() const
+{
+ return static_cast(items.size());
+}
+
+/**
+ * @brief Returns the layout item at the specified index.
+ * @param index The index of the item to retrieve.
+ * @return A pointer to the item at the specified index, or nullptr if out of range.
+ */
+QLayoutItem *FlowLayout::itemAt(const int index) const
+{
+ return (index >= 0 && index < items.size()) ? items.value(index) : nullptr;
+}
+
+/**
+ * @brief Removes and returns the item at the specified index.
+ * @param index The index of the item to remove.
+ * @return A pointer to the removed item, or nullptr if out of range.
+ */
+QLayoutItem *FlowLayout::takeAt(const int index)
+{
+ return (index >= 0 && index < items.size()) ? items.takeAt(index) : nullptr;
+}
+
+/**
+ * @brief Gets the horizontal spacing between items.
+ * @return The horizontal spacing if set, otherwise a smart default.
+ */
+int FlowLayout::horizontalSpacing() const
+{
+ return (horizontalMargin >= 0) ? horizontalMargin : smartSpacing(QStyle::PM_LayoutHorizontalSpacing);
+}
+
+/**
+ * @brief Gets the vertical spacing between items.
+ * @return The vertical spacing if set, otherwise a smart default.
+ */
+int FlowLayout::verticalSpacing() const
+{
+ return (verticalMargin >= 0) ? verticalMargin : smartSpacing(QStyle::PM_LayoutVerticalSpacing);
+}
+
+/**
+ * @brief Calculates smart spacing based on the parent widget style.
+ * @param pm The pixel metric to calculate.
+ * @return The calculated spacing value.
+ */
+int FlowLayout::smartSpacing(const QStyle::PixelMetric pm) const
+{
+ QObject *parent = this->parent();
+
+ if (!parent) {
+ return -1;
+ }
+
+ if (parent->isWidgetType()) {
+ const auto *pw = dynamic_cast(parent);
+ return pw->style()->pixelMetric(pm, nullptr, pw);
+ }
+
+ return dynamic_cast(parent)->spacing();
+}
+
+/**
+ * @brief Gets the width of the parent scroll area, if any.
+ * @return The width of the scroll area's viewport, or 0 if not found.
+ */
+int FlowLayout::getParentScrollAreaWidth() const
+{
+ QWidget *parent = parentWidget();
+
+ while (parent) {
+ if (const auto *scrollArea = qobject_cast(parent)) {
+ return scrollArea->viewport()->width();
+ }
+ parent = parent->parentWidget();
+ }
+
+ return 0;
+}
+
+/**
+ * @brief Gets the height of the parent scroll area, if any.
+ * @return The height of the scroll area's viewport, or 0 if not found.
+ */
+int FlowLayout::getParentScrollAreaHeight() const
+{
+ QWidget *parent = parentWidget();
+
+ while (parent) {
+ if (const auto *scrollArea = qobject_cast(parent)) {
+ return scrollArea->viewport()->height();
+ }
+ parent = parent->parentWidget();
+ }
+
+ return 0;
+}
diff --git a/cockatrice/src/client/ui/layouts/flow_layout.h b/cockatrice/src/client/ui/layouts/flow_layout.h
new file mode 100644
index 000000000..6fde7b802
--- /dev/null
+++ b/cockatrice/src/client/ui/layouts/flow_layout.h
@@ -0,0 +1,43 @@
+#ifndef FLOW_LAYOUT_H
+#define FLOW_LAYOUT_H
+
+#include
+#include
+#include
+#include
+
+class FlowLayout : public QLayout
+{
+public:
+ explicit FlowLayout(QWidget *parent = nullptr);
+ FlowLayout(QWidget *parent, int margin, int hSpacing, int vSpacing);
+ ~FlowLayout() override;
+
+ void addItem(QLayoutItem *item) override;
+ [[nodiscard]] int count() const override;
+ [[nodiscard]] QLayoutItem *itemAt(int index) const override;
+ QLayoutItem *takeAt(int index) override;
+ [[nodiscard]] int horizontalSpacing() const;
+
+ [[nodiscard]] Qt::Orientations expandingDirections() const override;
+ [[nodiscard]] bool hasHeightForWidth() const override;
+ [[nodiscard]] int heightForWidth(int width) const override;
+ [[nodiscard]] int verticalSpacing() const;
+ [[nodiscard]] int doLayout(const QRect &rect, bool testOnly) const;
+ [[nodiscard]] int smartSpacing(QStyle::PixelMetric pm) const;
+ [[nodiscard]] int getParentScrollAreaWidth() const;
+ [[nodiscard]] int getParentScrollAreaHeight() const;
+
+ void setGeometry(const QRect &rect) override;
+ virtual int layoutAllRows(int originX, int originY, int availableWidth);
+ virtual void layoutSingleRow(const QVector &rowItems, int x, int y);
+ [[nodiscard]] QSize sizeHint() const override;
+ [[nodiscard]] QSize minimumSize() const override;
+
+protected:
+ QList items; // List to store layout items
+ int horizontalMargin;
+ int verticalMargin;
+};
+
+#endif // FLOW_LAYOUT_H
\ No newline at end of file
diff --git a/cockatrice/src/client/ui/layouts/horizontal_flow_layout.cpp b/cockatrice/src/client/ui/layouts/horizontal_flow_layout.cpp
new file mode 100644
index 000000000..16f409db7
--- /dev/null
+++ b/cockatrice/src/client/ui/layouts/horizontal_flow_layout.cpp
@@ -0,0 +1,142 @@
+#include "horizontal_flow_layout.h"
+
+/**
+ * @brief Constructs a HorizontalFlowLayout instance with the specified parent widget.
+ * This layout arranges items in columns within the given height, automatically adjusting its width.
+ * @param parent The parent widget to which this layout belongs.
+ * @param margin The layout margin.
+ * @param hSpacing The horizontal spacing between items.
+ * @param vSpacing The vertical spacing between items.
+ */
+HorizontalFlowLayout::HorizontalFlowLayout(QWidget *parent, const int margin, const int hSpacing, const int vSpacing)
+ : FlowLayout(parent, margin, hSpacing, vSpacing)
+{
+}
+
+/**
+ * @brief Destructor for HorizontalFlowLayout, responsible for cleaning up layout items.
+ */
+HorizontalFlowLayout::~HorizontalFlowLayout()
+{
+ QLayoutItem *item;
+ while ((item = FlowLayout::takeAt(0))) {
+ delete item;
+ }
+}
+
+/**
+ * @brief Calculates the required width to display all items, given a specified height.
+ * This method arranges items into columns and determines the total width needed.
+ * @param height The available height for arranging layout items.
+ * @return The total width required to fit all items, organized in columns constrained by the given height.
+ */
+int HorizontalFlowLayout::heightForWidth(const int height) const
+{
+ int width = 0;
+ int colWidth = 0;
+ int colHeight = 0;
+
+ for (const QLayoutItem *item : items) {
+ if (item == nullptr || item->isEmpty()) {
+ continue;
+ }
+
+ int itemHeight = item->sizeHint().height();
+ if (colHeight + itemHeight > height) {
+ width += colWidth;
+ colHeight = itemHeight;
+ colWidth = item->sizeHint().width();
+ } else {
+ colHeight += itemHeight;
+ colWidth = qMax(colWidth, item->sizeHint().width());
+ }
+ }
+ width += colWidth; // Add width of the last column
+ return width;
+}
+
+/**
+ * @brief Sets the geometry of the layout items, arranging them in columns within the given height.
+ * @param rect The rectangle area defining the layout space.
+ */
+void HorizontalFlowLayout::setGeometry(const QRect &rect)
+{
+ const int availableHeight = qMax(rect.height(), getParentScrollAreaHeight());
+
+ const int totalWidth = layoutAllColumns(rect.x(), rect.y(), availableHeight);
+
+ if (QWidget *parentWidgetPtr = parentWidget()) {
+ parentWidgetPtr->setMinimumSize(totalWidth, availableHeight);
+ }
+}
+
+/**
+ * @brief Lays out items into columns according to the available height, starting from a given origin.
+ * Each column is arranged within `availableHeight`, wrapping to a new column as necessary.
+ * @param originX The x-coordinate for the layout start position.
+ * @param originY The y-coordinate for the layout start position.
+ * @param availableHeight The height within which each column is constrained.
+ * @return The total width after arranging all columns.
+ */
+int HorizontalFlowLayout::layoutAllColumns(const int originX, const int originY, const int availableHeight)
+{
+ QVector colItems; // Holds items for the current column
+ int currentXPosition = originX; // Tracks the x-coordinate while placing items
+ int currentYPosition = originY; // Tracks the y-coordinate, resetting for each new column
+
+ int colWidth = 0; // Tracks the maximum width of items in the current column
+
+ for (QLayoutItem *item : items) {
+ if (item == nullptr || item->isEmpty()) {
+ continue;
+ }
+
+ QSize itemSize = item->sizeHint(); // The suggested size for the current item
+
+ // Check if the current item fits in the remaining height of the current column
+ if (currentYPosition + itemSize.height() > availableHeight) {
+ // If not, layout the current column and start a new column
+ layoutSingleColumn(colItems, currentXPosition, originY);
+ colItems.clear(); // Reset the list for the new column
+ currentYPosition = originY; // Reset y-position to the column's start
+ currentXPosition += colWidth; // Move x-position to the next column
+ colWidth = 0; // Reset column width for the new column
+ }
+
+ // Add the item to the current column
+ colItems.append(item);
+ colWidth = qMax(colWidth, itemSize.width()); // Update the column's width to the widest item
+ currentYPosition += itemSize.height(); // Move y-position for the next item
+ }
+
+ // Layout the final column if there are any remaining items
+ layoutSingleColumn(colItems, currentXPosition, originY);
+
+ // Return the total width used, including the last column's width
+ return currentXPosition + colWidth;
+}
+
+/**
+ * @brief Arranges a single column of items within specified x and y starting positions.
+ * @param colItems A list of items to be arranged in the column.
+ * @param x The starting x-coordinate for the column.
+ * @param y The starting y-coordinate for the column.
+ */
+void HorizontalFlowLayout::layoutSingleColumn(const QVector &colItems, const int x, int y)
+{
+ for (QLayoutItem *item : colItems) {
+ if (item != nullptr && item->isEmpty()) {
+ continue;
+ }
+
+ // Get the maximum allowed size for the item
+ QSize itemMaxSize = item->widget()->maximumSize();
+ // Constrain the item's width and height to its size hint or maximum size
+ const int itemWidth = qMin(item->sizeHint().width(), itemMaxSize.width());
+ const int itemHeight = qMin(item->sizeHint().height(), itemMaxSize.height());
+ // Set the item's geometry based on the computed size and position
+ item->setGeometry(QRect(QPoint(x, y), QSize(itemWidth, itemHeight)));
+ // Move the y-position down by the item's height to place the next item below
+ y += itemHeight;
+ }
+}
diff --git a/cockatrice/src/client/ui/layouts/horizontal_flow_layout.h b/cockatrice/src/client/ui/layouts/horizontal_flow_layout.h
new file mode 100644
index 000000000..f6aebb284
--- /dev/null
+++ b/cockatrice/src/client/ui/layouts/horizontal_flow_layout.h
@@ -0,0 +1,19 @@
+#ifndef HORIZONTAL_FLOW_LAYOUT_H
+#define HORIZONTAL_FLOW_LAYOUT_H
+
+#include "flow_layout.h"
+
+class HorizontalFlowLayout : public FlowLayout
+{
+public:
+ explicit HorizontalFlowLayout(QWidget *parent = nullptr, int margin = 0, int hSpacing = 0, int vSpacing = 0);
+ ~HorizontalFlowLayout() override;
+
+ [[nodiscard]] int heightForWidth(int height) const override;
+
+ void setGeometry(const QRect &rect) override;
+ int layoutAllColumns(int originX, int originY, int availableHeight);
+ static void layoutSingleColumn(const QVector &colItems, int x, int y);
+};
+
+#endif // HORIZONTAL_FLOW_LAYOUT_H
\ No newline at end of file
diff --git a/cockatrice/src/client/ui/layouts/overlap_layout.cpp b/cockatrice/src/client/ui/layouts/overlap_layout.cpp
new file mode 100644
index 000000000..da44399a7
--- /dev/null
+++ b/cockatrice/src/client/ui/layouts/overlap_layout.cpp
@@ -0,0 +1,474 @@
+#include "overlap_layout.h"
+
+#include
+
+/**
+ * @class OverlapLayout
+ * @brief Custom layout class to arrange widgets with overlapping positions.
+ *
+ * The OverlapLayout class is a QLayout subclass that arranges child widgets
+ * in an overlapping configuration, allowing control over the overlap percentage
+ * and the number of rows or columns based on the chosen layout direction. This
+ * layout is particularly useful for visualizing elements that need to partially
+ * stack over one another, either horizontally or vertically.
+ */
+
+/**
+ * @brief Constructs an OverlapLayout with the specified parameters.
+ *
+ * Initializes a new OverlapLayout with the given overlap percentage, row or column limit,
+ * and layout direction. The overlap percentage determines how much each widget will
+ * overlap with the previous one. If maxColumns or maxRows are set to zero, it implies
+ * no limit in that respective dimension.
+ *
+ * @param overlapPercentage An integer representing the percentage of overlap between items (0-100).
+ * @param maxColumns The maximum number of columns allowed in the layout when in horizontal orientation (0 for
+ * unlimited).
+ * @param maxRows The maximum number of rows allowed in the layout when in vertical orientation (0 for unlimited).
+ * @param direction The orientation direction of the layout, either Qt::Horizontal or Qt::Vertical.
+ * @param parent The parent widget of this layout.
+ */
+OverlapLayout::OverlapLayout(QWidget *parent,
+ const int overlapPercentage,
+ const int maxColumns,
+ const int maxRows,
+ const Qt::Orientation direction)
+ : QLayout(parent), overlapPercentage(overlapPercentage), maxColumns(maxColumns), maxRows(maxRows),
+ direction(direction)
+{
+}
+
+/**
+ * @brief Destructor for OverlapLayout, ensuring cleanup of all layout items.
+ *
+ * Iterates through all layout items and deletes them. This prevents memory
+ * leaks by removing all child QLayoutItems stored in the layout.
+ */
+OverlapLayout::~OverlapLayout()
+{
+ QLayoutItem *item;
+ while ((item = OverlapLayout::takeAt(0))) {
+ delete item;
+ }
+}
+
+/**
+ * @brief Adds a new item to the layout.
+ *
+ * Appends a QLayoutItem to the internal list, allowing it to be positioned within the
+ * layout during the next geometry update. This method does not directly arrange the
+ * items; it merely adds them to the layout’s tracking.
+ *
+ * @param item Pointer to the QLayoutItem being added to this layout.
+ */
+void OverlapLayout::addItem(QLayoutItem *item)
+{
+ if (item != nullptr) {
+ itemList.append(item);
+ }
+}
+
+/**
+ * @brief Retrieves the total count of items within the layout.
+ *
+ * Returns the number of items stored in the layout's internal item list.
+ * This count reflects how many widgets or spacers the layout is currently managing.
+ *
+ * @return Integer count of items in the layout.
+ */
+int OverlapLayout::count() const
+{
+ return static_cast(itemList.size());
+}
+
+/**
+ * @brief Provides access to a layout item at a specified index.
+ *
+ * Allows retrieval of a QLayoutItem from the layout’s internal list
+ * by index. If the index is out of bounds, this function will return nullptr.
+ *
+ * @param index The index of the desired item.
+ * @return Pointer to the QLayoutItem at the specified index, or nullptr if index is invalid.
+ */
+QLayoutItem *OverlapLayout::itemAt(const int index) const
+{
+ return (index >= 0 && index < itemList.size()) ? itemList.value(index) : nullptr;
+}
+
+/**
+ * @brief Removes and returns a layout item at the specified index.
+ *
+ * Removes a QLayoutItem from the layout at the given index, reducing the layout's count.
+ * If the index is invalid, this function returns nullptr without any effect.
+ *
+ * @param index The index of the item to remove.
+ * @return Pointer to the removed QLayoutItem, or nullptr if index is invalid.
+ */
+QLayoutItem *OverlapLayout::takeAt(const int index)
+{
+ return (index >= 0 && index < itemList.size()) ? itemList.takeAt(index) : nullptr;
+}
+
+/**
+ * @brief Sets the geometry for the layout items, arranging them with the specified overlap.
+ * @param rect The rectangle defining the area within which the layout should arrange items.
+ */
+void OverlapLayout::setGeometry(const QRect &rect)
+{
+ // Call the base class implementation to ensure standard layout behavior.
+ QLayout::setGeometry(rect);
+
+ // If there are no items to layout, exit early.
+ if (itemList.isEmpty()) {
+ return;
+ }
+
+ // Get the parent widget for size and margin calculations.
+ const QWidget *parentWidget = this->parentWidget();
+ if (!parentWidget) {
+ return;
+ }
+
+ // Calculate available width and height, subtracting the parent's margins.
+ int availableWidth = parentWidget->width();
+ int availableHeight = parentWidget->height();
+ const QMargins margins = parentWidget->contentsMargins();
+ availableWidth -= margins.left() + margins.right();
+ availableHeight -= margins.top() + margins.bottom();
+
+ // Determine the maximum item width and height among all layout items.
+ int maxItemWidth = 0;
+ int maxItemHeight = 0;
+ for (QLayoutItem *item : itemList) {
+ if (item != nullptr && item->widget()) {
+ QSize itemSize = item->widget()->sizeHint();
+ maxItemWidth = qMax(maxItemWidth, itemSize.width());
+ maxItemHeight = qMax(maxItemHeight, itemSize.height());
+ }
+ }
+
+ // Calculate the overlap offsets based on the layout direction and overlap percentage.
+ const int overlapOffsetWidth = (direction == Qt::Horizontal) ? (maxItemWidth * overlapPercentage / 100) : 0;
+ const int overlapOffsetHeight = (direction == Qt::Vertical) ? (maxItemHeight * overlapPercentage / 100) : 0;
+
+ // Determine the number of columns based on layout constraints and available space.
+ int columns;
+ if (direction == Qt::Horizontal) {
+ if (maxColumns > 0) {
+ // Calculate the maximum possible columns given the available width and overlap.
+ const int availableColumns = (availableWidth + overlapOffsetWidth) / (maxItemWidth - overlapOffsetWidth);
+ // Use the smaller of maxColumns and availableColumns.
+ columns = qMin(maxColumns, availableColumns);
+ } else {
+ // If no maxColumns constraint, allow as many columns as possible.
+ columns = INT_MAX;
+ }
+ } else {
+ // If not a horizontal layout, column count is irrelevant.
+ columns = INT_MAX;
+ }
+
+ // Determine the number of rows based on layout constraints and available space.
+ int rows;
+ if (direction == Qt::Vertical) {
+ if (maxRows > 0) {
+ // Calculate the maximum possible rows given the available height and overlap.
+ const int availableRows = (availableHeight + overlapOffsetHeight) / (maxItemHeight - overlapOffsetHeight);
+ // Use the smaller of maxRows and availableRows.
+ rows = qMin(maxRows, availableRows);
+ } else {
+ // If no maxRows constraint, allow as many rows as possible.
+ rows = INT_MAX;
+ }
+ } else {
+ // If not a vertical layout, row count is irrelevant.
+ rows = INT_MAX;
+ }
+
+ // Initialize row and column indices.
+ int currentRow = 0;
+ int currentColumn = 0;
+
+ // Loop through all items and position them based on the calculated offsets.
+ for (const auto item : itemList) {
+ if (item == nullptr) {
+ continue;
+ }
+
+ // Calculate the position of the current item.
+ const int xPos = rect.left() + currentColumn * (maxItemWidth - overlapOffsetWidth);
+ const int yPos = rect.top() + currentRow * (maxItemHeight - overlapOffsetHeight);
+ item->setGeometry(QRect(xPos, yPos, maxItemWidth, maxItemHeight));
+
+ // Update row and column indices based on the layout direction.
+ if (direction == Qt::Horizontal) {
+ currentColumn++;
+ if (currentColumn >= columns) {
+ currentColumn = 0;
+ currentRow++;
+ }
+ } else {
+ currentRow++;
+ if (currentRow >= rows) {
+ currentRow = 0;
+ currentColumn++;
+ }
+ }
+ }
+}
+
+/**
+ * @brief Calculates the preferred size for the layout, considering overlap and orientation.
+ * @return The preferred layout size as a QSize object.
+ */
+QSize OverlapLayout::calculatePreferredSize() const
+{
+
+ // Determine the maximum item dimensions.
+ int maxItemWidth = 0;
+ int maxItemHeight = 0;
+ for (QLayoutItem *item : itemList) {
+ if (item == nullptr) {
+ continue;
+ }
+ if (!item->widget()) {
+ continue;
+ }
+
+ QSize itemSize = item->widget()->sizeHint();
+ maxItemWidth = qMax(maxItemWidth, itemSize.width());
+ maxItemHeight = qMax(maxItemHeight, itemSize.height());
+ }
+
+ // Calculate the overlap offsets.
+ const int extra_for_overlap = (100 - overlapPercentage);
+ const int overlapOffsetWidth =
+ (direction == Qt::Horizontal) ? qRound((maxItemWidth / 100.0) * extra_for_overlap) : 0;
+ const int overlapOffsetHeight =
+ (direction == Qt::Vertical) ? qRound((maxItemHeight / 100.0) * extra_for_overlap) : 0;
+
+ // Variables to hold the total dimensions based on layout orientation.
+ int totalWidth = 0;
+ int totalHeight = 0;
+
+ // Calculate the total size based on the layout direction and constraints.
+ if (direction == Qt::Horizontal) {
+ // Determine the number of columns:
+ // - Use maxColumns if it is greater than 0 (constraint set by the user).
+ // - Otherwise, set the number of columns to the total number of items in the layout.
+ const int numColumns = (maxColumns > 0) ? static_cast(maxColumns) : static_cast(itemList.size());
+
+ // Calculate the extra space required for overlaps between columns:
+ // - Each overlap reduces the effective width of subsequent items.
+ const int extra_space_for_overlaps = overlapOffsetWidth * (numColumns - 1);
+
+ // Total width:
+ // - The first item's width is fully included (maxItemWidth).
+ // - Add the space for all overlaps between adjacent items.
+ totalWidth = maxItemWidth + extra_space_for_overlaps;
+
+ // Determine the number of rows:
+ // - Use maxRows if maxColumns is set (to constrain the number of rows).
+ // - Otherwise, assume a single row.
+ const int numRows =
+ (maxColumns > 0) ? qCeil(static_cast(itemList.size()) / static_cast(numColumns)) : 1;
+
+ // Total height:
+ // - Multiply the number of rows by the item height (maxItemHeight).
+ // - Subtract the height overlap between rows to avoid double-counting.
+ totalHeight = maxItemHeight * numRows - (numRows - 1) * overlapOffsetHeight;
+ } else if (direction == Qt::Vertical) {
+ // Determine the number of columns:
+ // - Use maxRows to calculate how many columns are needed if a row constraint exists.
+ // - Otherwise, assume a single column.
+ const int numColumns =
+ (maxRows != 0) ? qCeil(static_cast(itemList.size()) / static_cast(maxRows)) : 1;
+
+ // Total width:
+ // - Multiply the number of columns by the item width (maxItemWidth).
+ // - Subtract the width overlap between columns to avoid double-counting.
+ totalWidth = maxItemWidth * numColumns - (numColumns - 1) * overlapOffsetWidth;
+
+ // Determine the number of rows:
+ // - Use maxRows if it is greater than 0 (constraint set by the user).
+ // - Otherwise, set the number of rows to the total number of items in the layout.
+ const int numRows = (maxRows > 0) ? static_cast(maxRows) : static_cast(itemList.size());
+
+ // Calculate the extra space required for overlaps between rows:
+ // - Each overlap reduces the effective height of subsequent items.
+ const int extraSpaceForOverlaps = overlapOffsetHeight * (numRows - 1);
+
+ // Total height:
+ // - The first item's height is fully included (maxItemHeight).
+ // - Add the space for all overlaps between adjacent items.
+ totalHeight = maxItemHeight + extraSpaceForOverlaps;
+ }
+
+ return {totalWidth, totalHeight};
+}
+
+/**
+ * @brief Returns the size hint for the layout, based on preferred size calculations.
+ *
+ * Provides a recommended size for the layout, useful for layouts that need to fit within
+ * a specific parent container size. This takes into account the preferred size and
+ * any specific item size requirements.
+ *
+ * @return The layout's recommended QSize.
+ */
+QSize OverlapLayout::sizeHint() const
+{
+ return calculatePreferredSize();
+}
+
+/**
+ * @brief Provides the minimum size hint for the layout, ensuring functionality within constraints.
+ *
+ * Defines a minimum workable size for the layout to prevent excessive compression
+ * that could distort item arrangement.
+ *
+ * @return The minimum QSize for this layout.
+ */
+QSize OverlapLayout::minimumSize() const
+{
+ return calculatePreferredSize();
+}
+
+/**
+ * @brief Sets the layout's orientation direction.
+ *
+ * @param _direction The new orientation direction (Qt::Horizontal or Qt::Vertical).
+ */
+void OverlapLayout::setDirection(const Qt::Orientation _direction)
+{
+ direction = _direction;
+}
+
+/**
+ * @brief Sets the maximum number of columns for horizontal orientation.
+ *
+ * @param _maxColumns New maximum column count.
+ */
+void OverlapLayout::setMaxColumns(const int _maxColumns)
+{
+ if (_maxColumns >= 0) {
+ maxColumns = _maxColumns;
+ }
+}
+
+/**
+ * @brief Sets the maximum number of rows for vertical orientation.
+ *
+ * @param _maxRows New maximum row count.
+ */
+void OverlapLayout::setMaxRows(const int _maxRows)
+{
+ if (_maxRows >= 0) {
+ maxRows = _maxRows;
+ }
+}
+
+/**
+ * @brief Calculates the maximum number of columns for a vertical overlap layout based on the current width.
+ *
+ * This function determines the maximum number of columns that can fit within the layout's width
+ * given the overlap percentage and item size, based on the current layout direction.
+ *
+ * @return Maximum number of columns that can fit within the layout width.
+ */
+int OverlapLayout::calculateMaxColumns() const
+{
+ if (direction != Qt::Vertical || itemList.isEmpty()) {
+ return 1; // Only relevant if the layout direction is vertical
+ }
+
+ // Determine maximum item width
+ int maxItemWidth = 0;
+ for (QLayoutItem *item : itemList) {
+ if (item == nullptr || !item->widget()) {
+ continue;
+ }
+
+ QSize itemSize = item->widget()->sizeHint();
+ maxItemWidth = qMax(maxItemWidth, itemSize.width());
+ }
+
+ const int availableWidth = parentWidget() ? parentWidget()->width() : 0;
+ // Determine the maximum number of columns that can fit
+ const int columns = availableWidth / maxItemWidth;
+
+ return qMax(1, columns);
+}
+
+/**
+ * @brief Calculates the maximum number of rows needed for a given number of columns in a vertical overlap layout.
+ *
+ * Determines how many rows are required to arrange all items given the calculated or specified number of columns.
+ *
+ * @param columns The number of columns available.
+ * @return The total number of rows required.
+ */
+int OverlapLayout::calculateRowsForColumns(const int columns) const
+{
+ if (direction != Qt::Vertical || itemList.isEmpty() || columns <= 0) {
+ return 1; // Only relevant if the layout direction is vertical and there are items
+ }
+
+ const int totalItems = static_cast(itemList.size());
+
+ return qCeil(totalItems / columns);
+}
+
+/**
+ * @brief Calculates the maximum number of rows for a horizontal overlap layout based on the current height.
+ *
+ * This function determines the maximum number of rows that can fit within the layout's height
+ * given the overlap percentage and item size, based on the current layout direction.
+ *
+ * @return Maximum number of rows that can fit within the layout height.
+ */
+int OverlapLayout::calculateMaxRows() const
+{
+ if (direction != Qt::Horizontal || itemList.isEmpty()) {
+ return 1; // Only relevant if the layout direction is horizontal
+ }
+
+ // Determine maximum item height
+ int maxItemHeight = 0;
+ for (QLayoutItem *item : itemList) {
+ if (item == nullptr || !item->widget()) {
+ continue;
+ }
+
+ QSize itemSize = item->widget()->sizeHint();
+ maxItemHeight = qMax(maxItemHeight, itemSize.height());
+ }
+
+ // Calculate the effective height of each item with the overlap applied
+ const int overlapOffsetHeight = (maxItemHeight * (100 - overlapPercentage)) / 100;
+ const int availableHeight = parentWidget() ? parentWidget()->height() : 0;
+
+ // Determine the maximum number of rows that can fit
+ const int rows = availableHeight / overlapOffsetHeight;
+
+ return qMax(1, rows);
+}
+
+/**
+ * @brief Calculates the maximum number of columns needed for a given number of rows in a horizontal overlap layout.
+ *
+ * Determines how many columns are required to arrange all items given the calculated or specified number of rows.
+ *
+ * @param rows The number of rows available.
+ * @return The total number of columns required.
+ */
+int OverlapLayout::calculateColumnsForRows(const int rows) const
+{
+ if (direction != Qt::Horizontal || itemList.isEmpty() || rows <= 0) {
+ return 1; // Only relevant if the layout direction is horizontal and there are items
+ }
+
+ const int totalItems = static_cast(itemList.size());
+
+ return qCeil(totalItems / rows);
+}
\ No newline at end of file
diff --git a/cockatrice/src/client/ui/layouts/overlap_layout.h b/cockatrice/src/client/ui/layouts/overlap_layout.h
new file mode 100644
index 000000000..1975cce89
--- /dev/null
+++ b/cockatrice/src/client/ui/layouts/overlap_layout.h
@@ -0,0 +1,44 @@
+#ifndef OVERLAP_LAYOUT_H
+#define OVERLAP_LAYOUT_H
+
+#include
+#include
+#include
+
+class OverlapLayout : public QLayout
+{
+public:
+ OverlapLayout(QWidget *parent = nullptr,
+ int overlapPercentage = 10,
+ int maxColumns = 2,
+ int maxRows = 2,
+ Qt::Orientation direction = Qt::Horizontal);
+ ~OverlapLayout();
+
+ void addItem(QLayoutItem *item) override;
+ int count() const override;
+ QLayoutItem *itemAt(int index) const override;
+ QLayoutItem *takeAt(int index) override;
+ void setGeometry(const QRect &rect) override;
+ QSize minimumSize() const override;
+ QSize sizeHint() const override;
+ void setMaxColumns(int _maxColumns);
+ void setMaxRows(int _maxRows);
+ int calculateMaxColumns() const;
+ int calculateRowsForColumns(int columns) const;
+ int calculateMaxRows() const;
+ int calculateColumnsForRows(int rows) const;
+ void setDirection(Qt::Orientation _direction);
+
+private:
+ QList itemList;
+ int overlapPercentage;
+ int maxColumns;
+ int maxRows;
+ Qt::Orientation direction;
+
+ // Calculate the preferred size of the layout
+ QSize calculatePreferredSize() const;
+};
+
+#endif // OVERLAP_LAYOUT_H
diff --git a/cockatrice/src/client/ui/layouts/vertical_flow_layout.cpp b/cockatrice/src/client/ui/layouts/vertical_flow_layout.cpp
new file mode 100644
index 000000000..90e154662
--- /dev/null
+++ b/cockatrice/src/client/ui/layouts/vertical_flow_layout.cpp
@@ -0,0 +1,144 @@
+#include "vertical_flow_layout.h"
+
+/**
+ * @brief Constructs a VerticalFlowLayout instance with the specified parent widget.
+ * This layout arranges items in rows within the given width, automatically adjusting its height.
+ * @param parent The parent widget to which this layout belongs.
+ * @param margin The layout margin.
+ * @param hSpacing The horizontal spacing between items.
+ * @param vSpacing The vertical spacing between items.
+ */
+VerticalFlowLayout::VerticalFlowLayout(QWidget *parent, const int margin, const int hSpacing, const int vSpacing)
+ : FlowLayout(parent, margin, hSpacing, vSpacing)
+{
+}
+
+/**
+ * @brief Destructor for VerticalFlowLayout, responsible for cleaning up layout items.
+ */
+VerticalFlowLayout::~VerticalFlowLayout()
+{
+ QLayoutItem *item;
+ while ((item = FlowLayout::takeAt(0))) {
+ delete item;
+ }
+}
+
+/**
+ * @brief Calculates the required height to display all items, given a specified width.
+ * This method arranges items into rows and determines the total height needed.
+ * @param width The available width for arranging layout items.
+ * @return The total height required to fit all items, organized in rows constrained by the given width.
+ */
+int VerticalFlowLayout::heightForWidth(const int width) const
+{
+ int height = 0;
+ int rowWidth = 0;
+ int rowHeight = 0;
+
+ for (const QLayoutItem *item : items) {
+ if (item == nullptr || item->isEmpty()) {
+ continue;
+ }
+
+ int itemWidth = item->sizeHint().width() + horizontalSpacing();
+ if (rowWidth + itemWidth > width) {
+ height += rowHeight + verticalSpacing();
+ rowWidth = itemWidth;
+ rowHeight = item->sizeHint().height();
+ } else {
+ rowWidth += itemWidth;
+ rowHeight = qMax(rowHeight, item->sizeHint().height());
+ }
+ }
+ height += rowHeight; // Add height of the last row
+ return height;
+}
+
+/**
+ * @brief Sets the geometry of the layout items, arranging them in rows within the given width.
+ * @param rect The rectangle area defining the layout space.
+ */
+void VerticalFlowLayout::setGeometry(const QRect &rect)
+{
+ // If we have a parent scroll area, we're clamped to that, else we use our own rectangle.
+ const int availableWidth = getParentScrollAreaWidth() == 0 ? rect.width() : getParentScrollAreaWidth();
+
+ const int totalHeight = layoutAllRows(rect.x(), rect.y(), availableWidth);
+
+ if (QWidget *parentWidgetPtr = parentWidget()) {
+ parentWidgetPtr->setMinimumSize(availableWidth, totalHeight);
+ }
+}
+
+/**
+ * @brief Lays out items into rows according to the available width, starting from a given origin.
+ * Each row is arranged within `availableWidth`, wrapping to a new row as necessary.
+ * @param originX The x-coordinate for the layout start position.
+ * @param originY The y-coordinate for the layout start position.
+ * @param availableWidth The width within which each row is constrained.
+ * @return The total height after arranging all rows.
+ */
+int VerticalFlowLayout::layoutAllRows(const int originX, const int originY, const int availableWidth)
+{
+ QVector rowItems; // Holds items for the current row
+ int currentXPosition = originX; // Tracks the x-coordinate while placing items
+ int currentYPosition = originY; // Tracks the y-coordinate, moving down after each row
+
+ int rowHeight = 0; // Tracks the maximum height of items in the current row
+
+ for (QLayoutItem *item : items) {
+ if (item == nullptr || item->isEmpty()) {
+ continue;
+ }
+
+ QSize itemSize = item->sizeHint(); // The suggested size for the current item
+ int itemWidth = itemSize.width() + horizontalSpacing(); // Item width plus spacing
+
+ // Check if the current item fits in the remaining width of the current row
+ if (currentXPosition + itemWidth > availableWidth) {
+ // If not, layout the current row and start a new row
+ layoutSingleRow(rowItems, originX, currentYPosition);
+ rowItems.clear(); // Reset the list for the new row
+ currentXPosition = originX; // Reset x-position to the row's start
+ currentYPosition += rowHeight + verticalSpacing(); // Move y-position down to the next row
+ rowHeight = 0; // Reset row height for the new row
+ }
+
+ // Add the item to the current row
+ rowItems.append(item);
+ rowHeight = qMax(rowHeight, itemSize.height()); // Update the row's height to the tallest item
+ currentXPosition += itemWidth + horizontalSpacing(); // Move x-position for the next item
+ }
+
+ // Layout the final row if there are any remaining items
+ layoutSingleRow(rowItems, originX, currentYPosition);
+
+ // Return the total height used, including the last row's height
+ return currentYPosition + rowHeight;
+}
+
+/**
+ * @brief Arranges a single row of items within specified x and y starting positions.
+ * @param rowItems A list of items to be arranged in the row.
+ * @param x The starting x-coordinate for the row.
+ * @param y The starting y-coordinate for the row.
+ */
+void VerticalFlowLayout::layoutSingleRow(const QVector &rowItems, int x, const int y)
+{
+ for (QLayoutItem *item : rowItems) {
+ if (item == nullptr || item->isEmpty()) {
+ continue;
+ }
+
+ // Get the maximum allowed size for the item
+ QSize itemMaxSize = item->widget()->maximumSize();
+ // Constrain the item's width and height to its size hint or maximum size
+ const int itemWidth = qMin(item->sizeHint().width(), itemMaxSize.width());
+ const int itemHeight = qMin(item->sizeHint().height(), itemMaxSize.height());
+ // Set the item's geometry based on the computed size and position
+ item->setGeometry(QRect(QPoint(x, y), QSize(itemWidth, itemHeight)));
+ // Move the x-position to the right, leaving space for horizontal spacing
+ x += itemWidth + horizontalSpacing();
+ }
+}
diff --git a/cockatrice/src/client/ui/layouts/vertical_flow_layout.h b/cockatrice/src/client/ui/layouts/vertical_flow_layout.h
new file mode 100644
index 000000000..5dcd4945c
--- /dev/null
+++ b/cockatrice/src/client/ui/layouts/vertical_flow_layout.h
@@ -0,0 +1,19 @@
+#ifndef VERTICAL_FLOW_LAYOUT_H
+#define VERTICAL_FLOW_LAYOUT_H
+
+#include "flow_layout.h"
+
+class VerticalFlowLayout : public FlowLayout
+{
+public:
+ explicit VerticalFlowLayout(QWidget *parent = nullptr, int margin = 0, int hSpacing = 0, int vSpacing = 0);
+ ~VerticalFlowLayout() override;
+
+ [[nodiscard]] int heightForWidth(int width) const override;
+
+ void setGeometry(const QRect &rect) override;
+ int layoutAllRows(int originX, int originY, int availableWidth) override;
+ void layoutSingleRow(const QVector &rowItems, int x, int y) override;
+};
+
+#endif // VERTICAL_FLOW_LAYOUT_H
\ No newline at end of file
diff --git a/cockatrice/src/lineeditcompleter.cpp b/cockatrice/src/client/ui/line_edit_completer.cpp
similarity index 99%
rename from cockatrice/src/lineeditcompleter.cpp
rename to cockatrice/src/client/ui/line_edit_completer.cpp
index f8cad38aa..31e82248d 100644
--- a/cockatrice/src/lineeditcompleter.cpp
+++ b/cockatrice/src/client/ui/line_edit_completer.cpp
@@ -1,4 +1,4 @@
-#include "lineeditcompleter.h"
+#include "line_edit_completer.h"
#include
#include
diff --git a/cockatrice/src/lineeditcompleter.h b/cockatrice/src/client/ui/line_edit_completer.h
similarity index 93%
rename from cockatrice/src/lineeditcompleter.h
rename to cockatrice/src/client/ui/line_edit_completer.h
index 5be1562d2..2eadb8347 100644
--- a/cockatrice/src/lineeditcompleter.h
+++ b/cockatrice/src/client/ui/line_edit_completer.h
@@ -1,7 +1,7 @@
#ifndef LINEEDITCOMPLETER_H
#define LINEEDITCOMPLETER_H
-#include "customlineedit.h"
+#include "../../deck/custom_line_edit.h"
#include
#include
diff --git a/cockatrice/src/phasestoolbar.cpp b/cockatrice/src/client/ui/phases_toolbar.cpp
similarity index 95%
rename from cockatrice/src/phasestoolbar.cpp
rename to cockatrice/src/client/ui/phases_toolbar.cpp
index a0202782c..036a7c19e 100644
--- a/cockatrice/src/phasestoolbar.cpp
+++ b/cockatrice/src/client/ui/phases_toolbar.cpp
@@ -1,10 +1,10 @@
-#include "phasestoolbar.h"
+#include "phases_toolbar.h"
#include "pb/command_draw_cards.pb.h"
#include "pb/command_next_turn.pb.h"
#include "pb/command_set_active_phase.pb.h"
#include "pb/command_set_card_attr.pb.h"
-#include "pixmapgenerator.h"
+#include "pixel_map_generator.h"
#include
#include
@@ -74,13 +74,15 @@ void PhaseButton::updateAnimation()
if (!highlightable)
return;
- if (active) {
- if (++activeAnimationCounter >= 10)
- activeAnimationTimer->stop();
+ // the counter ticks up to 10 when active and down to 0 when inactive
+ if (active && activeAnimationCounter < 10) {
+ ++activeAnimationCounter;
+ } else if (!active && activeAnimationCounter > 0) {
+ --activeAnimationCounter;
} else {
- if (--activeAnimationCounter <= 0)
- activeAnimationTimer->stop();
+ activeAnimationTimer->stop();
}
+
update();
}
diff --git a/cockatrice/src/phasestoolbar.h b/cockatrice/src/client/ui/phases_toolbar.h
similarity index 97%
rename from cockatrice/src/phasestoolbar.h
rename to cockatrice/src/client/ui/phases_toolbar.h
index 2de1cfe6a..3567e741b 100644
--- a/cockatrice/src/phasestoolbar.h
+++ b/cockatrice/src/client/ui/phases_toolbar.h
@@ -1,7 +1,7 @@
#ifndef PHASESTOOLBAR_H
#define PHASESTOOLBAR_H
-#include "abstractgraphicsitem.h"
+#include "../../game/board/abstract_graphics_item.h"
#include
#include
diff --git a/cockatrice/src/pictureloader.cpp b/cockatrice/src/client/ui/picture_loader.cpp
similarity index 83%
rename from cockatrice/src/pictureloader.cpp
rename to cockatrice/src/client/ui/picture_loader.cpp
index b7f4a7fc1..a28a98eac 100644
--- a/cockatrice/src/pictureloader.cpp
+++ b/cockatrice/src/client/ui/picture_loader.cpp
@@ -1,17 +1,14 @@
-#include "pictureloader.h"
+#include "picture_loader.h"
-#include "carddatabase.h"
-#include "main.h"
-#include "settingscache.h"
-#include "thememanager.h"
+#include "../../game/cards/card_database_manager.h"
+#include "../../settings/cache_settings.h"
#include
#include
#include
#include
-#include
#include
-#include
+#include
#include
#include
#include
@@ -23,7 +20,6 @@
#include
#include
#include
-#include
#include
#include
#include
@@ -43,6 +39,15 @@ PictureToLoad::PictureToLoad(CardInfoPtr _card)
sortedSets << CardSet::newInstance("", "", "", QDate());
}
std::sort(sortedSets.begin(), sortedSets.end(), SetDownloadPriorityComparator());
+ // If the pixmapCacheKey corresponds to a specific set, we have to try to load it first.
+ for (const auto &set : card->getSets()) {
+ if (QLatin1String("card_") + card->getName() + QString("_") + QString(set.getProperty("uuid")) ==
+ card->getPixmapCacheKey()) {
+ long long setIndex = sortedSets.indexOf(set.getPtr());
+ CardSetPtr setForCardProviderID = sortedSets.takeAt(setIndex);
+ sortedSets.prepend(setForCardProviderID);
+ }
+ }
// The first time called, nextSet will also populate the Urls for the first set.
nextSet();
}
@@ -127,6 +132,8 @@ PictureLoaderWorker::PictureLoaderWorker()
#endif
auto cache = new QNetworkDiskCache(this);
cache->setCacheDirectory(SettingsCache::instance().getNetworkCachePath());
+ cache->setMaximumCacheSize(1024L * 1024L *
+ static_cast(SettingsCache::instance().getNetworkCacheSizeInMB()));
// Note: the settings is in MB, but QNetworkDiskCache uses bytes
connect(&SettingsCache::instance(), &SettingsCache::networkCacheSizeChanged, cache,
[cache](int newSizeInMB) { cache->setMaximumCacheSize(1024L * 1024L * static_cast(newSizeInMB)); });
@@ -136,6 +143,13 @@ PictureLoaderWorker::PictureLoaderWorker()
networkManager->setRedirectPolicy(QNetworkRequest::ManualRedirectPolicy);
connect(networkManager, SIGNAL(finished(QNetworkReply *)), this, SLOT(picDownloadFinished(QNetworkReply *)));
+ cacheFilePath = SettingsCache::instance().getRedirectCachePath() + REDIRECT_CACHE_FILENAME;
+ loadRedirectCache();
+ cleanStaleEntries();
+
+ connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, this,
+ &PictureLoaderWorker::saveRedirectCache);
+
pictureLoaderThread = new QThread;
pictureLoaderThread->start(QThread::LowPriority);
moveToThread(pictureLoaderThread);
@@ -170,8 +184,11 @@ void PictureLoaderWorker::processLoadQueue()
qDebug().nospace() << "PictureLoader: [card: " << cardName << " set: " << setName
<< "]: Trying to load picture";
- if (cardImageExistsOnDisk(setName, correctedCardName)) {
- continue;
+ if (CardDatabaseManager::getInstance()->isProviderIdForPreferredPrinting(
+ cardName, cardBeingLoaded.getCard()->getPixmapCacheKey())) {
+ if (cardImageExistsOnDisk(setName, correctedCardName)) {
+ continue;
+ }
}
qDebug().nospace() << "PictureLoader: [card: " << cardName << " set: " << setName
@@ -190,7 +207,7 @@ bool PictureLoaderWorker::cardImageExistsOnDisk(QString &setName, QString &corre
QImageReader imgReader;
imgReader.setDecideFormatFromContent(true);
QList picsPaths = QList();
- QDirIterator it(customPicsPath, QDirIterator::Subdirectories);
+ QDirIterator it(customPicsPath, QDirIterator::Subdirectories | QDirIterator::FollowSymlinks);
// Recursively check all subdirectories of the CUSTOM folder
while (it.hasNext()) {
@@ -435,11 +452,104 @@ bool PictureLoaderWorker::imageIsBlackListed(const QByteArray &picData)
QNetworkReply *PictureLoaderWorker::makeRequest(const QUrl &url)
{
+ // Check if the redirect is cached
+ QUrl cachedRedirect = getCachedRedirect(url);
+ if (!cachedRedirect.isEmpty()) {
+ qDebug().nospace() << "PictureLoader: [card: " << cardBeingDownloaded.getCard()->getCorrectedName()
+ << " set: " << cardBeingDownloaded.getSetName() << "]: Using cached redirect for "
+ << url.toDisplayString() << " to " << cachedRedirect.toDisplayString();
+ return makeRequest(cachedRedirect); // Use the cached redirect
+ }
+
QNetworkRequest req(url);
+
if (!picDownload) {
req.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::AlwaysCache);
}
- return networkManager->get(req);
+
+ QNetworkReply *reply = networkManager->get(req);
+
+ connect(reply, &QNetworkReply::finished, this, [this, reply, url]() {
+ QVariant redirectTarget = reply->attribute(QNetworkRequest::RedirectionTargetAttribute);
+
+ if (redirectTarget.isValid()) {
+ QUrl redirectUrl = redirectTarget.toUrl();
+ if (redirectUrl.isRelative()) {
+ redirectUrl = url.resolved(redirectUrl);
+ }
+
+ cacheRedirect(url, redirectUrl);
+ qDebug().nospace() << "PictureLoader: [card: " << cardBeingDownloaded.getCard()->getCorrectedName()
+ << " set: " << cardBeingDownloaded.getSetName() << "]: Caching redirect from "
+ << url.toDisplayString() << " to " << redirectUrl.toDisplayString();
+ }
+
+ reply->deleteLater();
+ });
+
+ return reply;
+}
+
+void PictureLoaderWorker::cacheRedirect(const QUrl &originalUrl, const QUrl &redirectUrl)
+{
+ redirectCache[originalUrl] = qMakePair(redirectUrl, QDateTime::currentDateTimeUtc());
+ saveRedirectCache();
+}
+
+QUrl PictureLoaderWorker::getCachedRedirect(const QUrl &originalUrl) const
+{
+ if (redirectCache.contains(originalUrl)) {
+ return redirectCache[originalUrl].first;
+ }
+ return {};
+}
+
+void PictureLoaderWorker::loadRedirectCache()
+{
+ QSettings settings(cacheFilePath, QSettings::IniFormat);
+
+ redirectCache.clear();
+ int size = settings.beginReadArray(REDIRECT_HEADER_NAME);
+ for (int i = 0; i < size; ++i) {
+ settings.setArrayIndex(i);
+ QUrl originalUrl = settings.value(REDIRECT_ORIGINAL_URL).toUrl();
+ QUrl redirectUrl = settings.value(REDIRECT_URL).toUrl();
+ QDateTime timestamp = settings.value(REDIRECT_TIMESTAMP).toDateTime();
+
+ if (originalUrl.isValid() && redirectUrl.isValid()) {
+ redirectCache[originalUrl] = qMakePair(redirectUrl, timestamp);
+ }
+ }
+ settings.endArray();
+}
+
+void PictureLoaderWorker::saveRedirectCache() const
+{
+ QSettings settings(cacheFilePath, QSettings::IniFormat);
+
+ settings.beginWriteArray(REDIRECT_HEADER_NAME, static_cast(redirectCache.size()));
+ int index = 0;
+ for (auto it = redirectCache.cbegin(); it != redirectCache.cend(); ++it) {
+ settings.setArrayIndex(index++);
+ settings.setValue(REDIRECT_ORIGINAL_URL, it.key());
+ settings.setValue(REDIRECT_URL, it.value().first);
+ settings.setValue(REDIRECT_TIMESTAMP, it.value().second);
+ }
+ settings.endArray();
+}
+
+void PictureLoaderWorker::cleanStaleEntries()
+{
+ QDateTime now = QDateTime::currentDateTimeUtc();
+
+ auto it = redirectCache.begin();
+ while (it != redirectCache.end()) {
+ if (it.value().second.addDays(SettingsCache::instance().getRedirectCacheTtl()) < now) {
+ it = redirectCache.erase(it); // Remove stale entry
+ } else {
+ ++it;
+ }
+ }
}
void PictureLoaderWorker::picDownloadFinished(QNetworkReply *reply)
diff --git a/cockatrice/src/pictureloader.h b/cockatrice/src/client/ui/picture_loader.h
similarity index 82%
rename from cockatrice/src/pictureloader.h
rename to cockatrice/src/client/ui/picture_loader.h
index 65eafaea6..245114358 100644
--- a/cockatrice/src/pictureloader.h
+++ b/cockatrice/src/client/ui/picture_loader.h
@@ -1,7 +1,7 @@
#ifndef PICTURELOADER_H
#define PICTURELOADER_H
-#include "carddatabase.h"
+#include "../../game/cards/card_database.h"
#include
#include
@@ -11,6 +11,12 @@ class QNetworkAccessManager;
class QNetworkReply;
class QThread;
+#define REDIRECT_HEADER_NAME "redirects"
+#define REDIRECT_ORIGINAL_URL "original"
+#define REDIRECT_URL "redirect"
+#define REDIRECT_TIMESTAMP "timestamp"
+#define REDIRECT_CACHE_FILENAME "cache.ini"
+
class PictureToLoad
{
private:
@@ -83,6 +89,9 @@ private:
QList loadQueue;
QMutex mutex;
QNetworkAccessManager *networkManager;
+ QHash> redirectCache; // Stores redirect and timestamp
+ QString cacheFilePath; // Path to persistent storage
+ static constexpr int CacheTTLInDays = 30; // TODO: Make user configurable
QList cardsToDownload;
PictureToLoad cardBeingLoaded;
PictureToLoad cardBeingDownloaded;
@@ -91,6 +100,12 @@ private:
bool cardImageExistsOnDisk(QString &setName, QString &correctedCardName);
bool imageIsBlackListed(const QByteArray &);
QNetworkReply *makeRequest(const QUrl &url);
+ void cacheRedirect(const QUrl &originalUrl, const QUrl &redirectUrl);
+ QUrl getCachedRedirect(const QUrl &originalUrl) const;
+ void loadRedirectCache();
+ void saveRedirectCache() const;
+ void cleanStaleEntries();
+
private slots:
void picDownloadFinished(QNetworkReply *reply);
void picDownloadFailed();
diff --git a/cockatrice/src/pixmapgenerator.cpp b/cockatrice/src/client/ui/pixel_map_generator.cpp
similarity index 99%
rename from cockatrice/src/pixmapgenerator.cpp
rename to cockatrice/src/client/ui/pixel_map_generator.cpp
index 66d9283bb..09dc2d1ae 100644
--- a/cockatrice/src/pixmapgenerator.cpp
+++ b/cockatrice/src/client/ui/pixel_map_generator.cpp
@@ -1,4 +1,4 @@
-#include "pixmapgenerator.h"
+#include "pixel_map_generator.h"
#include "pb/serverinfo_user.pb.h"
diff --git a/cockatrice/src/pixmapgenerator.h b/cockatrice/src/client/ui/pixel_map_generator.h
similarity index 100%
rename from cockatrice/src/pixmapgenerator.h
rename to cockatrice/src/client/ui/pixel_map_generator.h
diff --git a/cockatrice/src/thememanager.cpp b/cockatrice/src/client/ui/theme_manager.cpp
similarity index 98%
rename from cockatrice/src/thememanager.cpp
rename to cockatrice/src/client/ui/theme_manager.cpp
index 45d3b10e0..249032dfa 100644
--- a/cockatrice/src/thememanager.cpp
+++ b/cockatrice/src/client/ui/theme_manager.cpp
@@ -1,6 +1,6 @@
-#include "thememanager.h"
+#include "theme_manager.h"
-#include "settingscache.h"
+#include "../../settings/cache_settings.h"
#include
#include
diff --git a/cockatrice/src/thememanager.h b/cockatrice/src/client/ui/theme_manager.h
similarity index 100%
rename from cockatrice/src/thememanager.h
rename to cockatrice/src/client/ui/theme_manager.h
diff --git a/cockatrice/src/tip_of_the_day.cpp b/cockatrice/src/client/ui/tip_of_the_day.cpp
similarity index 100%
rename from cockatrice/src/tip_of_the_day.cpp
rename to cockatrice/src/client/ui/tip_of_the_day.cpp
diff --git a/cockatrice/src/tip_of_the_day.h b/cockatrice/src/client/ui/tip_of_the_day.h
similarity index 100%
rename from cockatrice/src/tip_of_the_day.h
rename to cockatrice/src/client/ui/tip_of_the_day.h
diff --git a/cockatrice/src/cardinfowidget.cpp b/cockatrice/src/client/ui/widgets/cards/card_info_display_widget.cpp
similarity index 67%
rename from cockatrice/src/cardinfowidget.cpp
rename to cockatrice/src/client/ui/widgets/cards/card_info_display_widget.cpp
index 4578c8b66..6e774a5a4 100644
--- a/cockatrice/src/cardinfowidget.cpp
+++ b/cockatrice/src/client/ui/widgets/cards/card_info_display_widget.cpp
@@ -1,22 +1,23 @@
-#include "cardinfowidget.h"
+#include "card_info_display_widget.h"
-#include "cardinfopicture.h"
-#include "cardinfotext.h"
-#include "carditem.h"
-#include "main.h"
+#include "../../../../game/cards/card_database_manager.h"
+#include "../../../../game/cards/card_item.h"
+#include "../../../../main.h"
+#include "card_info_picture_widget.h"
+#include "card_info_text_widget.h"
#include
#include
#include
#include
-CardInfoWidget::CardInfoWidget(const QString &cardName, QWidget *parent, Qt::WindowFlags flags)
+CardInfoDisplayWidget::CardInfoDisplayWidget(const QString &cardName, QWidget *parent, Qt::WindowFlags flags)
: QFrame(parent, flags), aspectRatio((qreal)CARD_HEIGHT / (qreal)CARD_WIDTH), info(nullptr)
{
setContentsMargins(3, 3, 3, 3);
- pic = new CardInfoPicture();
+ pic = new CardInfoPictureWidget();
pic->setObjectName("pic");
- text = new CardInfoText();
+ text = new CardInfoTextWidget();
text->setObjectName("text");
connect(text, SIGNAL(linkActivated(const QString &)), this, SLOT(setCard(const QString &)));
@@ -42,7 +43,7 @@ CardInfoWidget::CardInfoWidget(const QString &cardName, QWidget *parent, Qt::Win
resize(width(), sizeHint().height());
}
-void CardInfoWidget::setCard(CardInfoPtr card)
+void CardInfoDisplayWidget::setCard(CardInfoPtr card)
{
if (info)
disconnect(info.data(), nullptr, this, nullptr);
@@ -54,20 +55,20 @@ void CardInfoWidget::setCard(CardInfoPtr card)
pic->setCard(info);
}
-void CardInfoWidget::setCard(const QString &cardName)
+void CardInfoDisplayWidget::setCard(const QString &cardName)
{
- setCard(db->guessCard(cardName));
+ setCard(CardDatabaseManager::getInstance()->guessCard(cardName));
if (info == nullptr) {
text->setInvalidCardName(cardName);
}
}
-void CardInfoWidget::setCard(AbstractCardItem *card)
+void CardInfoDisplayWidget::setCard(AbstractCardItem *card)
{
setCard(card->getInfo());
}
-void CardInfoWidget::clear()
+void CardInfoDisplayWidget::clear()
{
setCard((CardInfoPtr) nullptr);
}
diff --git a/cockatrice/src/cardinfowidget.h b/cockatrice/src/client/ui/widgets/cards/card_info_display_widget.h
similarity index 55%
rename from cockatrice/src/cardinfowidget.h
rename to cockatrice/src/client/ui/widgets/cards/card_info_display_widget.h
index fd1ec4a44..21498d10c 100644
--- a/cockatrice/src/cardinfowidget.h
+++ b/cockatrice/src/client/ui/widgets/cards/card_info_display_widget.h
@@ -1,28 +1,28 @@
#ifndef CARDINFOWIDGET_H
#define CARDINFOWIDGET_H
-#include "carddatabase.h"
+#include "../../../../game/cards/card_database.h"
#include
#include
#include
-class CardInfoPicture;
-class CardInfoText;
+class CardInfoPictureWidget;
+class CardInfoTextWidget;
class AbstractCardItem;
-class CardInfoWidget : public QFrame
+class CardInfoDisplayWidget : public QFrame
{
Q_OBJECT
private:
qreal aspectRatio;
CardInfoPtr info;
- CardInfoPicture *pic;
- CardInfoText *text;
+ CardInfoPictureWidget *pic;
+ CardInfoTextWidget *text;
public:
- explicit CardInfoWidget(const QString &cardName, QWidget *parent = nullptr, Qt::WindowFlags f = {});
+ explicit CardInfoDisplayWidget(const QString &cardName, QWidget *parent = nullptr, Qt::WindowFlags f = {});
public slots:
void setCard(CardInfoPtr card);
diff --git a/cockatrice/src/cardframe.cpp b/cockatrice/src/client/ui/widgets/cards/card_info_frame_widget.cpp
similarity index 69%
rename from cockatrice/src/cardframe.cpp
rename to cockatrice/src/client/ui/widgets/cards/card_info_frame_widget.cpp
index a9044652f..fd9c84750 100644
--- a/cockatrice/src/cardframe.cpp
+++ b/cockatrice/src/client/ui/widgets/cards/card_info_frame_widget.cpp
@@ -1,21 +1,22 @@
-#include "cardframe.h"
+#include "card_info_frame_widget.h"
-#include "cardinfopicture.h"
-#include "cardinfotext.h"
-#include "carditem.h"
-#include "main.h"
-#include "settingscache.h"
+#include "../../../../game/cards/card_database_manager.h"
+#include "../../../../game/cards/card_item.h"
+#include "../../../../settings/cache_settings.h"
+#include "card_info_picture_widget.h"
+#include "card_info_text_widget.h"
#include
#include
#include
-CardFrame::CardFrame(const QString &cardName, QWidget *parent) : QTabWidget(parent), info(nullptr), cardTextOnly(false)
+CardInfoFrameWidget::CardInfoFrameWidget(const QString &cardName, QWidget *parent)
+ : QTabWidget(parent), info(nullptr), cardTextOnly(false)
{
setContentsMargins(3, 3, 3, 3);
- pic = new CardInfoPicture();
+ pic = new CardInfoPictureWidget();
pic->setObjectName("pic");
- text = new CardInfoText();
+ text = new CardInfoTextWidget();
text->setObjectName("text");
connect(text, SIGNAL(linkActivated(const QString &)), this, SLOT(setCard(const QString &)));
@@ -57,17 +58,17 @@ CardFrame::CardFrame(const QString &cardName, QWidget *parent) : QTabWidget(pare
setViewMode(SettingsCache::instance().getCardInfoViewMode());
- setCard(db->getCard(cardName));
+ setCard(CardDatabaseManager::getInstance()->getCard(cardName));
}
-void CardFrame::retranslateUi()
+void CardInfoFrameWidget::retranslateUi()
{
setTabText(ImageOnlyView, tr("Image"));
setTabText(TextOnlyView, tr("Description"));
setTabText(ImageAndTextView, tr("Both"));
}
-void CardFrame::setViewMode(int mode)
+void CardInfoFrameWidget::setViewMode(int mode)
{
if (currentIndex() != mode)
setCurrentIndex(mode);
@@ -89,7 +90,7 @@ void CardFrame::setViewMode(int mode)
SettingsCache::instance().setCardInfoViewMode(mode);
}
-void CardFrame::setCard(CardInfoPtr card)
+void CardInfoFrameWidget::setCard(CardInfoPtr card)
{
if (info) {
disconnect(info.data(), nullptr, this, nullptr);
@@ -105,19 +106,24 @@ void CardFrame::setCard(CardInfoPtr card)
pic->setCard(info);
}
-void CardFrame::setCard(const QString &cardName)
+void CardInfoFrameWidget::setCard(const QString &cardName)
{
- setCard(db->guessCard(cardName));
+ setCard(CardDatabaseManager::getInstance()->guessCard(cardName));
}
-void CardFrame::setCard(AbstractCardItem *card)
+void CardInfoFrameWidget::setCard(const QString &cardName, const QString &providerId)
+{
+ setCard(CardDatabaseManager::getInstance()->getCardByNameAndProviderId(cardName, providerId));
+}
+
+void CardInfoFrameWidget::setCard(AbstractCardItem *card)
{
if (card) {
setCard(card->getInfo());
}
}
-void CardFrame::clearCard()
+void CardInfoFrameWidget::clearCard()
{
setCard((CardInfoPtr) nullptr);
}
diff --git a/cockatrice/src/cardframe.h b/cockatrice/src/client/ui/widgets/cards/card_info_frame_widget.h
similarity index 62%
rename from cockatrice/src/cardframe.h
rename to cockatrice/src/client/ui/widgets/cards/card_info_frame_widget.h
index 6e11c912b..a9a4a0597 100644
--- a/cockatrice/src/cardframe.h
+++ b/cockatrice/src/client/ui/widgets/cards/card_info_frame_widget.h
@@ -1,23 +1,23 @@
#ifndef CARDFRAME_H
#define CARDFRAME_H
-#include "carddatabase.h"
+#include "../../../../game/cards/card_database.h"
#include
class AbstractCardItem;
-class CardInfoPicture;
-class CardInfoText;
+class CardInfoPictureWidget;
+class CardInfoTextWidget;
class QVBoxLayout;
class QSplitter;
-class CardFrame : public QTabWidget
+class CardInfoFrameWidget : public QTabWidget
{
Q_OBJECT
private:
CardInfoPtr info;
- CardInfoPicture *pic;
- CardInfoText *text;
+ CardInfoPictureWidget *pic;
+ CardInfoTextWidget *text;
bool cardTextOnly;
QWidget *tab1, *tab2, *tab3;
QVBoxLayout *tab1Layout, *tab2Layout, *tab3Layout;
@@ -30,12 +30,13 @@ public:
TextOnlyView,
ImageAndTextView
};
- explicit CardFrame(const QString &cardName = QString(), QWidget *parent = nullptr);
+ explicit CardInfoFrameWidget(const QString &cardName = QString(), QWidget *parent = nullptr);
void retranslateUi();
public slots:
void setCard(CardInfoPtr card);
void setCard(const QString &cardName);
+ void setCard(const QString &cardName, const QString &providerId);
void setCard(AbstractCardItem *card);
void clearCard();
void setViewMode(int mode);
diff --git a/cockatrice/src/client/ui/widgets/cards/card_info_picture_enlarged_widget.cpp b/cockatrice/src/client/ui/widgets/cards/card_info_picture_enlarged_widget.cpp
new file mode 100644
index 000000000..47b8dddce
--- /dev/null
+++ b/cockatrice/src/client/ui/widgets/cards/card_info_picture_enlarged_widget.cpp
@@ -0,0 +1,95 @@
+#include "card_info_picture_enlarged_widget.h"
+
+#include "../../picture_loader.h"
+
+#include
+#include
+#include
+
+/**
+ * @brief Constructs a CardPictureEnlargedWidget.
+ * @param parent The parent widget.
+ *
+ * Sets the widget's window flags to keep it displayed as a tooltip overlay.
+ */
+CardInfoPictureEnlargedWidget::CardInfoPictureEnlargedWidget(QWidget *parent)
+ : QWidget(parent), pixmapDirty(true), info(nullptr)
+{
+ setWindowFlags(Qt::ToolTip); // Keeps this widget on top of everything
+ setAttribute(Qt::WA_TranslucentBackground);
+}
+
+/**
+ * @brief Loads the pixmap based on the given size and card information.
+ * @param size The desired size for the loaded pixmap.
+ *
+ * If card information is available, it loads the card's specific pixmap. Otherwise, it loads a default card back
+ * pixmap.
+ */
+void CardInfoPictureEnlargedWidget::loadPixmap(const QSize &size)
+{
+ if (info) {
+ PictureLoader::getPixmap(enlargedPixmap, info, size);
+ } else {
+ PictureLoader::getCardBackPixmap(enlargedPixmap, size);
+ }
+ pixmapDirty = false;
+}
+
+/**
+ * @brief Sets the pixmap for the widget based on a provided card.
+ * @param card The card information to load.
+ * @param size The desired size for the pixmap.
+ *
+ * Sets the widget's pixmap to the card image and resizes the widget to match the specified size. Triggers a repaint.
+ */
+void CardInfoPictureEnlargedWidget::setCardPixmap(CardInfoPtr card, const QSize size)
+{
+ info = std::move(card);
+ loadPixmap(size);
+
+ setFixedSize(size); // Set the widget size to the enlarged size
+
+ update(); // Trigger a repaint
+}
+
+/**
+ * @brief Custom paint event that draws the enlarged card image with rounded corners.
+ * @param event The paint event (unused).
+ *
+ * Checks if the pixmap is valid. Then, calculates the size and position for centering the
+ * scaled pixmap within the widget, applies rounded corners, and draws the pixmap.
+ */
+void CardInfoPictureEnlargedWidget::paintEvent(QPaintEvent *event)
+{
+ Q_UNUSED(event);
+
+ if (width() == 0 || height() == 0 || enlargedPixmap.isNull()) {
+ return;
+ }
+
+ if (pixmapDirty) {
+ loadPixmap(size());
+ }
+
+ // Scale the size of the pixmap to fit the widget while maintaining the aspect ratio
+ QSize scaledSize = enlargedPixmap.size().scaled(size().width(), size().height(), Qt::KeepAspectRatio);
+
+ // Calculate the position to center the scaled pixmap
+ QPoint topLeft{(width() - scaledSize.width()) / 2, (height() - scaledSize.height()) / 2};
+
+ // Define the radius for rounded corners
+ qreal radius = 0.05 * scaledSize.width(); // Adjust the radius as needed for rounded corners
+
+ QStylePainter painter(this);
+ // Fill the background with transparent color to ensure rounded corners are rendered properly
+ painter.fillRect(rect(), Qt::transparent); // Use the transparent background
+
+ QPainterPath shape;
+ shape.addRoundedRect(QRect(topLeft, scaledSize), radius, radius);
+ painter.setClipPath(shape); // Set the clipping path
+
+ // Draw the pixmap scaled to the calculated size
+ painter.drawItemPixmap(QRect(topLeft, scaledSize), Qt::AlignCenter,
+ enlargedPixmap.scaled(scaledSize, Qt::KeepAspectRatio, Qt::SmoothTransformation));
+}
diff --git a/cockatrice/src/client/ui/widgets/cards/card_info_picture_enlarged_widget.h b/cockatrice/src/client/ui/widgets/cards/card_info_picture_enlarged_widget.h
new file mode 100644
index 000000000..73b0a9cc5
--- /dev/null
+++ b/cockatrice/src/client/ui/widgets/cards/card_info_picture_enlarged_widget.h
@@ -0,0 +1,38 @@
+#ifndef CARD_PICTURE_ENLARGED_WIDGET_H
+#define CARD_PICTURE_ENLARGED_WIDGET_H
+
+#include "../../../../game/cards/card_database.h"
+
+#include
+#include
+
+class CardInfoPictureEnlargedWidget final : public QWidget
+{
+ Q_OBJECT
+
+public:
+ // Constructor
+ explicit CardInfoPictureEnlargedWidget(QWidget *parent = nullptr);
+
+ // Sets the card pixmap to display
+ void setCardPixmap(CardInfoPtr card, QSize size);
+
+protected:
+ // Handles the painting event for the enlarged card
+ void paintEvent(QPaintEvent *event) override;
+
+private:
+ // Cached pixmap for the enlarged card
+ QPixmap enlargedPixmap;
+
+ // Tracks if the pixmap needs to be refreshed/redrawn
+ bool pixmapDirty;
+
+ // Card information (card data pointer)
+ CardInfoPtr info;
+
+ // Loads the enlarged card pixmap
+ void loadPixmap(const QSize &size);
+};
+
+#endif // CARD_PICTURE_ENLARGED_WIDGET_H
diff --git a/cockatrice/src/client/ui/widgets/cards/card_info_picture_widget.cpp b/cockatrice/src/client/ui/widgets/cards/card_info_picture_widget.cpp
new file mode 100644
index 000000000..8b71c9000
--- /dev/null
+++ b/cockatrice/src/client/ui/widgets/cards/card_info_picture_widget.cpp
@@ -0,0 +1,238 @@
+#include "card_info_picture_widget.h"
+
+#include "../../../../game/cards/card_item.h"
+#include "../../picture_loader.h"
+
+#include
+#include
+#include
+#include
+
+/**
+ * @class CardInfoPictureWidget
+ * @brief Widget that displays an enlarged image of a card, loading the image based on the card's info or showing a
+ * default image.
+ *
+ * This widget can optionally display a larger version of the card's image when hovered over,
+ * depending on the `hoverToZoomEnabled` parameter.
+ */
+
+/**
+ * @brief Constructs a CardInfoPictureWidget.
+ * @param parent The parent widget, if any.
+ * @param hoverToZoomEnabled If this widget will spawn a larger widget when hovered over.
+ *
+ * Initializes the widget with a minimum height and sets the pixmap to a dirty state for initial loading.
+ */
+CardInfoPictureWidget::CardInfoPictureWidget(QWidget *parent, const bool hoverToZoomEnabled)
+ : QWidget(parent), info(nullptr), pixmapDirty(true), hoverToZoomEnabled(hoverToZoomEnabled)
+{
+ setMinimumHeight(baseHeight);
+ if (hoverToZoomEnabled) {
+ setMouseTracking(true);
+ }
+
+ enlargedPixmapWidget = new CardInfoPictureEnlargedWidget(this);
+ enlargedPixmapWidget->hide();
+
+ hoverTimer = new QTimer(this);
+ hoverTimer->setSingleShot(true);
+ connect(hoverTimer, &QTimer::timeout, this, &CardInfoPictureWidget::showEnlargedPixmap);
+}
+
+/**
+ * @brief Sets the card to be displayed and updates the pixmap.
+ * @param card A shared pointer to the card information (CardInfoPtr).
+ *
+ * Disconnects any existing signal connections from the previous card info and connects to the `pixmapUpdated`
+ * signal of the new card to automatically update the pixmap when the card image changes.
+ */
+void CardInfoPictureWidget::setCard(CardInfoPtr card)
+{
+ if (info) {
+ disconnect(info.data(), nullptr, this, nullptr);
+ }
+
+ info = std::move(card);
+
+ if (info) {
+ connect(info.data(), SIGNAL(pixmapUpdated()), this, SLOT(updatePixmap()));
+ }
+
+ updatePixmap();
+}
+
+/**
+ * @brief Sets the hover to zoom feature.
+ * @param enabled If true, enables the hover-to-zoom functionality; otherwise, disables it.
+ */
+void CardInfoPictureWidget::setHoverToZoomEnabled(const bool enabled)
+{
+ hoverToZoomEnabled = enabled;
+ setMouseTracking(enabled);
+}
+
+/**
+ * @brief Handles widget resizing by updating the pixmap size.
+ * @param event The resize event (unused).
+ *
+ * Calls `updatePixmap()` to ensure the image scales appropriately when the widget is resized.
+ */
+void CardInfoPictureWidget::resizeEvent(QResizeEvent *event)
+{
+ QWidget::resizeEvent(event);
+ updatePixmap();
+}
+
+/**
+ * @brief Sets the scale factor for the widget.
+ * @param scale The scale factor to apply.
+ *
+ * Adjusts the widget's size according to the scale factor and updates the pixmap.
+ */
+void CardInfoPictureWidget::setScaleFactor(const int scale)
+{
+ const int newWidth = baseWidth + scale * 20;
+ const int newHeight = static_cast(newWidth * aspectRatio);
+
+ scaleFactor = scale;
+
+ setFixedSize(newWidth, newHeight);
+ updatePixmap();
+}
+
+/**
+ * @brief Marks the pixmap as dirty and triggers a widget repaint.
+ *
+ * Sets `pixmapDirty` to true, indicating that the pixmap needs to be reloaded before the next display.
+ */
+void CardInfoPictureWidget::updatePixmap()
+{
+ pixmapDirty = true;
+ update();
+}
+
+/**
+ * @brief Loads the appropriate pixmap based on the current card info.
+ *
+ * If `info` is valid, loads the card's image. Otherwise, loads a default card back image.
+ */
+void CardInfoPictureWidget::loadPixmap()
+{
+ if (info) {
+ PictureLoader::getPixmap(resizedPixmap, info, size());
+ } else {
+ PictureLoader::getCardBackPixmap(resizedPixmap, size());
+ }
+
+ pixmapDirty = false;
+}
+
+/**
+ * @brief Custom paint event that draws the card image with rounded corners.
+ * @param event The paint event (unused).
+ *
+ * Checks if the pixmap needs to be reloaded. Then, calculates the size and position for centering the
+ * scaled pixmap within the widget, applies rounded corners, and draws the pixmap.
+ */
+void CardInfoPictureWidget::paintEvent(QPaintEvent *event)
+{
+ QWidget::paintEvent(event);
+ if (width() == 0 || height() == 0) {
+ return;
+ }
+
+ if (pixmapDirty) {
+ loadPixmap();
+ }
+
+ const QSize scaledSize = resizedPixmap.size().scaled(size(), Qt::KeepAspectRatio);
+ const QPoint topLeft{(width() - scaledSize.width()) / 2, (height() - scaledSize.height()) / 2};
+ const qreal radius = 0.05 * scaledSize.width();
+
+ QStylePainter painter(this);
+ QPainterPath shape;
+ shape.addRoundedRect(QRect(topLeft, scaledSize), radius, radius);
+ painter.setClipPath(shape);
+ painter.drawItemPixmap(QRect(topLeft, scaledSize), Qt::AlignCenter, resizedPixmap);
+}
+
+/**
+ * @brief Provides the recommended size for the widget based on the scale factor.
+ * @return The recommended widget size.
+ */
+QSize CardInfoPictureWidget::sizeHint() const
+{
+ return {static_cast(baseWidth * scaleFactor), static_cast(baseHeight * scaleFactor)};
+}
+
+/**
+ * @brief Starts the hover timer to show the enlarged pixmap on hover.
+ * @param event The enter event.
+ */
+#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
+void CardInfoPictureWidget::enterEvent(QEnterEvent *event)
+#else
+void CardInfoPictureWidget::enterEvent(QEvent *event)
+#endif
+{
+ QWidget::enterEvent(event); // Call the base class implementation
+
+ // If hover-to-zoom is enabled, start the hover timer
+ if (hoverToZoomEnabled) {
+ hoverTimer->start(hoverActivateThresholdInMs);
+ }
+
+ // Emit signal indicating a card is being hovered on
+ emit hoveredOnCard(info);
+}
+
+/**
+ * @brief Stops the hover timer and hides the enlarged pixmap when the mouse leaves.
+ * @param event The leave event.
+ */
+void CardInfoPictureWidget::leaveEvent(QEvent *event)
+{
+ QWidget::leaveEvent(event);
+ if (hoverToZoomEnabled) {
+ hoverTimer->stop();
+ enlargedPixmapWidget->hide();
+ }
+}
+
+/**
+ * @brief Moves the enlarged pixmap widget to follow the mouse cursor.
+ * @param event The mouse move event.
+ */
+void CardInfoPictureWidget::mouseMoveEvent(QMouseEvent *event)
+{
+ QWidget::mouseMoveEvent(event);
+ if (hoverToZoomEnabled && enlargedPixmapWidget->isVisible()) {
+ const QPointF cursorPos = QCursor::pos();
+ enlargedPixmapWidget->move(QPoint(static_cast(cursorPos.x()) + enlargedPixmapOffset,
+ static_cast(cursorPos.y()) + enlargedPixmapOffset));
+ }
+}
+
+/**
+ * @brief Displays the enlarged version of the card's pixmap near the cursor.
+ *
+ * If card information is available, the enlarged pixmap is loaded, positioned near the cursor,
+ * and displayed.
+ */
+void CardInfoPictureWidget::showEnlargedPixmap() const
+{
+ if (!info) {
+ return;
+ }
+
+ const QSize enlargedSize(static_cast(size().width() * scaleFactor),
+ static_cast(size().width() * aspectRatio * scaleFactor));
+ enlargedPixmapWidget->setCardPixmap(info, enlargedSize);
+
+ const QPointF cursorPos = QCursor::pos();
+ enlargedPixmapWidget->move(static_cast(cursorPos.x()) + enlargedPixmapOffset,
+ static_cast(cursorPos.y()) + enlargedPixmapOffset);
+
+ enlargedPixmapWidget->show();
+}
diff --git a/cockatrice/src/client/ui/widgets/cards/card_info_picture_widget.h b/cockatrice/src/client/ui/widgets/cards/card_info_picture_widget.h
new file mode 100644
index 000000000..e18df00d8
--- /dev/null
+++ b/cockatrice/src/client/ui/widgets/cards/card_info_picture_widget.h
@@ -0,0 +1,67 @@
+#ifndef CARD_INFO_PICTURE_H
+#define CARD_INFO_PICTURE_H
+
+#include "../../../../game/cards/card_database.h"
+#include "card_info_picture_enlarged_widget.h"
+
+#include
+#include
+
+class AbstractCardItem;
+
+class CardInfoPictureWidget : public QWidget
+{
+ Q_OBJECT
+
+public:
+ explicit CardInfoPictureWidget(QWidget *parent = nullptr, bool hoverToZoomEnabled = false);
+ CardInfoPtr getInfo()
+ {
+ return info;
+ }
+ [[nodiscard]] QSize sizeHint() const override;
+ void setHoverToZoomEnabled(bool enabled);
+
+public slots:
+ void setCard(CardInfoPtr card);
+ void setScaleFactor(int scale); // New slot for scaling
+ void updatePixmap();
+
+signals:
+ void hoveredOnCard(CardInfoPtr hoveredCard);
+
+protected:
+ void resizeEvent(QResizeEvent *event) override;
+ void paintEvent(QPaintEvent *) override;
+#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
+ void enterEvent(QEnterEvent *event) override; // Qt6 signature
+#else
+ void enterEvent(QEvent *event) override; // Qt5 signature
+#endif
+ void leaveEvent(QEvent *event) override;
+ void mouseMoveEvent(QMouseEvent *event) override;
+ void loadPixmap();
+ [[nodiscard]] const QPixmap &getResizedPixmap() const
+ {
+ return resizedPixmap;
+ }
+ void showEnlargedPixmap() const;
+
+private:
+ CardInfoPtr info;
+ qreal magicTheGatheringCardAspectRatio = 1.396;
+ qreal yuGiOhCardAspectRatio = 1.457;
+ qreal aspectRatio = magicTheGatheringCardAspectRatio;
+ int baseWidth = 200;
+ int baseHeight = 200;
+ double scaleFactor = 1.5;
+ QPixmap resizedPixmap;
+ bool pixmapDirty;
+ bool hoverToZoomEnabled;
+ int hoverActivateThresholdInMs = 500;
+ CardInfoPictureEnlargedWidget *enlargedPixmapWidget;
+ int enlargedPixmapOffset = 10;
+ QTimer *hoverTimer;
+};
+
+#endif
diff --git a/cockatrice/src/client/ui/widgets/cards/card_info_picture_with_text_overlay_widget.cpp b/cockatrice/src/client/ui/widgets/cards/card_info_picture_with_text_overlay_widget.cpp
new file mode 100644
index 000000000..f2e9442d0
--- /dev/null
+++ b/cockatrice/src/client/ui/widgets/cards/card_info_picture_with_text_overlay_widget.cpp
@@ -0,0 +1,218 @@
+#include "card_info_picture_with_text_overlay_widget.h"
+
+#include
+#include
+#include
+#include
+
+/**
+ * @brief Constructs a CardPictureWithTextOverlay widget.
+ * @param parent The parent widget.
+ * @param hoverToZoomEnabled If this widget will spawn a larger widget when hovered over.
+ * @param textColor The color of the overlay text.
+ * @param outlineColor The color of the outline around the text.
+ * @param fontSize The font size of the overlay text.
+ * @param alignment The alignment of the text within the overlay.
+ *
+ * Sets the widget's size policy and default border style.
+ */
+CardInfoPictureWithTextOverlayWidget::CardInfoPictureWithTextOverlayWidget(QWidget *parent,
+ const bool hoverToZoomEnabled,
+ const QColor &textColor,
+ const QColor &outlineColor,
+ const int fontSize,
+ const Qt::Alignment alignment)
+ : CardInfoPictureWidget(parent, hoverToZoomEnabled), textColor(textColor), outlineColor(outlineColor),
+ fontSize(fontSize), textAlignment(alignment)
+{
+ this->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
+}
+
+/**
+ * @brief Sets the overlay text to be displayed on the card.
+ * @param text The text to overlay.
+ *
+ * Updates the widget to display the new overlay text.
+ */
+void CardInfoPictureWithTextOverlayWidget::setOverlayText(const QString &text)
+{
+ overlayText = text;
+ update(); // Trigger a redraw to display the updated text
+}
+
+/**
+ * @brief Sets the color of the overlay text.
+ * @param color The new text color.
+ */
+void CardInfoPictureWithTextOverlayWidget::setTextColor(const QColor &color)
+{
+ textColor = color;
+ update();
+}
+
+/**
+ * @brief Sets the outline color around the overlay text.
+ * @param color The new outline color.
+ */
+void CardInfoPictureWithTextOverlayWidget::setOutlineColor(const QColor &color)
+{
+ outlineColor = color;
+ update();
+}
+
+/**
+ * @brief Sets the font size for the overlay text.
+ * @param size The new font size.
+ */
+void CardInfoPictureWithTextOverlayWidget::setFontSize(const int size)
+{
+ fontSize = size > 0 ? size : 1;
+ update();
+}
+
+/**
+ * @brief Sets the alignment of the overlay text within the widget.
+ * @param alignment The new text alignment.
+ */
+void CardInfoPictureWithTextOverlayWidget::setTextAlignment(const Qt::Alignment alignment)
+{
+ textAlignment = alignment;
+ update();
+}
+
+void CardInfoPictureWithTextOverlayWidget::mousePressEvent(QMouseEvent *event)
+{
+ emit imageClicked(event, this);
+}
+
+/**
+ * @brief Paints the widget, including both the card image and the text overlay.
+ * @param event The paint event.
+ *
+ * Draws the card image first, then overlays text on top. The text is wrapped and centered within the image.
+ */
+void CardInfoPictureWithTextOverlayWidget::paintEvent(QPaintEvent *event)
+{
+ // Call the base class's paintEvent to draw the card image
+ CardInfoPictureWidget::paintEvent(event);
+
+ // Now add the custom text overlay on top of the image
+ if (overlayText.isEmpty()) {
+ return;
+ }
+ QStylePainter painter(this);
+
+ // Set text properties
+ QFont font = painter.font();
+ font.setPointSize(fontSize);
+ painter.setFont(font);
+
+ // Get the pixmap from the base class using the getter
+ const QPixmap &pixmap = getResizedPixmap();
+ if (pixmap.isNull()) {
+ return;
+ }
+
+ // Calculate size and position for drawing
+ const QSize scaledSize = pixmap.size().scaled(size(), Qt::KeepAspectRatio);
+ const QPoint topLeft{(width() - scaledSize.width()) / 2, (height() - scaledSize.height()) / 2};
+ const QRect pixmapRect(topLeft, scaledSize);
+
+ // Prepare text wrapping
+ const QFontMetrics fontMetrics(font);
+ const int lineHeight = fontMetrics.height();
+ const int textWidth = pixmapRect.width();
+ QString wrappedText;
+
+ // Break the text into multiple lines to fit within the pixmap width
+ QString currentLine;
+ QStringList words = overlayText.split(' ');
+ for (const QString &word : words) {
+ if (fontMetrics.horizontalAdvance(currentLine + " " + word) > textWidth) {
+ wrappedText += currentLine + '\n';
+ currentLine = word;
+ } else {
+ if (!currentLine.isEmpty()) {
+ currentLine += " ";
+ }
+ currentLine += word;
+ }
+ }
+ wrappedText += currentLine;
+
+ // Calculate total text block height
+ const int totalTextHeight = static_cast(wrappedText.count('\n')) * lineHeight + lineHeight;
+
+ // Set up the text layout options
+ QTextOption textOption;
+ textOption.setAlignment(textAlignment);
+
+ // Create a text rectangle centered within the pixmap rect
+ auto textRect = QRect(pixmapRect.left(), pixmapRect.top(), pixmapRect.width(), totalTextHeight);
+ textRect.moveTop((pixmapRect.height() - totalTextHeight) / 2 + pixmapRect.top());
+
+ // Draw the outlined text
+ drawOutlinedText(painter, textRect, wrappedText, textOption);
+}
+
+/**
+ * @brief Draws text with an outline for visibility.
+ * @param painter The painter to draw the text.
+ * @param textRect The rectangle area to draw the text in.
+ * @param text The text to display.
+ * @param textOption The text layout options, such as alignment.
+ *
+ * Draws an outline around the text to enhance readability before drawing the main text.
+ */
+void CardInfoPictureWithTextOverlayWidget::drawOutlinedText(QPainter &painter,
+ const QRect &textRect,
+ const QString &text,
+ const QTextOption &textOption) const
+{
+ // Draw the black outline (outlineColor)
+ painter.setPen(outlineColor);
+ for (int dx = -1; dx <= 1; ++dx) {
+ for (int dy = -1; dy <= 1; ++dy) {
+ if (dx != 0 || dy != 0) {
+ QRect shiftedTextRect = textRect.translated(dx, dy);
+ painter.drawText(shiftedTextRect, text, textOption);
+ }
+ }
+ }
+
+ // Draw the main text (textColor)
+ painter.setPen(textColor);
+ painter.drawText(textRect, text, textOption);
+}
+
+/**
+ * @brief Provides the recommended size for this widget.
+ * @return The suggested widget size.
+ */
+QSize CardInfoPictureWithTextOverlayWidget::sizeHint() const
+{
+ return CardInfoPictureWidget::sizeHint();
+}
+
+/**
+ * @brief Provides the minimum recommended size for this widget.
+ * @return The minimum widget size.
+ */
+QSize CardInfoPictureWithTextOverlayWidget::minimumSizeHint() const
+{
+ // Same as sizeHint, but ensure that there is at least some space for the pixmap
+ const QPixmap &pixmap = getResizedPixmap();
+ const QSize pixmapSize = pixmap.isNull() ? QSize(0, 0) : pixmap.size();
+
+ // Get the font metrics for the overlay text
+ QFont font;
+ font.setPointSize(fontSize);
+ const QFontMetrics fontMetrics(font);
+
+ // Calculate the height required for the text
+ const QStringList lines = overlayText.split('\n');
+ const int totalTextHeight = static_cast(lines.size()) * fontMetrics.height();
+
+ // Return the maximum width and combined height
+ return {pixmapSize.width(), pixmapSize.height() + totalTextHeight};
+}
diff --git a/cockatrice/src/client/ui/widgets/cards/card_info_picture_with_text_overlay_widget.h b/cockatrice/src/client/ui/widgets/cards/card_info_picture_with_text_overlay_widget.h
new file mode 100644
index 000000000..0290ce3eb
--- /dev/null
+++ b/cockatrice/src/client/ui/widgets/cards/card_info_picture_with_text_overlay_widget.h
@@ -0,0 +1,50 @@
+#ifndef CARD_PICTURE_WITH_TEXT_OVERLAY_H
+#define CARD_PICTURE_WITH_TEXT_OVERLAY_H
+
+#include "card_info_picture_widget.h"
+
+#include
+#include
+#include
+
+class CardInfoPictureWithTextOverlayWidget final : public CardInfoPictureWidget
+{
+ Q_OBJECT
+
+public:
+ explicit CardInfoPictureWithTextOverlayWidget(QWidget *parent = nullptr,
+ bool hoverToZoomEnabled = false,
+ const QColor &textColor = Qt::white,
+ const QColor &outlineColor = Qt::black,
+ int fontSize = 12,
+ Qt::Alignment alignment = Qt::AlignCenter);
+
+ void setOverlayText(const QString &text);
+ void setTextColor(const QColor &color);
+ void setOutlineColor(const QColor &color);
+ void setFontSize(int size);
+ void setTextAlignment(Qt::Alignment alignment);
+
+ [[nodiscard]] QSize sizeHint() const override;
+signals:
+ void imageClicked(QMouseEvent *event, CardInfoPictureWithTextOverlayWidget *instance);
+
+protected:
+ void paintEvent(QPaintEvent *event) override;
+ void mousePressEvent(QMouseEvent *event) override;
+ [[nodiscard]] QSize minimumSizeHint() const override;
+
+private:
+ void drawOutlinedText(QPainter &painter,
+ const QRect &textRect,
+ const QString &text,
+ const QTextOption &textOption) const;
+
+ QString overlayText;
+ QColor textColor;
+ QColor outlineColor;
+ int fontSize;
+ Qt::Alignment textAlignment;
+};
+
+#endif // CARD_PICTURE_WITH_TEXT_OVERLAY_H
diff --git a/cockatrice/src/cardinfotext.cpp b/cockatrice/src/client/ui/widgets/cards/card_info_text_widget.cpp
similarity index 84%
rename from cockatrice/src/cardinfotext.cpp
rename to cockatrice/src/client/ui/widgets/cards/card_info_text_widget.cpp
index faf5da9f8..82944205f 100644
--- a/cockatrice/src/cardinfotext.cpp
+++ b/cockatrice/src/client/ui/widgets/cards/card_info_text_widget.cpp
@@ -1,14 +1,13 @@
-#include "cardinfotext.h"
+#include "card_info_text_widget.h"
-#include "carditem.h"
-#include "game_specific_terms.h"
-#include "main.h"
+#include "../../../../game/cards/card_item.h"
+#include "../../../../game/game_specific_terms.h"
#include