Merge branch 'master' into tooomm-qt5

This commit is contained in:
tooomm 2026-09-19 16:05:00 +02:00 committed by GitHub
commit b3813c202b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
162 changed files with 4862 additions and 653 deletions

View file

@ -403,6 +403,7 @@ set(cockatrice_SOURCES
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/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.h
src/interface/widgets/onboarding/first_run_wizard_page.cpp
@ -530,6 +531,7 @@ qt6_add_shaders(
"src/interface/widgets/onboarding/shaders"
FILES
src/interface/widgets/onboarding/shaders/brand_banner.frag
src/interface/widgets/onboarding/shaders/brand_plate.frag
)
qt6_add_resources(

View file

@ -63,6 +63,8 @@
<file>resources/icons/mana/W.svg</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/placeholder_printing_selector.svg</file>
@ -363,6 +365,8 @@
<file>resources/usericons/pawn_single.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_double.svg</file>
<file>resources/usericons/pawn_judge_single.svg</file>

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 MiB

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

Before After
Before After

View 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

View 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

View file

@ -1,6 +1,7 @@
#include "abstract_card_item.h"
#include "../../client/settings/cache_settings.h"
#include "../../interface/card_localization.h"
#include "../../interface/card_picture_loader/card_picture_loader.h"
#include "../game_scene.h"
#include "../z_values.h"
@ -26,6 +27,8 @@ AbstractCardItem::AbstractCardItem(QGraphicsItem *parent, const CardRef &cardRef
connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::displayCardNamesChanged, this,
[this] { update(); });
connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::cardLangChanged, this,
[this] { update(); });
refreshCardInfo();
connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::roundCardCornersChanged, this,
@ -171,7 +174,7 @@ void AbstractCardItem::paintPicture(QPainter *painter, const QSizeF &translatedS
if (SettingsCache::instance().debug().getShowCardId()) {
prefix = "#" + QString::number(id) + " ";
}
nameStr = prefix + cardRef.name;
nameStr = prefix + CardLocalization::displayName(getCardInfo());
}
painter->drawText(QRectF(3 * scaleFactor, 3 * scaleFactor, translatedSize.width() - 6 * scaleFactor,
translatedSize.height() - 6 * scaleFactor),

View 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

View file

@ -8,7 +8,7 @@
#include <QApplication>
#include <QBuffer>
#include <QDebug>
#include <QDirIterator>
#include <QDir>
#include <QFileInfo>
#include <QMainWindow>
#include <QMovie>
@ -20,6 +20,7 @@
#include <QThread>
#include <algorithm>
#include <libcockatrice/settings/cache_storage_settings.h>
#include <libcockatrice/settings/cards_display_settings.h>
#include <libcockatrice/settings/download_settings.h>
#include <libcockatrice/settings/paths_settings.h>
#include <utility>
@ -37,8 +38,10 @@ CardPictureLoader::CardPictureLoader() : QObject(nullptr)
&CardPictureLoader::picsPathChanged);
connect(&SettingsCache::instance().downloads(), &DownloadSettings::picDownloadChanged, this,
&CardPictureLoader::picDownloadChanged);
connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::cardLangChanged, this,
&CardPictureLoader::cardLangChanged);
qRegisterMetaType<ExactCard>();
qRegisterMetaType<ExactCard>("ExactCard");
connect(worker, &CardPictureLoaderWorker::imageLoaded, this, &CardPictureLoader::imageLoaded);
statusBar = new CardPictureLoaderStatusBar(nullptr);
@ -206,7 +209,49 @@ void CardPictureLoader::imageLoaded(const ExactCard &card, const QImage &image)
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) {
return;
@ -266,8 +311,9 @@ void CardPictureLoader::saveCardImageToLocalStorage(const ExactCard &card, const
QFileInfo outInfo(baseDir.filePath(relativePath));
// Do not overwrite existing files
if (outInfo.exists()) {
// Automatic cache writes (FILESYSTEM_CACHE) must never clobber an explicit user override.
// Only the explicit override paths pass allowOverwrite == true.
if (!allowOverwrite && outInfo.exists()) {
return;
}
@ -288,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()
{
QPixmapCache::clear();
@ -327,25 +489,11 @@ void CardPictureLoader::picsPathChanged()
QPixmapCache::clear();
}
bool CardPictureLoader::hasCustomArt()
void CardPictureLoader::cardLangChanged()
{
auto picsPath = SettingsCache::instance().paths().getPicsPath();
QDirIterator it(picsPath, QDir::Dirs | QDir::NoDotAndDotDot);
// Check if there is at least one non-directory file in the pics path, other
// than in the "downloadedPics" subdirectory.
while (it.hasNext()) {
QFileInfo dir(it.nextFileInfo());
if (it.fileName() == "downloadedPics") {
continue;
}
QDirIterator subIt(it.filePath(), QDir::Files, QDirIterator::Subdirectories | QDirIterator::FollowSymlinks);
if (subIt.hasNext()) {
return true;
}
}
return false;
// Localized images are fetched via a different URL, but the in-memory
// pixmap cache is keyed by card name/uuid, so drop everything cached
// (including failure timestamps) to force a reload in the new language.
QPixmapCache::clear();
failedAt.clear();
}

View file

@ -97,10 +97,17 @@ public:
static void cacheCardPixmaps(const QList<ExactCard> &cards);
/**
* @brief Check if the user has custom card art in the picsPath directory.
* @return True if any custom art exists.
* @brief Check if a local override image already exists for the card.
* @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.
@ -120,7 +127,9 @@ public slots:
* @param image Loaded QImage.
*/
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:
/**
@ -134,6 +143,12 @@ private slots:
* Clears the QPixmap cache to reload images.
*/
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

View file

@ -94,6 +94,10 @@ QImage CardPictureLoaderLocal::tryLoadCardImageFromDisk(const QString &setName,
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) {
QFileInfo fileInfo(path);
QDir dir = fileInfo.dir();
@ -105,7 +109,8 @@ QImage CardPictureLoaderLocal::tryLoadCardImageFromDisk(const QString &setName,
QStringList files = dir.entryList(QDir::Files);
for (const QString &file : files) {
if (!file.startsWith(baseName)) {
QFileInfo fi(file);
if (fi.completeBaseName() != baseName) {
continue;
}

View file

@ -84,8 +84,8 @@ void CardPictureLoaderWorker::queueRequest(const QUrl &url, CardPictureLoaderWor
SettingsCache::instance().cacheStorage().getCardPictureLoaderCacheMethod()) ==
CardPictureLoaderCacheMethod::CacheMethod::NETWORK_CACHE &&
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
// rate-limit
// A request that will be served from the disk cache never touches the network and therefore
// doesn't use up any of the rate limit, so it gets to skip the queue.
makeRequest(url, worker);
return;
}
@ -107,10 +107,13 @@ QNetworkReply *CardPictureLoaderWorker::makeRequest(const QUrl &url, CardPicture
req.setHeader(QNetworkRequest::UserAgentHeader, QString("Cockatrice %1").arg(VERSION_STRING));
req.setRawHeader("Accept", "image/avif,image/webp,image/apng,image/,/*;q=0.8");
bool useNetworkCache =
!picDownload && static_cast<CardPictureLoaderCacheMethod::CacheMethod>(
SettingsCache::instance().cacheStorage().getCardPictureLoaderCacheMethod()) ==
CardPictureLoaderCacheMethod::CacheMethod::NETWORK_CACHE;
// Cached entries are served straight from the disk cache even when picture downloads are
// enabled: re-fetching an already-cached image would burn the rate limit for nothing. Only a
// genuine cache miss goes to the network, and only when downloads are enabled.
bool useNetworkCache = static_cast<CardPictureLoaderCacheMethod::CacheMethod>(
SettingsCache::instance().cacheStorage().getCardPictureLoaderCacheMethod()) ==
CardPictureLoaderCacheMethod::CacheMethod::NETWORK_CACHE &&
(cache->metaData(url).isValid() || !picDownload);
req.setAttribute(QNetworkRequest::CacheLoadControlAttribute,
useNetworkCache ? QNetworkRequest::AlwaysCache : QNetworkRequest::AlwaysNetwork);

View file

@ -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);
if (!transformedUrl.isEmpty()) {
@ -282,8 +283,15 @@ QString CardPictureToLoad::transformUrl(const QString &urlTemplate) const
}
// language setting
transformMap["!sflang!"] = QString(QCoreApplication::translate(
"PictureLoader", "en", "code for scryfall's language property, not available for all languages"));
const QString cardLang = SettingsCache::instance().cardsDisplay().getCardLang();
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;
for (const QString &prop : transformMap.keys()) {

View file

@ -1,6 +1,5 @@
#include "palette_editor_dialog.h"
#include "../../client/settings/cache_settings.h"
#include "../theme_manager.h"
#include "palette_generator.h"
#include "palette_grid_widget.h"
@ -11,31 +10,11 @@
#include <QDialogButtonBox>
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QFrame>
#include <QGuiApplication>
#include <QLabel>
#include <QLoggingCategory>
#include <QMessageBox>
#include <QPushButton>
#include <QStyleHints>
#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)
: 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
// other read-only theme directories must be customised in the user-writable
// themes directory; otherwise the write would fail or be lost on upgrade.
if (!themeDirPath.isEmpty() && isDirReallyWritable(themeDirPath)) {
saveDir = themeDirPath;
} else {
saveDir = QDir(SettingsCache::instance().paths().getThemesPath()).absoluteFilePath(themeName);
if (!QDir().mkpath(saveDir)) {
qWarning() << "Failed to create palette save directory:" << saveDir;
}
}
saveDir = ThemeManager::writableThemeDir(themeName);
// Load both scheme configs upfront so switching is instant
loadSchemes();
@ -214,7 +186,7 @@ void PaletteEditorDialog::retranslateUi()
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()));
if (saveDir.isEmpty() || !isDirReallyWritable(saveDir)) {
if (saveDir.isEmpty() || !ThemeManager::isDirReallyWritable(saveDir)) {
saveBtn->setEnabled(false);
saveBtn->setToolTip(tr("Cannot save: this theme has no writable directory"));
}
@ -297,7 +269,7 @@ void PaletteEditorDialog::onSave()
if (it.key() == loadedScheme) {
continue;
}
if (it.value().colors == savedConfig.value(it.key()).colors) {
if (it.value() == savedConfig.value(it.key())) {
continue;
}
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.
if (workingConfig[loadedScheme].colors != savedConfig.value(loadedScheme).colors) {
if (workingConfig[loadedScheme] != savedConfig.value(loadedScheme)) {
if (!ThemeManager::commitPalette(saveDir, loadedScheme, workingConfig[loadedScheme])) {
QMessageBox::warning(this, tr("Save failed"),
tr("Could not write %1 to:\n%2").arg(PaletteConfig::fileName(loadedScheme), saveDir));

View file

@ -150,6 +150,17 @@ PaletteConfig fromAccent(const QColor &accent, int intensity, const QString &sch
cfg.colors[CG::Disabled][CR::HighlightedText] = disText;
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
QColor bright;
if (achromatic) {

View file

@ -1,5 +1,7 @@
#include "palette_grid_widget.h"
#include "../theme_manager.h"
#include <QApplication>
#include <QGridLayout>
#include <QLabel>
@ -45,6 +47,11 @@ static const QMap<QPalette::ColorRole, const char *> ROLE_DESCRIPTIONS = {
{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)
{
scroll = new QScrollArea(this);
@ -122,6 +129,46 @@ void PaletteGridWidget::buildGrid(QWidget *host)
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)
@ -166,6 +213,16 @@ void PaletteGridWidget::loadPalette(const PaletteConfig &cfg)
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
@ -176,5 +233,12 @@ PaletteConfig PaletteGridWidget::currentPaletteConfig() const
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;
}

View file

@ -31,6 +31,7 @@ private:
void refreshChromePalettes();
QMap<QPalette::ColorGroup, QMap<QPalette::ColorRole, ColorButton *>> colorButtons;
QMap<AppColor::Role, ColorButton *> appColorButtons;
QScrollArea *scroll;
QWidget *gridHost;
QVBoxLayout *layout;

View file

@ -16,7 +16,6 @@
#define DEFAULT_COLOR_MODERATOR_LEFT "#ffffff";
#define DEFAULT_COLOR_MODERATOR_RIGHT "#000000";
#define DEFAULT_COLOR_ADMIN "#ff2701";
#define DEFAULT_COLOR_DEVELOPER "#B8B8B8"
/**
* Clamps an svg render size so that rendering does not exceed a multiple of the requested size.
@ -362,6 +361,10 @@ static QString getIconType(const bool isBuddy, const UserLevelFlags &userLevelFl
return "pawn_judge";
}
if (userLevelFlags.testFlag(ServerInfo_User::IsDeveloper)) {
return "pawn_dev";
}
if (!privLevel.isEmpty() && privLevel.toLower() != "none") {
return QString("pawn_%1").arg(privLevel.toLower());
}
@ -382,8 +385,6 @@ QIcon UserLevelPixmapGenerator::generateIconDefault(int height,
if (userLevel.testFlag(ServerInfo_User::IsAdmin)) {
colorLeft = DEFAULT_COLOR_ADMIN;
} else if (userLevel.testFlag(ServerInfo_User::IsDeveloper)) {
colorLeft = DEFAULT_COLOR_DEVELOPER;
} else if (userLevel.testFlag(ServerInfo_User::IsModerator)) {
colorLeft = DEFAULT_COLOR_MODERATOR_LEFT;
colorRight = DEFAULT_COLOR_MODERATOR_RIGHT;

View file

@ -16,7 +16,7 @@ QString ThemeConfig::toIni() const
out += "[Appearance]\n";
out += QString("ColorScheme = %1\n").arg(colorScheme.isEmpty() ? "System" : colorScheme);
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;
}
@ -96,7 +96,7 @@ bool ThemeConfig::save(const QString &themeDirPath) const
bool PaletteConfig::hasPalette() const
{
return !colors.isEmpty();
return !colors.isEmpty() || !appColors.isEmpty();
}
QString PaletteConfig::toToml() const
@ -133,6 +133,24 @@ QString PaletteConfig::toToml() const
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;
}
@ -152,6 +170,7 @@ PaletteConfig PaletteConfig::fromFile(const QString &filePath)
}
QMetaEnum roleEnum = QMetaEnum::fromType<QPalette::ColorRole>();
QMetaEnum appEnum = QMetaEnum::fromType<AppColor::Role>();
QString currentSection;
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)) {
continue;
}
@ -216,11 +255,7 @@ PaletteConfig PaletteConfig::fromFile(const QString &filePath)
continue;
}
QColor color(value);
if (color.isValid()) {
cfg.colors[currentGroup][static_cast<QPalette::ColorRole>(roleInt)] = color;
}
cfg.colors[currentGroup][static_cast<QPalette::ColorRole>(roleInt)] = color;
}
return cfg;

View file

@ -3,9 +3,25 @@
#include <QColor>
#include <QMap>
#include <QObject>
#include <QPalette>
#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
{
QString colorScheme;
@ -21,7 +37,16 @@ struct ThemeConfig
struct PaletteConfig
{
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;
QString toToml() const;

View file

@ -6,6 +6,7 @@
#include <QApplication>
#include <QColor>
#include <QDebug>
#include <QFile>
#include <QFileInfo>
#include <QLibraryInfo>
#include <QMap>
@ -21,7 +22,7 @@
#include <Qt>
#include <libcockatrice/settings/paths_settings.h>
#define NONE_THEME_NAME "Default"
#define SYSTEM_THEME_NAME "System"
#define FUSION_THEME_NAME "Fusion"
#define STYLE_CSS_NAME "style.css"
#define HANDZONE_BG_NAME "handzone"
@ -95,7 +96,7 @@ struct PaletteColorInfo
static QString usableDefaultStyle(const QString &style)
{
// 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.
return style.compare("windows11", Qt::CaseInsensitive) == 0 ? QStringLiteral("windowsvista") : style;
}
@ -118,10 +119,16 @@ ThemeManager::ThemeManager(QObject *parent) : QObject(parent)
void ThemeManager::ensureThemeDirectoryExists()
{
if (SettingsCache::instance().getThemeName().isEmpty() ||
!getAvailableThemes().contains(SettingsCache::instance().getThemeName())) {
auto &settings = SettingsCache::instance();
// 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";
SettingsCache::instance().setThemeName(NONE_THEME_NAME);
settings.setThemeName(FUSION_THEME_NAME);
}
}
@ -184,11 +191,32 @@ QString ThemeManager::assetPath(QStringView prefix) const
return resolvedPlain.isEmpty() ? prefix.toString() : resolvedPlain;
}
bool ThemeManager::isBuiltInTheme()
// 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 auto themeName = SettingsCache::instance().getThemeName();
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;
}
return themeName == NONE_THEME_NAME || themeName == FUSION_THEME_NAME;
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.
@ -213,9 +241,7 @@ QStringMap &ThemeManager::getAvailableThemes()
// load themes from user profile dir
dir.setPath(SettingsCache::instance().paths().getThemesPath());
// add default value
availableThemes.insert(NONE_THEME_NAME, dir.absoluteFilePath("Default"));
availableThemes.insert(SYSTEM_THEME_NAME, dir.absoluteFilePath("System"));
availableThemes.insert(FUSION_THEME_NAME, dir.absoluteFilePath("Fusion"));
for (QString themeName : dir.entryList(QDir::AllDirs | QDir::NoDotAndDotDot, QDir::Name)) {
@ -331,7 +357,7 @@ bool ThemeManager::commitPalette(const QString &themeDirPath, const QString &col
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);
cfg.colorScheme = scheme;
@ -342,7 +368,7 @@ void ThemeManager::setColorScheme(const QString &scheme)
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);
cfg.styleName = styleName;
@ -373,7 +399,7 @@ void ThemeManager::applyStyleAndPalette(const QString &themeName,
Q_UNUSED(activeScheme)
#endif
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) {
styleName = "Fusion";
} else {
@ -416,6 +442,8 @@ void ThemeManager::applyStyleAndPalette(const QString &themeName,
qApp->setPalette(base);
qApp->setStyle(style);
currentAppColors = palCfg.appColors;
// Force every widget to re-polish and repaint immediately rather than
// waiting for natural expose events, which produces a patchwork of old
// and new colours during a live preview.
@ -428,6 +456,35 @@ void ThemeManager::applyStyleAndPalette(const QString &themeName,
style->polish(widget);
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()
@ -464,8 +521,19 @@ void ThemeManager::themeChangedSlot()
// ── Load palette: custom first, then theme default ────────────────────
PaletteConfig palette = PaletteConfig::fromScheme(dirPath, activeScheme);
if (!palette.hasPalette()) {
palette = ThemeManager::loadDefaultPaletteConfig(dirPath, themeName, activeScheme);
const PaletteConfig themeDefault = 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);

View file

@ -50,6 +50,7 @@ private:
QString currentThemePath;
std::array<QBrush, Role::MaxRole + 1> brushes;
QStringMap availableThemes;
QMap<AppColor::Role, QColor> currentAppColors;
/*
Internal cache for multiple backgrounds
*/
@ -65,7 +66,16 @@ protected:
const QString &activeScheme);
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
// (Dark/Light), falling back to the OS color scheme when it is "System".
bool isDarkMode(const QString &themeDirPath) const;
@ -115,12 +125,17 @@ public:
void reloadCurrentTheme();
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 getExtraBgBrush(Role zone, int zoneId = 0);
protected slots:
void themeChangedSlot();
signals:
void themeChanged();
void paletteChanged();
};
extern ThemeManager *themeManager;

View file

@ -74,6 +74,8 @@ CardInfoPictureWidget::CardInfoPictureWidget(QWidget *parent, const bool _hoverT
update();
});
connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::cardLangChanged, this,
&CardInfoPictureWidget::updatePixmap);
}
/**

View file

@ -1,6 +1,7 @@
#include "card_info_text_widget.h"
#include "../../../game_graphics/board/card_item.h"
#include "../../card_localization.h"
#include <QGridLayout>
#include <QLabel>
@ -10,7 +11,7 @@
#include <libcockatrice/card/game_specific_terms.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->setOpenExternalLinks(false);
@ -39,6 +40,12 @@ CardInfoTextWidget::CardInfoTextWidget(QWidget *parent) : QFrame(parent), info(n
grid->setRowStretch(1, 1);
retranslateUi();
connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::cardLangChanged, this, [this] {
if (currentCard) {
setCard(currentCard);
}
});
}
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>";
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()) {
QString setShort = exactCard.getPrinting().getSet()->getShortName().toHtmlEscaped();
@ -94,7 +101,8 @@ void CardInfoTextWidget::setCard(const ExactCard &exactCard)
}
text += "</table>";
setTexts(text, card->getText());
setTexts(text, CardLocalization::displayText(card));
currentCard = exactCard;
}
void CardInfoTextWidget::setInvalidCardName(const QString &cardName)

View file

@ -23,7 +23,7 @@ private:
QLabel *propsLabel;
QScrollArea *propsScroll;
QTextEdit *textLabel;
CardInfoPtr info;
ExactCard currentCard; ///< Last card set, re-rendered when the card language changes.
void setTexts(const QString &propsText, const QString &textText);
public:

View file

@ -345,7 +345,9 @@ ExactCard DeckEditorDeckDockWidget::getCurrentCard()
if (!current.isValid()) {
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 QModelIndex gparent = current.parent().parent();
@ -518,8 +520,11 @@ void DeckEditorDeckDockWidget::syncBannerCardComboBoxSelectionWithDeck()
void DeckEditorDeckDockWidget::setSelectedIndex(const QModelIndex &newCardIndex, bool preserveWidgetFocus)
{
const QModelIndex proxyIndex = proxy->mapFromSource(newCardIndex);
deckView->clearSelection();
deckView->setCurrentIndex(newCardIndex);
deckView->setCurrentIndex(proxyIndex);
deckView->scrollTo(proxyIndex);
recursiveExpand(newCardIndex);
if (!preserveWidgetFocus) {

View file

@ -1,13 +1,20 @@
#include "deck_state_manager.h"
#include "../../../client/settings/cache_settings.h"
#include <libcockatrice/card/database/card_database_manager.h>
#include <libcockatrice/deck_list/deck_list_history_manager.h>
#include <libcockatrice/deck_list/tree/inner_deck_list_node.h>
#include <libcockatrice/settings/cards_display_settings.h>
DeckStateManager::DeckStateManager(QObject *parent)
: QObject(parent), deckList(QSharedPointer<DeckList>(new DeckList)),
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] {
setModified(true);
emit historyChanged();
@ -260,7 +267,10 @@ bool DeckStateManager::swapCardAtIndex(const QModelIndex &idx)
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();
QModelIndex gparent = idx.parent().parent();
@ -277,7 +287,7 @@ bool DeckStateManager::swapCardAtIndex(const QModelIndex &idx)
QString reason = tr("Moved to %1 1 × \"%2\" (%3)") //
.arg(otherZoneName)
.arg(cardName)
.arg(displayCardName)
.arg(providerId);
return modifyDeck(reason, [&idx, &cardName, &providerId, &otherZoneName](auto model) {
@ -291,9 +301,8 @@ bool DeckStateManager::removeCardAtIndex(const QModelIndex &idx)
return false;
}
QString cardName = idx.siblingAtColumn(DeckListModelColumns::CARD_NAME).data().toString();
QString reason = tr("Removed \"%1\" (all copies)").arg(cardName);
QString reason =
tr("Removed \"%1\" (all copies)").arg(idx.siblingAtColumn(DeckListModelColumns::CARD_NAME).data().toString());
return modifyDeck(reason, [&idx](auto model) { return model->removeRow(idx.row(), idx.parent()); });
}

View file

@ -416,6 +416,11 @@ void DlgSettings::setTab(int index)
}
}
AbstractSettingsPage *DlgSettings::page(SettingsPage which) const
{
return pages.value(static_cast<int>(which));
}
void DlgSettings::updateLanguage()
{
qApp->removeTranslator(translator); // NOLINT(cppcoreguidelines-pro-type-static-cast-downcast)

View file

@ -54,6 +54,7 @@ public:
explicit DlgSettings(QWidget *parent = nullptr);
void setTab(int index);
AbstractSettingsPage *page(SettingsPage which) const;
private slots:
void onTabClicked(int index);

View file

@ -11,8 +11,8 @@ namespace HomeTabButtonColor
*/
enum Source
{
Automatic, ///< Extract color from background, or use theme color if no background
FromBackground, ///< Always extract color from background
FromThemeColors, ///< Use the theme's identity accent colors
FromBackground, ///< Extract colour from the background image
};
struct Entry
@ -23,7 +23,7 @@ struct Entry
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")}};
return entries;
@ -33,12 +33,12 @@ inline QList<Entry> all()
* Safely converts an int into the corresponding Source.
*
* @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)
{
if (value > FromBackground) {
return Automatic; // default
return FromThemeColors; // default
}
return static_cast<Source>(value);

View file

@ -11,6 +11,8 @@
#include "home_tab_button_color.h"
#include <QGroupBox>
#include <QLabel>
#include <QLinearGradient>
#include <QPainter>
#include <QPainterPath>
#include <QPushButton>
@ -21,8 +23,7 @@
#include <libcockatrice/settings/paths_settings.h>
HomeWidget::HomeWidget(QWidget *parent, TabSupervisor *_tabSupervisor)
: QWidget(parent), tabSupervisor(_tabSupervisor), background(themePixmap(QStringLiteral("backgrounds/home"))),
overlay(themePixmap(QStringLiteral("cockatrice")))
: QWidget(parent), tabSupervisor(_tabSupervisor), background(themePixmap(QStringLiteral("backgrounds/home")))
{
layout = new QGridLayout(this);
@ -54,6 +55,8 @@ HomeWidget::HomeWidget(QWidget *parent, TabSupervisor *_tabSupervisor)
// Lambda is cleaner to read than overloading this
connect(&SettingsCache::instance().appearance(), &AppearanceSettings::homeTabDisplayCardNameChanged, this,
[this] { repaint(); });
connect(&SettingsCache::instance().appearance(), &AppearanceSettings::homeTabBackgroundDimChanged, this,
[this] { repaint(); });
connect(&SettingsCache::instance(), &SettingsCache::themeChanged, this,
&HomeWidget::initializeBackgroundFromSource);
connect(&SettingsCache::instance(), &SettingsCache::themeChanged, this,
@ -61,12 +64,18 @@ HomeWidget::HomeWidget(QWidget *parent, TabSupervisor *_tabSupervisor)
// 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,
&HomeWidget::updateButtonsToBackgroundColor);
}
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) {
connect(CardDatabaseManager::getInstance(), &CardDatabase::cardDatabaseLoadingFinished, this,
&HomeWidget::initializeBackgroundFromSource);
@ -105,32 +114,24 @@ void HomeWidget::loadBackgroundSourceDeck()
backgroundSourceDeck = deckOpt.has_value() ? deckOpt.value().deckList : DeckList();
}
static bool isDefaultBackgroundAndTheme()
static QPair<QColor, QColor> paletteDerivedButtonColors()
{
QString sourceId = SettingsCache::instance().appearance().getHomeTabBackgroundSource();
return themeManager->isBuiltInTheme() && BackgroundSources::fromId(sourceId) == BackgroundSources::Theme;
return {themeManager->appColor(AppColor::AccentStrong), themeManager->appColor(AppColor::AccentSoft)};
}
QPair<QColor, QColor> HomeWidget::determineButtonColor() const
{
static QPair defaultColor = {QColor::fromRgb(20, 140, 60), QColor::fromRgb(120, 200, 80)};
auto colorSource =
HomeTabButtonColor::intToSource(SettingsCache::instance().appearance().getHomeTabButtonColorSourceIndex());
switch (colorSource) {
case HomeTabButtonColor::Automatic: {
if (isDefaultBackgroundAndTheme()) {
return defaultColor;
} else {
return extractDominantColors(background);
}
}
case HomeTabButtonColor::FromThemeColors:
return paletteDerivedButtonColors();
case HomeTabButtonColor::FromBackground:
return extractDominantColors(background);
}
return defaultColor;
return paletteDerivedButtonColors();
}
void HomeWidget::setRandomCard(ExactCard &newCard)
@ -234,10 +235,10 @@ QGroupBox *HomeWidget::createButtons()
QVBoxLayout *boxLayout = new QVBoxLayout;
boxLayout->setAlignment(Qt::AlignHCenter);
QLabel *logoLabel = new QLabel;
logoLabel->setPixmap(overlay.scaledToWidth(200, Qt::SmoothTransformation));
logoLabel = new QLabel;
logoLabel->setAlignment(Qt::AlignCenter);
boxLayout->addWidget(logoLabel);
updateLogoOverlay();
boxLayout->addSpacing(25);
connectButton = new HomeStyledButton("Connect/Play", gradientColors);
@ -365,13 +366,15 @@ void HomeWidget::paintEvent(QPaintEvent *event)
painter.drawPixmap(topLeft, toDraw);
}
// Draw translucent black overlay with rounded corners
QRectF overlayRect(5, 5, width() - 10, height() - 10);
QPainterPath roundedRectPath;
roundedRectPath.addRoundedRect(overlayRect, 20, 20);
if (SettingsCache::instance().appearance().getHomeTabBackgroundDim()) {
// Draw translucent black overlay with rounded corners
QRectF overlayRect(5, 5, width() - 10, height() - 10);
QPainterPath roundedRectPath;
roundedRectPath.addRoundedRect(overlayRect, 20, 20);
QColor semiTransparentBlack(0, 0, 0, static_cast<int>(255 * 0.33));
painter.fillPath(roundedRectPath, semiTransparentBlack);
QColor semiTransparentBlack(0, 0, 0, static_cast<int>(255 * 0.33));
painter.fillPath(roundedRectPath, semiTransparentBlack);
}
// Card name overlay (above the attribution, bottom-right)
QString cardName;
@ -436,3 +439,56 @@ void HomeWidget::paintEvent(QPaintEvent *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);
}
}

View file

@ -15,6 +15,9 @@
#include <QWidget>
#include <libcockatrice/network/client/abstract/abstract_client.h>
class QGridLayout;
class QLabel;
class HomeWidget : public QWidget
{
@ -41,13 +44,14 @@ private:
QPixmap background;
CardInfoPictureArtCropWidget *backgroundSourceCard = nullptr;
DeckList backgroundSourceDeck;
QPixmap overlay;
QLabel *logoLabel = nullptr;
QPair<QColor, QColor> gradientColors;
HomeStyledButton *connectButton;
void setRandomCard(ExactCard &newCard);
void loadBackgroundSourceDeck();
QPair<QColor, QColor> determineButtonColor() const;
void updateLogoOverlay();
};
#endif // HOME_WIDGET_H

View file

@ -38,6 +38,10 @@ class BannerShaderConfig : public QObject
Q_PROPERTY(QColor colorA READ colorA WRITE setColorA NOTIFY colorAChanged)
Q_PROPERTY(QColor colorB READ colorB WRITE setColorB NOTIFY colorBChanged)
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(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
{
return m_logoVisible;
@ -222,6 +274,10 @@ signals:
void colorAChanged();
void colorBChanged();
void accentChanged();
void glowColorChanged();
void brandStrongChanged();
void brandSoftChanged();
void vignetteMinChanged();
void logoVisibleChanged();
void logoGlowChanged();
@ -239,9 +295,16 @@ private:
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_colorB{0x0E, 0x0E, 0x12};
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;
qreal m_logoGlow = 1.0;

View 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

View file

@ -14,10 +14,31 @@
#include <QGroupBox>
#include <QLabel>
#include <QMessageBox>
#include <QPalette>
#include <QVBoxLayout>
#include <libcockatrice/settings/paths_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)
{
themeCombo = new QComboBox(this);
@ -30,6 +51,15 @@ ThemeSetupPage::ThemeSetupPage(QWidget *parent) : FirstRunWizardPage(parent)
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(schemeCombo, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &ThemeSetupPage::onSchemeChanged);
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
// half of 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 QString current = cfg.colorScheme;
@ -56,6 +87,14 @@ ThemeSetupPage::ThemeSetupPage(QWidget *parent) : FirstRunWizardPage(parent)
schemeCombo->setCurrentIndex(idx >= 0 ? idx : 0);
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();
});
@ -158,8 +197,14 @@ void ThemeSetupPage::maybeAutoGeneratePalette()
const QString dirPath = themeManager->getAvailableThemes().value(SettingsCache::instance().getThemeName());
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() ||
PaletteConfig::fromDefault(dirPath, scheme).hasPalette()) {
ThemeManager::loadDefaultPaletteConfig(dirPath, SettingsCache::instance().getThemeName(), scheme)
.hasPalette()) {
return; // theme already has something real to show -- leave it alone
}

View file

@ -53,6 +53,9 @@ private:
QComboBox *homeTabBackgroundCombo;
bool paletteDirty = false;
/// Theme whose identity accent currently seeds the picker.
QString lastSeededTheme;
};
#endif // THEME_SETUP_PAGE_H

View file

@ -16,6 +16,8 @@ Item {
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 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
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 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 uGlowColor: Qt.vector4d(bannerConfig.glowColor.r, bannerConfig.glowColor.g, bannerConfig.glowColor.b, 1.0)
property real uVignetteMin: bannerConfig.vignetteMin
property real uLogoGlow: bannerConfig.logoGlow
fragmentShader: "qrc:/onboarding/shaders/brand_banner.frag.qsb"
}
// The hero logo itself breathes cleanly over a 0.51.0 opacity range
Image {
id: logo
anchors.centerIn: parent
// The white logo sits at full opacity on top of the static gradient plate
// no glow, no breathing. The plate matches home_widget's QPainter composite.
Item {
id: logoHost
visible: bannerConfig.logoVisible
source: "qrc:/resources/cockatrice-logo-white.svg"
anchors.centerIn: parent
width: root.height * 0.6
height: width * (sourceSize.height > 0 ? sourceSize.height / Math.max(sourceSize.width, 1) : 1)
fillMode: Image.PreserveAspectFit
smooth: true
opacity: 0.5 + 0.5 * bannerConfig.logoGlow
sourceSize: Qt.size(256, 256)
height: width
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 {
origin.x: logo.width / 2
origin.y: logo.height / 2
xScale: 0.94 + 0.06 * bannerConfig.logoGlow
yScale: 0.94 + 0.06 * bannerConfig.logoGlow
// The logo's gradient plate, drawn behind the mark: a linear
// AccentSoft (light) -> AccentStrong (dark) sheet along the same
// top-left -> bottom-right userSpaceOnUse axis the baked-in SVG used,
// clipped to the bird silhouette via uSilhouette. The white highlight
// 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
}
}
}
}

View file

@ -1,7 +1,10 @@
#include "shader_banner_widget.h"
#include "../../theme_manager.h"
#include "banner_shader_config.h"
#include "brand_colors.h"
#include <QApplication>
#include <QPainter>
#include <QQmlContext>
#include <QQmlEngine>
@ -11,11 +14,98 @@
namespace
{
// Near-black base palette -- the background is dark and quiet so the green
// accent stands out.
constexpr QRgb kColorA = 0x1A1A20;
constexpr QRgb kColorB = 0x0E0E12;
constexpr QRgb kAccent = 0x8BDD6B;
// Curated near-black stage -- used only when the active palette resolves no
// usable window colour. Matches the banner's original design (dark and quiet
// so the accent stands out) and satisfies design-plans §2.1's "identity
// survives a bare palette".
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
class GradientFallbackWidget : public QWidget
@ -23,15 +113,27 @@ class GradientFallbackWidget : public QWidget
public:
using QWidget::QWidget;
void setColors(const QColor &a, const QColor &b)
{
if (a != colorA || b != colorB) {
colorA = a;
colorB = b;
}
}
protected:
void paintEvent(QPaintEvent *) override
{
QPainter painter(this);
QLinearGradient gradient(0, 0, width(), height());
gradient.setColorAt(0.0, QColor(kColorA));
gradient.setColorAt(1.0, QColor(kColorB));
gradient.setColorAt(0.0, colorA);
gradient.setColorAt(1.0, colorB);
painter.fillRect(rect(), gradient);
}
private:
QColor colorA{QColor(kFallbackColorA)};
QColor colorB{QColor(kFallbackColorB)};
};
BannerHost::BannerHost(QWidget *parent) : QWidget(parent)
@ -62,6 +164,9 @@ BannerHost::BannerHost(QWidget *parent) : QWidget(parent)
connect(&clock, &QTimer::timeout, this, &BannerHost::tick);
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);
updateAspect();
}
@ -123,9 +228,9 @@ void BannerHost::applyMotifPreset(Motif motif)
const Preset p = presetFor(motif);
config->setColorA(QColor(kColorA));
config->setColorB(QColor(kColorB));
config->setAccent(QColor(kAccent));
config->setColorA(bannerColorA);
config->setColorB(bannerColorB);
config->setAccent(bannerAccent);
config->setLogoVisible(motif == Motif::Welcome);
if (isFirstApply) {
@ -183,8 +288,34 @@ void BannerHost::hideEvent(QHideEvent *event)
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()
{
// 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) {
qreal t = elapsed.elapsed() / 1000.0;
config->setTime(t);

View file

@ -1,6 +1,7 @@
#ifndef SHADER_BANNER_WIDGET_H
#define SHADER_BANNER_WIDGET_H
#include <QColor>
#include <QElapsedTimer>
#include <QTimer>
#include <QWidget>
@ -53,6 +54,7 @@ protected:
private slots:
void tick();
void onSceneGraphFailed();
void applyThemeColors();
private:
struct Preset
@ -73,6 +75,12 @@ private:
BannerShaderConfig *config = 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;
QElapsedTimer elapsed;
Motif currentMotif = Motif::Welcome;

View file

@ -28,6 +28,8 @@ layout(std140, binding = 0) uniform buf
vec4 uColorA;
vec4 uColorB;
vec4 uAccent;
vec4 uGlowColor;
float uVignetteMin;
float uLogoGlow;
};
@ -119,7 +121,7 @@ vec3 backgroundField(vec2 uv, float time)
// 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);
col += uAccent.rgb * max(fog - 0.6, 0.0) * 0.10;
col += uAccent.rgb * max(fog - 0.6, 0.0) * 0.14;
return col;
}
@ -136,14 +138,17 @@ vec3 motifWelcome(vec2 uv, vec3 bg, float t)
vec2 center = vec2(asp * 0.5, 0.5);
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);
col += centreLight * 0.20 * uLogoGlow;
col += uGlowColor.rgb * centreLight * 0.20 * uLogoGlow;
// 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 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.
// 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 pY = fract(baseY + t * riseSpeed);
float size = 0.006 + hash21(vec2(fi * 2.9, uSeed * 4.7)) * 0.012;
float bright = 0.15 + hash21(vec2(fi * 6.1, uSeed * 0.9)) * 0.30;
float size = 0.010 + hash21(vec2(fi * 2.9, uSeed * 4.7)) * 0.014;
float bright = 0.18 + hash21(vec2(fi * 6.1, uSeed * 0.9)) * 0.32;
// Fade out near top/bottom edges
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
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
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
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
@ -323,19 +328,19 @@ vec3 motifAccount(vec2 uv, vec3 bg, float t)
vec2 ba = nodePos[j] - nodePos[i];
float h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0);
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
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
float ripplePhase = t * 0.4;
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;
}
@ -456,6 +461,8 @@ void main()
else if (uMode < 4.5) col = motifPreferences(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;
}

