mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-09-21 09:05:10 -07:00
Compare commits
No commits in common. "master" and "2026-08-25-Development-3.1.0-beta.9" have entirely different histories.
master
...
2026-08-25
473 changed files with 3199 additions and 20160 deletions
|
|
@ -8,7 +8,6 @@ RUN pacman --sync --refresh --sysupgrade --needed --noconfirm \
|
||||||
gtest \
|
gtest \
|
||||||
mariadb-libs \
|
mariadb-libs \
|
||||||
ninja \
|
ninja \
|
||||||
openssl \
|
|
||||||
protobuf \
|
protobuf \
|
||||||
qt6-base \
|
qt6-base \
|
||||||
qt6-declarative \
|
qt6-declarative \
|
||||||
|
|
|
||||||
|
|
@ -15,15 +15,14 @@ RUN apt-get update && \
|
||||||
libprotobuf-dev \
|
libprotobuf-dev \
|
||||||
libqt6multimedia6 \
|
libqt6multimedia6 \
|
||||||
libqt6sql6-mysql \
|
libqt6sql6-mysql \
|
||||||
libssl-dev \
|
|
||||||
ninja-build \
|
ninja-build \
|
||||||
protobuf-compiler \
|
protobuf-compiler \
|
||||||
qt6-declarative-dev \
|
|
||||||
qt6-image-formats-plugins \
|
qt6-image-formats-plugins \
|
||||||
qt6-l10n-tools \
|
qt6-l10n-tools \
|
||||||
qt6-multimedia-dev \
|
qt6-multimedia-dev \
|
||||||
qt6-shadertools-dev \
|
qt6-declarative-dev \
|
||||||
qt6-svg-dev \
|
qt6-svg-dev \
|
||||||
|
qt6-shadertools-dev \
|
||||||
qt6-tools-dev \
|
qt6-tools-dev \
|
||||||
qt6-tools-dev-tools \
|
qt6-tools-dev-tools \
|
||||||
qt6-websockets-dev \
|
qt6-websockets-dev \
|
||||||
|
|
|
||||||
|
|
@ -16,15 +16,14 @@ RUN apt-get update && \
|
||||||
libprotobuf-dev \
|
libprotobuf-dev \
|
||||||
libqt6multimedia6 \
|
libqt6multimedia6 \
|
||||||
libqt6sql6-mysql \
|
libqt6sql6-mysql \
|
||||||
libssl-dev \
|
|
||||||
ninja-build \
|
ninja-build \
|
||||||
protobuf-compiler \
|
protobuf-compiler \
|
||||||
qt6-declarative-dev \
|
|
||||||
qt6-image-formats-plugins \
|
qt6-image-formats-plugins \
|
||||||
qt6-l10n-tools \
|
qt6-l10n-tools \
|
||||||
qt6-multimedia-dev \
|
qt6-multimedia-dev \
|
||||||
qt6-shadertools-dev \
|
qt6-declarative-dev \
|
||||||
qt6-svg-dev \
|
qt6-svg-dev \
|
||||||
|
qt6-shadertools-dev \
|
||||||
qt6-tools-dev \
|
qt6-tools-dev \
|
||||||
qt6-tools-dev-tools \
|
qt6-tools-dev-tools \
|
||||||
qt6-websockets-dev \
|
qt6-websockets-dev \
|
||||||
|
|
|
||||||
|
|
@ -7,9 +7,8 @@ RUN dnf install -y \
|
||||||
git \
|
git \
|
||||||
mariadb-devel \
|
mariadb-devel \
|
||||||
ninja-build \
|
ninja-build \
|
||||||
openssl-devel \
|
|
||||||
protobuf-devel \
|
protobuf-devel \
|
||||||
qt6-{qtdeclarative,qtmultimedia,qtshadertools,qtsvg,qttools,qtwebsockets}-devel \
|
qt6-{qtdeclarative,qtshadertools,qttools,qtsvg,qtmultimedia,qtwebsockets}-devel \
|
||||||
qt6-qtimageformats \
|
qt6-qtimageformats \
|
||||||
rpm-build \
|
rpm-build \
|
||||||
xz-devel \
|
xz-devel \
|
||||||
|
|
|
||||||
|
|
@ -7,9 +7,8 @@ RUN dnf install -y \
|
||||||
git \
|
git \
|
||||||
mariadb-devel \
|
mariadb-devel \
|
||||||
ninja-build \
|
ninja-build \
|
||||||
openssl-devel \
|
|
||||||
protobuf-devel \
|
protobuf-devel \
|
||||||
qt6-{qtdeclarative,qtmultimedia,qtshadertools,qtsvg,qttools,qtwebsockets}-devel \
|
qt6-{qtdeclarative,qtshadertools,qttools,qtsvg,qtmultimedia,qtwebsockets}-devel \
|
||||||
qt6-qtimageformats \
|
qt6-qtimageformats \
|
||||||
rpm-build \
|
rpm-build \
|
||||||
xz-devel \
|
xz-devel \
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,6 @@ RUN apt-get update && \
|
||||||
libmariadb-dev-compat \
|
libmariadb-dev-compat \
|
||||||
libprotobuf-dev \
|
libprotobuf-dev \
|
||||||
libqt6sql6-mysql \
|
libqt6sql6-mysql \
|
||||||
libssl-dev \
|
|
||||||
ninja-build \
|
ninja-build \
|
||||||
protobuf-compiler \
|
protobuf-compiler \
|
||||||
qt6-tools-dev \
|
qt6-tools-dev \
|
||||||
|
|
|
||||||
|
|
@ -15,15 +15,14 @@ RUN apt-get update && \
|
||||||
libprotobuf-dev \
|
libprotobuf-dev \
|
||||||
libqt6multimedia6 \
|
libqt6multimedia6 \
|
||||||
libqt6sql6-mysql \
|
libqt6sql6-mysql \
|
||||||
libssl-dev \
|
|
||||||
ninja-build \
|
ninja-build \
|
||||||
protobuf-compiler \
|
protobuf-compiler \
|
||||||
qt6-declarative-dev \
|
|
||||||
qt6-image-formats-plugins \
|
qt6-image-formats-plugins \
|
||||||
qt6-l10n-tools \
|
qt6-l10n-tools \
|
||||||
qt6-multimedia-dev \
|
qt6-multimedia-dev \
|
||||||
qt6-shadertools-dev \
|
qt6-declarative-dev \
|
||||||
qt6-svg-dev \
|
qt6-svg-dev \
|
||||||
|
qt6-shadertools-dev \
|
||||||
qt6-tools-dev \
|
qt6-tools-dev \
|
||||||
qt6-tools-dev-tools \
|
qt6-tools-dev-tools \
|
||||||
qt6-websockets-dev \
|
qt6-websockets-dev \
|
||||||
|
|
|
||||||
|
|
@ -16,15 +16,14 @@ RUN apt-get update && \
|
||||||
libprotobuf-dev \
|
libprotobuf-dev \
|
||||||
libqt6multimedia6 \
|
libqt6multimedia6 \
|
||||||
libqt6sql6-mysql \
|
libqt6sql6-mysql \
|
||||||
libssl-dev \
|
|
||||||
ninja-build \
|
ninja-build \
|
||||||
protobuf-compiler \
|
protobuf-compiler \
|
||||||
qt6-declarative-dev \
|
|
||||||
qt6-image-formats-plugins \
|
qt6-image-formats-plugins \
|
||||||
qt6-l10n-tools \
|
qt6-l10n-tools \
|
||||||
qt6-multimedia-dev \
|
qt6-multimedia-dev \
|
||||||
qt6-shadertools-dev \
|
qt6-declarative-dev \
|
||||||
qt6-svg-dev \
|
qt6-svg-dev \
|
||||||
|
qt6-shadertools-dev \
|
||||||
qt6-tools-dev \
|
qt6-tools-dev \
|
||||||
qt6-tools-dev-tools \
|
qt6-tools-dev-tools \
|
||||||
qt6-websockets-dev \
|
qt6-websockets-dev \
|
||||||
|
|
|
||||||
|
|
@ -149,9 +149,6 @@ if [[ $MAKE_TEST ]]; then
|
||||||
fi
|
fi
|
||||||
if [[ $USE_CCACHE ]]; then
|
if [[ $USE_CCACHE ]]; then
|
||||||
flags+=("-DUSE_CCACHE=1")
|
flags+=("-DUSE_CCACHE=1")
|
||||||
# PCH-aware caching is required or ccache refuses to cache any TU that
|
|
||||||
# consumes a precompiled header, silently recompiling everything on every run.
|
|
||||||
ccache --set-config sloppiness=pch_defines,time_macros
|
|
||||||
if [[ $CCACHE_SIZE ]]; then
|
if [[ $CCACHE_SIZE ]]; then
|
||||||
# note, this setting persists after running the script
|
# note, this setting persists after running the script
|
||||||
ccache --max-size "$CCACHE_SIZE"
|
ccache --max-size "$CCACHE_SIZE"
|
||||||
|
|
@ -327,32 +324,4 @@ if [[ $MAKE_PACKAGE ]]; then
|
||||||
BUILD_DIR="$BUILD_DIR" .ci/name_build.sh "$PACKAGE_SUFFIX"
|
BUILD_DIR="$BUILD_DIR" .ci/name_build.sh "$PACKAGE_SUFFIX"
|
||||||
echo "::endgroup::"
|
echo "::endgroup::"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ $RUNNER_OS == Windows ]]; then
|
|
||||||
echo "::group::Check installer for build-tree artifacts"
|
|
||||||
cd "$BUILD_DIR"
|
|
||||||
package="$(find . -maxdepth 1 -type f -name 'Cockatrice-*.exe' -print -quit)"
|
|
||||||
if [[ ! $package ]]; then
|
|
||||||
echo "::error file=$0::Could not find installer to inspect"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
seven_zip="$(command -v 7z || true)"
|
|
||||||
if [[ ! $seven_zip ]]; then
|
|
||||||
seven_zip="/c/Program Files/7-Zip/7z.exe"
|
|
||||||
fi
|
|
||||||
if [[ ! -f $seven_zip ]]; then
|
|
||||||
echo "::warning file=$0::7-Zip not found, skipping installer content check"
|
|
||||||
else
|
|
||||||
echo "Inspecting $package"
|
|
||||||
# Fail the build if the installer contains any path left behind by the MSBuild or
|
|
||||||
# Qt AUTOMOC tooling (build-tree artifacts must live in the build dir, not the install)
|
|
||||||
if "$seven_zip" l "$package" |
|
|
||||||
grep -E "_autogen|\.dir[\\/]|\.tlog|(^|[\\/])x64[\\/]|(^|[\\/])\.qt[\\/]|(^|[\\/])\.qsb[\\/]|(^|[\\/])\.lupdate[\\/]|CMakeFiles"; then
|
|
||||||
echo "::error file=$0::Installer contains build-tree artifacts"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
echo "Installer content is clean"
|
|
||||||
fi
|
|
||||||
echo "::endgroup::"
|
|
||||||
fi
|
|
||||||
fi
|
fi
|
||||||
|
|
|
||||||
2
.github/workflows/codeql.yml
vendored
2
.github/workflows/codeql.yml
vendored
|
|
@ -40,7 +40,7 @@ jobs:
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: "Checkout repository"
|
- name: "Checkout repository"
|
||||||
uses: actions/checkout@v7
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: "Initialize CodeQL"
|
- name: "Initialize CodeQL"
|
||||||
uses: github/codeql-action/init@v4
|
uses: github/codeql-action/init@v4
|
||||||
|
|
|
||||||
55
.github/workflows/desktop-build.yml
vendored
55
.github/workflows/desktop-build.yml
vendored
|
|
@ -152,7 +152,7 @@ jobs:
|
||||||
env:
|
env:
|
||||||
CACHE: ${{ github.workspace }}/.cache/${{ matrix.distro }}${{ matrix.version }} # directory for caching docker image and ccache
|
CACHE: ${{ github.workspace }}/.cache/${{ matrix.distro }}${{ matrix.version }} # directory for caching docker image and ccache
|
||||||
CCACHE_EVICTION_AGE: 7d
|
CCACHE_EVICTION_AGE: 7d
|
||||||
CCACHE_SIZE: 600M # space of all repo is 10Gi: https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#usage-limits-and-eviction-policy
|
CCACHE_SIZE: 550M # space of all repo is 10Gi: https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#usage-limits-and-eviction-policy
|
||||||
CMAKE_GENERATOR: 'Ninja'
|
CMAKE_GENERATOR: 'Ninja'
|
||||||
NAME: ${{ matrix.distro }}${{ matrix.version }}
|
NAME: ${{ matrix.distro }}${{ matrix.version }}
|
||||||
|
|
||||||
|
|
@ -176,12 +176,8 @@ jobs:
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
source .ci/docker.sh
|
source .ci/docker.sh
|
||||||
args=()
|
RUN --server --debug --test --ccache "$CCACHE_SIZE" \
|
||||||
[[ $GITHUB_REF == "refs/heads/master" ]] && args+=(--evict-ccache "$CCACHE_EVICTION_AGE")
|
--cmake-generator "$CMAKE_GENERATOR"
|
||||||
args+=(--ccache "$CCACHE_SIZE")
|
|
||||||
args+=(--cmake-generator "$CMAKE_GENERATOR")
|
|
||||||
|
|
||||||
RUN --server --debug --test "${args[@]}"
|
|
||||||
|
|
||||||
- name: "Build release package"
|
- name: "Build release package"
|
||||||
id: build
|
id: build
|
||||||
|
|
@ -272,8 +268,8 @@ jobs:
|
||||||
make_package: 1
|
make_package: 1
|
||||||
override_target: 13
|
override_target: 13
|
||||||
package_suffix: "-macOS13_Intel"
|
package_suffix: "-macOS13_Intel"
|
||||||
qt_version: 6.11.*
|
qt_version: 6.11.1
|
||||||
qt_modules: qtimageformats qtmultimedia qtshadertools qtwebsockets
|
qt_modules: qtimageformats qtmultimedia qtwebsockets qtshadertools
|
||||||
soc: Intel
|
soc: Intel
|
||||||
type: Release
|
type: Release
|
||||||
use_ccache: 1
|
use_ccache: 1
|
||||||
|
|
@ -288,8 +284,8 @@ jobs:
|
||||||
make_package: 1
|
make_package: 1
|
||||||
override_target: 14
|
override_target: 14
|
||||||
package_suffix: "-macOS14"
|
package_suffix: "-macOS14"
|
||||||
qt_version: 6.11.*
|
qt_version: 6.11.1
|
||||||
qt_modules: qtimageformats qtmultimedia qtshadertools qtwebsockets
|
qt_modules: qtimageformats qtmultimedia qtwebsockets qtshadertools
|
||||||
soc: Apple
|
soc: Apple
|
||||||
type: Release
|
type: Release
|
||||||
use_ccache: 1
|
use_ccache: 1
|
||||||
|
|
@ -304,8 +300,8 @@ jobs:
|
||||||
make_package: 1
|
make_package: 1
|
||||||
override_target: 15
|
override_target: 15
|
||||||
package_suffix: "-macOS15"
|
package_suffix: "-macOS15"
|
||||||
qt_version: 6.11.*
|
qt_version: 6.11.1
|
||||||
qt_modules: qtimageformats qtmultimedia qtshadertools qtwebsockets
|
qt_modules: qtimageformats qtmultimedia qtwebsockets qtshadertools
|
||||||
soc: Apple
|
soc: Apple
|
||||||
type: Release
|
type: Release
|
||||||
use_ccache: 1
|
use_ccache: 1
|
||||||
|
|
@ -317,8 +313,8 @@ jobs:
|
||||||
|
|
||||||
ccache_eviction_age: 7d
|
ccache_eviction_age: 7d
|
||||||
cmake_generator: Ninja
|
cmake_generator: Ninja
|
||||||
qt_version: 6.11.*
|
qt_version: 6.11.1
|
||||||
qt_modules: qtimageformats qtmultimedia qtshadertools qtwebsockets
|
qt_modules: qtimageformats qtmultimedia qtwebsockets qtshadertools
|
||||||
soc: Apple
|
soc: Apple
|
||||||
type: Debug
|
type: Debug
|
||||||
use_ccache: 1
|
use_ccache: 1
|
||||||
|
|
@ -332,8 +328,8 @@ jobs:
|
||||||
cmake_generator_platform: x64
|
cmake_generator_platform: x64
|
||||||
make_package: 1
|
make_package: 1
|
||||||
package_suffix: "-Win10"
|
package_suffix: "-Win10"
|
||||||
qt_version: 6.11.*
|
qt_version: 6.11.1
|
||||||
qt_modules: qtimageformats qtmultimedia qtshadertools qtwebsockets
|
qt_modules: qtimageformats qtmultimedia qtwebsockets qtshadertools
|
||||||
type: Release
|
type: Release
|
||||||
|
|
||||||
name: ${{ matrix.os }} ${{ matrix.target }}${{ matrix.soc == 'Intel' && ' Intel' || '' }}${{ matrix.type == 'Debug' && ' Debug' || '' }}
|
name: ${{ matrix.os }} ${{ matrix.target }}${{ matrix.soc == 'Intel' && ' Intel' || '' }}${{ matrix.type == 'Debug' && ' Debug' || '' }}
|
||||||
|
|
@ -342,7 +338,7 @@ jobs:
|
||||||
timeout-minutes: 100
|
timeout-minutes: 100
|
||||||
env:
|
env:
|
||||||
CCACHE_DIR: ${{ github.workspace }}/.cache/
|
CCACHE_DIR: ${{ github.workspace }}/.cache/
|
||||||
CCACHE_SIZE: 600M # space of all repo is 10Gi: https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#usage-limits-and-eviction-policy
|
CCACHE_SIZE: 550M # space of all repo is 10Gi: https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#usage-limits-and-eviction-policy
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: "Checkout"
|
- name: "Checkout"
|
||||||
|
|
@ -368,20 +364,18 @@ jobs:
|
||||||
key: ccache-${{ matrix.runner }}_${{ matrix.override_target }}-Xcode${{ matrix.xcode }}
|
key: ccache-${{ matrix.runner }}_${{ matrix.override_target }}-Xcode${{ matrix.xcode }}
|
||||||
path: ${{ env.CCACHE_DIR }}
|
path: ${{ env.CCACHE_DIR }}
|
||||||
|
|
||||||
- name: "[macOS] Install aqtinstall"
|
- name: "Install aqtinstall"
|
||||||
if: matrix.os == 'macOS'
|
|
||||||
run: pipx install aqtinstall
|
run: pipx install aqtinstall
|
||||||
|
|
||||||
# Resolve given wildcard versions (e.g. Qt 6.6.*) to latest version via aqtinstall to avoid stale caches on new releases
|
# Resolve given wildcard versions (e.g. Qt 6.6.*) to latest version via aqtinstall to avoid stale caches on new releases
|
||||||
- name: "[macOS] Resolve latest Qt from ${{ matrix.qt_version }} input"
|
- name: "Resolve latest Qt patch version"
|
||||||
if: matrix.os == 'macOS'
|
|
||||||
env:
|
env:
|
||||||
QT_VERSION: ${{ matrix.qt_version }}
|
QT_VERSION: ${{ matrix.qt_version }}
|
||||||
id: resolve_qt_version
|
id: resolve_qt_version
|
||||||
shell: bash
|
shell: bash
|
||||||
run: .ci/resolve_latest_aqt_qt_version.sh "$QT_VERSION"
|
run: .ci/resolve_latest_aqt_qt_version.sh "$QT_VERSION"
|
||||||
|
|
||||||
- name: "[macOS] Restore thin Qt ${{ steps.resolve_qt_version.outputs.version }}"
|
- name: "[macOS] Restore thin Qt ${{ steps.resolve_qt_version.outputs.version }} libraries"
|
||||||
if: matrix.os == 'macOS'
|
if: matrix.os == 'macOS'
|
||||||
id: restore_qt
|
id: restore_qt
|
||||||
uses: actions/cache/restore@v6
|
uses: actions/cache/restore@v6
|
||||||
|
|
@ -391,22 +385,21 @@ jobs:
|
||||||
|
|
||||||
# Using jurplel/install-qt-action to install Qt without using brew
|
# Using jurplel/install-qt-action to install Qt without using brew
|
||||||
# Qt build using vcpkg either just fails or takes too long to build
|
# Qt build using vcpkg either just fails or takes too long to build
|
||||||
- name: "[macOS] Install fat Qt ${{ matrix.qt_version }}"
|
- name: "[macOS] Install fat Qt ${{ steps.resolve_qt_version.outputs.version }}"
|
||||||
if: matrix.os == 'macOS' && steps.restore_qt.outputs.cache-hit != 'true'
|
if: matrix.os == 'macOS' && steps.restore_qt.outputs.cache-hit != 'true'
|
||||||
uses: jurplel/install-qt-action@v4
|
uses: jurplel/install-qt-action@v4
|
||||||
with:
|
with:
|
||||||
cache: false
|
cache: false
|
||||||
# cache-key-prefix: Qt
|
dir: ${{ github.workspace }}
|
||||||
dir: ${{ github.workspace }} # thinning script depends on this location
|
|
||||||
modules: ${{ matrix.qt_modules }}
|
modules: ${{ matrix.qt_modules }}
|
||||||
version: ${{ matrix.qt_version }}
|
version: ${{ steps.resolve_qt_version.outputs.version }}
|
||||||
|
|
||||||
- name: "[macOS] Create thin Qt libraries"
|
- name: "[macOS] Create thin Qt libraries"
|
||||||
if: matrix.os == 'macOS' && steps.restore_qt.outputs.cache-hit != 'true'
|
if: matrix.os == 'macOS' && steps.restore_qt.outputs.cache-hit != 'true'
|
||||||
run: .ci/thin_macos_qtlib.sh
|
run: .ci/thin_macos_qtlib.sh
|
||||||
|
|
||||||
- name: "[macOS] Cache thin Qt libraries"
|
- name: "[macOS] Cache thin Qt libraries"
|
||||||
if: matrix.os == 'macOS' && steps.restore_qt.outputs.cache-hit != 'true' && github.ref == 'refs/heads/master'
|
if: matrix.os == 'macOS' && steps.restore_qt.outputs.cache-hit != 'true'
|
||||||
uses: actions/cache/save@v6
|
uses: actions/cache/save@v6
|
||||||
with:
|
with:
|
||||||
key: ${{ steps.restore_qt.outputs.cache-primary-key }}
|
key: ${{ steps.restore_qt.outputs.cache-primary-key }}
|
||||||
|
|
@ -418,10 +411,10 @@ jobs:
|
||||||
with:
|
with:
|
||||||
# Qt 6.11.0 only works with aqtinstall directly from git until aqtinstall 3.4 is released
|
# Qt 6.11.0 only works with aqtinstall directly from git until aqtinstall 3.4 is released
|
||||||
aqtsource: git+https://github.com/miurahr/aqtinstall.git
|
aqtsource: git+https://github.com/miurahr/aqtinstall.git
|
||||||
cache: ${{ github.ref == 'refs/heads/master' }}
|
cache: true
|
||||||
cache-key-prefix: Qt
|
cache-key-prefix: Qt
|
||||||
modules: ${{ matrix.qt_modules }}
|
modules: ${{ matrix.qt_modules }}
|
||||||
version: ${{ matrix.qt_version }}
|
version: ${{ steps.resolve_qt_version.outputs.version }}
|
||||||
|
|
||||||
- name: "[Windows] Install NSIS"
|
- name: "[Windows] Install NSIS"
|
||||||
if: matrix.os == 'Windows'
|
if: matrix.os == 'Windows'
|
||||||
|
|
@ -453,7 +446,7 @@ jobs:
|
||||||
PACKAGE_SUFFIX: '${{ matrix.package_suffix }}'
|
PACKAGE_SUFFIX: '${{ matrix.package_suffix }}'
|
||||||
TARGET_MACOS_VERSION: ${{ matrix.override_target }}
|
TARGET_MACOS_VERSION: ${{ matrix.override_target }}
|
||||||
USE_CCACHE: ${{ matrix.use_ccache }}
|
USE_CCACHE: ${{ matrix.use_ccache }}
|
||||||
VCPKG_BINARY_SOURCES: "clear;files,${{ steps.vcpkg-cache.outputs.path }},${{ case(github.ref == 'refs/heads/master', 'readwrite', 'read') }}"
|
VCPKG_BINARY_SOURCES: 'clear;files,${{ steps.vcpkg-cache.outputs.path }},readwrite'
|
||||||
VCPKG_DISABLE_METRICS: 1
|
VCPKG_DISABLE_METRICS: 1
|
||||||
VCPKG_FEATURE_FLAGS: dependencygraph
|
VCPKG_FEATURE_FLAGS: dependencygraph
|
||||||
run: .ci/compile.sh --server --test --vcpkg
|
run: .ci/compile.sh --server --test --vcpkg
|
||||||
|
|
|
||||||
4
.github/workflows/docker-release.yml
vendored
4
.github/workflows/docker-release.yml
vendored
|
|
@ -76,7 +76,7 @@ jobs:
|
||||||
uses: docker/build-push-action@v7
|
uses: docker/build-push-action@v7
|
||||||
with:
|
with:
|
||||||
cache-from: type=gha,scope=${{ env.CACHE_SCOPE }}
|
cache-from: type=gha,scope=${{ env.CACHE_SCOPE }}
|
||||||
cache-to: ${{ case(github.ref == 'refs/heads/master', format('type=gha,mode=max,scope={0}', env.CACHE_SCOPE), '') }}
|
cache-to: type=gha,mode=max,scope=${{ env.CACHE_SCOPE }}
|
||||||
context: .
|
context: .
|
||||||
platforms: ${{ matrix.platform }}
|
platforms: ${{ matrix.platform }}
|
||||||
push: false
|
push: false
|
||||||
|
|
@ -127,7 +127,7 @@ jobs:
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: "Download digests"
|
- name: "Download digests"
|
||||||
uses: actions/download-artifact@v8
|
uses: actions/download-artifact@v7
|
||||||
with:
|
with:
|
||||||
path: ${{ runner.temp }}/digests
|
path: ${{ runner.temp }}/digests
|
||||||
pattern: digest-*
|
pattern: digest-*
|
||||||
|
|
|
||||||
|
|
@ -5,23 +5,23 @@
|
||||||
# This file sets all the variables shared between the projects
|
# This file sets all the variables shared between the projects
|
||||||
# like the installation path, compilation flags etc..
|
# like the installation path, compilation flags etc..
|
||||||
|
|
||||||
# 3.16 required for Qt6 and target_precompile_headers()
|
# cmake 3.16 is required if using qt6
|
||||||
cmake_minimum_required(VERSION 3.16)
|
cmake_minimum_required(VERSION 3.10)
|
||||||
|
|
||||||
# Use compiler cache (ccache)
|
# Early detect ccache
|
||||||
option(USE_CCACHE "Cache the build results with ccache" ON)
|
option(USE_CCACHE "Cache the build results with ccache" ON)
|
||||||
# Treat warnings as errors (Debug builds only)
|
# Treat warnings as errors (Debug builds only)
|
||||||
option(WARNING_AS_ERROR "Treat warnings as errors in debug builds" ON)
|
option(WARNING_AS_ERROR "Treat warnings as errors in debug builds" ON)
|
||||||
# Check for translation updates
|
# Check for translation updates
|
||||||
option(UPDATE_TRANSLATIONS "Update translations on compile" OFF)
|
option(UPDATE_TRANSLATIONS "Update translations on compile" OFF)
|
||||||
# Compile Cockatrice
|
# Compile servatrice
|
||||||
option(WITH_CLIENT "Build Cockatrice client" ON)
|
option(WITH_SERVER "build servatrice" OFF)
|
||||||
# Compile Oracle
|
# Compile cockatrice
|
||||||
option(WITH_ORACLE "Build Cockatrice card database tool (Oracle)" ON)
|
option(WITH_CLIENT "build cockatrice" ON)
|
||||||
# Compile Servatrice
|
# Compile oracle
|
||||||
option(WITH_SERVER "Build Cockatrice server (Servatrice)" OFF)
|
option(WITH_ORACLE "build oracle" ON)
|
||||||
# Compile tests
|
# Compile tests
|
||||||
option(TEST "Build tests" OFF)
|
option(TEST "build tests" OFF)
|
||||||
# Use vcpkg regardless of OS
|
# Use vcpkg regardless of OS
|
||||||
option(USE_VCPKG "Use vcpkg regardless of OS" OFF)
|
option(USE_VCPKG "Use vcpkg regardless of OS" OFF)
|
||||||
|
|
||||||
|
|
@ -39,24 +39,13 @@ else()
|
||||||
)
|
)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
# ccache does not support MSVC and must not auto-engage on Windows
|
if(USE_CCACHE)
|
||||||
# (it is installed unintentionally on the Windows CI runner).
|
|
||||||
# NOTE: this keys off the target OS, so a mingw/Ninja configuration on Windows
|
|
||||||
# also opts out of ccache even though the GNUCXX branch below supports it.
|
|
||||||
if(USE_CCACHE AND NOT WIN32)
|
|
||||||
find_program(CCACHE_PROGRAM ccache)
|
find_program(CCACHE_PROGRAM ccache)
|
||||||
if(CCACHE_PROGRAM)
|
if(CCACHE_PROGRAM)
|
||||||
# Support Unix Makefiles and Ninja
|
# Support Unix Makefiles and Ninja
|
||||||
set_property(GLOBAL PROPERTY RULE_LAUNCH_COMPILE "${CCACHE_PROGRAM}")
|
set_property(GLOBAL PROPERTY RULE_LAUNCH_COMPILE "${CCACHE_PROGRAM}")
|
||||||
# PCH-aware caching, matching .ci/compile.sh: without this ccache refuses
|
|
||||||
# to cache any TU that consumes a precompiled header, so every PCH-backed
|
|
||||||
# target recompiles from scratch on each build.
|
|
||||||
execute_process(COMMAND ${CCACHE_PROGRAM} --set-config sloppiness=pch_defines,time_macros)
|
|
||||||
message(STATUS "Found CCache ${CCACHE_PROGRAM}")
|
message(STATUS "Found CCache ${CCACHE_PROGRAM}")
|
||||||
endif()
|
endif()
|
||||||
elseif(USE_CCACHE AND WIN32)
|
|
||||||
# An explicit opt-in must not disappear silently on Windows.
|
|
||||||
message(STATUS "ccache disabled: not supported for the MSVC toolchain on Windows")
|
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
if(WIN32 OR USE_VCPKG)
|
if(WIN32 OR USE_VCPKG)
|
||||||
|
|
@ -195,9 +184,6 @@ elseif(CMAKE_COMPILER_IS_GNUCXX)
|
||||||
set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} ${FLAG}")
|
set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} ${FLAG}")
|
||||||
endif()
|
endif()
|
||||||
endforeach()
|
endforeach()
|
||||||
|
|
||||||
# Reduce compiler I/O by using pipes between stages instead of temp files
|
|
||||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -pipe")
|
|
||||||
else()
|
else()
|
||||||
# other: osx/llvm, bsd/llvm
|
# other: osx/llvm, bsd/llvm
|
||||||
set(CMAKE_CXX_FLAGS_RELEASE "-O2")
|
set(CMAKE_CXX_FLAGS_RELEASE "-O2")
|
||||||
|
|
@ -206,9 +192,6 @@ else()
|
||||||
else()
|
else()
|
||||||
set(CMAKE_CXX_FLAGS_DEBUG "-g -O0 -Wall -Wextra")
|
set(CMAKE_CXX_FLAGS_DEBUG "-g -O0 -Wall -Wextra")
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
# Reduce compiler I/O by using pipes between stages instead of temp files
|
|
||||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -pipe")
|
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
# GNU systems need to define the Mersenne exponent for the RNG to compile w/o warning
|
# GNU systems need to define the Mersenne exponent for the RNG to compile w/o warning
|
||||||
|
|
@ -256,6 +239,11 @@ if(WIN32)
|
||||||
find_package(OpenSSL REQUIRED)
|
find_package(OpenSSL REQUIRED)
|
||||||
if(OPENSSL_FOUND)
|
if(OPENSSL_FOUND)
|
||||||
include_directories(${OPENSSL_INCLUDE_DIRS})
|
include_directories(${OPENSSL_INCLUDE_DIRS})
|
||||||
|
else()
|
||||||
|
message(
|
||||||
|
WARNING
|
||||||
|
"Could not find OpenSSL runtime libraries. They are not required for compiling, but needs to be available at runtime."
|
||||||
|
)
|
||||||
endif()
|
endif()
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
|
@ -293,7 +281,7 @@ if(UNIX)
|
||||||
if(CPACK_GENERATOR STREQUAL "RPM")
|
if(CPACK_GENERATOR STREQUAL "RPM")
|
||||||
set(CPACK_RPM_PACKAGE_LICENSE "GPLv2")
|
set(CPACK_RPM_PACKAGE_LICENSE "GPLv2")
|
||||||
set(CPACK_RPM_MAIN_COMPONENT "cockatrice")
|
set(CPACK_RPM_MAIN_COMPONENT "cockatrice")
|
||||||
set(CPACK_RPM_PACKAGE_REQUIRES "protobuf, qt6-qtimageformats, qt6-qtmultimedia, qt6-qtsvg, qt6-qttools")
|
set(CPACK_RPM_PACKAGE_REQUIRES "protobuf, qt6-qttools, qt6-qtsvg, qt6-qtmultimedia, qt6-qtimageformats")
|
||||||
set(CPACK_RPM_PACKAGE_GROUP "Amusements/Games")
|
set(CPACK_RPM_PACKAGE_GROUP "Amusements/Games")
|
||||||
set(CPACK_RPM_PACKAGE_URL "http://github.com/Cockatrice/Cockatrice")
|
set(CPACK_RPM_PACKAGE_URL "http://github.com/Cockatrice/Cockatrice")
|
||||||
# stop directories from making package conflicts
|
# stop directories from making package conflicts
|
||||||
|
|
@ -311,7 +299,7 @@ if(UNIX)
|
||||||
set(CPACK_DEBIAN_PACKAGE_SHLIBDEPS ON)
|
set(CPACK_DEBIAN_PACKAGE_SHLIBDEPS ON)
|
||||||
set(CPACK_DEBIAN_PACKAGE_SECTION "games")
|
set(CPACK_DEBIAN_PACKAGE_SECTION "games")
|
||||||
set(CPACK_DEBIAN_PACKAGE_HOMEPAGE "http://github.com/Cockatrice/Cockatrice")
|
set(CPACK_DEBIAN_PACKAGE_HOMEPAGE "http://github.com/Cockatrice/Cockatrice")
|
||||||
set(CPACK_DEBIAN_PACKAGE_DEPENDS "libqt6multimedia6, libqt6svg6, qt6-image-formats-plugins, qt6-qpa-plugins")
|
set(CPACK_DEBIAN_PACKAGE_DEPENDS "libqt6multimedia6, libqt6svg6, qt6-qpa-plugins, qt6-image-formats-plugins")
|
||||||
set(CPACK_DEBIAN_PACKAGE_RECOMMENDS "libqt6sql6-mysql") # for connecting servatrice to a mysql db
|
set(CPACK_DEBIAN_PACKAGE_RECOMMENDS "libqt6sql6-mysql") # for connecting servatrice to a mysql db
|
||||||
endif()
|
endif()
|
||||||
endif()
|
endif()
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,6 @@ RUN apt-get update \
|
||||||
libmariadb-dev-compat \
|
libmariadb-dev-compat \
|
||||||
libprotobuf-dev \
|
libprotobuf-dev \
|
||||||
libqt6sql6-mysql \
|
libqt6sql6-mysql \
|
||||||
libssl-dev \
|
|
||||||
qt6-websockets-dev \
|
qt6-websockets-dev \
|
||||||
protobuf-compiler \
|
protobuf-compiler \
|
||||||
qt6-tools-dev \
|
qt6-tools-dev \
|
||||||
|
|
@ -43,7 +42,6 @@ RUN apt-get update \
|
||||||
libprotobuf32t64 \
|
libprotobuf32t64 \
|
||||||
libqt6sql6-mysql \
|
libqt6sql6-mysql \
|
||||||
libqt6websockets6 \
|
libqt6websockets6 \
|
||||||
libssl3 \
|
|
||||||
&& apt-get clean \
|
&& apt-get clean \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
|
|
||||||
18
README.md
18
README.md
|
|
@ -149,15 +149,15 @@ You can then
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
The following flags (with their non-default values) can be passed to `cmake`:
|
The following flags (with their non-default values) can be passed to `cmake`:
|
||||||
| Flag | Description |
|
| Flag | Description |
|
||||||
| -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
|
| --- | --- |
|
||||||
| `-DWITH_SERVER=1` | Build <kbd>Servatrice</kbd> server |
|
| `-DWITH_SERVER=1` | Build <kbd>Servatrice</kbd> server |
|
||||||
| `-DWITH_CLIENT=0` | Don't build <kbd>Cockatrice</kbd> client |
|
| `-DWITH_CLIENT=0` | Don't build <kbd>Cockatrice</kbd> client |
|
||||||
| `-DWITH_ORACLE=0` | Don't build <kbd>Oracle</kbd> card database tool |
|
| `-DWITH_ORACLE=0` | Don't build <kbd>Oracle</kbd> card database tool |
|
||||||
| `-DCMAKE_BUILD_TYPE=Debug` | Compile in debug mode<br> Enables extra logging output, debug symbols, and much more verbose compiler warnings |
|
| `-DCMAKE_BUILD_TYPE=Debug` | Compile in debug mode<br> Enables extra logging output, debug symbols, and much more verbose compiler warnings |
|
||||||
| `-DWARNING_AS_ERROR=0` | Don't treat compilation warnings as errors in debug mode |
|
| `-DWARNING_AS_ERROR=0` | Don't treat compilation warnings as errors in debug mode |
|
||||||
| `-DUPDATE_TRANSLATIONS=1` | Configure `make` to update the translation .ts files for new strings in the source code<br> **Note:** `make clean` will remove the .ts files |
|
| `-DUPDATE_TRANSLATIONS=1` | Configure `make` to update the translation .ts files for new strings in the source code<br> **Note:** `make clean` will remove the .ts files |
|
||||||
| `-DTEST=1` | Enable regression tests<br> **Note:** `make test` to run tests, *googletest* will be downloaded if not available |
|
| `-DTEST=1` | Enable regression tests<br> **Note:** `make test` to run tests, *googletest* will be downloaded if not available |
|
||||||
|
|
||||||
|
|
||||||
# Run
|
# Run
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ if(WITH_CLIENT)
|
||||||
)
|
)
|
||||||
endif()
|
endif()
|
||||||
if(WITH_ORACLE)
|
if(WITH_ORACLE)
|
||||||
set(_ORACLE_NEEDED Concurrent Network Svg Widgets Xml)
|
set(_ORACLE_NEEDED Concurrent Network Svg Widgets)
|
||||||
endif()
|
endif()
|
||||||
if(TEST)
|
if(TEST)
|
||||||
# Union of Qt modules required across all test targets (independent of application targets).
|
# Union of Qt modules required across all test targets (independent of application targets).
|
||||||
|
|
|
||||||
|
|
@ -387,14 +387,19 @@ SectionEnd
|
||||||
|
|
||||||
Section "un.Application" UnSecApplication
|
Section "un.Application" UnSecApplication
|
||||||
SetShellVarContext all
|
SetShellVarContext all
|
||||||
|
RMDir /r "$INSTDIR\plugins"
|
||||||
|
RMDir /r "$INSTDIR\sounds"
|
||||||
|
RMDir /r "$INSTDIR\themes"
|
||||||
|
RMDir /r "$INSTDIR\translations"
|
||||||
|
Delete "$INSTDIR\*.exe"
|
||||||
|
Delete "$INSTDIR\*.dll"
|
||||||
|
Delete "$INSTDIR\qt.conf"
|
||||||
|
Delete "$INSTDIR\qdebug.txt"
|
||||||
|
Delete "$INSTDIR\servatrice.sql"
|
||||||
|
Delete "$INSTDIR\servatrice.ini.example"
|
||||||
|
RMDir "$INSTDIR"
|
||||||
|
|
||||||
; Remove the entire application directory so any file that is not part of
|
RMDir "$SMPROGRAMS\Cockatrice"
|
||||||
; the installed payload (e.g. build-tree artifacts such as *.dir folders,
|
|
||||||
; *_autogen and *.tlog files from a build) cannot survive between an
|
|
||||||
; uninstall and a fresh reinstall.
|
|
||||||
RMDir /r "$INSTDIR"
|
|
||||||
|
|
||||||
RMDir /r "$SMPROGRAMS\Cockatrice"
|
|
||||||
|
|
||||||
DeleteRegKey HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Cockatrice"
|
DeleteRegKey HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Cockatrice"
|
||||||
SectionEnd
|
SectionEnd
|
||||||
|
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
/** @file qtcore_pch.h
|
|
||||||
* @brief Precompiled header for all Qt targets (Qt Core only).
|
|
||||||
*
|
|
||||||
* Safe for every target that links Qt Core, including the headless
|
|
||||||
* Servatrice binary. Keep this header free of any widget/gui types.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include <QBasicTimer>
|
|
||||||
#include <QByteArray>
|
|
||||||
#include <QDateTime>
|
|
||||||
#include <QDebug>
|
|
||||||
#include <QFile>
|
|
||||||
#include <QHash>
|
|
||||||
#include <QList>
|
|
||||||
#include <QLoggingCategory>
|
|
||||||
#include <QMap>
|
|
||||||
#include <QMetaObject>
|
|
||||||
#include <QObject>
|
|
||||||
#include <QRandomGenerator>
|
|
||||||
#include <QSharedPointer>
|
|
||||||
#include <QString>
|
|
||||||
#include <QStringList>
|
|
||||||
#include <QTimer>
|
|
||||||
#include <QVariant>
|
|
||||||
|
|
@ -1,30 +0,0 @@
|
||||||
/** @file qtwidgets_pch.h
|
|
||||||
* @brief Precompiled header for GUI targets (Cockatrice client, Oracle).
|
|
||||||
*
|
|
||||||
* Includes the Qt Core precompiled header plus the heavy Gui, Widgets and
|
|
||||||
* Network layers that virtually every client translation unit re-parses.
|
|
||||||
* Do not use on Servatrice (headless, QT_DONT_USE_QTGUI).
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "qtcore_pch.h"
|
|
||||||
|
|
||||||
#include <QAction>
|
|
||||||
#include <QApplication>
|
|
||||||
#include <QFrame>
|
|
||||||
#include <QGraphicsItem>
|
|
||||||
#include <QGraphicsScene>
|
|
||||||
#include <QGraphicsView>
|
|
||||||
#include <QImage>
|
|
||||||
#include <QLabel>
|
|
||||||
#include <QLayout>
|
|
||||||
#include <QMainWindow>
|
|
||||||
#include <QMenu>
|
|
||||||
#include <QNetworkAccessManager>
|
|
||||||
#include <QNetworkReply>
|
|
||||||
#include <QPainter>
|
|
||||||
#include <QPushButton>
|
|
||||||
#include <QScrollArea>
|
|
||||||
#include <QTabWidget>
|
|
||||||
#include <QToolBar>
|
|
||||||
#include <QTreeWidget>
|
|
||||||
#include <QWidget>
|
|
||||||
|
|
@ -45,14 +45,11 @@ set(cockatrice_SOURCES
|
||||||
src/interface/widgets/dialogs/dlg_load_deck_from_website.cpp
|
src/interface/widgets/dialogs/dlg_load_deck_from_website.cpp
|
||||||
src/interface/widgets/dialogs/dlg_load_remote_deck.cpp
|
src/interface/widgets/dialogs/dlg_load_remote_deck.cpp
|
||||||
src/interface/widgets/dialogs/dlg_local_game_options.cpp
|
src/interface/widgets/dialogs/dlg_local_game_options.cpp
|
||||||
src/interface/widgets/dialogs/dlg_login_prompt.cpp
|
|
||||||
src/interface/widgets/dialogs/dlg_manage_sets.cpp
|
src/interface/widgets/dialogs/dlg_manage_sets.cpp
|
||||||
src/interface/widgets/dialogs/dlg_my_reports.cpp
|
src/interface/widgets/dialogs/dlg_my_reports.cpp
|
||||||
src/interface/widgets/dialogs/dlg_register.cpp
|
src/interface/widgets/dialogs/dlg_register.cpp
|
||||||
src/interface/widgets/dialogs/dlg_report_user.cpp
|
src/interface/widgets/dialogs/dlg_report_user.cpp
|
||||||
src/interface/widgets/dialogs/dlg_select_set_for_cards.cpp
|
src/interface/widgets/dialogs/dlg_select_set_for_cards.cpp
|
||||||
src/interface/widgets/dialogs/dlg_share_deck.cpp
|
|
||||||
src/interface/widgets/dialogs/dlg_shared_decks_preview.cpp
|
|
||||||
src/interface/widgets/dialogs/dlg_settings.cpp
|
src/interface/widgets/dialogs/dlg_settings.cpp
|
||||||
src/interface/widgets/dialogs/dlg_startup_card_check.cpp
|
src/interface/widgets/dialogs/dlg_startup_card_check.cpp
|
||||||
src/interface/widgets/dialogs/dlg_tip_of_the_day.cpp
|
src/interface/widgets/dialogs/dlg_tip_of_the_day.cpp
|
||||||
|
|
@ -60,9 +57,6 @@ set(cockatrice_SOURCES
|
||||||
src/interface/widgets/dialogs/dlg_view_log.cpp
|
src/interface/widgets/dialogs/dlg_view_log.cpp
|
||||||
src/interface/widgets/dialogs/override_printing_warning.cpp
|
src/interface/widgets/dialogs/override_printing_warning.cpp
|
||||||
src/interface/widgets/dialogs/tip_of_the_day.cpp
|
src/interface/widgets/dialogs/tip_of_the_day.cpp
|
||||||
src/interface/widgets/deck_share/deck_share_utils.cpp
|
|
||||||
src/interface/widgets/deck_share/shared_deck_preview_widget.cpp
|
|
||||||
src/interface/widgets/deck_share/share_bar_widget.cpp
|
|
||||||
src/filters/deck_filter_string.cpp
|
src/filters/deck_filter_string.cpp
|
||||||
src/filters/filter_builder.cpp
|
src/filters/filter_builder.cpp
|
||||||
src/filters/filter_tree_model.cpp
|
src/filters/filter_tree_model.cpp
|
||||||
|
|
@ -169,11 +163,9 @@ set(cockatrice_SOURCES
|
||||||
src/interface/palette_editor/palette_grid_widget.cpp
|
src/interface/palette_editor/palette_grid_widget.cpp
|
||||||
src/interface/palette_editor/palette_editor_dialog.cpp
|
src/interface/palette_editor/palette_editor_dialog.cpp
|
||||||
src/interface/widgets/cards/additional_info/color_identity_widget.cpp
|
src/interface/widgets/cards/additional_info/color_identity_widget.cpp
|
||||||
src/interface/widgets/cards/additional_info/deck_color_identity.cpp
|
|
||||||
src/interface/widgets/cards/additional_info/mana_cost_widget.cpp
|
src/interface/widgets/cards/additional_info/mana_cost_widget.cpp
|
||||||
src/interface/widgets/cards/additional_info/mana_symbol_widget.cpp
|
src/interface/widgets/cards/additional_info/mana_symbol_widget.cpp
|
||||||
src/interface/widgets/cards/art_crop_attribution.cpp
|
src/interface/widgets/cards/art_crop_attribution.cpp
|
||||||
src/interface/widgets/cards/card_art_utils.cpp
|
|
||||||
src/interface/widgets/cards/card_group_display_widgets/card_group_display_widget.cpp
|
src/interface/widgets/cards/card_group_display_widgets/card_group_display_widget.cpp
|
||||||
src/interface/widgets/cards/card_group_display_widgets/flat_card_group_display_widget.cpp
|
src/interface/widgets/cards/card_group_display_widgets/flat_card_group_display_widget.cpp
|
||||||
src/interface/widgets/cards/card_group_display_widgets/overlapped_card_group_display_widget.cpp
|
src/interface/widgets/cards/card_group_display_widgets/overlapped_card_group_display_widget.cpp
|
||||||
|
|
@ -222,7 +214,6 @@ set(cockatrice_SOURCES
|
||||||
src/interface/widgets/deck_editor/deck_editor_printing_selector_dock_widget.cpp
|
src/interface/widgets/deck_editor/deck_editor_printing_selector_dock_widget.cpp
|
||||||
src/interface/widgets/deck_editor/deck_list_style_proxy.cpp
|
src/interface/widgets/deck_editor/deck_list_style_proxy.cpp
|
||||||
src/interface/widgets/deck_editor/deck_state_manager.cpp
|
src/interface/widgets/deck_editor/deck_state_manager.cpp
|
||||||
src/interface/widgets/deck_editor/deck_zone_dialog.cpp
|
|
||||||
src/interface/widgets/deck_editor/printing_disabled_info_widget.cpp
|
src/interface/widgets/deck_editor/printing_disabled_info_widget.cpp
|
||||||
src/interface/widgets/general/background_sources.cpp
|
src/interface/widgets/general/background_sources.cpp
|
||||||
src/interface/widgets/general/display/background_plate_widget.cpp
|
src/interface/widgets/general/display/background_plate_widget.cpp
|
||||||
|
|
@ -280,7 +271,6 @@ set(cockatrice_SOURCES
|
||||||
src/interface/widgets/server/user/user_context_menu.cpp
|
src/interface/widgets/server/user/user_context_menu.cpp
|
||||||
src/interface/widgets/server/user/user_info_box.cpp
|
src/interface/widgets/server/user/user_info_box.cpp
|
||||||
src/interface/widgets/server/user/user_info_connection.cpp
|
src/interface/widgets/server/user/user_info_connection.cpp
|
||||||
src/interface/widgets/server/user/user_list_dialog.cpp
|
|
||||||
src/interface/widgets/server/user/user_list_manager.cpp
|
src/interface/widgets/server/user/user_list_manager.cpp
|
||||||
src/interface/widgets/server/user/user_list_painter.cpp
|
src/interface/widgets/server/user/user_list_painter.cpp
|
||||||
src/interface/widgets/server/user/user_list_panel_widget.cpp
|
src/interface/widgets/server/user/user_list_panel_widget.cpp
|
||||||
|
|
@ -324,8 +314,6 @@ set(cockatrice_SOURCES
|
||||||
src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_tag_display_widget.cpp
|
src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_tag_display_widget.cpp
|
||||||
src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_tag_item_widget.cpp
|
src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_tag_item_widget.cpp
|
||||||
src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_widget.cpp
|
src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_widget.cpp
|
||||||
src/interface/widgets/visual_deck_storage/deck_preview/public_deck_preview_widget.cpp
|
|
||||||
src/interface/widgets/visual_deck_storage/remote_public_decks_model.cpp
|
|
||||||
src/interface/widgets/visual_deck_storage/visual_deck_storage_folder_display_widget.cpp
|
src/interface/widgets/visual_deck_storage/visual_deck_storage_folder_display_widget.cpp
|
||||||
src/interface/widgets/visual_deck_storage/visual_deck_storage_model.cpp
|
src/interface/widgets/visual_deck_storage/visual_deck_storage_model.cpp
|
||||||
src/interface/widgets/visual_deck_storage/visual_deck_storage_quick_settings_widget.cpp
|
src/interface/widgets/visual_deck_storage/visual_deck_storage_quick_settings_widget.cpp
|
||||||
|
|
@ -391,13 +379,11 @@ set(cockatrice_SOURCES
|
||||||
src/interface/widgets/tabs/tab_card_art_rules.cpp
|
src/interface/widgets/tabs/tab_card_art_rules.cpp
|
||||||
src/interface/widgets/tabs/tab_deck_editor.cpp
|
src/interface/widgets/tabs/tab_deck_editor.cpp
|
||||||
src/interface/widgets/tabs/tab_deck_storage.cpp
|
src/interface/widgets/tabs/tab_deck_storage.cpp
|
||||||
src/interface/widgets/tabs/tab_developer.cpp
|
|
||||||
src/interface/widgets/tabs/tab_game.cpp
|
src/interface/widgets/tabs/tab_game.cpp
|
||||||
src/interface/widgets/tabs/tab_home.cpp
|
src/interface/widgets/tabs/tab_home.cpp
|
||||||
src/interface/widgets/tabs/tab_logs.cpp
|
src/interface/widgets/tabs/tab_logs.cpp
|
||||||
src/interface/widgets/tabs/tab_message.cpp
|
src/interface/widgets/tabs/tab_message.cpp
|
||||||
src/interface/widgets/tabs/tab_moderation.cpp
|
src/interface/widgets/tabs/tab_moderation.cpp
|
||||||
src/interface/widgets/tabs/tab_public_decks.cpp
|
|
||||||
src/interface/widgets/tabs/tab_report.cpp
|
src/interface/widgets/tabs/tab_report.cpp
|
||||||
src/interface/widgets/tabs/tab_replays.cpp
|
src/interface/widgets/tabs/tab_replays.cpp
|
||||||
src/interface/widgets/tabs/tab_room.cpp
|
src/interface/widgets/tabs/tab_room.cpp
|
||||||
|
|
@ -413,7 +399,6 @@ set(cockatrice_SOURCES
|
||||||
src/interface/widgets/tabs/api/commander_spellbook/commander_bracket_widget.cpp
|
src/interface/widgets/tabs/api/commander_spellbook/commander_bracket_widget.cpp
|
||||||
src/interface/widgets/tabs/api/commander_spellbook/handle_commander_brackets.cpp
|
src/interface/widgets/tabs/api/commander_spellbook/handle_commander_brackets.cpp
|
||||||
src/interface/widgets/onboarding/banner_shader_config.h
|
src/interface/widgets/onboarding/banner_shader_config.h
|
||||||
src/interface/widgets/onboarding/brand_colors.h
|
|
||||||
src/interface/widgets/onboarding/first_run_wizard.cpp
|
src/interface/widgets/onboarding/first_run_wizard.cpp
|
||||||
src/interface/widgets/onboarding/first_run_wizard.h
|
src/interface/widgets/onboarding/first_run_wizard.h
|
||||||
src/interface/widgets/onboarding/first_run_wizard_page.cpp
|
src/interface/widgets/onboarding/first_run_wizard_page.cpp
|
||||||
|
|
@ -454,8 +439,6 @@ set(cockatrice_SOURCES
|
||||||
src/interface/intents/intent_login.h
|
src/interface/intents/intent_login.h
|
||||||
src/interface/intents/intent_open_server_room_by_name.cpp
|
src/interface/intents/intent_open_server_room_by_name.cpp
|
||||||
src/interface/intents/intent_open_server_room_by_name.h
|
src/interface/intents/intent_open_server_room_by_name.h
|
||||||
src/interface/intents/intent_open_shared_deck.cpp
|
|
||||||
src/interface/intents/intent_open_shared_deck.h
|
|
||||||
src/interface/intents/url_parser.cpp
|
src/interface/intents/url_parser.cpp
|
||||||
src/interface/intents/url_parser.h
|
src/interface/intents/url_parser.h
|
||||||
src/interface/widgets/server/user/user_info_popup.cpp
|
src/interface/widgets/server/user/user_info_popup.cpp
|
||||||
|
|
@ -532,8 +515,6 @@ qt6_add_executable(
|
||||||
MANUAL_FINALIZATION
|
MANUAL_FINALIZATION
|
||||||
)
|
)
|
||||||
|
|
||||||
target_precompile_headers(cockatrice PRIVATE "${CMAKE_SOURCE_DIR}/cmake/pch/qtwidgets_pch.h")
|
|
||||||
|
|
||||||
qt6_add_shaders(
|
qt6_add_shaders(
|
||||||
cockatrice
|
cockatrice
|
||||||
"onboarding_shaders"
|
"onboarding_shaders"
|
||||||
|
|
@ -543,7 +524,6 @@ qt6_add_shaders(
|
||||||
"src/interface/widgets/onboarding/shaders"
|
"src/interface/widgets/onboarding/shaders"
|
||||||
FILES
|
FILES
|
||||||
src/interface/widgets/onboarding/shaders/brand_banner.frag
|
src/interface/widgets/onboarding/shaders/brand_banner.frag
|
||||||
src/interface/widgets/onboarding/shaders/brand_plate.frag
|
|
||||||
)
|
)
|
||||||
|
|
||||||
qt6_add_resources(
|
qt6_add_resources(
|
||||||
|
|
@ -668,35 +648,18 @@ if(WIN32)
|
||||||
set(qtconf_dest_dir .)
|
set(qtconf_dest_dir .)
|
||||||
|
|
||||||
install(
|
install(
|
||||||
DIRECTORY "$<TARGET_FILE_DIR:cockatrice>/"
|
DIRECTORY "${CMAKE_BINARY_DIR}/${PROJECT_NAME}/${CMAKE_BUILD_TYPE}/"
|
||||||
DESTINATION ./
|
DESTINATION ./
|
||||||
FILES_MATCHING
|
FILES_MATCHING
|
||||||
PATTERN "*.dll"
|
PATTERN "*.dll"
|
||||||
PATTERN "*.pdb" EXCLUDE
|
|
||||||
PATTERN "*.dir*" EXCLUDE
|
|
||||||
PATTERN "*_autogen*" EXCLUDE
|
|
||||||
PATTERN "*.tlog*" EXCLUDE
|
|
||||||
PATTERN "CMakeFiles*" EXCLUDE
|
|
||||||
PATTERN "x64*" EXCLUDE
|
|
||||||
PATTERN ".qt*" EXCLUDE
|
|
||||||
PATTERN ".qsb*" EXCLUDE
|
|
||||||
PATTERN ".lupdate*" EXCLUDE
|
|
||||||
)
|
)
|
||||||
|
|
||||||
install(
|
install(
|
||||||
DIRECTORY "${CMAKE_BINARY_DIR}/cockatrice/"
|
DIRECTORY "${CMAKE_BINARY_DIR}/cockatrice/"
|
||||||
DESTINATION ./
|
DESTINATION ./
|
||||||
FILES_MATCHING
|
FILES_MATCHING
|
||||||
|
PATTERN "CMakeFiles" EXCLUDE
|
||||||
PATTERN "*.ini"
|
PATTERN "*.ini"
|
||||||
PATTERN "CMakeFiles*" EXCLUDE
|
|
||||||
PATTERN "*.dir*" EXCLUDE
|
|
||||||
PATTERN "*_autogen*" EXCLUDE
|
|
||||||
PATTERN "*.tlog*" EXCLUDE
|
|
||||||
PATTERN "*.pdb" EXCLUDE
|
|
||||||
PATTERN "x64*" EXCLUDE
|
|
||||||
PATTERN ".qt*" EXCLUDE
|
|
||||||
PATTERN ".qsb*" EXCLUDE
|
|
||||||
PATTERN ".lupdate*" EXCLUDE
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Qt plugins: audio, iconengines, imageformats, multimedia, platforms, printsupport, styles, tls
|
# Qt plugins: audio, iconengines, imageformats, multimedia, platforms, printsupport, styles, tls
|
||||||
|
|
@ -749,6 +712,10 @@ Data = Resources\")
|
||||||
"
|
"
|
||||||
COMPONENT Runtime
|
COMPONENT Runtime
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if(OPENSSL_FOUND)
|
||||||
|
install(FILES ${OPENSSL_INCLUDE_DIRS} DESTINATION ./)
|
||||||
|
endif()
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
if(Qt6LinguistTools_FOUND)
|
if(Qt6LinguistTools_FOUND)
|
||||||
|
|
|
||||||
|
|
@ -63,8 +63,6 @@
|
||||||
<file>resources/icons/mana/W.svg</file>
|
<file>resources/icons/mana/W.svg</file>
|
||||||
|
|
||||||
<file>resources/backgrounds/home.png</file>
|
<file>resources/backgrounds/home.png</file>
|
||||||
<file>resources/backgrounds/home-dark.png</file>
|
|
||||||
<file>resources/backgrounds/home-light.png</file>
|
|
||||||
<file>resources/backgrounds/card_triplet.svg</file>
|
<file>resources/backgrounds/card_triplet.svg</file>
|
||||||
<file>resources/backgrounds/placeholder_printing_selector.svg</file>
|
<file>resources/backgrounds/placeholder_printing_selector.svg</file>
|
||||||
|
|
||||||
|
|
@ -365,8 +363,6 @@
|
||||||
|
|
||||||
<file>resources/usericons/pawn_single.svg</file>
|
<file>resources/usericons/pawn_single.svg</file>
|
||||||
<file>resources/usericons/pawn_double.svg</file>
|
<file>resources/usericons/pawn_double.svg</file>
|
||||||
<file>resources/usericons/pawn_dev_single.svg</file>
|
|
||||||
<file>resources/usericons/pawn_dev_double.svg</file>
|
|
||||||
<file>resources/usericons/pawn_donator_single.svg</file>
|
<file>resources/usericons/pawn_donator_single.svg</file>
|
||||||
<file>resources/usericons/pawn_donator_double.svg</file>
|
<file>resources/usericons/pawn_donator_double.svg</file>
|
||||||
<file>resources/usericons/pawn_judge_single.svg</file>
|
<file>resources/usericons/pawn_judge_single.svg</file>
|
||||||
|
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 12 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 16 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 16 MiB After Width: | Height: | Size: 12 MiB |
|
|
@ -52,7 +52,6 @@ In this list of examples below, each entry has an explanation and can be clicked
|
||||||
<dt><u>E</u>dition:</dt>
|
<dt><u>E</u>dition:</dt>
|
||||||
<dd>[set:lea](#set:lea) <small>(Cards that appear in Alpha, which has the set code LEA)</small></dd>
|
<dd>[set:lea](#set:lea) <small>(Cards that appear in Alpha, which has the set code LEA)</small></dd>
|
||||||
<dd>[e:lea OR e:leb](#e:lea OR e:leb) <small>(Cards that appear in Alpha or Beta)</small></dd>
|
<dd>[e:lea OR e:leb](#e:lea OR e:leb) <small>(Cards that appear in Alpha or Beta)</small></dd>
|
||||||
<dd>[e<8ED](#e<8ED) <small>(Cards that appear before 8th edition)</small></dd>
|
|
||||||
|
|
||||||
<dt>Negate:</dt>
|
<dt>Negate:</dt>
|
||||||
<dd>[c:wu -c:m](#c:wu -c:m) <small>(Any card that is white or blue, but not multicolored)</small></dd>
|
<dd>[c:wu -c:m](#c:wu -c:m) <small>(Any card that is white or blue, but not multicolored)</small></dd>
|
||||||
|
|
|
||||||
|
|
@ -1,343 +0,0 @@
|
||||||
<?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>
|
|
||||||
|
Before Width: | Height: | Size: 12 KiB |
|
|
@ -1,211 +0,0 @@
|
||||||
<?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>
|
|
||||||
|
Before Width: | Height: | Size: 9.1 KiB |
|
|
@ -1,6 +1,5 @@
|
||||||
#include "remote_connection_controller.h"
|
#include "remote_connection_controller.h"
|
||||||
|
|
||||||
#include "../../../interface/pixel_map_generator.h"
|
|
||||||
#include "../../settings/cache_settings.h"
|
#include "../../settings/cache_settings.h"
|
||||||
#include "../interface/widgets/dialogs/dlg_connect.h"
|
#include "../interface/widgets/dialogs/dlg_connect.h"
|
||||||
#include "../interface/widgets/dialogs/dlg_forgot_password_challenge.h"
|
#include "../interface/widgets/dialogs/dlg_forgot_password_challenge.h"
|
||||||
|
|
@ -181,7 +180,7 @@ void ConnectionController::onServerShutdownEvent(const Event_ServerShutdown &eve
|
||||||
"games will be lost.\nReason for shutdown: %1",
|
"games will be lost.\nReason for shutdown: %1",
|
||||||
"", event.minutes())
|
"", event.minutes())
|
||||||
.arg(QString::fromStdString(event.reason())));
|
.arg(QString::fromStdString(event.reason())));
|
||||||
serverShutdownMessageBox.setIconPixmap(themePixmap(QStringLiteral("cockatrice")).scaled(64, 64));
|
serverShutdownMessageBox.setIconPixmap(QPixmap("theme:cockatrice").scaled(64, 64));
|
||||||
serverShutdownMessageBox.setText(tr("Scheduled server shutdown"));
|
serverShutdownMessageBox.setText(tr("Scheduled server shutdown"));
|
||||||
serverShutdownMessageBox.setWindowModality(Qt::ApplicationModal);
|
serverShutdownMessageBox.setWindowModality(Qt::ApplicationModal);
|
||||||
serverShutdownMessageBox.setVisible(true);
|
serverShutdownMessageBox.setVisible(true);
|
||||||
|
|
|
||||||
|
|
@ -94,7 +94,7 @@ QStringMap &SoundEngine::getAvailableThemes()
|
||||||
QDir dir;
|
QDir dir;
|
||||||
availableThemes.clear();
|
availableThemes.clear();
|
||||||
|
|
||||||
// Load themes from user profile dir
|
// load themes from user profile dir
|
||||||
|
|
||||||
dir.setPath(SettingsCache::instance().getDataPath() + "/sounds");
|
dir.setPath(SettingsCache::instance().getDataPath() + "/sounds");
|
||||||
|
|
||||||
|
|
@ -104,7 +104,7 @@ QStringMap &SoundEngine::getAvailableThemes()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load themes from Cockatrice system dir
|
// load themes from cockatrice system dir
|
||||||
dir.setPath(qApp->applicationDirPath() +
|
dir.setPath(qApp->applicationDirPath() +
|
||||||
#ifdef Q_OS_MAC
|
#ifdef Q_OS_MAC
|
||||||
"/../Resources/sounds"
|
"/../Resources/sounds"
|
||||||
|
|
|
||||||
|
|
@ -43,12 +43,6 @@ NumericValue <- [0-9]+
|
||||||
|
|
||||||
static std::once_flag init;
|
static std::once_flag init;
|
||||||
|
|
||||||
// The peglib parser is a single permanent object, so the rule actions below cannot see
|
|
||||||
// per-instance state. The card language that the nested [[card name]] search matches
|
|
||||||
// against is passed through this thread-local context, which is live only while a
|
|
||||||
// DeckFilterString is being parsed, and copied into the nested FilterString closures.
|
|
||||||
thread_local CardSearchLanguage deckSearchLanguageContext;
|
|
||||||
|
|
||||||
static void setupParserRules()
|
static void setupParserRules()
|
||||||
{
|
{
|
||||||
// plumbing
|
// plumbing
|
||||||
|
|
@ -122,7 +116,7 @@ static void setupParserRules()
|
||||||
|
|
||||||
// actual functionality
|
// actual functionality
|
||||||
search["DeckContentQuery"] = [](const peg::SemanticValues &sv) -> DeckFilter {
|
search["DeckContentQuery"] = [](const peg::SemanticValues &sv) -> DeckFilter {
|
||||||
auto cardFilter = FilterString(std::any_cast<QString>(sv[0]), deckSearchLanguageContext);
|
auto cardFilter = FilterString(std::any_cast<QString>(sv[0]));
|
||||||
auto numberMatcher = sv.size() > 1 ? std::any_cast<NumberMatcher>(sv[1]) : [](int count) { return count > 0; };
|
auto numberMatcher = sv.size() > 1 ? std::any_cast<NumberMatcher>(sv[1]) : [](int count) { return count > 0; };
|
||||||
|
|
||||||
return [=](const DeckSearchData &data) -> bool {
|
return [=](const DeckSearchData &data) -> bool {
|
||||||
|
|
@ -192,7 +186,7 @@ DeckFilterString::DeckFilterString()
|
||||||
_error = "Not initialized";
|
_error = "Not initialized";
|
||||||
}
|
}
|
||||||
|
|
||||||
DeckFilterString::DeckFilterString(const QString &expr, const CardSearchLanguage &searchLanguage)
|
DeckFilterString::DeckFilterString(const QString &expr)
|
||||||
{
|
{
|
||||||
QByteArray ba = expr.simplified().toUtf8();
|
QByteArray ba = expr.simplified().toUtf8();
|
||||||
|
|
||||||
|
|
@ -205,8 +199,6 @@ DeckFilterString::DeckFilterString(const QString &expr, const CardSearchLanguage
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
deckSearchLanguageContext = searchLanguage;
|
|
||||||
|
|
||||||
search.set_logger([&](size_t /*ln*/, size_t col, const std::string &msg) {
|
search.set_logger([&](size_t /*ln*/, size_t col, const std::string &msg) {
|
||||||
_error = QString("Error at position %1: %2").arg(col).arg(QString::fromStdString(msg));
|
_error = QString("Error at position %1: %2").arg(col).arg(QString::fromStdString(msg));
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,6 @@
|
||||||
#include <QLoggingCategory>
|
#include <QLoggingCategory>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
#include <libcockatrice/card/card_localization.h>
|
|
||||||
|
|
||||||
inline Q_LOGGING_CATEGORY(DeckFilterStringLog, "deck_filter_string");
|
inline Q_LOGGING_CATEGORY(DeckFilterStringLog, "deck_filter_string");
|
||||||
|
|
||||||
|
|
@ -36,7 +35,7 @@ class DeckFilterString
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
DeckFilterString();
|
DeckFilterString();
|
||||||
explicit DeckFilterString(const QString &expr, const CardSearchLanguage &searchLanguage = {});
|
explicit DeckFilterString(const QString &expr);
|
||||||
bool check(const DeckSearchData &data) const
|
bool check(const DeckSearchData &data) const
|
||||||
{
|
{
|
||||||
return filter(data);
|
return filter(data);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
#include "filter_builder.h"
|
#include "filter_builder.h"
|
||||||
|
|
||||||
#include "../interface/pixel_map_generator.h"
|
|
||||||
#include "../interface/widgets/utility/custom_line_edit.h"
|
#include "../interface/widgets/utility/custom_line_edit.h"
|
||||||
|
|
||||||
#include <QComboBox>
|
#include <QComboBox>
|
||||||
|
|
@ -22,7 +21,7 @@ FilterBuilder::FilterBuilder(QWidget *parent) : QWidget(parent)
|
||||||
typeCombo->addItem(CardFilter::typeName(static_cast<CardFilter::Type>(i)), QVariant(i));
|
typeCombo->addItem(CardFilter::typeName(static_cast<CardFilter::Type>(i)), QVariant(i));
|
||||||
}
|
}
|
||||||
|
|
||||||
QPushButton *ok = new QPushButton(themePixmap(QStringLiteral("icons/increment")), QString());
|
QPushButton *ok = new QPushButton(QPixmap("theme:icons/increment"), QString());
|
||||||
ok->setObjectName("ok");
|
ok->setObjectName("ok");
|
||||||
ok->setMaximumSize(20, 20);
|
ok->setMaximumSize(20, 20);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,12 +13,12 @@ CounterState *CounterState::fromProto(const ServerInfo_Counter &counter, QObject
|
||||||
convertColorToQColor(counter.counter_color()), counter.radius(), counter.count(), parent);
|
convertColorToQColor(counter.counter_color()), counter.radius(), counter.count(), parent);
|
||||||
}
|
}
|
||||||
|
|
||||||
void CounterState::setValue(int newValue, bool skipDamageAnimation)
|
void CounterState::setValue(int newValue)
|
||||||
{
|
{
|
||||||
if (newValue == value) {
|
if (newValue == value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
int old = value;
|
int old = value;
|
||||||
value = newValue;
|
value = newValue;
|
||||||
emit valueChanged(old, newValue, skipDamageAnimation);
|
emit valueChanged(old, newValue);
|
||||||
}
|
}
|
||||||
|
|
@ -35,23 +35,10 @@ public:
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
void setValue(int newValue);
|
||||||
* @brief Set the counter value.
|
|
||||||
* @param newValue The new value.
|
|
||||||
* @param skipDamageAnimation When true, valueChanged is emitted with skipDamageAnimation=true, letting views
|
|
||||||
* suppress damage-related feedback (e.g. battlefield shimmer, life counter flash) for values set during replay
|
|
||||||
* rewinds.
|
|
||||||
*/
|
|
||||||
void setValue(int newValue, bool skipDamageAnimation = false);
|
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
/**
|
void valueChanged(int oldValue, int newValue);
|
||||||
* @brief Emitted whenever the value changes.
|
|
||||||
* @param oldValue The previous value.
|
|
||||||
* @param newValue The new value.
|
|
||||||
* @param skipDamageAnimation True when the change should not trigger damage/life-change feedback in views.
|
|
||||||
*/
|
|
||||||
void valueChanged(int oldValue, int newValue, bool skipDamageAnimation);
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
int id;
|
int id;
|
||||||
|
|
|
||||||
|
|
@ -430,13 +430,12 @@ void GameEventHandler::eventJoin(const Event_Join &event, int /*eventPlayerId*/,
|
||||||
QString playerName = QString::fromStdString(playerInfo.user_info().name());
|
QString playerName = QString::fromStdString(playerInfo.user_info().name());
|
||||||
emit addPlayerToAutoCompleteList(playerName);
|
emit addPlayerToAutoCompleteList(playerName);
|
||||||
|
|
||||||
PlayerManager *playerManager = game->getPlayerManager();
|
if (game->getPlayerManager()->getPlayers().contains(playerId)) {
|
||||||
if (playerManager->getPlayers().contains(playerId) || playerManager->getSpectators().contains(playerId)) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (playerInfo.spectator()) {
|
if (playerInfo.spectator()) {
|
||||||
playerManager->addSpectator(playerId, playerInfo);
|
game->getPlayerManager()->addSpectator(playerId, playerInfo);
|
||||||
emit logJoinSpectator(playerName);
|
emit logJoinSpectator(playerName);
|
||||||
emit spectatorJoined(playerInfo);
|
emit spectatorJoined(playerInfo);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -13,8 +13,7 @@
|
||||||
enum EventProcessingOption
|
enum EventProcessingOption
|
||||||
{
|
{
|
||||||
SKIP_REVEAL_WINDOW = 0x0001,
|
SKIP_REVEAL_WINDOW = 0x0001,
|
||||||
SKIP_TAP_ANIMATION = 0x0002,
|
SKIP_TAP_ANIMATION = 0x0002
|
||||||
SKIP_DAMAGE_ANIMATION = 0x0004
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Wrap it in a QFlags typedef
|
// Wrap it in a QFlags typedef
|
||||||
|
|
|
||||||
|
|
@ -262,15 +262,14 @@ void PlayerEventHandler::eventCreateCounter(const Event_CreateCounter &event)
|
||||||
player->addCounter(event.counter_info());
|
player->addCounter(event.counter_info());
|
||||||
}
|
}
|
||||||
|
|
||||||
void PlayerEventHandler::eventSetCounter(const Event_SetCounter &event, EventProcessingOptions options)
|
void PlayerEventHandler::eventSetCounter(const Event_SetCounter &event)
|
||||||
{
|
{
|
||||||
CounterState *ctr = player->getCounters().value(event.counter_id(), nullptr);
|
CounterState *ctr = player->getCounters().value(event.counter_id(), nullptr);
|
||||||
if (!ctr) {
|
if (!ctr) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
int oldValue = ctr->getValue();
|
int oldValue = ctr->getValue();
|
||||||
const bool skipDamageAnimation = options.testFlag(SKIP_DAMAGE_ANIMATION);
|
ctr->setValue(event.value());
|
||||||
ctr->setValue(event.value(), skipDamageAnimation);
|
|
||||||
emit logSetCounter(player, ctr->getName(), event.value(), oldValue);
|
emit logSetCounter(player, ctr->getName(), event.value(), oldValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -626,7 +625,7 @@ void PlayerEventHandler::processGameEvent(GameEvent::GameEventType type,
|
||||||
eventCreateCounter(event.GetExtension(Event_CreateCounter::ext));
|
eventCreateCounter(event.GetExtension(Event_CreateCounter::ext));
|
||||||
break;
|
break;
|
||||||
case GameEvent::SET_COUNTER:
|
case GameEvent::SET_COUNTER:
|
||||||
eventSetCounter(event.GetExtension(Event_SetCounter::ext), options);
|
eventSetCounter(event.GetExtension(Event_SetCounter::ext));
|
||||||
break;
|
break;
|
||||||
case GameEvent::DEL_COUNTER:
|
case GameEvent::DEL_COUNTER:
|
||||||
eventDelCounter(event.GetExtension(Event_DelCounter::ext));
|
eventDelCounter(event.GetExtension(Event_DelCounter::ext));
|
||||||
|
|
|
||||||
|
|
@ -153,7 +153,7 @@ public:
|
||||||
void eventCreateCounter(const Event_CreateCounter &event);
|
void eventCreateCounter(const Event_CreateCounter &event);
|
||||||
|
|
||||||
/// Set a player-level counter value.
|
/// Set a player-level counter value.
|
||||||
void eventSetCounter(const Event_SetCounter &event, EventProcessingOptions options);
|
void eventSetCounter(const Event_SetCounter &event);
|
||||||
|
|
||||||
/// Delete a player-level counter.
|
/// Delete a player-level counter.
|
||||||
void eventDelCounter(const Event_DelCounter &event);
|
void eventDelCounter(const Event_DelCounter &event);
|
||||||
|
|
|
||||||
|
|
@ -175,15 +175,7 @@ void PlayerLogic::processPlayerInfo(const ServerInfo_Player &info)
|
||||||
const ServerInfo_Card &cardInfo = zoneInfo.card_list(j);
|
const ServerInfo_Card &cardInfo = zoneInfo.card_list(j);
|
||||||
auto *card = new CardItem(this);
|
auto *card = new CardItem(this);
|
||||||
card->processCardInfo(cardInfo);
|
card->processCardInfo(cardInfo);
|
||||||
// Zones without coordinates (hand, piles, stack) preserve the order
|
zone->addCard(card, false, cardInfo.x(), cardInfo.y());
|
||||||
// they arrive in on the server in the positions of their cards list.
|
|
||||||
// The x coordinate of such cards is always 0, so inserting at it
|
|
||||||
// would reverse the list on reconnect. Append instead.
|
|
||||||
if (zoneInfo.with_coords()) {
|
|
||||||
zone->addCard(card, false, cardInfo.x(), cardInfo.y());
|
|
||||||
} else {
|
|
||||||
zone->addCard(card, false, -1);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (zoneInfo.has_always_reveal_top_card()) {
|
if (zoneInfo.has_always_reveal_top_card()) {
|
||||||
|
|
|
||||||
|
|
@ -75,14 +75,6 @@ PlayerLogic *PlayerManager::getPlayer(int playerId) const
|
||||||
return player;
|
return player;
|
||||||
}
|
}
|
||||||
|
|
||||||
void PlayerManager::clearSpectators()
|
|
||||||
{
|
|
||||||
const QList<int> spectatorIds = spectators.keys();
|
|
||||||
for (int spectatorId : spectatorIds) {
|
|
||||||
removeSpectator(spectatorId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void PlayerManager::onPlayerConceded(int playerId, bool conceded)
|
void PlayerManager::onPlayerConceded(int playerId, bool conceded)
|
||||||
{
|
{
|
||||||
// Everything else cares about this
|
// Everything else cares about this
|
||||||
|
|
|
||||||
|
|
@ -100,9 +100,6 @@ public:
|
||||||
emit spectatorRemoved(spectatorId, spectatorInfo);
|
emit spectatorRemoved(spectatorId, spectatorInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @brief Remove all spectators, emitting the removal signal for each. */
|
|
||||||
void clearSpectators();
|
|
||||||
|
|
||||||
[[nodiscard]] AbstractGame *getGame() const
|
[[nodiscard]] AbstractGame *getGame() const
|
||||||
{
|
{
|
||||||
return game;
|
return game;
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
#include "abstract_card_item.h"
|
#include "abstract_card_item.h"
|
||||||
|
|
||||||
#include "../../client/settings/cache_settings.h"
|
#include "../../client/settings/cache_settings.h"
|
||||||
#include "../../interface/card_localization.h"
|
|
||||||
#include "../../interface/card_picture_loader/card_picture_loader.h"
|
#include "../../interface/card_picture_loader/card_picture_loader.h"
|
||||||
#include "../game_scene.h"
|
#include "../game_scene.h"
|
||||||
#include "../z_values.h"
|
#include "../z_values.h"
|
||||||
|
|
@ -27,8 +26,6 @@ AbstractCardItem::AbstractCardItem(QGraphicsItem *parent, const CardRef &cardRef
|
||||||
|
|
||||||
connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::displayCardNamesChanged, this,
|
connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::displayCardNamesChanged, this,
|
||||||
[this] { update(); });
|
[this] { update(); });
|
||||||
connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::cardLangChanged, this,
|
|
||||||
[this] { update(); });
|
|
||||||
refreshCardInfo();
|
refreshCardInfo();
|
||||||
|
|
||||||
connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::roundCardCornersChanged, this,
|
connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::roundCardCornersChanged, this,
|
||||||
|
|
@ -174,7 +171,7 @@ void AbstractCardItem::paintPicture(QPainter *painter, const QSizeF &translatedS
|
||||||
if (SettingsCache::instance().debug().getShowCardId()) {
|
if (SettingsCache::instance().debug().getShowCardId()) {
|
||||||
prefix = "#" + QString::number(id) + " ";
|
prefix = "#" + QString::number(id) + " ";
|
||||||
}
|
}
|
||||||
nameStr = prefix + CardLocalization::displayName(getCardInfo());
|
nameStr = prefix + cardRef.name;
|
||||||
}
|
}
|
||||||
painter->drawText(QRectF(3 * scaleFactor, 3 * scaleFactor, translatedSize.width() - 6 * scaleFactor,
|
painter->drawText(QRectF(3 * scaleFactor, 3 * scaleFactor, translatedSize.width() - 6 * scaleFactor,
|
||||||
translatedSize.height() - 6 * scaleFactor),
|
translatedSize.height() - 6 * scaleFactor),
|
||||||
|
|
|
||||||
|
|
@ -29,9 +29,9 @@ AbstractCounter::AbstractCounter(CounterState *state,
|
||||||
{
|
{
|
||||||
setAcceptHoverEvents(true);
|
setAcceptHoverEvents(true);
|
||||||
|
|
||||||
connect(state, &CounterState::valueChanged, this, [this](int oldValue, int newValue, bool skipDamageAnimation) {
|
connect(state, &CounterState::valueChanged, this, [this](int oldValue, int newValue) {
|
||||||
value = newValue;
|
value = newValue;
|
||||||
onValueChanged(oldValue, newValue, skipDamageAnimation);
|
onValueChanged(oldValue, newValue);
|
||||||
update();
|
update();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -230,7 +230,7 @@ void AbstractCounterDialog::changeValue(int diff)
|
||||||
setTextValue(QString::number(curValue));
|
setTextValue(QString::number(curValue));
|
||||||
}
|
}
|
||||||
|
|
||||||
void AbstractCounter::onValueChanged(int /*oldValue*/, int /*newValue*/, bool /*skipDamageAnimation*/)
|
void AbstractCounter::onValueChanged(int /*oldValue*/, int /*newValue*/)
|
||||||
{
|
{
|
||||||
// Default: no feedback. Subclasses such as PlayerCounter override this to
|
// Default: no feedback. Subclasses such as PlayerCounter override this to
|
||||||
// flash the counter on meaningful changes (life gain/loss).
|
// flash the counter on meaningful changes (life gain/loss).
|
||||||
|
|
|
||||||
|
|
@ -39,9 +39,8 @@ protected:
|
||||||
* @brief Hook for subclasses that need per-value-change feedback (e.g. life-total flash).
|
* @brief Hook for subclasses that need per-value-change feedback (e.g. life-total flash).
|
||||||
*
|
*
|
||||||
* Called whenever the counter's value changes, before the item repaints.
|
* Called whenever the counter's value changes, before the item repaints.
|
||||||
* @param skipDamageAnimation True when damage-related feedback should be suppressed (replay rewinds).
|
|
||||||
*/
|
*/
|
||||||
virtual void onValueChanged(int oldValue, int newValue, bool skipDamageAnimation);
|
virtual void onValueChanged(int oldValue, int newValue);
|
||||||
|
|
||||||
void mousePressEvent(QGraphicsSceneMouseEvent *event) override;
|
void mousePressEvent(QGraphicsSceneMouseEvent *event) override;
|
||||||
void hoverEnterEvent(QGraphicsSceneHoverEvent *event) override;
|
void hoverEnterEvent(QGraphicsSceneHoverEvent *event) override;
|
||||||
|
|
|
||||||
|
|
@ -316,7 +316,7 @@ void CardItem::drawAttachArrow()
|
||||||
|
|
||||||
for (const auto &item : scene()->selectedItems()) {
|
for (const auto &item : scene()->selectedItems()) {
|
||||||
CardItem *card = qgraphicsitem_cast<CardItem *>(item);
|
CardItem *card = qgraphicsitem_cast<CardItem *>(item);
|
||||||
if (card == nullptr || card == this) {
|
if (card == nullptr) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (card->getZone() != state->getZone()) {
|
if (card->getZone() != state->getZone()) {
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <libcockatrice/card/card_info.h>
|
#include <libcockatrice/card/card_info.h>
|
||||||
#include <libcockatrice/deck_list/deck_list.h>
|
#include <libcockatrice/deck_list/deck_list.h>
|
||||||
|
#include <libcockatrice/deck_list/tree/deck_list_card_node.h>
|
||||||
#include <libcockatrice/settings/cards_display_settings.h>
|
#include <libcockatrice/settings/cards_display_settings.h>
|
||||||
|
|
||||||
DeckViewCardDragItem::DeckViewCardDragItem(DeckViewCard *_item,
|
DeckViewCardDragItem::DeckViewCardDragItem(DeckViewCard *_item,
|
||||||
|
|
@ -380,10 +381,12 @@ void DeckViewScene::rebuildTree()
|
||||||
addItem(container);
|
addItem(container);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cards in custom zones nested under a board are regular board cards in-game.
|
for (int j = 0; j < currentZone->size(); j++) {
|
||||||
// They are collected recursively (like every other consumer) and reported with
|
auto *currentCard = dynamic_cast<DecklistCardNode *>(currentZone->at(j));
|
||||||
// the top-level board zone as their origin, so that sideboard plans keep working.
|
if (!currentCard) {
|
||||||
for (auto *currentCard : deck->getCardNodes({currentZone->getName()})) {
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
for (int k = 0; k < currentCard->getNumber(); ++k) {
|
for (int k = 0; k < currentCard->getNumber(); ++k) {
|
||||||
auto *newCard = new DeckViewCard(container, currentCard->toCardRef(), currentZone->getName());
|
auto *newCard = new DeckViewCard(container, currentCard->toCardRef(), currentZone->getName());
|
||||||
container->addCard(newCard);
|
container->addCard(newCard);
|
||||||
|
|
|
||||||
|
|
@ -16,13 +16,11 @@
|
||||||
#include <QLineEdit>
|
#include <QLineEdit>
|
||||||
#include <QRadioButton>
|
#include <QRadioButton>
|
||||||
#include <QTreeView>
|
#include <QTreeView>
|
||||||
#include <libcockatrice/card/card_localization.h>
|
|
||||||
#include <libcockatrice/card/database/card_database_manager.h>
|
#include <libcockatrice/card/database/card_database_manager.h>
|
||||||
#include <libcockatrice/deck_list/deck_list.h>
|
#include <libcockatrice/deck_list/deck_list.h>
|
||||||
#include <libcockatrice/models/database/card_database_model.h>
|
#include <libcockatrice/models/database/card_database_model.h>
|
||||||
#include <libcockatrice/models/database/token/token_display_model.h>
|
#include <libcockatrice/models/database/token/token_display_model.h>
|
||||||
#include <libcockatrice/settings/card_override_settings.h>
|
#include <libcockatrice/settings/card_override_settings.h>
|
||||||
#include <libcockatrice/settings/cards_display_settings.h>
|
|
||||||
#include <libcockatrice/settings/interface_settings.h>
|
#include <libcockatrice/settings/interface_settings.h>
|
||||||
#include <libcockatrice/settings/layouts_settings.h>
|
#include <libcockatrice/settings/layouts_settings.h>
|
||||||
#include <libcockatrice/utility/string_limits.h>
|
#include <libcockatrice/utility/string_limits.h>
|
||||||
|
|
@ -90,17 +88,6 @@ DlgCreateToken::DlgCreateToken(const QStringList &_predefinedTokens, QWidget *pa
|
||||||
cardDatabaseDisplayModel = new TokenDisplayModel(this);
|
cardDatabaseDisplayModel = new TokenDisplayModel(this);
|
||||||
cardDatabaseDisplayModel->setSourceModel(cardDatabaseModel);
|
cardDatabaseDisplayModel->setSourceModel(cardDatabaseModel);
|
||||||
|
|
||||||
const auto applyCardSearchLanguage = [this]() {
|
|
||||||
const CardsDisplaySettings &cardsDisplay = SettingsCache::instance().cardsDisplay();
|
|
||||||
cardDatabaseDisplayModel->setSearchLanguage(CardSearchLanguage{
|
|
||||||
cardsDisplay.getCardLang(), static_cast<SearchLanguageMode>(cardsDisplay.getCardSearchLanguage())});
|
|
||||||
};
|
|
||||||
applyCardSearchLanguage();
|
|
||||||
connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::cardLangChanged, this,
|
|
||||||
applyCardSearchLanguage);
|
|
||||||
connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::cardSearchLanguageChanged, this,
|
|
||||||
applyCardSearchLanguage);
|
|
||||||
|
|
||||||
chooseTokenFromAllRadioButton = new QRadioButton(tr("Show &all tokens"));
|
chooseTokenFromAllRadioButton = new QRadioButton(tr("Show &all tokens"));
|
||||||
connect(chooseTokenFromAllRadioButton, &QRadioButton::toggled, this, &DlgCreateToken::actChooseTokenFromAll);
|
connect(chooseTokenFromAllRadioButton, &QRadioButton::toggled, this, &DlgCreateToken::actChooseTokenFromAll);
|
||||||
chooseTokenFromDeckRadioButton = new QRadioButton(tr("Show tokens from this &deck"));
|
chooseTokenFromDeckRadioButton = new QRadioButton(tr("Show tokens from this &deck"));
|
||||||
|
|
|
||||||
|
|
@ -44,16 +44,11 @@ GameScene::GameScene(PhasesToolbar *_phasesToolbar, QObject *parent)
|
||||||
|
|
||||||
GameScene::~GameScene()
|
GameScene::~GameScene()
|
||||||
{
|
{
|
||||||
// Sever all destroyed->removeAnimatedItem connections before the members below
|
// Sever all incoming connections (animated item destroy-tracking) before the
|
||||||
// are destroyed: the base QGraphicsScene destructor destroys the remaining items,
|
// members below are destroyed: the base QGraphicsScene destructor destroys the
|
||||||
// and their destroyed() signals must not reach slots that reference members that
|
// remaining items, and their destroyed() signals must not reach slots that
|
||||||
// no longer exist. The connection handle overload is used because the string-based
|
// reference members that no longer exist.
|
||||||
// disconnect(nullptr, nullptr, this, nullptr) is invalid (the sender must never be
|
disconnect(this);
|
||||||
// nullptr) and would otherwise fail to sever these pointer-to-member connections.
|
|
||||||
for (auto it = animationItemConnections.constBegin(); it != animationItemConnections.constEnd(); ++it) {
|
|
||||||
QObject::disconnect(*it);
|
|
||||||
}
|
|
||||||
animationItemConnections.clear();
|
|
||||||
|
|
||||||
delete animationTimer;
|
delete animationTimer;
|
||||||
animationTimer = nullptr;
|
animationTimer = nullptr;
|
||||||
|
|
@ -221,12 +216,7 @@ void GameScene::removePlayer(PlayerLogic *player)
|
||||||
|
|
||||||
clearArrowsForPlayer(player->getPlayerInfo()->getId());
|
clearArrowsForPlayer(player->getPlayerInfo()->getId());
|
||||||
|
|
||||||
// Closing a view removes it from zoneViews synchronously, so iterate over a
|
for (ZoneViewWidget *zone : zoneViews) {
|
||||||
// copy: otherwise a player with several open views (e.g. library and hand)
|
|
||||||
// only has the first one closed here and the remaining views are left
|
|
||||||
// pointing at a player that is about to be deleted.
|
|
||||||
const QList<ZoneViewWidget *> zoneViewCopy = zoneViews;
|
|
||||||
for (ZoneViewWidget *zone : zoneViewCopy) {
|
|
||||||
if (zone->getPlayer() == player) {
|
if (zone->getPlayer() == player) {
|
||||||
zone->close();
|
zone->close();
|
||||||
}
|
}
|
||||||
|
|
@ -669,10 +659,7 @@ CardItem *GameScene::findTopmostCardInZone(const QList<QGraphicsItem *> &items,
|
||||||
*/
|
*/
|
||||||
void GameScene::toggleZoneView(PlayerLogic *player, const QString &zoneName, int numberCards, bool isReversed)
|
void GameScene::toggleZoneView(PlayerLogic *player, const QString &zoneName, int numberCards, bool isReversed)
|
||||||
{
|
{
|
||||||
// Closing a view removes it from zoneViews synchronously, so iterate over a
|
for (auto &view : zoneViews) {
|
||||||
// copy to make sure every already-open matching view is closed.
|
|
||||||
const QList<ZoneViewWidget *> zoneViewCopy = zoneViews;
|
|
||||||
for (auto *view : zoneViewCopy) {
|
|
||||||
ZoneViewZone *temp = view->getZone();
|
ZoneViewZone *temp = view->getZone();
|
||||||
if (temp->getLogic()->getName() == zoneName && temp->getLogic()->getPlayer() == player &&
|
if (temp->getLogic()->getName() == zoneName && temp->getLogic()->getPlayer() == player &&
|
||||||
qobject_cast<ZoneViewZoneLogic *>(temp->getLogic())->getNumberCards() == numberCards) {
|
qobject_cast<ZoneViewZoneLogic *>(temp->getLogic())->getNumberCards() == numberCards) {
|
||||||
|
|
@ -790,15 +777,8 @@ void GameScene::registerAnimationItem(IAnimatedItem *item)
|
||||||
if (!object) {
|
if (!object) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Guard against duplicate connections using the connection map, not
|
if (!animatedItems.contains(object)) {
|
||||||
// animatedItems: the animation timer removes entries from animatedItems when an
|
connect(object, &QObject::destroyed, this, &GameScene::removeAnimatedItem);
|
||||||
// animation completes, but the destroyed->removeAnimatedItem connection must
|
|
||||||
// persist until the object is destroyed. Relying on animatedItems here would let
|
|
||||||
// a re-registered item (e.g. a life counter that flashes repeatedly) accumulate
|
|
||||||
// duplicate destroyed connections, the older ones of which would survive teardown.
|
|
||||||
if (!animationItemConnections.contains(object)) {
|
|
||||||
animationItemConnections.insert(object,
|
|
||||||
connect(object, &QObject::destroyed, this, &GameScene::removeAnimatedItem));
|
|
||||||
}
|
}
|
||||||
animatedItems.insert(object, item);
|
animatedItems.insert(object, item);
|
||||||
if (animationTimer && !animationTimer->isActive()) {
|
if (animationTimer && !animationTimer->isActive()) {
|
||||||
|
|
@ -817,7 +797,6 @@ void GameScene::unregisterAnimationItem(IAnimatedItem *item)
|
||||||
void GameScene::removeAnimatedItem(QObject *item)
|
void GameScene::removeAnimatedItem(QObject *item)
|
||||||
{
|
{
|
||||||
animatedItems.remove(item);
|
animatedItems.remove(item);
|
||||||
animationItemConnections.remove(item);
|
|
||||||
if (animationTimer && animatedItems.isEmpty()) {
|
if (animationTimer && animatedItems.isEmpty()) {
|
||||||
animationTimer->stop();
|
animationTimer->stop();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -54,11 +54,9 @@ private:
|
||||||
QPointer<CardItem> hoveredCard; ///< Currently hovered card
|
QPointer<CardItem> hoveredCard; ///< Currently hovered card
|
||||||
QBasicTimer *animationTimer; ///< Timer for scene animations
|
QBasicTimer *animationTimer; ///< Timer for scene animations
|
||||||
QHash<QObject *, IAnimatedItem *> animatedItems; ///< Items currently animating
|
QHash<QObject *, IAnimatedItem *> animatedItems; ///< Items currently animating
|
||||||
QHash<QObject *, QMetaObject::Connection>
|
int playerRotation; ///< Rotation offset for player layout
|
||||||
animationItemConnections; ///< destroyed->removeAnimatedItem handles per animated item
|
bool rearranging = false; ///< Guard against re-entrant rearrange
|
||||||
int playerRotation; ///< Rotation offset for player layout
|
bool needsReArrange = false; ///< Pending rearrange requested during a pass
|
||||||
bool rearranging = false; ///< Guard against re-entrant rearrange
|
|
||||||
bool needsReArrange = false; ///< Pending rearrange requested during a pass
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Updates which card is currently hovered based on scene coordinates.
|
* @brief Updates which card is currently hovered based on scene coordinates.
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
#include "hand_counter.h"
|
#include "hand_counter.h"
|
||||||
|
|
||||||
#include "../interface/pixel_map_generator.h"
|
|
||||||
#include "zones/card_zone.h"
|
#include "zones/card_zone.h"
|
||||||
|
|
||||||
#include <QGraphicsSceneMouseEvent>
|
#include <QGraphicsSceneMouseEvent>
|
||||||
|
|
@ -33,8 +32,7 @@ void HandCounter::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*op
|
||||||
QSize translatedSize = painter->combinedTransform().mapRect(boundingRect()).size().toSize();
|
QSize translatedSize = painter->combinedTransform().mapRect(boundingRect()).size().toSize();
|
||||||
QPixmap cachedPixmap;
|
QPixmap cachedPixmap;
|
||||||
if (!QPixmapCache::find("handCounter" + QString::number(translatedSize.width()), &cachedPixmap)) {
|
if (!QPixmapCache::find("handCounter" + QString::number(translatedSize.width()), &cachedPixmap)) {
|
||||||
cachedPixmap =
|
cachedPixmap = QPixmap("theme:hand").scaled(translatedSize, Qt::KeepAspectRatio, Qt::SmoothTransformation);
|
||||||
themePixmap(QStringLiteral("hand")).scaled(translatedSize, Qt::KeepAspectRatio, Qt::SmoothTransformation);
|
|
||||||
QPixmapCache::insert("handCounter" + QString::number(translatedSize.width()), cachedPixmap);
|
QPixmapCache::insert("handCounter" + QString::number(translatedSize.width()), cachedPixmap);
|
||||||
}
|
}
|
||||||
resetPainterTransform(painter);
|
resetPainterTransform(painter);
|
||||||
|
|
|
||||||
|
|
@ -12,13 +12,11 @@ TallyMenu::TallyMenu()
|
||||||
aTallyNone = createTallyAction(TallyType::None);
|
aTallyNone = createTallyAction(TallyType::None);
|
||||||
aTallySubtypes = createTallyAction(TallyType::Subtypes);
|
aTallySubtypes = createTallyAction(TallyType::Subtypes);
|
||||||
aTallyTotalPower = createTallyAction(TallyType::TotalPower);
|
aTallyTotalPower = createTallyAction(TallyType::TotalPower);
|
||||||
aTallyTotalToughness = createTallyAction(TallyType::TotalToughness);
|
|
||||||
|
|
||||||
addAction(aTallyNone);
|
addAction(aTallyNone);
|
||||||
addSeparator();
|
addSeparator();
|
||||||
addAction(aTallySubtypes);
|
addAction(aTallySubtypes);
|
||||||
addAction(aTallyTotalPower);
|
addAction(aTallyTotalPower);
|
||||||
addAction(aTallyTotalToughness);
|
|
||||||
|
|
||||||
retranslateUi();
|
retranslateUi();
|
||||||
}
|
}
|
||||||
|
|
@ -56,5 +54,4 @@ void TallyMenu::retranslateUi()
|
||||||
aTallyNone->setText(tr("None"));
|
aTallyNone->setText(tr("None"));
|
||||||
aTallySubtypes->setText(tr("Subtypes"));
|
aTallySubtypes->setText(tr("Subtypes"));
|
||||||
aTallyTotalPower->setText(tr("Total Power"));
|
aTallyTotalPower->setText(tr("Total Power"));
|
||||||
aTallyTotalToughness->setText(tr("Total Toughness"));
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,6 @@ private:
|
||||||
QAction *aTallyNone = nullptr;
|
QAction *aTallyNone = nullptr;
|
||||||
QAction *aTallySubtypes = nullptr;
|
QAction *aTallySubtypes = nullptr;
|
||||||
QAction *aTallyTotalPower = nullptr;
|
QAction *aTallyTotalPower = nullptr;
|
||||||
QAction *aTallyTotalToughness = nullptr;
|
|
||||||
|
|
||||||
QAction *createTallyAction(TallyType tallyType);
|
QAction *createTallyAction(TallyType tallyType);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@
|
||||||
#include "../../game/player/player_actions.h"
|
#include "../../game/player/player_actions.h"
|
||||||
#include "../../interface/card_picture_loader/card_picture_loader.h"
|
#include "../../interface/card_picture_loader/card_picture_loader.h"
|
||||||
#include "../../interface/widgets/cards/art_crop_attribution.h"
|
#include "../../interface/widgets/cards/art_crop_attribution.h"
|
||||||
#include "../../interface/widgets/cards/card_art_utils.h"
|
|
||||||
#include "../../interface/widgets/playmat/playmat_utils.h"
|
#include "../../interface/widgets/playmat/playmat_utils.h"
|
||||||
#include "../../interface/widgets/tabs/tab_game.h"
|
#include "../../interface/widgets/tabs/tab_game.h"
|
||||||
#include "../board/abstract_card_item.h"
|
#include "../board/abstract_card_item.h"
|
||||||
|
|
@ -252,8 +251,8 @@ void PlayerGraphicsItem::onCounterAdded(CounterState *state)
|
||||||
AbstractCounter *widget;
|
AbstractCounter *widget;
|
||||||
if (state->getName() == "life") {
|
if (state->getName() == "life") {
|
||||||
widget = playerTarget->addCounter(state);
|
widget = playerTarget->addCounter(state);
|
||||||
connect(state, &CounterState::valueChanged, this, [this](int oldValue, int newValue, bool skipDamageAnimation) {
|
connect(state, &CounterState::valueChanged, this, [this](int oldValue, int newValue) {
|
||||||
if (newValue < oldValue && !skipDamageAnimation) {
|
if (newValue < oldValue) {
|
||||||
tableZoneGraphicsItem->triggerDamageShimmer();
|
tableZoneGraphicsItem->triggerDamageShimmer();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -443,7 +442,7 @@ void PlayerGraphicsItem::updatePlaymat()
|
||||||
hasPlaymat = true;
|
hasPlaymat = true;
|
||||||
emit playmatChanged(true);
|
emit playmatChanged(true);
|
||||||
}
|
}
|
||||||
playmatPixmap = CardArtUtils::rotateSidewaysLayoutArt(fullRes, card);
|
playmatPixmap = fullRes;
|
||||||
update();
|
update();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -53,13 +53,13 @@ PlayerListWidget::PlayerListWidget(TabSupervisor *_tabSupervisor,
|
||||||
QWidget *parent)
|
QWidget *parent)
|
||||||
: QTreeWidget(parent), tabSupervisor(_tabSupervisor), client(_client), game(_game), gameStarted(false)
|
: QTreeWidget(parent), tabSupervisor(_tabSupervisor), client(_client), game(_game), gameStarted(false)
|
||||||
{
|
{
|
||||||
readyIcon = themePixmap(QStringLiteral("icons/ready_start"));
|
readyIcon = QPixmap("theme:icons/ready_start");
|
||||||
notReadyIcon = themePixmap(QStringLiteral("icons/not_ready_start"));
|
notReadyIcon = QPixmap("theme:icons/not_ready_start");
|
||||||
concededIcon = themePixmap(QStringLiteral("icons/conceded"));
|
concededIcon = QPixmap("theme:icons/conceded");
|
||||||
playerIcon = loadColorAdjustedPixmap("theme:icons/player");
|
playerIcon = loadColorAdjustedPixmap("theme:icons/player");
|
||||||
judgeIcon = loadColorAdjustedPixmap("theme:icons/scales");
|
judgeIcon = loadColorAdjustedPixmap("theme:icons/scales");
|
||||||
spectatorIcon = loadColorAdjustedPixmap("theme:icons/spectator");
|
spectatorIcon = loadColorAdjustedPixmap("theme:icons/spectator");
|
||||||
lockIcon = themePixmap(QStringLiteral("icons/lock"));
|
lockIcon = QPixmap("theme:icons/lock");
|
||||||
|
|
||||||
if (tabSupervisor) {
|
if (tabSupervisor) {
|
||||||
itemDelegate = new PlayerListItemDelegate(this);
|
itemDelegate = new PlayerListItemDelegate(this);
|
||||||
|
|
@ -92,11 +92,6 @@ void PlayerListWidget::retranslateUi()
|
||||||
|
|
||||||
void PlayerListWidget::addPlayer(const ServerInfo_PlayerProperties &player)
|
void PlayerListWidget::addPlayer(const ServerInfo_PlayerProperties &player)
|
||||||
{
|
{
|
||||||
if (players.contains(player.player_id())) {
|
|
||||||
updatePlayerProperties(player);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
QTreeWidgetItem *newPlayer = new PlayerListTWI;
|
QTreeWidgetItem *newPlayer = new PlayerListTWI;
|
||||||
players.insert(player.player_id(), newPlayer);
|
players.insert(player.player_id(), newPlayer);
|
||||||
updatePlayerProperties(player);
|
updatePlayerProperties(player);
|
||||||
|
|
@ -181,17 +176,6 @@ void PlayerListWidget::removePlayer(int playerId)
|
||||||
delete takeTopLevelItem(indexOfTopLevelItem(player));
|
delete takeTopLevelItem(indexOfTopLevelItem(player));
|
||||||
}
|
}
|
||||||
|
|
||||||
void PlayerListWidget::clearSpectators()
|
|
||||||
{
|
|
||||||
const QList<int> playerIds = players.keys();
|
|
||||||
for (int playerId : playerIds) {
|
|
||||||
QTreeWidgetItem *player = players.value(playerId, 0);
|
|
||||||
if (player && !player->data(1, Qt::UserRole).toBool()) {
|
|
||||||
removePlayer(playerId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void PlayerListWidget::setActivePlayer(int playerId)
|
void PlayerListWidget::setActivePlayer(int playerId)
|
||||||
{
|
{
|
||||||
QMapIterator<int, QTreeWidgetItem *> i(players);
|
QMapIterator<int, QTreeWidgetItem *> i(players);
|
||||||
|
|
|
||||||
|
|
@ -66,7 +66,6 @@ public slots:
|
||||||
void addPlayer(const ServerInfo_PlayerProperties &player);
|
void addPlayer(const ServerInfo_PlayerProperties &player);
|
||||||
void removePlayer(int playerId);
|
void removePlayer(int playerId);
|
||||||
void updatePlayerProperties(const ServerInfo_PlayerProperties &prop, int playerId = -1);
|
void updatePlayerProperties(const ServerInfo_PlayerProperties &prop, int playerId = -1);
|
||||||
void clearSpectators();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
|
||||||
|
|
@ -69,7 +69,7 @@ void PlayerCounter::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void PlayerCounter::onValueChanged(int oldValue, int newValue, bool skipDamageAnimation)
|
void PlayerCounter::onValueChanged(int oldValue, int newValue)
|
||||||
{
|
{
|
||||||
flashDelta = newValue - oldValue;
|
flashDelta = newValue - oldValue;
|
||||||
if (flashDelta == 0) {
|
if (flashDelta == 0) {
|
||||||
|
|
@ -81,11 +81,6 @@ void PlayerCounter::onValueChanged(int oldValue, int newValue, bool skipDamageAn
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (skipDamageAnimation) {
|
|
||||||
flashAlpha = 0.0;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
flashAlpha = 1.0;
|
flashAlpha = 1.0;
|
||||||
flashClock.start();
|
flashClock.start();
|
||||||
if (scene()) {
|
if (scene()) {
|
||||||
|
|
@ -137,18 +132,8 @@ void PlayerTarget::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*o
|
||||||
QRectF translatedRect = painter->combinedTransform().mapRect(avatarBoundingRect);
|
QRectF translatedRect = painter->combinedTransform().mapRect(avatarBoundingRect);
|
||||||
QSize translatedSize = translatedRect.size().toSize();
|
QSize translatedSize = translatedRect.size().toSize();
|
||||||
QPixmap cachedPixmap;
|
QPixmap cachedPixmap;
|
||||||
// The key must cover everything the generated pawn depends on: the rendered
|
|
||||||
// size, the user level, and the pixmap being drawn. fullPixmap.cacheKey() is
|
|
||||||
// 0 for every null pixmap, so the default-pawn branch additionally needs the
|
|
||||||
// pawn's privlevel (lowercased, matching UserLevelPixmapGenerator) and colors
|
|
||||||
// in the key — otherwise two players without a custom avatar (and the same
|
|
||||||
// user level) would share one cached pawn.
|
|
||||||
const QString cacheKey = "avatar" + QString::number(translatedSize.width()) + "_" +
|
const QString cacheKey = "avatar" + QString::number(translatedSize.width()) + "_" +
|
||||||
QString::number(translatedSize.height()) + "_" + QString::number(info->user_level()) +
|
QString::number(info->user_level()) + "_" + QString::number(fullPixmap.cacheKey());
|
||||||
"_" + QString::number(fullPixmap.cacheKey()) + "_" +
|
|
||||||
QString::fromStdString(info->privlevel()).toLower() + "_" +
|
|
||||||
QString::fromStdString(info->pawn_colors().left_side()) + "_" +
|
|
||||||
QString::fromStdString(info->pawn_colors().right_side());
|
|
||||||
if (!QPixmapCache::find(cacheKey, &cachedPixmap)) {
|
if (!QPixmapCache::find(cacheKey, &cachedPixmap)) {
|
||||||
cachedPixmap = QPixmap(translatedSize.width(), translatedSize.height());
|
cachedPixmap = QPixmap(translatedSize.width(), translatedSize.height());
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ class PlayerCounter : public AbstractCounter, public IAnimatedItem
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
protected:
|
protected:
|
||||||
void onValueChanged(int oldValue, int newValue, bool skipDamageAnimation) override;
|
void onValueChanged(int oldValue, int newValue) override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
static constexpr qreal flashDurationMs = 450.0;
|
static constexpr qreal flashDurationMs = 450.0;
|
||||||
|
|
|
||||||
|
|
@ -34,31 +34,3 @@ QList<TallyRow> StatsTally::computeTotalPower(const QList<CardItem *> &cards)
|
||||||
QString name = QCoreApplication::translate("StatsTally", "Total Power");
|
QString name = QCoreApplication::translate("StatsTally", "Total Power");
|
||||||
return {TallyRow{name, QString::number(total)}};
|
return {TallyRow{name, QString::number(total)}};
|
||||||
}
|
}
|
||||||
|
|
||||||
static int sumToughness(const QList<CardItem *> &cards)
|
|
||||||
{
|
|
||||||
int total = 0;
|
|
||||||
for (auto card : cards) {
|
|
||||||
QVariantList parsed = CardItem::parsePT(card->getPT());
|
|
||||||
if (parsed.size() == 2) {
|
|
||||||
int toughness = parsed.at(1).toInt(); // toInt will default to 0 if it's not an int
|
|
||||||
total += qMax(toughness, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return total;
|
|
||||||
}
|
|
||||||
|
|
||||||
QList<TallyRow> StatsTally::computeTotalToughness(const QList<CardItem *> &cards)
|
|
||||||
{
|
|
||||||
// don't bother if none of the cards have pt
|
|
||||||
bool hasPT =
|
|
||||||
std::any_of(cards.cbegin(), cards.cend(), [](const CardItem *card) { return !card->getPT().isEmpty(); });
|
|
||||||
if (!hasPT) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
int total = sumToughness(cards);
|
|
||||||
|
|
||||||
QString name = QCoreApplication::translate("StatsTally", "Total Toughness");
|
|
||||||
return {TallyRow{name, QString::number(total)}};
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -16,14 +16,6 @@ namespace StatsTally
|
||||||
*/
|
*/
|
||||||
QList<TallyRow> computeTotalPower(const QList<CardItem *> &cards);
|
QList<TallyRow> computeTotalPower(const QList<CardItem *> &cards);
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Sums the toughness of all selected cards
|
|
||||||
*
|
|
||||||
* @param cards The list of selected card items to analyze.
|
|
||||||
* @return A single row containing the total, or an empty list if none of the cards have pt
|
|
||||||
*/
|
|
||||||
QList<TallyRow> computeTotalToughness(const QList<CardItem *> &cards);
|
|
||||||
|
|
||||||
} // namespace StatsTally
|
} // namespace StatsTally
|
||||||
|
|
||||||
#endif // COCKATRICE_STATS_TALLY_H
|
#endif // COCKATRICE_STATS_TALLY_H
|
||||||
|
|
|
||||||
|
|
@ -21,8 +21,6 @@ QList<TallyRow> Tally::compute(const QList<CardItem *> &cards, const TallyType t
|
||||||
return SubtypeTally::countSubtypes(cards);
|
return SubtypeTally::countSubtypes(cards);
|
||||||
case TallyType::TotalPower:
|
case TallyType::TotalPower:
|
||||||
return StatsTally::computeTotalPower(cards);
|
return StatsTally::computeTotalPower(cards);
|
||||||
case TallyType::TotalToughness:
|
|
||||||
return StatsTally::computeTotalToughness(cards);
|
|
||||||
}
|
}
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,8 +21,7 @@ enum class TallyType
|
||||||
None,
|
None,
|
||||||
Subtypes,
|
Subtypes,
|
||||||
TotalPower,
|
TotalPower,
|
||||||
TotalToughness,
|
MaxValue = TotalPower // sentinel value
|
||||||
MaxValue = TotalToughness // sentinel value
|
|
||||||
};
|
};
|
||||||
|
|
||||||
namespace Tally
|
namespace Tally
|
||||||
|
|
|
||||||
|
|
@ -41,8 +41,7 @@ void HandZone::handleDropEvent(const QList<CardDragItem *> &dragItems,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
bool sameZone = startZone == getLogic();
|
x = calcDropIndexFromY(dropPoint.y());
|
||||||
x = calcDropIndexFromY(dropPoint.y(), !sameZone);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Command_MoveCard cmd;
|
Command_MoveCard cmd;
|
||||||
|
|
|
||||||
|
|
@ -83,7 +83,7 @@ SelectZone::StackLayoutParams SelectZone::buildStackParams(qreal minOffset) cons
|
||||||
return {cardCount, boundingRect().height(), cardHeight, offset, minOffset};
|
return {cardCount, boundingRect().height(), cardHeight, offset, minOffset};
|
||||||
}
|
}
|
||||||
|
|
||||||
int SelectZone::calcDropIndexFromY(qreal dropY, bool allowCountExpand, qreal minOffset) const
|
int SelectZone::calcDropIndexFromY(qreal dropY, qreal minOffset) const
|
||||||
{
|
{
|
||||||
const auto &cards = getLogic()->getCards();
|
const auto &cards = getLogic()->getCards();
|
||||||
if (cards.isEmpty()) {
|
if (cards.isEmpty()) {
|
||||||
|
|
@ -94,8 +94,7 @@ int SelectZone::calcDropIndexFromY(qreal dropY, bool allowCountExpand, qreal min
|
||||||
if (effectiveOffset <= 0.0) {
|
if (effectiveOffset <= 0.0) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
int max = allowCountExpand ? params.cardCount : params.cardCount - 1;
|
return qBound(0, qRound((dropY - start) / effectiveOffset), params.cardCount - 1);
|
||||||
return qBound(0, qRound((dropY - start) / effectiveOffset), max);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void SelectZone::restoreStaleEscapedCards()
|
void SelectZone::restoreStaleEscapedCards()
|
||||||
|
|
|
||||||
|
|
@ -104,12 +104,8 @@ protected:
|
||||||
/**
|
/**
|
||||||
* @brief Computes the card index at a given y-coordinate within the zone's vertical layout.
|
* @brief Computes the card index at a given y-coordinate within the zone's vertical layout.
|
||||||
* Returns 0 if the zone has no cards or the offset is zero.
|
* Returns 0 if the zone has no cards or the offset is zero.
|
||||||
*
|
|
||||||
* @param dropY The y-coordinate that the card was dropped at
|
|
||||||
* @param allowCountExpand If false, clamps the index at the number of cards minus 1
|
|
||||||
* @param minOffset Minimum offset to preserve
|
|
||||||
*/
|
*/
|
||||||
int calcDropIndexFromY(qreal dropY, bool allowCountExpand, qreal minOffset = 0.0) const;
|
int calcDropIndexFromY(qreal dropY, qreal minOffset = 0.0) const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Positions cards vertically with alternating left/right x-offsets.
|
* @brief Positions cards vertically with alternating left/right x-offsets.
|
||||||
|
|
|
||||||
|
|
@ -57,14 +57,18 @@ void StackZone::handleDropEvent(const QList<CardDragItem *> &dragItems,
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool sameZone = startZone == getLogic();
|
const auto &cards = getLogic()->getCards();
|
||||||
int index = calcDropIndexFromY(dropPoint.y(), !sameZone, MIN_CARD_VISIBLE);
|
int index;
|
||||||
if (sameZone) {
|
if (startZone == getLogic()) {
|
||||||
|
// Reordering within the zone: use drop position
|
||||||
|
index = calcDropIndexFromY(dropPoint.y(), MIN_CARD_VISIBLE);
|
||||||
// Same-zone no-op: don't move a card onto itself
|
// Same-zone no-op: don't move a card onto itself
|
||||||
const auto &cards = getLogic()->getCards();
|
|
||||||
if (!cards.isEmpty() && cards.at(index)->getId() == dragItems.at(0)->getId()) {
|
if (!cards.isEmpty() && cards.at(index)->getId() == dragItems.at(0)->getId()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// Coming from another zone: append at end (top of stack, rendered on top)
|
||||||
|
index = static_cast<int>(cards.size());
|
||||||
}
|
}
|
||||||
|
|
||||||
Command_MoveCard cmd;
|
Command_MoveCard cmd;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
#include "view_zone.h"
|
#include "view_zone.h"
|
||||||
|
|
||||||
#include "../../client/settings/cache_settings.h"
|
|
||||||
#include "../../game/player/player_actions.h"
|
#include "../../game/player/player_actions.h"
|
||||||
#include "../../game/player/player_logic.h"
|
#include "../../game/player/player_logic.h"
|
||||||
#include "../../game/zones/view_zone_logic.h"
|
#include "../../game/zones/view_zone_logic.h"
|
||||||
|
|
@ -12,13 +11,11 @@
|
||||||
#include <QGraphicsSceneWheelEvent>
|
#include <QGraphicsSceneWheelEvent>
|
||||||
#include <QPainter>
|
#include <QPainter>
|
||||||
#include <QtMath>
|
#include <QtMath>
|
||||||
#include <libcockatrice/card/card_localization.h>
|
|
||||||
#include <libcockatrice/protocol/pb/command_dump_zone.pb.h>
|
#include <libcockatrice/protocol/pb/command_dump_zone.pb.h>
|
||||||
#include <libcockatrice/protocol/pb/command_move_card.pb.h>
|
#include <libcockatrice/protocol/pb/command_move_card.pb.h>
|
||||||
#include <libcockatrice/protocol/pb/response_dump_zone.pb.h>
|
#include <libcockatrice/protocol/pb/response_dump_zone.pb.h>
|
||||||
#include <libcockatrice/protocol/pb/serverinfo_card.pb.h>
|
#include <libcockatrice/protocol/pb/serverinfo_card.pb.h>
|
||||||
#include <libcockatrice/protocol/pending_command.h>
|
#include <libcockatrice/protocol/pending_command.h>
|
||||||
#include <libcockatrice/settings/cards_display_settings.h>
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param parent the parent QGraphicsWidget containing the reveal zone
|
* @param parent the parent QGraphicsWidget containing the reveal zone
|
||||||
|
|
@ -256,10 +253,7 @@ ZoneViewZone::GridSize ZoneViewZone::positionCardsForDisplay(CardList &cards, Ca
|
||||||
|
|
||||||
void ZoneViewZone::setFilterString(const QString &_filterString)
|
void ZoneViewZone::setFilterString(const QString &_filterString)
|
||||||
{
|
{
|
||||||
const CardsDisplaySettings &cardsDisplay = SettingsCache::instance().cardsDisplay();
|
filterString = FilterString(_filterString);
|
||||||
filterString = FilterString(
|
|
||||||
_filterString, CardSearchLanguage{cardsDisplay.getCardLang(),
|
|
||||||
static_cast<SearchLanguageMode>(cardsDisplay.getCardSearchLanguage())});
|
|
||||||
reorganizeCards();
|
reorganizeCards();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,6 @@
|
||||||
#include <QStyle>
|
#include <QStyle>
|
||||||
#include <QStyleOption>
|
#include <QStyleOption>
|
||||||
#include <libcockatrice/protocol/pb/command_shuffle.pb.h>
|
#include <libcockatrice/protocol/pb/command_shuffle.pb.h>
|
||||||
#include <libcockatrice/settings/cards_display_settings.h>
|
|
||||||
#include <libcockatrice/settings/interface_settings.h>
|
#include <libcockatrice/settings/interface_settings.h>
|
||||||
|
|
||||||
namespace
|
namespace
|
||||||
|
|
@ -63,7 +62,7 @@ ZoneViewWidget::ZoneViewWidget(PlayerLogic *_player,
|
||||||
searchEdit.setPlaceholderText(tr("Search by card name (or search expressions)"));
|
searchEdit.setPlaceholderText(tr("Search by card name (or search expressions)"));
|
||||||
searchEdit.setClearButtonEnabled(true);
|
searchEdit.setClearButtonEnabled(true);
|
||||||
searchEdit.addAction(loadColorAdjustedPixmap("theme:icons/search"), QLineEdit::LeadingPosition);
|
searchEdit.addAction(loadColorAdjustedPixmap("theme:icons/search"), QLineEdit::LeadingPosition);
|
||||||
auto help = searchEdit.addAction(themePixmap(QStringLiteral("icons/info")), QLineEdit::TrailingPosition);
|
auto help = searchEdit.addAction(QPixmap("theme:icons/info"), QLineEdit::TrailingPosition);
|
||||||
|
|
||||||
connect(help, &QAction::triggered, this, [this] { createSearchSyntaxHelpWindow(&searchEdit); });
|
connect(help, &QAction::triggered, this, [this] { createSearchSyntaxHelpWindow(&searchEdit); });
|
||||||
|
|
||||||
|
|
@ -169,12 +168,6 @@ ZoneViewWidget::ZoneViewWidget(PlayerLogic *_player,
|
||||||
}
|
}
|
||||||
|
|
||||||
connect(&searchEdit, &QLineEdit::textChanged, zone, &ZoneViewZone::setFilterString);
|
connect(&searchEdit, &QLineEdit::textChanged, zone, &ZoneViewZone::setFilterString);
|
||||||
|
|
||||||
const auto applyCardSearchLanguage = [this] { zone->setFilterString(searchEdit.text()); };
|
|
||||||
connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::cardLangChanged, this,
|
|
||||||
applyCardSearchLanguage);
|
|
||||||
connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::cardSearchLanguageChanged, this,
|
|
||||||
applyCardSearchLanguage);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setLayout(vbox);
|
setLayout(vbox);
|
||||||
|
|
@ -556,7 +549,7 @@ void ZoneViewWidget::initStyleOption(QStyleOption *option) const
|
||||||
{
|
{
|
||||||
QStyleOptionTitleBar *titleBar = qstyleoption_cast<QStyleOptionTitleBar *>(option);
|
QStyleOptionTitleBar *titleBar = qstyleoption_cast<QStyleOptionTitleBar *>(option);
|
||||||
if (titleBar) {
|
if (titleBar) {
|
||||||
titleBar->icon = themePixmap(QStringLiteral("cockatrice"));
|
titleBar->icon = QPixmap("theme:cockatrice");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,59 +0,0 @@
|
||||||
#ifndef COCKATRICE_CARD_LOCALIZATION_H
|
|
||||||
#define COCKATRICE_CARD_LOCALIZATION_H
|
|
||||||
|
|
||||||
#include "../client/settings/cache_settings.h"
|
|
||||||
|
|
||||||
#include <QString>
|
|
||||||
#include <libcockatrice/card/card_info.h>
|
|
||||||
#include <libcockatrice/settings/cards_display_settings.h>
|
|
||||||
|
|
||||||
namespace CardLocalization
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* @brief The language code selected for localized card text and images.
|
|
||||||
*/
|
|
||||||
inline QString displayLang()
|
|
||||||
{
|
|
||||||
return SettingsCache::instance().cardsDisplay().getCardLang();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Card name in the configured display language, falling back to English.
|
|
||||||
* @param card The card to display.
|
|
||||||
* @return The localized name, or an empty string for a null card.
|
|
||||||
*/
|
|
||||||
inline QString displayName(const CardInfoPtr &card)
|
|
||||||
{
|
|
||||||
return card.isNull() ? QString() : card->getLocalizedName(displayLang());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Card rules text in the configured display language, falling back to English.
|
|
||||||
* @param card The card to display.
|
|
||||||
* @return The localized text, or an empty string for a null card.
|
|
||||||
*/
|
|
||||||
inline QString displayText(const CardInfoPtr &card)
|
|
||||||
{
|
|
||||||
return card.isNull() ? QString() : card->getLocalizedText(displayLang());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Card name in the configured display language, falling back to English.
|
|
||||||
* @param card The card to display.
|
|
||||||
*/
|
|
||||||
inline QString displayName(const CardInfo &card)
|
|
||||||
{
|
|
||||||
return card.getLocalizedName(displayLang());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Card rules text in the configured display language, falling back to English.
|
|
||||||
* @param card The card to display.
|
|
||||||
*/
|
|
||||||
inline QString displayText(const CardInfo &card)
|
|
||||||
{
|
|
||||||
return card.getLocalizedText(displayLang());
|
|
||||||
}
|
|
||||||
} // namespace CardLocalization
|
|
||||||
|
|
||||||
#endif // COCKATRICE_CARD_LOCALIZATION_H
|
|
||||||
|
|
@ -1,14 +1,13 @@
|
||||||
#include "card_picture_loader.h"
|
#include "card_picture_loader.h"
|
||||||
|
|
||||||
#include "../../client/settings/cache_settings.h"
|
#include "../../client/settings/cache_settings.h"
|
||||||
#include "../pixel_map_generator.h"
|
|
||||||
#include "card_picture_loader_cache_method.h"
|
#include "card_picture_loader_cache_method.h"
|
||||||
#include "card_picture_loader_local_schemes.h"
|
#include "card_picture_loader_local_schemes.h"
|
||||||
|
|
||||||
#include <QApplication>
|
#include <QApplication>
|
||||||
#include <QBuffer>
|
#include <QBuffer>
|
||||||
#include <QDebug>
|
#include <QDebug>
|
||||||
#include <QDir>
|
#include <QDirIterator>
|
||||||
#include <QFileInfo>
|
#include <QFileInfo>
|
||||||
#include <QMainWindow>
|
#include <QMainWindow>
|
||||||
#include <QMovie>
|
#include <QMovie>
|
||||||
|
|
@ -20,7 +19,6 @@
|
||||||
#include <QThread>
|
#include <QThread>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <libcockatrice/settings/cache_storage_settings.h>
|
#include <libcockatrice/settings/cache_storage_settings.h>
|
||||||
#include <libcockatrice/settings/cards_display_settings.h>
|
|
||||||
#include <libcockatrice/settings/download_settings.h>
|
#include <libcockatrice/settings/download_settings.h>
|
||||||
#include <libcockatrice/settings/paths_settings.h>
|
#include <libcockatrice/settings/paths_settings.h>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
@ -38,10 +36,8 @@ CardPictureLoader::CardPictureLoader() : QObject(nullptr)
|
||||||
&CardPictureLoader::picsPathChanged);
|
&CardPictureLoader::picsPathChanged);
|
||||||
connect(&SettingsCache::instance().downloads(), &DownloadSettings::picDownloadChanged, this,
|
connect(&SettingsCache::instance().downloads(), &DownloadSettings::picDownloadChanged, this,
|
||||||
&CardPictureLoader::picDownloadChanged);
|
&CardPictureLoader::picDownloadChanged);
|
||||||
connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::cardLangChanged, this,
|
|
||||||
&CardPictureLoader::cardLangChanged);
|
|
||||||
|
|
||||||
qRegisterMetaType<ExactCard>("ExactCard");
|
qRegisterMetaType<ExactCard>();
|
||||||
connect(worker, &CardPictureLoaderWorker::imageLoaded, this, &CardPictureLoader::imageLoaded);
|
connect(worker, &CardPictureLoaderWorker::imageLoaded, this, &CardPictureLoader::imageLoaded);
|
||||||
|
|
||||||
statusBar = new CardPictureLoaderStatusBar(nullptr);
|
statusBar = new CardPictureLoaderStatusBar(nullptr);
|
||||||
|
|
@ -66,7 +62,7 @@ void CardPictureLoader::getCardBackPixmap(QPixmap &pixmap, QSize size)
|
||||||
QString backCacheKey = "_trice_card_back_" + QString::number(size.width()) + "x" + QString::number(size.height());
|
QString backCacheKey = "_trice_card_back_" + QString::number(size.width()) + "x" + QString::number(size.height());
|
||||||
if (!QPixmapCache::find(backCacheKey, &pixmap)) {
|
if (!QPixmapCache::find(backCacheKey, &pixmap)) {
|
||||||
qCDebug(CardPictureLoaderLog) << "PictureLoader: cache miss for" << backCacheKey;
|
qCDebug(CardPictureLoaderLog) << "PictureLoader: cache miss for" << backCacheKey;
|
||||||
QPixmap tmpPixmap = themePixmap(QStringLiteral("cardback"));
|
QPixmap tmpPixmap("theme:cardback");
|
||||||
|
|
||||||
if (tmpPixmap.isNull()) {
|
if (tmpPixmap.isNull()) {
|
||||||
qCWarning(CardPictureLoaderLog) << "Failed to load 'theme:cardback'! Using fallback pixmap.";
|
qCWarning(CardPictureLoaderLog) << "Failed to load 'theme:cardback'! Using fallback pixmap.";
|
||||||
|
|
@ -87,7 +83,7 @@ void CardPictureLoader::getCardBackLoadingInProgressPixmap(QPixmap &pixmap, QSiz
|
||||||
"_trice_card_back_inprogress_" + QString::number(size.width()) + "x" + QString::number(size.height());
|
"_trice_card_back_inprogress_" + QString::number(size.width()) + "x" + QString::number(size.height());
|
||||||
if (!QPixmapCache::find(backCacheKey, &pixmap)) {
|
if (!QPixmapCache::find(backCacheKey, &pixmap)) {
|
||||||
qCDebug(CardPictureLoaderCardBackCacheFailLog) << "PictureLoader: cache miss for" << backCacheKey;
|
qCDebug(CardPictureLoaderCardBackCacheFailLog) << "PictureLoader: cache miss for" << backCacheKey;
|
||||||
QPixmap tmpPixmap = themePixmap(QStringLiteral("cardback"));
|
QPixmap tmpPixmap("theme:cardback");
|
||||||
|
|
||||||
if (tmpPixmap.isNull()) {
|
if (tmpPixmap.isNull()) {
|
||||||
qCWarning(CardPictureLoaderLog) << "Failed to load 'theme:cardback' for in-progress state! Using fallback.";
|
qCWarning(CardPictureLoaderLog) << "Failed to load 'theme:cardback' for in-progress state! Using fallback.";
|
||||||
|
|
@ -109,7 +105,7 @@ void CardPictureLoader::getCardBackLoadingFailedPixmap(QPixmap &pixmap, QSize si
|
||||||
"_trice_card_back_failed_" + QString::number(size.width()) + "x" + QString::number(size.height());
|
"_trice_card_back_failed_" + QString::number(size.width()) + "x" + QString::number(size.height());
|
||||||
if (!QPixmapCache::find(backCacheKey, &pixmap)) {
|
if (!QPixmapCache::find(backCacheKey, &pixmap)) {
|
||||||
qCDebug(CardPictureLoaderCardBackCacheFailLog) << "PictureLoader: cache miss for" << backCacheKey;
|
qCDebug(CardPictureLoaderCardBackCacheFailLog) << "PictureLoader: cache miss for" << backCacheKey;
|
||||||
QPixmap tmpPixmap = themePixmap(QStringLiteral("cardback"));
|
QPixmap tmpPixmap("theme:cardback");
|
||||||
|
|
||||||
if (tmpPixmap.isNull()) {
|
if (tmpPixmap.isNull()) {
|
||||||
qCWarning(CardPictureLoaderLog) << "Failed to load 'theme:cardback' for failed state! Using fallback.";
|
qCWarning(CardPictureLoaderLog) << "Failed to load 'theme:cardback' for failed state! Using fallback.";
|
||||||
|
|
@ -142,8 +138,7 @@ void CardPictureLoader::getPixmap(QPixmap &pixmap, const ExactCard &card, QSize
|
||||||
QPixmap bigPixmap;
|
QPixmap bigPixmap;
|
||||||
if (QPixmapCache::find(key, &bigPixmap)) {
|
if (QPixmapCache::find(key, &bigPixmap)) {
|
||||||
if (bigPixmap.isNull()) {
|
if (bigPixmap.isNull()) {
|
||||||
// Leave the pixmap null so callers fall back to a solid color
|
getCardBackLoadingFailedPixmap(pixmap, size);
|
||||||
// instead of showing the card back.
|
|
||||||
QDateTime failedAtTime = getInstance().failedAt.value(key);
|
QDateTime failedAtTime = getInstance().failedAt.value(key);
|
||||||
if (!failedAtTime.isValid() ||
|
if (!failedAtTime.isValid() ||
|
||||||
failedAtTime.addSecs(RETRY_FAILED_CARDS_SECS) < QDateTime::currentDateTime()) {
|
failedAtTime.addSecs(RETRY_FAILED_CARDS_SECS) < QDateTime::currentDateTime()) {
|
||||||
|
|
@ -209,49 +204,7 @@ void CardPictureLoader::imageLoaded(const ExactCard &card, const QImage &image)
|
||||||
card.emitPixmapUpdated();
|
card.emitPixmapUpdated();
|
||||||
}
|
}
|
||||||
|
|
||||||
void CardPictureLoader::deleteAllLocalOverrides(const ExactCard &card)
|
void CardPictureLoader::saveCardImageToLocalStorage(const ExactCard &card, const QPixmap &pixmap)
|
||||||
{
|
|
||||||
const QString picsRoot = SettingsCache::instance().paths().getPicsPath();
|
|
||||||
if (picsRoot.isEmpty() || !card) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
QDir baseDir(picsRoot);
|
|
||||||
if (!baseDir.cd("downloadedPics")) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const QString name = card.getInfo().getCorrectedName();
|
|
||||||
|
|
||||||
QString set, collector, uuid;
|
|
||||||
auto printing = card.getPrinting();
|
|
||||||
if (printing.getSet()) {
|
|
||||||
set = printing.getSet()->getCorrectedShortName();
|
|
||||||
collector = printing.getProperty("num");
|
|
||||||
uuid = printing.getUuid();
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const auto &scheme : CardPictureLoaderLocalSchemes::exportSchemes()) {
|
|
||||||
QString rel = CardPictureLoaderLocalSchemes::expandPattern(scheme.pattern, name, set, collector, uuid);
|
|
||||||
|
|
||||||
if (rel.isEmpty()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
rel += ".png";
|
|
||||||
rel = QDir::cleanPath(rel);
|
|
||||||
|
|
||||||
QString fullPath = baseDir.filePath(rel);
|
|
||||||
|
|
||||||
if (QFile::exists(fullPath)) {
|
|
||||||
QFile::remove(fullPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void CardPictureLoader::saveCardImageToLocalStorage(const ExactCard &card,
|
|
||||||
const QPixmap &pixmap,
|
|
||||||
const bool allowOverwrite)
|
|
||||||
{
|
{
|
||||||
if (pixmap.isNull() || !card) {
|
if (pixmap.isNull() || !card) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -311,9 +264,8 @@ void CardPictureLoader::saveCardImageToLocalStorage(const ExactCard &card,
|
||||||
|
|
||||||
QFileInfo outInfo(baseDir.filePath(relativePath));
|
QFileInfo outInfo(baseDir.filePath(relativePath));
|
||||||
|
|
||||||
// Automatic cache writes (FILESYSTEM_CACHE) must never clobber an explicit user override.
|
// Do not overwrite existing files
|
||||||
// Only the explicit override paths pass allowOverwrite == true.
|
if (outInfo.exists()) {
|
||||||
if (!allowOverwrite && outInfo.exists()) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -334,122 +286,6 @@ void CardPictureLoader::saveCardImageToLocalStorage(const ExactCard &card,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void CardPictureLoader::installPrintingOverrideOnLoad(const ExactCard &originalCard, const ExactCard &overrideCard)
|
|
||||||
{
|
|
||||||
// Overriding a card with itself is the reset case, not a real override: every code path below
|
|
||||||
// would re-enter itself through emitPixmapUpdated(). Reject it outright.
|
|
||||||
if (originalCard == overrideCard) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
CardInfoPtr cardPtr = overrideCard.getCardPtr();
|
|
||||||
if (!cardPtr) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Heap-allocate so the lambda can capture it before the connection is made
|
|
||||||
auto *connectionHandle = new QMetaObject::Connection;
|
|
||||||
|
|
||||||
*connectionHandle =
|
|
||||||
connect(cardPtr.data(), &CardInfo::pixmapUpdated, cardPtr.data(),
|
|
||||||
[originalCard, overrideCard, connectionHandle, this](const PrintingInfo &printing) {
|
|
||||||
// All printings share the same CardInfo, so ignore updates triggered by any
|
|
||||||
// other printing (e.g., the original card re-loading from disk).
|
|
||||||
if (printing != overrideCard.getPrinting()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
QPixmap pixmap;
|
|
||||||
if (QPixmapCache::find(overrideCard.getPixmapCacheKey(), &pixmap) && !pixmap.isNull()) {
|
|
||||||
// The override art has resolved — persist it and reflect it immediately.
|
|
||||||
// Retire the connection before emitting so the refresh can't re-enter.
|
|
||||||
saveCardImageToLocalStorage(originalCard, pixmap, /*allowOverwrite=*/true);
|
|
||||||
|
|
||||||
QObject::disconnect(*connectionHandle);
|
|
||||||
delete connectionHandle;
|
|
||||||
|
|
||||||
QPixmapCache::clear();
|
|
||||||
originalCard.emitPixmapUpdated();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// The art could not be resolved. Keep the connection armed so a late resolution
|
|
||||||
// still lands, and surface a visible refusal instead of a silent no-op. An
|
|
||||||
// override already on disk is left untouched and simply re-displayed.
|
|
||||||
QPixmapCache::clear();
|
|
||||||
if (!hasLocalOverrides(originalCard)) {
|
|
||||||
QPixmap refusedPixmap;
|
|
||||||
getCardBackLoadingFailedPixmap(refusedPixmap, QSize(480, 672));
|
|
||||||
QPixmapCache::insert(originalCard.getPixmapCacheKey(), refusedPixmap);
|
|
||||||
}
|
|
||||||
originalCard.emitPixmapUpdated();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Now enqueue; if the image is already loading (deduplicated in the worker),
|
|
||||||
// the signal will still fire when it completes
|
|
||||||
CardPictureLoader::getInstance().worker->enqueueImageLoad(overrideCard);
|
|
||||||
}
|
|
||||||
|
|
||||||
void CardPictureLoader::installPrintingOverride(const ExactCard &originalCard, const ExactCard &overrideCard)
|
|
||||||
{
|
|
||||||
// Same guard as installPrintingOverrideOnLoad: self-override is the reset case.
|
|
||||||
if (originalCard == overrideCard) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
QPixmap pixmap;
|
|
||||||
const QString key = overrideCard.getPixmapCacheKey();
|
|
||||||
|
|
||||||
if (QPixmapCache::find(key, &pixmap) && !pixmap.isNull()) {
|
|
||||||
// Already cached — save immediately; the caller refreshes the card.
|
|
||||||
saveCardImageToLocalStorage(originalCard, pixmap, /*allowOverwrite=*/true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cache miss or previously failed load — enqueue load and wait for the signal.
|
|
||||||
installPrintingOverrideOnLoad(originalCard, overrideCard);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool CardPictureLoader::hasLocalOverrides(const ExactCard &card)
|
|
||||||
{
|
|
||||||
const QString picsRoot = SettingsCache::instance().paths().getPicsPath();
|
|
||||||
if (picsRoot.isEmpty() || !card) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
QDir baseDir(picsRoot);
|
|
||||||
if (!baseDir.cd("downloadedPics")) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const QString name = card.getInfo().getCorrectedName();
|
|
||||||
|
|
||||||
QString set, collector, uuid;
|
|
||||||
const PrintingInfo printing = card.getPrinting();
|
|
||||||
if (printing.getSet()) {
|
|
||||||
set = printing.getSet()->getCorrectedShortName();
|
|
||||||
collector = printing.getProperty("num");
|
|
||||||
uuid = printing.getUuid();
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const auto &scheme : CardPictureLoaderLocalSchemes::exportSchemes()) {
|
|
||||||
QString rel = CardPictureLoaderLocalSchemes::expandPattern(scheme.pattern, name, set, collector, uuid);
|
|
||||||
|
|
||||||
if (rel.isEmpty()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
rel += ".png";
|
|
||||||
rel = QDir::cleanPath(rel);
|
|
||||||
|
|
||||||
if (QFile::exists(baseDir.filePath(rel))) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
void CardPictureLoader::clearPixmapCache()
|
void CardPictureLoader::clearPixmapCache()
|
||||||
{
|
{
|
||||||
QPixmapCache::clear();
|
QPixmapCache::clear();
|
||||||
|
|
@ -489,11 +325,31 @@ void CardPictureLoader::picsPathChanged()
|
||||||
QPixmapCache::clear();
|
QPixmapCache::clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
void CardPictureLoader::cardLangChanged()
|
bool CardPictureLoader::hasCustomArt()
|
||||||
{
|
{
|
||||||
// Localized images are fetched via a different URL, but the in-memory
|
auto picsPath = SettingsCache::instance().paths().getPicsPath();
|
||||||
// pixmap cache is keyed by card name/uuid, so drop everything cached
|
QDirIterator it(picsPath, QDir::Dirs | QDir::NoDotAndDotDot);
|
||||||
// (including failure timestamps) to force a reload in the new language.
|
|
||||||
QPixmapCache::clear();
|
// Check if there is at least one non-directory file in the pics path, other
|
||||||
failedAt.clear();
|
// than in the "downloadedPics" subdirectory.
|
||||||
|
while (it.hasNext()) {
|
||||||
|
#if (QT_VERSION >= QT_VERSION_CHECK(6, 3, 0))
|
||||||
|
QFileInfo dir(it.nextFileInfo());
|
||||||
|
#else
|
||||||
|
// nextFileInfo() is only available in Qt 6.3+, for previous versions, we build
|
||||||
|
// the QFileInfo from a QString which requires more system calls.
|
||||||
|
QFileInfo dir(it.next());
|
||||||
|
#endif
|
||||||
|
|
||||||
|
if (it.fileName() == "downloadedPics") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
QDirIterator subIt(it.filePath(), QDir::Files, QDirIterator::Subdirectories | QDirIterator::FollowSymlinks);
|
||||||
|
if (subIt.hasNext()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -97,17 +97,10 @@ public:
|
||||||
static void cacheCardPixmaps(const QList<ExactCard> &cards);
|
static void cacheCardPixmaps(const QList<ExactCard> &cards);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Check if a local override image already exists for the card.
|
* @brief Check if the user has custom card art in the picsPath directory.
|
||||||
* @param card The card to check.
|
* @return True if any custom art exists.
|
||||||
* @return True if the card has at least one locally stored override image.
|
|
||||||
*/
|
*/
|
||||||
static bool hasLocalOverrides(const ExactCard &card);
|
static bool hasCustomArt();
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Removes all locally stored override images for the card.
|
|
||||||
* @param card The card to remove the override images of.
|
|
||||||
*/
|
|
||||||
static void deleteAllLocalOverrides(const ExactCard &card);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Clears the in-memory QPixmap cache for all cards.
|
* @brief Clears the in-memory QPixmap cache for all cards.
|
||||||
|
|
@ -127,9 +120,7 @@ public slots:
|
||||||
* @param image Loaded QImage.
|
* @param image Loaded QImage.
|
||||||
*/
|
*/
|
||||||
void imageLoaded(const ExactCard &card, const QImage &image);
|
void imageLoaded(const ExactCard &card, const QImage &image);
|
||||||
void saveCardImageToLocalStorage(const ExactCard &card, const QPixmap &pixmap, bool allowOverwrite = false);
|
void saveCardImageToLocalStorage(const ExactCard &card, const QPixmap &pixmap);
|
||||||
void installPrintingOverride(const ExactCard &originalCard, const ExactCard &overrideCard);
|
|
||||||
void installPrintingOverrideOnLoad(const ExactCard &originalCard, const ExactCard &overrideCard);
|
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
/**
|
/**
|
||||||
|
|
@ -143,12 +134,6 @@ private slots:
|
||||||
* Clears the QPixmap cache to reload images.
|
* Clears the QPixmap cache to reload images.
|
||||||
*/
|
*/
|
||||||
void picsPathChanged();
|
void picsPathChanged();
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Triggered when the card language setting changes.
|
|
||||||
* Clears the in-memory picture caches so images reload in the new language.
|
|
||||||
*/
|
|
||||||
void cardLangChanged();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
|
||||||
|
|
@ -94,10 +94,6 @@ QImage CardPictureLoaderLocal::tryLoadCardImageFromDisk(const QString &setName,
|
||||||
candidatePaths << picsPath + "/downloadedPics/" + setName + "/" + nameVariant;
|
candidatePaths << picsPath + "/downloadedPics/" + setName + "/" + nameVariant;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Non-set-folder export schemes (e.g., Name_Set_Collector) write straight into
|
|
||||||
// downloadedPics/; check there as a fallback so local overrides round-trip.
|
|
||||||
candidatePaths << picsPath + "/downloadedPics/" + nameVariant;
|
|
||||||
|
|
||||||
for (const QString &path : candidatePaths) {
|
for (const QString &path : candidatePaths) {
|
||||||
QFileInfo fileInfo(path);
|
QFileInfo fileInfo(path);
|
||||||
QDir dir = fileInfo.dir();
|
QDir dir = fileInfo.dir();
|
||||||
|
|
@ -109,8 +105,7 @@ QImage CardPictureLoaderLocal::tryLoadCardImageFromDisk(const QString &setName,
|
||||||
|
|
||||||
QStringList files = dir.entryList(QDir::Files);
|
QStringList files = dir.entryList(QDir::Files);
|
||||||
for (const QString &file : files) {
|
for (const QString &file : files) {
|
||||||
QFileInfo fi(file);
|
if (!file.startsWith(baseName)) {
|
||||||
if (fi.completeBaseName() != baseName) {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -84,8 +84,8 @@ void CardPictureLoaderWorker::queueRequest(const QUrl &url, CardPictureLoaderWor
|
||||||
SettingsCache::instance().cacheStorage().getCardPictureLoaderCacheMethod()) ==
|
SettingsCache::instance().cacheStorage().getCardPictureLoaderCacheMethod()) ==
|
||||||
CardPictureLoaderCacheMethod::CacheMethod::NETWORK_CACHE &&
|
CardPictureLoaderCacheMethod::CacheMethod::NETWORK_CACHE &&
|
||||||
cache->metaData(url).isValid()) {
|
cache->metaData(url).isValid()) {
|
||||||
// A request that will be served from the disk cache never touches the network and therefore
|
// If we hit a cached url, we get to make the request for free, since it won't contribute towards the
|
||||||
// doesn't use up any of the rate limit, so it gets to skip the queue.
|
// rate-limit
|
||||||
makeRequest(url, worker);
|
makeRequest(url, worker);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -107,13 +107,10 @@ QNetworkReply *CardPictureLoaderWorker::makeRequest(const QUrl &url, CardPicture
|
||||||
req.setHeader(QNetworkRequest::UserAgentHeader, QString("Cockatrice %1").arg(VERSION_STRING));
|
req.setHeader(QNetworkRequest::UserAgentHeader, QString("Cockatrice %1").arg(VERSION_STRING));
|
||||||
req.setRawHeader("Accept", "image/avif,image/webp,image/apng,image/,/*;q=0.8");
|
req.setRawHeader("Accept", "image/avif,image/webp,image/apng,image/,/*;q=0.8");
|
||||||
|
|
||||||
// Cached entries are served straight from the disk cache even when picture downloads are
|
bool useNetworkCache =
|
||||||
// enabled: re-fetching an already-cached image would burn the rate limit for nothing. Only a
|
!picDownload && static_cast<CardPictureLoaderCacheMethod::CacheMethod>(
|
||||||
// genuine cache miss goes to the network, and only when downloads are enabled.
|
SettingsCache::instance().cacheStorage().getCardPictureLoaderCacheMethod()) ==
|
||||||
bool useNetworkCache = static_cast<CardPictureLoaderCacheMethod::CacheMethod>(
|
CardPictureLoaderCacheMethod::CacheMethod::NETWORK_CACHE;
|
||||||
SettingsCache::instance().cacheStorage().getCardPictureLoaderCacheMethod()) ==
|
|
||||||
CardPictureLoaderCacheMethod::CacheMethod::NETWORK_CACHE &&
|
|
||||||
(cache->metaData(url).isValid() || !picDownload);
|
|
||||||
|
|
||||||
req.setAttribute(QNetworkRequest::CacheLoadControlAttribute,
|
req.setAttribute(QNetworkRequest::CacheLoadControlAttribute,
|
||||||
useNetworkCache ? QNetworkRequest::AlwaysCache : QNetworkRequest::AlwaysNetwork);
|
useNetworkCache ? QNetworkRequest::AlwaysCache : QNetworkRequest::AlwaysNetwork);
|
||||||
|
|
|
||||||
|
|
@ -282,15 +282,8 @@ QString CardPictureToLoad::transformUrl(const QString &urlTemplate) const
|
||||||
}
|
}
|
||||||
|
|
||||||
// language setting
|
// language setting
|
||||||
const QString cardLang = SettingsCache::instance().cardsDisplay().getCardLang();
|
transformMap["!sflang!"] = QString(QCoreApplication::translate(
|
||||||
transformMap["!sflang!"] = cardLang;
|
"PictureLoader", "en", "code for scryfall's language property, not available for all languages"));
|
||||||
|
|
||||||
// The localized printing's own id is unknown, so Scryfall must resolve it by
|
|
||||||
// its translated name (see populateSetUrls); expose that name for the
|
|
||||||
// `/cards/named` template.
|
|
||||||
if (cardLang != "en") {
|
|
||||||
transformMap["!localizedName!"] = card.getInfo().getLocalizedName(cardLang);
|
|
||||||
}
|
|
||||||
|
|
||||||
QString transformedUrl = urlTemplate;
|
QString transformedUrl = urlTemplate;
|
||||||
for (const QString &prop : transformMap.keys()) {
|
for (const QString &prop : transformMap.keys()) {
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ enum Format
|
||||||
PlainText,
|
PlainText,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is Cockatrice's native deck file format, and supports deck metadata such as banner cards and tags.
|
* This is cockatrice's native deck file format, and supports deck metadata such as banner cards and tags.
|
||||||
* Stored as .cod files.
|
* Stored as .cod files.
|
||||||
*/
|
*/
|
||||||
Cockatrice
|
Cockatrice
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,7 @@ DeckLoader::loadFromFile(const QString &fileName, DeckFileFormat::Format fmt, bo
|
||||||
result = deckList.loadFromFile_Native(&file);
|
result = deckList.loadFromFile_Native(&file);
|
||||||
if (!result) {
|
if (!result) {
|
||||||
qCInfo(DeckLoaderLog) << "Failed to load " << fileName
|
qCInfo(DeckLoaderLog) << "Failed to load " << fileName
|
||||||
<< "as Cockatrice format; retrying as plain format";
|
<< "as cockatrice format; retrying as plain format";
|
||||||
file.seek(0);
|
file.seek(0);
|
||||||
result = deckList.loadFromFile_Plain(&file, CardNameNormalizer());
|
result = deckList.loadFromFile_Plain(&file, CardNameNormalizer());
|
||||||
fmt = DeckFileFormat::PlainText;
|
fmt = DeckFileFormat::PlainText;
|
||||||
|
|
@ -375,32 +375,15 @@ void DeckLoader::saveToStream_DeckHeader(QTextStream &out, const DeckList &deckL
|
||||||
void DeckLoader::saveToStream_DeckZone(QTextStream &out,
|
void DeckLoader::saveToStream_DeckZone(QTextStream &out,
|
||||||
const InnerDecklistNode *zoneNode,
|
const InnerDecklistNode *zoneNode,
|
||||||
bool addComments,
|
bool addComments,
|
||||||
bool addSetNameAndNumber,
|
bool addSetNameAndNumber)
|
||||||
const QString &boardZoneName)
|
|
||||||
{
|
{
|
||||||
// Nested sub-zones keep their owning board's identity: the top-level call
|
|
||||||
// passes no board, so the zone's own name is used; recursive calls carry the
|
|
||||||
// owning board down so the sideboard marker survives sub-zone nesting.
|
|
||||||
const QString owningBoardZoneName = boardZoneName.isEmpty() ? zoneNode->getName() : boardZoneName;
|
|
||||||
|
|
||||||
// group cards by card type and count the subtotals
|
// group cards by card type and count the subtotals
|
||||||
QMultiMap<QString, DecklistCardNode *> cardsByType;
|
QMultiMap<QString, DecklistCardNode *> cardsByType;
|
||||||
QMap<QString, int> cardTotalByType;
|
QMap<QString, int> cardTotalByType;
|
||||||
int cardTotal = 0;
|
int cardTotal = 0;
|
||||||
QList<const InnerDecklistNode *> subZones;
|
|
||||||
|
|
||||||
for (int j = 0; j < zoneNode->size(); j++) {
|
for (int j = 0; j < zoneNode->size(); j++) {
|
||||||
auto *card = dynamic_cast<DecklistCardNode *>(zoneNode->at(j));
|
auto *card = dynamic_cast<DecklistCardNode *>(zoneNode->at(j));
|
||||||
if (!card) {
|
|
||||||
// Cards collected in nested sub-zones are exported by recursion so
|
|
||||||
// they don't end up invisible in the plain text output. They are
|
|
||||||
// deferred until after this zone's own header and cards so they read
|
|
||||||
// as part of this zone's block.
|
|
||||||
if (auto *subZone = dynamic_cast<const InnerDecklistNode *>(zoneNode->at(j))) {
|
|
||||||
subZones.append(subZone);
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
CardInfoPtr info = CardDatabaseManager::query()->getCardInfo(card->getName());
|
CardInfoPtr info = CardDatabaseManager::query()->getCardInfo(card->getName());
|
||||||
QString cardType = info ? info->getMainCardType() : "unknown";
|
QString cardType = info ? info->getMainCardType() : "unknown";
|
||||||
|
|
@ -428,30 +411,25 @@ void DeckLoader::saveToStream_DeckZone(QTextStream &out,
|
||||||
|
|
||||||
QList<DecklistCardNode *> cards = cardsByType.values(cardType);
|
QList<DecklistCardNode *> cards = cardsByType.values(cardType);
|
||||||
|
|
||||||
saveToStream_DeckZoneCards(out, cards, addComments, addSetNameAndNumber, owningBoardZoneName);
|
saveToStream_DeckZoneCards(out, zoneNode, cards, addComments, addSetNameAndNumber);
|
||||||
|
|
||||||
if (addComments) {
|
if (addComments) {
|
||||||
out << "\n";
|
out << "\n";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Nested sub-zones come last, after the parent's own header and cards.
|
|
||||||
for (const auto *subZone : subZones) {
|
|
||||||
saveToStream_DeckZone(out, subZone, addComments, addSetNameAndNumber, owningBoardZoneName);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void DeckLoader::saveToStream_DeckZoneCards(QTextStream &out,
|
void DeckLoader::saveToStream_DeckZoneCards(QTextStream &out,
|
||||||
|
const InnerDecklistNode *zoneNode,
|
||||||
QList<DecklistCardNode *> cards,
|
QList<DecklistCardNode *> cards,
|
||||||
bool addComments,
|
bool addComments,
|
||||||
bool addSetNameAndNumber,
|
bool addSetNameAndNumber)
|
||||||
const QString &boardZoneName)
|
|
||||||
{
|
{
|
||||||
// QMultiMap sorts values in reverse order
|
// QMultiMap sorts values in reverse order
|
||||||
for (int i = cards.size() - 1; i >= 0; --i) {
|
for (int i = cards.size() - 1; i >= 0; --i) {
|
||||||
DecklistCardNode *card = cards[i];
|
DecklistCardNode *card = cards[i];
|
||||||
|
|
||||||
if (boardZoneName == DECK_ZONE_SIDE && addComments) {
|
if (zoneNode->getName() == DECK_ZONE_SIDE && addComments) {
|
||||||
out << "SB: ";
|
out << "SB: ";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -532,26 +510,9 @@ bool DeckLoader::convertToCockatriceFormat(LoadedDeck &deck)
|
||||||
|
|
||||||
void DeckLoader::printDeckListNode(QTextCursor *cursor, const InnerDecklistNode *node)
|
void DeckLoader::printDeckListNode(QTextCursor *cursor, const InnerDecklistNode *node)
|
||||||
{
|
{
|
||||||
if (!node || node->isEmpty()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const int totalColumns = 2;
|
const int totalColumns = 2;
|
||||||
|
|
||||||
// Dispatch children by type instead of trusting a whole-node height: a deck
|
if (node->height() == 1) {
|
||||||
// node may hold direct cards and nested zones side by side (custom zones),
|
|
||||||
// and an empty node would previously crash on at(0).
|
|
||||||
QVector<const AbstractDecklistCardNode *> cards;
|
|
||||||
QVector<const InnerDecklistNode *> subZones;
|
|
||||||
for (int i = 0; i < node->size(); i++) {
|
|
||||||
if (auto *card = dynamic_cast<const AbstractDecklistCardNode *>(node->at(i))) {
|
|
||||||
cards.append(card);
|
|
||||||
} else if (auto *zone = dynamic_cast<const InnerDecklistNode *>(node->at(i))) {
|
|
||||||
subZones.append(zone);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!cards.isEmpty()) {
|
|
||||||
QTextBlockFormat blockFormat;
|
QTextBlockFormat blockFormat;
|
||||||
QTextCharFormat charFormat;
|
QTextCharFormat charFormat;
|
||||||
charFormat.setFontPointSize(11);
|
charFormat.setFontPointSize(11);
|
||||||
|
|
@ -562,9 +523,9 @@ void DeckLoader::printDeckListNode(QTextCursor *cursor, const InnerDecklistNode
|
||||||
tableFormat.setCellPadding(0);
|
tableFormat.setCellPadding(0);
|
||||||
tableFormat.setCellSpacing(0);
|
tableFormat.setCellSpacing(0);
|
||||||
tableFormat.setBorder(0);
|
tableFormat.setBorder(0);
|
||||||
QTextTable *table = cursor->insertTable(cards.size() + 1, totalColumns, tableFormat);
|
QTextTable *table = cursor->insertTable(node->size() + 1, totalColumns, tableFormat);
|
||||||
for (int i = 0; i < cards.size(); i++) {
|
for (int i = 0; i < node->size(); i++) {
|
||||||
const AbstractDecklistCardNode *card = cards[i];
|
auto *card = dynamic_cast<AbstractDecklistCardNode *>(node->at(i));
|
||||||
|
|
||||||
QTextCharFormat cellCharFormat;
|
QTextCharFormat cellCharFormat;
|
||||||
cellCharFormat.setFontPointSize(9);
|
cellCharFormat.setFontPointSize(9);
|
||||||
|
|
@ -579,13 +540,7 @@ void DeckLoader::printDeckListNode(QTextCursor *cursor, const InnerDecklistNode
|
||||||
cellCursor = cell.firstCursorPosition();
|
cellCursor = cell.firstCursorPosition();
|
||||||
cellCursor.insertText(card->getName());
|
cellCursor.insertText(card->getName());
|
||||||
}
|
}
|
||||||
}
|
} else if (node->height() == 2) {
|
||||||
|
|
||||||
for (const InnerDecklistNode *subZone : subZones) {
|
|
||||||
if (subZone->isEmpty()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
QTextBlockFormat blockFormat;
|
QTextBlockFormat blockFormat;
|
||||||
QTextCharFormat charFormat;
|
QTextCharFormat charFormat;
|
||||||
charFormat.setFontPointSize(14);
|
charFormat.setFontPointSize(14);
|
||||||
|
|
@ -604,8 +559,10 @@ void DeckLoader::printDeckListNode(QTextCursor *cursor, const InnerDecklistNode
|
||||||
tableFormat.setColumnWidthConstraints(constraints);
|
tableFormat.setColumnWidthConstraints(constraints);
|
||||||
|
|
||||||
QTextTable *table = cursor->insertTable(1, totalColumns, tableFormat);
|
QTextTable *table = cursor->insertTable(1, totalColumns, tableFormat);
|
||||||
QTextCursor cellCursor = table->cellAt(0, 0).firstCursorPosition();
|
for (int i = 0; i < node->size(); i++) {
|
||||||
printDeckListNode(&cellCursor, subZone);
|
QTextCursor cellCursor = table->cellAt(0, (i * totalColumns) / node->size()).lastCursorPosition();
|
||||||
|
printDeckListNode(&cellCursor, dynamic_cast<InnerDecklistNode *>(node->at(i)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
cursor->movePosition(QTextCursor::End);
|
cursor->movePosition(QTextCursor::End);
|
||||||
|
|
|
||||||
|
|
@ -131,7 +131,7 @@ public:
|
||||||
static void printDeckList(QPrinter *printer, const DeckList &deckList);
|
static void printDeckList(QPrinter *printer, const DeckList &deckList);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts the given deck's file to the Cockatrice file format.
|
* Converts the given deck's file to the cockatrice file format.
|
||||||
* Uses the lastLoadInfo in the LoadedDeck to determine the current name of the file and where to save to.
|
* Uses the lastLoadInfo in the LoadedDeck to determine the current name of the file and where to save to.
|
||||||
* @param deck The deck to convert. Should have valid lastLoadInfo. Will update the lastLoadInfo.
|
* @param deck The deck to convert. Should have valid lastLoadInfo. Will update the lastLoadInfo.
|
||||||
* @return Whether the conversion succeeded.
|
* @return Whether the conversion succeeded.
|
||||||
|
|
@ -159,13 +159,12 @@ private:
|
||||||
static void saveToStream_DeckZone(QTextStream &out,
|
static void saveToStream_DeckZone(QTextStream &out,
|
||||||
const InnerDecklistNode *zoneNode,
|
const InnerDecklistNode *zoneNode,
|
||||||
bool addComments = true,
|
bool addComments = true,
|
||||||
bool addSetNameAndNumber = true,
|
bool addSetNameAndNumber = true);
|
||||||
const QString &boardZoneName = QString());
|
|
||||||
static void saveToStream_DeckZoneCards(QTextStream &out,
|
static void saveToStream_DeckZoneCards(QTextStream &out,
|
||||||
|
const InnerDecklistNode *zoneNode,
|
||||||
QList<DecklistCardNode *> cards,
|
QList<DecklistCardNode *> cards,
|
||||||
bool addComments = true,
|
bool addComments = true,
|
||||||
bool addSetNameAndNumber = true,
|
bool addSetNameAndNumber = true);
|
||||||
const QString &boardZoneName = QString());
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
#ifndef COCKATRICE_CONTEXT_OPEN_DECK_H
|
|
||||||
#define COCKATRICE_CONTEXT_OPEN_DECK_H
|
|
||||||
|
|
||||||
#include "context_connect_to_server.h"
|
|
||||||
|
|
||||||
#include <QString>
|
|
||||||
|
|
||||||
struct ContextOpenDeck
|
|
||||||
{
|
|
||||||
ContextConnectToServer serverContext;
|
|
||||||
QString shareToken;
|
|
||||||
};
|
|
||||||
|
|
||||||
#endif // COCKATRICE_CONTEXT_OPEN_DECK_H
|
|
||||||
|
|
@ -2,11 +2,10 @@
|
||||||
|
|
||||||
Intent::Intent(QObject *parent) : QObject(parent)
|
Intent::Intent(QObject *parent) : QObject(parent)
|
||||||
{
|
{
|
||||||
// An intent is done as soon as it reports success, failure, or cancellation.
|
// An intent is done as soon as it reports success or failure. Deleting it
|
||||||
// Deleting it also tears down its dependency chain and disconnects any signal wiring.
|
// also tears down its dependency chain and disconnects any signal wiring.
|
||||||
connect(this, &Intent::finished, this, &QObject::deleteLater);
|
connect(this, &Intent::finished, this, &QObject::deleteLater);
|
||||||
connect(this, &Intent::failed, this, &QObject::deleteLater);
|
connect(this, &Intent::failed, this, &QObject::deleteLater);
|
||||||
connect(this, &Intent::cancelled, this, &QObject::deleteLater);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Intent::~Intent() = default;
|
Intent::~Intent() = default;
|
||||||
|
|
@ -28,7 +27,6 @@ void Intent::runDependency(Intent *dependency)
|
||||||
this->execute();
|
this->execute();
|
||||||
});
|
});
|
||||||
connect(dependency, &Intent::failed, this, &Intent::failed);
|
connect(dependency, &Intent::failed, this, &Intent::failed);
|
||||||
connect(dependency, &Intent::cancelled, this, &Intent::cancelled);
|
|
||||||
|
|
||||||
dependency->execute();
|
dependency->execute();
|
||||||
}
|
}
|
||||||
|
|
@ -48,11 +46,3 @@ void Intent::emitFailed(const QString &reason)
|
||||||
emit failed(reason);
|
emit failed(reason);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Intent::emitCancelled()
|
|
||||||
{
|
|
||||||
if (!completed) {
|
|
||||||
completed = true;
|
|
||||||
emit cancelled();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,6 @@ public:
|
||||||
signals:
|
signals:
|
||||||
void finished();
|
void finished();
|
||||||
void failed(QString reason);
|
void failed(QString reason);
|
||||||
void cancelled();
|
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
// --- Subclasses must implement these ---
|
// --- Subclasses must implement these ---
|
||||||
|
|
@ -30,7 +29,6 @@ protected:
|
||||||
// Emit the outcome exactly once; ignore late signals after the intent is done.
|
// Emit the outcome exactly once; ignore late signals after the intent is done.
|
||||||
void emitFinished();
|
void emitFinished();
|
||||||
void emitFailed(const QString &reason);
|
void emitFailed(const QString &reason);
|
||||||
void emitCancelled();
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
bool completed = false;
|
bool completed = false;
|
||||||
|
|
|
||||||
|
|
@ -19,15 +19,13 @@ bool IntentJoinServerGame::checkPrecondition() const
|
||||||
if (remoteClient->getStatus() != ClientStatus::StatusLoggedIn) {
|
if (remoteClient->getStatus() != ClientStatus::StatusLoggedIn) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// serverName()/serverPort() reflect the server the client was configured
|
// peerPort() reflects the actual TCP peer, which may differ from the
|
||||||
// to connect to, which may differ from the actual TCP peer (e.g. when
|
// configured server port (e.g. when connecting through a proxy), so only
|
||||||
// connecting through a proxy), so compare those configured values. A link
|
// the hostname is compared here.
|
||||||
// naming the same host on another port is a different server and must not
|
if (remoteClient->peerName() != context->roomContext.serverContext.hostname) {
|
||||||
// reuse the session there.
|
|
||||||
if (remoteClient->serverName().compare(context->roomContext.serverContext.hostname, Qt::CaseInsensitive) != 0) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (QString::number(remoteClient->serverPort()) != context->roomContext.serverContext.port) {
|
if (QString::number(remoteClient->peerPort()) != context->roomContext.serverContext.port) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,9 @@
|
||||||
#include "intent_login.h"
|
#include "intent_login.h"
|
||||||
|
|
||||||
#include "../../client/settings/cache_settings.h"
|
#include "../../client/settings/cache_settings.h"
|
||||||
#include "../widgets/dialogs/dlg_login_prompt.h"
|
|
||||||
#include "libcockatrice/settings/servers_settings.h"
|
#include "libcockatrice/settings/servers_settings.h"
|
||||||
|
|
||||||
#include <QDialog>
|
IntentGetLoginCredentials::IntentGetLoginCredentials(ContextConnectToServer *_context) : Intent(), context(_context)
|
||||||
|
|
||||||
IntentGetLoginCredentials::IntentGetLoginCredentials(ContextConnectToServer *_context,
|
|
||||||
bool _promptForMissingCredentials)
|
|
||||||
: Intent(), context(_context), promptForMissingCredentials(_promptForMissingCredentials)
|
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -34,46 +29,5 @@ void IntentGetLoginCredentials::onPreconditionSatisfied()
|
||||||
|
|
||||||
void IntentGetLoginCredentials::onPreconditionNotSatisfied()
|
void IntentGetLoginCredentials::onPreconditionNotSatisfied()
|
||||||
{
|
{
|
||||||
// MainWindow::applyStartupDestination runs this intent on every launch for
|
emitFailed(tr("No saved credentials for this server"));
|
||||||
// users whose startup tab is Server / Server Room; keep that path quiet, as
|
|
||||||
// it was before the link-driven sign-in dialog existed.
|
|
||||||
if (!promptForMissingCredentials) {
|
|
||||||
emitFailed(tr("No saved credentials for this server"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// No credentials saved for the target server: ask the user for them. They
|
|
||||||
// opt into saving them so later links to the same server connect directly.
|
|
||||||
const QString serverText = context->hostname + ":" + context->port;
|
|
||||||
DlgLoginPrompt dialog(serverText);
|
|
||||||
// ApplicationModal: the dialog has no parent (the intent is not a widget),
|
|
||||||
// so WindowModal would not actually block any other window.
|
|
||||||
dialog.setWindowModality(Qt::ApplicationModal);
|
|
||||||
|
|
||||||
if (dialog.exec() != QDialog::Accepted) {
|
|
||||||
emitCancelled();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
context->username = dialog.username();
|
|
||||||
context->password = dialog.password();
|
|
||||||
|
|
||||||
if (dialog.savePassword() && !context->username.isEmpty()) {
|
|
||||||
ServersSettings &servers = SettingsCache::instance().servers();
|
|
||||||
// The host may already be saved under a friendly name (e.g. a public-server
|
|
||||||
// list entry) with no credentials; reuse that name instead of overwriting
|
|
||||||
// it with the raw hostname when addNewServer updates the entry in place.
|
|
||||||
QString saveName = context->hostname;
|
|
||||||
const int existingIndex = servers.findServerIndex(context->hostname, context->port);
|
|
||||||
if (existingIndex >= 0) {
|
|
||||||
saveName =
|
|
||||||
servers.getValue(QString("saveName%1").arg(existingIndex), "server", "server_details").toString();
|
|
||||||
if (saveName.isEmpty()) {
|
|
||||||
saveName = context->hostname;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
servers.addNewServer(saveName, context->hostname, context->port, context->username, context->password, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
emitFinished();
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,10 +9,7 @@ class IntentGetLoginCredentials : public Intent
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
public:
|
public:
|
||||||
// When promptForMissingCredentials is false (the default) a server without
|
IntentGetLoginCredentials(ContextConnectToServer *_context);
|
||||||
// saved credentials fails silently; only intent chains from cockatrice://
|
|
||||||
// links opt into the interactive sign-in dialog.
|
|
||||||
explicit IntentGetLoginCredentials(ContextConnectToServer *_context, bool _promptForMissingCredentials = false);
|
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
bool checkPrecondition() const override;
|
bool checkPrecondition() const override;
|
||||||
|
|
@ -21,7 +18,6 @@ protected:
|
||||||
|
|
||||||
private:
|
private:
|
||||||
ContextConnectToServer *context;
|
ContextConnectToServer *context;
|
||||||
bool promptForMissingCredentials;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // COCKATRICE_INTENT_LOGIN_H
|
#endif // COCKATRICE_INTENT_LOGIN_H
|
||||||
|
|
|
||||||
|
|
@ -1,208 +0,0 @@
|
||||||
#include "intent_open_shared_deck.h"
|
|
||||||
|
|
||||||
#include "../deck_loader/deck_loader.h"
|
|
||||||
#include "../widgets/dialogs/dlg_shared_decks_preview.h"
|
|
||||||
#include "../widgets/tabs/tab_supervisor.h"
|
|
||||||
#include "intent_connect_to_server.h"
|
|
||||||
|
|
||||||
#include <QMessageBox>
|
|
||||||
#include <QTimer>
|
|
||||||
#include <libcockatrice/card/database/card_database_querier.h>
|
|
||||||
#include <libcockatrice/protocol/pb/command_deck_share_download.pb.h>
|
|
||||||
#include <libcockatrice/protocol/pb/command_deck_share_list.pb.h>
|
|
||||||
#include <libcockatrice/protocol/pb/response.pb.h>
|
|
||||||
#include <libcockatrice/protocol/pb/response_deck_share_download.pb.h>
|
|
||||||
#include <libcockatrice/protocol/pb/response_deck_share_list.pb.h>
|
|
||||||
#include <libcockatrice/protocol/pb/serverinfo_deck_share_item.pb.h>
|
|
||||||
#include <libcockatrice/protocol/pending_command.h>
|
|
||||||
|
|
||||||
IntentOpenSharedDeck::IntentOpenSharedDeck(TabSupervisor *_tabSupervisor,
|
|
||||||
RemoteClient *_remoteClient,
|
|
||||||
const CardDatabaseQuerier *_querier,
|
|
||||||
std::unique_ptr<ContextOpenDeck> _context)
|
|
||||||
: Intent(), tabSupervisor(_tabSupervisor), remoteClient(_remoteClient), querier(_querier),
|
|
||||||
context(_context.release())
|
|
||||||
{
|
|
||||||
downloadTimer = new QTimer(this);
|
|
||||||
downloadTimer->setSingleShot(true);
|
|
||||||
downloadTimer->setInterval(15000);
|
|
||||||
connect(downloadTimer, &QTimer::timeout, this, &IntentOpenSharedDeck::onDownloadTimeout);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool IntentOpenSharedDeck::checkPrecondition() const
|
|
||||||
{
|
|
||||||
if (remoteClient->getStatus() != ClientStatus::StatusLoggedIn) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
// serverName()/serverPort() reflect the server the client was configured
|
|
||||||
// to connect to, which may differ from the actual TCP peer (e.g. when
|
|
||||||
// connecting through a proxy), so compare those configured values. The
|
|
||||||
// share token must be resolved against the host the link named — a link to
|
|
||||||
// the same host on another port is a different server.
|
|
||||||
if (remoteClient->serverName().compare(context->serverContext.hostname, Qt::CaseInsensitive) != 0) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return QString::number(remoteClient->serverPort()) == context->serverContext.port;
|
|
||||||
}
|
|
||||||
|
|
||||||
void IntentOpenSharedDeck::onPreconditionSatisfied()
|
|
||||||
{
|
|
||||||
// Resolve the share token to its items first; a share can contain more than
|
|
||||||
// one deck, and each item is downloaded by id. Time the round trip like the
|
|
||||||
// downloads, so a silent server cannot hang the chain forever.
|
|
||||||
listPhase = true;
|
|
||||||
downloadTimer->start();
|
|
||||||
|
|
||||||
Command_DeckShareList cmd;
|
|
||||||
cmd.set_token(context->shareToken.toStdString());
|
|
||||||
|
|
||||||
PendingCommand *pend = AbstractClient::prepareSessionCommand(cmd);
|
|
||||||
connect(pend, &PendingCommand::finished, this, &IntentOpenSharedDeck::listShareFinished);
|
|
||||||
remoteClient->sendCommand(pend);
|
|
||||||
}
|
|
||||||
|
|
||||||
void IntentOpenSharedDeck::onPreconditionNotSatisfied()
|
|
||||||
{
|
|
||||||
runDependency(new IntentConnectToServer(remoteClient, &context->serverContext));
|
|
||||||
}
|
|
||||||
|
|
||||||
void IntentOpenSharedDeck::listShareFinished(const Response &response, const CommandContainer & /* commandContainer */)
|
|
||||||
{
|
|
||||||
downloadTimer->stop();
|
|
||||||
listPhase = false;
|
|
||||||
|
|
||||||
if (response.response_code() != Response::RespOk) {
|
|
||||||
emitFailed(tr("The shared deck could not be found or has expired"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const Response_DeckShareList &resp = response.GetExtension(Response_DeckShareList::ext);
|
|
||||||
if (resp.items_size() == 0) {
|
|
||||||
emitFailed(tr("The shared deck is empty"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
QList<ServerInfo_DeckShareItem> items;
|
|
||||||
items.reserve(resp.items_size());
|
|
||||||
for (const ServerInfo_DeckShareItem &item : resp.items()) {
|
|
||||||
items.append(item);
|
|
||||||
itemNames.insert(item.id(), QString::fromStdString(item.name()));
|
|
||||||
}
|
|
||||||
|
|
||||||
const QString serverText = context->serverContext.hostname + ":" + context->serverContext.port;
|
|
||||||
|
|
||||||
// Ask the user which decks to open before downloading anything.
|
|
||||||
previewDialog = new DlgSharedDecksPreview(tabSupervisor, querier, QString::fromStdString(resp.name()),
|
|
||||||
resp.expires_at(), serverText, items);
|
|
||||||
connect(previewDialog, &DlgSharedDecksPreview::openRequested, this, &IntentOpenSharedDeck::startDownloads);
|
|
||||||
connect(previewDialog, &DlgSharedDecksPreview::cancelled, this, &IntentOpenSharedDeck::emitCancelled);
|
|
||||||
connect(previewDialog, &DlgSharedDecksPreview::cancelled, previewDialog, &QWidget::deleteLater);
|
|
||||||
previewDialog->show();
|
|
||||||
previewDialog->raise();
|
|
||||||
previewDialog->activateWindow();
|
|
||||||
}
|
|
||||||
|
|
||||||
void IntentOpenSharedDeck::startDownloads(const QList<int> &itemIds)
|
|
||||||
{
|
|
||||||
pendingItemIds = itemIds;
|
|
||||||
totalItems = itemIds.size();
|
|
||||||
completedItems = 0;
|
|
||||||
loadedDecks.clear();
|
|
||||||
downloadNextItem();
|
|
||||||
}
|
|
||||||
|
|
||||||
void IntentOpenSharedDeck::downloadNextItem()
|
|
||||||
{
|
|
||||||
if (pendingItemIds.isEmpty()) {
|
|
||||||
finishAll();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
currentItemId = pendingItemIds.takeFirst();
|
|
||||||
downloadTimer->start();
|
|
||||||
|
|
||||||
Command_DeckShareDownload cmd;
|
|
||||||
cmd.set_token(context->shareToken.toStdString());
|
|
||||||
cmd.set_item_id(currentItemId);
|
|
||||||
|
|
||||||
PendingCommand *pend = AbstractClient::prepareSessionCommand(cmd);
|
|
||||||
connect(pend, &PendingCommand::finished, this, &IntentOpenSharedDeck::downloadShareFinished);
|
|
||||||
remoteClient->sendCommand(pend);
|
|
||||||
}
|
|
||||||
|
|
||||||
void IntentOpenSharedDeck::downloadShareFinished(const Response &response,
|
|
||||||
const CommandContainer & /* commandContainer */)
|
|
||||||
{
|
|
||||||
downloadTimer->stop();
|
|
||||||
|
|
||||||
QString failureReason;
|
|
||||||
if (response.response_code() != Response::RespOk) {
|
|
||||||
failureReason = tr("Failed to download the shared deck");
|
|
||||||
} else {
|
|
||||||
const Response_DeckShareDownload &resp = response.GetExtension(Response_DeckShareDownload::ext);
|
|
||||||
const QString deckString = QString::fromStdString(resp.deck());
|
|
||||||
if (deckString.isEmpty()) {
|
|
||||||
failureReason = tr("The shared deck is empty");
|
|
||||||
} else {
|
|
||||||
std::optional<LoadedDeck> deckOpt =
|
|
||||||
DeckLoader::loadFromRemote(deckString, LoadedDeck::LoadInfo::NON_REMOTE_ID);
|
|
||||||
if (!deckOpt) {
|
|
||||||
failureReason = tr("The shared deck could not be loaded");
|
|
||||||
} else {
|
|
||||||
loadedDecks.append(deckOpt.value());
|
|
||||||
++completedItems;
|
|
||||||
previewDialog->setDownloadProgress(completedItems, totalItems,
|
|
||||||
itemNames.value(currentItemId, tr("Unknown deck")));
|
|
||||||
downloadNextItem();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onItemFailure(failureReason);
|
|
||||||
}
|
|
||||||
|
|
||||||
void IntentOpenSharedDeck::onItemFailure(const QString &reason)
|
|
||||||
{
|
|
||||||
downloadTimer->stop();
|
|
||||||
|
|
||||||
if (loadedDecks.isEmpty()) {
|
|
||||||
previewDialog->deleteLater();
|
|
||||||
emitFailed(reason);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const int downloadedCount = loadedDecks.size();
|
|
||||||
const QMessageBox::StandardButton answer = QMessageBox::question(
|
|
||||||
previewDialog, tr("Open shared decks"),
|
|
||||||
tr("Could not download the deck \"%1\".\n\n%n deck(s) were already downloaded. Open them?", "", downloadedCount)
|
|
||||||
.arg(itemNames.value(currentItemId, tr("Unknown deck"))),
|
|
||||||
QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes);
|
|
||||||
|
|
||||||
if (answer == QMessageBox::Yes) {
|
|
||||||
finishAll();
|
|
||||||
} else {
|
|
||||||
previewDialog->deleteLater();
|
|
||||||
emitCancelled();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void IntentOpenSharedDeck::onDownloadTimeout()
|
|
||||||
{
|
|
||||||
// The list phase has no preview dialog yet to report progress into; fail the
|
|
||||||
// whole intent instead of letting the shared deck hang in limbo.
|
|
||||||
if (listPhase) {
|
|
||||||
emitFailed(tr("Timed out while loading the shared deck"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
onItemFailure(tr("Timed out while downloading the shared deck"));
|
|
||||||
}
|
|
||||||
|
|
||||||
void IntentOpenSharedDeck::finishAll()
|
|
||||||
{
|
|
||||||
previewDialog->deleteLater();
|
|
||||||
for (const LoadedDeck &deck : loadedDecks) {
|
|
||||||
tabSupervisor->openDeckInNewTab(deck);
|
|
||||||
}
|
|
||||||
emitFinished();
|
|
||||||
}
|
|
||||||
|
|
@ -1,60 +0,0 @@
|
||||||
#ifndef COCKATRICE_INTENT_OPEN_SHARED_DECK_H
|
|
||||||
#define COCKATRICE_INTENT_OPEN_SHARED_DECK_H
|
|
||||||
|
|
||||||
#include "contexts/context_open_deck.h"
|
|
||||||
#include "intent.h"
|
|
||||||
#include "remote_client.h"
|
|
||||||
|
|
||||||
#include <QList>
|
|
||||||
#include <QMap>
|
|
||||||
#include <QScopedPointer>
|
|
||||||
#include <memory>
|
|
||||||
|
|
||||||
class TabSupervisor;
|
|
||||||
struct LoadedDeck;
|
|
||||||
class CardDatabaseQuerier;
|
|
||||||
class DlgSharedDecksPreview;
|
|
||||||
class QTimer;
|
|
||||||
|
|
||||||
class IntentOpenSharedDeck : public Intent
|
|
||||||
{
|
|
||||||
Q_OBJECT
|
|
||||||
|
|
||||||
public:
|
|
||||||
IntentOpenSharedDeck(TabSupervisor *_tabSupervisor,
|
|
||||||
RemoteClient *_remoteClient,
|
|
||||||
const CardDatabaseQuerier *_querier,
|
|
||||||
std::unique_ptr<ContextOpenDeck> _context);
|
|
||||||
|
|
||||||
protected:
|
|
||||||
bool checkPrecondition() const override;
|
|
||||||
void onPreconditionSatisfied() override;
|
|
||||||
void onPreconditionNotSatisfied() override;
|
|
||||||
|
|
||||||
private slots:
|
|
||||||
void listShareFinished(const Response &response, const CommandContainer &commandContainer);
|
|
||||||
void downloadShareFinished(const Response &response, const CommandContainer &commandContainer);
|
|
||||||
void onDownloadTimeout();
|
|
||||||
|
|
||||||
private:
|
|
||||||
void startDownloads(const QList<int> &itemIds);
|
|
||||||
void downloadNextItem();
|
|
||||||
void onItemFailure(const QString &reason);
|
|
||||||
void finishAll();
|
|
||||||
|
|
||||||
TabSupervisor *tabSupervisor;
|
|
||||||
RemoteClient *remoteClient;
|
|
||||||
const CardDatabaseQuerier *querier;
|
|
||||||
QScopedPointer<ContextOpenDeck> context;
|
|
||||||
DlgSharedDecksPreview *previewDialog = nullptr;
|
|
||||||
QTimer *downloadTimer;
|
|
||||||
QMap<int, QString> itemNames;
|
|
||||||
QList<int> pendingItemIds;
|
|
||||||
QList<LoadedDeck> loadedDecks;
|
|
||||||
bool listPhase = true;
|
|
||||||
int currentItemId = 0;
|
|
||||||
int totalItems = 0;
|
|
||||||
int completedItems = 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
#endif // COCKATRICE_INTENT_OPEN_SHARED_DECK_H
|
|
||||||
|
|
@ -1,28 +1,19 @@
|
||||||
#include "url_parser.h"
|
#include "url_parser.h"
|
||||||
|
|
||||||
#include "../../client/settings/cache_settings.h"
|
|
||||||
#include "../widgets/tabs/tab_room.h"
|
#include "../widgets/tabs/tab_room.h"
|
||||||
#include "../widgets/tabs/tab_supervisor.h"
|
#include "../widgets/tabs/tab_supervisor.h"
|
||||||
#include "../window_main.h"
|
#include "../window_main.h"
|
||||||
#include "contexts/context_join_game.h"
|
#include "contexts/context_join_game.h"
|
||||||
#include "contexts/context_open_deck.h"
|
|
||||||
#include "intent.h"
|
|
||||||
#include "intent_join_server_game.h"
|
#include "intent_join_server_game.h"
|
||||||
#include "intent_login.h"
|
#include "intent_login.h"
|
||||||
#include "intent_open_shared_deck.h"
|
|
||||||
|
|
||||||
#include <QDebug>
|
#include <QDebug>
|
||||||
#include <QLoggingCategory>
|
|
||||||
#include <QMessageBox>
|
#include <QMessageBox>
|
||||||
#include <QUrl>
|
#include <QUrl>
|
||||||
#include <QUrlQuery>
|
#include <QUrlQuery>
|
||||||
#include <libcockatrice/card/database/card_database_manager.h>
|
|
||||||
#include <libcockatrice/network/client/abstract/abstract_client.h>
|
#include <libcockatrice/network/client/abstract/abstract_client.h>
|
||||||
#include <libcockatrice/settings/servers_settings.h>
|
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
|
||||||
inline Q_LOGGING_CATEGORY(UrlParserLog, "url_parser");
|
|
||||||
|
|
||||||
IntentUrlParser::IntentUrlParser(QObject *parent, MainWindow *_mainWindow) : QObject(parent), mainWindow(_mainWindow)
|
IntentUrlParser::IntentUrlParser(QObject *parent, MainWindow *_mainWindow) : QObject(parent), mainWindow(_mainWindow)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
@ -38,33 +29,16 @@ void IntentUrlParser::handle(const QString &urlStr)
|
||||||
const QString action = url.host();
|
const QString action = url.host();
|
||||||
QUrlQuery query(url);
|
QUrlQuery query(url);
|
||||||
|
|
||||||
qCDebug(UrlParserLog) << "Parsing intent URL, action:" << action;
|
|
||||||
|
|
||||||
PendingIntentChain chain;
|
|
||||||
Intent *firstIntent = nullptr;
|
|
||||||
if (action == "joingame") {
|
if (action == "joingame") {
|
||||||
firstIntent = createJoinGameIntent(query, chain);
|
handleJoinGame(query);
|
||||||
} else if (action == "opendeck") {
|
} else if (action == "opendeck") {
|
||||||
firstIntent = createOpenDeckIntent(query, chain);
|
// handleOpenDeck(query);
|
||||||
} else {
|
} else {
|
||||||
qWarning() << "Unknown intent:" << action;
|
qWarning() << "Unknown intent:" << action;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (firstIntent == nullptr) {
|
|
||||||
// The link was invalid or the user declined the confirm: nothing runs.
|
|
||||||
// Report the idle state when no other chain is queued so that a startup
|
|
||||||
// launch (which skipped its own connection for this URL) falls back to it.
|
|
||||||
if (!chainRunning && pendingChains.isEmpty()) {
|
|
||||||
emit urlChainFinished(mainWindow->getRemoteClient()->getStatus() == StatusLoggedIn);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
pendingChains.append(chain);
|
|
||||||
startNextChain();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Intent *IntentUrlParser::createJoinGameIntent(const QUrlQuery &query, PendingIntentChain &chain)
|
void IntentUrlParser::handleJoinGame(const QUrlQuery &query)
|
||||||
{
|
{
|
||||||
auto showError = [this](const QString &message) { QMessageBox::warning(mainWindow, tr("Open game"), message); };
|
auto showError = [this](const QString &message) { QMessageBox::warning(mainWindow, tr("Open game"), message); };
|
||||||
|
|
||||||
|
|
@ -75,21 +49,21 @@ Intent *IntentUrlParser::createJoinGameIntent(const QUrlQuery &query, PendingInt
|
||||||
|
|
||||||
if (ctx->roomContext.serverContext.hostname.isEmpty()) {
|
if (ctx->roomContext.serverContext.hostname.isEmpty()) {
|
||||||
showError(tr("Missing or empty hostname in the game link"));
|
showError(tr("Missing or empty hostname in the game link"));
|
||||||
return nullptr;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ok = false;
|
bool ok = false;
|
||||||
ctx->roomContext.serverContext.port.toUShort(&ok);
|
ctx->roomContext.serverContext.port.toUShort(&ok);
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
showError(tr("Invalid or missing port in the game link"));
|
showError(tr("Invalid or missing port in the game link"));
|
||||||
return nullptr;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx->roomContext.roomId = query.queryItemValue("roomid").toInt(&ok);
|
ctx->roomContext.roomId = query.queryItemValue("roomid").toInt(&ok);
|
||||||
|
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
showError(tr("Invalid or missing room id in the game link"));
|
showError(tr("Invalid or missing room id in the game link"));
|
||||||
return nullptr;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ok = false;
|
ok = false;
|
||||||
|
|
@ -97,7 +71,7 @@ Intent *IntentUrlParser::createJoinGameIntent(const QUrlQuery &query, PendingInt
|
||||||
|
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
showError(tr("Invalid or missing game id in the game link"));
|
showError(tr("Invalid or missing game id in the game link"));
|
||||||
return nullptr;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const QString gameDescription = query.queryItemValue("game", QUrl::FullyDecoded);
|
const QString gameDescription = query.queryItemValue("game", QUrl::FullyDecoded);
|
||||||
|
|
@ -106,33 +80,24 @@ Intent *IntentUrlParser::createJoinGameIntent(const QUrlQuery &query, PendingInt
|
||||||
const QMessageBox::StandardButton answer = QMessageBox::question(
|
const QMessageBox::StandardButton answer = QMessageBox::question(
|
||||||
mainWindow, tr("Join game"), message, QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes);
|
mainWindow, tr("Join game"), message, QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes);
|
||||||
if (answer != QMessageBox::Yes) {
|
if (answer != QMessageBox::Yes) {
|
||||||
return nullptr;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
RemoteClient *client = mainWindow->getRemoteClient();
|
|
||||||
ContextConnectToServer *serverContext = &ctx->roomContext.serverContext;
|
|
||||||
|
|
||||||
// The join game intent owns the context and the credential lookup; once the
|
// The join game intent owns the context and the credential lookup; once the
|
||||||
// chain finishes (or fails) it deletes the whole tree.
|
// chain finishes (or fails) it deletes the whole tree.
|
||||||
auto joinGameIntent = new IntentJoinServerGame(mainWindow->getTabSupervisor(), client, std::move(ctx));
|
ContextConnectToServer *serverContext = &ctx->roomContext.serverContext;
|
||||||
|
auto joinGameIntent =
|
||||||
|
new IntentJoinServerGame(mainWindow->getTabSupervisor(), mainWindow->getRemoteClient(), std::move(ctx));
|
||||||
joinGameIntent->setParent(this);
|
joinGameIntent->setParent(this);
|
||||||
chain.intents.append(joinGameIntent);
|
|
||||||
|
auto getLoginCredentialsIntent = new IntentGetLoginCredentials(serverContext);
|
||||||
|
getLoginCredentialsIntent->setParent(joinGameIntent);
|
||||||
|
|
||||||
|
connect(getLoginCredentialsIntent, &Intent::finished, joinGameIntent, &Intent::execute);
|
||||||
|
connect(getLoginCredentialsIntent, &Intent::failed, joinGameIntent, &Intent::failed);
|
||||||
connect(joinGameIntent, &Intent::failed, this, [showError](const QString &reason) { showError(reason); });
|
connect(joinGameIntent, &Intent::failed, this, [showError](const QString &reason) { showError(reason); });
|
||||||
|
|
||||||
Intent *firstIntent = joinGameIntent;
|
getLoginCredentialsIntent->execute();
|
||||||
if (!isConnectedTo(serverContext->hostname, serverContext->port)) {
|
|
||||||
auto getLoginCredentialsIntent =
|
|
||||||
new IntentGetLoginCredentials(serverContext, /*promptForMissingCredentials=*/true);
|
|
||||||
getLoginCredentialsIntent->setParent(joinGameIntent);
|
|
||||||
chain.intents.insert(0, getLoginCredentialsIntent);
|
|
||||||
|
|
||||||
connect(getLoginCredentialsIntent, &Intent::finished, joinGameIntent, &Intent::execute);
|
|
||||||
connect(getLoginCredentialsIntent, &Intent::failed, joinGameIntent, &Intent::failed);
|
|
||||||
connect(getLoginCredentialsIntent, &Intent::cancelled, joinGameIntent, &Intent::cancelled);
|
|
||||||
firstIntent = getLoginCredentialsIntent;
|
|
||||||
}
|
|
||||||
|
|
||||||
return firstIntent;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QString IntentUrlParser::generateJoinGameMessage(const ContextJoinGame &context, const QString &gameDescription)
|
QString IntentUrlParser::generateJoinGameMessage(const ContextJoinGame &context, const QString &gameDescription)
|
||||||
|
|
@ -169,270 +134,3 @@ QString IntentUrlParser::generateJoinGameMessage(const ContextJoinGame &context,
|
||||||
.arg(gameDescription, gameIdStr, roomTab->getRoomName(), server)
|
.arg(gameDescription, gameIdStr, roomTab->getRoomName(), server)
|
||||||
: tr("Join game \"%1\" (#%2) on %3?").arg(gameDescription, gameIdStr, server);
|
: tr("Join game \"%1\" (#%2) on %3?").arg(gameDescription, gameIdStr, server);
|
||||||
}
|
}
|
||||||
|
|
||||||
Intent *IntentUrlParser::createOpenDeckIntent(const QUrlQuery &query, PendingIntentChain &chain)
|
|
||||||
{
|
|
||||||
auto showError = [this](const QString &message) {
|
|
||||||
QMessageBox::warning(mainWindow, tr("Open shared deck"), message);
|
|
||||||
};
|
|
||||||
|
|
||||||
auto ctx = std::make_unique<ContextOpenDeck>();
|
|
||||||
|
|
||||||
ctx->serverContext.hostname = query.queryItemValue("hostname");
|
|
||||||
ctx->serverContext.port = query.queryItemValue("port");
|
|
||||||
ctx->shareToken = query.queryItemValue("share");
|
|
||||||
|
|
||||||
qCDebug(UrlParserLog) << "Open-deck intent: host" << ctx->serverContext.hostname << "port"
|
|
||||||
<< ctx->serverContext.port << "token length" << ctx->shareToken.length();
|
|
||||||
|
|
||||||
if (ctx->serverContext.hostname.isEmpty()) {
|
|
||||||
showError(tr("Missing or empty hostname in the share link"));
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ok = false;
|
|
||||||
const quint16 port = ctx->serverContext.port.toUShort(&ok);
|
|
||||||
if (!ok || port == 0) {
|
|
||||||
showError(tr("Invalid or missing port in the share link"));
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ctx->shareToken.isEmpty()) {
|
|
||||||
showError(tr("Missing or empty share value in the share link"));
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
RemoteClient *client = mainWindow->getRemoteClient();
|
|
||||||
|
|
||||||
// The open deck download needs a connection to the link's server. Ask before
|
|
||||||
// taking the session anywhere it isn't already, naming the host we would
|
|
||||||
// connect to. Remember the link's target when it moves us away from a live
|
|
||||||
// session so a failed or cancelled chain can restore the session it left.
|
|
||||||
// The hostname is link-supplied and percent-decoded, so escape it: QMessageBox
|
|
||||||
// renders AutoText, and markup in a hostname would otherwise flip the whole
|
|
||||||
// prompt to rich text and let a link pad the message the user is shown.
|
|
||||||
const bool alreadyConnected = isConnectedTo(ctx->serverContext.hostname, ctx->serverContext.port);
|
|
||||||
if (!alreadyConnected) {
|
|
||||||
const QString target =
|
|
||||||
QStringLiteral("%1:%2").arg(ctx->serverContext.hostname.toHtmlEscaped(), ctx->serverContext.port);
|
|
||||||
|
|
||||||
if (client->getStatus() == StatusLoggedIn) {
|
|
||||||
const QString current =
|
|
||||||
QStringLiteral("%1:%2").arg(client->serverName(), QString::number(client->serverPort()));
|
|
||||||
const QMessageBox::StandardButton answer = QMessageBox::question(
|
|
||||||
mainWindow, tr("Open shared deck"),
|
|
||||||
tr("Opening this share link connects you to %1 instead of %2.\n\nContinue?").arg(target, current),
|
|
||||||
QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes);
|
|
||||||
if (answer != QMessageBox::Yes) {
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
chain.migrationTargetHost = ctx->serverContext.hostname;
|
|
||||||
chain.migrationTargetPort = ctx->serverContext.port;
|
|
||||||
chain.pendingRestore = true;
|
|
||||||
} else {
|
|
||||||
// Fresh connection is harmless to wander away from, but a server the
|
|
||||||
// client has never been configured for deserves a harder warning (no
|
|
||||||
// by default) so a stray link cannot silently steer the client there.
|
|
||||||
const bool knownHost = SettingsCache::instance().servers().findHostIndex(ctx->serverContext.hostname) >= 0;
|
|
||||||
const QMessageBox::StandardButton answer =
|
|
||||||
knownHost
|
|
||||||
? QMessageBox::question(mainWindow, tr("Open shared deck"),
|
|
||||||
tr("Opening this share link connects you to %1.\n\nContinue?").arg(target))
|
|
||||||
: QMessageBox::warning(mainWindow, tr("Open shared deck"),
|
|
||||||
tr("Opening this share link connects you to %1, a server you have "
|
|
||||||
"never connected to before.\n\nContinue?")
|
|
||||||
.arg(target),
|
|
||||||
QMessageBox::Yes | QMessageBox::No, QMessageBox::No);
|
|
||||||
if (answer != QMessageBox::Yes) {
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ContextConnectToServer *serverContext = &ctx->serverContext;
|
|
||||||
|
|
||||||
// The open deck intent owns the context and the credential lookup; once
|
|
||||||
// the chain finishes (or fails) it deletes the whole tree.
|
|
||||||
auto openDeckIntent =
|
|
||||||
new IntentOpenSharedDeck(mainWindow->getTabSupervisor(), client, CardDatabaseManager::query(), std::move(ctx));
|
|
||||||
openDeckIntent->setParent(this);
|
|
||||||
chain.intents.append(openDeckIntent);
|
|
||||||
connect(openDeckIntent, &Intent::failed, this, [showError](const QString &reason) { showError(reason); });
|
|
||||||
|
|
||||||
Intent *firstIntent = openDeckIntent;
|
|
||||||
if (!isConnectedTo(serverContext->hostname, serverContext->port)) {
|
|
||||||
auto getLoginCredentialsIntent =
|
|
||||||
new IntentGetLoginCredentials(serverContext, /*promptForMissingCredentials=*/true);
|
|
||||||
getLoginCredentialsIntent->setParent(openDeckIntent);
|
|
||||||
chain.intents.insert(0, getLoginCredentialsIntent);
|
|
||||||
|
|
||||||
connect(getLoginCredentialsIntent, &Intent::finished, openDeckIntent, &Intent::execute);
|
|
||||||
connect(getLoginCredentialsIntent, &Intent::failed, openDeckIntent, &Intent::failed);
|
|
||||||
connect(getLoginCredentialsIntent, &Intent::cancelled, openDeckIntent, &Intent::cancelled);
|
|
||||||
firstIntent = getLoginCredentialsIntent;
|
|
||||||
}
|
|
||||||
|
|
||||||
return firstIntent;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool IntentUrlParser::isConnectedTo(const QString &hostname, const QString &port) const
|
|
||||||
{
|
|
||||||
// serverName() reflects the server the client was configured to connect to,
|
|
||||||
// which may differ from the actual TCP peer (e.g. when connecting through a
|
|
||||||
// proxy), so compare the configured host and port — exactly what a link
|
|
||||||
// names. A link to the same host on another port is a different server and
|
|
||||||
// must not silently reuse an existing session there.
|
|
||||||
RemoteClient *client = mainWindow->getRemoteClient();
|
|
||||||
return client->getStatus() == StatusLoggedIn && client->serverName().compare(hostname, Qt::CaseInsensitive) == 0 &&
|
|
||||||
QString::number(client->serverPort()) == port;
|
|
||||||
}
|
|
||||||
|
|
||||||
void IntentUrlParser::startNextChain()
|
|
||||||
{
|
|
||||||
if (chainRunning || pendingChains.isEmpty()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
chainRunning = true;
|
|
||||||
|
|
||||||
PendingIntentChain &chain = pendingChains.first();
|
|
||||||
if (chain.intents.isEmpty()) {
|
|
||||||
pendingChains.removeFirst();
|
|
||||||
chainRunning = false;
|
|
||||||
startNextChain();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Snapshot the session this chain moves away from now that it actually
|
|
||||||
// runs. Chains are parsed while earlier ones are still queued, so a capture
|
|
||||||
// at parse time would follow whichever server the chain before it settled
|
|
||||||
// on, not the one the user is really on when this link is handled.
|
|
||||||
if (chain.pendingRestore) {
|
|
||||||
RemoteClient *client = mainWindow->getRemoteClient();
|
|
||||||
chain.previousServerHost = client->serverName();
|
|
||||||
chain.previousServerPort = QString::number(client->serverPort());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only the last intent completes the chain; its terminal signal ends the
|
|
||||||
// whole run. Cancellation of an intermediate intent (e.g. declined login
|
|
||||||
// prompt) is forwarded onto the last intent in the chain builders above.
|
|
||||||
Intent *finalIntent = chain.intents.last();
|
|
||||||
connect(finalIntent, &Intent::finished, this, [this]() { chainEnded(true); });
|
|
||||||
connect(finalIntent, &Intent::failed, this, [this]() { chainEnded(false); });
|
|
||||||
connect(finalIntent, &Intent::cancelled, this, [this]() { chainEnded(false); });
|
|
||||||
// Backstop: if the final intent is destroyed without emitting a terminal
|
|
||||||
// signal (e.g. a network error dropped it while running), end the chain so
|
|
||||||
// later links are not queued and dropped for the rest of the session.
|
|
||||||
chainBackstopConnection = connect(finalIntent, &QObject::destroyed, this, &IntentUrlParser::onChainIntentDestroyed);
|
|
||||||
|
|
||||||
chain.intents.first()->execute();
|
|
||||||
}
|
|
||||||
|
|
||||||
void IntentUrlParser::chainEnded(bool chainSucceeded)
|
|
||||||
{
|
|
||||||
chainRunning = false;
|
|
||||||
QObject::disconnect(chainBackstopConnection);
|
|
||||||
|
|
||||||
const PendingIntentChain chain = pendingChains.takeFirst();
|
|
||||||
|
|
||||||
// Only a failed or cancelled chain restores the session the link migrated
|
|
||||||
// away from; a successful one leaves the user where they are.
|
|
||||||
if (chain.pendingRestore && !chainSucceeded) {
|
|
||||||
restorePreviousServer(chain);
|
|
||||||
}
|
|
||||||
|
|
||||||
startNextChain();
|
|
||||||
|
|
||||||
// Only report the terminal state once the queue has fully drained, so a
|
|
||||||
// queued follow-up link keeps the startup fallback out of the picture.
|
|
||||||
if (!chainRunning && pendingChains.isEmpty()) {
|
|
||||||
emit urlChainFinished(mainWindow->getRemoteClient()->getStatus() == StatusLoggedIn);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void IntentUrlParser::onChainIntentDestroyed()
|
|
||||||
{
|
|
||||||
if (!chainRunning) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
qCWarning(UrlParserLog) << "Share-link intent destroyed without a terminal signal; ending its chain";
|
|
||||||
chainEnded(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
void IntentUrlParser::restorePreviousServer(const PendingIntentChain &chain)
|
|
||||||
{
|
|
||||||
if (chain.previousServerHost.isEmpty()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
RemoteClient *client = mainWindow->getRemoteClient();
|
|
||||||
const ClientStatus status = client->getStatus();
|
|
||||||
|
|
||||||
// A failed/cancelled chain can fire while the client is still settling the
|
|
||||||
// in-flight connection attempt (wrong password, connect timeout). Only
|
|
||||||
// decide once the client has settled into logged-in or disconnected;
|
|
||||||
// deciding mid-connect would strand the user offline from their previous
|
|
||||||
// server.
|
|
||||||
if (status == StatusDisconnected || status == StatusLoggedIn) {
|
|
||||||
restoreToPreviousServer(chain);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
auto waitConnection = std::make_shared<QMetaObject::Connection>();
|
|
||||||
*waitConnection = connect(client, &RemoteClient::statusChanged, this, [this, chain, client, waitConnection]() {
|
|
||||||
const ClientStatus settled = client->getStatus();
|
|
||||||
if (settled == StatusDisconnected || settled == StatusLoggedIn) {
|
|
||||||
QObject::disconnect(*waitConnection);
|
|
||||||
restoreToPreviousServer(chain);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void IntentUrlParser::restoreToPreviousServer(const PendingIntentChain &chain)
|
|
||||||
{
|
|
||||||
RemoteClient *client = mainWindow->getRemoteClient();
|
|
||||||
|
|
||||||
// Back on the previous server already → nothing to undo.
|
|
||||||
if (client->serverName().compare(chain.previousServerHost, Qt::CaseInsensitive) == 0 &&
|
|
||||||
QString::number(client->serverPort()) == chain.previousServerPort) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// When logged in somewhere, only intervene if that somewhere is the server
|
|
||||||
// the link moved us to; if the user went elsewhere on their own, leave them.
|
|
||||||
if (client->getStatus() == StatusLoggedIn) {
|
|
||||||
const bool onMigrationTarget =
|
|
||||||
client->serverName().compare(chain.migrationTargetHost, Qt::CaseInsensitive) == 0 &&
|
|
||||||
QString::number(client->serverPort()) == chain.migrationTargetPort;
|
|
||||||
if (!onMigrationTarget) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ServersSettings &servers = SettingsCache::instance().servers();
|
|
||||||
const int index = servers.findServerIndex(chain.previousServerHost, chain.previousServerPort);
|
|
||||||
if (index >= 0 && servers.hasLoginData(chain.previousServerHost, chain.previousServerPort)) {
|
|
||||||
const QString username =
|
|
||||||
servers.getValue(QString("username%1").arg(index), "server", "server_details").toString();
|
|
||||||
const QString password =
|
|
||||||
servers.getValue(QString("password%1").arg(index), "server", "server_details").toString();
|
|
||||||
client->connectToServer(chain.previousServerHost, chain.previousServerPort.toUInt(), username, password);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
client->disconnectFromServer();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (client->getStatus() != StatusDisconnected) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// The link's connection attempt failed: reconnect to the previous server
|
|
||||||
// when credentials are saved, otherwise stay offline.
|
|
||||||
ServersSettings &servers = SettingsCache::instance().servers();
|
|
||||||
const int index = servers.findServerIndex(chain.previousServerHost, chain.previousServerPort);
|
|
||||||
if (index >= 0 && servers.hasLoginData(chain.previousServerHost, chain.previousServerPort)) {
|
|
||||||
const QString username =
|
|
||||||
servers.getValue(QString("username%1").arg(index), "server", "server_details").toString();
|
|
||||||
const QString password =
|
|
||||||
servers.getValue(QString("password%1").arg(index), "server", "server_details").toString();
|
|
||||||
client->connectToServer(chain.previousServerHost, chain.previousServerPort.toUInt(), username, password);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,46 +1,10 @@
|
||||||
#ifndef COCKATRICE_URL_PARSER_H
|
#ifndef COCKATRICE_URL_PARSER_H
|
||||||
#define COCKATRICE_URL_PARSER_H
|
#define COCKATRICE_URL_PARSER_H
|
||||||
|
|
||||||
#include <QList>
|
|
||||||
#include <QObject>
|
#include <QObject>
|
||||||
#include <QUrlQuery>
|
#include <QUrlQuery>
|
||||||
|
|
||||||
class Intent;
|
|
||||||
class MainWindow;
|
class MainWindow;
|
||||||
struct ContextJoinGame;
|
struct ContextJoinGame;
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief One queued intent chain with the session-migration bookkeeping for it.
|
|
||||||
*
|
|
||||||
* The restore fields are per-chain on purpose: chains are parsed while earlier
|
|
||||||
* ones are still queued, so parser-wide state would let one chain's failure
|
|
||||||
* consume the restore data another chain recorded.
|
|
||||||
*/
|
|
||||||
struct PendingIntentChain
|
|
||||||
{
|
|
||||||
QList<Intent *> intents;
|
|
||||||
|
|
||||||
// Snapshot of the session in place when this chain started running, so a
|
|
||||||
// queued chain follows whichever server the chain before it settled on.
|
|
||||||
QString previousServerHost;
|
|
||||||
QString previousServerPort;
|
|
||||||
|
|
||||||
// Recorded at parse time when the user confirmed migrating away from a live
|
|
||||||
// session to the host/port named by the link.
|
|
||||||
QString migrationTargetHost;
|
|
||||||
QString migrationTargetPort;
|
|
||||||
bool pendingRestore = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Parses cockatrice:// links and runs them as serialized intent chains.
|
|
||||||
*
|
|
||||||
* Links are parsed by action (joingame/opendeck) and translated into an intent
|
|
||||||
* chain. Chains are queued and run one at a time: a document can hand multiple
|
|
||||||
* links to the window while an earlier chain still connects, and running two
|
|
||||||
* connect chains concurrently tears the connection down. urlChainFinished is
|
|
||||||
* emitted once the queue has fully drained.
|
|
||||||
*/
|
|
||||||
class IntentUrlParser : public QObject
|
class IntentUrlParser : public QObject
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
@ -48,28 +12,12 @@ class IntentUrlParser : public QObject
|
||||||
public:
|
public:
|
||||||
IntentUrlParser(QObject *parent, MainWindow *mainWindow);
|
IntentUrlParser(QObject *parent, MainWindow *mainWindow);
|
||||||
void handle(const QString &urlStr);
|
void handle(const QString &urlStr);
|
||||||
|
void handleJoinGame(const QUrlQuery &query);
|
||||||
signals:
|
|
||||||
/** @brief Emitted when the last queued chain ended; carries whether the client is logged in. */
|
|
||||||
void urlChainFinished(bool connected);
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
Intent *createJoinGameIntent(const QUrlQuery &query, PendingIntentChain &chain);
|
|
||||||
Intent *createOpenDeckIntent(const QUrlQuery &query, PendingIntentChain &chain);
|
|
||||||
QString generateJoinGameMessage(const ContextJoinGame &context, const QString &gameDescription);
|
QString generateJoinGameMessage(const ContextJoinGame &context, const QString &gameDescription);
|
||||||
[[nodiscard]] bool isConnectedTo(const QString &hostname, const QString &port) const;
|
|
||||||
void startNextChain();
|
|
||||||
void chainEnded(bool chainSucceeded);
|
|
||||||
void onChainIntentDestroyed();
|
|
||||||
void restorePreviousServer(const PendingIntentChain &chain);
|
|
||||||
void restoreToPreviousServer(const PendingIntentChain &chain);
|
|
||||||
|
|
||||||
MainWindow *mainWindow;
|
MainWindow *mainWindow;
|
||||||
QList<PendingIntentChain> pendingChains;
|
|
||||||
bool chainRunning = false;
|
|
||||||
// Disconnects the destroyed-signal backstop once a chain ends, so an old
|
|
||||||
// intent's deferred deletion cannot end the chain that runs after it.
|
|
||||||
QMetaObject::Connection chainBackstopConnection;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // COCKATRICE_URL_PARSER_H
|
#endif // COCKATRICE_URL_PARSER_H
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
#include "palette_editor_dialog.h"
|
#include "palette_editor_dialog.h"
|
||||||
|
|
||||||
|
#include "../../client/settings/cache_settings.h"
|
||||||
#include "../theme_manager.h"
|
#include "../theme_manager.h"
|
||||||
#include "palette_generator.h"
|
#include "palette_generator.h"
|
||||||
#include "palette_grid_widget.h"
|
#include "palette_grid_widget.h"
|
||||||
|
|
@ -10,11 +11,31 @@
|
||||||
#include <QDialogButtonBox>
|
#include <QDialogButtonBox>
|
||||||
#include <QDir>
|
#include <QDir>
|
||||||
#include <QFile>
|
#include <QFile>
|
||||||
|
#include <QFileInfo>
|
||||||
#include <QFrame>
|
#include <QFrame>
|
||||||
|
#include <QGuiApplication>
|
||||||
#include <QLabel>
|
#include <QLabel>
|
||||||
|
#include <QLoggingCategory>
|
||||||
#include <QMessageBox>
|
#include <QMessageBox>
|
||||||
#include <QPushButton>
|
#include <QPushButton>
|
||||||
|
#include <QStyleHints>
|
||||||
#include <QTimer>
|
#include <QTimer>
|
||||||
|
#include <libcockatrice/settings/paths_settings.h>
|
||||||
|
|
||||||
|
// Probe whether a directory is truly writable by trying to create and remove a
|
||||||
|
// temporary file. QFileInfo::isWritable() on a directory is unreliable (notably
|
||||||
|
// on Windows where UAC VirtualStore can make a system dir appear writable).
|
||||||
|
static bool isDirReallyWritable(const QString &dirPath)
|
||||||
|
{
|
||||||
|
const QString probe = QDir(dirPath).absoluteFilePath(".cockatrice_write_test");
|
||||||
|
QFile f(probe);
|
||||||
|
if (!f.open(QIODevice::WriteOnly)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
f.close();
|
||||||
|
f.remove();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
PaletteEditorDialog::PaletteEditorDialog(const QString &_themeDirPath, const QString &_themeName, QWidget *parent)
|
PaletteEditorDialog::PaletteEditorDialog(const QString &_themeDirPath, const QString &_themeName, QWidget *parent)
|
||||||
: QDialog(parent), themeDirPath(_themeDirPath), themeName(_themeName)
|
: QDialog(parent), themeDirPath(_themeDirPath), themeName(_themeName)
|
||||||
|
|
@ -25,7 +46,14 @@ PaletteEditorDialog::PaletteEditorDialog(const QString &_themeDirPath, const QSt
|
||||||
// Resolve a writable directory for saving. Built-in (Default / Fusion) and
|
// Resolve a writable directory for saving. Built-in (Default / Fusion) and
|
||||||
// other read-only theme directories must be customised in the user-writable
|
// other read-only theme directories must be customised in the user-writable
|
||||||
// themes directory; otherwise the write would fail or be lost on upgrade.
|
// themes directory; otherwise the write would fail or be lost on upgrade.
|
||||||
saveDir = ThemeManager::writableThemeDir(themeName);
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Load both scheme configs upfront so switching is instant
|
// Load both scheme configs upfront so switching is instant
|
||||||
loadSchemes();
|
loadSchemes();
|
||||||
|
|
@ -186,7 +214,7 @@ void PaletteEditorDialog::retranslateUi()
|
||||||
resetBtn->setToolTip(tr("Discard unsaved edits and restore the last saved palette"));
|
resetBtn->setToolTip(tr("Discard unsaved edits and restore the last saved palette"));
|
||||||
saveBtn->setToolTip(tr("Write palette-%1.toml and reload the theme").arg(loadedScheme.toLower()));
|
saveBtn->setToolTip(tr("Write palette-%1.toml and reload the theme").arg(loadedScheme.toLower()));
|
||||||
|
|
||||||
if (saveDir.isEmpty() || !ThemeManager::isDirReallyWritable(saveDir)) {
|
if (saveDir.isEmpty() || !isDirReallyWritable(saveDir)) {
|
||||||
saveBtn->setEnabled(false);
|
saveBtn->setEnabled(false);
|
||||||
saveBtn->setToolTip(tr("Cannot save: this theme has no writable directory"));
|
saveBtn->setToolTip(tr("Cannot save: this theme has no writable directory"));
|
||||||
}
|
}
|
||||||
|
|
@ -269,7 +297,7 @@ void PaletteEditorDialog::onSave()
|
||||||
if (it.key() == loadedScheme) {
|
if (it.key() == loadedScheme) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (it.value() == savedConfig.value(it.key())) {
|
if (it.value().colors == savedConfig.value(it.key()).colors) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (!ThemeManager::commitPalette(saveDir, it.key(), it.value())) {
|
if (!ThemeManager::commitPalette(saveDir, it.key(), it.value())) {
|
||||||
|
|
@ -280,7 +308,7 @@ void PaletteEditorDialog::onSave()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Commit the active scheme last so the global colour scheme matches.
|
// Commit the active scheme last so the global colour scheme matches.
|
||||||
if (workingConfig[loadedScheme] != savedConfig.value(loadedScheme)) {
|
if (workingConfig[loadedScheme].colors != savedConfig.value(loadedScheme).colors) {
|
||||||
if (!ThemeManager::commitPalette(saveDir, loadedScheme, workingConfig[loadedScheme])) {
|
if (!ThemeManager::commitPalette(saveDir, loadedScheme, workingConfig[loadedScheme])) {
|
||||||
QMessageBox::warning(this, tr("Save failed"),
|
QMessageBox::warning(this, tr("Save failed"),
|
||||||
tr("Could not write %1 to:\n%2").arg(PaletteConfig::fileName(loadedScheme), saveDir));
|
tr("Could not write %1 to:\n%2").arg(PaletteConfig::fileName(loadedScheme), saveDir));
|
||||||
|
|
|
||||||
|
|
@ -150,17 +150,6 @@ PaletteConfig fromAccent(const QColor &accent, int intensity, const QString &sch
|
||||||
cfg.colors[CG::Disabled][CR::HighlightedText] = disText;
|
cfg.colors[CG::Disabled][CR::HighlightedText] = disText;
|
||||||
cfg.colors[CG::Inactive][CR::HighlightedText] = dark ? Qt::white : Qt::black;
|
cfg.colors[CG::Inactive][CR::HighlightedText] = dark ? Qt::white : Qt::black;
|
||||||
|
|
||||||
// Accent: same primary hue as Highlight, so palettes derived from a
|
|
||||||
// QuickSetup accent always carry a matching Accent role.
|
|
||||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 6, 0)
|
|
||||||
set3(CR::Accent, hl, disText, hl);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// Application role colors: Strong tracks the primary accent, while Soft is
|
|
||||||
// the lightened, desaturated companion used for button-gradient highlights.
|
|
||||||
cfg.appColors[AppColor::AccentStrong] = hl;
|
|
||||||
cfg.appColors[AppColor::AccentSoft] = hsl(accent.lightness() + 60, qRound(accent.hslSaturation() * 70 / 100.0));
|
|
||||||
|
|
||||||
// BrightText
|
// BrightText
|
||||||
QColor bright;
|
QColor bright;
|
||||||
if (achromatic) {
|
if (achromatic) {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,5 @@
|
||||||
#include "palette_grid_widget.h"
|
#include "palette_grid_widget.h"
|
||||||
|
|
||||||
#include "../theme_manager.h"
|
|
||||||
|
|
||||||
#include <QApplication>
|
#include <QApplication>
|
||||||
#include <QGridLayout>
|
#include <QGridLayout>
|
||||||
#include <QLabel>
|
#include <QLabel>
|
||||||
|
|
@ -47,11 +45,6 @@ static const QMap<QPalette::ColorRole, const char *> ROLE_DESCRIPTIONS = {
|
||||||
{QPalette::Shadow, QT_TR_NOOP("Very dark shadow colour")},
|
{QPalette::Shadow, QT_TR_NOOP("Very dark shadow colour")},
|
||||||
};
|
};
|
||||||
|
|
||||||
static const QMap<AppColor::Role, const char *> APP_ROLE_DESCRIPTIONS = {
|
|
||||||
{AppColor::AccentStrong, QT_TR_NOOP("Vivid primary accent (e.g. home-tab button gradient start)")},
|
|
||||||
{AppColor::AccentSoft, QT_TR_NOOP("Lightened, desaturated accent (e.g. home-tab button gradient end)")},
|
|
||||||
};
|
|
||||||
|
|
||||||
PaletteGridWidget::PaletteGridWidget(QWidget *parent) : QWidget(parent)
|
PaletteGridWidget::PaletteGridWidget(QWidget *parent) : QWidget(parent)
|
||||||
{
|
{
|
||||||
scroll = new QScrollArea(this);
|
scroll = new QScrollArea(this);
|
||||||
|
|
@ -129,46 +122,6 @@ void PaletteGridWidget::buildGrid(QWidget *host)
|
||||||
grid->addWidget(btn, row + 1, col + 1, Qt::AlignHCenter | Qt::AlignVCenter);
|
grid->addWidget(btn, row + 1, col + 1, Qt::AlignHCenter | Qt::AlignVCenter);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Application color section: one ColorButton per role below the role grid.
|
|
||||||
// These are not tied to a color group, so a single button spans the row.
|
|
||||||
QMetaEnum appEnum = QMetaEnum::fromType<AppColor::Role>();
|
|
||||||
|
|
||||||
const int appHeaderRow = roles.size() + 1;
|
|
||||||
|
|
||||||
auto *appHeader = new QLabel(tr("App colors"), host);
|
|
||||||
appHeader->setToolTip(tr("Application-specific colors layered on top of the Qt palette"));
|
|
||||||
QFont appHeaderFont = appHeader->font();
|
|
||||||
appHeaderFont.setBold(true);
|
|
||||||
appHeader->setFont(appHeaderFont);
|
|
||||||
appHeader->setAutoFillBackground(true);
|
|
||||||
appHeader->setContentsMargins(4, 4, 4, 4);
|
|
||||||
grid->addWidget(appHeader, appHeaderRow, 0, 1, 4);
|
|
||||||
headerLabels.append(appHeader);
|
|
||||||
|
|
||||||
for (int i = 0; i < appEnum.keyCount(); ++i) {
|
|
||||||
auto role = static_cast<AppColor::Role>(appEnum.value(i));
|
|
||||||
const int row = appHeaderRow + 1 + i;
|
|
||||||
|
|
||||||
if (i % 2 == 0) {
|
|
||||||
for (int col = 0; col < 4; ++col) {
|
|
||||||
auto *shade = new QWidget(host);
|
|
||||||
shade->setAutoFillBackground(true);
|
|
||||||
grid->addWidget(shade, row, col);
|
|
||||||
rowShadeWidgets.push_back(shade);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
auto *label = new QLabel(QString(appEnum.valueToKey(role)), host);
|
|
||||||
label->setToolTip(APP_ROLE_DESCRIPTIONS.value(role, {}));
|
|
||||||
label->setContentsMargins(4, 2, 8, 2);
|
|
||||||
grid->addWidget(label, row, 0);
|
|
||||||
|
|
||||||
auto *btn = new ColorButton(host);
|
|
||||||
connect(btn, &ColorButton::colorChanged, this, [this] { emit paletteChanged(); });
|
|
||||||
appColorButtons[role] = btn;
|
|
||||||
grid->addWidget(btn, row, 1, Qt::AlignHCenter | Qt::AlignVCenter);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void PaletteGridWidget::changeEvent(QEvent *e)
|
void PaletteGridWidget::changeEvent(QEvent *e)
|
||||||
|
|
@ -213,16 +166,6 @@ void PaletteGridWidget::loadPalette(const PaletteConfig &cfg)
|
||||||
colorButtons[group][role]->setColor(color);
|
colorButtons[group][role]->setColor(color);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
QMetaEnum appEnum = QMetaEnum::fromType<AppColor::Role>();
|
|
||||||
for (int i = 0; i < appEnum.keyCount(); ++i) {
|
|
||||||
auto role = static_cast<AppColor::Role>(appEnum.value(i));
|
|
||||||
QColor color = cfg.appColors.value(role);
|
|
||||||
if (!color.isValid()) {
|
|
||||||
color = themeManager->appColor(role);
|
|
||||||
}
|
|
||||||
appColorButtons[role]->setColor(color);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
PaletteConfig PaletteGridWidget::currentPaletteConfig() const
|
PaletteConfig PaletteGridWidget::currentPaletteConfig() const
|
||||||
|
|
@ -233,12 +176,5 @@ PaletteConfig PaletteGridWidget::currentPaletteConfig() const
|
||||||
cfg.colors[group][role] = colorButtons[group][role]->getColor();
|
cfg.colors[group][role] = colorButtons[group][role]->getColor();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
QMetaEnum appEnum = QMetaEnum::fromType<AppColor::Role>();
|
|
||||||
for (int i = 0; i < appEnum.keyCount(); ++i) {
|
|
||||||
auto role = static_cast<AppColor::Role>(appEnum.value(i));
|
|
||||||
cfg.appColors[role] = appColorButtons[role]->getColor();
|
|
||||||
}
|
|
||||||
|
|
||||||
return cfg;
|
return cfg;
|
||||||
}
|
}
|
||||||
|
|
@ -31,7 +31,6 @@ private:
|
||||||
void refreshChromePalettes();
|
void refreshChromePalettes();
|
||||||
|
|
||||||
QMap<QPalette::ColorGroup, QMap<QPalette::ColorRole, ColorButton *>> colorButtons;
|
QMap<QPalette::ColorGroup, QMap<QPalette::ColorRole, ColorButton *>> colorButtons;
|
||||||
QMap<AppColor::Role, ColorButton *> appColorButtons;
|
|
||||||
QScrollArea *scroll;
|
QScrollArea *scroll;
|
||||||
QWidget *gridHost;
|
QWidget *gridHost;
|
||||||
QVBoxLayout *layout;
|
QVBoxLayout *layout;
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,5 @@
|
||||||
#include "pixel_map_generator.h"
|
#include "pixel_map_generator.h"
|
||||||
|
|
||||||
#include "theme_manager.h"
|
|
||||||
|
|
||||||
#include <QApplication>
|
#include <QApplication>
|
||||||
#include <QDomDocument>
|
#include <QDomDocument>
|
||||||
#include <QFile>
|
#include <QFile>
|
||||||
|
|
@ -84,13 +82,7 @@ static QPixmap loadSvg(const QString &svgPath, const QSize &size, bool expandOnl
|
||||||
/**
|
/**
|
||||||
* Try to load path image from non-SVG formats, otherwise fall back to SVG.
|
* Try to load path image from non-SVG formats, otherwise fall back to SVG.
|
||||||
* This is to allow custom themes to support non-SVG format type overrides, since SVG requires custom loading.
|
* This is to allow custom themes to support non-SVG format type overrides, since SVG requires custom loading.
|
||||||
*
|
* @param path The path to the file, with no file extension. File formats will be automatically detected.
|
||||||
* The path may already carry the resolved file extension (e.g. via
|
|
||||||
* ThemeManager::assetPath); such paths are loaded directly. Otherwise a
|
|
||||||
* format-agnostic lookup probes png, jpg and finally svg.
|
|
||||||
*
|
|
||||||
* @param path The path to the file, with no file extension unless the caller
|
|
||||||
* already resolved it. File formats will be automatically detected.
|
|
||||||
* @param size The desired size of the pixmap.
|
* @param size The desired size of the pixmap.
|
||||||
* @param expandOnly If true, then keep the size of the initial pixmap to at least the size (Only relevant if SVG).
|
* @param expandOnly If true, then keep the size of the initial pixmap to at least the size (Only relevant if SVG).
|
||||||
*
|
*
|
||||||
|
|
@ -98,19 +90,6 @@ static QPixmap loadSvg(const QString &svgPath, const QSize &size, bool expandOnl
|
||||||
*/
|
*/
|
||||||
static QPixmap tryLoadImage(const QString &path, const QSize &size, bool expandOnly = false)
|
static QPixmap tryLoadImage(const QString &path, const QSize &size, bool expandOnly = false)
|
||||||
{
|
{
|
||||||
if (path.endsWith(QLatin1String(".svg"), Qt::CaseInsensitive)) {
|
|
||||||
return loadSvg(path, size, expandOnly);
|
|
||||||
}
|
|
||||||
if (path.endsWith(QLatin1String(".png"), Qt::CaseInsensitive) ||
|
|
||||||
path.endsWith(QLatin1String(".jpg"), Qt::CaseInsensitive) ||
|
|
||||||
path.endsWith(QLatin1String(".jpeg"), Qt::CaseInsensitive)) {
|
|
||||||
QPixmap pix(path);
|
|
||||||
if (!pix.isNull()) {
|
|
||||||
return pix.scaled(size, Qt::KeepAspectRatio, Qt::SmoothTransformation);
|
|
||||||
}
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
const auto formats = {"png", "jpg"};
|
const auto formats = {"png", "jpg"};
|
||||||
|
|
||||||
QPixmap returnPixmap;
|
QPixmap returnPixmap;
|
||||||
|
|
@ -132,8 +111,7 @@ QPixmap PhasePixmapGenerator::generatePixmap(int height, QString name)
|
||||||
return pmCache.value(key);
|
return pmCache.value(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
QPixmap pixmap = tryLoadImage(QStringLiteral("theme:") + themeManager->assetPath(QStringLiteral("phases/") + name),
|
QPixmap pixmap = tryLoadImage("theme:phases/" + name, QSize(height, height));
|
||||||
QSize(height, height));
|
|
||||||
|
|
||||||
pmCache.insert(key, pixmap);
|
pmCache.insert(key, pixmap);
|
||||||
return pixmap;
|
return pixmap;
|
||||||
|
|
@ -361,10 +339,6 @@ static QString getIconType(const bool isBuddy, const UserLevelFlags &userLevelFl
|
||||||
return "pawn_judge";
|
return "pawn_judge";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (userLevelFlags.testFlag(ServerInfo_User::IsDeveloper)) {
|
|
||||||
return "pawn_dev";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!privLevel.isEmpty() && privLevel.toLower() != "none") {
|
if (!privLevel.isEmpty() && privLevel.toLower() != "none") {
|
||||||
return QString("pawn_%1").arg(privLevel.toLower());
|
return QString("pawn_%1").arg(privLevel.toLower());
|
||||||
}
|
}
|
||||||
|
|
@ -422,8 +396,7 @@ QPixmap LockPixmapGenerator::generatePixmap(int height)
|
||||||
return pmCache.value(key);
|
return pmCache.value(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
QPixmap pixmap = tryLoadImage(QStringLiteral("theme:") + themeManager->assetPath(QStringLiteral("icons/lock")),
|
QPixmap pixmap = tryLoadImage("theme:icons/lock", QSize(height, height), true);
|
||||||
QSize(height, height), true);
|
|
||||||
pmCache.insert(key, pixmap);
|
pmCache.insert(key, pixmap);
|
||||||
return pixmap;
|
return pixmap;
|
||||||
}
|
}
|
||||||
|
|
@ -438,8 +411,7 @@ QPixmap DropdownIconPixmapGenerator::generatePixmap(int height, bool expanded)
|
||||||
}
|
}
|
||||||
|
|
||||||
QString name = expanded ? "dropdown_expanded" : "dropdown_collapsed";
|
QString name = expanded ? "dropdown_expanded" : "dropdown_collapsed";
|
||||||
QPixmap pixmap = tryLoadImage(QStringLiteral("theme:") + themeManager->assetPath(QStringLiteral("icons/") + name),
|
QPixmap pixmap = tryLoadImage("theme:icons/" + name, QSize(height, height), true);
|
||||||
QSize(height, height), true);
|
|
||||||
|
|
||||||
pmCache.insert(key, pixmap);
|
pmCache.insert(key, pixmap);
|
||||||
return pixmap;
|
return pixmap;
|
||||||
|
|
@ -500,13 +472,6 @@ QHash<QString, QPixmap> ManaSymbolPixmapGenerator::scaledCache;
|
||||||
|
|
||||||
QPixmap loadColorAdjustedPixmap(const QString &name)
|
QPixmap loadColorAdjustedPixmap(const QString &name)
|
||||||
{
|
{
|
||||||
// Prefer an authored scheme-qualified variant when one exists for this asset.
|
|
||||||
const QString variant = themeManager->schemeVariantPath(QStringView(name).mid(QStringLiteral("theme:").size()));
|
|
||||||
if (!variant.isEmpty()) {
|
|
||||||
return QPixmap(QStringLiteral("theme:") + variant);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Legacy fallback: runtime-invert for dark mode when no authored variant.
|
|
||||||
if (qApp->palette().windowText().color().lightness() > 200) {
|
if (qApp->palette().windowText().color().lightness() > 200) {
|
||||||
QImage img(name);
|
QImage img(name);
|
||||||
img.invertPixels();
|
img.invertPixels();
|
||||||
|
|
@ -517,21 +482,3 @@ QPixmap loadColorAdjustedPixmap(const QString &name)
|
||||||
return QPixmap(name);
|
return QPixmap(name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
QPixmap themePixmap(QStringView prefix)
|
|
||||||
{
|
|
||||||
const QString resolved = themeManager->assetPath(prefix);
|
|
||||||
return QPixmap(QStringLiteral("theme:") + resolved);
|
|
||||||
}
|
|
||||||
|
|
||||||
void clearPixmapGeneratorCaches()
|
|
||||||
{
|
|
||||||
PhasePixmapGenerator::clear();
|
|
||||||
CounterPixmapGenerator::clear();
|
|
||||||
PingPixmapGenerator::clear();
|
|
||||||
CountryPixmapGenerator::clear();
|
|
||||||
UserLevelPixmapGenerator::clear();
|
|
||||||
LockPixmapGenerator::clear();
|
|
||||||
DropdownIconPixmapGenerator::clear();
|
|
||||||
ManaSymbolPixmapGenerator::clear();
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -156,15 +156,4 @@ public:
|
||||||
|
|
||||||
QPixmap loadColorAdjustedPixmap(const QString &name);
|
QPixmap loadColorAdjustedPixmap(const QString &name);
|
||||||
|
|
||||||
// Loads a "theme:" asset (with no file extension in prefix), preferring the
|
|
||||||
// scheme-qualified variant (prefix-dark / prefix-light, resolved via
|
|
||||||
// ThemeManager::assetPath) and falling back to the plain asset. Callers load
|
|
||||||
// the returned path directly. Use for scheme-sensitive pixmaps like
|
|
||||||
// backgrounds, the card back, and the app logo.
|
|
||||||
QPixmap themePixmap(QStringView prefix);
|
|
||||||
|
|
||||||
// Clears every PixmapGenerator's static cache so scheme variants are
|
|
||||||
// re-resolved when the active theme or color scheme changes.
|
|
||||||
void clearPixmapGeneratorCaches();
|
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ QString ThemeConfig::toIni() const
|
||||||
out += "[Appearance]\n";
|
out += "[Appearance]\n";
|
||||||
out += QString("ColorScheme = %1\n").arg(colorScheme.isEmpty() ? "System" : colorScheme);
|
out += QString("ColorScheme = %1\n").arg(colorScheme.isEmpty() ? "System" : colorScheme);
|
||||||
out += "\n[Style]\n";
|
out += "\n[Style]\n";
|
||||||
out += QString("Name = %1\n").arg(styleName.isEmpty() ? "System" : styleName);
|
out += QString("Name = %1\n").arg(styleName.isEmpty() ? "Default" : styleName);
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -96,7 +96,7 @@ bool ThemeConfig::save(const QString &themeDirPath) const
|
||||||
|
|
||||||
bool PaletteConfig::hasPalette() const
|
bool PaletteConfig::hasPalette() const
|
||||||
{
|
{
|
||||||
return !colors.isEmpty() || !appColors.isEmpty();
|
return !colors.isEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
QString PaletteConfig::toToml() const
|
QString PaletteConfig::toToml() const
|
||||||
|
|
@ -133,24 +133,6 @@ QString PaletteConfig::toToml() const
|
||||||
out += "\n";
|
out += "\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!appColors.isEmpty()) {
|
|
||||||
QMetaEnum appEnum = QMetaEnum::fromType<AppColor::Role>();
|
|
||||||
|
|
||||||
out += "[AppColors]\n";
|
|
||||||
|
|
||||||
for (auto it = appColors.cbegin(); it != appColors.cend(); ++it) {
|
|
||||||
const char *roleName = appEnum.valueToKey(it.key());
|
|
||||||
|
|
||||||
if (!roleName) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
out += QString("%1 = %2\n").arg(QString(roleName), -20).arg(it.value().name(QColor::HexArgb));
|
|
||||||
}
|
|
||||||
|
|
||||||
out += "\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -170,7 +152,6 @@ PaletteConfig PaletteConfig::fromFile(const QString &filePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
QMetaEnum roleEnum = QMetaEnum::fromType<QPalette::ColorRole>();
|
QMetaEnum roleEnum = QMetaEnum::fromType<QPalette::ColorRole>();
|
||||||
QMetaEnum appEnum = QMetaEnum::fromType<AppColor::Role>();
|
|
||||||
|
|
||||||
QString currentSection;
|
QString currentSection;
|
||||||
QPalette::ColorGroup currentGroup = QPalette::Active;
|
QPalette::ColorGroup currentGroup = QPalette::Active;
|
||||||
|
|
@ -221,26 +202,6 @@ PaletteConfig PaletteConfig::fromFile(const QString &filePath)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
QColor color(value);
|
|
||||||
|
|
||||||
if (!color.isValid()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentSection.compare("AppColors", Qt::CaseInsensitive) == 0) {
|
|
||||||
if (key.startsWith("AppColor::")) {
|
|
||||||
key = key.mid(10);
|
|
||||||
}
|
|
||||||
|
|
||||||
int appRoleInt = appEnum.keyToValue(key.toUtf8().constData());
|
|
||||||
|
|
||||||
if (appRoleInt >= 0) {
|
|
||||||
cfg.appColors[static_cast<AppColor::Role>(appRoleInt)] = color;
|
|
||||||
}
|
|
||||||
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!currentSection.startsWith("Palette", Qt::CaseInsensitive)) {
|
if (!currentSection.startsWith("Palette", Qt::CaseInsensitive)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -255,7 +216,11 @@ PaletteConfig PaletteConfig::fromFile(const QString &filePath)
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg.colors[currentGroup][static_cast<QPalette::ColorRole>(roleInt)] = color;
|
QColor color(value);
|
||||||
|
|
||||||
|
if (color.isValid()) {
|
||||||
|
cfg.colors[currentGroup][static_cast<QPalette::ColorRole>(roleInt)] = color;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return cfg;
|
return cfg;
|
||||||
|
|
|
||||||
|
|
@ -3,25 +3,9 @@
|
||||||
|
|
||||||
#include <QColor>
|
#include <QColor>
|
||||||
#include <QMap>
|
#include <QMap>
|
||||||
#include <QObject>
|
|
||||||
#include <QPalette>
|
#include <QPalette>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
|
|
||||||
// Application-specific color roles, layered on top of the fixed QPalette role
|
|
||||||
// set. Stored in the same palette-<scheme>.toml under an [AppColors] section
|
|
||||||
// and editable from the palette editor, so theme authors can control colors
|
|
||||||
// beyond what Qt's palette can express.
|
|
||||||
namespace AppColor
|
|
||||||
{
|
|
||||||
Q_NAMESPACE
|
|
||||||
enum Role
|
|
||||||
{
|
|
||||||
AccentStrong,
|
|
||||||
AccentSoft,
|
|
||||||
};
|
|
||||||
Q_ENUM_NS(Role)
|
|
||||||
} // namespace AppColor
|
|
||||||
|
|
||||||
struct ThemeConfig
|
struct ThemeConfig
|
||||||
{
|
{
|
||||||
QString colorScheme;
|
QString colorScheme;
|
||||||
|
|
@ -37,16 +21,7 @@ struct ThemeConfig
|
||||||
struct PaletteConfig
|
struct PaletteConfig
|
||||||
{
|
{
|
||||||
QMap<QPalette::ColorGroup, QMap<QPalette::ColorRole, QColor>> colors;
|
QMap<QPalette::ColorGroup, QMap<QPalette::ColorRole, QColor>> colors;
|
||||||
QMap<AppColor::Role, QColor> appColors;
|
|
||||||
|
|
||||||
bool operator==(const PaletteConfig &rhs) const
|
|
||||||
{
|
|
||||||
return colors == rhs.colors && appColors == rhs.appColors;
|
|
||||||
}
|
|
||||||
bool operator!=(const PaletteConfig &rhs) const
|
|
||||||
{
|
|
||||||
return !(*this == rhs);
|
|
||||||
}
|
|
||||||
bool hasPalette() const;
|
bool hasPalette() const;
|
||||||
QString toToml() const;
|
QString toToml() const;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,10 @@
|
||||||
#include "theme_manager.h"
|
#include "theme_manager.h"
|
||||||
|
|
||||||
#include "../../client/settings/cache_settings.h"
|
#include "../../client/settings/cache_settings.h"
|
||||||
#include "pixel_map_generator.h"
|
|
||||||
|
|
||||||
#include <QApplication>
|
#include <QApplication>
|
||||||
#include <QColor>
|
#include <QColor>
|
||||||
#include <QDebug>
|
#include <QDebug>
|
||||||
#include <QFile>
|
|
||||||
#include <QFileInfo>
|
|
||||||
#include <QLibraryInfo>
|
#include <QLibraryInfo>
|
||||||
#include <QMap>
|
#include <QMap>
|
||||||
#include <QMetaEnum>
|
#include <QMetaEnum>
|
||||||
|
|
@ -22,7 +19,7 @@
|
||||||
#include <Qt>
|
#include <Qt>
|
||||||
#include <libcockatrice/settings/paths_settings.h>
|
#include <libcockatrice/settings/paths_settings.h>
|
||||||
|
|
||||||
#define SYSTEM_THEME_NAME "System"
|
#define NONE_THEME_NAME "Default"
|
||||||
#define FUSION_THEME_NAME "Fusion"
|
#define FUSION_THEME_NAME "Fusion"
|
||||||
#define STYLE_CSS_NAME "style.css"
|
#define STYLE_CSS_NAME "style.css"
|
||||||
#define HANDZONE_BG_NAME "handzone"
|
#define HANDZONE_BG_NAME "handzone"
|
||||||
|
|
@ -96,7 +93,7 @@ struct PaletteColorInfo
|
||||||
static QString usableDefaultStyle(const QString &style)
|
static QString usableDefaultStyle(const QString &style)
|
||||||
{
|
{
|
||||||
// The Windows 11 native style is broken: when the OS default
|
// The Windows 11 native style is broken: when the OS default
|
||||||
// ("System" theme selection) would use it, fall back to the Vista style.
|
// ("Default" theme selection) would use it, fall back to the Vista style.
|
||||||
// Explicitly choosing "windows11" in a theme is still honored.
|
// Explicitly choosing "windows11" in a theme is still honored.
|
||||||
return style.compare("windows11", Qt::CaseInsensitive) == 0 ? QStringLiteral("windowsvista") : style;
|
return style.compare("windows11", Qt::CaseInsensitive) == 0 ? QStringLiteral("windowsvista") : style;
|
||||||
}
|
}
|
||||||
|
|
@ -119,16 +116,10 @@ ThemeManager::ThemeManager(QObject *parent) : QObject(parent)
|
||||||
|
|
||||||
void ThemeManager::ensureThemeDirectoryExists()
|
void ThemeManager::ensureThemeDirectoryExists()
|
||||||
{
|
{
|
||||||
auto &settings = SettingsCache::instance();
|
if (SettingsCache::instance().getThemeName().isEmpty() ||
|
||||||
|
!getAvailableThemes().contains(SettingsCache::instance().getThemeName())) {
|
||||||
// Migrate the old "Default" theme name to "System"
|
|
||||||
if (settings.getThemeName() == "Default") {
|
|
||||||
settings.setThemeName(SYSTEM_THEME_NAME);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (settings.getThemeName().isEmpty() || !getAvailableThemes().contains(settings.getThemeName())) {
|
|
||||||
qCInfo(ThemeManagerLog) << "Theme name not set, setting default value";
|
qCInfo(ThemeManagerLog) << "Theme name not set, setting default value";
|
||||||
settings.setThemeName(FUSION_THEME_NAME);
|
SettingsCache::instance().setThemeName(NONE_THEME_NAME);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -149,74 +140,11 @@ bool ThemeManager::isDarkMode(const QString &themeDirPath) const
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
QString ThemeManager::schemeVariantPath(QStringView prefix) const
|
bool ThemeManager::isBuiltInTheme()
|
||||||
{
|
{
|
||||||
static const QStringList formats = {QStringLiteral(".png"), QStringLiteral(".jpg"), QStringLiteral(".jpeg"),
|
const auto themeName = SettingsCache::instance().getThemeName();
|
||||||
QStringLiteral(".svg")};
|
|
||||||
const QString scheme = isDarkMode(currentThemePath) ? QStringLiteral("dark") : QStringLiteral("light");
|
|
||||||
const QString variantStem = prefix.toString() + QLatin1Char('-') + scheme;
|
|
||||||
|
|
||||||
for (const QString &format : formats) {
|
return themeName == NONE_THEME_NAME || themeName == FUSION_THEME_NAME;
|
||||||
if (QFileInfo::exists(QStringLiteral("theme:") + variantStem + format)) {
|
|
||||||
return variantStem + format;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return QString();
|
|
||||||
}
|
|
||||||
|
|
||||||
QString ThemeManager::assetPath(QStringView prefix) const
|
|
||||||
{
|
|
||||||
// Probe order mirrors tryLoadImage: a theme may override the default SVG
|
|
||||||
// with a raster of the same stem, so raster wins over SVG within a stem.
|
|
||||||
static const QStringList formats = {QStringLiteral(".png"), QStringLiteral(".jpg"), QStringLiteral(".jpeg"),
|
|
||||||
QStringLiteral(".svg")};
|
|
||||||
|
|
||||||
auto findExisting = [](const QString &stem) {
|
|
||||||
for (const QString &format : formats) {
|
|
||||||
if (QFileInfo::exists(QStringLiteral("theme:") + stem + format)) {
|
|
||||||
return stem + format;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return QString();
|
|
||||||
};
|
|
||||||
|
|
||||||
// Prefer the scheme-qualified variant when it exists, else the plain
|
|
||||||
// asset as the super fallback. Both return the resolved path including
|
|
||||||
// its file extension so callers can load it directly.
|
|
||||||
const QString variant = schemeVariantPath(prefix);
|
|
||||||
if (!variant.isEmpty()) {
|
|
||||||
return variant;
|
|
||||||
}
|
|
||||||
const QString resolvedPlain = findExisting(prefix.toString());
|
|
||||||
return resolvedPlain.isEmpty() ? prefix.toString() : resolvedPlain;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Probe whether a directory is truly writable by trying to create and remove a
|
|
||||||
// temporary file. QFileInfo::isWritable() on a directory is unreliable (notably
|
|
||||||
// on Windows where UAC VirtualStore can make a system dir appear writable).
|
|
||||||
bool ThemeManager::isDirReallyWritable(const QString &dirPath)
|
|
||||||
{
|
|
||||||
const QString probe = QDir(dirPath).absoluteFilePath(".cockatrice_write_test");
|
|
||||||
QFile f(probe);
|
|
||||||
if (!f.open(QIODevice::WriteOnly)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
f.close();
|
|
||||||
f.remove();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
QString ThemeManager::writableThemeDir(const QString &themeName)
|
|
||||||
{
|
|
||||||
// All theme writes go to the user themes directory regardless of whether
|
|
||||||
// the resolved (system) theme directory happens to be writable. Even when a
|
|
||||||
// write would succeed in-place, routing it to the user directory keeps the
|
|
||||||
// install intact and guarantees changes survive upgrades.
|
|
||||||
const QString dirPath = QDir(SettingsCache::instance().paths().getThemesPath()).absoluteFilePath(themeName);
|
|
||||||
if (!QDir().mkpath(dirPath)) {
|
|
||||||
qWarning() << "Failed to create theme save directory:" << dirPath;
|
|
||||||
}
|
|
||||||
return dirPath;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// System (read-only) themes location, relative to the application binary.
|
// System (read-only) themes location, relative to the application binary.
|
||||||
|
|
@ -241,7 +169,9 @@ QStringMap &ThemeManager::getAvailableThemes()
|
||||||
// load themes from user profile dir
|
// load themes from user profile dir
|
||||||
dir.setPath(SettingsCache::instance().paths().getThemesPath());
|
dir.setPath(SettingsCache::instance().paths().getThemesPath());
|
||||||
|
|
||||||
availableThemes.insert(SYSTEM_THEME_NAME, dir.absoluteFilePath("System"));
|
// add default value
|
||||||
|
availableThemes.insert(NONE_THEME_NAME, dir.absoluteFilePath("Default"));
|
||||||
|
|
||||||
availableThemes.insert(FUSION_THEME_NAME, dir.absoluteFilePath("Fusion"));
|
availableThemes.insert(FUSION_THEME_NAME, dir.absoluteFilePath("Fusion"));
|
||||||
|
|
||||||
for (QString themeName : dir.entryList(QDir::AllDirs | QDir::NoDotAndDotDot, QDir::Name)) {
|
for (QString themeName : dir.entryList(QDir::AllDirs | QDir::NoDotAndDotDot, QDir::Name)) {
|
||||||
|
|
@ -250,7 +180,7 @@ QStringMap &ThemeManager::getAvailableThemes()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load themes from Cockatrice system dir
|
// load themes from cockatrice system dir
|
||||||
dir.setPath(systemThemesBasePath());
|
dir.setPath(systemThemesBasePath());
|
||||||
|
|
||||||
for (QString themeName : dir.entryList(QDir::AllDirs | QDir::NoDotAndDotDot, QDir::Name)) {
|
for (QString themeName : dir.entryList(QDir::AllDirs | QDir::NoDotAndDotDot, QDir::Name)) {
|
||||||
|
|
@ -265,7 +195,7 @@ QStringMap &ThemeManager::getAvailableThemes()
|
||||||
QBrush ThemeManager::loadBrush(QString fileName, QColor fallbackColor)
|
QBrush ThemeManager::loadBrush(QString fileName, QColor fallbackColor)
|
||||||
{
|
{
|
||||||
QBrush brush;
|
QBrush brush;
|
||||||
QPixmap tmp = QPixmap("theme:" + assetPath(QStringLiteral("zones/") + fileName));
|
QPixmap tmp = QPixmap("theme:zones/" + fileName);
|
||||||
if (tmp.isNull()) {
|
if (tmp.isNull()) {
|
||||||
brush.setColor(fallbackColor);
|
brush.setColor(fallbackColor);
|
||||||
brush.setStyle(Qt::SolidPattern);
|
brush.setStyle(Qt::SolidPattern);
|
||||||
|
|
@ -279,7 +209,7 @@ QBrush ThemeManager::loadBrush(QString fileName, QColor fallbackColor)
|
||||||
QBrush ThemeManager::loadExtraBrush(QString fileName, QBrush &fallbackBrush)
|
QBrush ThemeManager::loadExtraBrush(QString fileName, QBrush &fallbackBrush)
|
||||||
{
|
{
|
||||||
QBrush brush;
|
QBrush brush;
|
||||||
QPixmap tmp = QPixmap("theme:" + assetPath(QStringLiteral("zones/") + fileName));
|
QPixmap tmp = QPixmap("theme:zones/" + fileName);
|
||||||
|
|
||||||
if (tmp.isNull()) {
|
if (tmp.isNull()) {
|
||||||
brush = fallbackBrush;
|
brush = fallbackBrush;
|
||||||
|
|
@ -357,7 +287,7 @@ bool ThemeManager::commitPalette(const QString &themeDirPath, const QString &col
|
||||||
|
|
||||||
void ThemeManager::setColorScheme(const QString &scheme)
|
void ThemeManager::setColorScheme(const QString &scheme)
|
||||||
{
|
{
|
||||||
const QString dirPath = writableThemeDir(SettingsCache::instance().getThemeName());
|
const QString dirPath = getAvailableThemes().value(SettingsCache::instance().getThemeName());
|
||||||
ThemeConfig cfg = ThemeConfig::fromThemeDir(dirPath);
|
ThemeConfig cfg = ThemeConfig::fromThemeDir(dirPath);
|
||||||
|
|
||||||
cfg.colorScheme = scheme;
|
cfg.colorScheme = scheme;
|
||||||
|
|
@ -368,7 +298,7 @@ void ThemeManager::setColorScheme(const QString &scheme)
|
||||||
|
|
||||||
void ThemeManager::setStyleName(const QString &styleName)
|
void ThemeManager::setStyleName(const QString &styleName)
|
||||||
{
|
{
|
||||||
const QString dirPath = writableThemeDir(SettingsCache::instance().getThemeName());
|
const QString dirPath = getAvailableThemes().value(SettingsCache::instance().getThemeName());
|
||||||
ThemeConfig cfg = ThemeConfig::fromThemeDir(dirPath);
|
ThemeConfig cfg = ThemeConfig::fromThemeDir(dirPath);
|
||||||
|
|
||||||
cfg.styleName = styleName;
|
cfg.styleName = styleName;
|
||||||
|
|
@ -399,7 +329,7 @@ void ThemeManager::applyStyleAndPalette(const QString &themeName,
|
||||||
Q_UNUSED(activeScheme)
|
Q_UNUSED(activeScheme)
|
||||||
#endif
|
#endif
|
||||||
QString styleName = themeCfg.styleName;
|
QString styleName = themeCfg.styleName;
|
||||||
if (styleName.isEmpty() || styleName.compare("System", Qt::CaseInsensitive) == 0) {
|
if (styleName.isEmpty() || styleName.compare("Default", Qt::CaseInsensitive) == 0) {
|
||||||
if (themeName == FUSION_THEME_NAME) {
|
if (themeName == FUSION_THEME_NAME) {
|
||||||
styleName = "Fusion";
|
styleName = "Fusion";
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -442,8 +372,6 @@ void ThemeManager::applyStyleAndPalette(const QString &themeName,
|
||||||
qApp->setPalette(base);
|
qApp->setPalette(base);
|
||||||
qApp->setStyle(style);
|
qApp->setStyle(style);
|
||||||
|
|
||||||
currentAppColors = palCfg.appColors;
|
|
||||||
|
|
||||||
// Force every widget to re-polish and repaint immediately rather than
|
// Force every widget to re-polish and repaint immediately rather than
|
||||||
// waiting for natural expose events, which produces a patchwork of old
|
// waiting for natural expose events, which produces a patchwork of old
|
||||||
// and new colours during a live preview.
|
// and new colours during a live preview.
|
||||||
|
|
@ -456,35 +384,6 @@ void ThemeManager::applyStyleAndPalette(const QString &themeName,
|
||||||
style->polish(widget);
|
style->polish(widget);
|
||||||
widget->update();
|
widget->update();
|
||||||
}
|
}
|
||||||
|
|
||||||
emit paletteChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
QColor ThemeManager::appColor(AppColor::Role role) const
|
|
||||||
{
|
|
||||||
const auto it = currentAppColors.constFind(role);
|
|
||||||
if (it != currentAppColors.constEnd()) {
|
|
||||||
return it.value();
|
|
||||||
}
|
|
||||||
|
|
||||||
// QPalette::Accent was introduced in Qt 6.6 and several shipped palettes
|
|
||||||
// set it to a value barely distinguishable from Window, so it is not a
|
|
||||||
// reliable accent source. The selection highlight is the stable accent
|
|
||||||
// (Accent defaults to Highlight when unset), and deriving from it
|
|
||||||
// unconditionally keeps every Qt version rendering identically.
|
|
||||||
const QColor accent = qApp->palette().color(QPalette::Active, QPalette::Highlight);
|
|
||||||
|
|
||||||
if (role == AppColor::AccentSoft) {
|
|
||||||
constexpr int SOFT_SATURATION_PERCENT = 70;
|
|
||||||
constexpr int SOFT_LIGHTNESS_OFFSET = 60;
|
|
||||||
|
|
||||||
// Light end of the gradient: same hue, softened and lightened
|
|
||||||
return QColor::fromHsl(qMax(0, accent.hslHue()),
|
|
||||||
qBound(0, qRound(accent.hslSaturation() * SOFT_SATURATION_PERCENT / 100.0), 255),
|
|
||||||
qBound(0, accent.lightness() + SOFT_LIGHTNESS_OFFSET, 255));
|
|
||||||
}
|
|
||||||
|
|
||||||
return accent;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void ThemeManager::themeChangedSlot()
|
void ThemeManager::themeChangedSlot()
|
||||||
|
|
@ -494,19 +393,9 @@ void ThemeManager::themeChangedSlot()
|
||||||
currentThemePath = dirPath;
|
currentThemePath = dirPath;
|
||||||
QDir dir(dirPath);
|
QDir dir(dirPath);
|
||||||
|
|
||||||
// CSS — prefer the scheme-qualified stylesheet (style-dark.css /
|
// CSS
|
||||||
// style-light.css) when present, else the plain style.css as fallback.
|
if (!dirPath.isEmpty() && dir.exists(STYLE_CSS_NAME)) {
|
||||||
if (!dirPath.isEmpty()) {
|
qApp->setStyleSheet("file:///" + dir.absoluteFilePath(STYLE_CSS_NAME));
|
||||||
const QString scheme = isDarkMode(dirPath) ? QStringLiteral("dark") : QStringLiteral("light");
|
|
||||||
const QString schemeCss = QFileInfo(QStringLiteral(STYLE_CSS_NAME)).completeBaseName() + QLatin1Char('-') +
|
|
||||||
scheme + QStringLiteral(".css");
|
|
||||||
if (dir.exists(schemeCss)) {
|
|
||||||
qApp->setStyleSheet("file:///" + dir.absoluteFilePath(schemeCss));
|
|
||||||
} else if (dir.exists(STYLE_CSS_NAME)) {
|
|
||||||
qApp->setStyleSheet("file:///" + dir.absoluteFilePath(STYLE_CSS_NAME));
|
|
||||||
} else {
|
|
||||||
qApp->setStyleSheet("");
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
qApp->setStyleSheet("");
|
qApp->setStyleSheet("");
|
||||||
}
|
}
|
||||||
|
|
@ -521,19 +410,8 @@ void ThemeManager::themeChangedSlot()
|
||||||
|
|
||||||
// ── Load palette: custom first, then theme default ────────────────────
|
// ── Load palette: custom first, then theme default ────────────────────
|
||||||
PaletteConfig palette = PaletteConfig::fromScheme(dirPath, activeScheme);
|
PaletteConfig palette = PaletteConfig::fromScheme(dirPath, activeScheme);
|
||||||
const PaletteConfig themeDefault = ThemeManager::loadDefaultPaletteConfig(dirPath, themeName, activeScheme);
|
if (!palette.hasPalette()) {
|
||||||
if (palette.hasPalette()) {
|
palette = ThemeManager::loadDefaultPaletteConfig(dirPath, themeName, activeScheme);
|
||||||
// A custom palette written before [AppColors] existed carries no app
|
|
||||||
// colors; merge the theme's shipped defaults so the identity colors
|
|
||||||
// survive (hasPalette() counts an app-colors-only file as a palette,
|
|
||||||
// so those are kept wholesale and never reach here empty).
|
|
||||||
for (auto it = themeDefault.appColors.cbegin(); it != themeDefault.appColors.cend(); ++it) {
|
|
||||||
if (!palette.appColors.contains(it.key())) {
|
|
||||||
palette.appColors.insert(it.key(), it.value());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
palette = themeDefault;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
applyStyleAndPalette(themeName, themeCfg, palette, activeScheme);
|
applyStyleAndPalette(themeName, themeCfg, palette, activeScheme);
|
||||||
|
|
@ -568,7 +446,6 @@ void ThemeManager::themeChangedSlot()
|
||||||
}
|
}
|
||||||
|
|
||||||
QPixmapCache::clear();
|
QPixmapCache::clear();
|
||||||
clearPixmapGeneratorCaches();
|
|
||||||
|
|
||||||
emit themeChanged();
|
emit themeChanged();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,6 @@ private:
|
||||||
QString currentThemePath;
|
QString currentThemePath;
|
||||||
std::array<QBrush, Role::MaxRole + 1> brushes;
|
std::array<QBrush, Role::MaxRole + 1> brushes;
|
||||||
QStringMap availableThemes;
|
QStringMap availableThemes;
|
||||||
QMap<AppColor::Role, QColor> currentAppColors;
|
|
||||||
/*
|
/*
|
||||||
Internal cache for multiple backgrounds
|
Internal cache for multiple backgrounds
|
||||||
*/
|
*/
|
||||||
|
|
@ -66,16 +65,7 @@ protected:
|
||||||
const QString &activeScheme);
|
const QString &activeScheme);
|
||||||
|
|
||||||
public:
|
public:
|
||||||
// Resolves the directory to write theme changes to for the given theme
|
bool isBuiltInTheme();
|
||||||
// name. The resolved theme dir (user or system) is used when writable;
|
|
||||||
// read-only system themes fall back to the user themes directory, creating
|
|
||||||
// it if needed, so customisations never get lost on upgrade.
|
|
||||||
static QString writableThemeDir(const QString &themeName);
|
|
||||||
// Probe whether a directory is truly writable by trying to create and remove
|
|
||||||
// a temporary file. QFileInfo::isWritable() on a directory is unreliable
|
|
||||||
// (notably on Windows where UAC VirtualStore can make a system dir appear
|
|
||||||
// writable).
|
|
||||||
static bool isDirReallyWritable(const QString &dirPath);
|
|
||||||
// Explicit color scheme of the theme: theme.cfg's ColorScheme setting
|
// Explicit color scheme of the theme: theme.cfg's ColorScheme setting
|
||||||
// (Dark/Light), falling back to the OS color scheme when it is "System".
|
// (Dark/Light), falling back to the OS color scheme when it is "System".
|
||||||
bool isDarkMode(const QString &themeDirPath) const;
|
bool isDarkMode(const QString &themeDirPath) const;
|
||||||
|
|
@ -97,20 +87,6 @@ public:
|
||||||
// Load/save per-scheme palette colors
|
// Load/save per-scheme palette colors
|
||||||
static PaletteConfig loadPaletteConfig(const QString &themeDirPath, const QString &colorScheme);
|
static PaletteConfig loadPaletteConfig(const QString &themeDirPath, const QString &colorScheme);
|
||||||
static bool savePaletteConfig(const QString &themeDirPath, const QString &colorScheme, const PaletteConfig &cfg);
|
static bool savePaletteConfig(const QString &themeDirPath, const QString &colorScheme, const PaletteConfig &cfg);
|
||||||
// Resolve prefix to a scheme-qualified "theme:" path. Existence is probed
|
|
||||||
// internally across the formats themes may ship (.png/.jpg/.svg), so
|
|
||||||
// callers load the returned path directly. Prefers "<prefix>-<dark|light>"
|
|
||||||
// when a file exists at that stem, otherwise the plain "<prefix>" as the
|
|
||||||
// super fallback. The resolved scheme covers explicit light/dark as well
|
|
||||||
// as OS-resolved "system". Returns the path with its file extension when a
|
|
||||||
// match is found; unqualified assets keep working unchanged.
|
|
||||||
QString assetPath(QStringView prefix) const;
|
|
||||||
// Like assetPath, but resolves only the scheme-qualified variant
|
|
||||||
// ("<prefix>-<dark|light>.<ext>") and returns an empty string when no
|
|
||||||
// variant exists — it never falls back to the plain "<prefix>" asset.
|
|
||||||
// Callers that must distinguish "no authored variant" (e.g. to keep a
|
|
||||||
// legacy runtime fallback alive) should use this instead of assetPath.
|
|
||||||
QString schemeVariantPath(QStringView prefix) const;
|
|
||||||
// Load the theme's shipped default palette, falling back to the system
|
// Load the theme's shipped default palette, falling back to the system
|
||||||
// theme directory when it is absent from the resolved (user) directory.
|
// theme directory when it is absent from the resolved (user) directory.
|
||||||
static PaletteConfig
|
static PaletteConfig
|
||||||
|
|
@ -125,17 +101,12 @@ public:
|
||||||
void reloadCurrentTheme();
|
void reloadCurrentTheme();
|
||||||
void previewPalette(const PaletteConfig &cfg, const QString &scheme);
|
void previewPalette(const PaletteConfig &cfg, const QString &scheme);
|
||||||
|
|
||||||
// Resolves an application color role: the theme's stored [AppColors] value
|
|
||||||
// when present, otherwise a palette-accent-derived fallback.
|
|
||||||
QColor appColor(AppColor::Role role) const;
|
|
||||||
|
|
||||||
QBrush &getBgBrush(Role zone);
|
QBrush &getBgBrush(Role zone);
|
||||||
QBrush getExtraBgBrush(Role zone, int zoneId = 0);
|
QBrush getExtraBgBrush(Role zone, int zoneId = 0);
|
||||||
protected slots:
|
protected slots:
|
||||||
void themeChangedSlot();
|
void themeChangedSlot();
|
||||||
signals:
|
signals:
|
||||||
void themeChanged();
|
void themeChanged();
|
||||||
void paletteChanged();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
extern ThemeManager *themeManager;
|
extern ThemeManager *themeManager;
|
||||||
|
|
|
||||||
|
|
@ -85,7 +85,7 @@ void ColorIdentityWidget::resizeEvent(QResizeEvent *event)
|
||||||
}
|
}
|
||||||
lastWidth = totalWidth;
|
lastWidth = totalWidth;
|
||||||
|
|
||||||
const int totalHeight = qMax(0, totalWidth / 6); // Set height to 1/4 of the width
|
const int totalHeight = totalWidth / 6; // Set height to 1/4 of the width
|
||||||
setFixedHeight(totalHeight);
|
setFixedHeight(totalHeight);
|
||||||
|
|
||||||
const int count = layout->count();
|
const int count = layout->count();
|
||||||
|
|
@ -97,10 +97,6 @@ void ColorIdentityWidget::resizeEvent(QResizeEvent *event)
|
||||||
const int availableWidth = totalWidth - (spacing * (count - 1));
|
const int availableWidth = totalWidth - (spacing * (count - 1));
|
||||||
const int iconSize = qMin(availableWidth / count, totalHeight); // Ensure icons fit within the new height
|
const int iconSize = qMin(availableWidth / count, totalHeight); // Ensure icons fit within the new height
|
||||||
|
|
||||||
if (iconSize <= 0) {
|
|
||||||
lastIconSize = iconSize;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (iconSize == lastIconSize) {
|
if (iconSize == lastIconSize) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,37 +0,0 @@
|
||||||
#include "deck_color_identity.h"
|
|
||||||
|
|
||||||
#include <QSet>
|
|
||||||
#include <libcockatrice/card/database/card_database_querier.h>
|
|
||||||
#include <libcockatrice/deck_list/deck_list.h>
|
|
||||||
#include <libcockatrice/deck_list/tree/inner_deck_list_node.h>
|
|
||||||
|
|
||||||
QString getDeckColorIdentity(const DeckList &deck, const CardDatabaseQuerier *db)
|
|
||||||
{
|
|
||||||
const QStringList cardList = deck.getCardList({DECK_ZONE_MAIN, DECK_ZONE_SIDE});
|
|
||||||
if (cardList.isEmpty()) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
QSet<QChar> colorSet; // A set to collect unique color symbols (e.g., W, U, B, R, G)
|
|
||||||
|
|
||||||
for (const QString &cardName : cardList) {
|
|
||||||
CardInfoPtr currentCard = db->getCardInfo(cardName);
|
|
||||||
if (currentCard) {
|
|
||||||
const QString colors = currentCard->getColors(); // returns something like "WUB"
|
|
||||||
for (const QChar &color : colors) {
|
|
||||||
colorSet.insert(color);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure the color identity is in WUBRG order
|
|
||||||
QString colorIdentity;
|
|
||||||
const QString wubrgOrder = "WUBRG";
|
|
||||||
for (const QChar &color : wubrgOrder) {
|
|
||||||
if (colorSet.contains(color)) {
|
|
||||||
colorIdentity.append(color);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return colorIdentity;
|
|
||||||
}
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
#ifndef COCKATRICE_DECK_COLOR_IDENTITY_H
|
|
||||||
#define COCKATRICE_DECK_COLOR_IDENTITY_H
|
|
||||||
|
|
||||||
#include <QString>
|
|
||||||
|
|
||||||
class CardDatabaseQuerier;
|
|
||||||
class DeckList;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Computes the color identity of a deck (e.g. "WUBRG") from the color
|
|
||||||
* symbols of all cards in the main deck and sideboard, ordered WUBRG.
|
|
||||||
*
|
|
||||||
* Shared as a free function so the deck storage previews and the deck share
|
|
||||||
* dialog compute identities identically.
|
|
||||||
*
|
|
||||||
* @param db Card database used to look up card color symbols.
|
|
||||||
*/
|
|
||||||
QString getDeckColorIdentity(const DeckList &deck, const CardDatabaseQuerier *db);
|
|
||||||
|
|
||||||
#endif // COCKATRICE_DECK_COLOR_IDENTITY_H
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
#include "card_art_utils.h"
|
|
||||||
|
|
||||||
#include <QTransform>
|
|
||||||
#include <libcockatrice/card/printing/exact_card.h>
|
|
||||||
|
|
||||||
namespace CardArtUtils
|
|
||||||
{
|
|
||||||
QPixmap rotateSidewaysLayoutArt(const QPixmap &art, const ExactCard &card)
|
|
||||||
{
|
|
||||||
if (!card.getInfo().getUiAttributes().landscapeOrientation) {
|
|
||||||
return art;
|
|
||||||
}
|
|
||||||
|
|
||||||
QTransform transform;
|
|
||||||
transform.rotate(90);
|
|
||||||
return art.transformed(transform, Qt::SmoothTransformation);
|
|
||||||
}
|
|
||||||
} // namespace CardArtUtils
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue