mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2025-12-20 22:32:47 -08:00
Compare commits
295 Commits
2024-06-16
...
2025-01-11
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
503985a080 | ||
|
|
9f466162b0 | ||
|
|
8bea3f8997 | ||
|
|
1a3df84f0a | ||
|
|
2b3c47148e | ||
|
|
59ca4397e2 | ||
|
|
98266b0739 | ||
|
|
5a82ff106d | ||
|
|
2194430019 | ||
|
|
1f11015a2f | ||
|
|
c3421669d5 | ||
|
|
6e8adddc6d | ||
|
|
22a6ded4f0 | ||
|
|
0d7669db2c | ||
|
|
9526bca168 | ||
|
|
0683431f35 | ||
|
|
70790264b8 | ||
|
|
c8a68c83e3 | ||
|
|
23171f79d0 | ||
|
|
b7f05a12a3 | ||
|
|
6078dd092a | ||
|
|
81b85e97df | ||
|
|
cc16b8779c | ||
|
|
62f7c7f9ce | ||
|
|
7496e79e8c | ||
|
|
b8cf3e2cab | ||
|
|
93fab3d78f | ||
|
|
9c38c9ed1b | ||
|
|
38e99f2e87 | ||
|
|
68226786a2 | ||
|
|
455cd9717a | ||
|
|
fa79c5c36a | ||
|
|
0402d4b853 | ||
|
|
8a427955e7 | ||
|
|
bb4214e28a | ||
|
|
f924b04efd | ||
|
|
62f60867a9 | ||
|
|
b5844f1244 | ||
|
|
8c0093d453 | ||
|
|
34df4cd060 | ||
|
|
99eea3a662 | ||
|
|
6e1047032d | ||
|
|
b2a8748bc6 | ||
|
|
ded6d5b8eb | ||
|
|
832842c20c | ||
|
|
b43e4ae469 | ||
|
|
026afeb885 | ||
|
|
b6793a5e01 | ||
|
|
d231264a16 | ||
|
|
6e02bdec2e | ||
|
|
cfaadc40b1 | ||
|
|
93475b43a5 | ||
|
|
3348e051a1 | ||
|
|
dad1aea128 | ||
|
|
dec001114a | ||
|
|
1ce7b9f7de | ||
|
|
2ff99f12d8 | ||
|
|
6679705254 | ||
|
|
7eafac5b1a | ||
|
|
ac3aa949ad | ||
|
|
b4036c8671 | ||
|
|
4e0de1c066 | ||
|
|
f32890916d | ||
|
|
24a0dac420 | ||
|
|
716bc00533 | ||
|
|
32dd18998d | ||
|
|
5e62069444 | ||
|
|
bf63dc4ab7 | ||
|
|
7679546e30 | ||
|
|
45b11dc984 | ||
|
|
25d21a3da6 | ||
|
|
c8d49b5bf9 | ||
|
|
f737d9a794 | ||
|
|
df9c5ae53c | ||
|
|
e0829a75d2 | ||
|
|
1f58f7e93d | ||
|
|
14807ba036 | ||
|
|
75fb3894a6 | ||
|
|
18119bd11b | ||
|
|
4c7796537f | ||
|
|
3452cb01d0 | ||
|
|
6a151ef97a | ||
|
|
e3d651668c | ||
|
|
7a5704beaa | ||
|
|
37b78a9a4c | ||
|
|
8bc5a9d581 | ||
|
|
57ed162b79 | ||
|
|
3524231500 | ||
|
|
5cfe2b4762 | ||
|
|
a8bac1e468 | ||
|
|
4f798286af | ||
|
|
8a04b2d69d | ||
|
|
17893d9747 | ||
|
|
8af49406cd | ||
|
|
3b068b79fe | ||
|
|
ce14e83e78 | ||
|
|
f213d6fda7 | ||
|
|
83db00d7a3 | ||
|
|
7e9bd88eb4 | ||
|
|
ea716ca440 | ||
|
|
3cd7a04002 | ||
|
|
914002f846 | ||
|
|
17b82a186f | ||
|
|
7a8e957476 | ||
|
|
6dfd354973 | ||
|
|
956c12eb32 | ||
|
|
d5ae4eed26 | ||
|
|
ca486e5ed9 | ||
|
|
de63066b0b | ||
|
|
c7ca55ceb5 | ||
|
|
024bef7ded | ||
|
|
34d3d60f95 | ||
|
|
ed907d7c6f | ||
|
|
9d7fd66546 | ||
|
|
9934841950 | ||
|
|
432fe1100b | ||
|
|
d987628935 | ||
|
|
4c3ceae0e4 | ||
|
|
2b9d7538bf | ||
|
|
4ca1fc083d | ||
|
|
e7585271fb | ||
|
|
6e6824117d | ||
|
|
3e5f2fd8b2 | ||
|
|
6e470d788e | ||
|
|
a40d8092ce | ||
|
|
0234a70bfd | ||
|
|
705b1e0c2b | ||
|
|
69379334f9 | ||
|
|
12e50a1f2f | ||
|
|
ec17a477be | ||
|
|
205e1c7a59 | ||
|
|
ffb60c06cb | ||
|
|
2280f59ee6 | ||
|
|
0d4dd63edc | ||
|
|
69f1f4c1a5 | ||
|
|
d930d9c237 | ||
|
|
9c782d130f | ||
|
|
f12053f39d | ||
|
|
bcf6ca4f87 | ||
|
|
46619bb425 | ||
|
|
cdd870a129 | ||
|
|
7a1b7b9438 | ||
|
|
2183ada1f2 | ||
|
|
1d9e64ec73 | ||
|
|
5339be318e | ||
|
|
e1ba39c437 | ||
|
|
07ee271478 | ||
|
|
4823cce622 | ||
|
|
23099f7e8b | ||
|
|
5bdbd51fa8 | ||
|
|
a0e5871c6e | ||
|
|
3cf0904651 | ||
|
|
2bd06ff0fd | ||
|
|
6ea333d0f1 | ||
|
|
91d2485940 | ||
|
|
0d99b2bcf4 | ||
|
|
a54a424f84 | ||
|
|
3514699f5b | ||
|
|
d196988cab | ||
|
|
03aff83135 | ||
|
|
17e6bfaca6 | ||
|
|
90281262be | ||
|
|
5bbc118920 | ||
|
|
dde2f8b9ad | ||
|
|
d41aa30e10 | ||
|
|
231d0380a7 | ||
|
|
a5c509981b | ||
|
|
71b01e6110 | ||
|
|
c716f85962 | ||
|
|
245d51caea | ||
|
|
e588917f6c | ||
|
|
27e5d21b6b | ||
|
|
b894b75e6a | ||
|
|
116397cdb3 | ||
|
|
a6b5abf271 | ||
|
|
fd5a649246 | ||
|
|
e8e57989ba | ||
|
|
03db4ccce6 | ||
|
|
c9d5d5609c | ||
|
|
ac16206ddb | ||
|
|
5f8bcbd02d | ||
|
|
a0f74134bb | ||
|
|
0463a6fd70 | ||
|
|
a5de633c64 | ||
|
|
b2ad2acff3 | ||
|
|
4ee6ff73e0 | ||
|
|
628bdde939 | ||
|
|
e9b78c1c59 | ||
|
|
315cbc0925 | ||
|
|
69741d858c | ||
|
|
20d99a78b6 | ||
|
|
2d68393e07 | ||
|
|
8cb1470643 | ||
|
|
8d9b27bf47 | ||
|
|
0c5d9f1a7d | ||
|
|
a7d88c06c1 | ||
|
|
2735000fcf | ||
|
|
a39de270cd | ||
|
|
10f11213d3 | ||
|
|
3b49cbf73b | ||
|
|
e4cfe08113 | ||
|
|
fa02cb885c | ||
|
|
69b864fa02 | ||
|
|
b9ed9a6c0b | ||
|
|
5156495b47 | ||
|
|
b92047bc3f | ||
|
|
70559d32df | ||
|
|
bb84b75db9 | ||
|
|
f634177973 | ||
|
|
e33ff37c82 | ||
|
|
d2bc7f6ac0 | ||
|
|
5ef1ca06f5 | ||
|
|
1d8651bc00 | ||
|
|
17eabf2004 | ||
|
|
37bb1367db | ||
|
|
24b5dab456 | ||
|
|
c6bfc8b8ea | ||
|
|
f2b0fa164e | ||
|
|
0ca8bdb3a8 | ||
|
|
a8471f62bc | ||
|
|
5f1c03682f | ||
|
|
3255ed3ffb | ||
|
|
c51b54c0c5 | ||
|
|
a3f0807d47 | ||
|
|
27055944df | ||
|
|
7b1653034b | ||
|
|
39d8ca050f | ||
|
|
50274cb66d | ||
|
|
bd60a9fd2e | ||
|
|
83409c32c4 | ||
|
|
1bc92623dc | ||
|
|
f73196841a | ||
|
|
86a4b130ff | ||
|
|
f4e2f117c3 | ||
|
|
8ef92d26c5 | ||
|
|
c8336df49d | ||
|
|
c2fe3cda35 | ||
|
|
c54f47efbf | ||
|
|
3c40cc4b7d | ||
|
|
f0fb77bade | ||
|
|
e894e78346 | ||
|
|
dd04c610ec | ||
|
|
2e674efe50 | ||
|
|
4d394c31f9 | ||
|
|
11d58abbc3 | ||
|
|
5f4ad87a47 | ||
|
|
e43a21866c | ||
|
|
6652012f4c | ||
|
|
0c4e8ca290 | ||
|
|
230a2c5c62 | ||
|
|
590fb7f533 | ||
|
|
e8b88248f2 | ||
|
|
c6ba1b6a4e | ||
|
|
c4c52bd8c0 | ||
|
|
c633a792f5 | ||
|
|
8d5421d9da | ||
|
|
b041f4ace2 | ||
|
|
d26f96db9e | ||
|
|
fa999880ee | ||
|
|
d1e0f9dfc5 | ||
|
|
2d86938375 | ||
|
|
4865269a73 | ||
|
|
038ce3dcec | ||
|
|
43b997fe40 | ||
|
|
44e92f61ca | ||
|
|
b4bfa17cee | ||
|
|
500b694cc6 | ||
|
|
b998282304 | ||
|
|
b704216553 | ||
|
|
03ec02a749 | ||
|
|
248ea82573 | ||
|
|
bbe125beee | ||
|
|
95cd1c6f87 | ||
|
|
1c2107ae8f | ||
|
|
e826e17c6c | ||
|
|
b111f0921c | ||
|
|
090a48515c | ||
|
|
b8555d8c42 | ||
|
|
cf1f4f12a9 | ||
|
|
ef4413633a | ||
|
|
c5bb38e907 | ||
|
|
9f515fc804 | ||
|
|
245edcefdd | ||
|
|
153f73c308 | ||
|
|
315837b267 | ||
|
|
ea8da24215 | ||
|
|
1ab723ca64 | ||
|
|
f8bc6cf998 | ||
|
|
8687163cca | ||
|
|
e261e16d99 | ||
|
|
bdcd083eea | ||
|
|
c4bf9eb61c | ||
|
|
0994d10410 | ||
|
|
291c535edb | ||
|
|
f04702fdd1 |
@@ -1,4 +1,4 @@
|
||||
FROM fedora:39
|
||||
FROM fedora:41
|
||||
|
||||
RUN dnf install -y \
|
||||
ccache \
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM ubuntu:focal
|
||||
FROM ubuntu:20.04
|
||||
|
||||
RUN apt-get update && \
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM ubuntu:jammy
|
||||
FROM ubuntu:22.04
|
||||
|
||||
RUN apt-get update && \
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM ubuntu:noble
|
||||
FROM ubuntu:24.04
|
||||
|
||||
RUN apt-get update && \
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
|
||||
@@ -1,25 +0,0 @@
|
||||
FROM ubuntu:bionic
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
build-essential \
|
||||
ccache \
|
||||
clang-format \
|
||||
cmake \
|
||||
file \
|
||||
g++ \
|
||||
git \
|
||||
liblzma-dev \
|
||||
libmariadb-dev-compat \
|
||||
libprotobuf-dev \
|
||||
libqt5multimedia5-plugins \
|
||||
libqt5sql5-mysql \
|
||||
libqt5svg5-dev \
|
||||
libqt5websockets5-dev \
|
||||
protobuf-compiler \
|
||||
qt5-default \
|
||||
qt5-image-formats-plugins \
|
||||
qtmultimedia5-dev \
|
||||
qttools5-dev \
|
||||
qttools5-dev-tools \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
@@ -189,6 +189,12 @@ fi
|
||||
|
||||
if [[ $MAKE_PACKAGE ]]; then
|
||||
echo "::group::Create package"
|
||||
|
||||
if [[ $RUNNER_OS == macOS ]]; then
|
||||
# Workaround https://github.com/actions/runner-images/issues/7522
|
||||
echo "killing XProtectBehaviorService"; sudo pkill -9 XProtect >/dev/null || true;
|
||||
echo "waiting for XProtectBehaviorService kill"; while pgrep "XProtect"; do sleep 3; done;
|
||||
fi
|
||||
cmake --build . --target package --config "$BUILDTYPE"
|
||||
echo "::endgroup::"
|
||||
|
||||
|
||||
17
.ci/macos.entitlements
Normal file
17
.ci/macos.entitlements
Normal file
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.app-sandbox</key>
|
||||
<false/>
|
||||
|
||||
<key>com.apple.security.cs.disable-library-validation</key>
|
||||
<true/>
|
||||
|
||||
<key>com.apple.security.cs.disable-executable-page-protection</key>
|
||||
<true/>
|
||||
|
||||
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -4,24 +4,24 @@
|
||||
git push -d origin --REPLACE-WITH-BETA-LIST--
|
||||
-->
|
||||
|
||||
<!-- This list of binaries should be updated every time the ci is changed to
|
||||
<!-- This list of binaries should be updated every time the CI is changed to
|
||||
include different targets -->
|
||||
<pre>
|
||||
<b>Pre-compiled binaries we serve:</b>
|
||||
- <kbd>Windows 7+</kbd>
|
||||
- <kbd>Windows 10+</kbd>
|
||||
- <kbd>macOS 10.15+</kbd> ("Catalina")
|
||||
- <kbd>macOS 13+</kbd> ("Ventura")
|
||||
- <kbd>Ubuntu 18.04 LTS</kbd> ("Bionic Beaver")
|
||||
- <kbd>Ubuntu 20.04 LTS</kbd> ("Focal Fossa")
|
||||
- <kbd>Ubuntu 22.04 LTS</kbd> ("Jammy Jellyfish")
|
||||
- <kbd>Windows 7+</kbd>
|
||||
- <kbd>macOS 14+</kbd> ("Sonoma") / Apple M
|
||||
- <kbd>macOS 13+</kbd> ("Ventura") / Intel
|
||||
- <kbd>Ubuntu 24.04 LTS</kbd> ("Noble Numbat")
|
||||
- <kbd>Debian 11</kbd> ("Bullseye")
|
||||
- <kbd>Ubuntu 22.04 LTS</kbd> ("Jammy Jellyfish")
|
||||
- <kbd>Ubuntu 20.04 LTS</kbd> ("Focal Fossa")
|
||||
- <kbd>Debian 12</kbd> ("Bookworm")
|
||||
- <kbd>Fedora 39</kbd>
|
||||
- <kbd>Debian 11</kbd> ("Bullseye")
|
||||
- <kbd>Fedora 41</kbd>
|
||||
- <kbd>Fedora 40</kbd>
|
||||
<kbd>We are also packaged in Arch Linux's official community repository, courtesy of @FFY00</kbd></i>
|
||||
<kbd>General Linux support is available via a flatpak package (Flathub)</kbd></i>
|
||||
|
||||
<i>We are also packaged in <kbd>Arch Linux</kbd>'s official "extra" repository, courtesy of @FFY00</i>
|
||||
<i>General Linux support is available via a <kbd>flatpak</kbd> package (Flathub)</i>
|
||||
</pre>
|
||||
|
||||
|
||||
@@ -29,22 +29,24 @@ include different targets -->
|
||||
|
||||
We're pleased to announce the newest official release: <kbd>--REPLACE-WITH-RELEASE-TITLE--</kbd>
|
||||
|
||||
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
|
||||
<!-- this optional section puts a warning banner for problems with updating
|
||||
> ⚠️ **With this release, we no longer provide a ready-to-install binary for:**
|
||||
> [!IMPORTANT]
|
||||
> **With this release, we no longer provide a ready-to-install binary for:**
|
||||
> --DEPRECATED-OS-HERE--
|
||||
-->
|
||||
|
||||
- Run the internal software updater: <kbd>Help → Check for Client Updates</kbd>
|
||||
Run the internal software updater: <kbd>Help → Check for Client Updates</kbd>
|
||||
|
||||
Don't forget to update your card database right after! (<kbd>Help → Check for Card Updates...</kbd>)
|
||||
|
||||
@@ -61,14 +63,14 @@ Remove empty headers when done.
|
||||
-->
|
||||
|
||||
<!-- Highlights of the release -->
|
||||
### ⚠️ Important:
|
||||
### 🔖 Highlights:
|
||||
### ✨ New Features:
|
||||
### 🐛 Fixed Bugs / Resolved issues:
|
||||
### 🐛 Fixed Bugs / Resolved Issues:
|
||||
|
||||
<!-- Complete list of changes (foldable) -->
|
||||
<details>
|
||||
<summary>
|
||||
📘 <b>Show all changes</b> (--REPLACE-WITH-COMMIT-COUNT-- commits)
|
||||
<b>Show all changes</b> (--REPLACE-WITH-COMMIT-COUNT-- commits)
|
||||
</summary>
|
||||
|
||||
### User Interface
|
||||
@@ -89,5 +91,6 @@ Remove empty headers when done.
|
||||
|
||||
## Special Thanks
|
||||
<!-- Personalise this a bit! -->
|
||||
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! 🙏
|
||||
<!-- We'd like to especially recognize @ZeldaZach, --ADD-CONTRIBUTORS-HERE-- for their help in preparing so many amazing new features for the user base. -->
|
||||
|
||||
191
.github/workflows/desktop-build.yml
vendored
191
.github/workflows/desktop-build.yml
vendored
@@ -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,38 @@ 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
|
||||
os: macos-14
|
||||
xcode: "15.4"
|
||||
- target: 15
|
||||
soc: Apple
|
||||
os: macos-15
|
||||
xcode: "16.2"
|
||||
type: Release
|
||||
core_count: 3
|
||||
make_package: 1
|
||||
|
||||
- target: 15
|
||||
soc: Apple
|
||||
os: macos-15
|
||||
xcode: "16.2"
|
||||
type: Debug
|
||||
core_count: 3
|
||||
|
||||
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,31 +246,91 @@ 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: |
|
||||
brew update
|
||||
brew install protobuf qt --force-bottle
|
||||
|
||||
- name: Build on Xcode ${{matrix.xcode}}
|
||||
- name: Build & Sign on Xcode ${{matrix.xcode}}
|
||||
shell: bash
|
||||
id: build
|
||||
env:
|
||||
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_CERTIFICATE: ${{ secrets.PROD_MACOS_CERTIFICATE }}
|
||||
MACOS_CERTIFICATE_PWD: ${{ secrets.PROD_MACOS_CERTIFICATE_PWD }}
|
||||
MACOS_CERTIFICATE_NAME: ${{ secrets.PROD_MACOS_CERTIFICATE_NAME }}
|
||||
MACOS_CI_KEYCHAIN_PWD: ${{ secrets.PROD_MACOS_CI_KEYCHAIN_PWD }}
|
||||
# macOS runner have 3 cores usually - only the macos-13 image has 4:
|
||||
# https://docs.github.com/en/actions/using-github-hosted-runners/using-github-hosted-runners/about-github-hosted-runners#standard-github-hosted-runners-for-public-repositories
|
||||
# https://github.com/actions/runner-images?tab=readme-ov-file#available-images
|
||||
run: |
|
||||
if [[ -n "$MACOS_CERTIFICATE_NAME" ]]
|
||||
then
|
||||
echo $MACOS_CERTIFICATE | base64 --decode > certificate.p12
|
||||
security create-keychain -p "$MACOS_CI_KEYCHAIN_PWD" build.keychain
|
||||
security default-keychain -s build.keychain
|
||||
security set-keychain-settings -t 3600 -l build.keychain
|
||||
security unlock-keychain -p "$MACOS_CI_KEYCHAIN_PWD" build.keychain
|
||||
security import certificate.p12 -k build.keychain -P "$MACOS_CERTIFICATE_PWD" -T /usr/bin/codesign
|
||||
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$MACOS_CI_KEYCHAIN_PWD" build.keychain
|
||||
fi
|
||||
.ci/compile.sh --server --parallel ${{matrix.core_count}}
|
||||
|
||||
- name: Sign app bundle
|
||||
if: matrix.make_package
|
||||
env:
|
||||
MACOS_CERTIFICATE_NAME: ${{ secrets.PROD_MACOS_CERTIFICATE_NAME }}
|
||||
MACOS_CI_KEYCHAIN_PWD: ${{ secrets.PROD_MACOS_CI_KEYCHAIN_PWD }}
|
||||
run: |
|
||||
if [[ -n "$MACOS_CERTIFICATE_NAME" ]]
|
||||
then
|
||||
security unlock-keychain -p "$MACOS_CI_KEYCHAIN_PWD" build.keychain
|
||||
/usr/bin/codesign --sign="$MACOS_CERTIFICATE_NAME" --entitlements=".ci/macos.entitlements" --options=runtime --force --deep --timestamp --verbose ${{steps.build.outputs.path}}
|
||||
fi
|
||||
|
||||
- name: Notarize app bundle
|
||||
if: matrix.make_package
|
||||
env:
|
||||
MACOS_NOTARIZATION_APPLE_ID: ${{ secrets.PROD_MACOS_NOTARIZATION_APPLE_ID }}
|
||||
MACOS_NOTARIZATION_TEAM_ID: ${{ secrets.PROD_MACOS_NOTARIZATION_TEAM_ID }}
|
||||
MACOS_NOTARIZATION_PWD: ${{ secrets.PROD_MACOS_NOTARIZATION_PWD }}
|
||||
run: |
|
||||
if [[ -n "$MACOS_NOTARIZATION_APPLE_ID" ]]
|
||||
then
|
||||
# Store the notarization credentials so that we can prevent a UI password dialog from blocking the CI
|
||||
echo "Create keychain profile"
|
||||
xcrun notarytool store-credentials "notarytool-profile" --apple-id "$MACOS_NOTARIZATION_APPLE_ID" --team-id "$MACOS_NOTARIZATION_TEAM_ID" --password "$MACOS_NOTARIZATION_PWD"
|
||||
|
||||
# We can't notarize an app bundle directly, but we need to compress it as an archive.
|
||||
# Therefore, we create a zip file containing our app bundle, so that we can send it to the
|
||||
# notarization service
|
||||
echo "Creating temp notarization archive"
|
||||
ditto -c -k --keepParent ${{steps.build.outputs.path}} "notarization.zip"
|
||||
|
||||
# Here we send the notarization request to the Apple's Notarization service, waiting for the result.
|
||||
# This typically takes a few seconds inside a CI environment, but it might take more depending on the App
|
||||
# characteristics. Visit the Notarization docs for more information and strategies on how to optimize it if
|
||||
# you're curious
|
||||
echo "Notarize app"
|
||||
xcrun notarytool submit "notarization.zip" --keychain-profile "notarytool-profile" --wait
|
||||
|
||||
# Finally, we need to "attach the staple" to our executable, which will allow our app to be
|
||||
# validated by macOS even when an internet connection is not available.
|
||||
echo "Attach staple"
|
||||
xcrun stapler staple ${{steps.build.outputs.path}}
|
||||
fi
|
||||
|
||||
- name: Upload artifact
|
||||
if: matrix.make_package
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: macOS-${{matrix.target}}-dmg
|
||||
name: macOS${{matrix.target}}${{ matrix.soc == 'Intel' && '_Intel' || '' }}${{ matrix.type == 'Debug' && '_Debug' || '' }}-dmg
|
||||
path: ${{steps.build.outputs.path}}
|
||||
if-no-files-found: error
|
||||
|
||||
@@ -275,13 +352,11 @@ jobs:
|
||||
- target: 7
|
||||
qt_version: 5.15.*
|
||||
qt_arch: msvc2019_64
|
||||
qt_tools: "tools_opensslv3_x64"
|
||||
|
||||
- target: 10
|
||||
qt_version: 6.5.*
|
||||
qt_version: 6.6.*
|
||||
qt_arch: msvc2019_64
|
||||
qt_tools: "tools_opensslv3_x64"
|
||||
qt_modules: "qtmultimedia qtwebsockets"
|
||||
qt_modules: "qtimageformats qtmultimedia qtwebsockets"
|
||||
|
||||
name: Windows ${{matrix.target}}
|
||||
needs: configure
|
||||
@@ -302,7 +377,7 @@ jobs:
|
||||
submodules: recursive
|
||||
|
||||
- name: Install Qt ${{matrix.qt_version}}
|
||||
uses: jurplel/install-qt-action@v3
|
||||
uses: jurplel/install-qt-action@v4
|
||||
with:
|
||||
cache: true
|
||||
setup-python: false
|
||||
|
||||
4
.github/workflows/translations-pull.yml
vendored
4
.github/workflows/translations-pull.yml
vendored
@@ -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
|
||||
|
||||
4
.github/workflows/translations-push.yml
vendored
4
.github/workflows/translations-push.yml
vendored
@@ -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
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -6,10 +6,11 @@ mysql.cnf
|
||||
.DS_Store
|
||||
.idea/
|
||||
*.aps
|
||||
cmake-build-debug*
|
||||
cmake-build*
|
||||
preferences
|
||||
compile_commands.json
|
||||
.vs/
|
||||
.vscode/
|
||||
.cache
|
||||
.gdb_history
|
||||
cockatrice/resources/config/qtlogging.ini
|
||||
|
||||
@@ -74,16 +74,16 @@ endif()
|
||||
|
||||
# A project name is needed for CPack
|
||||
# Version can be overriden by git tags, see cmake/getversion.cmake
|
||||
project("Cockatrice" VERSION 2.9.1)
|
||||
project("Cockatrice" VERSION 2.10.0)
|
||||
|
||||
# Set release name if not provided via env/cmake var
|
||||
if(NOT DEFINED GIT_TAG_RELEASENAME)
|
||||
set(GIT_TAG_RELEASENAME "Rings of the Wild")
|
||||
endif()
|
||||
|
||||
# Use c++17 for all targets
|
||||
# Use c++20 for all targets
|
||||
set(CMAKE_CXX_STANDARD
|
||||
17
|
||||
20
|
||||
CACHE STRING "C++ ISO Standard"
|
||||
)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED True)
|
||||
@@ -140,10 +140,14 @@ endif()
|
||||
|
||||
# Define proper compilation flags
|
||||
if(MSVC)
|
||||
# Visual Studio: Maximum optimization, disable warning C4251, establish C++17 compatibility
|
||||
set(CMAKE_CXX_FLAGS_RELEASE "/Ox /MD /wd4251 /Zc:__cplusplus /std:c++17 /permissive- /W4")
|
||||
# Generate complete debugging information
|
||||
#set(CMAKE_CXX_FLAGS_DEBUG "/Zi")
|
||||
# Visual Studio: Disable Warning C4251, C++20 compatibility, Multi-threaded Builds, Warn Detection, Unwind Semantics
|
||||
set(CMAKE_CXX_FLAGS "/wd4251 /Zc:__cplusplus /std:c++20 /permissive- /W4 /MP /EHsc")
|
||||
# Visual Studio: Maximum Optimization, Multi-threaded DLL
|
||||
set(CMAKE_CXX_FLAGS_RELEASE "/Ox /MD")
|
||||
# Visual Studio: No Optimization, Multi-threaded Debug DLL, Debug Symbols
|
||||
set(CMAKE_CXX_FLAGS_DEBUG "/Od /MDd /Zi")
|
||||
|
||||
add_compile_definitions(_SILENCE_STDEXT_ARR_ITERS_DEPRECATION_WARNING)
|
||||
elseif(CMAKE_COMPILER_IS_GNUCXX)
|
||||
# linux/gcc, bsd/gcc, windows/mingw
|
||||
include(CheckCXXCompilerFlag)
|
||||
@@ -156,7 +160,7 @@ elseif(CMAKE_COMPILER_IS_GNUCXX)
|
||||
endif()
|
||||
|
||||
if(APPLE)
|
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=gnu++17")
|
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=gnu++20")
|
||||
endif()
|
||||
|
||||
set(ADDITIONAL_DEBUG_FLAGS
|
||||
@@ -264,13 +268,14 @@ if(UNIX)
|
||||
set(CPACK_PACKAGE_ICON "${CMAKE_CURRENT_SOURCE_DIR}/cockatrice/resources/appicon.icns")
|
||||
set(CPACK_DMG_DS_STORE_SETUP_SCRIPT "${CMAKE_CURRENT_SOURCE_DIR}/cmake/CMakeDMGSetup.script")
|
||||
set(CPACK_DMG_BACKGROUND_IMAGE "${CMAKE_CURRENT_SOURCE_DIR}/cmake/dmgBackground.tif")
|
||||
set(CPACK_PRE_BUILD_SCRIPTS "${CMAKE_CURRENT_SOURCE_DIR}/cmake/SignMacApplications.cmake")
|
||||
else()
|
||||
# linux
|
||||
if(CPACK_GENERATOR STREQUAL "RPM")
|
||||
set(CPACK_RPM_PACKAGE_LICENSE "GPLv2")
|
||||
set(CPACK_RPM_MAIN_COMPONENT "cockatrice")
|
||||
if(Qt6_FOUND)
|
||||
set(CPACK_RPM_PACKAGE_REQUIRES "protobuf, qt6-qttools, qt6-qtsvg, qt6-qtmultimedia")
|
||||
set(CPACK_RPM_PACKAGE_REQUIRES "protobuf, qt6-qttools, qt6-qtsvg, qt6-qtmultimedia, qt6-qtimageformats")
|
||||
elseif(Qt5_FOUND)
|
||||
set(CPACK_RPM_PACKAGE_REQUIRES "protobuf, qt5-qttools, qt5-qtsvg, qt5-qtmultimedia")
|
||||
endif()
|
||||
@@ -292,7 +297,7 @@ if(UNIX)
|
||||
set(CPACK_DEBIAN_PACKAGE_SECTION "games")
|
||||
set(CPACK_DEBIAN_PACKAGE_HOMEPAGE "http://github.com/Cockatrice/Cockatrice")
|
||||
if(Qt6_FOUND)
|
||||
set(CPACK_DEBIAN_PACKAGE_DEPENDS "libqt6multimedia6, libqt6svg6, qt6-qpa-plugins")
|
||||
set(CPACK_DEBIAN_PACKAGE_DEPENDS "libqt6multimedia6, libqt6svg6, qt6-qpa-plugins, qt6-image-formats-plugins")
|
||||
elseif(Qt5_FOUND)
|
||||
set(CPACK_DEBIAN_PACKAGE_DEPENDS "libqt5multimedia5-plugins, libqt5svg5")
|
||||
endif()
|
||||
|
||||
32
Dockerfile
32
Dockerfile
@@ -1,19 +1,20 @@
|
||||
FROM ubuntu:bionic
|
||||
MAINTAINER Zach Halpern <zahalpern+github@gmail.com>
|
||||
FROM ubuntu:24.04
|
||||
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
RUN apt-get update && apt-get install -y\
|
||||
build-essential\
|
||||
cmake\
|
||||
git\
|
||||
libprotobuf-dev\
|
||||
libqt5sql5-mysql\
|
||||
libmysqlclient-dev\
|
||||
libqt5websockets5-dev\
|
||||
protobuf-compiler\
|
||||
qt5-default\
|
||||
qtbase5-dev\
|
||||
qttools5-dev-tools\
|
||||
qttools5-dev
|
||||
build-essential \
|
||||
cmake \
|
||||
file \
|
||||
g++ \
|
||||
git \
|
||||
libmariadb-dev-compat \
|
||||
libprotobuf-dev \
|
||||
libqt6sql6-mysql \
|
||||
qt6-websockets-dev \
|
||||
protobuf-compiler \
|
||||
qt6-tools-dev \
|
||||
qt6-tools-dev-tools
|
||||
|
||||
COPY . /home/servatrice/code/
|
||||
WORKDIR /home/servatrice/code
|
||||
@@ -25,7 +26,6 @@ RUN cmake .. -DWITH_SERVER=1 -DWITH_CLIENT=0 -DWITH_ORACLE=0 -DWITH_DBCONVERTER=
|
||||
|
||||
WORKDIR /home/servatrice
|
||||
|
||||
EXPOSE 4747
|
||||
EXPOSE 4747 4748
|
||||
|
||||
ENTRYPOINT [ "servatrice", "--log-to-console" ]
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<p align='center'>
|
||||
<a href="#cockatrice"><b>Cockatrice</b></a> <b>|</b>
|
||||
<a href="#download-">Download</a> <b>|</b>
|
||||
<a href="#get-involved--">Get Involved</a> <b>|</b>
|
||||
<a href="#get-involved-">Get Involved</a> <b>|</b>
|
||||
<a href="#community-resources">Community</a> <b>|</b>
|
||||
<a href="#translations-">Translations</a> <b>|</b>
|
||||
<a href="#build--">Build</a> <b>|</b>
|
||||
@@ -18,7 +18,7 @@
|
||||
<br><pre>
|
||||
<b>To get started, ⇢ [view our webpage](https://cockatrice.github.io/)</b><br>
|
||||
<b>To get support or suggest changes ⇢ [file an issue](https://github.com/Cockatrice/Cockatrice/issues) ([How?](https://github.com/Cockatrice/Cockatrice/wiki/How-to-Create-a-GitHub-Ticket-Regarding-Cockatrice))</b>
|
||||
<b>To help with development, see how to [get involved](#get-involved--)</b>
|
||||
<b>To help with development, see how to [get involved](#get-involved-)</b>
|
||||
</pre><br>
|
||||
|
||||
|
||||
@@ -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.<br>
|
||||
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.<br>
|
||||
For support regarding specific servers, please contact that server's admin or forum for support rather than asking here.<br>
|
||||
|
||||
To contribute code to the project, please review [the guidelines](https://github.com/Cockatrice/Cockatrice/blob/master/.github/CONTRIBUTING.md).
|
||||
|
||||
@@ -5,7 +5,7 @@ OutFile "@CPACK_TOPLEVEL_DIRECTORY@/@CPACK_OUTPUT_FILE_NAME@"
|
||||
|
||||
!define INST_DIR "@CPACK_TEMPORARY_DIRECTORY@"
|
||||
|
||||
RequestExecutionlevel highest
|
||||
RequestExecutionlevel admin
|
||||
SetCompressor LZMA
|
||||
|
||||
Var NormalDestDir
|
||||
@@ -235,6 +235,13 @@ ${If} $PortableMode = 0
|
||||
WriteUninstaller "$INSTDIR\uninstall.exe"
|
||||
${GetSize} "$INSTDIR" "/S=0K" $0 $1 $2
|
||||
IntFmt $0 "0x%08X" $0
|
||||
|
||||
; Enable Windows User-Mode Dumps
|
||||
; https://learn.microsoft.com/en-us/windows/win32/wer/collecting-user-mode-dumps
|
||||
WriteRegStr HKLM "Software\Microsoft\Windows\Windows Error Reporting\LocalDumps\cockatrice.exe" "DumpFolder" "%LOCALAPPDATA%\CrashDumps\Cockatrice"
|
||||
WriteRegDWORD HKLM "Software\Microsoft\Windows\Windows Error Reporting\LocalDumps\cockatrice.exe" "DumpCount" "5"
|
||||
WriteRegDWORD HKLM "Software\Microsoft\Windows\Windows Error Reporting\LocalDumps\cockatrice.exe" "DumpType" "2"
|
||||
|
||||
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Cockatrice" "DisplayIcon" "$INSTDIR\cockatrice.exe"
|
||||
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Cockatrice" "DisplayName" "Cockatrice"
|
||||
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Cockatrice" "DisplayVersion" "@CPACK_PACKAGE_VERSION_MAJOR@.@CPACK_PACKAGE_VERSION_MINOR@.@CPACK_PACKAGE_VERSION_PATCH@"
|
||||
|
||||
27
cmake/SignMacApplications.cmake
Normal file
27
cmake/SignMacApplications.cmake
Normal file
@@ -0,0 +1,27 @@
|
||||
# This script re-signs all apps after CPack packages them. This is necessary because CPack modifies
|
||||
# the library references used by Cockatrice to App relative paths, invalidating the code signature.
|
||||
string(LENGTH $ENV{MACOS_CERTIFICATE_NAME} MACOS_CERTIFICATE_NAME_LEN)
|
||||
|
||||
if(APPLE AND MACOS_CERTIFICATE_NAME_LEN GREATER 0)
|
||||
set(APPLICATIONS "cockatrice" "servatrice" "oracle" "dbconverter")
|
||||
foreach(app_name IN LISTS APPLICATIONS)
|
||||
set(FULL_APP_PATH "${CPACK_TEMPORARY_INSTALL_DIRECTORY}/${app_name}.app")
|
||||
|
||||
message(STATUS "Signing Interior Dynamically Loaded Libraries for ${app_name}.app")
|
||||
execute_process(COMMAND "find" "${FULL_APP_PATH}" "-name" "*.dylib" OUTPUT_VARIABLE INTERIOR_DLLS)
|
||||
string(REPLACE "\n" ";" INTERIOR_DLLS_LIST ${INTERIOR_DLLS})
|
||||
|
||||
foreach(INTERIOR_DLL IN LISTS INTERIOR_DLLS_LIST)
|
||||
execute_process(
|
||||
COMMAND "codesign" "--sign" "$ENV{MACOS_CERTIFICATE_NAME}" "--entitlements" "../.ci/macos.entitlements"
|
||||
"--options" "runtime" "--force" "--deep" "--timestamp" "--verbose" "${INTERIOR_DLL}"
|
||||
)
|
||||
endforeach()
|
||||
|
||||
message(STATUS "Signing Exterior Applications ${app_name}.app")
|
||||
execute_process(
|
||||
COMMAND "codesign" "--sign" "$ENV{MACOS_CERTIFICATE_NAME}" "--entitlements" "../.ci/macos.entitlements"
|
||||
"--options" "runtime" "--force" "--deep" "--timestamp" "--verbose" "${FULL_APP_PATH}"
|
||||
)
|
||||
endforeach()
|
||||
endif()
|
||||
@@ -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()
|
||||
|
||||
@@ -5,133 +5,173 @@
|
||||
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/client/ui/widgets/cards/card_size_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_move_top_cards_until.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/dialogs/dlg_load_deck.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/client/ui/widgets/general/display/dynamic_font_size_label.cpp
|
||||
src/client/ui/widgets/general/display/dynamic_font_size_push_button.cpp
|
||||
src/client/ui/widgets/general/display/shadow_background_label.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/ui/widgets/printing_selector/all_zones_card_amount_widget.cpp
|
||||
src/client/ui/widgets/printing_selector/card_amount_widget.cpp
|
||||
src/client/ui/widgets/printing_selector/printing_selector.cpp
|
||||
src/client/ui/widgets/printing_selector/printing_selector_card_display_widget.cpp
|
||||
src/client/ui/widgets/printing_selector/printing_selector_card_overlay_widget.cpp
|
||||
src/client/ui/widgets/printing_selector/printing_selector_card_search_widget.cpp
|
||||
src/client/ui/widgets/printing_selector/printing_selector_card_selection_widget.cpp
|
||||
src/client/ui/widgets/printing_selector/printing_selector_card_sorting_widget.cpp
|
||||
src/client/ui/widgets/printing_selector/printing_selector_view_options_toolbar_widget.cpp
|
||||
src/client/ui/widgets/printing_selector/printing_selector_view_options_widget.cpp
|
||||
src/client/ui/widgets/printing_selector/set_name_and_collectors_number_display_widget.cpp
|
||||
src/client/network/release_channel.cpp
|
||||
src/client/network/client_update_checker.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/recents_settings.cpp
|
||||
src/settings/servers_settings.cpp
|
||||
src/settings/settings_manager.cpp
|
||||
src/settings/cache_settings.cpp
|
||||
src/settings/shortcuts_settings.cpp
|
||||
src/settings/shortcut_treeview.cpp
|
||||
src/settings/card_override_settings.cpp
|
||||
src/settings/debug_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
|
||||
src/client/tabs/visual_deck_storage/tab_deck_storage_visual.cpp
|
||||
src/client/ui/widgets/cards/deck_preview_card_picture_widget.cpp
|
||||
src/client/ui/widgets/visual_deck_storage/visual_deck_storage_widget.cpp
|
||||
src/client/ui/widgets/visual_deck_storage/visual_deck_storage_search_widget.cpp
|
||||
${VERSION_STRING_CPP}
|
||||
)
|
||||
|
||||
add_subdirectory(sounds)
|
||||
add_subdirectory(themes)
|
||||
configure_file(
|
||||
${CMAKE_SOURCE_DIR}/cockatrice/resources/config/qtlogging.ini ${CMAKE_BINARY_DIR}/cockatrice/qtlogging.ini COPYONLY
|
||||
)
|
||||
|
||||
set(cockatrice_RESOURCES cockatrice.qrc)
|
||||
|
||||
@@ -252,7 +292,7 @@ if(APPLE)
|
||||
set(plugin_dest_dir cockatrice.app/Contents/Plugins)
|
||||
set(qtconf_dest_dir cockatrice.app/Contents/Resources)
|
||||
|
||||
# Qt plugins: audio (Qt5), iconengines, imageformats, platforms, printsupport (Qt5), styles, tls (Qt6)
|
||||
# Qt plugins: audio (Qt5), iconengines, imageformats, multimedia (Qt6), platforms, printsupport (Qt5), styles, tls (Qt6)
|
||||
install(
|
||||
DIRECTORY "${QT_PLUGINS_DIR}/"
|
||||
DESTINATION ${plugin_dest_dir}
|
||||
@@ -263,6 +303,7 @@ if(APPLE)
|
||||
PATTERN "audio/*.dylib"
|
||||
PATTERN "iconengines/*.dylib"
|
||||
PATTERN "imageformats/*.dylib"
|
||||
PATTERN "multimedia/*.dylib"
|
||||
PATTERN "platforms/*.dylib"
|
||||
PATTERN "printsupport/*.dylib"
|
||||
PATTERN "styles/*.dylib"
|
||||
@@ -303,7 +344,7 @@ if(WIN32)
|
||||
PATTERN "*.dll"
|
||||
)
|
||||
|
||||
# Qt plugins: audio (Qt5), iconengines, imageformats, platforms, printsupport (Qt5), styles, tls (Qt6)
|
||||
# Qt plugins: audio (Qt5), iconengines, imageformats, multimedia (Qt6) platforms, printsupport (Qt5), styles, tls (Qt6)
|
||||
install(
|
||||
DIRECTORY "${QT_PLUGINS_DIR}/"
|
||||
DESTINATION ${plugin_dest_dir}
|
||||
@@ -315,6 +356,7 @@ if(WIN32)
|
||||
PATTERN "imageformats/*.dll"
|
||||
PATTERN "mediaservice/dsengine.dll"
|
||||
PATTERN "mediaservice/wmfengine.dll"
|
||||
PATTERN "multimedia/*.dll"
|
||||
PATTERN "platforms/qdirect2d.dll"
|
||||
PATTERN "platforms/qminimal.dll"
|
||||
PATTERN "platforms/qoffscreen.dll"
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
<file>resources/icons/search.svg</file>
|
||||
<file>resources/icons/settings.svg</file>
|
||||
<file>resources/icons/spectator.svg</file>
|
||||
<file>resources/icons/swap.svg</file>
|
||||
<file>resources/icons/sync.svg</file>
|
||||
<file>resources/icons/tab_changed.svg</file>
|
||||
<file>resources/icons/update.png</file>
|
||||
@@ -41,6 +42,8 @@
|
||||
<file>resources/config/deckeditor.svg</file>
|
||||
<file>resources/config/shorcuts.svg</file>
|
||||
<file>resources/config/sound.svg</file>
|
||||
<file>resources/config/debug.ini</file>
|
||||
<file>resources/config/qtlogging.ini</file>
|
||||
|
||||
<file>resources/counters/w.svg</file>
|
||||
<file>resources/counters/w_highlight.svg</file>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
11
cockatrice/resources/config/debug.ini
Normal file
11
cockatrice/resources/config/debug.ini
Normal file
@@ -0,0 +1,11 @@
|
||||
[debug]
|
||||
showCardId=false
|
||||
|
||||
[localgame]
|
||||
onStartup=false
|
||||
playerCount=1
|
||||
;deck\Player 1=path/to/deck
|
||||
;deck\Player 2=path/to/deck
|
||||
|
||||
; Fun Fact: You can assign a deck to your username and it will auto load and ready when you join a server game
|
||||
;deck\Your Username Here=path/to/deck
|
||||
2
cockatrice/resources/config/qtlogging.ini
Normal file
2
cockatrice/resources/config/qtlogging.ini
Normal file
@@ -0,0 +1,2 @@
|
||||
[Rules]
|
||||
picture_loader.debug = true
|
||||
12
cockatrice/resources/icons/swap.svg
Normal file
12
cockatrice/resources/icons/swap.svg
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg xmlns:xlink="http://www.w3.org/1999/xlink" fill="#000000" version="1.1" id="Capa_1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="800px" height="800px" viewBox="0 0 71.753 71.753"
|
||||
xml:space="preserve">
|
||||
<g>
|
||||
<path d="M39.798,20.736H28.172v20.738L11.625,41.47V20.736H0L19.899,0.839L39.798,20.736z M51.855,70.914l19.897-19.896H60.129
|
||||
V30.282l-16.547-0.004v20.74H31.957L51.855,70.914z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 648 B |
@@ -1,66 +0,0 @@
|
||||
#include "cardinfopicture.h"
|
||||
|
||||
#include "carditem.h"
|
||||
#include "main.h"
|
||||
#include "pictureloader.h"
|
||||
|
||||
#include <QStylePainter>
|
||||
#include <QWidget>
|
||||
|
||||
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);
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
#ifndef CARDINFOPICTURE_H
|
||||
#define CARDINFOPICTURE_H
|
||||
|
||||
#include "carddatabase.h"
|
||||
|
||||
#include <QWidget>
|
||||
|
||||
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
|
||||
@@ -1,36 +0,0 @@
|
||||
#ifndef CARDINFOWIDGET_H
|
||||
#define CARDINFOWIDGET_H
|
||||
|
||||
#include "carddatabase.h"
|
||||
|
||||
#include <QComboBox>
|
||||
#include <QFrame>
|
||||
#include <QStringList>
|
||||
|
||||
class CardInfoPicture;
|
||||
class CardInfoText;
|
||||
class AbstractCardItem;
|
||||
|
||||
class CardInfoWidget : public QFrame
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
private:
|
||||
qreal aspectRatio;
|
||||
CardInfoPtr info;
|
||||
CardInfoPicture *pic;
|
||||
CardInfoText *text;
|
||||
|
||||
public:
|
||||
explicit CardInfoWidget(const QString &cardName, QWidget *parent = nullptr, Qt::WindowFlags f = {});
|
||||
|
||||
public slots:
|
||||
void setCard(CardInfoPtr card);
|
||||
void setCard(const QString &cardName);
|
||||
void setCard(AbstractCardItem *card);
|
||||
|
||||
private slots:
|
||||
void clear();
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -1,63 +0,0 @@
|
||||
#include "cardlist.h"
|
||||
|
||||
#include "carddatabase.h"
|
||||
#include "carditem.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
CardList::CardList(bool _contentsKnown) : QList<CardItem *>(), 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);
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
#ifndef CARDLIST_H
|
||||
#define CARDLIST_H
|
||||
|
||||
#include <QList>
|
||||
|
||||
class CardItem;
|
||||
|
||||
class CardList : public QList<CardItem *>
|
||||
{
|
||||
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
|
||||
@@ -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 <google/protobuf/descriptor.h>
|
||||
|
||||
@@ -52,7 +52,7 @@ AbstractClient::AbstractClient(QObject *parent)
|
||||
FeatureSet features;
|
||||
features.initalizeFeatureList(clientFeatures);
|
||||
|
||||
connect(this, SIGNAL(sigQueuePendingCommand(PendingCommand *)), this, SLOT(queuePendingCommand(PendingCommand *)));
|
||||
connect(this, &AbstractClient::sigQueuePendingCommand, this, &AbstractClient::queuePendingCommand);
|
||||
}
|
||||
|
||||
AbstractClient::~AbstractClient()
|
||||
@@ -48,6 +48,7 @@ class AbstractClient : public QObject
|
||||
Q_OBJECT
|
||||
signals:
|
||||
void statusChanged(ClientStatus _status);
|
||||
void maxPingTime(int seconds, int maxSeconds);
|
||||
|
||||
// Room events
|
||||
void roomEventReceived(const RoomEvent &event);
|
||||
@@ -1,4 +1,4 @@
|
||||
#include "keysignals.h"
|
||||
#include "key_signals.h"
|
||||
|
||||
#include <QKeyEvent>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#include "gettextwithmax.h"
|
||||
#include "get_text_with_max.h"
|
||||
|
||||
QString getTextWithMax(QWidget *parent,
|
||||
const QString &title,
|
||||
35
cockatrice/src/client/network/client_update_checker.cpp
Normal file
35
cockatrice/src/client/network/client_update_checker.cpp
Normal file
@@ -0,0 +1,35 @@
|
||||
#include "client_update_checker.h"
|
||||
|
||||
#include "../../settings/cache_settings.h"
|
||||
#include "release_channel.h"
|
||||
|
||||
ClientUpdateChecker::ClientUpdateChecker(QObject *parent) : QObject(parent)
|
||||
{
|
||||
}
|
||||
|
||||
void ClientUpdateChecker::check()
|
||||
{
|
||||
auto releaseChannel = SettingsCache::instance().getUpdateReleaseChannel();
|
||||
|
||||
finishedCheckConnection =
|
||||
connect(releaseChannel, &ReleaseChannel::finishedCheck, this, &ClientUpdateChecker::actFinishedCheck);
|
||||
errorConnection = connect(releaseChannel, &ReleaseChannel::error, this, &ClientUpdateChecker::actError);
|
||||
|
||||
releaseChannel->checkForUpdates();
|
||||
}
|
||||
|
||||
void ClientUpdateChecker::actFinishedCheck(bool needToUpdate, bool isCompatible, Release *release)
|
||||
{
|
||||
disconnect(finishedCheckConnection);
|
||||
disconnect(errorConnection);
|
||||
|
||||
emit finishedCheck(needToUpdate, isCompatible, release);
|
||||
}
|
||||
|
||||
void ClientUpdateChecker::actError(const QString &errorString)
|
||||
{
|
||||
disconnect(finishedCheckConnection);
|
||||
disconnect(errorConnection);
|
||||
|
||||
emit error(errorString);
|
||||
}
|
||||
45
cockatrice/src/client/network/client_update_checker.h
Normal file
45
cockatrice/src/client/network/client_update_checker.h
Normal file
@@ -0,0 +1,45 @@
|
||||
#ifndef CLIENT_UPDATE_CHECKER_H
|
||||
#define CLIENT_UPDATE_CHECKER_H
|
||||
#include <QObject>
|
||||
|
||||
class Release;
|
||||
|
||||
/**
|
||||
* We use a singleton instance of UpdateChannel, which can cause interference and feedback loops when multiple objects
|
||||
* connect to it.
|
||||
*
|
||||
* This class encapsulates the usage of that UpdateChannel to ensure that the check only happens once per connection and
|
||||
* the connection is destroyed after it's been used.
|
||||
*/
|
||||
class ClientUpdateChecker : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
QMetaObject::Connection finishedCheckConnection;
|
||||
QMetaObject::Connection errorConnection;
|
||||
|
||||
void actFinishedCheck(bool needToUpdate, bool isCompatible, Release *release);
|
||||
void actError(const QString &errorString);
|
||||
|
||||
public:
|
||||
explicit ClientUpdateChecker(QObject *parent = nullptr);
|
||||
/**
|
||||
* Actually performs the check, using the currently selected update channel in the settings.
|
||||
* Any resulting signals will only be sent once.
|
||||
* This method should only be called ONCE per instance.
|
||||
*/
|
||||
void check();
|
||||
|
||||
signals:
|
||||
/**
|
||||
* Forwarded from UpdateChannel::finishedCheck
|
||||
*/
|
||||
void finishedCheck(bool needToUpdate, bool isCompatible, Release *release);
|
||||
|
||||
/**
|
||||
* Forwarded from UpdateChannel::error
|
||||
*/
|
||||
void error(const QString &errorString);
|
||||
};
|
||||
|
||||
#endif // CLIENT_UPDATE_CHECKER_H
|
||||
@@ -1,4 +1,4 @@
|
||||
#include "releasechannel.h"
|
||||
#include "release_channel.h"
|
||||
|
||||
#include "version_string.h"
|
||||
|
||||
@@ -21,11 +21,8 @@
|
||||
|
||||
#define GIT_SHORT_HASH_LEN 7
|
||||
|
||||
int ReleaseChannel::sharedIndex = 0;
|
||||
|
||||
ReleaseChannel::ReleaseChannel() : netMan(new QNetworkAccessManager(this)), response(nullptr), lastRelease(nullptr)
|
||||
{
|
||||
index = sharedIndex++;
|
||||
}
|
||||
|
||||
ReleaseChannel::~ReleaseChannel()
|
||||
@@ -38,7 +35,7 @@ void ReleaseChannel::checkForUpdates()
|
||||
QString releaseChannelUrl = getReleaseChannelUrl();
|
||||
qDebug() << "Searching for updates on the channel: " << releaseChannelUrl;
|
||||
response = netMan->get(QNetworkRequest(releaseChannelUrl));
|
||||
connect(response, SIGNAL(finished()), this, SLOT(releaseListFinished()));
|
||||
connect(response, &QNetworkReply::finished, this, &ReleaseChannel::releaseListFinished);
|
||||
}
|
||||
|
||||
// Different release channel checking functions for different operating systems
|
||||
@@ -89,7 +86,7 @@ QString StableReleaseChannel::getManualDownloadUrl() const
|
||||
|
||||
QString StableReleaseChannel::getName() const
|
||||
{
|
||||
return tr("Stable Releases");
|
||||
return tr("Default");
|
||||
}
|
||||
|
||||
QString StableReleaseChannel::getReleaseChannelUrl() const
|
||||
@@ -112,7 +109,7 @@ void StableReleaseChannel::releaseListFinished()
|
||||
QVariantMap resultMap = jsonResponse.toVariant().toMap();
|
||||
if (!(resultMap.contains("name") && resultMap.contains("html_url") && resultMap.contains("tag_name") &&
|
||||
resultMap.contains("published_at"))) {
|
||||
qWarning() << "Invalid received from the release update server.";
|
||||
qWarning() << "Invalid received from the release update server:" << resultMap;
|
||||
emit error(tr("Invalid reply received from the release update server."));
|
||||
return;
|
||||
}
|
||||
@@ -158,7 +155,7 @@ void StableReleaseChannel::releaseListFinished()
|
||||
QString url = QString(STABLETAG_URL) + tagName;
|
||||
qDebug() << "Searching for commit hash corresponding to stable channel tag: " << tagName;
|
||||
response = netMan->get(QNetworkRequest(url));
|
||||
connect(response, SIGNAL(finished()), this, SLOT(tagListFinished()));
|
||||
connect(response, &QNetworkReply::finished, this, &StableReleaseChannel::tagListFinished);
|
||||
}
|
||||
|
||||
void StableReleaseChannel::tagListFinished()
|
||||
@@ -203,7 +200,7 @@ QString BetaReleaseChannel::getManualDownloadUrl() const
|
||||
|
||||
QString BetaReleaseChannel::getName() const
|
||||
{
|
||||
return tr("Beta Releases");
|
||||
return tr("Beta");
|
||||
}
|
||||
|
||||
QString BetaReleaseChannel::getReleaseChannelUrl() const
|
||||
@@ -260,7 +257,7 @@ void BetaReleaseChannel::releaseListFinished()
|
||||
|
||||
qDebug() << "Searching for a corresponding file on the beta channel: " << betaBuildDownloadUrl;
|
||||
response = netMan->get(QNetworkRequest(betaBuildDownloadUrl));
|
||||
connect(response, SIGNAL(finished()), this, SLOT(fileListFinished()));
|
||||
connect(response, &QNetworkReply::finished, this, &BetaReleaseChannel::fileListFinished);
|
||||
}
|
||||
|
||||
void BetaReleaseChannel::fileListFinished()
|
||||
@@ -82,9 +82,6 @@ public:
|
||||
~ReleaseChannel() override;
|
||||
|
||||
protected:
|
||||
// shared by all instances
|
||||
static int sharedIndex;
|
||||
int index;
|
||||
QNetworkAccessManager *netMan;
|
||||
QNetworkReply *response;
|
||||
Release *lastRelease;
|
||||
@@ -94,10 +91,6 @@ protected:
|
||||
virtual QString getReleaseChannelUrl() const = 0;
|
||||
|
||||
public:
|
||||
int getIndex() const
|
||||
{
|
||||
return index;
|
||||
}
|
||||
Release *getLastRelease()
|
||||
{
|
||||
return lastRelease;
|
||||
193
cockatrice/src/client/network/replay_timeline_widget.cpp
Normal file
193
cockatrice/src/client/network/replay_timeline_widget.cpp
Normal file
@@ -0,0 +1,193 @@
|
||||
#include "replay_timeline_widget.h"
|
||||
|
||||
#include <QPainter>
|
||||
#include <QPainterPath>
|
||||
#include <QPalette>
|
||||
#include <QTimer>
|
||||
|
||||
ReplayTimelineWidget::ReplayTimelineWidget(QWidget *parent)
|
||||
: QWidget(parent), maxBinValue(1), maxTime(1), timeScaleFactor(1.0), currentVisualTime(0), currentProcessedTime(0),
|
||||
currentEvent(0)
|
||||
{
|
||||
replayTimer = new QTimer(this);
|
||||
connect(replayTimer, &QTimer::timeout, this, &ReplayTimelineWidget::replayTimerTimeout);
|
||||
|
||||
rewindBufferingTimer = new QTimer(this);
|
||||
rewindBufferingTimer->setSingleShot(true);
|
||||
connect(rewindBufferingTimer, &QTimer::timeout, this, &ReplayTimelineWidget::processRewind);
|
||||
}
|
||||
|
||||
void ReplayTimelineWidget::setTimeline(const QList<int> &_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)currentVisualTime / maxTime;
|
||||
painter.fillRect(0, 0, static_cast<int>(w), height() - 1, barColor);
|
||||
}
|
||||
|
||||
void ReplayTimelineWidget::mousePressEvent(QMouseEvent *event)
|
||||
{
|
||||
#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
|
||||
int newTime = static_cast<int>((qint64)maxTime * (qint64)event->position().x() / width());
|
||||
#else
|
||||
int newTime = static_cast<int>((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 < currentProcessedTime;
|
||||
currentVisualTime = 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(SettingsCache::instance().getRewindBufferingMs());
|
||||
} else {
|
||||
// otherwise, process the rewind immediately
|
||||
processRewind();
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
{
|
||||
currentVisualTime += TIMER_INTERVAL_MS;
|
||||
|
||||
processNewEvents(NORMAL_PLAYBACK);
|
||||
|
||||
if (!(currentVisualTime % 1000))
|
||||
update();
|
||||
}
|
||||
|
||||
/// Processes all unprocessed events up to the current time.
|
||||
void ReplayTimelineWidget::processNewEvents(PlaybackMode playbackMode)
|
||||
{
|
||||
currentProcessedTime = currentVisualTime;
|
||||
|
||||
while ((currentEvent < replayTimeline.size()) && (replayTimeline[currentEvent] < currentProcessedTime)) {
|
||||
Player::EventProcessingOptions options;
|
||||
|
||||
// backwards skip => always skip reveal windows
|
||||
// forwards skip => skip reveal windows that don't happen within a big skip of the target
|
||||
if (playbackMode == BACKWARD_SKIP || currentProcessedTime - replayTimeline[currentEvent] > BIG_SKIP_MS)
|
||||
options |= Player::EventProcessingOption::SKIP_REVEAL_WINDOW;
|
||||
|
||||
// backwards skip => always skip tap animation
|
||||
if (playbackMode == BACKWARD_SKIP)
|
||||
options |= Player::EventProcessingOption::SKIP_TAP_ANIMATION;
|
||||
|
||||
emit processNextEvent(options);
|
||||
++currentEvent;
|
||||
}
|
||||
if (currentEvent == replayTimeline.size()) {
|
||||
emit replayFinished();
|
||||
replayTimer->stop();
|
||||
}
|
||||
}
|
||||
|
||||
void ReplayTimelineWidget::setTimeScaleFactor(qreal _timeScaleFactor)
|
||||
{
|
||||
timeScaleFactor = _timeScaleFactor;
|
||||
replayTimer->setInterval(static_cast<int>(TIMER_INTERVAL_MS / timeScaleFactor));
|
||||
}
|
||||
|
||||
void ReplayTimelineWidget::startReplay()
|
||||
{
|
||||
replayTimer->start(static_cast<int>(TIMER_INTERVAL_MS / timeScaleFactor));
|
||||
}
|
||||
|
||||
void ReplayTimelineWidget::stopReplay()
|
||||
{
|
||||
replayTimer->stop();
|
||||
}
|
||||
|
||||
void ReplayTimelineWidget::skipByAmount(int amount)
|
||||
{
|
||||
skipToTime(currentVisualTime + amount, amount < 0);
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
#ifndef REPLAY_TIMELINE_WIDGET
|
||||
#define REPLAY_TIMELINE_WIDGET
|
||||
|
||||
#include "../../game/player/player.h"
|
||||
|
||||
#include <QList>
|
||||
#include <QMouseEvent>
|
||||
#include <QWidget>
|
||||
@@ -12,23 +14,44 @@ 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;
|
||||
|
||||
QTimer *replayTimer;
|
||||
QTimer *rewindBufferingTimer;
|
||||
QList<int> replayTimeline;
|
||||
QList<int> histogram;
|
||||
static const int binLength;
|
||||
int maxBinValue, maxTime;
|
||||
qreal timeScaleFactor;
|
||||
int currentTime;
|
||||
int currentVisualTime; // time currently displayed by the timeline
|
||||
int currentProcessedTime; // time that events are currently processed up to. Could differ from visual time due to
|
||||
// rewind buffering
|
||||
int currentEvent;
|
||||
|
||||
void skipToTime(int newTime, bool doRewindBuffering);
|
||||
void handleBackwardsSkip(bool doRewindBuffering);
|
||||
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<int> &_replayTimeline);
|
||||
QSize sizeHint() const override;
|
||||
@@ -41,6 +64,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;
|
||||
@@ -1,4 +1,4 @@
|
||||
#include "setsmodel.h"
|
||||
#include "sets_model.h"
|
||||
|
||||
#include <QSortFilterProxyModel>
|
||||
|
||||
@@ -195,6 +195,13 @@ void SetsModel::swapRows(int oldRow, int newRow)
|
||||
emit dataChanged(index(0, 0), index(rowCount() - 1, columnCount() - 1));
|
||||
}
|
||||
|
||||
void SetsModel::restoreOriginalOrder()
|
||||
{
|
||||
int numRows = rowCount();
|
||||
sets.defaultSort();
|
||||
emit dataChanged(index(0, 0), index(numRows - 1, columnCount() - 1));
|
||||
}
|
||||
|
||||
void SetsModel::sort(int column, Qt::SortOrder order)
|
||||
{
|
||||
QMultiMap<QString, CardSetPtr> setMap;
|
||||
@@ -1,7 +1,7 @@
|
||||
#ifndef SETSMODEL_H
|
||||
#define SETSMODEL_H
|
||||
|
||||
#include "carddatabase.h"
|
||||
#include "../../game/cards/card_database.h"
|
||||
|
||||
#include <QAbstractTableModel>
|
||||
#include <QMimeData>
|
||||
@@ -49,7 +49,8 @@ public:
|
||||
LongNameCol,
|
||||
ShortNameCol,
|
||||
SetTypeCol,
|
||||
ReleaseDateCol
|
||||
ReleaseDateCol,
|
||||
PriorityCol
|
||||
};
|
||||
enum Role
|
||||
{
|
||||
@@ -80,6 +81,7 @@ public:
|
||||
void sort(int column, Qt::SortOrder order = Qt::AscendingOrder);
|
||||
void save(CardDatabase *db);
|
||||
void restore(CardDatabase *db);
|
||||
void restoreOriginalOrder();
|
||||
};
|
||||
|
||||
class SetsDisplayModel : public QSortFilterProxyModel
|
||||
@@ -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 <QApplication>
|
||||
#include <QCryptographicHash>
|
||||
@@ -44,10 +45,10 @@ void SpoilerBackgroundUpdater::downloadFromURL(QUrl url, bool saveResults)
|
||||
|
||||
if (saveResults) {
|
||||
// This will write out to the file (used for spoiler.xml)
|
||||
connect(reply, SIGNAL(finished()), this, SLOT(actDownloadFinishedSpoilersFile()));
|
||||
connect(reply, &QNetworkReply::finished, this, &SpoilerBackgroundUpdater::actDownloadFinishedSpoilersFile);
|
||||
} else {
|
||||
// This will check the status (used to see if we're in spoiler season or not)
|
||||
connect(reply, SIGNAL(finished()), this, SLOT(actCheckIfSpoilerSeasonEnabled()));
|
||||
connect(reply, &QNetworkReply::finished, this, &SpoilerBackgroundUpdater::actCheckIfSpoilerSeasonEnabled);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#include "soundengine.h"
|
||||
#include "sound_engine.h"
|
||||
|
||||
#include "settingscache.h"
|
||||
#include "../settings/cache_settings.h"
|
||||
|
||||
#include <QDir>
|
||||
#include <QMediaPlayer>
|
||||
@@ -12,11 +12,11 @@
|
||||
#define DEFAULT_THEME_NAME "Default"
|
||||
#define TEST_SOUND_FILENAME "player_join"
|
||||
|
||||
SoundEngine::SoundEngine(QObject *parent) : QObject(parent), player(nullptr)
|
||||
SoundEngine::SoundEngine(QObject *parent) : QObject(parent), player(nullptr), audioOutput(nullptr)
|
||||
{
|
||||
ensureThemeDirectoryExists();
|
||||
connect(&SettingsCache::instance(), SIGNAL(soundThemeChanged()), this, SLOT(themeChangedSlot()));
|
||||
connect(&SettingsCache::instance(), SIGNAL(soundEnabledChanged()), this, SLOT(soundEnabledChanged()));
|
||||
connect(&SettingsCache::instance(), &SettingsCache::soundThemeChanged, this, &SoundEngine::themeChangedSlot);
|
||||
connect(&SettingsCache::instance(), &SettingsCache::soundEnabledChanged, this, &SoundEngine::soundEnabledChanged);
|
||||
|
||||
soundEnabledChanged();
|
||||
themeChangedSlot();
|
||||
@@ -28,6 +28,10 @@ SoundEngine::~SoundEngine()
|
||||
player->deleteLater();
|
||||
player = nullptr;
|
||||
}
|
||||
if (audioOutput) {
|
||||
audioOutput->deleteLater();
|
||||
audioOutput = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void SoundEngine::soundEnabledChanged()
|
||||
@@ -37,8 +41,8 @@ void SoundEngine::soundEnabledChanged()
|
||||
if (!player) {
|
||||
player = new QMediaPlayer;
|
||||
#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
|
||||
auto qAudioOutput = new QAudioOutput;
|
||||
player->setAudioOutput(qAudioOutput);
|
||||
audioOutput = new QAudioOutput(player);
|
||||
player->setAudioOutput(audioOutput);
|
||||
#endif
|
||||
}
|
||||
} else {
|
||||
@@ -48,6 +52,10 @@ void SoundEngine::soundEnabledChanged()
|
||||
player->deleteLater();
|
||||
player = nullptr;
|
||||
}
|
||||
if (audioOutput) {
|
||||
audioOutput->deleteLater();
|
||||
audioOutput = nullptr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
#ifndef SOUNDENGINE_H
|
||||
#define SOUNDENGINE_H
|
||||
|
||||
#include <QAudioOutput>
|
||||
#include <QMap>
|
||||
#include <QMediaPlayer>
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
|
||||
class QAudioOutput;
|
||||
class QBuffer;
|
||||
|
||||
typedef QMap<QString, QString> QStringMap;
|
||||
@@ -23,6 +23,7 @@ public:
|
||||
private:
|
||||
QStringMap availableThemes;
|
||||
QMap<QString, QString> audioData;
|
||||
QAudioOutput *audioOutput;
|
||||
QMediaPlayer *player;
|
||||
|
||||
protected:
|
||||
@@ -1,6 +1,6 @@
|
||||
#include "tab.h"
|
||||
|
||||
#include "cardinfowidget.h"
|
||||
#include "../ui/widgets/cards/card_info_display_widget.h"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QDebug>
|
||||
@@ -12,14 +12,16 @@ Tab::Tab(TabSupervisor *_tabSupervisor, QWidget *parent)
|
||||
setAttribute(Qt::WA_DeleteOnClose);
|
||||
}
|
||||
|
||||
void Tab::showCardInfoPopup(const QPoint &pos, const QString &cardName)
|
||||
void Tab::showCardInfoPopup(const QPoint &pos, const QString &cardName, const QString &providerId)
|
||||
{
|
||||
if (infoPopup) {
|
||||
infoPopup->deleteLater();
|
||||
}
|
||||
currentCardName = cardName;
|
||||
infoPopup = new CardInfoWidget(
|
||||
cardName, 0, Qt::Widget | Qt::FramelessWindowHint | Qt::X11BypassWindowManagerHint | Qt::WindowStaysOnTopHint);
|
||||
currentProviderId = providerId;
|
||||
infoPopup = new CardInfoDisplayWidget(cardName, providerId, nullptr,
|
||||
Qt::Widget | Qt::FramelessWindowHint | Qt::X11BypassWindowManagerHint |
|
||||
Qt::WindowStaysOnTopHint);
|
||||
infoPopup->setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||
|
||||
auto screenRect = qApp->primaryScreen()->geometry();
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
class QMenu;
|
||||
class TabSupervisor;
|
||||
class CardInfoWidget;
|
||||
class CardInfoDisplayWidget;
|
||||
|
||||
class Tab : public QMainWindow
|
||||
{
|
||||
@@ -21,13 +21,13 @@ protected:
|
||||
tabMenus.append(menu);
|
||||
}
|
||||
protected slots:
|
||||
void showCardInfoPopup(const QPoint &pos, const QString &cardName);
|
||||
void showCardInfoPopup(const QPoint &pos, const QString &cardName, const QString &providerId);
|
||||
void deleteCardInfoPopup(const QString &cardName);
|
||||
|
||||
private:
|
||||
QString currentCardName;
|
||||
QString currentCardName, currentProviderId;
|
||||
bool contentsChanged;
|
||||
CardInfoWidget *infoPopup;
|
||||
CardInfoDisplayWidget *infoPopup;
|
||||
QList<QMenu *> tabMenus;
|
||||
|
||||
public:
|
||||
@@ -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 <QHBoxLayout>
|
||||
#include <QPushButton>
|
||||
@@ -30,29 +30,22 @@ TabUserLists::TabUserLists(TabSupervisor *_tabSupervisor,
|
||||
userInfoBox = new UserInfoBox(client, true);
|
||||
userInfoBox->updateInfo(userInfo);
|
||||
|
||||
connect(allUsersList, SIGNAL(openMessageDialog(const QString &, bool)), this,
|
||||
SIGNAL(openMessageDialog(const QString &, bool)));
|
||||
connect(buddyList, SIGNAL(openMessageDialog(const QString &, bool)), this,
|
||||
SIGNAL(openMessageDialog(const QString &, bool)));
|
||||
connect(ignoreList, SIGNAL(openMessageDialog(const QString &, bool)), this,
|
||||
SIGNAL(openMessageDialog(const QString &, bool)));
|
||||
connect(allUsersList, &UserList::openMessageDialog, this, &TabUserLists::openMessageDialog);
|
||||
connect(buddyList, &UserList::openMessageDialog, this, &TabUserLists::openMessageDialog);
|
||||
connect(ignoreList, &UserList::openMessageDialog, this, &TabUserLists::openMessageDialog);
|
||||
|
||||
connect(client, SIGNAL(userJoinedEventReceived(const Event_UserJoined &)), this,
|
||||
SLOT(processUserJoinedEvent(const Event_UserJoined &)));
|
||||
connect(client, SIGNAL(userLeftEventReceived(const Event_UserLeft &)), this,
|
||||
SLOT(processUserLeftEvent(const Event_UserLeft &)));
|
||||
connect(client, SIGNAL(buddyListReceived(const QList<ServerInfo_User> &)), this,
|
||||
SLOT(buddyListReceived(const QList<ServerInfo_User> &)));
|
||||
connect(client, SIGNAL(ignoreListReceived(const QList<ServerInfo_User> &)), this,
|
||||
SLOT(ignoreListReceived(const QList<ServerInfo_User> &)));
|
||||
connect(client, SIGNAL(addToListEventReceived(const Event_AddToList &)), this,
|
||||
SLOT(processAddToListEvent(const Event_AddToList &)));
|
||||
connect(client, SIGNAL(removeFromListEventReceived(const Event_RemoveFromList &)), this,
|
||||
SLOT(processRemoveFromListEvent(const Event_RemoveFromList &)));
|
||||
connect(client, &AbstractClient::userJoinedEventReceived, this, &TabUserLists::processUserJoinedEvent);
|
||||
connect(client, &AbstractClient::userLeftEventReceived, this, &TabUserLists::processUserLeftEvent);
|
||||
connect(client, &AbstractClient::buddyListReceived, this, &TabUserLists::buddyListReceived);
|
||||
connect(client, &AbstractClient::ignoreListReceived, this, &TabUserLists::ignoreListReceived);
|
||||
connect(client, &AbstractClient::addToListEventReceived, this, &TabUserLists::processAddToListEvent);
|
||||
connect(client, &AbstractClient::removeFromListEventReceived, this, &TabUserLists::processRemoveFromListEvent);
|
||||
|
||||
PendingCommand *pend = client->prepareSessionCommand(Command_ListUsers());
|
||||
connect(pend, SIGNAL(finished(Response, CommandContainer, QVariant)), this,
|
||||
SLOT(processListUsersResponse(const Response &)));
|
||||
connect(pend,
|
||||
static_cast<void (PendingCommand::*)(const Response &, const CommandContainer &, const QVariant &)>(
|
||||
&PendingCommand::finished),
|
||||
this, &TabUserLists::processListUsersResponse);
|
||||
client->sendCommand(pend);
|
||||
|
||||
QVBoxLayout *vbox = new QVBoxLayout;
|
||||
@@ -63,9 +56,9 @@ TabUserLists::TabUserLists(TabSupervisor *_tabSupervisor,
|
||||
addBuddyEdit = new LineEditUnfocusable;
|
||||
addBuddyEdit->setMaxLength(MAX_NAME_LENGTH);
|
||||
addBuddyEdit->setPlaceholderText(tr("Add to Buddy List"));
|
||||
connect(addBuddyEdit, SIGNAL(returnPressed()), this, SLOT(addToBuddyList()));
|
||||
connect(addBuddyEdit, &LineEditUnfocusable::returnPressed, this, &TabUserLists::addToBuddyList);
|
||||
QPushButton *addBuddyButton = new QPushButton("Add");
|
||||
connect(addBuddyButton, SIGNAL(clicked()), this, SLOT(addToBuddyList()));
|
||||
connect(addBuddyButton, &QPushButton::clicked, this, &TabUserLists::addToBuddyList);
|
||||
addToBuddyList->addWidget(addBuddyEdit);
|
||||
addToBuddyList->addWidget(addBuddyButton);
|
||||
|
||||
@@ -73,9 +66,9 @@ TabUserLists::TabUserLists(TabSupervisor *_tabSupervisor,
|
||||
addIgnoreEdit = new LineEditUnfocusable;
|
||||
addIgnoreEdit->setMaxLength(MAX_NAME_LENGTH);
|
||||
addIgnoreEdit->setPlaceholderText(tr("Add to Ignore List"));
|
||||
connect(addIgnoreEdit, SIGNAL(returnPressed()), this, SLOT(addToIgnoreList()));
|
||||
connect(addIgnoreEdit, &LineEditUnfocusable::returnPressed, this, &TabUserLists::addToIgnoreList);
|
||||
QPushButton *addIgnoreButton = new QPushButton("Add");
|
||||
connect(addIgnoreButton, SIGNAL(clicked()), this, SLOT(addToIgnoreList()));
|
||||
connect(addIgnoreButton, &QPushButton::clicked, this, &TabUserLists::addToIgnoreList);
|
||||
addToIgnoreList->addWidget(addIgnoreEdit);
|
||||
addToIgnoreList->addWidget(addIgnoreButton);
|
||||
|
||||
250
cockatrice/src/client/tabs/tab_admin.cpp
Normal file
250
cockatrice/src/client/tabs/tab_admin.cpp
Normal file
@@ -0,0 +1,250 @@
|
||||
#include "tab_admin.h"
|
||||
|
||||
#include "../../server/pending_command.h"
|
||||
#include "../game_logic/abstract_client.h"
|
||||
#include "pb/admin_commands.pb.h"
|
||||
#include "pb/event_replay_added.pb.h"
|
||||
#include "pb/moderator_commands.pb.h"
|
||||
#include "trice_limits.h"
|
||||
|
||||
#include <QDialogButtonBox>
|
||||
#include <QGridLayout>
|
||||
#include <QGroupBox>
|
||||
#include <QLabel>
|
||||
#include <QLineEdit>
|
||||
#include <QMessageBox>
|
||||
#include <QPushButton>
|
||||
#include <QSpinBox>
|
||||
|
||||
ShutdownDialog::ShutdownDialog(QWidget *parent) : QDialog(parent)
|
||||
{
|
||||
QLabel *reasonLabel = new QLabel(tr("&Reason for shutdown:"));
|
||||
reasonEdit = new QLineEdit;
|
||||
reasonEdit->setMaxLength(MAX_TEXT_LENGTH);
|
||||
reasonLabel->setBuddy(reasonEdit);
|
||||
QLabel *minutesLabel = new QLabel(tr("&Time until shutdown (minutes):"));
|
||||
minutesEdit = new QSpinBox;
|
||||
minutesLabel->setBuddy(minutesEdit);
|
||||
minutesEdit->setMinimum(0);
|
||||
minutesEdit->setValue(5);
|
||||
minutesEdit->setMaximum(999);
|
||||
|
||||
QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
|
||||
connect(buttonBox, &QDialogButtonBox::accepted, this, &ShutdownDialog::accept);
|
||||
connect(buttonBox, &QDialogButtonBox::rejected, this, &ShutdownDialog::reject);
|
||||
|
||||
QGridLayout *mainLayout = new QGridLayout;
|
||||
mainLayout->addWidget(reasonLabel, 0, 0);
|
||||
mainLayout->addWidget(reasonEdit, 0, 1);
|
||||
mainLayout->addWidget(minutesLabel, 1, 0);
|
||||
mainLayout->addWidget(minutesEdit, 1, 1);
|
||||
mainLayout->addWidget(buttonBox, 2, 0, 1, 2);
|
||||
|
||||
setLayout(mainLayout);
|
||||
setWindowTitle(tr("Shut down server"));
|
||||
}
|
||||
|
||||
QString ShutdownDialog::getReason() const
|
||||
{
|
||||
return reasonEdit->text();
|
||||
}
|
||||
|
||||
int ShutdownDialog::getMinutes() const
|
||||
{
|
||||
return minutesEdit->value();
|
||||
}
|
||||
|
||||
TabAdmin::TabAdmin(TabSupervisor *_tabSupervisor, AbstractClient *_client, bool _fullAdmin, QWidget *parent)
|
||||
: Tab(_tabSupervisor, parent), locked(true), client(_client), fullAdmin(_fullAdmin)
|
||||
{
|
||||
updateServerMessageButton = new QPushButton;
|
||||
connect(updateServerMessageButton, &QPushButton::clicked, this, &TabAdmin::actUpdateServerMessage);
|
||||
shutdownServerButton = new QPushButton;
|
||||
connect(shutdownServerButton, &QPushButton::clicked, this, &TabAdmin::actShutdownServer);
|
||||
reloadConfigButton = new QPushButton;
|
||||
connect(reloadConfigButton, &QPushButton::clicked, this, &TabAdmin::actReloadConfig);
|
||||
|
||||
grantReplayAccessButton = new QPushButton;
|
||||
grantReplayAccessButton->setEnabled(false);
|
||||
connect(grantReplayAccessButton, &QPushButton::clicked, this, &TabAdmin::actGrantReplayAccess);
|
||||
replayIdToGrant = new QLineEdit;
|
||||
replayIdToGrant->setMaximumWidth(500);
|
||||
replayIdToGrant->setValidator(new QIntValidator(0, INT_MAX, this));
|
||||
connect(replayIdToGrant, &QLineEdit::textChanged, this,
|
||||
[=, this]() { grantReplayAccessButton->setEnabled(!replayIdToGrant->text().isEmpty()); });
|
||||
auto *grandReplayAccessLayout = new QGridLayout(this);
|
||||
grandReplayAccessLayout->addWidget(replayIdToGrant, 0, 0);
|
||||
grandReplayAccessLayout->addWidget(grantReplayAccessButton, 0, 1);
|
||||
|
||||
activateUserButton = new QPushButton;
|
||||
activateUserButton->setEnabled(false);
|
||||
connect(activateUserButton, &QPushButton::clicked, this, &TabAdmin::actForceActivateUser);
|
||||
userToActivate = new QLineEdit;
|
||||
userToActivate->setMaximumWidth(500);
|
||||
connect(userToActivate, &QLineEdit::textChanged, this,
|
||||
[=, this]() { activateUserButton->setEnabled(!userToActivate->text().isEmpty()); });
|
||||
auto *activateUserLayout = new QGridLayout(this);
|
||||
activateUserLayout->addWidget(userToActivate, 0, 0);
|
||||
activateUserLayout->addWidget(activateUserButton, 0, 1);
|
||||
|
||||
QVBoxLayout *vbox = new QVBoxLayout;
|
||||
vbox->addWidget(updateServerMessageButton);
|
||||
vbox->addWidget(shutdownServerButton);
|
||||
vbox->addWidget(reloadConfigButton);
|
||||
vbox->addLayout(grandReplayAccessLayout);
|
||||
vbox->addLayout(activateUserLayout);
|
||||
vbox->addStretch();
|
||||
|
||||
adminGroupBox = new QGroupBox;
|
||||
adminGroupBox->setLayout(vbox);
|
||||
adminGroupBox->setEnabled(false);
|
||||
|
||||
unlockButton = new QPushButton;
|
||||
connect(unlockButton, &QPushButton::clicked, this, &TabAdmin::actUnlock);
|
||||
lockButton = new QPushButton;
|
||||
lockButton->setEnabled(false);
|
||||
connect(lockButton, &QPushButton::clicked, this, &TabAdmin::actLock);
|
||||
|
||||
QVBoxLayout *mainLayout = new QVBoxLayout;
|
||||
mainLayout->addWidget(adminGroupBox);
|
||||
mainLayout->addWidget(unlockButton);
|
||||
mainLayout->addWidget(lockButton);
|
||||
|
||||
retranslateUi();
|
||||
|
||||
QWidget *mainWidget = new QWidget(this);
|
||||
mainWidget->setLayout(mainLayout);
|
||||
setCentralWidget(mainWidget);
|
||||
|
||||
actUnlock();
|
||||
}
|
||||
|
||||
void TabAdmin::retranslateUi()
|
||||
{
|
||||
updateServerMessageButton->setText(tr("Update server &message"));
|
||||
shutdownServerButton->setText(tr("&Shut down server"));
|
||||
reloadConfigButton->setText(tr("&Reload configuration"));
|
||||
adminGroupBox->setTitle(tr("Server administration functions"));
|
||||
|
||||
replayIdToGrant->setPlaceholderText(tr("Replay ID"));
|
||||
grantReplayAccessButton->setText(tr("Grant Replay Access"));
|
||||
|
||||
userToActivate->setPlaceholderText(tr("Username to Activate"));
|
||||
activateUserButton->setText(tr("Force Activate User"));
|
||||
|
||||
unlockButton->setText(tr("&Unlock functions"));
|
||||
lockButton->setText(tr("&Lock functions"));
|
||||
}
|
||||
|
||||
void TabAdmin::actUpdateServerMessage()
|
||||
{
|
||||
client->sendCommand(client->prepareAdminCommand(Command_UpdateServerMessage()));
|
||||
}
|
||||
|
||||
void TabAdmin::actShutdownServer()
|
||||
{
|
||||
ShutdownDialog dlg;
|
||||
if (dlg.exec()) {
|
||||
Command_ShutdownServer cmd;
|
||||
cmd.set_reason(dlg.getReason().toStdString());
|
||||
cmd.set_minutes(dlg.getMinutes());
|
||||
|
||||
client->sendCommand(AbstractClient::prepareAdminCommand(cmd));
|
||||
}
|
||||
}
|
||||
|
||||
void TabAdmin::actReloadConfig()
|
||||
{
|
||||
Command_ReloadConfig cmd;
|
||||
client->sendCommand(client->prepareAdminCommand(cmd));
|
||||
}
|
||||
|
||||
void TabAdmin::actGrantReplayAccess()
|
||||
{
|
||||
if (!replayIdToGrant) {
|
||||
return;
|
||||
}
|
||||
|
||||
Command_GrantReplayAccess cmd;
|
||||
cmd.set_replay_id(replayIdToGrant->text().toUInt());
|
||||
cmd.set_moderator_name(client->getUserName().toStdString());
|
||||
|
||||
auto *pend = client->prepareModeratorCommand(cmd);
|
||||
connect(pend,
|
||||
QOverload<const Response &, const CommandContainer &, const QVariant &>::of(&PendingCommand::finished),
|
||||
this, &TabAdmin::grantReplayAccessProcessResponse);
|
||||
client->sendCommand(pend);
|
||||
}
|
||||
|
||||
void TabAdmin::actForceActivateUser()
|
||||
{
|
||||
if (!userToActivate) {
|
||||
return;
|
||||
}
|
||||
|
||||
Command_ForceActivateUser cmd;
|
||||
cmd.set_username_to_activate(userToActivate->text().trimmed().toStdString());
|
||||
cmd.set_moderator_name(client->getUserName().toStdString());
|
||||
|
||||
auto *pend = client->prepareModeratorCommand(cmd);
|
||||
connect(pend,
|
||||
QOverload<const Response &, const CommandContainer &, const QVariant &>::of(&PendingCommand::finished),
|
||||
this, &TabAdmin::activateUserProcessResponse);
|
||||
client->sendCommand(pend);
|
||||
}
|
||||
|
||||
void TabAdmin::grantReplayAccessProcessResponse(const Response &response, const CommandContainer &, const QVariant &)
|
||||
{
|
||||
auto *event = new Event_ReplayAdded();
|
||||
|
||||
switch (response.response_code()) {
|
||||
case Response::RespOk:
|
||||
client->replayAddedEventReceived(*event);
|
||||
QMessageBox::information(this, tr("Success"), tr("Replay access granted"));
|
||||
break;
|
||||
case Response::RespContextError:
|
||||
QMessageBox::critical(this, tr("Error"), tr("Unable to grant replay access. Replay ID invalid"));
|
||||
break;
|
||||
default:
|
||||
QMessageBox::critical(this, tr("Error"), tr("Unable to grant replay access. Internal error"));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void TabAdmin::activateUserProcessResponse(const Response &response, const CommandContainer &, const QVariant &)
|
||||
{
|
||||
switch (response.response_code()) {
|
||||
case Response::RespActivationAccepted:
|
||||
QMessageBox::information(this, tr("Success"), tr("User successfully activated"));
|
||||
break;
|
||||
case Response::RespNameNotFound:
|
||||
QMessageBox::critical(this, tr("Error"), tr("Unable to activate user. Username invalid"));
|
||||
break;
|
||||
case Response::RespActivationFailed:
|
||||
QMessageBox::critical(this, tr("Error"), tr("Unable to activate user. User already active"));
|
||||
break;
|
||||
default:
|
||||
QMessageBox::critical(this, tr("Error"), tr("Unable to activate user. Internal error"));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void TabAdmin::actUnlock()
|
||||
{
|
||||
if (fullAdmin)
|
||||
adminGroupBox->setEnabled(true);
|
||||
lockButton->setEnabled(true);
|
||||
unlockButton->setEnabled(false);
|
||||
locked = false;
|
||||
emit adminLockChanged(false);
|
||||
}
|
||||
|
||||
void TabAdmin::actLock()
|
||||
{
|
||||
if (fullAdmin)
|
||||
adminGroupBox->setEnabled(false);
|
||||
lockButton->setEnabled(false);
|
||||
unlockButton->setEnabled(true);
|
||||
locked = true;
|
||||
emit adminLockChanged(true);
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
#ifndef TAB_ADMIN_H
|
||||
#define TAB_ADMIN_H
|
||||
|
||||
#include "pb/commands.pb.h"
|
||||
#include "pb/response.pb.h"
|
||||
#include "tab.h"
|
||||
|
||||
#include <QDialog>
|
||||
@@ -32,15 +34,21 @@ private:
|
||||
bool locked;
|
||||
AbstractClient *client;
|
||||
bool fullAdmin;
|
||||
QPushButton *updateServerMessageButton, *shutdownServerButton, *reloadConfigButton;
|
||||
QPushButton *updateServerMessageButton, *shutdownServerButton, *reloadConfigButton, *grantReplayAccessButton,
|
||||
*activateUserButton;
|
||||
QGroupBox *adminGroupBox;
|
||||
QPushButton *unlockButton, *lockButton;
|
||||
QLineEdit *replayIdToGrant, *userToActivate;
|
||||
signals:
|
||||
void adminLockChanged(bool lock);
|
||||
private slots:
|
||||
void actUpdateServerMessage();
|
||||
void actShutdownServer();
|
||||
void actReloadConfig();
|
||||
void actGrantReplayAccess();
|
||||
void actForceActivateUser();
|
||||
void grantReplayAccessProcessResponse(const Response &resp, const CommandContainer &, const QVariant &);
|
||||
void activateUserProcessResponse(const Response &response, const CommandContainer &, const QVariant &);
|
||||
|
||||
void actUnlock();
|
||||
void actLock();
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,10 @@
|
||||
#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 "../ui/widgets/printing_selector/printing_selector.h"
|
||||
#include "tab.h"
|
||||
|
||||
#include <QAbstractItemModel>
|
||||
@@ -14,51 +15,43 @@ class CardDatabaseDisplayModel;
|
||||
class DeckListModel;
|
||||
class QTreeView;
|
||||
|
||||
class CardFrame;
|
||||
class CardInfoFrameWidget;
|
||||
class QTextEdit;
|
||||
class QLabel;
|
||||
class DeckLoader;
|
||||
class Response;
|
||||
class FilterTreeModel;
|
||||
class FilterBuilder;
|
||||
class QComboBox;
|
||||
class QGroupBox;
|
||||
class QMessageBox;
|
||||
class QHBoxLayout;
|
||||
class QVBoxLayout;
|
||||
class QPushButton;
|
||||
class QDockWidget;
|
||||
|
||||
class SearchLineEdit : public LineEditUnfocusable
|
||||
{
|
||||
private:
|
||||
QTreeView *treeView;
|
||||
|
||||
protected:
|
||||
void keyPressEvent(QKeyEvent *event) override;
|
||||
|
||||
public:
|
||||
SearchLineEdit() : LineEditUnfocusable(), treeView(nullptr)
|
||||
{
|
||||
}
|
||||
void setTreeView(QTreeView *_treeView)
|
||||
{
|
||||
treeView = _treeView;
|
||||
}
|
||||
};
|
||||
|
||||
class TabDeckEditor : public Tab
|
||||
{
|
||||
Q_OBJECT
|
||||
private slots:
|
||||
void updateName(const QString &name);
|
||||
void updateComments();
|
||||
void updateBannerCardComboBox();
|
||||
void setBannerCard(int);
|
||||
void updateHash();
|
||||
void updateCardInfoLeft(const QModelIndex ¤t, const QModelIndex &previous);
|
||||
void updateCardInfoRight(const QModelIndex ¤t, const QModelIndex &previous);
|
||||
void updatePrintingSelectorDatabase(const QModelIndex ¤t, const QModelIndex &previous);
|
||||
void updatePrintingSelectorDeckView(const QModelIndex ¤t, const QModelIndex &previous);
|
||||
void updateSearch(const QString &search);
|
||||
void databaseCustomMenu(QPoint point);
|
||||
void decklistCustomMenu(QPoint point);
|
||||
void updateRecentlyOpened();
|
||||
|
||||
void actNewDeck();
|
||||
void actLoadDeck();
|
||||
void actOpenRecent(const QString &fileName);
|
||||
void actClearRecents();
|
||||
bool actSaveDeck();
|
||||
bool actSaveDeckAs();
|
||||
void actLoadDeckFromClipboard();
|
||||
@@ -100,11 +93,29 @@ 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);
|
||||
void decrementCardHelper(QString zoneName);
|
||||
bool swapCard(const QModelIndex &idx);
|
||||
void recursiveExpand(const QModelIndex &index);
|
||||
void openDeckFromFile(const QString &fileName, DeckOpenLocation deckOpenLocation);
|
||||
|
||||
QModelIndexList getSelectedCardNodes() const;
|
||||
|
||||
CardDatabaseModel *databaseModel;
|
||||
CardDatabaseDisplayModel *databaseDisplayModel;
|
||||
@@ -113,7 +124,8 @@ private:
|
||||
|
||||
QTreeView *deckView;
|
||||
KeySignals deckViewKeySignals;
|
||||
CardFrame *cardInfo;
|
||||
CardInfoFrameWidget *cardInfo;
|
||||
PrintingSelector *printingSelector;
|
||||
SearchLineEdit *searchEdit;
|
||||
KeySignals searchKeySignals;
|
||||
|
||||
@@ -121,6 +133,8 @@ private:
|
||||
LineEditUnfocusable *nameEdit;
|
||||
QLabel *commentsLabel;
|
||||
QTextEdit *commentsEdit;
|
||||
QLabel *bannerCardLabel;
|
||||
QComboBox *bannerCardComboBox;
|
||||
QLabel *hashLabel1;
|
||||
LineEditUnfocusable *hashLabel;
|
||||
FilterTreeModel *filterModel;
|
||||
@@ -128,16 +142,16 @@ private:
|
||||
KeySignals filterViewKeySignals;
|
||||
QWidget *filterBox;
|
||||
|
||||
QMenu *deckMenu, *viewMenu, *cardInfoDockMenu, *deckDockMenu, *filterDockMenu, *analyzeDeckMenu,
|
||||
*saveDeckToClipboardMenu;
|
||||
QAction *aNewDeck, *aLoadDeck, *aSaveDeck, *aSaveDeckAs, *aLoadDeckFromClipboard, *aSaveDeckToClipboard,
|
||||
*aSaveDeckToClipboardRaw, *aPrintDeck, *aExportDeckDecklist, *aAnalyzeDeckDeckstats, *aAnalyzeDeckTappedout,
|
||||
*aClose;
|
||||
QMenu *deckMenu, *viewMenu, *cardInfoDockMenu, *deckDockMenu, *filterDockMenu, *printingSelectorDockMenu,
|
||||
*analyzeDeckMenu, *saveDeckToClipboardMenu, *loadRecentDeckMenu;
|
||||
QAction *aNewDeck, *aLoadDeck, *aClearRecents, *aSaveDeck, *aSaveDeckAs, *aLoadDeckFromClipboard,
|
||||
*aSaveDeckToClipboard, *aSaveDeckToClipboardRaw, *aPrintDeck, *aExportDeckDecklist, *aAnalyzeDeckDeckstats,
|
||||
*aAnalyzeDeckTappedout, *aClose;
|
||||
QAction *aClearFilterAll, *aClearFilterOne;
|
||||
QAction *aAddCard, *aAddCardToSideboard, *aRemoveCard, *aIncrement, *aDecrement;
|
||||
QAction *aAddCard, *aAddCardToSideboard, *aRemoveCard, *aIncrement, *aDecrement, *aSwapCard;
|
||||
QAction *aResetLayout;
|
||||
QAction *aCardInfoDockVisible, *aCardInfoDockFloating, *aDeckDockVisible, *aDeckDockFloating, *aFilterDockVisible,
|
||||
*aFilterDockFloating;
|
||||
*aFilterDockFloating, *aPrintingSelectorDockVisible, *aPrintingSelectorDockFloating;
|
||||
|
||||
bool modified;
|
||||
QVBoxLayout *centralFrame;
|
||||
@@ -145,6 +159,7 @@ private:
|
||||
QDockWidget *cardInfoDock;
|
||||
QDockWidget *deckDock;
|
||||
QDockWidget *filterDock;
|
||||
QDockWidget *printingSelectorDock;
|
||||
QWidget *centralWidget;
|
||||
|
||||
public:
|
||||
@@ -158,12 +173,16 @@ public:
|
||||
void createDeckDock();
|
||||
void createCardInfoDock();
|
||||
void createFiltersDock();
|
||||
void createPrintingSelectorDock();
|
||||
void createMenus();
|
||||
void createCentralFrame();
|
||||
void updateCardInfo(CardInfoPtr _card);
|
||||
|
||||
public slots:
|
||||
void closeRequest() override;
|
||||
void showPrintingSelector();
|
||||
signals:
|
||||
void openDeckEditor(const DeckLoader *deckLoader);
|
||||
void deckEditorClosing(TabDeckEditor *tab);
|
||||
};
|
||||
|
||||
@@ -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,13 +15,11 @@
|
||||
#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 <QAction>
|
||||
#include <QApplication>
|
||||
#include <QDebug>
|
||||
#include <QDesktopServices>
|
||||
#include <QFileSystemModel>
|
||||
#include <QGroupBox>
|
||||
#include <QHBoxLayout>
|
||||
@@ -27,6 +28,7 @@
|
||||
#include <QMessageBox>
|
||||
#include <QToolBar>
|
||||
#include <QTreeView>
|
||||
#include <QUrl>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
TabDeckStorage::TabDeckStorage(TabSupervisor *_tabSupervisor, AbstractClient *_client)
|
||||
@@ -41,16 +43,33 @@ TabDeckStorage::TabDeckStorage(TabSupervisor *_tabSupervisor, AbstractClient *_c
|
||||
localDirView->setColumnHidden(1, true);
|
||||
localDirView->setRootIndex(localDirModel->index(localDirModel->rootPath(), 0));
|
||||
localDirView->setSortingEnabled(true);
|
||||
localDirView->setSelectionMode(QAbstractItemView::ExtendedSelection);
|
||||
localDirView->header()->setSectionResizeMode(QHeaderView::ResizeToContents);
|
||||
localDirView->header()->setSortIndicator(0, Qt::AscendingOrder);
|
||||
|
||||
leftToolBar = new QToolBar;
|
||||
connect(localDirView, &QTreeView::doubleClicked, this, &TabDeckStorage::actLocalDoubleClick);
|
||||
|
||||
// Left side layout
|
||||
/* put an invisible dummy QToolBar in the leftmost column so that the main toolbar is centered.
|
||||
* Really ugly workaround, but I couldn't figure out the proper way to make it centered */
|
||||
QToolBar *dummyToolBar = new QToolBar(this);
|
||||
QSizePolicy sizePolicy = dummyToolBar->sizePolicy();
|
||||
sizePolicy.setRetainSizeWhenHidden(true);
|
||||
dummyToolBar->setSizePolicy(sizePolicy);
|
||||
dummyToolBar->setVisible(false);
|
||||
|
||||
leftToolBar = new QToolBar(this);
|
||||
leftToolBar->setOrientation(Qt::Horizontal);
|
||||
leftToolBar->setIconSize(QSize(32, 32));
|
||||
QHBoxLayout *leftToolBarLayout = new QHBoxLayout;
|
||||
leftToolBarLayout->addStretch();
|
||||
leftToolBarLayout->addWidget(leftToolBar);
|
||||
leftToolBarLayout->addStretch();
|
||||
|
||||
QToolBar *leftRightmostToolBar = new QToolBar(this);
|
||||
leftRightmostToolBar->setOrientation(Qt::Horizontal);
|
||||
leftRightmostToolBar->setIconSize(QSize(32, 32));
|
||||
|
||||
QGridLayout *leftToolBarLayout = new QGridLayout;
|
||||
leftToolBarLayout->addWidget(dummyToolBar, 0, 0, Qt::AlignLeft);
|
||||
leftToolBarLayout->addWidget(leftToolBar, 0, 1, Qt::AlignHCenter);
|
||||
leftToolBarLayout->addWidget(leftRightmostToolBar, 0, 2, Qt::AlignRight);
|
||||
|
||||
QVBoxLayout *leftVbox = new QVBoxLayout;
|
||||
leftVbox->addWidget(localDirView);
|
||||
@@ -58,6 +77,7 @@ TabDeckStorage::TabDeckStorage(TabSupervisor *_tabSupervisor, AbstractClient *_c
|
||||
leftGroupBox = new QGroupBox;
|
||||
leftGroupBox->setLayout(leftVbox);
|
||||
|
||||
// Right side layout
|
||||
rightToolBar = new QToolBar;
|
||||
rightToolBar->setOrientation(Qt::Horizontal);
|
||||
rightToolBar->setIconSize(QSize(32, 32));
|
||||
@@ -68,25 +88,38 @@ TabDeckStorage::TabDeckStorage(TabSupervisor *_tabSupervisor, AbstractClient *_c
|
||||
|
||||
serverDirView = new RemoteDeckList_TreeWidget(client);
|
||||
|
||||
connect(serverDirView, &QTreeView::doubleClicked, this, &TabDeckStorage::actRemoteDoubleClick);
|
||||
|
||||
QVBoxLayout *rightVbox = new QVBoxLayout;
|
||||
rightVbox->addWidget(serverDirView);
|
||||
rightVbox->addLayout(rightToolBarLayout);
|
||||
rightGroupBox = new QGroupBox;
|
||||
rightGroupBox->setLayout(rightVbox);
|
||||
|
||||
// combine layouts
|
||||
QHBoxLayout *hbox = new QHBoxLayout;
|
||||
hbox->addWidget(leftGroupBox);
|
||||
hbox->addWidget(rightGroupBox);
|
||||
|
||||
// Left side actions
|
||||
aOpenLocalDeck = new QAction(this);
|
||||
aOpenLocalDeck->setIcon(QPixmap("theme:icons/pencil"));
|
||||
connect(aOpenLocalDeck, SIGNAL(triggered()), this, SLOT(actOpenLocalDeck()));
|
||||
aUpload = new QAction(this);
|
||||
aUpload->setIcon(QPixmap("theme:icons/arrow_right_green"));
|
||||
connect(aUpload, SIGNAL(triggered()), this, SLOT(actUpload()));
|
||||
aNewLocalFolder = new QAction(this);
|
||||
aNewLocalFolder->setIcon(qApp->style()->standardIcon(QStyle::SP_FileDialogNewFolder));
|
||||
connect(aNewLocalFolder, &QAction::triggered, this, &TabDeckStorage::actNewLocalFolder);
|
||||
aDeleteLocalDeck = new QAction(this);
|
||||
aDeleteLocalDeck->setIcon(QPixmap("theme:icons/remove_row"));
|
||||
connect(aDeleteLocalDeck, SIGNAL(triggered()), this, SLOT(actDeleteLocalDeck()));
|
||||
|
||||
aOpenDecksFolder = new QAction(this);
|
||||
aOpenDecksFolder->setIcon(qApp->style()->standardIcon(QStyle::SP_DirOpenIcon));
|
||||
connect(aOpenDecksFolder, &QAction::triggered, this, &TabDeckStorage::actOpenDecksFolder);
|
||||
|
||||
// Right side actions
|
||||
aOpenRemoteDeck = new QAction(this);
|
||||
aOpenRemoteDeck->setIcon(QPixmap("theme:icons/pencil"));
|
||||
connect(aOpenRemoteDeck, SIGNAL(triggered()), this, SLOT(actOpenRemoteDeck()));
|
||||
@@ -100,9 +133,14 @@ TabDeckStorage::TabDeckStorage(TabSupervisor *_tabSupervisor, AbstractClient *_c
|
||||
aDeleteRemoteDeck->setIcon(QPixmap("theme:icons/remove_row"));
|
||||
connect(aDeleteRemoteDeck, SIGNAL(triggered()), this, SLOT(actDeleteRemoteDeck()));
|
||||
|
||||
// Add actions to toolbars
|
||||
leftToolBar->addAction(aOpenLocalDeck);
|
||||
leftToolBar->addAction(aUpload);
|
||||
leftToolBar->addAction(aNewLocalFolder);
|
||||
leftToolBar->addAction(aDeleteLocalDeck);
|
||||
|
||||
leftRightmostToolBar->addAction(aOpenDecksFolder);
|
||||
|
||||
rightToolBar->addAction(aOpenRemoteDeck);
|
||||
rightToolBar->addAction(aDownload);
|
||||
rightToolBar->addAction(aNewFolder);
|
||||
@@ -124,9 +162,11 @@ void TabDeckStorage::retranslateUi()
|
||||
aUpload->setText(tr("Upload deck"));
|
||||
aOpenRemoteDeck->setText(tr("Open in deck editor"));
|
||||
aDownload->setText(tr("Download deck"));
|
||||
aNewLocalFolder->setText(tr("New folder"));
|
||||
aNewFolder->setText(tr("New folder"));
|
||||
aDeleteLocalDeck->setText(tr("Delete"));
|
||||
aDeleteRemoteDeck->setText(tr("Delete"));
|
||||
aOpenDecksFolder->setText(tr("Open decks folder"));
|
||||
}
|
||||
|
||||
QString TabDeckStorage::getTargetPath() const
|
||||
@@ -147,43 +187,61 @@ QString TabDeckStorage::getTargetPath() const
|
||||
}
|
||||
}
|
||||
|
||||
void TabDeckStorage::actLocalDoubleClick(const QModelIndex &curLeft)
|
||||
{
|
||||
if (!localDirModel->isDir(curLeft)) {
|
||||
actOpenLocalDeck();
|
||||
}
|
||||
}
|
||||
|
||||
void TabDeckStorage::actOpenLocalDeck()
|
||||
{
|
||||
QModelIndex curLeft = localDirView->selectionModel()->currentIndex();
|
||||
if (localDirModel->isDir(curLeft))
|
||||
return;
|
||||
QString filePath = localDirModel->filePath(curLeft);
|
||||
QModelIndexList curLefts = localDirView->selectionModel()->selectedRows();
|
||||
for (const auto &curLeft : curLefts) {
|
||||
if (localDirModel->isDir(curLeft))
|
||||
continue;
|
||||
QString filePath = localDirModel->filePath(curLeft);
|
||||
|
||||
DeckLoader deckLoader;
|
||||
if (!deckLoader.loadFromFile(filePath, DeckLoader::CockatriceFormat))
|
||||
return;
|
||||
DeckLoader deckLoader;
|
||||
if (!deckLoader.loadFromFile(filePath, DeckLoader::CockatriceFormat))
|
||||
continue;
|
||||
|
||||
emit openDeckEditor(&deckLoader);
|
||||
SettingsCache::instance().recents().updateRecentlyOpenedDeckPaths(filePath);
|
||||
|
||||
emit openDeckEditor(&deckLoader);
|
||||
}
|
||||
}
|
||||
|
||||
void TabDeckStorage::actUpload()
|
||||
{
|
||||
QModelIndex curLeft = localDirView->selectionModel()->currentIndex();
|
||||
if (localDirModel->isDir(curLeft))
|
||||
QModelIndexList curLefts = localDirView->selectionModel()->selectedRows();
|
||||
if (curLefts.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
QString targetPath = getTargetPath();
|
||||
if (targetPath.length() > MAX_NAME_LENGTH) {
|
||||
qCritical() << "target path to upload to is too long" << targetPath;
|
||||
return;
|
||||
}
|
||||
|
||||
QString filePath = localDirModel->filePath(curLeft);
|
||||
for (const auto &curLeft : curLefts) {
|
||||
if (localDirModel->isDir(curLeft)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
QString filePath = localDirModel->filePath(curLeft);
|
||||
uploadDeck(filePath, targetPath);
|
||||
}
|
||||
}
|
||||
|
||||
void TabDeckStorage::uploadDeck(const QString &filePath, const QString &targetPath)
|
||||
{
|
||||
QFile deckFile(filePath);
|
||||
QFileInfo deckFileInfo(deckFile);
|
||||
|
||||
QString deckString;
|
||||
DeckLoader deck;
|
||||
bool error = !deck.loadFromFile(filePath, DeckLoader::CockatriceFormat);
|
||||
if (!error) {
|
||||
deckString = deck.writeToString_Native();
|
||||
error = deckString.length() > MAX_FILE_LENGTH;
|
||||
}
|
||||
if (error) {
|
||||
if (!deck.loadFromFile(filePath, DeckLoader::CockatriceFormat)) {
|
||||
QMessageBox::critical(this, tr("Error"), tr("Invalid deck file"));
|
||||
return;
|
||||
}
|
||||
@@ -202,6 +260,12 @@ void TabDeckStorage::actUpload()
|
||||
deck.setName(deck.getName().left(MAX_NAME_LENGTH));
|
||||
}
|
||||
|
||||
QString deckString = deck.writeToString_Native();
|
||||
if (deckString.length() > MAX_FILE_LENGTH) {
|
||||
QMessageBox::critical(this, tr("Error"), tr("Invalid deck file"));
|
||||
return;
|
||||
}
|
||||
|
||||
Command_DeckUpload cmd;
|
||||
cmd.set_path(targetPath.toStdString());
|
||||
cmd.set_deck_list(deckString.toStdString());
|
||||
@@ -226,34 +290,73 @@ void TabDeckStorage::uploadFinished(const Response &r, const CommandContainer &c
|
||||
serverDirView->addFileToTree(resp.new_file(), serverDirView->getNodeByPath(QString::fromStdString(cmd.path())));
|
||||
}
|
||||
|
||||
void TabDeckStorage::actDeleteLocalDeck()
|
||||
void TabDeckStorage::actNewLocalFolder()
|
||||
{
|
||||
QModelIndex curLeft = localDirView->selectionModel()->currentIndex();
|
||||
if (localDirModel->isDir(curLeft))
|
||||
|
||||
QModelIndex dirIndex;
|
||||
if (curLeft.isValid() && !localDirModel->isDir(curLeft)) {
|
||||
dirIndex = curLeft.parent();
|
||||
} else {
|
||||
dirIndex = curLeft;
|
||||
}
|
||||
|
||||
bool ok;
|
||||
QString folderName =
|
||||
QInputDialog::getText(this, tr("New folder"), tr("Name of new folder:"), QLineEdit::Normal, "", &ok);
|
||||
if (!ok || folderName.isEmpty())
|
||||
return;
|
||||
|
||||
if (QMessageBox::warning(this, tr("Delete local file"),
|
||||
tr("Are you sure you want to delete \"%1\"?").arg(localDirModel->fileName(curLeft)),
|
||||
localDirModel->mkdir(dirIndex, folderName);
|
||||
}
|
||||
|
||||
void TabDeckStorage::actDeleteLocalDeck()
|
||||
{
|
||||
const QModelIndexList curLefts = localDirView->selectionModel()->selectedRows();
|
||||
|
||||
if (curLefts.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (QMessageBox::warning(this, tr("Delete local file"), tr("Are you sure you want to delete the selected files?"),
|
||||
QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes)
|
||||
return;
|
||||
|
||||
localDirModel->remove(curLeft);
|
||||
for (const auto &curLeft : curLefts) {
|
||||
if (curLeft.isValid()) {
|
||||
localDirModel->remove(curLeft);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void TabDeckStorage::actOpenDecksFolder()
|
||||
{
|
||||
QString dir = localDirModel->rootPath();
|
||||
QDesktopServices::openUrl(QUrl::fromLocalFile(dir));
|
||||
}
|
||||
|
||||
void TabDeckStorage::actRemoteDoubleClick(const QModelIndex &curRight)
|
||||
{
|
||||
if (dynamic_cast<RemoteDeckList_TreeModel::FileNode *>(serverDirView->getNode(curRight))) {
|
||||
actOpenRemoteDeck();
|
||||
}
|
||||
}
|
||||
|
||||
void TabDeckStorage::actOpenRemoteDeck()
|
||||
{
|
||||
RemoteDeckList_TreeModel::FileNode *curRight =
|
||||
dynamic_cast<RemoteDeckList_TreeModel::FileNode *>(serverDirView->getCurrentItem());
|
||||
if (!curRight)
|
||||
return;
|
||||
for (const auto &curRight : serverDirView->getCurrentSelection()) {
|
||||
RemoteDeckList_TreeModel::FileNode *node = dynamic_cast<RemoteDeckList_TreeModel::FileNode *>(curRight);
|
||||
if (!node)
|
||||
continue;
|
||||
|
||||
Command_DeckDownload cmd;
|
||||
cmd.set_deck_id(curRight->getId());
|
||||
Command_DeckDownload cmd;
|
||||
cmd.set_deck_id(node->getId());
|
||||
|
||||
PendingCommand *pend = client->prepareSessionCommand(cmd);
|
||||
connect(pend, SIGNAL(finished(Response, CommandContainer, QVariant)), this,
|
||||
SLOT(openRemoteDeckFinished(Response, CommandContainer)));
|
||||
client->sendCommand(pend);
|
||||
PendingCommand *pend = client->prepareSessionCommand(cmd);
|
||||
connect(pend, SIGNAL(finished(Response, CommandContainer, QVariant)), this,
|
||||
SLOT(openRemoteDeckFinished(Response, CommandContainer)));
|
||||
client->sendCommand(pend);
|
||||
}
|
||||
}
|
||||
|
||||
void TabDeckStorage::openRemoteDeckFinished(const Response &r, const CommandContainer &commandContainer)
|
||||
@@ -273,30 +376,46 @@ void TabDeckStorage::openRemoteDeckFinished(const Response &r, const CommandCont
|
||||
|
||||
void TabDeckStorage::actDownload()
|
||||
{
|
||||
QString filePath;
|
||||
QModelIndex curLeft = localDirView->selectionModel()->currentIndex();
|
||||
if (!curLeft.isValid())
|
||||
filePath = localDirModel->rootPath();
|
||||
else {
|
||||
while (!localDirModel->isDir(curLeft))
|
||||
curLeft = curLeft.parent();
|
||||
filePath = localDirModel->filePath(curLeft);
|
||||
while (!localDirModel->isDir(curLeft)) {
|
||||
curLeft = curLeft.parent();
|
||||
}
|
||||
|
||||
RemoteDeckList_TreeModel::FileNode *curRight =
|
||||
dynamic_cast<RemoteDeckList_TreeModel::FileNode *>(serverDirView->getCurrentItem());
|
||||
if (!curRight)
|
||||
return;
|
||||
filePath += QString("/deck_%1.cod").arg(curRight->getId());
|
||||
for (const auto curRight : serverDirView->selectionModel()->selectedRows()) {
|
||||
downloadNodeAtIndex(curLeft, curRight);
|
||||
}
|
||||
}
|
||||
|
||||
Command_DeckDownload cmd;
|
||||
cmd.set_deck_id(curRight->getId());
|
||||
void TabDeckStorage::downloadNodeAtIndex(const QModelIndex &curLeft, const QModelIndex &curRight)
|
||||
{
|
||||
auto node = serverDirView->getNode(curRight);
|
||||
if (const auto dirNode = dynamic_cast<RemoteDeckList_TreeModel::DirectoryNode *>(node)) {
|
||||
// node at index is a folder
|
||||
const QString name = dirNode->getName();
|
||||
|
||||
PendingCommand *pend = client->prepareSessionCommand(cmd);
|
||||
pend->setExtraData(filePath);
|
||||
connect(pend, SIGNAL(finished(Response, CommandContainer, QVariant)), this,
|
||||
SLOT(downloadFinished(Response, CommandContainer, QVariant)));
|
||||
client->sendCommand(pend);
|
||||
const auto dirIndex = curLeft.isValid() ? curLeft : localDirModel->index(localDirModel->rootPath());
|
||||
const auto newDirIndex = localDirModel->mkdir(dirIndex, name);
|
||||
|
||||
int rows = serverDirView->model()->rowCount(curRight);
|
||||
for (int i = 0; i < rows; i++) {
|
||||
const auto childIndex = serverDirView->model()->index(i, 0, curRight);
|
||||
downloadNodeAtIndex(newDirIndex, childIndex);
|
||||
}
|
||||
} else if (const auto fileNode = dynamic_cast<RemoteDeckList_TreeModel::FileNode *>(node)) {
|
||||
// node at index is a deck
|
||||
const QString dirPath = curLeft.isValid() ? localDirModel->filePath(curLeft) : localDirModel->rootPath();
|
||||
const QString filePath = dirPath + QString("/deck_%1.cod").arg(fileNode->getId());
|
||||
|
||||
Command_DeckDownload cmd;
|
||||
cmd.set_deck_id(fileNode->getId());
|
||||
|
||||
PendingCommand *pend = client->prepareSessionCommand(cmd);
|
||||
pend->setExtraData(filePath);
|
||||
connect(pend, SIGNAL(finished(Response, CommandContainer, QVariant)), this,
|
||||
SLOT(downloadFinished(Response, CommandContainer, QVariant)));
|
||||
client->sendCommand(pend);
|
||||
}
|
||||
// node at index is invalid
|
||||
}
|
||||
|
||||
void TabDeckStorage::downloadFinished(const Response &r,
|
||||
@@ -352,12 +471,30 @@ void TabDeckStorage::newFolderFinished(const Response &response, const CommandCo
|
||||
|
||||
void TabDeckStorage::actDeleteRemoteDeck()
|
||||
{
|
||||
PendingCommand *pend;
|
||||
RemoteDeckList_TreeModel::Node *curRight = serverDirView->getCurrentItem();
|
||||
if (!curRight)
|
||||
auto curRights = serverDirView->getCurrentSelection();
|
||||
|
||||
if (curRights.isEmpty()) {
|
||||
return;
|
||||
RemoteDeckList_TreeModel::DirectoryNode *dir = dynamic_cast<RemoteDeckList_TreeModel::DirectoryNode *>(curRight);
|
||||
if (dir) {
|
||||
}
|
||||
|
||||
if (QMessageBox::warning(this, tr("Delete remote decks"), tr("Are you sure you want to delete the selected decks?"),
|
||||
QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const auto &curRight : curRights) {
|
||||
deleteRemoteDeck(curRight);
|
||||
}
|
||||
}
|
||||
|
||||
void TabDeckStorage::deleteRemoteDeck(const RemoteDeckList_TreeModel::Node *curRight)
|
||||
{
|
||||
if (!curRight) {
|
||||
return;
|
||||
}
|
||||
|
||||
PendingCommand *pend;
|
||||
if (const auto *dir = dynamic_cast<const RemoteDeckList_TreeModel::DirectoryNode *>(curRight)) {
|
||||
QString targetPath = dir->getPath();
|
||||
if (targetPath.isEmpty())
|
||||
return;
|
||||
@@ -365,22 +502,13 @@ void TabDeckStorage::actDeleteRemoteDeck()
|
||||
qCritical() << "target path to delete is too long" << targetPath;
|
||||
return;
|
||||
}
|
||||
if (QMessageBox::warning(this, tr("Delete remote folder"),
|
||||
tr("Are you sure you want to delete \"%1\"?").arg(targetPath),
|
||||
QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes)
|
||||
return;
|
||||
Command_DeckDelDir cmd;
|
||||
cmd.set_path(targetPath.toStdString());
|
||||
pend = client->prepareSessionCommand(cmd);
|
||||
connect(pend, SIGNAL(finished(Response, CommandContainer, QVariant)), this,
|
||||
SLOT(deleteFolderFinished(Response, CommandContainer)));
|
||||
} else {
|
||||
RemoteDeckList_TreeModel::FileNode *deckNode = dynamic_cast<RemoteDeckList_TreeModel::FileNode *>(curRight);
|
||||
if (QMessageBox::warning(this, tr("Delete remote deck"),
|
||||
tr("Are you sure you want to delete \"%1\"?").arg(deckNode->getName()),
|
||||
QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes)
|
||||
return;
|
||||
|
||||
const auto *deckNode = dynamic_cast<const RemoteDeckList_TreeModel::FileNode *>(curRight);
|
||||
Command_DeckDel cmd;
|
||||
cmd.set_deck_id(deckNode->getId());
|
||||
pend = client->prepareSessionCommand(cmd);
|
||||
@@ -1,6 +1,7 @@
|
||||
#ifndef TAB_DECK_STORAGE_H
|
||||
#define TAB_DECK_STORAGE_H
|
||||
|
||||
#include "../../server/remote/remote_decklist_tree_widget.h"
|
||||
#include "tab.h"
|
||||
|
||||
class AbstractClient;
|
||||
@@ -10,7 +11,6 @@ class QToolBar;
|
||||
class QTreeWidget;
|
||||
class QTreeWidgetItem;
|
||||
class QGroupBox;
|
||||
class RemoteDeckList_TreeWidget;
|
||||
class CommandContainer;
|
||||
class Response;
|
||||
class DeckLoader;
|
||||
@@ -26,16 +26,29 @@ private:
|
||||
RemoteDeckList_TreeWidget *serverDirView;
|
||||
QGroupBox *leftGroupBox, *rightGroupBox;
|
||||
|
||||
QAction *aOpenLocalDeck, *aUpload, *aDeleteLocalDeck, *aOpenRemoteDeck, *aDownload, *aNewFolder, *aDeleteRemoteDeck;
|
||||
QAction *aOpenLocalDeck, *aUpload, *aNewLocalFolder, *aDeleteLocalDeck;
|
||||
QAction *aOpenDecksFolder;
|
||||
QAction *aOpenRemoteDeck, *aDownload, *aNewFolder, *aDeleteRemoteDeck;
|
||||
QString getTargetPath() const;
|
||||
|
||||
void uploadDeck(const QString &filePath, const QString &targetPath);
|
||||
void deleteRemoteDeck(const RemoteDeckList_TreeModel::Node *node);
|
||||
|
||||
void downloadNodeAtIndex(const QModelIndex &curLeft, const QModelIndex &curRight);
|
||||
|
||||
private slots:
|
||||
void actLocalDoubleClick(const QModelIndex &curLeft);
|
||||
void actOpenLocalDeck();
|
||||
|
||||
void actUpload();
|
||||
void uploadFinished(const Response &r, const CommandContainer &commandContainer);
|
||||
|
||||
void actNewLocalFolder();
|
||||
void actDeleteLocalDeck();
|
||||
|
||||
void actOpenDecksFolder();
|
||||
|
||||
void actRemoteDoubleClick(const QModelIndex &curRight);
|
||||
void actOpenRemoteDeck();
|
||||
void openRemoteDeckFinished(const Response &r, const CommandContainer &commandContainer);
|
||||
|
||||
@@ -1,21 +1,33 @@
|
||||
#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_deck.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 +57,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 <QAction>
|
||||
#include <QCompleter>
|
||||
@@ -103,13 +105,19 @@ DeckViewContainer::DeckViewContainer(int _playerId, TabGame *parent)
|
||||
{
|
||||
loadLocalButton = new QPushButton;
|
||||
loadRemoteButton = new QPushButton;
|
||||
unloadDeckButton = new QPushButton;
|
||||
unloadDeckButton->setEnabled(false);
|
||||
readyStartButton = new ToggleButton;
|
||||
readyStartButton->setEnabled(false);
|
||||
forceStartGameButton = new QPushButton;
|
||||
forceStartGameButton->setEnabled(parent->isHost());
|
||||
sideboardLockButton = new ToggleButton;
|
||||
sideboardLockButton->setEnabled(false);
|
||||
|
||||
connect(loadLocalButton, SIGNAL(clicked()), this, SLOT(loadLocalDeck()));
|
||||
connect(readyStartButton, SIGNAL(clicked()), this, SLOT(readyStart()));
|
||||
connect(unloadDeckButton, &QPushButton::clicked, this, &DeckViewContainer::unloadDeck);
|
||||
connect(forceStartGameButton, &QPushButton::clicked, this, &DeckViewContainer::forceStart);
|
||||
connect(sideboardLockButton, SIGNAL(clicked()), this, SLOT(sideboardLockButtonClicked()));
|
||||
connect(sideboardLockButton, SIGNAL(stateChanged()), this, SLOT(updateSideboardLockButtonText()));
|
||||
|
||||
@@ -122,17 +130,28 @@ DeckViewContainer::DeckViewContainer(int _playerId, TabGame *parent)
|
||||
auto *buttonHBox = new QHBoxLayout;
|
||||
buttonHBox->addWidget(loadLocalButton);
|
||||
buttonHBox->addWidget(loadRemoteButton);
|
||||
buttonHBox->addWidget(unloadDeckButton);
|
||||
buttonHBox->addWidget(readyStartButton);
|
||||
buttonHBox->addWidget(sideboardLockButton);
|
||||
if (forceStartGameButton->isEnabled()) {
|
||||
buttonHBox->addWidget(forceStartGameButton);
|
||||
}
|
||||
buttonHBox->setContentsMargins(0, 0, 0, 0);
|
||||
buttonHBox->addStretch();
|
||||
|
||||
deckView = new DeckView;
|
||||
connect(deckView, SIGNAL(newCardAdded(AbstractCardItem *)), this, SIGNAL(newCardAdded(AbstractCardItem *)));
|
||||
connect(deckView, SIGNAL(sideboardPlanChanged()), this, SLOT(sideboardPlanChanged()));
|
||||
deckView->setVisible(false);
|
||||
|
||||
auto *deckViewLayout = new QVBoxLayout;
|
||||
visualDeckStorageWidget = new VisualDeckStorageWidget(this);
|
||||
connect(visualDeckStorageWidget, &VisualDeckStorageWidget::imageDoubleClicked, this,
|
||||
&DeckViewContainer::replaceDeckStorageWithDeckView);
|
||||
|
||||
deckViewLayout = new QVBoxLayout;
|
||||
deckViewLayout->addLayout(buttonHBox);
|
||||
deckViewLayout->addWidget(deckView);
|
||||
deckViewLayout->addWidget(visualDeckStorageWidget);
|
||||
deckViewLayout->setContentsMargins(0, 0, 0, 0);
|
||||
setLayout(deckViewLayout);
|
||||
|
||||
@@ -145,7 +164,9 @@ void DeckViewContainer::retranslateUi()
|
||||
{
|
||||
loadLocalButton->setText(tr("Load deck..."));
|
||||
loadRemoteButton->setText(tr("Load remote deck..."));
|
||||
unloadDeckButton->setText(tr("Unload deck..."));
|
||||
readyStartButton->setText(tr("Ready to start"));
|
||||
forceStartGameButton->setText(tr("Force start"));
|
||||
updateSideboardLockButtonText();
|
||||
}
|
||||
|
||||
@@ -154,6 +175,7 @@ void DeckViewContainer::setButtonsVisible(bool _visible)
|
||||
loadLocalButton->setVisible(_visible);
|
||||
loadRemoteButton->setVisible(_visible);
|
||||
readyStartButton->setVisible(_visible);
|
||||
forceStartGameButton->setVisible(_visible);
|
||||
sideboardLockButton->setVisible(_visible);
|
||||
}
|
||||
|
||||
@@ -255,19 +277,32 @@ 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"));
|
||||
}
|
||||
}
|
||||
|
||||
void DeckViewContainer::loadLocalDeck()
|
||||
void DeckViewContainer::replaceDeckStorageWithDeckView(QMouseEvent *event, DeckPreviewCardPictureWidget *instance)
|
||||
{
|
||||
QFileDialog dialog(this, tr("Load deck"));
|
||||
dialog.setDirectory(SettingsCache::instance().getDeckPath());
|
||||
dialog.setNameFilters(DeckLoader::fileNameFilters);
|
||||
if (!dialog.exec())
|
||||
return;
|
||||
|
||||
QString fileName = dialog.selectedFiles().at(0);
|
||||
Q_UNUSED(event);
|
||||
QString fileName = instance->filePath;
|
||||
DeckLoader::FileFormat fmt = DeckLoader::getFormatFromName(fileName);
|
||||
QString deckString;
|
||||
DeckLoader deck;
|
||||
@@ -282,6 +317,51 @@ void DeckViewContainer::loadLocalDeck()
|
||||
return;
|
||||
}
|
||||
|
||||
Command_DeckSelect cmd;
|
||||
cmd.set_deck(deckString.toStdString());
|
||||
PendingCommand *pend = parentGame->prepareGameCommand(cmd);
|
||||
connect(pend, SIGNAL(finished(Response, CommandContainer, QVariant)), this,
|
||||
SLOT(deckSelectFinished(const Response &)));
|
||||
parentGame->sendGameCommand(pend, playerId);
|
||||
visualDeckStorageWidget->setVisible(false);
|
||||
deckView->setVisible(true);
|
||||
deckViewLayout->update();
|
||||
unloadDeckButton->setEnabled(true);
|
||||
}
|
||||
|
||||
void DeckViewContainer::unloadDeck()
|
||||
{
|
||||
deckView->setVisible(false);
|
||||
visualDeckStorageWidget->setVisible(true);
|
||||
deckViewLayout->update();
|
||||
unloadDeckButton->setEnabled(false);
|
||||
}
|
||||
|
||||
void DeckViewContainer::loadLocalDeck()
|
||||
{
|
||||
DlgLoadDeck dialog(this);
|
||||
if (!dialog.exec())
|
||||
return;
|
||||
|
||||
loadDeckFromFile(dialog.selectedFiles().at(0));
|
||||
}
|
||||
|
||||
void DeckViewContainer::loadDeckFromFile(const QString &filePath)
|
||||
{
|
||||
DeckLoader::FileFormat fmt = DeckLoader::getFormatFromName(filePath);
|
||||
QString deckString;
|
||||
DeckLoader deck;
|
||||
|
||||
bool error = !deck.loadFromFile(filePath, fmt);
|
||||
if (!error) {
|
||||
deckString = deck.writeToString_Native();
|
||||
error = deckString.length() > MAX_FILE_LENGTH;
|
||||
}
|
||||
if (error) {
|
||||
QMessageBox::critical(this, tr("Error"), tr("The selected file could not be loaded."));
|
||||
return;
|
||||
}
|
||||
|
||||
Command_DeckSelect cmd;
|
||||
cmd.set_deck(deckString.toStdString());
|
||||
PendingCommand *pend = parentGame->prepareGameCommand(cmd);
|
||||
@@ -307,7 +387,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);
|
||||
}
|
||||
|
||||
@@ -318,6 +399,14 @@ void DeckViewContainer::readyStart()
|
||||
parentGame->sendGameCommand(cmd, playerId);
|
||||
}
|
||||
|
||||
void DeckViewContainer::forceStart()
|
||||
{
|
||||
Command_ReadyStart cmd;
|
||||
cmd.set_force_start(true);
|
||||
cmd.set_ready(true);
|
||||
parentGame->sendGameCommand(cmd, playerId);
|
||||
}
|
||||
|
||||
void DeckViewContainer::sideboardLockButtonClicked()
|
||||
{
|
||||
Command_SetSideboardLock cmd;
|
||||
@@ -342,6 +431,18 @@ void DeckViewContainer::setReadyStart(bool ready)
|
||||
sideboardLockButton->setEnabled(!readyStartButton->getState() && readyStartButton->isEnabled());
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the ready start to true, then sends the ready command so the server responds to the update
|
||||
*/
|
||||
void DeckViewContainer::readyAndUpdate()
|
||||
{
|
||||
setReadyStart(true);
|
||||
|
||||
Command_ReadyStart cmd;
|
||||
cmd.set_ready(true);
|
||||
parentGame->sendGameCommand(cmd, playerId);
|
||||
}
|
||||
|
||||
void DeckViewContainer::setSideboardLocked(bool locked)
|
||||
{
|
||||
sideboardLockButton->setState(!locked);
|
||||
@@ -422,7 +523,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);
|
||||
@@ -498,6 +601,12 @@ void TabGame::updatePlayerListDockTitle()
|
||||
(playerListDock->isWindow() ? tabText : QString()));
|
||||
}
|
||||
|
||||
bool TabGame::isMainPlayerConceded() const
|
||||
{
|
||||
Player *player = players.value(localPlayerId, nullptr);
|
||||
return player && player->getConceded();
|
||||
}
|
||||
|
||||
void TabGame::retranslateUi()
|
||||
{
|
||||
QString tabText = " | " + (replay ? tr("Replay") : tr("Game")) + " #" + QString::number(gameInfo.game_id());
|
||||
@@ -539,7 +648,11 @@ void TabGame::retranslateUi()
|
||||
if (aGameInfo)
|
||||
aGameInfo->setText(tr("Game &information"));
|
||||
if (aConcede) {
|
||||
aConcede->setText(tr("&Concede"));
|
||||
if (isMainPlayerConceded()) {
|
||||
aConcede->setText(tr("Un&concede"));
|
||||
} else {
|
||||
aConcede->setText(tr("&Concede"));
|
||||
}
|
||||
}
|
||||
if (aLeaveGame) {
|
||||
aLeaveGame->setText(tr("&Leave game"));
|
||||
@@ -576,7 +689,7 @@ void TabGame::retranslateUi()
|
||||
|
||||
aResetLayout->setText(tr("Reset layout"));
|
||||
|
||||
cardInfo->retranslateUi();
|
||||
cardInfoFrameWidget->retranslateUi();
|
||||
|
||||
QMapIterator<int, Player *> i(players);
|
||||
while (i.hasNext())
|
||||
@@ -593,39 +706,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()
|
||||
@@ -700,6 +814,12 @@ void TabGame::actSay()
|
||||
if (completer->popup()->isVisible())
|
||||
return;
|
||||
|
||||
if (sayEdit->text().startsWith("/card ")) {
|
||||
cardInfoFrameWidget->setCard(sayEdit->text().mid(6));
|
||||
sayEdit->clear();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!sayEdit->text().isEmpty()) {
|
||||
Command_GameSay cmd;
|
||||
cmd.set_message(sayEdit->text().toStdString());
|
||||
@@ -811,6 +931,15 @@ Player *TabGame::addPlayer(int playerId, const ServerInfo_User &info)
|
||||
connect(deckView, SIGNAL(newCardAdded(AbstractCardItem *)), this, SLOT(newCardAdded(AbstractCardItem *)));
|
||||
deckViewContainers.insert(playerId, deckView);
|
||||
deckViewContainerLayout->addWidget(deckView);
|
||||
|
||||
// auto load deck for player if that debug setting is enabled
|
||||
QString deckPath = SettingsCache::instance().debug().getDeckPathForPlayer(newPlayer->getName());
|
||||
if (!deckPath.isEmpty()) {
|
||||
QTimer::singleShot(0, this, [deckView, deckPath] {
|
||||
deckView->loadDeckFromFile(deckPath);
|
||||
deckView->readyAndUpdate();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
gameMenu->insertMenu(playersSeparator, newPlayer->getPlayerMenu());
|
||||
@@ -835,11 +964,16 @@ Player *TabGame::addPlayer(int playerId, const ServerInfo_User &info)
|
||||
}
|
||||
}
|
||||
|
||||
// update menu text when player concedes so that "concede" gets updated to "unconcede"
|
||||
connect(newPlayer, &Player::playerCountChanged, this, &TabGame::retranslateUi);
|
||||
|
||||
emit playerAdded(newPlayer);
|
||||
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 +1048,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 +1221,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,8 +1489,8 @@ void TabGame::eventSetActivePhase(const Event_SetActivePhase &event,
|
||||
|
||||
void TabGame::newCardAdded(AbstractCardItem *card)
|
||||
{
|
||||
connect(card, SIGNAL(hovered(AbstractCardItem *)), cardInfo, SLOT(setCard(AbstractCardItem *)));
|
||||
connect(card, SIGNAL(showCardInfoPopup(QPoint, QString)), this, SLOT(showCardInfoPopup(QPoint, QString)));
|
||||
connect(card, SIGNAL(hovered(AbstractCardItem *)), cardInfoFrameWidget, SLOT(setCard(AbstractCardItem *)));
|
||||
connect(card, &AbstractCardItem::showCardInfoPopup, this, &TabGame::showCardInfoPopup);
|
||||
connect(card, SIGNAL(deleteCardInfoPopup(QString)), this, SLOT(deleteCardInfoPopup(QString)));
|
||||
connect(card, SIGNAL(cardShiftClicked(QString)), this, SLOT(linkCardToChat(QString)));
|
||||
}
|
||||
@@ -1704,32 +1839,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,
|
||||
[this] { timelineWidget->skipByAmount(ReplayTimelineWidget::SMALL_SKIP_MS); });
|
||||
|
||||
aReplaySkipBackward = new QAction(timelineWidget);
|
||||
timelineWidget->addAction(aReplaySkipBackward);
|
||||
connect(aReplaySkipBackward, &QAction::triggered, this,
|
||||
[this] { timelineWidget->skipByAmount(-ReplayTimelineWidget::SMALL_SKIP_MS); });
|
||||
|
||||
aReplaySkipForwardBig = new QAction(timelineWidget);
|
||||
timelineWidget->addAction(aReplaySkipForwardBig);
|
||||
connect(aReplaySkipForwardBig, &QAction::triggered, this,
|
||||
[this] { timelineWidget->skipByAmount(ReplayTimelineWidget::BIG_SKIP_MS); });
|
||||
|
||||
aReplaySkipBackwardBig = new QAction(timelineWidget);
|
||||
timelineWidget->addAction(aReplaySkipBackwardBig);
|
||||
connect(aReplaySkipBackwardBig, &QAction::triggered, this,
|
||||
[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();
|
||||
@@ -1758,20 +1916,20 @@ void TabGame::createDeckViewContainerWidget(bool bReplay)
|
||||
deckViewContainerWidget->setLayout(deckViewContainerLayout);
|
||||
}
|
||||
|
||||
void TabGame::viewCardInfo(const QString &cardName)
|
||||
void TabGame::viewCardInfo(const QString &cardName, const QString &providerId) const
|
||||
{
|
||||
cardInfo->setCard(cardName);
|
||||
cardInfoFrameWidget->setCard(cardName, providerId);
|
||||
}
|
||||
|
||||
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,8 +1971,8 @@ 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(showCardInfoPopup(QPoint, QString)), this, SLOT(showCardInfoPopup(QPoint, QString)));
|
||||
connect(messageLog, SIGNAL(cardNameHovered(QString)), cardInfoFrameWidget, SLOT(setCard(QString)));
|
||||
connect(messageLog, &MessageLogWidget::showCardInfoPopup, this, &TabGame::showCardInfoPopup);
|
||||
connect(messageLog, SIGNAL(deleteCardInfoPopup(QString)), this, SLOT(deleteCardInfoPopup(QString)));
|
||||
|
||||
if (!bReplay) {
|
||||
@@ -1,10 +1,12 @@
|
||||
#ifndef TAB_GAME_H
|
||||
#define TAB_GAME_H
|
||||
|
||||
#include "../../client/tearoff_menu.h"
|
||||
#include "../../game/player/player.h"
|
||||
#include "../ui/widgets/visual_deck_storage/visual_deck_storage_widget.h"
|
||||
#include "pb/event_leave.pb.h"
|
||||
#include "pb/serverinfo_game.pb.h"
|
||||
#include "tab.h"
|
||||
#include "tearoffmenu.h"
|
||||
|
||||
#include <QCompleter>
|
||||
#include <QMap>
|
||||
@@ -15,7 +17,7 @@ class CardDatabase;
|
||||
class GameView;
|
||||
class DeckView;
|
||||
class GameScene;
|
||||
class CardFrame;
|
||||
class CardInfoFrameWidget;
|
||||
class MessageLogWidget;
|
||||
class QTimer;
|
||||
class QSplitter;
|
||||
@@ -47,7 +49,6 @@ class Event_Ping;
|
||||
class Event_GameSay;
|
||||
class Event_Kicked;
|
||||
class Event_ReverseTurn;
|
||||
class Player;
|
||||
class CardZone;
|
||||
class AbstractCardItem;
|
||||
class CardItem;
|
||||
@@ -86,15 +87,20 @@ class DeckViewContainer : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
private:
|
||||
QPushButton *loadLocalButton, *loadRemoteButton;
|
||||
QVBoxLayout *deckViewLayout;
|
||||
QPushButton *loadLocalButton, *loadRemoteButton, *unloadDeckButton, *forceStartGameButton;
|
||||
ToggleButton *readyStartButton, *sideboardLockButton;
|
||||
DeckView *deckView;
|
||||
VisualDeckStorageWidget *visualDeckStorageWidget;
|
||||
TabGame *parentGame;
|
||||
int playerId;
|
||||
private slots:
|
||||
void replaceDeckStorageWithDeckView(QMouseEvent *event, DeckPreviewCardPictureWidget *instance);
|
||||
void loadLocalDeck();
|
||||
void loadRemoteDeck();
|
||||
void unloadDeck();
|
||||
void readyStart();
|
||||
void forceStart();
|
||||
void deckSelectFinished(const Response &r);
|
||||
void sideboardPlanChanged();
|
||||
void sideboardLockButtonClicked();
|
||||
@@ -109,8 +115,10 @@ public:
|
||||
void retranslateUi();
|
||||
void setButtonsVisible(bool _visible);
|
||||
void setReadyStart(bool ready);
|
||||
void readyAndUpdate();
|
||||
void setSideboardLocked(bool locked);
|
||||
void setDeck(const DeckLoader &deck);
|
||||
void loadDeckFromFile(const QString &filePath);
|
||||
};
|
||||
|
||||
class TabGame : public Tab
|
||||
@@ -146,9 +154,10 @@ private:
|
||||
int currentReplayStep;
|
||||
QList<int> 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;
|
||||
@@ -175,6 +184,8 @@ private:
|
||||
|
||||
Player *addPlayer(int playerId, const ServerInfo_User &info);
|
||||
|
||||
bool isMainPlayerConceded() const;
|
||||
|
||||
void startGame(bool resuming);
|
||||
void stopGame();
|
||||
void closeGame();
|
||||
@@ -218,11 +229,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,13 +315,15 @@ 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<const ::google::protobuf::Message *> &cmdList);
|
||||
public slots:
|
||||
void sendGameCommand(PendingCommand *pend, int playerId = -1);
|
||||
void sendGameCommand(const ::google::protobuf::Message &command, int playerId = -1);
|
||||
void viewCardInfo(const QString &cardName);
|
||||
void viewCardInfo(const QString &cardName, const QString &providerId = "") const;
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -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 <QCheckBox>
|
||||
@@ -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 <QApplication>
|
||||
@@ -26,7 +26,7 @@ TabMessage::TabMessage(TabSupervisor *_tabSupervisor,
|
||||
otherUserInfo(new ServerInfo_User(_otherUserInfo)), userOnline(true)
|
||||
{
|
||||
chatView = new ChatView(tabSupervisor, tabSupervisor, 0, true);
|
||||
connect(chatView, SIGNAL(showCardInfoPopup(QPoint, QString)), this, SLOT(showCardInfoPopup(QPoint, QString)));
|
||||
connect(chatView, &ChatView::showCardInfoPopup, this, &TabMessage::showCardInfoPopup);
|
||||
connect(chatView, SIGNAL(deleteCardInfoPopup(QString)), this, SLOT(deleteCardInfoPopup(QString)));
|
||||
connect(chatView, SIGNAL(addMentionTag(QString)), this, SLOT(addMentionTag(QString)));
|
||||
sayEdit = new LineEditUnfocusable;
|
||||
449
cockatrice/src/client/tabs/tab_replays.cpp
Normal file
449
cockatrice/src/client/tabs/tab_replays.cpp
Normal file
@@ -0,0 +1,449 @@
|
||||
#include "tab_replays.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"
|
||||
#include "pb/event_replay_added.pb.h"
|
||||
#include "pb/game_replay.pb.h"
|
||||
#include "pb/response.pb.h"
|
||||
#include "pb/response_replay_download.pb.h"
|
||||
#include "tab_game.h"
|
||||
|
||||
#include <QAction>
|
||||
#include <QApplication>
|
||||
#include <QDesktopServices>
|
||||
#include <QFileSystemModel>
|
||||
#include <QGroupBox>
|
||||
#include <QHBoxLayout>
|
||||
#include <QHeaderView>
|
||||
#include <QInputDialog>
|
||||
#include <QMessageBox>
|
||||
#include <QToolBar>
|
||||
#include <QTreeView>
|
||||
#include <QUrl>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
TabReplays::TabReplays(TabSupervisor *_tabSupervisor, AbstractClient *_client) : Tab(_tabSupervisor), client(_client)
|
||||
{
|
||||
localDirModel = new QFileSystemModel(this);
|
||||
localDirModel->setRootPath(SettingsCache::instance().getReplaysPath());
|
||||
localDirModel->sort(0, Qt::AscendingOrder);
|
||||
|
||||
localDirView = new QTreeView;
|
||||
localDirView->setModel(localDirModel);
|
||||
localDirView->setColumnHidden(1, true);
|
||||
localDirView->setRootIndex(localDirModel->index(localDirModel->rootPath(), 0));
|
||||
localDirView->setSortingEnabled(true);
|
||||
localDirView->setSelectionMode(QAbstractItemView::ExtendedSelection);
|
||||
localDirView->header()->setSectionResizeMode(QHeaderView::ResizeToContents);
|
||||
localDirView->header()->setSortIndicator(0, Qt::AscendingOrder);
|
||||
|
||||
// Left side layout
|
||||
/* put an invisible dummy QToolBar in the leftmost column so that the main toolbar is centered.
|
||||
* Really ugly workaround, but I couldn't figure out the proper way to make it centered */
|
||||
QToolBar *dummyToolBar = new QToolBar(this);
|
||||
QSizePolicy sizePolicy = dummyToolBar->sizePolicy();
|
||||
sizePolicy.setRetainSizeWhenHidden(true);
|
||||
dummyToolBar->setSizePolicy(sizePolicy);
|
||||
dummyToolBar->setVisible(false);
|
||||
|
||||
leftToolBar = new QToolBar(this);
|
||||
leftToolBar->setOrientation(Qt::Horizontal);
|
||||
leftToolBar->setIconSize(QSize(32, 32));
|
||||
|
||||
QToolBar *leftRightmostToolBar = new QToolBar(this);
|
||||
leftRightmostToolBar->setOrientation(Qt::Horizontal);
|
||||
leftRightmostToolBar->setIconSize(QSize(32, 32));
|
||||
|
||||
QGridLayout *leftToolBarLayout = new QGridLayout;
|
||||
leftToolBarLayout->addWidget(dummyToolBar, 0, 0, Qt::AlignLeft);
|
||||
leftToolBarLayout->addWidget(leftToolBar, 0, 1, Qt::AlignHCenter);
|
||||
leftToolBarLayout->addWidget(leftRightmostToolBar, 0, 2, Qt::AlignRight);
|
||||
|
||||
QVBoxLayout *leftVbox = new QVBoxLayout;
|
||||
leftVbox->addWidget(localDirView);
|
||||
leftVbox->addLayout(leftToolBarLayout);
|
||||
leftGroupBox = new QGroupBox;
|
||||
leftGroupBox->setLayout(leftVbox);
|
||||
|
||||
// Right side layout
|
||||
rightToolBar = new QToolBar;
|
||||
rightToolBar->setOrientation(Qt::Horizontal);
|
||||
rightToolBar->setIconSize(QSize(32, 32));
|
||||
QHBoxLayout *rightToolBarLayout = new QHBoxLayout;
|
||||
rightToolBarLayout->addStretch();
|
||||
rightToolBarLayout->addWidget(rightToolBar);
|
||||
rightToolBarLayout->addStretch();
|
||||
|
||||
serverDirView = new RemoteReplayList_TreeWidget(client);
|
||||
|
||||
QVBoxLayout *rightVbox = new QVBoxLayout;
|
||||
rightVbox->addWidget(serverDirView);
|
||||
rightVbox->addLayout(rightToolBarLayout);
|
||||
rightGroupBox = new QGroupBox;
|
||||
rightGroupBox->setLayout(rightVbox);
|
||||
|
||||
// combine layouts
|
||||
QHBoxLayout *hbox = new QHBoxLayout;
|
||||
hbox->addWidget(leftGroupBox);
|
||||
hbox->addWidget(rightGroupBox);
|
||||
|
||||
// Left side actions
|
||||
aOpenLocalReplay = new QAction(this);
|
||||
aOpenLocalReplay->setIcon(QPixmap("theme:icons/view"));
|
||||
connect(aOpenLocalReplay, SIGNAL(triggered()), this, SLOT(actOpenLocalReplay()));
|
||||
connect(localDirView, SIGNAL(doubleClicked(const QModelIndex &)), this, SLOT(actOpenLocalReplay()));
|
||||
aRenameLocal = new QAction(this);
|
||||
aRenameLocal->setIcon(QPixmap("theme:icons/pencil"));
|
||||
connect(aRenameLocal, &QAction::triggered, this, &TabReplays::actRenameLocal);
|
||||
aNewLocalFolder = new QAction(this);
|
||||
aNewLocalFolder->setIcon(qApp->style()->standardIcon(QStyle::SP_FileDialogNewFolder));
|
||||
connect(aNewLocalFolder, &QAction::triggered, this, &TabReplays::actNewLocalFolder);
|
||||
aDeleteLocalReplay = new QAction(this);
|
||||
aDeleteLocalReplay->setIcon(QPixmap("theme:icons/remove_row"));
|
||||
connect(aDeleteLocalReplay, SIGNAL(triggered()), this, SLOT(actDeleteLocalReplay()));
|
||||
|
||||
aOpenReplaysFolder = new QAction(this);
|
||||
aOpenReplaysFolder->setIcon(qApp->style()->standardIcon(QStyle::SP_DirOpenIcon));
|
||||
connect(aOpenReplaysFolder, &QAction::triggered, this, &TabReplays::actOpenReplaysFolder);
|
||||
|
||||
// Right side actions
|
||||
aOpenRemoteReplay = new QAction(this);
|
||||
aOpenRemoteReplay->setIcon(QPixmap("theme:icons/view"));
|
||||
connect(aOpenRemoteReplay, SIGNAL(triggered()), this, SLOT(actOpenRemoteReplay()));
|
||||
connect(serverDirView, SIGNAL(doubleClicked(const QModelIndex &)), this, SLOT(actOpenRemoteReplay()));
|
||||
aDownload = new QAction(this);
|
||||
aDownload->setIcon(QPixmap("theme:icons/arrow_left_green"));
|
||||
connect(aDownload, SIGNAL(triggered()), this, SLOT(actDownload()));
|
||||
aKeep = new QAction(this);
|
||||
aKeep->setIcon(QPixmap("theme:icons/lock"));
|
||||
connect(aKeep, SIGNAL(triggered()), this, SLOT(actKeepRemoteReplay()));
|
||||
aDeleteRemoteReplay = new QAction(this);
|
||||
aDeleteRemoteReplay->setIcon(QPixmap("theme:icons/remove_row"));
|
||||
connect(aDeleteRemoteReplay, SIGNAL(triggered()), this, SLOT(actDeleteRemoteReplay()));
|
||||
|
||||
// Add actions to toolbars
|
||||
leftToolBar->addAction(aOpenLocalReplay);
|
||||
leftToolBar->addAction(aRenameLocal);
|
||||
leftToolBar->addAction(aNewLocalFolder);
|
||||
leftToolBar->addAction(aDeleteLocalReplay);
|
||||
|
||||
leftRightmostToolBar->addAction(aOpenReplaysFolder);
|
||||
|
||||
rightToolBar->addAction(aOpenRemoteReplay);
|
||||
rightToolBar->addAction(aDownload);
|
||||
rightToolBar->addAction(aKeep);
|
||||
rightToolBar->addAction(aDeleteRemoteReplay);
|
||||
|
||||
retranslateUi();
|
||||
|
||||
QWidget *mainWidget = new QWidget(this);
|
||||
mainWidget->setLayout(hbox);
|
||||
setCentralWidget(mainWidget);
|
||||
|
||||
connect(client, SIGNAL(replayAddedEventReceived(const Event_ReplayAdded &)), this,
|
||||
SLOT(replayAddedEventReceived(const Event_ReplayAdded &)));
|
||||
}
|
||||
|
||||
void TabReplays::retranslateUi()
|
||||
{
|
||||
leftGroupBox->setTitle(tr("Local file system"));
|
||||
rightGroupBox->setTitle(tr("Server replay storage"));
|
||||
|
||||
aOpenLocalReplay->setText(tr("Watch replay"));
|
||||
aRenameLocal->setText(tr("Rename"));
|
||||
aNewLocalFolder->setText(tr("New folder"));
|
||||
aDeleteLocalReplay->setText(tr("Delete"));
|
||||
aOpenReplaysFolder->setText(tr("Open replays folder"));
|
||||
aOpenRemoteReplay->setText(tr("Watch replay"));
|
||||
aDownload->setText(tr("Download replay"));
|
||||
aKeep->setText(tr("Toggle expiration lock"));
|
||||
aDeleteRemoteReplay->setText(tr("Delete"));
|
||||
}
|
||||
|
||||
void TabReplays::actLocalDoubleClick(const QModelIndex &curLeft)
|
||||
{
|
||||
if (!localDirModel->isDir(curLeft)) {
|
||||
actOpenLocalReplay();
|
||||
}
|
||||
}
|
||||
|
||||
void TabReplays::actOpenLocalReplay()
|
||||
{
|
||||
QModelIndexList curLefts = localDirView->selectionModel()->selectedRows();
|
||||
for (const auto &curLeft : curLefts) {
|
||||
if (localDirModel->isDir(curLeft))
|
||||
continue;
|
||||
QString filePath = localDirModel->filePath(curLeft);
|
||||
|
||||
QFile f(filePath);
|
||||
if (!f.open(QIODevice::ReadOnly))
|
||||
continue;
|
||||
QByteArray _data = f.readAll();
|
||||
f.close();
|
||||
|
||||
GameReplay *replay = new GameReplay;
|
||||
replay->ParseFromArray(_data.data(), _data.size());
|
||||
|
||||
emit openReplay(replay);
|
||||
}
|
||||
}
|
||||
|
||||
void TabReplays::actRenameLocal()
|
||||
{
|
||||
QModelIndexList curLefts = localDirView->selectionModel()->selectedRows();
|
||||
for (const auto &curLeft : curLefts) {
|
||||
const QFileInfo info = localDirModel->fileInfo(curLeft);
|
||||
|
||||
const QString oldName = info.baseName();
|
||||
const QString title = info.isDir() ? tr("Rename local folder") : tr("Rename local file");
|
||||
|
||||
bool ok;
|
||||
QString newName = QInputDialog::getText(this, title, tr("New name:"), QLineEdit::Normal, oldName, &ok);
|
||||
if (!ok) { // terminate all remaining selections if user cancels
|
||||
return;
|
||||
}
|
||||
if (newName.isEmpty() || oldName == newName) {
|
||||
continue;
|
||||
}
|
||||
|
||||
QString newFileName = newName;
|
||||
if (!info.suffix().isEmpty()) {
|
||||
newFileName += "." + info.suffix();
|
||||
}
|
||||
const QString newFilePath = QFileInfo(info.dir(), newFileName).filePath();
|
||||
|
||||
if (!QFile::rename(info.filePath(), newFilePath)) {
|
||||
QMessageBox::critical(this, tr("Error"), tr("Rename failed"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void TabReplays::actNewLocalFolder()
|
||||
{
|
||||
QModelIndex curLeft = localDirView->selectionModel()->currentIndex();
|
||||
|
||||
QModelIndex dirIndex;
|
||||
if (curLeft.isValid() && !localDirModel->isDir(curLeft)) {
|
||||
dirIndex = curLeft.parent();
|
||||
} else {
|
||||
dirIndex = curLeft;
|
||||
}
|
||||
|
||||
bool ok;
|
||||
QString folderName =
|
||||
QInputDialog::getText(this, tr("New folder"), tr("Name of new folder:"), QLineEdit::Normal, "", &ok);
|
||||
if (!ok || folderName.isEmpty())
|
||||
return;
|
||||
|
||||
localDirModel->mkdir(dirIndex, folderName);
|
||||
}
|
||||
|
||||
void TabReplays::actDeleteLocalReplay()
|
||||
{
|
||||
QModelIndexList curLefts = localDirView->selectionModel()->selectedRows();
|
||||
|
||||
if (curLefts.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (QMessageBox::warning(this, tr("Delete local file"), tr("Are you sure you want to delete the selected files?"),
|
||||
QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const auto &curLeft : curLefts) {
|
||||
if (curLeft.isValid()) {
|
||||
localDirModel->remove(curLeft);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void TabReplays::actOpenReplaysFolder()
|
||||
{
|
||||
QString dir = localDirModel->rootPath();
|
||||
QDesktopServices::openUrl(QUrl::fromLocalFile(dir));
|
||||
}
|
||||
|
||||
void TabReplays::actRemoteDoubleClick(const QModelIndex &curRight)
|
||||
{
|
||||
if (serverDirView->getReplay(curRight)) {
|
||||
actOpenRemoteReplay();
|
||||
}
|
||||
}
|
||||
|
||||
void TabReplays::actOpenRemoteReplay()
|
||||
{
|
||||
auto const curRights = serverDirView->getSelectedReplays();
|
||||
|
||||
for (const auto curRight : curRights) {
|
||||
if (!curRight) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Command_ReplayDownload cmd;
|
||||
cmd.set_replay_id(curRight->replay_id());
|
||||
|
||||
PendingCommand *pend = client->prepareSessionCommand(cmd);
|
||||
connect(pend, SIGNAL(finished(Response, CommandContainer, QVariant)), this,
|
||||
SLOT(openRemoteReplayFinished(const Response &)));
|
||||
client->sendCommand(pend);
|
||||
}
|
||||
}
|
||||
|
||||
void TabReplays::openRemoteReplayFinished(const Response &r)
|
||||
{
|
||||
if (r.response_code() != Response::RespOk)
|
||||
return;
|
||||
|
||||
const Response_ReplayDownload &resp = r.GetExtension(Response_ReplayDownload::ext);
|
||||
GameReplay *replay = new GameReplay;
|
||||
replay->ParseFromString(resp.replay_data());
|
||||
|
||||
emit openReplay(replay);
|
||||
}
|
||||
|
||||
void TabReplays::actDownload()
|
||||
{
|
||||
QModelIndex curLeft = localDirView->selectionModel()->currentIndex();
|
||||
while (!localDirModel->isDir(curLeft)) {
|
||||
curLeft = curLeft.parent();
|
||||
}
|
||||
|
||||
for (const auto curRight : serverDirView->selectionModel()->selectedRows()) {
|
||||
downloadNodeAtIndex(curLeft, curRight);
|
||||
}
|
||||
}
|
||||
|
||||
void TabReplays::downloadNodeAtIndex(const QModelIndex &curLeft, const QModelIndex &curRight)
|
||||
{
|
||||
if (const auto replayMatch = serverDirView->getReplayMatch(curRight)) {
|
||||
// node at index is a folder
|
||||
const QString name =
|
||||
QString::number(replayMatch->game_id()) + "_" + QString::fromStdString(replayMatch->game_name());
|
||||
|
||||
const auto dirIndex = curLeft.isValid() ? curLeft : localDirModel->index(localDirModel->rootPath());
|
||||
const auto newDirIndex = localDirModel->mkdir(dirIndex, name);
|
||||
|
||||
int rows = serverDirView->model()->rowCount(curRight);
|
||||
for (int i = 0; i < rows; i++) {
|
||||
const auto childIndex = serverDirView->model()->index(i, 0, curRight);
|
||||
downloadNodeAtIndex(newDirIndex, childIndex);
|
||||
}
|
||||
} else if (const auto replay = serverDirView->getReplay(curRight)) {
|
||||
// node at index is a replay
|
||||
const QString dirPath = curLeft.isValid() ? localDirModel->filePath(curLeft) : localDirModel->rootPath();
|
||||
const QString filePath = dirPath + QString("/replay_%1.cor").arg(replay->replay_id());
|
||||
|
||||
Command_ReplayDownload cmd;
|
||||
cmd.set_replay_id(replay->replay_id());
|
||||
|
||||
PendingCommand *pend = client->prepareSessionCommand(cmd);
|
||||
pend->setExtraData(filePath);
|
||||
connect(pend, SIGNAL(finished(Response, CommandContainer, QVariant)), this,
|
||||
SLOT(downloadFinished(Response, CommandContainer, QVariant)));
|
||||
client->sendCommand(pend);
|
||||
}
|
||||
// node at index was invalid
|
||||
}
|
||||
|
||||
void TabReplays::downloadFinished(const Response &r,
|
||||
const CommandContainer & /* commandContainer */,
|
||||
const QVariant &extraData)
|
||||
{
|
||||
if (r.response_code() != Response::RespOk)
|
||||
return;
|
||||
|
||||
const Response_ReplayDownload &resp = r.GetExtension(Response_ReplayDownload::ext);
|
||||
QString filePath = extraData.toString();
|
||||
|
||||
const std::string &_data = resp.replay_data();
|
||||
QFile f(filePath);
|
||||
f.open(QIODevice::WriteOnly);
|
||||
f.write((const char *)_data.data(), _data.size());
|
||||
f.close();
|
||||
}
|
||||
|
||||
void TabReplays::actKeepRemoteReplay()
|
||||
{
|
||||
const auto curRights = serverDirView->getSelectedReplayMatches();
|
||||
|
||||
if (curRights.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const auto curRight : curRights) {
|
||||
Command_ReplayModifyMatch cmd;
|
||||
cmd.set_game_id(curRight->game_id());
|
||||
cmd.set_do_not_hide(!curRight->do_not_hide());
|
||||
|
||||
PendingCommand *pend = client->prepareSessionCommand(cmd);
|
||||
connect(pend, SIGNAL(finished(Response, CommandContainer, QVariant)), this,
|
||||
SLOT(keepRemoteReplayFinished(Response, CommandContainer)));
|
||||
client->sendCommand(pend);
|
||||
}
|
||||
}
|
||||
|
||||
void TabReplays::keepRemoteReplayFinished(const Response &r, const CommandContainer &commandContainer)
|
||||
{
|
||||
if (r.response_code() != Response::RespOk)
|
||||
return;
|
||||
|
||||
const Command_ReplayModifyMatch &cmd =
|
||||
commandContainer.session_command(0).GetExtension(Command_ReplayModifyMatch::ext);
|
||||
|
||||
ServerInfo_ReplayMatch temp;
|
||||
temp.set_do_not_hide(cmd.do_not_hide());
|
||||
|
||||
serverDirView->updateMatchInfo(cmd.game_id(), temp);
|
||||
}
|
||||
|
||||
void TabReplays::actDeleteRemoteReplay()
|
||||
{
|
||||
const auto curRights = serverDirView->getSelectedReplayMatches();
|
||||
|
||||
if (curRights.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (QMessageBox::warning(this, tr("Delete remote replay"),
|
||||
tr("Are you sure you want to delete the selected replays?"),
|
||||
QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const auto curRight : curRights) {
|
||||
Command_ReplayDeleteMatch cmd;
|
||||
cmd.set_game_id(curRight->game_id());
|
||||
|
||||
PendingCommand *pend = client->prepareSessionCommand(cmd);
|
||||
connect(pend, SIGNAL(finished(Response, CommandContainer, QVariant)), this,
|
||||
SLOT(deleteRemoteReplayFinished(Response, CommandContainer)));
|
||||
client->sendCommand(pend);
|
||||
}
|
||||
}
|
||||
|
||||
void TabReplays::deleteRemoteReplayFinished(const Response &r, const CommandContainer &commandContainer)
|
||||
{
|
||||
if (r.response_code() != Response::RespOk)
|
||||
return;
|
||||
|
||||
const Command_ReplayDeleteMatch &cmd =
|
||||
commandContainer.session_command(0).GetExtension(Command_ReplayDeleteMatch::ext);
|
||||
serverDirView->removeMatchInfo(cmd.game_id());
|
||||
}
|
||||
|
||||
void TabReplays::replayAddedEventReceived(const Event_ReplayAdded &event)
|
||||
{
|
||||
if (event.has_match_info()) {
|
||||
// 99.9% of events will have match info (Normal Workflow)
|
||||
serverDirView->addMatchInfo(event.match_info());
|
||||
} else {
|
||||
// When a Moderator force adds a replay, we need to refresh their view
|
||||
serverDirView->refreshTree();
|
||||
}
|
||||
}
|
||||
@@ -25,12 +25,22 @@ private:
|
||||
RemoteReplayList_TreeWidget *serverDirView;
|
||||
QGroupBox *leftGroupBox, *rightGroupBox;
|
||||
|
||||
QAction *aOpenLocalReplay, *aDeleteLocalReplay, *aOpenRemoteReplay, *aDownload, *aKeep, *aDeleteRemoteReplay;
|
||||
private slots:
|
||||
void actOpenLocalReplay();
|
||||
QAction *aOpenLocalReplay, *aRenameLocal, *aNewLocalFolder, *aDeleteLocalReplay;
|
||||
QAction *aOpenReplaysFolder;
|
||||
QAction *aOpenRemoteReplay, *aDownload, *aKeep, *aDeleteRemoteReplay;
|
||||
|
||||
void downloadNodeAtIndex(const QModelIndex &curLeft, const QModelIndex &curRight);
|
||||
|
||||
private slots:
|
||||
void actLocalDoubleClick(const QModelIndex &curLeft);
|
||||
void actRenameLocal();
|
||||
void actOpenLocalReplay();
|
||||
void actNewLocalFolder();
|
||||
void actDeleteLocalReplay();
|
||||
|
||||
void actOpenReplaysFolder();
|
||||
|
||||
void actRemoteDoubleClick(const QModelIndex &curLeft);
|
||||
void actOpenRemoteReplay();
|
||||
void openRemoteReplayFinished(const Response &r);
|
||||
|
||||
@@ -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 <QApplication>
|
||||
#include <QCompleter>
|
||||
@@ -55,7 +55,7 @@ TabRoom::TabRoom(TabSupervisor *_tabSupervisor,
|
||||
connect(chatView, SIGNAL(showMentionPopup(const QString &)), this, SLOT(actShowMentionPopup(const QString &)));
|
||||
connect(chatView, SIGNAL(messageClickedSignal()), this, SLOT(focusTab()));
|
||||
connect(chatView, SIGNAL(openMessageDialog(QString, bool)), this, SIGNAL(openMessageDialog(QString, bool)));
|
||||
connect(chatView, SIGNAL(showCardInfoPopup(QPoint, QString)), this, SLOT(showCardInfoPopup(QPoint, QString)));
|
||||
connect(chatView, &ChatView::showCardInfoPopup, this, &TabRoom::showCardInfoPopup);
|
||||
connect(chatView, SIGNAL(deleteCardInfoPopup(QString)), this, SLOT(deleteCardInfoPopup(QString)));
|
||||
connect(chatView, SIGNAL(addMentionTag(QString)), this, SLOT(addMentionTag(QString)));
|
||||
connect(&SettingsCache::instance(), SIGNAL(chatMentionCompleterChanged()), this, SLOT(actCompleterChanged()));
|
||||
@@ -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 <QFocusEvent>
|
||||
@@ -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 <QCheckBox>
|
||||
#include <QDebug>
|
||||
@@ -1,18 +1,20 @@
|
||||
#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"
|
||||
#include "pb/game_event_container.pb.h"
|
||||
#include "pb/game_replay.pb.h"
|
||||
#include "pb/moderator_commands.pb.h"
|
||||
#include "pb/room_commands.pb.h"
|
||||
#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 +25,7 @@
|
||||
#include "tab_replays.h"
|
||||
#include "tab_room.h"
|
||||
#include "tab_server.h"
|
||||
#include "userlist.h"
|
||||
#include "visual_deck_storage/tab_deck_storage_visual.h"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QDebug>
|
||||
@@ -112,18 +114,14 @@ TabSupervisor::TabSupervisor(AbstractClient *_client, QWidget *parent)
|
||||
tabBar()->setStyle(new MacOSTabFixStyle);
|
||||
#endif
|
||||
|
||||
connect(this, SIGNAL(currentChanged(int)), this, SLOT(updateCurrent(int)));
|
||||
connect(this, &TabSupervisor::currentChanged, this, &TabSupervisor::updateCurrent);
|
||||
|
||||
connect(client, SIGNAL(roomEventReceived(const RoomEvent &)), this, SLOT(processRoomEvent(const RoomEvent &)));
|
||||
connect(client, SIGNAL(gameEventContainerReceived(const GameEventContainer &)), this,
|
||||
SLOT(processGameEventContainer(const GameEventContainer &)));
|
||||
connect(client, SIGNAL(gameJoinedEventReceived(const Event_GameJoined &)), this,
|
||||
SLOT(gameJoined(const Event_GameJoined &)));
|
||||
connect(client, SIGNAL(userMessageEventReceived(const Event_UserMessage &)), this,
|
||||
SLOT(processUserMessageEvent(const Event_UserMessage &)));
|
||||
connect(client, SIGNAL(maxPingTime(int, int)), this, SLOT(updatePingTime(int, int)));
|
||||
connect(client, SIGNAL(notifyUserEventReceived(const Event_NotifyUser &)), this,
|
||||
SLOT(processNotifyUserEvent(const Event_NotifyUser &)));
|
||||
connect(client, &AbstractClient::roomEventReceived, this, &TabSupervisor::processRoomEvent);
|
||||
connect(client, &AbstractClient::gameEventContainerReceived, this, &TabSupervisor::processGameEventContainer);
|
||||
connect(client, &AbstractClient::gameJoinedEventReceived, this, &TabSupervisor::gameJoined);
|
||||
connect(client, &AbstractClient::userMessageEventReceived, this, &TabSupervisor::processUserMessageEvent);
|
||||
connect(client, &AbstractClient::maxPingTime, this, &TabSupervisor::updatePingTime);
|
||||
connect(client, &AbstractClient::notifyUserEventReceived, this, &TabSupervisor::processNotifyUserEvent);
|
||||
|
||||
retranslateUi();
|
||||
}
|
||||
@@ -203,8 +201,8 @@ QString TabSupervisor::sanitizeHtml(QString dirty) const
|
||||
|
||||
int TabSupervisor::myAddTab(Tab *tab)
|
||||
{
|
||||
connect(tab, SIGNAL(userEvent(bool)), this, SLOT(tabUserEvent(bool)));
|
||||
connect(tab, SIGNAL(tabTextChanged(Tab *, QString)), this, SLOT(updateTabText(Tab *, QString)));
|
||||
connect(tab, &TabGame::userEvent, this, &TabSupervisor::tabUserEvent);
|
||||
connect(tab, &TabGame::tabTextChanged, this, &TabSupervisor::updateTabText);
|
||||
|
||||
QString tabText = tab->getTabText();
|
||||
int idx = addTab(tab, sanitizeTabName(tabText));
|
||||
@@ -219,27 +217,24 @@ void TabSupervisor::start(const ServerInfo_User &_userInfo)
|
||||
userInfo = new ServerInfo_User(_userInfo);
|
||||
|
||||
tabServer = new TabServer(this, client);
|
||||
connect(tabServer, SIGNAL(roomJoined(const ServerInfo_Room &, bool)), this,
|
||||
SLOT(addRoomTab(const ServerInfo_Room &, bool)));
|
||||
connect(tabServer, &TabServer::roomJoined, this, &TabSupervisor::addRoomTab);
|
||||
myAddTab(tabServer);
|
||||
|
||||
tabUserLists = new TabUserLists(this, client, *userInfo);
|
||||
connect(tabUserLists, SIGNAL(openMessageDialog(const QString &, bool)), this,
|
||||
SLOT(addMessageTab(const QString &, bool)));
|
||||
connect(tabUserLists, SIGNAL(userJoined(ServerInfo_User)), this, SLOT(processUserJoined(ServerInfo_User)));
|
||||
connect(tabUserLists, SIGNAL(userLeft(const QString &)), this, SLOT(processUserLeft(const QString &)));
|
||||
connect(tabUserLists, &TabUserLists::openMessageDialog, this, &TabSupervisor::addMessageTab);
|
||||
connect(tabUserLists, &TabUserLists::userJoined, this, &TabSupervisor::processUserJoined);
|
||||
connect(tabUserLists, &TabUserLists::userLeft, this, &TabSupervisor::processUserLeft);
|
||||
myAddTab(tabUserLists);
|
||||
|
||||
updatePingTime(0, -1);
|
||||
|
||||
if (userInfo->user_level() & ServerInfo_User::IsRegistered) {
|
||||
tabDeckStorage = new TabDeckStorage(this, client);
|
||||
connect(tabDeckStorage, SIGNAL(openDeckEditor(const DeckLoader *)), this,
|
||||
SLOT(addDeckEditorTab(const DeckLoader *)));
|
||||
connect(tabDeckStorage, &TabDeckStorage::openDeckEditor, this, &TabSupervisor::addDeckEditorTab);
|
||||
myAddTab(tabDeckStorage);
|
||||
|
||||
tabReplays = new TabReplays(this, client);
|
||||
connect(tabReplays, SIGNAL(openReplay(GameReplay *)), this, SLOT(openReplay(GameReplay *)));
|
||||
connect(tabReplays, &TabReplays::openReplay, this, &TabSupervisor::openReplay);
|
||||
myAddTab(tabReplays);
|
||||
} else {
|
||||
tabDeckStorage = 0;
|
||||
@@ -248,7 +243,7 @@ void TabSupervisor::start(const ServerInfo_User &_userInfo)
|
||||
|
||||
if (userInfo->user_level() & ServerInfo_User::IsModerator) {
|
||||
tabAdmin = new TabAdmin(this, client, (userInfo->user_level() & ServerInfo_User::IsAdmin));
|
||||
connect(tabAdmin, SIGNAL(adminLockChanged(bool)), this, SIGNAL(adminLockChanged(bool)));
|
||||
connect(tabAdmin, &TabAdmin::adminLockChanged, this, &TabSupervisor::adminLockChanged);
|
||||
myAddTab(tabAdmin);
|
||||
|
||||
tabLog = new TabLog(this, client);
|
||||
@@ -272,10 +267,9 @@ void TabSupervisor::startLocal(const QList<AbstractClient *> &_clients)
|
||||
userInfo = new ServerInfo_User;
|
||||
localClients = _clients;
|
||||
for (int i = 0; i < localClients.size(); ++i)
|
||||
connect(localClients[i], SIGNAL(gameEventContainerReceived(const GameEventContainer &)), this,
|
||||
SLOT(processGameEventContainer(const GameEventContainer &)));
|
||||
connect(localClients.first(), SIGNAL(gameJoinedEventReceived(const Event_GameJoined &)), this,
|
||||
SLOT(localGameJoined(const Event_GameJoined &)));
|
||||
connect(localClients[i], &AbstractClient::gameEventContainerReceived, this,
|
||||
&TabSupervisor::processGameEventContainer);
|
||||
connect(localClients.first(), &AbstractClient::gameJoinedEventReceived, this, &TabSupervisor::localGameJoined);
|
||||
}
|
||||
|
||||
void TabSupervisor::stop()
|
||||
@@ -310,19 +304,28 @@ void TabSupervisor::stop()
|
||||
tabAdmin = 0;
|
||||
tabLog = 0;
|
||||
|
||||
QMapIterator<int, TabRoom *> roomIterator(roomTabs);
|
||||
while (roomIterator.hasNext())
|
||||
roomIterator.next().value()->deleteLater();
|
||||
for (const auto tab : deckEditorTabs) {
|
||||
disconnect(tab, nullptr, this, nullptr);
|
||||
tab->deleteLater();
|
||||
}
|
||||
deckEditorTabs.clear();
|
||||
|
||||
for (auto i = roomTabs.cbegin(), end = roomTabs.cend(); i != end; ++i) {
|
||||
disconnect(i.value(), nullptr, this, nullptr);
|
||||
i.value()->deleteLater();
|
||||
}
|
||||
roomTabs.clear();
|
||||
|
||||
QMapIterator<int, TabGame *> gameIterator(gameTabs);
|
||||
while (gameIterator.hasNext())
|
||||
gameIterator.next().value()->deleteLater();
|
||||
for (auto i = gameTabs.cbegin(), end = gameTabs.cend(); i != end; ++i) {
|
||||
disconnect(i.value(), nullptr, this, nullptr);
|
||||
i.value()->deleteLater();
|
||||
}
|
||||
gameTabs.clear();
|
||||
|
||||
QListIterator<TabGame *> replayIterator(replayTabs);
|
||||
while (replayIterator.hasNext())
|
||||
replayIterator.next()->deleteLater();
|
||||
for (const auto tab : replayTabs) {
|
||||
disconnect(tab, nullptr, this, nullptr);
|
||||
tab->deleteLater();
|
||||
}
|
||||
replayTabs.clear();
|
||||
|
||||
delete userInfo;
|
||||
@@ -350,7 +353,7 @@ void TabSupervisor::addCloseButtonToTab(Tab *tab, int tabIndex)
|
||||
QTabBar::ButtonPosition closeSide =
|
||||
(QTabBar::ButtonPosition)tabBar()->style()->styleHint(QStyle::SH_TabBar_CloseButtonPosition, 0, tabBar());
|
||||
CloseButton *closeButton = new CloseButton;
|
||||
connect(closeButton, SIGNAL(clicked()), this, SLOT(closeButtonPressed()));
|
||||
connect(closeButton, &CloseButton::clicked, this, &TabSupervisor::closeButtonPressed);
|
||||
closeButton->setProperty("tab", QVariant::fromValue((QObject *)tab));
|
||||
tabBar()->setTabButton(tabIndex, closeSide, closeButton);
|
||||
}
|
||||
@@ -367,9 +370,9 @@ void TabSupervisor::gameJoined(const Event_GameJoined &event)
|
||||
QString::fromStdString(event.game_types(i).description()));
|
||||
|
||||
TabGame *tab = new TabGame(this, QList<AbstractClient *>() << client, event, roomGameTypes);
|
||||
connect(tab, SIGNAL(gameClosing(TabGame *)), this, SLOT(gameLeft(TabGame *)));
|
||||
connect(tab, SIGNAL(openMessageDialog(const QString &, bool)), this, SLOT(addMessageTab(const QString &, bool)));
|
||||
connect(tab, SIGNAL(openDeckEditor(const DeckLoader *)), this, SLOT(addDeckEditorTab(const DeckLoader *)));
|
||||
connect(tab, &TabGame::gameClosing, this, &TabSupervisor::gameLeft);
|
||||
connect(tab, &TabGame::openMessageDialog, this, &TabSupervisor::addMessageTab);
|
||||
connect(tab, &TabGame::openDeckEditor, this, &TabSupervisor::addDeckEditorTab);
|
||||
int tabIndex = myAddTab(tab);
|
||||
addCloseButtonToTab(tab, tabIndex);
|
||||
gameTabs.insert(event.game_info().game_id(), tab);
|
||||
@@ -379,8 +382,8 @@ void TabSupervisor::gameJoined(const Event_GameJoined &event)
|
||||
void TabSupervisor::localGameJoined(const Event_GameJoined &event)
|
||||
{
|
||||
TabGame *tab = new TabGame(this, localClients, event, QMap<int, QString>());
|
||||
connect(tab, SIGNAL(gameClosing(TabGame *)), this, SLOT(gameLeft(TabGame *)));
|
||||
connect(tab, SIGNAL(openDeckEditor(const DeckLoader *)), this, SLOT(addDeckEditorTab(const DeckLoader *)));
|
||||
connect(tab, &TabGame::gameClosing, this, &TabSupervisor::gameLeft);
|
||||
connect(tab, &TabGame::openDeckEditor, this, &TabSupervisor::addDeckEditorTab);
|
||||
int tabIndex = myAddTab(tab);
|
||||
addCloseButtonToTab(tab, tabIndex);
|
||||
gameTabs.insert(event.game_info().game_id(), tab);
|
||||
@@ -408,9 +411,9 @@ void TabSupervisor::gameLeft(TabGame *tab)
|
||||
void TabSupervisor::addRoomTab(const ServerInfo_Room &info, bool setCurrent)
|
||||
{
|
||||
TabRoom *tab = new TabRoom(this, client, userInfo, info);
|
||||
connect(tab, SIGNAL(maximizeClient()), this, SLOT(maximizeMainWindow()));
|
||||
connect(tab, SIGNAL(roomClosing(TabRoom *)), this, SLOT(roomLeft(TabRoom *)));
|
||||
connect(tab, SIGNAL(openMessageDialog(const QString &, bool)), this, SLOT(addMessageTab(const QString &, bool)));
|
||||
connect(tab, &TabRoom::maximizeClient, this, &TabSupervisor::maximizeMainWindow);
|
||||
connect(tab, &TabRoom::roomClosing, this, &TabSupervisor::roomLeft);
|
||||
connect(tab, &TabRoom::openMessageDialog, this, &TabSupervisor::addMessageTab);
|
||||
int tabIndex = myAddTab(tab);
|
||||
addCloseButtonToTab(tab, tabIndex);
|
||||
roomTabs.insert(info.room_id(), tab);
|
||||
@@ -430,7 +433,7 @@ void TabSupervisor::roomLeft(TabRoom *tab)
|
||||
void TabSupervisor::openReplay(GameReplay *replay)
|
||||
{
|
||||
TabGame *replayTab = new TabGame(this, replay);
|
||||
connect(replayTab, SIGNAL(gameClosing(TabGame *)), this, SLOT(replayLeft(TabGame *)));
|
||||
connect(replayTab, &TabGame::gameClosing, this, &TabSupervisor::replayLeft);
|
||||
int tabIndex = myAddTab(replayTab);
|
||||
addCloseButtonToTab(replayTab, tabIndex);
|
||||
replayTabs.append(replayTab);
|
||||
@@ -466,8 +469,8 @@ TabMessage *TabSupervisor::addMessageTab(const QString &receiverName, bool focus
|
||||
}
|
||||
|
||||
tab = new TabMessage(this, client, *userInfo, otherUser);
|
||||
connect(tab, SIGNAL(talkClosing(TabMessage *)), this, SLOT(talkLeft(TabMessage *)));
|
||||
connect(tab, SIGNAL(maximizeClient()), this, SLOT(maximizeMainWindow()));
|
||||
connect(tab, &TabMessage::talkClosing, this, &TabSupervisor::talkLeft);
|
||||
connect(tab, &TabMessage::maximizeClient, this, &TabSupervisor::maximizeMainWindow);
|
||||
int tabIndex = myAddTab(tab);
|
||||
addCloseButtonToTab(tab, tabIndex);
|
||||
messageTabs.insert(receiverName, tab);
|
||||
@@ -495,7 +498,8 @@ TabDeckEditor *TabSupervisor::addDeckEditorTab(const DeckLoader *deckToOpen)
|
||||
TabDeckEditor *tab = new TabDeckEditor(this);
|
||||
if (deckToOpen)
|
||||
tab->setDeck(new DeckLoader(*deckToOpen));
|
||||
connect(tab, SIGNAL(deckEditorClosing(TabDeckEditor *)), this, SLOT(deckEditorClosed(TabDeckEditor *)));
|
||||
connect(tab, &TabDeckEditor::deckEditorClosing, this, &TabSupervisor::deckEditorClosed);
|
||||
connect(tab, &TabDeckEditor::openDeckEditor, this, &TabSupervisor::addDeckEditorTab);
|
||||
int tabIndex = myAddTab(tab);
|
||||
addCloseButtonToTab(tab, tabIndex);
|
||||
deckEditorTabs.append(tab);
|
||||
@@ -503,6 +507,15 @@ TabDeckEditor *TabSupervisor::addDeckEditorTab(const DeckLoader *deckToOpen)
|
||||
return tab;
|
||||
}
|
||||
|
||||
TabDeckStorageVisual *TabSupervisor::addVisualDeckStorageTab()
|
||||
{
|
||||
TabDeckStorageVisual *tab = new TabDeckStorageVisual(this, client);
|
||||
int tabIndex = myAddTab(tab);
|
||||
addCloseButtonToTab(tab, tabIndex);
|
||||
setCurrentWidget(tab);
|
||||
return tab;
|
||||
}
|
||||
|
||||
void TabSupervisor::deckEditorClosed(TabDeckEditor *tab)
|
||||
{
|
||||
if (tab == currentWidget())
|
||||
@@ -541,7 +554,7 @@ void TabSupervisor::processGameEventContainer(const GameEventContainer &cont)
|
||||
{
|
||||
TabGame *tab = gameTabs.value(cont.game_id());
|
||||
if (tab)
|
||||
tab->processGameEventContainer(cont, qobject_cast<AbstractClient *>(sender()));
|
||||
tab->processGameEventContainer(cont, qobject_cast<AbstractClient *>(sender()), {});
|
||||
else
|
||||
qDebug() << "gameEvent: invalid gameId";
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
#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 "visual_deck_storage/tab_deck_storage_visual.h"
|
||||
|
||||
#include <QAbstractButton>
|
||||
#include <QCommonStyle>
|
||||
@@ -130,6 +131,7 @@ signals:
|
||||
|
||||
public slots:
|
||||
TabDeckEditor *addDeckEditorTab(const DeckLoader *deckToOpen);
|
||||
TabDeckStorageVisual *addVisualDeckStorageTab();
|
||||
void openReplay(GameReplay *replay);
|
||||
void maximizeMainWindow();
|
||||
private slots:
|
||||
@@ -0,0 +1,104 @@
|
||||
#include "tab_deck_storage_visual.h"
|
||||
|
||||
#include "../../../game/cards/card_database_model.h"
|
||||
#include "../tab_supervisor.h"
|
||||
#include "pb/command_deck_del.pb.h"
|
||||
|
||||
#include <QAction>
|
||||
#include <QDebug>
|
||||
#include <QDirIterator>
|
||||
#include <QFileSystemModel>
|
||||
#include <QHBoxLayout>
|
||||
#include <QInputDialog>
|
||||
#include <QMessageBox>
|
||||
#include <QMouseEvent>
|
||||
#include <QScreen>
|
||||
#include <QToolBar>
|
||||
#include <QTreeView>
|
||||
|
||||
class FlowLayout;
|
||||
TabDeckStorageVisual::TabDeckStorageVisual(TabSupervisor *_tabSupervisor, AbstractClient *_client)
|
||||
: Tab(_tabSupervisor), client(_client)
|
||||
{
|
||||
deck_list_model = new DeckListModel(this);
|
||||
deck_list_model->setObjectName("visualDeckModel");
|
||||
|
||||
QWidget *container = new QWidget(this);
|
||||
QVBoxLayout *layout = new QVBoxLayout(container);
|
||||
container->setLayout(layout);
|
||||
this->setCentralWidget(container);
|
||||
|
||||
leftToolBar = new QToolBar;
|
||||
leftToolBar->setOrientation(Qt::Horizontal);
|
||||
leftToolBar->setIconSize(QSize(32, 32));
|
||||
QHBoxLayout *leftToolBarLayout = new QHBoxLayout(this);
|
||||
leftToolBarLayout->addStretch();
|
||||
leftToolBarLayout->addWidget(leftToolBar);
|
||||
leftToolBarLayout->addStretch();
|
||||
|
||||
aOpenLocalDeck = new QAction(this);
|
||||
aOpenLocalDeck->setIcon(QPixmap("theme:icons/pencil"));
|
||||
connect(aOpenLocalDeck, SIGNAL(triggered()), this, SLOT(actOpenLocalDeck()));
|
||||
aDeleteLocalDeck = new QAction(this);
|
||||
aDeleteLocalDeck->setIcon(QPixmap("theme:icons/remove_row"));
|
||||
connect(aDeleteLocalDeck, SIGNAL(triggered()), this, SLOT(actDeleteLocalDeck()));
|
||||
|
||||
connect(this, &TabDeckStorageVisual::openDeckEditor, tabSupervisor, &TabSupervisor::addDeckEditorTab);
|
||||
|
||||
leftToolBar->addAction(aOpenLocalDeck);
|
||||
leftToolBar->addAction(aDeleteLocalDeck);
|
||||
|
||||
visualDeckStorageWidget = new VisualDeckStorageWidget(this);
|
||||
connect(visualDeckStorageWidget, &VisualDeckStorageWidget::imageDoubleClicked, this,
|
||||
&TabDeckStorageVisual::actOpenLocalDeck);
|
||||
|
||||
// layout->addWidget(leftToolBar);
|
||||
layout->addWidget(visualDeckStorageWidget);
|
||||
|
||||
retranslateUi();
|
||||
}
|
||||
|
||||
void TabDeckStorageVisual::closeRequest()
|
||||
{
|
||||
this->close();
|
||||
}
|
||||
|
||||
void TabDeckStorageVisual::retranslateUi()
|
||||
{
|
||||
aOpenLocalDeck->setText(tr("Open in deck editor"));
|
||||
aDeleteLocalDeck->setText(tr("Delete"));
|
||||
}
|
||||
|
||||
QString TabDeckStorageVisual::getTargetPath() const
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
void TabDeckStorageVisual::actOpenLocalDeck(QMouseEvent *event, DeckPreviewCardPictureWidget *instance)
|
||||
{
|
||||
(void)event;
|
||||
DeckLoader deckLoader;
|
||||
if (!deckLoader.loadFromFile(instance->filePath, DeckLoader::CockatriceFormat))
|
||||
return;
|
||||
|
||||
emit openDeckEditor(&deckLoader);
|
||||
}
|
||||
|
||||
void TabDeckStorageVisual::actDeleteLocalDeck()
|
||||
{
|
||||
QModelIndex curLeft = localDirView->selectionModel()->currentIndex();
|
||||
if (localDirModel->isDir(curLeft))
|
||||
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)
|
||||
return;
|
||||
|
||||
localDirModel->remove(curLeft);
|
||||
}
|
||||
|
||||
void TabDeckStorageVisual::cardUpdateFinished(int exitCode, QProcess::ExitStatus exitStatus)
|
||||
{
|
||||
qDebug() << "Card update process finished with exit code:" << exitCode << "and exit status:" << exitStatus;
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
#ifndef TAB_DECK_STORAGE_VISUAL_H
|
||||
#define TAB_DECK_STORAGE_VISUAL_H
|
||||
|
||||
#include "../../../deck/deck_list_model.h"
|
||||
#include "../../../deck/deck_view.h"
|
||||
#include "../../ui/widgets/cards/deck_preview_card_picture_widget.h"
|
||||
#include "../../ui/widgets/visual_deck_storage/visual_deck_storage_widget.h"
|
||||
#include "../tab.h"
|
||||
|
||||
#include <QProcess>
|
||||
|
||||
class AbstractClient;
|
||||
class QTreeView;
|
||||
class QFileSystemModel;
|
||||
class QToolBar;
|
||||
class QTreeWidget;
|
||||
class QTreeWidgetItem;
|
||||
class QGroupBox;
|
||||
class CommandContainer;
|
||||
class Response;
|
||||
class DeckLoader;
|
||||
|
||||
class TabDeckStorageVisual final : public Tab
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
TabDeckStorageVisual(TabSupervisor *_tabSupervisor, AbstractClient *_client);
|
||||
|
||||
void retranslateUi() override;
|
||||
QString getTabText() const override
|
||||
{
|
||||
return tr("Visual Deck storage");
|
||||
}
|
||||
public slots:
|
||||
void cardUpdateFinished(int exitCode, QProcess::ExitStatus exitStatus);
|
||||
void closeRequest() override;
|
||||
void actOpenLocalDeck(QMouseEvent *event, DeckPreviewCardPictureWidget *instance);
|
||||
void actDeleteLocalDeck();
|
||||
signals:
|
||||
void openDeckEditor(const DeckLoader *deckLoader);
|
||||
|
||||
private:
|
||||
QWidget *container;
|
||||
QHBoxLayout *layout;
|
||||
AbstractClient *client;
|
||||
QTreeView *localDirView;
|
||||
QFileSystemModel *localDirModel;
|
||||
QToolBar *leftToolBar;
|
||||
QGroupBox *leftGroupBox;
|
||||
VisualDeckStorageWidget *visualDeckStorageWidget;
|
||||
DeckListModel *deck_list_model;
|
||||
QAction *aOpenLocalDeck, *aDeleteLocalDeck;
|
||||
QString getTargetPath() const;
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -1,4 +1,4 @@
|
||||
#include "tappedout_interface.h"
|
||||
#include "tapped_out_interface.h"
|
||||
|
||||
#include "decklist.h"
|
||||
|
||||
@@ -14,7 +14,7 @@ TappedOutInterface::TappedOutInterface(CardDatabase &_cardDatabase, QObject *par
|
||||
: QObject(parent), cardDatabase(_cardDatabase)
|
||||
{
|
||||
manager = new QNetworkAccessManager(this);
|
||||
connect(manager, SIGNAL(finished(QNetworkReply *)), this, SLOT(queryFinished(QNetworkReply *)));
|
||||
connect(manager, &QNetworkAccessManager::finished, this, &TappedOutInterface::queryFinished);
|
||||
}
|
||||
|
||||
void TappedOutInterface::queryFinished(QNetworkReply *reply)
|
||||
@@ -115,7 +115,7 @@ struct CopyMainOrSide
|
||||
}
|
||||
};
|
||||
|
||||
void TappedOutInterface::copyDeckSplitMainAndSide(const DeckList &source, DeckList &mainboard, DeckList &sideboard)
|
||||
void TappedOutInterface::copyDeckSplitMainAndSide(DeckList &source, DeckList &mainboard, DeckList &sideboard)
|
||||
{
|
||||
CopyMainOrSide copyMainOrSide(cardDatabase, mainboard, sideboard);
|
||||
source.forEachCard(copyMainOrSide);
|
||||
@@ -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 <QObject>
|
||||
@@ -24,7 +24,7 @@ private:
|
||||
QNetworkAccessManager *manager;
|
||||
|
||||
CardDatabase &cardDatabase;
|
||||
void copyDeckSplitMainAndSide(const DeckList &source, DeckList &mainboard, DeckList &sideboard);
|
||||
void copyDeckSplitMainAndSide(DeckList &source, DeckList &mainboard, DeckList &sideboard);
|
||||
private slots:
|
||||
void queryFinished(QNetworkReply *reply);
|
||||
void getAnalyzeRequestData(DeckList *deck, QByteArray *data);
|
||||
@@ -1,29 +1,29 @@
|
||||
#pragma once
|
||||
|
||||
#include "settingscache.h"
|
||||
#include "../settings/cache_settings.h"
|
||||
|
||||
#include <QMenu>
|
||||
|
||||
class TearOffMenu : public QMenu
|
||||
{
|
||||
public:
|
||||
TearOffMenu(const QString &title, QWidget *parent = nullptr) : QMenu(title, parent)
|
||||
explicit TearOffMenu(const QString &title, QWidget *parent = nullptr) : QMenu(title, parent)
|
||||
{
|
||||
connect(&SettingsCache::instance(), &SettingsCache::useTearOffMenusChanged, this,
|
||||
[=](bool state) { setTearOffEnabled(state); });
|
||||
[this](const bool state) { setTearOffEnabled(state); });
|
||||
setTearOffEnabled(SettingsCache::instance().getUseTearOffMenus());
|
||||
}
|
||||
|
||||
TearOffMenu(QWidget *parent = nullptr) : QMenu(parent)
|
||||
explicit TearOffMenu(QWidget *parent = nullptr) : QMenu(parent)
|
||||
{
|
||||
connect(&SettingsCache::instance(), &SettingsCache::useTearOffMenusChanged, this,
|
||||
[=](bool state) { setTearOffEnabled(state); });
|
||||
[this](const bool state) { setTearOffEnabled(state); });
|
||||
setTearOffEnabled(SettingsCache::instance().getUseTearOffMenus());
|
||||
}
|
||||
|
||||
TearOffMenu *addTearOffMenu(const QString &title)
|
||||
{
|
||||
TearOffMenu *menu = new TearOffMenu(title, this);
|
||||
auto *menu = new TearOffMenu(title, this);
|
||||
addMenu(menu);
|
||||
return menu;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
#include "translatecountername.h"
|
||||
#include "translate_counter_name.h"
|
||||
|
||||
const QMap<QString, QString> TranslateCounterName::translated = {
|
||||
{"life", QT_TRANSLATE_NOOP("TranslateCounterName", "Life")},
|
||||
337
cockatrice/src/client/ui/layouts/flow_layout.cpp
Normal file
337
cockatrice/src/client/ui/layouts/flow_layout.cpp
Normal file
@@ -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 <QDebug>
|
||||
#include <QLayoutItem>
|
||||
#include <QScrollArea>
|
||||
#include <QStyle>
|
||||
|
||||
/**
|
||||
* @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<QLayoutItem *> 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<QLayoutItem *> &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<int>(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<QWidget *>(parent);
|
||||
return pw->style()->pixelMetric(pm, nullptr, pw);
|
||||
}
|
||||
|
||||
return dynamic_cast<QLayout *>(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<QScrollArea *>(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<QScrollArea *>(parent)) {
|
||||
return scrollArea->viewport()->height();
|
||||
}
|
||||
parent = parent->parentWidget();
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
43
cockatrice/src/client/ui/layouts/flow_layout.h
Normal file
43
cockatrice/src/client/ui/layouts/flow_layout.h
Normal file
@@ -0,0 +1,43 @@
|
||||
#ifndef FLOW_LAYOUT_H
|
||||
#define FLOW_LAYOUT_H
|
||||
|
||||
#include <QLayout>
|
||||
#include <QList>
|
||||
#include <QWidget>
|
||||
#include <qstyle.h>
|
||||
|
||||
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<QLayoutItem *> &rowItems, int x, int y);
|
||||
[[nodiscard]] QSize sizeHint() const override;
|
||||
[[nodiscard]] QSize minimumSize() const override;
|
||||
|
||||
protected:
|
||||
QList<QLayoutItem *> items; // List to store layout items
|
||||
int horizontalMargin;
|
||||
int verticalMargin;
|
||||
};
|
||||
|
||||
#endif // FLOW_LAYOUT_H
|
||||
142
cockatrice/src/client/ui/layouts/horizontal_flow_layout.cpp
Normal file
142
cockatrice/src/client/ui/layouts/horizontal_flow_layout.cpp
Normal file
@@ -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<QLayoutItem *> 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<QLayoutItem *> &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;
|
||||
}
|
||||
}
|
||||
19
cockatrice/src/client/ui/layouts/horizontal_flow_layout.h
Normal file
19
cockatrice/src/client/ui/layouts/horizontal_flow_layout.h
Normal file
@@ -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<QLayoutItem *> &colItems, int x, int y);
|
||||
};
|
||||
|
||||
#endif // HORIZONTAL_FLOW_LAYOUT_H
|
||||
474
cockatrice/src/client/ui/layouts/overlap_layout.cpp
Normal file
474
cockatrice/src/client/ui/layouts/overlap_layout.cpp
Normal file
@@ -0,0 +1,474 @@
|
||||
#include "overlap_layout.h"
|
||||
|
||||
#include <QtMath>
|
||||
|
||||
/**
|
||||
* @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<int>(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<int>(maxColumns) : static_cast<int>(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<double>(itemList.size()) / static_cast<double>(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<double>(itemList.size()) / static_cast<double>(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<int>(maxRows) : static_cast<int>(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<int>(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<int>(itemList.size());
|
||||
|
||||
return qCeil(totalItems / rows);
|
||||
}
|
||||
44
cockatrice/src/client/ui/layouts/overlap_layout.h
Normal file
44
cockatrice/src/client/ui/layouts/overlap_layout.h
Normal file
@@ -0,0 +1,44 @@
|
||||
#ifndef OVERLAP_LAYOUT_H
|
||||
#define OVERLAP_LAYOUT_H
|
||||
|
||||
#include <QLayout>
|
||||
#include <QList>
|
||||
#include <QWidget>
|
||||
|
||||
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<QLayoutItem *> itemList;
|
||||
int overlapPercentage;
|
||||
int maxColumns;
|
||||
int maxRows;
|
||||
Qt::Orientation direction;
|
||||
|
||||
// Calculate the preferred size of the layout
|
||||
QSize calculatePreferredSize() const;
|
||||
};
|
||||
|
||||
#endif // OVERLAP_LAYOUT_H
|
||||
144
cockatrice/src/client/ui/layouts/vertical_flow_layout.cpp
Normal file
144
cockatrice/src/client/ui/layouts/vertical_flow_layout.cpp
Normal file
@@ -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<QLayoutItem *> 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<QLayoutItem *> &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();
|
||||
}
|
||||
}
|
||||
19
cockatrice/src/client/ui/layouts/vertical_flow_layout.h
Normal file
19
cockatrice/src/client/ui/layouts/vertical_flow_layout.h
Normal file
@@ -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<QLayoutItem *> &rowItems, int x, int y) override;
|
||||
};
|
||||
|
||||
#endif // VERTICAL_FLOW_LAYOUT_H
|
||||
@@ -1,4 +1,4 @@
|
||||
#include "lineeditcompleter.h"
|
||||
#include "line_edit_completer.h"
|
||||
|
||||
#include <QAbstractItemView>
|
||||
#include <QCompleter>
|
||||
@@ -1,7 +1,7 @@
|
||||
#ifndef LINEEDITCOMPLETER_H
|
||||
#define LINEEDITCOMPLETER_H
|
||||
|
||||
#include "customlineedit.h"
|
||||
#include "../../deck/custom_line_edit.h"
|
||||
|
||||
#include <QFocusEvent>
|
||||
#include <QKeyEvent>
|
||||
@@ -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 <QAction>
|
||||
#include <QDebug>
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#ifndef PHASESTOOLBAR_H
|
||||
#define PHASESTOOLBAR_H
|
||||
|
||||
#include "abstractgraphicsitem.h"
|
||||
#include "../../game/board/abstract_graphics_item.h"
|
||||
|
||||
#include <QFrame>
|
||||
#include <QGraphicsObject>
|
||||
@@ -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 <QApplication>
|
||||
#include <QBuffer>
|
||||
#include <QCryptographicHash>
|
||||
#include <QDebug>
|
||||
#include <QDir>
|
||||
#include <QDirIterator>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QImageReader>
|
||||
#include <QMovie>
|
||||
#include <QNetworkAccessManager>
|
||||
@@ -23,12 +20,14 @@
|
||||
#include <QRegularExpression>
|
||||
#include <QScreen>
|
||||
#include <QSet>
|
||||
#include <QSvgRenderer>
|
||||
#include <QThread>
|
||||
#include <QUrl>
|
||||
#include <algorithm>
|
||||
#include <qloggingcategory.h>
|
||||
#include <utility>
|
||||
|
||||
Q_LOGGING_CATEGORY(PictureLoaderLog, "picture_loader")
|
||||
|
||||
// never cache more than 300 cards at once for a single deck
|
||||
#define CACHED_CARD_PER_DECK_MAX 300
|
||||
|
||||
@@ -36,13 +35,30 @@ PictureToLoad::PictureToLoad(CardInfoPtr _card)
|
||||
: card(std::move(_card)), urlTemplates(SettingsCache::instance().downloads().getAllURLs())
|
||||
{
|
||||
if (card) {
|
||||
for (const auto &set : card->getSets()) {
|
||||
sortedSets << set.getPtr();
|
||||
for (const auto &cardInfoPerSetList : card->getSets()) {
|
||||
for (const auto &set : cardInfoPerSetList) {
|
||||
sortedSets << set.getPtr();
|
||||
}
|
||||
}
|
||||
if (sortedSets.empty()) {
|
||||
sortedSets << CardSet::newInstance("", "", "", QDate());
|
||||
}
|
||||
std::sort(sortedSets.begin(), sortedSets.end(), SetDownloadPriorityComparator());
|
||||
|
||||
// If the user hasn't disabled arts other than their personal preference...
|
||||
if (!SettingsCache::instance().getOverrideAllCardArtWithPersonalPreference()) {
|
||||
// If the pixmapCacheKey corresponds to a specific set, we have to try to load it first.
|
||||
for (const auto &cardInfoPerSetList : card->getSets()) {
|
||||
for (const auto &set : cardInfoPerSetList) {
|
||||
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 +143,8 @@ PictureLoaderWorker::PictureLoaderWorker()
|
||||
#endif
|
||||
auto cache = new QNetworkDiskCache(this);
|
||||
cache->setCacheDirectory(SettingsCache::instance().getNetworkCachePath());
|
||||
cache->setMaximumCacheSize(1024L * 1024L *
|
||||
static_cast<qint64>(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<qint64>(newSizeInMB)); });
|
||||
@@ -136,6 +154,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);
|
||||
@@ -167,15 +192,18 @@ void PictureLoaderWorker::processLoadQueue()
|
||||
QString cardName = cardBeingLoaded.getCard()->getName();
|
||||
QString correctedCardName = cardBeingLoaded.getCard()->getCorrectedName();
|
||||
|
||||
qDebug().nospace() << "PictureLoader: [card: " << cardName << " set: " << setName
|
||||
<< "]: Trying to load picture";
|
||||
qCDebug(PictureLoaderLog).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
|
||||
<< "]: No custom picture, trying to download";
|
||||
qCDebug(PictureLoaderLog).nospace() << "PictureLoader: [card: " << cardName << " set: " << setName
|
||||
<< "]: No custom picture, trying to download";
|
||||
cardsToDownload.append(cardBeingLoaded);
|
||||
cardBeingLoaded.clear();
|
||||
if (!downloadRunning) {
|
||||
@@ -190,7 +218,7 @@ bool PictureLoaderWorker::cardImageExistsOnDisk(QString &setName, QString &corre
|
||||
QImageReader imgReader;
|
||||
imgReader.setDecideFormatFromContent(true);
|
||||
QList<QString> picsPaths = QList<QString>();
|
||||
QDirIterator it(customPicsPath, QDirIterator::Subdirectories);
|
||||
QDirIterator it(customPicsPath, QDirIterator::Subdirectories | QDirIterator::FollowSymlinks);
|
||||
|
||||
// Recursively check all subdirectories of the CUSTOM folder
|
||||
while (it.hasNext()) {
|
||||
@@ -217,22 +245,22 @@ bool PictureLoaderWorker::cardImageExistsOnDisk(QString &setName, QString &corre
|
||||
for (const auto &_picsPath : picsPaths) {
|
||||
imgReader.setFileName(_picsPath);
|
||||
if (imgReader.read(&image)) {
|
||||
qDebug().nospace() << "PictureLoader: [card: " << correctedCardname << " set: " << setName
|
||||
<< "]: Picture found on disk.";
|
||||
qCDebug(PictureLoaderLog).nospace()
|
||||
<< "PictureLoader: [card: " << correctedCardname << " set: " << setName << "]: Picture found on disk.";
|
||||
imageLoaded(cardBeingLoaded.getCard(), image);
|
||||
return true;
|
||||
}
|
||||
imgReader.setFileName(_picsPath + ".full");
|
||||
if (imgReader.read(&image)) {
|
||||
qDebug().nospace() << "PictureLoader: [card: " << correctedCardname << " set: " << setName
|
||||
<< "]: Picture.full found on disk.";
|
||||
qCDebug(PictureLoaderLog).nospace() << "PictureLoader: [card: " << correctedCardname << " set: " << setName
|
||||
<< "]: Picture.full found on disk.";
|
||||
imageLoaded(cardBeingLoaded.getCard(), image);
|
||||
return true;
|
||||
}
|
||||
imgReader.setFileName(_picsPath + ".xlhq");
|
||||
if (imgReader.read(&image)) {
|
||||
qDebug().nospace() << "PictureLoader: [card: " << correctedCardname << " set: " << setName
|
||||
<< "]: Picture.xlhq found on disk.";
|
||||
qCDebug(PictureLoaderLog).nospace() << "PictureLoader: [card: " << correctedCardname << " set: " << setName
|
||||
<< "]: Picture.xlhq found on disk.";
|
||||
imageLoaded(cardBeingLoaded.getCard(), image);
|
||||
return true;
|
||||
}
|
||||
@@ -278,29 +306,33 @@ static int parse(const QString &urlTemplate,
|
||||
}
|
||||
QString propertyValue = getProperty(cardPropertyName);
|
||||
if (propertyValue.isEmpty()) {
|
||||
qDebug().nospace() << "PictureLoader: [card: " << cardName << " set: " << setName << "]: Requested "
|
||||
<< propType << "property (" << cardPropertyName << ") for Url template (" << urlTemplate
|
||||
<< ") is not available";
|
||||
qCDebug(PictureLoaderLog).nospace()
|
||||
<< "PictureLoader: [card: " << cardName << " set: " << setName << "]: Requested " << propType
|
||||
<< "property (" << cardPropertyName << ") for Url template (" << urlTemplate << ") is not available";
|
||||
return 1;
|
||||
} else {
|
||||
int propLength = propertyValue.length();
|
||||
if (subStrLen > 0 && subStrPos + subStrLen > propLength) {
|
||||
qDebug().nospace() << "PictureLoader: [card: " << cardName << " set: " << setName << "]: Requested "
|
||||
<< propType << " property (" << cardPropertyName << ") for Url template ("
|
||||
<< urlTemplate << ") is smaller than substr specification (" << subStrPos << " + "
|
||||
<< subStrLen << " > " << propLength << ")";
|
||||
return 1;
|
||||
} else {
|
||||
propertyValue = propertyValue.mid(subStrPos, subStrLen);
|
||||
propLength = subStrLen;
|
||||
if (subStrLen > 0) {
|
||||
if (subStrPos + subStrLen > propLength) {
|
||||
qCDebug(PictureLoaderLog).nospace()
|
||||
<< "PictureLoader: [card: " << cardName << " set: " << setName << "]: Requested " << propType
|
||||
<< " property (" << cardPropertyName << ") for Url template (" << urlTemplate
|
||||
<< ") is smaller than substr specification (" << subStrPos << " + " << subStrLen << " > "
|
||||
<< propLength << ")";
|
||||
return 1;
|
||||
} else {
|
||||
propertyValue = propertyValue.mid(subStrPos, subStrLen);
|
||||
propLength = subStrLen;
|
||||
}
|
||||
}
|
||||
|
||||
if (!fillWith.isEmpty()) {
|
||||
int fillLength = fillWith.length();
|
||||
if (fillLength < propLength) {
|
||||
qDebug().nospace() << "PictureLoader: [card: " << cardName << " set: " << setName << "]: Requested "
|
||||
<< propType << " property (" << cardPropertyName << ") for Url template ("
|
||||
<< urlTemplate << ") is longer than fill specification (" << fillWith << ")";
|
||||
qCDebug(PictureLoaderLog).nospace()
|
||||
<< "PictureLoader: [card: " << cardName << " set: " << setName << "]: Requested " << propType
|
||||
<< " property (" << cardPropertyName << ") for Url template (" << urlTemplate
|
||||
<< ") is longer than fill specification (" << fillWith << ")";
|
||||
return 1;
|
||||
} else {
|
||||
|
||||
@@ -367,9 +399,9 @@ QString PictureToLoad::transformUrl(const QString &urlTemplate) const
|
||||
* populated in this card, so it should return an empty string,
|
||||
* indicating an invalid Url.
|
||||
*/
|
||||
qDebug().nospace() << "PictureLoader: [card: " << cardName << " set: " << setName
|
||||
<< "]: Requested information (" << prop << ") for Url template (" << urlTemplate
|
||||
<< ") is not available";
|
||||
qCDebug(PictureLoaderLog).nospace()
|
||||
<< "PictureLoader: [card: " << cardName << " set: " << setName << "]: Requested information ("
|
||||
<< prop << ") for Url template (" << urlTemplate << ") is not available";
|
||||
return QString();
|
||||
}
|
||||
}
|
||||
@@ -397,9 +429,10 @@ void PictureLoaderWorker::startNextPicDownload()
|
||||
picDownloadFailed();
|
||||
} else {
|
||||
QUrl url(picUrl);
|
||||
qDebug().nospace() << "PictureLoader: [card: " << cardBeingDownloaded.getCard()->getCorrectedName()
|
||||
<< " set: " << cardBeingDownloaded.getSetName() << "]: Trying to fetch picture from url "
|
||||
<< url.toDisplayString();
|
||||
qCDebug(PictureLoaderLog).nospace()
|
||||
<< "PictureLoader: [card: " << cardBeingDownloaded.getCard()->getCorrectedName()
|
||||
<< " set: " << cardBeingDownloaded.getSetName() << "]: Trying to fetch picture from url "
|
||||
<< url.toDisplayString();
|
||||
makeRequest(url);
|
||||
}
|
||||
}
|
||||
@@ -415,10 +448,11 @@ void PictureLoaderWorker::picDownloadFailed()
|
||||
loadQueue.prepend(cardBeingDownloaded);
|
||||
mutex.unlock();
|
||||
} else {
|
||||
qDebug().nospace() << "PictureLoader: [card: " << cardBeingDownloaded.getCard()->getCorrectedName()
|
||||
<< " set: " << cardBeingDownloaded.getSetName() << "]: Picture NOT found, "
|
||||
<< (picDownload ? "download failed" : "downloads disabled")
|
||||
<< ", no more url combinations to try: BAILING OUT";
|
||||
qCDebug(PictureLoaderLog).nospace()
|
||||
<< "PictureLoader: [card: " << cardBeingDownloaded.getCard()->getCorrectedName()
|
||||
<< " set: " << cardBeingDownloaded.getSetName() << "]: Picture NOT found, "
|
||||
<< (picDownload ? "download failed" : "downloads disabled")
|
||||
<< ", no more url combinations to try: BAILING OUT";
|
||||
imageLoaded(cardBeingDownloaded.getCard(), QImage());
|
||||
cardBeingDownloaded.clear();
|
||||
}
|
||||
@@ -433,11 +467,106 @@ 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()) {
|
||||
qCDebug(PictureLoaderLog).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);
|
||||
qCDebug(PictureLoaderLog).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<int>(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)
|
||||
@@ -446,19 +575,19 @@ void PictureLoaderWorker::picDownloadFinished(QNetworkReply *reply)
|
||||
|
||||
if (reply->error()) {
|
||||
if (isFromCache) {
|
||||
qDebug().nospace() << "PictureLoader: [card: " << cardBeingDownloaded.getCard()->getName()
|
||||
<< " set: " << cardBeingDownloaded.getSetName()
|
||||
<< "]: Removing corrupted cache file for url " << reply->url().toDisplayString()
|
||||
<< " and retrying (" << reply->errorString() << ")";
|
||||
qCDebug(PictureLoaderLog).nospace()
|
||||
<< "PictureLoader: [card: " << cardBeingDownloaded.getCard()->getName()
|
||||
<< " set: " << cardBeingDownloaded.getSetName() << "]: Removing corrupted cache file for url "
|
||||
<< reply->url().toDisplayString() << " and retrying (" << reply->errorString() << ")";
|
||||
|
||||
networkManager->cache()->remove(reply->url());
|
||||
|
||||
makeRequest(reply->url());
|
||||
} else {
|
||||
qDebug().nospace() << "PictureLoader: [card: " << cardBeingDownloaded.getCard()->getName()
|
||||
<< " set: " << cardBeingDownloaded.getSetName()
|
||||
<< "]: " << (picDownload ? "Download" : "Cache search") << " failed for url "
|
||||
<< reply->url().toDisplayString() << " (" << reply->errorString() << ")";
|
||||
qCDebug(PictureLoaderLog).nospace()
|
||||
<< "PictureLoader: [card: " << cardBeingDownloaded.getCard()->getName()
|
||||
<< " set: " << cardBeingDownloaded.getSetName() << "]: " << (picDownload ? "Download" : "Cache search")
|
||||
<< " failed for url " << reply->url().toDisplayString() << " (" << reply->errorString() << ")";
|
||||
|
||||
picDownloadFailed();
|
||||
startNextPicDownload();
|
||||
@@ -473,9 +602,10 @@ void PictureLoaderWorker::picDownloadFinished(QNetworkReply *reply)
|
||||
if (statusCode == 301 || statusCode == 302 || statusCode == 303 || statusCode == 305 || statusCode == 307 ||
|
||||
statusCode == 308) {
|
||||
QUrl redirectUrl = reply->header(QNetworkRequest::LocationHeader).toUrl();
|
||||
qDebug().nospace() << "PictureLoader: [card: " << cardBeingDownloaded.getCard()->getName()
|
||||
<< " set: " << cardBeingDownloaded.getSetName() << "]: following "
|
||||
<< (isFromCache ? "cached redirect" : "redirect") << " to " << redirectUrl.toDisplayString();
|
||||
qCDebug(PictureLoaderLog).nospace()
|
||||
<< "PictureLoader: [card: " << cardBeingDownloaded.getCard()->getName()
|
||||
<< " set: " << cardBeingDownloaded.getSetName() << "]: following "
|
||||
<< (isFromCache ? "cached redirect" : "redirect") << " to " << redirectUrl.toDisplayString();
|
||||
makeRequest(redirectUrl);
|
||||
reply->deleteLater();
|
||||
return;
|
||||
@@ -485,9 +615,9 @@ void PictureLoaderWorker::picDownloadFinished(QNetworkReply *reply)
|
||||
const QByteArray &picData = reply->peek(reply->size());
|
||||
|
||||
if (imageIsBlackListed(picData)) {
|
||||
qDebug().nospace() << "PictureLoader: [card: " << cardBeingDownloaded.getCard()->getName()
|
||||
<< " set: " << cardBeingDownloaded.getSetName()
|
||||
<< "]: Picture found, but blacklisted, will consider it as not found";
|
||||
qCDebug(PictureLoaderLog).nospace() << "PictureLoader: [card: " << cardBeingDownloaded.getCard()->getName()
|
||||
<< " set: " << cardBeingDownloaded.getSetName()
|
||||
<< "]: Picture found, but blacklisted, will consider it as not found";
|
||||
|
||||
picDownloadFailed();
|
||||
reply->deleteLater();
|
||||
@@ -520,19 +650,19 @@ void PictureLoaderWorker::picDownloadFinished(QNetworkReply *reply)
|
||||
imageLoaded(cardBeingDownloaded.getCard(), testImage);
|
||||
logSuccessMessage = true;
|
||||
} else {
|
||||
qDebug().nospace() << "PictureLoader: [card: " << cardBeingDownloaded.getCard()->getName()
|
||||
<< " set: " << cardBeingDownloaded.getSetName() << "]: Possible "
|
||||
<< (isFromCache ? "cached" : "downloaded") << " picture at "
|
||||
<< reply->url().toDisplayString() << " could not be loaded: " << reply->errorString();
|
||||
qCDebug(PictureLoaderLog).nospace()
|
||||
<< "PictureLoader: [card: " << cardBeingDownloaded.getCard()->getName()
|
||||
<< " set: " << cardBeingDownloaded.getSetName() << "]: Possible " << (isFromCache ? "cached" : "downloaded")
|
||||
<< " picture at " << reply->url().toDisplayString() << " could not be loaded: " << reply->errorString();
|
||||
|
||||
picDownloadFailed();
|
||||
}
|
||||
|
||||
if (logSuccessMessage) {
|
||||
qDebug().nospace() << "PictureLoader: [card: " << cardBeingDownloaded.getCard()->getName()
|
||||
<< " set: " << cardBeingDownloaded.getSetName() << "]: Image successfully "
|
||||
<< (isFromCache ? "loaded from cached" : "downloaded from") << " url "
|
||||
<< reply->url().toDisplayString();
|
||||
qCDebug(PictureLoaderLog).nospace()
|
||||
<< "PictureLoader: [card: " << cardBeingDownloaded.getCard()->getName()
|
||||
<< " set: " << cardBeingDownloaded.getSetName() << "]: Image successfully "
|
||||
<< (isFromCache ? "loaded from cached" : "downloaded from") << " url " << reply->url().toDisplayString();
|
||||
}
|
||||
|
||||
reply->deleteLater();
|
||||
@@ -599,7 +729,27 @@ void PictureLoader::getCardBackPixmap(QPixmap &pixmap, QSize size)
|
||||
{
|
||||
QString backCacheKey = "_trice_card_back_" + QString::number(size.width()) + QString::number(size.height());
|
||||
if (!QPixmapCache::find(backCacheKey, &pixmap)) {
|
||||
qDebug() << "PictureLoader: cache fail for" << backCacheKey;
|
||||
qCDebug(PictureLoaderLog) << "PictureLoader: cache fail for" << backCacheKey;
|
||||
pixmap = QPixmap("theme:cardback").scaled(size, Qt::KeepAspectRatio, Qt::SmoothTransformation);
|
||||
QPixmapCache::insert(backCacheKey, pixmap);
|
||||
}
|
||||
}
|
||||
|
||||
void PictureLoader::getCardBackLoadingInProgressPixmap(QPixmap &pixmap, QSize size)
|
||||
{
|
||||
QString backCacheKey = "_trice_card_back_" + QString::number(size.width()) + QString::number(size.height());
|
||||
if (!QPixmapCache::find(backCacheKey, &pixmap)) {
|
||||
qCDebug(PictureLoaderLog) << "PictureLoader: cache fail for" << backCacheKey;
|
||||
pixmap = QPixmap("theme:cardback").scaled(size, Qt::KeepAspectRatio, Qt::SmoothTransformation);
|
||||
QPixmapCache::insert(backCacheKey, pixmap);
|
||||
}
|
||||
}
|
||||
|
||||
void PictureLoader::getCardBackLoadingFailedPixmap(QPixmap &pixmap, QSize size)
|
||||
{
|
||||
QString backCacheKey = "_trice_card_back_" + QString::number(size.width()) + QString::number(size.height());
|
||||
if (!QPixmapCache::find(backCacheKey, &pixmap)) {
|
||||
qCDebug(PictureLoaderLog) << "PictureLoader: cache fail for" << backCacheKey;
|
||||
pixmap = QPixmap("theme:cardback").scaled(size, Qt::KeepAspectRatio, Qt::SmoothTransformation);
|
||||
QPixmapCache::insert(backCacheKey, pixmap);
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
#ifndef PICTURELOADER_H
|
||||
#define PICTURELOADER_H
|
||||
|
||||
#include "carddatabase.h"
|
||||
#include "../../game/cards/card_database.h"
|
||||
|
||||
#include <QList>
|
||||
#include <QMap>
|
||||
@@ -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<PictureToLoad> loadQueue;
|
||||
QMutex mutex;
|
||||
QNetworkAccessManager *networkManager;
|
||||
QHash<QUrl, QPair<QUrl, QDateTime>> redirectCache; // Stores redirect and timestamp
|
||||
QString cacheFilePath; // Path to persistent storage
|
||||
static constexpr int CacheTTLInDays = 30; // TODO: Make user configurable
|
||||
QList<PictureToLoad> 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();
|
||||
@@ -127,6 +142,8 @@ private:
|
||||
public:
|
||||
static void getPixmap(QPixmap &pixmap, CardInfoPtr card, QSize size);
|
||||
static void getCardBackPixmap(QPixmap &pixmap, QSize size);
|
||||
static void getCardBackLoadingInProgressPixmap(QPixmap &pixmap, QSize size);
|
||||
static void getCardBackLoadingFailedPixmap(QPixmap &pixmap, QSize size);
|
||||
static void clearPixmapCache(CardInfoPtr card);
|
||||
static void clearPixmapCache();
|
||||
static void cacheCardPixmaps(QList<CardInfoPtr> cards);
|
||||
@@ -1,4 +1,4 @@
|
||||
#include "pixmapgenerator.h"
|
||||
#include "pixel_map_generator.h"
|
||||
|
||||
#include "pb/serverinfo_user.pb.h"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#include "thememanager.h"
|
||||
#include "theme_manager.h"
|
||||
|
||||
#include "settingscache.h"
|
||||
#include "../../settings/cache_settings.h"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QColor>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user