View file

@ -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;
}

View file

@ -1,13 +1,19 @@
#include "printing_selector_card_overlay_widget.h"
#include "../../../client/settings/cache_settings.h"
#include "../../card_picture_loader/card_picture_loader.h"
#include "../cards/card_info_picture_widget.h"
#include "printing_selector_card_display_widget.h"
#include <QApplication>
#include <QFileDialog>
#include <QImageReader>
#include <QLabel>
#include <QMenu>
#include <QMessageBox>
#include <QMouseEvent>
#include <QPixmapCache>
#include <QScreen>
#include <QVBoxLayout>
#include <QtMath>
#include <libcockatrice/card/database/card_database_manager.h>
@ -15,6 +21,49 @@
#include <libcockatrice/settings/card_override_settings.h>
#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.
*
@ -50,6 +99,31 @@ PrintingSelectorCardOverlayWidget::PrintingSelectorCardOverlayWidget(QWidget *pa
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
connect(this, &PrintingSelectorCardOverlayWidget::cardPreferenceChanged, this,
&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
* for each related card. When an action is triggered, the card information is updated, and the
* printing selector is shown.
* The menu includes the card art preference (Pin/Unpin Printing), the Image Overrides
* submenu (Load Custom Image, Clear Custom Image, and one entry per alternate printing with a
* 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)
{
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);
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
auto *relatedMenu = new QMenu(tr("Show Related cards"));
auto *relatedMenu = new QMenu(tr("Show Related cards"), &menu);
menu.addMenu(relatedMenu);
auto relatedCards = rootCard.getInfo().getAllRelatedCards();
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));
previewSourceMenu = nullptr;
}
/**
@ -291,3 +431,136 @@ void PrintingSelectorCardOverlayWidget::initializePinBadge()
pinBadge->setVisible(false);
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();
}
}

View file

@ -13,6 +13,9 @@
#include <libcockatrice/models/deck_list/deck_list_model.h>
class QAction;
class QMenu;
class PrintingSelectorCardOverlayWidget : public QWidget
{
Q_OBJECT
@ -43,11 +46,19 @@ private slots:
private:
void initializePinBadge();
void loadCustomImage();
void showPreviewForAction(QAction *action);
void refreshPreview();
void hidePreview();
CardInfoPictureWidget *cardInfoPicture;
AllZonesCardAmountWidget *allZonesCardAmountWidget;
QLabel *pinBadge = nullptr;
AbstractTabDeckEditor *deckEditor;
ExactCard rootCard;
QLabel *cardOverridePreviewLabel = nullptr;
ExactCard hoveredOverrideCard;
QMenu *previewSourceMenu = nullptr;
QAction *hoveredOverrideAction = nullptr;
};
#endif // PRINTING_SELECTOR_CARD_OVERLAY_WIDGET_H

View file

@ -369,7 +369,7 @@ void GameSelector::joinGame(const ServerInfo_Game &game, const bool asSpectator,
return;
}
bool overrideRestrictions = !tabSupervisor->getAdminLocked();
bool overrideRestrictions = tabSupervisor->canOverrideGameRestrictions();
// Joining a full game without override privileges silently becomes a
// spectator join, so ask first instead of surprising the player.
@ -462,7 +462,7 @@ void GameSelector::enableButtonsForIndex(const QModelIndex &current)
}
const ServerInfo_Game &game = gameListModel->getGame(current.data(Qt::UserRole).toInt());
bool overrideRestrictions = !tabSupervisor->getAdminLocked();
bool overrideRestrictions = tabSupervisor->canOverrideGameRestrictions();
spectateButton->setEnabled(game.spectators_allowed() || overrideRestrictions);
joinButton->setEnabled(game.player_count() < game.max_players() || overrideRestrictions);

View file

@ -59,8 +59,8 @@ AppearanceSettingsPage::AppearanceSettingsPage()
connect(&schemeCombo, &QComboBox::currentIndexChanged, this,
[this] { themeManager->setColorScheme(schemeCombo.currentData().toString()); });
// Qt widget style; "Default" lets the application decide
styleCombo.addItem(tr("Default"), QStringLiteral("Default"));
// Qt widget style; "System" lets the application decide
styleCombo.addItem(tr("System"), QStringLiteral("System"));
for (const QString &key : QStyleFactory::keys()) {
styleCombo.addItem(key, key);
}
@ -132,6 +132,10 @@ AppearanceSettingsPage::AppearanceSettingsPage()
connect(&homeTabDisplayCardNameCheckBox, &QCheckBox::QT_STATE_CHANGED, &settings.appearance(),
&AppearanceSettings::setHomeTabDisplayCardName);
homeTabBackgroundDimCheckBox.setChecked(settings.appearance().getHomeTabBackgroundDim());
connect(&homeTabBackgroundDimCheckBox, &QCheckBox::QT_STATE_CHANGED, &settings.appearance(),
&AppearanceSettings::setHomeTabBackgroundDim);
for (const auto &entry : HomeTabButtonColor::all()) {
homeTabButtonColorSourceBox.addItem(QObject::tr(entry.trKey));
}
@ -150,6 +154,7 @@ AppearanceSettingsPage::AppearanceSettingsPage()
homeTabGrid->addWidget(&homeTabDisplayCardNameCheckBox, 2, 0, 1, 2);
homeTabGrid->addWidget(&homeTabButtonColorSourceLabel, 3, 0);
homeTabGrid->addWidget(&homeTabButtonColorSourceBox, 3, 1);
homeTabGrid->addWidget(&homeTabBackgroundDimCheckBox, 4, 0, 1, 2);
homeTabGroupBox = new QGroupBox;
homeTabGroupBox->setLayout(homeTabGrid);
@ -508,9 +513,12 @@ void AppearanceSettingsPage::retranslateUi()
homeTabBackgroundShuffleFrequencyLabel.setText(tr("Home tab background shuffle frequency:"));
homeTabBackgroundShuffleFrequencySpinBox.setSpecialValueText(tr("Disabled"));
homeTabDisplayCardNameCheckBox.setText(tr("Display card name of background in bottom right"));
homeTabBackgroundDimCheckBox.setText(tr("Dim the home tab background"));
homeTabBackgroundDimCheckBox.setToolTip(
tr("Draw a translucent overlay over the home tab background so buttons and text stand out"));
homeTabButtonColorSourceLabel.setText(tr("Home tab button color:"));
homeTabButtonColorSourceBox.setToolTip(
tr("Automatic: extract from background if present, otherwise use theme default"));
tr("Use the theme's identity accent colors, or extract colors from the background image"));
playmatGroupBox->setTitle(tr("Playmat settings"));
playmatVisibilityLabel.setText(tr("Playmat visibility:"));

View file

@ -41,6 +41,7 @@ private:
QLabel homeTabBackgroundShuffleFrequencyLabel;
QSpinBox homeTabBackgroundShuffleFrequencySpinBox;
QCheckBox homeTabDisplayCardNameCheckBox;
QCheckBox homeTabBackgroundDimCheckBox;
QLabel homeTabButtonColorSourceLabel;
QComboBox homeTabButtonColorSourceBox;

View file

@ -1,15 +1,21 @@
#include "general_settings_page.h"
#include "../../../client/settings/cache_settings.h"
#include "../interface/card_picture_loader/card_picture_loader.h"
#include "../main.h"
#include "../server/user/user_info_connection.h"
#include "update/client/release_channel.h"
#include <QCoreApplication>
#include <QFile>
#include <QFileDialog>
#include <QGridLayout>
#include <QLineEdit>
#include <QMessageBox>
#include <QTranslator>
#include <libcockatrice/card/card_localization.h>
#include <libcockatrice/settings/cards_display_settings.h>
#include <libcockatrice/settings/download_settings.h>
#include <libcockatrice/settings/paths_settings.h>
#include <libcockatrice/settings/personal_settings.h>
#include <libcockatrice/settings/tabs_settings.h>
@ -46,10 +52,27 @@ GeneralSettingsPage::GeneralSettingsPage()
connect(&languageBox, qOverload<int>(&QComboBox::currentIndexChanged), this,
&GeneralSettingsPage::languageBoxChanged);
// card text & images language, independent of the UI language
cardLanguageBox.addItem(tr("English"), "en");
for (const QString &code : CardLocalization::supportedLanguages()) {
cardLanguageBox.addItem(CardLocalization::languageDisplayName(code), code);
}
const int cardLangIndex = cardLanguageBox.findData(SettingsCache::instance().cardsDisplay().getCardLang());
cardLanguageBox.setCurrentIndex(cardLangIndex < 0 ? 0 : cardLangIndex);
connect(&cardLanguageBox, qOverload<int>(&QComboBox::currentIndexChanged), this,
&GeneralSettingsPage::cardLanguageBoxChanged);
auto *languageGrid = new QGridLayout;
languageGrid->addWidget(&languageLabel, 0, 0);
languageGrid->addWidget(&languageBox, 0, 1);
languageGrid->addWidget(&advertiseTranslationPageLabel, 1, 1, Qt::AlignRight);
languageGrid->addWidget(&cardLanguageLabel, 1, 0);
languageGrid->addWidget(&cardLanguageBox, 1, 1);
languageGrid->addWidget(&cardLanguageNoteLabel, 2, 1);
languageGrid->addWidget(&advertiseTranslationPageLabel, 3, 1, Qt::AlignRight);
cardLanguageNoteLabel.setWordWrap(true);
cardLanguageNoteLabel.setAlignment(Qt::AlignLeft | Qt::AlignVCenter);
languageGroupBox = new QGroupBox;
languageGroupBox->setLayout(languageGrid);
@ -412,6 +435,52 @@ void GeneralSettingsPage::languageBoxChanged(int index)
SettingsCache::instance().personal().setLang(languageBox.itemData(index).toString());
}
void GeneralSettingsPage::cardLanguageBoxChanged(int index)
{
const QString lang = cardLanguageBox.itemData(index).toString();
SettingsCache::instance().cardsDisplay().setCardLang(lang);
// Switching to a non-default language only takes effect after the card
// database is re-imported with that language selected; English data is always
// present, so switching back to English needs no prompt.
if (lang == "en") {
return;
}
// The binary cache does not track the language its entries were imported in,
// and the downloaded pictures were fetched with English art names, so both are
// stale until Oracle re-imports the database in the new language: drop them.
QFile::remove(SettingsCache::instance().getCardDatabasePath() + ".cache");
CardPictureLoader::clearNetworkCache();
CardPictureLoader::clearPixmapCache();
// Art is resolved by the translated card name for non-English languages, so the
// matching Scryfall URL is added to the top of the download list. It stays
// visible in the deck editor settings, where it can be removed or reordered.
const bool localizedUrlAdded = SettingsCache::instance().downloads().addLocalizedScryfallUrl();
QString message = tr("<p>The card database only contains English card data. To see cards in <b>%1</b>, "
"<b>Oracle</b> must run once with this language selected and re-import the card "
"database.</p>"
"<p>The cached database and the downloaded card pictures have been cleared, so a "
"re-import is picked up without stale entries.</p>")
.arg(cardLanguageBox.itemText(index));
if (localizedUrlAdded) {
message += tr("<p>The Scryfall URL that resolves card art by translated name was added to the top of your "
"download list. You can remove or reorder it any time.</p>");
}
message += tr("<p>Run Oracle now?</p>");
const QMessageBox::StandardButton answer = QMessageBox::question(
this, tr("Card text & images language changed"), message, QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes);
// The answer only controls whether Oracle starts right away; the caches stay
// cleared so the next import or launch rebuilds them in the new language.
if (answer == QMessageBox::Yes) {
emit cardDatabaseUpdateRequested();
}
}
void GeneralSettingsPage::updateStartupServerControlsVisibility()
{
const int index = startupTabSelector.currentIndex();
@ -429,6 +498,10 @@ void GeneralSettingsPage::retranslateUi()
languageGroupBox->setTitle(tr("Language settings"));
languageLabel.setText(tr("Language:"));
cardLanguageBox.setItemText(0, tr("English"));
cardLanguageLabel.setText(tr("Card text & images language:"));
cardLanguageNoteLabel.setText(
tr("Foreign card names, text and art apply after you update the card database (Oracle)."));
advertiseTranslationPageLabel.setText(
QString("<a href='%1'>%2</a>").arg(WIKI_TRANSLATION_FAQ).arg(tr("How to help with translations")));

View file

@ -23,6 +23,10 @@ public:
static QStringList findQmFiles();
static QString languageName(const QString &lang);
signals:
/// Request to re-import the card database with the newly selected card language
void cardDatabaseUpdateRequested();
private slots:
void deckPathButtonClicked();
void filtersPathButtonClicked();
@ -33,6 +37,7 @@ private slots:
void tokenDatabasePathButtonClicked();
void resetAllPathsClicked();
void languageBoxChanged(int index);
void cardLanguageBoxChanged(int index);
void updateStartupServerControlsVisibility();
private:
@ -46,6 +51,10 @@ private:
QComboBox languageBox;
QLabel advertiseTranslationPageLabel;
QLabel cardLanguageLabel;
QComboBox cardLanguageBox;
QLabel cardLanguageNoteLabel;
QLabel updateReleaseChannelLabel;
QComboBox updateReleaseChannelBox;
QCheckBox startupUpdateCheckCheckBox;

View file

@ -1,5 +1,6 @@
#include "archidekt_api_response_deck_display_widget.h"
#include "../../../../../../client/settings/cache_settings.h"
#include "../../../../../deck_loader/card_node_function.h"
#include "../../../../../deck_loader/deck_loader.h"
#include "../../../../cards/card_size_widget.h"
@ -10,6 +11,7 @@
#include <QSortFilterProxyModel>
#include <libcockatrice/card/import/card_name_normalizer.h>
#include <libcockatrice/settings/cards_display_settings.h>
ArchidektApiResponseDeckDisplayWidget::ArchidektApiResponseDeckDisplayWidget(QWidget *parent,
ArchidektApiResponseDeck _response,
@ -120,6 +122,9 @@ ArchidektApiResponseDeckDisplayWidget::ArchidektApiResponseDeckDisplayWidget(QWi
}
model = new DeckListModel(this);
model->setDisplayLanguage(SettingsCache::instance().cardsDisplay().getCardLang());
connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::cardLangChanged, model,
[this](const QString &lang) { model->setDisplayLanguage(lang); });
connect(model, &DeckListModel::modelReset, this, &ArchidektApiResponseDeckDisplayWidget::decklistModelReset);
auto decklist = QSharedPointer<DeckList>(new DeckList);

View file

@ -13,6 +13,7 @@
#include <libcockatrice/protocol/pb/event_list_rooms.pb.h>
#include <libcockatrice/protocol/pb/event_server_message.pb.h>
#include <libcockatrice/protocol/pb/response_join_room.pb.h>
#include <libcockatrice/protocol/pb/room_commands.pb.h>
#include <libcockatrice/protocol/pb/session_commands.pb.h>
#include <libcockatrice/protocol/pending_command.h>
@ -185,25 +186,37 @@ void TabServer::processServerMessageEvent(const Event_ServerMessage &event)
void TabServer::joinRoom(int id, bool setCurrent)
{
TabRoom *room = tabSupervisor->getRoomTabs().value(id);
if (!room) {
Command_JoinRoom cmd;
cmd.set_room_id(id);
PendingCommand *pend = client->prepareSessionCommand(cmd);
pend->setExtraData(setCurrent);
connect(pend, &PendingCommand::finished, this,
[this, id](const Response &r, const CommandContainer &c, const QVariant &v) {
joinRoomFinished(r, c, v, id);
});
client->sendCommand(pend);
if (room) {
if (setCurrent) {
tabSupervisor->setCurrentWidget((QWidget *)room);
}
return;
}
if (setCurrent) {
tabSupervisor->setCurrentWidget((QWidget *)room);
auto pendingIt = pendingRoomJoins.find(id);
if (pendingIt != pendingRoomJoins.end()) {
// A join for this room is already in flight: the room tab opens when its response
// arrives. Fold the new request into the pending one so that, for example, clicking
// a room the selector is auto-joining does not send a second Command_JoinRoom - the
// server would reject that duplicate with RespContextError.
if (setCurrent) {
pendingIt.value() = true;
}
return;
}
pendingRoomJoins.insert(id, setCurrent);
Command_JoinRoom cmd;
cmd.set_room_id(id);
PendingCommand *pend = client->prepareSessionCommand(cmd);
pend->setExtraData(setCurrent);
connect(
pend, &PendingCommand::finished, this,
[this, id](const Response &r, const CommandContainer &c, const QVariant &v) { joinRoomFinished(r, c, v, id); });
client->sendCommand(pend);
}
void TabServer::joinRoomFinished(const Response &r,
@ -211,34 +224,72 @@ void TabServer::joinRoomFinished(const Response &r,
const QVariant &extraData,
int roomId)
{
const bool setCurrent = pendingRoomJoins.value(roomId, extraData.toBool());
pendingRoomJoins.remove(roomId);
const bool healedJoin = healedRoomJoins.contains(roomId);
healedRoomJoins.remove(roomId);
switch (r.response_code()) {
case Response::RespOk:
break;
case Response::RespNameNotFound:
QMessageBox::critical(this, tr("Error"),
tr("Failed to join the server room: it doesn't exist on the server."));
if (setCurrent) {
QMessageBox::critical(this, tr("Error"),
tr("Failed to join the server room: it doesn't exist on the server."));
}
emit roomJoinFailed(roomId);
return;
case Response::RespContextError:
QMessageBox::critical(
this, tr("Error"),
tr("The server thinks you are in the server room but your client is unable to display it. "
"Try restarting your client."));
emit roomJoinFailed(roomId);
if (healedJoin) {
// The rejoin below was already answered and the server still rejects the join, so
// the stale-membership heal cannot help: surface the error. The guard was already
// released above so a later user-initiated join may try a fresh heal.
if (setCurrent) {
QMessageBox::critical(
this, tr("Error"),
tr("The server thinks you are in the server room but your client is unable to display it. "
"Try restarting your client."));
}
emit roomJoinFailed(roomId);
return;
}
// The server already had us registered in the room even though no tab was open,
// usually because two join attempts for the same room overlapped. Leaving and
// rejoining makes the server reply with a fresh RespOk so the tab is displayed
// without requiring a client restart. The guard above covers exactly the rejoin that
// leaveAndRejoinRoom triggers, so a server that keeps replying with RespContextError
// gets one heal attempt per join instead of an endless recursion.
healedRoomJoins.insert(roomId);
leaveAndRejoinRoom(roomId, setCurrent);
return;
case Response::RespUserLevelTooLow:
QMessageBox::critical(this, tr("Error"),
tr("You do not have the required permission to join this server room."));
if (setCurrent) {
QMessageBox::critical(this, tr("Error"),
tr("You do not have the required permission to join this server room."));
}
emit roomJoinFailed(roomId);
return;
default:
QMessageBox::critical(
this, tr("Error"),
tr("Failed to join the server room due to an unknown error: %1.").arg(r.response_code()));
if (setCurrent) {
QMessageBox::critical(
this, tr("Error"),
tr("Failed to join the server room due to an unknown error: %1.").arg(r.response_code()));
}
emit roomJoinFailed(roomId);
return;
}
const Response_JoinRoom &resp = r.GetExtension(Response_JoinRoom::ext);
emit roomJoined(resp.room_info(), extraData.toBool());
emit roomJoined(resp.room_info(), setCurrent);
}
void TabServer::leaveAndRejoinRoom(int roomId, bool setCurrent)
{
// Clear the stale room membership server-side. The leave is sent before the rejoin below,
// so the server no longer considers us a member by the time the join arrives. The leave
// response is intentionally not awaited: commands are processed in send order on the
// connection, and a failed leave (RespNotInRoom) only means the membership was already gone.
client->sendCommand(client->prepareRoomCommand(Command_LeaveRoom(), roomId));
joinRoom(roomId, setCurrent);
}

View file

@ -10,6 +10,8 @@
#include "tab.h"
#include <QGroupBox>
#include <QHash>
#include <QSet>
#include <QTextBrowser>
#include <QTreeWidget>
@ -58,10 +60,17 @@ private slots:
int roomId);
private:
void leaveAndRejoinRoom(int roomId, bool setCurrent);
AbstractClient *client;
RoomSelector *roomSelector;
QTextBrowser *serverInfoBox;
bool shouldEmitUpdate = false;
/** Room ids with a join command in flight, mapped to whether the tab should be focused once it opens. */
QHash<int, bool> pendingRoomJoins;
/** Room ids for which a stale-membership heal (leave + rejoin) is currently in flight. Released as soon as the
* rejoin has been answered, so a heal is attempted at most once per join. */
QSet<int> healedRoomJoins;
public:
TabServer(TabSupervisor *_tabSupervisor, AbstractClient *_client);

