mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-09-27 08:24:39 -07:00
Compare commits
41 commits
d18e28a65b
...
380bddb15e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
380bddb15e | ||
|
|
30ed2a69a0 | ||
|
|
8f674aab78 | ||
|
|
51365960fe | ||
|
|
7b39cf98d5 | ||
|
|
6c1d1c7b58 | ||
|
|
456db56058 | ||
|
|
cca4af0ec7 | ||
|
|
e11c915a0c | ||
|
|
1c93309952 | ||
|
|
5ace88c111 | ||
|
|
69d32ed853 | ||
|
|
f4d5fc181d | ||
|
|
733deac0bb | ||
|
|
2a3a8982a6 | ||
|
|
87443d58f7 | ||
|
|
61c1215a30 | ||
|
|
fd82b140a8 | ||
|
|
5d025ca0bd | ||
|
|
a85203e457 | ||
|
|
e9bf1e6e46 | ||
|
|
202a5ac958 | ||
|
|
d5d99e4dfb | ||
|
|
7d867b9745 | ||
|
|
69e8f80fa1 | ||
|
|
df3defa890 | ||
|
|
030e4f5871 | ||
|
|
6e5c58069b | ||
|
|
ebeae48652 | ||
|
|
36f998e466 | ||
|
|
760fe88fa3 | ||
|
|
fff506dbbe | ||
|
|
8e0bdafb14 | ||
|
|
9b0d62c152 | ||
|
|
6cdeb0c428 | ||
|
|
745e94f332 | ||
|
|
b01e107908 | ||
|
|
8d30ac54f0 | ||
|
|
fb482f037e | ||
|
|
0f0e46a177 | ||
|
|
2fe59d6326 |
304 changed files with 8477 additions and 975 deletions
4
.github/workflows/desktop-build.yml
vendored
4
.github/workflows/desktop-build.yml
vendored
|
|
@ -152,7 +152,7 @@ jobs:
|
||||||
env:
|
env:
|
||||||
CACHE: ${{ github.workspace }}/.cache/${{ matrix.distro }}${{ matrix.version }} # directory for caching docker image and ccache
|
CACHE: ${{ github.workspace }}/.cache/${{ matrix.distro }}${{ matrix.version }} # directory for caching docker image and ccache
|
||||||
CCACHE_EVICTION_AGE: 7d
|
CCACHE_EVICTION_AGE: 7d
|
||||||
CCACHE_SIZE: 550M # space of all repo is 10Gi: https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#usage-limits-and-eviction-policy
|
CCACHE_SIZE: 600M # space of all repo is 10Gi: https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#usage-limits-and-eviction-policy
|
||||||
CMAKE_GENERATOR: 'Ninja'
|
CMAKE_GENERATOR: 'Ninja'
|
||||||
NAME: ${{ matrix.distro }}${{ matrix.version }}
|
NAME: ${{ matrix.distro }}${{ matrix.version }}
|
||||||
|
|
||||||
|
|
@ -342,7 +342,7 @@ jobs:
|
||||||
timeout-minutes: 100
|
timeout-minutes: 100
|
||||||
env:
|
env:
|
||||||
CCACHE_DIR: ${{ github.workspace }}/.cache/
|
CCACHE_DIR: ${{ github.workspace }}/.cache/
|
||||||
CCACHE_SIZE: 550M # space of all repo is 10Gi: https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#usage-limits-and-eviction-policy
|
CCACHE_SIZE: 600M # space of all repo is 10Gi: https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#usage-limits-and-eviction-policy
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: "Checkout"
|
- name: "Checkout"
|
||||||
|
|
|
||||||
18
README.md
18
README.md
|
|
@ -149,15 +149,15 @@ You can then
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
The following flags (with their non-default values) can be passed to `cmake`:
|
The following flags (with their non-default values) can be passed to `cmake`:
|
||||||
| Flag | Description |
|
| Flag | Description |
|
||||||
| --- | --- |
|
| -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| `-DWITH_SERVER=1` | Build <kbd>Servatrice</kbd> server |
|
| `-DWITH_SERVER=1` | Build <kbd>Servatrice</kbd> server |
|
||||||
| `-DWITH_CLIENT=0` | Don't build <kbd>Cockatrice</kbd> client |
|
| `-DWITH_CLIENT=0` | Don't build <kbd>Cockatrice</kbd> client |
|
||||||
| `-DWITH_ORACLE=0` | Don't build <kbd>Oracle</kbd> card database tool |
|
| `-DWITH_ORACLE=0` | Don't build <kbd>Oracle</kbd> card database tool |
|
||||||
| `-DCMAKE_BUILD_TYPE=Debug` | Compile in debug mode<br> Enables extra logging output, debug symbols, and much more verbose compiler warnings |
|
| `-DCMAKE_BUILD_TYPE=Debug` | Compile in debug mode<br> Enables extra logging output, debug symbols, and much more verbose compiler warnings |
|
||||||
| `-DWARNING_AS_ERROR=0` | Don't treat compilation warnings as errors in debug mode |
|
| `-DWARNING_AS_ERROR=0` | Don't treat compilation warnings as errors in debug mode |
|
||||||
| `-DUPDATE_TRANSLATIONS=1` | Configure `make` to update the translation .ts files for new strings in the source code<br> **Note:** `make clean` will remove the .ts files |
|
| `-DUPDATE_TRANSLATIONS=1` | Configure `make` to update the translation .ts files for new strings in the source code<br> **Note:** `make clean` will remove the .ts files |
|
||||||
| `-DTEST=1` | Enable regression tests<br> **Note:** `make test` to run tests, *googletest* will be downloaded if not available |
|
| `-DTEST=1` | Enable regression tests<br> **Note:** `make test` to run tests, *googletest* will be downloaded if not available |
|
||||||
|
|
||||||
|
|
||||||
# Run
|
# Run
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ if(WITH_CLIENT)
|
||||||
)
|
)
|
||||||
endif()
|
endif()
|
||||||
if(WITH_ORACLE)
|
if(WITH_ORACLE)
|
||||||
set(_ORACLE_NEEDED Concurrent Network Svg Widgets)
|
set(_ORACLE_NEEDED Concurrent Network Svg Widgets Xml)
|
||||||
endif()
|
endif()
|
||||||
if(TEST)
|
if(TEST)
|
||||||
# Union of Qt modules required across all test targets (independent of application targets).
|
# Union of Qt modules required across all test targets (independent of application targets).
|
||||||
|
|
|
||||||
|
|
@ -166,6 +166,7 @@ set(cockatrice_SOURCES
|
||||||
src/interface/widgets/cards/additional_info/mana_cost_widget.cpp
|
src/interface/widgets/cards/additional_info/mana_cost_widget.cpp
|
||||||
src/interface/widgets/cards/additional_info/mana_symbol_widget.cpp
|
src/interface/widgets/cards/additional_info/mana_symbol_widget.cpp
|
||||||
src/interface/widgets/cards/art_crop_attribution.cpp
|
src/interface/widgets/cards/art_crop_attribution.cpp
|
||||||
|
src/interface/widgets/cards/card_art_utils.cpp
|
||||||
src/interface/widgets/cards/card_group_display_widgets/card_group_display_widget.cpp
|
src/interface/widgets/cards/card_group_display_widgets/card_group_display_widget.cpp
|
||||||
src/interface/widgets/cards/card_group_display_widgets/flat_card_group_display_widget.cpp
|
src/interface/widgets/cards/card_group_display_widgets/flat_card_group_display_widget.cpp
|
||||||
src/interface/widgets/cards/card_group_display_widgets/overlapped_card_group_display_widget.cpp
|
src/interface/widgets/cards/card_group_display_widgets/overlapped_card_group_display_widget.cpp
|
||||||
|
|
@ -381,6 +382,7 @@ set(cockatrice_SOURCES
|
||||||
src/interface/widgets/tabs/tab_card_art_rules.cpp
|
src/interface/widgets/tabs/tab_card_art_rules.cpp
|
||||||
src/interface/widgets/tabs/tab_deck_editor.cpp
|
src/interface/widgets/tabs/tab_deck_editor.cpp
|
||||||
src/interface/widgets/tabs/tab_deck_storage.cpp
|
src/interface/widgets/tabs/tab_deck_storage.cpp
|
||||||
|
src/interface/widgets/tabs/tab_developer.cpp
|
||||||
src/interface/widgets/tabs/tab_game.cpp
|
src/interface/widgets/tabs/tab_game.cpp
|
||||||
src/interface/widgets/tabs/tab_home.cpp
|
src/interface/widgets/tabs/tab_home.cpp
|
||||||
src/interface/widgets/tabs/tab_logs.cpp
|
src/interface/widgets/tabs/tab_logs.cpp
|
||||||
|
|
@ -401,6 +403,7 @@ set(cockatrice_SOURCES
|
||||||
src/interface/widgets/tabs/api/commander_spellbook/commander_bracket_widget.cpp
|
src/interface/widgets/tabs/api/commander_spellbook/commander_bracket_widget.cpp
|
||||||
src/interface/widgets/tabs/api/commander_spellbook/handle_commander_brackets.cpp
|
src/interface/widgets/tabs/api/commander_spellbook/handle_commander_brackets.cpp
|
||||||
src/interface/widgets/onboarding/banner_shader_config.h
|
src/interface/widgets/onboarding/banner_shader_config.h
|
||||||
|
src/interface/widgets/onboarding/brand_colors.h
|
||||||
src/interface/widgets/onboarding/first_run_wizard.cpp
|
src/interface/widgets/onboarding/first_run_wizard.cpp
|
||||||
src/interface/widgets/onboarding/first_run_wizard.h
|
src/interface/widgets/onboarding/first_run_wizard.h
|
||||||
src/interface/widgets/onboarding/first_run_wizard_page.cpp
|
src/interface/widgets/onboarding/first_run_wizard_page.cpp
|
||||||
|
|
@ -528,6 +531,7 @@ qt6_add_shaders(
|
||||||
"src/interface/widgets/onboarding/shaders"
|
"src/interface/widgets/onboarding/shaders"
|
||||||
FILES
|
FILES
|
||||||
src/interface/widgets/onboarding/shaders/brand_banner.frag
|
src/interface/widgets/onboarding/shaders/brand_banner.frag
|
||||||
|
src/interface/widgets/onboarding/shaders/brand_plate.frag
|
||||||
)
|
)
|
||||||
|
|
||||||
qt6_add_resources(
|
qt6_add_resources(
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,8 @@
|
||||||
<file>resources/icons/mana/W.svg</file>
|
<file>resources/icons/mana/W.svg</file>
|
||||||
|
|
||||||
<file>resources/backgrounds/home.png</file>
|
<file>resources/backgrounds/home.png</file>
|
||||||
|
<file>resources/backgrounds/home-dark.png</file>
|
||||||
|
<file>resources/backgrounds/home-light.png</file>
|
||||||
<file>resources/backgrounds/card_triplet.svg</file>
|
<file>resources/backgrounds/card_triplet.svg</file>
|
||||||
<file>resources/backgrounds/placeholder_printing_selector.svg</file>
|
<file>resources/backgrounds/placeholder_printing_selector.svg</file>
|
||||||
|
|
||||||
|
|
@ -363,6 +365,8 @@
|
||||||
|
|
||||||
<file>resources/usericons/pawn_single.svg</file>
|
<file>resources/usericons/pawn_single.svg</file>
|
||||||
<file>resources/usericons/pawn_double.svg</file>
|
<file>resources/usericons/pawn_double.svg</file>
|
||||||
|
<file>resources/usericons/pawn_dev_single.svg</file>
|
||||||
|
<file>resources/usericons/pawn_dev_double.svg</file>
|
||||||
<file>resources/usericons/pawn_donator_single.svg</file>
|
<file>resources/usericons/pawn_donator_single.svg</file>
|
||||||
<file>resources/usericons/pawn_donator_double.svg</file>
|
<file>resources/usericons/pawn_donator_double.svg</file>
|
||||||
<file>resources/usericons/pawn_judge_single.svg</file>
|
<file>resources/usericons/pawn_judge_single.svg</file>
|
||||||
|
|
|
||||||
BIN
cockatrice/resources/backgrounds/home-dark.png
Normal file
BIN
cockatrice/resources/backgrounds/home-dark.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 12 MiB |
BIN
cockatrice/resources/backgrounds/home-light.png
Normal file
BIN
cockatrice/resources/backgrounds/home-light.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 16 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 12 MiB After Width: | Height: | Size: 16 MiB |
343
cockatrice/resources/usericons/pawn_dev_double.svg
Normal file
343
cockatrice/resources/usericons/pawn_dev_double.svg
Normal file
|
|
@ -0,0 +1,343 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
width="100"
|
||||||
|
height="100"
|
||||||
|
id="svg5322"
|
||||||
|
version="1.1"
|
||||||
|
inkscape:version="1.4.2 (ebf0e940, 2025-05-08)"
|
||||||
|
sodipodi:docname="pawn_dev_double.svg"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/">
|
||||||
|
<defs
|
||||||
|
id="defs3">
|
||||||
|
<inkscape:perspective
|
||||||
|
sodipodi:type="inkscape:persp3d"
|
||||||
|
inkscape:vp_x="0 : 526.18109 : 1"
|
||||||
|
inkscape:vp_y="0 : 1000 : 0"
|
||||||
|
inkscape:vp_z="744.09448 : 526.18109 : 1"
|
||||||
|
inkscape:persp3d-origin="372.04724 : 350.78739 : 1"
|
||||||
|
id="perspective5328"/>
|
||||||
|
<inkscape:perspective
|
||||||
|
id="perspective5305"
|
||||||
|
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
|
||||||
|
inkscape:vp_z="1 : 0.5 : 1"
|
||||||
|
inkscape:vp_y="0 : 1000 : 0"
|
||||||
|
inkscape:vp_x="0 : 0.5 : 1"
|
||||||
|
sodipodi:type="inkscape:persp3d"/>
|
||||||
|
<linearGradient
|
||||||
|
id="linearGradient5181">
|
||||||
|
<stop
|
||||||
|
style="stop-color:#0fbb00;stop-opacity:1;"
|
||||||
|
offset="0"
|
||||||
|
id="stop5183"/>
|
||||||
|
<stop
|
||||||
|
style="stop-color:#064400;stop-opacity:1;"
|
||||||
|
offset="1"
|
||||||
|
id="stop5185"/>
|
||||||
|
</linearGradient>
|
||||||
|
<radialGradient
|
||||||
|
inkscape:collect="always"
|
||||||
|
xlink:href="#linearGradient3600-2"
|
||||||
|
id="radialGradient3606-7"
|
||||||
|
cx="324.32715"
|
||||||
|
cy="131.40274"
|
||||||
|
fx="324.32715"
|
||||||
|
fy="131.40274"
|
||||||
|
r="25.501276"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
gradientTransform="matrix(0.92332021,0.38403097,-0.41592401,1.0000002,78.192026,-120.05314)"/>
|
||||||
|
<linearGradient
|
||||||
|
id="linearGradient3600-2">
|
||||||
|
<stop
|
||||||
|
style="stop-color:#ffc33d;stop-opacity:1;"
|
||||||
|
offset="0"
|
||||||
|
id="stop3602-4"/>
|
||||||
|
<stop
|
||||||
|
style="stop-color:#e09900;stop-opacity:1;"
|
||||||
|
offset="1"
|
||||||
|
id="stop3604-9"/>
|
||||||
|
</linearGradient>
|
||||||
|
<inkscape:perspective
|
||||||
|
id="perspective5478"
|
||||||
|
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
|
||||||
|
inkscape:vp_z="1 : 0.5 : 1"
|
||||||
|
inkscape:vp_y="0 : 1000 : 0"
|
||||||
|
inkscape:vp_x="0 : 0.5 : 1"
|
||||||
|
sodipodi:type="inkscape:persp3d"/>
|
||||||
|
<linearGradient
|
||||||
|
id="linearGradient5189">
|
||||||
|
<stop
|
||||||
|
style="stop-color:#000ec9;stop-opacity:1;"
|
||||||
|
offset="0"
|
||||||
|
id="stop5191"/>
|
||||||
|
<stop
|
||||||
|
style="stop-color:#000657;stop-opacity:1;"
|
||||||
|
offset="1"
|
||||||
|
id="stop5193"/>
|
||||||
|
</linearGradient>
|
||||||
|
<radialGradient
|
||||||
|
inkscape:collect="always"
|
||||||
|
xlink:href="#linearGradient3600-4"
|
||||||
|
id="radialGradient3606-1"
|
||||||
|
cx="324.32715"
|
||||||
|
cy="131.40274"
|
||||||
|
fx="324.32715"
|
||||||
|
fy="131.40274"
|
||||||
|
r="25.501276"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
gradientTransform="matrix(0.92332021,0.38403097,-0.41592401,1.0000002,78.192026,-120.05314)"/>
|
||||||
|
<linearGradient
|
||||||
|
id="linearGradient3600-4">
|
||||||
|
<stop
|
||||||
|
style="stop-color:#ffc33d;stop-opacity:1;"
|
||||||
|
offset="0"
|
||||||
|
id="stop3602-3"/>
|
||||||
|
<stop
|
||||||
|
style="stop-color:#e09900;stop-opacity:1;"
|
||||||
|
offset="1"
|
||||||
|
id="stop3604-5"/>
|
||||||
|
</linearGradient>
|
||||||
|
<inkscape:perspective
|
||||||
|
id="perspective5559"
|
||||||
|
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
|
||||||
|
inkscape:vp_z="1 : 0.5 : 1"
|
||||||
|
inkscape:vp_y="0 : 1000 : 0"
|
||||||
|
inkscape:vp_x="0 : 0.5 : 1"
|
||||||
|
sodipodi:type="inkscape:persp3d"/>
|
||||||
|
<linearGradient
|
||||||
|
inkscape:collect="always"
|
||||||
|
xlink:href="#linearGradient5173"
|
||||||
|
id="linearGradient5179"
|
||||||
|
x1="167.33386"
|
||||||
|
y1="178.83276"
|
||||||
|
x2="244.78181"
|
||||||
|
y2="178.83276"
|
||||||
|
gradientUnits="userSpaceOnUse"/>
|
||||||
|
<linearGradient
|
||||||
|
id="linearGradient5173">
|
||||||
|
<stop
|
||||||
|
style="stop-color:#f50000;stop-opacity:1;"
|
||||||
|
offset="0"
|
||||||
|
id="stop5175"/>
|
||||||
|
<stop
|
||||||
|
style="stop-color:#950000;stop-opacity:1;"
|
||||||
|
offset="1"
|
||||||
|
id="stop5177"/>
|
||||||
|
</linearGradient>
|
||||||
|
<radialGradient
|
||||||
|
inkscape:collect="always"
|
||||||
|
xlink:href="#linearGradient3600"
|
||||||
|
id="radialGradient5169"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
gradientTransform="matrix(0.92332021,0.38403097,-0.41592401,1.0000002,78.192026,-120.05314)"
|
||||||
|
cx="324.32715"
|
||||||
|
cy="131.40274"
|
||||||
|
fx="324.32715"
|
||||||
|
fy="131.40274"
|
||||||
|
r="25.501276"/>
|
||||||
|
<linearGradient
|
||||||
|
id="linearGradient3600">
|
||||||
|
<stop
|
||||||
|
style="stop-color:#ffc13d;stop-opacity:1;"
|
||||||
|
offset="0"
|
||||||
|
id="stop3602"/>
|
||||||
|
<stop
|
||||||
|
style="stop-color:#e09900;stop-opacity:1;"
|
||||||
|
offset="1"
|
||||||
|
id="stop3604"/>
|
||||||
|
</linearGradient>
|
||||||
|
<radialGradient
|
||||||
|
r="25.501276"
|
||||||
|
fy="131.40274"
|
||||||
|
fx="324.32715"
|
||||||
|
cy="131.40274"
|
||||||
|
cx="324.32715"
|
||||||
|
gradientTransform="matrix(0.92332021,0.38403097,-0.41592401,1.0000002,78.192026,-120.05314)"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
id="radialGradient5574"
|
||||||
|
xlink:href="#linearGradient3600"
|
||||||
|
inkscape:collect="always"/>
|
||||||
|
<inkscape:perspective
|
||||||
|
id="perspective5663"
|
||||||
|
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
|
||||||
|
inkscape:vp_z="1 : 0.5 : 1"
|
||||||
|
inkscape:vp_y="0 : 1000 : 0"
|
||||||
|
inkscape:vp_x="0 : 0.5 : 1"
|
||||||
|
sodipodi:type="inkscape:persp3d"/>
|
||||||
|
<radialGradient
|
||||||
|
inkscape:collect="always"
|
||||||
|
xlink:href="#linearGradient3600-7"
|
||||||
|
id="radialGradient3606-8"
|
||||||
|
cx="324.32715"
|
||||||
|
cy="131.40274"
|
||||||
|
fx="324.32715"
|
||||||
|
fy="131.40274"
|
||||||
|
r="25.501276"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
gradientTransform="matrix(0.92332021,0.38403097,-0.41592401,1.0000002,78.192026,-120.05314)"/>
|
||||||
|
<linearGradient
|
||||||
|
id="linearGradient3600-7">
|
||||||
|
<stop
|
||||||
|
style="stop-color:#ffc13d;stop-opacity:1;"
|
||||||
|
offset="0"
|
||||||
|
id="stop3602-7"/>
|
||||||
|
<stop
|
||||||
|
style="stop-color:#e09900;stop-opacity:1;"
|
||||||
|
offset="1"
|
||||||
|
id="stop3604-6"/>
|
||||||
|
</linearGradient>
|
||||||
|
<radialGradient
|
||||||
|
inkscape:collect="always"
|
||||||
|
xlink:href="#linearGradient3600-7"
|
||||||
|
id="radialGradient5254"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
gradientTransform="matrix(0.92332021,0.38403097,-0.41592401,1.0000002,78.192026,-120.05314)"
|
||||||
|
cx="324.32715"
|
||||||
|
cy="131.40274"
|
||||||
|
fx="324.32715"
|
||||||
|
fy="131.40274"
|
||||||
|
r="25.501276"/>
|
||||||
|
<linearGradient
|
||||||
|
inkscape:collect="always"
|
||||||
|
xlink:href="#linearGradient5189-1"
|
||||||
|
id="linearGradient5394"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
x1="385.03503"
|
||||||
|
y1="180.09546"
|
||||||
|
x2="462.48297"
|
||||||
|
y2="180.09546"
|
||||||
|
gradientTransform="matrix(0.96839241,0,0,0.96839241,-360.365,847.52359)"/>
|
||||||
|
<linearGradient
|
||||||
|
id="linearGradient5189-1">
|
||||||
|
<stop
|
||||||
|
style="stop-color:#000ec9;stop-opacity:1;"
|
||||||
|
offset="0"
|
||||||
|
id="stop5191-0"/>
|
||||||
|
<stop
|
||||||
|
style="stop-color:#000657;stop-opacity:1;"
|
||||||
|
offset="1"
|
||||||
|
id="stop5193-4"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient
|
||||||
|
inkscape:collect="always"
|
||||||
|
xlink:href="#linearGradient5173-1"
|
||||||
|
id="linearGradient5581"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
x1="167.33386"
|
||||||
|
y1="178.83276"
|
||||||
|
x2="244.78181"
|
||||||
|
y2="178.83276"
|
||||||
|
gradientTransform="matrix(0.96839241,0,0,0.96839241,-149.54484,848.74636)"/>
|
||||||
|
<linearGradient
|
||||||
|
id="linearGradient5173-1">
|
||||||
|
<stop
|
||||||
|
style="stop-color:#f50000;stop-opacity:1;"
|
||||||
|
offset="0"
|
||||||
|
id="stop5175-5"/>
|
||||||
|
<stop
|
||||||
|
style="stop-color:#950000;stop-opacity:1;"
|
||||||
|
offset="1"
|
||||||
|
id="stop5177-3"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient
|
||||||
|
inkscape:collect="always"
|
||||||
|
xlink:href="#linearGradient5181-9"
|
||||||
|
id="linearGradient5782"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
x1="282.50455"
|
||||||
|
y1="181.61069"
|
||||||
|
x2="359.95248"
|
||||||
|
y2="181.61069"
|
||||||
|
gradientTransform="matrix(0.96839241,0,0,0.96839241,-261.07526,846.05625)"/>
|
||||||
|
<linearGradient
|
||||||
|
id="linearGradient5181-9">
|
||||||
|
<stop
|
||||||
|
style="stop-color:#80d600;stop-opacity:1;"
|
||||||
|
offset="0"
|
||||||
|
id="stop5183-3"/>
|
||||||
|
<stop
|
||||||
|
style="stop-color:#80d600;stop-opacity:1;"
|
||||||
|
offset="1"
|
||||||
|
id="stop5185-0"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient
|
||||||
|
y2="181.61069"
|
||||||
|
x2="359.95248"
|
||||||
|
y1="181.61069"
|
||||||
|
x1="282.50455"
|
||||||
|
gradientTransform="matrix(0.96839241,0,0,0.96839241,-175.71812,893.2775)"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
id="linearGradient5799"
|
||||||
|
xlink:href="#linearGradient5181-9"
|
||||||
|
inkscape:collect="always"/>
|
||||||
|
</defs>
|
||||||
|
<sodipodi:namedview
|
||||||
|
inkscape:document-units="mm"
|
||||||
|
id="base"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1.0"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pageshadow="2"
|
||||||
|
inkscape:zoom="6.0735294"
|
||||||
|
inkscape:cx="53.757869"
|
||||||
|
inkscape:cy="53.840194"
|
||||||
|
inkscape:current-layer="layer1"
|
||||||
|
showgrid="false"
|
||||||
|
inkscape:window-width="1696"
|
||||||
|
inkscape:window-height="1051"
|
||||||
|
inkscape:window-x="98"
|
||||||
|
inkscape:window-y="1118"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
showguides="true">
|
||||||
|
<sodipodi:guide
|
||||||
|
position="49.829627,61.114263"
|
||||||
|
orientation="1,0"
|
||||||
|
id="guide1"
|
||||||
|
inkscape:locked="false"/>
|
||||||
|
</sodipodi:namedview>
|
||||||
|
<metadata
|
||||||
|
id="metadata4">
|
||||||
|
<rdf:RDF>
|
||||||
|
<cc:Work
|
||||||
|
rdf:about="">
|
||||||
|
<dc:format>image/svg+xml</dc:format>
|
||||||
|
<dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
|
||||||
|
<dc:title/>
|
||||||
|
</cc:Work>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
<g
|
||||||
|
inkscape:label="Layer 1"
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer1"
|
||||||
|
transform="translate(0,-952.36218)">
|
||||||
|
<path
|
||||||
|
style="fill-opacity:1;stroke:black;stroke-width:2.78220296;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
|
||||||
|
d="M 49.84375 1.71875 C 36.719738 1.71875 26.0625 12.375988 26.0625 25.5 C 26.0625 32.977454 29.538325 39.612734 34.9375 43.96875 C 24.439951 49.943698 17.919149 62.196126 14.3125 75.65625 C 9.0380874 95.34065 30.224013 98.21875 49.84375 98.21875 C 69.463486 98.21875 90.549327 94.96715 85.375 75.65625 C 81.693381 61.916246 75.224585 49.827177 64.8125 43.9375 C 70.181573 39.580662 73.59375 32.953205 73.59375 25.5 C 73.59375 12.375988 62.967762 1.71875 49.84375 1.71875 z "
|
||||||
|
transform="translate(0,952.36218)"
|
||||||
|
id="left"/>
|
||||||
|
<path
|
||||||
|
style="opacity:1;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:1.73577702;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="m 51.28696,1001.834 0,-46.98372 1.434151,0.16768 c 5.155008,0.60274 9.462857,2.72154 12.938257,6.36366 4.74393,4.9715 6.87913,11.35611 6.16464,18.43328 -0.53702,5.31935 -3.09008,10.59498 -6.83833,14.13074 l -1.94072,1.83069 3.04083,2.20427 c 3.58084,2.5957 7.18975,6.4912 9.55296,10.3116 4.89572,7.9144 9.23593,21.4918 8.50487,26.6055 -0.81312,5.6877 -5.43872,9.6977 -13.62216,11.8093 -3.80822,0.9826 -7.68056,1.4713 -14.763321,1.8633 l -4.471177,0.2474 0,-46.9837 z"
|
||||||
|
id="right"
|
||||||
|
inkscape:connector-curvature="0"/>
|
||||||
|
<path
|
||||||
|
d="m 478.409,116.617 c -0.368,-4.271 -3.181,-7.94 -7.2,-9.403 -4.029,-1.472 -8.539,-0.47 -11.57,2.556 l -62.015,62.011 -68.749,-21.768 -21.768,-68.748 62.016,-62.016 c 3.035,-3.032 4.025,-7.543 2.563,-11.565 -1.477,-4.03 -5.137,-6.837 -9.417,-7.207 -37.663,-3.245 -74.566,10.202 -101.247,36.887 -36.542,36.545 -46.219,89.911 -29.083,135.399 -1.873,1.578 -3.721,3.25 -5.544,5.053 L 19.386,373.152 c -0.073,0.071 -0.145,0.149 -0.224,0.219 -24.345,24.346 -24.345,63.959 0,88.309 24.349,24.344 63.672,24.048 88.013,-0.298 0.105,-0.098 0.201,-0.196 0.297,-0.305 L 301.104,252.456 c 1.765,-1.773 3.404,-3.628 4.949,-5.532 45.5,17.167 98.9,7.513 135.474,-29.056 26.675,-26.687 40.131,-63.593 36.882,-101.251 z M 75.98,435.38 c -8.971,8.969 -23.5,8.963 -32.47,0 -8.967,-8.961 -8.967,-23.502 0,-32.466 8.97,-8.963 23.499,-8.963 32.47,0 8.967,8.964 8.967,23.505 0,32.466 z"
|
||||||
|
id="path1"
|
||||||
|
style="display:inline;fill:#A1A1A1;stroke-width:9.87059588;stroke-dasharray:none;fill-opacity:1;stroke:#000000;stroke-opacity:1"
|
||||||
|
transform="matrix(0.19145387,0,0,0.19145387,4.0816072,956.99677)"
|
||||||
|
inkscape:label="wrench" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 12 KiB |
211
cockatrice/resources/usericons/pawn_dev_single.svg
Normal file
211
cockatrice/resources/usericons/pawn_dev_single.svg
Normal file
|
|
@ -0,0 +1,211 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
width="100"
|
||||||
|
height="100"
|
||||||
|
id="svg5322"
|
||||||
|
version="1.1"
|
||||||
|
inkscape:version="1.4.2 (ebf0e940, 2025-05-08)"
|
||||||
|
sodipodi:docname="pawn_dev_single.svg"
|
||||||
|
xml:space="preserve"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/"><defs
|
||||||
|
id="defs3"><inkscape:perspective
|
||||||
|
sodipodi:type="inkscape:persp3d"
|
||||||
|
inkscape:vp_x="0 : 526.18109 : 1"
|
||||||
|
inkscape:vp_y="0 : 1000 : 0"
|
||||||
|
inkscape:vp_z="744.09448 : 526.18109 : 1"
|
||||||
|
inkscape:persp3d-origin="372.04724 : 350.78739 : 1"
|
||||||
|
id="perspective5328" /><inkscape:perspective
|
||||||
|
id="perspective5305"
|
||||||
|
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
|
||||||
|
inkscape:vp_z="1 : 0.5 : 1"
|
||||||
|
inkscape:vp_y="0 : 1000 : 0"
|
||||||
|
inkscape:vp_x="0 : 0.5 : 1"
|
||||||
|
sodipodi:type="inkscape:persp3d" /><linearGradient
|
||||||
|
id="linearGradient5181"><stop
|
||||||
|
style="stop-color:#0fbb00;stop-opacity:1;"
|
||||||
|
offset="0"
|
||||||
|
id="stop5183" /><stop
|
||||||
|
style="stop-color:#064400;stop-opacity:1;"
|
||||||
|
offset="1"
|
||||||
|
id="stop5185" /></linearGradient><radialGradient
|
||||||
|
inkscape:collect="always"
|
||||||
|
xlink:href="#linearGradient3600-2"
|
||||||
|
id="radialGradient3606-7"
|
||||||
|
cx="324.32715"
|
||||||
|
cy="131.40274"
|
||||||
|
fx="324.32715"
|
||||||
|
fy="131.40274"
|
||||||
|
r="25.501276"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
gradientTransform="matrix(0.92332021,0.38403097,-0.41592401,1.0000002,78.192026,-120.05314)" /><linearGradient
|
||||||
|
id="linearGradient3600-2"><stop
|
||||||
|
style="stop-color:#ffc33d;stop-opacity:1;"
|
||||||
|
offset="0"
|
||||||
|
id="stop3602-4" /><stop
|
||||||
|
style="stop-color:#e09900;stop-opacity:1;"
|
||||||
|
offset="1"
|
||||||
|
id="stop3604-9" /></linearGradient><inkscape:perspective
|
||||||
|
id="perspective5478"
|
||||||
|
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
|
||||||
|
inkscape:vp_z="1 : 0.5 : 1"
|
||||||
|
inkscape:vp_y="0 : 1000 : 0"
|
||||||
|
inkscape:vp_x="0 : 0.5 : 1"
|
||||||
|
sodipodi:type="inkscape:persp3d" /><linearGradient
|
||||||
|
id="linearGradient5189"><stop
|
||||||
|
style="stop-color:#000ec9;stop-opacity:1;"
|
||||||
|
offset="0"
|
||||||
|
id="stop5191" /><stop
|
||||||
|
style="stop-color:#000657;stop-opacity:1;"
|
||||||
|
offset="1"
|
||||||
|
id="stop5193" /></linearGradient><radialGradient
|
||||||
|
inkscape:collect="always"
|
||||||
|
xlink:href="#linearGradient3600-4"
|
||||||
|
id="radialGradient3606-1"
|
||||||
|
cx="324.32715"
|
||||||
|
cy="131.40274"
|
||||||
|
fx="324.32715"
|
||||||
|
fy="131.40274"
|
||||||
|
r="25.501276"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
gradientTransform="matrix(0.92332021,0.38403097,-0.41592401,1.0000002,78.192026,-120.05314)" /><linearGradient
|
||||||
|
id="linearGradient3600-4"><stop
|
||||||
|
style="stop-color:#ffc33d;stop-opacity:1;"
|
||||||
|
offset="0"
|
||||||
|
id="stop3602-3" /><stop
|
||||||
|
style="stop-color:#e09900;stop-opacity:1;"
|
||||||
|
offset="1"
|
||||||
|
id="stop3604-5" /></linearGradient><inkscape:perspective
|
||||||
|
id="perspective5559"
|
||||||
|
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
|
||||||
|
inkscape:vp_z="1 : 0.5 : 1"
|
||||||
|
inkscape:vp_y="0 : 1000 : 0"
|
||||||
|
inkscape:vp_x="0 : 0.5 : 1"
|
||||||
|
sodipodi:type="inkscape:persp3d" /><linearGradient
|
||||||
|
inkscape:collect="always"
|
||||||
|
xlink:href="#linearGradient5173"
|
||||||
|
id="linearGradient5179"
|
||||||
|
x1="167.33386"
|
||||||
|
y1="178.83276"
|
||||||
|
x2="244.78181"
|
||||||
|
y2="178.83276"
|
||||||
|
gradientUnits="userSpaceOnUse" /><linearGradient
|
||||||
|
id="linearGradient5173"><stop
|
||||||
|
style="stop-color:#f50000;stop-opacity:1;"
|
||||||
|
offset="0"
|
||||||
|
id="stop5175" /><stop
|
||||||
|
style="stop-color:#950000;stop-opacity:1;"
|
||||||
|
offset="1"
|
||||||
|
id="stop5177" /></linearGradient><radialGradient
|
||||||
|
inkscape:collect="always"
|
||||||
|
xlink:href="#linearGradient3600"
|
||||||
|
id="radialGradient5169"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
gradientTransform="matrix(0.92332021,0.38403097,-0.41592401,1.0000002,78.192026,-120.05314)"
|
||||||
|
cx="324.32715"
|
||||||
|
cy="131.40274"
|
||||||
|
fx="324.32715"
|
||||||
|
fy="131.40274"
|
||||||
|
r="25.501276" /><linearGradient
|
||||||
|
id="linearGradient3600"><stop
|
||||||
|
style="stop-color:#ffc13d;stop-opacity:1;"
|
||||||
|
offset="0"
|
||||||
|
id="stop3602" /><stop
|
||||||
|
style="stop-color:#e09900;stop-opacity:1;"
|
||||||
|
offset="1"
|
||||||
|
id="stop3604" /></linearGradient><radialGradient
|
||||||
|
r="25.501276"
|
||||||
|
fy="131.40274"
|
||||||
|
fx="324.32715"
|
||||||
|
cy="131.40274"
|
||||||
|
cx="324.32715"
|
||||||
|
gradientTransform="matrix(0.92332021,0.38403097,-0.41592401,1.0000002,78.192026,-120.05314)"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
id="radialGradient5574"
|
||||||
|
xlink:href="#linearGradient3600"
|
||||||
|
inkscape:collect="always" /><inkscape:perspective
|
||||||
|
id="perspective5663"
|
||||||
|
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
|
||||||
|
inkscape:vp_z="1 : 0.5 : 1"
|
||||||
|
inkscape:vp_y="0 : 1000 : 0"
|
||||||
|
inkscape:vp_x="0 : 0.5 : 1"
|
||||||
|
sodipodi:type="inkscape:persp3d" /><radialGradient
|
||||||
|
inkscape:collect="always"
|
||||||
|
xlink:href="#linearGradient3600-7"
|
||||||
|
id="radialGradient3606-8"
|
||||||
|
cx="324.32715"
|
||||||
|
cy="131.40274"
|
||||||
|
fx="324.32715"
|
||||||
|
fy="131.40274"
|
||||||
|
r="25.501276"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
gradientTransform="matrix(0.92332021,0.38403097,-0.41592401,1.0000002,78.192026,-120.05314)" /><linearGradient
|
||||||
|
id="linearGradient3600-7"><stop
|
||||||
|
style="stop-color:#ffc13d;stop-opacity:1;"
|
||||||
|
offset="0"
|
||||||
|
id="stop3602-7" /><stop
|
||||||
|
style="stop-color:#e09900;stop-opacity:1;"
|
||||||
|
offset="1"
|
||||||
|
id="stop3604-6" /></linearGradient><radialGradient
|
||||||
|
r="25.501276"
|
||||||
|
fy="131.40274"
|
||||||
|
fx="324.32715"
|
||||||
|
cy="131.40274"
|
||||||
|
cx="324.32715"
|
||||||
|
gradientTransform="matrix(0.92332021,0.38403097,-0.41592401,1.0000002,78.192026,-120.05314)"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
id="radialGradient5676"
|
||||||
|
xlink:href="#linearGradient3600-7"
|
||||||
|
inkscape:collect="always" /></defs><sodipodi:namedview
|
||||||
|
inkscape:document-units="mm"
|
||||||
|
id="base"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1.0"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pageshadow="2"
|
||||||
|
inkscape:zoom="3.1582091"
|
||||||
|
inkscape:cx="104.64791"
|
||||||
|
inkscape:cy="56.04442"
|
||||||
|
inkscape:current-layer="layer1"
|
||||||
|
showgrid="false"
|
||||||
|
inkscape:window-width="1920"
|
||||||
|
inkscape:window-height="1027"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="25"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
showguides="true"><sodipodi:guide
|
||||||
|
position="50.002551,111.99556"
|
||||||
|
orientation="1,0"
|
||||||
|
id="guide3"
|
||||||
|
inkscape:locked="false" /></sodipodi:namedview><metadata
|
||||||
|
id="metadata4"><rdf:RDF><cc:Work
|
||||||
|
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /></cc:Work></rdf:RDF></metadata><g
|
||||||
|
inkscape:label="Layer 1"
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer1"
|
||||||
|
transform="translate(0,-952.36218)"
|
||||||
|
style="display:inline">
|
||||||
|
<path
|
||||||
|
style="display:inline;opacity:1;fill-opacity:1;stroke:#000000;stroke-width:2.7822;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="M 49.84375,1.71875 C 36.719738,1.71875 26.0625,12.375988 26.0625,25.5 c 0,7.477454 3.475825,14.112734 8.875,18.46875 -10.497549,5.974948 -17.018351,18.227376 -20.625,31.6875 -5.2744126,19.6844 15.911513,22.5625 35.53125,22.5625 19.619736,0 40.705577,-3.2516 35.53125,-22.5625 C 81.693381,61.916246 75.224585,49.827177 64.8125,43.9375 70.181573,39.580662 73.59375,32.953205 73.59375,25.5 c 0,-13.124012 -10.625988,-23.78125 -23.75,-23.78125 z"
|
||||||
|
id="left"
|
||||||
|
sodipodi:insensitive="true"
|
||||||
|
transform="translate(0,952.36218)" />
|
||||||
|
<path
|
||||||
|
d="m 478.409,116.617 c -0.368,-4.271 -3.181,-7.94 -7.2,-9.403 -4.029,-1.472 -8.539,-0.47 -11.57,2.556 l -62.015,62.011 -68.749,-21.768 -21.768,-68.748 62.016,-62.016 c 3.035,-3.032 4.025,-7.543 2.563,-11.565 -1.477,-4.03 -5.137,-6.837 -9.417,-7.207 -37.663,-3.245 -74.566,10.202 -101.247,36.887 -36.542,36.545 -46.219,89.911 -29.083,135.399 -1.873,1.578 -3.721,3.25 -5.544,5.053 L 19.386,373.152 c -0.073,0.071 -0.145,0.149 -0.224,0.219 -24.345,24.346 -24.345,63.959 0,88.309 24.349,24.344 63.672,24.048 88.013,-0.298 0.105,-0.098 0.201,-0.196 0.297,-0.305 L 301.104,252.456 c 1.765,-1.773 3.404,-3.628 4.949,-5.532 45.5,17.167 98.9,7.513 135.474,-29.056 26.675,-26.687 40.131,-63.593 36.882,-101.251 z M 75.98,435.38 c -8.971,8.969 -23.5,8.963 -32.47,0 -8.967,-8.961 -8.967,-23.502 0,-32.466 8.97,-8.963 23.499,-8.963 32.47,0 8.967,8.964 8.967,23.505 0,32.466 z"
|
||||||
|
id="path1"
|
||||||
|
style="display:inline;fill:#A1A1A1;stroke-width:9.87059588;stroke-dasharray:none;fill-opacity:1;stroke:#000000;stroke-opacity:1"
|
||||||
|
transform="matrix(0.19145387,0,0,0.19145387,4.0816072,956.99677)"
|
||||||
|
inkscape:label="wrench" /></g></svg>
|
||||||
|
After Width: | Height: | Size: 9.1 KiB |
|
|
@ -1,5 +1,6 @@
|
||||||
#include "remote_connection_controller.h"
|
#include "remote_connection_controller.h"
|
||||||
|
|
||||||
|
#include "../../../interface/pixel_map_generator.h"
|
||||||
#include "../../settings/cache_settings.h"
|
#include "../../settings/cache_settings.h"
|
||||||
#include "../interface/widgets/dialogs/dlg_connect.h"
|
#include "../interface/widgets/dialogs/dlg_connect.h"
|
||||||
#include "../interface/widgets/dialogs/dlg_forgot_password_challenge.h"
|
#include "../interface/widgets/dialogs/dlg_forgot_password_challenge.h"
|
||||||
|
|
@ -180,7 +181,7 @@ void ConnectionController::onServerShutdownEvent(const Event_ServerShutdown &eve
|
||||||
"games will be lost.\nReason for shutdown: %1",
|
"games will be lost.\nReason for shutdown: %1",
|
||||||
"", event.minutes())
|
"", event.minutes())
|
||||||
.arg(QString::fromStdString(event.reason())));
|
.arg(QString::fromStdString(event.reason())));
|
||||||
serverShutdownMessageBox.setIconPixmap(QPixmap("theme:cockatrice").scaled(64, 64));
|
serverShutdownMessageBox.setIconPixmap(themePixmap(QStringLiteral("cockatrice")).scaled(64, 64));
|
||||||
serverShutdownMessageBox.setText(tr("Scheduled server shutdown"));
|
serverShutdownMessageBox.setText(tr("Scheduled server shutdown"));
|
||||||
serverShutdownMessageBox.setWindowModality(Qt::ApplicationModal);
|
serverShutdownMessageBox.setWindowModality(Qt::ApplicationModal);
|
||||||
serverShutdownMessageBox.setVisible(true);
|
serverShutdownMessageBox.setVisible(true);
|
||||||
|
|
|
||||||
|
|
@ -94,7 +94,7 @@ QStringMap &SoundEngine::getAvailableThemes()
|
||||||
QDir dir;
|
QDir dir;
|
||||||
availableThemes.clear();
|
availableThemes.clear();
|
||||||
|
|
||||||
// load themes from user profile dir
|
// Load themes from user profile dir
|
||||||
|
|
||||||
dir.setPath(SettingsCache::instance().getDataPath() + "/sounds");
|
dir.setPath(SettingsCache::instance().getDataPath() + "/sounds");
|
||||||
|
|
||||||
|
|
@ -104,7 +104,7 @@ QStringMap &SoundEngine::getAvailableThemes()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// load themes from cockatrice system dir
|
// Load themes from Cockatrice system dir
|
||||||
dir.setPath(qApp->applicationDirPath() +
|
dir.setPath(qApp->applicationDirPath() +
|
||||||
#ifdef Q_OS_MAC
|
#ifdef Q_OS_MAC
|
||||||
"/../Resources/sounds"
|
"/../Resources/sounds"
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
#include "filter_builder.h"
|
#include "filter_builder.h"
|
||||||
|
|
||||||
|
#include "../interface/pixel_map_generator.h"
|
||||||
#include "../interface/widgets/utility/custom_line_edit.h"
|
#include "../interface/widgets/utility/custom_line_edit.h"
|
||||||
|
|
||||||
#include <QComboBox>
|
#include <QComboBox>
|
||||||
|
|
@ -21,7 +22,7 @@ FilterBuilder::FilterBuilder(QWidget *parent) : QWidget(parent)
|
||||||
typeCombo->addItem(CardFilter::typeName(static_cast<CardFilter::Type>(i)), QVariant(i));
|
typeCombo->addItem(CardFilter::typeName(static_cast<CardFilter::Type>(i)), QVariant(i));
|
||||||
}
|
}
|
||||||
|
|
||||||
QPushButton *ok = new QPushButton(QPixmap("theme:icons/increment"), QString());
|
QPushButton *ok = new QPushButton(themePixmap(QStringLiteral("icons/increment")), QString());
|
||||||
ok->setObjectName("ok");
|
ok->setObjectName("ok");
|
||||||
ok->setMaximumSize(20, 20);
|
ok->setMaximumSize(20, 20);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,12 +13,12 @@ CounterState *CounterState::fromProto(const ServerInfo_Counter &counter, QObject
|
||||||
convertColorToQColor(counter.counter_color()), counter.radius(), counter.count(), parent);
|
convertColorToQColor(counter.counter_color()), counter.radius(), counter.count(), parent);
|
||||||
}
|
}
|
||||||
|
|
||||||
void CounterState::setValue(int newValue)
|
void CounterState::setValue(int newValue, bool skipDamageAnimation)
|
||||||
{
|
{
|
||||||
if (newValue == value) {
|
if (newValue == value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
int old = value;
|
int old = value;
|
||||||
value = newValue;
|
value = newValue;
|
||||||
emit valueChanged(old, newValue);
|
emit valueChanged(old, newValue, skipDamageAnimation);
|
||||||
}
|
}
|
||||||
|
|
@ -35,10 +35,23 @@ public:
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
void setValue(int newValue);
|
/**
|
||||||
|
* @brief Set the counter value.
|
||||||
|
* @param newValue The new value.
|
||||||
|
* @param skipDamageAnimation When true, valueChanged is emitted with skipDamageAnimation=true, letting views
|
||||||
|
* suppress damage-related feedback (e.g. battlefield shimmer, life counter flash) for values set during replay
|
||||||
|
* rewinds.
|
||||||
|
*/
|
||||||
|
void setValue(int newValue, bool skipDamageAnimation = false);
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void valueChanged(int oldValue, int newValue);
|
/**
|
||||||
|
* @brief Emitted whenever the value changes.
|
||||||
|
* @param oldValue The previous value.
|
||||||
|
* @param newValue The new value.
|
||||||
|
* @param skipDamageAnimation True when the change should not trigger damage/life-change feedback in views.
|
||||||
|
*/
|
||||||
|
void valueChanged(int oldValue, int newValue, bool skipDamageAnimation);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
int id;
|
int id;
|
||||||
|
|
|
||||||
|
|
@ -430,12 +430,13 @@ void GameEventHandler::eventJoin(const Event_Join &event, int /*eventPlayerId*/,
|
||||||
QString playerName = QString::fromStdString(playerInfo.user_info().name());
|
QString playerName = QString::fromStdString(playerInfo.user_info().name());
|
||||||
emit addPlayerToAutoCompleteList(playerName);
|
emit addPlayerToAutoCompleteList(playerName);
|
||||||
|
|
||||||
if (game->getPlayerManager()->getPlayers().contains(playerId)) {
|
PlayerManager *playerManager = game->getPlayerManager();
|
||||||
|
if (playerManager->getPlayers().contains(playerId) || playerManager->getSpectators().contains(playerId)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (playerInfo.spectator()) {
|
if (playerInfo.spectator()) {
|
||||||
game->getPlayerManager()->addSpectator(playerId, playerInfo);
|
playerManager->addSpectator(playerId, playerInfo);
|
||||||
emit logJoinSpectator(playerName);
|
emit logJoinSpectator(playerName);
|
||||||
emit spectatorJoined(playerInfo);
|
emit spectatorJoined(playerInfo);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,8 @@
|
||||||
enum EventProcessingOption
|
enum EventProcessingOption
|
||||||
{
|
{
|
||||||
SKIP_REVEAL_WINDOW = 0x0001,
|
SKIP_REVEAL_WINDOW = 0x0001,
|
||||||
SKIP_TAP_ANIMATION = 0x0002
|
SKIP_TAP_ANIMATION = 0x0002,
|
||||||
|
SKIP_DAMAGE_ANIMATION = 0x0004
|
||||||
};
|
};
|
||||||
|
|
||||||
// Wrap it in a QFlags typedef
|
// Wrap it in a QFlags typedef
|
||||||
|
|
|
||||||
|
|
@ -262,14 +262,15 @@ void PlayerEventHandler::eventCreateCounter(const Event_CreateCounter &event)
|
||||||
player->addCounter(event.counter_info());
|
player->addCounter(event.counter_info());
|
||||||
}
|
}
|
||||||
|
|
||||||
void PlayerEventHandler::eventSetCounter(const Event_SetCounter &event)
|
void PlayerEventHandler::eventSetCounter(const Event_SetCounter &event, EventProcessingOptions options)
|
||||||
{
|
{
|
||||||
CounterState *ctr = player->getCounters().value(event.counter_id(), nullptr);
|
CounterState *ctr = player->getCounters().value(event.counter_id(), nullptr);
|
||||||
if (!ctr) {
|
if (!ctr) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
int oldValue = ctr->getValue();
|
int oldValue = ctr->getValue();
|
||||||
ctr->setValue(event.value());
|
const bool skipDamageAnimation = options.testFlag(SKIP_DAMAGE_ANIMATION);
|
||||||
|
ctr->setValue(event.value(), skipDamageAnimation);
|
||||||
emit logSetCounter(player, ctr->getName(), event.value(), oldValue);
|
emit logSetCounter(player, ctr->getName(), event.value(), oldValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -625,7 +626,7 @@ void PlayerEventHandler::processGameEvent(GameEvent::GameEventType type,
|
||||||
eventCreateCounter(event.GetExtension(Event_CreateCounter::ext));
|
eventCreateCounter(event.GetExtension(Event_CreateCounter::ext));
|
||||||
break;
|
break;
|
||||||
case GameEvent::SET_COUNTER:
|
case GameEvent::SET_COUNTER:
|
||||||
eventSetCounter(event.GetExtension(Event_SetCounter::ext));
|
eventSetCounter(event.GetExtension(Event_SetCounter::ext), options);
|
||||||
break;
|
break;
|
||||||
case GameEvent::DEL_COUNTER:
|
case GameEvent::DEL_COUNTER:
|
||||||
eventDelCounter(event.GetExtension(Event_DelCounter::ext));
|
eventDelCounter(event.GetExtension(Event_DelCounter::ext));
|
||||||
|
|
|
||||||
|
|
@ -153,7 +153,7 @@ public:
|
||||||
void eventCreateCounter(const Event_CreateCounter &event);
|
void eventCreateCounter(const Event_CreateCounter &event);
|
||||||
|
|
||||||
/// Set a player-level counter value.
|
/// Set a player-level counter value.
|
||||||
void eventSetCounter(const Event_SetCounter &event);
|
void eventSetCounter(const Event_SetCounter &event, EventProcessingOptions options);
|
||||||
|
|
||||||
/// Delete a player-level counter.
|
/// Delete a player-level counter.
|
||||||
void eventDelCounter(const Event_DelCounter &event);
|
void eventDelCounter(const Event_DelCounter &event);
|
||||||
|
|
|
||||||
|
|
@ -175,7 +175,15 @@ void PlayerLogic::processPlayerInfo(const ServerInfo_Player &info)
|
||||||
const ServerInfo_Card &cardInfo = zoneInfo.card_list(j);
|
const ServerInfo_Card &cardInfo = zoneInfo.card_list(j);
|
||||||
auto *card = new CardItem(this);
|
auto *card = new CardItem(this);
|
||||||
card->processCardInfo(cardInfo);
|
card->processCardInfo(cardInfo);
|
||||||
zone->addCard(card, false, cardInfo.x(), cardInfo.y());
|
// Zones without coordinates (hand, piles, stack) preserve the order
|
||||||
|
// they arrive in on the server in the positions of their cards list.
|
||||||
|
// The x coordinate of such cards is always 0, so inserting at it
|
||||||
|
// would reverse the list on reconnect. Append instead.
|
||||||
|
if (zoneInfo.with_coords()) {
|
||||||
|
zone->addCard(card, false, cardInfo.x(), cardInfo.y());
|
||||||
|
} else {
|
||||||
|
zone->addCard(card, false, -1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (zoneInfo.has_always_reveal_top_card()) {
|
if (zoneInfo.has_always_reveal_top_card()) {
|
||||||
|
|
|
||||||
|
|
@ -75,6 +75,14 @@ PlayerLogic *PlayerManager::getPlayer(int playerId) const
|
||||||
return player;
|
return player;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void PlayerManager::clearSpectators()
|
||||||
|
{
|
||||||
|
const QList<int> spectatorIds = spectators.keys();
|
||||||
|
for (int spectatorId : spectatorIds) {
|
||||||
|
removeSpectator(spectatorId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void PlayerManager::onPlayerConceded(int playerId, bool conceded)
|
void PlayerManager::onPlayerConceded(int playerId, bool conceded)
|
||||||
{
|
{
|
||||||
// Everything else cares about this
|
// Everything else cares about this
|
||||||
|
|
|
||||||
|
|
@ -100,6 +100,9 @@ public:
|
||||||
emit spectatorRemoved(spectatorId, spectatorInfo);
|
emit spectatorRemoved(spectatorId, spectatorInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @brief Remove all spectators, emitting the removal signal for each. */
|
||||||
|
void clearSpectators();
|
||||||
|
|
||||||
[[nodiscard]] AbstractGame *getGame() const
|
[[nodiscard]] AbstractGame *getGame() const
|
||||||
{
|
{
|
||||||
return game;
|
return game;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
#include "abstract_card_item.h"
|
#include "abstract_card_item.h"
|
||||||
|
|
||||||
#include "../../client/settings/cache_settings.h"
|
#include "../../client/settings/cache_settings.h"
|
||||||
|
#include "../../interface/card_localization.h"
|
||||||
#include "../../interface/card_picture_loader/card_picture_loader.h"
|
#include "../../interface/card_picture_loader/card_picture_loader.h"
|
||||||
#include "../game_scene.h"
|
#include "../game_scene.h"
|
||||||
#include "../z_values.h"
|
#include "../z_values.h"
|
||||||
|
|
@ -26,6 +27,8 @@ AbstractCardItem::AbstractCardItem(QGraphicsItem *parent, const CardRef &cardRef
|
||||||
|
|
||||||
connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::displayCardNamesChanged, this,
|
connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::displayCardNamesChanged, this,
|
||||||
[this] { update(); });
|
[this] { update(); });
|
||||||
|
connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::cardLangChanged, this,
|
||||||
|
[this] { update(); });
|
||||||
refreshCardInfo();
|
refreshCardInfo();
|
||||||
|
|
||||||
connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::roundCardCornersChanged, this,
|
connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::roundCardCornersChanged, this,
|
||||||
|
|
@ -171,7 +174,7 @@ void AbstractCardItem::paintPicture(QPainter *painter, const QSizeF &translatedS
|
||||||
if (SettingsCache::instance().debug().getShowCardId()) {
|
if (SettingsCache::instance().debug().getShowCardId()) {
|
||||||
prefix = "#" + QString::number(id) + " ";
|
prefix = "#" + QString::number(id) + " ";
|
||||||
}
|
}
|
||||||
nameStr = prefix + cardRef.name;
|
nameStr = prefix + CardLocalization::displayName(getCardInfo());
|
||||||
}
|
}
|
||||||
painter->drawText(QRectF(3 * scaleFactor, 3 * scaleFactor, translatedSize.width() - 6 * scaleFactor,
|
painter->drawText(QRectF(3 * scaleFactor, 3 * scaleFactor, translatedSize.width() - 6 * scaleFactor,
|
||||||
translatedSize.height() - 6 * scaleFactor),
|
translatedSize.height() - 6 * scaleFactor),
|
||||||
|
|
|
||||||
|
|
@ -29,9 +29,9 @@ AbstractCounter::AbstractCounter(CounterState *state,
|
||||||
{
|
{
|
||||||
setAcceptHoverEvents(true);
|
setAcceptHoverEvents(true);
|
||||||
|
|
||||||
connect(state, &CounterState::valueChanged, this, [this](int oldValue, int newValue) {
|
connect(state, &CounterState::valueChanged, this, [this](int oldValue, int newValue, bool skipDamageAnimation) {
|
||||||
value = newValue;
|
value = newValue;
|
||||||
onValueChanged(oldValue, newValue);
|
onValueChanged(oldValue, newValue, skipDamageAnimation);
|
||||||
update();
|
update();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -230,7 +230,7 @@ void AbstractCounterDialog::changeValue(int diff)
|
||||||
setTextValue(QString::number(curValue));
|
setTextValue(QString::number(curValue));
|
||||||
}
|
}
|
||||||
|
|
||||||
void AbstractCounter::onValueChanged(int /*oldValue*/, int /*newValue*/)
|
void AbstractCounter::onValueChanged(int /*oldValue*/, int /*newValue*/, bool /*skipDamageAnimation*/)
|
||||||
{
|
{
|
||||||
// Default: no feedback. Subclasses such as PlayerCounter override this to
|
// Default: no feedback. Subclasses such as PlayerCounter override this to
|
||||||
// flash the counter on meaningful changes (life gain/loss).
|
// flash the counter on meaningful changes (life gain/loss).
|
||||||
|
|
|
||||||
|
|
@ -39,8 +39,9 @@ protected:
|
||||||
* @brief Hook for subclasses that need per-value-change feedback (e.g. life-total flash).
|
* @brief Hook for subclasses that need per-value-change feedback (e.g. life-total flash).
|
||||||
*
|
*
|
||||||
* Called whenever the counter's value changes, before the item repaints.
|
* Called whenever the counter's value changes, before the item repaints.
|
||||||
|
* @param skipDamageAnimation True when damage-related feedback should be suppressed (replay rewinds).
|
||||||
*/
|
*/
|
||||||
virtual void onValueChanged(int oldValue, int newValue);
|
virtual void onValueChanged(int oldValue, int newValue, bool skipDamageAnimation);
|
||||||
|
|
||||||
void mousePressEvent(QGraphicsSceneMouseEvent *event) override;
|
void mousePressEvent(QGraphicsSceneMouseEvent *event) override;
|
||||||
void hoverEnterEvent(QGraphicsSceneHoverEvent *event) override;
|
void hoverEnterEvent(QGraphicsSceneHoverEvent *event) override;
|
||||||
|
|
|
||||||
|
|
@ -316,7 +316,7 @@ void CardItem::drawAttachArrow()
|
||||||
|
|
||||||
for (const auto &item : scene()->selectedItems()) {
|
for (const auto &item : scene()->selectedItems()) {
|
||||||
CardItem *card = qgraphicsitem_cast<CardItem *>(item);
|
CardItem *card = qgraphicsitem_cast<CardItem *>(item);
|
||||||
if (card == nullptr) {
|
if (card == nullptr || card == this) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (card->getZone() != state->getZone()) {
|
if (card->getZone() != state->getZone()) {
|
||||||
|
|
|
||||||
|
|
@ -221,7 +221,12 @@ void GameScene::removePlayer(PlayerLogic *player)
|
||||||
|
|
||||||
clearArrowsForPlayer(player->getPlayerInfo()->getId());
|
clearArrowsForPlayer(player->getPlayerInfo()->getId());
|
||||||
|
|
||||||
for (ZoneViewWidget *zone : zoneViews) {
|
// Closing a view removes it from zoneViews synchronously, so iterate over a
|
||||||
|
// copy: otherwise a player with several open views (e.g. library and hand)
|
||||||
|
// only has the first one closed here and the remaining views are left
|
||||||
|
// pointing at a player that is about to be deleted.
|
||||||
|
const QList<ZoneViewWidget *> zoneViewCopy = zoneViews;
|
||||||
|
for (ZoneViewWidget *zone : zoneViewCopy) {
|
||||||
if (zone->getPlayer() == player) {
|
if (zone->getPlayer() == player) {
|
||||||
zone->close();
|
zone->close();
|
||||||
}
|
}
|
||||||
|
|
@ -664,7 +669,10 @@ CardItem *GameScene::findTopmostCardInZone(const QList<QGraphicsItem *> &items,
|
||||||
*/
|
*/
|
||||||
void GameScene::toggleZoneView(PlayerLogic *player, const QString &zoneName, int numberCards, bool isReversed)
|
void GameScene::toggleZoneView(PlayerLogic *player, const QString &zoneName, int numberCards, bool isReversed)
|
||||||
{
|
{
|
||||||
for (auto &view : zoneViews) {
|
// Closing a view removes it from zoneViews synchronously, so iterate over a
|
||||||
|
// copy to make sure every already-open matching view is closed.
|
||||||
|
const QList<ZoneViewWidget *> zoneViewCopy = zoneViews;
|
||||||
|
for (auto *view : zoneViewCopy) {
|
||||||
ZoneViewZone *temp = view->getZone();
|
ZoneViewZone *temp = view->getZone();
|
||||||
if (temp->getLogic()->getName() == zoneName && temp->getLogic()->getPlayer() == player &&
|
if (temp->getLogic()->getName() == zoneName && temp->getLogic()->getPlayer() == player &&
|
||||||
qobject_cast<ZoneViewZoneLogic *>(temp->getLogic())->getNumberCards() == numberCards) {
|
qobject_cast<ZoneViewZoneLogic *>(temp->getLogic())->getNumberCards() == numberCards) {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
#include "hand_counter.h"
|
#include "hand_counter.h"
|
||||||
|
|
||||||
|
#include "../interface/pixel_map_generator.h"
|
||||||
#include "zones/card_zone.h"
|
#include "zones/card_zone.h"
|
||||||
|
|
||||||
#include <QGraphicsSceneMouseEvent>
|
#include <QGraphicsSceneMouseEvent>
|
||||||
|
|
@ -32,7 +33,8 @@ void HandCounter::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*op
|
||||||
QSize translatedSize = painter->combinedTransform().mapRect(boundingRect()).size().toSize();
|
QSize translatedSize = painter->combinedTransform().mapRect(boundingRect()).size().toSize();
|
||||||
QPixmap cachedPixmap;
|
QPixmap cachedPixmap;
|
||||||
if (!QPixmapCache::find("handCounter" + QString::number(translatedSize.width()), &cachedPixmap)) {
|
if (!QPixmapCache::find("handCounter" + QString::number(translatedSize.width()), &cachedPixmap)) {
|
||||||
cachedPixmap = QPixmap("theme:hand").scaled(translatedSize, Qt::KeepAspectRatio, Qt::SmoothTransformation);
|
cachedPixmap =
|
||||||
|
themePixmap(QStringLiteral("hand")).scaled(translatedSize, Qt::KeepAspectRatio, Qt::SmoothTransformation);
|
||||||
QPixmapCache::insert("handCounter" + QString::number(translatedSize.width()), cachedPixmap);
|
QPixmapCache::insert("handCounter" + QString::number(translatedSize.width()), cachedPixmap);
|
||||||
}
|
}
|
||||||
resetPainterTransform(painter);
|
resetPainterTransform(painter);
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
#include "../../game/player/player_actions.h"
|
#include "../../game/player/player_actions.h"
|
||||||
#include "../../interface/card_picture_loader/card_picture_loader.h"
|
#include "../../interface/card_picture_loader/card_picture_loader.h"
|
||||||
#include "../../interface/widgets/cards/art_crop_attribution.h"
|
#include "../../interface/widgets/cards/art_crop_attribution.h"
|
||||||
|
#include "../../interface/widgets/cards/card_art_utils.h"
|
||||||
#include "../../interface/widgets/playmat/playmat_utils.h"
|
#include "../../interface/widgets/playmat/playmat_utils.h"
|
||||||
#include "../../interface/widgets/tabs/tab_game.h"
|
#include "../../interface/widgets/tabs/tab_game.h"
|
||||||
#include "../board/abstract_card_item.h"
|
#include "../board/abstract_card_item.h"
|
||||||
|
|
@ -251,8 +252,8 @@ void PlayerGraphicsItem::onCounterAdded(CounterState *state)
|
||||||
AbstractCounter *widget;
|
AbstractCounter *widget;
|
||||||
if (state->getName() == "life") {
|
if (state->getName() == "life") {
|
||||||
widget = playerTarget->addCounter(state);
|
widget = playerTarget->addCounter(state);
|
||||||
connect(state, &CounterState::valueChanged, this, [this](int oldValue, int newValue) {
|
connect(state, &CounterState::valueChanged, this, [this](int oldValue, int newValue, bool skipDamageAnimation) {
|
||||||
if (newValue < oldValue) {
|
if (newValue < oldValue && !skipDamageAnimation) {
|
||||||
tableZoneGraphicsItem->triggerDamageShimmer();
|
tableZoneGraphicsItem->triggerDamageShimmer();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -442,7 +443,7 @@ void PlayerGraphicsItem::updatePlaymat()
|
||||||
hasPlaymat = true;
|
hasPlaymat = true;
|
||||||
emit playmatChanged(true);
|
emit playmatChanged(true);
|
||||||
}
|
}
|
||||||
playmatPixmap = fullRes;
|
playmatPixmap = CardArtUtils::rotateSidewaysLayoutArt(fullRes, card);
|
||||||
update();
|
update();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -53,13 +53,13 @@ PlayerListWidget::PlayerListWidget(TabSupervisor *_tabSupervisor,
|
||||||
QWidget *parent)
|
QWidget *parent)
|
||||||
: QTreeWidget(parent), tabSupervisor(_tabSupervisor), client(_client), game(_game), gameStarted(false)
|
: QTreeWidget(parent), tabSupervisor(_tabSupervisor), client(_client), game(_game), gameStarted(false)
|
||||||
{
|
{
|
||||||
readyIcon = QPixmap("theme:icons/ready_start");
|
readyIcon = themePixmap(QStringLiteral("icons/ready_start"));
|
||||||
notReadyIcon = QPixmap("theme:icons/not_ready_start");
|
notReadyIcon = themePixmap(QStringLiteral("icons/not_ready_start"));
|
||||||
concededIcon = QPixmap("theme:icons/conceded");
|
concededIcon = themePixmap(QStringLiteral("icons/conceded"));
|
||||||
playerIcon = loadColorAdjustedPixmap("theme:icons/player");
|
playerIcon = loadColorAdjustedPixmap("theme:icons/player");
|
||||||
judgeIcon = loadColorAdjustedPixmap("theme:icons/scales");
|
judgeIcon = loadColorAdjustedPixmap("theme:icons/scales");
|
||||||
spectatorIcon = loadColorAdjustedPixmap("theme:icons/spectator");
|
spectatorIcon = loadColorAdjustedPixmap("theme:icons/spectator");
|
||||||
lockIcon = QPixmap("theme:icons/lock");
|
lockIcon = themePixmap(QStringLiteral("icons/lock"));
|
||||||
|
|
||||||
if (tabSupervisor) {
|
if (tabSupervisor) {
|
||||||
itemDelegate = new PlayerListItemDelegate(this);
|
itemDelegate = new PlayerListItemDelegate(this);
|
||||||
|
|
@ -92,6 +92,11 @@ void PlayerListWidget::retranslateUi()
|
||||||
|
|
||||||
void PlayerListWidget::addPlayer(const ServerInfo_PlayerProperties &player)
|
void PlayerListWidget::addPlayer(const ServerInfo_PlayerProperties &player)
|
||||||
{
|
{
|
||||||
|
if (players.contains(player.player_id())) {
|
||||||
|
updatePlayerProperties(player);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
QTreeWidgetItem *newPlayer = new PlayerListTWI;
|
QTreeWidgetItem *newPlayer = new PlayerListTWI;
|
||||||
players.insert(player.player_id(), newPlayer);
|
players.insert(player.player_id(), newPlayer);
|
||||||
updatePlayerProperties(player);
|
updatePlayerProperties(player);
|
||||||
|
|
@ -176,6 +181,17 @@ void PlayerListWidget::removePlayer(int playerId)
|
||||||
delete takeTopLevelItem(indexOfTopLevelItem(player));
|
delete takeTopLevelItem(indexOfTopLevelItem(player));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void PlayerListWidget::clearSpectators()
|
||||||
|
{
|
||||||
|
const QList<int> playerIds = players.keys();
|
||||||
|
for (int playerId : playerIds) {
|
||||||
|
QTreeWidgetItem *player = players.value(playerId, 0);
|
||||||
|
if (player && !player->data(1, Qt::UserRole).toBool()) {
|
||||||
|
removePlayer(playerId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void PlayerListWidget::setActivePlayer(int playerId)
|
void PlayerListWidget::setActivePlayer(int playerId)
|
||||||
{
|
{
|
||||||
QMapIterator<int, QTreeWidgetItem *> i(players);
|
QMapIterator<int, QTreeWidgetItem *> i(players);
|
||||||
|
|
|
||||||
|
|
@ -66,6 +66,7 @@ public slots:
|
||||||
void addPlayer(const ServerInfo_PlayerProperties &player);
|
void addPlayer(const ServerInfo_PlayerProperties &player);
|
||||||
void removePlayer(int playerId);
|
void removePlayer(int playerId);
|
||||||
void updatePlayerProperties(const ServerInfo_PlayerProperties &prop, int playerId = -1);
|
void updatePlayerProperties(const ServerInfo_PlayerProperties &prop, int playerId = -1);
|
||||||
|
void clearSpectators();
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
|
||||||
|
|
@ -69,7 +69,7 @@ void PlayerCounter::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void PlayerCounter::onValueChanged(int oldValue, int newValue)
|
void PlayerCounter::onValueChanged(int oldValue, int newValue, bool skipDamageAnimation)
|
||||||
{
|
{
|
||||||
flashDelta = newValue - oldValue;
|
flashDelta = newValue - oldValue;
|
||||||
if (flashDelta == 0) {
|
if (flashDelta == 0) {
|
||||||
|
|
@ -81,6 +81,11 @@ void PlayerCounter::onValueChanged(int oldValue, int newValue)
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (skipDamageAnimation) {
|
||||||
|
flashAlpha = 0.0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
flashAlpha = 1.0;
|
flashAlpha = 1.0;
|
||||||
flashClock.start();
|
flashClock.start();
|
||||||
if (scene()) {
|
if (scene()) {
|
||||||
|
|
@ -132,8 +137,18 @@ void PlayerTarget::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*o
|
||||||
QRectF translatedRect = painter->combinedTransform().mapRect(avatarBoundingRect);
|
QRectF translatedRect = painter->combinedTransform().mapRect(avatarBoundingRect);
|
||||||
QSize translatedSize = translatedRect.size().toSize();
|
QSize translatedSize = translatedRect.size().toSize();
|
||||||
QPixmap cachedPixmap;
|
QPixmap cachedPixmap;
|
||||||
|
// The key must cover everything the generated pawn depends on: the rendered
|
||||||
|
// size, the user level, and the pixmap being drawn. fullPixmap.cacheKey() is
|
||||||
|
// 0 for every null pixmap, so the default-pawn branch additionally needs the
|
||||||
|
// pawn's privlevel (lowercased, matching UserLevelPixmapGenerator) and colors
|
||||||
|
// in the key — otherwise two players without a custom avatar (and the same
|
||||||
|
// user level) would share one cached pawn.
|
||||||
const QString cacheKey = "avatar" + QString::number(translatedSize.width()) + "_" +
|
const QString cacheKey = "avatar" + QString::number(translatedSize.width()) + "_" +
|
||||||
QString::number(info->user_level()) + "_" + QString::number(fullPixmap.cacheKey());
|
QString::number(translatedSize.height()) + "_" + QString::number(info->user_level()) +
|
||||||
|
"_" + QString::number(fullPixmap.cacheKey()) + "_" +
|
||||||
|
QString::fromStdString(info->privlevel()).toLower() + "_" +
|
||||||
|
QString::fromStdString(info->pawn_colors().left_side()) + "_" +
|
||||||
|
QString::fromStdString(info->pawn_colors().right_side());
|
||||||
if (!QPixmapCache::find(cacheKey, &cachedPixmap)) {
|
if (!QPixmapCache::find(cacheKey, &cachedPixmap)) {
|
||||||
cachedPixmap = QPixmap(translatedSize.width(), translatedSize.height());
|
cachedPixmap = QPixmap(translatedSize.width(), translatedSize.height());
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ class PlayerCounter : public AbstractCounter, public IAnimatedItem
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
protected:
|
protected:
|
||||||
void onValueChanged(int oldValue, int newValue) override;
|
void onValueChanged(int oldValue, int newValue, bool skipDamageAnimation) override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
static constexpr qreal flashDurationMs = 450.0;
|
static constexpr qreal flashDurationMs = 450.0;
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,7 @@ ZoneViewWidget::ZoneViewWidget(PlayerLogic *_player,
|
||||||
searchEdit.setPlaceholderText(tr("Search by card name (or search expressions)"));
|
searchEdit.setPlaceholderText(tr("Search by card name (or search expressions)"));
|
||||||
searchEdit.setClearButtonEnabled(true);
|
searchEdit.setClearButtonEnabled(true);
|
||||||
searchEdit.addAction(loadColorAdjustedPixmap("theme:icons/search"), QLineEdit::LeadingPosition);
|
searchEdit.addAction(loadColorAdjustedPixmap("theme:icons/search"), QLineEdit::LeadingPosition);
|
||||||
auto help = searchEdit.addAction(QPixmap("theme:icons/info"), QLineEdit::TrailingPosition);
|
auto help = searchEdit.addAction(themePixmap(QStringLiteral("icons/info")), QLineEdit::TrailingPosition);
|
||||||
|
|
||||||
connect(help, &QAction::triggered, this, [this] { createSearchSyntaxHelpWindow(&searchEdit); });
|
connect(help, &QAction::triggered, this, [this] { createSearchSyntaxHelpWindow(&searchEdit); });
|
||||||
|
|
||||||
|
|
@ -549,7 +549,7 @@ void ZoneViewWidget::initStyleOption(QStyleOption *option) const
|
||||||
{
|
{
|
||||||
QStyleOptionTitleBar *titleBar = qstyleoption_cast<QStyleOptionTitleBar *>(option);
|
QStyleOptionTitleBar *titleBar = qstyleoption_cast<QStyleOptionTitleBar *>(option);
|
||||||
if (titleBar) {
|
if (titleBar) {
|
||||||
titleBar->icon = QPixmap("theme:cockatrice");
|
titleBar->icon = themePixmap(QStringLiteral("cockatrice"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
59
cockatrice/src/interface/card_localization.h
Normal file
59
cockatrice/src/interface/card_localization.h
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
#ifndef COCKATRICE_CARD_LOCALIZATION_H
|
||||||
|
#define COCKATRICE_CARD_LOCALIZATION_H
|
||||||
|
|
||||||
|
#include "../client/settings/cache_settings.h"
|
||||||
|
|
||||||
|
#include <QString>
|
||||||
|
#include <libcockatrice/card/card_info.h>
|
||||||
|
#include <libcockatrice/settings/cards_display_settings.h>
|
||||||
|
|
||||||
|
namespace CardLocalization
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @brief The language code selected for localized card text and images.
|
||||||
|
*/
|
||||||
|
inline QString displayLang()
|
||||||
|
{
|
||||||
|
return SettingsCache::instance().cardsDisplay().getCardLang();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Card name in the configured display language, falling back to English.
|
||||||
|
* @param card The card to display.
|
||||||
|
* @return The localized name, or an empty string for a null card.
|
||||||
|
*/
|
||||||
|
inline QString displayName(const CardInfoPtr &card)
|
||||||
|
{
|
||||||
|
return card.isNull() ? QString() : card->getLocalizedName(displayLang());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Card rules text in the configured display language, falling back to English.
|
||||||
|
* @param card The card to display.
|
||||||
|
* @return The localized text, or an empty string for a null card.
|
||||||
|
*/
|
||||||
|
inline QString displayText(const CardInfoPtr &card)
|
||||||
|
{
|
||||||
|
return card.isNull() ? QString() : card->getLocalizedText(displayLang());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Card name in the configured display language, falling back to English.
|
||||||
|
* @param card The card to display.
|
||||||
|
*/
|
||||||
|
inline QString displayName(const CardInfo &card)
|
||||||
|
{
|
||||||
|
return card.getLocalizedName(displayLang());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Card rules text in the configured display language, falling back to English.
|
||||||
|
* @param card The card to display.
|
||||||
|
*/
|
||||||
|
inline QString displayText(const CardInfo &card)
|
||||||
|
{
|
||||||
|
return card.getLocalizedText(displayLang());
|
||||||
|
}
|
||||||
|
} // namespace CardLocalization
|
||||||
|
|
||||||
|
#endif // COCKATRICE_CARD_LOCALIZATION_H
|
||||||
|
|
@ -1,13 +1,14 @@
|
||||||
#include "card_picture_loader.h"
|
#include "card_picture_loader.h"
|
||||||
|
|
||||||
#include "../../client/settings/cache_settings.h"
|
#include "../../client/settings/cache_settings.h"
|
||||||
|
#include "../pixel_map_generator.h"
|
||||||
#include "card_picture_loader_cache_method.h"
|
#include "card_picture_loader_cache_method.h"
|
||||||
#include "card_picture_loader_local_schemes.h"
|
#include "card_picture_loader_local_schemes.h"
|
||||||
|
|
||||||
#include <QApplication>
|
#include <QApplication>
|
||||||
#include <QBuffer>
|
#include <QBuffer>
|
||||||
#include <QDebug>
|
#include <QDebug>
|
||||||
#include <QDirIterator>
|
#include <QDir>
|
||||||
#include <QFileInfo>
|
#include <QFileInfo>
|
||||||
#include <QMainWindow>
|
#include <QMainWindow>
|
||||||
#include <QMovie>
|
#include <QMovie>
|
||||||
|
|
@ -19,6 +20,7 @@
|
||||||
#include <QThread>
|
#include <QThread>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <libcockatrice/settings/cache_storage_settings.h>
|
#include <libcockatrice/settings/cache_storage_settings.h>
|
||||||
|
#include <libcockatrice/settings/cards_display_settings.h>
|
||||||
#include <libcockatrice/settings/download_settings.h>
|
#include <libcockatrice/settings/download_settings.h>
|
||||||
#include <libcockatrice/settings/paths_settings.h>
|
#include <libcockatrice/settings/paths_settings.h>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
@ -36,8 +38,10 @@ CardPictureLoader::CardPictureLoader() : QObject(nullptr)
|
||||||
&CardPictureLoader::picsPathChanged);
|
&CardPictureLoader::picsPathChanged);
|
||||||
connect(&SettingsCache::instance().downloads(), &DownloadSettings::picDownloadChanged, this,
|
connect(&SettingsCache::instance().downloads(), &DownloadSettings::picDownloadChanged, this,
|
||||||
&CardPictureLoader::picDownloadChanged);
|
&CardPictureLoader::picDownloadChanged);
|
||||||
|
connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::cardLangChanged, this,
|
||||||
|
&CardPictureLoader::cardLangChanged);
|
||||||
|
|
||||||
qRegisterMetaType<ExactCard>();
|
qRegisterMetaType<ExactCard>("ExactCard");
|
||||||
connect(worker, &CardPictureLoaderWorker::imageLoaded, this, &CardPictureLoader::imageLoaded);
|
connect(worker, &CardPictureLoaderWorker::imageLoaded, this, &CardPictureLoader::imageLoaded);
|
||||||
|
|
||||||
statusBar = new CardPictureLoaderStatusBar(nullptr);
|
statusBar = new CardPictureLoaderStatusBar(nullptr);
|
||||||
|
|
@ -62,7 +66,7 @@ void CardPictureLoader::getCardBackPixmap(QPixmap &pixmap, QSize size)
|
||||||
QString backCacheKey = "_trice_card_back_" + QString::number(size.width()) + "x" + QString::number(size.height());
|
QString backCacheKey = "_trice_card_back_" + QString::number(size.width()) + "x" + QString::number(size.height());
|
||||||
if (!QPixmapCache::find(backCacheKey, &pixmap)) {
|
if (!QPixmapCache::find(backCacheKey, &pixmap)) {
|
||||||
qCDebug(CardPictureLoaderLog) << "PictureLoader: cache miss for" << backCacheKey;
|
qCDebug(CardPictureLoaderLog) << "PictureLoader: cache miss for" << backCacheKey;
|
||||||
QPixmap tmpPixmap("theme:cardback");
|
QPixmap tmpPixmap = themePixmap(QStringLiteral("cardback"));
|
||||||
|
|
||||||
if (tmpPixmap.isNull()) {
|
if (tmpPixmap.isNull()) {
|
||||||
qCWarning(CardPictureLoaderLog) << "Failed to load 'theme:cardback'! Using fallback pixmap.";
|
qCWarning(CardPictureLoaderLog) << "Failed to load 'theme:cardback'! Using fallback pixmap.";
|
||||||
|
|
@ -83,7 +87,7 @@ void CardPictureLoader::getCardBackLoadingInProgressPixmap(QPixmap &pixmap, QSiz
|
||||||
"_trice_card_back_inprogress_" + QString::number(size.width()) + "x" + QString::number(size.height());
|
"_trice_card_back_inprogress_" + QString::number(size.width()) + "x" + QString::number(size.height());
|
||||||
if (!QPixmapCache::find(backCacheKey, &pixmap)) {
|
if (!QPixmapCache::find(backCacheKey, &pixmap)) {
|
||||||
qCDebug(CardPictureLoaderCardBackCacheFailLog) << "PictureLoader: cache miss for" << backCacheKey;
|
qCDebug(CardPictureLoaderCardBackCacheFailLog) << "PictureLoader: cache miss for" << backCacheKey;
|
||||||
QPixmap tmpPixmap("theme:cardback");
|
QPixmap tmpPixmap = themePixmap(QStringLiteral("cardback"));
|
||||||
|
|
||||||
if (tmpPixmap.isNull()) {
|
if (tmpPixmap.isNull()) {
|
||||||
qCWarning(CardPictureLoaderLog) << "Failed to load 'theme:cardback' for in-progress state! Using fallback.";
|
qCWarning(CardPictureLoaderLog) << "Failed to load 'theme:cardback' for in-progress state! Using fallback.";
|
||||||
|
|
@ -105,7 +109,7 @@ void CardPictureLoader::getCardBackLoadingFailedPixmap(QPixmap &pixmap, QSize si
|
||||||
"_trice_card_back_failed_" + QString::number(size.width()) + "x" + QString::number(size.height());
|
"_trice_card_back_failed_" + QString::number(size.width()) + "x" + QString::number(size.height());
|
||||||
if (!QPixmapCache::find(backCacheKey, &pixmap)) {
|
if (!QPixmapCache::find(backCacheKey, &pixmap)) {
|
||||||
qCDebug(CardPictureLoaderCardBackCacheFailLog) << "PictureLoader: cache miss for" << backCacheKey;
|
qCDebug(CardPictureLoaderCardBackCacheFailLog) << "PictureLoader: cache miss for" << backCacheKey;
|
||||||
QPixmap tmpPixmap("theme:cardback");
|
QPixmap tmpPixmap = themePixmap(QStringLiteral("cardback"));
|
||||||
|
|
||||||
if (tmpPixmap.isNull()) {
|
if (tmpPixmap.isNull()) {
|
||||||
qCWarning(CardPictureLoaderLog) << "Failed to load 'theme:cardback' for failed state! Using fallback.";
|
qCWarning(CardPictureLoaderLog) << "Failed to load 'theme:cardback' for failed state! Using fallback.";
|
||||||
|
|
@ -138,7 +142,8 @@ void CardPictureLoader::getPixmap(QPixmap &pixmap, const ExactCard &card, QSize
|
||||||
QPixmap bigPixmap;
|
QPixmap bigPixmap;
|
||||||
if (QPixmapCache::find(key, &bigPixmap)) {
|
if (QPixmapCache::find(key, &bigPixmap)) {
|
||||||
if (bigPixmap.isNull()) {
|
if (bigPixmap.isNull()) {
|
||||||
getCardBackLoadingFailedPixmap(pixmap, size);
|
// Leave the pixmap null so callers fall back to a solid color
|
||||||
|
// instead of showing the card back.
|
||||||
QDateTime failedAtTime = getInstance().failedAt.value(key);
|
QDateTime failedAtTime = getInstance().failedAt.value(key);
|
||||||
if (!failedAtTime.isValid() ||
|
if (!failedAtTime.isValid() ||
|
||||||
failedAtTime.addSecs(RETRY_FAILED_CARDS_SECS) < QDateTime::currentDateTime()) {
|
failedAtTime.addSecs(RETRY_FAILED_CARDS_SECS) < QDateTime::currentDateTime()) {
|
||||||
|
|
@ -204,7 +209,49 @@ void CardPictureLoader::imageLoaded(const ExactCard &card, const QImage &image)
|
||||||
card.emitPixmapUpdated();
|
card.emitPixmapUpdated();
|
||||||
}
|
}
|
||||||
|
|
||||||
void CardPictureLoader::saveCardImageToLocalStorage(const ExactCard &card, const QPixmap &pixmap)
|
void CardPictureLoader::deleteAllLocalOverrides(const ExactCard &card)
|
||||||
|
{
|
||||||
|
const QString picsRoot = SettingsCache::instance().paths().getPicsPath();
|
||||||
|
if (picsRoot.isEmpty() || !card) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QDir baseDir(picsRoot);
|
||||||
|
if (!baseDir.cd("downloadedPics")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QString name = card.getInfo().getCorrectedName();
|
||||||
|
|
||||||
|
QString set, collector, uuid;
|
||||||
|
auto printing = card.getPrinting();
|
||||||
|
if (printing.getSet()) {
|
||||||
|
set = printing.getSet()->getCorrectedShortName();
|
||||||
|
collector = printing.getProperty("num");
|
||||||
|
uuid = printing.getUuid();
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const auto &scheme : CardPictureLoaderLocalSchemes::exportSchemes()) {
|
||||||
|
QString rel = CardPictureLoaderLocalSchemes::expandPattern(scheme.pattern, name, set, collector, uuid);
|
||||||
|
|
||||||
|
if (rel.isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
rel += ".png";
|
||||||
|
rel = QDir::cleanPath(rel);
|
||||||
|
|
||||||
|
QString fullPath = baseDir.filePath(rel);
|
||||||
|
|
||||||
|
if (QFile::exists(fullPath)) {
|
||||||
|
QFile::remove(fullPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void CardPictureLoader::saveCardImageToLocalStorage(const ExactCard &card,
|
||||||
|
const QPixmap &pixmap,
|
||||||
|
const bool allowOverwrite)
|
||||||
{
|
{
|
||||||
if (pixmap.isNull() || !card) {
|
if (pixmap.isNull() || !card) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -264,8 +311,9 @@ void CardPictureLoader::saveCardImageToLocalStorage(const ExactCard &card, const
|
||||||
|
|
||||||
QFileInfo outInfo(baseDir.filePath(relativePath));
|
QFileInfo outInfo(baseDir.filePath(relativePath));
|
||||||
|
|
||||||
// Do not overwrite existing files
|
// Automatic cache writes (FILESYSTEM_CACHE) must never clobber an explicit user override.
|
||||||
if (outInfo.exists()) {
|
// Only the explicit override paths pass allowOverwrite == true.
|
||||||
|
if (!allowOverwrite && outInfo.exists()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -286,6 +334,122 @@ void CardPictureLoader::saveCardImageToLocalStorage(const ExactCard &card, const
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void CardPictureLoader::installPrintingOverrideOnLoad(const ExactCard &originalCard, const ExactCard &overrideCard)
|
||||||
|
{
|
||||||
|
// Overriding a card with itself is the reset case, not a real override: every code path below
|
||||||
|
// would re-enter itself through emitPixmapUpdated(). Reject it outright.
|
||||||
|
if (originalCard == overrideCard) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
CardInfoPtr cardPtr = overrideCard.getCardPtr();
|
||||||
|
if (!cardPtr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Heap-allocate so the lambda can capture it before the connection is made
|
||||||
|
auto *connectionHandle = new QMetaObject::Connection;
|
||||||
|
|
||||||
|
*connectionHandle =
|
||||||
|
connect(cardPtr.data(), &CardInfo::pixmapUpdated, cardPtr.data(),
|
||||||
|
[originalCard, overrideCard, connectionHandle, this](const PrintingInfo &printing) {
|
||||||
|
// All printings share the same CardInfo, so ignore updates triggered by any
|
||||||
|
// other printing (e.g., the original card re-loading from disk).
|
||||||
|
if (printing != overrideCard.getPrinting()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QPixmap pixmap;
|
||||||
|
if (QPixmapCache::find(overrideCard.getPixmapCacheKey(), &pixmap) && !pixmap.isNull()) {
|
||||||
|
// The override art has resolved — persist it and reflect it immediately.
|
||||||
|
// Retire the connection before emitting so the refresh can't re-enter.
|
||||||
|
saveCardImageToLocalStorage(originalCard, pixmap, /*allowOverwrite=*/true);
|
||||||
|
|
||||||
|
QObject::disconnect(*connectionHandle);
|
||||||
|
delete connectionHandle;
|
||||||
|
|
||||||
|
QPixmapCache::clear();
|
||||||
|
originalCard.emitPixmapUpdated();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The art could not be resolved. Keep the connection armed so a late resolution
|
||||||
|
// still lands, and surface a visible refusal instead of a silent no-op. An
|
||||||
|
// override already on disk is left untouched and simply re-displayed.
|
||||||
|
QPixmapCache::clear();
|
||||||
|
if (!hasLocalOverrides(originalCard)) {
|
||||||
|
QPixmap refusedPixmap;
|
||||||
|
getCardBackLoadingFailedPixmap(refusedPixmap, QSize(480, 672));
|
||||||
|
QPixmapCache::insert(originalCard.getPixmapCacheKey(), refusedPixmap);
|
||||||
|
}
|
||||||
|
originalCard.emitPixmapUpdated();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Now enqueue; if the image is already loading (deduplicated in the worker),
|
||||||
|
// the signal will still fire when it completes
|
||||||
|
CardPictureLoader::getInstance().worker->enqueueImageLoad(overrideCard);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CardPictureLoader::installPrintingOverride(const ExactCard &originalCard, const ExactCard &overrideCard)
|
||||||
|
{
|
||||||
|
// Same guard as installPrintingOverrideOnLoad: self-override is the reset case.
|
||||||
|
if (originalCard == overrideCard) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QPixmap pixmap;
|
||||||
|
const QString key = overrideCard.getPixmapCacheKey();
|
||||||
|
|
||||||
|
if (QPixmapCache::find(key, &pixmap) && !pixmap.isNull()) {
|
||||||
|
// Already cached — save immediately; the caller refreshes the card.
|
||||||
|
saveCardImageToLocalStorage(originalCard, pixmap, /*allowOverwrite=*/true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache miss or previously failed load — enqueue load and wait for the signal.
|
||||||
|
installPrintingOverrideOnLoad(originalCard, overrideCard);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CardPictureLoader::hasLocalOverrides(const ExactCard &card)
|
||||||
|
{
|
||||||
|
const QString picsRoot = SettingsCache::instance().paths().getPicsPath();
|
||||||
|
if (picsRoot.isEmpty() || !card) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
QDir baseDir(picsRoot);
|
||||||
|
if (!baseDir.cd("downloadedPics")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QString name = card.getInfo().getCorrectedName();
|
||||||
|
|
||||||
|
QString set, collector, uuid;
|
||||||
|
const PrintingInfo printing = card.getPrinting();
|
||||||
|
if (printing.getSet()) {
|
||||||
|
set = printing.getSet()->getCorrectedShortName();
|
||||||
|
collector = printing.getProperty("num");
|
||||||
|
uuid = printing.getUuid();
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const auto &scheme : CardPictureLoaderLocalSchemes::exportSchemes()) {
|
||||||
|
QString rel = CardPictureLoaderLocalSchemes::expandPattern(scheme.pattern, name, set, collector, uuid);
|
||||||
|
|
||||||
|
if (rel.isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
rel += ".png";
|
||||||
|
rel = QDir::cleanPath(rel);
|
||||||
|
|
||||||
|
if (QFile::exists(baseDir.filePath(rel))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
void CardPictureLoader::clearPixmapCache()
|
void CardPictureLoader::clearPixmapCache()
|
||||||
{
|
{
|
||||||
QPixmapCache::clear();
|
QPixmapCache::clear();
|
||||||
|
|
@ -325,31 +489,11 @@ void CardPictureLoader::picsPathChanged()
|
||||||
QPixmapCache::clear();
|
QPixmapCache::clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool CardPictureLoader::hasCustomArt()
|
void CardPictureLoader::cardLangChanged()
|
||||||
{
|
{
|
||||||
auto picsPath = SettingsCache::instance().paths().getPicsPath();
|
// Localized images are fetched via a different URL, but the in-memory
|
||||||
QDirIterator it(picsPath, QDir::Dirs | QDir::NoDotAndDotDot);
|
// pixmap cache is keyed by card name/uuid, so drop everything cached
|
||||||
|
// (including failure timestamps) to force a reload in the new language.
|
||||||
// Check if there is at least one non-directory file in the pics path, other
|
QPixmapCache::clear();
|
||||||
// than in the "downloadedPics" subdirectory.
|
failedAt.clear();
|
||||||
while (it.hasNext()) {
|
|
||||||
#if (QT_VERSION >= QT_VERSION_CHECK(6, 3, 0))
|
|
||||||
QFileInfo dir(it.nextFileInfo());
|
|
||||||
#else
|
|
||||||
// nextFileInfo() is only available in Qt 6.3+, for previous versions, we build
|
|
||||||
// the QFileInfo from a QString which requires more system calls.
|
|
||||||
QFileInfo dir(it.next());
|
|
||||||
#endif
|
|
||||||
|
|
||||||
if (it.fileName() == "downloadedPics") {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
QDirIterator subIt(it.filePath(), QDir::Files, QDirIterator::Subdirectories | QDirIterator::FollowSymlinks);
|
|
||||||
if (subIt.hasNext()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -97,10 +97,17 @@ public:
|
||||||
static void cacheCardPixmaps(const QList<ExactCard> &cards);
|
static void cacheCardPixmaps(const QList<ExactCard> &cards);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Check if the user has custom card art in the picsPath directory.
|
* @brief Check if a local override image already exists for the card.
|
||||||
* @return True if any custom art exists.
|
* @param card The card to check.
|
||||||
|
* @return True if the card has at least one locally stored override image.
|
||||||
*/
|
*/
|
||||||
static bool hasCustomArt();
|
static bool hasLocalOverrides(const ExactCard &card);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Removes all locally stored override images for the card.
|
||||||
|
* @param card The card to remove the override images of.
|
||||||
|
*/
|
||||||
|
static void deleteAllLocalOverrides(const ExactCard &card);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Clears the in-memory QPixmap cache for all cards.
|
* @brief Clears the in-memory QPixmap cache for all cards.
|
||||||
|
|
@ -120,7 +127,9 @@ public slots:
|
||||||
* @param image Loaded QImage.
|
* @param image Loaded QImage.
|
||||||
*/
|
*/
|
||||||
void imageLoaded(const ExactCard &card, const QImage &image);
|
void imageLoaded(const ExactCard &card, const QImage &image);
|
||||||
void saveCardImageToLocalStorage(const ExactCard &card, const QPixmap &pixmap);
|
void saveCardImageToLocalStorage(const ExactCard &card, const QPixmap &pixmap, bool allowOverwrite = false);
|
||||||
|
void installPrintingOverride(const ExactCard &originalCard, const ExactCard &overrideCard);
|
||||||
|
void installPrintingOverrideOnLoad(const ExactCard &originalCard, const ExactCard &overrideCard);
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
/**
|
/**
|
||||||
|
|
@ -134,6 +143,12 @@ private slots:
|
||||||
* Clears the QPixmap cache to reload images.
|
* Clears the QPixmap cache to reload images.
|
||||||
*/
|
*/
|
||||||
void picsPathChanged();
|
void picsPathChanged();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Triggered when the card language setting changes.
|
||||||
|
* Clears the in-memory picture caches so images reload in the new language.
|
||||||
|
*/
|
||||||
|
void cardLangChanged();
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
|
||||||
|
|
@ -94,6 +94,10 @@ QImage CardPictureLoaderLocal::tryLoadCardImageFromDisk(const QString &setName,
|
||||||
candidatePaths << picsPath + "/downloadedPics/" + setName + "/" + nameVariant;
|
candidatePaths << picsPath + "/downloadedPics/" + setName + "/" + nameVariant;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Non-set-folder export schemes (e.g., Name_Set_Collector) write straight into
|
||||||
|
// downloadedPics/; check there as a fallback so local overrides round-trip.
|
||||||
|
candidatePaths << picsPath + "/downloadedPics/" + nameVariant;
|
||||||
|
|
||||||
for (const QString &path : candidatePaths) {
|
for (const QString &path : candidatePaths) {
|
||||||
QFileInfo fileInfo(path);
|
QFileInfo fileInfo(path);
|
||||||
QDir dir = fileInfo.dir();
|
QDir dir = fileInfo.dir();
|
||||||
|
|
@ -105,7 +109,8 @@ QImage CardPictureLoaderLocal::tryLoadCardImageFromDisk(const QString &setName,
|
||||||
|
|
||||||
QStringList files = dir.entryList(QDir::Files);
|
QStringList files = dir.entryList(QDir::Files);
|
||||||
for (const QString &file : files) {
|
for (const QString &file : files) {
|
||||||
if (!file.startsWith(baseName)) {
|
QFileInfo fi(file);
|
||||||
|
if (fi.completeBaseName() != baseName) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -84,8 +84,8 @@ void CardPictureLoaderWorker::queueRequest(const QUrl &url, CardPictureLoaderWor
|
||||||
SettingsCache::instance().cacheStorage().getCardPictureLoaderCacheMethod()) ==
|
SettingsCache::instance().cacheStorage().getCardPictureLoaderCacheMethod()) ==
|
||||||
CardPictureLoaderCacheMethod::CacheMethod::NETWORK_CACHE &&
|
CardPictureLoaderCacheMethod::CacheMethod::NETWORK_CACHE &&
|
||||||
cache->metaData(url).isValid()) {
|
cache->metaData(url).isValid()) {
|
||||||
// If we hit a cached url, we get to make the request for free, since it won't contribute towards the
|
// A request that will be served from the disk cache never touches the network and therefore
|
||||||
// rate-limit
|
// doesn't use up any of the rate limit, so it gets to skip the queue.
|
||||||
makeRequest(url, worker);
|
makeRequest(url, worker);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -107,10 +107,13 @@ QNetworkReply *CardPictureLoaderWorker::makeRequest(const QUrl &url, CardPicture
|
||||||
req.setHeader(QNetworkRequest::UserAgentHeader, QString("Cockatrice %1").arg(VERSION_STRING));
|
req.setHeader(QNetworkRequest::UserAgentHeader, QString("Cockatrice %1").arg(VERSION_STRING));
|
||||||
req.setRawHeader("Accept", "image/avif,image/webp,image/apng,image/,/*;q=0.8");
|
req.setRawHeader("Accept", "image/avif,image/webp,image/apng,image/,/*;q=0.8");
|
||||||
|
|
||||||
bool useNetworkCache =
|
// Cached entries are served straight from the disk cache even when picture downloads are
|
||||||
!picDownload && static_cast<CardPictureLoaderCacheMethod::CacheMethod>(
|
// enabled: re-fetching an already-cached image would burn the rate limit for nothing. Only a
|
||||||
SettingsCache::instance().cacheStorage().getCardPictureLoaderCacheMethod()) ==
|
// genuine cache miss goes to the network, and only when downloads are enabled.
|
||||||
CardPictureLoaderCacheMethod::CacheMethod::NETWORK_CACHE;
|
bool useNetworkCache = static_cast<CardPictureLoaderCacheMethod::CacheMethod>(
|
||||||
|
SettingsCache::instance().cacheStorage().getCardPictureLoaderCacheMethod()) ==
|
||||||
|
CardPictureLoaderCacheMethod::CacheMethod::NETWORK_CACHE &&
|
||||||
|
(cache->metaData(url).isValid() || !picDownload);
|
||||||
|
|
||||||
req.setAttribute(QNetworkRequest::CacheLoadControlAttribute,
|
req.setAttribute(QNetworkRequest::CacheLoadControlAttribute,
|
||||||
useNetworkCache ? QNetworkRequest::AlwaysCache : QNetworkRequest::AlwaysNetwork);
|
useNetworkCache ? QNetworkRequest::AlwaysCache : QNetworkRequest::AlwaysNetwork);
|
||||||
|
|
|
||||||
|
|
@ -94,7 +94,8 @@ void CardPictureToLoad::populateSetUrls()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const QString &urlTemplate : urlTemplates) {
|
const QStringList orderedTemplates = urlTemplates;
|
||||||
|
for (const QString &urlTemplate : orderedTemplates) {
|
||||||
QString transformedUrl = transformUrl(urlTemplate);
|
QString transformedUrl = transformUrl(urlTemplate);
|
||||||
|
|
||||||
if (!transformedUrl.isEmpty()) {
|
if (!transformedUrl.isEmpty()) {
|
||||||
|
|
@ -282,8 +283,15 @@ QString CardPictureToLoad::transformUrl(const QString &urlTemplate) const
|
||||||
}
|
}
|
||||||
|
|
||||||
// language setting
|
// language setting
|
||||||
transformMap["!sflang!"] = QString(QCoreApplication::translate(
|
const QString cardLang = SettingsCache::instance().cardsDisplay().getCardLang();
|
||||||
"PictureLoader", "en", "code for scryfall's language property, not available for all languages"));
|
transformMap["!sflang!"] = cardLang;
|
||||||
|
|
||||||
|
// The localized printing's own id is unknown, so Scryfall must resolve it by
|
||||||
|
// its translated name (see populateSetUrls); expose that name for the
|
||||||
|
// `/cards/named` template.
|
||||||
|
if (cardLang != "en") {
|
||||||
|
transformMap["!localizedName!"] = card.getInfo().getLocalizedName(cardLang);
|
||||||
|
}
|
||||||
|
|
||||||
QString transformedUrl = urlTemplate;
|
QString transformedUrl = urlTemplate;
|
||||||
for (const QString &prop : transformMap.keys()) {
|
for (const QString &prop : transformMap.keys()) {
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ enum Format
|
||||||
PlainText,
|
PlainText,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is cockatrice's native deck file format, and supports deck metadata such as banner cards and tags.
|
* This is Cockatrice's native deck file format, and supports deck metadata such as banner cards and tags.
|
||||||
* Stored as .cod files.
|
* Stored as .cod files.
|
||||||
*/
|
*/
|
||||||
Cockatrice
|
Cockatrice
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,7 @@ DeckLoader::loadFromFile(const QString &fileName, DeckFileFormat::Format fmt, bo
|
||||||
result = deckList.loadFromFile_Native(&file);
|
result = deckList.loadFromFile_Native(&file);
|
||||||
if (!result) {
|
if (!result) {
|
||||||
qCInfo(DeckLoaderLog) << "Failed to load " << fileName
|
qCInfo(DeckLoaderLog) << "Failed to load " << fileName
|
||||||
<< "as cockatrice format; retrying as plain format";
|
<< "as Cockatrice format; retrying as plain format";
|
||||||
file.seek(0);
|
file.seek(0);
|
||||||
result = deckList.loadFromFile_Plain(&file, CardNameNormalizer());
|
result = deckList.loadFromFile_Plain(&file, CardNameNormalizer());
|
||||||
fmt = DeckFileFormat::PlainText;
|
fmt = DeckFileFormat::PlainText;
|
||||||
|
|
|
||||||
|
|
@ -131,7 +131,7 @@ public:
|
||||||
static void printDeckList(QPrinter *printer, const DeckList &deckList);
|
static void printDeckList(QPrinter *printer, const DeckList &deckList);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts the given deck's file to the cockatrice file format.
|
* Converts the given deck's file to the Cockatrice file format.
|
||||||
* Uses the lastLoadInfo in the LoadedDeck to determine the current name of the file and where to save to.
|
* Uses the lastLoadInfo in the LoadedDeck to determine the current name of the file and where to save to.
|
||||||
* @param deck The deck to convert. Should have valid lastLoadInfo. Will update the lastLoadInfo.
|
* @param deck The deck to convert. Should have valid lastLoadInfo. Will update the lastLoadInfo.
|
||||||
* @return Whether the conversion succeeded.
|
* @return Whether the conversion succeeded.
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
#include "palette_editor_dialog.h"
|
#include "palette_editor_dialog.h"
|
||||||
|
|
||||||
#include "../../client/settings/cache_settings.h"
|
|
||||||
#include "../theme_manager.h"
|
#include "../theme_manager.h"
|
||||||
#include "palette_generator.h"
|
#include "palette_generator.h"
|
||||||
#include "palette_grid_widget.h"
|
#include "palette_grid_widget.h"
|
||||||
|
|
@ -11,31 +10,11 @@
|
||||||
#include <QDialogButtonBox>
|
#include <QDialogButtonBox>
|
||||||
#include <QDir>
|
#include <QDir>
|
||||||
#include <QFile>
|
#include <QFile>
|
||||||
#include <QFileInfo>
|
|
||||||
#include <QFrame>
|
#include <QFrame>
|
||||||
#include <QGuiApplication>
|
|
||||||
#include <QLabel>
|
#include <QLabel>
|
||||||
#include <QLoggingCategory>
|
|
||||||
#include <QMessageBox>
|
#include <QMessageBox>
|
||||||
#include <QPushButton>
|
#include <QPushButton>
|
||||||
#include <QStyleHints>
|
|
||||||
#include <QTimer>
|
#include <QTimer>
|
||||||
#include <libcockatrice/settings/paths_settings.h>
|
|
||||||
|
|
||||||
// Probe whether a directory is truly writable by trying to create and remove a
|
|
||||||
// temporary file. QFileInfo::isWritable() on a directory is unreliable (notably
|
|
||||||
// on Windows where UAC VirtualStore can make a system dir appear writable).
|
|
||||||
static bool isDirReallyWritable(const QString &dirPath)
|
|
||||||
{
|
|
||||||
const QString probe = QDir(dirPath).absoluteFilePath(".cockatrice_write_test");
|
|
||||||
QFile f(probe);
|
|
||||||
if (!f.open(QIODevice::WriteOnly)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
f.close();
|
|
||||||
f.remove();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
PaletteEditorDialog::PaletteEditorDialog(const QString &_themeDirPath, const QString &_themeName, QWidget *parent)
|
PaletteEditorDialog::PaletteEditorDialog(const QString &_themeDirPath, const QString &_themeName, QWidget *parent)
|
||||||
: QDialog(parent), themeDirPath(_themeDirPath), themeName(_themeName)
|
: QDialog(parent), themeDirPath(_themeDirPath), themeName(_themeName)
|
||||||
|
|
@ -46,14 +25,7 @@ PaletteEditorDialog::PaletteEditorDialog(const QString &_themeDirPath, const QSt
|
||||||
// Resolve a writable directory for saving. Built-in (Default / Fusion) and
|
// Resolve a writable directory for saving. Built-in (Default / Fusion) and
|
||||||
// other read-only theme directories must be customised in the user-writable
|
// other read-only theme directories must be customised in the user-writable
|
||||||
// themes directory; otherwise the write would fail or be lost on upgrade.
|
// themes directory; otherwise the write would fail or be lost on upgrade.
|
||||||
if (!themeDirPath.isEmpty() && isDirReallyWritable(themeDirPath)) {
|
saveDir = ThemeManager::writableThemeDir(themeName);
|
||||||
saveDir = themeDirPath;
|
|
||||||
} else {
|
|
||||||
saveDir = QDir(SettingsCache::instance().paths().getThemesPath()).absoluteFilePath(themeName);
|
|
||||||
if (!QDir().mkpath(saveDir)) {
|
|
||||||
qWarning() << "Failed to create palette save directory:" << saveDir;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load both scheme configs upfront so switching is instant
|
// Load both scheme configs upfront so switching is instant
|
||||||
loadSchemes();
|
loadSchemes();
|
||||||
|
|
@ -214,7 +186,7 @@ void PaletteEditorDialog::retranslateUi()
|
||||||
resetBtn->setToolTip(tr("Discard unsaved edits and restore the last saved palette"));
|
resetBtn->setToolTip(tr("Discard unsaved edits and restore the last saved palette"));
|
||||||
saveBtn->setToolTip(tr("Write palette-%1.toml and reload the theme").arg(loadedScheme.toLower()));
|
saveBtn->setToolTip(tr("Write palette-%1.toml and reload the theme").arg(loadedScheme.toLower()));
|
||||||
|
|
||||||
if (saveDir.isEmpty() || !isDirReallyWritable(saveDir)) {
|
if (saveDir.isEmpty() || !ThemeManager::isDirReallyWritable(saveDir)) {
|
||||||
saveBtn->setEnabled(false);
|
saveBtn->setEnabled(false);
|
||||||
saveBtn->setToolTip(tr("Cannot save: this theme has no writable directory"));
|
saveBtn->setToolTip(tr("Cannot save: this theme has no writable directory"));
|
||||||
}
|
}
|
||||||
|
|
@ -297,7 +269,7 @@ void PaletteEditorDialog::onSave()
|
||||||
if (it.key() == loadedScheme) {
|
if (it.key() == loadedScheme) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (it.value().colors == savedConfig.value(it.key()).colors) {
|
if (it.value() == savedConfig.value(it.key())) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (!ThemeManager::commitPalette(saveDir, it.key(), it.value())) {
|
if (!ThemeManager::commitPalette(saveDir, it.key(), it.value())) {
|
||||||
|
|
@ -308,7 +280,7 @@ void PaletteEditorDialog::onSave()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Commit the active scheme last so the global colour scheme matches.
|
// Commit the active scheme last so the global colour scheme matches.
|
||||||
if (workingConfig[loadedScheme].colors != savedConfig.value(loadedScheme).colors) {
|
if (workingConfig[loadedScheme] != savedConfig.value(loadedScheme)) {
|
||||||
if (!ThemeManager::commitPalette(saveDir, loadedScheme, workingConfig[loadedScheme])) {
|
if (!ThemeManager::commitPalette(saveDir, loadedScheme, workingConfig[loadedScheme])) {
|
||||||
QMessageBox::warning(this, tr("Save failed"),
|
QMessageBox::warning(this, tr("Save failed"),
|
||||||
tr("Could not write %1 to:\n%2").arg(PaletteConfig::fileName(loadedScheme), saveDir));
|
tr("Could not write %1 to:\n%2").arg(PaletteConfig::fileName(loadedScheme), saveDir));
|
||||||
|
|
|
||||||
|
|
@ -150,6 +150,17 @@ PaletteConfig fromAccent(const QColor &accent, int intensity, const QString &sch
|
||||||
cfg.colors[CG::Disabled][CR::HighlightedText] = disText;
|
cfg.colors[CG::Disabled][CR::HighlightedText] = disText;
|
||||||
cfg.colors[CG::Inactive][CR::HighlightedText] = dark ? Qt::white : Qt::black;
|
cfg.colors[CG::Inactive][CR::HighlightedText] = dark ? Qt::white : Qt::black;
|
||||||
|
|
||||||
|
// Accent: same primary hue as Highlight, so palettes derived from a
|
||||||
|
// QuickSetup accent always carry a matching Accent role.
|
||||||
|
#if QT_VERSION >= QT_VERSION_CHECK(6, 6, 0)
|
||||||
|
set3(CR::Accent, hl, disText, hl);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Application role colors: Strong tracks the primary accent, while Soft is
|
||||||
|
// the lightened, desaturated companion used for button-gradient highlights.
|
||||||
|
cfg.appColors[AppColor::AccentStrong] = hl;
|
||||||
|
cfg.appColors[AppColor::AccentSoft] = hsl(accent.lightness() + 60, qRound(accent.hslSaturation() * 70 / 100.0));
|
||||||
|
|
||||||
// BrightText
|
// BrightText
|
||||||
QColor bright;
|
QColor bright;
|
||||||
if (achromatic) {
|
if (achromatic) {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
#include "palette_grid_widget.h"
|
#include "palette_grid_widget.h"
|
||||||
|
|
||||||
|
#include "../theme_manager.h"
|
||||||
|
|
||||||
#include <QApplication>
|
#include <QApplication>
|
||||||
#include <QGridLayout>
|
#include <QGridLayout>
|
||||||
#include <QLabel>
|
#include <QLabel>
|
||||||
|
|
@ -45,6 +47,11 @@ static const QMap<QPalette::ColorRole, const char *> ROLE_DESCRIPTIONS = {
|
||||||
{QPalette::Shadow, QT_TR_NOOP("Very dark shadow colour")},
|
{QPalette::Shadow, QT_TR_NOOP("Very dark shadow colour")},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
static const QMap<AppColor::Role, const char *> APP_ROLE_DESCRIPTIONS = {
|
||||||
|
{AppColor::AccentStrong, QT_TR_NOOP("Vivid primary accent (e.g. home-tab button gradient start)")},
|
||||||
|
{AppColor::AccentSoft, QT_TR_NOOP("Lightened, desaturated accent (e.g. home-tab button gradient end)")},
|
||||||
|
};
|
||||||
|
|
||||||
PaletteGridWidget::PaletteGridWidget(QWidget *parent) : QWidget(parent)
|
PaletteGridWidget::PaletteGridWidget(QWidget *parent) : QWidget(parent)
|
||||||
{
|
{
|
||||||
scroll = new QScrollArea(this);
|
scroll = new QScrollArea(this);
|
||||||
|
|
@ -122,6 +129,46 @@ void PaletteGridWidget::buildGrid(QWidget *host)
|
||||||
grid->addWidget(btn, row + 1, col + 1, Qt::AlignHCenter | Qt::AlignVCenter);
|
grid->addWidget(btn, row + 1, col + 1, Qt::AlignHCenter | Qt::AlignVCenter);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Application color section: one ColorButton per role below the role grid.
|
||||||
|
// These are not tied to a color group, so a single button spans the row.
|
||||||
|
QMetaEnum appEnum = QMetaEnum::fromType<AppColor::Role>();
|
||||||
|
|
||||||
|
const int appHeaderRow = roles.size() + 1;
|
||||||
|
|
||||||
|
auto *appHeader = new QLabel(tr("App colors"), host);
|
||||||
|
appHeader->setToolTip(tr("Application-specific colors layered on top of the Qt palette"));
|
||||||
|
QFont appHeaderFont = appHeader->font();
|
||||||
|
appHeaderFont.setBold(true);
|
||||||
|
appHeader->setFont(appHeaderFont);
|
||||||
|
appHeader->setAutoFillBackground(true);
|
||||||
|
appHeader->setContentsMargins(4, 4, 4, 4);
|
||||||
|
grid->addWidget(appHeader, appHeaderRow, 0, 1, 4);
|
||||||
|
headerLabels.append(appHeader);
|
||||||
|
|
||||||
|
for (int i = 0; i < appEnum.keyCount(); ++i) {
|
||||||
|
auto role = static_cast<AppColor::Role>(appEnum.value(i));
|
||||||
|
const int row = appHeaderRow + 1 + i;
|
||||||
|
|
||||||
|
if (i % 2 == 0) {
|
||||||
|
for (int col = 0; col < 4; ++col) {
|
||||||
|
auto *shade = new QWidget(host);
|
||||||
|
shade->setAutoFillBackground(true);
|
||||||
|
grid->addWidget(shade, row, col);
|
||||||
|
rowShadeWidgets.push_back(shade);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto *label = new QLabel(QString(appEnum.valueToKey(role)), host);
|
||||||
|
label->setToolTip(APP_ROLE_DESCRIPTIONS.value(role, {}));
|
||||||
|
label->setContentsMargins(4, 2, 8, 2);
|
||||||
|
grid->addWidget(label, row, 0);
|
||||||
|
|
||||||
|
auto *btn = new ColorButton(host);
|
||||||
|
connect(btn, &ColorButton::colorChanged, this, [this] { emit paletteChanged(); });
|
||||||
|
appColorButtons[role] = btn;
|
||||||
|
grid->addWidget(btn, row, 1, Qt::AlignHCenter | Qt::AlignVCenter);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void PaletteGridWidget::changeEvent(QEvent *e)
|
void PaletteGridWidget::changeEvent(QEvent *e)
|
||||||
|
|
@ -166,6 +213,16 @@ void PaletteGridWidget::loadPalette(const PaletteConfig &cfg)
|
||||||
colorButtons[group][role]->setColor(color);
|
colorButtons[group][role]->setColor(color);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QMetaEnum appEnum = QMetaEnum::fromType<AppColor::Role>();
|
||||||
|
for (int i = 0; i < appEnum.keyCount(); ++i) {
|
||||||
|
auto role = static_cast<AppColor::Role>(appEnum.value(i));
|
||||||
|
QColor color = cfg.appColors.value(role);
|
||||||
|
if (!color.isValid()) {
|
||||||
|
color = themeManager->appColor(role);
|
||||||
|
}
|
||||||
|
appColorButtons[role]->setColor(color);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
PaletteConfig PaletteGridWidget::currentPaletteConfig() const
|
PaletteConfig PaletteGridWidget::currentPaletteConfig() const
|
||||||
|
|
@ -176,5 +233,12 @@ PaletteConfig PaletteGridWidget::currentPaletteConfig() const
|
||||||
cfg.colors[group][role] = colorButtons[group][role]->getColor();
|
cfg.colors[group][role] = colorButtons[group][role]->getColor();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QMetaEnum appEnum = QMetaEnum::fromType<AppColor::Role>();
|
||||||
|
for (int i = 0; i < appEnum.keyCount(); ++i) {
|
||||||
|
auto role = static_cast<AppColor::Role>(appEnum.value(i));
|
||||||
|
cfg.appColors[role] = appColorButtons[role]->getColor();
|
||||||
|
}
|
||||||
|
|
||||||
return cfg;
|
return cfg;
|
||||||
}
|
}
|
||||||
|
|
@ -31,6 +31,7 @@ private:
|
||||||
void refreshChromePalettes();
|
void refreshChromePalettes();
|
||||||
|
|
||||||
QMap<QPalette::ColorGroup, QMap<QPalette::ColorRole, ColorButton *>> colorButtons;
|
QMap<QPalette::ColorGroup, QMap<QPalette::ColorRole, ColorButton *>> colorButtons;
|
||||||
|
QMap<AppColor::Role, ColorButton *> appColorButtons;
|
||||||
QScrollArea *scroll;
|
QScrollArea *scroll;
|
||||||
QWidget *gridHost;
|
QWidget *gridHost;
|
||||||
QVBoxLayout *layout;
|
QVBoxLayout *layout;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
#include "pixel_map_generator.h"
|
#include "pixel_map_generator.h"
|
||||||
|
|
||||||
|
#include "theme_manager.h"
|
||||||
|
|
||||||
#include <QApplication>
|
#include <QApplication>
|
||||||
#include <QDomDocument>
|
#include <QDomDocument>
|
||||||
#include <QFile>
|
#include <QFile>
|
||||||
|
|
@ -82,7 +84,13 @@ static QPixmap loadSvg(const QString &svgPath, const QSize &size, bool expandOnl
|
||||||
/**
|
/**
|
||||||
* Try to load path image from non-SVG formats, otherwise fall back to SVG.
|
* Try to load path image from non-SVG formats, otherwise fall back to SVG.
|
||||||
* This is to allow custom themes to support non-SVG format type overrides, since SVG requires custom loading.
|
* This is to allow custom themes to support non-SVG format type overrides, since SVG requires custom loading.
|
||||||
* @param path The path to the file, with no file extension. File formats will be automatically detected.
|
*
|
||||||
|
* The path may already carry the resolved file extension (e.g. via
|
||||||
|
* ThemeManager::assetPath); such paths are loaded directly. Otherwise a
|
||||||
|
* format-agnostic lookup probes png, jpg and finally svg.
|
||||||
|
*
|
||||||
|
* @param path The path to the file, with no file extension unless the caller
|
||||||
|
* already resolved it. File formats will be automatically detected.
|
||||||
* @param size The desired size of the pixmap.
|
* @param size The desired size of the pixmap.
|
||||||
* @param expandOnly If true, then keep the size of the initial pixmap to at least the size (Only relevant if SVG).
|
* @param expandOnly If true, then keep the size of the initial pixmap to at least the size (Only relevant if SVG).
|
||||||
*
|
*
|
||||||
|
|
@ -90,6 +98,19 @@ static QPixmap loadSvg(const QString &svgPath, const QSize &size, bool expandOnl
|
||||||
*/
|
*/
|
||||||
static QPixmap tryLoadImage(const QString &path, const QSize &size, bool expandOnly = false)
|
static QPixmap tryLoadImage(const QString &path, const QSize &size, bool expandOnly = false)
|
||||||
{
|
{
|
||||||
|
if (path.endsWith(QLatin1String(".svg"), Qt::CaseInsensitive)) {
|
||||||
|
return loadSvg(path, size, expandOnly);
|
||||||
|
}
|
||||||
|
if (path.endsWith(QLatin1String(".png"), Qt::CaseInsensitive) ||
|
||||||
|
path.endsWith(QLatin1String(".jpg"), Qt::CaseInsensitive) ||
|
||||||
|
path.endsWith(QLatin1String(".jpeg"), Qt::CaseInsensitive)) {
|
||||||
|
QPixmap pix(path);
|
||||||
|
if (!pix.isNull()) {
|
||||||
|
return pix.scaled(size, Qt::KeepAspectRatio, Qt::SmoothTransformation);
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
const auto formats = {"png", "jpg"};
|
const auto formats = {"png", "jpg"};
|
||||||
|
|
||||||
QPixmap returnPixmap;
|
QPixmap returnPixmap;
|
||||||
|
|
@ -111,7 +132,8 @@ QPixmap PhasePixmapGenerator::generatePixmap(int height, QString name)
|
||||||
return pmCache.value(key);
|
return pmCache.value(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
QPixmap pixmap = tryLoadImage("theme:phases/" + name, QSize(height, height));
|
QPixmap pixmap = tryLoadImage(QStringLiteral("theme:") + themeManager->assetPath(QStringLiteral("phases/") + name),
|
||||||
|
QSize(height, height));
|
||||||
|
|
||||||
pmCache.insert(key, pixmap);
|
pmCache.insert(key, pixmap);
|
||||||
return pixmap;
|
return pixmap;
|
||||||
|
|
@ -339,6 +361,10 @@ static QString getIconType(const bool isBuddy, const UserLevelFlags &userLevelFl
|
||||||
return "pawn_judge";
|
return "pawn_judge";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (userLevelFlags.testFlag(ServerInfo_User::IsDeveloper)) {
|
||||||
|
return "pawn_dev";
|
||||||
|
}
|
||||||
|
|
||||||
if (!privLevel.isEmpty() && privLevel.toLower() != "none") {
|
if (!privLevel.isEmpty() && privLevel.toLower() != "none") {
|
||||||
return QString("pawn_%1").arg(privLevel.toLower());
|
return QString("pawn_%1").arg(privLevel.toLower());
|
||||||
}
|
}
|
||||||
|
|
@ -396,7 +422,8 @@ QPixmap LockPixmapGenerator::generatePixmap(int height)
|
||||||
return pmCache.value(key);
|
return pmCache.value(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
QPixmap pixmap = tryLoadImage("theme:icons/lock", QSize(height, height), true);
|
QPixmap pixmap = tryLoadImage(QStringLiteral("theme:") + themeManager->assetPath(QStringLiteral("icons/lock")),
|
||||||
|
QSize(height, height), true);
|
||||||
pmCache.insert(key, pixmap);
|
pmCache.insert(key, pixmap);
|
||||||
return pixmap;
|
return pixmap;
|
||||||
}
|
}
|
||||||
|
|
@ -411,7 +438,8 @@ QPixmap DropdownIconPixmapGenerator::generatePixmap(int height, bool expanded)
|
||||||
}
|
}
|
||||||
|
|
||||||
QString name = expanded ? "dropdown_expanded" : "dropdown_collapsed";
|
QString name = expanded ? "dropdown_expanded" : "dropdown_collapsed";
|
||||||
QPixmap pixmap = tryLoadImage("theme:icons/" + name, QSize(height, height), true);
|
QPixmap pixmap = tryLoadImage(QStringLiteral("theme:") + themeManager->assetPath(QStringLiteral("icons/") + name),
|
||||||
|
QSize(height, height), true);
|
||||||
|
|
||||||
pmCache.insert(key, pixmap);
|
pmCache.insert(key, pixmap);
|
||||||
return pixmap;
|
return pixmap;
|
||||||
|
|
@ -472,6 +500,13 @@ QHash<QString, QPixmap> ManaSymbolPixmapGenerator::scaledCache;
|
||||||
|
|
||||||
QPixmap loadColorAdjustedPixmap(const QString &name)
|
QPixmap loadColorAdjustedPixmap(const QString &name)
|
||||||
{
|
{
|
||||||
|
// Prefer an authored scheme-qualified variant when one exists for this asset.
|
||||||
|
const QString variant = themeManager->schemeVariantPath(QStringView(name).mid(QStringLiteral("theme:").size()));
|
||||||
|
if (!variant.isEmpty()) {
|
||||||
|
return QPixmap(QStringLiteral("theme:") + variant);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Legacy fallback: runtime-invert for dark mode when no authored variant.
|
||||||
if (qApp->palette().windowText().color().lightness() > 200) {
|
if (qApp->palette().windowText().color().lightness() > 200) {
|
||||||
QImage img(name);
|
QImage img(name);
|
||||||
img.invertPixels();
|
img.invertPixels();
|
||||||
|
|
@ -482,3 +517,21 @@ QPixmap loadColorAdjustedPixmap(const QString &name)
|
||||||
return QPixmap(name);
|
return QPixmap(name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QPixmap themePixmap(QStringView prefix)
|
||||||
|
{
|
||||||
|
const QString resolved = themeManager->assetPath(prefix);
|
||||||
|
return QPixmap(QStringLiteral("theme:") + resolved);
|
||||||
|
}
|
||||||
|
|
||||||
|
void clearPixmapGeneratorCaches()
|
||||||
|
{
|
||||||
|
PhasePixmapGenerator::clear();
|
||||||
|
CounterPixmapGenerator::clear();
|
||||||
|
PingPixmapGenerator::clear();
|
||||||
|
CountryPixmapGenerator::clear();
|
||||||
|
UserLevelPixmapGenerator::clear();
|
||||||
|
LockPixmapGenerator::clear();
|
||||||
|
DropdownIconPixmapGenerator::clear();
|
||||||
|
ManaSymbolPixmapGenerator::clear();
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -156,4 +156,15 @@ public:
|
||||||
|
|
||||||
QPixmap loadColorAdjustedPixmap(const QString &name);
|
QPixmap loadColorAdjustedPixmap(const QString &name);
|
||||||
|
|
||||||
|
// Loads a "theme:" asset (with no file extension in prefix), preferring the
|
||||||
|
// scheme-qualified variant (prefix-dark / prefix-light, resolved via
|
||||||
|
// ThemeManager::assetPath) and falling back to the plain asset. Callers load
|
||||||
|
// the returned path directly. Use for scheme-sensitive pixmaps like
|
||||||
|
// backgrounds, the card back, and the app logo.
|
||||||
|
QPixmap themePixmap(QStringView prefix);
|
||||||
|
|
||||||
|
// Clears every PixmapGenerator's static cache so scheme variants are
|
||||||
|
// re-resolved when the active theme or color scheme changes.
|
||||||
|
void clearPixmapGeneratorCaches();
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ QString ThemeConfig::toIni() const
|
||||||
out += "[Appearance]\n";
|
out += "[Appearance]\n";
|
||||||
out += QString("ColorScheme = %1\n").arg(colorScheme.isEmpty() ? "System" : colorScheme);
|
out += QString("ColorScheme = %1\n").arg(colorScheme.isEmpty() ? "System" : colorScheme);
|
||||||
out += "\n[Style]\n";
|
out += "\n[Style]\n";
|
||||||
out += QString("Name = %1\n").arg(styleName.isEmpty() ? "Default" : styleName);
|
out += QString("Name = %1\n").arg(styleName.isEmpty() ? "System" : styleName);
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -96,7 +96,7 @@ bool ThemeConfig::save(const QString &themeDirPath) const
|
||||||
|
|
||||||
bool PaletteConfig::hasPalette() const
|
bool PaletteConfig::hasPalette() const
|
||||||
{
|
{
|
||||||
return !colors.isEmpty();
|
return !colors.isEmpty() || !appColors.isEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
QString PaletteConfig::toToml() const
|
QString PaletteConfig::toToml() const
|
||||||
|
|
@ -133,6 +133,24 @@ QString PaletteConfig::toToml() const
|
||||||
out += "\n";
|
out += "\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!appColors.isEmpty()) {
|
||||||
|
QMetaEnum appEnum = QMetaEnum::fromType<AppColor::Role>();
|
||||||
|
|
||||||
|
out += "[AppColors]\n";
|
||||||
|
|
||||||
|
for (auto it = appColors.cbegin(); it != appColors.cend(); ++it) {
|
||||||
|
const char *roleName = appEnum.valueToKey(it.key());
|
||||||
|
|
||||||
|
if (!roleName) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
out += QString("%1 = %2\n").arg(QString(roleName), -20).arg(it.value().name(QColor::HexArgb));
|
||||||
|
}
|
||||||
|
|
||||||
|
out += "\n";
|
||||||
|
}
|
||||||
|
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -152,6 +170,7 @@ PaletteConfig PaletteConfig::fromFile(const QString &filePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
QMetaEnum roleEnum = QMetaEnum::fromType<QPalette::ColorRole>();
|
QMetaEnum roleEnum = QMetaEnum::fromType<QPalette::ColorRole>();
|
||||||
|
QMetaEnum appEnum = QMetaEnum::fromType<AppColor::Role>();
|
||||||
|
|
||||||
QString currentSection;
|
QString currentSection;
|
||||||
QPalette::ColorGroup currentGroup = QPalette::Active;
|
QPalette::ColorGroup currentGroup = QPalette::Active;
|
||||||
|
|
@ -202,6 +221,26 @@ PaletteConfig PaletteConfig::fromFile(const QString &filePath)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QColor color(value);
|
||||||
|
|
||||||
|
if (!color.isValid()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentSection.compare("AppColors", Qt::CaseInsensitive) == 0) {
|
||||||
|
if (key.startsWith("AppColor::")) {
|
||||||
|
key = key.mid(10);
|
||||||
|
}
|
||||||
|
|
||||||
|
int appRoleInt = appEnum.keyToValue(key.toUtf8().constData());
|
||||||
|
|
||||||
|
if (appRoleInt >= 0) {
|
||||||
|
cfg.appColors[static_cast<AppColor::Role>(appRoleInt)] = color;
|
||||||
|
}
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (!currentSection.startsWith("Palette", Qt::CaseInsensitive)) {
|
if (!currentSection.startsWith("Palette", Qt::CaseInsensitive)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -216,11 +255,7 @@ PaletteConfig PaletteConfig::fromFile(const QString &filePath)
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
QColor color(value);
|
cfg.colors[currentGroup][static_cast<QPalette::ColorRole>(roleInt)] = color;
|
||||||
|
|
||||||
if (color.isValid()) {
|
|
||||||
cfg.colors[currentGroup][static_cast<QPalette::ColorRole>(roleInt)] = color;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return cfg;
|
return cfg;
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,25 @@
|
||||||
|
|
||||||
#include <QColor>
|
#include <QColor>
|
||||||
#include <QMap>
|
#include <QMap>
|
||||||
|
#include <QObject>
|
||||||
#include <QPalette>
|
#include <QPalette>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
|
|
||||||
|
// Application-specific color roles, layered on top of the fixed QPalette role
|
||||||
|
// set. Stored in the same palette-<scheme>.toml under an [AppColors] section
|
||||||
|
// and editable from the palette editor, so theme authors can control colors
|
||||||
|
// beyond what Qt's palette can express.
|
||||||
|
namespace AppColor
|
||||||
|
{
|
||||||
|
Q_NAMESPACE
|
||||||
|
enum Role
|
||||||
|
{
|
||||||
|
AccentStrong,
|
||||||
|
AccentSoft,
|
||||||
|
};
|
||||||
|
Q_ENUM_NS(Role)
|
||||||
|
} // namespace AppColor
|
||||||
|
|
||||||
struct ThemeConfig
|
struct ThemeConfig
|
||||||
{
|
{
|
||||||
QString colorScheme;
|
QString colorScheme;
|
||||||
|
|
@ -21,7 +37,16 @@ struct ThemeConfig
|
||||||
struct PaletteConfig
|
struct PaletteConfig
|
||||||
{
|
{
|
||||||
QMap<QPalette::ColorGroup, QMap<QPalette::ColorRole, QColor>> colors;
|
QMap<QPalette::ColorGroup, QMap<QPalette::ColorRole, QColor>> colors;
|
||||||
|
QMap<AppColor::Role, QColor> appColors;
|
||||||
|
|
||||||
|
bool operator==(const PaletteConfig &rhs) const
|
||||||
|
{
|
||||||
|
return colors == rhs.colors && appColors == rhs.appColors;
|
||||||
|
}
|
||||||
|
bool operator!=(const PaletteConfig &rhs) const
|
||||||
|
{
|
||||||
|
return !(*this == rhs);
|
||||||
|
}
|
||||||
bool hasPalette() const;
|
bool hasPalette() const;
|
||||||
QString toToml() const;
|
QString toToml() const;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,13 @@
|
||||||
#include "theme_manager.h"
|
#include "theme_manager.h"
|
||||||
|
|
||||||
#include "../../client/settings/cache_settings.h"
|
#include "../../client/settings/cache_settings.h"
|
||||||
|
#include "pixel_map_generator.h"
|
||||||
|
|
||||||
#include <QApplication>
|
#include <QApplication>
|
||||||
#include <QColor>
|
#include <QColor>
|
||||||
#include <QDebug>
|
#include <QDebug>
|
||||||
|
#include <QFile>
|
||||||
|
#include <QFileInfo>
|
||||||
#include <QLibraryInfo>
|
#include <QLibraryInfo>
|
||||||
#include <QMap>
|
#include <QMap>
|
||||||
#include <QMetaEnum>
|
#include <QMetaEnum>
|
||||||
|
|
@ -19,7 +22,7 @@
|
||||||
#include <Qt>
|
#include <Qt>
|
||||||
#include <libcockatrice/settings/paths_settings.h>
|
#include <libcockatrice/settings/paths_settings.h>
|
||||||
|
|
||||||
#define NONE_THEME_NAME "Default"
|
#define SYSTEM_THEME_NAME "System"
|
||||||
#define FUSION_THEME_NAME "Fusion"
|
#define FUSION_THEME_NAME "Fusion"
|
||||||
#define STYLE_CSS_NAME "style.css"
|
#define STYLE_CSS_NAME "style.css"
|
||||||
#define HANDZONE_BG_NAME "handzone"
|
#define HANDZONE_BG_NAME "handzone"
|
||||||
|
|
@ -93,7 +96,7 @@ struct PaletteColorInfo
|
||||||
static QString usableDefaultStyle(const QString &style)
|
static QString usableDefaultStyle(const QString &style)
|
||||||
{
|
{
|
||||||
// The Windows 11 native style is broken: when the OS default
|
// The Windows 11 native style is broken: when the OS default
|
||||||
// ("Default" theme selection) would use it, fall back to the Vista style.
|
// ("System" theme selection) would use it, fall back to the Vista style.
|
||||||
// Explicitly choosing "windows11" in a theme is still honored.
|
// Explicitly choosing "windows11" in a theme is still honored.
|
||||||
return style.compare("windows11", Qt::CaseInsensitive) == 0 ? QStringLiteral("windowsvista") : style;
|
return style.compare("windows11", Qt::CaseInsensitive) == 0 ? QStringLiteral("windowsvista") : style;
|
||||||
}
|
}
|
||||||
|
|
@ -116,10 +119,16 @@ ThemeManager::ThemeManager(QObject *parent) : QObject(parent)
|
||||||
|
|
||||||
void ThemeManager::ensureThemeDirectoryExists()
|
void ThemeManager::ensureThemeDirectoryExists()
|
||||||
{
|
{
|
||||||
if (SettingsCache::instance().getThemeName().isEmpty() ||
|
auto &settings = SettingsCache::instance();
|
||||||
!getAvailableThemes().contains(SettingsCache::instance().getThemeName())) {
|
|
||||||
|
// Migrate the old "Default" theme name to "System"
|
||||||
|
if (settings.getThemeName() == "Default") {
|
||||||
|
settings.setThemeName(SYSTEM_THEME_NAME);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (settings.getThemeName().isEmpty() || !getAvailableThemes().contains(settings.getThemeName())) {
|
||||||
qCInfo(ThemeManagerLog) << "Theme name not set, setting default value";
|
qCInfo(ThemeManagerLog) << "Theme name not set, setting default value";
|
||||||
SettingsCache::instance().setThemeName(NONE_THEME_NAME);
|
settings.setThemeName(FUSION_THEME_NAME);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -140,11 +149,74 @@ bool ThemeManager::isDarkMode(const QString &themeDirPath) const
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ThemeManager::isBuiltInTheme()
|
QString ThemeManager::schemeVariantPath(QStringView prefix) const
|
||||||
{
|
{
|
||||||
const auto themeName = SettingsCache::instance().getThemeName();
|
static const QStringList formats = {QStringLiteral(".png"), QStringLiteral(".jpg"), QStringLiteral(".jpeg"),
|
||||||
|
QStringLiteral(".svg")};
|
||||||
|
const QString scheme = isDarkMode(currentThemePath) ? QStringLiteral("dark") : QStringLiteral("light");
|
||||||
|
const QString variantStem = prefix.toString() + QLatin1Char('-') + scheme;
|
||||||
|
|
||||||
return themeName == NONE_THEME_NAME || themeName == FUSION_THEME_NAME;
|
for (const QString &format : formats) {
|
||||||
|
if (QFileInfo::exists(QStringLiteral("theme:") + variantStem + format)) {
|
||||||
|
return variantStem + format;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return QString();
|
||||||
|
}
|
||||||
|
|
||||||
|
QString ThemeManager::assetPath(QStringView prefix) const
|
||||||
|
{
|
||||||
|
// Probe order mirrors tryLoadImage: a theme may override the default SVG
|
||||||
|
// with a raster of the same stem, so raster wins over SVG within a stem.
|
||||||
|
static const QStringList formats = {QStringLiteral(".png"), QStringLiteral(".jpg"), QStringLiteral(".jpeg"),
|
||||||
|
QStringLiteral(".svg")};
|
||||||
|
|
||||||
|
auto findExisting = [](const QString &stem) {
|
||||||
|
for (const QString &format : formats) {
|
||||||
|
if (QFileInfo::exists(QStringLiteral("theme:") + stem + format)) {
|
||||||
|
return stem + format;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return QString();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Prefer the scheme-qualified variant when it exists, else the plain
|
||||||
|
// asset as the super fallback. Both return the resolved path including
|
||||||
|
// its file extension so callers can load it directly.
|
||||||
|
const QString variant = schemeVariantPath(prefix);
|
||||||
|
if (!variant.isEmpty()) {
|
||||||
|
return variant;
|
||||||
|
}
|
||||||
|
const QString resolvedPlain = findExisting(prefix.toString());
|
||||||
|
return resolvedPlain.isEmpty() ? prefix.toString() : resolvedPlain;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Probe whether a directory is truly writable by trying to create and remove a
|
||||||
|
// temporary file. QFileInfo::isWritable() on a directory is unreliable (notably
|
||||||
|
// on Windows where UAC VirtualStore can make a system dir appear writable).
|
||||||
|
bool ThemeManager::isDirReallyWritable(const QString &dirPath)
|
||||||
|
{
|
||||||
|
const QString probe = QDir(dirPath).absoluteFilePath(".cockatrice_write_test");
|
||||||
|
QFile f(probe);
|
||||||
|
if (!f.open(QIODevice::WriteOnly)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
f.close();
|
||||||
|
f.remove();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString ThemeManager::writableThemeDir(const QString &themeName)
|
||||||
|
{
|
||||||
|
// All theme writes go to the user themes directory regardless of whether
|
||||||
|
// the resolved (system) theme directory happens to be writable. Even when a
|
||||||
|
// write would succeed in-place, routing it to the user directory keeps the
|
||||||
|
// install intact and guarantees changes survive upgrades.
|
||||||
|
const QString dirPath = QDir(SettingsCache::instance().paths().getThemesPath()).absoluteFilePath(themeName);
|
||||||
|
if (!QDir().mkpath(dirPath)) {
|
||||||
|
qWarning() << "Failed to create theme save directory:" << dirPath;
|
||||||
|
}
|
||||||
|
return dirPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
// System (read-only) themes location, relative to the application binary.
|
// System (read-only) themes location, relative to the application binary.
|
||||||
|
|
@ -169,9 +241,7 @@ QStringMap &ThemeManager::getAvailableThemes()
|
||||||
// load themes from user profile dir
|
// load themes from user profile dir
|
||||||
dir.setPath(SettingsCache::instance().paths().getThemesPath());
|
dir.setPath(SettingsCache::instance().paths().getThemesPath());
|
||||||
|
|
||||||
// add default value
|
availableThemes.insert(SYSTEM_THEME_NAME, dir.absoluteFilePath("System"));
|
||||||
availableThemes.insert(NONE_THEME_NAME, dir.absoluteFilePath("Default"));
|
|
||||||
|
|
||||||
availableThemes.insert(FUSION_THEME_NAME, dir.absoluteFilePath("Fusion"));
|
availableThemes.insert(FUSION_THEME_NAME, dir.absoluteFilePath("Fusion"));
|
||||||
|
|
||||||
for (QString themeName : dir.entryList(QDir::AllDirs | QDir::NoDotAndDotDot, QDir::Name)) {
|
for (QString themeName : dir.entryList(QDir::AllDirs | QDir::NoDotAndDotDot, QDir::Name)) {
|
||||||
|
|
@ -180,7 +250,7 @@ QStringMap &ThemeManager::getAvailableThemes()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// load themes from cockatrice system dir
|
// Load themes from Cockatrice system dir
|
||||||
dir.setPath(systemThemesBasePath());
|
dir.setPath(systemThemesBasePath());
|
||||||
|
|
||||||
for (QString themeName : dir.entryList(QDir::AllDirs | QDir::NoDotAndDotDot, QDir::Name)) {
|
for (QString themeName : dir.entryList(QDir::AllDirs | QDir::NoDotAndDotDot, QDir::Name)) {
|
||||||
|
|
@ -195,7 +265,7 @@ QStringMap &ThemeManager::getAvailableThemes()
|
||||||
QBrush ThemeManager::loadBrush(QString fileName, QColor fallbackColor)
|
QBrush ThemeManager::loadBrush(QString fileName, QColor fallbackColor)
|
||||||
{
|
{
|
||||||
QBrush brush;
|
QBrush brush;
|
||||||
QPixmap tmp = QPixmap("theme:zones/" + fileName);
|
QPixmap tmp = QPixmap("theme:" + assetPath(QStringLiteral("zones/") + fileName));
|
||||||
if (tmp.isNull()) {
|
if (tmp.isNull()) {
|
||||||
brush.setColor(fallbackColor);
|
brush.setColor(fallbackColor);
|
||||||
brush.setStyle(Qt::SolidPattern);
|
brush.setStyle(Qt::SolidPattern);
|
||||||
|
|
@ -209,7 +279,7 @@ QBrush ThemeManager::loadBrush(QString fileName, QColor fallbackColor)
|
||||||
QBrush ThemeManager::loadExtraBrush(QString fileName, QBrush &fallbackBrush)
|
QBrush ThemeManager::loadExtraBrush(QString fileName, QBrush &fallbackBrush)
|
||||||
{
|
{
|
||||||
QBrush brush;
|
QBrush brush;
|
||||||
QPixmap tmp = QPixmap("theme:zones/" + fileName);
|
QPixmap tmp = QPixmap("theme:" + assetPath(QStringLiteral("zones/") + fileName));
|
||||||
|
|
||||||
if (tmp.isNull()) {
|
if (tmp.isNull()) {
|
||||||
brush = fallbackBrush;
|
brush = fallbackBrush;
|
||||||
|
|
@ -287,7 +357,7 @@ bool ThemeManager::commitPalette(const QString &themeDirPath, const QString &col
|
||||||
|
|
||||||
void ThemeManager::setColorScheme(const QString &scheme)
|
void ThemeManager::setColorScheme(const QString &scheme)
|
||||||
{
|
{
|
||||||
const QString dirPath = getAvailableThemes().value(SettingsCache::instance().getThemeName());
|
const QString dirPath = writableThemeDir(SettingsCache::instance().getThemeName());
|
||||||
ThemeConfig cfg = ThemeConfig::fromThemeDir(dirPath);
|
ThemeConfig cfg = ThemeConfig::fromThemeDir(dirPath);
|
||||||
|
|
||||||
cfg.colorScheme = scheme;
|
cfg.colorScheme = scheme;
|
||||||
|
|
@ -298,7 +368,7 @@ void ThemeManager::setColorScheme(const QString &scheme)
|
||||||
|
|
||||||
void ThemeManager::setStyleName(const QString &styleName)
|
void ThemeManager::setStyleName(const QString &styleName)
|
||||||
{
|
{
|
||||||
const QString dirPath = getAvailableThemes().value(SettingsCache::instance().getThemeName());
|
const QString dirPath = writableThemeDir(SettingsCache::instance().getThemeName());
|
||||||
ThemeConfig cfg = ThemeConfig::fromThemeDir(dirPath);
|
ThemeConfig cfg = ThemeConfig::fromThemeDir(dirPath);
|
||||||
|
|
||||||
cfg.styleName = styleName;
|
cfg.styleName = styleName;
|
||||||
|
|
@ -329,7 +399,7 @@ void ThemeManager::applyStyleAndPalette(const QString &themeName,
|
||||||
Q_UNUSED(activeScheme)
|
Q_UNUSED(activeScheme)
|
||||||
#endif
|
#endif
|
||||||
QString styleName = themeCfg.styleName;
|
QString styleName = themeCfg.styleName;
|
||||||
if (styleName.isEmpty() || styleName.compare("Default", Qt::CaseInsensitive) == 0) {
|
if (styleName.isEmpty() || styleName.compare("System", Qt::CaseInsensitive) == 0) {
|
||||||
if (themeName == FUSION_THEME_NAME) {
|
if (themeName == FUSION_THEME_NAME) {
|
||||||
styleName = "Fusion";
|
styleName = "Fusion";
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -372,6 +442,8 @@ void ThemeManager::applyStyleAndPalette(const QString &themeName,
|
||||||
qApp->setPalette(base);
|
qApp->setPalette(base);
|
||||||
qApp->setStyle(style);
|
qApp->setStyle(style);
|
||||||
|
|
||||||
|
currentAppColors = palCfg.appColors;
|
||||||
|
|
||||||
// Force every widget to re-polish and repaint immediately rather than
|
// Force every widget to re-polish and repaint immediately rather than
|
||||||
// waiting for natural expose events, which produces a patchwork of old
|
// waiting for natural expose events, which produces a patchwork of old
|
||||||
// and new colours during a live preview.
|
// and new colours during a live preview.
|
||||||
|
|
@ -384,6 +456,35 @@ void ThemeManager::applyStyleAndPalette(const QString &themeName,
|
||||||
style->polish(widget);
|
style->polish(widget);
|
||||||
widget->update();
|
widget->update();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
emit paletteChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
QColor ThemeManager::appColor(AppColor::Role role) const
|
||||||
|
{
|
||||||
|
const auto it = currentAppColors.constFind(role);
|
||||||
|
if (it != currentAppColors.constEnd()) {
|
||||||
|
return it.value();
|
||||||
|
}
|
||||||
|
|
||||||
|
// QPalette::Accent was introduced in Qt 6.6 and several shipped palettes
|
||||||
|
// set it to a value barely distinguishable from Window, so it is not a
|
||||||
|
// reliable accent source. The selection highlight is the stable accent
|
||||||
|
// (Accent defaults to Highlight when unset), and deriving from it
|
||||||
|
// unconditionally keeps every Qt version rendering identically.
|
||||||
|
const QColor accent = qApp->palette().color(QPalette::Active, QPalette::Highlight);
|
||||||
|
|
||||||
|
if (role == AppColor::AccentSoft) {
|
||||||
|
constexpr int SOFT_SATURATION_PERCENT = 70;
|
||||||
|
constexpr int SOFT_LIGHTNESS_OFFSET = 60;
|
||||||
|
|
||||||
|
// Light end of the gradient: same hue, softened and lightened
|
||||||
|
return QColor::fromHsl(qMax(0, accent.hslHue()),
|
||||||
|
qBound(0, qRound(accent.hslSaturation() * SOFT_SATURATION_PERCENT / 100.0), 255),
|
||||||
|
qBound(0, accent.lightness() + SOFT_LIGHTNESS_OFFSET, 255));
|
||||||
|
}
|
||||||
|
|
||||||
|
return accent;
|
||||||
}
|
}
|
||||||
|
|
||||||
void ThemeManager::themeChangedSlot()
|
void ThemeManager::themeChangedSlot()
|
||||||
|
|
@ -393,9 +494,19 @@ void ThemeManager::themeChangedSlot()
|
||||||
currentThemePath = dirPath;
|
currentThemePath = dirPath;
|
||||||
QDir dir(dirPath);
|
QDir dir(dirPath);
|
||||||
|
|
||||||
// CSS
|
// CSS — prefer the scheme-qualified stylesheet (style-dark.css /
|
||||||
if (!dirPath.isEmpty() && dir.exists(STYLE_CSS_NAME)) {
|
// style-light.css) when present, else the plain style.css as fallback.
|
||||||
qApp->setStyleSheet("file:///" + dir.absoluteFilePath(STYLE_CSS_NAME));
|
if (!dirPath.isEmpty()) {
|
||||||
|
const QString scheme = isDarkMode(dirPath) ? QStringLiteral("dark") : QStringLiteral("light");
|
||||||
|
const QString schemeCss = QFileInfo(QStringLiteral(STYLE_CSS_NAME)).completeBaseName() + QLatin1Char('-') +
|
||||||
|
scheme + QStringLiteral(".css");
|
||||||
|
if (dir.exists(schemeCss)) {
|
||||||
|
qApp->setStyleSheet("file:///" + dir.absoluteFilePath(schemeCss));
|
||||||
|
} else if (dir.exists(STYLE_CSS_NAME)) {
|
||||||
|
qApp->setStyleSheet("file:///" + dir.absoluteFilePath(STYLE_CSS_NAME));
|
||||||
|
} else {
|
||||||
|
qApp->setStyleSheet("");
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
qApp->setStyleSheet("");
|
qApp->setStyleSheet("");
|
||||||
}
|
}
|
||||||
|
|
@ -410,8 +521,19 @@ void ThemeManager::themeChangedSlot()
|
||||||
|
|
||||||
// ── Load palette: custom first, then theme default ────────────────────
|
// ── Load palette: custom first, then theme default ────────────────────
|
||||||
PaletteConfig palette = PaletteConfig::fromScheme(dirPath, activeScheme);
|
PaletteConfig palette = PaletteConfig::fromScheme(dirPath, activeScheme);
|
||||||
if (!palette.hasPalette()) {
|
const PaletteConfig themeDefault = ThemeManager::loadDefaultPaletteConfig(dirPath, themeName, activeScheme);
|
||||||
palette = ThemeManager::loadDefaultPaletteConfig(dirPath, themeName, activeScheme);
|
if (palette.hasPalette()) {
|
||||||
|
// A custom palette written before [AppColors] existed carries no app
|
||||||
|
// colors; merge the theme's shipped defaults so the identity colors
|
||||||
|
// survive (hasPalette() counts an app-colors-only file as a palette,
|
||||||
|
// so those are kept wholesale and never reach here empty).
|
||||||
|
for (auto it = themeDefault.appColors.cbegin(); it != themeDefault.appColors.cend(); ++it) {
|
||||||
|
if (!palette.appColors.contains(it.key())) {
|
||||||
|
palette.appColors.insert(it.key(), it.value());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
palette = themeDefault;
|
||||||
}
|
}
|
||||||
|
|
||||||
applyStyleAndPalette(themeName, themeCfg, palette, activeScheme);
|
applyStyleAndPalette(themeName, themeCfg, palette, activeScheme);
|
||||||
|
|
@ -446,6 +568,7 @@ void ThemeManager::themeChangedSlot()
|
||||||
}
|
}
|
||||||
|
|
||||||
QPixmapCache::clear();
|
QPixmapCache::clear();
|
||||||
|
clearPixmapGeneratorCaches();
|
||||||
|
|
||||||
emit themeChanged();
|
emit themeChanged();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,7 @@ private:
|
||||||
QString currentThemePath;
|
QString currentThemePath;
|
||||||
std::array<QBrush, Role::MaxRole + 1> brushes;
|
std::array<QBrush, Role::MaxRole + 1> brushes;
|
||||||
QStringMap availableThemes;
|
QStringMap availableThemes;
|
||||||
|
QMap<AppColor::Role, QColor> currentAppColors;
|
||||||
/*
|
/*
|
||||||
Internal cache for multiple backgrounds
|
Internal cache for multiple backgrounds
|
||||||
*/
|
*/
|
||||||
|
|
@ -65,7 +66,16 @@ protected:
|
||||||
const QString &activeScheme);
|
const QString &activeScheme);
|
||||||
|
|
||||||
public:
|
public:
|
||||||
bool isBuiltInTheme();
|
// Resolves the directory to write theme changes to for the given theme
|
||||||
|
// name. The resolved theme dir (user or system) is used when writable;
|
||||||
|
// read-only system themes fall back to the user themes directory, creating
|
||||||
|
// it if needed, so customisations never get lost on upgrade.
|
||||||
|
static QString writableThemeDir(const QString &themeName);
|
||||||
|
// Probe whether a directory is truly writable by trying to create and remove
|
||||||
|
// a temporary file. QFileInfo::isWritable() on a directory is unreliable
|
||||||
|
// (notably on Windows where UAC VirtualStore can make a system dir appear
|
||||||
|
// writable).
|
||||||
|
static bool isDirReallyWritable(const QString &dirPath);
|
||||||
// Explicit color scheme of the theme: theme.cfg's ColorScheme setting
|
// Explicit color scheme of the theme: theme.cfg's ColorScheme setting
|
||||||
// (Dark/Light), falling back to the OS color scheme when it is "System".
|
// (Dark/Light), falling back to the OS color scheme when it is "System".
|
||||||
bool isDarkMode(const QString &themeDirPath) const;
|
bool isDarkMode(const QString &themeDirPath) const;
|
||||||
|
|
@ -87,6 +97,20 @@ public:
|
||||||
// Load/save per-scheme palette colors
|
// Load/save per-scheme palette colors
|
||||||
static PaletteConfig loadPaletteConfig(const QString &themeDirPath, const QString &colorScheme);
|
static PaletteConfig loadPaletteConfig(const QString &themeDirPath, const QString &colorScheme);
|
||||||
static bool savePaletteConfig(const QString &themeDirPath, const QString &colorScheme, const PaletteConfig &cfg);
|
static bool savePaletteConfig(const QString &themeDirPath, const QString &colorScheme, const PaletteConfig &cfg);
|
||||||
|
// Resolve prefix to a scheme-qualified "theme:" path. Existence is probed
|
||||||
|
// internally across the formats themes may ship (.png/.jpg/.svg), so
|
||||||
|
// callers load the returned path directly. Prefers "<prefix>-<dark|light>"
|
||||||
|
// when a file exists at that stem, otherwise the plain "<prefix>" as the
|
||||||
|
// super fallback. The resolved scheme covers explicit light/dark as well
|
||||||
|
// as OS-resolved "system". Returns the path with its file extension when a
|
||||||
|
// match is found; unqualified assets keep working unchanged.
|
||||||
|
QString assetPath(QStringView prefix) const;
|
||||||
|
// Like assetPath, but resolves only the scheme-qualified variant
|
||||||
|
// ("<prefix>-<dark|light>.<ext>") and returns an empty string when no
|
||||||
|
// variant exists — it never falls back to the plain "<prefix>" asset.
|
||||||
|
// Callers that must distinguish "no authored variant" (e.g. to keep a
|
||||||
|
// legacy runtime fallback alive) should use this instead of assetPath.
|
||||||
|
QString schemeVariantPath(QStringView prefix) const;
|
||||||
// Load the theme's shipped default palette, falling back to the system
|
// Load the theme's shipped default palette, falling back to the system
|
||||||
// theme directory when it is absent from the resolved (user) directory.
|
// theme directory when it is absent from the resolved (user) directory.
|
||||||
static PaletteConfig
|
static PaletteConfig
|
||||||
|
|
@ -101,12 +125,17 @@ public:
|
||||||
void reloadCurrentTheme();
|
void reloadCurrentTheme();
|
||||||
void previewPalette(const PaletteConfig &cfg, const QString &scheme);
|
void previewPalette(const PaletteConfig &cfg, const QString &scheme);
|
||||||
|
|
||||||
|
// Resolves an application color role: the theme's stored [AppColors] value
|
||||||
|
// when present, otherwise a palette-accent-derived fallback.
|
||||||
|
QColor appColor(AppColor::Role role) const;
|
||||||
|
|
||||||
QBrush &getBgBrush(Role zone);
|
QBrush &getBgBrush(Role zone);
|
||||||
QBrush getExtraBgBrush(Role zone, int zoneId = 0);
|
QBrush getExtraBgBrush(Role zone, int zoneId = 0);
|
||||||
protected slots:
|
protected slots:
|
||||||
void themeChangedSlot();
|
void themeChangedSlot();
|
||||||
signals:
|
signals:
|
||||||
void themeChanged();
|
void themeChanged();
|
||||||
|
void paletteChanged();
|
||||||
};
|
};
|
||||||
|
|
||||||
extern ThemeManager *themeManager;
|
extern ThemeManager *themeManager;
|
||||||
|
|
|
||||||
18
cockatrice/src/interface/widgets/cards/card_art_utils.cpp
Normal file
18
cockatrice/src/interface/widgets/cards/card_art_utils.cpp
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
#include "card_art_utils.h"
|
||||||
|
|
||||||
|
#include <QTransform>
|
||||||
|
#include <libcockatrice/card/printing/exact_card.h>
|
||||||
|
|
||||||
|
namespace CardArtUtils
|
||||||
|
{
|
||||||
|
QPixmap rotateSidewaysLayoutArt(const QPixmap &art, const ExactCard &card)
|
||||||
|
{
|
||||||
|
if (!card.getInfo().getUiAttributes().landscapeOrientation) {
|
||||||
|
return art;
|
||||||
|
}
|
||||||
|
|
||||||
|
QTransform transform;
|
||||||
|
transform.rotate(90);
|
||||||
|
return art.transformed(transform, Qt::SmoothTransformation);
|
||||||
|
}
|
||||||
|
} // namespace CardArtUtils
|
||||||
25
cockatrice/src/interface/widgets/cards/card_art_utils.h
Normal file
25
cockatrice/src/interface/widgets/cards/card_art_utils.h
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
#ifndef CARD_ART_UTILS_H
|
||||||
|
#define CARD_ART_UTILS_H
|
||||||
|
|
||||||
|
#include <QPixmap>
|
||||||
|
|
||||||
|
class ExactCard;
|
||||||
|
|
||||||
|
namespace CardArtUtils
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @brief Rotates a card's art upright when its layout shows sideways.
|
||||||
|
*
|
||||||
|
* Sideways-layout cards (planes, sieges/battles, split cards) store their
|
||||||
|
* landscape artwork rotated 90° inside a portrait frame. Art-crop displays,
|
||||||
|
* playmat art, and the card-info picture must show such art upright before
|
||||||
|
* sampling or painting. Portrait cards are returned unchanged.
|
||||||
|
*
|
||||||
|
* @param art The card pixmap to orient.
|
||||||
|
* @param card The card describing the art orientation.
|
||||||
|
* @return @p art rotated 90° clockwise for sideways-layout cards, else @p art.
|
||||||
|
*/
|
||||||
|
QPixmap rotateSidewaysLayoutArt(const QPixmap &art, const ExactCard &card);
|
||||||
|
} // namespace CardArtUtils
|
||||||
|
|
||||||
|
#endif // CARD_ART_UTILS_H
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
#include "../../../interface/card_picture_loader/card_picture_loader.h"
|
#include "../../../interface/card_picture_loader/card_picture_loader.h"
|
||||||
#include "../../../interface/widgets/tabs/tab_supervisor.h"
|
#include "../../../interface/widgets/tabs/tab_supervisor.h"
|
||||||
#include "../../window_main.h"
|
#include "../../window_main.h"
|
||||||
|
#include "card_art_utils.h"
|
||||||
|
|
||||||
#include <QMenu>
|
#include <QMenu>
|
||||||
#include <QMouseEvent>
|
#include <QMouseEvent>
|
||||||
|
|
@ -73,6 +74,8 @@ CardInfoPictureWidget::CardInfoPictureWidget(QWidget *parent, const bool _hoverT
|
||||||
|
|
||||||
update();
|
update();
|
||||||
});
|
});
|
||||||
|
connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::cardLangChanged, this,
|
||||||
|
&CardInfoPictureWidget::updatePixmap);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -193,12 +196,7 @@ void CardInfoPictureWidget::paintEvent(QPaintEvent *event)
|
||||||
|
|
||||||
QPixmap transformedPixmap = resizedPixmap; // Default pixmap
|
QPixmap transformedPixmap = resizedPixmap; // Default pixmap
|
||||||
if (SettingsCache::instance().cardsDisplay().getAutoRotateSidewaysLayoutCards()) {
|
if (SettingsCache::instance().cardsDisplay().getAutoRotateSidewaysLayoutCards()) {
|
||||||
if (exactCard.getInfo().getUiAttributes().landscapeOrientation) {
|
transformedPixmap = CardArtUtils::rotateSidewaysLayoutArt(resizedPixmap, exactCard);
|
||||||
// Rotate pixmap 90 degrees to the left
|
|
||||||
QTransform transform;
|
|
||||||
transform.rotate(90);
|
|
||||||
transformedPixmap = resizedPixmap.transformed(transform, Qt::SmoothTransformation);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle DPI scaling
|
// Handle DPI scaling
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
#include "card_info_text_widget.h"
|
#include "card_info_text_widget.h"
|
||||||
|
|
||||||
#include "../../../game_graphics/board/card_item.h"
|
#include "../../../game_graphics/board/card_item.h"
|
||||||
|
#include "../../card_localization.h"
|
||||||
|
|
||||||
#include <QGridLayout>
|
#include <QGridLayout>
|
||||||
#include <QLabel>
|
#include <QLabel>
|
||||||
|
|
@ -10,7 +11,7 @@
|
||||||
#include <libcockatrice/card/game_specific_terms.h>
|
#include <libcockatrice/card/game_specific_terms.h>
|
||||||
#include <libcockatrice/card/relation/card_relation.h>
|
#include <libcockatrice/card/relation/card_relation.h>
|
||||||
|
|
||||||
CardInfoTextWidget::CardInfoTextWidget(QWidget *parent) : QFrame(parent), info(nullptr)
|
CardInfoTextWidget::CardInfoTextWidget(QWidget *parent) : QFrame(parent)
|
||||||
{
|
{
|
||||||
propsLabel = new QLabel;
|
propsLabel = new QLabel;
|
||||||
propsLabel->setOpenExternalLinks(false);
|
propsLabel->setOpenExternalLinks(false);
|
||||||
|
|
@ -39,6 +40,12 @@ CardInfoTextWidget::CardInfoTextWidget(QWidget *parent) : QFrame(parent), info(n
|
||||||
grid->setRowStretch(1, 1);
|
grid->setRowStretch(1, 1);
|
||||||
|
|
||||||
retranslateUi();
|
retranslateUi();
|
||||||
|
|
||||||
|
connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::cardLangChanged, this, [this] {
|
||||||
|
if (currentCard) {
|
||||||
|
setCard(currentCard);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void CardInfoTextWidget::setTexts(const QString &propsText, const QString &textText)
|
void CardInfoTextWidget::setTexts(const QString &propsText, const QString &textText)
|
||||||
|
|
@ -60,7 +67,7 @@ void CardInfoTextWidget::setCard(const ExactCard &exactCard)
|
||||||
|
|
||||||
QString text = "<table width=\"100%\" border=0 cellspacing=0 cellpadding=0>";
|
QString text = "<table width=\"100%\" border=0 cellspacing=0 cellpadding=0>";
|
||||||
text += QString("<tr><td>%1</td><td width=\"5\"></td><td>%2</td></tr>")
|
text += QString("<tr><td>%1</td><td width=\"5\"></td><td>%2</td></tr>")
|
||||||
.arg(tr("Name:"), card->getName().toHtmlEscaped());
|
.arg(tr("Name:"), CardLocalization::displayName(card).toHtmlEscaped());
|
||||||
|
|
||||||
if (!exactCard.getPrinting().isEmpty()) {
|
if (!exactCard.getPrinting().isEmpty()) {
|
||||||
QString setShort = exactCard.getPrinting().getSet()->getShortName().toHtmlEscaped();
|
QString setShort = exactCard.getPrinting().getSet()->getShortName().toHtmlEscaped();
|
||||||
|
|
@ -94,7 +101,8 @@ void CardInfoTextWidget::setCard(const ExactCard &exactCard)
|
||||||
}
|
}
|
||||||
|
|
||||||
text += "</table>";
|
text += "</table>";
|
||||||
setTexts(text, card->getText());
|
setTexts(text, CardLocalization::displayText(card));
|
||||||
|
currentCard = exactCard;
|
||||||
}
|
}
|
||||||
|
|
||||||
void CardInfoTextWidget::setInvalidCardName(const QString &cardName)
|
void CardInfoTextWidget::setInvalidCardName(const QString &cardName)
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ private:
|
||||||
QLabel *propsLabel;
|
QLabel *propsLabel;
|
||||||
QScrollArea *propsScroll;
|
QScrollArea *propsScroll;
|
||||||
QTextEdit *textLabel;
|
QTextEdit *textLabel;
|
||||||
CardInfoPtr info;
|
ExactCard currentCard; ///< Last card set, re-rendered when the card language changes.
|
||||||
void setTexts(const QString &propsText, const QString &textText);
|
void setTexts(const QString &propsText, const QString &textText);
|
||||||
|
|
||||||
public:
|
public:
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
#include "abstract_analytics_panel_widget.h"
|
#include "abstract_analytics_panel_widget.h"
|
||||||
|
|
||||||
|
#include "../../pixel_map_generator.h"
|
||||||
#include "deck_list_statistics_analyzer.h"
|
#include "deck_list_statistics_analyzer.h"
|
||||||
|
|
||||||
#include <QPushButton>
|
#include <QPushButton>
|
||||||
|
|
@ -20,7 +21,7 @@ AbstractAnalyticsPanelWidget::AbstractAnalyticsPanelWidget(QWidget *parent, Deck
|
||||||
|
|
||||||
// config button
|
// config button
|
||||||
configureButton = new QPushButton(this);
|
configureButton = new QPushButton(this);
|
||||||
configureButton->setIcon(QPixmap("theme:icons/cogwheel"));
|
configureButton->setIcon(themePixmap(QStringLiteral("icons/cogwheel")));
|
||||||
configureButton->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum);
|
configureButton->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum);
|
||||||
connect(configureButton, &QPushButton::clicked, this, &AbstractAnalyticsPanelWidget::applyConfigFromDialog);
|
connect(configureButton, &QPushButton::clicked, this, &AbstractAnalyticsPanelWidget::applyConfigFromDialog);
|
||||||
bannerAndSettingsLayout->addWidget(configureButton, 0);
|
bannerAndSettingsLayout->addWidget(configureButton, 0);
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ DeckEditorDatabaseDisplayWidget::DeckEditorDatabaseDisplayWidget(QWidget *parent
|
||||||
searchEdit->setPlaceholderText(tr("Search by card name (or search expressions)"));
|
searchEdit->setPlaceholderText(tr("Search by card name (or search expressions)"));
|
||||||
searchEdit->setClearButtonEnabled(true);
|
searchEdit->setClearButtonEnabled(true);
|
||||||
searchEdit->addAction(loadColorAdjustedPixmap("theme:icons/search"), QLineEdit::LeadingPosition);
|
searchEdit->addAction(loadColorAdjustedPixmap("theme:icons/search"), QLineEdit::LeadingPosition);
|
||||||
auto help = searchEdit->addAction(QPixmap("theme:icons/info"), QLineEdit::TrailingPosition);
|
auto help = searchEdit->addAction(themePixmap(QStringLiteral("icons/info")), QLineEdit::TrailingPosition);
|
||||||
|
|
||||||
setFocusProxy(searchEdit);
|
setFocusProxy(searchEdit);
|
||||||
setFocusPolicy(Qt::ClickFocus);
|
setFocusPolicy(Qt::ClickFocus);
|
||||||
|
|
@ -59,13 +59,13 @@ DeckEditorDatabaseDisplayWidget::DeckEditorDatabaseDisplayWidget(QWidget *parent
|
||||||
&DeckEditorDatabaseDisplayWidget::onRelatedCardClicked);
|
&DeckEditorDatabaseDisplayWidget::onRelatedCardClicked);
|
||||||
|
|
||||||
aAddCard = new QAction(QString(), this);
|
aAddCard = new QAction(QString(), this);
|
||||||
aAddCard->setIcon(QPixmap("theme:icons/arrow_right_green"));
|
aAddCard->setIcon(themePixmap(QStringLiteral("icons/arrow_right_green")));
|
||||||
connect(aAddCard, &QAction::triggered, this, &DeckEditorDatabaseDisplayWidget::actAddCardToMainDeck);
|
connect(aAddCard, &QAction::triggered, this, &DeckEditorDatabaseDisplayWidget::actAddCardToMainDeck);
|
||||||
auto *tbAddCard = new QToolButton(this);
|
auto *tbAddCard = new QToolButton(this);
|
||||||
tbAddCard->setDefaultAction(aAddCard);
|
tbAddCard->setDefaultAction(aAddCard);
|
||||||
|
|
||||||
aAddCardToSideboard = new QAction(QString(), this);
|
aAddCardToSideboard = new QAction(QString(), this);
|
||||||
aAddCardToSideboard->setIcon(QPixmap("theme:icons/arrow_right_blue"));
|
aAddCardToSideboard->setIcon(themePixmap(QStringLiteral("icons/arrow_right_blue")));
|
||||||
connect(aAddCardToSideboard, &QAction::triggered, this, &DeckEditorDatabaseDisplayWidget::actAddCardToSideboard);
|
connect(aAddCardToSideboard, &QAction::triggered, this, &DeckEditorDatabaseDisplayWidget::actAddCardToSideboard);
|
||||||
auto *tbAddCardToSideboard = new QToolButton(this);
|
auto *tbAddCardToSideboard = new QToolButton(this);
|
||||||
tbAddCardToSideboard->setDefaultAction(aAddCardToSideboard);
|
tbAddCardToSideboard->setDefaultAction(aAddCardToSideboard);
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
#include "../../../client/settings/cache_settings.h"
|
#include "../../../client/settings/cache_settings.h"
|
||||||
#include "../../../client/settings/shortcuts_settings.h"
|
#include "../../../client/settings/shortcuts_settings.h"
|
||||||
|
#include "../../pixel_map_generator.h"
|
||||||
#include "../playmat/playmat_settings_dialog.h"
|
#include "../playmat/playmat_settings_dialog.h"
|
||||||
#include "../settings_page/user_interface_settings_page.h"
|
#include "../settings_page/user_interface_settings_page.h"
|
||||||
#include "../tabs/api/commander_spellbook/commander_bracket_widget.h"
|
#include "../tabs/api/commander_spellbook/commander_bracket_widget.h"
|
||||||
|
|
@ -192,25 +193,25 @@ void DeckEditorDeckDockWidget::createDeckDock()
|
||||||
&DeckEditorDeckDockWidget::applyActiveGroupCriteria);
|
&DeckEditorDeckDockWidget::applyActiveGroupCriteria);
|
||||||
|
|
||||||
aIncrement = new QAction(QString(), this);
|
aIncrement = new QAction(QString(), this);
|
||||||
aIncrement->setIcon(QPixmap("theme:icons/increment"));
|
aIncrement->setIcon(themePixmap(QStringLiteral("icons/increment")));
|
||||||
connect(aIncrement, &QAction::triggered, this, &DeckEditorDeckDockWidget::actIncrementSelection);
|
connect(aIncrement, &QAction::triggered, this, &DeckEditorDeckDockWidget::actIncrementSelection);
|
||||||
auto *tbIncrement = new QToolButton(this);
|
auto *tbIncrement = new QToolButton(this);
|
||||||
tbIncrement->setDefaultAction(aIncrement);
|
tbIncrement->setDefaultAction(aIncrement);
|
||||||
|
|
||||||
aDecrement = new QAction(QString(), this);
|
aDecrement = new QAction(QString(), this);
|
||||||
aDecrement->setIcon(QPixmap("theme:icons/decrement"));
|
aDecrement->setIcon(themePixmap(QStringLiteral("icons/decrement")));
|
||||||
connect(aDecrement, &QAction::triggered, this, &DeckEditorDeckDockWidget::actDecrementSelection);
|
connect(aDecrement, &QAction::triggered, this, &DeckEditorDeckDockWidget::actDecrementSelection);
|
||||||
auto *tbDecrement = new QToolButton(this);
|
auto *tbDecrement = new QToolButton(this);
|
||||||
tbDecrement->setDefaultAction(aDecrement);
|
tbDecrement->setDefaultAction(aDecrement);
|
||||||
|
|
||||||
aRemoveCard = new QAction(QString(), this);
|
aRemoveCard = new QAction(QString(), this);
|
||||||
aRemoveCard->setIcon(QPixmap("theme:icons/remove_row"));
|
aRemoveCard->setIcon(themePixmap(QStringLiteral("icons/remove_row")));
|
||||||
connect(aRemoveCard, &QAction::triggered, this, &DeckEditorDeckDockWidget::actRemoveCard);
|
connect(aRemoveCard, &QAction::triggered, this, &DeckEditorDeckDockWidget::actRemoveCard);
|
||||||
auto *tbRemoveCard = new QToolButton(this);
|
auto *tbRemoveCard = new QToolButton(this);
|
||||||
tbRemoveCard->setDefaultAction(aRemoveCard);
|
tbRemoveCard->setDefaultAction(aRemoveCard);
|
||||||
|
|
||||||
aSwapCard = new QAction(QString(), this);
|
aSwapCard = new QAction(QString(), this);
|
||||||
aSwapCard->setIcon(QPixmap("theme:icons/swap"));
|
aSwapCard->setIcon(themePixmap(QStringLiteral("icons/swap")));
|
||||||
connect(aSwapCard, &QAction::triggered, this, &DeckEditorDeckDockWidget::actSwapSelection);
|
connect(aSwapCard, &QAction::triggered, this, &DeckEditorDeckDockWidget::actSwapSelection);
|
||||||
auto *tbSwapCard = new QToolButton(this);
|
auto *tbSwapCard = new QToolButton(this);
|
||||||
tbSwapCard->setDefaultAction(aSwapCard);
|
tbSwapCard->setDefaultAction(aSwapCard);
|
||||||
|
|
@ -344,7 +345,9 @@ ExactCard DeckEditorDeckDockWidget::getCurrentCard()
|
||||||
if (!current.isValid()) {
|
if (!current.isValid()) {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
const QString cardName = current.siblingAtColumn(DeckListModelColumns::CARD_NAME).data().toString();
|
// The display role holds the localized card name; the edit role always carries the
|
||||||
|
// canonical English name needed to look the card up in the database.
|
||||||
|
const QString cardName = current.siblingAtColumn(DeckListModelColumns::CARD_NAME).data(Qt::EditRole).toString();
|
||||||
const QString cardProviderID = current.siblingAtColumn(DeckListModelColumns::CARD_PROVIDER_ID).data().toString();
|
const QString cardProviderID = current.siblingAtColumn(DeckListModelColumns::CARD_PROVIDER_ID).data().toString();
|
||||||
const QModelIndex gparent = current.parent().parent();
|
const QModelIndex gparent = current.parent().parent();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
#include "../../../client/settings/shortcuts_settings.h"
|
#include "../../../client/settings/shortcuts_settings.h"
|
||||||
#include "../../../filters/filter_builder.h"
|
#include "../../../filters/filter_builder.h"
|
||||||
#include "../../../filters/filter_tree_model.h"
|
#include "../../../filters/filter_tree_model.h"
|
||||||
|
#include "../../pixel_map_generator.h"
|
||||||
|
|
||||||
#include <QGridLayout>
|
#include <QGridLayout>
|
||||||
#include <QMenu>
|
#include <QMenu>
|
||||||
|
|
@ -42,11 +43,11 @@ void DeckEditorFilterDockWidget::createFiltersDock()
|
||||||
connect(filterBuilder, &FilterBuilder::add, filterModel, &FilterTreeModel::addFilter);
|
connect(filterBuilder, &FilterBuilder::add, filterModel, &FilterTreeModel::addFilter);
|
||||||
|
|
||||||
aClearFilterOne = new QAction(QString(), this);
|
aClearFilterOne = new QAction(QString(), this);
|
||||||
aClearFilterOne->setIcon(QPixmap("theme:icons/decrement"));
|
aClearFilterOne->setIcon(themePixmap(QStringLiteral("icons/decrement")));
|
||||||
connect(aClearFilterOne, &QAction::triggered, this, &DeckEditorFilterDockWidget::actClearFilterOne);
|
connect(aClearFilterOne, &QAction::triggered, this, &DeckEditorFilterDockWidget::actClearFilterOne);
|
||||||
|
|
||||||
aClearFilterAll = new QAction(QString(), this);
|
aClearFilterAll = new QAction(QString(), this);
|
||||||
aClearFilterAll->setIcon(QPixmap("theme:icons/clearsearch"));
|
aClearFilterAll->setIcon(themePixmap(QStringLiteral("icons/clearsearch")));
|
||||||
connect(aClearFilterAll, &QAction::triggered, this, &DeckEditorFilterDockWidget::actClearFilterAll);
|
connect(aClearFilterAll, &QAction::triggered, this, &DeckEditorFilterDockWidget::actClearFilterAll);
|
||||||
|
|
||||||
auto *filterDelOne = new QToolButton();
|
auto *filterDelOne = new QToolButton();
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
#include "deck_list_history_manager_widget.h"
|
#include "deck_list_history_manager_widget.h"
|
||||||
|
|
||||||
|
#include "../../pixel_map_generator.h"
|
||||||
#include "deck_state_manager.h"
|
#include "deck_state_manager.h"
|
||||||
|
|
||||||
DeckListHistoryManagerWidget::DeckListHistoryManagerWidget(DeckStateManager *_deckStateManager,
|
DeckListHistoryManagerWidget::DeckListHistoryManagerWidget(DeckStateManager *_deckStateManager,
|
||||||
|
|
@ -10,7 +11,7 @@ DeckListHistoryManagerWidget::DeckListHistoryManagerWidget(DeckStateManager *_de
|
||||||
layout = new QHBoxLayout(this);
|
layout = new QHBoxLayout(this);
|
||||||
|
|
||||||
aUndo = new QAction(QString(), this);
|
aUndo = new QAction(QString(), this);
|
||||||
aUndo->setIcon(QPixmap("theme:icons/arrow_undo"));
|
aUndo->setIcon(themePixmap(QStringLiteral("icons/arrow_undo")));
|
||||||
aUndo->setShortcut(QKeySequence::Undo);
|
aUndo->setShortcut(QKeySequence::Undo);
|
||||||
aUndo->setShortcutContext(Qt::ApplicationShortcut);
|
aUndo->setShortcutContext(Qt::ApplicationShortcut);
|
||||||
connect(aUndo, &QAction::triggered, this, &DeckListHistoryManagerWidget::doUndo);
|
connect(aUndo, &QAction::triggered, this, &DeckListHistoryManagerWidget::doUndo);
|
||||||
|
|
@ -19,7 +20,7 @@ DeckListHistoryManagerWidget::DeckListHistoryManagerWidget(DeckStateManager *_de
|
||||||
undoButton->setDefaultAction(aUndo);
|
undoButton->setDefaultAction(aUndo);
|
||||||
|
|
||||||
aRedo = new QAction(QString(), this);
|
aRedo = new QAction(QString(), this);
|
||||||
aRedo->setIcon(QPixmap("theme:icons/arrow_redo"));
|
aRedo->setIcon(themePixmap(QStringLiteral("icons/arrow_redo")));
|
||||||
aRedo->setShortcut(QKeySequence::Redo);
|
aRedo->setShortcut(QKeySequence::Redo);
|
||||||
aRedo->setShortcutContext(Qt::ApplicationShortcut);
|
aRedo->setShortcutContext(Qt::ApplicationShortcut);
|
||||||
connect(aRedo, &QAction::triggered, this, &DeckListHistoryManagerWidget::doRedo);
|
connect(aRedo, &QAction::triggered, this, &DeckListHistoryManagerWidget::doRedo);
|
||||||
|
|
@ -31,7 +32,7 @@ DeckListHistoryManagerWidget::DeckListHistoryManagerWidget(DeckStateManager *_de
|
||||||
layout->addWidget(redoButton);
|
layout->addWidget(redoButton);
|
||||||
|
|
||||||
historyButton = new SettingsButtonWidget(this);
|
historyButton = new SettingsButtonWidget(this);
|
||||||
historyButton->setButtonIcon(QPixmap("theme:icons/arrow_history"));
|
historyButton->setButtonIcon(themePixmap(QStringLiteral("icons/arrow_history")));
|
||||||
|
|
||||||
historyLabel = new QLabel(this);
|
historyLabel = new QLabel(this);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,20 @@
|
||||||
#include "deck_state_manager.h"
|
#include "deck_state_manager.h"
|
||||||
|
|
||||||
|
#include "../../../client/settings/cache_settings.h"
|
||||||
|
|
||||||
#include <libcockatrice/card/database/card_database_manager.h>
|
#include <libcockatrice/card/database/card_database_manager.h>
|
||||||
#include <libcockatrice/deck_list/deck_list_history_manager.h>
|
#include <libcockatrice/deck_list/deck_list_history_manager.h>
|
||||||
#include <libcockatrice/deck_list/tree/inner_deck_list_node.h>
|
#include <libcockatrice/deck_list/tree/inner_deck_list_node.h>
|
||||||
|
#include <libcockatrice/settings/cards_display_settings.h>
|
||||||
|
|
||||||
DeckStateManager::DeckStateManager(QObject *parent)
|
DeckStateManager::DeckStateManager(QObject *parent)
|
||||||
: QObject(parent), deckList(QSharedPointer<DeckList>(new DeckList)),
|
: QObject(parent), deckList(QSharedPointer<DeckList>(new DeckList)),
|
||||||
deckListModel(new DeckListModel(this, deckList)), historyManager(new DeckListHistoryManager(this))
|
deckListModel(new DeckListModel(this, deckList)), historyManager(new DeckListHistoryManager(this))
|
||||||
{
|
{
|
||||||
|
deckListModel->setDisplayLanguage(SettingsCache::instance().cardsDisplay().getCardLang());
|
||||||
|
connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::cardLangChanged, deckListModel,
|
||||||
|
[this](const QString &lang) { deckListModel->setDisplayLanguage(lang); });
|
||||||
|
|
||||||
connect(historyManager, &DeckListHistoryManager::undoRedoStateChanged, this, [this] {
|
connect(historyManager, &DeckListHistoryManager::undoRedoStateChanged, this, [this] {
|
||||||
setModified(true);
|
setModified(true);
|
||||||
emit historyChanged();
|
emit historyChanged();
|
||||||
|
|
@ -260,7 +267,10 @@ bool DeckStateManager::swapCardAtIndex(const QModelIndex &idx)
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
QString cardName = idx.siblingAtColumn(DeckListModelColumns::CARD_NAME).data().toString();
|
// The display role holds the localized card name; the edit role always carries the
|
||||||
|
// canonical English name needed to look the card up in the database.
|
||||||
|
QString displayCardName = idx.siblingAtColumn(DeckListModelColumns::CARD_NAME).data().toString();
|
||||||
|
QString cardName = idx.siblingAtColumn(DeckListModelColumns::CARD_NAME).data(Qt::EditRole).toString();
|
||||||
QString providerId = idx.siblingAtColumn(DeckListModelColumns::CARD_PROVIDER_ID).data().toString();
|
QString providerId = idx.siblingAtColumn(DeckListModelColumns::CARD_PROVIDER_ID).data().toString();
|
||||||
QModelIndex gparent = idx.parent().parent();
|
QModelIndex gparent = idx.parent().parent();
|
||||||
|
|
||||||
|
|
@ -277,7 +287,7 @@ bool DeckStateManager::swapCardAtIndex(const QModelIndex &idx)
|
||||||
|
|
||||||
QString reason = tr("Moved to %1 1 × \"%2\" (%3)") //
|
QString reason = tr("Moved to %1 1 × \"%2\" (%3)") //
|
||||||
.arg(otherZoneName)
|
.arg(otherZoneName)
|
||||||
.arg(cardName)
|
.arg(displayCardName)
|
||||||
.arg(providerId);
|
.arg(providerId);
|
||||||
|
|
||||||
return modifyDeck(reason, [&idx, &cardName, &providerId, &otherZoneName](auto model) {
|
return modifyDeck(reason, [&idx, &cardName, &providerId, &otherZoneName](auto model) {
|
||||||
|
|
@ -291,9 +301,8 @@ bool DeckStateManager::removeCardAtIndex(const QModelIndex &idx)
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
QString cardName = idx.siblingAtColumn(DeckListModelColumns::CARD_NAME).data().toString();
|
QString reason =
|
||||||
|
tr("Removed \"%1\" (all copies)").arg(idx.siblingAtColumn(DeckListModelColumns::CARD_NAME).data().toString());
|
||||||
QString reason = tr("Removed \"%1\" (all copies)").arg(cardName);
|
|
||||||
|
|
||||||
return modifyDeck(reason, [&idx](auto model) { return model->removeRow(idx.row(), idx.parent()); });
|
return modifyDeck(reason, [&idx](auto model) { return model->removeRow(idx.row(), idx.parent()); });
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
#include "dlg_connect.h"
|
#include "dlg_connect.h"
|
||||||
|
|
||||||
#include "../../../client/settings/cache_settings.h"
|
#include "../../../client/settings/cache_settings.h"
|
||||||
|
#include "../../pixel_map_generator.h"
|
||||||
|
|
||||||
#include <QCheckBox>
|
#include <QCheckBox>
|
||||||
#include <QComboBox>
|
#include <QComboBox>
|
||||||
|
|
@ -21,7 +22,7 @@ DlgConnect::DlgConnect(QWidget *parent) : QDialog(parent)
|
||||||
previousHosts = new QComboBox(this);
|
previousHosts = new QComboBox(this);
|
||||||
|
|
||||||
btnDeleteServer = new QPushButton(this);
|
btnDeleteServer = new QPushButton(this);
|
||||||
btnDeleteServer->setIcon(QPixmap("theme:icons/remove_row"));
|
btnDeleteServer->setIcon(themePixmap(QStringLiteral("icons/remove_row")));
|
||||||
btnDeleteServer->setToolTip(tr("Delete the currently selected saved server"));
|
btnDeleteServer->setToolTip(tr("Delete the currently selected saved server"));
|
||||||
btnDeleteServer->setFixedWidth(30);
|
btnDeleteServer->setFixedWidth(30);
|
||||||
|
|
||||||
|
|
@ -29,7 +30,7 @@ DlgConnect::DlgConnect(QWidget *parent) : QDialog(parent)
|
||||||
|
|
||||||
hps = new HandlePublicServers(this);
|
hps = new HandlePublicServers(this);
|
||||||
btnRefreshServers = new QPushButton(this);
|
btnRefreshServers = new QPushButton(this);
|
||||||
btnRefreshServers->setIcon(QPixmap("theme:icons/sync"));
|
btnRefreshServers->setIcon(themePixmap(QStringLiteral("icons/sync")));
|
||||||
btnRefreshServers->setToolTip(tr("Refresh the server list with known public servers"));
|
btnRefreshServers->setToolTip(tr("Refresh the server list with known public servers"));
|
||||||
btnRefreshServers->setFixedWidth(30);
|
btnRefreshServers->setFixedWidth(30);
|
||||||
|
|
||||||
|
|
@ -99,7 +100,7 @@ DlgConnect::DlgConnect(QWidget *parent) : QDialog(parent)
|
||||||
updateDisplayInfo(previousHosts->currentText());
|
updateDisplayInfo(previousHosts->currentText());
|
||||||
|
|
||||||
btnForgotPassword = new QPushButton(this);
|
btnForgotPassword = new QPushButton(this);
|
||||||
btnForgotPassword->setIcon(QPixmap("theme:icons/forgot_password"));
|
btnForgotPassword->setIcon(themePixmap(QStringLiteral("icons/forgot_password")));
|
||||||
btnForgotPassword->setToolTip(tr("Reset Password"));
|
btnForgotPassword->setToolTip(tr("Reset Password"));
|
||||||
btnForgotPassword->setFixedWidth(30);
|
btnForgotPassword->setFixedWidth(30);
|
||||||
connect(btnForgotPassword, &QPushButton::released, this, &DlgConnect::actForgotPassword);
|
connect(btnForgotPassword, &QPushButton::released, this, &DlgConnect::actForgotPassword);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
#include "dlg_edit_tokens.h"
|
#include "dlg_edit_tokens.h"
|
||||||
|
|
||||||
|
#include "../../pixel_map_generator.h"
|
||||||
#include "../interface/widgets/utility/get_text_with_max.h"
|
#include "../interface/widgets/utility/get_text_with_max.h"
|
||||||
|
|
||||||
#include <QAction>
|
#include <QAction>
|
||||||
|
|
@ -90,10 +91,10 @@ DlgEditTokens::DlgEditTokens(QWidget *parent) : QDialog(parent), currentCard(nul
|
||||||
&DlgEditTokens::tokenSelectionChanged);
|
&DlgEditTokens::tokenSelectionChanged);
|
||||||
|
|
||||||
QAction *aAddToken = new QAction(tr("Add token"), this);
|
QAction *aAddToken = new QAction(tr("Add token"), this);
|
||||||
aAddToken->setIcon(QPixmap("theme:icons/increment"));
|
aAddToken->setIcon(themePixmap(QStringLiteral("icons/increment")));
|
||||||
connect(aAddToken, &QAction::triggered, this, &DlgEditTokens::actAddToken);
|
connect(aAddToken, &QAction::triggered, this, &DlgEditTokens::actAddToken);
|
||||||
QAction *aRemoveToken = new QAction(tr("Remove token"), this);
|
QAction *aRemoveToken = new QAction(tr("Remove token"), this);
|
||||||
aRemoveToken->setIcon(QPixmap("theme:icons/decrement"));
|
aRemoveToken->setIcon(themePixmap(QStringLiteral("icons/decrement")));
|
||||||
connect(aRemoveToken, &QAction::triggered, this, &DlgEditTokens::actRemoveToken);
|
connect(aRemoveToken, &QAction::triggered, this, &DlgEditTokens::actRemoveToken);
|
||||||
|
|
||||||
auto *databaseToolBar = new QToolBar;
|
auto *databaseToolBar = new QToolBar;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
#include "dlg_manage_sets.h"
|
#include "dlg_manage_sets.h"
|
||||||
|
|
||||||
#include "../../../client/settings/cache_settings.h"
|
#include "../../../client/settings/cache_settings.h"
|
||||||
|
#include "../../pixel_map_generator.h"
|
||||||
#include "../interface/card_picture_loader/card_picture_loader.h"
|
#include "../interface/card_picture_loader/card_picture_loader.h"
|
||||||
#include "../interface/widgets/utility/custom_line_edit.h"
|
#include "../interface/widgets/utility/custom_line_edit.h"
|
||||||
|
|
||||||
|
|
@ -35,28 +36,28 @@ WndSets::WndSets(QWidget *parent) : QMainWindow(parent)
|
||||||
setsEditToolBar->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum);
|
setsEditToolBar->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum);
|
||||||
|
|
||||||
aTop = new QAction(QString(), this);
|
aTop = new QAction(QString(), this);
|
||||||
aTop->setIcon(QPixmap("theme:icons/arrow_top_green"));
|
aTop->setIcon(themePixmap(QStringLiteral("icons/arrow_top_green")));
|
||||||
aTop->setToolTip(tr("Move selected set to the top"));
|
aTop->setToolTip(tr("Move selected set to the top"));
|
||||||
aTop->setEnabled(false);
|
aTop->setEnabled(false);
|
||||||
connect(aTop, &QAction::triggered, this, &WndSets::actTop);
|
connect(aTop, &QAction::triggered, this, &WndSets::actTop);
|
||||||
setsEditToolBar->addAction(aTop);
|
setsEditToolBar->addAction(aTop);
|
||||||
|
|
||||||
aUp = new QAction(QString(), this);
|
aUp = new QAction(QString(), this);
|
||||||
aUp->setIcon(QPixmap("theme:icons/arrow_up_green"));
|
aUp->setIcon(themePixmap(QStringLiteral("icons/arrow_up_green")));
|
||||||
aUp->setToolTip(tr("Move selected set up"));
|
aUp->setToolTip(tr("Move selected set up"));
|
||||||
aUp->setEnabled(false);
|
aUp->setEnabled(false);
|
||||||
connect(aUp, &QAction::triggered, this, &WndSets::actUp);
|
connect(aUp, &QAction::triggered, this, &WndSets::actUp);
|
||||||
setsEditToolBar->addAction(aUp);
|
setsEditToolBar->addAction(aUp);
|
||||||
|
|
||||||
aDown = new QAction(QString(), this);
|
aDown = new QAction(QString(), this);
|
||||||
aDown->setIcon(QPixmap("theme:icons/arrow_down_green"));
|
aDown->setIcon(themePixmap(QStringLiteral("icons/arrow_down_green")));
|
||||||
aDown->setToolTip(tr("Move selected set down"));
|
aDown->setToolTip(tr("Move selected set down"));
|
||||||
aDown->setEnabled(false);
|
aDown->setEnabled(false);
|
||||||
connect(aDown, &QAction::triggered, this, &WndSets::actDown);
|
connect(aDown, &QAction::triggered, this, &WndSets::actDown);
|
||||||
setsEditToolBar->addAction(aDown);
|
setsEditToolBar->addAction(aDown);
|
||||||
|
|
||||||
aBottom = new QAction(QString(), this);
|
aBottom = new QAction(QString(), this);
|
||||||
aBottom->setIcon(QPixmap("theme:icons/arrow_bottom_green"));
|
aBottom->setIcon(themePixmap(QStringLiteral("icons/arrow_bottom_green")));
|
||||||
aBottom->setToolTip(tr("Move selected set to the bottom"));
|
aBottom->setToolTip(tr("Move selected set to the bottom"));
|
||||||
aBottom->setEnabled(false);
|
aBottom->setEnabled(false);
|
||||||
connect(aBottom, &QAction::triggered, this, &WndSets::actBottom);
|
connect(aBottom, &QAction::triggered, this, &WndSets::actBottom);
|
||||||
|
|
@ -66,7 +67,7 @@ WndSets::WndSets(QWidget *parent) : QMainWindow(parent)
|
||||||
searchField = new LineEditUnfocusable;
|
searchField = new LineEditUnfocusable;
|
||||||
searchField->setObjectName("searchEdit");
|
searchField->setObjectName("searchEdit");
|
||||||
searchField->setPlaceholderText(tr("Search by set name, code, type, or release date"));
|
searchField->setPlaceholderText(tr("Search by set name, code, type, or release date"));
|
||||||
searchField->addAction(QPixmap("theme:icons/search"), LineEditUnfocusable::LeadingPosition);
|
searchField->addAction(themePixmap(QStringLiteral("icons/search")), LineEditUnfocusable::LeadingPosition);
|
||||||
searchField->setClearButtonEnabled(true);
|
searchField->setClearButtonEnabled(true);
|
||||||
setFocusProxy(searchField);
|
setFocusProxy(searchField);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
#include "dlg_register.h"
|
#include "dlg_register.h"
|
||||||
|
|
||||||
#include "../../../client/settings/cache_settings.h"
|
#include "../../../client/settings/cache_settings.h"
|
||||||
|
#include "../../pixel_map_generator.h"
|
||||||
#include "../server/handle_public_servers.h"
|
#include "../server/handle_public_servers.h"
|
||||||
#include "../server/user/user_info_connection.h"
|
#include "../server/user/user_info_connection.h"
|
||||||
|
|
||||||
|
|
@ -24,7 +25,7 @@ DlgRegister::DlgRegister(QWidget *parent) : QDialog(parent)
|
||||||
previousHosts = new QComboBox(this);
|
previousHosts = new QComboBox(this);
|
||||||
|
|
||||||
btnDeleteServer = new QPushButton(this);
|
btnDeleteServer = new QPushButton(this);
|
||||||
btnDeleteServer->setIcon(QPixmap("theme:icons/remove_row"));
|
btnDeleteServer->setIcon(themePixmap(QStringLiteral("icons/remove_row")));
|
||||||
btnDeleteServer->setToolTip(tr("Delete the currently selected saved server"));
|
btnDeleteServer->setToolTip(tr("Delete the currently selected saved server"));
|
||||||
btnDeleteServer->setFixedWidth(30);
|
btnDeleteServer->setFixedWidth(30);
|
||||||
|
|
||||||
|
|
@ -32,7 +33,7 @@ DlgRegister::DlgRegister(QWidget *parent) : QDialog(parent)
|
||||||
|
|
||||||
hps = new HandlePublicServers(this);
|
hps = new HandlePublicServers(this);
|
||||||
btnRefreshServers = new QPushButton(this);
|
btnRefreshServers = new QPushButton(this);
|
||||||
btnRefreshServers->setIcon(QPixmap("theme:icons/sync"));
|
btnRefreshServers->setIcon(themePixmap(QStringLiteral("icons/sync")));
|
||||||
btnRefreshServers->setToolTip(tr("Refresh the server list with known public servers"));
|
btnRefreshServers->setToolTip(tr("Refresh the server list with known public servers"));
|
||||||
btnRefreshServers->setFixedWidth(30);
|
btnRefreshServers->setFixedWidth(30);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@
|
||||||
#include "dlg_settings.h"
|
#include "dlg_settings.h"
|
||||||
|
|
||||||
#include "../../../client/settings/cache_settings.h"
|
#include "../../../client/settings/cache_settings.h"
|
||||||
|
#include "../../pixel_map_generator.h"
|
||||||
#include "../main.h"
|
#include "../main.h"
|
||||||
#include "../settings_page/appearance_settings_page.h"
|
#include "../settings_page/appearance_settings_page.h"
|
||||||
#include "../settings_page/deck_editor_settings_page.h"
|
#include "../settings_page/deck_editor_settings_page.h"
|
||||||
|
|
@ -96,7 +97,7 @@ void DlgSettings::setupUi()
|
||||||
// Search bar
|
// Search bar
|
||||||
searchEdit = new QLineEdit;
|
searchEdit = new QLineEdit;
|
||||||
searchEdit->setClearButtonEnabled(true);
|
searchEdit->setClearButtonEnabled(true);
|
||||||
searchEdit->addAction(QPixmap("theme:icons/search"), QLineEdit::LeadingPosition);
|
searchEdit->addAction(themePixmap(QStringLiteral("icons/search")), QLineEdit::LeadingPosition);
|
||||||
searchEdit->installEventFilter(this);
|
searchEdit->installEventFilter(this);
|
||||||
connect(searchEdit, &QLineEdit::textChanged, this, &DlgSettings::onSearchTextChanged);
|
connect(searchEdit, &QLineEdit::textChanged, this, &DlgSettings::onSearchTextChanged);
|
||||||
|
|
||||||
|
|
@ -132,7 +133,7 @@ void DlgSettings::setupUi()
|
||||||
pagesWidget->addWidget(makeScrollable(userInterfacePage));
|
pagesWidget->addWidget(makeScrollable(userInterfacePage));
|
||||||
pagesWidget->addWidget(makeScrollable(deckEditorPage));
|
pagesWidget->addWidget(makeScrollable(deckEditorPage));
|
||||||
pagesWidget->addWidget(makeScrollable(storagePage));
|
pagesWidget->addWidget(makeScrollable(storagePage));
|
||||||
pagesWidget->addWidget(messagesPage);
|
pagesWidget->addWidget(makeScrollable(messagesPage));
|
||||||
pagesWidget->addWidget(soundPage);
|
pagesWidget->addWidget(soundPage);
|
||||||
pagesWidget->addWidget(shortcutsPage);
|
pagesWidget->addWidget(shortcutsPage);
|
||||||
|
|
||||||
|
|
@ -415,6 +416,11 @@ void DlgSettings::setTab(int index)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AbstractSettingsPage *DlgSettings::page(SettingsPage which) const
|
||||||
|
{
|
||||||
|
return pages.value(static_cast<int>(which));
|
||||||
|
}
|
||||||
|
|
||||||
void DlgSettings::updateLanguage()
|
void DlgSettings::updateLanguage()
|
||||||
{
|
{
|
||||||
qApp->removeTranslator(translator); // NOLINT(cppcoreguidelines-pro-type-static-cast-downcast)
|
qApp->removeTranslator(translator); // NOLINT(cppcoreguidelines-pro-type-static-cast-downcast)
|
||||||
|
|
@ -477,13 +483,13 @@ void DlgSettings::closeEvent(QCloseEvent *event)
|
||||||
case Invalid:
|
case Invalid:
|
||||||
loadErrorMessage = tr("Your card database is invalid.\n\n"
|
loadErrorMessage = tr("Your card database is invalid.\n\n"
|
||||||
"Cockatrice may not function correctly with an invalid database\n\n"
|
"Cockatrice may not function correctly with an invalid database\n\n"
|
||||||
"You may need to rerun oracle to update your card database.\n\n"
|
"You may need to rerun Oracle to update your card database.\n\n"
|
||||||
"Would you like to change your database location setting?");
|
"Would you like to change your database location setting?");
|
||||||
break;
|
break;
|
||||||
case VersionTooOld:
|
case VersionTooOld:
|
||||||
loadErrorMessage = tr("Your card database version is too old.\n\n"
|
loadErrorMessage = tr("Your card database version is too old.\n\n"
|
||||||
"This can cause problems loading card information or images\n\n"
|
"This can cause problems loading card information or images\n\n"
|
||||||
"Usually this can be fixed by rerunning oracle to to update your card database.\n\n"
|
"Usually this can be fixed by rerunning Oracle to to update your card database.\n\n"
|
||||||
"Would you like to change your database location setting?");
|
"Would you like to change your database location setting?");
|
||||||
break;
|
break;
|
||||||
case NotLoaded:
|
case NotLoaded:
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,7 @@ public:
|
||||||
|
|
||||||
explicit DlgSettings(QWidget *parent = nullptr);
|
explicit DlgSettings(QWidget *parent = nullptr);
|
||||||
void setTab(int index);
|
void setTab(int index);
|
||||||
|
AbstractSettingsPage *page(SettingsPage which) const;
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void onTabClicked(int index);
|
void onTabClicked(int index);
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,8 @@ namespace HomeTabButtonColor
|
||||||
*/
|
*/
|
||||||
enum Source
|
enum Source
|
||||||
{
|
{
|
||||||
Automatic, ///< Extract color from background, or use theme color if no background
|
FromThemeColors, ///< Use the theme's identity accent colors
|
||||||
FromBackground, ///< Always extract color from background
|
FromBackground, ///< Extract colour from the background image
|
||||||
};
|
};
|
||||||
|
|
||||||
struct Entry
|
struct Entry
|
||||||
|
|
@ -23,7 +23,7 @@ struct Entry
|
||||||
|
|
||||||
inline QList<Entry> all()
|
inline QList<Entry> all()
|
||||||
{
|
{
|
||||||
static QList<Entry> entries = {{Automatic, QT_TR_NOOP("Automatic")},
|
static QList<Entry> entries = {{FromThemeColors, QT_TR_NOOP("From theme colors")},
|
||||||
{FromBackground, QT_TR_NOOP("Extract from background")}};
|
{FromBackground, QT_TR_NOOP("Extract from background")}};
|
||||||
|
|
||||||
return entries;
|
return entries;
|
||||||
|
|
@ -33,12 +33,12 @@ inline QList<Entry> all()
|
||||||
* Safely converts an int into the corresponding Source.
|
* Safely converts an int into the corresponding Source.
|
||||||
*
|
*
|
||||||
* @param value The int value
|
* @param value The int value
|
||||||
* @return The Source. Returns Source::Automatic if the value is not within range
|
* @return The Source. Returns Source::FromThemeColors if the value is not within range
|
||||||
*/
|
*/
|
||||||
inline Source intToSource(int value)
|
inline Source intToSource(int value)
|
||||||
{
|
{
|
||||||
if (value > FromBackground) {
|
if (value > FromBackground) {
|
||||||
return Automatic; // default
|
return FromThemeColors; // default
|
||||||
}
|
}
|
||||||
|
|
||||||
return static_cast<Source>(value);
|
return static_cast<Source>(value);
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
#include "../../../client/settings/cache_settings.h"
|
#include "../../../client/settings/cache_settings.h"
|
||||||
#include "../../../interface/widgets/tabs/tab_supervisor.h"
|
#include "../../../interface/widgets/tabs/tab_supervisor.h"
|
||||||
|
#include "../../pixel_map_generator.h"
|
||||||
#include "../../theme_manager.h"
|
#include "../../theme_manager.h"
|
||||||
#include "../../window_main.h"
|
#include "../../window_main.h"
|
||||||
#include "../cards/art_crop_attribution.h"
|
#include "../cards/art_crop_attribution.h"
|
||||||
|
|
@ -10,6 +11,8 @@
|
||||||
#include "home_tab_button_color.h"
|
#include "home_tab_button_color.h"
|
||||||
|
|
||||||
#include <QGroupBox>
|
#include <QGroupBox>
|
||||||
|
#include <QLabel>
|
||||||
|
#include <QLinearGradient>
|
||||||
#include <QPainter>
|
#include <QPainter>
|
||||||
#include <QPainterPath>
|
#include <QPainterPath>
|
||||||
#include <QPushButton>
|
#include <QPushButton>
|
||||||
|
|
@ -20,7 +23,7 @@
|
||||||
#include <libcockatrice/settings/paths_settings.h>
|
#include <libcockatrice/settings/paths_settings.h>
|
||||||
|
|
||||||
HomeWidget::HomeWidget(QWidget *parent, TabSupervisor *_tabSupervisor)
|
HomeWidget::HomeWidget(QWidget *parent, TabSupervisor *_tabSupervisor)
|
||||||
: QWidget(parent), tabSupervisor(_tabSupervisor), background("theme:backgrounds/home"), overlay("theme:cockatrice")
|
: QWidget(parent), tabSupervisor(_tabSupervisor), background(themePixmap(QStringLiteral("backgrounds/home")))
|
||||||
{
|
{
|
||||||
layout = new QGridLayout(this);
|
layout = new QGridLayout(this);
|
||||||
|
|
||||||
|
|
@ -52,16 +55,27 @@ HomeWidget::HomeWidget(QWidget *parent, TabSupervisor *_tabSupervisor)
|
||||||
// Lambda is cleaner to read than overloading this
|
// Lambda is cleaner to read than overloading this
|
||||||
connect(&SettingsCache::instance().appearance(), &AppearanceSettings::homeTabDisplayCardNameChanged, this,
|
connect(&SettingsCache::instance().appearance(), &AppearanceSettings::homeTabDisplayCardNameChanged, this,
|
||||||
[this] { repaint(); });
|
[this] { repaint(); });
|
||||||
|
connect(&SettingsCache::instance().appearance(), &AppearanceSettings::homeTabBackgroundDimChanged, this,
|
||||||
|
[this] { repaint(); });
|
||||||
connect(&SettingsCache::instance(), &SettingsCache::themeChanged, this,
|
connect(&SettingsCache::instance(), &SettingsCache::themeChanged, this,
|
||||||
&HomeWidget::initializeBackgroundFromSource);
|
&HomeWidget::initializeBackgroundFromSource);
|
||||||
connect(&SettingsCache::instance(), &SettingsCache::themeChanged, this,
|
connect(&SettingsCache::instance(), &SettingsCache::themeChanged, this,
|
||||||
&HomeWidget::updateButtonsToBackgroundColor);
|
&HomeWidget::updateButtonsToBackgroundColor);
|
||||||
|
// Scheme flips (light/dark/system with an OS switch) fire on themeManager,
|
||||||
|
// not on SettingsCache::themeChanged, so re-resolve the variant background.
|
||||||
|
connect(themeManager, &ThemeManager::themeChanged, this, &HomeWidget::initializeBackgroundFromSource);
|
||||||
|
connect(themeManager, &ThemeManager::paletteChanged, this, &HomeWidget::updateButtonsToBackgroundColor);
|
||||||
|
connect(themeManager, &ThemeManager::paletteChanged, this, &HomeWidget::updateLogoOverlay);
|
||||||
connect(&SettingsCache::instance().appearance(), &AppearanceSettings::homeTabButtonColorChanged, this,
|
connect(&SettingsCache::instance().appearance(), &AppearanceSettings::homeTabButtonColorChanged, this,
|
||||||
&HomeWidget::updateButtonsToBackgroundColor);
|
&HomeWidget::updateButtonsToBackgroundColor);
|
||||||
}
|
}
|
||||||
|
|
||||||
void HomeWidget::initializeBackgroundFromSource()
|
void HomeWidget::initializeBackgroundFromSource()
|
||||||
{
|
{
|
||||||
|
// The featured logo is theme/scheme-derived too; reload it alongside the
|
||||||
|
// background so a theme or appearance switch doesn't leave it stale.
|
||||||
|
updateLogoOverlay();
|
||||||
|
|
||||||
if (CardDatabaseManager::getInstance()->getLoadStatus() != LoadStatus::Ok) {
|
if (CardDatabaseManager::getInstance()->getLoadStatus() != LoadStatus::Ok) {
|
||||||
connect(CardDatabaseManager::getInstance(), &CardDatabase::cardDatabaseLoadingFinished, this,
|
connect(CardDatabaseManager::getInstance(), &CardDatabase::cardDatabaseLoadingFinished, this,
|
||||||
&HomeWidget::initializeBackgroundFromSource);
|
&HomeWidget::initializeBackgroundFromSource);
|
||||||
|
|
@ -74,7 +88,7 @@ void HomeWidget::initializeBackgroundFromSource()
|
||||||
switch (backgroundSourceType) {
|
switch (backgroundSourceType) {
|
||||||
case BackgroundSources::Theme:
|
case BackgroundSources::Theme:
|
||||||
cardChangeTimer->stop();
|
cardChangeTimer->stop();
|
||||||
background = QPixmap("theme:backgrounds/home");
|
background = themePixmap(QStringLiteral("backgrounds/home"));
|
||||||
backgroundSourceDeck = DeckList();
|
backgroundSourceDeck = DeckList();
|
||||||
backgroundSourceCard->setCard(ExactCard());
|
backgroundSourceCard->setCard(ExactCard());
|
||||||
updateButtonsToBackgroundColor();
|
updateButtonsToBackgroundColor();
|
||||||
|
|
@ -100,32 +114,24 @@ void HomeWidget::loadBackgroundSourceDeck()
|
||||||
backgroundSourceDeck = deckOpt.has_value() ? deckOpt.value().deckList : DeckList();
|
backgroundSourceDeck = deckOpt.has_value() ? deckOpt.value().deckList : DeckList();
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool isDefaultBackgroundAndTheme()
|
static QPair<QColor, QColor> paletteDerivedButtonColors()
|
||||||
{
|
{
|
||||||
QString sourceId = SettingsCache::instance().appearance().getHomeTabBackgroundSource();
|
return {themeManager->appColor(AppColor::AccentStrong), themeManager->appColor(AppColor::AccentSoft)};
|
||||||
return themeManager->isBuiltInTheme() && BackgroundSources::fromId(sourceId) == BackgroundSources::Theme;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QPair<QColor, QColor> HomeWidget::determineButtonColor() const
|
QPair<QColor, QColor> HomeWidget::determineButtonColor() const
|
||||||
{
|
{
|
||||||
static QPair defaultColor = {QColor::fromRgb(20, 140, 60), QColor::fromRgb(120, 200, 80)};
|
|
||||||
|
|
||||||
auto colorSource =
|
auto colorSource =
|
||||||
HomeTabButtonColor::intToSource(SettingsCache::instance().appearance().getHomeTabButtonColorSourceIndex());
|
HomeTabButtonColor::intToSource(SettingsCache::instance().appearance().getHomeTabButtonColorSourceIndex());
|
||||||
|
|
||||||
switch (colorSource) {
|
switch (colorSource) {
|
||||||
case HomeTabButtonColor::Automatic: {
|
case HomeTabButtonColor::FromThemeColors:
|
||||||
if (isDefaultBackgroundAndTheme()) {
|
return paletteDerivedButtonColors();
|
||||||
return defaultColor;
|
|
||||||
} else {
|
|
||||||
return extractDominantColors(background);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case HomeTabButtonColor::FromBackground:
|
case HomeTabButtonColor::FromBackground:
|
||||||
return extractDominantColors(background);
|
return extractDominantColors(background);
|
||||||
}
|
}
|
||||||
|
|
||||||
return defaultColor;
|
return paletteDerivedButtonColors();
|
||||||
}
|
}
|
||||||
|
|
||||||
void HomeWidget::setRandomCard(ExactCard &newCard)
|
void HomeWidget::setRandomCard(ExactCard &newCard)
|
||||||
|
|
@ -229,10 +235,10 @@ QGroupBox *HomeWidget::createButtons()
|
||||||
QVBoxLayout *boxLayout = new QVBoxLayout;
|
QVBoxLayout *boxLayout = new QVBoxLayout;
|
||||||
boxLayout->setAlignment(Qt::AlignHCenter);
|
boxLayout->setAlignment(Qt::AlignHCenter);
|
||||||
|
|
||||||
QLabel *logoLabel = new QLabel;
|
logoLabel = new QLabel;
|
||||||
logoLabel->setPixmap(overlay.scaledToWidth(200, Qt::SmoothTransformation));
|
|
||||||
logoLabel->setAlignment(Qt::AlignCenter);
|
logoLabel->setAlignment(Qt::AlignCenter);
|
||||||
boxLayout->addWidget(logoLabel);
|
boxLayout->addWidget(logoLabel);
|
||||||
|
updateLogoOverlay();
|
||||||
boxLayout->addSpacing(25);
|
boxLayout->addSpacing(25);
|
||||||
|
|
||||||
connectButton = new HomeStyledButton("Connect/Play", gradientColors);
|
connectButton = new HomeStyledButton("Connect/Play", gradientColors);
|
||||||
|
|
@ -360,13 +366,15 @@ void HomeWidget::paintEvent(QPaintEvent *event)
|
||||||
painter.drawPixmap(topLeft, toDraw);
|
painter.drawPixmap(topLeft, toDraw);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw translucent black overlay with rounded corners
|
if (SettingsCache::instance().appearance().getHomeTabBackgroundDim()) {
|
||||||
QRectF overlayRect(5, 5, width() - 10, height() - 10);
|
// Draw translucent black overlay with rounded corners
|
||||||
QPainterPath roundedRectPath;
|
QRectF overlayRect(5, 5, width() - 10, height() - 10);
|
||||||
roundedRectPath.addRoundedRect(overlayRect, 20, 20);
|
QPainterPath roundedRectPath;
|
||||||
|
roundedRectPath.addRoundedRect(overlayRect, 20, 20);
|
||||||
|
|
||||||
QColor semiTransparentBlack(0, 0, 0, static_cast<int>(255 * 0.33));
|
QColor semiTransparentBlack(0, 0, 0, static_cast<int>(255 * 0.33));
|
||||||
painter.fillPath(roundedRectPath, semiTransparentBlack);
|
painter.fillPath(roundedRectPath, semiTransparentBlack);
|
||||||
|
}
|
||||||
|
|
||||||
// Card name overlay (above the attribution, bottom-right)
|
// Card name overlay (above the attribution, bottom-right)
|
||||||
QString cardName;
|
QString cardName;
|
||||||
|
|
@ -431,3 +439,56 @@ void HomeWidget::paintEvent(QPaintEvent *event)
|
||||||
|
|
||||||
QWidget::paintEvent(event);
|
QWidget::paintEvent(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void HomeWidget::updateLogoOverlay()
|
||||||
|
{
|
||||||
|
// Emulate cockatrice.svg in Qt rather than rendering the baked-in SVG.
|
||||||
|
// The SVG has no separate plate: the gradient fills the bird's silhouette
|
||||||
|
// paths (light #c9fd62/AccentSoft at the top-left, dark #139740/AccentStrong
|
||||||
|
// toward the bottom-right — the SVG's linearGradient4265-7-8 stops along
|
||||||
|
// its userSpaceOnUse axis), and the white highlight path
|
||||||
|
// (cockatrice-logo-white) sits on top. So we paint that gradient clipped to
|
||||||
|
// the full logo silhouette (the full-color logo's alpha), then overlay the
|
||||||
|
// white mark. Colours stay fully theme-driven and independent of the static
|
||||||
|
// greens baked into the SVG.
|
||||||
|
const QColor strong = themeManager->appColor(AppColor::AccentStrong);
|
||||||
|
const QColor soft = themeManager->appColor(AppColor::AccentSoft);
|
||||||
|
|
||||||
|
const QPixmap silhouette = themePixmap(QStringLiteral("cockatrice")).scaledToWidth(200, Qt::SmoothTransformation);
|
||||||
|
const QPixmap whiteMark =
|
||||||
|
themePixmap(QStringLiteral("cockatrice-logo-white")).scaledToWidth(200, Qt::SmoothTransformation);
|
||||||
|
if (silhouette.isNull() || whiteMark.isNull()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QPixmap composite(silhouette.size());
|
||||||
|
composite.fill(Qt::transparent);
|
||||||
|
|
||||||
|
{
|
||||||
|
QPainter painter(&composite);
|
||||||
|
painter.setRenderHint(QPainter::Antialiasing);
|
||||||
|
painter.setRenderHint(QPainter::SmoothPixmapTransform);
|
||||||
|
|
||||||
|
// Recreate cockatrice.svg's own gradient geometry (linearGradient
|
||||||
|
// 4265-7-8, userSpaceOnUse): light AccentSoft at S=(-8.097,-97.746),
|
||||||
|
// dark AccentStrong at E=(162.455,295.208), on the SVG's 300x300
|
||||||
|
// canvas. Scale those coordinates to this composite's size.
|
||||||
|
const qreal scale = composite.width() / 300.0;
|
||||||
|
QLinearGradient gradient(QPointF(-8.097, -97.746) * scale, QPointF(162.455, 295.208) * scale);
|
||||||
|
gradient.setColorAt(0.0, soft);
|
||||||
|
gradient.setColorAt(1.0, strong);
|
||||||
|
painter.fillRect(composite.rect(), gradient);
|
||||||
|
|
||||||
|
// Clip the gradient to the full logo silhouette exactly as the SVG's
|
||||||
|
// gradient paths are confined to the bird.
|
||||||
|
painter.setCompositionMode(QPainter::CompositionMode_DestinationIn);
|
||||||
|
painter.drawPixmap(0, 0, silhouette);
|
||||||
|
|
||||||
|
painter.setCompositionMode(QPainter::CompositionMode_SourceOver);
|
||||||
|
painter.drawPixmap(0, 0, whiteMark);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (logoLabel) {
|
||||||
|
logoLabel->setPixmap(composite);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,9 @@
|
||||||
#include <QWidget>
|
#include <QWidget>
|
||||||
#include <libcockatrice/network/client/abstract/abstract_client.h>
|
#include <libcockatrice/network/client/abstract/abstract_client.h>
|
||||||
|
|
||||||
|
class QGridLayout;
|
||||||
|
class QLabel;
|
||||||
|
|
||||||
class HomeWidget : public QWidget
|
class HomeWidget : public QWidget
|
||||||
{
|
{
|
||||||
|
|
||||||
|
|
@ -41,13 +44,14 @@ private:
|
||||||
QPixmap background;
|
QPixmap background;
|
||||||
CardInfoPictureArtCropWidget *backgroundSourceCard = nullptr;
|
CardInfoPictureArtCropWidget *backgroundSourceCard = nullptr;
|
||||||
DeckList backgroundSourceDeck;
|
DeckList backgroundSourceDeck;
|
||||||
QPixmap overlay;
|
QLabel *logoLabel = nullptr;
|
||||||
QPair<QColor, QColor> gradientColors;
|
QPair<QColor, QColor> gradientColors;
|
||||||
HomeStyledButton *connectButton;
|
HomeStyledButton *connectButton;
|
||||||
|
|
||||||
void setRandomCard(ExactCard &newCard);
|
void setRandomCard(ExactCard &newCard);
|
||||||
void loadBackgroundSourceDeck();
|
void loadBackgroundSourceDeck();
|
||||||
QPair<QColor, QColor> determineButtonColor() const;
|
QPair<QColor, QColor> determineButtonColor() const;
|
||||||
|
void updateLogoOverlay();
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // HOME_WIDGET_H
|
#endif // HOME_WIDGET_H
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,10 @@ class BannerShaderConfig : public QObject
|
||||||
Q_PROPERTY(QColor colorA READ colorA WRITE setColorA NOTIFY colorAChanged)
|
Q_PROPERTY(QColor colorA READ colorA WRITE setColorA NOTIFY colorAChanged)
|
||||||
Q_PROPERTY(QColor colorB READ colorB WRITE setColorB NOTIFY colorBChanged)
|
Q_PROPERTY(QColor colorB READ colorB WRITE setColorB NOTIFY colorBChanged)
|
||||||
Q_PROPERTY(QColor accent READ accent WRITE setAccent NOTIFY accentChanged)
|
Q_PROPERTY(QColor accent READ accent WRITE setAccent NOTIFY accentChanged)
|
||||||
|
Q_PROPERTY(QColor glowColor READ glowColor WRITE setGlowColor NOTIFY glowColorChanged)
|
||||||
|
Q_PROPERTY(QColor brandStrong READ brandStrong WRITE setBrandStrong NOTIFY brandStrongChanged)
|
||||||
|
Q_PROPERTY(QColor brandSoft READ brandSoft WRITE setBrandSoft NOTIFY brandSoftChanged)
|
||||||
|
Q_PROPERTY(qreal vignetteMin READ vignetteMin WRITE setVignetteMin NOTIFY vignetteMinChanged)
|
||||||
|
|
||||||
Q_PROPERTY(bool logoVisible READ logoVisible WRITE setLogoVisible NOTIFY logoVisibleChanged)
|
Q_PROPERTY(bool logoVisible READ logoVisible WRITE setLogoVisible NOTIFY logoVisibleChanged)
|
||||||
Q_PROPERTY(qreal logoGlow READ logoGlow WRITE setLogoGlow NOTIFY logoGlowChanged)
|
Q_PROPERTY(qreal logoGlow READ logoGlow WRITE setLogoGlow NOTIFY logoGlowChanged)
|
||||||
|
|
@ -185,6 +189,54 @@ public:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QColor glowColor() const
|
||||||
|
{
|
||||||
|
return m_glowColor;
|
||||||
|
}
|
||||||
|
void setGlowColor(const QColor &c)
|
||||||
|
{
|
||||||
|
if (c != m_glowColor) {
|
||||||
|
m_glowColor = c;
|
||||||
|
emit glowColorChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QColor brandStrong() const
|
||||||
|
{
|
||||||
|
return m_brandStrong;
|
||||||
|
}
|
||||||
|
void setBrandStrong(const QColor &c)
|
||||||
|
{
|
||||||
|
if (c != m_brandStrong) {
|
||||||
|
m_brandStrong = c;
|
||||||
|
emit brandStrongChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QColor brandSoft() const
|
||||||
|
{
|
||||||
|
return m_brandSoft;
|
||||||
|
}
|
||||||
|
void setBrandSoft(const QColor &c)
|
||||||
|
{
|
||||||
|
if (c != m_brandSoft) {
|
||||||
|
m_brandSoft = c;
|
||||||
|
emit brandSoftChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
qreal vignetteMin() const
|
||||||
|
{
|
||||||
|
return m_vignetteMin;
|
||||||
|
}
|
||||||
|
void setVignetteMin(qreal v)
|
||||||
|
{
|
||||||
|
if (v != m_vignetteMin) {
|
||||||
|
m_vignetteMin = v;
|
||||||
|
emit vignetteMinChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
bool logoVisible() const
|
bool logoVisible() const
|
||||||
{
|
{
|
||||||
return m_logoVisible;
|
return m_logoVisible;
|
||||||
|
|
@ -222,6 +274,10 @@ signals:
|
||||||
void colorAChanged();
|
void colorAChanged();
|
||||||
void colorBChanged();
|
void colorBChanged();
|
||||||
void accentChanged();
|
void accentChanged();
|
||||||
|
void glowColorChanged();
|
||||||
|
void brandStrongChanged();
|
||||||
|
void brandSoftChanged();
|
||||||
|
void vignetteMinChanged();
|
||||||
void logoVisibleChanged();
|
void logoVisibleChanged();
|
||||||
void logoGlowChanged();
|
void logoGlowChanged();
|
||||||
|
|
||||||
|
|
@ -239,9 +295,16 @@ private:
|
||||||
|
|
||||||
bool m_frontIsA = true;
|
bool m_frontIsA = true;
|
||||||
|
|
||||||
|
// Curated fallback seed values -- BannerHost overwrites these with
|
||||||
|
// palette-derived colours (see shader_banner_widget.cpp) before the first
|
||||||
|
// paint, so they only matter as a safe pre-first-apply default.
|
||||||
QColor m_colorA{0x1A, 0x1A, 0x20};
|
QColor m_colorA{0x1A, 0x1A, 0x20};
|
||||||
QColor m_colorB{0x0E, 0x0E, 0x12};
|
QColor m_colorB{0x0E, 0x0E, 0x12};
|
||||||
QColor m_accent{0x8B, 0xDD, 0x6B};
|
QColor m_accent{0x8B, 0xDD, 0x6B};
|
||||||
|
QColor m_glowColor{Qt::white};
|
||||||
|
QColor m_brandStrong{0x13, 0x97, 0x40};
|
||||||
|
QColor m_brandSoft{0xC9, 0xFD, 0x62};
|
||||||
|
qreal m_vignetteMin = 0.62;
|
||||||
|
|
||||||
bool m_logoVisible = false;
|
bool m_logoVisible = false;
|
||||||
qreal m_logoGlow = 1.0;
|
qreal m_logoGlow = 1.0;
|
||||||
|
|
|
||||||
15
cockatrice/src/interface/widgets/onboarding/brand_colors.h
Normal file
15
cockatrice/src/interface/widgets/onboarding/brand_colors.h
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
#ifndef BRAND_COLORS_H
|
||||||
|
#define BRAND_COLORS_H
|
||||||
|
|
||||||
|
#include <QColor>
|
||||||
|
|
||||||
|
/** @brief Cockatrice brand green.
|
||||||
|
*
|
||||||
|
* Single source of truth for the onboarding brand accent: it backs the
|
||||||
|
* banner's shader-accent uniform as the curated fallback when the active
|
||||||
|
* palette resolves no usable Highlight, and it preseads the wizard's
|
||||||
|
* QuickSetupPanel so a freshly generated palette keeps the brand identity
|
||||||
|
* until the user picks their own look. */
|
||||||
|
inline const QColor kCockatriceBrandGreen(0x8B, 0xDD, 0x6B);
|
||||||
|
|
||||||
|
#endif // BRAND_COLORS_H
|
||||||
|
|
@ -182,6 +182,13 @@ void FirstRunWizard::onCardDatabaseUpdateFinished(bool success)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void FirstRunWizard::onCardDatabaseUpdateProgress(const QString &stage, qint64 done, qint64 total)
|
||||||
|
{
|
||||||
|
if (cardDatabasePage) {
|
||||||
|
cardDatabasePage->onUpdateProgress(stage, done, total);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void FirstRunWizard::finish()
|
void FirstRunWizard::finish()
|
||||||
{
|
{
|
||||||
accept();
|
accept();
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,9 @@ public slots:
|
||||||
/** @brief Forwarded from MainWindow once the background card database update process exits. */
|
/** @brief Forwarded from MainWindow once the background card database update process exits. */
|
||||||
void onCardDatabaseUpdateFinished(bool success);
|
void onCardDatabaseUpdateFinished(bool success);
|
||||||
|
|
||||||
|
/** @brief Forwarded from MainWindow while the background card database update process runs. */
|
||||||
|
void onCardDatabaseUpdateProgress(const QString &stage, qint64 done, qint64 total);
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
void closeEvent(QCloseEvent *event) override;
|
void closeEvent(QCloseEvent *event) override;
|
||||||
void changeEvent(QEvent *event) override;
|
void changeEvent(QEvent *event) override;
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@
|
||||||
#include <QTimer>
|
#include <QTimer>
|
||||||
#include <QUrl>
|
#include <QUrl>
|
||||||
#include <QVBoxLayout>
|
#include <QVBoxLayout>
|
||||||
|
#include <climits>
|
||||||
#include <libcockatrice/card/database/card_database_manager.h>
|
#include <libcockatrice/card/database/card_database_manager.h>
|
||||||
#include <libcockatrice/settings/updates_settings.h>
|
#include <libcockatrice/settings/updates_settings.h>
|
||||||
|
|
||||||
|
|
@ -179,6 +180,25 @@ void CardDatabaseSetupPage::onUpdateFinished(bool success)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void CardDatabaseSetupPage::onUpdateProgress(const QString &stage, qint64 done, qint64 total)
|
||||||
|
{
|
||||||
|
if (state != State::Running) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
progressBar->setRange(0, total > 0 ? static_cast<int>(qMin<qint64>(total, INT_MAX)) : 0);
|
||||||
|
progressBar->setValue(static_cast<int>(qMin<qint64>(done, INT_MAX)));
|
||||||
|
if (total > 0) {
|
||||||
|
const int percent = static_cast<int>((100.0 * done) / total);
|
||||||
|
if (stage == QLatin1String("download")) {
|
||||||
|
statusLabel->setText(tr("Downloading the card database (%1%)…").arg(percent));
|
||||||
|
} else if (stage == QLatin1String("scan")) {
|
||||||
|
statusLabel->setText(tr("Parsing the card database (%1%)…").arg(percent));
|
||||||
|
} else if (stage == QLatin1String("import")) {
|
||||||
|
statusLabel->setText(tr("Importing cards (%1%)…").arg(percent));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
QString CardDatabaseSetupPage::nextButtonText() const
|
QString CardDatabaseSetupPage::nextButtonText() const
|
||||||
{
|
{
|
||||||
return state == State::NotStarted ? tr("Download") : QString();
|
return state == State::NotStarted ? tr("Download") : QString();
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ public:
|
||||||
void retranslateUi() override;
|
void retranslateUi() override;
|
||||||
|
|
||||||
void onUpdateFinished(bool success);
|
void onUpdateFinished(bool success);
|
||||||
|
void onUpdateProgress(const QString &stage, qint64 done, qint64 total);
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void updateRequested();
|
void updateRequested();
|
||||||
|
|
|
||||||
|
|
@ -14,10 +14,31 @@
|
||||||
#include <QGroupBox>
|
#include <QGroupBox>
|
||||||
#include <QLabel>
|
#include <QLabel>
|
||||||
#include <QMessageBox>
|
#include <QMessageBox>
|
||||||
|
#include <QPalette>
|
||||||
#include <QVBoxLayout>
|
#include <QVBoxLayout>
|
||||||
#include <libcockatrice/settings/paths_settings.h>
|
#include <libcockatrice/settings/paths_settings.h>
|
||||||
#include <libcockatrice/settings/personal_settings.h>
|
#include <libcockatrice/settings/personal_settings.h>
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
/** @brief A theme's shipped identity accent, immune to any user- or auto-
|
||||||
|
* generated palette that may currently be masking appColor(). */
|
||||||
|
QColor themeIdentityAccent(const QString &themeDirPath, const QString &themeName)
|
||||||
|
{
|
||||||
|
for (const QString &scheme : {QStringLiteral("Light"), QStringLiteral("Dark")}) {
|
||||||
|
const PaletteConfig cfg = ThemeManager::loadDefaultPaletteConfig(themeDirPath, themeName, scheme);
|
||||||
|
if (cfg.appColors.contains(AppColor::AccentStrong)) {
|
||||||
|
return cfg.appColors.value(AppColor::AccentStrong);
|
||||||
|
}
|
||||||
|
const QColor highlight = cfg.colors.value(QPalette::Active).value(QPalette::Highlight);
|
||||||
|
if (highlight.isValid()) {
|
||||||
|
return highlight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
ThemeSetupPage::ThemeSetupPage(QWidget *parent) : FirstRunWizardPage(parent)
|
ThemeSetupPage::ThemeSetupPage(QWidget *parent) : FirstRunWizardPage(parent)
|
||||||
{
|
{
|
||||||
themeCombo = new QComboBox(this);
|
themeCombo = new QComboBox(this);
|
||||||
|
|
@ -30,6 +51,15 @@ ThemeSetupPage::ThemeSetupPage(QWidget *parent) : FirstRunWizardPage(parent)
|
||||||
|
|
||||||
quickSetupPanel = new QuickSetupPanel(this);
|
quickSetupPanel = new QuickSetupPanel(this);
|
||||||
|
|
||||||
|
// Seed the picker from the current theme's own identity accent rather than
|
||||||
|
// a hardcoded brand green: Plasma seeds violet, Fusion green, etc., and it
|
||||||
|
// is immune to stale generated palettes that may mask appColor(). This was
|
||||||
|
// initially a brand-green workaround from before Fusion became the default.
|
||||||
|
// setAccentColor blocks signals, so this never triggers a generation.
|
||||||
|
lastSeededTheme = SettingsCache::instance().getThemeName();
|
||||||
|
quickSetupPanel->setAccentColor(
|
||||||
|
themeIdentityAccent(themeManager->getAvailableThemes().value(lastSeededTheme), lastSeededTheme));
|
||||||
|
|
||||||
connect(themeCombo, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &ThemeSetupPage::onThemeChanged);
|
connect(themeCombo, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &ThemeSetupPage::onThemeChanged);
|
||||||
connect(schemeCombo, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &ThemeSetupPage::onSchemeChanged);
|
connect(schemeCombo, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &ThemeSetupPage::onSchemeChanged);
|
||||||
connect(quickSetupPanel, &QuickSetupPanel::valueChanged, this, &ThemeSetupPage::onGenerateFromAccent);
|
connect(quickSetupPanel, &QuickSetupPanel::valueChanged, this, &ThemeSetupPage::onGenerateFromAccent);
|
||||||
|
|
@ -47,7 +77,8 @@ ThemeSetupPage::ThemeSetupPage(QWidget *parent) : FirstRunWizardPage(parent)
|
||||||
// Mirrors AppearanceSettingsPage's identical listener for the combo-sync
|
// Mirrors AppearanceSettingsPage's identical listener for the combo-sync
|
||||||
// half of this.
|
// half of this.
|
||||||
connect(themeManager, &ThemeManager::themeChanged, this, [this] {
|
connect(themeManager, &ThemeManager::themeChanged, this, [this] {
|
||||||
const QString newDir = themeManager->getAvailableThemes().value(SettingsCache::instance().getThemeName());
|
const QString newTheme = SettingsCache::instance().getThemeName();
|
||||||
|
const QString newDir = themeManager->getAvailableThemes().value(newTheme);
|
||||||
const ThemeConfig cfg = ThemeConfig::fromThemeDir(newDir);
|
const ThemeConfig cfg = ThemeConfig::fromThemeDir(newDir);
|
||||||
const QString current = cfg.colorScheme;
|
const QString current = cfg.colorScheme;
|
||||||
|
|
||||||
|
|
@ -56,6 +87,14 @@ ThemeSetupPage::ThemeSetupPage(QWidget *parent) : FirstRunWizardPage(parent)
|
||||||
schemeCombo->setCurrentIndex(idx >= 0 ? idx : 0);
|
schemeCombo->setCurrentIndex(idx >= 0 ? idx : 0);
|
||||||
schemeCombo->blockSignals(false);
|
schemeCombo->blockSignals(false);
|
||||||
|
|
||||||
|
// Keep the picker's accent in step with the theme's own identity; the
|
||||||
|
// swatch seeded at construction would otherwise stay stale (e.g. green
|
||||||
|
// from a previous theme) when the user toggles themes.
|
||||||
|
if (newTheme != lastSeededTheme) {
|
||||||
|
lastSeededTheme = newTheme;
|
||||||
|
quickSetupPanel->setAccentColor(themeIdentityAccent(newDir, newTheme));
|
||||||
|
}
|
||||||
|
|
||||||
maybeAutoGeneratePalette();
|
maybeAutoGeneratePalette();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -158,8 +197,14 @@ void ThemeSetupPage::maybeAutoGeneratePalette()
|
||||||
const QString dirPath = themeManager->getAvailableThemes().value(SettingsCache::instance().getThemeName());
|
const QString dirPath = themeManager->getAvailableThemes().value(SettingsCache::instance().getThemeName());
|
||||||
const QString scheme = resolvedScheme();
|
const QString scheme = resolvedScheme();
|
||||||
|
|
||||||
|
// The theme dir may resolve to the user profile even for built-in themes
|
||||||
|
// (getAvailableThemes gives the user copy precedence), so consult the
|
||||||
|
// shipped palette too -- both via loadDefaultPaletteConfig's system fallback.
|
||||||
|
// Without it, scheme flips regenerate a fresh palette from the picker accent
|
||||||
|
// and clobber the curated colours the theme explicitly ships.
|
||||||
if (PaletteConfig::fromScheme(dirPath, scheme).hasPalette() ||
|
if (PaletteConfig::fromScheme(dirPath, scheme).hasPalette() ||
|
||||||
PaletteConfig::fromDefault(dirPath, scheme).hasPalette()) {
|
ThemeManager::loadDefaultPaletteConfig(dirPath, SettingsCache::instance().getThemeName(), scheme)
|
||||||
|
.hasPalette()) {
|
||||||
return; // theme already has something real to show -- leave it alone
|
return; // theme already has something real to show -- leave it alone
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,9 @@ private:
|
||||||
QComboBox *homeTabBackgroundCombo;
|
QComboBox *homeTabBackgroundCombo;
|
||||||
|
|
||||||
bool paletteDirty = false;
|
bool paletteDirty = false;
|
||||||
|
|
||||||
|
/// Theme whose identity accent currently seeds the picker.
|
||||||
|
QString lastSeededTheme;
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // THEME_SETUP_PAGE_H
|
#endif // THEME_SETUP_PAGE_H
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,8 @@ Item {
|
||||||
property vector4d uColorA: Qt.vector4d(bannerConfig.colorA.r, bannerConfig.colorA.g, bannerConfig.colorA.b, 1.0)
|
property vector4d uColorA: Qt.vector4d(bannerConfig.colorA.r, bannerConfig.colorA.g, bannerConfig.colorA.b, 1.0)
|
||||||
property vector4d uColorB: Qt.vector4d(bannerConfig.colorB.r, bannerConfig.colorB.g, bannerConfig.colorB.b, 1.0)
|
property vector4d uColorB: Qt.vector4d(bannerConfig.colorB.r, bannerConfig.colorB.g, bannerConfig.colorB.b, 1.0)
|
||||||
property vector4d uAccent: Qt.vector4d(bannerConfig.accent.r, bannerConfig.accent.g, bannerConfig.accent.b, 1.0)
|
property vector4d uAccent: Qt.vector4d(bannerConfig.accent.r, bannerConfig.accent.g, bannerConfig.accent.b, 1.0)
|
||||||
|
property vector4d uGlowColor: Qt.vector4d(bannerConfig.glowColor.r, bannerConfig.glowColor.g, bannerConfig.glowColor.b, 1.0)
|
||||||
|
property real uVignetteMin: bannerConfig.vignetteMin
|
||||||
property real uLogoGlow: bannerConfig.logoGlow
|
property real uLogoGlow: bannerConfig.logoGlow
|
||||||
fragmentShader: "qrc:/onboarding/shaders/brand_banner.frag.qsb"
|
fragmentShader: "qrc:/onboarding/shaders/brand_banner.frag.qsb"
|
||||||
}
|
}
|
||||||
|
|
@ -33,30 +35,62 @@ Item {
|
||||||
property vector4d uColorA: Qt.vector4d(bannerConfig.colorA.r, bannerConfig.colorA.g, bannerConfig.colorA.b, 1.0)
|
property vector4d uColorA: Qt.vector4d(bannerConfig.colorA.r, bannerConfig.colorA.g, bannerConfig.colorA.b, 1.0)
|
||||||
property vector4d uColorB: Qt.vector4d(bannerConfig.colorB.r, bannerConfig.colorB.g, bannerConfig.colorB.b, 1.0)
|
property vector4d uColorB: Qt.vector4d(bannerConfig.colorB.r, bannerConfig.colorB.g, bannerConfig.colorB.b, 1.0)
|
||||||
property vector4d uAccent: Qt.vector4d(bannerConfig.accent.r, bannerConfig.accent.g, bannerConfig.accent.b, 1.0)
|
property vector4d uAccent: Qt.vector4d(bannerConfig.accent.r, bannerConfig.accent.g, bannerConfig.accent.b, 1.0)
|
||||||
|
property vector4d uGlowColor: Qt.vector4d(bannerConfig.glowColor.r, bannerConfig.glowColor.g, bannerConfig.glowColor.b, 1.0)
|
||||||
|
property real uVignetteMin: bannerConfig.vignetteMin
|
||||||
property real uLogoGlow: bannerConfig.logoGlow
|
property real uLogoGlow: bannerConfig.logoGlow
|
||||||
fragmentShader: "qrc:/onboarding/shaders/brand_banner.frag.qsb"
|
fragmentShader: "qrc:/onboarding/shaders/brand_banner.frag.qsb"
|
||||||
}
|
}
|
||||||
|
|
||||||
// The hero logo itself — breathes cleanly over a 0.5–1.0 opacity range
|
// The white logo sits at full opacity on top of the static gradient plate —
|
||||||
Image {
|
// no glow, no breathing. The plate matches home_widget's QPainter composite.
|
||||||
id: logo
|
Item {
|
||||||
anchors.centerIn: parent
|
id: logoHost
|
||||||
visible: bannerConfig.logoVisible
|
visible: bannerConfig.logoVisible
|
||||||
source: "qrc:/resources/cockatrice-logo-white.svg"
|
anchors.centerIn: parent
|
||||||
width: root.height * 0.6
|
width: root.height * 0.6
|
||||||
height: width * (sourceSize.height > 0 ? sourceSize.height / Math.max(sourceSize.width, 1) : 1)
|
height: width
|
||||||
fillMode: Image.PreserveAspectFit
|
|
||||||
smooth: true
|
|
||||||
opacity: 0.5 + 0.5 * bannerConfig.logoGlow
|
|
||||||
sourceSize: Qt.size(256, 256)
|
|
||||||
|
|
||||||
Behavior on opacity { NumberAnimation { duration: 300; easing.type: Easing.InOutSine } }
|
// The full-color logo renders beneath the white mark and is consumed as
|
||||||
|
// a texture (layer.enabled) by the plate shader's silhouette mask, so
|
||||||
|
// the gradient is clipped to the bird exactly as the SVG's gradient
|
||||||
|
// paths are. It is never drawn to the screen itself.
|
||||||
|
Image {
|
||||||
|
id: silhouetteMask
|
||||||
|
anchors.fill: parent
|
||||||
|
source: "qrc:/resources/cockatrice.svg"
|
||||||
|
sourceSize: Qt.size(256, 256)
|
||||||
|
fillMode: Image.PreserveAspectFit
|
||||||
|
smooth: true
|
||||||
|
visible: false
|
||||||
|
layer.enabled: true
|
||||||
|
layer.smooth: true
|
||||||
|
}
|
||||||
|
|
||||||
transform: Scale {
|
// The logo's gradient plate, drawn behind the mark: a linear
|
||||||
origin.x: logo.width / 2
|
// AccentSoft (light) -> AccentStrong (dark) sheet along the same
|
||||||
origin.y: logo.height / 2
|
// top-left -> bottom-right userSpaceOnUse axis the baked-in SVG used,
|
||||||
xScale: 0.94 + 0.06 * bannerConfig.logoGlow
|
// clipped to the bird silhouette via uSilhouette. The white highlight
|
||||||
yScale: 0.94 + 0.06 * bannerConfig.logoGlow
|
// path above is theme independent. Sized to the logo itself — no
|
||||||
|
// rounded badge, matching home_widget's QPainter composite. Static.
|
||||||
|
// Small ShaderEffect, Qt 6.4-safe.
|
||||||
|
ShaderEffect {
|
||||||
|
id: brandPlate
|
||||||
|
anchors.fill: parent
|
||||||
|
property vector4d uStrong: Qt.vector4d(bannerConfig.brandStrong.r, bannerConfig.brandStrong.g,
|
||||||
|
bannerConfig.brandStrong.b, 1.0)
|
||||||
|
property vector4d uSoft: Qt.vector4d(bannerConfig.brandSoft.r, bannerConfig.brandSoft.g,
|
||||||
|
bannerConfig.brandSoft.b, 1.0)
|
||||||
|
property var uSilhouette: silhouetteMask
|
||||||
|
fragmentShader: "qrc:/onboarding/shaders/brand_plate.frag.qsb"
|
||||||
|
}
|
||||||
|
|
||||||
|
Image {
|
||||||
|
id: logoImage
|
||||||
|
anchors.fill: parent
|
||||||
|
source: "qrc:/resources/cockatrice-logo-white.svg"
|
||||||
|
sourceSize: Qt.size(256, 256)
|
||||||
|
fillMode: Image.PreserveAspectFit
|
||||||
|
smooth: true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,7 +1,10 @@
|
||||||
#include "shader_banner_widget.h"
|
#include "shader_banner_widget.h"
|
||||||
|
|
||||||
|
#include "../../theme_manager.h"
|
||||||
#include "banner_shader_config.h"
|
#include "banner_shader_config.h"
|
||||||
|
#include "brand_colors.h"
|
||||||
|
|
||||||
|
#include <QApplication>
|
||||||
#include <QPainter>
|
#include <QPainter>
|
||||||
#include <QQmlContext>
|
#include <QQmlContext>
|
||||||
#include <QQmlEngine>
|
#include <QQmlEngine>
|
||||||
|
|
@ -11,11 +14,98 @@
|
||||||
|
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
// Near-black base palette -- the background is dark and quiet so the green
|
// Curated near-black stage -- used only when the active palette resolves no
|
||||||
// accent stands out.
|
// usable window colour. Matches the banner's original design (dark and quiet
|
||||||
constexpr QRgb kColorA = 0x1A1A20;
|
// so the accent stands out) and satisfies design-plans §2.1's "identity
|
||||||
constexpr QRgb kColorB = 0x0E0E12;
|
// survives a bare palette".
|
||||||
constexpr QRgb kAccent = 0x8BDD6B;
|
constexpr QRgb kFallbackColorA = 0x1A1A20;
|
||||||
|
constexpr QRgb kFallbackColorB = 0x0E0E12;
|
||||||
|
|
||||||
|
struct SuggestedColors
|
||||||
|
{
|
||||||
|
QColor colorA;
|
||||||
|
QColor colorB;
|
||||||
|
QColor accent;
|
||||||
|
QColor glowColor;
|
||||||
|
QColor brandStrong;
|
||||||
|
QColor brandSoft;
|
||||||
|
qreal vignetteMin = 0.62;
|
||||||
|
};
|
||||||
|
|
||||||
|
SuggestedColors suggestedBannerColors()
|
||||||
|
{
|
||||||
|
const QPalette &pal = qApp->palette();
|
||||||
|
const QColor window = pal.color(QPalette::Active, QPalette::Window);
|
||||||
|
// Identity accent: the theme's [AppColors] AccentStrong, which appColor()
|
||||||
|
// resolves to QPalette::Highlight when a theme doesn't pin AccentStrong.
|
||||||
|
// Reading bare Highlight ignored curated accent tokens (Plasma's violet
|
||||||
|
// vs Default's green) whenever a palette didn't set the role itself.
|
||||||
|
const QColor accentStrong = themeManager->appColor(AppColor::AccentStrong);
|
||||||
|
if (!window.isValid() || !accentStrong.isValid()) {
|
||||||
|
return {QColor(kFallbackColorA),
|
||||||
|
QColor(kFallbackColorB),
|
||||||
|
kCockatriceBrandGreen,
|
||||||
|
QColor(Qt::white),
|
||||||
|
kCockatriceBrandGreen,
|
||||||
|
QColor(0xC9, 0xFD, 0x62),
|
||||||
|
0.62};
|
||||||
|
}
|
||||||
|
|
||||||
|
// The theme's brand pair: AccentStrong is the deep green, AccentSoft the
|
||||||
|
// lime. These two appColors form the logo's "surrounding gradient" (deep
|
||||||
|
// core grading out to the soft, brand-toned glow) on both the banner and
|
||||||
|
// the home screen.
|
||||||
|
const QColor brandStrong = accentStrong;
|
||||||
|
const QColor brandSoft = themeManager->appColor(AppColor::AccentSoft);
|
||||||
|
|
||||||
|
// Dress the stage for the scheme so the banner never fights the
|
||||||
|
// surrounding window in either mode. Dark palettes keep the original
|
||||||
|
// quiet near-black stage (lightness 29 → 16) with the theme's window
|
||||||
|
// hue; light palettes get a pastel "frosted accent" treatment built from
|
||||||
|
// the accent hue instead of a plain near-white copy: a coloured wash that
|
||||||
|
// clearly belongs to the theme.
|
||||||
|
const qreal luma = 0.299 * window.red() + 0.587 * window.green() + 0.114 * window.blue();
|
||||||
|
const bool lightStage = luma > 115.0;
|
||||||
|
if (lightStage) {
|
||||||
|
const int hue = accentStrong.hslHue();
|
||||||
|
// Achromatic accents (grey) get a neutral near-white stage instead.
|
||||||
|
const int stageSat = hue < 0 ? 0 : 64;
|
||||||
|
const int hueSafe = hue < 0 ? 0 : hue;
|
||||||
|
// Depth is what stops a light stage reading as a washed-out near-white
|
||||||
|
// copy of the page behind the banner: deepen the lower pastel band and
|
||||||
|
// raise saturation so the hue is clearly present while staying frosted.
|
||||||
|
auto pastel = [hueSafe, stageSat](int lightness) { return QColor::fromHsl(hueSafe, stageSat, lightness); };
|
||||||
|
auto pastelLower = [hueSafe](int lightness) { return QColor::fromHsl(hueSafe, 76, lightness); };
|
||||||
|
// Brightness-lifted accent for additive glows: the raw accent on a
|
||||||
|
// light stage must be mid-bright to read instead of washing out, so
|
||||||
|
// lift lightness and saturation together.
|
||||||
|
const int accentLightness = qBound(158, accentStrong.lightness() + 82, 198);
|
||||||
|
const int accentSaturation = hue < 0 ? 0 : qMax(accentStrong.hslSaturation(), 180);
|
||||||
|
const QColor liftedAccent =
|
||||||
|
hue < 0 ? accentStrong : QColor::fromHsl(hueSafe, accentSaturation, accentLightness);
|
||||||
|
// The centre glow (and logo tint in QML) uses the deep accent itself:
|
||||||
|
// a coloured halo/fill behind the logo instead of a white or black one.
|
||||||
|
return {pastel(214), pastelLower(186), liftedAccent, accentStrong, brandStrong, brandSoft, 0.80};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dark stage: force the window hue down to the banner's curated darkness,
|
||||||
|
// scaling saturation away so chromatic palettes tint it without going
|
||||||
|
// muddy. The accent is the bright, brand-driven tone (hue from the accent
|
||||||
|
// itself, never the -- often grey -- window), and it drives both the
|
||||||
|
// embers/fog and the logo glow so the mark tints like the light stage.
|
||||||
|
auto stage = [&window](int lightness) {
|
||||||
|
const int hue = window.hslHue();
|
||||||
|
const int saturation = hue < 0 ? 0 : qBound(0, qRound(window.hslSaturation() * (lightness / 40.0)), 255);
|
||||||
|
return QColor::fromHsl(hue, saturation, lightness);
|
||||||
|
};
|
||||||
|
const int accentHue = accentStrong.hslHue();
|
||||||
|
const int accentHueSafe = accentHue < 0 ? 0 : accentHue;
|
||||||
|
const int accentLightness = qBound(150, accentStrong.lightness() + 70, 185);
|
||||||
|
const int accentSaturation = accentHue < 0 ? 0 : qMax(accentStrong.hslSaturation(), 160);
|
||||||
|
const QColor accent =
|
||||||
|
accentHue < 0 ? accentStrong : QColor::fromHsl(accentHueSafe, accentSaturation, accentLightness);
|
||||||
|
return {stage(29), stage(16), accent, accent, brandStrong, brandSoft, 0.62};
|
||||||
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
class GradientFallbackWidget : public QWidget
|
class GradientFallbackWidget : public QWidget
|
||||||
|
|
@ -23,15 +113,27 @@ class GradientFallbackWidget : public QWidget
|
||||||
public:
|
public:
|
||||||
using QWidget::QWidget;
|
using QWidget::QWidget;
|
||||||
|
|
||||||
|
void setColors(const QColor &a, const QColor &b)
|
||||||
|
{
|
||||||
|
if (a != colorA || b != colorB) {
|
||||||
|
colorA = a;
|
||||||
|
colorB = b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
void paintEvent(QPaintEvent *) override
|
void paintEvent(QPaintEvent *) override
|
||||||
{
|
{
|
||||||
QPainter painter(this);
|
QPainter painter(this);
|
||||||
QLinearGradient gradient(0, 0, width(), height());
|
QLinearGradient gradient(0, 0, width(), height());
|
||||||
gradient.setColorAt(0.0, QColor(kColorA));
|
gradient.setColorAt(0.0, colorA);
|
||||||
gradient.setColorAt(1.0, QColor(kColorB));
|
gradient.setColorAt(1.0, colorB);
|
||||||
painter.fillRect(rect(), gradient);
|
painter.fillRect(rect(), gradient);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
QColor colorA{QColor(kFallbackColorA)};
|
||||||
|
QColor colorB{QColor(kFallbackColorB)};
|
||||||
};
|
};
|
||||||
|
|
||||||
BannerHost::BannerHost(QWidget *parent) : QWidget(parent)
|
BannerHost::BannerHost(QWidget *parent) : QWidget(parent)
|
||||||
|
|
@ -62,6 +164,9 @@ BannerHost::BannerHost(QWidget *parent) : QWidget(parent)
|
||||||
connect(&clock, &QTimer::timeout, this, &BannerHost::tick);
|
connect(&clock, &QTimer::timeout, this, &BannerHost::tick);
|
||||||
clock.setInterval(16); // ~60fps; the shader itself is cheap, this is just a wall clock
|
clock.setInterval(16); // ~60fps; the shader itself is cheap, this is just a wall clock
|
||||||
|
|
||||||
|
connect(themeManager, &ThemeManager::themeChanged, this, &BannerHost::applyThemeColors);
|
||||||
|
applyThemeColors();
|
||||||
|
|
||||||
applyMotifPreset(currentMotif);
|
applyMotifPreset(currentMotif);
|
||||||
updateAspect();
|
updateAspect();
|
||||||
}
|
}
|
||||||
|
|
@ -123,9 +228,9 @@ void BannerHost::applyMotifPreset(Motif motif)
|
||||||
|
|
||||||
const Preset p = presetFor(motif);
|
const Preset p = presetFor(motif);
|
||||||
|
|
||||||
config->setColorA(QColor(kColorA));
|
config->setColorA(bannerColorA);
|
||||||
config->setColorB(QColor(kColorB));
|
config->setColorB(bannerColorB);
|
||||||
config->setAccent(QColor(kAccent));
|
config->setAccent(bannerAccent);
|
||||||
config->setLogoVisible(motif == Motif::Welcome);
|
config->setLogoVisible(motif == Motif::Welcome);
|
||||||
|
|
||||||
if (isFirstApply) {
|
if (isFirstApply) {
|
||||||
|
|
@ -183,8 +288,34 @@ void BannerHost::hideEvent(QHideEvent *event)
|
||||||
clock.stop();
|
clock.stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void BannerHost::applyThemeColors()
|
||||||
|
{
|
||||||
|
const SuggestedColors colors = suggestedBannerColors();
|
||||||
|
bannerColorA = colors.colorA;
|
||||||
|
bannerColorB = colors.colorB;
|
||||||
|
bannerAccent = colors.accent;
|
||||||
|
|
||||||
|
if (usingFallback) {
|
||||||
|
fallback->setColors(bannerColorA, bannerColorB);
|
||||||
|
fallback->update();
|
||||||
|
} else if (config) {
|
||||||
|
config->setColorA(bannerColorA);
|
||||||
|
config->setColorB(bannerColorB);
|
||||||
|
config->setAccent(bannerAccent);
|
||||||
|
config->setGlowColor(colors.glowColor);
|
||||||
|
config->setBrandStrong(colors.brandStrong);
|
||||||
|
config->setBrandSoft(colors.brandSoft);
|
||||||
|
config->setVignetteMin(colors.vignetteMin);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void BannerHost::tick()
|
void BannerHost::tick()
|
||||||
{
|
{
|
||||||
|
// Palette previews (e.g. accent drags in the wizard's QuickSetupPanel)
|
||||||
|
// apply qApp->palette() without firing themeChanged, so re-derive here;
|
||||||
|
// BannerShaderConfig's setters are equality-guarded, so this is a no-op
|
||||||
|
// unless the colours actually changed.
|
||||||
|
applyThemeColors();
|
||||||
if (config) {
|
if (config) {
|
||||||
qreal t = elapsed.elapsed() / 1000.0;
|
qreal t = elapsed.elapsed() / 1000.0;
|
||||||
config->setTime(t);
|
config->setTime(t);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
#ifndef SHADER_BANNER_WIDGET_H
|
#ifndef SHADER_BANNER_WIDGET_H
|
||||||
#define SHADER_BANNER_WIDGET_H
|
#define SHADER_BANNER_WIDGET_H
|
||||||
|
|
||||||
|
#include <QColor>
|
||||||
#include <QElapsedTimer>
|
#include <QElapsedTimer>
|
||||||
#include <QTimer>
|
#include <QTimer>
|
||||||
#include <QWidget>
|
#include <QWidget>
|
||||||
|
|
@ -53,6 +54,7 @@ protected:
|
||||||
private slots:
|
private slots:
|
||||||
void tick();
|
void tick();
|
||||||
void onSceneGraphFailed();
|
void onSceneGraphFailed();
|
||||||
|
void applyThemeColors();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
struct Preset
|
struct Preset
|
||||||
|
|
@ -73,6 +75,12 @@ private:
|
||||||
BannerShaderConfig *config = nullptr;
|
BannerShaderConfig *config = nullptr;
|
||||||
GradientFallbackWidget *fallback = nullptr;
|
GradientFallbackWidget *fallback = nullptr;
|
||||||
|
|
||||||
|
// Palette-derived banner colours -- the theme's window hue forced down to
|
||||||
|
// the banner's curated darkness, plus the theme's Highlight as accent.
|
||||||
|
QColor bannerColorA;
|
||||||
|
QColor bannerColorB;
|
||||||
|
QColor bannerAccent;
|
||||||
|
|
||||||
QTimer clock;
|
QTimer clock;
|
||||||
QElapsedTimer elapsed;
|
QElapsedTimer elapsed;
|
||||||
Motif currentMotif = Motif::Welcome;
|
Motif currentMotif = Motif::Welcome;
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,8 @@ layout(std140, binding = 0) uniform buf
|
||||||
vec4 uColorA;
|
vec4 uColorA;
|
||||||
vec4 uColorB;
|
vec4 uColorB;
|
||||||
vec4 uAccent;
|
vec4 uAccent;
|
||||||
|
vec4 uGlowColor;
|
||||||
|
float uVignetteMin;
|
||||||
float uLogoGlow;
|
float uLogoGlow;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -119,7 +121,7 @@ vec3 backgroundField(vec2 uv, float time)
|
||||||
|
|
||||||
// Accent-coloured fog layer: flowNoise peaks above 0.6 contribute accent
|
// Accent-coloured fog layer: flowNoise peaks above 0.6 contribute accent
|
||||||
float fog = flowNoise(uv * 0.8 + vec2(100.0, 50.0), time * 0.02);
|
float fog = flowNoise(uv * 0.8 + vec2(100.0, 50.0), time * 0.02);
|
||||||
col += uAccent.rgb * max(fog - 0.6, 0.0) * 0.10;
|
col += uAccent.rgb * max(fog - 0.6, 0.0) * 0.14;
|
||||||
|
|
||||||
return col;
|
return col;
|
||||||
}
|
}
|
||||||
|
|
@ -136,14 +138,17 @@ vec3 motifWelcome(vec2 uv, vec3 bg, float t)
|
||||||
vec2 center = vec2(asp * 0.5, 0.5);
|
vec2 center = vec2(asp * 0.5, 0.5);
|
||||||
float cDist = length(ac - center);
|
float cDist = length(ac - center);
|
||||||
|
|
||||||
// Centre bloom at logo position; intensity scales with uLogoGlow
|
// Centre bloom at logo position; intensity scales with uLogoGlow. The
|
||||||
|
// QML brandGlow halo now supplies the primary logo surround (the two
|
||||||
|
// brand appColors), so this shader bloom is deliberately kept as a subtle
|
||||||
|
// ambience rather than a competing glow.
|
||||||
float centreLight = bloom(cDist, 0.08 * asp, 0.40 * asp);
|
float centreLight = bloom(cDist, 0.08 * asp, 0.40 * asp);
|
||||||
col += centreLight * 0.20 * uLogoGlow;
|
col += uGlowColor.rgb * centreLight * 0.20 * uLogoGlow;
|
||||||
|
|
||||||
// Flow-noise shimmer gated by Gaussian mask at centre
|
// Flow-noise shimmer gated by Gaussian mask at centre
|
||||||
float shimmer = flowNoise(ac * 0.8 + vec2(55.0, 33.0), t * 0.05) * 0.5 + 0.5;
|
float shimmer = flowNoise(ac * 0.8 + vec2(55.0, 33.0), t * 0.05) * 0.5 + 0.5;
|
||||||
float shimmerMask = exp(-(cDist * cDist) / (0.18 * asp * 0.18 * asp));
|
float shimmerMask = exp(-(cDist * cDist) / (0.18 * asp * 0.18 * asp));
|
||||||
col += shimmer * shimmerMask * 0.04 * uLogoGlow;
|
col += uGlowColor.rgb * shimmer * shimmerMask * 0.04 * uLogoGlow;
|
||||||
|
|
||||||
// 48 ember particles: hash-seeded position, speed, size, brightness.
|
// 48 ember particles: hash-seeded position, speed, size, brightness.
|
||||||
// Embers within a distance threshold of centre are deflected into an
|
// Embers within a distance threshold of centre are deflected into an
|
||||||
|
|
@ -162,8 +167,8 @@ vec3 motifWelcome(vec2 uv, vec3 bg, float t)
|
||||||
float pX = baseX * asp + sin(t * driftFreq + fi * 1.7) * driftAmp * asp;
|
float pX = baseX * asp + sin(t * driftFreq + fi * 1.7) * driftAmp * asp;
|
||||||
float pY = fract(baseY + t * riseSpeed);
|
float pY = fract(baseY + t * riseSpeed);
|
||||||
|
|
||||||
float size = 0.006 + hash21(vec2(fi * 2.9, uSeed * 4.7)) * 0.012;
|
float size = 0.010 + hash21(vec2(fi * 2.9, uSeed * 4.7)) * 0.014;
|
||||||
float bright = 0.15 + hash21(vec2(fi * 6.1, uSeed * 0.9)) * 0.30;
|
float bright = 0.18 + hash21(vec2(fi * 6.1, uSeed * 0.9)) * 0.32;
|
||||||
|
|
||||||
// Fade out near top/bottom edges
|
// Fade out near top/bottom edges
|
||||||
float edgeFade = smoothstep(0.0, 0.12, pY) * smoothstep(1.0, 0.88, pY);
|
float edgeFade = smoothstep(0.0, 0.12, pY) * smoothstep(1.0, 0.88, pY);
|
||||||
|
|
@ -232,7 +237,7 @@ vec3 motifCardDatabase(vec2 uv, vec3 bg, float t)
|
||||||
|
|
||||||
// Semi-transparent dark fill
|
// Semi-transparent dark fill
|
||||||
float fill = smoothstep(0.015, -0.005, d);
|
float fill = smoothstep(0.015, -0.005, d);
|
||||||
col = mix(col, uColorB.rgb * 0.55, fill * 0.50);
|
col = mix(col, uColorB.rgb * 0.60, fill * 0.62);
|
||||||
|
|
||||||
// Accent outline
|
// Accent outline
|
||||||
float edge = smoothstep(0.035, 0.0, abs(d));
|
float edge = smoothstep(0.035, 0.0, abs(d));
|
||||||
|
|
@ -309,7 +314,7 @@ vec3 motifAccount(vec2 uv, vec3 bg, float t)
|
||||||
|
|
||||||
// Node glow via bloom; intensity modulated by pulse
|
// Node glow via bloom; intensity modulated by pulse
|
||||||
float dist = length(ac - pos);
|
float dist = length(ac - pos);
|
||||||
col += uAccent.rgb * bloom(dist, 0.018, 0.08) * mix(0.20, 0.45, pulse);
|
col += uAccent.rgb * bloom(dist, 0.018, 0.08) * mix(0.30, 0.55, pulse);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Edges: connect nodes within a radius threshold
|
// Edges: connect nodes within a radius threshold
|
||||||
|
|
@ -323,19 +328,19 @@ vec3 motifAccount(vec2 uv, vec3 bg, float t)
|
||||||
vec2 ba = nodePos[j] - nodePos[i];
|
vec2 ba = nodePos[j] - nodePos[i];
|
||||||
float h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0);
|
float h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0);
|
||||||
float lineDist = length(pa - ba * h);
|
float lineDist = length(pa - ba * h);
|
||||||
col += uAccent.rgb * smoothstep(0.010, 0.0, lineDist) * strength * 0.10;
|
col += uAccent.rgb * smoothstep(0.010, 0.0, lineDist) * strength * 0.14;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Central bloom at banner centre
|
// Central bloom at banner centre
|
||||||
float cDist = length(ac - center);
|
float cDist = length(ac - center);
|
||||||
col += uAccent.rgb * bloom(cDist, 0.04, 0.25) * 0.12;
|
col += uAccent.rgb * bloom(cDist, 0.04, 0.25) * 0.20;
|
||||||
|
|
||||||
// Periodic expanding ring from centre
|
// Periodic expanding ring from centre
|
||||||
float ripplePhase = t * 0.4;
|
float ripplePhase = t * 0.4;
|
||||||
float rippleDist = abs(cDist - fract(ripplePhase) * asp * 0.7);
|
float rippleDist = abs(cDist - fract(ripplePhase) * asp * 0.7);
|
||||||
col += uAccent.rgb * smoothstep(0.02, 0.0, rippleDist) * 0.10;
|
col += uAccent.rgb * smoothstep(0.02, 0.0, rippleDist) * 0.14;
|
||||||
|
|
||||||
return col;
|
return col;
|
||||||
}
|
}
|
||||||
|
|
@ -456,6 +461,8 @@ void main()
|
||||||
else if (uMode < 4.5) col = motifPreferences(uv, bg, t);
|
else if (uMode < 4.5) col = motifPreferences(uv, bg, t);
|
||||||
else col = motifFinish(uv, bg, t);
|
else col = motifFinish(uv, bg, t);
|
||||||
|
|
||||||
col *= mix(0.62, 1.0, vignette(uv));
|
// Corner vignette; uVignetteMin is scheme-driven (0.62 on dark stages,
|
||||||
|
// gentler on light ones so near-white corners don't go muddy grey).
|
||||||
|
col *= mix(uVignetteMin, 1.0, vignette(uv));
|
||||||
fragColor = vec4(col, 1.0) * qt_Opacity;
|
fragColor = vec4(col, 1.0) * qt_Opacity;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
#version 440
|
||||||
|
|
||||||
|
// The logo's gradient plate, drawn by us rather than the baked-in SVG: a
|
||||||
|
// linear blend between the two brand appColors (light AccentSoft at the
|
||||||
|
// top-left grading to dark AccentStrong at the bottom-right, mirroring
|
||||||
|
// cockatrice.svg's linearGradient4265-7-8 userSpaceOnUse axis), clipped to
|
||||||
|
// the bird's full silhouette via the full-color logo's alpha (uSilhouette).
|
||||||
|
// The white highlight path (cockatrice-logo-white) is overlaid in QML on top,
|
||||||
|
// exactly as the SVG stacks its white path over the gradient paths. Fully
|
||||||
|
// static: no glow, no breathing — the plate just sits there like the home
|
||||||
|
// widget's QPainter composite.
|
||||||
|
|
||||||
|
layout(location = 0) in vec2 qt_TexCoord0;
|
||||||
|
layout(location = 0) out vec4 fragColor;
|
||||||
|
|
||||||
|
layout(std140, binding = 0) uniform buf
|
||||||
|
{
|
||||||
|
mat4 qt_Matrix;
|
||||||
|
float qt_Opacity;
|
||||||
|
vec4 uStrong;
|
||||||
|
vec4 uSoft;
|
||||||
|
};
|
||||||
|
|
||||||
|
// The full-color logo's alpha channel acts as the silhouette mask: the
|
||||||
|
// gradient only appears inside the bird, exactly like the SVG's gradient paths.
|
||||||
|
layout(binding = 1) uniform sampler2D uSilhouette;
|
||||||
|
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
// Recreate cockatrice.svg's own gradient geometry (linearGradient4265-7-8,
|
||||||
|
// userSpaceOnUse): light AccentSoft at the start point S=(-8.097,-97.746),
|
||||||
|
// dark AccentStrong at the end E=(162.455,295.208), on the SVG's 300x300
|
||||||
|
// canvas. Normalized to UV space, V=E-S=(0.5685,1.3098), so
|
||||||
|
// t = dot(uv - S_norm, V)/|V|^2 with S_norm=(-0.0270,-0.3258).
|
||||||
|
float t = clamp(dot(qt_TexCoord0 - vec2(-0.02699, -0.32582), vec2(0.56851, 1.30985)) / 2.03891, 0.0, 1.0);
|
||||||
|
vec3 color = mix(uSoft.rgb, uStrong.rgb, t);
|
||||||
|
|
||||||
|
// Anti-aliased silhouette clip from the full-color logo's alpha.
|
||||||
|
float alpha = texture(uSilhouette, qt_TexCoord0).a;
|
||||||
|
|
||||||
|
fragColor = vec4(color * alpha, alpha) * qt_Opacity;
|
||||||
|
}
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
#include "../../card_picture_loader/card_picture_loader.h"
|
#include "../../card_picture_loader/card_picture_loader.h"
|
||||||
#include "../cards/art_crop_attribution.h"
|
#include "../cards/art_crop_attribution.h"
|
||||||
|
#include "../cards/card_art_utils.h"
|
||||||
#include "../utility/completer_utils.h"
|
#include "../utility/completer_utils.h"
|
||||||
#include "card_database_display_model.h"
|
#include "card_database_display_model.h"
|
||||||
#include "card_database_model.h"
|
#include "card_database_model.h"
|
||||||
|
|
@ -276,7 +277,7 @@ void PlaymatSettingsDialog::reloadPreview()
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
currentPixmap = fullRes;
|
currentPixmap = CardArtUtils::rotateSidewaysLayoutArt(fullRes, card);
|
||||||
preview->setPixmap(currentPixmap);
|
preview->setPixmap(currentPixmap);
|
||||||
preview->setParams(currentParams);
|
preview->setParams(currentParams);
|
||||||
preview->setAttribution(buildArtAttribution(card));
|
preview->setAttribution(buildArtAttribution(card));
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,19 @@
|
||||||
#include "printing_selector_card_overlay_widget.h"
|
#include "printing_selector_card_overlay_widget.h"
|
||||||
|
|
||||||
#include "../../../client/settings/cache_settings.h"
|
#include "../../../client/settings/cache_settings.h"
|
||||||
|
#include "../../card_picture_loader/card_picture_loader.h"
|
||||||
#include "../cards/card_info_picture_widget.h"
|
#include "../cards/card_info_picture_widget.h"
|
||||||
#include "printing_selector_card_display_widget.h"
|
#include "printing_selector_card_display_widget.h"
|
||||||
|
|
||||||
|
#include <QApplication>
|
||||||
|
#include <QFileDialog>
|
||||||
#include <QImageReader>
|
#include <QImageReader>
|
||||||
#include <QLabel>
|
#include <QLabel>
|
||||||
#include <QMenu>
|
#include <QMenu>
|
||||||
|
#include <QMessageBox>
|
||||||
#include <QMouseEvent>
|
#include <QMouseEvent>
|
||||||
|
#include <QPixmapCache>
|
||||||
|
#include <QScreen>
|
||||||
#include <QVBoxLayout>
|
#include <QVBoxLayout>
|
||||||
#include <QtMath>
|
#include <QtMath>
|
||||||
#include <libcockatrice/card/database/card_database_manager.h>
|
#include <libcockatrice/card/database/card_database_manager.h>
|
||||||
|
|
@ -15,6 +21,49 @@
|
||||||
#include <libcockatrice/settings/card_override_settings.h>
|
#include <libcockatrice/settings/card_override_settings.h>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @brief Places the preview beside the highlighted action inside the given screen.
|
||||||
|
*
|
||||||
|
* Side-aware: hugs the side of the action that has room, aligned with its row, then
|
||||||
|
* clamps every edge so the preview always lands fully on-screen on first show.
|
||||||
|
*/
|
||||||
|
QPoint
|
||||||
|
previewPositionNear(const QRect &actionRect, const QSize &labelSize, const QRect &screenGeometry, int previewOffset)
|
||||||
|
{
|
||||||
|
const bool rightFits = actionRect.right() + previewOffset + labelSize.width() <= screenGeometry.right();
|
||||||
|
const bool leftFits = actionRect.left() - previewOffset - labelSize.width() >= screenGeometry.left();
|
||||||
|
|
||||||
|
int x;
|
||||||
|
if (rightFits) {
|
||||||
|
x = actionRect.right() + previewOffset;
|
||||||
|
} else if (leftFits) {
|
||||||
|
x = actionRect.left() - previewOffset - labelSize.width();
|
||||||
|
} else {
|
||||||
|
x = actionRect.left();
|
||||||
|
}
|
||||||
|
x = qMax(screenGeometry.left(), x);
|
||||||
|
x = qMin(screenGeometry.right() - labelSize.width() + 1, x);
|
||||||
|
|
||||||
|
const bool belowFits = actionRect.bottom() + previewOffset + labelSize.height() <= screenGeometry.bottom();
|
||||||
|
const bool aboveFits = actionRect.top() - previewOffset - labelSize.height() >= screenGeometry.top();
|
||||||
|
|
||||||
|
int y;
|
||||||
|
if (belowFits) {
|
||||||
|
y = actionRect.bottom() + previewOffset;
|
||||||
|
} else if (aboveFits) {
|
||||||
|
y = actionRect.top() - previewOffset - labelSize.height();
|
||||||
|
} else {
|
||||||
|
y = actionRect.top();
|
||||||
|
}
|
||||||
|
y = qMax(screenGeometry.top(), y);
|
||||||
|
y = qMin(screenGeometry.bottom() - labelSize.height() + 1, y);
|
||||||
|
|
||||||
|
return {x, y};
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Constructs a PrintingSelectorCardOverlayWidget for displaying a card overlay.
|
* @brief Constructs a PrintingSelectorCardOverlayWidget for displaying a card overlay.
|
||||||
*
|
*
|
||||||
|
|
@ -50,6 +99,31 @@ PrintingSelectorCardOverlayWidget::PrintingSelectorCardOverlayWidget(QWidget *pa
|
||||||
|
|
||||||
initializePinBadge();
|
initializePinBadge();
|
||||||
|
|
||||||
|
// Parent the preview to this overlay so it is destroyed with it (Qt::ToolTip keeps
|
||||||
|
// it a frameless, non-activating top-level window despite the parent).
|
||||||
|
cardOverridePreviewLabel = new QLabel(this, Qt::ToolTip);
|
||||||
|
cardOverridePreviewLabel->setWindowFlag(Qt::FramelessWindowHint);
|
||||||
|
cardOverridePreviewLabel->setAttribute(Qt::WA_ShowWithoutActivating);
|
||||||
|
cardOverridePreviewLabel->setScaledContents(true);
|
||||||
|
cardOverridePreviewLabel->hide();
|
||||||
|
|
||||||
|
// While the preview is visible, keep it honest: when the hovered printing's art
|
||||||
|
// resolves (all alternate printings share the root card's CardInfo), redraw it in place.
|
||||||
|
if (rootCard.getCardPtr()) {
|
||||||
|
connect(rootCard.getCardPtr().data(), &CardInfo::pixmapUpdated, this, [this] {
|
||||||
|
if (cardOverridePreviewLabel->isVisible()) {
|
||||||
|
refreshPreview();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Alt-Tab / app-inactive must not strand the floating preview.
|
||||||
|
connect(qApp, &QGuiApplication::applicationStateChanged, this, [this](Qt::ApplicationState state) {
|
||||||
|
if (state != Qt::ApplicationActive) {
|
||||||
|
hidePreview();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Update when this overlay emits cardPreferenceChanged or when size/scale changes
|
// Update when this overlay emits cardPreferenceChanged or when size/scale changes
|
||||||
connect(this, &PrintingSelectorCardOverlayWidget::cardPreferenceChanged, this,
|
connect(this, &PrintingSelectorCardOverlayWidget::cardPreferenceChanged, this,
|
||||||
&PrintingSelectorCardOverlayWidget::updatePinBadgeVisibility);
|
&PrintingSelectorCardOverlayWidget::updatePinBadgeVisibility);
|
||||||
|
|
@ -185,19 +259,23 @@ void PrintingSelectorCardOverlayWidget::leaveEvent(QEvent *event)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Creates and shows a custom context menu when the right mouse button is clicked.
|
* @brief Creates and shows the card-overlay context menu.
|
||||||
*
|
*
|
||||||
* The context menu includes an option to show related cards, which displays a submenu with actions
|
* The menu includes the card art preference (Pin/Unpin Printing), the Image Overrides
|
||||||
* for each related card. When an action is triggered, the card information is updated, and the
|
* submenu (Load Custom Image, Clear Custom Image, and one entry per alternate printing with a
|
||||||
* printing selector is shown.
|
* live preview), and the Show Related cards submenu.
|
||||||
*
|
*
|
||||||
* @param point The position of the mouse when the right-click occurred.
|
* @param point The local position the menu should pop at.
|
||||||
*/
|
*/
|
||||||
void PrintingSelectorCardOverlayWidget::customMenu(QPoint point)
|
void PrintingSelectorCardOverlayWidget::customMenu(QPoint point)
|
||||||
{
|
{
|
||||||
QMenu menu;
|
QMenu menu;
|
||||||
|
|
||||||
auto *preferenceMenu = new QMenu(tr("Preference"));
|
hidePreview(); // Clear any preview state left over from a previous menu run.
|
||||||
|
|
||||||
|
// Submenus are owned by the stack-allocated top-level menu (addMenu() does not
|
||||||
|
// transfer ownership).
|
||||||
|
auto *preferenceMenu = new QMenu(tr("Preference"), &menu);
|
||||||
menu.addMenu(preferenceMenu);
|
menu.addMenu(preferenceMenu);
|
||||||
|
|
||||||
const auto &preferredProviderId =
|
const auto &preferredProviderId =
|
||||||
|
|
@ -219,8 +297,66 @@ void PrintingSelectorCardOverlayWidget::customMenu(QPoint point)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
menu.addSeparator();
|
||||||
|
|
||||||
|
auto *overrideMenu = new QMenu(tr("Image Overrides"), &menu);
|
||||||
|
|
||||||
|
auto *loadCustomAction = overrideMenu->addAction(tr("Load Custom Image..."));
|
||||||
|
auto *clearOverrideAction = overrideMenu->addAction(tr("Clear Custom Image"));
|
||||||
|
|
||||||
|
// Nothing to clear on a card that has no local override yet.
|
||||||
|
clearOverrideAction->setEnabled(CardPictureLoader::hasLocalOverrides(rootCard));
|
||||||
|
|
||||||
|
overrideMenu->addSeparator();
|
||||||
|
|
||||||
|
const auto &allSets = rootCard.getInfo().getSets();
|
||||||
|
|
||||||
|
for (const auto &set : allSets) {
|
||||||
|
for (const auto &printing : set) {
|
||||||
|
if (printing == rootCard.getPrinting()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The submenu is already scoped to this card, so the rows lead with set +
|
||||||
|
// collector; only printings with a distinct display name add their own name.
|
||||||
|
const CardSetPtr cardSet = printing.getSet();
|
||||||
|
if (!cardSet) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString label = tr("%1 %2").arg(cardSet->getCorrectedShortName(), printing.getProperty("num"));
|
||||||
|
|
||||||
|
auto *action = overrideMenu->addAction(label);
|
||||||
|
|
||||||
|
ExactCard overrideCard(rootCard.getCardPtr(), printing);
|
||||||
|
action->setData(QVariant::fromValue(overrideCard));
|
||||||
|
|
||||||
|
connect(action, &QAction::triggered, this, [this, overrideCard]() {
|
||||||
|
CardPictureLoader::getInstance().installPrintingOverride(rootCard, overrideCard);
|
||||||
|
QPixmapCache::clear();
|
||||||
|
rootCard.emitPixmapUpdated(); // refresh the overlay art in place, like the other paths
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
connect(clearOverrideAction, &QAction::triggered, this, [this]() {
|
||||||
|
CardPictureLoader::deleteAllLocalOverrides(rootCard);
|
||||||
|
QPixmapCache::clear();
|
||||||
|
rootCard.emitPixmapUpdated(); // force UI refresh
|
||||||
|
});
|
||||||
|
|
||||||
|
connect(loadCustomAction, &QAction::triggered, this, &PrintingSelectorCardOverlayWidget::loadCustomImage);
|
||||||
|
|
||||||
|
connect(overrideMenu, &QMenu::hovered, this, &PrintingSelectorCardOverlayWidget::showPreviewForAction);
|
||||||
|
connect(overrideMenu, &QMenu::aboutToHide, this, &PrintingSelectorCardOverlayWidget::hidePreview);
|
||||||
|
connect(overrideMenu, &QMenu::triggered, this, &PrintingSelectorCardOverlayWidget::hidePreview);
|
||||||
|
|
||||||
|
menu.addMenu(overrideMenu);
|
||||||
|
|
||||||
|
menu.addSeparator();
|
||||||
|
|
||||||
// filling out the related cards submenu
|
// filling out the related cards submenu
|
||||||
auto *relatedMenu = new QMenu(tr("Show Related cards"));
|
auto *relatedMenu = new QMenu(tr("Show Related cards"), &menu);
|
||||||
menu.addMenu(relatedMenu);
|
menu.addMenu(relatedMenu);
|
||||||
auto relatedCards = rootCard.getInfo().getAllRelatedCards();
|
auto relatedCards = rootCard.getInfo().getAllRelatedCards();
|
||||||
if (relatedCards.isEmpty()) {
|
if (relatedCards.isEmpty()) {
|
||||||
|
|
@ -235,7 +371,11 @@ void PrintingSelectorCardOverlayWidget::customMenu(QPoint point)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// The preview anchors itself to this popup's global geometry while it is open, so the
|
||||||
|
// pointer must stay valid for the whole exec() and be dropped before the stack unwinds.
|
||||||
|
previewSourceMenu = overrideMenu;
|
||||||
menu.exec(this->mapToGlobal(point));
|
menu.exec(this->mapToGlobal(point));
|
||||||
|
previewSourceMenu = nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -291,3 +431,136 @@ void PrintingSelectorCardOverlayWidget::initializePinBadge()
|
||||||
pinBadge->setVisible(false);
|
pinBadge->setVisible(false);
|
||||||
pinBadge->raise();
|
pinBadge->raise();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Asks for an image file and installs it as the card's custom art.
|
||||||
|
*
|
||||||
|
* Unreadable files answer with a visible warning instead of a silent no-op.
|
||||||
|
*/
|
||||||
|
void PrintingSelectorCardOverlayWidget::loadCustomImage()
|
||||||
|
{
|
||||||
|
QString filePath = QFileDialog::getOpenFileName(this, tr("Select Card Image"), QString(),
|
||||||
|
tr("Images (*.png *.jpg *.jpeg *.webp)"));
|
||||||
|
|
||||||
|
if (filePath.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QPixmap pixmap(filePath);
|
||||||
|
if (pixmap.isNull()) {
|
||||||
|
// No silent paths: a file that cannot be read answers visibly instead of a no-op.
|
||||||
|
QMessageBox::warning(this, tr("Load Custom Image"), tr("The selected file could not be read as an image."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
CardPictureLoader::getInstance().saveCardImageToLocalStorage(rootCard, pixmap, true);
|
||||||
|
|
||||||
|
QPixmapCache::clear();
|
||||||
|
rootCard.emitPixmapUpdated();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Shows the hover preview for a highlighted printing entry in the Image Overrides submenu.
|
||||||
|
*
|
||||||
|
* QMenu::hovered fires on keyboard highlight too, so the preview appears when arrows walk
|
||||||
|
* onto a printing entry, not only under the mouse.
|
||||||
|
*
|
||||||
|
* Non-printing entries (e.g., Load Custom Image, Clear Custom Image) hide the preview.
|
||||||
|
*
|
||||||
|
* @param action The action that was highlighted.
|
||||||
|
*/
|
||||||
|
void PrintingSelectorCardOverlayWidget::showPreviewForAction(QAction *action)
|
||||||
|
{
|
||||||
|
if (!action) {
|
||||||
|
hidePreview();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QVariant data = action->data();
|
||||||
|
|
||||||
|
if (!data.canConvert<ExactCard>()) {
|
||||||
|
hidePreview();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ExactCard previewCard = qvariant_cast<ExactCard>(data);
|
||||||
|
if (previewCard.isEmpty()) {
|
||||||
|
hidePreview();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
hoveredOverrideCard = previewCard;
|
||||||
|
hoveredOverrideAction = action;
|
||||||
|
refreshPreview();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Renders the hover preview for the currently highlighted printing.
|
||||||
|
*
|
||||||
|
* The preview shows the loading placeholder while the art is pending and swaps in the real
|
||||||
|
* art when it resolves. The label is positioned against its already-resized geometry so the
|
||||||
|
* first-ever show at the screen's edges stays fully on-screen.
|
||||||
|
*/
|
||||||
|
void PrintingSelectorCardOverlayWidget::refreshPreview()
|
||||||
|
{
|
||||||
|
if (hoveredOverrideCard.isEmpty()) {
|
||||||
|
hidePreview();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr QSize previewSize(240, 336);
|
||||||
|
constexpr int previewOffset = 20;
|
||||||
|
|
||||||
|
QPixmap pixmap;
|
||||||
|
CardPictureLoader::getPixmap(pixmap, hoveredOverrideCard, previewSize);
|
||||||
|
|
||||||
|
if (pixmap.isNull()) {
|
||||||
|
// Keep the preview honest while loading: show the loading placeholder instead of a void.
|
||||||
|
// Fetch at the logical size and let the label scale it, so the placeholder matches the
|
||||||
|
// real art's footprint rather than doubling on HiDPI displays.
|
||||||
|
CardPictureLoader::getCardBackLoadingInProgressPixmap(pixmap, previewSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
cardOverridePreviewLabel->setPixmap(pixmap);
|
||||||
|
// QPixmap::size() is physical pixels; the label layout must use the device-independent size
|
||||||
|
// so the preview keeps a constant footprint across DPI settings (QScreen geometry is logical).
|
||||||
|
const QSize labelSize = pixmap.deviceIndependentSize().toSize();
|
||||||
|
cardOverridePreviewLabel->resize(labelSize);
|
||||||
|
|
||||||
|
// Anchor the preview to the walked submenu popup rather than QCursor::pos(), which is idle
|
||||||
|
// under keyboard-only operation: a keyboard-highlighted row must preview at the same place as
|
||||||
|
// a hovered one. The mouse path is unchanged in effect — the popup sits under the cursor, so
|
||||||
|
// the preview stays beside the row in both modalities.
|
||||||
|
const QMenu *popup = previewSourceMenu;
|
||||||
|
if (!popup || !popup->isVisible() || !hoveredOverrideAction) {
|
||||||
|
hidePreview();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QRect popupGeometry = popup->geometry();
|
||||||
|
const QRect actionRectLocal = popup->actionGeometry(hoveredOverrideAction);
|
||||||
|
const QRect actionRect(popupGeometry.topLeft() + actionRectLocal.topLeft(), actionRectLocal.size());
|
||||||
|
|
||||||
|
QScreen *screen = QGuiApplication::screenAt(popupGeometry.center());
|
||||||
|
if (!screen) {
|
||||||
|
hidePreview();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const QRect &screenGeometry = screen->geometry();
|
||||||
|
|
||||||
|
cardOverridePreviewLabel->move(previewPositionNear(actionRect, labelSize, screenGeometry, previewOffset));
|
||||||
|
cardOverridePreviewLabel->show();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Hides the hover preview and forgets the currently highlighted printing.
|
||||||
|
*/
|
||||||
|
void PrintingSelectorCardOverlayWidget::hidePreview()
|
||||||
|
{
|
||||||
|
hoveredOverrideCard = ExactCard();
|
||||||
|
hoveredOverrideAction = nullptr;
|
||||||
|
|
||||||
|
if (cardOverridePreviewLabel) {
|
||||||
|
cardOverridePreviewLabel->hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,9 @@
|
||||||
|
|
||||||
#include <libcockatrice/models/deck_list/deck_list_model.h>
|
#include <libcockatrice/models/deck_list/deck_list_model.h>
|
||||||
|
|
||||||
|
class QAction;
|
||||||
|
class QMenu;
|
||||||
|
|
||||||
class PrintingSelectorCardOverlayWidget : public QWidget
|
class PrintingSelectorCardOverlayWidget : public QWidget
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
@ -43,11 +46,19 @@ private slots:
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void initializePinBadge();
|
void initializePinBadge();
|
||||||
|
void loadCustomImage();
|
||||||
|
void showPreviewForAction(QAction *action);
|
||||||
|
void refreshPreview();
|
||||||
|
void hidePreview();
|
||||||
CardInfoPictureWidget *cardInfoPicture;
|
CardInfoPictureWidget *cardInfoPicture;
|
||||||
AllZonesCardAmountWidget *allZonesCardAmountWidget;
|
AllZonesCardAmountWidget *allZonesCardAmountWidget;
|
||||||
QLabel *pinBadge = nullptr;
|
QLabel *pinBadge = nullptr;
|
||||||
AbstractTabDeckEditor *deckEditor;
|
AbstractTabDeckEditor *deckEditor;
|
||||||
ExactCard rootCard;
|
ExactCard rootCard;
|
||||||
|
QLabel *cardOverridePreviewLabel = nullptr;
|
||||||
|
ExactCard hoveredOverrideCard;
|
||||||
|
QMenu *previewSourceMenu = nullptr;
|
||||||
|
QAction *hoveredOverrideAction = nullptr;
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // PRINTING_SELECTOR_CARD_OVERLAY_WIDGET_H
|
#endif // PRINTING_SELECTOR_CARD_OVERLAY_WIDGET_H
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
#include "settings_button_widget.h"
|
#include "settings_button_widget.h"
|
||||||
|
|
||||||
|
#include "../../pixel_map_generator.h"
|
||||||
|
|
||||||
#include <QApplication>
|
#include <QApplication>
|
||||||
#include <QHBoxLayout>
|
#include <QHBoxLayout>
|
||||||
#include <QMouseEvent>
|
#include <QMouseEvent>
|
||||||
|
|
@ -8,7 +10,7 @@
|
||||||
SettingsButtonWidget::SettingsButtonWidget(QWidget *parent)
|
SettingsButtonWidget::SettingsButtonWidget(QWidget *parent)
|
||||||
: QWidget(parent), button(new QToolButton(this)), popup(new SettingsPopupWidget(nullptr))
|
: QWidget(parent), button(new QToolButton(this)), popup(new SettingsPopupWidget(nullptr))
|
||||||
{
|
{
|
||||||
button->setIcon(QPixmap("theme:icons/cogwheel"));
|
button->setIcon(themePixmap(QStringLiteral("icons/cogwheel")));
|
||||||
button->setCheckable(true);
|
button->setCheckable(true);
|
||||||
button->setFixedSize(32, 32);
|
button->setFixedSize(32, 32);
|
||||||
connect(button, &QToolButton::clicked, this, &SettingsButtonWidget::togglePopup);
|
connect(button, &QToolButton::clicked, this, &SettingsButtonWidget::togglePopup);
|
||||||
|
|
|
||||||
|
|
@ -142,8 +142,10 @@ void ReplayManager::processNewEvents(PlaybackMode playbackMode)
|
||||||
}
|
}
|
||||||
|
|
||||||
// backwards skip => always skip tap animation
|
// backwards skip => always skip tap animation
|
||||||
|
// backwards skip => always skip damage animation (battlefield shimmer / life counter flash)
|
||||||
if (playbackMode == BACKWARD_SKIP) {
|
if (playbackMode == BACKWARD_SKIP) {
|
||||||
options |= SKIP_TAP_ANIMATION;
|
options |= SKIP_TAP_ANIMATION;
|
||||||
|
options |= SKIP_DAMAGE_ANIMATION;
|
||||||
}
|
}
|
||||||
|
|
||||||
emit eventReplayed(replay->event_list(currentEvent), options);
|
emit eventReplayed(replay->event_list(currentEvent), options);
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
#include "../../../client/settings/cache_settings.h"
|
#include "../../../client/settings/cache_settings.h"
|
||||||
#include "../../../client/settings/shortcuts_settings.h"
|
#include "../../../client/settings/shortcuts_settings.h"
|
||||||
|
#include "../../pixel_map_generator.h"
|
||||||
#include "../interface/widgets/tabs/tab_game.h"
|
#include "../interface/widgets/tabs/tab_game.h"
|
||||||
#include "replay_manager.h"
|
#include "replay_manager.h"
|
||||||
#include "replay_quick_settings_widget.h"
|
#include "replay_quick_settings_widget.h"
|
||||||
|
|
@ -50,15 +51,15 @@ ReplayWidget::ReplayWidget(QWidget *parent, GameReplay *replay)
|
||||||
replayPlayButton = new QToolButton;
|
replayPlayButton = new QToolButton;
|
||||||
replayPlayButton->setIconSize(QSize(32, 32));
|
replayPlayButton->setIconSize(QSize(32, 32));
|
||||||
QIcon playButtonIcon = QIcon();
|
QIcon playButtonIcon = QIcon();
|
||||||
playButtonIcon.addPixmap(QPixmap("theme:replay/start"), QIcon::Normal, QIcon::Off);
|
playButtonIcon.addPixmap(themePixmap(QStringLiteral("replay/start")), QIcon::Normal, QIcon::Off);
|
||||||
playButtonIcon.addPixmap(QPixmap("theme:replay/pause"), QIcon::Normal, QIcon::On);
|
playButtonIcon.addPixmap(themePixmap(QStringLiteral("replay/pause")), QIcon::Normal, QIcon::On);
|
||||||
replayPlayButton->setIcon(playButtonIcon);
|
replayPlayButton->setIcon(playButtonIcon);
|
||||||
replayPlayButton->setCheckable(true);
|
replayPlayButton->setCheckable(true);
|
||||||
connect(replayPlayButton, &QToolButton::toggled, this, &ReplayWidget::replayPlayButtonToggled);
|
connect(replayPlayButton, &QToolButton::toggled, this, &ReplayWidget::replayPlayButtonToggled);
|
||||||
|
|
||||||
replayFastForwardButton = new QToolButton;
|
replayFastForwardButton = new QToolButton;
|
||||||
replayFastForwardButton->setIconSize(QSize(32, 32));
|
replayFastForwardButton->setIconSize(QSize(32, 32));
|
||||||
replayFastForwardButton->setIcon(QPixmap("theme:replay/fastforward"));
|
replayFastForwardButton->setIcon(themePixmap(QStringLiteral("replay/fastforward")));
|
||||||
replayFastForwardButton->setCheckable(true);
|
replayFastForwardButton->setCheckable(true);
|
||||||
connect(replayFastForwardButton, &QToolButton::toggled, this, &ReplayWidget::updateTimeScaleFactor);
|
connect(replayFastForwardButton, &QToolButton::toggled, this, &ReplayWidget::updateTimeScaleFactor);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
#include "game_selector.h"
|
#include "game_selector.h"
|
||||||
|
|
||||||
|
#include "../../pixel_map_generator.h"
|
||||||
#include "../interface/widgets/dialogs/dlg_create_game.h"
|
#include "../interface/widgets/dialogs/dlg_create_game.h"
|
||||||
#include "../interface/widgets/dialogs/dlg_filter_games.h"
|
#include "../interface/widgets/dialogs/dlg_filter_games.h"
|
||||||
#include "../interface/widgets/tabs/tab_account.h"
|
#include "../interface/widgets/tabs/tab_account.h"
|
||||||
|
|
@ -95,10 +96,10 @@ GameSelector::GameSelector(AbstractClient *_client,
|
||||||
}
|
}
|
||||||
|
|
||||||
filterButton = new QPushButton;
|
filterButton = new QPushButton;
|
||||||
filterButton->setIcon(QPixmap("theme:icons/search"));
|
filterButton->setIcon(themePixmap(QStringLiteral("icons/search")));
|
||||||
connect(filterButton, &QPushButton::clicked, this, &GameSelector::actSetFilter);
|
connect(filterButton, &QPushButton::clicked, this, &GameSelector::actSetFilter);
|
||||||
clearFilterButton = new QPushButton;
|
clearFilterButton = new QPushButton;
|
||||||
clearFilterButton->setIcon(QPixmap("theme:icons/clearsearch"));
|
clearFilterButton->setIcon(themePixmap(QStringLiteral("icons/clearsearch")));
|
||||||
bool filtersSetToDefault = showFilters && gameListProxyModel->areFilterParametersSetToDefaults();
|
bool filtersSetToDefault = showFilters && gameListProxyModel->areFilterParametersSetToDefaults();
|
||||||
clearFilterButton->setEnabled(!filtersSetToDefault);
|
clearFilterButton->setEnabled(!filtersSetToDefault);
|
||||||
connect(clearFilterButton, &QPushButton::clicked, this, &GameSelector::actClearFilter);
|
connect(clearFilterButton, &QPushButton::clicked, this, &GameSelector::actClearFilter);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
#include "remote_replay_list_tree_widget.h"
|
#include "remote_replay_list_tree_widget.h"
|
||||||
|
|
||||||
|
#include "../../../pixel_map_generator.h"
|
||||||
|
|
||||||
#include <QFileIconProvider>
|
#include <QFileIconProvider>
|
||||||
#include <QHeaderView>
|
#include <QHeaderView>
|
||||||
#include <QSortFilterProxyModel>
|
#include <QSortFilterProxyModel>
|
||||||
|
|
@ -37,7 +39,7 @@ RemoteReplayList_TreeModel::RemoteReplayList_TreeModel(AbstractClient *_client,
|
||||||
QFileIconProvider fip;
|
QFileIconProvider fip;
|
||||||
dirIcon = fip.icon(QFileIconProvider::Folder);
|
dirIcon = fip.icon(QFileIconProvider::Folder);
|
||||||
fileIcon = fip.icon(QFileIconProvider::File);
|
fileIcon = fip.icon(QFileIconProvider::File);
|
||||||
lockIcon = QPixmap("theme:icons/lock");
|
lockIcon = themePixmap(QStringLiteral("icons/lock"));
|
||||||
}
|
}
|
||||||
|
|
||||||
RemoteReplayList_TreeModel::~RemoteReplayList_TreeModel()
|
RemoteReplayList_TreeModel::~RemoteReplayList_TreeModel()
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
#include "user_card_art_provider.h"
|
#include "user_card_art_provider.h"
|
||||||
|
|
||||||
#include "../../../card_picture_loader/card_picture_loader.h"
|
#include "../../../card_picture_loader/card_picture_loader.h"
|
||||||
|
#include "../../cards/card_art_utils.h"
|
||||||
|
|
||||||
#include <QPointer>
|
#include <QPointer>
|
||||||
#include <libcockatrice/card/database/card_database_manager.h>
|
#include <libcockatrice/card/database/card_database_manager.h>
|
||||||
|
|
@ -52,16 +53,25 @@ void UserCardArtProvider::requestCardArt(const QString &userName, const QString
|
||||||
processQueue();
|
processQueue();
|
||||||
}
|
}
|
||||||
|
|
||||||
QPixmap UserCardArtProvider::cropCardArt(const QPixmap &fullRes)
|
QPixmap UserCardArtProvider::cropCardArt(const QPixmap &fullRes, const ExactCard &card)
|
||||||
{
|
{
|
||||||
const QSize sz = fullRes.size();
|
QPixmap source = fullRes;
|
||||||
|
|
||||||
|
// Sideways-layout cards (plane, siege/battle, split) store their landscape
|
||||||
|
// artwork rotated 90° inside a portrait frame. Rotate it upright first so
|
||||||
|
// the crop below lands on the horizontal art, mirroring the way
|
||||||
|
// CardInfoPictureWidget displays these cards.
|
||||||
|
const bool landscape = card.getInfo().getUiAttributes().landscapeOrientation;
|
||||||
|
source = CardArtUtils::rotateSidewaysLayoutArt(source, card);
|
||||||
|
|
||||||
|
const QSize sz = source.size();
|
||||||
const int marginX = sz.width() * 0.07;
|
const int marginX = sz.width() * 0.07;
|
||||||
const int topMargin = sz.height() * 0.11;
|
const int topMargin = landscape ? sz.height() * 0.05 : sz.height() * 0.11;
|
||||||
const int bottomMargin = sz.height() * 0.45;
|
const int bottomMargin = landscape ? sz.height() * 0.42 : sz.height() * 0.45;
|
||||||
|
|
||||||
const QRect foilRect(marginX, topMargin, sz.width() - 2 * marginX, sz.height() - topMargin - bottomMargin);
|
const QRect artRect(marginX, topMargin, sz.width() - 2 * marginX, sz.height() - topMargin - bottomMargin);
|
||||||
|
|
||||||
return fullRes.copy(foilRect.intersected(fullRes.rect()));
|
return source.copy(artRect.intersected(source.rect()));
|
||||||
}
|
}
|
||||||
|
|
||||||
void UserCardArtProvider::insertIntoCache(const QString &key, const QPixmap &pixmap)
|
void UserCardArtProvider::insertIntoCache(const QString &key, const QPixmap &pixmap)
|
||||||
|
|
@ -111,7 +121,7 @@ void UserCardArtProvider::processQueue()
|
||||||
|
|
||||||
// Synchronous hit (already loaded/on disk)
|
// Synchronous hit (already loaded/on disk)
|
||||||
if (!fullRes.isNull()) {
|
if (!fullRes.isNull()) {
|
||||||
insertIntoCache(key, cropCardArt(fullRes));
|
insertIntoCache(key, cropCardArt(fullRes, card));
|
||||||
pending.remove(key);
|
pending.remove(key);
|
||||||
|
|
||||||
emit cardArtUpdated(userName);
|
emit cardArtUpdated(userName);
|
||||||
|
|
@ -135,7 +145,7 @@ void UserCardArtProvider::processQueue()
|
||||||
CardPictureLoader::getPixmap(fullRes, card, QSize(745, 1040));
|
CardPictureLoader::getPixmap(fullRes, card, QSize(745, 1040));
|
||||||
|
|
||||||
if (!fullRes.isNull()) {
|
if (!fullRes.isNull()) {
|
||||||
self->insertIntoCache(key, self->cropCardArt(fullRes));
|
self->insertIntoCache(key, self->cropCardArt(fullRes, card));
|
||||||
}
|
}
|
||||||
|
|
||||||
self->pending.remove(key);
|
self->pending.remove(key);
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@
|
||||||
#include <QPixmap>
|
#include <QPixmap>
|
||||||
#include <QQueue>
|
#include <QQueue>
|
||||||
#include <QSet>
|
#include <QSet>
|
||||||
|
#include <libcockatrice/card/printing/exact_card.h>
|
||||||
|
|
||||||
class UserCardArtProvider : public QObject
|
class UserCardArtProvider : public QObject
|
||||||
{
|
{
|
||||||
|
|
@ -16,7 +17,7 @@ public:
|
||||||
|
|
||||||
void requestCardArt(const QString &userName, const QString &cardName, const QString &providerId);
|
void requestCardArt(const QString &userName, const QString &cardName, const QString &providerId);
|
||||||
const QMap<QString, QPixmap> &cache() const;
|
const QMap<QString, QPixmap> &cache() const;
|
||||||
static QPixmap cropCardArt(const QPixmap &fullRes);
|
static QPixmap cropCardArt(const QPixmap &fullRes, const ExactCard &card);
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void cardArtUpdated(const QString &userName);
|
void cardArtUpdated(const QString &userName);
|
||||||
|
|
|
||||||
|
|
@ -560,7 +560,7 @@ void UserCardArtSettingsDialog::reloadPreview()
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
currentPixmap = UserCardArtProvider::cropCardArt(fullRes);
|
currentPixmap = UserCardArtProvider::cropCardArt(fullRes, card);
|
||||||
preview->setPixmap(currentPixmap);
|
preview->setPixmap(currentPixmap);
|
||||||
preview->setParams(currentParams);
|
preview->setParams(currentParams);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,8 @@ UserContextMenu::UserContextMenu(TabSupervisor *_tabSupervisor, QWidget *parent,
|
||||||
aDemoteFromMod = new QAction(QString(), this);
|
aDemoteFromMod = new QAction(QString(), this);
|
||||||
aPromoteToJudge = new QAction(QString(), this);
|
aPromoteToJudge = new QAction(QString(), this);
|
||||||
aDemoteFromJudge = new QAction(QString(), this);
|
aDemoteFromJudge = new QAction(QString(), this);
|
||||||
|
aPromoteToDeveloper = new QAction(QString(), this);
|
||||||
|
aDemoteFromDeveloper = new QAction(QString(), this);
|
||||||
aGetAdminNotes = new QAction(QString(), this);
|
aGetAdminNotes = new QAction(QString(), this);
|
||||||
aInvestigateUser = new QAction(QString(), this);
|
aInvestigateUser = new QAction(QString(), this);
|
||||||
|
|
||||||
|
|
@ -76,6 +78,8 @@ void UserContextMenu::retranslateUi()
|
||||||
aDemoteFromMod->setText(tr("Dem&ote user from moderator"));
|
aDemoteFromMod->setText(tr("Dem&ote user from moderator"));
|
||||||
aPromoteToJudge->setText(tr("Promote user to &judge"));
|
aPromoteToJudge->setText(tr("Promote user to &judge"));
|
||||||
aDemoteFromJudge->setText(tr("Demote user from judge"));
|
aDemoteFromJudge->setText(tr("Demote user from judge"));
|
||||||
|
aPromoteToDeveloper->setText(tr("Promote user to &developer"));
|
||||||
|
aDemoteFromDeveloper->setText(tr("Demote user from de&veloper"));
|
||||||
aGetAdminNotes->setText(tr("View admin notes"));
|
aGetAdminNotes->setText(tr("View admin notes"));
|
||||||
aInvestigateUser->setText(tr("Investigate user"));
|
aInvestigateUser->setText(tr("Investigate user"));
|
||||||
}
|
}
|
||||||
|
|
@ -268,7 +272,7 @@ void UserContextMenu::adjustMod_processUserResponse(const Response &resp, const
|
||||||
const Command_AdjustMod &cmd = commandContainer.admin_command(0).GetExtension(Command_AdjustMod::ext);
|
const Command_AdjustMod &cmd = commandContainer.admin_command(0).GetExtension(Command_AdjustMod::ext);
|
||||||
|
|
||||||
if (resp.response_code() == Response::RespOk) {
|
if (resp.response_code() == Response::RespOk) {
|
||||||
if (cmd.should_be_mod() || cmd.should_be_judge()) {
|
if (cmd.should_be_mod() || cmd.should_be_judge() || cmd.should_be_developer()) {
|
||||||
QMessageBox::information(static_cast<QWidget *>(parent()), tr("Success"),
|
QMessageBox::information(static_cast<QWidget *>(parent()), tr("Success"),
|
||||||
tr("Successfully promoted user."));
|
tr("Successfully promoted user."));
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -276,7 +280,7 @@ void UserContextMenu::adjustMod_processUserResponse(const Response &resp, const
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
if (cmd.should_be_mod() || cmd.should_be_judge()) {
|
if (cmd.should_be_mod() || cmd.should_be_judge() || cmd.should_be_developer()) {
|
||||||
QMessageBox::information(static_cast<QWidget *>(parent()), tr("Failed"), tr("Failed to promote user."));
|
QMessageBox::information(static_cast<QWidget *>(parent()), tr("Failed"), tr("Failed to promote user."));
|
||||||
} else {
|
} else {
|
||||||
QMessageBox::information(static_cast<QWidget *>(parent()), tr("Failed"), tr("Failed to demote user."));
|
QMessageBox::information(static_cast<QWidget *>(parent()), tr("Failed"), tr("Failed to demote user."));
|
||||||
|
|
@ -437,9 +441,18 @@ void UserContextMenu::showContextMenu(const QPoint &pos,
|
||||||
(tabSupervisor->getUserInfo()->user_level() & ServerInfo_User::IsAdmin)) {
|
(tabSupervisor->getUserInfo()->user_level() & ServerInfo_User::IsAdmin)) {
|
||||||
menu->addAction(aPromoteToJudge);
|
menu->addAction(aPromoteToJudge);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (userLevel.testFlag(ServerInfo_User::IsDeveloper) &&
|
||||||
|
(tabSupervisor->getUserInfo()->user_level() & ServerInfo_User::IsAdmin)) {
|
||||||
|
menu->addAction(aDemoteFromDeveloper);
|
||||||
|
|
||||||
|
} else if (userLevel.testFlag(ServerInfo_User::IsRegistered) &&
|
||||||
|
(tabSupervisor->getUserInfo()->user_level() & ServerInfo_User::IsAdmin)) {
|
||||||
|
menu->addAction(aPromoteToDeveloper);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
aDetails->setEnabled(true);
|
aDetails->setEnabled(true);
|
||||||
aChat->setEnabled(anotherUser && online);
|
aChat->setEnabled(anotherUser && online && !userListProxy->isUserIgnored(userName));
|
||||||
aShowGames->setEnabled(online);
|
aShowGames->setEnabled(online);
|
||||||
aReport->setEnabled(anotherUser);
|
aReport->setEnabled(anotherUser);
|
||||||
aAddToBuddyList->setEnabled(anotherUser);
|
aAddToBuddyList->setEnabled(anotherUser);
|
||||||
|
|
@ -455,6 +468,10 @@ void UserContextMenu::showContextMenu(const QPoint &pos,
|
||||||
aInvestigateUser->setEnabled(anotherUser);
|
aInvestigateUser->setEnabled(anotherUser);
|
||||||
aPromoteToMod->setEnabled(anotherUser);
|
aPromoteToMod->setEnabled(anotherUser);
|
||||||
aDemoteFromMod->setEnabled(anotherUser);
|
aDemoteFromMod->setEnabled(anotherUser);
|
||||||
|
aPromoteToJudge->setEnabled(anotherUser);
|
||||||
|
aDemoteFromJudge->setEnabled(anotherUser);
|
||||||
|
aPromoteToDeveloper->setEnabled(anotherUser);
|
||||||
|
aDemoteFromDeveloper->setEnabled(anotherUser);
|
||||||
|
|
||||||
QAction *actionClicked = menu->exec(pos);
|
QAction *actionClicked = menu->exec(pos);
|
||||||
if (actionClicked == nullptr) {
|
if (actionClicked == nullptr) {
|
||||||
|
|
@ -489,6 +506,8 @@ void UserContextMenu::showContextMenu(const QPoint &pos,
|
||||||
execAdjustMod(userName, actionClicked == aPromoteToMod);
|
execAdjustMod(userName, actionClicked == aPromoteToMod);
|
||||||
} else if (actionClicked == aPromoteToJudge || actionClicked == aDemoteFromJudge) {
|
} else if (actionClicked == aPromoteToJudge || actionClicked == aDemoteFromJudge) {
|
||||||
execAdjustJudge(userName, actionClicked == aPromoteToJudge);
|
execAdjustJudge(userName, actionClicked == aPromoteToJudge);
|
||||||
|
} else if (actionClicked == aPromoteToDeveloper || actionClicked == aDemoteFromDeveloper) {
|
||||||
|
execAdjustDeveloper(userName, actionClicked == aPromoteToDeveloper);
|
||||||
} else if (actionClicked == aBanHistory) {
|
} else if (actionClicked == aBanHistory) {
|
||||||
execBanHistory(userName);
|
execBanHistory(userName);
|
||||||
} else if (actionClicked == aWarnUser) {
|
} else if (actionClicked == aWarnUser) {
|
||||||
|
|
@ -606,7 +625,15 @@ void UserContextMenu::execAddToIgnore(const QString &userName)
|
||||||
Command_AddToList cmd;
|
Command_AddToList cmd;
|
||||||
cmd.set_list("ignore");
|
cmd.set_list("ignore");
|
||||||
cmd.set_user_name(userName.toStdString());
|
cmd.set_user_name(userName.toStdString());
|
||||||
client->sendCommand(client->prepareSessionCommand(cmd));
|
PendingCommand *pend = client->prepareSessionCommand(cmd);
|
||||||
|
connect(pend, &PendingCommand::finished, this,
|
||||||
|
[this, userName](const Response &response, const CommandContainer &, const QVariant &) {
|
||||||
|
if (response.response_code() == Response::RespOk) {
|
||||||
|
QMessageBox::information(static_cast<QWidget *>(parent()), tr("Ignore list"),
|
||||||
|
tr("%1 has been added to your ignore list.").arg(userName));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
client->sendCommand(pend);
|
||||||
}
|
}
|
||||||
|
|
||||||
void UserContextMenu::execRemoveFromIgnore(const QString &userName)
|
void UserContextMenu::execRemoveFromIgnore(const QString &userName)
|
||||||
|
|
@ -699,3 +726,13 @@ void UserContextMenu::execAdjustJudge(const QString &userName, bool shouldBeJudg
|
||||||
connect(pend, &PendingCommand::finished, this, &UserContextMenu::adjustMod_processUserResponse);
|
connect(pend, &PendingCommand::finished, this, &UserContextMenu::adjustMod_processUserResponse);
|
||||||
client->sendCommand(pend);
|
client->sendCommand(pend);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void UserContextMenu::execAdjustDeveloper(const QString &userName, bool shouldBeDeveloper)
|
||||||
|
{
|
||||||
|
Command_AdjustMod cmd;
|
||||||
|
cmd.set_user_name(userName.toStdString());
|
||||||
|
cmd.set_should_be_developer(shouldBeDeveloper);
|
||||||
|
PendingCommand *pend = client->prepareAdminCommand(cmd);
|
||||||
|
connect(pend, &PendingCommand::finished, this, &UserContextMenu::adjustMod_processUserResponse);
|
||||||
|
client->sendCommand(pend);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,7 @@ private:
|
||||||
QAction *aBan, *aBanHistory;
|
QAction *aBan, *aBanHistory;
|
||||||
QAction *aPromoteToMod, *aDemoteFromMod;
|
QAction *aPromoteToMod, *aDemoteFromMod;
|
||||||
QAction *aPromoteToJudge, *aDemoteFromJudge;
|
QAction *aPromoteToJudge, *aDemoteFromJudge;
|
||||||
|
QAction *aPromoteToDeveloper, *aDemoteFromDeveloper;
|
||||||
QAction *aWarnUser, *aWarnHistory;
|
QAction *aWarnUser, *aWarnHistory;
|
||||||
QAction *aGetAdminNotes;
|
QAction *aGetAdminNotes;
|
||||||
std::function<QList<GameInviteOption>()> gameInviteLinkProvider;
|
std::function<QList<GameInviteOption>()> gameInviteLinkProvider;
|
||||||
|
|
@ -123,6 +124,7 @@ public:
|
||||||
void execInvestigateUser(const QString &userName);
|
void execInvestigateUser(const QString &userName);
|
||||||
void execAdjustMod(const QString &userName, bool shouldBeMod);
|
void execAdjustMod(const QString &userName, bool shouldBeMod);
|
||||||
void execAdjustJudge(const QString &userName, bool shouldBeJudge);
|
void execAdjustJudge(const QString &userName, bool shouldBeJudge);
|
||||||
|
void execAdjustDeveloper(const QString &userName, bool shouldBeDeveloper);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void execInvite(const QString &userName, const GameInviteOption &option);
|
void execInvite(const QString &userName, const GameInviteOption &option);
|
||||||
|
|
|
||||||
|
|
@ -122,6 +122,8 @@ void UserInfoBox::updateInfo(const ServerInfo_User &user)
|
||||||
QString userLevelText;
|
QString userLevelText;
|
||||||
if (userLevel.testFlag(ServerInfo_User::IsAdmin)) {
|
if (userLevel.testFlag(ServerInfo_User::IsAdmin)) {
|
||||||
userLevelText = tr("Administrator");
|
userLevelText = tr("Administrator");
|
||||||
|
} else if (userLevel.testFlag(ServerInfo_User::IsDeveloper)) {
|
||||||
|
userLevelText = tr("Developer");
|
||||||
} else if (userLevel.testFlag(ServerInfo_User::IsModerator)) {
|
} else if (userLevel.testFlag(ServerInfo_User::IsModerator)) {
|
||||||
userLevelText = tr("Moderator");
|
userLevelText = tr("Moderator");
|
||||||
} else if (userLevel.testFlag(ServerInfo_User::IsRegistered)) {
|
} else if (userLevel.testFlag(ServerInfo_User::IsRegistered)) {
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue