mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2025-12-21 14:50:26 -08:00
Compare commits
356 Commits
zach_fix_4
...
replays_fi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0436bc4133 | ||
|
|
ca2d438cda | ||
|
|
c148c8df7f | ||
|
|
0cbad25385 | ||
|
|
7b94d5d501 | ||
|
|
ee938342f3 | ||
|
|
cb64a5eea0 | ||
|
|
80165c28a9 | ||
|
|
315c224f24 | ||
|
|
55f624b634 | ||
|
|
82b257b589 | ||
|
|
a51ca9f9cb | ||
|
|
7e19b52926 | ||
|
|
2d02955f8b | ||
|
|
3a740f0bde | ||
|
|
455d68f9ea | ||
|
|
2def02e140 | ||
|
|
23bd18a04c | ||
|
|
d09b9eb533 | ||
|
|
25caae6d0f | ||
|
|
a717e715b6 | ||
|
|
c079715c46 | ||
|
|
f6c1253e84 | ||
|
|
8462b6e906 | ||
|
|
cca82f59eb | ||
|
|
81662b7fec | ||
|
|
d2c2128e9b | ||
|
|
686645c1e4 | ||
|
|
9df71fe1e8 | ||
|
|
6309e7e318 | ||
|
|
767e83c879 | ||
|
|
78d54b0ef2 | ||
|
|
497e4f1be0 | ||
|
|
6072df3522 | ||
|
|
ba89495dc0 | ||
|
|
a417b049da | ||
|
|
883f1a5c11 | ||
|
|
dd8ac14f99 | ||
|
|
9bd024d39f | ||
|
|
e4611a8616 | ||
|
|
3f41e5dd77 | ||
|
|
a6fc88c79a | ||
|
|
3a4ec1062b | ||
|
|
7347ba88ac | ||
|
|
3b544a36a8 | ||
|
|
2851d0c7e6 | ||
|
|
2b296badea | ||
|
|
a12c4ee909 | ||
|
|
7db9c9115e | ||
|
|
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 | ||
|
|
b7fbc12ac0 | ||
|
|
e2ab8db958 | ||
|
|
34d70980e8 | ||
|
|
e45c4042fe | ||
|
|
ce8092318e | ||
|
|
c95cc1dd9d | ||
|
|
1f72877728 | ||
|
|
93b40343d9 | ||
|
|
ba10108207 | ||
|
|
c28f66d673 | ||
|
|
59f327f97a | ||
|
|
872c92a244 |
@@ -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/*
|
||||
@@ -163,7 +163,7 @@ echo "::group::Build project"
|
||||
if [[ $RUNNER_OS == Windows ]]; then
|
||||
# Enable MTT, see https://devblogs.microsoft.com/cppblog/improved-parallelism-in-msbuild/
|
||||
# and https://devblogs.microsoft.com/cppblog/cpp-build-throughput-investigation-and-tune-up/#multitooltask-mtt
|
||||
cmake --build . "${buildflags[@]}" -- -p:UseMultiToolTask=true
|
||||
cmake --build . "${buildflags[@]}" -- -p:UseMultiToolTask=true -p:EnableClServerMode=true
|
||||
else
|
||||
cmake --build . "${buildflags[@]}"
|
||||
fi
|
||||
@@ -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. -->
|
||||
|
||||
247
.github/workflows/desktop-build.yml
vendored
247
.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,33 +201,38 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- target: Debug # tests only
|
||||
os: macos-latest
|
||||
xcode: 14.2
|
||||
qt_version: homebrew
|
||||
type: Debug
|
||||
do_tests: 1
|
||||
|
||||
- target: 10.15_Catalina
|
||||
os: macos-11
|
||||
xcode: 11.7 # allows using macOS 10.15 SDK
|
||||
qt_version: 6.2.* # 6.2 is last LTS compatible with 10.15, see https://doc.qt.io/qt-6.5/macos.html
|
||||
qt_modules: "qtmultimedia qtwebsockets"
|
||||
type: Release
|
||||
do_tests: 1
|
||||
make_package: 1
|
||||
use_old_protobuf: 1
|
||||
qt_py7zrversion: '==0.20.*'
|
||||
|
||||
- target: 13_Ventura
|
||||
- target: 13
|
||||
soc: Intel
|
||||
os: macos-13
|
||||
xcode: 14.3.1
|
||||
qt_version: homebrew
|
||||
xcode: "14.3.1"
|
||||
type: Release
|
||||
do_tests: 1
|
||||
core_count: 4
|
||||
make_package: 1
|
||||
|
||||
name: macOS ${{matrix.target}}
|
||||
- target: 14
|
||||
soc: Apple
|
||||
os: macos-14
|
||||
xcode: "15.4"
|
||||
type: Release
|
||||
core_count: 3
|
||||
make_package: 1
|
||||
|
||||
- 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}}${{ matrix.soc == 'Intel' && ' Intel' || '' }}${{ matrix.type == 'Debug' && ' Debug' || '' }}
|
||||
needs: configure
|
||||
runs-on: ${{matrix.os}}
|
||||
continue-on-error: ${{matrix.allow-failure == 'yes'}}
|
||||
@@ -236,56 +244,93 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install dependencies using homebrew
|
||||
- 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:
|
||||
install_qt: ${{matrix.qt_version}}
|
||||
use_old_protobuf: ${{matrix.use_old_protobuf}}
|
||||
HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK: 1
|
||||
run: |
|
||||
brew update
|
||||
if [[ $use_old_protobuf == 1 ]]; then
|
||||
brew install protobuf@21
|
||||
brew link --force protobuf@21
|
||||
else
|
||||
brew install protobuf
|
||||
brew link --force protobuf
|
||||
fi
|
||||
if [[ $install_qt == homebrew ]]; then
|
||||
brew install qt --force-bottle
|
||||
else # for some reason the tests fail with the action installed qt?
|
||||
brew install googletest
|
||||
fi
|
||||
brew install protobuf qt --force-bottle
|
||||
|
||||
- name: Install Qt ${{matrix.qt_version}} for ${{matrix.target}}
|
||||
if: matrix.qt_version != 'homebrew'
|
||||
uses: jurplel/install-qt-action@v3
|
||||
with:
|
||||
cache: true
|
||||
setup-python: false
|
||||
version: ${{matrix.qt_version}}
|
||||
modules: ${{matrix.qt_modules}}
|
||||
py7zrversion: ${{matrix.qt_py7zrversion}}
|
||||
|
||||
- name: Build on Xcode ${{matrix.xcode}}
|
||||
- name: Build & Sign on Xcode ${{matrix.xcode}}
|
||||
shell: bash
|
||||
id: build
|
||||
env:
|
||||
BUILDTYPE: '${{matrix.type}}'
|
||||
MAKE_TEST: '${{matrix.do_tests}}'
|
||||
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
|
||||
|
||||
@@ -307,24 +352,24 @@ 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
|
||||
runs-on: windows-2019
|
||||
runs-on: windows-2022
|
||||
env:
|
||||
CMAKE_GENERATOR: 'Visual Studio 16 2019'
|
||||
CMAKE_GENERATOR: 'Visual Studio 17 2022'
|
||||
|
||||
steps:
|
||||
- name: Add msbuild to PATH
|
||||
id: add-msbuild
|
||||
uses: microsoft/setup-msbuild@v2
|
||||
with:
|
||||
msbuild-architecture: x64
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
@@ -332,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).
|
||||
|
||||
@@ -24,7 +24,7 @@ if(WIN32)
|
||||
get_filename_component(_path ${_path}/../../ ABSOLUTE)
|
||||
|
||||
foreach(redist_file ${REDIST_FILE_NAMES})
|
||||
if(EXISTS "${_path}/${REDIST_FILE}")
|
||||
if(EXISTS "${_path}/${redist_file}")
|
||||
set(VCREDISTRUNTIME_FOUND "YES")
|
||||
set(VCREDISTRUNTIME_FILE ${_path}/${redist_file})
|
||||
break()
|
||||
|
||||
@@ -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@"
|
||||
@@ -248,20 +255,20 @@ ${If} $PortableMode = 0
|
||||
WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Cockatrice" "VersionMajor" "@CPACK_PACKAGE_VERSION_MAJOR@"
|
||||
WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Cockatrice" "VersionMinor" "@CPACK_PACKAGE_VERSION_MINOR@"
|
||||
|
||||
IfFileExists "$INSTDIR\vcredist_x86.exe" VcRedist86Exists PastVcRedist86Check
|
||||
IfFileExists "$INSTDIR\vc_redist.x86.exe" VcRedist86Exists PastVcRedist86Check
|
||||
VcRedist86Exists:
|
||||
ExecWait '"$INSTDIR\vcredist_x86.exe" /passive /norestart'
|
||||
ExecWait '"$INSTDIR\vc_redist.x86.exe" /passive /norestart'
|
||||
DetailPrint "Wait to ensure unlock of vc_redist file after installation..."
|
||||
Sleep 3000
|
||||
Delete "$INSTDIR\vcredist_x86.exe"
|
||||
Delete "$INSTDIR\vc_redist.x86.exe"
|
||||
PastVcRedist86Check:
|
||||
|
||||
IfFileExists "$INSTDIR\vcredist_x64.exe" VcRedist64Exists PastVcRedist64Check
|
||||
IfFileExists "$INSTDIR\vc_redist.x64.exe" VcRedist64Exists PastVcRedist64Check
|
||||
VcRedist64Exists:
|
||||
ExecWait '"$INSTDIR\vcredist_x64.exe" /passive /norestart'
|
||||
ExecWait '"$INSTDIR\vc_redist.x64.exe" /passive /norestart'
|
||||
DetailPrint "Sleep to ensure unlock of vc_redist file after installation..."
|
||||
Sleep 3000
|
||||
Delete "$INSTDIR\vcredist_x64.exe"
|
||||
Delete "$INSTDIR\vc_redist.x64.exe"
|
||||
PastVcRedist64Check:
|
||||
|
||||
${Else}
|
||||
|
||||
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,132 +5,187 @@
|
||||
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_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/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/deckview/deck_view.cpp
|
||||
src/game/deckview/deck_view_container.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/picture_loader.cpp
|
||||
src/client/ui/picture_loader/picture_loader_worker.cpp
|
||||
src/client/ui/picture_loader/picture_to_load.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_manager.cpp
|
||||
src/server/user/user_list_widget.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/deck_preview/deck_preview_color_identity_widget.cpp
|
||||
src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_color_identity_filter_widget.cpp
|
||||
src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_tag_addition_widget.cpp
|
||||
src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_tag_display_widget.cpp
|
||||
src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_tag_dialog.cpp
|
||||
src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_tag_item_widget.cpp
|
||||
src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_deck_tags_display_widget.cpp
|
||||
src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_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
|
||||
src/client/ui/widgets/visual_deck_storage/visual_deck_storage_sort_widget.cpp
|
||||
src/client/ui/widgets/visual_deck_storage/visual_deck_storage_tag_filter_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)
|
||||
|
||||
@@ -251,7 +306,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}
|
||||
@@ -262,6 +317,7 @@ if(APPLE)
|
||||
PATTERN "audio/*.dylib"
|
||||
PATTERN "iconengines/*.dylib"
|
||||
PATTERN "imageformats/*.dylib"
|
||||
PATTERN "multimedia/*.dylib"
|
||||
PATTERN "platforms/*.dylib"
|
||||
PATTERN "printsupport/*.dylib"
|
||||
PATTERN "styles/*.dylib"
|
||||
@@ -302,7 +358,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}
|
||||
@@ -314,6 +370,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
|
||||
3
cockatrice/resources/config/qtlogging.ini
Normal file
3
cockatrice/resources/config/qtlogging.ini
Normal file
@@ -0,0 +1,3 @@
|
||||
[Rules]
|
||||
picture_loader.debug = true
|
||||
deck_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,
|
||||
@@ -2,7 +2,7 @@
|
||||
#ifndef GETTEXTWITHMAX_H
|
||||
#define GETTEXTWITHMAX_H
|
||||
|
||||
#include "stringsizes.h"
|
||||
#include "trice_limits.h"
|
||||
|
||||
#include <QInputDialog>
|
||||
|
||||
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,20 +160,24 @@ 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
|
||||
if (trayIcon) {
|
||||
QList<QByteArray> lines = data.split('\n');
|
||||
|
||||
foreach (QByteArray line, lines) {
|
||||
for (const QByteArray &line : lines) {
|
||||
if (line.contains("Created At:")) {
|
||||
QString timeStamp = QString(line).replace("Created At:", "").trimmed();
|
||||
timeStamp.chop(6); // Remove " (UTC)"
|
||||
|
||||
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), audioOutput(nullptr), player(nullptr)
|
||||
{
|
||||
ensureThemeDirectoryExists();
|
||||
connect(&SettingsCache::instance(), SIGNAL(soundThemeChanged()), this, SLOT(themeChangedSlot()));
|
||||
connect(&SettingsCache::instance(), SIGNAL(soundEnabledChanged()), this, SLOT(soundEnabledChanged()));
|
||||
connect(&SettingsCache::instance(), &SettingsCache::soundThemeChanged, this, &SoundEngine::themeChangedSlot);
|
||||
connect(&SettingsCache::instance(), &SettingsCache::soundEnabledChanged, this, &SoundEngine::soundEnabledChanged);
|
||||
|
||||
soundEnabledChanged();
|
||||
themeChangedSlot();
|
||||
@@ -28,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,25 +1,29 @@
|
||||
#include "tab.h"
|
||||
|
||||
#include "cardinfowidget.h"
|
||||
#include "../ui/widgets/cards/card_info_display_widget.h"
|
||||
#include "./tab_supervisor.h"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QCloseEvent>
|
||||
#include <QDebug>
|
||||
#include <QScreen>
|
||||
|
||||
Tab::Tab(TabSupervisor *_tabSupervisor, QWidget *parent)
|
||||
: QMainWindow(parent), tabSupervisor(_tabSupervisor), contentsChanged(false), infoPopup(0)
|
||||
Tab::Tab(TabSupervisor *_tabSupervisor)
|
||||
: QMainWindow(_tabSupervisor), tabSupervisor(_tabSupervisor), contentsChanged(false), infoPopup(0)
|
||||
{
|
||||
setAttribute(Qt::WA_DeleteOnClose);
|
||||
}
|
||||
|
||||
void Tab::showCardInfoPopup(const QPoint &pos, const QString &cardName)
|
||||
void Tab::showCardInfoPopup(const QPoint &pos, const QString &cardName, const QString &providerId)
|
||||
{
|
||||
if (infoPopup) {
|
||||
infoPopup->deleteLater();
|
||||
}
|
||||
currentCardName = cardName;
|
||||
infoPopup = new 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();
|
||||
@@ -39,3 +43,17 @@ void Tab::deleteCardInfoPopup(const QString &cardName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides the closeEvent in order to emit a close signal
|
||||
*/
|
||||
void Tab::closeEvent(QCloseEvent *event)
|
||||
{
|
||||
emit closed();
|
||||
event->accept();
|
||||
}
|
||||
|
||||
void Tab::closeRequest(bool /*forced*/)
|
||||
{
|
||||
close();
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
class QMenu;
|
||||
class TabSupervisor;
|
||||
class CardInfoWidget;
|
||||
class CardInfoDisplayWidget;
|
||||
|
||||
class Tab : public QMainWindow
|
||||
{
|
||||
@@ -13,6 +13,12 @@ class Tab : public QMainWindow
|
||||
signals:
|
||||
void userEvent(bool globalEvent = true);
|
||||
void tabTextChanged(Tab *tab, const QString &newTabText);
|
||||
/**
|
||||
* Emitted when the tab is closed (because Qt doesn't provide a built-in close signal)
|
||||
* This signal is emitted from this class's overridden Tab::closeEvent method.
|
||||
* Make sure any subclasses that override closeEvent still emit this signal from there.
|
||||
*/
|
||||
void closed();
|
||||
|
||||
protected:
|
||||
TabSupervisor *tabSupervisor;
|
||||
@@ -21,17 +27,18 @@ protected:
|
||||
tabMenus.append(menu);
|
||||
}
|
||||
protected slots:
|
||||
void showCardInfoPopup(const QPoint &pos, const QString &cardName);
|
||||
void showCardInfoPopup(const QPoint &pos, const QString &cardName, const QString &providerId);
|
||||
void deleteCardInfoPopup(const QString &cardName);
|
||||
void closeEvent(QCloseEvent *event) override;
|
||||
|
||||
private:
|
||||
QString currentCardName;
|
||||
QString currentCardName, currentProviderId;
|
||||
bool contentsChanged;
|
||||
CardInfoWidget *infoPopup;
|
||||
CardInfoDisplayWidget *infoPopup;
|
||||
QList<QMenu *> tabMenus;
|
||||
|
||||
public:
|
||||
Tab(TabSupervisor *_tabSupervisor, QWidget *parent = nullptr);
|
||||
explicit Tab(TabSupervisor *_tabSupervisor);
|
||||
const QList<QMenu *> &getTabMenus() const
|
||||
{
|
||||
return tabMenus;
|
||||
@@ -50,9 +57,13 @@ public:
|
||||
}
|
||||
virtual QString getTabText() const = 0;
|
||||
virtual void retranslateUi() = 0;
|
||||
virtual void closeRequest()
|
||||
{
|
||||
}
|
||||
/**
|
||||
* Sends a request to close the tab.
|
||||
* Signals for cleanup should be emitted from this method instead of the destructor.
|
||||
*
|
||||
* @param forced whether this close request was initiated by the user or forced by the server.
|
||||
*/
|
||||
virtual void closeRequest(bool forced = false);
|
||||
virtual void tabActivated()
|
||||
{
|
||||
}
|
||||
239
cockatrice/src/client/tabs/tab_account.cpp
Normal file
239
cockatrice/src/client/tabs/tab_account.cpp
Normal file
@@ -0,0 +1,239 @@
|
||||
#include "tab_account.h"
|
||||
|
||||
#include "../../deck/custom_line_edit.h"
|
||||
#include "../../server/pending_command.h"
|
||||
#include "../../server/user/user_info_box.h"
|
||||
#include "../../server/user/user_list_manager.h"
|
||||
#include "../../server/user/user_list_widget.h"
|
||||
#include "../game_logic/abstract_client.h"
|
||||
#include "../sound_engine.h"
|
||||
#include "pb/event_add_to_list.pb.h"
|
||||
#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 "tab_supervisor.h"
|
||||
#include "trice_limits.h"
|
||||
|
||||
#include <QPushButton>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
TabAccount::TabAccount(TabSupervisor *_tabSupervisor, AbstractClient *_client, const ServerInfo_User &userInfo)
|
||||
: Tab(_tabSupervisor), client(_client)
|
||||
{
|
||||
allUsersList = new UserListWidget(_tabSupervisor, client, UserListWidget::AllUsersList);
|
||||
buddyList = new UserListWidget(_tabSupervisor, client, UserListWidget::BuddyList);
|
||||
ignoreList = new UserListWidget(_tabSupervisor, client, UserListWidget::IgnoreList);
|
||||
userInfoBox = new UserInfoBox(client, true);
|
||||
userInfoBox->updateInfo(userInfo);
|
||||
|
||||
connect(allUsersList, &UserListWidget::openMessageDialog, this, &TabAccount::openMessageDialog);
|
||||
connect(buddyList, &UserListWidget::openMessageDialog, this, &TabAccount::openMessageDialog);
|
||||
connect(ignoreList, &UserListWidget::openMessageDialog, this, &TabAccount::openMessageDialog);
|
||||
|
||||
connect(client, &AbstractClient::userJoinedEventReceived, this, &TabAccount::processUserJoinedEvent);
|
||||
connect(client, &AbstractClient::userLeftEventReceived, this, &TabAccount::processUserLeftEvent);
|
||||
connect(client, &AbstractClient::buddyListReceived, this, &TabAccount::buddyListReceived);
|
||||
connect(client, &AbstractClient::ignoreListReceived, this, &TabAccount::ignoreListReceived);
|
||||
connect(client, &AbstractClient::addToListEventReceived, this, &TabAccount::processAddToListEvent);
|
||||
connect(client, &AbstractClient::removeFromListEventReceived, this, &TabAccount::processRemoveFromListEvent);
|
||||
|
||||
// Attempt to populate the tab with the cache
|
||||
buddyListReceived(tabSupervisor->getUserListManager()->getBuddyList().values());
|
||||
ignoreListReceived(tabSupervisor->getUserListManager()->getIgnoreList().values());
|
||||
|
||||
PendingCommand *pend = AbstractClient::prepareSessionCommand(Command_ListUsers());
|
||||
connect(pend, &PendingCommand::finished, this, &TabAccount::processListUsersResponse);
|
||||
client->sendCommand(pend);
|
||||
|
||||
auto *vbox = new QVBoxLayout;
|
||||
vbox->addWidget(userInfoBox);
|
||||
vbox->addWidget(allUsersList);
|
||||
|
||||
auto *addToBuddyList = new QHBoxLayout;
|
||||
addBuddyEdit = new LineEditUnfocusable;
|
||||
addBuddyEdit->setMaxLength(MAX_NAME_LENGTH);
|
||||
addBuddyEdit->setPlaceholderText(tr("Add to Buddy List"));
|
||||
connect(addBuddyEdit, &LineEditUnfocusable::returnPressed, this, &TabAccount::addToBuddyList);
|
||||
auto *addBuddyButton = new QPushButton("Add");
|
||||
connect(addBuddyButton, &QPushButton::clicked, this, &TabAccount::addToBuddyList);
|
||||
addToBuddyList->addWidget(addBuddyEdit);
|
||||
addToBuddyList->addWidget(addBuddyButton);
|
||||
|
||||
auto *addToIgnoreList = new QHBoxLayout;
|
||||
addIgnoreEdit = new LineEditUnfocusable;
|
||||
addIgnoreEdit->setMaxLength(MAX_NAME_LENGTH);
|
||||
addIgnoreEdit->setPlaceholderText(tr("Add to Ignore List"));
|
||||
connect(addIgnoreEdit, &LineEditUnfocusable::returnPressed, this, &TabAccount::addToIgnoreList);
|
||||
auto *addIgnoreButton = new QPushButton("Add");
|
||||
connect(addIgnoreButton, &QPushButton::clicked, this, &TabAccount::addToIgnoreList);
|
||||
addToIgnoreList->addWidget(addIgnoreEdit);
|
||||
addToIgnoreList->addWidget(addIgnoreButton);
|
||||
|
||||
auto *buddyPanel = new QVBoxLayout;
|
||||
buddyPanel->addWidget(buddyList);
|
||||
buddyPanel->addLayout(addToBuddyList);
|
||||
|
||||
auto *ignorePanel = new QVBoxLayout;
|
||||
ignorePanel->addWidget(ignoreList);
|
||||
ignorePanel->addLayout(addToIgnoreList);
|
||||
|
||||
auto *mainLayout = new QHBoxLayout;
|
||||
mainLayout->addLayout(buddyPanel);
|
||||
mainLayout->addLayout(ignorePanel);
|
||||
mainLayout->addLayout(vbox);
|
||||
|
||||
retranslateUi();
|
||||
|
||||
auto *mainWidget = new QWidget(this);
|
||||
mainWidget->setLayout(mainLayout);
|
||||
setCentralWidget(mainWidget);
|
||||
}
|
||||
|
||||
void TabAccount::addToBuddyList()
|
||||
{
|
||||
const QString &userName = addBuddyEdit->text();
|
||||
if (userName.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const std::string listName = "buddy";
|
||||
addToList(listName, userName);
|
||||
addBuddyEdit->clear();
|
||||
}
|
||||
|
||||
void TabAccount::addToIgnoreList()
|
||||
{
|
||||
const QString &userName = addIgnoreEdit->text();
|
||||
if (userName.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const std::string listName = "ignore";
|
||||
addToList(listName, userName);
|
||||
addIgnoreEdit->clear();
|
||||
}
|
||||
|
||||
void TabAccount::addToList(const std::string &listName, const QString &userName)
|
||||
{
|
||||
Command_AddToList cmd;
|
||||
cmd.set_list(listName);
|
||||
cmd.set_user_name(userName.toStdString());
|
||||
|
||||
client->sendCommand(AbstractClient::prepareSessionCommand(cmd));
|
||||
}
|
||||
|
||||
void TabAccount::retranslateUi()
|
||||
{
|
||||
allUsersList->retranslateUi();
|
||||
buddyList->retranslateUi();
|
||||
ignoreList->retranslateUi();
|
||||
userInfoBox->retranslateUi();
|
||||
}
|
||||
|
||||
void TabAccount::processListUsersResponse(const Response &response)
|
||||
{
|
||||
const Response_ListUsers &resp = response.GetExtension(Response_ListUsers::ext);
|
||||
for (int i = 0; i < resp.user_list_size(); ++i) {
|
||||
const ServerInfo_User &info = resp.user_list(i);
|
||||
const QString &userName = QString::fromStdString(info.name());
|
||||
allUsersList->processUserInfo(info, true);
|
||||
ignoreList->setUserOnline(userName, true);
|
||||
buddyList->setUserOnline(userName, true);
|
||||
}
|
||||
|
||||
allUsersList->sortItems();
|
||||
ignoreList->sortItems();
|
||||
buddyList->sortItems();
|
||||
}
|
||||
|
||||
void TabAccount::processUserJoinedEvent(const Event_UserJoined &event)
|
||||
{
|
||||
const ServerInfo_User &info = event.user_info();
|
||||
const QString &userName = QString::fromStdString(info.name());
|
||||
|
||||
allUsersList->processUserInfo(info, true);
|
||||
ignoreList->setUserOnline(userName, true);
|
||||
buddyList->setUserOnline(userName, true);
|
||||
|
||||
allUsersList->sortItems();
|
||||
ignoreList->sortItems();
|
||||
buddyList->sortItems();
|
||||
|
||||
if (buddyList->getUsers().keys().contains(userName)) {
|
||||
soundEngine->playSound("buddy_join");
|
||||
}
|
||||
|
||||
emit userJoined(info);
|
||||
}
|
||||
|
||||
void TabAccount::processUserLeftEvent(const Event_UserLeft &event)
|
||||
{
|
||||
const QString &userName = QString::fromStdString(event.name());
|
||||
|
||||
if (buddyList->getUsers().keys().contains(userName)) {
|
||||
soundEngine->playSound("buddy_leave");
|
||||
}
|
||||
|
||||
if (allUsersList->deleteUser(userName)) {
|
||||
ignoreList->setUserOnline(userName, false);
|
||||
buddyList->setUserOnline(userName, false);
|
||||
ignoreList->sortItems();
|
||||
buddyList->sortItems();
|
||||
|
||||
emit userLeft(userName);
|
||||
}
|
||||
}
|
||||
|
||||
void TabAccount::buddyListReceived(const QList<ServerInfo_User> &_buddyList)
|
||||
{
|
||||
for (const auto &user : _buddyList) {
|
||||
buddyList->processUserInfo(user, false);
|
||||
}
|
||||
buddyList->sortItems();
|
||||
}
|
||||
|
||||
void TabAccount::ignoreListReceived(const QList<ServerInfo_User> &_ignoreList)
|
||||
{
|
||||
for (const auto &user : _ignoreList) {
|
||||
ignoreList->processUserInfo(user, false);
|
||||
}
|
||||
ignoreList->sortItems();
|
||||
}
|
||||
|
||||
void TabAccount::processAddToListEvent(const Event_AddToList &event)
|
||||
{
|
||||
const ServerInfo_User &info = event.user_info();
|
||||
const bool online = allUsersList->getUsers().contains(QString::fromStdString(info.name()));
|
||||
const QString &list = QString::fromStdString(event.list_name());
|
||||
|
||||
UserListWidget *userList;
|
||||
if (list == "buddy") {
|
||||
userList = buddyList;
|
||||
} else if (list == "ignore") {
|
||||
userList = ignoreList;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
userList->processUserInfo(info, online);
|
||||
userList->sortItems();
|
||||
}
|
||||
|
||||
void TabAccount::processRemoveFromListEvent(const Event_RemoveFromList &event)
|
||||
{
|
||||
const auto &list = QString::fromStdString(event.list_name());
|
||||
const auto &user = QString::fromStdString(event.user_name());
|
||||
|
||||
UserListWidget *userList;
|
||||
if (list == "buddy") {
|
||||
userList = buddyList;
|
||||
} else if (list == "ignore") {
|
||||
userList = ignoreList;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
userList->deleteUser(user);
|
||||
}
|
||||
@@ -5,25 +5,25 @@
|
||||
#include "tab.h"
|
||||
|
||||
class AbstractClient;
|
||||
class UserList;
|
||||
class UserInfoBox;
|
||||
class LineEditUnfocusable;
|
||||
|
||||
class Event_AddToList;
|
||||
class Event_ListRooms;
|
||||
class Event_RemoveFromList;
|
||||
class Event_UserJoined;
|
||||
class Event_UserLeft;
|
||||
class LineEditUnfocusable;
|
||||
class Response;
|
||||
class ServerInfo_User;
|
||||
class Event_AddToList;
|
||||
class Event_RemoveFromList;
|
||||
class UserInfoBox;
|
||||
class UserListWidget;
|
||||
|
||||
class TabUserLists : public Tab
|
||||
class TabAccount : public Tab
|
||||
{
|
||||
Q_OBJECT
|
||||
signals:
|
||||
void openMessageDialog(const QString &userName, bool focus);
|
||||
void userLeft(const QString &userName);
|
||||
void userJoined(const ServerInfo_User &userInfo);
|
||||
|
||||
private slots:
|
||||
void processListUsersResponse(const Response &response);
|
||||
void processUserJoinedEvent(const Event_UserJoined &event);
|
||||
@@ -37,36 +37,21 @@ private slots:
|
||||
|
||||
private:
|
||||
AbstractClient *client;
|
||||
UserList *allUsersList;
|
||||
UserList *buddyList;
|
||||
UserList *ignoreList;
|
||||
UserListWidget *allUsersList;
|
||||
UserListWidget *buddyList;
|
||||
UserListWidget *ignoreList;
|
||||
UserInfoBox *userInfoBox;
|
||||
LineEditUnfocusable *addBuddyEdit;
|
||||
LineEditUnfocusable *addIgnoreEdit;
|
||||
void addToList(const std::string &listName, const QString &userName);
|
||||
|
||||
public:
|
||||
TabUserLists(TabSupervisor *_tabSupervisor,
|
||||
AbstractClient *_client,
|
||||
const ServerInfo_User &userInfo,
|
||||
QWidget *parent = nullptr);
|
||||
void retranslateUi();
|
||||
QString getTabText() const
|
||||
explicit TabAccount(TabSupervisor *_tabSupervisor, AbstractClient *_client, const ServerInfo_User &userInfo);
|
||||
void retranslateUi() override;
|
||||
[[nodiscard]] QString getTabText() const override
|
||||
{
|
||||
return tr("Account");
|
||||
}
|
||||
const UserList *getAllUsersList() const
|
||||
{
|
||||
return allUsersList;
|
||||
}
|
||||
const UserList *getBuddyList() const
|
||||
{
|
||||
return buddyList;
|
||||
}
|
||||
const UserList *getIgnoreList() const
|
||||
{
|
||||
return ignoreList;
|
||||
}
|
||||
};
|
||||
|
||||
#endif
|
||||
248
cockatrice/src/client/tabs/tab_admin.cpp
Normal file
248
cockatrice/src/client/tabs/tab_admin.cpp
Normal file
@@ -0,0 +1,248 @@
|
||||
#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)
|
||||
: Tab(_tabSupervisor), 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, &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)
|
||||
{
|
||||
auto *event = new Event_ReplayAdded();
|
||||
|
||||
switch (response.response_code()) {
|
||||
case Response::RespOk:
|
||||
client->replayAddedEventReceived(*event);
|
||||
QMessageBox::information(this, tr("Success"), tr("Replay access granted"));
|
||||
break;
|
||||
case Response::RespContextError:
|
||||
QMessageBox::critical(this, tr("Error"), tr("Unable to grant replay access. Replay ID invalid"));
|
||||
break;
|
||||
default:
|
||||
QMessageBox::critical(this, tr("Error"), tr("Unable to grant replay access. Internal error"));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void TabAdmin::activateUserProcessResponse(const Response &response)
|
||||
{
|
||||
switch (response.response_code()) {
|
||||
case Response::RespActivationAccepted:
|
||||
QMessageBox::information(this, tr("Success"), tr("User successfully activated"));
|
||||
break;
|
||||
case Response::RespNameNotFound:
|
||||
QMessageBox::critical(this, tr("Error"), tr("Unable to activate user. Username invalid"));
|
||||
break;
|
||||
case Response::RespActivationFailed:
|
||||
QMessageBox::critical(this, tr("Error"), tr("Unable to activate user. User already active"));
|
||||
break;
|
||||
default:
|
||||
QMessageBox::critical(this, tr("Error"), tr("Unable to activate user. Internal error"));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void TabAdmin::actUnlock()
|
||||
{
|
||||
if (fullAdmin)
|
||||
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>
|
||||
@@ -20,7 +22,7 @@ private:
|
||||
QSpinBox *minutesEdit;
|
||||
|
||||
public:
|
||||
ShutdownDialog(QWidget *parent = nullptr);
|
||||
explicit ShutdownDialog(QWidget *parent = nullptr);
|
||||
QString getReason() const;
|
||||
int getMinutes() const;
|
||||
};
|
||||
@@ -32,23 +34,29 @@ private:
|
||||
bool locked;
|
||||
AbstractClient *client;
|
||||
bool fullAdmin;
|
||||
QPushButton *updateServerMessageButton, *shutdownServerButton, *reloadConfigButton;
|
||||
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 &response);
|
||||
void activateUserProcessResponse(const Response &response);
|
||||
|
||||
void actUnlock();
|
||||
void actLock();
|
||||
|
||||
public:
|
||||
TabAdmin(TabSupervisor *_tabSupervisor, AbstractClient *_client, bool _fullAdmin, QWidget *parent = nullptr);
|
||||
void retranslateUi();
|
||||
QString getTabText() const
|
||||
TabAdmin(TabSupervisor *_tabSupervisor, AbstractClient *_client, bool _fullAdmin);
|
||||
void retranslateUi() override;
|
||||
QString getTabText() const override
|
||||
{
|
||||
return tr("Administration");
|
||||
}
|
||||
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,56 +15,50 @@ 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();
|
||||
void actSaveDeckToClipboard();
|
||||
void actSaveDeckToClipboardNoSetNameAndNumber();
|
||||
void actSaveDeckToClipboardRaw();
|
||||
void actSaveDeckToClipboardRawNoSetNameAndNumber();
|
||||
void actPrintDeck();
|
||||
void actExportDeckDecklist();
|
||||
void actAnalyzeDeckDeckstats();
|
||||
@@ -100,11 +95,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 +126,8 @@ private:
|
||||
|
||||
QTreeView *deckView;
|
||||
KeySignals deckViewKeySignals;
|
||||
CardFrame *cardInfo;
|
||||
CardInfoFrameWidget *cardInfo;
|
||||
PrintingSelector *printingSelector;
|
||||
SearchLineEdit *searchEdit;
|
||||
KeySignals searchKeySignals;
|
||||
|
||||
@@ -121,6 +135,8 @@ private:
|
||||
LineEditUnfocusable *nameEdit;
|
||||
QLabel *commentsLabel;
|
||||
QTextEdit *commentsEdit;
|
||||
QLabel *bannerCardLabel;
|
||||
QComboBox *bannerCardComboBox;
|
||||
QLabel *hashLabel1;
|
||||
LineEditUnfocusable *hashLabel;
|
||||
FilterTreeModel *filterModel;
|
||||
@@ -128,16 +144,17 @@ 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, *aSaveDeckToClipboardNoSetNameAndNumber, *aSaveDeckToClipboardRaw,
|
||||
*aSaveDeckToClipboardRawNoSetNameAndNumber, *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,11 +162,11 @@ private:
|
||||
QDockWidget *cardInfoDock;
|
||||
QDockWidget *deckDock;
|
||||
QDockWidget *filterDock;
|
||||
QDockWidget *printingSelectorDock;
|
||||
QWidget *centralWidget;
|
||||
|
||||
public:
|
||||
explicit TabDeckEditor(TabSupervisor *_tabSupervisor, QWidget *parent = nullptr);
|
||||
~TabDeckEditor() override;
|
||||
explicit TabDeckEditor(TabSupervisor *_tabSupervisor);
|
||||
void retranslateUi() override;
|
||||
QString getTabText() const override;
|
||||
void setDeck(DeckLoader *_deckLoader);
|
||||
@@ -158,12 +175,16 @@ public:
|
||||
void createDeckDock();
|
||||
void createCardInfoDock();
|
||||
void createFiltersDock();
|
||||
void createPrintingSelectorDock();
|
||||
void createMenus();
|
||||
void createCentralFrame();
|
||||
void updateCardInfo(CardInfoPtr _card);
|
||||
|
||||
public slots:
|
||||
void closeRequest() override;
|
||||
void closeRequest(bool forced = false) 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, true))
|
||||
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);
|
||||
|
||||
@@ -51,8 +64,8 @@ private slots:
|
||||
|
||||
public:
|
||||
TabDeckStorage(TabSupervisor *_tabSupervisor, AbstractClient *_client);
|
||||
void retranslateUi();
|
||||
QString getTabText() const
|
||||
void retranslateUi() override;
|
||||
QString getTabText() const override
|
||||
{
|
||||
return tr("Deck storage");
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,26 +1,27 @@
|
||||
#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>
|
||||
#include <QPushButton>
|
||||
|
||||
class UserListProxy;
|
||||
class DeckViewContainer;
|
||||
class AbstractClient;
|
||||
class CardDatabase;
|
||||
class GameView;
|
||||
class DeckView;
|
||||
class GameScene;
|
||||
class CardFrame;
|
||||
class CardInfoFrameWidget;
|
||||
class MessageLogWidget;
|
||||
class QTimer;
|
||||
class QSplitter;
|
||||
class QLabel;
|
||||
class QPushButton;
|
||||
class QToolButton;
|
||||
class QMenu;
|
||||
class ZoneViewLayout;
|
||||
@@ -47,11 +48,9 @@ class Event_Ping;
|
||||
class Event_GameSay;
|
||||
class Event_Kicked;
|
||||
class Event_ReverseTurn;
|
||||
class Player;
|
||||
class CardZone;
|
||||
class AbstractCardItem;
|
||||
class CardItem;
|
||||
class TabGame;
|
||||
class DeckLoader;
|
||||
class QVBoxLayout;
|
||||
class QHBoxLayout;
|
||||
@@ -62,63 +61,13 @@ class LineEditCompleter;
|
||||
class QDockWidget;
|
||||
class QStackedWidget;
|
||||
|
||||
class ToggleButton : public QPushButton
|
||||
{
|
||||
Q_OBJECT
|
||||
private:
|
||||
bool state;
|
||||
signals:
|
||||
void stateChanged();
|
||||
|
||||
public:
|
||||
ToggleButton(QWidget *parent = nullptr);
|
||||
bool getState() const
|
||||
{
|
||||
return state;
|
||||
}
|
||||
void setState(bool _state);
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent *event);
|
||||
};
|
||||
|
||||
class DeckViewContainer : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
private:
|
||||
QPushButton *loadLocalButton, *loadRemoteButton;
|
||||
ToggleButton *readyStartButton, *sideboardLockButton;
|
||||
DeckView *deckView;
|
||||
TabGame *parentGame;
|
||||
int playerId;
|
||||
private slots:
|
||||
void loadLocalDeck();
|
||||
void loadRemoteDeck();
|
||||
void readyStart();
|
||||
void deckSelectFinished(const Response &r);
|
||||
void sideboardPlanChanged();
|
||||
void sideboardLockButtonClicked();
|
||||
void updateSideboardLockButtonText();
|
||||
void refreshShortcuts();
|
||||
signals:
|
||||
void newCardAdded(AbstractCardItem *card);
|
||||
void notIdle();
|
||||
|
||||
public:
|
||||
DeckViewContainer(int _playerId, TabGame *parent);
|
||||
void retranslateUi();
|
||||
void setButtonsVisible(bool _visible);
|
||||
void setReadyStart(bool ready);
|
||||
void setSideboardLocked(bool locked);
|
||||
void setDeck(const DeckLoader &deck);
|
||||
};
|
||||
|
||||
class TabGame : public Tab
|
||||
{
|
||||
Q_OBJECT
|
||||
private:
|
||||
QTimer *gameTimer;
|
||||
int secondsElapsed;
|
||||
UserListProxy *userListProxy;
|
||||
QList<AbstractClient *> clients;
|
||||
ServerInfo_Game gameInfo;
|
||||
QMap<int, QString> roomGameTypes;
|
||||
@@ -146,9 +95,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,9 +125,12 @@ private:
|
||||
|
||||
Player *addPlayer(int playerId, const ServerInfo_User &info);
|
||||
|
||||
bool isMainPlayerConceded() const;
|
||||
|
||||
void startGame(bool resuming);
|
||||
void stopGame();
|
||||
void closeGame();
|
||||
bool leaveGame();
|
||||
|
||||
void eventSpectatorSay(const Event_GameSay &event, int eventPlayerId, const GameEventContext &context);
|
||||
void eventSpectatorLeave(const Event_Leave &event, int eventPlayerId, const GameEventContext &context);
|
||||
@@ -218,11 +171,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);
|
||||
@@ -231,7 +184,6 @@ private slots:
|
||||
|
||||
void actGameInfo();
|
||||
void actConcede();
|
||||
void actLeaveGame();
|
||||
void actRemoveLocalArrows();
|
||||
void actRotateViewCW();
|
||||
void actRotateViewCCW();
|
||||
@@ -260,14 +212,15 @@ private slots:
|
||||
|
||||
public:
|
||||
TabGame(TabSupervisor *_tabSupervisor,
|
||||
UserListProxy *_userListProxy,
|
||||
QList<AbstractClient *> &_clients,
|
||||
const Event_GameJoined &event,
|
||||
const QMap<int, QString> &_roomGameTypes);
|
||||
TabGame(TabSupervisor *_tabSupervisor, GameReplay *replay);
|
||||
TabGame(TabSupervisor *_tabSupervisor, UserListProxy *_userListProxy, GameReplay *replay);
|
||||
~TabGame() override;
|
||||
void retranslateUi() override;
|
||||
void updatePlayerListDockTitle();
|
||||
void closeRequest() override;
|
||||
void closeRequest(bool forced = false) override;
|
||||
const QMap<int, Player *> &getPlayers() const
|
||||
{
|
||||
return players;
|
||||
@@ -304,13 +257,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,12 +1,12 @@
|
||||
#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 "stringsizes.h"
|
||||
#include "trice_limits.h"
|
||||
|
||||
#include <QCheckBox>
|
||||
#include <QDialogButtonBox>
|
||||
@@ -21,8 +21,7 @@
|
||||
#include <QtGui>
|
||||
#include <QtWidgets>
|
||||
|
||||
TabLog::TabLog(TabSupervisor *_tabSupervisor, AbstractClient *_client, QWidget *parent)
|
||||
: Tab(_tabSupervisor, parent), client(_client)
|
||||
TabLog::TabLog(TabSupervisor *_tabSupervisor, AbstractClient *_client) : Tab(_tabSupervisor), client(_client)
|
||||
{
|
||||
roomTable = new QTableWidget();
|
||||
roomTable->setColumnCount(6);
|
||||
@@ -53,10 +53,10 @@ private slots:
|
||||
void restartLayout();
|
||||
|
||||
public:
|
||||
TabLog(TabSupervisor *_tabSupervisor, AbstractClient *_client, QWidget *parent = nullptr);
|
||||
~TabLog();
|
||||
void retranslateUi();
|
||||
QString getTabText() const
|
||||
TabLog(TabSupervisor *_tabSupervisor, AbstractClient *_client);
|
||||
~TabLog() override;
|
||||
void retranslateUi() override;
|
||||
QString getTabText() const override
|
||||
{
|
||||
return tr("Logs");
|
||||
}
|
||||
@@ -1,16 +1,17 @@
|
||||
#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 "../../server/user/user_list_manager.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 "stringsizes.h"
|
||||
#include "trice_limits.h"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QDebug>
|
||||
@@ -25,8 +26,8 @@ TabMessage::TabMessage(TabSupervisor *_tabSupervisor,
|
||||
: Tab(_tabSupervisor), client(_client), ownUserInfo(new ServerInfo_User(_ownUserInfo)),
|
||||
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)));
|
||||
chatView = new ChatView(tabSupervisor, tabSupervisor->getUserListManager(), 0, true);
|
||||
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;
|
||||
@@ -38,7 +39,7 @@ TabMessage::TabMessage(TabSupervisor *_tabSupervisor,
|
||||
vbox->addWidget(sayEdit);
|
||||
|
||||
aLeave = new QAction(this);
|
||||
connect(aLeave, SIGNAL(triggered()), this, SLOT(actLeave()));
|
||||
connect(aLeave, &QAction::triggered, this, [this] { closeRequest(); });
|
||||
|
||||
messageMenu = new QMenu(this);
|
||||
messageMenu->addAction(aLeave);
|
||||
@@ -53,7 +54,6 @@ TabMessage::TabMessage(TabSupervisor *_tabSupervisor,
|
||||
|
||||
TabMessage::~TabMessage()
|
||||
{
|
||||
emit talkClosing(this);
|
||||
delete ownUserInfo;
|
||||
delete otherUserInfo;
|
||||
}
|
||||
@@ -86,9 +86,10 @@ QString TabMessage::getTabText() const
|
||||
return tr("%1 - Private chat").arg(QString::fromStdString(otherUserInfo->name()));
|
||||
}
|
||||
|
||||
void TabMessage::closeRequest()
|
||||
void TabMessage::closeRequest(bool /*forced*/)
|
||||
{
|
||||
actLeave();
|
||||
emit talkClosing(this);
|
||||
close();
|
||||
}
|
||||
|
||||
void TabMessage::sendMessage()
|
||||
@@ -114,11 +115,6 @@ void TabMessage::messageSent(const Response &response)
|
||||
"This user is ignoring you, they cannot see your messages in main chat and you cannot join their games."));
|
||||
}
|
||||
|
||||
void TabMessage::actLeave()
|
||||
{
|
||||
deleteLater();
|
||||
}
|
||||
|
||||
void TabMessage::processUserMessageEvent(const Event_UserMessage &event)
|
||||
{
|
||||
auto userInfo = event.sender_name() == otherUserInfo->name() ? otherUserInfo : ownUserInfo;
|
||||
@@ -29,7 +29,6 @@ signals:
|
||||
void maximizeClient();
|
||||
private slots:
|
||||
void sendMessage();
|
||||
void actLeave();
|
||||
void messageSent(const Response &response);
|
||||
void addMentionTag(QString mentionTag);
|
||||
void messageClicked();
|
||||
@@ -39,12 +38,12 @@ public:
|
||||
AbstractClient *_client,
|
||||
const ServerInfo_User &_ownUserInfo,
|
||||
const ServerInfo_User &_otherUserInfo);
|
||||
~TabMessage();
|
||||
void retranslateUi();
|
||||
void closeRequest();
|
||||
void tabActivated();
|
||||
~TabMessage() override;
|
||||
void retranslateUi() override;
|
||||
void closeRequest(bool forced = false) override;
|
||||
void tabActivated() override;
|
||||
QString getUserName() const;
|
||||
QString getTabText() const;
|
||||
QString getTabText() const override;
|
||||
|
||||
void processUserMessageEvent(const Event_UserMessage &event);
|
||||
|
||||
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);
|
||||
|
||||
@@ -49,8 +59,8 @@ signals:
|
||||
|
||||
public:
|
||||
TabReplays(TabSupervisor *_tabSupervisor, AbstractClient *_client);
|
||||
void retranslateUi();
|
||||
QString getTabText() const
|
||||
void retranslateUi() override;
|
||||
QString getTabText() const override
|
||||
{
|
||||
return tr("Game replays");
|
||||
}
|
||||
@@ -1,11 +1,15 @@
|
||||
#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_manager.h"
|
||||
#include "../../server/user/user_list_widget.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 +17,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 "stringsizes.h"
|
||||
#include "tab_account.h"
|
||||
#include "tab_supervisor.h"
|
||||
#include "userlist.h"
|
||||
#include "trice_limits.h"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QCompleter>
|
||||
@@ -35,9 +36,10 @@
|
||||
TabRoom::TabRoom(TabSupervisor *_tabSupervisor,
|
||||
AbstractClient *_client,
|
||||
ServerInfo_User *_ownUser,
|
||||
const UserListProxy *_userListProxy,
|
||||
const ServerInfo_Room &info)
|
||||
: Tab(_tabSupervisor), client(_client), roomId(info.room_id()), roomName(QString::fromStdString(info.name())),
|
||||
ownUser(_ownUser)
|
||||
ownUser(_ownUser), userListProxy(_userListProxy)
|
||||
{
|
||||
const int gameTypeListSize = info.gametype_list_size();
|
||||
for (int i = 0; i < gameTypeListSize; ++i)
|
||||
@@ -47,15 +49,15 @@ TabRoom::TabRoom(TabSupervisor *_tabSupervisor,
|
||||
QMap<int, GameTypeMap> tempMap;
|
||||
tempMap.insert(info.room_id(), gameTypes);
|
||||
gameSelector = new GameSelector(client, tabSupervisor, this, QMap<int, QString>(), tempMap, true, true);
|
||||
userList = new UserList(tabSupervisor, client, UserList::RoomList);
|
||||
userList = new UserListWidget(tabSupervisor, client, UserListWidget::RoomList);
|
||||
connect(userList, SIGNAL(openMessageDialog(const QString &, bool)), this,
|
||||
SIGNAL(openMessageDialog(const QString &, bool)));
|
||||
|
||||
chatView = new ChatView(tabSupervisor, tabSupervisor, nullptr, true, this);
|
||||
chatView = new ChatView(tabSupervisor, userListProxy, nullptr, true, this);
|
||||
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()));
|
||||
@@ -101,7 +103,7 @@ TabRoom::TabRoom(TabSupervisor *_tabSupervisor,
|
||||
hbox->addWidget(userList, 1);
|
||||
|
||||
aLeaveRoom = new QAction(this);
|
||||
connect(aLeaveRoom, SIGNAL(triggered()), this, SLOT(actLeaveRoom()));
|
||||
connect(aLeaveRoom, &QAction::triggered, this, [this] { closeRequest(); });
|
||||
|
||||
roomMenu = new QMenu(this);
|
||||
roomMenu->addAction(aLeaveRoom);
|
||||
@@ -135,11 +137,6 @@ TabRoom::TabRoom(TabSupervisor *_tabSupervisor,
|
||||
setCentralWidget(mainWidget);
|
||||
}
|
||||
|
||||
TabRoom::~TabRoom()
|
||||
{
|
||||
emit roomClosing(this);
|
||||
}
|
||||
|
||||
void TabRoom::retranslateUi()
|
||||
{
|
||||
gameSelector->retranslateUi();
|
||||
@@ -175,9 +172,11 @@ void TabRoom::actShowPopup(const QString &message)
|
||||
}
|
||||
}
|
||||
|
||||
void TabRoom::closeRequest()
|
||||
void TabRoom::closeRequest(bool /*forced*/)
|
||||
{
|
||||
actLeaveRoom();
|
||||
sendRoomCommand(prepareRoomCommand(Command_LeaveRoom()));
|
||||
emit roomClosing(this);
|
||||
close();
|
||||
}
|
||||
|
||||
void TabRoom::tabActivated()
|
||||
@@ -216,12 +215,6 @@ void TabRoom::sayFinished(const Response &response)
|
||||
chatView->appendMessage(tr("You are flooding the chat. Please wait a couple of seconds."));
|
||||
}
|
||||
|
||||
void TabRoom::actLeaveRoom()
|
||||
{
|
||||
sendRoomCommand(prepareRoomCommand(Command_LeaveRoom()));
|
||||
deleteLater();
|
||||
}
|
||||
|
||||
void TabRoom::actClearChat()
|
||||
{
|
||||
chatView->clearChat();
|
||||
@@ -291,7 +284,7 @@ void TabRoom::processRoomSayEvent(const Event_RoomSay &event)
|
||||
QString senderName = QString::fromStdString(event.name());
|
||||
QString message = QString::fromStdString(event.message());
|
||||
|
||||
if (tabSupervisor->getUserListsTab()->getIgnoreList()->getUsers().contains(senderName))
|
||||
if (userListProxy->getOnlineUser(senderName))
|
||||
return;
|
||||
|
||||
UserListTWI *twi = userList->getUsers().value(senderName);
|
||||
@@ -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>
|
||||
@@ -9,6 +9,8 @@
|
||||
#include <QKeyEvent>
|
||||
#include <QMap>
|
||||
|
||||
class UserListProxy;
|
||||
class UserListManager;
|
||||
namespace google
|
||||
{
|
||||
namespace protobuf
|
||||
@@ -17,7 +19,7 @@ class Message;
|
||||
}
|
||||
} // namespace google
|
||||
class AbstractClient;
|
||||
class UserList;
|
||||
class UserListWidget;
|
||||
class QLabel;
|
||||
class ChatView;
|
||||
class QPushButton;
|
||||
@@ -48,7 +50,8 @@ private:
|
||||
QMap<int, QString> gameTypes;
|
||||
|
||||
GameSelector *gameSelector;
|
||||
UserList *userList;
|
||||
UserListWidget *userList;
|
||||
const UserListProxy *userListProxy;
|
||||
ChatView *chatView;
|
||||
QLabel *sayLabel;
|
||||
LineEditCompleter *sayEdit;
|
||||
@@ -70,7 +73,6 @@ signals:
|
||||
private slots:
|
||||
void sendMessage();
|
||||
void sayFinished(const Response &response);
|
||||
void actLeaveRoom();
|
||||
void actClearChat();
|
||||
void actOpenChatSettings();
|
||||
void addMentionTag(QString mentionTag);
|
||||
@@ -90,11 +92,11 @@ public:
|
||||
TabRoom(TabSupervisor *_tabSupervisor,
|
||||
AbstractClient *_client,
|
||||
ServerInfo_User *_ownUser,
|
||||
const UserListProxy *_userListProxy,
|
||||
const ServerInfo_Room &info);
|
||||
~TabRoom();
|
||||
void retranslateUi();
|
||||
void closeRequest();
|
||||
void tabActivated();
|
||||
void retranslateUi() override;
|
||||
void closeRequest(bool forced = false) override;
|
||||
void tabActivated() override;
|
||||
void processRoomEvent(const RoomEvent &event);
|
||||
int getRoomId() const
|
||||
{
|
||||
@@ -108,7 +110,7 @@ public:
|
||||
{
|
||||
return roomName;
|
||||
}
|
||||
QString getTabText() const
|
||||
QString getTabText() const override
|
||||
{
|
||||
return roomName;
|
||||
}
|
||||
@@ -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_widget.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>
|
||||
@@ -138,8 +138,7 @@ void RoomSelector::joinClicked()
|
||||
emit joinRoomRequest(id, true);
|
||||
}
|
||||
|
||||
TabServer::TabServer(TabSupervisor *_tabSupervisor, AbstractClient *_client, QWidget *parent)
|
||||
: Tab(_tabSupervisor, parent), client(_client)
|
||||
TabServer::TabServer(TabSupervisor *_tabSupervisor, AbstractClient *_client) : Tab(_tabSupervisor), client(_client)
|
||||
{
|
||||
roomSelector = new RoomSelector(client);
|
||||
serverInfoBox = new QTextBrowser;
|
||||
@@ -10,7 +10,7 @@
|
||||
class AbstractClient;
|
||||
class QTextEdit;
|
||||
class QLabel;
|
||||
class UserList;
|
||||
class UserListWidget;
|
||||
class QPushButton;
|
||||
|
||||
class Event_ListRooms;
|
||||
@@ -34,7 +34,7 @@ signals:
|
||||
void joinRoomRequest(int, bool setCurrent);
|
||||
|
||||
public:
|
||||
RoomSelector(AbstractClient *_client, QWidget *parent = nullptr);
|
||||
explicit RoomSelector(AbstractClient *_client, QWidget *parent = nullptr);
|
||||
void retranslateUi();
|
||||
};
|
||||
|
||||
@@ -55,9 +55,9 @@ private:
|
||||
bool shouldEmitUpdate = false;
|
||||
|
||||
public:
|
||||
TabServer(TabSupervisor *_tabSupervisor, AbstractClient *_client, QWidget *parent = nullptr);
|
||||
void retranslateUi();
|
||||
QString getTabText() const
|
||||
TabServer(TabSupervisor *_tabSupervisor, AbstractClient *_client);
|
||||
void retranslateUi() override;
|
||||
QString getTabText() const override
|
||||
{
|
||||
return tr("Server");
|
||||
}
|
||||
@@ -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_manager.h"
|
||||
#include "../../server/user/user_list_widget.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/moderator_commands.pb.h"
|
||||
#include "pb/game_replay.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,10 +25,9 @@
|
||||
#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>
|
||||
#include <QMessageBox>
|
||||
#include <QPainter>
|
||||
#include <QSystemTrayIcon>
|
||||
@@ -47,15 +48,15 @@ CloseButton::CloseButton(QWidget *parent) : QAbstractButton(parent)
|
||||
{
|
||||
setFocusPolicy(Qt::NoFocus);
|
||||
setCursor(Qt::ArrowCursor);
|
||||
resize(sizeHint());
|
||||
resize(this->sizeHint());
|
||||
}
|
||||
|
||||
QSize CloseButton::sizeHint() const
|
||||
{
|
||||
ensurePolished();
|
||||
int width = style()->pixelMetric(QStyle::PM_TabCloseIndicatorWidth, 0, this);
|
||||
int height = style()->pixelMetric(QStyle::PM_TabCloseIndicatorHeight, 0, this);
|
||||
return QSize(width, height);
|
||||
int width = style()->pixelMetric(QStyle::PM_TabCloseIndicatorWidth, nullptr, this);
|
||||
int height = style()->pixelMetric(QStyle::PM_TabCloseIndicatorHeight, nullptr, this);
|
||||
return {width, height};
|
||||
}
|
||||
|
||||
#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
|
||||
@@ -87,10 +88,9 @@ void CloseButton::paintEvent(QPaintEvent * /*event*/)
|
||||
if (isDown())
|
||||
opt.state |= QStyle::State_Sunken;
|
||||
|
||||
if (const QTabBar *tb = qobject_cast<const QTabBar *>(parent())) {
|
||||
if (const auto *tb = qobject_cast<const QTabBar *>(parent())) {
|
||||
int index = tb->currentIndex();
|
||||
QTabBar::ButtonPosition position =
|
||||
(QTabBar::ButtonPosition)style()->styleHint(QStyle::SH_TabBar_CloseButtonPosition, 0, tb);
|
||||
auto position = (QTabBar::ButtonPosition)style()->styleHint(QStyle::SH_TabBar_CloseButtonPosition, nullptr, tb);
|
||||
if (tb->tabButton(index, position) == this)
|
||||
opt.state |= QStyle::State_Selected;
|
||||
}
|
||||
@@ -98,9 +98,10 @@ void CloseButton::paintEvent(QPaintEvent * /*event*/)
|
||||
style()->drawPrimitive(QStyle::PE_IndicatorTabClose, &opt, &p, this);
|
||||
}
|
||||
|
||||
TabSupervisor::TabSupervisor(AbstractClient *_client, QWidget *parent)
|
||||
: QTabWidget(parent), userInfo(0), client(_client), tabServer(0), tabUserLists(0), tabDeckStorage(0), tabReplays(0),
|
||||
tabAdmin(0), tabLog(0)
|
||||
TabSupervisor::TabSupervisor(AbstractClient *_client, QMenu *tabsMenu, QWidget *parent)
|
||||
: QTabWidget(parent), userInfo(nullptr), client(_client), tabsMenu(tabsMenu), tabVisualDeckStorage(nullptr),
|
||||
tabServer(nullptr), tabAccount(nullptr), tabDeckStorage(nullptr), tabReplays(nullptr), tabAdmin(nullptr),
|
||||
tabLog(nullptr), isLocalGame(false)
|
||||
{
|
||||
setElideMode(Qt::ElideRight);
|
||||
setMovable(true);
|
||||
@@ -112,18 +113,56 @@ TabSupervisor::TabSupervisor(AbstractClient *_client, QWidget *parent)
|
||||
tabBar()->setStyle(new MacOSTabFixStyle);
|
||||
#endif
|
||||
|
||||
connect(this, SIGNAL(currentChanged(int)), this, SLOT(updateCurrent(int)));
|
||||
userListManager = new UserListManager(client, this);
|
||||
|
||||
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 tab changes
|
||||
connect(this, &TabSupervisor::currentChanged, this, &TabSupervisor::updateCurrent);
|
||||
|
||||
// connect client
|
||||
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);
|
||||
|
||||
// create tabs menu actions
|
||||
aTabDeckEditor = new QAction(this);
|
||||
connect(aTabDeckEditor, &QAction::triggered, this, [this] { addDeckEditorTab(nullptr); });
|
||||
|
||||
aTabVisualDeckStorage = new QAction(this);
|
||||
aTabVisualDeckStorage->setCheckable(true);
|
||||
connect(aTabVisualDeckStorage, &QAction::triggered, this, &TabSupervisor::actTabVisualDeckStorage);
|
||||
|
||||
aTabServer = new QAction(this);
|
||||
aTabServer->setCheckable(true);
|
||||
connect(aTabServer, &QAction::triggered, this, &TabSupervisor::actTabServer);
|
||||
|
||||
aTabAccount = new QAction(this);
|
||||
aTabAccount->setCheckable(true);
|
||||
connect(aTabAccount, &QAction::triggered, this, &TabSupervisor::actTabAccount);
|
||||
|
||||
aTabDeckStorage = new QAction(this);
|
||||
aTabDeckStorage->setCheckable(true);
|
||||
connect(aTabDeckStorage, &QAction::triggered, this, &TabSupervisor::actTabDeckStorage);
|
||||
|
||||
aTabReplays = new QAction(this);
|
||||
aTabReplays->setCheckable(true);
|
||||
connect(aTabReplays, &QAction::triggered, this, &TabSupervisor::actTabReplays);
|
||||
|
||||
aTabAdmin = new QAction(this);
|
||||
aTabAdmin->setCheckable(true);
|
||||
connect(aTabAdmin, &QAction::triggered, this, &TabSupervisor::actTabAdmin);
|
||||
|
||||
aTabLog = new QAction(this);
|
||||
aTabLog->setCheckable(true);
|
||||
connect(aTabLog, &QAction::triggered, this, &TabSupervisor::actTabLog);
|
||||
|
||||
connect(&SettingsCache::instance().shortcuts(), &ShortcutsSettings::shortCutChanged, this,
|
||||
&TabSupervisor::refreshShortcuts);
|
||||
refreshShortcuts();
|
||||
|
||||
resetTabsMenu();
|
||||
|
||||
retranslateUi();
|
||||
}
|
||||
@@ -135,12 +174,23 @@ TabSupervisor::~TabSupervisor()
|
||||
|
||||
void TabSupervisor::retranslateUi()
|
||||
{
|
||||
// tab menu actions
|
||||
aTabDeckEditor->setText(tr("Deck Editor"));
|
||||
aTabVisualDeckStorage->setText(tr("&Visual Deck storage"));
|
||||
aTabServer->setText(tr("Server"));
|
||||
aTabAccount->setText(tr("Account"));
|
||||
aTabDeckStorage->setText(tr("Deck storage"));
|
||||
aTabReplays->setText(tr("Game replays"));
|
||||
aTabAdmin->setText(tr("Administration"));
|
||||
aTabLog->setText(tr("Logs"));
|
||||
|
||||
// tabs
|
||||
QList<Tab *> tabs;
|
||||
tabs.append(tabServer);
|
||||
tabs.append(tabReplays);
|
||||
tabs.append(tabDeckStorage);
|
||||
tabs.append(tabAdmin);
|
||||
tabs.append(tabUserLists);
|
||||
tabs.append(tabAccount);
|
||||
tabs.append(tabLog);
|
||||
QMapIterator<int, TabRoom *> roomIterator(roomTabs);
|
||||
while (roomIterator.hasNext())
|
||||
@@ -158,14 +208,21 @@ void TabSupervisor::retranslateUi()
|
||||
while (messageIterator.hasNext())
|
||||
tabs.append(messageIterator.next().value());
|
||||
|
||||
for (int i = 0; i < tabs.size(); ++i)
|
||||
if (tabs[i]) {
|
||||
int idx = indexOf(tabs[i]);
|
||||
QString tabText = tabs[i]->getTabText();
|
||||
for (auto &tab : tabs) {
|
||||
if (tab) {
|
||||
int idx = indexOf(tab);
|
||||
QString tabText = tab->getTabText();
|
||||
setTabText(idx, sanitizeTabName(tabText));
|
||||
setTabToolTip(idx, sanitizeHtml(tabText));
|
||||
tabs[i]->retranslateUi();
|
||||
tab->retranslateUi();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void TabSupervisor::refreshShortcuts()
|
||||
{
|
||||
ShortcutsSettings &shortcuts = SettingsCache::instance().shortcuts();
|
||||
aTabDeckEditor->setShortcuts(shortcuts.getShortcut("MainWindow/aDeckEditor"));
|
||||
}
|
||||
|
||||
bool TabSupervisor::closeRequest()
|
||||
@@ -178,7 +235,7 @@ bool TabSupervisor::closeRequest()
|
||||
}
|
||||
}
|
||||
|
||||
foreach (TabDeckEditor *tab, deckEditorTabs) {
|
||||
for (TabDeckEditor *tab : deckEditorTabs) {
|
||||
if (!tab->confirmClose())
|
||||
return false;
|
||||
}
|
||||
@@ -191,71 +248,127 @@ AbstractClient *TabSupervisor::getClient() const
|
||||
return localClients.isEmpty() ? client : localClients.first();
|
||||
}
|
||||
|
||||
QString TabSupervisor::sanitizeTabName(QString dirty) const
|
||||
QString TabSupervisor::sanitizeTabName(QString dirty)
|
||||
{
|
||||
return dirty.replace("&", "&&");
|
||||
}
|
||||
|
||||
QString TabSupervisor::sanitizeHtml(QString dirty) const
|
||||
QString TabSupervisor::sanitizeHtml(QString dirty)
|
||||
{
|
||||
return dirty.replace("&", "&").replace("<", "<").replace(">", ">").replace("\"", """);
|
||||
}
|
||||
|
||||
int TabSupervisor::myAddTab(Tab *tab)
|
||||
/**
|
||||
* If the action is not in the target checked state, then set it to that state by triggering the action.
|
||||
* If the action is already in the target checked state, then do nothing.
|
||||
*
|
||||
* This allows us to programmatically trigger a QAction::triggered signal for a specific checked state.
|
||||
*/
|
||||
static void checkAndTrigger(QAction *checkableAction, bool checked)
|
||||
{
|
||||
connect(tab, SIGNAL(userEvent(bool)), this, SLOT(tabUserEvent(bool)));
|
||||
connect(tab, SIGNAL(tabTextChanged(Tab *, QString)), this, SLOT(updateTabText(Tab *, QString)));
|
||||
if (checkableAction->isChecked() != checked) {
|
||||
checkableAction->trigger();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the always-available tabs, depending on settings.
|
||||
*/
|
||||
void TabSupervisor::initStartupTabs()
|
||||
{
|
||||
addDeckEditorTab(nullptr);
|
||||
|
||||
checkAndTrigger(aTabVisualDeckStorage, SettingsCache::instance().getTabVisualDeckStorageOpen());
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the tab to the TabSupervisor's tab bar.
|
||||
*
|
||||
* @param tab The Tab to add
|
||||
* @param manager The menu action that corresponds to this tab, if this is a single-instance managed tab. Pass in
|
||||
* nullptr if this is not a managed tab.
|
||||
* @return The index of the added tab in the tab widget's tab menu
|
||||
*/
|
||||
int TabSupervisor::myAddTab(Tab *tab, QAction *manager)
|
||||
{
|
||||
connect(tab, &TabGame::userEvent, this, &TabSupervisor::tabUserEvent);
|
||||
connect(tab, &TabGame::tabTextChanged, this, &TabSupervisor::updateTabText);
|
||||
|
||||
QString tabText = tab->getTabText();
|
||||
int idx = addTab(tab, sanitizeTabName(tabText));
|
||||
setTabToolTip(idx, sanitizeHtml(tabText));
|
||||
|
||||
addCloseButtonToTab(tab, idx, manager);
|
||||
|
||||
return idx;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a usable close button to the tab.
|
||||
*
|
||||
* @param tab The Tab
|
||||
* @param tabIndex The tab bar index of the tab
|
||||
* @param manager The menu action that corresponds to this tab, if this is a single-instance managed tab. Pass in
|
||||
* nullptr if this is not a managed tab.
|
||||
*/
|
||||
void TabSupervisor::addCloseButtonToTab(Tab *tab, int tabIndex, QAction *manager)
|
||||
{
|
||||
auto closeSide = static_cast<QTabBar::ButtonPosition>(
|
||||
tabBar()->style()->styleHint(QStyle::SH_TabBar_CloseButtonPosition, nullptr, tabBar()));
|
||||
auto *closeButton = new CloseButton(tab);
|
||||
if (manager) {
|
||||
// If managed, all close requests should go through the menu action
|
||||
connect(closeButton, &CloseButton::clicked, this, [manager] { checkAndTrigger(manager, false); });
|
||||
} else {
|
||||
connect(closeButton, &CloseButton::clicked, tab, [tab] { tab->closeRequest(); });
|
||||
}
|
||||
tabBar()->setTabButton(tabIndex, closeSide, closeButton);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the tabs menu to the tabs that are always available
|
||||
*/
|
||||
void TabSupervisor::resetTabsMenu()
|
||||
{
|
||||
tabsMenu->clear();
|
||||
tabsMenu->addAction(aTabDeckEditor);
|
||||
tabsMenu->addSeparator();
|
||||
tabsMenu->addAction(aTabVisualDeckStorage);
|
||||
}
|
||||
|
||||
void TabSupervisor::start(const ServerInfo_User &_userInfo)
|
||||
{
|
||||
isLocalGame = false;
|
||||
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)));
|
||||
myAddTab(tabServer);
|
||||
userListManager->handleConnect();
|
||||
|
||||
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 &)));
|
||||
myAddTab(tabUserLists);
|
||||
resetTabsMenu();
|
||||
|
||||
tabsMenu->addSeparator();
|
||||
tabsMenu->addAction(aTabServer);
|
||||
tabsMenu->addAction(aTabAccount);
|
||||
|
||||
checkAndTrigger(aTabServer, SettingsCache::instance().getTabServerOpen());
|
||||
checkAndTrigger(aTabAccount, SettingsCache::instance().getTabAccountOpen());
|
||||
|
||||
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 *)));
|
||||
myAddTab(tabDeckStorage);
|
||||
tabsMenu->addAction(aTabDeckStorage);
|
||||
tabsMenu->addAction(aTabReplays);
|
||||
|
||||
tabReplays = new TabReplays(this, client);
|
||||
connect(tabReplays, SIGNAL(openReplay(GameReplay *)), this, SLOT(openReplay(GameReplay *)));
|
||||
myAddTab(tabReplays);
|
||||
} else {
|
||||
tabDeckStorage = 0;
|
||||
tabReplays = 0;
|
||||
checkAndTrigger(aTabDeckStorage, SettingsCache::instance().getTabDeckStorageOpen());
|
||||
checkAndTrigger(aTabReplays, SettingsCache::instance().getTabReplaysOpen());
|
||||
}
|
||||
|
||||
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)));
|
||||
myAddTab(tabAdmin);
|
||||
tabsMenu->addSeparator();
|
||||
tabsMenu->addAction(aTabAdmin);
|
||||
tabsMenu->addAction(aTabLog);
|
||||
|
||||
tabLog = new TabLog(this, client);
|
||||
myAddTab(tabLog);
|
||||
} else {
|
||||
tabAdmin = 0;
|
||||
tabLog = 0;
|
||||
checkAndTrigger(aTabAdmin, SettingsCache::instance().getTabAdminOpen());
|
||||
checkAndTrigger(aTabLog, SettingsCache::instance().getTabLogOpen());
|
||||
}
|
||||
|
||||
retranslateUi();
|
||||
@@ -263,70 +376,195 @@ void TabSupervisor::start(const ServerInfo_User &_userInfo)
|
||||
|
||||
void TabSupervisor::startLocal(const QList<AbstractClient *> &_clients)
|
||||
{
|
||||
tabUserLists = 0;
|
||||
tabDeckStorage = 0;
|
||||
tabReplays = 0;
|
||||
tabAdmin = 0;
|
||||
tabLog = 0;
|
||||
resetTabsMenu();
|
||||
|
||||
tabAccount = nullptr;
|
||||
tabDeckStorage = nullptr;
|
||||
tabReplays = nullptr;
|
||||
tabAdmin = nullptr;
|
||||
tabLog = nullptr;
|
||||
isLocalGame = true;
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Call this when Cockatrice disconnects from the server in order to clean up.
|
||||
*/
|
||||
void TabSupervisor::stop()
|
||||
{
|
||||
if ((!client) && localClients.isEmpty())
|
||||
return;
|
||||
|
||||
resetTabsMenu();
|
||||
|
||||
if (!localClients.isEmpty()) {
|
||||
for (int i = 0; i < localClients.size(); ++i)
|
||||
localClients[i]->deleteLater();
|
||||
for (auto &localClient : localClients) {
|
||||
localClient->deleteLater();
|
||||
}
|
||||
localClients.clear();
|
||||
|
||||
emit localGameEnded();
|
||||
} else {
|
||||
if (tabUserLists)
|
||||
tabUserLists->deleteLater();
|
||||
if (tabServer)
|
||||
tabServer->deleteLater();
|
||||
if (tabDeckStorage)
|
||||
tabDeckStorage->deleteLater();
|
||||
if (tabReplays)
|
||||
tabReplays->deleteLater();
|
||||
if (tabAdmin)
|
||||
tabAdmin->deleteLater();
|
||||
if (tabLog)
|
||||
tabLog->deleteLater();
|
||||
if (tabAccount) {
|
||||
tabAccount->closeRequest(true);
|
||||
}
|
||||
if (tabServer) {
|
||||
tabServer->closeRequest(true);
|
||||
}
|
||||
if (tabDeckStorage) {
|
||||
tabDeckStorage->closeRequest(true);
|
||||
}
|
||||
if (tabReplays) {
|
||||
tabReplays->closeRequest(true);
|
||||
}
|
||||
if (tabAdmin) {
|
||||
tabAdmin->closeRequest(true);
|
||||
}
|
||||
if (tabLog) {
|
||||
tabLog->closeRequest(true);
|
||||
}
|
||||
}
|
||||
tabUserLists = 0;
|
||||
tabServer = 0;
|
||||
tabDeckStorage = 0;
|
||||
tabReplays = 0;
|
||||
tabAdmin = 0;
|
||||
tabLog = 0;
|
||||
|
||||
QMapIterator<int, TabRoom *> roomIterator(roomTabs);
|
||||
while (roomIterator.hasNext())
|
||||
roomIterator.next().value()->deleteLater();
|
||||
roomTabs.clear();
|
||||
QList<Tab *> tabsToDelete;
|
||||
|
||||
QMapIterator<int, TabGame *> gameIterator(gameTabs);
|
||||
while (gameIterator.hasNext())
|
||||
gameIterator.next().value()->deleteLater();
|
||||
gameTabs.clear();
|
||||
for (auto i = roomTabs.cbegin(), end = roomTabs.cend(); i != end; ++i) {
|
||||
tabsToDelete << i.value();
|
||||
}
|
||||
|
||||
QListIterator<TabGame *> replayIterator(replayTabs);
|
||||
while (replayIterator.hasNext())
|
||||
replayIterator.next()->deleteLater();
|
||||
replayTabs.clear();
|
||||
for (auto i = gameTabs.cbegin(), end = gameTabs.cend(); i != end; ++i) {
|
||||
tabsToDelete << i.value();
|
||||
}
|
||||
|
||||
for (auto i = messageTabs.cbegin(), end = messageTabs.cend(); i != end; ++i) {
|
||||
tabsToDelete << i.value();
|
||||
}
|
||||
|
||||
for (const auto tab : tabsToDelete) {
|
||||
tab->closeRequest(true);
|
||||
}
|
||||
|
||||
userListManager->handleDisconnect();
|
||||
|
||||
delete userInfo;
|
||||
userInfo = 0;
|
||||
userInfo = nullptr;
|
||||
}
|
||||
|
||||
void TabSupervisor::actTabVisualDeckStorage(bool checked)
|
||||
{
|
||||
SettingsCache::instance().setTabVisualDeckStorageOpen(checked);
|
||||
if (checked && !tabVisualDeckStorage) {
|
||||
tabVisualDeckStorage = new TabDeckStorageVisual(this);
|
||||
myAddTab(tabVisualDeckStorage, aTabVisualDeckStorage);
|
||||
setCurrentWidget(tabVisualDeckStorage);
|
||||
connect(tabVisualDeckStorage, &Tab::closed, this, [this] {
|
||||
tabVisualDeckStorage = nullptr;
|
||||
aTabVisualDeckStorage->setChecked(false);
|
||||
});
|
||||
} else if (!checked && tabVisualDeckStorage) {
|
||||
tabVisualDeckStorage->closeRequest();
|
||||
}
|
||||
}
|
||||
|
||||
void TabSupervisor::actTabServer(bool checked)
|
||||
{
|
||||
SettingsCache::instance().setTabServerOpen(checked);
|
||||
if (checked && !tabServer) {
|
||||
tabServer = new TabServer(this, client);
|
||||
connect(tabServer, &TabServer::roomJoined, this, &TabSupervisor::addRoomTab);
|
||||
myAddTab(tabServer, aTabServer);
|
||||
connect(tabServer, &Tab::closed, this, [this] {
|
||||
tabServer = nullptr;
|
||||
aTabServer->setChecked(false);
|
||||
});
|
||||
} else if (!checked && tabServer) {
|
||||
tabServer->closeRequest();
|
||||
}
|
||||
}
|
||||
|
||||
void TabSupervisor::actTabAccount(bool checked)
|
||||
{
|
||||
SettingsCache::instance().setTabAccountOpen(checked);
|
||||
if (checked && !tabAccount) {
|
||||
tabAccount = new TabAccount(this, client, *userInfo);
|
||||
connect(tabAccount, &TabAccount::openMessageDialog, this, &TabSupervisor::addMessageTab);
|
||||
connect(tabAccount, &TabAccount::userJoined, this, &TabSupervisor::processUserJoined);
|
||||
connect(tabAccount, &TabAccount::userLeft, this, &TabSupervisor::processUserLeft);
|
||||
myAddTab(tabAccount, aTabAccount);
|
||||
connect(tabAccount, &Tab::closed, this, [this] {
|
||||
tabAccount = nullptr;
|
||||
aTabAccount->setChecked(false);
|
||||
});
|
||||
} else if (!checked && tabAccount) {
|
||||
tabAccount->closeRequest();
|
||||
}
|
||||
}
|
||||
|
||||
void TabSupervisor::actTabDeckStorage(bool checked)
|
||||
{
|
||||
SettingsCache::instance().setTabDeckStorageOpen(checked);
|
||||
if (checked && !tabDeckStorage) {
|
||||
tabDeckStorage = new TabDeckStorage(this, client);
|
||||
connect(tabDeckStorage, &TabDeckStorage::openDeckEditor, this, &TabSupervisor::addDeckEditorTab);
|
||||
myAddTab(tabDeckStorage, aTabDeckStorage);
|
||||
connect(tabDeckStorage, &Tab::closed, this, [this] {
|
||||
tabDeckStorage = nullptr;
|
||||
aTabDeckStorage->setChecked(false);
|
||||
});
|
||||
} else if (!checked && tabDeckStorage) {
|
||||
tabDeckStorage->closeRequest();
|
||||
}
|
||||
}
|
||||
|
||||
void TabSupervisor::actTabReplays(bool checked)
|
||||
{
|
||||
SettingsCache::instance().setTabReplaysOpen(checked);
|
||||
if (checked && !tabReplays) {
|
||||
tabReplays = new TabReplays(this, client);
|
||||
connect(tabReplays, &TabReplays::openReplay, this, &TabSupervisor::openReplay);
|
||||
myAddTab(tabReplays, aTabReplays);
|
||||
connect(tabReplays, &Tab::closed, this, [this] {
|
||||
tabReplays = nullptr;
|
||||
aTabReplays->setChecked(false);
|
||||
});
|
||||
} else if (!checked && tabReplays) {
|
||||
tabReplays->closeRequest();
|
||||
}
|
||||
}
|
||||
|
||||
void TabSupervisor::actTabAdmin(bool checked)
|
||||
{
|
||||
SettingsCache::instance().setTabAdminOpen(checked);
|
||||
if (checked && !tabAdmin) {
|
||||
tabAdmin = new TabAdmin(this, client, (userInfo->user_level() & ServerInfo_User::IsAdmin));
|
||||
connect(tabAdmin, &TabAdmin::adminLockChanged, this, &TabSupervisor::adminLockChanged);
|
||||
myAddTab(tabAdmin, aTabAdmin);
|
||||
connect(tabAdmin, &Tab::closed, this, [this] {
|
||||
tabAdmin = nullptr;
|
||||
aTabAdmin->setChecked(false);
|
||||
});
|
||||
} else if (!checked && tabAdmin) {
|
||||
tabAdmin->closeRequest();
|
||||
}
|
||||
}
|
||||
|
||||
void TabSupervisor::actTabLog(bool checked)
|
||||
{
|
||||
SettingsCache::instance().setTabLogOpen(checked);
|
||||
if (checked && !tabLog) {
|
||||
tabLog = new TabLog(this, client);
|
||||
myAddTab(tabLog, aTabLog);
|
||||
connect(tabLog, &Tab::closed, this, [this] {
|
||||
tabLog = nullptr;
|
||||
aTabAdmin->setChecked(false);
|
||||
});
|
||||
} else if (!checked && tabLog) {
|
||||
tabLog->closeRequest();
|
||||
}
|
||||
}
|
||||
|
||||
void TabSupervisor::updatePingTime(int value, int max)
|
||||
@@ -339,22 +577,6 @@ void TabSupervisor::updatePingTime(int value, int max)
|
||||
setTabIcon(indexOf(tabServer), QIcon(PingPixmapGenerator::generatePixmap(15, value, max)));
|
||||
}
|
||||
|
||||
void TabSupervisor::closeButtonPressed()
|
||||
{
|
||||
Tab *tab = static_cast<Tab *>(static_cast<CloseButton *>(sender())->property("tab").value<QObject *>());
|
||||
tab->closeRequest();
|
||||
}
|
||||
|
||||
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()));
|
||||
closeButton->setProperty("tab", QVariant::fromValue((QObject *)tab));
|
||||
tabBar()->setTabButton(tabIndex, closeSide, closeButton);
|
||||
}
|
||||
|
||||
void TabSupervisor::gameJoined(const Event_GameJoined &event)
|
||||
{
|
||||
QMap<int, QString> roomGameTypes;
|
||||
@@ -366,23 +588,21 @@ void TabSupervisor::gameJoined(const Event_GameJoined &event)
|
||||
roomGameTypes.insert(event.game_types(i).game_type_id(),
|
||||
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 *)));
|
||||
int tabIndex = myAddTab(tab);
|
||||
addCloseButtonToTab(tab, tabIndex);
|
||||
auto *tab = new TabGame(this, userListManager, QList<AbstractClient *>() << client, event, roomGameTypes);
|
||||
connect(tab, &TabGame::gameClosing, this, &TabSupervisor::gameLeft);
|
||||
connect(tab, &TabGame::openMessageDialog, this, &TabSupervisor::addMessageTab);
|
||||
connect(tab, &TabGame::openDeckEditor, this, &TabSupervisor::addDeckEditorTab);
|
||||
myAddTab(tab);
|
||||
gameTabs.insert(event.game_info().game_id(), tab);
|
||||
setCurrentWidget(tab);
|
||||
}
|
||||
|
||||
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 *)));
|
||||
int tabIndex = myAddTab(tab);
|
||||
addCloseButtonToTab(tab, tabIndex);
|
||||
auto *tab = new TabGame(this, userListManager, localClients, event, QMap<int, QString>());
|
||||
connect(tab, &TabGame::gameClosing, this, &TabSupervisor::gameLeft);
|
||||
connect(tab, &TabGame::openDeckEditor, this, &TabSupervisor::addDeckEditorTab);
|
||||
myAddTab(tab);
|
||||
gameTabs.insert(event.game_info().game_id(), tab);
|
||||
setCurrentWidget(tab);
|
||||
|
||||
@@ -407,12 +627,11 @@ 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)));
|
||||
int tabIndex = myAddTab(tab);
|
||||
addCloseButtonToTab(tab, tabIndex);
|
||||
auto *tab = new TabRoom(this, client, userInfo, userListManager, info);
|
||||
connect(tab, &TabRoom::maximizeClient, this, &TabSupervisor::maximizeMainWindow);
|
||||
connect(tab, &TabRoom::roomClosing, this, &TabSupervisor::roomLeft);
|
||||
connect(tab, &TabRoom::openMessageDialog, this, &TabSupervisor::addMessageTab);
|
||||
myAddTab(tab);
|
||||
roomTabs.insert(info.room_id(), tab);
|
||||
if (setCurrent)
|
||||
setCurrentWidget(tab);
|
||||
@@ -429,10 +648,9 @@ 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 *)));
|
||||
int tabIndex = myAddTab(replayTab);
|
||||
addCloseButtonToTab(replayTab, tabIndex);
|
||||
auto *replayTab = new TabGame(this, userListManager, replay);
|
||||
connect(replayTab, &TabGame::gameClosing, this, &TabSupervisor::replayLeft);
|
||||
myAddTab(replayTab);
|
||||
replayTabs.append(replayTab);
|
||||
setCurrentWidget(replayTab);
|
||||
}
|
||||
@@ -451,11 +669,11 @@ TabMessage *TabSupervisor::addMessageTab(const QString &receiverName, bool focus
|
||||
return nullptr;
|
||||
|
||||
ServerInfo_User otherUser;
|
||||
UserListTWI *twi = tabUserLists->getAllUsersList()->getUsers().value(receiverName);
|
||||
if (twi)
|
||||
otherUser = twi->getUserInfo();
|
||||
else
|
||||
if (auto user = userListManager->getOnlineUser(receiverName)) {
|
||||
otherUser = ServerInfo_User(*user);
|
||||
} else {
|
||||
otherUser.set_name(receiverName.toStdString());
|
||||
}
|
||||
|
||||
TabMessage *tab;
|
||||
tab = messageTabs.value(QString::fromStdString(otherUser.name()));
|
||||
@@ -466,10 +684,9 @@ 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()));
|
||||
int tabIndex = myAddTab(tab);
|
||||
addCloseButtonToTab(tab, tabIndex);
|
||||
connect(tab, &TabMessage::talkClosing, this, &TabSupervisor::talkLeft);
|
||||
connect(tab, &TabMessage::maximizeClient, this, &TabSupervisor::maximizeMainWindow);
|
||||
myAddTab(tab);
|
||||
messageTabs.insert(receiverName, tab);
|
||||
if (focus)
|
||||
setCurrentWidget(tab);
|
||||
@@ -492,12 +709,12 @@ void TabSupervisor::talkLeft(TabMessage *tab)
|
||||
|
||||
TabDeckEditor *TabSupervisor::addDeckEditorTab(const DeckLoader *deckToOpen)
|
||||
{
|
||||
TabDeckEditor *tab = new TabDeckEditor(this);
|
||||
auto *tab = new TabDeckEditor(this);
|
||||
if (deckToOpen)
|
||||
tab->setDeck(new DeckLoader(*deckToOpen));
|
||||
connect(tab, SIGNAL(deckEditorClosing(TabDeckEditor *)), this, SLOT(deckEditorClosed(TabDeckEditor *)));
|
||||
int tabIndex = myAddTab(tab);
|
||||
addCloseButtonToTab(tab, tabIndex);
|
||||
connect(tab, &TabDeckEditor::deckEditorClosing, this, &TabSupervisor::deckEditorClosed);
|
||||
connect(tab, &TabDeckEditor::openDeckEditor, this, &TabSupervisor::addDeckEditorTab);
|
||||
myAddTab(tab);
|
||||
deckEditorTabs.append(tab);
|
||||
setCurrentWidget(tab);
|
||||
return tab;
|
||||
@@ -541,7 +758,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";
|
||||
}
|
||||
@@ -553,9 +770,9 @@ void TabSupervisor::processUserMessageEvent(const Event_UserMessage &event)
|
||||
if (!tab)
|
||||
tab = messageTabs.value(QString::fromStdString(event.receiver_name()));
|
||||
if (!tab) {
|
||||
UserListTWI *twi = tabUserLists->getAllUsersList()->getUsers().value(senderName);
|
||||
if (twi) {
|
||||
UserLevelFlags userLevel = UserLevelFlags(twi->getUserInfo().user_level());
|
||||
const ServerInfo_User *onlineUserInfo = userListManager->getOnlineUser(senderName);
|
||||
if (onlineUserInfo) {
|
||||
auto userLevel = UserLevelFlags(onlineUserInfo->user_level());
|
||||
if (SettingsCache::instance().getIgnoreUnregisteredUserMessages() &&
|
||||
!userLevel.testFlag(ServerInfo_User::IsRegistered))
|
||||
// Flags are additive, so reg/mod/admin are all IsRegistered
|
||||
@@ -589,15 +806,15 @@ void TabSupervisor::processUserLeft(const QString &userName)
|
||||
void TabSupervisor::processUserJoined(const ServerInfo_User &userInfoJoined)
|
||||
{
|
||||
QString userName = QString::fromStdString(userInfoJoined.name());
|
||||
if (isUserBuddy(userName)) {
|
||||
Tab *tab = static_cast<Tab *>(getUserListsTab());
|
||||
|
||||
if (tab != currentWidget()) {
|
||||
tab->setContentsChanged(true);
|
||||
QPixmap avatarPixmap =
|
||||
UserLevelPixmapGenerator::generatePixmap(13, (UserLevelFlags)userInfoJoined.user_level(), true,
|
||||
QString::fromStdString(userInfoJoined.privlevel()));
|
||||
setTabIcon(indexOf(tab), QPixmap(avatarPixmap));
|
||||
if (userListManager->isUserBuddy(userName)) {
|
||||
if (auto *tab = getTabAccount()) {
|
||||
if (tab != currentWidget()) {
|
||||
tab->setContentsChanged(true);
|
||||
QPixmap avatarPixmap =
|
||||
UserLevelPixmapGenerator::generatePixmap(13, (UserLevelFlags)userInfoJoined.user_level(), true,
|
||||
QString::fromStdString(userInfoJoined.privlevel()));
|
||||
setTabIcon(indexOf(tab), QPixmap(avatarPixmap));
|
||||
}
|
||||
}
|
||||
|
||||
if (SettingsCache::instance().getBuddyConnectNotificationsEnabled()) {
|
||||
@@ -685,57 +902,6 @@ void TabSupervisor::processNotifyUserEvent(const Event_NotifyUser &event)
|
||||
}
|
||||
}
|
||||
|
||||
bool TabSupervisor::isOwnUserRegistered() const
|
||||
{
|
||||
return userInfo != nullptr && (userInfo->user_level() & ServerInfo_User::IsRegistered) != 0;
|
||||
}
|
||||
|
||||
QString TabSupervisor::getOwnUsername() const
|
||||
{
|
||||
return userInfo != nullptr ? QString::fromStdString(userInfo->name()) : QString();
|
||||
}
|
||||
|
||||
bool TabSupervisor::isUserBuddy(const QString &userName) const
|
||||
{
|
||||
if (!getUserListsTab())
|
||||
return false;
|
||||
if (!getUserListsTab()->getBuddyList())
|
||||
return false;
|
||||
QMap<QString, UserListTWI *> buddyList = getUserListsTab()->getBuddyList()->getUsers();
|
||||
bool senderIsBuddy = buddyList.contains(userName);
|
||||
return senderIsBuddy;
|
||||
}
|
||||
|
||||
bool TabSupervisor::isUserIgnored(const QString &userName) const
|
||||
{
|
||||
if (!getUserListsTab())
|
||||
return false;
|
||||
if (!getUserListsTab()->getIgnoreList())
|
||||
return false;
|
||||
QMap<QString, UserListTWI *> buddyList = getUserListsTab()->getIgnoreList()->getUsers();
|
||||
bool senderIsBuddy = buddyList.contains(userName);
|
||||
return senderIsBuddy;
|
||||
}
|
||||
|
||||
const ServerInfo_User *TabSupervisor::getOnlineUser(const QString &userName) const
|
||||
{
|
||||
if (!getUserListsTab())
|
||||
return nullptr;
|
||||
if (!getUserListsTab()->getAllUsersList())
|
||||
return nullptr;
|
||||
QMap<QString, UserListTWI *> userList = getUserListsTab()->getAllUsersList()->getUsers();
|
||||
const QString &userNameToMatchLower = userName.toLower();
|
||||
QMap<QString, UserListTWI *>::iterator i;
|
||||
|
||||
for (i = userList.begin(); i != userList.end(); ++i)
|
||||
if (i.key().toLower() == userNameToMatchLower) {
|
||||
const ServerInfo_User &_userInfo = i.value()->getUserInfo();
|
||||
return &_userInfo;
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
};
|
||||
|
||||
bool TabSupervisor::switchToGameTabIfAlreadyExists(const int gameId)
|
||||
{
|
||||
bool isGameTabExists = false;
|
||||
@@ -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/user/user_list_proxy.h"
|
||||
#include "visual_deck_storage/tab_deck_storage_visual.h"
|
||||
|
||||
#include <QAbstractButton>
|
||||
#include <QCommonStyle>
|
||||
@@ -10,6 +11,7 @@
|
||||
#include <QProxyStyle>
|
||||
#include <QTabWidget>
|
||||
|
||||
class UserListManager;
|
||||
class QMenu;
|
||||
class AbstractClient;
|
||||
class Tab;
|
||||
@@ -20,7 +22,7 @@ class TabDeckStorage;
|
||||
class TabReplays;
|
||||
class TabAdmin;
|
||||
class TabMessage;
|
||||
class TabUserLists;
|
||||
class TabAccount;
|
||||
class TabDeckEditor;
|
||||
class TabLog;
|
||||
class RoomEvent;
|
||||
@@ -37,39 +39,42 @@ class MacOSTabFixStyle : public QProxyStyle
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
QRect subElementRect(SubElement, const QStyleOption *, const QWidget *) const;
|
||||
QRect subElementRect(SubElement, const QStyleOption *, const QWidget *) const override;
|
||||
};
|
||||
|
||||
class CloseButton : public QAbstractButton
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
CloseButton(QWidget *parent = nullptr);
|
||||
QSize sizeHint() const;
|
||||
inline QSize minimumSizeHint() const
|
||||
explicit CloseButton(QWidget *parent = nullptr);
|
||||
QSize sizeHint() const override;
|
||||
inline QSize minimumSizeHint() const override
|
||||
{
|
||||
return sizeHint();
|
||||
}
|
||||
|
||||
protected:
|
||||
#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
|
||||
void enterEvent(QEnterEvent *event);
|
||||
void enterEvent(QEnterEvent *event) override;
|
||||
#else
|
||||
void enterEvent(QEvent *event);
|
||||
void enterEvent(QEvent *event) override;
|
||||
#endif
|
||||
void leaveEvent(QEvent *event);
|
||||
void paintEvent(QPaintEvent *event);
|
||||
void leaveEvent(QEvent *event) override;
|
||||
void paintEvent(QPaintEvent *event) override;
|
||||
};
|
||||
|
||||
class TabSupervisor : public QTabWidget, public UserlistProxy
|
||||
class TabSupervisor : public QTabWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
private:
|
||||
ServerInfo_User *userInfo;
|
||||
AbstractClient *client;
|
||||
UserListManager *userListManager;
|
||||
QList<AbstractClient *> localClients;
|
||||
QMenu *tabsMenu;
|
||||
TabDeckStorageVisual *tabVisualDeckStorage;
|
||||
TabServer *tabServer;
|
||||
TabUserLists *tabUserLists;
|
||||
TabAccount *tabAccount;
|
||||
TabDeckStorage *tabDeckStorage;
|
||||
TabReplays *tabReplays;
|
||||
TabAdmin *tabAdmin;
|
||||
@@ -79,16 +84,22 @@ private:
|
||||
QList<TabGame *> replayTabs;
|
||||
QMap<QString, TabMessage *> messageTabs;
|
||||
QList<TabDeckEditor *> deckEditorTabs;
|
||||
int myAddTab(Tab *tab);
|
||||
void addCloseButtonToTab(Tab *tab, int tabIndex);
|
||||
QString sanitizeTabName(QString dirty) const;
|
||||
QString sanitizeHtml(QString dirty) const;
|
||||
bool isLocalGame;
|
||||
|
||||
QAction *aTabDeckEditor, *aTabVisualDeckStorage, *aTabServer, *aTabAccount, *aTabDeckStorage, *aTabReplays,
|
||||
*aTabAdmin, *aTabLog;
|
||||
|
||||
int myAddTab(Tab *tab, QAction *manager = nullptr);
|
||||
void addCloseButtonToTab(Tab *tab, int tabIndex, QAction *manager);
|
||||
static QString sanitizeTabName(QString dirty);
|
||||
static QString sanitizeHtml(QString dirty);
|
||||
void resetTabsMenu();
|
||||
|
||||
public:
|
||||
TabSupervisor(AbstractClient *_client, QWidget *parent = nullptr);
|
||||
~TabSupervisor();
|
||||
explicit TabSupervisor(AbstractClient *_client, QMenu *tabsMenu, QWidget *parent = nullptr);
|
||||
~TabSupervisor() override;
|
||||
void retranslateUi();
|
||||
void initStartupTabs();
|
||||
void start(const ServerInfo_User &userInfo);
|
||||
void startLocal(const QList<AbstractClient *> &_clients);
|
||||
void stop();
|
||||
@@ -100,28 +111,27 @@ public:
|
||||
{
|
||||
return gameTabs.size();
|
||||
}
|
||||
TabUserLists *getUserListsTab() const
|
||||
TabAccount *getTabAccount() const
|
||||
{
|
||||
return tabUserLists;
|
||||
return tabAccount;
|
||||
}
|
||||
ServerInfo_User *getUserInfo() const
|
||||
{
|
||||
return userInfo;
|
||||
}
|
||||
AbstractClient *getClient() const;
|
||||
const UserListManager *getUserListManager() const
|
||||
{
|
||||
return userListManager;
|
||||
}
|
||||
const QMap<int, TabRoom *> &getRoomTabs() const
|
||||
{
|
||||
return roomTabs;
|
||||
}
|
||||
bool getAdminLocked() const;
|
||||
bool closeRequest();
|
||||
bool isOwnUserRegistered() const;
|
||||
QString getOwnUsername() const;
|
||||
bool isUserBuddy(const QString &userName) const;
|
||||
bool isUserIgnored(const QString &userName) const;
|
||||
const ServerInfo_User *getOnlineUser(const QString &userName) const;
|
||||
bool switchToGameTabIfAlreadyExists(const int gameId);
|
||||
void actShowPopup(const QString &message);
|
||||
static void actShowPopup(const QString &message);
|
||||
signals:
|
||||
void setMenu(const QList<QMenu *> &newMenuList = QList<QMenu *>());
|
||||
void localGameEnded();
|
||||
@@ -133,7 +143,16 @@ public slots:
|
||||
void openReplay(GameReplay *replay);
|
||||
void maximizeMainWindow();
|
||||
private slots:
|
||||
void closeButtonPressed();
|
||||
void refreshShortcuts();
|
||||
|
||||
void actTabVisualDeckStorage(bool checked);
|
||||
void actTabServer(bool checked);
|
||||
void actTabAccount(bool checked);
|
||||
void actTabDeckStorage(bool checked);
|
||||
void actTabReplays(bool checked);
|
||||
void actTabAdmin(bool checked);
|
||||
void actTabLog(bool checked);
|
||||
|
||||
void updateCurrent(int index);
|
||||
void updatePingTime(int value, int max);
|
||||
void gameJoined(const Event_GameJoined &event);
|
||||
@@ -0,0 +1,33 @@
|
||||
#include "tab_deck_storage_visual.h"
|
||||
|
||||
#include "../../../game/cards/card_database_model.h"
|
||||
#include "../../ui/widgets/cards/deck_preview_card_picture_widget.h"
|
||||
#include "../../ui/widgets/visual_deck_storage/visual_deck_storage_widget.h"
|
||||
#include "../tab_supervisor.h"
|
||||
#include "pb/command_deck_del.pb.h"
|
||||
|
||||
#include <QMouseEvent>
|
||||
|
||||
TabDeckStorageVisual::TabDeckStorageVisual(TabSupervisor *_tabSupervisor)
|
||||
: Tab(_tabSupervisor), visualDeckStorageWidget(new VisualDeckStorageWidget(this))
|
||||
{
|
||||
connect(this, &TabDeckStorageVisual::openDeckEditor, tabSupervisor, &TabSupervisor::addDeckEditorTab);
|
||||
connect(visualDeckStorageWidget, &VisualDeckStorageWidget::deckPreviewDoubleClicked, this,
|
||||
&TabDeckStorageVisual::actOpenLocalDeck);
|
||||
|
||||
auto *widget = new QWidget(this);
|
||||
auto *layout = new QVBoxLayout(widget);
|
||||
widget->setLayout(layout);
|
||||
this->setCentralWidget(widget);
|
||||
layout->addWidget(visualDeckStorageWidget);
|
||||
}
|
||||
|
||||
void TabDeckStorageVisual::actOpenLocalDeck(QMouseEvent * /*event*/, DeckPreviewWidget *instance)
|
||||
{
|
||||
DeckLoader deckLoader;
|
||||
if (!deckLoader.loadFromFile(instance->filePath, DeckLoader::CockatriceFormat, true)) {
|
||||
return;
|
||||
}
|
||||
|
||||
emit openDeckEditor(&deckLoader);
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
#ifndef TAB_DECK_STORAGE_VISUAL_H
|
||||
#define TAB_DECK_STORAGE_VISUAL_H
|
||||
|
||||
#include "../tab.h"
|
||||
|
||||
class AbstractClient;
|
||||
class CommandContainer;
|
||||
class DeckLoader;
|
||||
class DeckPreviewWidget;
|
||||
class QFileSystemModel;
|
||||
class QGroupBox;
|
||||
class QToolBar;
|
||||
class QTreeView;
|
||||
class QTreeWidget;
|
||||
class QTreeWidgetItem;
|
||||
class Response;
|
||||
class VisualDeckStorageWidget;
|
||||
|
||||
class TabDeckStorageVisual final : public Tab
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit TabDeckStorageVisual(TabSupervisor *_tabSupervisor);
|
||||
void retranslateUi() override{};
|
||||
[[nodiscard]] QString getTabText() const override
|
||||
{
|
||||
return tr("Visual Deck storage");
|
||||
}
|
||||
|
||||
public slots:
|
||||
void actOpenLocalDeck(QMouseEvent * /*event*/, DeckPreviewWidget *instance);
|
||||
|
||||
signals:
|
||||
void openDeckEditor(const DeckLoader *deckLoader);
|
||||
|
||||
private:
|
||||
VisualDeckStorageWidget *visualDeckStorageWidget;
|
||||
};
|
||||
|
||||
#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>
|
||||
158
cockatrice/src/client/ui/picture_loader/picture_loader.cpp
Normal file
158
cockatrice/src/client/ui/picture_loader/picture_loader.cpp
Normal file
@@ -0,0 +1,158 @@
|
||||
#include "picture_loader.h"
|
||||
|
||||
#include "../../../settings/cache_settings.h"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QBuffer>
|
||||
#include <QDebug>
|
||||
#include <QDirIterator>
|
||||
#include <QFileInfo>
|
||||
#include <QMovie>
|
||||
#include <QNetworkDiskCache>
|
||||
#include <QNetworkRequest>
|
||||
#include <QPainter>
|
||||
#include <QPixmapCache>
|
||||
#include <QScreen>
|
||||
#include <QThread>
|
||||
#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
|
||||
|
||||
PictureLoader::PictureLoader() : QObject(nullptr)
|
||||
{
|
||||
worker = new PictureLoaderWorker;
|
||||
connect(&SettingsCache::instance(), SIGNAL(picsPathChanged()), this, SLOT(picsPathChanged()));
|
||||
connect(&SettingsCache::instance(), SIGNAL(picDownloadChanged()), this, SLOT(picDownloadChanged()));
|
||||
|
||||
connect(worker, SIGNAL(imageLoaded(CardInfoPtr, const QImage &)), this,
|
||||
SLOT(imageLoaded(CardInfoPtr, const QImage &)));
|
||||
}
|
||||
|
||||
PictureLoader::~PictureLoader()
|
||||
{
|
||||
worker->deleteLater();
|
||||
}
|
||||
|
||||
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)) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
void PictureLoader::getPixmap(QPixmap &pixmap, CardInfoPtr card, QSize size)
|
||||
{
|
||||
if (card == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
// search for an exact size copy of the picture in cache
|
||||
QString key = card->getPixmapCacheKey();
|
||||
QString sizeKey = key + QLatin1Char('_') + QString::number(size.width()) + QString::number(size.height());
|
||||
if (QPixmapCache::find(sizeKey, &pixmap))
|
||||
return;
|
||||
|
||||
// load the image and create a copy of the correct size
|
||||
QPixmap bigPixmap;
|
||||
if (QPixmapCache::find(key, &bigPixmap)) {
|
||||
QScreen *screen = qApp->primaryScreen();
|
||||
qreal dpr = screen->devicePixelRatio();
|
||||
pixmap = bigPixmap.scaled(size * dpr, Qt::KeepAspectRatio, Qt::SmoothTransformation);
|
||||
pixmap.setDevicePixelRatio(dpr);
|
||||
QPixmapCache::insert(sizeKey, pixmap);
|
||||
return;
|
||||
}
|
||||
|
||||
// add the card to the load queue
|
||||
getInstance().worker->enqueueImageLoad(card);
|
||||
}
|
||||
|
||||
void PictureLoader::imageLoaded(CardInfoPtr card, const QImage &image)
|
||||
{
|
||||
if (image.isNull()) {
|
||||
QPixmapCache::insert(card->getPixmapCacheKey(), QPixmap());
|
||||
} else {
|
||||
if (card->getUpsideDownArt()) {
|
||||
QImage mirrorImage = image.mirrored(true, true);
|
||||
QPixmapCache::insert(card->getPixmapCacheKey(), QPixmap::fromImage(mirrorImage));
|
||||
} else {
|
||||
QPixmapCache::insert(card->getPixmapCacheKey(), QPixmap::fromImage(image));
|
||||
}
|
||||
}
|
||||
|
||||
card->emitPixmapUpdated();
|
||||
}
|
||||
|
||||
void PictureLoader::clearPixmapCache(CardInfoPtr card)
|
||||
{
|
||||
if (card) {
|
||||
QPixmapCache::remove(card->getPixmapCacheKey());
|
||||
}
|
||||
}
|
||||
|
||||
void PictureLoader::clearPixmapCache()
|
||||
{
|
||||
QPixmapCache::clear();
|
||||
}
|
||||
|
||||
void PictureLoader::clearNetworkCache()
|
||||
{
|
||||
getInstance().worker->clearNetworkCache();
|
||||
}
|
||||
|
||||
void PictureLoader::cacheCardPixmaps(QList<CardInfoPtr> cards)
|
||||
{
|
||||
QPixmap tmp;
|
||||
int max = qMin(cards.size(), CACHED_CARD_PER_DECK_MAX);
|
||||
for (int i = 0; i < max; ++i) {
|
||||
const CardInfoPtr &card = cards.at(i);
|
||||
if (!card) {
|
||||
continue;
|
||||
}
|
||||
|
||||
QString key = card->getPixmapCacheKey();
|
||||
if (QPixmapCache::find(key, &tmp)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
getInstance().worker->enqueueImageLoad(card);
|
||||
}
|
||||
}
|
||||
|
||||
void PictureLoader::picDownloadChanged()
|
||||
{
|
||||
QPixmapCache::clear();
|
||||
}
|
||||
|
||||
void PictureLoader::picsPathChanged()
|
||||
{
|
||||
QPixmapCache::clear();
|
||||
}
|
||||
45
cockatrice/src/client/ui/picture_loader/picture_loader.h
Normal file
45
cockatrice/src/client/ui/picture_loader/picture_loader.h
Normal file
@@ -0,0 +1,45 @@
|
||||
#ifndef PICTURELOADER_H
|
||||
#define PICTURELOADER_H
|
||||
|
||||
#include "../../../game/cards/card_database.h"
|
||||
#include "picture_loader_worker.h"
|
||||
|
||||
class PictureLoader : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
static PictureLoader &getInstance()
|
||||
{
|
||||
static PictureLoader instance;
|
||||
return instance;
|
||||
}
|
||||
|
||||
private:
|
||||
explicit PictureLoader();
|
||||
~PictureLoader() override;
|
||||
// Singleton - Don't implement copy constructor and assign operator
|
||||
PictureLoader(PictureLoader const &);
|
||||
void operator=(PictureLoader const &);
|
||||
|
||||
PictureLoaderWorker *worker;
|
||||
|
||||
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);
|
||||
|
||||
public slots:
|
||||
static void clearNetworkCache();
|
||||
|
||||
private slots:
|
||||
void picDownloadChanged();
|
||||
void picsPathChanged();
|
||||
|
||||
public slots:
|
||||
void imageLoaded(CardInfoPtr card, const QImage &image);
|
||||
};
|
||||
#endif
|
||||
@@ -0,0 +1,462 @@
|
||||
#include "picture_loader_worker.h"
|
||||
|
||||
#include "../../../game/cards/card_database_manager.h"
|
||||
#include "../../../settings/cache_settings.h"
|
||||
|
||||
#include <QBuffer>
|
||||
#include <QDirIterator>
|
||||
#include <QLoggingCategory>
|
||||
#include <QMovie>
|
||||
#include <QNetworkDiskCache>
|
||||
#include <QNetworkReply>
|
||||
#include <QThread>
|
||||
|
||||
Q_LOGGING_CATEGORY(PictureLoaderWorkerLog, "picture_loader.worker");
|
||||
|
||||
// Card back returned by gatherer when card is not found
|
||||
QStringList PictureLoaderWorker::md5Blacklist = QStringList() << "db0c48db407a907c16ade38de048a441";
|
||||
|
||||
PictureLoaderWorker::PictureLoaderWorker()
|
||||
: QObject(nullptr), picsPath(SettingsCache::instance().getPicsPath()),
|
||||
customPicsPath(SettingsCache::instance().getCustomPicsPath()),
|
||||
picDownload(SettingsCache::instance().getPicDownload()), downloadRunning(false), loadQueueRunning(false)
|
||||
{
|
||||
connect(this, SIGNAL(startLoadQueue()), this, SLOT(processLoadQueue()), Qt::QueuedConnection);
|
||||
connect(&SettingsCache::instance(), SIGNAL(picsPathChanged()), this, SLOT(picsPathChanged()));
|
||||
connect(&SettingsCache::instance(), SIGNAL(picDownloadChanged()), this, SLOT(picDownloadChanged()));
|
||||
|
||||
networkManager = new QNetworkAccessManager(this);
|
||||
// We need a timeout to ensure requests don't hang indefinitely in case of
|
||||
// cache corruption, see related Qt bug: https://bugreports.qt.io/browse/QTBUG-111397
|
||||
// Use Qt's default timeout (30s, as of 2023-02-22)
|
||||
#if (QT_VERSION >= QT_VERSION_CHECK(5, 15, 0))
|
||||
networkManager->setTransferTimeout();
|
||||
#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)); });
|
||||
networkManager->setCache(cache);
|
||||
// Use a ManualRedirectPolicy since we keep track of redirects in picDownloadFinished
|
||||
// We can't use NoLessSafeRedirectPolicy because it is not applied with AlwaysCache
|
||||
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);
|
||||
}
|
||||
|
||||
PictureLoaderWorker::~PictureLoaderWorker()
|
||||
{
|
||||
pictureLoaderThread->deleteLater();
|
||||
}
|
||||
|
||||
void PictureLoaderWorker::processLoadQueue()
|
||||
{
|
||||
if (loadQueueRunning) {
|
||||
return;
|
||||
}
|
||||
|
||||
loadQueueRunning = true;
|
||||
while (true) {
|
||||
mutex.lock();
|
||||
if (loadQueue.isEmpty()) {
|
||||
mutex.unlock();
|
||||
loadQueueRunning = false;
|
||||
return;
|
||||
}
|
||||
cardBeingLoaded = loadQueue.takeFirst();
|
||||
mutex.unlock();
|
||||
|
||||
QString setName = cardBeingLoaded.getSetName();
|
||||
QString cardName = cardBeingLoaded.getCard()->getName();
|
||||
QString correctedCardName = cardBeingLoaded.getCard()->getCorrectedName();
|
||||
|
||||
qCDebug(PictureLoaderWorkerLog).nospace()
|
||||
<< "PictureLoader: [card: " << cardName << " set: " << setName << "]: Trying to load picture";
|
||||
|
||||
if (CardDatabaseManager::getInstance()->isProviderIdForPreferredPrinting(
|
||||
cardName, cardBeingLoaded.getCard()->getPixmapCacheKey())) {
|
||||
if (cardImageExistsOnDisk(setName, correctedCardName)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
qCDebug(PictureLoaderWorkerLog).nospace() << "PictureLoader: [card: " << cardName << " set: " << setName
|
||||
<< "]: No custom picture, trying to download";
|
||||
cardsToDownload.append(cardBeingLoaded);
|
||||
cardBeingLoaded.clear();
|
||||
if (!downloadRunning) {
|
||||
startNextPicDownload();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool PictureLoaderWorker::cardImageExistsOnDisk(QString &setName, QString &correctedCardname)
|
||||
{
|
||||
QImage image;
|
||||
QImageReader imgReader;
|
||||
imgReader.setDecideFormatFromContent(true);
|
||||
QList<QString> picsPaths = QList<QString>();
|
||||
QDirIterator it(customPicsPath, QDirIterator::Subdirectories | QDirIterator::FollowSymlinks);
|
||||
|
||||
// Recursively check all subdirectories of the CUSTOM folder
|
||||
while (it.hasNext()) {
|
||||
QString thisPath(it.next());
|
||||
QFileInfo thisFileInfo(thisPath);
|
||||
|
||||
if (thisFileInfo.isFile() &&
|
||||
(thisFileInfo.fileName() == correctedCardname || thisFileInfo.completeBaseName() == correctedCardname ||
|
||||
thisFileInfo.baseName() == correctedCardname)) {
|
||||
picsPaths << thisPath; // Card found in the CUSTOM directory, somewhere
|
||||
}
|
||||
}
|
||||
|
||||
if (!setName.isEmpty()) {
|
||||
picsPaths << picsPath + "/" + setName + "/" + correctedCardname
|
||||
// We no longer store downloaded images there, but don't just ignore
|
||||
// stuff that old versions have put there.
|
||||
<< picsPath + "/downloadedPics/" + setName + "/" + correctedCardname;
|
||||
}
|
||||
|
||||
// Iterates through the list of paths, searching for images with the desired
|
||||
// name with any QImageReader-supported
|
||||
// extension
|
||||
for (const auto &_picsPath : picsPaths) {
|
||||
imgReader.setFileName(_picsPath);
|
||||
if (imgReader.read(&image)) {
|
||||
qCDebug(PictureLoaderWorkerLog).nospace()
|
||||
<< "PictureLoader: [card: " << correctedCardname << " set: " << setName << "]: Picture found on disk.";
|
||||
imageLoaded(cardBeingLoaded.getCard(), image);
|
||||
return true;
|
||||
}
|
||||
imgReader.setFileName(_picsPath + ".full");
|
||||
if (imgReader.read(&image)) {
|
||||
qCDebug(PictureLoaderWorkerLog).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)) {
|
||||
qCDebug(PictureLoaderWorkerLog).nospace() << "PictureLoader: [card: " << correctedCardname
|
||||
<< " set: " << setName << "]: Picture.xlhq found on disk.";
|
||||
imageLoaded(cardBeingLoaded.getCard(), image);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void PictureLoaderWorker::startNextPicDownload()
|
||||
{
|
||||
if (cardsToDownload.isEmpty()) {
|
||||
cardBeingDownloaded.clear();
|
||||
downloadRunning = false;
|
||||
return;
|
||||
}
|
||||
|
||||
downloadRunning = true;
|
||||
|
||||
cardBeingDownloaded = cardsToDownload.takeFirst();
|
||||
|
||||
QString picUrl = cardBeingDownloaded.getCurrentUrl();
|
||||
|
||||
if (picUrl.isEmpty()) {
|
||||
downloadRunning = false;
|
||||
picDownloadFailed();
|
||||
} else {
|
||||
QUrl url(picUrl);
|
||||
qCDebug(PictureLoaderWorkerLog).nospace()
|
||||
<< "PictureLoader: [card: " << cardBeingDownloaded.getCard()->getCorrectedName()
|
||||
<< " set: " << cardBeingDownloaded.getSetName() << "]: Trying to fetch picture from url "
|
||||
<< url.toDisplayString();
|
||||
makeRequest(url);
|
||||
}
|
||||
}
|
||||
|
||||
void PictureLoaderWorker::picDownloadFailed()
|
||||
{
|
||||
/* Take advantage of short circuiting here to call the nextUrl until one
|
||||
is not available. Only once nextUrl evaluates to false will this move
|
||||
on to nextSet. If the Urls for a particular card are empty, this will
|
||||
effectively go through the sets for that card. */
|
||||
if (cardBeingDownloaded.nextUrl() || cardBeingDownloaded.nextSet()) {
|
||||
mutex.lock();
|
||||
loadQueue.prepend(cardBeingDownloaded);
|
||||
mutex.unlock();
|
||||
} else {
|
||||
qCDebug(PictureLoaderWorkerLog).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();
|
||||
}
|
||||
emit startLoadQueue();
|
||||
}
|
||||
|
||||
bool PictureLoaderWorker::imageIsBlackListed(const QByteArray &picData)
|
||||
{
|
||||
QString md5sum = QCryptographicHash::hash(picData, QCryptographicHash::Md5).toHex();
|
||||
return md5Blacklist.contains(md5sum);
|
||||
}
|
||||
|
||||
QNetworkReply *PictureLoaderWorker::makeRequest(const QUrl &url)
|
||||
{
|
||||
// Check if the redirect is cached
|
||||
QUrl cachedRedirect = getCachedRedirect(url);
|
||||
if (!cachedRedirect.isEmpty()) {
|
||||
qCDebug(PictureLoaderWorkerLog).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);
|
||||
}
|
||||
|
||||
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(PictureLoaderWorkerLog).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)
|
||||
{
|
||||
bool isFromCache = reply->attribute(QNetworkRequest::SourceIsFromCacheAttribute).toBool();
|
||||
|
||||
if (reply->error()) {
|
||||
if (isFromCache) {
|
||||
qCDebug(PictureLoaderWorkerLog).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 {
|
||||
qCDebug(PictureLoaderWorkerLog).nospace()
|
||||
<< "PictureLoader: [card: " << cardBeingDownloaded.getCard()->getName()
|
||||
<< " set: " << cardBeingDownloaded.getSetName() << "]: " << (picDownload ? "Download" : "Cache search")
|
||||
<< " failed for url " << reply->url().toDisplayString() << " (" << reply->errorString() << ")";
|
||||
|
||||
picDownloadFailed();
|
||||
startNextPicDownload();
|
||||
}
|
||||
|
||||
reply->deleteLater();
|
||||
return;
|
||||
}
|
||||
|
||||
// List of status codes from https://doc.qt.io/qt-6/qnetworkreply.html#redirected
|
||||
int statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||
if (statusCode == 301 || statusCode == 302 || statusCode == 303 || statusCode == 305 || statusCode == 307 ||
|
||||
statusCode == 308) {
|
||||
QUrl redirectUrl = reply->header(QNetworkRequest::LocationHeader).toUrl();
|
||||
qCDebug(PictureLoaderWorkerLog).nospace()
|
||||
<< "PictureLoader: [card: " << cardBeingDownloaded.getCard()->getName()
|
||||
<< " set: " << cardBeingDownloaded.getSetName() << "]: following "
|
||||
<< (isFromCache ? "cached redirect" : "redirect") << " to " << redirectUrl.toDisplayString();
|
||||
makeRequest(redirectUrl);
|
||||
reply->deleteLater();
|
||||
return;
|
||||
}
|
||||
|
||||
// peek is used to keep the data in the buffer for use by QImageReader
|
||||
const QByteArray &picData = reply->peek(reply->size());
|
||||
|
||||
if (imageIsBlackListed(picData)) {
|
||||
qCDebug(PictureLoaderWorkerLog).nospace()
|
||||
<< "PictureLoader: [card: " << cardBeingDownloaded.getCard()->getName()
|
||||
<< " set: " << cardBeingDownloaded.getSetName()
|
||||
<< "]: Picture found, but blacklisted, will consider it as not found";
|
||||
|
||||
picDownloadFailed();
|
||||
reply->deleteLater();
|
||||
startNextPicDownload();
|
||||
return;
|
||||
}
|
||||
|
||||
QImage testImage;
|
||||
|
||||
QImageReader imgReader;
|
||||
imgReader.setDecideFormatFromContent(true);
|
||||
imgReader.setDevice(reply);
|
||||
|
||||
bool logSuccessMessage = false;
|
||||
|
||||
static const int riffHeaderSize = 12; // RIFF_HEADER_SIZE from webp/format_constants.h
|
||||
auto replyHeader = reply->peek(riffHeaderSize);
|
||||
|
||||
if (replyHeader.startsWith("RIFF") && replyHeader.endsWith("WEBP")) {
|
||||
auto imgBuf = QBuffer(this);
|
||||
imgBuf.setData(reply->readAll());
|
||||
|
||||
auto movie = QMovie(&imgBuf);
|
||||
movie.start();
|
||||
movie.stop();
|
||||
|
||||
imageLoaded(cardBeingDownloaded.getCard(), movie.currentImage());
|
||||
logSuccessMessage = true;
|
||||
} else if (imgReader.read(&testImage)) {
|
||||
imageLoaded(cardBeingDownloaded.getCard(), testImage);
|
||||
logSuccessMessage = true;
|
||||
} else {
|
||||
qCDebug(PictureLoaderWorkerLog).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) {
|
||||
qCDebug(PictureLoaderWorkerLog).nospace()
|
||||
<< "PictureLoader: [card: " << cardBeingDownloaded.getCard()->getName()
|
||||
<< " set: " << cardBeingDownloaded.getSetName() << "]: Image successfully "
|
||||
<< (isFromCache ? "loaded from cached" : "downloaded from") << " url " << reply->url().toDisplayString();
|
||||
}
|
||||
|
||||
reply->deleteLater();
|
||||
startNextPicDownload();
|
||||
}
|
||||
|
||||
void PictureLoaderWorker::enqueueImageLoad(CardInfoPtr card)
|
||||
{
|
||||
QMutexLocker locker(&mutex);
|
||||
|
||||
// avoid queueing the same card more than once
|
||||
if (!card || card == cardBeingLoaded.getCard() || card == cardBeingDownloaded.getCard()) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const PictureToLoad &pic : loadQueue) {
|
||||
if (pic.getCard() == card)
|
||||
return;
|
||||
}
|
||||
|
||||
for (const PictureToLoad &pic : cardsToDownload) {
|
||||
if (pic.getCard() == card)
|
||||
return;
|
||||
}
|
||||
|
||||
loadQueue.append(PictureToLoad(card));
|
||||
emit startLoadQueue();
|
||||
}
|
||||
|
||||
void PictureLoaderWorker::picDownloadChanged()
|
||||
{
|
||||
QMutexLocker locker(&mutex);
|
||||
picDownload = SettingsCache::instance().getPicDownload();
|
||||
}
|
||||
|
||||
void PictureLoaderWorker::picsPathChanged()
|
||||
{
|
||||
QMutexLocker locker(&mutex);
|
||||
picsPath = SettingsCache::instance().getPicsPath();
|
||||
customPicsPath = SettingsCache::instance().getCustomPicsPath();
|
||||
}
|
||||
|
||||
void PictureLoaderWorker::clearNetworkCache()
|
||||
{
|
||||
networkManager->cache()->clear();
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
#ifndef PICTURE_LOADER_WORKER_H
|
||||
#define PICTURE_LOADER_WORKER_H
|
||||
|
||||
#include "../../../game/cards/card_database.h"
|
||||
#include "picture_to_load.h"
|
||||
|
||||
#include <QMutex>
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QObject>
|
||||
|
||||
#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 PictureLoaderWorker : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit PictureLoaderWorker();
|
||||
~PictureLoaderWorker() override;
|
||||
|
||||
void enqueueImageLoad(CardInfoPtr card);
|
||||
void clearNetworkCache();
|
||||
|
||||
private:
|
||||
static QStringList md5Blacklist;
|
||||
|
||||
QThread *pictureLoaderThread;
|
||||
QString picsPath, customPicsPath;
|
||||
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;
|
||||
bool picDownload, downloadRunning, loadQueueRunning;
|
||||
void startNextPicDownload();
|
||||
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();
|
||||
|
||||
void picDownloadChanged();
|
||||
void picsPathChanged();
|
||||
public slots:
|
||||
void processLoadQueue();
|
||||
|
||||
signals:
|
||||
void startLoadQueue();
|
||||
void imageLoaded(CardInfoPtr card, const QImage &image);
|
||||
};
|
||||
|
||||
#endif // PICTURE_LOADER_WORKER_H
|
||||
244
cockatrice/src/client/ui/picture_loader/picture_to_load.cpp
Normal file
244
cockatrice/src/client/ui/picture_loader/picture_to_load.cpp
Normal file
@@ -0,0 +1,244 @@
|
||||
#include "picture_to_load.h"
|
||||
|
||||
#include "../../../settings/cache_settings.h"
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QDate>
|
||||
#include <QRegularExpression>
|
||||
#include <QUrl>
|
||||
#include <algorithm>
|
||||
#include <qloggingcategory.h>
|
||||
|
||||
Q_LOGGING_CATEGORY(PictureToLoadLog, "picture_loader.picture_to_load")
|
||||
|
||||
PictureToLoad::PictureToLoad(CardInfoPtr _card)
|
||||
: card(std::move(_card)), urlTemplates(SettingsCache::instance().downloads().getAllURLs())
|
||||
{
|
||||
if (card) {
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
void PictureToLoad::populateSetUrls()
|
||||
{
|
||||
/* currentSetUrls is a list, populated each time a new set is requested for a particular card
|
||||
and Urls are removed from it as a download is attempted from each one. Custom Urls for
|
||||
a set are given higher priority, so should be placed first in the list. */
|
||||
currentSetUrls.clear();
|
||||
|
||||
if (card && currentSet) {
|
||||
QString setCustomURL = card->getCustomPicURL(currentSet->getShortName());
|
||||
|
||||
if (!setCustomURL.isEmpty()) {
|
||||
currentSetUrls.append(setCustomURL);
|
||||
}
|
||||
}
|
||||
|
||||
for (const QString &urlTemplate : urlTemplates) {
|
||||
QString transformedUrl = transformUrl(urlTemplate);
|
||||
|
||||
if (!transformedUrl.isEmpty()) {
|
||||
currentSetUrls.append(transformedUrl);
|
||||
}
|
||||
}
|
||||
|
||||
/* Call nextUrl to make sure currentUrl is up-to-date
|
||||
but we don't need the result here. */
|
||||
(void)nextUrl();
|
||||
}
|
||||
|
||||
bool PictureToLoad::nextSet()
|
||||
{
|
||||
if (!sortedSets.isEmpty()) {
|
||||
currentSet = sortedSets.takeFirst();
|
||||
populateSetUrls();
|
||||
return true;
|
||||
}
|
||||
currentSet = {};
|
||||
return false;
|
||||
}
|
||||
|
||||
bool PictureToLoad::nextUrl()
|
||||
{
|
||||
if (!currentSetUrls.isEmpty()) {
|
||||
currentUrl = currentSetUrls.takeFirst();
|
||||
return true;
|
||||
}
|
||||
currentUrl = QString();
|
||||
return false;
|
||||
}
|
||||
|
||||
QString PictureToLoad::getSetName() const
|
||||
{
|
||||
if (currentSet) {
|
||||
return currentSet->getCorrectedShortName();
|
||||
} else {
|
||||
return QString();
|
||||
}
|
||||
}
|
||||
|
||||
static int parse(const QString &urlTemplate,
|
||||
const QString &propType,
|
||||
const QString &cardName,
|
||||
const QString &setName,
|
||||
std::function<QString(const QString &)> getProperty,
|
||||
QMap<QString, QString> &transformMap)
|
||||
{
|
||||
static const QRegularExpression rxFillWith("^(.+)_fill_with_(.+)$");
|
||||
static const QRegularExpression rxSubStr("^(.+)_substr_(\\d+)_(\\d+)$");
|
||||
|
||||
const QRegularExpression rxCardProp("!" + propType + ":([^!]+)!");
|
||||
|
||||
auto matches = rxCardProp.globalMatch(urlTemplate);
|
||||
while (matches.hasNext()) {
|
||||
auto match = matches.next();
|
||||
QString templatePropertyName = match.captured(1);
|
||||
auto fillMatch = rxFillWith.match(templatePropertyName);
|
||||
QString cardPropertyName;
|
||||
QString fillWith;
|
||||
int subStrPos = 0;
|
||||
int subStrLen = -1;
|
||||
if (fillMatch.hasMatch()) {
|
||||
cardPropertyName = fillMatch.captured(1);
|
||||
fillWith = fillMatch.captured(2);
|
||||
} else {
|
||||
fillWith = QString();
|
||||
auto subStrMatch = rxSubStr.match(templatePropertyName);
|
||||
if (subStrMatch.hasMatch()) {
|
||||
cardPropertyName = subStrMatch.captured(1);
|
||||
subStrPos = subStrMatch.captured(2).toInt();
|
||||
subStrLen = subStrMatch.captured(3).toInt();
|
||||
} else {
|
||||
cardPropertyName = templatePropertyName;
|
||||
}
|
||||
}
|
||||
QString propertyValue = getProperty(cardPropertyName);
|
||||
if (propertyValue.isEmpty()) {
|
||||
qCDebug(PictureToLoadLog).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) {
|
||||
if (subStrPos + subStrLen > propLength) {
|
||||
qCDebug(PictureToLoadLog).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) {
|
||||
qCDebug(PictureToLoadLog).nospace()
|
||||
<< "PictureLoader: [card: " << cardName << " set: " << setName << "]: Requested " << propType
|
||||
<< " property (" << cardPropertyName << ") for Url template (" << urlTemplate
|
||||
<< ") is longer than fill specification (" << fillWith << ")";
|
||||
return 1;
|
||||
} else {
|
||||
|
||||
propertyValue = fillWith.left(fillLength - propLength) + propertyValue;
|
||||
}
|
||||
}
|
||||
|
||||
transformMap["!" + propType + ":" + templatePropertyName + "!"] = propertyValue;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
QString PictureToLoad::transformUrl(const QString &urlTemplate) const
|
||||
{
|
||||
/* This function takes Url templates and substitutes actual card details
|
||||
into the url. This is used for making Urls with follow a predictable format
|
||||
for downloading images. If information is requested by the template that is
|
||||
not populated for this specific card/set combination, an empty string is returned.*/
|
||||
|
||||
CardSetPtr set = getCurrentSet();
|
||||
|
||||
QMap<QString, QString> transformMap = QMap<QString, QString>();
|
||||
QString setName = getSetName();
|
||||
|
||||
// name
|
||||
QString cardName = card->getName();
|
||||
transformMap["!name!"] = cardName;
|
||||
transformMap["!name_lower!"] = card->getName().toLower();
|
||||
transformMap["!corrected_name!"] = card->getCorrectedName();
|
||||
transformMap["!corrected_name_lower!"] = card->getCorrectedName().toLower();
|
||||
|
||||
// card properties
|
||||
if (parse(
|
||||
urlTemplate, "prop", cardName, setName, [&](const QString &name) { return card->getProperty(name); },
|
||||
transformMap)) {
|
||||
return QString();
|
||||
}
|
||||
|
||||
if (set) {
|
||||
transformMap["!setcode!"] = set->getShortName();
|
||||
transformMap["!setcode_lower!"] = set->getShortName().toLower();
|
||||
transformMap["!setname!"] = set->getLongName();
|
||||
transformMap["!setname_lower!"] = set->getLongName().toLower();
|
||||
|
||||
if (parse(
|
||||
urlTemplate, "set", cardName, setName,
|
||||
[&](const QString &name) { return card->getSetProperty(set->getShortName(), name); }, transformMap)) {
|
||||
return QString();
|
||||
}
|
||||
}
|
||||
|
||||
// language setting
|
||||
transformMap["!sflang!"] = QString(QCoreApplication::translate(
|
||||
"PictureLoader", "en", "code for scryfall's language property, not available for all languages"));
|
||||
|
||||
QString transformedUrl = urlTemplate;
|
||||
for (const QString &prop : transformMap.keys()) {
|
||||
if (transformedUrl.contains(prop)) {
|
||||
if (!transformMap[prop].isEmpty()) {
|
||||
transformedUrl.replace(prop, QUrl::toPercentEncoding(transformMap[prop]));
|
||||
} else {
|
||||
/* This means the template is requesting information that is not
|
||||
* populated in this card, so it should return an empty string,
|
||||
* indicating an invalid Url.
|
||||
*/
|
||||
qCDebug(PictureToLoadLog).nospace()
|
||||
<< "PictureLoader: [card: " << cardName << " set: " << setName << "]: Requested information ("
|
||||
<< prop << ") for Url template (" << urlTemplate << ") is not available";
|
||||
return QString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return transformedUrl;
|
||||
}
|
||||
60
cockatrice/src/client/ui/picture_loader/picture_to_load.h
Normal file
60
cockatrice/src/client/ui/picture_loader/picture_to_load.h
Normal file
@@ -0,0 +1,60 @@
|
||||
#ifndef PICTURE_TO_LOAD_H
|
||||
#define PICTURE_TO_LOAD_H
|
||||
|
||||
#include "../../../game/cards/card_database.h"
|
||||
|
||||
class PictureToLoad
|
||||
{
|
||||
private:
|
||||
class SetDownloadPriorityComparator
|
||||
{
|
||||
public:
|
||||
/*
|
||||
* Returns true if a has higher download priority than b
|
||||
* Enabled sets have priority over disabled sets
|
||||
* Both groups follows the user-defined order
|
||||
*/
|
||||
inline bool operator()(const CardSetPtr &a, const CardSetPtr &b) const
|
||||
{
|
||||
if (a->getEnabled()) {
|
||||
return !b->getEnabled() || a->getSortKey() < b->getSortKey();
|
||||
} else {
|
||||
return !b->getEnabled() && a->getSortKey() < b->getSortKey();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
CardInfoPtr card;
|
||||
QList<CardSetPtr> sortedSets;
|
||||
QList<QString> urlTemplates;
|
||||
QList<QString> currentSetUrls;
|
||||
QString currentUrl;
|
||||
CardSetPtr currentSet;
|
||||
|
||||
public:
|
||||
explicit PictureToLoad(CardInfoPtr _card = CardInfoPtr());
|
||||
|
||||
CardInfoPtr getCard() const
|
||||
{
|
||||
return card;
|
||||
}
|
||||
void clear()
|
||||
{
|
||||
card.clear();
|
||||
}
|
||||
QString getCurrentUrl() const
|
||||
{
|
||||
return currentUrl;
|
||||
}
|
||||
CardSetPtr getCurrentSet() const
|
||||
{
|
||||
return currentSet;
|
||||
}
|
||||
QString getSetName() const;
|
||||
QString transformUrl(const QString &urlTemplate) const;
|
||||
bool nextSet();
|
||||
bool nextUrl();
|
||||
void populateSetUrls();
|
||||
};
|
||||
|
||||
#endif // PICTURE_TO_LOAD_H
|
||||
@@ -1,4 +1,4 @@
|
||||
#include "pixmapgenerator.h"
|
||||
#include "pixel_map_generator.h"
|
||||
|
||||
#include "pb/serverinfo_user.pb.h"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user