View file

@ -1451,6 +1451,11 @@ bool TabSupervisor::getAdminLocked() const
return tabAdmin->getLocked();
}
bool TabSupervisor::canOverrideGameRestrictions() const
{
return !getAdminLocked() || (userInfo->user_level() & ServerInfo_User::IsJudge);
}
void TabSupervisor::processNotifyUserEvent(const Event_NotifyUser &event)
{

View file

@ -171,6 +171,7 @@ public:
[[nodiscard]] QList<GameInviteOption> getGameInviteLinksForRoom(int roomId) const;
void sendInviteToUser(const QString &userName, const QString &inviteText);
[[nodiscard]] bool getAdminLocked() const;
[[nodiscard]] bool canOverrideGameRestrictions() const;
void closeEvent(QCloseEvent *event) override;
bool switchToGameTabIfAlreadyExists(const int gameId);
static void actShowPopup(const QString &message);

View file

@ -33,6 +33,7 @@
#include "../interface/widgets/dialogs/dlg_update.h"
#include "../interface/widgets/dialogs/dlg_view_log.h"
#include "../interface/widgets/onboarding/first_run_wizard.h"
#include "../interface/widgets/settings_page/general_settings_page.h"
#include "../interface/widgets/tabs/tab_game.h"
#include "../interface/widgets/tabs/tab_server.h"
#include "../interface/widgets/tabs/tab_supervisor.h"
@ -246,6 +247,8 @@ void MainWindow::actFullScreen(bool checked)
void MainWindow::actSettings()
{
DlgSettings dlg(this);
auto *generalPage = qobject_cast<GeneralSettingsPage *>(dlg.page(DlgSettings::GeneralPage));
connect(generalPage, &GeneralSettingsPage::cardDatabaseUpdateRequested, this, &MainWindow::actCheckCardUpdates);
dlg.exec();
}

View file

@ -2,7 +2,7 @@
#
# Add themes subfolders
set(defthemes Default Fabric Fusion Leather Plasma VelvetMarble)
set(defthemes Fabric Fusion Leather Plasma VelvetMarble System)
if(UNIX)
if(APPLE)

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 MiB

View file

@ -0,0 +1,74 @@
# Fabric — dark identity palette (navy-cloth chrome)
[Palette]
WindowText = #e8ecf2
Button = #2b374a
Light = #3a4759
Midlight = #333f50
Dark = #141a23
Mid = #252c38
Text = #e8ecf2
BrightText = #8ec2ff
ButtonText = #f2f4f8
Base = #222c3d
Window = #18202e
Shadow = #0b0f15
Highlight = #4a82e6
HighlightedText = #ffffff
Link = #7fb0f2
LinkVisited = #a98ad9
AlternateBase = #1d2636
ToolTipBase = #2b3546
ToolTipText = #f2f4f8
PlaceholderText = #6ee8ecf2
Accent = #4a82e6
[Palette.Disabled]
WindowText = #9d9d9d
Button = #18202e
Light = #3a4759
Midlight = #333f50
Dark = #141a23
Mid = #252c38
Text = #9d9d9d
BrightText = #8ec2ff
ButtonText = #787878
Base = #18202e
Window = #18202e
Shadow = #0b0f15
Highlight = #222d40
HighlightedText = #9d9d9d
Link = #308cc6
LinkVisited = #b450ff
AlternateBase = #1d2636
ToolTipBase = #2b3546
ToolTipText = #f2f4f8
PlaceholderText = #6ee8ecf2
Accent = #9d9d9d
[Palette.Inactive]
WindowText = #e8ecf2
Button = #2b374a
Light = #3a4759
Midlight = #333f50
Dark = #141a23
Mid = #252c38
Text = #e8ecf2
BrightText = #8ec2ff
ButtonText = #f2f4f8
Base = #222c3d
Window = #18202e
Shadow = #0b0f15
Highlight = #222d40
HighlightedText = #ffffff
Link = #7fb0f2
LinkVisited = #a98ad9
AlternateBase = #1d2636
ToolTipBase = #2b3546
ToolTipText = #f2f4f8
PlaceholderText = #6ee8ecf2
Accent = #4a82e6
[AppColors]
AccentStrong = #3a6fd6
AccentSoft = #8fb8f2

View file

@ -0,0 +1,74 @@
# Fabric — light identity palette (navy-cloth chrome)
[Palette]
WindowText = #1c2330
Button = #c8d6ea
Light = #ffffff
Midlight = #b9c6dc
Dark = #8fa0bd
Mid = #a3b3cd
Text = #1c2330
BrightText = #1d4fa0
ButtonText = #141c29
Base = #f6f8fc
Window = #dbe3ef
Shadow = #5a6a85
Highlight = #2f6fd6
HighlightedText = #ffffff
Link = #2555b0
LinkVisited = #6a4bb8
AlternateBase = #e9eef7
ToolTipBase = #e9eff8
ToolTipText = #141c29
PlaceholderText = #7a1c2330
Accent = #2f6fd6
[Palette.Disabled]
WindowText = #787878
Button = #dbe3ef
Light = #ffffff
Midlight = #b9c6dc
Dark = #8fa0bd
Mid = #a3b3cd
Text = #787878
BrightText = #1d4fa0
ButtonText = #969696
Base = #dbe3ef
Window = #dbe3ef
Shadow = #5a6a85
Highlight = #cdd8e9
HighlightedText = #787878
Link = #0000ff
LinkVisited = #ff00ff
AlternateBase = #e9eef7
ToolTipBase = #e9eff8
ToolTipText = #141c29
PlaceholderText = #7a1c2330
Accent = #787878
[Palette.Inactive]
WindowText = #1c2330
Button = #c8d6ea
Light = #ffffff
Midlight = #b9c6dc
Dark = #8fa0bd
Mid = #a3b3cd
Text = #1c2330
BrightText = #1d4fa0
ButtonText = #141c29
Base = #f6f8fc
Window = #dbe3ef
Shadow = #5a6a85
Highlight = #cdd8e9
HighlightedText = #000000
Link = #2555b0
LinkVisited = #6a4bb8
AlternateBase = #e9eef7
ToolTipBase = #e9eff8
ToolTipText = #141c29
PlaceholderText = #7a1c2330
Accent = #2f6fd6
[AppColors]
AccentStrong = #3a6fd6
AccentSoft = #8fb8f2

View file

@ -0,0 +1,5 @@
[Appearance]
ColorScheme = System
[Style]
Name = Fusion

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 313 KiB

After

Width:  |  Height:  |  Size: 105 KiB

Before After
Before After

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 147 KiB

After

Width:  |  Height:  |  Size: 104 KiB

Before After
Before After

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 154 KiB

After

Width:  |  Height:  |  Size: 101 KiB

Before After
Before After

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 257 KiB

After

Width:  |  Height:  |  Size: 102 KiB

Before After
Before After

View file

@ -11,15 +11,15 @@ ButtonText = #ffffffff
Base = #ff2d2d2d
Window = #ff1e1e1e
Shadow = #ff000000
Highlight = #ff148c3c
Highlight = #ff139740
HighlightedText = #ffffffff
Link = #ff00f652
LinkVisited = #ff00d346
Link = #ffc9fd62
LinkVisited = #ff9ad43e
AlternateBase = #ff353535
ToolTipBase = #ff3c3c3c
ToolTipText = #ffd4d4d4
PlaceholderText = #80ffffff
Accent = #ff00d346
Accent = #ffc9fd62
[Palette.Disabled]
WindowText = #ff9d9d9d
@ -34,7 +34,7 @@ ButtonText = #ff9d9d9d
Base = #ff1e1e1e
Window = #ff1e1e1e
Shadow = #ff000000
Highlight = #ff148c3c
Highlight = #ff139740
HighlightedText = #ffffffff
Link = #ff308cc6
LinkVisited = #ffff00ff
@ -59,11 +59,15 @@ Window = #ff1e1e1e
Shadow = #ff000000
Highlight = #ff1e1e1e
HighlightedText = #ffffffff
Link = #ff00f652
LinkVisited = #ff00d346
Link = #ffc9fd62
LinkVisited = #ff9ad43e
AlternateBase = #ff353535
ToolTipBase = #ff3c3c3c
ToolTipText = #ffd4d4d4
PlaceholderText = #80ffffff
Accent = #ff1e1e1e
[AppColors]
AccentStrong = #ff139740
AccentSoft = #ffc9fd62

View file

@ -11,15 +11,15 @@ ButtonText = #ff000000
Base = #ffffffff
Window = #fff0f0f0
Shadow = #ff696969
Highlight = #ff148c3c
Highlight = #ff139740
HighlightedText = #ffffffff
Link = #ff0d5f28
LinkVisited = #ff08401b
Link = #ff0e6b31
LinkVisited = #ff0a521f
AlternateBase = #ffe9e7e3
ToolTipBase = #ffffffdc
ToolTipText = #ff000000
PlaceholderText = #80000000
Accent = #ff107532
Accent = #ff139740
[Palette.Disabled]
WindowText = #ff787878
@ -34,7 +34,7 @@ ButtonText = #ff787878
Base = #fff0f0f0
Window = #fff0f0f0
Shadow = #ff000000
Highlight = #ff148c3c
Highlight = #ff139740
HighlightedText = #ffffffff
Link = #ff0000ff
LinkVisited = #ffff00ff
@ -59,11 +59,15 @@ Window = #fff0f0f0
Shadow = #ff696969
Highlight = #fff0f0f0
HighlightedText = #ff000000
Link = #ff0d5f28
LinkVisited = #ff08401b
Link = #ff0e6b31
LinkVisited = #ff0a521f
AlternateBase = #ffe9e7e3
ToolTipBase = #ffffffdc
ToolTipText = #ff000000
PlaceholderText = #80000000
Accent = #fff0f0f0
[AppColors]
AccentStrong = #ff139740
AccentSoft = #ffc9fd62

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

View file

@ -0,0 +1,74 @@
# Leather — dark identity palette (black-brown chrome with brass accents)
[Palette]
WindowText = #eee9e2
Button = #332d26
Light = #4a4239
Midlight = #3d3730
Dark = #131009
Mid = #26221c
Text = #eee9e2
BrightText = #e3c27e
ButtonText = #f5f1ea
Base = #26221d
Window = #1d1a16
Shadow = #0d0b07
Highlight = #4a5f8f
HighlightedText = #ffffff
Link = #cfa263
LinkVisited = #9a7db8
AlternateBase = #211d19
ToolTipBase = #37302a
ToolTipText = #f5f1ea
PlaceholderText = #6eeee9e2
Accent = #c9995a
[Palette.Disabled]
WindowText = #9d9d9d
Button = #1d1a16
Light = #4a4239
Midlight = #3d3730
Dark = #131009
Mid = #26221c
Text = #9d9d9d
BrightText = #e3c27e
ButtonText = #787878
Base = #1d1a16
Window = #1d1a16
Shadow = #0d0b07
Highlight = #2d2822
HighlightedText = #9d9d9d
Link = #308cc6
LinkVisited = #b450ff
AlternateBase = #211d19
ToolTipBase = #37302a
ToolTipText = #f5f1ea
PlaceholderText = #6eeee9e2
Accent = #9d9d9d
[Palette.Inactive]
WindowText = #eee9e2
Button = #332d26
Light = #4a4239
Midlight = #3d3730
Dark = #131009
Mid = #26221c
Text = #eee9e2
BrightText = #e3c27e
ButtonText = #f5f1ea
Base = #26221d
Window = #1d1a16
Shadow = #0d0b07
Highlight = #2d2822
HighlightedText = #ffffff
Link = #cfa263
LinkVisited = #9a7db8
AlternateBase = #211d19
ToolTipBase = #37302a
ToolTipText = #f5f1ea
PlaceholderText = #6eeee9e2
Accent = #c9995a
[AppColors]
AccentStrong = #b7823b
AccentSoft = #e4c58f

View file

@ -0,0 +1,74 @@
# Leather — light identity palette (black-brown chrome with brass accents)
[Palette]
WindowText = #2a2114
Button = #dfcbb0
Light = #ffffff
Midlight = #c6b08c
Dark = #a18a64
Mid = #b29a74
Text = #2a2114
BrightText = #7a531c
ButtonText = #1f1710
Base = #fdf8ee
Window = #f0e2cb
Shadow = #5d4d33
Highlight = #34508c
HighlightedText = #ffffff
Link = #8a5e1e
LinkVisited = #6b5190
AlternateBase = #f5ebd7
ToolTipBase = #fff7e8
ToolTipText = #1f1710
PlaceholderText = #7a2a2114
Accent = #a5712f
[Palette.Disabled]
WindowText = #787878
Button = #f0e2cb
Light = #ffffff
Midlight = #c6b08c
Dark = #a18a64
Mid = #b29a74
Text = #787878
BrightText = #7a531c
ButtonText = #969696
Base = #f0e2cb
Window = #f0e2cb
Shadow = #5d4d33
Highlight = #ecd9bb
HighlightedText = #787878
Link = #0000ff
LinkVisited = #ff00ff
AlternateBase = #f5ebd7
ToolTipBase = #fff7e8
ToolTipText = #1f1710
PlaceholderText = #7a2a2114
Accent = #787878
[Palette.Inactive]
WindowText = #2a2114
Button = #dfcbb0
Light = #ffffff
Midlight = #c6b08c
Dark = #a18a64
Mid = #b29a74
Text = #2a2114
BrightText = #7a531c
ButtonText = #1f1710
Base = #fdf8ee
Window = #f0e2cb
Shadow = #5d4d33
Highlight = #ecd9bb
HighlightedText = #000000
Link = #8a5e1e
LinkVisited = #6b5190
AlternateBase = #f5ebd7
ToolTipBase = #fff7e8
ToolTipText = #1f1710
PlaceholderText = #7a2a2114
Accent = #a5712f
[AppColors]
AccentStrong = #b7823b
AccentSoft = #e4c58f

View file

@ -0,0 +1,5 @@
[Appearance]
ColorScheme = System
[Style]
Name = Fusion

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 151 KiB

After

Width:  |  Height:  |  Size: 112 KiB

Before After
Before After

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 160 KiB

After

Width:  |  Height:  |  Size: 112 KiB

Before After
Before After

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 165 KiB

After

Width:  |  Height:  |  Size: 112 KiB

Before After
Before After

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 174 KiB

After

Width:  |  Height:  |  Size: 112 KiB

Before After
Before After

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

View file

@ -0,0 +1,74 @@
# Plasma — dark identity palette (electric violet chrome with cyan sparks)
[Palette]
WindowText = #eeebff
Button = #2a2440
Light = #453d63
Midlight = #383252
Dark = #0e0c16
Mid = #211e31
Text = #eeebff
BrightText = #7fd7ff
ButtonText = #f6f4ff
Base = #1e1a2e
Window = #151220
Shadow = #08070e
Highlight = #6a4df0
HighlightedText = #ffffff
Link = #37c7e8
LinkVisited = #9b7aff
AlternateBase = #1a1726
ToolTipBase = #2c2745
ToolTipText = #f6f4ff
PlaceholderText = #6eeeebff
Accent = #6a4df0
[Palette.Disabled]
WindowText = #9d9d9d
Button = #151220
Light = #453d63
Midlight = #383252
Dark = #0e0c16
Mid = #211e31
Text = #9d9d9d
BrightText = #7fd7ff
ButtonText = #787878
Base = #151220
Window = #151220
Shadow = #08070e
Highlight = #211c32
HighlightedText = #9d9d9d
Link = #308cc6
LinkVisited = #b450ff
AlternateBase = #1a1726
ToolTipBase = #2c2745
ToolTipText = #f6f4ff
PlaceholderText = #6eeeebff
Accent = #9d9d9d
[Palette.Inactive]
WindowText = #eeebff
Button = #2a2440
Light = #453d63
Midlight = #383252
Dark = #0e0c16
Mid = #211e31
Text = #eeebff
BrightText = #7fd7ff
ButtonText = #f6f4ff
Base = #1e1a2e
Window = #151220
Shadow = #08070e
Highlight = #211c32
HighlightedText = #ffffff
Link = #37c7e8
LinkVisited = #9b7aff
AlternateBase = #1a1726
ToolTipBase = #2c2745
ToolTipText = #f6f4ff
PlaceholderText = #6eeeebff
Accent = #6a4df0
[AppColors]
AccentStrong = #5b3ee0
AccentSoft = #a28bf7

View file

@ -0,0 +1,74 @@
# Plasma — light identity palette (electric violet chrome with cyan sparks)
[Palette]
WindowText = #1d1830
Button = #cfc6f0
Light = #ffffff
Midlight = #b7adde
Dark = #9589c8
Mid = #a49ad2
Text = #1d1830
BrightText = #10406e
ButtonText = #120d26
Base = #f9f7ff
Window = #e3dcf7
Shadow = #554c85
Highlight = #5a3ee2
HighlightedText = #ffffff
Link = #0f8fb5
LinkVisited = #6a45d6
AlternateBase = #ece8fa
ToolTipBase = #f5f3ff
ToolTipText = #120d26
PlaceholderText = #7a1d1830
Accent = #5a3ee2
[Palette.Disabled]
WindowText = #787878
Button = #e3dcf7
Light = #ffffff
Midlight = #b7adde
Dark = #9589c8
Mid = #a49ad2
Text = #787878
BrightText = #10406e
ButtonText = #969696
Base = #e3dcf7
Window = #e3dcf7
Shadow = #554c85
Highlight = #d6ccf3
HighlightedText = #787878
Link = #0000ff
LinkVisited = #ff00ff
AlternateBase = #ece8fa
ToolTipBase = #f5f3ff
ToolTipText = #120d26
PlaceholderText = #7a1d1830
Accent = #787878
[Palette.Inactive]
WindowText = #1d1830
Button = #cfc6f0
Light = #ffffff
Midlight = #b7adde
Dark = #9589c8
Mid = #a49ad2
Text = #1d1830
BrightText = #10406e
ButtonText = #120d26
Base = #f9f7ff
Window = #e3dcf7
Shadow = #554c85
Highlight = #d6ccf3
HighlightedText = #000000
Link = #0f8fb5
LinkVisited = #6a45d6
AlternateBase = #ece8fa
ToolTipBase = #f5f3ff
ToolTipText = #120d26
PlaceholderText = #7a1d1830
Accent = #5a3ee2
[AppColors]
AccentStrong = #5b3ee0
AccentSoft = #a28bf7

View file

@ -0,0 +1,5 @@
[Appearance]
ColorScheme = System
[Style]
Name = Fusion

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Before After
Before After

Binary file not shown.

After

Width:  |  Height:  |  Size: 2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Before After
Before After

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Before After
Before After

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Before After
Before After

View file

@ -61,3 +61,7 @@ ToolTipBase = #ffffffdc
ToolTipText = #ff000000
PlaceholderText = #6effffff
[AppColors]
AccentStrong = #ff148c3c
AccentSoft = #ff78c850

View file

@ -0,0 +1,67 @@
[Palette]
WindowText = #ff000000
Button = #fff0f0f0
Light = #ffffffff
Midlight = #ffe3e3e3
Dark = #ffa0a0a0
Mid = #ffa0a0a0
Text = #ff000000
BrightText = #ffffffff
ButtonText = #ff000000
Base = #ffffffff
Window = #fff0f0f0
Shadow = #ff696969
HighlightedText = #ffffffff
Link = #ff0d5f28
LinkVisited = #ff08401b
AlternateBase = #ffe9e7e3
ToolTipBase = #ffffffdc
ToolTipText = #ff000000
PlaceholderText = #80000000
[Palette.Disabled]
WindowText = #ff787878
Button = #fff0f0f0
Light = #ffffffff
Midlight = #fff7f7f7
Dark = #ffa0a0a0
Mid = #ffa0a0a0
Text = #ff787878
BrightText = #ffffffff
ButtonText = #ff787878
Base = #fff0f0f0
Window = #fff0f0f0
Shadow = #ff000000
HighlightedText = #ffffffff
Link = #ff0000ff
LinkVisited = #ffff00ff
AlternateBase = #fff7f7f7
ToolTipBase = #ffffffdc
ToolTipText = #ff000000
PlaceholderText = #80000000
[Palette.Inactive]
WindowText = #ff000000
Button = #fff0f0f0
Light = #ffffffff
Midlight = #ffe3e3e3
Dark = #ffa0a0a0
Mid = #ffa0a0a0
Text = #ff000000
BrightText = #ffffffff
ButtonText = #ff000000
Base = #ffffffff
Window = #fff0f0f0
Shadow = #ff696969
HighlightedText = #ff000000
Link = #ff0d5f28
LinkVisited = #ff08401b
AlternateBase = #ffe9e7e3
ToolTipBase = #ffffffdc
ToolTipText = #ff000000
PlaceholderText = #80000000
[AppColors]
AccentStrong = #ff148c3c
AccentSoft = #ff78c850

View file

@ -2,4 +2,4 @@
ColorScheme = Light
[Style]
Name = Default
Name = System

Some files were not shown because too many files have changed in this diff Show more