diff --git a/.ci/Fedora45/Dockerfile b/.ci/Fedora43/Dockerfile similarity index 96% rename from .ci/Fedora45/Dockerfile rename to .ci/Fedora43/Dockerfile index a7c49b249..8aa345651 100644 --- a/.ci/Fedora45/Dockerfile +++ b/.ci/Fedora43/Dockerfile @@ -1,4 +1,4 @@ -FROM fedora:45 +FROM fedora:43 RUN dnf install -y \ ca-certificates \ diff --git a/.ci/compile.sh b/.ci/compile.sh index 7414b2dc3..9be8ba741 100755 --- a/.ci/compile.sh +++ b/.ci/compile.sh @@ -175,9 +175,9 @@ fi if [[ $MAKE_SANITIZE ]]; then flags+=("-DENABLE_SANITIZERS=ON") else - # Every leg states the flag explicitly rather than relying on the CMake - # default, so a build directory reused from a sanitized configuration can - # never leak libasan/libubsan into a shipped binary. + # The release-package step re-runs cmake in the same build dir, so an + # ENABLE_SANITIZERS=ON left over from the debug step would otherwise leak + # into the shipped binaries (libasan/libubsan Depends in the DEB). flags+=("-DENABLE_SANITIZERS=OFF") fi if [[ $MAKE_COVERAGE ]]; then @@ -185,6 +185,9 @@ if [[ $MAKE_COVERAGE ]]; then fi if [[ $USE_CCACHE ]]; then 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 # note, this setting persists after running the script ccache --max-size "$CCACHE_SIZE" @@ -203,11 +206,6 @@ if [[ ${WITH_CRASH_REPORTING:-} == "ON" ]]; then # developer builds never reach Sentry. flags+=("-DWITH_CRASH_REPORTING=ON") fi -if [[ ${COCKATRICE_DEBUG_INFO_LEVEL:-} ]]; then - # CI lowers the DWARF level so one build configuration fits its compiler-cache bound; - # see the option in CMakeLists.txt. Empty keeps the built-in flags. - flags+=("-DCOCKATRICE_DEBUG_INFO_LEVEL=$COCKATRICE_DEBUG_INFO_LEVEL") -fi if [[ ${BUILDTYPE:-Release} == "Release" && -n ${COCKATRICE_SENTRY_DSN:-} ]]; then # Release-only: Debug builds stay DSN-free so crashes from developer builds # never reach Sentry. Empty (local builds, forks without the secret, and non-tag @@ -380,21 +378,6 @@ echo "Running cmake with flags: ${flags[*]}" cmake .. "${flags[@]}" echo "::endgroup::" -if [[ $USE_CCACHE ]]; then - # PCH-aware caching is required or ccache refuses to cache any TU that consumes a - # precompiled header, silently recompiling everything on every run. The mtime and ctime - # entries cover files created during the build itself: the moc/uic/rcc output and the - # protobuf headers are generated after ccache starts, so without them every TU including - # one is read as a race and never cached. Checked-out sources are older than the compile - # that follows them and do not trip the check. - # - # This has to run after configure: CMakeLists.txt sets sloppiness=pch_defines,time_macros - # at configure time, and --set-config replaces the whole list rather than extending it, so - # setting it earlier would leave the build below with two entries. Re-asserting all four - # here is what actually reaches the compiler; the order is the point. - ccache --set-config sloppiness=pch_defines,time_macros,include_file_mtime,include_file_ctime -fi - echo "::group::Build project" cpuinfo echo "Running cmake --build with flags: ${buildflags[*]}" diff --git a/.ci/docker.sh b/.ci/docker.sh index c78ba3c31..64ea6baa7 100644 --- a/.ci/docker.sh +++ b/.ci/docker.sh @@ -181,11 +181,6 @@ function RUN () if [[ ${WITH_CRASH_REPORTING:-} ]]; then args+=(--env "WITH_CRASH_REPORTING=${WITH_CRASH_REPORTING}") fi - # Same reason: the DWARF level reaches compile.sh only if it is handed to the container, - # and without it CI would cache objects twice the size its bounds were measured for. - if [[ ${COCKATRICE_DEBUG_INFO_LEVEL:-} ]]; then - args+=(--env "COCKATRICE_DEBUG_INFO_LEVEL=${COCKATRICE_DEBUG_INFO_LEVEL}") - fi # shellcheck disable=2086 docker run "${args[@]}" $RUN_ARGS "$IMAGE_NAME" bash "$BUILD_SCRIPT" $RUN_OPTS "$@" return $? diff --git a/.ci/release_template.md b/.ci/release_template.md index 8eb4c7ebd..23b475150 100644 --- a/.ci/release_template.md +++ b/.ci/release_template.md @@ -21,8 +21,8 @@ Available pre-compiled binaries for installation: • Ubuntu 24.04 LTS Noble Numbat • Debian 13 Trixie • Debian 12 Bookworm - • Fedora 45 • Fedora 44 + • Fedora 43 General Linux support is available via a flatpak package hosted at Flathub (x86 & ARM)! Thanks to courtesy of @FFY00, the app is also available in Arch Linux's official extra repository. diff --git a/.github/workflows/desktop-build.yml b/.github/workflows/desktop-build.yml index b859556aa..df054ea26 100644 --- a/.github/workflows/desktop-build.yml +++ b/.github/workflows/desktop-build.yml @@ -33,18 +33,10 @@ on: - 'CMakeLists.txt' - 'vcpkg.json' - 'vcpkg' # needed to match submodule bumps (gitlink) - # The legs the pull-request matrix leaves out run here instead, so the legacy distros and - # the slow macOS runner stay covered without gating a pull request on them. Tags still - # build every leg, because a tag is what ships. - schedule: - - cron: '23 3 * * *' # Cancel earlier, unfinished runs of this workflow on the same branch (unless on tag --> release) concurrency: - # The event name is part of the group because a nightly run and a push run on master share - # refs/heads/master but must not cancel each other: the nightly would drop the push's vcpkg - # import, and a merge landing during the nightly would drop that night's coverage. - group: "${{ github.workflow }} @ ${{ github.ref_name }} (${{ github.event_name }})" + group: "${{ github.workflow }} @ ${{ github.ref_name }}" cancel-in-progress: ${{ github.ref_type != 'tag' }} jobs: @@ -149,8 +141,6 @@ jobs: allow-failure: yes package: skip # We are packaged in Arch already - debug_test: yes # see the note on Fedora 45 below - ccache_size: 850M # Debug plus the suite: 660 MB of objects at -g1 - distro: Servatrice_Debian version: 12 @@ -158,145 +148,77 @@ jobs: package: DEB server_only: yes test: skip - # 4.7 min and the server alone: nothing here for a compiler cache to save. - persist_ccache: 'false' + + - distro: Debian + version: 12 + + package: DEB + test: skip # Running tests on all distros is superfluous - distro: Debian version: 13 package: DEB - ccache_size: 800M # Release plus the suite: 614 MB of objects at -g1 - - # The single Fedora leg used to build Debug for the suite and Release for the - # package in one job (measured on Fedora 44: 30.8 min, the only leg slower than - # Windows), so the package compile waited behind the Debug one. Two legs run in - # parallel and each keeps a cache that holds exactly one configuration. - # - # Arch and Fedora 45 are the two newest toolchains, and the -Wno-error= exceptions - # in CMakeLists.txt exist for exactly their warnings. This leg compiles Debug - # because only Debug passes -Werror, so it is where a new warning still fails. - - distro: Fedora - version: 45 - variant: debug # separate image and ccache from the release leg below - - package: skip - debug_test: yes - ccache_size: 850M # Debug plus the suite: 660 MB of objects at -g1 - distro: Fedora - version: 45 - variant: release # separate image and ccache from the debug leg above + version: 43 package: RPM - test: skip # The debug leg above runs this distro's suite - ccache_size: 800M # Release plus the package: 614 MB of objects at -g1 + test: skip # Running tests on all distros is superfluous + + - distro: Fedora + version: 44 + + package: RPM + + - distro: Ubuntu + version: 24.04 + + package: DEB + test: skip # Running tests on all distros is superfluous - distro: Ubuntu version: 26.04 package: DEB - test: skip # The sanitizer leg below runs this distro's suite - # 18.1 min, and the same distro as the sanitize leg below, which runs this suite. - persist_ccache: 'false' - # The one leg that runs the suite under AddressSanitizer and - # UndefinedBehaviorSanitizer. Debian 13 and Fedora 45 keep covering the - # uninstrumented suite, so nothing is lost by instrumenting this one. - # - # It packages nothing. A sanitized Debug build is not something to ship, and - # carrying the package build here meant 18 minutes of Release compile queued - # behind the instrumented one in a single job. The leg above publishes this - # distro's DEB. + # The one leg that runs the suite under AddressSanitizer and + # UndefinedBehaviorSanitizer. Debian 13 and Fedora 44 keep covering the + # uninstrumented suite, so nothing is lost by instrumenting this one. + sanitize: yes + - distro: Ubuntu version: 26.04 - variant: sanitize # separate image and ccache, same distro + variant: cover # separate image and ccache, same distro + # The one leg that runs instrumented for line coverage and publishes the report as + # a workflow artifact. It is a separate leg rather than a second flag on the + # sanitizer leg because the two instruments answer different questions: the + # sanitizer leg reports what the run did wrong, the coverage leg reports what the + # run never touched at all. Combining them would also slow both down. Packaging is + # skipped because the leg above already publishes the DEB for this distro. + coverage: yes package: skip - sanitize: yes - ccache_size: 1200M # instrumented objects are larger than the 614 MB measured; estimated name: ${{ matrix.distro }} ${{ matrix.version }}${{ matrix.variant && format(' {0}', matrix.variant) || '' }} needs: configure - # The nightly legs live in build-linux-nightly below. A leg is in exactly one of the two - # matrices, so nothing that reviews a change waits for a distro nobody is changing for. - if: github.event_name != 'schedule' runs-on: ubuntu-latest # https://github.com/actions/runner-images continue-on-error: ${{ matrix.allow-failure == 'yes' }} timeout-minutes: 70 env: CACHE: ${{ github.workspace }}/.cache/${{ matrix.distro }}${{ matrix.version }}${{ matrix.variant }} # directory for caching docker image and ccache CCACHE_EVICTION_AGE: 7d - # The bound is sized against the repository's 10 GB cache quota - # (https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#usage-limits-and-eviction-policy), - # not against what one leg would like: the quota also holds about 0.9 GB of buildkit - # layers (mode=min in docker-release.yml), 0.8 GB of vcpkg, 0.7 GB of Qt and the CodeQL - # trap caches (about 1 GB each, which prune-caches below does not manage). - # - # Measured 2026-11-04: one configuration costs 1.2 GB with full DWARF and 660 MB with - # -g1 (COCKATRICE_DEBUG_INFO_LEVEL below), and the size of the cache decides the hit - # rate far more sharply than its fill suggests. A 6G cache served 1059 of 1059 calls; - # the 700M bound on the same leg served 14%, because ccache fills to the limit and then - # evicts the entries the next build is about to ask for. A cache that cannot hold a - # whole configuration therefore buys almost nothing, and the quota goes to fewer legs - # at a size that holds one: - # - # Arch Debug + suite 660 MB working set 850M - # Debian 13 Release + suite 614 MB 800M - # Fedora 45 debug Debug + suite 660 MB 850M - # Fedora 45 release Release + package 614 MB 800M - # Ubuntu 26.04 sanitize ~1.0 GB 1200M - # - # That is about 5.3 GB of bounds, and with the fixed entries above it puts the quota at - # roughly 9.8 GB of 10 - so only legs worth their share get one. A leg keeps no cache - # when it duplicates another leg's coverage (Ubuntu 26.04 behind the sanitize leg of - # the same distro; macOS 15 and macOS 27 Debug behind macOS 14; Servatrice covers the - # server alone and finishes in 4.7 min). Those four run cold: 4.7, 18.1, 16.5 and - # 15.8 min. The five that stay compile the client on the toolchains a change can break - # and are the ones a cache shortens by the most. Measured job totals on master - # 2026-11-04 also show the ceiling this leaves: Windows 10 is 29.5 min and compiles for - # 23.7 of it with no compiler cache at all, so Windows still sets the pull-request - # wall clock no matter how warm these legs are (see build-vcpkg). - # - # Re-add a leg only with a bound that holds its whole configuration: a smaller one is - # worse than none. - # - # Every key listed in LIVE_CCACHE_KEYS in prune-caches below must match this matrix. - CCACHE_SIZE: ${{ matrix.ccache_size || '700M' }} - # Debug information dominates a cached object: with the built-in flags one - # configuration is 1.2 GB and no per-leg bound can hold it, so CI lowers the level to - # 1 (file and line, which is what a failing test or a sanitizer backtrace reports) and - # one configuration halves. Empty on tags and on the nightly legs, which ship binaries - # and keep full DWARF for the Sentry symbols; a tag restores a cache it cannot use and - # is slower, which is accepted because a tag is rare and only pull requests gate. - COCKATRICE_DEBUG_INFO_LEVEL: ${{ github.ref_type != 'tag' && '1' || '' }} + CCACHE_SIZE: 600M # space of all repo is 10Gi: https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#usage-limits-and-eviction-policy CMAKE_GENERATOR: 'Ninja' NAME: ${{ matrix.distro }}${{ matrix.version }} IMAGE_SUFFIX: ${{ matrix.variant }} - # Which build the suite runs in, decided once per leg here so the steps' if: conditions - # and their shells cannot drift apart. A step's own env is not visible to its if:, which - # is why these live at job level rather than on the release step. - # DEBUG_TEST the instrumented legs (symbols and instrumentation matter) and the two - # newest distro toolchains, which only fail on a warning in Debug. - # RUN_TESTS every other leg that tests runs the suite against the release build it may - # also package, so testing and packaging cost one compile instead of two. - DEBUG_TEST: ${{ matrix.sanitize == 'yes' || matrix.coverage == 'yes' || matrix.debug_test == 'yes' }} - RUN_TESTS: ${{ matrix.test != 'skip' && !(matrix.sanitize == 'yes' || matrix.coverage == 'yes' || matrix.debug_test == 'yes') }} - # Whether this leg restores and saves its compiler cache. Not every pull-request leg - # keeps one: a cache only pays for itself when its bound holds a whole configuration - # (see the sizing comment above), and a leg that duplicates another leg's coverage - # spends quota without adding any. The nightly-only legs keep none either. Fedora 45 - # states it per leg: its two configurations have their own job and key, so neither - # bound is shared with a build whose objects it would evict. - PERSIST_CCACHE: ${{ matrix.persist_ccache || 'true' }} - # Shared with build-linux-nightly so the two leg sets run the same steps. - steps: &linux-steps + steps: - name: "Checkout" uses: actions/checkout@v7 - name: "Restore compiler cache (ccache)" id: restore_ccache - if: env.PERSIST_CCACHE == 'true' uses: actions/cache/restore@v6 with: key: ccache-${{ matrix.distro }}${{ matrix.version }}${{ matrix.variant }} @@ -306,11 +228,8 @@ jobs: shell: bash run: source .ci/docker.sh --build - # The instrumented legs and the two newest distro toolchains build and test Debug; every - # other leg runs its suite against the release build below, so testing and packaging cost - # one compile instead of two. - name: "Build debug and test" - if: env.DEBUG_TEST == 'true' + if: matrix.test != 'skip' shell: bash env: sanitize: '${{ matrix.sanitize }}' @@ -350,23 +269,15 @@ jobs: path: coverage/ if-no-files-found: warn - # One build serves both purposes: the suite runs against the same configuration that - # gets packaged, so a packaging leg no longer pays for a full Debug compile it then - # throws away. macOS and Windows have always tested their release build. - # - # Instrumented and debug-test legs reach this step only when they also package; their - # suite already ran in the debug step above, so the tests are not repeated here. - - name: "Build release, test and package" + - name: "Build release package" id: build - if: matrix.package != 'skip' || env.RUN_TESTS == 'true' + if: matrix.package != 'skip' shell: bash env: SUFFIX: '-${{ matrix.distro }}${{ matrix.version }}' package: '${{ matrix.package }}' server_only: '${{ matrix.server_only }}' - # Only a leg that packages builds the shipping configuration; a release build that - # only runs tests stays free of the crash reporting SDK. - WITH_CRASH_REPORTING: ${{ matrix.package != 'skip' && 'ON' || '' }} + WITH_CRASH_REPORTING: 'ON' # Tag builds only: artifacts from master pushes and PR branches must not report to # Sentry. Forks get no secrets on pull_request, so they keep the mock fallback either way. # configure.outputs.tag is set for ANY tag, so beta (Development) tags get the DSN @@ -376,7 +287,6 @@ jobs: source .ci/docker.sh args=() [[ $server_only == yes ]] && args+=(--no-client) - [[ $RUN_TESTS == true ]] && args+=(--test) [[ $GITHUB_REF == "refs/heads/master" ]] && args+=(--evict-ccache "$CCACHE_EVICTION_AGE") args+=(--ccache "$CCACHE_SIZE") args+=(--cmake-generator "$CMAKE_GENERATOR") @@ -386,14 +296,7 @@ jobs: # configures, and empty keeps the mock fallback. export RUN_ARGS="${RUN_ARGS:-} -e COCKATRICE_SENTRY_DSN" - # --package takes an optional type, so the flag and its argument have to travel - # together or the type would become the next flag. - package_args=() - [[ -n $package && $package != skip ]] && package_args+=(--package "$package") - - # compile.sh runs the tests before the package target, so a failing suite stops the - # leg before anything is built into an installer. - RUN --server --release "${package_args[@]}" "${args[@]}" + RUN --server --release --package "$package" "${args[@]}" - name: "Install sentry-cli" if: matrix.package != 'skip' && needs.configure.outputs.tag != null @@ -442,7 +345,7 @@ jobs: # Delete used cache to emulate a ccache update. See https://github.com/actions/cache/issues/342 - name: "Delete remote compiler cache (ccache)" - if: github.ref == 'refs/heads/master' && env.PERSIST_CCACHE == 'true' && steps.restore_ccache.outputs.cache-hit + if: github.ref == 'refs/heads/master' && steps.restore_ccache.outputs.cache-hit continue-on-error: true env: CACHE_PRIMARY_KEY: ${{ steps.restore_ccache.outputs.cache-primary-key }} @@ -453,7 +356,7 @@ jobs: fi - name: "Save updated compiler cache (ccache)" - if: github.ref == 'refs/heads/master' && env.PERSIST_CCACHE == 'true' + if: github.ref == 'refs/heads/master' uses: actions/cache/save@v6 with: key: ${{ steps.restore_ccache.outputs.cache-primary-key }} @@ -495,93 +398,39 @@ jobs: GH_TOKEN: ${{ github.token }} run: gh attestation verify "$BUILD_PATH" --repo Cockatrice/Cockatrice - build-linux-nightly: - strategy: - fail-fast: false - matrix: - include: - - distro: Debian - version: 12 # previous stable, still covered by the nightly - - package: DEB - test: skip # Running tests on all distros is superfluous - - - distro: Fedora - version: 44 # previous release, still covered by the nightly - - package: RPM - test: skip # Running tests on all distros is superfluous - - - distro: Ubuntu - version: 24.04 # previous LTS, still covered by the nightly - - package: DEB - test: skip # Running tests on all distros is superfluous - - # The one leg that runs instrumented for line coverage and publishes the report as - # a workflow artifact. It stays a separate leg rather than a second flag on the - # sanitizer leg because the two instruments answer different questions: the - # sanitizer leg reports what the run did wrong, the coverage leg reports what the - # run never touched at all. Combining them would also slow both down. - # - # It moved here from build-linux: gcovr.cfg sets no fail_under, so the leg cannot - # fail a build, and nothing consumes the report per change, which made it an - # artifact nobody read on every pull request. On the default branch it becomes a - # trend instead. Image and ccache keys come from distro, version and variant, so - # they are unchanged and the warm cache that leg built still matches. - - distro: Ubuntu - version: 26.04 - variant: cover # separate image and ccache, same distro - - coverage: yes - package: skip - # A report must not redden the nightly run: vcpkg-cache-import only imports the - # archives of a run whose conclusion is success. - allow-failure: yes - - name: ${{ matrix.distro }} ${{ matrix.version }}${{ matrix.variant && format(' {0}', matrix.variant) || '' }} - needs: configure - # A nightly and a tag build cover the legs a pull request or a merge does not wait for: - # the previous release of each distro, and the one instrument that only reports. - if: github.event_name == 'schedule' || github.ref_type == 'tag' - runs-on: ubuntu-latest # https://github.com/actions/runner-images - continue-on-error: ${{ matrix.allow-failure == 'yes' }} - timeout-minutes: 70 - env: - CACHE: ${{ github.workspace }}/.cache/${{ matrix.distro }}${{ matrix.version }}${{ matrix.variant }} # directory for caching docker image and ccache - CCACHE_EVICTION_AGE: 7d - # These legs do not keep a compiler cache: they run once a day (and on tags) and gate - # nothing, so their share of the 10 GB quota goes to the pull-request legs instead. - # A cold nightly build costs minutes on a runner nobody waits for; a cold pull request - # build costs a review. CCACHE_SIZE is still passed so ccache trims its scratch dir. - PERSIST_CCACHE: 'false' - CCACHE_SIZE: 600M - CMAKE_GENERATOR: 'Ninja' - NAME: ${{ matrix.distro }}${{ matrix.version }} - IMAGE_SUFFIX: ${{ matrix.variant }} - # The shared steps below gate on these, so they have to be derived here too. Left - # undefined they silently skipped the build the leg exists for: the coverage leg - # found build/ empty and its report collection failed on every nightly and tag run. - # Same derivation as the pull request job above. - DEBUG_TEST: ${{ matrix.sanitize == 'yes' || matrix.coverage == 'yes' || matrix.debug_test == 'yes' }} - RUN_TESTS: ${{ matrix.test != 'skip' && !(matrix.sanitize == 'yes' || matrix.coverage == 'yes' || matrix.debug_test == 'yes') }} - steps: *linux-steps - build-vcpkg: strategy: fail-fast: false matrix: include: - os: macOS - target: 14 # EOL 2026-09-14 - runner: xcode-27 # https://github.com/actions/runner-images/blob/main/images/macos/xcode-27-arm64-Readme.md + target: 13 # EOL 2025-09-15 + runner: macos-15-intel # https://github.com/actions/runner-images/blob/main/images/macos/macos-15-Readme.md + # macos-26-intel is very slow and fails in CPack during DMG config if not increasing Finder timeout + + ccache_eviction_age: 7d + cmake_generator: Ninja + make_package: 1 + override_target: 13 + package_suffix: "-macOS13_Intel" + qt_version: 6.11.* + qt_modules: qtimageformats qtmultimedia qtshadertools qtwebsockets + soc: Intel + type: Release + use_ccache: 1 + vcpkg_scope: 'vcpkg/macos13-x64-xcode26.3/' + vcpkg_upload: 1 # only one leg per vcpkg_scope hands its archives over + xcode: "26.3" + + - os: macOS + target: 14 # EOL 2026-?? + runner: macos-26 # https://github.com/actions/runner-images/blob/main/images/macos/macos-26-arm64-Readme.md ccache_eviction_age: 7d cmake_generator: Ninja make_package: 1 override_target: 14 package_suffix: "-macOS14" - ccache_size: 800M # one Release configuration plus the suite at -g1 qt_version: 6.11.* qt_modules: qtimageformats qtmultimedia qtshadertools qtwebsockets soc: Apple @@ -590,17 +439,17 @@ jobs: vcpkg_deployment_target: 14 vcpkg_scope: 'vcpkg/macos14-arm64-xcode26.6/' vcpkg_upload: 1 # only one leg per vcpkg_scope hands its archives over + xcode: "26.6" - os: macOS - target: 15 # EOL 2027-?? - runner: xcode-27 # https://github.com/actions/runner-images/blob/main/images/macos/xcode-27-arm64-Readme.md + target: 15 + runner: macos-26 # https://github.com/actions/runner-images/blob/main/images/macos/macos-26-arm64-Readme.md ccache_eviction_age: 7d cmake_generator: Ninja make_package: 1 override_target: 15 package_suffix: "-macOS15" - persist_ccache: 'false' # 16.5 min: shorter than the Windows leg qt_version: 6.11.* qt_modules: qtimageformats qtmultimedia qtshadertools qtwebsockets soc: Apple @@ -608,14 +457,14 @@ jobs: use_ccache: 1 vcpkg_deployment_target: 14 vcpkg_scope: 'vcpkg/macos14-arm64-xcode26.6/' + xcode: "26.6" - os: macOS - target: 27 - runner: xcode-27 # https://github.com/actions/runner-images/blob/main/images/macos/xcode-27-arm64-Readme.md + target: 26 + runner: macos-26 # https://github.com/actions/runner-images/blob/main/images/macos/macos-26-arm64-Readme.md ccache_eviction_age: 7d cmake_generator: Ninja - persist_ccache: 'false' # 15.8 min: shorter than the Windows leg qt_version: 6.11.* qt_modules: qtimageformats qtmultimedia qtshadertools qtwebsockets soc: Apple @@ -623,6 +472,7 @@ jobs: use_ccache: 1 vcpkg_deployment_target: 14 vcpkg_scope: 'vcpkg/macos14-arm64-xcode26.6/' + xcode: "26.6" - os: Windows target: 10 @@ -647,26 +497,13 @@ jobs: name: ${{ matrix.os }} ${{ matrix.target }}${{ matrix.soc == 'Intel' && ' Intel' || '' }}${{ matrix.type == 'Debug' && ' Debug' || '' }} needs: [configure, vcpkg-baseline] - # The nightly leg lives in build-vcpkg-nightly below, next to the Linux nightly legs. - if: github.event_name != 'schedule' runs-on: ${{ matrix.runner }} timeout-minutes: 100 env: CCACHE_DIR: ${{ github.workspace }}/.cache/ - # One macOS configuration is about 600 MB of objects at -g1, so its bound needs to be - # 800M to hold one whole; a smaller cache serves a few percent and is worthless (see - # the sizing comment in build-linux). macOS 14 keeps the one macOS cache: it runs the - # suite against the shipping Release build and is the longest of the three at 24.2 min. - # macOS 15 (16.5) and macOS 27 Debug (15.8) duplicate its coverage and are shorter, so - # they run cold. - CCACHE_SIZE: ${{ matrix.ccache_size || '700M' }} - PERSIST_CCACHE: ${{ matrix.persist_ccache || 'true' }} - # MSVC has no -g level, so the Windows leg does not ask for one; it has no compiler - # cache to shrink anyway (see the sizing comment in build-linux). - COCKATRICE_DEBUG_INFO_LEVEL: ${{ matrix.os != 'Windows' && github.ref_type != 'tag' && '1' || '' }} + 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 - # Shared with build-vcpkg-nightly so the two leg sets run the same steps. - steps: &vcpkg-steps + steps: - name: "Checkout" uses: actions/checkout@v7 with: @@ -683,11 +520,11 @@ jobs: run: brew install ccache - name: "[macOS] Restore compiler cache (ccache)" - if: matrix.os == 'macOS' && matrix.use_ccache == 1 && env.PERSIST_CCACHE == 'true' + if: matrix.os == 'macOS' && matrix.use_ccache == 1 id: restore_ccache uses: actions/cache/restore@v6 with: - key: ccache-${{ matrix.os }}${{ matrix.override_target }}-${{ matrix.runner }} + key: ccache-${{ matrix.runner }}_${{ matrix.override_target }}-Xcode${{ matrix.xcode }} path: ${{ env.CCACHE_DIR }} - name: "[macOS] Install aqtinstall" @@ -802,6 +639,7 @@ jobs: # configure.outputs.tag is set for ANY tag, so beta (Development) tags get the DSN # and the symbol upload below just like full releases. COCKATRICE_SENTRY_DSN: ${{ needs.configure.outputs.tag != null && secrets.SENTRY_DSN || '' }} + DEVELOPER_DIR: '/Applications/Xcode_${{ matrix.xcode }}.app/Contents/Developer' GITHUB_TOKEN: ${{ github.token }} # needed for vcpkg dependency graph updates, see VCPKG_FEATURE_FLAGS MACOS_CERTIFICATE: ${{ secrets.PROD_MACOS_CERTIFICATE }} MACOS_CERTIFICATE_NAME: ${{ secrets.PROD_MACOS_CERTIFICATE_NAME }} @@ -1013,7 +851,7 @@ jobs: # Delete used cache to emulate a ccache update. See https://github.com/actions/cache/issues/342 - name: "[macOS] Delete remote compiler cache (ccache)" - if: matrix.os == 'macOS' && matrix.use_ccache == 1 && env.PERSIST_CCACHE == 'true' && github.ref == 'refs/heads/master' && steps.restore_ccache.outputs.cache-hit + if: matrix.os == 'macOS' && matrix.use_ccache == 1 && github.ref == 'refs/heads/master' && steps.restore_ccache.outputs.cache-hit continue-on-error: true env: CACHE_PRIMARY_KEY: ${{ steps.restore_ccache.outputs.cache-primary-key }} @@ -1024,7 +862,7 @@ jobs: fi - name: "[macOS] Cache updated compiler cache (ccache)" - if: matrix.os == 'macOS' && matrix.use_ccache == 1 && env.PERSIST_CCACHE == 'true' && github.ref == 'refs/heads/master' + if: matrix.os == 'macOS' && matrix.use_ccache == 1 && github.ref == 'refs/heads/master' uses: actions/cache/save@v6 with: key: ${{ steps.restore_ccache.outputs.cache-primary-key }} @@ -1140,44 +978,6 @@ jobs: GH_TOKEN: ${{ github.token }} run: gh attestation verify "$BUILD_PATH" --repo Cockatrice/Cockatrice - build-vcpkg-nightly: - strategy: - fail-fast: false - matrix: - include: - - os: macOS - target: 13 # EOL 2025-09-15 - runner: macos-15-intel # https://github.com/actions/runner-images/blob/main/images/macos/macos-15-Readme.md - # macos-26-intel is very slow and fails in CPack during DMG config if not increasing Finder timeout - - ccache_eviction_age: 7d - cmake_generator: Ninja - make_package: 1 - override_target: 13 - package_suffix: "-macOS13_Intel" - qt_version: 6.11.* - qt_modules: qtimageformats qtmultimedia qtshadertools qtwebsockets - soc: Intel - type: Release - use_ccache: 1 - vcpkg_scope: 'vcpkg/macos13-x64-xcode26.3/' - vcpkg_upload: 1 # only one leg per vcpkg_scope hands its archives over - - name: ${{ matrix.os }} ${{ matrix.target }}${{ matrix.soc == 'Intel' && ' Intel' || '' }}${{ matrix.type == 'Debug' && ' Debug' || '' }} - needs: configure - # The EOL target on the slowest runner is too slow to gate a change, so it runs in the - # nightly and on tags only, where the vcpkg cache import also has to accept schedule runs. - if: github.event_name == 'schedule' || github.ref_type == 'tag' - runs-on: ${{ matrix.runner }} - timeout-minutes: 100 - env: - CCACHE_DIR: ${{ github.workspace }}/.cache/ - # Like build-linux-nightly: this leg keeps no compiler cache, so the quota stays with - # the pull-request legs (see PERSIST_CCACHE there). - PERSIST_CCACHE: 'false' - CCACHE_SIZE: 600M - steps: *vcpkg-steps - prune-caches: name: Prune caches # Only master writes caches at all, so only master may clean them up too @@ -1186,66 +986,38 @@ jobs: continue-on-error: true # freeing space must never fail a build steps: - # The repository holds 10 GB of cache against entries that want more, and GitHub evicts - # the least recently used entry whenever that is exceeded. The vcpkg entries are the - # reason that never happens: the restore above touches every entry under the prefix on - # every run, so a dead ABI generation keeps its access date fresh forever and age based - # eviction can never reach it. Deleting by age alone cannot fix that, only knowing which - # scope an entry belongs to can. The same is true of a compiler cache of a leg that no - # longer keeps one: its restore would keep touching it, so the live key list below is - # what retires it. - # - # docker-release's image builds keep their layer cache in the same quota: one blob per - # layer, keyed buildkit-blob-1-, plus one index--1- entry per image - # scope that lists them. They are written with mode=min, so only the final image stage - # is kept and the set stays small. A layer a build actually needs is downloaded, which - # refreshes its access date, so an old date means the builds have stopped using it and - # age is the right test. Dropping a blob an index still lists costs the next image build - # one rebuilt layer; docker-release gates nothing, so that is cheaper than keeping the - # entries forever. + # The repository holds 10Gi of cache against entries that want more, and GitHub + # evicts the least recently used entry whenever that is exceeded. The vcpkg + # entries are the reason that never happens: the restore above touches every + # entry under the prefix on every run, so a dead ABI generation keeps its access + # date fresh forever and age based eviction can never reach it. Deleting by age + # alone cannot fix that, only knowing which scope an entry belongs to can. - name: "Delete unusable cache entries" shell: bash env: GH_TOKEN: ${{ github.token }} - # Must list every vcpkg_scope of the build-vcpkg and build-vcpkg-nightly - # matrices above, or the entries of a scope that is still in use get deleted - # on the next run. + # Must list every vcpkg_scope of the build-vcpkg matrix above, or the entries of + # a scope that is still in use get deleted on the next run. LIVE_VCPKG_SCOPES: 'vcpkg/macos13-x64-xcode26.3/ vcpkg/macos14-arm64-xcode26.6/ vcpkg/windows10-x64-vs2026/' - # Must list the ccache key of every leg that keeps a compiler cache - # (PERSIST_CCACHE in build-linux and build-vcpkg), or a live entry is deleted. - # The nightly legs and the pull-request legs that finish under the Windows leg - # keep none, so their leftover entries are reclaimed here instead of waiting out - # the age rule. - LIVE_CCACHE_KEYS: 'ccache-Arch ccache-Debian13 ccache-Fedora45debug ccache-Fedora45release ccache-Ubuntu26.04sanitize ccache-macOS14-xcode-27' run: | gh api --paginate "repos/$GITHUB_REPOSITORY/actions/caches?per_page=100" \ --jq '.actions_caches[] | [.id, .ref, .key, .last_accessed_at] | @tsv' >"$RUNNER_TEMP/caches.tsv" cutoff="$(( $(date -u +%s) - 7 * 24 * 3600 ))" live=" ${LIVE_VCPKG_SCOPES} " - live_ccache=" ${LIVE_CCACHE_KEYS} " - deleted=0 while IFS=$'\t' read -r id ref key accessed; do [[ -n $id ]] || continue - # Only ever touch the key shapes our workflows create (buildkit-* and index-* - # come from docker-release.yml), so that an entry of some other workflow can - # never be deleted by accident + # Only ever touch the key shapes this workflow creates, so that an entry of + # some other workflow can never be deleted by accident case "$key" in - vcpkg/* | ccache-* | Qt* | buildkit-* | index-*) ;; + vcpkg/* | ccache-* | Qt*) ;; *) continue ;; esac reason="" if [[ $ref != refs/heads/master ]]; then reason="scoped to $ref" - elif [[ $key == ccache-* ]]; then - # An exact token compare, because one key can be a prefix of another: - # ccache-Ubuntu26.04 is a prefix of ccache-Ubuntu26.04sanitize, and only - # the sanitize entry is live. - if [[ $live_ccache != *" $key "* ]]; then - reason="no pull-request leg keeps it" - fi elif [[ $key == vcpkg/* ]]; then # "/", cut the hash off the end to get the scope scope="vcpkg/${key#vcpkg/}" diff --git a/.github/workflows/docker-release.yml b/.github/workflows/docker-release.yml index 1537d6d4d..937386940 100644 --- a/.github/workflows/docker-release.yml +++ b/.github/workflows/docker-release.yml @@ -84,18 +84,12 @@ jobs: password: ${{ github.token }} # Don't push for non-publishing triggers - # - # The layer cache shares the repository's 10 GB Actions cache quota with the compiler - # caches (see CCACHE_SIZE in desktop-build.yml). mode=min keeps only the final image - # stage: the build stage sits behind `COPY . .`, so its compile layer is invalidated by - # every source change and was never read back, yet it was the bulk of the ~2 GB this - # cache held. The final runtime stage is the part that still pays off. - name: "Build image" if: steps.login.outcome != 'success' uses: docker/build-push-action@v7 with: cache-from: type=gha,scope=${{ env.CACHE_SCOPE }} - cache-to: ${{ case(github.ref == 'refs/heads/master', format('type=gha,mode=min,scope={0}', env.CACHE_SCOPE), '') }} + cache-to: ${{ case(github.ref == 'refs/heads/master', format('type=gha,mode=max,scope={0}', env.CACHE_SCOPE), '') }} context: . platforms: ${{ matrix.platform }} push: false @@ -107,7 +101,7 @@ jobs: uses: docker/build-push-action@v7 with: cache-from: type=gha,scope=${{ env.CACHE_SCOPE }} - cache-to: type=gha,mode=min,scope=${{ env.CACHE_SCOPE }} + cache-to: type=gha,mode=max,scope=${{ env.CACHE_SCOPE }} context: . labels: | org.opencontainers.image.description=${{ env.OCI_DESCRIPTION }} diff --git a/.github/workflows/vcpkg-cache-import.yml b/.github/workflows/vcpkg-cache-import.yml index 0a377a8fd..3cc7b2203 100644 --- a/.github/workflows/vcpkg-cache-import.yml +++ b/.github/workflows/vcpkg-cache-import.yml @@ -4,14 +4,12 @@ name: Import vcpkg cache # rebuilds protobuf, openssl and abseil from source and saves them into the shared # cache, where every later run can restore them. # -# Only master pushes and the nightly schedule run on master are imported here: the -# collect step, keys.txt and the zip contents all come from the workflow run being -# imported, so importing from an unmerged pull request would let anyone who can push -# a branch — or a bot actor such as Dependabot, whose PRs are same-repo — promote -# arbitrary binaries into the scope that master and tag builds restore and link into -# signed releases. Forks build untrusted code and must never reach that scope either. -# A scheduled run is the default branch at the commit a master push already built, so -# it carries the same trust as a push. +# Only master pushes are imported here: the collect step, keys.txt and the zip +# contents all come from the workflow run being imported, so importing from an +# unmerged pull request would let anyone who can push a branch — or a bot actor +# such as Dependabot, whose PRs are same-repo — promote arbitrary binaries into +# the scope that master and tag builds restore and link into signed releases. +# Forks build untrusted code and must never reach that scope either. # # repository_dispatch is not used on purpose: it would also resolve to the default branch, # but it counts as a low-trust trigger and may only write there when it declares a @@ -40,7 +38,7 @@ jobs: # the shared scope even if they somehow target the default branch. if: >- github.event.workflow_run.conclusion == 'success' && - (github.event.workflow_run.event == 'push' || github.event.workflow_run.event == 'schedule') && + github.event.workflow_run.event == 'push' && github.event.workflow_run.head_branch == 'master' && github.event.workflow_run.head_repository.full_name == github.repository && !endsWith(github.event.workflow_run.actor.login, '[bot]') && diff --git a/.gitmodules b/.gitmodules index e69c8e686..732fec7ca 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,6 +4,3 @@ [submodule "doxygen-awesome-css"] path = doc/doxygen/theme url = https://github.com/jothepro/doxygen-awesome-css.git -[submodule "external/qttesting"] - path = external/qttesting - url = https://github.com/Kitware/QtTesting.git diff --git a/CMakeLists.txt b/CMakeLists.txt index 53508fcd1..8a25fe2c6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -20,8 +20,6 @@ option(WITH_CLIENT "Build Cockatrice client" ON) option(WITH_ORACLE "Build Cockatrice card database tool (Oracle)" ON) # Compile Servatrice option(WITH_SERVER "Build Cockatrice server (Servatrice)" OFF) -# Compile GUI tests (QtTesting widget-level playback) -option(WITH_GUI_TEST "Build the QtTesting-based GUI test harness" OFF) # Compile tests option(TEST "Build tests" OFF) # Build with AddressSanitizer and UndefinedBehaviorSanitizer @@ -31,14 +29,6 @@ option(ENABLE_COVERAGE "Build instrumented for GCC line coverage" OFF) # Use vcpkg regardless of OS option(USE_VCPKG "Use vcpkg regardless of OS" OFF) -# The GUI test harness lives in a submodule; a checkout without it must still configure. -if(WITH_GUI_TEST AND NOT EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/external/qttesting/CMakeLists.txt") - message(WARNING "WITH_GUI_TEST is ON but external/qttesting is missing, so GUI tests are disabled.\n" - "Run 'git submodule update --init external/qttesting' to enable them." - ) - set(WITH_GUI_TEST OFF) -endif() - # Default to "Release" build type # User-provided value for CMAKE_BUILD_TYPE must be checked before the PROJECT() call if(DEFINED CMAKE_BUILD_TYPE) @@ -65,10 +55,6 @@ if(USE_CCACHE AND NOT WIN32) # 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. - # The include_file_mtime/include_file_ctime entries that CI also sets are - # deliberately not set here: --set-config writes the user's global - # ccache.conf, and those two would turn off the modified-during-compile - # check for every project on the machine, not just this one. execute_process(COMMAND ${CCACHE_PROGRAM} --set-config sloppiness=pch_defines,time_macros) message(STATUS "Found CCache ${CCACHE_PROGRAM}") endif() @@ -85,8 +71,6 @@ if(WIN32 OR USE_VCPKG) ) # Qt path set by user or env var if(QTDIR - OR QTDIR32 - OR QTDIR64 OR DEFINED ENV{QTDIR} OR DEFINED ENV{QTDIR32} OR DEFINED ENV{QTDIR64} @@ -238,27 +222,6 @@ include(Sanitizers) include(Coverage) include(CrashReporting) -# Debug information is what makes a cached object large, and a build configuration needs more -# room than a per-leg cache bound can spare when it carries full DWARF: ccache then fills to -# its limit and evicts the entries the next build is about to ask for, so it serves a few -# percent of calls while holding most of a build. CI sets a lower level so one whole -# configuration fits; level 1 keeps file and line, which is what a failing test or a sanitizer -# backtrace reports. Empty (the default) leaves local, distro and release builds exactly as -# they were. This comes after CrashReporting, so the -g it adds for Sentry symbols cannot -# raise the level again. -set(COCKATRICE_DEBUG_INFO_LEVEL - "" - CACHE STRING "DWARF level for Debug and Release objects (empty keeps the built-in flags)" -) -if(NOT COCKATRICE_DEBUG_INFO_LEVEL STREQUAL "") - if(MSVC) - message(WARNING "COCKATRICE_DEBUG_INFO_LEVEL is ignored on MSVC") - else() - string(APPEND CMAKE_CXX_FLAGS_DEBUG " -g${COCKATRICE_DEBUG_INFO_LEVEL}") - string(APPEND CMAKE_CXX_FLAGS_RELEASE " -g${COCKATRICE_DEBUG_INFO_LEVEL}") - endif() -endif() - # GNU systems need to define the Mersenne exponent for the RNG to compile w/o warning if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang") add_definitions("-DSFMT_MEXP=19937") @@ -273,13 +236,13 @@ else() set(_lib_suffix 32) endif() -if(NOT "${QTDIR${_lib_suffix}}" STREQUAL "") +if(DEFINED QTDIR${_lib_suffix}) list(APPEND CMAKE_PREFIX_PATH "${QTDIR${_lib_suffix}}") -elseif(NOT "${QTDIR}" STREQUAL "") +elseif(DEFINED QTDIR) list(APPEND CMAKE_PREFIX_PATH "${QTDIR}") -elseif(NOT "$ENV{QTDIR${_lib_suffix}}" STREQUAL "") +elseif(DEFINED ENV{QTDIR${_lib_suffix}}) list(APPEND CMAKE_PREFIX_PATH "$ENV{QTDIR${_lib_suffix}}") -elseif(NOT "$ENV{QTDIR}" STREQUAL "") +elseif(DEFINED ENV{QTDIR}) list(APPEND CMAKE_PREFIX_PATH "$ENV{QTDIR}") endif() @@ -498,34 +461,6 @@ if(WITH_ORACLE) set(CPACK_INSTALL_CMAKE_PROJECTS "Oracle;Oracle;ALL;/" ${CPACK_INSTALL_CMAKE_PROJECTS}) endif() -if(WITH_GUI_TEST) - set(QtTesting_QT_VERSION "6") - # QtTesting registers its own unit tests when BUILD_TESTING is on. They target the - # Qt version the harness was written for and fail on Qt 6.11 (toolbutton - # long-press recording), and they are not Cockatrice's tests, so suppress them - # for the duration of the subdirectory instead of painting ctest red. - if(DEFINED BUILD_TESTING) - set(_cockatrice_saved_build_testing ${BUILD_TESTING}) - endif() - set(BUILD_TESTING - OFF - CACHE BOOL "Build the testing tree." FORCE - ) - add_subdirectory(external/qttesting) - if(DEFINED _cockatrice_saved_build_testing) - set(BUILD_TESTING - ${_cockatrice_saved_build_testing} - CACHE BOOL "Build the testing tree." FORCE - ) - else() - unset(BUILD_TESTING CACHE) - endif() - # cl.exe has no warnings-as-errors here, and it rejects -Wno-error with D8021. - if(NOT MSVC) - target_compile_options(qttesting PRIVATE -Wno-error) - endif() -endif() - if(TEST) include(CTest) add_subdirectory(tests) diff --git a/Testing/README.md b/Testing/README.md deleted file mode 100644 index cb990ec29..000000000 --- a/Testing/README.md +++ /dev/null @@ -1,66 +0,0 @@ -# GUI tests - -Widget-level playback tests for the Cockatrice client, driven by -[QtTesting](https://github.com/Kitware/QtTesting) (`external/qttesting`, a submodule). -Each `*_test.xml` file is a list of events that QtTesting replays against the running -application; a test passes when every event is handled without error. - -## Running - -```bash -git submodule update --init external/qttesting # once -cmake -S . -B build/debug -DCMAKE_BUILD_TYPE=Debug -DWITH_SERVER=0 -DWITH_GUI_TEST=ON -cmake --build build/debug -j 24 - -./Testing/run_test.sh --all # headless, offscreen -./Testing/run_test.sh Testing/smoke_test.xml # one file -./Testing/run_test.sh --visual Testing/smoke_test.xml # shows the window -``` - -`WITH_GUI_TEST` is on by default; a checkout without the submodule configures with a -warning and no harness. `COCKATRICE_BIN` points the runner at another build, and -`TEST_TIMEOUT` (seconds, default 180) bounds a single test — a failing event inside a -modal dialog can otherwise leave the process alive. The client also hard-exits a few -seconds after playback returns, whatever the result. - -The tests are **not** part of `ctest`: they need a card database and a developer's -settings, so they stay a local loop for now. - -## Caveats - -- **Not hermetic.** The client reads the local settings and card database, so a test can - pass on one machine and fail on another. Avoid asserting things that restored - workspace state or user preferences decide (for example the number of open tabs). -- **English labels only.** Menu entries are addressed by their (translated) text, so the - tests expect the source language. Widgets are addressed by objectName paths, which are - stable across translations. -- Actions that open a dialog are followed by a `pause` event: playback continues in the - dialog's own event loop, and the pause gives the UI time to build itself. -- Widget paths include Qt-internal names where a QTabWidget is involved - (`qt_tabwidget_stackedwidget`), and the deck views in a game are indexed - (`tabbedDeckView_0`, `tabbedDeckView_1`). - -## Writing a test - -A test is a `` list. Widgets are named by their path from the main window, e.g. -`MainWindow/tabSupervisor/stackedWidget/TabGame/phasesMenu`. Useful commands: - -| Command | Arguments | Effect | -|---------|-----------|--------| -| `activate` | action or menu name | triggers a QAction (menus are opened first) | -| `key` | Qt key code | presses that key on the focused widget | -| `pause` | milliseconds | waits, letting the UI settle | -| `clickCard` | `card,zone` | double click the card found in that zone (plays it from hand) | -| `flipCard` | `card,zone` | right-click → *Turn Over* | -| `cardAction` | `card,zone,label` | right-click → first menu entry whose label contains `label` (submenus included) | -| `moveCard` | `card,srcZone,dstZone` | drags the card onto the destination zone | -| `drawArrow` | `srcCard,srcZone,dstCard,dstZone` | right-drag an arrow between two cards | -| `createToken` | `name,color,pt` | creates a token as the active local player | -| `selectItem` | `item label` | selects the tree/list entry with that label | -| `typeText` | `text` | types text one key event at a time | -| `expectProperty` | `property,value` | fails playback unless the widget property matches | - -The card, item and property commands are `pqCockatriceEventPlayer` -(`cockatrice/src/game_graphics/pq_cockatrice_event_player.h`). They resolve cards by name -and zone through the `QGraphicsScene` and send real mouse events at the card's current -viewport position, so a test does not depend on the window size or screen resolution. diff --git a/Testing/card_actions_test.xml b/Testing/card_actions_test.xml deleted file mode 100644 index 8d6de4c2b..000000000 --- a/Testing/card_actions_test.xml +++ /dev/null @@ -1,69 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Testing/deck_editor_open_test.xml b/Testing/deck_editor_open_test.xml deleted file mode 100644 index ab74b1c63..000000000 --- a/Testing/deck_editor_open_test.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/Testing/deck_editor_search_test.xml b/Testing/deck_editor_search_test.xml deleted file mode 100644 index 6641ac3b2..000000000 --- a/Testing/deck_editor_search_test.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - - - - - diff --git a/Testing/dialog_lifecycle_test.xml b/Testing/dialog_lifecycle_test.xml deleted file mode 100644 index 00510b802..000000000 --- a/Testing/dialog_lifecycle_test.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/Testing/game_mechanics_test.xml b/Testing/game_mechanics_test.xml deleted file mode 100644 index efef78707..000000000 --- a/Testing/game_mechanics_test.xml +++ /dev/null @@ -1,101 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Testing/integration_test.xml b/Testing/integration_test.xml deleted file mode 100644 index 33191d1ac..000000000 --- a/Testing/integration_test.xml +++ /dev/null @@ -1,45 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Testing/local_game_lifecycle_test.xml b/Testing/local_game_lifecycle_test.xml deleted file mode 100644 index bdc964921..000000000 --- a/Testing/local_game_lifecycle_test.xml +++ /dev/null @@ -1,178 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Testing/menu_open_close_test.xml b/Testing/menu_open_close_test.xml deleted file mode 100644 index d894b47b5..000000000 --- a/Testing/menu_open_close_test.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - - - - - diff --git a/Testing/nonmodal_manage_sets_test.xml b/Testing/nonmodal_manage_sets_test.xml deleted file mode 100644 index 5fe2bfc0c..000000000 --- a/Testing/nonmodal_manage_sets_test.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/Testing/nonmodal_tip_test.xml b/Testing/nonmodal_tip_test.xml deleted file mode 100644 index 0f4ffc825..000000000 --- a/Testing/nonmodal_tip_test.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/Testing/nonmodal_view_log_test.xml b/Testing/nonmodal_view_log_test.xml deleted file mode 100644 index 3369f1fd5..000000000 --- a/Testing/nonmodal_view_log_test.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/Testing/run_test.sh b/Testing/run_test.sh deleted file mode 100755 index 8ff3fe028..000000000 --- a/Testing/run_test.sh +++ /dev/null @@ -1,118 +0,0 @@ -#!/bin/bash -# Run QtTesting XML test files against the Cockatrice build. -# -# Usage: -# ./Testing/run_test.sh [--visual] [--all | ...] -# -# Options: -# --visual Run with native display (shows the GUI window). -# Default is headless (QT_QPA_PLATFORM=offscreen). -# --all Run every *.xml file in Testing/. -# -# Environment: -# COCKATRICE_BIN Path to the client binary (default: build/debug/cockatrice/cockatrice, -# or the .app bundle binary on macOS). -# TEST_TIMEOUT Seconds a single test may run before it is killed (default: 180). -# Uses timeout, or gtimeout from coreutils on macOS. -# -# Requires the debug build to exist at build/debug/cockatrice/cockatrice. - -set -euo pipefail -shopt -s nullglob - -REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd -P)" -if [ -n "${COCKATRICE_BIN:-}" ]; then - COCKATRICE="$COCKATRICE_BIN" -elif [ "$(uname -s)" = "Darwin" ]; then - # A macOS build produces an app bundle, not a bare executable. - COCKATRICE="$REPO_ROOT/build/debug/cockatrice/cockatrice.app/Contents/MacOS/cockatrice" -else - COCKATRICE="$REPO_ROOT/build/debug/cockatrice/cockatrice" -fi -TEST_DIR="$REPO_ROOT/Testing" -TEST_TIMEOUT="${TEST_TIMEOUT:-180}" - -# Stock macOS has no `timeout`; coreutils provides `gtimeout`. Without either the -# runner still works, but a hung test is not killed. -TIMEOUT=() -if command -v timeout >/dev/null 2>&1; then - TIMEOUT=(timeout --foreground "$TEST_TIMEOUT") -elif command -v gtimeout >/dev/null 2>&1; then - TIMEOUT=(gtimeout --foreground "$TEST_TIMEOUT") -else - echo "Warning: neither timeout nor gtimeout found; tests are not time-limited." >&2 -fi - -if [ ! -f "$COCKATRICE" ]; then - echo "Error: $COCKATRICE not found. Build first:" >&2 - echo " cmake --build build/debug" >&2 - exit 1 -fi - -VISUAL=0 -if [ "${1:-}" = "--visual" ]; then - VISUAL=1 - shift -fi - -if [ "${1:-}" = "--all" ]; then - shift - TEST_FILES=("$TEST_DIR"/*.xml) - if [ ${#TEST_FILES[@]} -eq 0 ]; then - echo "No *.xml files found in $TEST_DIR" >&2 - exit 1 - fi -elif [ $# -ge 1 ]; then - TEST_FILES=("$@") -else - echo "Usage: $0 [--visual] [--all | ...]" >&2 - echo " Run a single test: $0 Testing/smoke_test.xml" >&2 - echo " Run all tests: $0 --all" >&2 - echo " Run visually: $0 --visual Testing/smoke_test.xml" >&2 - echo " Run all visually: $0 --visual --all" >&2 - exit 1 -fi - -PASS=0 -FAIL=0 -FAILED_TESTS=() - -run_one() { - local file="$1" - - local -a cmd=() - if [ "${#TIMEOUT[@]}" -gt 0 ]; then - cmd=("${TIMEOUT[@]}") - fi - cmd+=("$COCKATRICE" --no-connect --test "$file") - - if [ "$VISUAL" -eq 1 ]; then - "${cmd[@]}" - else - env QT_QPA_PLATFORM=offscreen "${cmd[@]}" - fi -} - -for f in "${TEST_FILES[@]}"; do - name="$(basename "$f")" - printf "%-40s " "$name" - if run_one "$f"; then - echo "PASS" - PASS=$((PASS + 1)) - else - echo "FAIL" - FAIL=$((FAIL + 1)) - FAILED_TESTS+=("$name") - fi -done - -TOTAL=$((PASS + FAIL)) -echo "" -echo "Results: $PASS/$TOTAL passed, $FAIL failed" -if [ "$FAIL" -gt 0 ]; then - echo "Failed:" - for t in "${FAILED_TESTS[@]}"; do - echo " $t" - done - exit 1 -fi diff --git a/Testing/settings_dialog_test.xml b/Testing/settings_dialog_test.xml deleted file mode 100644 index 2dd9dda0a..000000000 --- a/Testing/settings_dialog_test.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/Testing/settings_navigation_test.xml b/Testing/settings_navigation_test.xml deleted file mode 100644 index c112966e0..000000000 --- a/Testing/settings_navigation_test.xml +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Testing/settings_search_test.xml b/Testing/settings_search_test.xml deleted file mode 100644 index ba60251f0..000000000 --- a/Testing/settings_search_test.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - diff --git a/Testing/smoke_test.xml b/Testing/smoke_test.xml deleted file mode 100644 index a31edbdd4..000000000 --- a/Testing/smoke_test.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/cmake/FindQtRuntime.cmake b/cmake/FindQtRuntime.cmake index 7b644276d..2d74aca4d 100644 --- a/cmake/FindQtRuntime.cmake +++ b/cmake/FindQtRuntime.cmake @@ -1,6 +1,6 @@ # Find a compatible Qt version # Inputs: WITH_SERVER, WITH_CLIENT, WITH_ORACLE -# Optional Input: Qt6_DIR -- Directory containing Qt6Config.cmake +# Optional Input: QT6_DIR -- Hint as to where Qt6 lives on the system # Output: COCKATRICE_QT_VERSION_NAME -- Example values: Qt6 # Output: SERVATRICE_QT_MODULES # Output: COCKATRICE_QT_MODULES @@ -57,17 +57,7 @@ find_package( QUIET HINTS ${Qt6_DIR} ) if(NOT Qt6_FOUND) - set(_QT_DIAGNOSTIC "") - if(Qt6_NOT_FOUND_MESSAGE) - set(_QT_DIAGNOSTIC "\nQt package diagnostic: ${Qt6_NOT_FOUND_MESSAGE}") - endif() - message(FATAL_ERROR - "No suitable version of Qt was found. Qt 6.4.2 or newer with these components is required: " - "${REQUIRED_QT_COMPONENTS};Linguist.\n" - "Set QTDIR or CMAKE_PREFIX_PATH to the Qt installation prefix, or Qt6_DIR to the directory " - "containing Qt6Config.cmake." - "${_QT_DIAGNOSTIC}" - ) + message(FATAL_ERROR "No suitable version of Qt was found") endif() set(COCKATRICE_QT_VERSION_NAME Qt6) diff --git a/cockatrice/CMakeLists.txt b/cockatrice/CMakeLists.txt index f34961b0b..642d998a0 100644 --- a/cockatrice/CMakeLists.txt +++ b/cockatrice/CMakeLists.txt @@ -15,7 +15,6 @@ set(cockatrice_SOURCES src/client/network/update/client/client_update_checker.cpp src/client/network/update/client/release_channel.cpp src/client/network/update/card_spoiler/spoiler_background_updater.cpp - src/client/network/update/card_spoiler/spoiler_file.cpp src/client/crash/crash_reporter.cpp src/client/latency_graph_widget.cpp src/client/latency_status_widget.cpp @@ -57,7 +56,6 @@ set(cockatrice_SOURCES src/interface/widgets/dialogs/dlg_settings.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_turn_order.cpp src/interface/widgets/dialogs/dlg_update.cpp src/interface/widgets/dialogs/dlg_view_log.cpp src/interface/widgets/dialogs/override_printing_warning.cpp @@ -151,13 +149,11 @@ set(cockatrice_SOURCES src/interface/widgets/utility/reversed_completer_model.cpp src/interface/pixel_map_generator.cpp src/interface/theme_config.cpp - src/interface/theme_icon.cpp src/interface/theme_manager.cpp src/interface/palette_editor/color_button.cpp src/interface/palette_editor/palette_generator.cpp src/interface/palette_editor/quick_setup_panel.cpp src/interface/palette_editor/palette_grid_widget.cpp - src/interface/palette_editor/font_grid_widget.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/deck_color_identity.cpp @@ -253,8 +249,6 @@ set(cockatrice_SOURCES src/interface/widgets/playmat/playmat_settings_dialog.cpp src/interface/widgets/quick_settings/settings_button_widget.cpp src/interface/widgets/quick_settings/settings_popup_widget.cpp - src/interface/widgets/replay/replay_event_index.cpp - src/interface/widgets/replay/replay_event_sidebar.cpp src/interface/widgets/replay/replay_manager.cpp src/interface/widgets/replay/replay_quick_settings_widget.cpp src/interface/widgets/replay/replay_timeline_widget.cpp @@ -455,8 +449,6 @@ set(cockatrice_SOURCES src/interface/intents/intent_open_shared_deck.h src/interface/intents/url_parser.cpp src/interface/intents/url_parser.h - src/interface/widgets/server/user/user_games_model.cpp - src/interface/widgets/server/user/user_games_model.h src/interface/widgets/server/user/user_info_popup.cpp src/interface/widgets/server/user/user_info_popup.h ) @@ -500,15 +492,7 @@ if(APPLE) set(cockatrice_SOURCES ${cockatrice_SOURCES} ${CMAKE_CURRENT_SOURCE_DIR}/resources/appicon.icns) endif(APPLE) -# The fonts and card artwork produce a large C++ byte array with qt_add_resources. -# Embed the data in the compiled resource object instead, keeping compiler memory -# independent of that payload. Qt's two-pass resource build is unsupported on iOS. -if(IOS) - qt_add_resources(cockatrice_RESOURCES_RCC ${cockatrice_RESOURCES}) -else() - qt_add_big_resources(cockatrice_RESOURCES_RCC ${cockatrice_RESOURCES}) -endif() -set_source_files_properties(${cockatrice_RESOURCES} PROPERTIES SKIP_AUTORCC ON) +qt_add_resources(cockatrice_RESOURCES_RCC ${cockatrice_RESOURCES}) # Declare path variables set(ICONDIR @@ -534,7 +518,6 @@ qt_add_executable( WIN32 MACOSX_BUNDLE ${cockatrice_SOURCES} - ${cockatrice_RESOURCES} ${cockatrice_RESOURCES_RCC} ${cockatrice_MOC_SRCS} MANUAL_FINALIZATION @@ -581,14 +564,6 @@ target_link_libraries( cockatrice_target_enable_crash_reporting(cockatrice) -if(WITH_GUI_TEST) - # These two translate between XML test files and the QtTesting event pipeline; - # they need the QtTesting headers, so they only exist with the harness. - target_sources(cockatrice PRIVATE src/game_graphics/pq_cockatrice_event_player.cpp src/interface/recording.cpp) - target_link_libraries(cockatrice PRIVATE QtTesting) - target_compile_definitions(cockatrice PRIVATE WITH_GUI_TEST) -endif() - if(UNIX) if(APPLE) set(MACOSX_BUNDLE_INFO_STRING "${PROJECT_NAME}") diff --git a/cockatrice/cockatrice.qrc b/cockatrice/cockatrice.qrc index 229cdd125..4ec3d2385 100644 --- a/cockatrice/cockatrice.qrc +++ b/cockatrice/cockatrice.qrc @@ -6,18 +6,19 @@ resources/hand.svg resources/hr.jpg - resources/icons/arrow_down.svg + resources/icons/arrow_bottom_green.svg + resources/icons/arrow_down_green.svg resources/icons/arrow_history.svg - resources/icons/arrow_left.svg + resources/icons/arrow_left_green.svg resources/icons/arrow_redo.svg - resources/icons/arrow_right.svg - resources/icons/arrow_right_sideboard.svg - resources/icons/arrow_to_bottom.svg - resources/icons/arrow_to_top.svg - resources/icons/arrow_up.svg + resources/icons/arrow_right_blue.svg + resources/icons/arrow_right_green.svg + resources/icons/arrow_top_green.svg + resources/icons/arrow_up_green.svg resources/icons/arrow_undo.svg resources/icons/circle_half_stroke.svg resources/icons/clearsearch.svg + resources/icons/cogwheel.svg resources/icons/conceded.svg resources/icons/decrement.svg resources/icons/delete.svg @@ -27,6 +28,7 @@ resources/icons/filter.svg resources/icons/floppy_disk.svg resources/icons/forgot_password.svg + resources/icons/gear.svg resources/icons/increment.svg resources/icons/info.svg resources/icons/lock.svg @@ -39,6 +41,7 @@ resources/icons/reload.svg resources/icons/remove_row.svg resources/icons/rename.svg + resources/icons/scale_balanced.svg resources/icons/scales.svg resources/icons/scroll.svg resources/icons/search.svg @@ -49,6 +52,7 @@ resources/icons/swap.svg resources/icons/sync.svg resources/icons/tab_changed.svg + resources/icons/update.png resources/icons/view.svg resources/icons/mana/B.svg diff --git a/cockatrice/resources/config/qtlogging.ini b/cockatrice/resources/config/qtlogging.ini index e4b845af9..7ac0d9ca4 100644 --- a/cockatrice/resources/config/qtlogging.ini +++ b/cockatrice/resources/config/qtlogging.ini @@ -10,7 +10,6 @@ # or set = false to disable logging #main = true -#recording = true #qt_translator = true #window_main.* = true #release_channel = true diff --git a/cockatrice/resources/icons/arrow_bottom_green.svg b/cockatrice/resources/icons/arrow_bottom_green.svg new file mode 100644 index 000000000..3a45053ad --- /dev/null +++ b/cockatrice/resources/icons/arrow_bottom_green.svg @@ -0,0 +1,793 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + diff --git a/cockatrice/resources/icons/arrow_down.svg b/cockatrice/resources/icons/arrow_down.svg deleted file mode 100644 index 0ff237f52..000000000 --- a/cockatrice/resources/icons/arrow_down.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/cockatrice/resources/icons/arrow_down_green.svg b/cockatrice/resources/icons/arrow_down_green.svg new file mode 100644 index 000000000..574c12060 --- /dev/null +++ b/cockatrice/resources/icons/arrow_down_green.svg @@ -0,0 +1,793 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + diff --git a/cockatrice/resources/icons/arrow_history.svg b/cockatrice/resources/icons/arrow_history.svg index 29374d23a..d130758e6 100644 --- a/cockatrice/resources/icons/arrow_history.svg +++ b/cockatrice/resources/icons/arrow_history.svg @@ -1,5 +1,8 @@ - - - - - + + + + history + + + + \ No newline at end of file diff --git a/cockatrice/resources/icons/arrow_left.svg b/cockatrice/resources/icons/arrow_left.svg deleted file mode 100644 index 7a88deddb..000000000 --- a/cockatrice/resources/icons/arrow_left.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/cockatrice/resources/icons/arrow_left_green.svg b/cockatrice/resources/icons/arrow_left_green.svg new file mode 100644 index 000000000..1b9eb0684 --- /dev/null +++ b/cockatrice/resources/icons/arrow_left_green.svg @@ -0,0 +1,791 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + diff --git a/cockatrice/resources/icons/arrow_redo.svg b/cockatrice/resources/icons/arrow_redo.svg index 141260269..dd89e3314 100644 --- a/cockatrice/resources/icons/arrow_redo.svg +++ b/cockatrice/resources/icons/arrow_redo.svg @@ -1,4 +1,40 @@ - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/cockatrice/resources/icons/arrow_right.svg b/cockatrice/resources/icons/arrow_right.svg deleted file mode 100644 index f0ac5d5bf..000000000 --- a/cockatrice/resources/icons/arrow_right.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/cockatrice/resources/icons/arrow_right_blue.svg b/cockatrice/resources/icons/arrow_right_blue.svg new file mode 100644 index 000000000..334cd68fd --- /dev/null +++ b/cockatrice/resources/icons/arrow_right_blue.svg @@ -0,0 +1,789 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/cockatrice/resources/icons/arrow_right_green.svg b/cockatrice/resources/icons/arrow_right_green.svg new file mode 100644 index 000000000..db491985d --- /dev/null +++ b/cockatrice/resources/icons/arrow_right_green.svg @@ -0,0 +1,789 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/cockatrice/resources/icons/arrow_right_sideboard.svg b/cockatrice/resources/icons/arrow_right_sideboard.svg deleted file mode 100644 index 79a8cd5d3..000000000 --- a/cockatrice/resources/icons/arrow_right_sideboard.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/cockatrice/resources/icons/arrow_to_bottom.svg b/cockatrice/resources/icons/arrow_to_bottom.svg deleted file mode 100644 index 291a527d2..000000000 --- a/cockatrice/resources/icons/arrow_to_bottom.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/cockatrice/resources/icons/arrow_to_top.svg b/cockatrice/resources/icons/arrow_to_top.svg deleted file mode 100644 index aac38cc33..000000000 --- a/cockatrice/resources/icons/arrow_to_top.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/cockatrice/resources/icons/arrow_top_green.svg b/cockatrice/resources/icons/arrow_top_green.svg new file mode 100644 index 000000000..c515c6a1d --- /dev/null +++ b/cockatrice/resources/icons/arrow_top_green.svg @@ -0,0 +1,793 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + diff --git a/cockatrice/resources/icons/arrow_undo.svg b/cockatrice/resources/icons/arrow_undo.svg index c748b37b8..2e182ffb2 100644 --- a/cockatrice/resources/icons/arrow_undo.svg +++ b/cockatrice/resources/icons/arrow_undo.svg @@ -1,4 +1,40 @@ - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/cockatrice/resources/icons/arrow_up.svg b/cockatrice/resources/icons/arrow_up.svg deleted file mode 100644 index ac32b5c92..000000000 --- a/cockatrice/resources/icons/arrow_up.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/cockatrice/resources/icons/arrow_up_green.svg b/cockatrice/resources/icons/arrow_up_green.svg new file mode 100644 index 000000000..1d0f61112 --- /dev/null +++ b/cockatrice/resources/icons/arrow_up_green.svg @@ -0,0 +1,792 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/cockatrice/resources/icons/circle_half_stroke.svg b/cockatrice/resources/icons/circle_half_stroke.svg index c16f84858..42e4dbca2 100644 --- a/cockatrice/resources/icons/circle_half_stroke.svg +++ b/cockatrice/resources/icons/circle_half_stroke.svg @@ -1,4 +1 @@ - - - - + \ No newline at end of file diff --git a/cockatrice/resources/icons/clearsearch.svg b/cockatrice/resources/icons/clearsearch.svg index 49d883c68..be50d68d6 100644 --- a/cockatrice/resources/icons/clearsearch.svg +++ b/cockatrice/resources/icons/clearsearch.svg @@ -1,4 +1,276 @@ - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + diff --git a/cockatrice/resources/icons/cogwheel.svg b/cockatrice/resources/icons/cogwheel.svg new file mode 100644 index 000000000..8c7aad617 --- /dev/null +++ b/cockatrice/resources/icons/cogwheel.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/cockatrice/resources/icons/conceded.svg b/cockatrice/resources/icons/conceded.svg index b9faf047c..a487f18bb 100644 --- a/cockatrice/resources/icons/conceded.svg +++ b/cockatrice/resources/icons/conceded.svg @@ -1,4 +1,103 @@ - - - + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + diff --git a/cockatrice/resources/icons/decrement.svg b/cockatrice/resources/icons/decrement.svg index 033245f85..0ceddf69f 100644 --- a/cockatrice/resources/icons/decrement.svg +++ b/cockatrice/resources/icons/decrement.svg @@ -1,3 +1,205 @@ - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + diff --git a/cockatrice/resources/icons/delete.svg b/cockatrice/resources/icons/delete.svg index d79312f1e..008d8d429 100644 --- a/cockatrice/resources/icons/delete.svg +++ b/cockatrice/resources/icons/delete.svg @@ -1,5 +1,357 @@ - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/cockatrice/resources/icons/dragon.svg b/cockatrice/resources/icons/dragon.svg index cc48d1f5c..f45af39ef 100644 --- a/cockatrice/resources/icons/dragon.svg +++ b/cockatrice/resources/icons/dragon.svg @@ -1,6 +1 @@ - - - - - - + \ No newline at end of file diff --git a/cockatrice/resources/icons/dropdown_collapsed.svg b/cockatrice/resources/icons/dropdown_collapsed.svg index 429c24974..52e833e65 100644 --- a/cockatrice/resources/icons/dropdown_collapsed.svg +++ b/cockatrice/resources/icons/dropdown_collapsed.svg @@ -1,3 +1,10 @@ - - + + + + + ]> + + diff --git a/cockatrice/resources/icons/dropdown_expanded.svg b/cockatrice/resources/icons/dropdown_expanded.svg index 0b24aeb48..fa3ee205c 100644 --- a/cockatrice/resources/icons/dropdown_expanded.svg +++ b/cockatrice/resources/icons/dropdown_expanded.svg @@ -1,3 +1,10 @@ - - + + + + + ]> + + diff --git a/cockatrice/resources/icons/filter.svg b/cockatrice/resources/icons/filter.svg index d9cdf879c..d53d9ba29 100644 --- a/cockatrice/resources/icons/filter.svg +++ b/cockatrice/resources/icons/filter.svg @@ -1,3 +1,12 @@ - - - + + + + + + + + \ No newline at end of file diff --git a/cockatrice/resources/icons/floppy_disk.svg b/cockatrice/resources/icons/floppy_disk.svg index 10b9e8172..3c5b5054d 100644 --- a/cockatrice/resources/icons/floppy_disk.svg +++ b/cockatrice/resources/icons/floppy_disk.svg @@ -1,5 +1 @@ - - - - - + \ No newline at end of file diff --git a/cockatrice/resources/icons/forgot_password.svg b/cockatrice/resources/icons/forgot_password.svg index 3bfb3bdb1..f938ca3f4 100644 --- a/cockatrice/resources/icons/forgot_password.svg +++ b/cockatrice/resources/icons/forgot_password.svg @@ -1,6 +1,6 @@ - - - - - + + + + + diff --git a/cockatrice/resources/icons/gear.svg b/cockatrice/resources/icons/gear.svg new file mode 100644 index 000000000..fdee3a297 --- /dev/null +++ b/cockatrice/resources/icons/gear.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/cockatrice/resources/icons/increment.svg b/cockatrice/resources/icons/increment.svg index 52dbdb2aa..6d5f7a85a 100644 --- a/cockatrice/resources/icons/increment.svg +++ b/cockatrice/resources/icons/increment.svg @@ -1,4 +1,199 @@ - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + diff --git a/cockatrice/resources/icons/info.svg b/cockatrice/resources/icons/info.svg index 1305f6b3d..00b23cc2c 100644 --- a/cockatrice/resources/icons/info.svg +++ b/cockatrice/resources/icons/info.svg @@ -1,5 +1,43 @@ - - - - + + + + + + + + + + diff --git a/cockatrice/resources/icons/lock.svg b/cockatrice/resources/icons/lock.svg index f150ece49..b7ebf1a53 100644 --- a/cockatrice/resources/icons/lock.svg +++ b/cockatrice/resources/icons/lock.svg @@ -1,4 +1,750 @@ - - - + + + + + + + + + padlock + + + + icon + + lock + security + secure + padlock + signs_and_symbols + + + + + AJ Ashton + + + + + AJ Ashton + + + + + AJ Ashton + + + + image/svg+xml + + + en + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/cockatrice/resources/icons/not_ready_start.svg b/cockatrice/resources/icons/not_ready_start.svg index c20a5cfe6..73afda9db 100644 --- a/cockatrice/resources/icons/not_ready_start.svg +++ b/cockatrice/resources/icons/not_ready_start.svg @@ -1,4 +1,368 @@ - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + diff --git a/cockatrice/resources/icons/pen_to_square.svg b/cockatrice/resources/icons/pen_to_square.svg index 7d3e161ac..60ff1d9c4 100644 --- a/cockatrice/resources/icons/pen_to_square.svg +++ b/cockatrice/resources/icons/pen_to_square.svg @@ -1,4 +1 @@ - - - - + \ No newline at end of file diff --git a/cockatrice/resources/icons/pencil.svg b/cockatrice/resources/icons/pencil.svg index d274e98db..c8b81f698 100644 --- a/cockatrice/resources/icons/pencil.svg +++ b/cockatrice/resources/icons/pencil.svg @@ -1,4 +1,123 @@ - - - + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + diff --git a/cockatrice/resources/icons/pin.svg b/cockatrice/resources/icons/pin.svg index 9c173286b..71d4c9c9c 100644 --- a/cockatrice/resources/icons/pin.svg +++ b/cockatrice/resources/icons/pin.svg @@ -1,4 +1,24 @@ - - - + + + + + + diff --git a/cockatrice/resources/icons/player.svg b/cockatrice/resources/icons/player.svg index 9fbe39a11..e83b24654 100644 --- a/cockatrice/resources/icons/player.svg +++ b/cockatrice/resources/icons/player.svg @@ -1,8 +1,142 @@ - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/cockatrice/resources/icons/ready_start.svg b/cockatrice/resources/icons/ready_start.svg index 4c4ea5d2d..238d2aa46 100644 --- a/cockatrice/resources/icons/ready_start.svg +++ b/cockatrice/resources/icons/ready_start.svg @@ -1,3 +1,81 @@ - - + + + + + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/cockatrice/resources/icons/reload.svg b/cockatrice/resources/icons/reload.svg index e09b1b7ef..102995876 100644 --- a/cockatrice/resources/icons/reload.svg +++ b/cockatrice/resources/icons/reload.svg @@ -1,4 +1,7 @@ - - - - + + + + Svg Vector Icons : http://www.onlinewebfonts.com/icon + + \ No newline at end of file diff --git a/cockatrice/resources/icons/remove_row.svg b/cockatrice/resources/icons/remove_row.svg index 5d00ed57c..6f133cdb6 100644 --- a/cockatrice/resources/icons/remove_row.svg +++ b/cockatrice/resources/icons/remove_row.svg @@ -1,4 +1,356 @@ - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + diff --git a/cockatrice/resources/icons/rename.svg b/cockatrice/resources/icons/rename.svg index bab850808..c68a00246 100644 --- a/cockatrice/resources/icons/rename.svg +++ b/cockatrice/resources/icons/rename.svg @@ -1,7 +1,10 @@ - - - - - - + + + + + + + diff --git a/cockatrice/resources/icons/scale_balanced.svg b/cockatrice/resources/icons/scale_balanced.svg new file mode 100644 index 000000000..fae468d38 --- /dev/null +++ b/cockatrice/resources/icons/scale_balanced.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/cockatrice/resources/icons/scales.svg b/cockatrice/resources/icons/scales.svg index 4b2e16fcc..a410921dd 100644 --- a/cockatrice/resources/icons/scales.svg +++ b/cockatrice/resources/icons/scales.svg @@ -1,7 +1,6 @@ - - - - - - + + + + + diff --git a/cockatrice/resources/icons/scroll.svg b/cockatrice/resources/icons/scroll.svg index edcf29d11..f4828e679 100644 --- a/cockatrice/resources/icons/scroll.svg +++ b/cockatrice/resources/icons/scroll.svg @@ -1,6 +1 @@ - - - - - - + \ No newline at end of file diff --git a/cockatrice/resources/icons/search.svg b/cockatrice/resources/icons/search.svg index be4a87f00..05439a12e 100644 --- a/cockatrice/resources/icons/search.svg +++ b/cockatrice/resources/icons/search.svg @@ -1,4 +1,198 @@ - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + \ No newline at end of file diff --git a/cockatrice/resources/icons/settings.svg b/cockatrice/resources/icons/settings.svg index 3942a0916..77a2ccfa9 100644 --- a/cockatrice/resources/icons/settings.svg +++ b/cockatrice/resources/icons/settings.svg @@ -1,4 +1,280 @@ - - - + + + + + + + + Ruota dentata grigia + + + + unsorted + + + + + Open Clip Art Library + + + + + Architetto Francesco Rollandin + + + + + Architetto Francesco Rollandin + + + + image/svg+xml + + + en + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/cockatrice/resources/icons/share.svg b/cockatrice/resources/icons/share.svg index 9d6ff21a0..a84f55847 100644 --- a/cockatrice/resources/icons/share.svg +++ b/cockatrice/resources/icons/share.svg @@ -1,7 +1,25 @@ - - - - - - + + + + + + + + + diff --git a/cockatrice/resources/icons/sort_arrow_down.svg b/cockatrice/resources/icons/sort_arrow_down.svg index e10b401f2..adf10f9f0 100644 --- a/cockatrice/resources/icons/sort_arrow_down.svg +++ b/cockatrice/resources/icons/sort_arrow_down.svg @@ -1,7 +1 @@ - - - - - - - + \ No newline at end of file diff --git a/cockatrice/resources/icons/spectator.svg b/cockatrice/resources/icons/spectator.svg index 19242f058..c580c448d 100644 --- a/cockatrice/resources/icons/spectator.svg +++ b/cockatrice/resources/icons/spectator.svg @@ -1,4 +1,106 @@ - - - + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/cockatrice/resources/icons/swap.svg b/cockatrice/resources/icons/swap.svg index 4eddb8c08..a18ca2f06 100644 --- a/cockatrice/resources/icons/swap.svg +++ b/cockatrice/resources/icons/swap.svg @@ -1,6 +1,12 @@ - - - - - - + + + + + + + + \ No newline at end of file diff --git a/cockatrice/resources/icons/sync.svg b/cockatrice/resources/icons/sync.svg index fa00b3024..e287b1993 100644 --- a/cockatrice/resources/icons/sync.svg +++ b/cockatrice/resources/icons/sync.svg @@ -1,6 +1,6 @@ - - - - - + + + + + diff --git a/cockatrice/resources/icons/tab_changed.svg b/cockatrice/resources/icons/tab_changed.svg index 1d9b8d09c..ae09ef8fd 100644 --- a/cockatrice/resources/icons/tab_changed.svg +++ b/cockatrice/resources/icons/tab_changed.svg @@ -1,4 +1,116 @@ - - - + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/cockatrice/resources/icons/update.png b/cockatrice/resources/icons/update.png new file mode 100644 index 000000000..825c0aee7 Binary files /dev/null and b/cockatrice/resources/icons/update.png differ diff --git a/cockatrice/resources/icons/view.svg b/cockatrice/resources/icons/view.svg index 19242f058..e5676d825 100644 --- a/cockatrice/resources/icons/view.svg +++ b/cockatrice/resources/icons/view.svg @@ -1,4 +1,29 @@ - - - - + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + \ No newline at end of file diff --git a/cockatrice/src/client/network/interfaces/deck_analysis_url.h b/cockatrice/src/client/network/interfaces/deck_analysis_url.h deleted file mode 100644 index 425056270..000000000 --- a/cockatrice/src/client/network/interfaces/deck_analysis_url.h +++ /dev/null @@ -1,42 +0,0 @@ -/** - * @file deck_analysis_url.h - * @ingroup ApiInterfaces - */ - -#ifndef DECK_ANALYSIS_URL_H -#define DECK_ANALYSIS_URL_H - -#include -#include - -/** - * Validates a deck address returned by a deck analysis service. - * - * Only the expected host (optionally "www."-prefixed), HTTPS, default ports and - * URLs without user information are accepted. Legacy HTTP addresses for the same - * service are upgraded to HTTPS in place instead of being rejected. - * - * Relative addresses must be resolved against the response URL by the caller. - * - * @return true when @p url names a deck on @p serviceHost; false otherwise. - */ -inline bool normalizeAnalysisUrl(QUrl &url, const QString &serviceHost) -{ - const QString host = url.host().toLower(); - const bool legacyHttp = url.scheme() == QStringLiteral("http"); - const int defaultPort = legacyHttp ? 80 : 443; - if (!url.isValid() || (!legacyHttp && url.scheme() != QStringLiteral("https")) || !url.userInfo().isEmpty() || - !url.path().startsWith('/') || (url.port() != -1 && url.port() != defaultPort) || - (host != serviceHost && host != QStringLiteral("www.") + serviceHost)) { - return false; - } - // Both services accept HTTPS. Older public metadata may still identify the - // same deck with HTTP; retain its host/path/query while using HTTPS to open it. - if (legacyHttp) { - url.setScheme(QStringLiteral("https")); - url.setPort(-1); - } - return true; -} - -#endif diff --git a/cockatrice/src/client/network/interfaces/deck_stats_interface.cpp b/cockatrice/src/client/network/interfaces/deck_stats_interface.cpp index 22be120c4..508f0bdc9 100644 --- a/cockatrice/src/client/network/interfaces/deck_stats_interface.cpp +++ b/cockatrice/src/client/network/interfaces/deck_stats_interface.cpp @@ -1,6 +1,5 @@ #include "deck_stats_interface.h" -#include "deck_analysis_url.h" #include "libcockatrice/card/card_info.h" #include "libcockatrice/card/database/card_database_querier.h" #include "libcockatrice/deck_list/tree/inner_deck_list_node.h" @@ -11,14 +10,12 @@ #include #include #include -#include #include #include #include #include -#include #include -#include +#include #include #include #include @@ -32,56 +29,30 @@ DeckStatsInterface::DeckStatsInterface(QObject *parent) : QObject(parent) connect(manager, &QNetworkAccessManager::finished, this, &DeckStatsInterface::queryFinished); } -void DeckStatsInterface::showError(const QString &message) -{ - // The notice belongs to the originating editor, not this one-shot request. - // Retire the request before opening UI: opening a window may reenter Qt. - QWidget *owner = qobject_cast(parent()); - auto *notice = new QMessageBox(QMessageBox::Critical, tr("Deck analysis"), message, QMessageBox::Ok, owner); - notice->setTextFormat(Qt::PlainText); - notice->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextSelectableByKeyboard); - notice->setAttribute(Qt::WA_DeleteOnClose); - deleteLater(); - notice->open(); -} - void DeckStatsInterface::queryFinished(QNetworkReply *reply) { - if (replyHandled) { - return; - } - replyHandled = true; - // All terminal paths release the reply before showing UI or invoking an - // external URL handler, either of which may destroy the originating editor. - reply->deleteLater(); if (reply->error() != QNetworkReply::NoError) { - showError(reply->errorString()); + QMessageBox::critical(nullptr, tr("Error"), reply->errorString()); + reply->deleteLater(); + deleteLater(); return; } - const QString data = QString::fromUtf8(reply->readAll()); - static const QRegularExpression rx( - R"(]*\bproperty\s*=\s*(['"])og:url\1)[^>]*\bcontent\s*=\s*(['"])(.*?)\2)", - QRegularExpression::CaseInsensitiveOption | QRegularExpression::DotMatchesEverythingOption); - const auto match = rx.match(data); + QString data(reply->readAll()); + reply->deleteLater(); + + static const QRegularExpression rx("url().resolved(QUrl(address, QUrl::StrictMode)); - if (address.trimmed().isEmpty() || !normalizeAnalysisUrl(deckUrl, QStringLiteral("deckstats.net"))) { - showError(tr("The reply from Deckstats contained an invalid deck address.")); - return; - } + QString deckUrl = match.captured(1); + QDesktopServices::openUrl(deckUrl); - QPointer guard(this); deleteLater(); - if (!QDesktopServices::openUrl(deckUrl) && guard) { - showError(tr("Could not open your browser. Open this address to see the analyzed deck:\n%1") - .arg(deckUrl.toDisplayString())); - } } void DeckStatsInterface::getAnalyzeRequestData(const DeckList &deck, QByteArray &data) @@ -89,8 +60,12 @@ void DeckStatsInterface::getAnalyzeRequestData(const DeckList &deck, QByteArray DeckList deckWithoutTokens; copyDeckWithoutTokens(deck, deckWithoutTokens); - data.append("deck=" + QUrl::toPercentEncoding(deckWithoutTokens.writeToString_Plain())); - data.append("&decktitle=" + QUrl::toPercentEncoding(deck.getName())); + QUrl params; + QUrlQuery urlQuery; + urlQuery.addQueryItem("deck", deckWithoutTokens.writeToString_Plain()); + urlQuery.addQueryItem("decktitle", deck.getName()); + params.setQuery(urlQuery); + data.append(params.query(QUrl::EncodeReserved).toUtf8()); } void DeckStatsInterface::analyzeDeck(const DeckList &deck) @@ -109,9 +84,7 @@ void DeckStatsInterface::copyDeckWithoutTokens(const DeckList &source, DeckList { auto copyIfNotAToken = [&destination](const auto node, const auto card) { CardInfoPtr dbCard = CardDatabaseManager::query()->getCardInfo(card->getName()); - // Missing local card data is not evidence that a card is a token. Keep - // its original name and quantity so the service analyzes the whole deck. - if ((!dbCard || !dbCard->getIsToken()) && node->getName() != DECK_ZONE_MAYBEBOARD) { + if (dbCard && !dbCard->getIsToken() && node->getName() != DECK_ZONE_MAYBEBOARD) { DecklistCardNode *addedCard = destination.addCard(card->getName(), node->getName(), -1); addedCard->setNumber(card->getNumber()); } diff --git a/cockatrice/src/client/network/interfaces/deck_stats_interface.h b/cockatrice/src/client/network/interfaces/deck_stats_interface.h index b70646403..e7791075c 100644 --- a/cockatrice/src/client/network/interfaces/deck_stats_interface.h +++ b/cockatrice/src/client/network/interfaces/deck_stats_interface.h @@ -11,7 +11,6 @@ #include class QByteArray; -class QString; class QNetworkAccessManager; class QNetworkReply; class DeckList; @@ -21,8 +20,6 @@ class DeckStatsInterface : public QObject Q_OBJECT private: QNetworkAccessManager *manager; - bool replyHandled = false; - void showError(const QString &message); /** * Deckstats doesn't recognize token cards, and instead tries to find the diff --git a/cockatrice/src/client/network/interfaces/tapped_out_interface.cpp b/cockatrice/src/client/network/interfaces/tapped_out_interface.cpp index a57877b6a..34dd03765 100644 --- a/cockatrice/src/client/network/interfaces/tapped_out_interface.cpp +++ b/cockatrice/src/client/network/interfaces/tapped_out_interface.cpp @@ -1,6 +1,5 @@ #include "tapped_out_interface.h" -#include "deck_analysis_url.h" #include "libcockatrice/card/card_info.h" #include "libcockatrice/card/database/card_database_querier.h" #include "libcockatrice/deck_list/tree/inner_deck_list_node.h" @@ -14,17 +13,15 @@ #include #include #include -#include #include #include #include #include #include #include -#include #include +#include #include -#include #include #include #include @@ -38,67 +35,54 @@ TappedOutInterface::TappedOutInterface(QObject *parent) : QObject(parent) connect(manager, &QNetworkAccessManager::finished, this, &TappedOutInterface::queryFinished); } -void TappedOutInterface::showError(const QString &message) -{ - // The notice belongs to the originating editor, not this one-shot request. - // Retire the request before opening UI: opening a window may reenter Qt. - QWidget *owner = qobject_cast(parent()); - auto *notice = new QMessageBox(QMessageBox::Critical, tr("Deck analysis"), message, QMessageBox::Ok, owner); - notice->setTextFormat(Qt::PlainText); - notice->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextSelectableByKeyboard); - notice->setAttribute(Qt::WA_DeleteOnClose); - deleteLater(); - notice->open(); -} - void TappedOutInterface::queryFinished(QNetworkReply *reply) { - if (replyHandled) { - return; - } - replyHandled = true; - // All terminal paths release the reply before showing UI or invoking an - // external URL handler, either of which may destroy the originating editor. - reply->deleteLater(); if (reply->error() != QNetworkReply::NoError) { - showError(reply->errorString()); - return; - } - - const int httpStatus = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); - if (httpStatus >= 200 && httpStatus < 400 && reply->hasRawHeader("Location")) { - const QByteArray location = reply->rawHeader("Location"); - QUrl deckUrl = reply->url().resolved(QUrl::fromEncoded(location, QUrl::StrictMode)); - if (location.trimmed().isEmpty() || !normalizeAnalysisUrl(deckUrl, QStringLiteral("tappedout.net"))) { - showError(tr("The reply from TappedOut contained an invalid deck address.")); - return; - } - QPointer guard(this); + QMessageBox::critical(nullptr, tr("Error"), reply->errorString()); + reply->deleteLater(); deleteLater(); - if (!QDesktopServices::openUrl(deckUrl) && guard) { - showError(tr("Could not open your browser. Open this address to see the analyzed deck:\n%1") - .arg(deckUrl.toDisplayString())); - } return; } - const QString data = QString::fromUtf8(reply->readAll()); - QStringList errorMessageList = {tr("Unable to analyze the deck on TappedOut.")}; - static const QRegularExpression errorsRx("
(.*?)", - QRegularExpression::DotMatchesEverythingOption); - const auto match = errorsRx.match(data); - if (match.hasMatch()) { - static const QRegularExpression itemRx("
  • (.*?)
  • ", QRegularExpression::DotMatchesEverythingOption); - auto items = itemRx.globalMatch(match.captured(1)); - while (items.hasNext()) { - const QString error = QTextDocumentFragment::fromHtml(items.next().captured(1)).toPlainText().simplified(); - if (!error.isEmpty()) { - errorMessageList.append(error); + int httpStatus = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + if (reply->hasRawHeader("Location")) { + /* + * If the reply contains a "Location" header, a relative URL to the deck on TappedOut + * can be extracted from the header. The http status is a 302 "redirect". + */ + QString deckUrl = reply->rawHeader("Location"); + qCInfo(TappedOutInterfaceLog) << "Tappedout: good reply, http status" << httpStatus << "location" << deckUrl; + QDesktopServices::openUrl("https://tappedout.net" + deckUrl); + } else { + /* + * Otherwise, the deck has not been parsed correctly. Error messages can be extracted + * from the html. Css pseudo selector for errors: $("div.alert-danger > ul > li") + */ + QString data(reply->readAll()); + QStringList errorMessageList = {tr("Unable to analyze the deck.")}; + + static const QRegularExpression rx("
    (.*?)"); + auto match = rx.match(data); + if (match.hasMatch()) { + QString errors = match.captured(1); + static const QRegularExpression rx2("
  • (.*?)
  • "); + static const QRegularExpression rxremove("<[^>]*>"); + auto matchIterator = rx2.globalMatch(errors); + while (matchIterator.hasNext()) { + auto match2 = matchIterator.next(); + errorMessageList.append(match2.captured(1).remove(rxremove).simplified()); } } + + QString errorMessage = errorMessageList.join("\n"); + qCWarning(TappedOutInterfaceLog) << "Tappedout: bad reply, http status" << httpStatus << "size" << data.size() + << "message" << errorMessage; + + QMessageBox::critical(nullptr, tr("Error"), errorMessage); } - qCWarning(TappedOutInterfaceLog) << "TappedOut analysis failed, HTTP status" << httpStatus; - showError(errorMessageList.join('\n')); + + reply->deleteLater(); + deleteLater(); } void TappedOutInterface::getAnalyzeRequestData(const DeckList &deck, QByteArray &data) @@ -106,9 +90,13 @@ void TappedOutInterface::getAnalyzeRequestData(const DeckList &deck, QByteArray DeckList mainboard, sideboard; copyDeckSplitMainAndSide(deck, mainboard, sideboard); - data.append("name=" + QUrl::toPercentEncoding(deck.getName())); - data.append("&mainboard=" + QUrl::toPercentEncoding(mainboard.writeToString_Plain(false, true))); - data.append("&sideboard=" + QUrl::toPercentEncoding(sideboard.writeToString_Plain(false, true))); + QUrl params; + QUrlQuery urlQuery; + urlQuery.addQueryItem("name", deck.getName()); + urlQuery.addQueryItem("mainboard", mainboard.writeToString_Plain(false, true)); + urlQuery.addQueryItem("sideboard", sideboard.writeToString_Plain(false, true)); + params.setQuery(urlQuery); + data.append(params.query(QUrl::EncodeReserved).toUtf8()); } void TappedOutInterface::analyzeDeck(const DeckList &deck) @@ -129,9 +117,7 @@ void TappedOutInterface::copyDeckSplitMainAndSide(const DeckList &source, DeckLi { auto copyMainOrSide = [&mainboard, &sideboard](const auto node, const auto card) { CardInfoPtr dbCard = CardDatabaseManager::query()->getCardInfo(card->getName()); - // Exclude confirmed tokens, but preserve cards missing from the local - // database, including newly released cards in an imported deck. - if ((dbCard && dbCard->getIsToken()) || node->getName() == DECK_ZONE_MAYBEBOARD) { + if (!dbCard || dbCard->getIsToken() || node->getName() == DECK_ZONE_MAYBEBOARD) { return; } diff --git a/cockatrice/src/client/network/interfaces/tapped_out_interface.h b/cockatrice/src/client/network/interfaces/tapped_out_interface.h index 27e8c2273..615991b07 100644 --- a/cockatrice/src/client/network/interfaces/tapped_out_interface.h +++ b/cockatrice/src/client/network/interfaces/tapped_out_interface.h @@ -14,7 +14,6 @@ inline Q_LOGGING_CATEGORY(TappedOutInterfaceLog, "tapped_out_interface"); class QByteArray; -class QString; class QNetworkAccessManager; class QNetworkReply; class DeckList; @@ -30,8 +29,6 @@ class TappedOutInterface : public QObject Q_OBJECT private: QNetworkAccessManager *manager; - bool replyHandled = false; - void showError(const QString &message); void copyDeckSplitMainAndSide(const DeckList &source, DeckList &mainboard, DeckList &sideboard); private slots: diff --git a/cockatrice/src/client/network/parsers/deck_link_to_api_transformer.cpp b/cockatrice/src/client/network/parsers/deck_link_to_api_transformer.cpp index 2b307735d..7127b0a29 100644 --- a/cockatrice/src/client/network/parsers/deck_link_to_api_transformer.cpp +++ b/cockatrice/src/client/network/parsers/deck_link_to_api_transformer.cpp @@ -15,18 +15,14 @@ static const QString ARCHIDEKT_SUFFIX = "/?format=json"; static const QString MOXFIELD_BASE = "https://api.moxfield.com/v2/decks/all/"; static const QString MOXFIELD_SUFFIX = "/"; -static const QString DECKSTATS_BASE = "https://deckstats.net/decks/"; static const QString DECKSTATS_SUFFIX = "?include_comments=1&export_mtgarena=1"; -static const QString MANABOX_BASE = "https://cloud.manabox.app/decks/"; - bool parseDeckUrl(const QString &url, ParsedDeckInfo &outInfo) { static QRegularExpression rxTappedOut("tappedout\\.net/(?:mtg-decks/)?([^/?#]+)"); static QRegularExpression rxArchidekt("archidekt\\.com/decks/(\\d+)"); static QRegularExpression rxMoxfield("moxfield\\.com/decks/([a-zA-Z0-9_-]+)"); static QRegularExpression rxDeckstats("deckstats\\.net/decks/(\\d+/[a-zA-Z0-9_-]+)"); - static QRegularExpression rxManabox("manabox\\.app/decks/([a-zA-Z0-9_-]+)"); QRegularExpressionMatch match; @@ -53,18 +49,11 @@ bool parseDeckUrl(const QString &url, ParsedDeckInfo &outInfo) return true; } else if ((match = rxDeckstats.match(url)).hasMatch()) { QString deckPath = match.captured(1); - outInfo = ParsedDeckInfo{.baseUrl = DECKSTATS_BASE, + outInfo = ParsedDeckInfo{.baseUrl = "https://deckstats.net/decks/", .deckID = deckPath, - .fullUrl = DECKSTATS_BASE + deckPath + DECKSTATS_SUFFIX, + .fullUrl = "https://deckstats.net/decks/" + deckPath + DECKSTATS_SUFFIX, .provider = DeckProvider::Deckstats}; return true; - } else if ((match = rxManabox.match(url)).hasMatch()) { - QString deckPath = match.captured(1); - outInfo = ParsedDeckInfo{.baseUrl = MANABOX_BASE, - .deckID = deckPath, - .fullUrl = MANABOX_BASE + deckPath, - .provider = DeckProvider::Manabox}; - return true; } return false; diff --git a/cockatrice/src/client/network/parsers/deck_link_to_api_transformer.h b/cockatrice/src/client/network/parsers/deck_link_to_api_transformer.h index d3bff976f..bbdd70c97 100644 --- a/cockatrice/src/client/network/parsers/deck_link_to_api_transformer.h +++ b/cockatrice/src/client/network/parsers/deck_link_to_api_transformer.h @@ -15,7 +15,6 @@ enum class DeckProvider Archidekt, Moxfield, Deckstats, - Manabox, Unknown }; diff --git a/cockatrice/src/client/network/parsers/interface_json_deck_parser.h b/cockatrice/src/client/network/parsers/interface_json_deck_parser.h index 7e15d472f..87fde4d54 100644 --- a/cockatrice/src/client/network/parsers/interface_json_deck_parser.h +++ b/cockatrice/src/client/network/parsers/interface_json_deck_parser.h @@ -8,19 +8,12 @@ #define INTERFACE_JSON_DECK_PARSER_H #include "../../../interface/deck_loader/card_node_function.h" -#include "libcockatrice/deck_list/tree/inner_deck_list_node.h" #include #include -#include -#include -#include -#include #include #include -inline Q_LOGGING_CATEGORY(InterfaceJsonDeckParserLog, "interface_json_deck_parser"); - class IJsonDeckParser { public: @@ -39,6 +32,9 @@ public: QString deckName = obj.value("name").toString(); QString deckDescription = obj.value("description").toString(); + deckList.setName(deckName); + deckList.setComments(deckDescription); + QString outputText; QTextStream outStream(&outputText); @@ -54,14 +50,8 @@ public: outStream << quantity << ' ' << cardName << " (" << setName << ") " << collectorNumber << '\n'; } - outStream.flush(); - outStream.seek(0); deckList.loadFromStream_Plain(outStream, false, CardNameNormalizer()); - deckList.setName(deckName); - deckList.setComments(deckDescription); deckList.forEachCard(CardNodeFunction::ResolveProviderId()); - deckList.setName(deckName); - deckList.setComments(deckDescription); return deckList; } @@ -77,6 +67,9 @@ public: QString deckName = obj.value("name").toString(); QString deckDescription = obj.value("description").toString(); + deckList.setName(deckName); + deckList.setComments(deckDescription); + QString outputText; QTextStream outStream(&outputText); @@ -104,14 +97,8 @@ public: outStream << quantity << ' ' << cardName << " (" << setName << ") " << collectorNumber << '\n'; } - outStream.flush(); - outStream.seek(0); deckList.loadFromStream_Plain(outStream, false, CardNameNormalizer()); - deckList.setName(deckName); - deckList.setComments(deckDescription); deckList.forEachCard(CardNodeFunction::ResolveProviderId()); - deckList.setName(deckName); - deckList.setComments(deckDescription); QJsonObject commandersObj = obj.value("commanders").toObject(); if (!commandersObj.isEmpty()) { @@ -127,113 +114,6 @@ public: } } - // Moxfield stores Oathbreaker signature spells in their own board, - // separate from both mainboard and commanders. They are playable deck - // cards, but must not replace the Oathbreaker as the banner card. - const QJsonObject signatureSpells = obj.value("signatureSpells").toObject(); - for (auto it = signatureSpells.begin(); it != signatureSpells.end(); ++it) { - const QJsonObject entry = it.value().toObject(); - const QJsonObject card = entry.value("card").toObject(); - const QString name = card.value("name").toString(); - const int quantity = entry.value("quantity").toInt(); - if (name.isEmpty() || quantity <= 0) { - continue; - } - const QString set = card.value("set").toString().toUpper(); - const QString collectorNumber = card.value("cn").toString(); - const QString providerId = card.value("scryfall_id").toString(); - deckList.addCards(name, quantity, DECK_ZONE_MAIN, -1, set, collectorNumber, providerId); - } - - return deckList; - } -}; - -class ManaboxJsonParser : public IJsonDeckParser -{ - enum BoardType : quint32 - { - MainBoard = 0, - SideBoard = 1, - Commander = 2, - MaybeBoard = 3, - }; - -public: - DeckList parse(const QJsonObject &obj) override - { - DeckList deckList; - - QString deckName = obj.value("name").toString(); - - QString outputText; - QString mainboardText; - QString sideboardText; - QTextStream mainboardStream(&mainboardText); - QTextStream sideboardStream(&sideboardText); - QTextStream outStream(&outputText); - - auto commanderList = QList(); - auto maybeboardList = QList(); - for (auto entry : obj.value("cards").toArray()) { - auto card = entry.toObject(); - - auto collectorNumber = card.value("collectorNumber").toString(); - auto cardName = card.value("name").toString(); - auto boardType = static_cast(card.value("boardCategory").toInt()); - auto setName = card.value("setId").toString(); - auto quantity = card.value("quantity").toInt(); - auto providerId = card.value("scryfallId").toString(); - - switch (boardType) { - case MainBoard: - mainboardStream << quantity << ' ' << cardName << " (" << setName << ") " << collectorNumber - << '\n'; - break; - case SideBoard: - sideboardStream << "SB: " << quantity << ' ' << cardName << " (" << setName << ") " - << collectorNumber << '\n'; - break; - case MaybeBoard: - maybeboardList.append(card); - break; - case Commander: - commanderList.append(card); - break; - default: - qCWarning(InterfaceJsonDeckParserLog) - << "Unknown ManaBox Zone" << card.value("boardCategory").toInt() << "for" << cardName; - break; - } - } - - outStream << mainboardText << sideboardText; - - deckList.loadFromStream_Plain(outStream, false, CardNameNormalizer()); - deckList.setName(deckName); - deckList.forEachCard(CardNodeFunction::ResolveProviderId()); - - for (const auto &maybeboardCard : maybeboardList) { - QString cardName = maybeboardCard.value("name").toString(); - QString setName = maybeboardCard.value("setId").toString().toUpper(); - QString collectorNumber = maybeboardCard.value("collectorNumber").toString(); - QString providerId = maybeboardCard.value("scryfallId").toString(); - - for (int x = 0; x < maybeboardCard.value("quantity").toInt(); x++) { - deckList.addCard(cardName, DECK_ZONE_MAYBEBOARD, -1, setName, collectorNumber, providerId); - } - } - for (const auto &commanderCard : commanderList) { - - QString commanderName = commanderCard.value("name").toString(); - QString setName = commanderCard.value("setId").toString().toUpper(); - QString collectorNumber = commanderCard.value("collectorNumber").toString(); - QString providerId = commanderCard.value("scryfallId").toString(); - - deckList.setBannerCard({commanderName, providerId}); - deckList.addCard(commanderName, DECK_ZONE_MAIN, -1, setName, collectorNumber, providerId); - } - return deckList; } }; diff --git a/cockatrice/src/client/network/update/card_spoiler/spoiler_background_updater.cpp b/cockatrice/src/client/network/update/card_spoiler/spoiler_background_updater.cpp index d74414d4a..62668c4d9 100644 --- a/cockatrice/src/client/network/update/card_spoiler/spoiler_background_updater.cpp +++ b/cockatrice/src/client/network/update/card_spoiler/spoiler_background_updater.cpp @@ -2,305 +2,253 @@ #include "../../../../main.h" #include "../../../settings/cache_settings.h" -#include "spoiler_file.h" -#include +#include +#include +#include #include +#include #include #include #include +#include +#include +#include +#include #include #include #include +#include #include -#include +#include #include #include -#include +#include +#include #include #include #include #include #include +#include +#include #include -namespace -{ -const QUrl SPOILERS_STATUS_URL( - QStringLiteral("https://raw.githubusercontent.com/Cockatrice/Magic-Spoiler/files/SpoilerSeasonEnabled")); -const QUrl SPOILERS_URL(QStringLiteral("https://raw.githubusercontent.com/Cockatrice/Magic-Spoiler/files/spoiler.xml")); -// Request coordination stays on the GUI thread; reload workers never access it. -QPointer currentUpdater; -QPointer> activeReload; -quint64 newestOperation = 0; +#define SPOILERS_STATUS_URL "https://raw.githubusercontent.com/Cockatrice/Magic-Spoiler/files/SpoilerSeasonEnabled" +#define SPOILERS_URL "https://raw.githubusercontent.com/Cockatrice/Magic-Spoiler/files/spoiler.xml" -bool removeSpoilerFile(const QString &path) +SpoilerBackgroundUpdater::SpoilerBackgroundUpdater(QObject *apParent) : QObject(apParent), cardUpdateProcess(nullptr) { - QFile file(path); - if (!file.exists()) { - return false; - } - if (!file.remove()) { - qCWarning(SpoilerBackgroundUpdaterLog) << "Could not remove spoiler database" << path << file.errorString(); - return false; - } - return true; -} -} // namespace - -SpoilerBackgroundUpdater::SpoilerBackgroundUpdater(QObject *parent, bool forceReload) - : QObject(parent), enabledAtStart(SettingsCache::instance().downloads().getDownloadSpoilersStatus()), - forceReload(forceReload), targetPath(SettingsCache::instance().paths().getSpoilerCardDatabasePath()), - operationId(++newestOperation), manager(NetworkManagerFactory::create(this)), notificationIcon(trayIcon) -{ - connect(&SettingsCache::instance().downloads(), &DownloadSettings::downloadSpoilerStatusChanged, this, - &SpoilerBackgroundUpdater::cancelIfSettingsChanged); - connect(&SettingsCache::instance().paths(), &PathsSettings::cardDatabasePathChanged, this, - &SpoilerBackgroundUpdater::cancelIfSettingsChanged); - // Even a disabled/no-op operation completes after its caller can connect signals. - QTimer::singleShot(0, this, &SpoilerBackgroundUpdater::begin); -} - -void SpoilerBackgroundUpdater::begin() -{ - if (completed) { - return; - } - if (cancelled || operationId != newestOperation || - (reloadOnly ? SettingsCache::instance().downloads().getDownloadSpoilersStatus() - : (!enabledAtStart || !SettingsCache::instance().downloads().getDownloadSpoilersStatus())) || - targetPath != SettingsCache::instance().paths().getSpoilerCardDatabasePath()) { - finish(); - return; - } - const auto previous = currentUpdater; - if (activeReload) { - // The existing database API stores its final load status after its parser - // mutex is released. Do not overlap updater-owned calls to that API. - connect(activeReload, &QFutureWatcher::finished, this, &SpoilerBackgroundUpdater::begin, - static_cast(Qt::QueuedConnection | Qt::SingleShotConnection)); - if (previous && previous != this) { - previous->cancel(); - } - return; - } - currentUpdater = this; - QPointer guard(this); - if (previous && previous != this) { - previous->cancel(); - } - if (!guard) { - return; - } - if (!isCurrent()) { - finish(); - } else if (reloadOnly) { - if (QFileInfo::exists(targetPath) && !removeSpoilerFile(targetPath)) { - finish(tr("The spoiler database could not be removed. Check its file permissions and use Reload Card " - "Database to retry.")); - return; - } - reloadDatabase(false); + isSpoilerDownloadEnabled = SettingsCache::instance().downloads().getDownloadSpoilersStatus(); + if (isSpoilerDownloadEnabled) { + // Start the process of checking if we're in spoiler season + // File exists means we're in spoiler season + startSpoilerDownloadProcess(SPOILERS_STATUS_URL, false); } else { - downloadFromURL(SPOILERS_STATUS_URL, false); + qCInfo(SpoilerBackgroundUpdaterLog) << "Spoilers Disabled"; } } -bool SpoilerBackgroundUpdater::isCurrent() const +void SpoilerBackgroundUpdater::startSpoilerDownloadProcess(QString url, bool saveResults) { - return !completed && !cancelled && currentUpdater == this && operationId == newestOperation && - (reloadOnly != SettingsCache::instance().downloads().getDownloadSpoilersStatus()) && - targetPath == SettingsCache::instance().paths().getSpoilerCardDatabasePath(); + auto spoilerURL = QUrl(url); + downloadFromURL(spoilerURL, saveResults); } -void SpoilerBackgroundUpdater::cancelIfSettingsChanged() +void SpoilerBackgroundUpdater::downloadFromURL(QUrl url, bool saveResults) { - if (reloadOnly == SettingsCache::instance().downloads().getDownloadSpoilersStatus() || - targetPath != SettingsCache::instance().paths().getSpoilerCardDatabasePath()) { - cancel(); - } -} - -void SpoilerBackgroundUpdater::cancel() -{ - if (completed) { - return; - } - cancelled = true; - const auto reply = activeReply; - activeReply.clear(); - QPointer guard(this); - if (reply) { - reply->abort(); - if (reply) { - reply->deleteLater(); - } - } - // A running reload owns no updater state. Allow its completion to retire its - // watcher before a heap-owned updater is released by the caller. - if (guard && !reloading) { - finish(); - } -} - -void SpoilerBackgroundUpdater::downloadFromURL(const QUrl &url, bool saveResults) -{ - QNetworkRequest request(url); + auto *nam = NetworkManagerFactory::create(this); + auto request = QNetworkRequest(url); request.setHeader(QNetworkRequest::UserAgentHeader, QString("Cockatrice %1").arg(VERSION_STRING)); - request.setTransferTimeout(60000); - activeReply = manager->get(request); - if (saveResults) { - connect(activeReply, &QNetworkReply::finished, this, - &SpoilerBackgroundUpdater::actDownloadFinishedSpoilersFile); - } else { - connect(activeReply, &QNetworkReply::finished, this, &SpoilerBackgroundUpdater::actCheckIfSpoilerSeasonEnabled); - } -} + QNetworkReply *reply = nam->get(request); -void SpoilerBackgroundUpdater::actCheckIfSpoilerSeasonEnabled() -{ - auto *reply = qobject_cast(sender()); - if (!reply || reply != activeReply) { - return; - } - activeReply.clear(); - reply->deleteLater(); - if (!isCurrent()) { - finish(); - return; - } - if (reply->error() == QNetworkReply::NoError) { - downloadFromURL(SPOILERS_URL, true); - } else if (reply->error() == QNetworkReply::ContentNotFoundError) { - const bool existed = QFileInfo::exists(targetPath); - seasonEnded = existed; - if (existed && !removeSpoilerFile(targetPath)) { - finish(tr("The spoiler season has ended, but the old spoiler database could not be removed.")); - return; - } - if (existed || forceReload) { - reloadDatabase(false); - } else { - finish(); - } + if (saveResults) { + // This will write out to the file (used for spoiler.xml) + connect(reply, &QNetworkReply::finished, this, &SpoilerBackgroundUpdater::actDownloadFinishedSpoilersFile); } else { - finish(tr("Could not check spoiler availability: %1").arg(reply->errorString())); + // This will check the status (used to see if we're in spoiler season or not) + connect(reply, &QNetworkReply::finished, this, &SpoilerBackgroundUpdater::actCheckIfSpoilerSeasonEnabled); } } void SpoilerBackgroundUpdater::actDownloadFinishedSpoilersFile() { - auto *reply = qobject_cast(sender()); - if (!reply || reply != activeReply) { - return; - } - activeReply.clear(); - reply->deleteLater(); - if (!isCurrent()) { - finish(); - return; - } - if (reply->error() != QNetworkReply::NoError) { - finish(tr("Could not download spoilers: %1").arg(reply->errorString())); - return; - } - spoilerData = reply->readAll(); - QString error; - const auto result = SpoilerFile::save(targetPath, spoilerData, [this] { return isCurrent(); }, error); - if (result == SpoilerFile::SaveResult::Failed) { - finish(isCurrent() ? error : QString()); - } else if (result == SpoilerFile::SaveResult::Written || forceReload) { - reloadDatabase(result == SpoilerFile::SaveResult::Written); + // Check for server reply + auto *reply = dynamic_cast(sender()); + QNetworkReply::NetworkError errorCode = reply->error(); + + if (errorCode == QNetworkReply::NoError) { + spoilerData = reply->readAll(); + + // Save the spoiler.xml file to the disk + saveDownloadedFile(spoilerData); + + reply->deleteLater(); + emit spoilerCheckerDone(); } else { - finish(); - } -} - -void SpoilerBackgroundUpdater::reloadDatabase(bool updated) -{ - reloading = true; - auto *database = CardDatabaseManager::getInstance(); - // The job outlives an individual request, so destroying that request cannot - // let a newer updater overlap the worker that is still completing. - auto *watcher = new QFutureWatcher(QCoreApplication::instance()); - activeReload = watcher; - connect(watcher, &QFutureWatcher::finished, watcher, [watcher] { - if (activeReload == watcher) { - activeReload.clear(); - } - watcher->deleteLater(); - }); - connect(watcher, &QFutureWatcher::finished, this, [this, watcher, database, updated] { - bool loaded = false; - try { - loaded = watcher->result() && database->getLoadStatus() == Ok; - } catch (const std::exception &error) { - qCWarning(SpoilerBackgroundUpdaterLog) << "Spoiler database reload failed:" << error.what(); - } - reloading = false; - if (!isCurrent()) { - finish(); - } else if (!loaded) { - finish(reloadOnly - ? tr("Could not reload card data after disabling spoilers. Use Reload Card Database to retry.") - : tr("Could not reload card data after updating spoilers. Use Update Spoilers to retry the " - "reload.")); - } else { - finish({}, updated); - } - }); - watcher->setFuture(QtConcurrent::run([database] { - database->reloadCardDatabasesAndNotify(); - return true; - })); -} - -void SpoilerBackgroundUpdater::finish(const QString &error, bool updated) -{ - if (completed) { - return; - } - completed = true; - spoilerData = QByteArray(); - if (currentUpdater == this) { - currentUpdater.clear(); - } - QPointer guard(this); - if (!error.isEmpty() && !cancelled) { - qCWarning(SpoilerBackgroundUpdaterLog) << error; - if (notificationIcon) { - notificationIcon->showMessage(tr("Spoiler update failed"), error); - } - if (!guard) { - return; - } - emit spoilerUpdateFailed(error); - } else if (updated && !cancelled) { - if (notificationIcon) { - notificationIcon->showMessage(tr("Spoilers have been updated!"), - tr("The card database now includes the downloaded spoilers.")); - } - } else if (seasonEnded && !cancelled && notificationIcon) { - notificationIcon->showMessage(tr("Spoiler season has ended"), - tr("The spoiler database was removed. Please run Oracle to get released cards.")); - } - if (guard) { + qCWarning(SpoilerBackgroundUpdaterLog) << "Error downloading spoilers file" << errorCode; emit spoilerCheckerDone(); } } -SpoilerBackgroundUpdater *SpoilerBackgroundUpdater::current() +bool SpoilerBackgroundUpdater::deleteSpoilerFile() { - return currentUpdater; + QString fileName = SettingsCache::instance().paths().getSpoilerCardDatabasePath(); + QFileInfo fi(fileName); + QDir fileDir(fi.path()); + QFile file(fileName); + + // Delete the spoiler.xml file + if (file.exists() && file.remove()) { + qCInfo(SpoilerBackgroundUpdaterLog) << "Deleting spoiler.xml"; + return true; + } + + qCInfo(SpoilerBackgroundUpdaterLog) << "Error: Spoiler.xml not found or not deleted"; + return false; } -SpoilerBackgroundUpdater *SpoilerBackgroundUpdater::deleteSpoilerFile() +void SpoilerBackgroundUpdater::actCheckIfSpoilerSeasonEnabled() { - // Deletion and the following reload share the operation lifetime. Callers can - // observe either failure before the queued operation starts, and an explicit - // retry still reloads when the file has already been removed. - auto *updater = new SpoilerBackgroundUpdater(QCoreApplication::instance(), true); - updater->reloadOnly = true; - connect(updater, &SpoilerBackgroundUpdater::spoilerCheckerDone, updater, &QObject::deleteLater); - return updater; + auto *response = dynamic_cast(sender()); + QNetworkReply::NetworkError errorCode = response->error(); + + if (errorCode == QNetworkReply::ContentNotFoundError) { + // Spoiler season is offline at this point, so the spoiler.xml file can be safely deleted + // The user should run Oracle to get the latest card information + if (deleteSpoilerFile() && trayIcon) { + trayIcon->showMessage(tr("Spoilers season has ended"), tr("Deleting spoiler.xml. Please run Oracle")); + } + + qCInfo(SpoilerBackgroundUpdaterLog) << "Spoiler Season Offline"; + emit spoilerCheckerDone(); + } else if (errorCode == QNetworkReply::NoError) { + qCInfo(SpoilerBackgroundUpdaterLog) << "Spoiler Service Online"; + startSpoilerDownloadProcess(SPOILERS_URL, true); + } else if (errorCode == QNetworkReply::HostNotFoundError) { + if (trayIcon) { + trayIcon->showMessage(tr("Spoilers download failed"), tr("No internet connection")); + } + + qCWarning(SpoilerBackgroundUpdaterLog) << "Spoiler download failed due to no internet connection"; + emit spoilerCheckerDone(); + } else { + if (trayIcon) { + trayIcon->showMessage(tr("Spoilers download failed"), tr("Error") + " " + (short)errorCode); + } + + qCWarning(SpoilerBackgroundUpdaterLog) << "Spoiler download failed with reason" << errorCode; + emit spoilerCheckerDone(); + } +} + +bool SpoilerBackgroundUpdater::saveDownloadedFile(QByteArray data) +{ + QString fileName = SettingsCache::instance().paths().getSpoilerCardDatabasePath(); + QFileInfo fi(fileName); + QDir fileDir(fi.path()); + + if (!fileDir.exists() && !fileDir.mkpath(fileDir.absolutePath())) { + return false; + } + + // Check if the data matches. If it does, then spoilers are up to date. + if (getHash(fileName) == getHash(data)) { + if (trayIcon) { + trayIcon->showMessage(tr("Spoilers already up to date"), tr("No new spoilers added")); + } + + qCInfo(SpoilerBackgroundUpdaterLog) << "Spoilers Up to Date"; + return false; + } + + QFile file(fileName); + if (!file.open(QIODevice::WriteOnly)) { + qCWarning(SpoilerBackgroundUpdaterLog) << "Spoiler Service Error: File open (w) failed for" << fileName; + file.close(); + return false; + } + + if (file.write(data) == -1) { + qCWarning(SpoilerBackgroundUpdaterLog) << "Spoiler Service Error: File write (w) failed for" << fileName; + file.close(); + return false; + } + + file.close(); + + // Data written, so reload the card database + qCInfo(SpoilerBackgroundUpdaterLog) << "Spoiler Service Data Written"; + // Spoilers can introduce sets the config has never seen; the loader never + // runs checkUnknownSets() itself, so run it once the finished snapshot has + // been swapped in. + auto *watcher = new QFutureWatcher(this); + connect(watcher, &QFutureWatcher::finished, this, [watcher] { + watcher->deleteLater(); + CardDatabaseManager::getInstance()->checkUnknownSets(); + }); + watcher->setFuture(QtConcurrent::run([] { CardDatabaseManager::getInstance()->loadCardDatabases(); })); + + // If the user has notifications enabled, let them know + // when the database was last updated + if (trayIcon) { + QList lines = data.split('\n'); + + for (const QByteArray &line : lines) { + if (line.contains("Created At:")) { + QString timeStamp = QString(line).replace("Created At:", "").trimmed(); + timeStamp.chop(6); // Remove " (UTC)" + + auto utcTime = QLocale().toDateTime(timeStamp, "ddd, MMM dd yyyy, hh:mm:ss"); +#if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0) + utcTime.setTimeZone(QTimeZone::UTC); +#else + utcTime.setTimeSpec(Qt::UTC); +#endif + + QString localTime = utcTime.toLocalTime().toString("MMM d, hh:mm"); + + trayIcon->showMessage(tr("Spoilers have been updated!"), tr("Last change:") + " " + localTime); + emit spoilersUpdatedSuccessfully(); + return true; + } + } + } + + return true; +} + +QByteArray SpoilerBackgroundUpdater::getHash(const QString fileName) +{ + QFile file(fileName); + + if (file.open(QFile::ReadOnly)) { + // Only read the first 512 bytes (enough to get the "created" tag) + const QByteArray bytes = file.read(512); + + QCryptographicHash hash(QCryptographicHash::Algorithm::Md5); + hash.addData(bytes); + + qCInfo(SpoilerBackgroundUpdaterLog) << "File Hash =" << hash.result(); + + file.close(); + return hash.result(); + } else { + qCWarning(SpoilerBackgroundUpdaterLog) << "getHash ReadOnly failed!"; + file.close(); + return QByteArray(); + } +} + +QByteArray SpoilerBackgroundUpdater::getHash(QByteArray data) +{ + // Only read the first 512 bytes (enough to get the "created" tag) + const QByteArray bytes = data.left(512); + + QCryptographicHash hash(QCryptographicHash::Algorithm::Md5); + hash.addData(bytes); + + qCInfo(SpoilerBackgroundUpdaterLog) << "Data Hash =" << hash.result(); + + return hash.result(); } diff --git a/cockatrice/src/client/network/update/card_spoiler/spoiler_background_updater.h b/cockatrice/src/client/network/update/card_spoiler/spoiler_background_updater.h index fd90eb688..a6c4ed00f 100644 --- a/cockatrice/src/client/network/update/card_spoiler/spoiler_background_updater.h +++ b/cockatrice/src/client/network/update/card_spoiler/spoiler_background_updater.h @@ -2,6 +2,7 @@ * @file spoiler_background_updater.h * @ingroup Client */ +//! \todo Document this file. #ifndef COCKATRICE_SPOILER_DOWNLOADER_H #define COCKATRICE_SPOILER_DOWNLOADER_H @@ -9,13 +10,10 @@ #include #include #include -#include #include #include -class QNetworkAccessManager; -class QNetworkReply; -class QSystemTrayIcon; +class QProcess; class QUrl; inline Q_LOGGING_CATEGORY(SpoilerBackgroundUpdaterLog, "spoiler_background_updater"); @@ -24,48 +22,30 @@ class SpoilerBackgroundUpdater : public QObject { Q_OBJECT public: - explicit SpoilerBackgroundUpdater(QObject *parent = nullptr, bool forceReload = false); + explicit SpoilerBackgroundUpdater(QObject *apParent = nullptr); inline QString getCardUpdaterBinaryName() { return "oracle"; } - // Queues removal and reload; the application owns the returned updater and - // deletes it on completion. Callers can connect its failure/completion signals. - static SpoilerBackgroundUpdater *deleteSpoilerFile(); - // The operation currently running, if any. The updater outlives the page that - // started it, so a reopened page uses this to observe the in-flight update. - static SpoilerBackgroundUpdater *current(); + QByteArray getHash(const QString fileName); + QByteArray getHash(QByteArray data); + static bool deleteSpoilerFile(); private slots: - void begin(); void actDownloadFinishedSpoilersFile(); void actCheckIfSpoilerSeasonEnabled(); - void cancelIfSettingsChanged(); private: - const bool enabledAtStart; - const bool forceReload; - const QString targetPath; - const quint64 operationId; - QNetworkAccessManager *manager; - QPointer activeReply; - QPointer notificationIcon; + bool isSpoilerDownloadEnabled; + QProcess *cardUpdateProcess; QByteArray spoilerData; - bool completed = false; - bool cancelled = false; - bool reloading = false; - bool seasonEnded = false; - bool reloadOnly = false; - - bool isCurrent() const; - void cancel(); - void finish(const QString &error = {}, bool updated = false); - void downloadFromURL(const QUrl &url, bool saveResults); - void reloadDatabase(bool updated); + void startSpoilerDownloadProcess(QString url, bool saveResults); + void downloadFromURL(QUrl url, bool saveResults); + bool saveDownloadedFile(QByteArray data); signals: + void spoilersUpdatedSuccessfully(); void spoilerCheckerDone(); - void spoilerUpdateFailed(const QString &message); }; -#endif // COCKATRICE_SPOILER_DOWNLOADER_H +#endif // COCKATRICE_SPOILER_DOWNLOADER_H \ No newline at end of file diff --git a/cockatrice/src/client/network/update/card_spoiler/spoiler_file.cpp b/cockatrice/src/client/network/update/card_spoiler/spoiler_file.cpp deleted file mode 100644 index 554ea1108..000000000 --- a/cockatrice/src/client/network/update/card_spoiler/spoiler_file.cpp +++ /dev/null @@ -1,109 +0,0 @@ -#include "spoiler_file.h" - -#include -#include -#include -#include -#include -#include -#include - -namespace -{ -QByteArray hashFile(const QString &path) -{ - QFile file(path); - if (!file.open(QIODevice::ReadOnly)) { - return {}; - } - QCryptographicHash hash(QCryptographicHash::Sha256); - return hash.addData(&file) ? hash.result() : QByteArray(); -} -} // namespace - -namespace SpoilerFile -{ -QString validate(const QByteArray &data) -{ - QXmlStreamReader xml(data); - if (!xml.readNextStartElement() || xml.name() != QStringLiteral("cockatrice_carddatabase")) { - return QCoreApplication::translate( - "SpoilerBackgroundUpdater", - "The download is not a Cockatrice card database. The previous spoiler file was preserved."); - } - bool versionOk = false; - const int version = xml.attributes().value(QStringLiteral("version")).toInt(&versionOk); - if (!versionOk || (version != 3 && version != 4)) { - return QCoreApplication::translate( - "SpoilerBackgroundUpdater", - "The spoiler database uses an unsupported format. The previous spoiler file was preserved."); - } - bool hasCards = false; - int depth = 1; - while (!xml.atEnd()) { - const auto token = xml.readNext(); - if (token == QXmlStreamReader::StartElement) { - ++depth; - if (depth == 2 && xml.name() == QStringLiteral("cards")) { - hasCards = true; - } - } else if (token == QXmlStreamReader::EndElement) { - --depth; - } - } - if (xml.hasError() || !hasCards) { - return QCoreApplication::translate( - "SpoilerBackgroundUpdater", - "The spoiler database is incomplete or invalid. The previous spoiler file was preserved."); - } - return {}; -} - -SaveResult -save(const QString &targetPath, const QByteArray &data, const std::function &isCurrent, QString &error) -{ - error = validate(data); - if (!error.isEmpty()) { - return SaveResult::Failed; - } - if (!isCurrent()) { - error = QCoreApplication::translate("SpoilerBackgroundUpdater", - "The spoiler update was cancelled because its settings changed."); - return SaveResult::Failed; - } - if (hashFile(targetPath) == QCryptographicHash::hash(data, QCryptographicHash::Sha256)) { - return SaveResult::Unchanged; - } - const QDir directory = QFileInfo(targetPath).dir(); - if (!directory.exists() && !directory.mkpath(QStringLiteral("."))) { - error = QCoreApplication::translate("SpoilerBackgroundUpdater", - "Could not create the spoiler database directory: %1") - .arg(directory.absolutePath()); - return SaveResult::Failed; - } - QSaveFile file(targetPath); - file.setDirectWriteFallback(false); - if (!file.open(QIODevice::WriteOnly) || file.write(data) != data.size() || !file.flush()) { - error = QCoreApplication::translate( - "SpoilerBackgroundUpdater", - "Could not save the spoiler database to %1: %2. The previous file was preserved.") - .arg(targetPath, file.errorString()); - return SaveResult::Failed; - } - if (!isCurrent()) { - file.cancelWriting(); - error = QCoreApplication::translate("SpoilerBackgroundUpdater", - "The spoiler update was cancelled because its settings changed."); - return SaveResult::Failed; - } - if (!file.commit()) { - error = QCoreApplication::translate( - "SpoilerBackgroundUpdater", - "Could not replace the spoiler database at %1: %2. The previous file was preserved.") - .arg(targetPath, file.errorString()); - return SaveResult::Failed; - } - return SaveResult::Written; -} - -} // namespace SpoilerFile diff --git a/cockatrice/src/client/network/update/card_spoiler/spoiler_file.h b/cockatrice/src/client/network/update/card_spoiler/spoiler_file.h deleted file mode 100644 index 3a138cdea..000000000 --- a/cockatrice/src/client/network/update/card_spoiler/spoiler_file.h +++ /dev/null @@ -1,24 +0,0 @@ -#ifndef COCKATRICE_SPOILER_FILE_H -#define COCKATRICE_SPOILER_FILE_H - -#include -#include -#include - -namespace SpoilerFile -{ -enum class SaveResult -{ - Failed, - Unchanged, - Written -}; - -QString validate(const QByteArray &data); -// Validate before opening the destination, compare complete files, and replace -// atomically. Check operation ownership both before writing and before committing. -SaveResult -save(const QString &targetPath, const QByteArray &data, const std::function &isCurrent, QString &error); -} // namespace SpoilerFile - -#endif // COCKATRICE_SPOILER_FILE_H diff --git a/cockatrice/src/client/network/update/client/update_downloader.cpp b/cockatrice/src/client/network/update/client/update_downloader.cpp index 8b5ef3d6c..8868876fa 100644 --- a/cockatrice/src/client/network/update/client/update_downloader.cpp +++ b/cockatrice/src/client/network/update/client/update_downloader.cpp @@ -1,16 +1,15 @@ #include "update_downloader.h" +#include +#include #include -#include +#include #include #include #include -#include -#include #include #include #include -#include UpdateDownloader::UpdateDownloader(QObject *parent) : QObject(parent), response(nullptr) { @@ -24,116 +23,48 @@ void UpdateDownloader::beginDownload(QUrl downloadUrl) originalUrl = downloadUrl; } - if (auto *old = std::exchange(response, nullptr)) { - old->abort(); - old->deleteLater(); - } - - errorReported = false; - cancelRequested = false; - - // The transfer timeout comes from the manager (NetworkManagerFactory). response = netMan->get(QNetworkRequest(downloadUrl)); connect(response, &QNetworkReply::finished, this, &UpdateDownloader::fileFinished); - connect(response, &QNetworkReply::errorOccurred, this, &UpdateDownloader::downloadError); connect(response, &QNetworkReply::downloadProgress, this, &UpdateDownloader::downloadProgress); - connect(this, &UpdateDownloader::stopDownload, this, &UpdateDownloader::cancelDownload); + connect(this, &UpdateDownloader::stopDownload, response, &QNetworkReply::abort); } -void UpdateDownloader::downloadError(QNetworkReply::NetworkError networkError) +void UpdateDownloader::downloadError(QNetworkReply::NetworkError) { - // A reply that was superseded by beginDownload() can still deliver a - // queued error; it must not be reported against the current reply. - if (response == nullptr || sender() != response || errorReported) { + if (response == nullptr) { return; } - // A transfer timeout aborts the reply, so it arrives as OperationCanceledError. - // Only an abort the user asked for is suppressed; every other cancel is a - // stalled transfer and has to be reported. - if (networkError == QNetworkReply::OperationCanceledError) { - if (cancelRequested) { - return; - } - - errorReported = true; - emit error(tr("The download timed out. Check your connection and try again.")); - return; - } - - errorReported = true; - emit error(response->errorString()); -} - -void UpdateDownloader::cancelDownload() -{ - cancelRequested = true; - if (response != nullptr) { - response->abort(); - } + emit error(response->errorString().toUtf8()); } void UpdateDownloader::fileFinished() { - // An aborted, superseded reply also emits finished(). Only the reply - // currently stored in response may be consumed. - if (response == nullptr || sender() != response) { + // If we finished but there's a redirect, follow it + QVariant redirect = response->attribute(QNetworkRequest::RedirectionTargetAttribute); + if (!redirect.isNull()) { + beginDownload(redirect.toUrl()); return; } - // errorOccurred normally reports this first; keep the check for errors that - // only surface when the reply finishes. - if (response->error() != QNetworkReply::NoError) { - downloadError(response->error()); - response->deleteLater(); - response = nullptr; + // Handle any errors we had + if (response->error()) { + emit error(response->errorString()); return; } - // Give the update its own private directory. The name is unpredictable, so a - // local user cannot pre-create or symlink the path, and the original file - // name (including the installer suffix) is preserved inside it. - QTemporaryDir downloadDir(QDir::temp().filePath(QStringLiteral("cockatrice-update-XXXXXX"))); - if (!downloadDir.isValid()) { - emit error(tr("Could not create a temporary directory for the download.")); - response->deleteLater(); - response = nullptr; - return; - } - downloadDir.setAutoRemove(false); + // Work out the file name of the download + QString fileName = QDir::temp().path() + QDir::separator() + originalUrl.toString().section('/', -1); - const QString remoteName = QFileInfo(originalUrl.path()).fileName(); - const QString fileName = downloadDir.filePath(remoteName.isEmpty() ? QStringLiteral("update") : remoteName); - - QSaveFile file(fileName); + // Save the build in a temporary directory + QFile file(fileName); if (!file.open(QIODevice::WriteOnly)) { - downloadDir.remove(); - emit error(tr("Could not open the file for writing: %1").arg(file.errorString())); - response->deleteLater(); - response = nullptr; + emit error(tr("Could not open the file for reading.")); return; } - const QByteArray data = response->readAll(); - if (file.write(data) != data.size()) { - file.cancelWriting(); - downloadDir.remove(); - emit error(tr("Could not write the downloaded file to disk.")); - response->deleteLater(); - response = nullptr; - return; - } - - if (!file.commit()) { - downloadDir.remove(); - emit error(tr("Could not save the downloaded file: %1").arg(file.errorString())); - response->deleteLater(); - response = nullptr; - return; - } - - response->deleteLater(); - response = nullptr; + file.write(response->readAll()); + file.close(); // Emit the success signal with a URL to the download file emit downloadSuccessful(QUrl::fromLocalFile(fileName)); diff --git a/cockatrice/src/client/network/update/client/update_downloader.h b/cockatrice/src/client/network/update/client/update_downloader.h index 7704eca38..8aeefb22a 100644 --- a/cockatrice/src/client/network/update/client/update_downloader.h +++ b/cockatrice/src/client/network/update/client/update_downloader.h @@ -32,13 +32,10 @@ private: QUrl originalUrl; QNetworkAccessManager *netMan; QNetworkReply *response; - bool errorReported = false; - bool cancelRequested = false; private slots: void fileFinished(); void downloadProgress(qint64 bytesRead, qint64 totalBytes); void downloadError(QNetworkReply::NetworkError); - void cancelDownload(); }; #endif // COCKATRICE_UPDATEDOWNLOADER_H diff --git a/cockatrice/src/client/settings/cache_settings.cpp b/cockatrice/src/client/settings/cache_settings.cpp index b40e26377..08eccf1e4 100644 --- a/cockatrice/src/client/settings/cache_settings.cpp +++ b/cockatrice/src/client/settings/cache_settings.cpp @@ -6,7 +6,6 @@ #include "shortcuts_settings.h" #include -#include #include #include #include @@ -48,32 +47,7 @@ Q_GLOBAL_STATIC(SettingsCache, settingsCache) SettingsCache &SettingsCache::instance() { - SettingsCache &cache = *settingsCache; // returns a QT managed singleton reference - // The release channels own QNetworkAccessManagers, which must be destroyed - // while the application object is alive. ~SettingsCache() runs during - // static teardown, after QApplication is gone, so hook the application's - // shutdown instead. Retried here because the first access may predate the - // application object. - cache.installShutdownHook(); - return cache; -} - -void SettingsCache::installShutdownHook() -{ - if (shutdownHookInstalled) { - return; - } - QCoreApplication *application = QCoreApplication::instance(); - if (application == nullptr) { - return; - } - connect(application, &QCoreApplication::aboutToQuit, this, [this] { - for (ReleaseChannel *releaseChannel : std::as_const(releaseChannels)) { - delete releaseChannel; - } - releaseChannels.clear(); - }); - shutdownHookInstalled = true; + return *settingsCache; // returns a QT managed singleton reference } QString SettingsCache::getDataPath() diff --git a/cockatrice/src/client/settings/cache_settings.h b/cockatrice/src/client/settings/cache_settings.h index 8ccf6f76b..9aeb88a18 100644 --- a/cockatrice/src/client/settings/cache_settings.h +++ b/cockatrice/src/client/settings/cache_settings.h @@ -90,13 +90,8 @@ private: QString themeName; QList releaseChannels; - bool shutdownHookInstalled = false; bool isPortableBuild; - // Connects the aboutToQuit handler that deletes the release channels while - // the application object is still alive; a no-op before it exists. - void installShutdownHook(); - [[nodiscard]] QString getSafeConfigPath(QString configEntry, QString defaultPath) const; [[nodiscard]] QString getSafeConfigFilePath(QString configEntry, QString defaultPath) const; void loadPaths(); diff --git a/cockatrice/src/client/settings/shortcut_treeview.cpp b/cockatrice/src/client/settings/shortcut_treeview.cpp index 2501798cb..4a5bafb01 100644 --- a/cockatrice/src/client/settings/shortcut_treeview.cpp +++ b/cockatrice/src/client/settings/shortcut_treeview.cpp @@ -1,6 +1,5 @@ #include "shortcut_treeview.h" -#include "../../interface/theme_manager.h" #include "cache_settings.h" #include "shortcuts_settings.h" @@ -79,18 +78,6 @@ ShortcutTreeView::ShortcutTreeView(QWidget *parent) : QTreeView(parent) connect(&SettingsCache::instance().shortcuts(), &ShortcutsSettings::shortCutChanged, this, &ShortcutTreeView::refreshShortcuts); - - // Group headers store their font on the item; re-apply the role family on - // font changes (setFont emits itemChanged, which repaints the rows). - connect(themeManager, &ThemeManager::fontsChanged, this, [this] { - QFont groupFont = themeManager->appFont(AppFont::InterfaceDefault); - groupFont.setBold(true); - for (int row = 0; row < shortcutsModel->rowCount(); ++row) { - if (auto *groupItem = shortcutsModel->item(row)) { - groupItem->setFont(groupFont); - } - } - }); } void ShortcutTreeView::populateShortcutsModel() @@ -106,8 +93,7 @@ void ShortcutTreeView::populateShortcutsModel() curParent = parentItems.value(group); } else { curParent = new QStandardItem(group); - // Family comes from the InterfaceDefault theme role; bold stays caller-side. - QFont font = themeManager->appFont(AppFont::InterfaceDefault); + static QFont font = curParent->font(); font.setBold(true); curParent->setFont(font); parentItems.insert(group, curParent); diff --git a/cockatrice/src/filters/filter_builder.cpp b/cockatrice/src/filters/filter_builder.cpp index 9c2a91a5b..22679abb1 100644 --- a/cockatrice/src/filters/filter_builder.cpp +++ b/cockatrice/src/filters/filter_builder.cpp @@ -1,6 +1,6 @@ #include "filter_builder.h" -#include "../interface/theme_icon.h" +#include "../interface/pixel_map_generator.h" #include "../interface/widgets/utility/custom_line_edit.h" #include @@ -30,7 +30,7 @@ FilterBuilder::FilterBuilder(QWidget *parent) : QWidget(parent) typeCombo->addItem(CardFilter::typeName(static_cast(i)), QVariant(i)); } - QPushButton *ok = new QPushButton(loadThemedIcon(QStringLiteral("icons/increment")), QString()); + QPushButton *ok = new QPushButton(themePixmap(QStringLiteral("icons/increment")), QString()); ok->setObjectName("ok"); ok->setMaximumSize(20, 20); diff --git a/cockatrice/src/filters/filter_tree_model.cpp b/cockatrice/src/filters/filter_tree_model.cpp index 66fe39821..9b55ce2fd 100644 --- a/cockatrice/src/filters/filter_tree_model.cpp +++ b/cockatrice/src/filters/filter_tree_model.cpp @@ -1,7 +1,5 @@ #include "filter_tree_model.h" -#include "../interface/theme_manager.h" - #include #include #include @@ -41,12 +39,6 @@ FilterTreeModel::FilterTreeModel(QObject *parent) : QAbstractItemModel(parent) connect(fTree, &FilterTree::postInsertRow, this, &FilterTreeModel::proxyEndInsertRow); connect(fTree, &FilterTree::preRemoveRow, this, &FilterTreeModel::proxyBeginRemoveRow); connect(fTree, &FilterTree::postRemoveRow, this, &FilterTreeModel::proxyEndRemoveRow); - // FontRole data reads the DeckList role live; rebroadcast so attached - // views re-query instead of waiting for an incidental repaint. - connect(themeManager, &ThemeManager::fontsChanged, this, [this] { - emit layoutAboutToBeChanged(); - emit layoutChanged(); - }); } FilterTreeModel::~FilterTreeModel() @@ -210,8 +202,7 @@ QVariant FilterTreeModel::data(const QModelIndex &index, int role) const switch (role) { case Qt::FontRole: if (!node->isLeaf()) { - // Family comes from the DeckList theme role; bold stays caller-side. - QFont f = themeManager->appFont(AppFont::DeckList); + QFont f; f.setBold(true); return f; } diff --git a/cockatrice/src/game_graphics/board/abstract_card_item.cpp b/cockatrice/src/game_graphics/board/abstract_card_item.cpp index 84e062cd7..0e8b6ced3 100644 --- a/cockatrice/src/game_graphics/board/abstract_card_item.cpp +++ b/cockatrice/src/game_graphics/board/abstract_card_item.cpp @@ -3,7 +3,6 @@ #include "../../client/settings/cache_settings.h" #include "../../interface/card_localization.h" #include "../../interface/card_picture_loader/card_picture_loader.h" -#include "../../interface/theme_manager.h" #include "../card_dimensions.h" #include "../game_scene.h" #include "../z_values.h" @@ -155,7 +154,7 @@ void AbstractCardItem::transformPainter(QPainter *painter, const QSizeF &transla painter->rotate(angle); painter->translate(-translatedSize.width() / 2, -translatedSize.height() / 2); - QFont f = themeManager->appFont(AppFont::CardText); + QFont f; f.setPixelSize(fontSize * scale); painter->setFont(f); diff --git a/cockatrice/src/game_graphics/board/abstract_graphics_item.cpp b/cockatrice/src/game_graphics/board/abstract_graphics_item.cpp index 3a740aa98..50e80d359 100644 --- a/cockatrice/src/game_graphics/board/abstract_graphics_item.cpp +++ b/cockatrice/src/game_graphics/board/abstract_graphics_item.cpp @@ -1,7 +1,5 @@ #include "abstract_graphics_item.h" -#include "../../interface/theme_manager.h" - #include #include #include @@ -22,7 +20,7 @@ void AbstractGraphicsItem::paintNumberEllipse(int number, painter->save(); QString numStr = QString::number(number); - QFont font = themeManager->cardTextFont(); + QFont font("Serif"); font.setPixelSize(fontSize); font.setWeight(QFont::Bold); diff --git a/cockatrice/src/game_graphics/board/counter_general.cpp b/cockatrice/src/game_graphics/board/counter_general.cpp index 23618e6bf..4a8bbd6a4 100644 --- a/cockatrice/src/game_graphics/board/counter_general.cpp +++ b/cockatrice/src/game_graphics/board/counter_general.cpp @@ -1,7 +1,6 @@ #include "counter_general.h" #include "../../interface/pixel_map_generator.h" -#include "../../interface/theme_manager.h" #include "abstract_graphics_item.h" #include @@ -42,7 +41,7 @@ void GeneralCounter::paint(QPainter *painter, const QStyleOptionGraphicsItem * / painter->drawPixmap(QPoint(0, 0), pixmap); if (value) { - QFont f = themeManager->cardTextFont(); + QFont f("Serif"); f.setPixelSize(qMax((int)(radius * scaleFactor), 10)); f.setWeight(QFont::Bold); painter->setPen(Qt::black); diff --git a/cockatrice/src/game_graphics/deckview/deck_view.cpp b/cockatrice/src/game_graphics/deckview/deck_view.cpp index f62d33335..6e0b0eaaa 100644 --- a/cockatrice/src/game_graphics/deckview/deck_view.cpp +++ b/cockatrice/src/game_graphics/deckview/deck_view.cpp @@ -240,8 +240,8 @@ void DeckViewCardContainer::paint(QPainter *painter, const QStyleOptionGraphicsI painter->drawLine(QPointF(0, separatorY), QPointF(width, separatorY)); painter->setPen(QColor(Qt::white)); - // Family comes from the DeckList theme role; pixel sizes/weights unchanged. - QFont f = themeManager->deckListFont(); + QFont f("Serif"); + f.setStyleHint(QFont::Serif); f.setPixelSize(24); f.setWeight(QFont::Bold); painter->setFont(f); @@ -292,8 +292,8 @@ QList> DeckViewCardContainer::getRowsAndCols() const int DeckViewCardContainer::getCardTypeTextWidth() const { - // Family comes from the DeckList theme role; pixel size/weight unchanged. - QFont f = themeManager->deckListFont(); + QFont f("Serif"); + f.setStyleHint(QFont::Serif); f.setPixelSize(16); f.setWeight(QFont::Bold); QFontMetrics fm(f); @@ -366,15 +366,6 @@ void DeckViewCardContainer::setWidth(qreal _width) DeckViewScene::DeckViewScene(QObject *parent) : QGraphicsScene(parent), locked(true), deck(0), optimalAspectRatio(1.0) { - // Zone headers read the DeckList role at paint time and cache their - // rendering, so touch every item to invalidate the caches and pick up the - // new family on font changes (containers cannot connect this themselves: - // QGraphicsItem is not a QObject). - connect(themeManager, &ThemeManager::fontsChanged, this, [this] { - for (auto *item : items()) { - item->update(); - } - }); } DeckViewScene::~DeckViewScene() diff --git a/cockatrice/src/game_graphics/deckview/deck_view_container.cpp b/cockatrice/src/game_graphics/deckview/deck_view_container.cpp index fe63d7a6f..3bef9e59e 100644 --- a/cockatrice/src/game_graphics/deckview/deck_view_container.cpp +++ b/cockatrice/src/game_graphics/deckview/deck_view_container.cpp @@ -71,8 +71,6 @@ void ToggleButton::setState(bool _state) DeckViewContainer::DeckViewContainer(int _playerId, TabGame *parent) : QWidget(nullptr), visualDeckStorageWidget(nullptr), parentGame(parent), playerId(_playerId) { - setObjectName("deckViewContainer"); - loadLocalButton = new QPushButton; loadRemoteButton = new QPushButton; loadFromClipboardButton = new QPushButton; @@ -82,14 +80,6 @@ DeckViewContainer::DeckViewContainer(int _playerId, TabGame *parent) forceStartGameButton = new QPushButton; sideboardLockButton = new ToggleButton; - loadLocalButton->setObjectName("loadLocalButton"); - loadRemoteButton->setObjectName("loadRemoteButton"); - loadFromClipboardButton->setObjectName("loadFromClipboardButton"); - readyStartButton->setObjectName("readyStartButton"); - forceStartGameButton->setObjectName("forceStartGameButton"); - unloadDeckButton->setObjectName("unloadDeckButton"); - sideboardLockButton->setObjectName("sideboardLockButton"); - connect(loadLocalButton, &QPushButton::clicked, this, &DeckViewContainer::loadLocalDeck); connect(loadRemoteButton, &QPushButton::clicked, this, &DeckViewContainer::loadRemoteDeck); connect(loadFromClipboardButton, &QPushButton::clicked, this, &DeckViewContainer::loadFromClipboard); diff --git a/cockatrice/src/game_graphics/deckview/tabbed_deck_view_container.cpp b/cockatrice/src/game_graphics/deckview/tabbed_deck_view_container.cpp index 75ede0ca8..90cbed9a1 100644 --- a/cockatrice/src/game_graphics/deckview/tabbed_deck_view_container.cpp +++ b/cockatrice/src/game_graphics/deckview/tabbed_deck_view_container.cpp @@ -14,7 +14,6 @@ class TabGame; TabbedDeckViewContainer::TabbedDeckViewContainer(int _playerId, TabGame *parent) : QTabWidget(nullptr), playerId(_playerId), parentGame(parent) { - setObjectName(QString("tabbedDeckView_%1").arg(_playerId)); setTabsClosable(true); connect(this, &QTabWidget::tabCloseRequested, this, &TabbedDeckViewContainer::closeTab); diff --git a/cockatrice/src/game_graphics/game_scene.cpp b/cockatrice/src/game_graphics/game_scene.cpp index 128899a13..db1040006 100644 --- a/cockatrice/src/game_graphics/game_scene.cpp +++ b/cockatrice/src/game_graphics/game_scene.cpp @@ -1,7 +1,6 @@ #include "game_scene.h" #include "../client/settings/cache_settings.h" -#include "../interface/theme_manager.h" #include "animated_item.h" #include "board/abstract_card_item.h" #include "board/arrow_item.h" @@ -36,7 +35,6 @@ #include #include #include -#include #include #include #include @@ -66,14 +64,6 @@ GameScene::GameScene(PhasesToolbar *_phasesToolbar, QObject *parent) addItem(phasesToolbar); connect(&SettingsCache::instance().userInterface(), &InterfaceSettings::minPlayersForMultiColumnLayoutChanged, this, &GameScene::rearrange); - // CardText items (P/T boxes, counters, player names) read the role at - // paint time and cache their rendering, so touch every item to invalidate - // the caches and pick up the new family on font changes. - connect(themeManager, &ThemeManager::fontsChanged, this, [this] { - for (auto *item : items()) { - item->update(); - } - }); rearrange(); } @@ -442,37 +432,6 @@ QList GameScene::collectActivePlayers(int &firstPlayerIndex) cons } } } - - // Lay out the table in seat order rather than id order. Players missing - // from the tracked order keep id order at the end. - QList seatOrder; - for (auto *pgItem : playerViews.values()) { - if (PlayerLogic *p = pgItem->getLogic()) { - seatOrder = p->getGame()->getGameState()->getSeatOrder(); - break; - } - } - if (!seatOrder.isEmpty()) { - std::sort(activePlayers.begin(), activePlayers.end(), [&](PlayerLogic *a, PlayerLogic *b) { - int indexA = seatOrder.indexOf(a->getPlayerInfo()->getId()); - int indexB = seatOrder.indexOf(b->getPlayerInfo()->getId()); - if (indexA == -1) { - indexA = seatOrder.size() + a->getPlayerInfo()->getId(); - } - if (indexB == -1) { - indexB = seatOrder.size() + b->getPlayerInfo()->getId(); - } - return indexA < indexB; - }); - // Recompute the local-player index after sorting. - firstPlayerIndex = 0; - for (int i = 0; i < activePlayers.size(); ++i) { - if (activePlayers[i]->getPlayerInfo()->getLocal()) { - firstPlayerIndex = i; - break; - } - } - } return activePlayers; } diff --git a/cockatrice/src/game_graphics/game_view.cpp b/cockatrice/src/game_graphics/game_view.cpp index 7a5aa073a..cb59b3d7d 100644 --- a/cockatrice/src/game_graphics/game_view.cpp +++ b/cockatrice/src/game_graphics/game_view.cpp @@ -2,14 +2,12 @@ #include "../client/settings/cache_settings.h" #include "../client/settings/shortcuts_settings.h" -#include "../interface/theme_manager.h" #include "game_scene.h" #include #include #include #include -#include #include #include #include @@ -26,21 +24,6 @@ #include #include -namespace -{ -QFont deckOverlayFont(int pixelSize, bool bold) -{ - // The drag/total counters and the tally columns are in-game overlays, not - // deck list text: keep the monospace family the stylesheets used to set so - // digit widths stay stable as counts tick and `white-space: pre` lines up. - QFont font(QStringLiteral("monospace")); - font.setStyleHint(QFont::Monospace); - font.setPixelSize(pixelSize); - font.setBold(bold); - return font; -} -} // namespace - // QRubberBand calls raise() in showEvent() and changeEvent() to stay on top of siblings. // This subclass disables that behavior so dragCountLabel can appear above it. class SelectionRubberBand : public QRubberBand @@ -65,7 +48,6 @@ protected: GameView::GameView(GameScene *scene, QWidget *parent) : QGraphicsView(scene, parent), rubberBand(0) { - setObjectName("gameView"); setBackgroundBrush(QBrush(QColor(0, 0, 0))); setRenderHints(QPainter::TextAntialiasing | QPainter::Antialiasing); setViewportUpdateMode(BoundingRectViewportUpdate); @@ -93,14 +75,15 @@ GameView::GameView(GameScene *scene, QWidget *parent) : QGraphicsView(scene, par rubberBand = new SelectionRubberBand(QRubberBand::Rectangle, this); const QString baseProperties = "color: white; " + "font-family: monospace; " "background-color: rgba(0, 0, 0, 160); " "border-radius: 3px; " "padding: 1px 2px; " "white-space: pre;"; - const QString dragCountLabelStyle = baseProperties; - const QString totalCountLabelStyle = baseProperties; - const QString subtypeTallyLabelStyle = baseProperties; + const QString dragCountLabelStyle = baseProperties + "font-size: 14px; font-weight: bold;"; + const QString totalCountLabelStyle = baseProperties + "font-size: 16px; font-weight: bold;"; + const QString subtypeTallyLabelStyle = baseProperties + "font-size: 12px;"; dragCountLabel = new QLabel(this); dragCountLabel->setStyleSheet(dragCountLabelStyle); @@ -117,27 +100,6 @@ GameView::GameView(GameScene *scene, QWidget *parent) : QGraphicsView(scene, par tallyLayout->setContentsMargins(2, 2, 2, 2); tallyLayout->setSpacing(2); tallyContainer->hide(); - - applyDeckOverlayFonts(); -} - -void GameView::applyDeckOverlayFonts() -{ - dragCountLabel->setFont(deckOverlayFont(14, true)); - totalCountLabel->setFont(deckOverlayFont(16, true)); - tallyContainer->setFont(deckOverlayFont(12, false)); - for (int i = 0; i < tallyLayout->count(); ++i) { - if (auto *label = qobject_cast(tallyLayout->itemAt(i)->widget())) { - // Count column (odd positions) keeps the bold emphasis. - const bool count = i % 2 == 1; - label->setFont(deckOverlayFont(count ? 14 : 12, count)); - } - } - - // The container keeps the size computed from the previous fonts, so force - // rebuildTallyLabels() to re-measure it instead of reusing cachedTallyRows. - cachedTallyRows.clear(); - updateTotalSelectionCount(); } void GameView::resizeEvent(QResizeEvent *event) @@ -257,8 +219,9 @@ QSize GameView::rebuildTallyLabels(const QList &entries) { clearTallyLabels(); - const QString nameStyle = QStringLiteral("color: white; background: transparent;"); - const QString countStyle = QStringLiteral("color: white; background: transparent;"); + const QString nameStyle = QStringLiteral("color: white; font-size: 12px; background: transparent;"); + const QString countStyle = + QStringLiteral("color: white; font-size: 14px; font-weight: bold; background: transparent;"); int totalHeight = 0; int maxNameWidth = 0; @@ -268,13 +231,11 @@ QSize GameView::rebuildTallyLabels(const QList &entries) for (const TallyRow &entry : entries) { auto *nameLabel = new QLabel(entry.name, tallyContainer); nameLabel->setStyleSheet(nameStyle); - nameLabel->setFont(deckOverlayFont(12, false)); nameLabel->setAlignment(Qt::AlignRight | Qt::AlignVCenter); tallyLayout->addWidget(nameLabel, row, 0); auto *countLabel = new QLabel(entry.value, tallyContainer); countLabel->setStyleSheet(countStyle); - countLabel->setFont(deckOverlayFont(14, true)); countLabel->setAlignment(Qt::AlignRight | Qt::AlignVCenter); tallyLayout->addWidget(countLabel, row, 1); diff --git a/cockatrice/src/game_graphics/game_view.h b/cockatrice/src/game_graphics/game_view.h index 99a73a0a8..e57267734 100644 --- a/cockatrice/src/game_graphics/game_view.h +++ b/cockatrice/src/game_graphics/game_view.h @@ -50,7 +50,6 @@ private slots: void refreshShortcuts(); void updateTotalSelectionCount(const QSize &viewSize = QSize()); void setFocusDisabled(bool disabled); - void applyDeckOverlayFonts(); public slots: void updateSceneRect(const QRectF &rect); diff --git a/cockatrice/src/game_graphics/log/message_log_widget.cpp b/cockatrice/src/game_graphics/log/message_log_widget.cpp index d526766af..046a1ae90 100644 --- a/cockatrice/src/game_graphics/log/message_log_widget.cpp +++ b/cockatrice/src/game_graphics/log/message_log_widget.cpp @@ -4,8 +4,6 @@ #include "../../client/settings/card_counter_settings.h" #include "../../client/sound_engine.h" #include "../../interface/card_localization.h" -#include "../../interface/theme_icon.h" -#include "../../interface/theme_manager.h" #include "../../interface/widgets/server/chat_view/chat_view.h" #include "../board/card_item.h" #include "../board/translate_counter_name.h" @@ -14,12 +12,8 @@ #include #include #include -#include -#include #include #include -#include -#include #include #include #include @@ -126,7 +120,6 @@ MessageLogWidget::getFromStr(CardZoneLogic *zone, QString cardName, int position void MessageLogWidget::containerProcessingDone() { currentContext = MessageContext_None; - contextJudgeName.clear(); messageSuffix = messagePrefix = QString(); } @@ -612,20 +605,6 @@ void MessageLogWidget::logReverseTurn(PlayerLogic *player, bool reversed) .arg(reversed ? tr("reversed") : tr("normal"))); } -void MessageLogWidget::logTurnOrderChanged(QStringList playerNames, bool randomized) -{ - QStringList sanitizedNames; - for (const QString &name : playerNames) { - sanitizedNames.append(sanitizeHtml(name)); - } - const QString order = sanitizedNames.join(" -> "); - if (randomized) { - appendHtmlServerMessage(tr("Randomized turn order: %1.").arg(order)); - } else { - appendHtmlServerMessage(tr("New turn order set: %1.").arg(order)); - } -} - void MessageLogWidget::logRollDie(PlayerLogic *player, int sides, const QList &rolls) { if (rolls.length() == 1) { @@ -862,27 +841,8 @@ void MessageLogWidget::logUndoDrawFailed(PlayerLogic *player) void MessageLogWidget::setContextJudgeName(QString name) { - contextJudgeName = std::move(name); - refreshJudgeSuffix(); -} - -void MessageLogWidget::refreshJudgeSuffix() -{ - const QPixmap pixmap = loadThemedIcon(QStringLiteral("icons/scales")).pixmap(QSize(24, 24)); - if (!pixmap.isNull()) { - // Rich text cannot resolve `currentColor`; register a palette-rendered - // image so already-appended judge tags repaint on a theme switch. - document()->addResource(QTextDocument::ImageResource, QUrl(QStringLiteral("cockatrice:scales")), pixmap); - document()->markContentsDirty(0, document()->characterCount()); - } - - if (contextJudgeName.isEmpty()) { - return; - } - - messagePrefix = QStringLiteral("").arg(palette().color(QPalette::WindowText).name()); - messageSuffix = - QStringLiteral(" [ %1]").arg(sanitizeHtml(contextJudgeName)); + messagePrefix = QString(""); + messageSuffix = QString(" [ %1]").arg(sanitizeHtml(name)); } void MessageLogWidget::appendHtmlServerMessage(const QString &html, bool optionalIsBold, QString optionalFontColor) @@ -931,6 +891,4 @@ void MessageLogWidget::connectToPlayerEventHandler(PlayerEventHandler *playerEve MessageLogWidget::MessageLogWidget(TabSupervisor *_tabSupervisor, AbstractGame *_game, QWidget *parent) : ChatView(_tabSupervisor, _game, true, parent), currentContext(MessageContext_None) { - connect(themeManager, &ThemeManager::paletteChanged, this, &MessageLogWidget::refreshJudgeSuffix); - connect(themeManager, &ThemeManager::themeChanged, this, &MessageLogWidget::refreshJudgeSuffix); } diff --git a/cockatrice/src/game_graphics/log/message_log_widget.h b/cockatrice/src/game_graphics/log/message_log_widget.h index 3ec5ff93c..5642a76e9 100644 --- a/cockatrice/src/game_graphics/log/message_log_widget.h +++ b/cockatrice/src/game_graphics/log/message_log_widget.h @@ -39,10 +39,8 @@ private: MessageContext currentContext; QString messagePrefix, messageSuffix; - QString contextJudgeName; static QPair getFromStr(CardZoneLogic *zone, QString cardName, int position, bool ownerChange); - void refreshJudgeSuffix(); public: void connectToPlayerEventHandler(PlayerEventHandler *player); @@ -96,7 +94,6 @@ public slots: int amount, bool isLentToAnotherPlayer); void logReverseTurn(PlayerLogic *player, bool reversed); - void logTurnOrderChanged(QStringList playerNames, bool randomized); void logRollDie(PlayerLogic *player, int sides, const QList &rolls); void logSay(PlayerLogic *player, QString message); void logSetActivePhase(int phase); diff --git a/cockatrice/src/game_graphics/player/player_list_widget.cpp b/cockatrice/src/game_graphics/player/player_list_widget.cpp index 6895e7db8..7b358010f 100644 --- a/cockatrice/src/game_graphics/player/player_list_widget.cpp +++ b/cockatrice/src/game_graphics/player/player_list_widget.cpp @@ -1,7 +1,6 @@ #include "player_list_widget.h" #include "../../interface/pixel_map_generator.h" -#include "../../interface/theme_icon.h" #include "../../interface/widgets/server/user/user_context_menu.h" #include "libcockatrice/protocol/pb/serverinfo_user.pb.h" #include "user_level.h" @@ -68,13 +67,13 @@ PlayerListWidget::PlayerListWidget(TabSupervisor *_tabSupervisor, QWidget *parent) : QTreeWidget(parent), tabSupervisor(_tabSupervisor), client(_client), game(_game), gameStarted(false) { - readyIcon = loadThemedIcon(QStringLiteral("icons/ready_start")); - notReadyIcon = loadThemedIcon(QStringLiteral("icons/not_ready_start")); - concededIcon = loadThemedIcon(QStringLiteral("icons/conceded")); - playerIcon = loadThemedIcon(QStringLiteral("icons/player")); - judgeIcon = loadThemedIcon(QStringLiteral("icons/scales")); - spectatorIcon = loadThemedIcon(QStringLiteral("icons/spectator")); - lockIcon = loadThemedIcon(QStringLiteral("icons/lock")); + readyIcon = themePixmap(QStringLiteral("icons/ready_start")); + notReadyIcon = themePixmap(QStringLiteral("icons/not_ready_start")); + concededIcon = themePixmap(QStringLiteral("icons/conceded")); + playerIcon = loadColorAdjustedPixmap("theme:icons/player"); + judgeIcon = loadColorAdjustedPixmap("theme:icons/scales"); + spectatorIcon = loadColorAdjustedPixmap("theme:icons/spectator"); + lockIcon = themePixmap(QStringLiteral("icons/lock")); if (tabSupervisor) { itemDelegate = new PlayerListItemDelegate(this); diff --git a/cockatrice/src/game_graphics/player/player_target.cpp b/cockatrice/src/game_graphics/player/player_target.cpp index 0d10ba4ab..1cd97e7b5 100644 --- a/cockatrice/src/game_graphics/player/player_target.cpp +++ b/cockatrice/src/game_graphics/player/player_target.cpp @@ -2,7 +2,6 @@ #include "../../client/settings/cache_settings.h" #include "../../interface/pixel_map_generator.h" -#include "../../interface/theme_manager.h" #include "../board/abstract_counter.h" #include "../board/abstract_graphics_item.h" #include "../board/arrow_target.h" @@ -77,7 +76,7 @@ void PlayerCounter::paint(QPainter *painter, const QStyleOptionGraphicsItem * /* QRectF translatedRect = path.controlPointRect(); QSize translatedSize = translatedRect.size().toSize(); - QFont font = themeManager->cardTextFont(); + QFont font("Serif"); font.setWeight(QFont::Bold); font.setPixelSize(qMax(qRound(translatedSize.height() / 1.3), 9)); painter->setFont(font); @@ -227,7 +226,7 @@ void PlayerTarget::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*o name = name.mid(0, 10) + "..."; } - QFont font = themeManager->appFont(AppFont::CardText); + QFont font; font.setPixelSize(qMax(qRound(translatedNameRect.height() / 1.5), 9)); painter->setFont(font); painter->setPen(Qt::white); diff --git a/cockatrice/src/game_graphics/pq_cockatrice_event_player.cpp b/cockatrice/src/game_graphics/pq_cockatrice_event_player.cpp deleted file mode 100644 index c6376ef75..000000000 --- a/cockatrice/src/game_graphics/pq_cockatrice_event_player.cpp +++ /dev/null @@ -1,628 +0,0 @@ -#include "pq_cockatrice_event_player.h" - -#include "../../interface/widgets/tabs/tab_game.h" -#include "board/card_item.h" -#include "game_scene.h" -#include "game_view.h" -#include "zones/card_zone.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -Q_LOGGING_CATEGORY(PQ_COCKATRICE_EVENT_PLAYER, "cockatrice.guiTest.eventPlayer") - -namespace -{ -/** - * Strips Qt's mnemonic markers so a test can name a menu entry by the label the - * user sees ("Turn Over" rather than "T&urn Over"). - */ -QString normalizeActionText(const QString &text) -{ - QString normalized = text; - normalized.remove('&'); - return normalized.simplified(); -} - -/** - * Maps a printable character onto the key a US keyboard would report for it, so - * typing produces the same QKeyEvents a recording would. - */ -Qt::Key keyForCharacter(QChar character) -{ - if (character.isLetter()) { - return static_cast(Qt::Key_A + (character.toUpper().unicode() - 'A')); - } - if (character.isDigit()) { - return static_cast(Qt::Key_0 + (character.unicode() - '0')); - } - switch (character.unicode()) { - case ' ': - return Qt::Key_Space; - case '\n': - return Qt::Key_Return; - case '\t': - return Qt::Key_Tab; - case '-': - return Qt::Key_Minus; - case '_': - return Qt::Key_Underscore; - case '/': - return Qt::Key_Slash; - case ',': - return Qt::Key_Comma; - case '.': - return Qt::Key_Period; - case '\'': - return Qt::Key_Apostrophe; - case ';': - return Qt::Key_Semicolon; - case ':': - return Qt::Key_Colon; - case '"': - return Qt::Key_QuoteDbl; - case '&': - return Qt::Key_Ampersand; - case '(': - return Qt::Key_ParenLeft; - case ')': - return Qt::Key_ParenRight; - case '[': - return Qt::Key_BracketLeft; - case ']': - return Qt::Key_BracketRight; - default: - return Qt::Key_unknown; - } -} - -/** Depth-first search for the first model index whose label matches @p text. */ -QModelIndex findModelIndexByText(const QAbstractItemModel *model, const QString &text, const QModelIndex &parent = {}) -{ - for (int row = 0; row < model->rowCount(parent); ++row) { - const QModelIndex index = model->index(row, 0, parent); - if (index.data(Qt::DisplayRole).toString() == text) { - return index; - } - const QModelIndex child = findModelIndexByText(model, text, index); - if (child.isValid()) { - return child; - } - } - return {}; -} - -bool needsShift(QChar character) -{ - if (character.isUpper()) { - return true; - } - return QStringLiteral("_:\"&()").contains(character); -} -} // namespace - -pqCockatriceEventPlayer::pqCockatriceEventPlayer(QObject *parent) : pqWidgetEventPlayer(parent) -{ -} - -bool pqCockatriceEventPlayer::playEvent(QObject *object, const QString &command, const QString &arguments, bool &error) -{ - // A known command that fails is a handled-but-failed event: QtTesting logs - // "Unhandled event" when playEvent returns false, so report through `error` - // (the contract playExpectProperty already follows) and still return true. - const auto run = [&error](bool ok) { - if (!ok) { - error = true; - } - return true; - }; - - if (command == "clickCard") { - return run(playClickCard(object, arguments)); - } else if (command == "flipCard") { - return run(playFlipCard(object, arguments)); - } else if (command == "cardAction") { - return run(playCardAction(object, arguments)); - } else if (command == "moveCard") { - return run(playMoveCard(object, arguments)); - } else if (command == "drawArrow") { - return run(playDrawArrow(object, arguments)); - } else if (command == "createToken") { - return run(playCreateToken(object, arguments)); - } else if (command == "selectItem") { - return run(playSelectItem(object, arguments)); - } else if (command == "typeText") { - return run(playTypeText(object, arguments)); - } else if (command == "expectProperty") { - return playExpectProperty(object, arguments, error); - } - return false; -} - -QGraphicsItem * -pqCockatriceEventPlayer::findCardInScene(QGraphicsScene *scene, const QString &cardName, const QString &zoneName) -{ - for (QGraphicsItem *gi : scene->items()) { - if (gi->type() != typeCard) { - continue; - } - auto *card = static_cast(gi); - if (!card->getZone() || card->getZone()->getName() != zoneName) { - continue; - } - QString actualName = card->getName(); - if (actualName == cardName || actualName.endsWith(" " + cardName)) { - return card; - } - } - return nullptr; -} - -QGraphicsItem *pqCockatriceEventPlayer::findZoneInScene(QGraphicsScene *scene, const QString &zoneName) -{ - for (QGraphicsItem *gi : scene->items()) { - if (gi->type() != typeZone) { - continue; - } - auto *zone = static_cast(gi); - if (zone->getLogic() && zone->getLogic()->getName() == zoneName) { - return zone; - } - } - return nullptr; -} - -QAction *pqCockatriceEventPlayer::findActionInMenu(QMenu *menu, const QString &actionText) -{ - if (!menu || actionText.isEmpty()) { - return nullptr; - } - for (QAction *action : menu->actions()) { - if (action->isSeparator()) { - continue; - } - if (normalizeActionText(action->text()).contains(actionText, Qt::CaseInsensitive)) { - return action; - } - if (QMenu *subMenu = action->menu()) { - if (QAction *found = findActionInMenu(subMenu, actionText)) { - return found; - } - } - } - return nullptr; -} - -QMenu *pqCockatriceEventPlayer::visiblePopupMenu() -{ - for (QWidget *w : QApplication::topLevelWidgets()) { - auto *menu = qobject_cast(w); - if (menu && menu->isVisible()) { - return menu; - } - } - return nullptr; -} - -QPoint pqCockatriceEventPlayer::viewportPoint(QGraphicsView *view, const QGraphicsItem *item) -{ - return view->mapFromScene(item->sceneBoundingRect().center()); -} -void pqCockatriceEventPlayer::sendMouseEvent(QGraphicsView *view, - QEvent::Type type, - const QPoint &pos, - Qt::MouseButton button, - Qt::MouseButtons buttons) -{ - QMouseEvent event(type, pos, view->viewport()->mapToGlobal(pos), button, buttons, Qt::NoModifier); - qApp->notify(view->viewport(), &event); -} - -bool pqCockatriceEventPlayer::playClickCard(QObject *object, const QString &arguments) -{ - auto *gv = qobject_cast(object); - if (!gv) { - qCCritical(PQ_COCKATRICE_EVENT_PLAYER) << "clickCard: target widget is not a QGraphicsView"; - return false; - } - - QStringList parts = arguments.split(','); - if (parts.size() < 2) { - qCCritical(PQ_COCKATRICE_EVENT_PLAYER) << "clickCard: arguments must be 'cardName,zoneName', got:" << arguments; - return false; - } - - QGraphicsScene *scene = gv->scene(); - if (!scene) { - qCCritical(PQ_COCKATRICE_EVENT_PLAYER) << "clickCard: no scene on the QGraphicsView"; - return false; - } - - auto *target = findCardInScene(scene, parts[0].trimmed(), parts[1].trimmed()); - if (!target) { - qCCritical(PQ_COCKATRICE_EVENT_PLAYER) - << "clickCard: card" << parts[0].trimmed() << "not found in zone" << parts[1].trimmed(); - return false; - } - - const QPoint pos = viewportPoint(gv, target); - // A double click is the play gesture from hand: press/release, then the - // double-click event and its release, exactly as a recording produces it. - sendMouseEvent(gv, QEvent::MouseButtonPress, pos, Qt::LeftButton, Qt::LeftButton); - sendMouseEvent(gv, QEvent::MouseButtonRelease, pos, Qt::LeftButton, Qt::LeftButton); - sendMouseEvent(gv, QEvent::MouseButtonDblClick, pos, Qt::LeftButton, Qt::LeftButton); - sendMouseEvent(gv, QEvent::MouseButtonRelease, pos, Qt::LeftButton, Qt::LeftButton); - - return true; -} - -bool pqCockatriceEventPlayer::playFlipCard(QObject *object, const QString &arguments) -{ - auto *gv = qobject_cast(object); - if (!gv) { - qCCritical(PQ_COCKATRICE_EVENT_PLAYER) << "flipCard: target widget is not a QGraphicsView"; - return false; - } - - QStringList parts = arguments.split(','); - if (parts.size() < 2) { - qCCritical(PQ_COCKATRICE_EVENT_PLAYER) << "flipCard: arguments must be 'cardName,zoneName', got:" << arguments; - return false; - } - - QGraphicsScene *scene = gv->scene(); - if (!scene) { - qCCritical(PQ_COCKATRICE_EVENT_PLAYER) << "flipCard: no scene"; - return false; - } - - auto *card = qgraphicsitem_cast(findCardInScene(scene, parts[0].trimmed(), parts[1].trimmed())); - if (!card) { - qCCritical(PQ_COCKATRICE_EVENT_PLAYER) - << "flipCard: card" << parts[0].trimmed() << "not found in zone" << parts[1].trimmed(); - return false; - } - - return invokeCardMenuAction("flipCard", gv, card, QStringLiteral("Turn Over")); -} - -bool pqCockatriceEventPlayer::playCardAction(QObject *object, const QString &arguments) -{ - auto *gv = qobject_cast(object); - if (!gv) { - qCCritical(PQ_COCKATRICE_EVENT_PLAYER) << "cardAction: target widget is not a QGraphicsView"; - return false; - } - - QStringList parts = arguments.split(','); - if (parts.size() < 3) { - qCCritical(PQ_COCKATRICE_EVENT_PLAYER) - << "cardAction: arguments must be 'cardName,zoneName,actionText', got:" << arguments; - return false; - } - - QGraphicsScene *scene = gv->scene(); - if (!scene) { - qCCritical(PQ_COCKATRICE_EVENT_PLAYER) << "cardAction: no scene"; - return false; - } - - auto *card = qgraphicsitem_cast(findCardInScene(scene, parts[0].trimmed(), parts[1].trimmed())); - if (!card) { - qCCritical(PQ_COCKATRICE_EVENT_PLAYER) - << "cardAction: card" << parts[0].trimmed() << "not found in zone" << parts[1].trimmed(); - return false; - } - - return invokeCardMenuAction("cardAction", gv, card, parts.mid(2).join(',').trimmed()); -} - -bool pqCockatriceEventPlayer::invokeCardMenuAction(const char *command, - QGraphicsView *view, - CardItem *card, - const QString &actionText) -{ - const QPoint pos = viewportPoint(view, card); - sendMouseEvent(view, QEvent::MouseButtonPress, pos, Qt::RightButton, Qt::RightButton); - sendMouseEvent(view, QEvent::MouseButtonRelease, pos, Qt::RightButton, Qt::NoButton); - qApp->processEvents(); - - QMenu *menu = visiblePopupMenu(); - if (!menu) { - qCWarning(PQ_COCKATRICE_EVENT_PLAYER) << command << ": no popup menu appeared after right-click"; - return false; - } - - QAction *action = findActionInMenu(menu, actionText); - if (!action) { - qCWarning(PQ_COCKATRICE_EVENT_PLAYER) << command << ": no context menu action matching" << actionText; - menu->close(); - return false; - } - - action->trigger(); - menu->close(); - qApp->processEvents(); - return true; -} - -bool pqCockatriceEventPlayer::playMoveCard(QObject *object, const QString &arguments) -{ - auto *gv = qobject_cast(object); - if (!gv) { - qCCritical(PQ_COCKATRICE_EVENT_PLAYER) << "moveCard: target widget is not a QGraphicsView"; - return false; - } - - QStringList parts = arguments.split(','); - if (parts.size() < 3) { - qCCritical(PQ_COCKATRICE_EVENT_PLAYER) - << "moveCard: arguments must be 'cardName,srcZoneName,dstZoneName', got:" << arguments; - return false; - } - - const QString cardName = parts[0].trimmed(); - const QString srcZoneName = parts[1].trimmed(); - const QString dstZoneName = parts[2].trimmed(); - - QGraphicsScene *scene = gv->scene(); - if (!scene) { - qCCritical(PQ_COCKATRICE_EVENT_PLAYER) << "moveCard: no scene"; - return false; - } - - auto *card = findCardInScene(scene, cardName, srcZoneName); - if (!card) { - qCCritical(PQ_COCKATRICE_EVENT_PLAYER) << "moveCard: card" << cardName << "not found in zone" << srcZoneName; - return false; - } - - auto *dstZone = findZoneInScene(scene, dstZoneName); - if (!dstZone) { - qCCritical(PQ_COCKATRICE_EVENT_PLAYER) << "moveCard: destination zone" << dstZoneName << "not found"; - return false; - } - - const QPoint srcPos = viewportPoint(gv, card); - const QPoint dstPos = viewportPoint(gv, dstZone); - - // A drag has to travel past Qt's start distance before the scene turns it - // into a card drag item, otherwise the move is interpreted as a click. - const int dragDistance = 2 * QApplication::startDragDistance() + 10; - const QPoint pastDragStart = srcPos + QPoint(dragDistance, 0); - - sendMouseEvent(gv, QEvent::MouseButtonPress, srcPos, Qt::LeftButton, Qt::LeftButton); - sendMouseEvent(gv, QEvent::MouseMove, pastDragStart, Qt::LeftButton, Qt::LeftButton); - qApp->processEvents(); - sendMouseEvent(gv, QEvent::MouseMove, dstPos, Qt::LeftButton, Qt::LeftButton); - qApp->processEvents(); - sendMouseEvent(gv, QEvent::MouseButtonRelease, dstPos, Qt::LeftButton, Qt::NoButton); - qApp->processEvents(); - - return true; -} - -bool pqCockatriceEventPlayer::playDrawArrow(QObject *object, const QString &arguments) -{ - auto *gv = qobject_cast(object); - if (!gv) { - qCCritical(PQ_COCKATRICE_EVENT_PLAYER) << "drawArrow: target widget is not a QGraphicsView"; - return false; - } - - QStringList parts = arguments.split(','); - if (parts.size() < 4) { - qCCritical(PQ_COCKATRICE_EVENT_PLAYER) - << "drawArrow: arguments must be 'srcCard,srcZone,dstCard,dstZone', got:" << arguments; - return false; - } - - const QString srcCardName = parts[0].trimmed(); - const QString srcZoneName = parts[1].trimmed(); - const QString dstCardName = parts[2].trimmed(); - const QString dstZoneName = parts[3].trimmed(); - - QGraphicsScene *scene = gv->scene(); - if (!scene) { - qCCritical(PQ_COCKATRICE_EVENT_PLAYER) << "drawArrow: no scene"; - return false; - } - - auto *srcCard = findCardInScene(scene, srcCardName, srcZoneName); - if (!srcCard) { - qCCritical(PQ_COCKATRICE_EVENT_PLAYER) - << "drawArrow: source card" << srcCardName << "not found in zone" << srcZoneName; - return false; - } - - auto *dstCard = findCardInScene(scene, dstCardName, dstZoneName); - if (!dstCard) { - qCCritical(PQ_COCKATRICE_EVENT_PLAYER) - << "drawArrow: dest card" << dstCardName << "not found in zone" << dstZoneName; - return false; - } - - const QPoint srcPos = viewportPoint(gv, srcCard); - const QPoint dstPos = viewportPoint(gv, dstCard); - - const int dragDistance = 2 * QApplication::startDragDistance() + 10; - const QPoint pastDragStart = srcPos + QPoint(dragDistance, 0); - - sendMouseEvent(gv, QEvent::MouseButtonPress, srcPos, Qt::RightButton, Qt::RightButton); - sendMouseEvent(gv, QEvent::MouseMove, pastDragStart, Qt::RightButton, Qt::RightButton); - qApp->processEvents(); - sendMouseEvent(gv, QEvent::MouseMove, dstPos, Qt::RightButton, Qt::RightButton); - sendMouseEvent(gv, QEvent::MouseButtonRelease, dstPos, Qt::RightButton, Qt::NoButton); - qApp->processEvents(); - - return true; -} - -bool pqCockatriceEventPlayer::playCreateToken(QObject *object, const QString &arguments) -{ - auto *gv = qobject_cast(object); - if (!gv) { - qCCritical(PQ_COCKATRICE_EVENT_PLAYER) << "createToken: target widget is not a QGraphicsView"; - return false; - } - - QStringList parts = arguments.split(','); - if (parts.size() < 3) { - qCCritical(PQ_COCKATRICE_EVENT_PLAYER) << "createToken: arguments must be 'name,color,pt', got:" << arguments; - return false; - } - - QString tokenName = parts[0].trimmed(); - QString tokenColor = parts[1].trimmed(); - QString tokenPt = parts[2].trimmed(); - - TabGame *tabGame = nullptr; - QWidget *w = gv; - while (w) { - tabGame = qobject_cast(w); - if (tabGame) { - break; - } - w = w->parentWidget(); - } - if (!tabGame) { - qCCritical(PQ_COCKATRICE_EVENT_PLAYER) << "createToken: TabGame not found"; - return false; - } - - AbstractGame *game = tabGame->getGame(); - if (!game) { - qCCritical(PQ_COCKATRICE_EVENT_PLAYER) << "createToken: no game"; - return false; - } - - int activePlayerId = game->getGameState()->getActivePlayer(); - PlayerLogic *player = game->getPlayerManager()->getActiveLocalPlayer(activePlayerId); - if (!player) { - qCCritical(PQ_COCKATRICE_EVENT_PLAYER) << "createToken: no active local player"; - return false; - } - - TokenInfo info; - info.name = tokenName; - info.color = tokenColor; - info.pt = tokenPt; - info.destroy = true; - info.faceDown = false; - - player->getPlayerActions()->actCreateToken(info); - - for (int i = 0; i < 10; ++i) { - qApp->processEvents(); - } - - return true; -} - -bool pqCockatriceEventPlayer::playSelectItem(QObject *object, const QString &arguments) -{ - const QString text = arguments.trimmed(); - if (text.isEmpty()) { - qCCritical(PQ_COCKATRICE_EVENT_PLAYER) << "selectItem: needs the label of the item to select"; - return false; - } - - if (auto *tree = qobject_cast(object)) { - QTreeWidgetItemIterator item(tree); - while (*item) { - if ((*item)->text(0) == text) { - tree->setCurrentItem(*item); - qApp->processEvents(); - return true; - } - ++item; - } - qCCritical(PQ_COCKATRICE_EVENT_PLAYER) << "selectItem: no tree item labelled" << text; - return false; - } - - if (auto *view = qobject_cast(object)) { - const QModelIndex index = findModelIndexByText(view->model(), text); - if (!index.isValid()) { - qCCritical(PQ_COCKATRICE_EVENT_PLAYER) << "selectItem: no item labelled" << text; - return false; - } - view->setCurrentIndex(index); - qApp->processEvents(); - return true; - } - - qCCritical(PQ_COCKATRICE_EVENT_PLAYER) << "selectItem: target is neither a tree widget nor an item view"; - return false; -} - -bool pqCockatriceEventPlayer::playTypeText(QObject *object, const QString &arguments) -{ - auto *widget = qobject_cast(object); - if (!widget) { - qCCritical(PQ_COCKATRICE_EVENT_PLAYER) << "typeText: target is not a QWidget"; - return false; - } - - for (const QChar character : arguments) { - const Qt::Key key = keyForCharacter(character); - const Qt::KeyboardModifiers modifiers = needsShift(character) ? Qt::ShiftModifier : Qt::NoModifier; - QKeyEvent press(QEvent::KeyPress, key, modifiers, QString(character)); - QKeyEvent release(QEvent::KeyRelease, key, modifiers, QString(character)); - qApp->notify(widget, &press); - qApp->notify(widget, &release); - } - qApp->processEvents(); - - return true; -} - -bool pqCockatriceEventPlayer::playExpectProperty(QObject *object, const QString &arguments, bool &error) -{ - if (!object) { - qCCritical(PQ_COCKATRICE_EVENT_PLAYER) << "expectProperty: no target object"; - error = true; - return true; - } - - QStringList parts = arguments.split(','); - if (parts.size() < 2) { - qCCritical(PQ_COCKATRICE_EVENT_PLAYER) - << "expectProperty: arguments must be 'propertyName,expectedValue', got:" << arguments; - error = true; - return true; - } - - const QString propertyName = parts[0].trimmed(); - const QString expected = parts.mid(1).join(','); - - const QVariant actual = object->property(propertyName.toUtf8().constData()); - if (!actual.isValid()) { - qCCritical(PQ_COCKATRICE_EVENT_PLAYER) - << "expectProperty:" << object->objectName() << "has no property" << propertyName; - error = true; - return true; - } - - if (actual.toString() != expected) { - qCCritical(PQ_COCKATRICE_EVENT_PLAYER) << "expectProperty:" << object->objectName() << propertyName << "is" - << actual.toString() << "but the test expected" << expected; - error = true; - return true; - } - - return true; -} diff --git a/cockatrice/src/game_graphics/pq_cockatrice_event_player.h b/cockatrice/src/game_graphics/pq_cockatrice_event_player.h deleted file mode 100644 index 89d88af77..000000000 --- a/cockatrice/src/game_graphics/pq_cockatrice_event_player.h +++ /dev/null @@ -1,91 +0,0 @@ -#ifndef PQ_COCKATRICE_EVENT_PLAYER_H -#define PQ_COCKATRICE_EVENT_PLAYER_H - -#include "pqWidgetEventPlayer.h" - -#include - -class CardItem; -class QAction; -class QGraphicsItem; -class QGraphicsScene; -class QGraphicsView; -class QMenu; - -/** - * Custom event player for Cockatrice game interactions. - * - * Card commands are resolution independent: the card is looked up in the - * QGraphicsScene by name and zone, then mouse events are sent at the card's - * current viewport position. That keeps recordings usable across screen sizes - * and window geometries. - * - * Commands: - * clickCard "cardName,zoneName" - * left press/release + double click — plays a card from hand, taps a - * permanent, opens the deck view entry, depending on the zone - * flipCard "cardName,zoneName" - * right-click → "Turn Over" (shorthand for cardAction) - * cardAction "cardName,zoneName,actionText" - * right-click the card and trigger the first context menu action whose - * label contains actionText; submenus are searched as well, so - * "Add counter (A)" works - * moveCard "cardName,srcZoneName,dstZoneName" - * left-drag the card onto the destination zone — plays a card, moves it - * to the graveyard/exile, puts it back into hand, ... - * drawArrow "srcCard,srcZone,dstCard,dstZone" - * right-drag an arrow from one card to another - * createToken "name,color,pt" - * create a token through the active local player, as the table context - * menu's "Create token" dialog would - * selectItem "item text" - * selects the first item of a tree/list widget whose label matches, for - * navigating the settings navigation tree or similar label-driven views - * typeText "some text" - * types the text into the widget one key event at a time — use it for - * deck lists and search boxes instead of one event per character - * expectProperty "propertyName,expectedValue" - * assertion: playback fails unless object->property(propertyName) - * equals expectedValue — for example "count,2" on the tab widget - */ -class pqCockatriceEventPlayer : public pqWidgetEventPlayer -{ - Q_OBJECT - typedef pqWidgetEventPlayer Superclass; - -public: - explicit pqCockatriceEventPlayer(QObject *parent = nullptr); - - using Superclass::playEvent; - - bool playEvent(QObject *object, const QString &command, const QString &arguments, bool &error) override; - -private: - bool playClickCard(QObject *object, const QString &arguments); - bool playFlipCard(QObject *object, const QString &arguments); - bool playCardAction(QObject *object, const QString &arguments); - bool playMoveCard(QObject *object, const QString &arguments); - bool playDrawArrow(QObject *object, const QString &arguments); - bool playCreateToken(QObject *object, const QString &arguments); - bool playSelectItem(QObject *object, const QString &arguments); - bool playTypeText(QObject *object, const QString &arguments); - bool playExpectProperty(QObject *object, const QString &arguments, bool &error); - - /** @brief Triggers the context menu entry @p actionText after right-clicking @p card. */ - bool invokeCardMenuAction(const char *command, QGraphicsView *view, CardItem *card, const QString &actionText); - - static QGraphicsItem *findCardInScene(QGraphicsScene *scene, const QString &cardName, const QString &zoneName); - static QGraphicsItem *findZoneInScene(QGraphicsScene *scene, const QString &zoneName); - static QAction *findActionInMenu(QMenu *menu, const QString &actionText); - static QMenu *visiblePopupMenu(); - - /** @brief Viewport position of the item's centre, as the mouse events have to be sent. */ - static QPoint viewportPoint(QGraphicsView *view, const QGraphicsItem *item); - static void sendMouseEvent(QGraphicsView *view, - QEvent::Type type, - const QPoint &pos, - Qt::MouseButton button, - Qt::MouseButtons buttons); -}; - -#endif // PQ_COCKATRICE_EVENT_PLAYER_H diff --git a/cockatrice/src/game_graphics/zones/view_zone_widget.cpp b/cockatrice/src/game_graphics/zones/view_zone_widget.cpp index 3c6cf9d02..b1ca42ee1 100644 --- a/cockatrice/src/game_graphics/zones/view_zone_widget.cpp +++ b/cockatrice/src/game_graphics/zones/view_zone_widget.cpp @@ -3,7 +3,6 @@ #include "../../client/settings/cache_settings.h" #include "../../filters/syntax_help.h" #include "../../interface/pixel_map_generator.h" -#include "../../interface/theme_icon.h" #include "../card_dimensions.h" #include "../z_values.h" #include "libcockatrice/utility/macros.h" @@ -94,8 +93,8 @@ ZoneViewWidget::ZoneViewWidget(PlayerLogic *_player, searchEdit.setFocusPolicy(Qt::ClickFocus); searchEdit.setPlaceholderText(tr("Search by card name (or search expressions)")); searchEdit.setClearButtonEnabled(true); - searchEdit.addAction(loadThemedIcon(QStringLiteral("icons/search")), QLineEdit::LeadingPosition); - auto help = searchEdit.addAction(loadThemedIcon(QStringLiteral("icons/info")), QLineEdit::TrailingPosition); + searchEdit.addAction(loadColorAdjustedPixmap("theme:icons/search"), QLineEdit::LeadingPosition); + auto help = searchEdit.addAction(themePixmap(QStringLiteral("icons/info")), QLineEdit::TrailingPosition); connect(help, &QAction::triggered, this, [this] { createSearchSyntaxHelpWindow(&searchEdit); }); diff --git a/cockatrice/src/interface/deck_loader/deck_loader.cpp b/cockatrice/src/interface/deck_loader/deck_loader.cpp index c8fa2a6f8..d413a6c04 100644 --- a/cockatrice/src/interface/deck_loader/deck_loader.cpp +++ b/cockatrice/src/interface/deck_loader/deck_loader.cpp @@ -1,6 +1,5 @@ #include "deck_loader.h" -#include "../theme_manager.h" #include "libcockatrice/card/card_info.h" #include "libcockatrice/card/database/card_database_querier.h" #include "libcockatrice/deck_list/tree/abstract_deck_list_card_node.h" @@ -39,7 +38,6 @@ #include #include #include -#include #include #include #include @@ -373,12 +371,8 @@ bool DeckLoader::saveToStream_Plain(QTextStream &out, saveToStream_DeckHeader(out, deckList); } - // Plain deck readers expect the main deck before the sideboard. Reorder a - // copy so native saves and the relative order of other zones stay intact. - auto zones = deckList.getZoneNodes(); - std::stable_partition(zones.begin(), zones.end(), - [](const auto *zone) { return zone->getName() == DECK_ZONE_MAIN; }); - for (auto zoneNode : zones) { + // loop zones + for (auto zoneNode : deckList.getZoneNodes()) { saveToStream_DeckZone(out, zoneNode, addComments, addSetNameAndNumber); // end of zone @@ -585,8 +579,6 @@ void DeckLoader::printDeckListNode(QTextCursor *cursor, const InnerDecklistNode if (!cards.isEmpty()) { QTextBlockFormat blockFormat; QTextCharFormat charFormat; - // Family comes from the DeckList theme role; sizes/weights unchanged. - charFormat.setFontFamilies({themeManager->deckListFont().family()}); charFormat.setFontPointSize(11); charFormat.setFontWeight(QFont::Bold); cursor->insertBlock(blockFormat, charFormat); @@ -600,7 +592,6 @@ void DeckLoader::printDeckListNode(QTextCursor *cursor, const InnerDecklistNode const AbstractDecklistCardNode *card = cards[i]; QTextCharFormat cellCharFormat; - cellCharFormat.setFontFamilies({themeManager->deckListFont().family()}); cellCharFormat.setFontPointSize(9); QTextTableCell cell = table->cellAt(i, 0); @@ -622,7 +613,6 @@ void DeckLoader::printDeckListNode(QTextCursor *cursor, const InnerDecklistNode QTextBlockFormat blockFormat; QTextCharFormat charFormat; - charFormat.setFontFamilies({themeManager->deckListFont().family()}); charFormat.setFontPointSize(14); charFormat.setFontWeight(QFont::Bold); @@ -650,18 +640,14 @@ void DeckLoader::printDeckList(QPrinter *printer, const DeckList &deckList) { QTextDocument doc; - // Family comes from the DeckList theme role; the print sizes/weights below stay unchanged. - // Print output is a one-shot snapshot and intentionally does not subscribe - // to fontsChanged (there is no live view to repaint); reprinting picks up - // the current role font. - QFont font = themeManager->deckListFont(); + QFont font("Serif"); + font.setStyleHint(QFont::Serif); doc.setDefaultFont(font); QTextCursor cursor(&doc); QTextBlockFormat headerBlockFormat; QTextCharFormat headerCharFormat; - headerCharFormat.setFontFamilies({themeManager->deckListFont().family()}); headerCharFormat.setFontPointSize(16); headerCharFormat.setFontWeight(QFont::Bold); diff --git a/cockatrice/src/interface/palette_editor/color_button.cpp b/cockatrice/src/interface/palette_editor/color_button.cpp index ae9bd6297..a7db23d31 100644 --- a/cockatrice/src/interface/palette_editor/color_button.cpp +++ b/cockatrice/src/interface/palette_editor/color_button.cpp @@ -1,7 +1,5 @@ #include "color_button.h" -#include "../theme_manager.h" - #include #include #include @@ -23,7 +21,6 @@ ColorButton::ColorButton(QWidget *parent) : QToolButton(parent) setCursor(Qt::PointingHandCursor); setToolTip(tr("Click to pick a color")); connect(this, &QToolButton::clicked, this, &ColorButton::pickColor); - connect(themeManager, &ThemeManager::fontsChanged, this, &ColorButton::updateSwatch); } void ColorButton::setColor(const QColor &c) @@ -76,8 +73,7 @@ void ColorButton::updateSwatch() // Hex label — black or white for contrast int luma = 0.299 * color.red() + 0.587 * color.green() + 0.114 * color.blue(); painter.setPen((luma > 128 && color.alpha() > 80) ? QColor(0, 0, 0, 180) : QColor(255, 255, 255, 200)); - // Family comes from the InterfaceDefault theme role; the 8px size stays caller-side. - QFont f = themeManager->appFont(AppFont::InterfaceDefault); + QFont f = font(); f.setPixelSize(8); painter.setFont(f); painter.drawText(rect(), Qt::AlignCenter, color.name().toUpper()); diff --git a/cockatrice/src/interface/palette_editor/font_grid_widget.cpp b/cockatrice/src/interface/palette_editor/font_grid_widget.cpp deleted file mode 100644 index 44ad59551..000000000 --- a/cockatrice/src/interface/palette_editor/font_grid_widget.cpp +++ /dev/null @@ -1,136 +0,0 @@ -#include "font_grid_widget.h" - -#include "../theme_manager.h" - -#include -#include -#include -#include -#include -#include -#include -#include - -static constexpr int MIN_FONT_SIZE = 6; -static constexpr int MAX_FONT_SIZE = 72; -static constexpr int FALLBACK_FONT_SIZE = 10; - -static const QMap APP_FONT_DESCRIPTIONS = { - {AppFont::InterfaceDefault, QT_TR_NOOP("Default font for menus, dialogs and widgets")}, - {AppFont::ChatLog, QT_TR_NOOP("Font for chat messages and logs")}, - {AppFont::CardText, QT_TR_NOOP("Font for text rendered on cards")}, - {AppFont::DeckList, QT_TR_NOOP("Font for deck lists and the deck editor")}, -}; - -static QString roleDisplayName(AppFont::Role role) -{ - switch (role) { - case AppFont::InterfaceDefault: - return FontGridWidget::tr("Interface default"); - case AppFont::ChatLog: - return FontGridWidget::tr("Chat log"); - case AppFont::CardText: - return FontGridWidget::tr("Card text"); - case AppFont::DeckList: - return FontGridWidget::tr("Deck list"); - default: - return QString(); - } -} - -FontGridWidget::FontGridWidget(QWidget *parent) : QWidget(parent) -{ - auto *grid = new QGridLayout(this); - grid->setSpacing(3); - grid->setContentsMargins(12, 8, 12, 8); - grid->setColumnStretch(0, 0); - grid->setColumnStretch(1, 1); - grid->setColumnStretch(2, 0); - - QMetaEnum fontEnum = QMetaEnum::fromType(); - for (int i = 0; i < fontEnum.keyCount(); ++i) { - auto role = static_cast(fontEnum.value(i)); - - auto *label = new QLabel(this); - grid->addWidget(label, i, 0); - - auto *familyCombo = new QFontComboBox(this); - grid->addWidget(familyCombo, i, 1); - connect(familyCombo, &QFontComboBox::currentFontChanged, this, [this] { emit fontsChanged(); }); - - auto *sizeSpin = new QSpinBox(this); - sizeSpin->setRange(MIN_FONT_SIZE, MAX_FONT_SIZE); - grid->addWidget(sizeSpin, i, 2); - connect(sizeSpin, &QSpinBox::valueChanged, this, [this] { emit fontsChanged(); }); - - label->setBuddy(familyCombo); - - fontRows[role] = {label, familyCombo, sizeSpin}; - } - - retranslateUi(); -} - -void FontGridWidget::loadFonts(const FontConfig &cfg) -{ - // Suppress fontsChanged while materializing fallback rows so the initial - // load never reads as a user edit (mirrors paletteGrid blocking). - blockSignals(true); - QMetaEnum fontEnum = QMetaEnum::fromType(); - for (int i = 0; i < fontEnum.keyCount(); ++i) { - auto role = static_cast(fontEnum.value(i)); - const FontRow &row = fontRows[role]; - - // Roles absent from the file resolve the same way the runtime does: an unset - // role falls back to the config's InterfaceDefault, and only then to the - // startup font. appFont() would return the live preview instead, so Reset and - // a first load must not measure against it. - const QFont fallback = cfg.fonts.value(AppFont::InterfaceDefault, themeManager->defaultAppFont(role)); - const QFont font = cfg.fonts.value(role, fallback); - row.familyCombo->setCurrentFont(font); - - int size = font.pointSize(); - if (size <= 0) { - size = qApp->font().pointSize(); - } - if (size <= 0) { - size = FALLBACK_FONT_SIZE; - } - row.sizeSpin->setValue(qBound(MIN_FONT_SIZE, size, MAX_FONT_SIZE)); - } - blockSignals(false); -} - -FontConfig FontGridWidget::currentFontConfig() const -{ - FontConfig cfg; - QMetaEnum fontEnum = QMetaEnum::fromType(); - for (int i = 0; i < fontEnum.keyCount(); ++i) { - auto role = static_cast(fontEnum.value(i)); - const FontRow &row = fontRows.value(role); - - // Canonical family + point size only: a fresh QFont carries default - // (non-bold, non-italic) weight/style, so nothing combo-side leaks in. - QFont font(row.familyCombo->currentFont().family()); - font.setPointSize(row.sizeSpin->value()); - cfg.fonts[role] = font; - } - return cfg; -} - -void FontGridWidget::retranslateUi() -{ - QMetaEnum fontEnum = QMetaEnum::fromType(); - for (int i = 0; i < fontEnum.keyCount(); ++i) { - auto role = static_cast(fontEnum.value(i)); - const FontRow &row = fontRows.value(role); - - row.label->setText(roleDisplayName(role)); - row.label->setToolTip(tr(APP_FONT_DESCRIPTIONS.value(role, ""))); - row.familyCombo->setToolTip(tr(APP_FONT_DESCRIPTIONS.value(role, ""))); - row.familyCombo->setAccessibleName(roleDisplayName(role)); - row.sizeSpin->setToolTip(tr("Point size for %1").arg(roleDisplayName(role))); - row.sizeSpin->setAccessibleName(tr("Point size for %1").arg(roleDisplayName(role))); - row.sizeSpin->setSuffix(tr(" pt")); - } -} diff --git a/cockatrice/src/interface/palette_editor/font_grid_widget.h b/cockatrice/src/interface/palette_editor/font_grid_widget.h deleted file mode 100644 index 8c80306d3..000000000 --- a/cockatrice/src/interface/palette_editor/font_grid_widget.h +++ /dev/null @@ -1,43 +0,0 @@ -#ifndef COCKATRICE_FONT_GRID_WIDGET_H -#define COCKATRICE_FONT_GRID_WIDGET_H - -#include "../theme_config.h" - -#include -#include -#include - -class QFontComboBox; -class QGridLayout; -class QLabel; -class QSpinBox; - -// Per-role font editors — one (family, point size) row per AppFont::Role. -// Owns the load/read round-trip for FontConfig but has no file I/O, mirroring -// PaletteGridWidget. Only family and point size are edited; weight and italic -// stay caller-side concerns and are stripped when reading back. -class FontGridWidget : public QWidget -{ - Q_OBJECT -public: - explicit FontGridWidget(QWidget *parent = nullptr); - - void loadFonts(const FontConfig &cfg); - FontConfig currentFontConfig() const; - void retranslateUi(); - -signals: - void fontsChanged(); - -private: - struct FontRow - { - QLabel *label = nullptr; - QFontComboBox *familyCombo = nullptr; - QSpinBox *sizeSpin = nullptr; - }; - - QMap fontRows; -}; - -#endif // COCKATRICE_FONT_GRID_WIDGET_H diff --git a/cockatrice/src/interface/palette_editor/palette_editor_dialog.cpp b/cockatrice/src/interface/palette_editor/palette_editor_dialog.cpp index 5bcfbb251..b6271291d 100644 --- a/cockatrice/src/interface/palette_editor/palette_editor_dialog.cpp +++ b/cockatrice/src/interface/palette_editor/palette_editor_dialog.cpp @@ -2,7 +2,6 @@ #include "../theme_config.h" #include "../theme_manager.h" -#include "font_grid_widget.h" #include "palette_generator.h" #include "palette_grid_widget.h" #include "quick_setup_panel.h" @@ -42,7 +41,6 @@ PaletteEditorDialog::PaletteEditorDialog(const QString &_themeDirPath, const QSt // Load both scheme configs upfront so switching is instant loadSchemes(); - loadFonts(); loadedScheme = themeManager->isDarkMode(themeDirPath) ? "Dark" : "Light"; @@ -127,33 +125,6 @@ void PaletteEditorDialog::setupUi() paletteGrid->setVisible(false); root->addWidget(paletteGrid, 1); - // Font section — sibling to the palette grid, sharing its collapsible - // advanced area pattern. Fonts are shared per theme, not split by scheme. - fontToggleButton = new QPushButton(this); - fontToggleButton->setCheckable(true); - fontToggleButton->setChecked(false); - fontToggleButton->setFlat(true); - fontToggleButton->setStyleSheet("QPushButton { text-align: left; padding: 5px 12px; font-weight: bold; }" - "QPushButton:checked { }"); - root->addWidget(fontToggleButton); - - fontSeparator = makeSeparator(); - fontSeparator->setVisible(false); - root->addWidget(fontSeparator); - - fontSection = new QWidget; - auto *fontSectionLayout = new QVBoxLayout(fontSection); - fontSectionLayout->setContentsMargins(0, 0, 0, 0); - fontSectionLayout->setSpacing(0); - fontNoteLabel = new QLabel(fontSection); - fontNoteLabel->setContentsMargins(12, 4, 12, 0); - fontNoteLabel->setWordWrap(true); - fontSectionLayout->addWidget(fontNoteLabel); - fontGrid = new FontGridWidget(fontSection); - fontSectionLayout->addWidget(fontGrid); - fontSection->setVisible(false); - root->addWidget(fontSection); - // Footer root->addWidget(makeSeparator()); footer = new QWidget; @@ -186,10 +157,6 @@ void PaletteEditorDialog::setupUi() connect(autoApplyTimer, &QTimer::timeout, this, &PaletteEditorDialog::onApply); connect(quickSetupPanel, &QuickSetupPanel::valueChanged, this, &PaletteEditorDialog::onGenerateFromAccent); connect(paletteGrid, &PaletteGridWidget::paletteChanged, this, [this] { autoApplyTimer->start(); }); - connect(fontGrid, &FontGridWidget::fontsChanged, this, [this] { - fontsDirty = true; - autoApplyTimer->start(); - }); connect(revertButton, &QPushButton::clicked, this, &PaletteEditorDialog::onRevertToDefault); connect(resetBtn, &QPushButton::clicked, this, &PaletteEditorDialog::onReset); connect(saveBtn, &QPushButton::clicked, this, &PaletteEditorDialog::onSave); @@ -199,32 +166,15 @@ void PaletteEditorDialog::setupUi() paletteGridToggleButton->setText(open ? tr("▼ Edit Palette") : tr("▶ Edit Palette")); paletteGridSeparator->setVisible(open); paletteGrid->setVisible(open); - updateAdvancedHeight(); - }); - connect(fontToggleButton, &QPushButton::toggled, this, [this](bool open) { - fontToggleButton->setText(open ? tr("▼ Edit Fonts") : tr("▶ Edit Fonts")); - fontSeparator->setVisible(open); - fontSection->setVisible(open); - updateAdvancedHeight(); - }); -} -void PaletteEditorDialog::updateAdvancedHeight() -{ - int height = 220; - if (paletteGridToggleButton->isChecked()) { - height += 460; - } - if (fontToggleButton->isChecked()) { - height += 200; - } - setMinimumHeight(height); - - if (paletteGrid->isVisible() || fontSection->isVisible()) { - resize(width(), height); - } else { - adjustSize(); // shrinks to fit just the visible content - } + if (open) { + setMinimumHeight(680); + resize(width(), 680); + } else { + setMinimumHeight(220); + adjustSize(); // shrinks to fit just the visible content + } + }); } void PaletteEditorDialog::retranslateUi() @@ -239,16 +189,12 @@ void PaletteEditorDialog::retranslateUi() editingLabel->setText(tr("Editing:")); paletteGridToggleButton->setText(tr("▶ Edit Palette")); paletteGridToggleButton->setToolTip(tr("Show or hide the per-role colour grid for manual tweaks")); - fontToggleButton->setText(fontSection->isVisible() ? tr("▼ Edit Fonts") : tr("▶ Edit Fonts")); - fontToggleButton->setToolTip(tr("Show or hide the per-purpose font settings for this theme")); - fontNoteLabel->setText(tr("Fonts belong to theme %1 (shared by Light and Dark schemes)").arg(themeName)); - fontGrid->retranslateUi(); revertButton->setText(tr("↺ Revert to theme default")); resetBtn->setText(tr("Reset")); saveBtn->setText(tr("Save")); - resetBtn->setToolTip(tr("Discard unsaved edits and restore the last saved palette and fonts")); - saveBtn->setToolTip(tr("Write palette-%1.toml and fonts.toml, then reload the theme").arg(loadedScheme.toLower())); + resetBtn->setToolTip(tr("Discard unsaved edits and restore the last saved palette")); + saveBtn->setToolTip(tr("Write palette-%1.toml and reload the theme").arg(loadedScheme.toLower())); if (saveDir.isEmpty() || !ThemeManager::isDirReallyWritable(saveDir)) { saveBtn->setEnabled(false); @@ -282,13 +228,6 @@ void PaletteEditorDialog::loadSchemes() } } -void PaletteEditorDialog::loadFonts() -{ - savedFontConfig = ThemeManager::loadDefaultFontConfig(themeDirPath, themeName); - fontGrid->loadFonts(savedFontConfig); - fontsDirty = false; -} - void PaletteEditorDialog::seedAccentFromScheme(const QString &scheme) { QColor seed = workingConfig.value(scheme).colors.value(QPalette::Active).value(QPalette::Highlight); @@ -319,9 +258,6 @@ void PaletteEditorDialog::onGenerateFromAccent(const QColor &accent, int intensi void PaletteEditorDialog::onApply() { themeManager->previewPalette(paletteGrid->currentPaletteConfig(), loadedScheme); - if (fontsDirty) { - themeManager->previewFonts(fontGrid->currentFontConfig()); - } } void PaletteEditorDialog::onSave() @@ -372,20 +308,6 @@ void PaletteEditorDialog::onSave() savedConfig[it.key()] = it.value(); } - // Persist the shared per-theme fonts when they changed. - if (fontsDirty) { - const FontConfig fontCfg = fontGrid->currentFontConfig(); - if (fontCfg != savedFontConfig) { - if (!ThemeManager::saveFontConfig(saveDir, fontCfg)) { - QMessageBox::warning(this, tr("Save failed"), - tr("Could not write %1 to:\n%2").arg(FontConfig::fileName(), saveDir)); - return; - } - savedFontConfig = fontCfg; - } - fontsDirty = false; - } - themeManager->reloadCurrentTheme(); accept(); } @@ -394,12 +316,6 @@ void PaletteEditorDialog::onReset() { workingConfig[loadedScheme] = savedConfig[loadedScheme]; paletteGrid->loadPalette(savedConfig[loadedScheme]); - // Restore the live theme before re-reading the grid: loadFonts resolves - // unset roles, and the preview has currentFonts pointing at the edited - // config until the theme is reloaded. - themeManager->reloadCurrentTheme(); - fontGrid->loadFonts(savedFontConfig); - fontsDirty = false; seedAccentFromScheme(loadedScheme); } @@ -434,27 +350,6 @@ void PaletteEditorDialog::onRevertToDefault() workingConfig[loadedScheme] = def; paletteGrid->loadPalette(def); seedAccentFromScheme(loadedScheme); - - // Revert the shared per-theme fonts the same way: delete the user-written - // fonts.toml so the theme falls back to its shipped default, then reflect - // the resolved fonts in the editor. - QFile::remove(QDir(saveDir).absoluteFilePath(FontConfig::fileName())); - - // Reload the live theme so the font revert takes effect immediately. - themeManager->reloadCurrentTheme(); - - const FontConfig fontDef = ThemeManager::loadDefaultFontConfig(themeDirPath, themeName); - savedFontConfig = fontDef; - fontGrid->loadFonts(fontDef); - fontsDirty = false; -} - -void PaletteEditorDialog::reject() -{ - // Discard any unsaved live preview (palette and fonts) so closing without - // Save restores the active theme instead of leaking the preview globally. - themeManager->reloadCurrentTheme(); - QDialog::reject(); } void PaletteEditorDialog::changeEvent(QEvent *e) diff --git a/cockatrice/src/interface/palette_editor/palette_editor_dialog.h b/cockatrice/src/interface/palette_editor/palette_editor_dialog.h index 5e47536b5..e29a70cb0 100644 --- a/cockatrice/src/interface/palette_editor/palette_editor_dialog.h +++ b/cockatrice/src/interface/palette_editor/palette_editor_dialog.h @@ -15,7 +15,6 @@ class QComboBox; class QDialogButtonBox; class QPushButton; class PaletteGridWidget; -class FontGridWidget; class QuickSetupPanel; class QFrame; class QWidget; @@ -26,7 +25,6 @@ class PaletteEditorDialog : public QDialog public: explicit PaletteEditorDialog(const QString &themeDirPath, const QString &themeName, QWidget *parent = nullptr); void loadSchemes(); - void reject() override; private slots: void onSave(); @@ -40,10 +38,8 @@ private: void setupUi(); void retranslateUi(); void refreshChromePalettes(); - void updateAdvancedHeight(); void loadScheme(const QString &scheme); // snapshot current, switch, load void seedAccentFromScheme(const QString &scheme); - void loadFonts(); // shared per theme, not split by light/dark // Sub-widgets QWidget *header; @@ -53,11 +49,6 @@ private: PaletteGridWidget *paletteGrid = nullptr; QPushButton *paletteGridToggleButton = nullptr; QFrame *paletteGridSeparator = nullptr; - FontGridWidget *fontGrid = nullptr; - QPushButton *fontToggleButton = nullptr; - QFrame *fontSeparator = nullptr; - QWidget *fontSection = nullptr; - QLabel *fontNoteLabel = nullptr; QWidget *footer; QComboBox *schemeComboBox = nullptr; QDialogButtonBox *buttonBox = nullptr; @@ -77,18 +68,10 @@ private: QMap workingConfig; QMap savedConfig; - // Fonts are shared per theme (fonts.toml, not split by light/dark), so a - // single saved config suffices; the live edits are read from fontGrid. - FontConfig savedFontConfig; - // Set only by user-driven fontsChanged after the initial load completes. - // Gates both previewFonts and the fonts.toml write so a palette-only Save - // never materializes fallback fonts to disk. - bool fontsDirty = false; - QTimer *autoApplyTimer = nullptr; protected: void changeEvent(QEvent *e) override; }; -#endif // COCKATRICE_PALETTE_EDITOR_DIALOG_H +#endif // COCKATRICE_PALETTE_EDITOR_DIALOG_H \ No newline at end of file diff --git a/cockatrice/src/interface/palette_editor/palette_grid_widget.cpp b/cockatrice/src/interface/palette_editor/palette_grid_widget.cpp index 392f01d0d..463c4038d 100644 --- a/cockatrice/src/interface/palette_editor/palette_grid_widget.cpp +++ b/cockatrice/src/interface/palette_editor/palette_grid_widget.cpp @@ -76,18 +76,6 @@ PaletteGridWidget::PaletteGridWidget(QWidget *parent) : QWidget(parent) layout = QtUtils::makeLayout(this); layout->addWidget(scroll); - - connect(themeManager, &ThemeManager::fontsChanged, this, &PaletteGridWidget::applyInterfaceDefaultFont); -} - -void PaletteGridWidget::applyInterfaceDefaultFont() -{ - // The editor's own chrome follows the InterfaceDefault theme role; bold stays caller-side. - for (auto *label : headerLabels) { - QFont headerFont = themeManager->appFont(AppFont::InterfaceDefault); - headerFont.setBold(true); - label->setFont(headerFont); - } } void PaletteGridWidget::buildGrid(QWidget *host) @@ -113,8 +101,7 @@ void PaletteGridWidget::buildGrid(QWidget *host) auto *label = new QLabel(groupHeaders[col], host); label->setAlignment(Qt::AlignCenter); label->setToolTip(groupTips[col]); - // Family comes from the InterfaceDefault theme role; bold stays caller-side. - label->setFont(QtUtils::boldFont(themeManager->appFont(AppFont::InterfaceDefault))); + label->setFont(QtUtils::boldFont(label->font())); label->setAutoFillBackground(true); label->setContentsMargins(4, 4, 4, 4); grid->addWidget(label, 0, col + 1); @@ -159,7 +146,7 @@ void PaletteGridWidget::buildGrid(QWidget *host) auto *appHeader = new QLabel(tr("App colors"), host); appHeader->setToolTip(tr("Application-specific colors layered on top of the Qt palette")); - appHeader->setFont(QtUtils::boldFont(themeManager->appFont(AppFont::InterfaceDefault))); + appHeader->setFont(QtUtils::boldFont(appHeader->font())); appHeader->setAutoFillBackground(true); appHeader->setContentsMargins(4, 4, 4, 4); grid->addWidget(appHeader, appHeaderRow, 0, 1, 4); diff --git a/cockatrice/src/interface/palette_editor/palette_grid_widget.h b/cockatrice/src/interface/palette_editor/palette_grid_widget.h index 7c527a064..f1ff41ec2 100644 --- a/cockatrice/src/interface/palette_editor/palette_grid_widget.h +++ b/cockatrice/src/interface/palette_editor/palette_grid_widget.h @@ -34,10 +34,6 @@ private: void changeEvent(QEvent *e); void refreshChromePalettes(); -private slots: - void applyInterfaceDefaultFont(); - -private: QMap> colorButtons; QMap appColorButtons; QScrollArea *scroll; diff --git a/cockatrice/src/interface/palette_editor/quick_setup_panel.cpp b/cockatrice/src/interface/palette_editor/quick_setup_panel.cpp index 00eac3253..04f98dcc3 100644 --- a/cockatrice/src/interface/palette_editor/quick_setup_panel.cpp +++ b/cockatrice/src/interface/palette_editor/quick_setup_panel.cpp @@ -1,6 +1,5 @@ #include "quick_setup_panel.h" -#include "../theme_manager.h" #include "color_button.h" #include @@ -29,15 +28,10 @@ QuickSetupPanel::QuickSetupPanel(QWidget *parent) : QWidget(parent) labelLow = new QLabel(this); labelHigh = new QLabel(this); - // Family comes from the InterfaceDefault theme role; the *0.82 size stays caller-side. - auto applySmallFont = [this]() { - QFont small = themeManager->appFont(AppFont::InterfaceDefault); - small.setPointSizeF(small.pointSizeF() * 0.82); - labelLow->setFont(small); - labelHigh->setFont(small); - }; - applySmallFont(); - connect(themeManager, &ThemeManager::fontsChanged, this, applySmallFont); + QFont small = labelLow->font(); + small.setPointSizeF(small.pointSizeF() * 0.82); + labelLow->setFont(small); + labelHigh->setFont(small); QPalette dimmed = labelLow->palette(); dimmed.setColor(QPalette::WindowText, qApp->palette().color(QPalette::Mid)); labelLow->setPalette(dimmed); diff --git a/cockatrice/src/interface/pixel_map_generator.cpp b/cockatrice/src/interface/pixel_map_generator.cpp index 037293af9..2f5c4b6dd 100644 --- a/cockatrice/src/interface/pixel_map_generator.cpp +++ b/cockatrice/src/interface/pixel_map_generator.cpp @@ -1,7 +1,6 @@ #include "pixel_map_generator.h" #include "libcockatrice/network/server/remote/user_level.h" -#include "theme_icon.h" #include "theme_manager.h" #include @@ -16,6 +15,7 @@ #include #include #include +#include #include #include #include @@ -436,19 +436,33 @@ QIcon UserLevelPixmapGenerator::generateIconWithColorOverride(int height, QMap UserLevelPixmapGenerator::iconCache; -QPixmap DropdownIconPixmapGenerator::generatePixmap(int height, bool expanded) +QPixmap LockPixmapGenerator::generatePixmap(int height) { - const QString key = QString::number(expanded) + ":" + QString::number(height); + + int key = height; if (pmCache.contains(key)) { return pmCache.value(key); } - // Render through the themed icon engine: it keeps the glyph's aspect ratio, - // follows the palette, and renders at the device pixel ratio instead of - // handing a DPR-1 bitmap to a label that would upscale and blur it on a 2x - // display. - const QString prefix = QStringLiteral("icons/") + (expanded ? "dropdown_expanded" : "dropdown_collapsed"); - const QPixmap pixmap = loadThemedIcon(prefix).pixmap(QSize(height, height), qApp->devicePixelRatio()); + QPixmap pixmap = tryLoadImage(QStringLiteral("theme:") + themeManager->assetPath(QStringLiteral("icons/lock")), + QSize(height, height), true); + pmCache.insert(key, pixmap); + return pixmap; +} + +QMap LockPixmapGenerator::pmCache; + +QPixmap DropdownIconPixmapGenerator::generatePixmap(int height, bool expanded) +{ + QString key = QString::number(expanded) + ":" + QString::number(height); + if (pmCache.contains(key)) { + return pmCache.value(key); + } + + QString name = expanded ? "dropdown_expanded" : "dropdown_collapsed"; + QPixmap pixmap = tryLoadImage(QStringLiteral("theme:") + themeManager->assetPath(QStringLiteral("icons/") + name), + QSize(height, height), true); + pmCache.insert(key, pixmap); return pixmap; } @@ -506,11 +520,30 @@ QPixmap ManaSymbolPixmapGenerator::generatePixmap(const QString &symbol, const Q QHash ManaSymbolPixmapGenerator::masterCache; QHash ManaSymbolPixmapGenerator::scaledCache; +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) { + QImage img(name); + img.invertPixels(); + QPixmap result; + result.convertFromImage(img); + return result; + } else { + return QPixmap(name); + } +} + QPixmap themePixmap(QStringView prefix) { const QString resolved = themeManager->assetPath(prefix); - const QPixmap sentinel = loadSentinelSvgPixmap(resolved); - return sentinel.isNull() ? QPixmap(QStringLiteral("theme:") + resolved) : sentinel; + return QPixmap(QStringLiteral("theme:") + resolved); } void clearPixmapGeneratorCaches() @@ -520,6 +553,7 @@ void clearPixmapGeneratorCaches() PingPixmapGenerator::clear(); CountryPixmapGenerator::clear(); UserLevelPixmapGenerator::clear(); + LockPixmapGenerator::clear(); DropdownIconPixmapGenerator::clear(); ManaSymbolPixmapGenerator::clear(); } diff --git a/cockatrice/src/interface/pixel_map_generator.h b/cockatrice/src/interface/pixel_map_generator.h index 951f4231b..7c01bb332 100644 --- a/cockatrice/src/interface/pixel_map_generator.h +++ b/cockatrice/src/interface/pixel_map_generator.h @@ -106,6 +106,19 @@ public: } }; +class LockPixmapGenerator +{ +private: + static QMap pmCache; + +public: + static QPixmap generatePixmap(int height); + static void clear() + { + pmCache.clear(); + } +}; + class DropdownIconPixmapGenerator { private: @@ -147,11 +160,13 @@ public: } }; -// Returns a "theme:" asset (with no file extension in prefix), preferring the +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. Token-driven -// SVGs resolve their `currentColor` sentinel from the palette. Use for -// scheme-sensitive pixmaps like backgrounds, the card back, and the app logo. +// 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 diff --git a/cockatrice/src/interface/recording.cpp b/cockatrice/src/interface/recording.cpp deleted file mode 100644 index 2b11255f9..000000000 --- a/cockatrice/src/interface/recording.cpp +++ /dev/null @@ -1,102 +0,0 @@ -#include "recording.h" - -#include "pqEventTypes.h" - -#include -#include -#include -#include - -XMLEventObserver::XMLEventObserver(QObject *parent) : pqEventObserver(parent) -{ -} - -XMLEventObserver::~XMLEventObserver() = default; - -void XMLEventObserver::setStream(QTextStream *stream) -{ - if (xmlStream) { - xmlStream->writeEndElement(); - xmlStream->writeEndDocument(); - xmlStream.reset(); - } - if (Stream) { - *Stream << xmlString; - } - xmlString.clear(); - pqEventObserver::setStream(stream); - if (Stream) { - xmlStream.reset(new QXmlStreamWriter(&xmlString)); - xmlStream->setAutoFormatting(true); - xmlStream->writeStartDocument(); - xmlStream->writeStartElement(QStringLiteral("events")); - } -} - -void XMLEventObserver::onRecordEvent(const QString &widget, - const QString &command, - const QString &arguments, - const int &eventType) -{ - if (!xmlStream) { - return; - } - - xmlStream->writeStartElement(QStringLiteral("event")); - xmlStream->writeAttribute(QStringLiteral("widget"), widget); - if (eventType == pqEventTypes::ACTION_EVENT) { - xmlStream->writeAttribute(QStringLiteral("command"), command); - } else { // pqEventTypes::CHECK_EVENT - xmlStream->writeAttribute(QStringLiteral("property"), command); - } - xmlStream->writeAttribute(QStringLiteral("arguments"), arguments); - xmlStream->writeEndElement(); -} - -XMLEventSource::XMLEventSource(QObject *parent) : pqEventSource(parent) -{ -} - -XMLEventSource::~XMLEventSource() = default; - -void XMLEventSource::setContent(const QString &xmlFileName) -{ - xmlStream.reset(); - - QFile xml(xmlFileName); - if (!xml.open(QIODevice::ReadOnly)) { - qCWarning(RecordingLog) << "Could not read test file" << xmlFileName; - return; - } - - xmlStream.reset(new QXmlStreamReader(xml.readAll())); -} - -int XMLEventSource::getNextEvent(QString &widget, QString &command, QString &arguments, int &eventType) -{ - if (!xmlStream) { - return DONE; - } - - while (!xmlStream->atEnd()) { - if (xmlStream->readNext() != QXmlStreamReader::StartElement || xmlStream->name() != QStringLiteral("event")) { - continue; - } - - const QXmlStreamAttributes attributes = xmlStream->attributes(); - eventType = pqEventTypes::ACTION_EVENT; - widget = attributes.value(QStringLiteral("widget")).toString(); - command = attributes.value(QStringLiteral("command")).toString(); - if (command.isEmpty()) { - // A recorded check event carries a property instead of a command. - command = attributes.value(QStringLiteral("property")).toString(); - } - arguments = attributes.value(QStringLiteral("arguments")).toString(); - return SUCCESS; - } - - if (xmlStream->hasError()) { - qCWarning(RecordingLog) << "Malformed test file:" << xmlStream->errorString(); - } - return DONE; -} diff --git a/cockatrice/src/interface/recording.h b/cockatrice/src/interface/recording.h deleted file mode 100644 index a5d813ea9..000000000 --- a/cockatrice/src/interface/recording.h +++ /dev/null @@ -1,54 +0,0 @@ -#ifndef COCKATRICE_RECORDING_H -#define COCKATRICE_RECORDING_H - -#include "pqEventObserver.h" -#include "pqEventSource.h" - -#include -#include - -inline Q_LOGGING_CATEGORY(RecordingLog, "recording"); - -class QXmlStreamReader; -class QXmlStreamWriter; - -/** - * @brief Writes a session recording as an XML test file. - * - * The events are buffered in memory and flushed to the stream when recording - * ends, because the stream is handed over once the file name is known. - */ -class XMLEventObserver : public pqEventObserver -{ - QScopedPointer xmlStream; - QString xmlString; - -public: - explicit XMLEventObserver(QObject *parent); - ~XMLEventObserver() override; - -protected: - void setStream(QTextStream *stream) override; - void onRecordEvent(const QString &widget, - const QString &command, - const QString &arguments, - const int &eventType) override; -}; - -/** - * @brief Plays the events of an XML test file back, one at a time. - */ -class XMLEventSource : public pqEventSource -{ - QScopedPointer xmlStream; - -public: - explicit XMLEventSource(QObject *parent); - ~XMLEventSource() override; - -protected: - void setContent(const QString &xmlFileName) override; - int getNextEvent(QString &widget, QString &command, QString &arguments, int &eventType) override; -}; - -#endif // COCKATRICE_RECORDING_H diff --git a/cockatrice/src/interface/theme_config.cpp b/cockatrice/src/interface/theme_config.cpp index 54143d7d3..652e322c8 100644 --- a/cockatrice/src/interface/theme_config.cpp +++ b/cockatrice/src/interface/theme_config.cpp @@ -304,150 +304,4 @@ QPalette PaletteConfig::apply(QPalette base) const } return base; -} - -bool FontConfig::hasFonts() const -{ - return !fonts.isEmpty(); -} - -QString FontConfig::toToml() const -{ - QMetaEnum roleEnum = QMetaEnum::fromType(); - - QString entries; - - for (auto it = fonts.cbegin(); it != fonts.cend(); ++it) { - const char *roleName = roleEnum.valueToKey(it.key()); - - if (!roleName) { - continue; - } - - QString family = it.value().family().trimmed(); - int pointSize = it.value().pointSize(); - - if (family.isEmpty() || pointSize <= 0) { - continue; - } - - entries += QString("%1 = \"%2, %3\"\n").arg(QString(roleName), -20).arg(family).arg(pointSize); - } - - if (entries.isEmpty()) { - return {}; - } - - return "[Fonts]\n" + entries + "\n"; -} - -QString FontConfig::fileName() -{ - return "fonts.toml"; -} - -FontConfig FontConfig::fromFile(const QString &filePath) -{ - FontConfig cfg; - - QFile f(filePath); - - if (!f.open(QIODevice::ReadOnly | QIODevice::Text)) { - return cfg; - } - - QMetaEnum roleEnum = QMetaEnum::fromType(); - - QString currentSection; - - QTextStream in(&f); - - while (!in.atEnd()) { - QString line = in.readLine().trimmed(); - - if (line.isEmpty() || line.startsWith('#') || line.startsWith(';')) { - continue; - } - - if (line.startsWith('[') && line.endsWith(']')) { - currentSection = line.mid(1, line.length() - 2).trimmed(); - continue; - } - - if (currentSection.compare("Fonts", Qt::CaseInsensitive) != 0) { - continue; - } - - int eq = line.indexOf('='); - - if (eq < 0) { - continue; - } - - QString key = line.left(eq).trimmed(); - QString value = line.mid(eq + 1).trimmed(); - - // Strip inline comments if preceded by whitespace - for (int i = 1; i < value.size(); ++i) { - if ((value[i] == '#' || value[i] == ';') && value[i - 1].isSpace()) { - value = value.left(i).trimmed(); - break; - } - } - - // Strip surrounding quotes, if any - if (value.size() >= 2 && - ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith('\'') && value.endsWith('\'')))) { - value = value.mid(1, value.size() - 2).trimmed(); - } - - int comma = value.lastIndexOf(','); - - if (comma < 0) { - continue; - } - - QString family = value.left(comma).trimmed(); - - // Tolerate a partially-quoted family, e.g. "Inter", 10 - if (family.size() >= 2 && - ((family.startsWith('"') && family.endsWith('"')) || (family.startsWith('\'') && family.endsWith('\'')))) { - family = family.mid(1, family.size() - 2).trimmed(); - } - - bool ok = false; - int pointSize = value.mid(comma + 1).trimmed().toInt(&ok); - - if (!ok || family.isEmpty() || pointSize <= 0) { - continue; - } - - if (key.startsWith("AppFont::")) { - key = key.mid(QStringLiteral("AppFont::").size()); - } - - int roleInt = roleEnum.keyToValue(key.toUtf8().constData()); - - if (roleInt < 0) { - continue; - } - - // Canonical family + point size only: a fresh QFont carries default - // (non-bold, non-italic) weight/style, so nothing caller-side leaks in. - // No validation against installed fonts; Qt substitution applies. - QFont font(family); - font.setPointSize(pointSize); - cfg.fonts[static_cast(roleInt)] = font; - } - - return cfg; -} - -FontConfig FontConfig::fromThemeDir(const QString &themeDirPath) -{ - if (themeDirPath.isEmpty()) { - return {}; - } - - return fromFile(QDir(themeDirPath).absoluteFilePath(fileName())); -} +} \ No newline at end of file diff --git a/cockatrice/src/interface/theme_config.h b/cockatrice/src/interface/theme_config.h index 84601a2bf..8b91613f5 100644 --- a/cockatrice/src/interface/theme_config.h +++ b/cockatrice/src/interface/theme_config.h @@ -2,7 +2,6 @@ #define COCKATRICE_THEME_CONFIG_H #include -#include #include #include #include @@ -24,23 +23,6 @@ enum Role Q_ENUM_NS(Role) } // namespace AppColor -// Application-specific font roles, one configurable family + point size per -// purpose. Stored in a shared fonts.toml under a [Fonts] section (one file per -// theme, not split by light/dark). Only family and point size are stored; -// weight/italic are stripped on load and stay caller-side concerns. -namespace AppFont -{ -Q_NAMESPACE -enum Role -{ - InterfaceDefault, - ChatLog, - CardText, - DeckList, -}; -Q_ENUM_NS(Role) -} // namespace AppFont - struct ThemeConfig { QString colorScheme; @@ -78,33 +60,4 @@ struct PaletteConfig QPalette apply(QPalette base) const; }; -/** @brief Per-purpose font configuration, stored as family + point size only. - * - * Serialized to a shared fonts.toml file with a [Fonts] section, e.g. - * @code - * [Fonts] - * InterfaceDefault = "Inter, 10" - * @endcode - */ -struct FontConfig -{ - QMap fonts; - - bool operator==(const FontConfig &rhs) const - { - return fonts == rhs.fonts; - } - bool operator!=(const FontConfig &rhs) const - { - return !(*this == rhs); - } - bool hasFonts() const; - QString toToml() const; - - static QString fileName(); - - static FontConfig fromFile(const QString &filePath); - static FontConfig fromThemeDir(const QString &themeDirPath); -}; - -#endif // COCKATRICE_THEME_CONFIG_H +#endif // COCKATRICE_THEME_CONFIG_H \ No newline at end of file diff --git a/cockatrice/src/interface/theme_icon.cpp b/cockatrice/src/interface/theme_icon.cpp deleted file mode 100644 index 161c436ca..000000000 --- a/cockatrice/src/interface/theme_icon.cpp +++ /dev/null @@ -1,300 +0,0 @@ -#include "theme_icon.h" - -#include "libcockatrice/utility/svg_recolor.h" -#include "theme_manager.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace -{ -/** - * @brief Palette color that replaces `currentColor`, following the icon mode. - */ -QByteArray sentinelColor(QIcon::Mode mode, QPalette::ColorRole role) -{ - const QPalette::ColorGroup group = mode == QIcon::Disabled ? QPalette::Disabled : QPalette::Active; - const QPalette::ColorRole effective = - mode == QIcon::Selected && role == QPalette::WindowText ? QPalette::HighlightedText : role; - return qApp->palette().color(group, effective).name(QColor::HexRgb).toLatin1(); -} - -/** - * @brief Reads an SVG through the `theme:` search path. - */ -QByteArray readSvg(const QString &resolvedPath) -{ - QFile file(QStringLiteral("theme:") + resolvedPath); - if (!file.open(QIODevice::ReadOnly)) { - return {}; - } - return file.readAll(); -} - -/** - * @brief Renders SVG bytes, scaled to @p size when one is given. - * - * An empty size means the asset's intrinsic size; @p devicePixelRatio only - * applies in that case so HiDPI callers still get a crisp glyph. - */ -QPixmap renderSvg(const QByteArray &data, const QSize &size, qreal devicePixelRatio) -{ - QSvgRenderer renderer(data); - if (!renderer.isValid()) { - return {}; - } - - QSize renderSize = size; - if (renderSize.isEmpty()) { - renderSize = renderer.defaultSize(); - renderSize = - QSize(qRound(renderSize.width() * devicePixelRatio), qRound(renderSize.height() * devicePixelRatio)); - } - if (!renderSize.isValid() || renderSize.isEmpty()) { - return {}; - } - - QPixmap pixmap(renderSize); - pixmap.fill(Qt::transparent); - QPainter painter(&pixmap); - renderer.render(&painter); - return pixmap; -} - -bool hasSentinel(const QByteArray &svg) -{ - return !svg.isEmpty() && QString::fromUtf8(svg).contains(QStringLiteral("currentColor"), Qt::CaseInsensitive); -} - -QString cacheKey(const QString &path, const QByteArray &variant, const QSize &size, qreal devicePixelRatio) -{ - return path + QLatin1Char('|') + QString::fromLatin1(variant) + QLatin1Char('|') + QString::number(size.width()) + - QLatin1Char('x') + QString::number(size.height()) + QLatin1Char('@') + QString::number(devicePixelRatio); -} - -/** - * @brief QIcon engine rendering a `theme:` SVG with `currentColor` resolved on demand. - * - * Qt's SVG renderer has no ambient color, so the sentinel is substituted from - * the current palette on every pixmap request. Asset resolution, the file bytes - * and the intrinsic size are cached until the theme changes: a repaint then - * does no file I/O at all. The render cache is keyed by resolved path and pixel - * size plus a variant: the palette color for sentinel SVGs, the icon mode for - * assets that carry their own colors. A palette change therefore produces new - * keys instead of reusing art built under the previous palette, and the - * non-sentinel path (raster overrides, plain SVGs) is cached like any other. - */ -class ThemeSvgIconEngine final : public QObject, public QIconEngine -{ -public: - ThemeSvgIconEngine(QString _prefix, QPalette::ColorRole _role) : prefix(std::move(_prefix)), role(_role) - { - if (themeManager) { - // A theme switch changes the resolved asset (scheme-qualified - // variants, raster overrides) and a palette change invalidates - // every cached colour, so both drop the caches instead of - // re-reading theme.cfg and the assets on every repaint. - connect(themeManager, &ThemeManager::themeChanged, this, &ThemeSvgIconEngine::invalidate); - connect(themeManager, &ThemeManager::paletteChanged, this, &ThemeSvgIconEngine::invalidate); - } - } - - QIconEngine *clone() const override - { - return new ThemeSvgIconEngine(prefix, role); - } - - void paint(QPainter *painter, const QRect &rect, QIcon::Mode mode, QIcon::State) override - { - const qreal devicePixelRatio = painter->device() ? painter->device()->devicePixelRatioF() : 1.0; - const QSize logicalSize = fitToSize(rect.size()); - if (logicalSize.isEmpty()) { - return; - } - const QSize pixelSize(qRound(logicalSize.width() * devicePixelRatio), - qRound(logicalSize.height() * devicePixelRatio)); - const QPixmap pixmap = render(pixelSize, devicePixelRatio, mode); - if (!pixmap.isNull()) { - // QRect::center() would floor and drop the last row/column of a - // 16x16 glyph; the center has to be fractional so glyphs stay - // unshifted and unsclipped in item views. - const QSizeF contentSize = pixmap.deviceIndependentSize(); - const QRectF targetRect( - QRectF(rect).center() - QPointF(contentSize.width() / 2.0, contentSize.height() / 2.0), contentSize); - painter->drawPixmap(targetRect, pixmap, QRectF(pixmap.rect())); - } - } - - QPixmap pixmap(const QSize &size, QIcon::Mode mode, QIcon::State) override - { - return render(fitToSize(size), 1.0, mode); - } - - QPixmap scaledPixmap(const QSize &size, QIcon::Mode mode, QIcon::State, qreal scale) override - { - const QSize logicalSize = fitToSize(size); -#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0) - const QSize pixelSize(qRound(logicalSize.width() * scale), qRound(logicalSize.height() * scale)); -#else - // Qt < 6.8 passes the device-dependent size to the engine. - const QSize pixelSize = logicalSize; -#endif - return render(pixelSize, scale, mode); - } - - QSize actualSize(const QSize &size, QIcon::Mode, QIcon::State) override - { - return fitToSize(size); - } - -private: - void invalidate() - { - hasResolvedAsset = false; - hasIntrinsicSize = false; - cache.clear(); - } - - void ensureResolved() const - { - if (hasResolvedAsset && (!themeManager || themeManager->getCurrentThemePath() == resolvedThemePath)) { - return; - } - resolvedThemePath = themeManager ? themeManager->getCurrentThemePath() : QString(); - resolvedPath = themeManager ? themeManager->assetPath(prefix) : QString(); - resolvedAssetIsSvg = resolvedPath.endsWith(QStringLiteral(".svg"), Qt::CaseInsensitive); - resolvedSvg = resolvedAssetIsSvg ? readSvg(resolvedPath) : QByteArray(); - resolvedHasSentinel = hasSentinel(resolvedSvg); - hasResolvedAsset = true; - hasIntrinsicSize = false; - } - - QSize intrinsicSize() const - { - ensureResolved(); - if (!hasIntrinsicSize) { - if (resolvedAssetIsSvg) { - QSvgRenderer renderer(resolvedSvg); - intrinsicSizeCache = renderer.isValid() ? renderer.defaultSize() : QSize(); - } else { - // A theme may override a glyph with a raster; take its real - // aspect ratio instead of assuming the 24x24 SVG default, so - // actualSize() and the rendered pixmap agree. - QImageReader reader(QStringLiteral("theme:") + resolvedPath); - intrinsicSizeCache = reader.size(); - } - if (intrinsicSizeCache.isEmpty()) { - intrinsicSizeCache = QSize(24, 24); - } - hasIntrinsicSize = true; - } - return intrinsicSizeCache; - } - - QSize fitToSize(const QSize &size) const - { - if (size.isEmpty()) { - return size; - } - return intrinsicSize().scaled(size, Qt::KeepAspectRatio); - } - - QPixmap fromAsset(const QSize &pixelSize, QIcon::Mode mode) const - { - QPixmap pixmap(QStringLiteral("theme:") + resolvedPath); - if (!pixelSize.isEmpty() && pixmap.size() != pixelSize) { - pixmap = pixmap.scaled(pixelSize, Qt::KeepAspectRatio, Qt::SmoothTransformation); - } - if (mode == QIcon::Disabled && !pixmap.isNull()) { - pixmap = QApplication::style()->generatedIconPixmap(QIcon::Disabled, pixmap, nullptr); - } - return pixmap; - } - - QPixmap render(const QSize &pixelSize, qreal devicePixelRatio, QIcon::Mode mode) const - { - ensureResolved(); - - // Same cache for both paths: sentinel art varies by palette color, art - // with its own colors varies by mode (Disabled is generated). A color - // variant is never a bare digit, so the two key spaces cannot collide. - const QByteArray variant = - resolvedHasSentinel ? sentinelColor(mode, role) : QByteArray::number(static_cast(mode)); - const QString key = cacheKey(resolvedPath, variant, pixelSize, devicePixelRatio); - const auto cached = cache.constFind(key); - if (cached != cache.constEnd()) { - return cached.value(); - } - - QPixmap pixmap; - if (resolvedHasSentinel) { - pixmap = renderSvg(resolveSvgColorSentinel(resolvedSvg, variant), pixelSize, devicePixelRatio); - } else { - // Authored art carries its own colors: use Qt's normal loader so - // raster overrides and non-SVG assets keep working. - pixmap = fromAsset(pixelSize, mode); - } - if (!pixmap.isNull()) { - // Set the ratio before caching: assigning it to a cached pixmap - // afterwards detaches (deep-copies) it on every paint. - pixmap.setDevicePixelRatio(devicePixelRatio); - } - if (cache.size() >= 64) { - cache.clear(); - } - cache.insert(key, pixmap); - return pixmap; - } - - QString prefix; - QPalette::ColorRole role; - mutable QHash cache; - mutable QString resolvedPath; - mutable QString resolvedThemePath; - mutable QByteArray resolvedSvg; - mutable QSize intrinsicSizeCache; - mutable bool resolvedAssetIsSvg = false; - mutable bool resolvedHasSentinel = false; - mutable bool hasResolvedAsset = false; - mutable bool hasIntrinsicSize = false; -}; -} // namespace - -QIcon loadThemedIcon(QStringView prefix, QPalette::ColorRole role) -{ - return QIcon(new ThemeSvgIconEngine(prefix.toString(), role)); -} - -QPixmap loadSentinelSvgPixmap(const QString &resolvedPath, QPalette::ColorRole role, const QSize &size) -{ - if (!resolvedPath.endsWith(QStringLiteral(".svg"), Qt::CaseInsensitive)) { - return {}; - } - - const QByteArray svg = readSvg(resolvedPath); - if (!hasSentinel(svg)) { - return {}; - } - - const QByteArray color = qApp->palette().color(role).name(QColor::HexRgb).toLatin1(); - return renderSvg(resolveSvgColorSentinel(svg, color), size, 1.0); -} diff --git a/cockatrice/src/interface/theme_icon.h b/cockatrice/src/interface/theme_icon.h deleted file mode 100644 index e26015ea1..000000000 --- a/cockatrice/src/interface/theme_icon.h +++ /dev/null @@ -1,50 +0,0 @@ -/** - * @file theme_icon.h - * @ingroup Interface - */ - -#ifndef THEME_ICON_H -#define THEME_ICON_H - -#include -#include -#include -#include -#include -#include - -/** - * @brief Builds a palette-aware icon from a `theme:` asset. - * - * SVG glyphs may use the `currentColor` sentinel. A plain QPixmap snapshot - * cannot follow palette changes, so this returns a QIcon whose engine resolves - * the sentinel from the current palette every time it is asked for a pixmap. - * Icons therefore stay legible when the user switches between light and dark - * themes without recreating any actions or buttons. - * - * @param prefix Asset prefix relative to the `theme:` search path, with no file - * extension (for example `icons/search`), matching themePixmap(). - * @param role Palette role whose color replaces the sentinel. Selected glyphs - * use HighlightedText unless a role other than WindowText is given. - * @return An icon that follows the application palette. - */ -QIcon loadThemedIcon(QStringView prefix, QPalette::ColorRole role = QPalette::WindowText); - -/** - * @brief Renders a resolved theme SVG when it carries the `currentColor` sentinel. - * - * Non-SVG assets and SVGs without the sentinel return a null pixmap so callers - * can fall back to their usual loading path (including legacy inversion). - * - * @param resolvedPath Asset path relative to the `theme:` search path, as - * returned by ThemeManager::assetPath (includes extension). - * @param role Palette role whose color replaces the sentinel. - * @param size Pixel size to render at; an empty size uses the SVG's intrinsic size. - * @return The recolored pixmap, or a null pixmap when the asset is not a - * sentinel-carrying SVG. - */ -QPixmap loadSentinelSvgPixmap(const QString &resolvedPath, - QPalette::ColorRole role = QPalette::WindowText, - const QSize &size = {}); - -#endif // THEME_ICON_H diff --git a/cockatrice/src/interface/theme_manager.cpp b/cockatrice/src/interface/theme_manager.cpp index f120af8e4..893cdc795 100644 --- a/cockatrice/src/interface/theme_manager.cpp +++ b/cockatrice/src/interface/theme_manager.cpp @@ -12,7 +12,6 @@ #include #include #include -#include #include #include #include @@ -128,10 +127,8 @@ static QString usableDefaultStyle(const QString &style) ThemeManager::ThemeManager(QObject *parent) : QObject(parent) { defaultStyleName = usableDefaultStyle(qApp->style()->objectName()); - // Capture the untouched application palette and font before any theme is - // applied. + // Capture the untouched application palette before any theme is applied. defaultPalette = qApp->palette(); - defaultFont = qApp->font(); ensureThemeDirectoryExists(); #if (QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)) connect(QGuiApplication::styleHints(), &QStyleHints::colorSchemeChanged, this, [this] { @@ -443,46 +440,6 @@ PaletteConfig ThemeManager::loadDefaultPaletteConfig(const QString &themeDirPath return cfg; } -FontConfig ThemeManager::loadFontConfig(const QString &themeDirPath) -{ - if (themeDirPath.isEmpty()) { - return {}; - } - return FontConfig::fromThemeDir(themeDirPath); -} - -FontConfig ThemeManager::loadDefaultFontConfig(const QString &themeDirPath, const QString &themeName) -{ - FontConfig cfg = FontConfig::fromThemeDir(themeDirPath); - if (!cfg.hasFonts()) { - // The shipped fonts.toml may live in the system theme directory rather - // than the resolved (user) theme directory, so built-in themes still - // get their curated fonts. - cfg = FontConfig::fromThemeDir(QDir(systemThemesBasePath()).absoluteFilePath(themeName)); - } - return cfg; -} - -bool ThemeManager::saveFontConfig(const QString &themeDirPath, const FontConfig &cfg) -{ - if (themeDirPath.isEmpty()) { - return false; - } - - QDir dir(themeDirPath); - if (!dir.exists()) { - dir.mkpath("."); - } - - QFile f(dir.absoluteFilePath(FontConfig::fileName())); - if (!f.open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate)) { - return false; - } - - QTextStream(&f) << cfg.toToml(); - return true; -} - bool ThemeManager::commitPalette(const QString &themeDirPath, const QString &colorScheme, const PaletteConfig &cfg) { if (!savePaletteConfig(themeDirPath, colorScheme, cfg)) { @@ -531,24 +488,6 @@ void ThemeManager::previewPalette(const PaletteConfig &cfg, const QString &schem applyStyleAndPalette(themeName, themeCfg, cfg, scheme); } -void ThemeManager::previewFonts(const FontConfig &cfg) -{ - currentFonts = cfg; - - // QApplication::setFont already sends a font change to every widget, so a - // repolish pass is not needed for fonts (unlike the palette path). - const auto it = cfg.fonts.constFind(AppFont::InterfaceDefault); - if (it != cfg.fonts.constEnd()) { - qApp->setFont(it.value()); - } else { - // A config without InterfaceDefault means "no override": restore the - // startup font instead of leaving the last preview applied. - qApp->setFont(defaultFont); - } - - emit fontsChanged(); -} - void ThemeManager::applyStyleAndPalette(const QString &themeName, const ThemeConfig &themeCfg, const PaletteConfig &palCfg, @@ -652,56 +591,6 @@ QColor ThemeManager::appColor(AppColor::Role role) const return accent; } -QFont ThemeManager::appFont(AppFont::Role role) const -{ - const auto it = currentFonts.fonts.constFind(role); - if (it != currentFonts.fonts.constEnd()) { - return it.value(); - } - - // Role fallbacks preserve the pre-theme defaults when no fonts.toml is - // present: an unset role resolves to the application font. Surfaces that - // hardcoded a different family ask for it explicitly: counter painters - // through cardTextFont(), the deck view headers and printed deck list - // through deckListFont(), the report dialogs through - // report_utils::reportLogFont(). Only the family basis is fixed there; - // sizes stay caller-side. - return qApp->font(); -} - -bool ThemeManager::hasAppFont(AppFont::Role role) const -{ - return currentFonts.fonts.contains(role); -} - -QFont ThemeManager::cardTextFont() const -{ - if (hasAppFont(AppFont::CardText)) { - return appFont(AppFont::CardText); - } - QFont serifFont(QStringLiteral("Serif")); - serifFont.setStyleHint(QFont::Serif); - return serifFont; -} - -QFont ThemeManager::deckListFont() const -{ - if (hasAppFont(AppFont::DeckList)) { - return appFont(AppFont::DeckList); - } - QFont serifFont(QStringLiteral("Serif")); - serifFont.setStyleHint(QFont::Serif); - return serifFont; -} - -QFont ThemeManager::defaultAppFont(AppFont::Role /*role*/) const -{ - // No per-role scaling and no preview: an unset role resolves to the font - // captured at startup, which is what the theme falls back to once the - // editor closes. - return defaultFont; -} - void ThemeManager::themeChangedSlot() { QString themeName = SettingsCache::instance().getThemeName(); @@ -753,19 +642,6 @@ void ThemeManager::themeChangedSlot() applyStyleAndPalette(themeName, themeCfg, palette, activeScheme); - // ── Load fonts: shared per theme, no light/dark split ─────────────────── - FontConfig fonts = loadDefaultFontConfig(dirPath, themeName); - currentFonts = fonts; - const auto fontIt = currentFonts.fonts.constFind(AppFont::InterfaceDefault); - if (fontIt != currentFonts.fonts.constEnd()) { - qApp->setFont(fontIt.value()); - } else { - // No theme font: restore the pristine application font captured at - // startup, so a reverted preview cannot leak into the live theme. - qApp->setFont(defaultFont); - } - emit fontsChanged(); - QStringList resources; if (!dirPath.isEmpty()) { resources << dir.absolutePath(); diff --git a/cockatrice/src/interface/theme_manager.h b/cockatrice/src/interface/theme_manager.h index ffe1043b5..c1ab65685 100644 --- a/cockatrice/src/interface/theme_manager.h +++ b/cockatrice/src/interface/theme_manager.h @@ -11,7 +11,6 @@ #include #include -#include #include #include #include @@ -48,15 +47,10 @@ private: // palette is applied. Used as the base when a theme supplies no palette, so // switching away from a custom palette restores the original colours. QPalette defaultPalette; - // Pristine application font captured at startup, before any theme font is - // applied. Restored when a theme (or a preview) supplies no InterfaceDefault - // font, so switching away from a themed font cannot leave it behind. - QFont defaultFont; QString currentThemePath; std::array brushes; QStringMap availableThemes; QMap currentAppColors; - FontConfig currentFonts; /* Internal cache for multiple backgrounds */ @@ -111,9 +105,6 @@ public: // Load/save per-scheme palette colors static PaletteConfig loadPaletteConfig(const QString &themeDirPath, const QString &colorScheme); static bool savePaletteConfig(const QString &themeDirPath, const QString &colorScheme, const PaletteConfig &cfg); - // Load/save shared per-theme fonts (fonts.toml, not split by scheme) - static FontConfig loadFontConfig(const QString &themeDirPath); - static bool saveFontConfig(const QString &themeDirPath, const FontConfig &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 "-" @@ -132,9 +123,6 @@ public: // theme directory when it is absent from the resolved (user) directory. static PaletteConfig loadDefaultPaletteConfig(const QString &themeDirPath, const QString &themeName, const QString &colorScheme); - // Load the theme's shipped default fonts, falling back to the system theme - // directory when fonts.toml is absent from the resolved (user) directory. - static FontConfig loadDefaultFontConfig(const QString &themeDirPath, const QString &themeName); /** @brief Writes cfg to disk as the theme's palette-.toml and updates the * theme's stored colour scheme to match. Shared by PaletteEditorDialog::onSave * and FirstRunWizard's theme step so the two "generate + keep" paths can't drift. */ @@ -144,31 +132,10 @@ public: void reloadCurrentTheme(); void previewPalette(const PaletteConfig &cfg, const QString &scheme); - void previewFonts(const FontConfig &cfg); // 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; - /** @brief Resolves an application font role: the theme's stored [Fonts] - * value when present, otherwise the application font. Roles that - * had a historical hardcoded family ask for it through - * cardTextFont() and deckListFont() instead. */ - QFont appFont(AppFont::Role role) const; - /** @brief Whether the active theme defines this role in fonts.toml. */ - bool hasAppFont(AppFont::Role role) const; - /** @brief The CardText role with its historical Serif fallback. Used by the - * counter painters, which were Serif before the role existed. */ - QFont cardTextFont() const; - /** @brief The DeckList role with its historical Serif fallback. Used by the - * two surfaces that hardcoded Serif before the role existed (the - * deck view zone headers and the printed deck list); every other - * DeckList reader falls back to the application font. */ - QFont deckListFont() const; - /** @brief Resolves an application font role against the startup font only, - * ignoring any previewed config in currentFonts. Editors that show - * "what this role resolves to" must use this, because appFont() - * reflects a live preview while the editor is open. */ - QFont defaultAppFont(AppFont::Role role) const; QBrush &getBgBrush(Role zone); QBrush getExtraBgBrush(Role zone, int zoneId = 0); @@ -177,7 +144,6 @@ protected slots: signals: void themeChanged(); void paletteChanged(); - void fontsChanged(); }; extern ThemeManager *themeManager; diff --git a/cockatrice/src/interface/widgets/cards/art_crop_attribution.cpp b/cockatrice/src/interface/widgets/cards/art_crop_attribution.cpp index 8475a0ed9..c4b347047 100644 --- a/cockatrice/src/interface/widgets/cards/art_crop_attribution.cpp +++ b/cockatrice/src/interface/widgets/cards/art_crop_attribution.cpp @@ -1,6 +1,5 @@ #include "art_crop_attribution.h" -#include "../../theme_manager.h" #include "libcockatrice/card/printing/printing_info.h" #include @@ -36,10 +35,7 @@ QRectF paintArtAttribution(QPainter &painter, painter.save(); - // Family comes from the CardText theme role; the incoming painter size - // stays the basis for the scale factor exactly as before. QFont font = painter.font(); - font.setFamily(themeManager->appFont(AppFont::CardText).family()); font.setPointSizeF(qMax(6.0, font.pointSizeF() * scale)); painter.setFont(font); diff --git a/cockatrice/src/interface/widgets/cards/card_art_utils.cpp b/cockatrice/src/interface/widgets/cards/card_art_utils.cpp index a7e633ad2..ebf79282e 100644 --- a/cockatrice/src/interface/widgets/cards/card_art_utils.cpp +++ b/cockatrice/src/interface/widgets/cards/card_art_utils.cpp @@ -2,8 +2,6 @@ #include "libcockatrice/card/card_info.h" -#include -#include #include #include #include @@ -20,23 +18,4 @@ QPixmap rotateSidewaysLayoutArt(const QPixmap &art, const ExactCard &card) transform.rotate(90); return art.transformed(transform, Qt::SmoothTransformation); } - -QPixmap cropCardArt(const QPixmap &art, const ExactCard &card) -{ - if (art.isNull()) { - return {}; - } - - const QPixmap source = rotateSidewaysLayoutArt(art, card); - const bool landscape = card.getInfo().getUiAttributes().landscapeOrientation; - const QSize size = source.size(); - const int marginX = size.width() * 0.07; - // Landscape name boxes extend below 5% of the upright image. Remove the - // whole box here, rather than relying on the banner's narrow viewport to - // hide it: the same saved offset is also used by the taller profile popup. - const int topMargin = size.height() * (landscape ? 0.15 : 0.11); - const int bottomMargin = size.height() * 0.45; - const QRect crop(marginX, topMargin, size.width() - 2 * marginX, size.height() - topMargin - bottomMargin); - return source.copy(crop.intersected(source.rect())); -} -} // namespace CardArtUtils +} // namespace CardArtUtils \ No newline at end of file diff --git a/cockatrice/src/interface/widgets/cards/card_art_utils.h b/cockatrice/src/interface/widgets/cards/card_art_utils.h index ac3b82380..5c331a12c 100644 --- a/cockatrice/src/interface/widgets/cards/card_art_utils.h +++ b/cockatrice/src/interface/widgets/cards/card_art_utils.h @@ -20,13 +20,6 @@ namespace CardArtUtils * @return @p art rotated 90° clockwise for sideways-layout cards, else @p art. */ QPixmap rotateSidewaysLayoutArt(const QPixmap &art, const ExactCard &card); - -/** - * Return the upright artwork used by banner previews, user rows and profile - * popups. The crop excludes conventional name and rules boxes before callers apply - * their saved pan and zoom, so a taller popup cannot expose the card frame. - */ -QPixmap cropCardArt(const QPixmap &art, const ExactCard &card); } // namespace CardArtUtils -#endif // CARD_ART_UTILS_H +#endif // CARD_ART_UTILS_H \ No newline at end of file diff --git a/cockatrice/src/interface/widgets/cards/card_info_picture_with_text_overlay_widget.cpp b/cockatrice/src/interface/widgets/cards/card_info_picture_with_text_overlay_widget.cpp index 56fe0bda4..04fe9ab69 100644 --- a/cockatrice/src/interface/widgets/cards/card_info_picture_with_text_overlay_widget.cpp +++ b/cockatrice/src/interface/widgets/cards/card_info_picture_with_text_overlay_widget.cpp @@ -1,7 +1,5 @@ #include "card_info_picture_with_text_overlay_widget.h" -#include "../../theme_manager.h" - #include #include #include @@ -45,7 +43,6 @@ CardInfoPictureWithTextOverlayWidget::CardInfoPictureWithTextOverlayWidget(QWidg fontSize(fontSize), textAlignment(alignment), highlighted(false) { this->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); - connect(themeManager, &ThemeManager::fontsChanged, this, &CardInfoPictureWithTextOverlayWidget::applyCardTextFont); } /** @@ -100,13 +97,6 @@ void CardInfoPictureWithTextOverlayWidget::setTextAlignment(const Qt::Alignment update(); } -void CardInfoPictureWithTextOverlayWidget::applyCardTextFont() -{ - // The overlay paint reads the CardText role font at paint time, so a - // theme font change only needs a repaint here. - update(); -} - void CardInfoPictureWithTextOverlayWidget::setHighlighted(bool _highlighted) { if (highlighted == _highlighted) { @@ -178,10 +168,8 @@ void CardInfoPictureWithTextOverlayWidget::paintEvent(QPaintEvent *event) return; } - // Calculate the optimal font size. The family comes from the CardText - // theme role; the fontSize member stays the starting size and the - // shrink-to-fit logic below is unchanged. - QFont font = themeManager->appFont(AppFont::CardText); + // Calculate the optimal font size + QFont font = painter.font(); int optimalFontSize = fontSize; // Start with the user-defined font size const QFontMetrics baseMetrics(font); int textWidth = pixmapRect.width(); @@ -296,7 +284,7 @@ QSize CardInfoPictureWithTextOverlayWidget::minimumSizeHint() const const QSize pixmapSize = pixmap.isNull() ? QSize(0, 0) : pixmap.size(); // Get the font metrics for the overlay text - QFont font = themeManager->appFont(AppFont::CardText); + QFont font; font.setPointSize(fontSize); const QFontMetrics fontMetrics(font); diff --git a/cockatrice/src/interface/widgets/cards/card_info_picture_with_text_overlay_widget.h b/cockatrice/src/interface/widgets/cards/card_info_picture_with_text_overlay_widget.h index 41019f543..6c547dc13 100644 --- a/cockatrice/src/interface/widgets/cards/card_info_picture_with_text_overlay_widget.h +++ b/cockatrice/src/interface/widgets/cards/card_info_picture_with_text_overlay_widget.h @@ -47,9 +47,6 @@ protected: void mousePressEvent(QMouseEvent *event) override; [[nodiscard]] QSize minimumSizeHint() const override; -private slots: - void applyCardTextFont(); - private: void drawOutlinedText(QPainter &painter, const QRect &textRect, diff --git a/cockatrice/src/interface/widgets/cards/deck_card_zone_display_widget.cpp b/cockatrice/src/interface/widgets/cards/deck_card_zone_display_widget.cpp index 3ec2053b1..d7eef6c18 100644 --- a/cockatrice/src/interface/widgets/cards/deck_card_zone_display_widget.cpp +++ b/cockatrice/src/interface/widgets/cards/deck_card_zone_display_widget.cpp @@ -69,7 +69,6 @@ DeckCardZoneDisplayWidget::DeckCardZoneDisplayWidget(QWidget *parent, updateZoneCardCount(); connect(deckListModel, &QAbstractItemModel::rowsInserted, this, &DeckCardZoneDisplayWidget::onCategoryAddition); - connect(deckListModel, &QAbstractItemModel::layoutChanged, this, &DeckCardZoneDisplayWidget::displayCards); if (selectionModel) { connect(selectionModel, &QItemSelectionModel::selectionChanged, this, &DeckCardZoneDisplayWidget::onSelectionChanged); @@ -175,9 +174,6 @@ void DeckCardZoneDisplayWidget::displayCards() for (const QPersistentModelIndex &persistent : rows) { constructAppropriateWidget(persistent); - // Reuse existing widgets when a model sort moves their persistent rows. - // This preserves collapsed groups and card selection during live edits. - cardGroupLayout->insertWidget(persistent.row(), indexToWidgetMap.value(persistent)); } } diff --git a/cockatrice/src/interface/widgets/deck_analytics/abstract_analytics_panel_widget.cpp b/cockatrice/src/interface/widgets/deck_analytics/abstract_analytics_panel_widget.cpp index c8571ab08..730043d95 100644 --- a/cockatrice/src/interface/widgets/deck_analytics/abstract_analytics_panel_widget.cpp +++ b/cockatrice/src/interface/widgets/deck_analytics/abstract_analytics_panel_widget.cpp @@ -1,6 +1,6 @@ #include "abstract_analytics_panel_widget.h" -#include "../../theme_icon.h" +#include "../../pixel_map_generator.h" #include "../general/display/banner_widget.h" #include "deck_list_statistics_analyzer.h" @@ -28,7 +28,7 @@ AbstractAnalyticsPanelWidget::AbstractAnalyticsPanelWidget(QWidget *parent, Deck // config button configureButton = new QPushButton(this); - configureButton->setIcon(loadThemedIcon(QStringLiteral("icons/settings"))); + configureButton->setIcon(themePixmap(QStringLiteral("icons/cogwheel"))); configureButton->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); connect(configureButton, &QPushButton::clicked, this, &AbstractAnalyticsPanelWidget::applyConfigFromDialog); bannerAndSettingsLayout->addWidget(configureButton, 0); diff --git a/cockatrice/src/interface/widgets/deck_analytics/resizable_panel.cpp b/cockatrice/src/interface/widgets/deck_analytics/resizable_panel.cpp index df2a645f1..85dba6468 100644 --- a/cockatrice/src/interface/widgets/deck_analytics/resizable_panel.cpp +++ b/cockatrice/src/interface/widgets/deck_analytics/resizable_panel.cpp @@ -1,6 +1,5 @@ #include "resizable_panel.h" -#include "../interface/theme_manager.h" #include "abstract_analytics_panel_widget.h" #include "libcockatrice/utility/qt_utils.h" @@ -13,7 +12,6 @@ #include #include #include -#include #include #include #include @@ -95,17 +93,9 @@ ResizablePanel::ResizablePanel(const QString &_typeId, AbstractAnalyticsPanelWid dragButton->setStyleSheet(QString("QPushButton { " "background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 %1, stop:1 %2); " - "border: none; color: %3; }" + "border: none; color: %3; font-size: 10px; }" "QPushButton:hover { background: %4; }") .arg(mid.name(), dark.name(), placeholderText.name(), midLight.name())); - // Family comes from the InterfaceDefault theme role; the 10px size stays caller-side. - auto applyDragFont = [this]() { - QFont dragFont = themeManager->appFont(AppFont::InterfaceDefault); - dragFont.setPixelSize(10); - dragButton->setFont(dragFont); - }; - applyDragFont(); - connect(themeManager, &ThemeManager::fontsChanged, this, applyDragFont); resizeHandle->setStyleSheet(QString("QWidget { background: qlineargradient(x1:0, y1:0, x2:0, y2:1, " "stop:0 %1, stop:1 %2); }") diff --git a/cockatrice/src/interface/widgets/deck_editor/deck_editor_database_display_widget.cpp b/cockatrice/src/interface/widgets/deck_editor/deck_editor_database_display_widget.cpp index f25e6c0ff..7ed398535 100644 --- a/cockatrice/src/interface/widgets/deck_editor/deck_editor_database_display_widget.cpp +++ b/cockatrice/src/interface/widgets/deck_editor/deck_editor_database_display_widget.cpp @@ -3,7 +3,7 @@ #include "../../../client/settings/cache_settings.h" #include "../../../filters/syntax_help.h" #include "../../key_signals.h" -#include "../../theme_icon.h" +#include "../../pixel_map_generator.h" #include "../utility/custom_line_edit.h" #include "card_database_display_model.h" #include "card_database_model.h" @@ -40,8 +40,8 @@ DeckEditorDatabaseDisplayWidget::DeckEditorDatabaseDisplayWidget(QWidget *parent searchEdit->setObjectName("searchEdit"); searchEdit->setPlaceholderText(tr("Search by card name (or search expressions)")); searchEdit->setClearButtonEnabled(true); - searchEdit->addAction(loadThemedIcon(QStringLiteral("icons/search")), QLineEdit::LeadingPosition); - auto help = searchEdit->addAction(loadThemedIcon(QStringLiteral("icons/info")), QLineEdit::TrailingPosition); + searchEdit->addAction(loadColorAdjustedPixmap("theme:icons/search"), QLineEdit::LeadingPosition); + auto help = searchEdit->addAction(themePixmap(QStringLiteral("icons/info")), QLineEdit::TrailingPosition); setFocusProxy(searchEdit); setFocusPolicy(Qt::ClickFocus); @@ -83,13 +83,13 @@ DeckEditorDatabaseDisplayWidget::DeckEditorDatabaseDisplayWidget(QWidget *parent &DeckEditorDatabaseDisplayWidget::onRelatedCardClicked); aAddCard = new QAction(QString(), this); - aAddCard->setIcon(loadThemedIcon(QStringLiteral("icons/arrow_right"))); + aAddCard->setIcon(themePixmap(QStringLiteral("icons/arrow_right_green"))); connect(aAddCard, &QAction::triggered, this, &DeckEditorDatabaseDisplayWidget::actAddCardToMainDeck); auto *tbAddCard = new QToolButton(this); tbAddCard->setDefaultAction(aAddCard); aAddCardToSideboard = new QAction(QString(), this); - aAddCardToSideboard->setIcon(loadThemedIcon(QStringLiteral("icons/arrow_right_sideboard"))); + aAddCardToSideboard->setIcon(themePixmap(QStringLiteral("icons/arrow_right_blue"))); connect(aAddCardToSideboard, &QAction::triggered, this, &DeckEditorDatabaseDisplayWidget::actAddCardToSideboard); auto *tbAddCardToSideboard = new QToolButton(this); tbAddCardToSideboard->setDefaultAction(aAddCardToSideboard); diff --git a/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.cpp b/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.cpp index 47d2a1aae..37dd57459 100644 --- a/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.cpp +++ b/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.cpp @@ -3,7 +3,7 @@ #include "../../../client/settings/cache_settings.h" #include "../../../client/settings/shortcuts_settings.h" #include "../../key_signals.h" -#include "../../theme_icon.h" +#include "../../pixel_map_generator.h" #include "../playmat/playmat_settings_dialog.h" #include "../quick_settings/settings_button_widget.h" #include "../tabs/abstract_tab_deck_editor.h" @@ -49,7 +49,6 @@ #include #include #include -#include #include #include #include @@ -188,17 +187,10 @@ void DeckEditorDeckDockWidget::createDeckDock() formatLabel = new QLabel(this); formatComboBox = new QComboBox(this); - formatComboBox->setObjectName("deckFormatComboBox"); formatComboBox->addItem(tr("Loading Database...")); formatComboBox->setEnabled(false); // Disable until loaded commanderBracketWidget = new CommanderBracketWidget(this); - connect(formatComboBox, QOverload::of(&QComboBox::currentIndexChanged), this, [this](int index) { - formatComboBox->setPlaceholderText(QString()); - formatComboBox->setToolTip(QString()); - deckStateManager->setFormat(index >= 0 ? formatComboBox->itemData(index).toString() : QString()); - commanderBracketWidget->setDeck(deckStateManager->getDeckListShared()); - }); commentsLabel = new QLabel(); commentsLabel->setObjectName("commentsLabel"); @@ -248,25 +240,25 @@ void DeckEditorDeckDockWidget::createDeckDock() &DeckEditorDeckDockWidget::applyActiveGroupCriteria); aIncrement = new QAction(QString(), this); - aIncrement->setIcon(loadThemedIcon(QStringLiteral("icons/increment"))); + aIncrement->setIcon(themePixmap(QStringLiteral("icons/increment"))); connect(aIncrement, &QAction::triggered, this, &DeckEditorDeckDockWidget::actIncrementSelection); auto *tbIncrement = new QToolButton(this); tbIncrement->setDefaultAction(aIncrement); aDecrement = new QAction(QString(), this); - aDecrement->setIcon(loadThemedIcon(QStringLiteral("icons/decrement"))); + aDecrement->setIcon(themePixmap(QStringLiteral("icons/decrement"))); connect(aDecrement, &QAction::triggered, this, &DeckEditorDeckDockWidget::actDecrementSelection); auto *tbDecrement = new QToolButton(this); tbDecrement->setDefaultAction(aDecrement); aRemoveCard = new QAction(QString(), this); - aRemoveCard->setIcon(loadThemedIcon(QStringLiteral("icons/remove_row"))); + aRemoveCard->setIcon(themePixmap(QStringLiteral("icons/remove_row"))); connect(aRemoveCard, &QAction::triggered, this, &DeckEditorDeckDockWidget::actRemoveCard); auto *tbRemoveCard = new QToolButton(this); tbRemoveCard->setDefaultAction(aRemoveCard); aSwapCard = new QAction(QString(), this); - aSwapCard->setIcon(loadThemedIcon(QStringLiteral("icons/swap"))); + aSwapCard->setIcon(themePixmap(QStringLiteral("icons/swap"))); connect(aSwapCard, &QAction::triggered, this, &DeckEditorDeckDockWidget::actSwapSelection); auto *tbSwapCard = new QToolButton(this); tbSwapCard->setDefaultAction(aSwapCard); @@ -363,9 +355,6 @@ void DeckEditorDeckDockWidget::initializeFormats() { QStringList allFormats = CardDatabaseManager::query()->getAllFormatsWithCount().keys(); - // Rebuilding the list is a view update, not a user edit. In particular, - // clear() emits currentIndexChanged(-1), which must not clear the deck. - const QSignalBlocker blocker(formatComboBox); formatComboBox->clear(); // Remove "Loading Database..." formatComboBox->setEnabled(true); @@ -375,25 +364,28 @@ void DeckEditorDeckDockWidget::initializeFormats() formatComboBox->addItem(formatName, formatName); // store the raw key in itemData } - syncFormatSelection(); + QString format = deckStateManager->getMetadata().gameFormat; + if (!format.isEmpty()) { + formatComboBox->setCurrentIndex(formatComboBox->findData(format)); + } else { + // Ensure no selection is visible initially + formatComboBox->setCurrentIndex(-1); + } + connect(formatComboBox, QOverload::of(&QComboBox::currentIndexChanged), this, [this](int index) { + QString formatKey; + if (index >= 0) { + formatKey = formatComboBox->itemData(index).toString(); + deckStateManager->setFormat(formatKey); + } else { + deckStateManager->setFormat(""); // clear format if deselected + } + + commanderBracketWidget->setDeck(deckStateManager->getDeckListShared()); + }); commanderBracketWidget->setDeck(deckStateManager->getDeckListShared()); } -void DeckEditorDeckDockWidget::syncFormatSelection() -{ - if (!formatComboBox->isEnabled()) { - return; - } - const QSignalBlocker blocker(formatComboBox); - const QString format = deckStateManager->getMetadata().gameFormat; - const int index = format.isEmpty() ? -1 : formatComboBox->findData(format); - const QString unavailable = index < 0 && !format.isEmpty() ? tr("Unavailable: %1").arg(format) : QString(); - formatComboBox->setPlaceholderText(unavailable); - formatComboBox->setToolTip(unavailable); - formatComboBox->setCurrentIndex(index); -} - ExactCard DeckEditorDeckDockWidget::getCurrentCard() { QModelIndex current = deckView->selectionModel()->currentIndex(); @@ -604,7 +596,9 @@ void DeckEditorDeckDockWidget::syncDisplayWidgetsToModel() updatePlaymatLabel(); updateHash(); - syncFormatSelection(); + formatComboBox->blockSignals(true); + formatComboBox->setCurrentIndex(formatComboBox->findData(deckStateManager->getMetadata().gameFormat)); + formatComboBox->blockSignals(false); commanderBracketWidget->setDeck(deckStateManager->getDeckListShared()); @@ -768,6 +762,18 @@ void DeckEditorDeckDockWidget::actSwapSelection() update(); } +void DeckEditorDeckDockWidget::actDecrementCard(const ExactCard &card, QString zoneName) +{ + if (!card) { + return; + } + if (card.getInfo().getIsToken()) { + zoneName = DECK_ZONE_TOKENS; + } + + deckStateManager->decrementCard(card, zoneName); +} + void DeckEditorDeckDockWidget::actDecrementSelection() { auto selectedRows = getSelectedCardNodeSourceIndices(); @@ -1048,7 +1054,6 @@ void DeckEditorDeckDockWidget::retranslateUi() commentsLabel->setText(tr("&Comments:")); activeGroupCriteriaLabel->setText(tr("Group by:")); formatLabel->setText(tr("Format:")); - syncFormatSelection(); commanderBracketWidget->retranslateUi(); hashLabel1->setText(tr("Hash:")); diff --git a/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.h b/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.h index 02910e390..97ab8823e 100644 --- a/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.h +++ b/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.h @@ -70,6 +70,7 @@ public slots: void syncDisplayWidgetsToModel(); void actAddCard(const ExactCard &card, const QString &zoneName); void actIncrementSelection(); + void actDecrementCard(const ExactCard &card, QString zoneName); void actDecrementSelection(); void actSwapCard(const ExactCard &card, const QString &zoneName); void actSwapSelection(); @@ -110,7 +111,6 @@ private: DeckListModel *getModel() const; [[nodiscard]] QModelIndexList getSelectedCardNodeSourceIndices() const; void offsetCountAtIndex(const QModelIndex &idx, bool isIncrement); - void syncFormatSelection(); void addMoveToZoneMenu(QMenu *menu, const QModelIndex &sourceCardIndex, const QString ¤tBoardName); void addChangeBoardMenu(QMenu *menu, const QString &zoneName); diff --git a/cockatrice/src/interface/widgets/deck_editor/deck_editor_filter_dock_widget.cpp b/cockatrice/src/interface/widgets/deck_editor/deck_editor_filter_dock_widget.cpp index 8122d1847..adeb5c3e1 100644 --- a/cockatrice/src/interface/widgets/deck_editor/deck_editor_filter_dock_widget.cpp +++ b/cockatrice/src/interface/widgets/deck_editor/deck_editor_filter_dock_widget.cpp @@ -5,7 +5,7 @@ #include "../../../filters/filter_builder.h" #include "../../../filters/filter_tree_model.h" #include "../../key_signals.h" -#include "../../theme_icon.h" +#include "../../pixel_map_generator.h" #include "../tabs/abstract_tab_deck_editor.h" #include "libcockatrice/filters/filter_card.h" @@ -64,11 +64,11 @@ void DeckEditorFilterDockWidget::createFiltersDock() connect(filterBuilder, &FilterBuilder::add, filterModel, &FilterTreeModel::addFilter); aClearFilterOne = new QAction(QString(), this); - aClearFilterOne->setIcon(loadThemedIcon(QStringLiteral("icons/decrement"))); + aClearFilterOne->setIcon(themePixmap(QStringLiteral("icons/decrement"))); connect(aClearFilterOne, &QAction::triggered, this, &DeckEditorFilterDockWidget::actClearFilterOne); aClearFilterAll = new QAction(QString(), this); - aClearFilterAll->setIcon(loadThemedIcon(QStringLiteral("icons/clearsearch"))); + aClearFilterAll->setIcon(themePixmap(QStringLiteral("icons/clearsearch"))); connect(aClearFilterAll, &QAction::triggered, this, &DeckEditorFilterDockWidget::actClearFilterAll); auto *filterDelOne = new QToolButton(); diff --git a/cockatrice/src/interface/widgets/deck_editor/deck_list_history_manager_widget.cpp b/cockatrice/src/interface/widgets/deck_editor/deck_list_history_manager_widget.cpp index a1914e1b5..209dc7273 100644 --- a/cockatrice/src/interface/widgets/deck_editor/deck_list_history_manager_widget.cpp +++ b/cockatrice/src/interface/widgets/deck_editor/deck_list_history_manager_widget.cpp @@ -1,6 +1,6 @@ #include "deck_list_history_manager_widget.h" -#include "../../theme_icon.h" +#include "../../pixel_map_generator.h" #include "../quick_settings/settings_button_widget.h" #include "deck_state_manager.h" #include "libcockatrice/deck_list/deck_list_history_manager.h" @@ -32,7 +32,7 @@ DeckListHistoryManagerWidget::DeckListHistoryManagerWidget(DeckStateManager *_de layout = new QHBoxLayout(this); aUndo = new QAction(QString(), this); - aUndo->setIcon(loadThemedIcon(QStringLiteral("icons/arrow_undo"))); + aUndo->setIcon(themePixmap(QStringLiteral("icons/arrow_undo"))); aUndo->setShortcut(QKeySequence::Undo); aUndo->setShortcutContext(Qt::ApplicationShortcut); connect(aUndo, &QAction::triggered, this, &DeckListHistoryManagerWidget::doUndo); @@ -41,7 +41,7 @@ DeckListHistoryManagerWidget::DeckListHistoryManagerWidget(DeckStateManager *_de undoButton->setDefaultAction(aUndo); aRedo = new QAction(QString(), this); - aRedo->setIcon(loadThemedIcon(QStringLiteral("icons/arrow_redo"))); + aRedo->setIcon(themePixmap(QStringLiteral("icons/arrow_redo"))); aRedo->setShortcut(QKeySequence::Redo); aRedo->setShortcutContext(Qt::ApplicationShortcut); connect(aRedo, &QAction::triggered, this, &DeckListHistoryManagerWidget::doRedo); @@ -53,7 +53,7 @@ DeckListHistoryManagerWidget::DeckListHistoryManagerWidget(DeckStateManager *_de layout->addWidget(redoButton); historyButton = new SettingsButtonWidget(this); - historyButton->setButtonIcon(loadThemedIcon(QStringLiteral("icons/arrow_history"))); + historyButton->setButtonIcon(themePixmap(QStringLiteral("icons/arrow_history"))); historyLabel = new QLabel(this); diff --git a/cockatrice/src/interface/widgets/deck_editor/deck_list_style_proxy.cpp b/cockatrice/src/interface/widgets/deck_editor/deck_list_style_proxy.cpp index a9c9cad7a..1e1aefe58 100644 --- a/cockatrice/src/interface/widgets/deck_editor/deck_list_style_proxy.cpp +++ b/cockatrice/src/interface/widgets/deck_editor/deck_list_style_proxy.cpp @@ -1,7 +1,5 @@ #include "deck_list_style_proxy.h" -#include "../../theme_manager.h" - #include #include #include @@ -10,16 +8,6 @@ #include #include -DeckListStyleProxy::DeckListStyleProxy(QObject *parent) : QIdentityProxyModel(parent) -{ - // FontRole data reads the DeckList role live; rebroadcast so attached - // views re-query instead of waiting for an incidental repaint. - connect(themeManager, &ThemeManager::fontsChanged, this, [this] { - emit layoutAboutToBeChanged(); - emit layoutChanged(); - }); -} - QVariant DeckListStyleProxy::data(const QModelIndex &index, int role) const { QModelIndex src = mapToSource(index); @@ -32,8 +20,7 @@ QVariant DeckListStyleProxy::data(const QModelIndex &index, int role) const bool isCard = src.data(DeckRoles::IsCardRole).toBool(); if (role == Qt::FontRole && !isCard) { - // Family comes from the DeckList theme role; bold stays caller-side. - QFont f = themeManager->appFont(AppFont::DeckList); + QFont f; f.setBold(true); return f; } diff --git a/cockatrice/src/interface/widgets/deck_editor/deck_list_style_proxy.h b/cockatrice/src/interface/widgets/deck_editor/deck_list_style_proxy.h index 3149da6de..e4d1de4f4 100644 --- a/cockatrice/src/interface/widgets/deck_editor/deck_list_style_proxy.h +++ b/cockatrice/src/interface/widgets/deck_editor/deck_list_style_proxy.h @@ -8,7 +8,7 @@ class DeckListStyleProxy : public QIdentityProxyModel { Q_OBJECT public: - explicit DeckListStyleProxy(QObject *parent = nullptr); + using QIdentityProxyModel::QIdentityProxyModel; [[nodiscard]] QVariant data(const QModelIndex &index, int role) const override; }; diff --git a/cockatrice/src/interface/widgets/deck_editor/deck_state_manager.cpp b/cockatrice/src/interface/widgets/deck_editor/deck_state_manager.cpp index cbf9dd3c4..a7e231ecf 100644 --- a/cockatrice/src/interface/widgets/deck_editor/deck_state_manager.cpp +++ b/cockatrice/src/interface/widgets/deck_editor/deck_state_manager.cpp @@ -211,15 +211,15 @@ QModelIndex DeckStateManager::addCard(const ExactCard &card, const QString &zone return {}; } - QString targetZone = card.getInfo().getIsToken() ? DECK_ZONE_TOKENS : zoneName; + QString zone = card.getInfo().getIsToken() ? DECK_ZONE_TOKENS : zoneName; CardSetPtr set = card.getPrinting().getSet(); QString setName = set ? set->getCorrectedShortName() : ""; QString reason = - tr("Added (%1): %2 (%3) %4").arg(targetZone, card.getName(), setName, card.getPrinting().getProperty("num")); + tr("Added (%1): %2 (%3) %4").arg(zone, card.getName(), setName, card.getPrinting().getProperty("num")); - QModelIndex idx = modifyDeck(reason, [&card, &targetZone](auto model) { return model->addCard(card, targetZone); }); + QModelIndex idx = modifyDeck(reason, [&card, &zone](auto model) { return model->addCard(card, zone); }); if (idx.isValid()) { emit focusIndexChanged(idx, true); @@ -234,22 +234,10 @@ QModelIndex DeckStateManager::decrementCard(const ExactCard &card, const QString return {}; } - QString targetZone = zoneName; - QString providerId = card.getPrinting().getUuid(); QString collectorNumber = card.getPrinting().getProperty("num"); - QModelIndex idx = deckListModel->findCard(card.getName(), targetZone, providerId, collectorNumber); - - // Tokens are usually added to the token zone, but it is possible for imported/hand-edited - // decks to contain them in the main or sideboards. This is why we only try to remove the - // token from the token zone if it does not exist in the given zone, allowing us to still - // remove it from there. - if (!idx.isValid() && card.getInfo().getIsToken() && targetZone != DECK_ZONE_TOKENS) { - targetZone = DECK_ZONE_TOKENS; - idx = deckListModel->findCard(card.getName(), targetZone, providerId, collectorNumber); - } - + QModelIndex idx = deckListModel->findCard(card.getName(), zoneName, providerId, collectorNumber); if (!idx.isValid()) { return {}; } @@ -261,7 +249,7 @@ QModelIndex DeckStateManager::decrementCard(const ExactCard &card, const QString } // old index is no longer safe since rows could have been removed - QModelIndex newIdx = deckListModel->findCard(card.getName(), targetZone, providerId, collectorNumber); + QModelIndex newIdx = deckListModel->findCard(card.getName(), zoneName, providerId, collectorNumber); if (newIdx.isValid()) { emit focusIndexChanged(newIdx, true); } diff --git a/cockatrice/src/interface/widgets/deck_editor/deck_state_manager.h b/cockatrice/src/interface/widgets/deck_editor/deck_state_manager.h index 0612147eb..54c59eb79 100644 --- a/cockatrice/src/interface/widgets/deck_editor/deck_state_manager.h +++ b/cockatrice/src/interface/widgets/deck_editor/deck_state_manager.h @@ -188,18 +188,17 @@ public: ///@} /** - * @brief Adds the given card to the given zone. Tokens always go to DECK_ZONE_TOKENS, ignoring the given zone. + * @brief Adds the given card to the given zone. * Saves the operation to history if successful. * * @param card The card to add - * @param zoneName The zone to add the card to if the card is not a token + * @param zoneName The zone to add the card to * @return The index of the added card */ QModelIndex addCard(const ExactCard &card, const QString &zoneName); /** * @brief Removes 1 copy of the given card from the given zone. - * If the card is a token and is not in the given zone, then it is removed from DECK_ZONE_TOKENS instead. * Saves the operation to history if successful. * * @param card The card to remove diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_connect.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_connect.cpp index daee5f1f9..1f567989e 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_connect.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_connect.cpp @@ -1,7 +1,7 @@ #include "dlg_connect.h" #include "../../../client/settings/cache_settings.h" -#include "../../theme_icon.h" +#include "../../pixel_map_generator.h" #include "../server/handle_public_servers.h" #include "../server/user/user_info_connection.h" #include "libcockatrice/utility/macros.h" @@ -33,12 +33,11 @@ class QWidget; DlgConnect::DlgConnect(QWidget *parent) : QDialog(parent) { - setObjectName("connectDialog"); previousHostButton = new QRadioButton(tr("Known Hosts"), this); previousHosts = new QComboBox(this); btnDeleteServer = new QPushButton(this); - btnDeleteServer->setIcon(loadThemedIcon(QStringLiteral("icons/remove_row"))); + btnDeleteServer->setIcon(themePixmap(QStringLiteral("icons/remove_row"))); btnDeleteServer->setToolTip(tr("Delete the currently selected saved server")); btnDeleteServer->setFixedWidth(30); @@ -46,7 +45,7 @@ DlgConnect::DlgConnect(QWidget *parent) : QDialog(parent) hps = new HandlePublicServers(this); btnRefreshServers = new QPushButton(this); - btnRefreshServers->setIcon(loadThemedIcon(QStringLiteral("icons/sync"))); + btnRefreshServers->setIcon(themePixmap(QStringLiteral("icons/sync"))); btnRefreshServers->setToolTip(tr("Refresh the server list with known public servers")); btnRefreshServers->setFixedWidth(30); @@ -86,11 +85,6 @@ DlgConnect::DlgConnect(QWidget *parent) : QDialog(parent) passwordLabel->setBuddy(passwordEdit); passwordEdit->setEchoMode(QLineEdit::Password); - hostEdit->setObjectName("connectHostEdit"); - portEdit->setObjectName("connectPortEdit"); - playernameEdit->setObjectName("connectPlayernameEdit"); - passwordEdit->setObjectName("connectPasswordEdit"); - savePasswordCheckBox = new QCheckBox(tr("&Save password")); autoConnectCheckBox = new QCheckBox(tr("A&uto connect")); @@ -121,7 +115,7 @@ DlgConnect::DlgConnect(QWidget *parent) : QDialog(parent) updateDisplayInfo(previousHosts->currentText()); btnForgotPassword = new QPushButton(this); - btnForgotPassword->setIcon(loadThemedIcon(QStringLiteral("icons/forgot_password"))); + btnForgotPassword->setIcon(themePixmap(QStringLiteral("icons/forgot_password"))); btnForgotPassword->setToolTip(tr("Reset Password")); btnForgotPassword->setFixedWidth(30); connect(btnForgotPassword, &QPushButton::released, this, &DlgConnect::actForgotPassword); @@ -296,13 +290,11 @@ void DlgConnect::updateDisplayInfo(const QString &saveName) playernameEdit->setFocus(); savePasswordCheckBox->setChecked(savePasswordStatus); storedVerifier.clear(); - savedVerifier.clear(); if (savePasswordStatus) { const QString stored = _data.at(4); if (stored.startsWith("$")) { storedVerifier = stored; - savedVerifier = stored; passwordEdit->clear(); passwordEdit->setPlaceholderText(tr("Password saved")); } else { @@ -339,7 +331,6 @@ void DlgConnect::newHostSelected(bool state) passwordEdit->clear(); passwordEdit->setPlaceholderText(QString()); storedVerifier.clear(); - savedVerifier.clear(); saveEdit->clear(); saveEdit->setPlaceholderText(tr("Unique Server Name")); saveEdit->setDisabled(false); @@ -355,19 +346,9 @@ void DlgConnect::passwordSaved(QT_STATE_CHANGED_T state) Q_UNUSED(state); if (savePasswordCheckBox->isChecked()) { autoConnectCheckBox->setEnabled(true); - // Re-ticking keeps a password the user has started typing; otherwise restore the saved - // login this dialog was opened with, so unticking and changing your mind is harmless. - if (passwordEdit->text().isEmpty()) { - storedVerifier = savedVerifier; - passwordEdit->setPlaceholderText(savedVerifier.isEmpty() ? QString() : tr("Password saved")); - } } else { autoConnectCheckBox->setChecked(false); autoConnectCheckBox->setEnabled(false); - // Unticking means the password typed below is sent for this connection: stop sending the - // saved verifier instead, and stop claiming a password is saved. - storedVerifier.clear(); - passwordEdit->setPlaceholderText(QString()); } } @@ -375,11 +356,10 @@ void DlgConnect::actOk() { ServersSettings &servers = SettingsCache::instance().servers(); - // Never write a newly typed plaintext password to disk when a verifier is available: the - // typed value is used for this connection and a fresh verifier is persisted after a - // successful login. Without any verifier we keep the previous (plaintext legacy) behavior. - const QString verifierToSave = storedVerifier.isEmpty() ? savedVerifier : storedVerifier; - const QString passwordToSave = verifierToSave.isEmpty() ? passwordEdit->text() : verifierToSave; + // Never write a newly typed plaintext password to disk when a verifier is already stored: + // the typed value is used for this connection and a fresh verifier is persisted after a + // successful login. Without a stored verifier we keep the previous (plaintext legacy) behavior. + const QString passwordToSave = storedVerifier.isEmpty() ? passwordEdit->text() : storedVerifier; if (newHostButton->isChecked()) { if (saveEdit->text().isEmpty()) { diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_connect.h b/cockatrice/src/interface/widgets/dialogs/dlg_connect.h index dad3623f5..bef5f136e 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_connect.h +++ b/cockatrice/src/interface/widgets/dialogs/dlg_connect.h @@ -98,10 +98,6 @@ private: QPushButton *btnConnect, *btnForgotPassword, *btnRefreshServers, *btnDeleteServer; QMap> savedHostList; HandlePublicServers *hps; - //! \brief Saved "$scrypt$..." verifier as loaded for the selected profile, kept so that - //! re-ticking "Save password" after unticking restores it. - QString savedVerifier; - //! \brief Verifier in effect for this connection: empty while "Save password" is unticked. QString storedVerifier; const QString placeHolderText = tr("Downloading..."); }; diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_create_game.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_create_game.cpp index ada4237f7..104b695e6 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_create_game.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_create_game.cpp @@ -116,17 +116,14 @@ void DlgCreateGame::sharedCtor() shareDecklistsOnLoadCheckBox = new QCheckBox(tr("Open decklists in lobby")); - shufflePlayersCheckBox = new QCheckBox(tr("Randomize turn order (seats) on each game start")); - createGameAsJudgeCheckBox = new QCheckBox(tr("Create game as judge")); auto *gameSetupOptionsLayout = new QGridLayout; gameSetupOptionsLayout->addWidget(startingLifeTotalLabel, 0, 0); gameSetupOptionsLayout->addWidget(startingLifeTotalEdit, 0, 1); - gameSetupOptionsLayout->addWidget(shareDecklistsOnLoadCheckBox, 1, 0, 1, 2); - gameSetupOptionsLayout->addWidget(shufflePlayersCheckBox, 2, 0, 1, 2); + gameSetupOptionsLayout->addWidget(shareDecklistsOnLoadCheckBox, 1, 0); if (room && room->getUserInfo()->user_level() & ServerInfo_User::IsJudge) { - gameSetupOptionsLayout->addWidget(createGameAsJudgeCheckBox, 3, 0, 1, 2); + gameSetupOptionsLayout->addWidget(createGameAsJudgeCheckBox, 2, 0); } else { createGameAsJudgeCheckBox->setChecked(false); createGameAsJudgeCheckBox->setHidden(true); @@ -187,7 +184,6 @@ DlgCreateGame::DlgCreateGame(TabRoom *_room, const QMap &_gameType createGameAsSpectatorCheckBox->setChecked(SettingsCache::instance().game().getCreateGameAsSpectator()); startingLifeTotalEdit->setValue(SettingsCache::instance().game().getDefaultStartingLifeTotal()); shareDecklistsOnLoadCheckBox->setChecked(SettingsCache::instance().game().getShareDecklistsOnLoad()); - shufflePlayersCheckBox->setChecked(SettingsCache::instance().game().getShufflePlayers()); if (!rememberGameSettings->isChecked()) { actReset(); @@ -221,7 +217,6 @@ DlgCreateGame::DlgCreateGame(const ServerInfo_Game &gameInfo, const QMapsetEnabled(false); startingLifeTotalEdit->setEnabled(false); shareDecklistsOnLoadCheckBox->setEnabled(false); - shufflePlayersCheckBox->setEnabled(false); descriptionEdit->setText(QString::fromStdString(gameInfo.description())); maxPlayersEdit->setValue(gameInfo.max_players()); @@ -232,7 +227,6 @@ DlgCreateGame::DlgCreateGame(const ServerInfo_Game &gameInfo, const QMapsetChecked(gameInfo.spectators_can_chat()); spectatorsSeeEverythingCheckBox->setChecked(gameInfo.spectators_omniscient()); shareDecklistsOnLoadCheckBox->setChecked(gameInfo.share_decklists_on_load()); - shufflePlayersCheckBox->setChecked(gameInfo.shuffle_players()); QSet types; for (int i = 0; i < gameInfo.game_types_size(); ++i) { @@ -270,7 +264,6 @@ void DlgCreateGame::actReset() startingLifeTotalEdit->setValue(20); shareDecklistsOnLoadCheckBox->setChecked(false); - shufflePlayersCheckBox->setChecked(false); createGameAsJudgeCheckBox->setChecked(false); QMapIterator gameTypeCheckBoxIterator(gameTypeCheckBoxes); @@ -301,7 +294,6 @@ void DlgCreateGame::actOK() cmd.set_join_as_spectator(createGameAsSpectatorCheckBox->isChecked()); cmd.set_starting_life_total(startingLifeTotalEdit->value()); cmd.set_share_decklists_on_load(shareDecklistsOnLoadCheckBox->isChecked()); - cmd.set_shuffle_players(shufflePlayersCheckBox->isChecked()); auto _gameTypes = QString(); QMapIterator gameTypeCheckBoxIterator(gameTypeCheckBoxes); @@ -326,7 +318,6 @@ void DlgCreateGame::actOK() SettingsCache::instance().game().setCreateGameAsSpectator(createGameAsSpectatorCheckBox->isChecked()); SettingsCache::instance().game().setDefaultStartingLifeTotal(startingLifeTotalEdit->value()); SettingsCache::instance().game().setShareDecklistsOnLoad(shareDecklistsOnLoadCheckBox->isChecked()); - SettingsCache::instance().game().setShufflePlayers(shufflePlayersCheckBox->isChecked()); SettingsCache::instance().game().setGameTypes(_gameTypes); } PendingCommand *pend = room->prepareRoomCommand(cmd); diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_create_game.h b/cockatrice/src/interface/widgets/dialogs/dlg_create_game.h index 58356c9b6..71b09997c 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_create_game.h +++ b/cockatrice/src/interface/widgets/dialogs/dlg_create_game.h @@ -51,7 +51,6 @@ private: QCheckBox *spectatorsAllowedCheckBox, *spectatorsNeedPasswordCheckBox, *spectatorsCanTalkCheckBox, *spectatorsSeeEverythingCheckBox, *createGameAsJudgeCheckBox, *createGameAsSpectatorCheckBox; QCheckBox *shareDecklistsOnLoadCheckBox; - QCheckBox *shufflePlayersCheckBox; QDialogButtonBox *buttonBox; QPushButton *clearButton; QCheckBox *rememberGameSettings; diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_edit_tokens.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_edit_tokens.cpp index b06481c4b..cbf61a394 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_edit_tokens.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_edit_tokens.cpp @@ -1,6 +1,6 @@ #include "dlg_edit_tokens.h" -#include "../../theme_icon.h" +#include "../../pixel_map_generator.h" #include "../interface/widgets/utility/get_text_with_max.h" #include "card_database_display_model.h" #include "libcockatrice/card/card_info.h" @@ -40,7 +40,6 @@ class QWidget; DlgEditTokens::DlgEditTokens(QWidget *parent) : QDialog(parent), currentCard(nullptr) { - setObjectName("editTokensDialog"); nameLabel = new QLabel(tr("&Name:")); nameEdit = new QLineEdit; nameEdit->setMaxLength(MAX_NAME_LENGTH); @@ -108,10 +107,10 @@ DlgEditTokens::DlgEditTokens(QWidget *parent) : QDialog(parent), currentCard(nul &DlgEditTokens::tokenSelectionChanged); QAction *aAddToken = new QAction(tr("Add token"), this); - aAddToken->setIcon(loadThemedIcon(QStringLiteral("icons/increment"))); + aAddToken->setIcon(themePixmap(QStringLiteral("icons/increment"))); connect(aAddToken, &QAction::triggered, this, &DlgEditTokens::actAddToken); QAction *aRemoveToken = new QAction(tr("Remove token"), this); - aRemoveToken->setIcon(loadThemedIcon(QStringLiteral("icons/decrement"))); + aRemoveToken->setIcon(themePixmap(QStringLiteral("icons/decrement"))); connect(aRemoveToken, &QAction::triggered, this, &DlgEditTokens::actRemoveToken); auto *databaseToolBar = new QToolBar; diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_load_deck_from_clipboard.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_load_deck_from_clipboard.cpp index c1d0676ca..2c0fd3f6f 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_load_deck_from_clipboard.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_load_deck_from_clipboard.cpp @@ -28,16 +28,12 @@ class QWidget; */ AbstractDlgDeckTextEdit::AbstractDlgDeckTextEdit(QWidget *parent) : QDialog(parent) { - setObjectName("dlgDeckTextEdit"); - contentsEdit = new QPlainTextEdit; - contentsEdit->setObjectName("contentsEdit"); refreshButton = new QPushButton(tr("&Refresh")); connect(refreshButton, &QPushButton::clicked, this, &AbstractDlgDeckTextEdit::actRefresh); auto *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); - buttonBox->setObjectName("buttonBox"); buttonBox->addButton(refreshButton, QDialogButtonBox::ActionRole); connect(buttonBox, &QDialogButtonBox::accepted, this, &AbstractDlgDeckTextEdit::actOK); connect(buttonBox, &QDialogButtonBox::rejected, this, &AbstractDlgDeckTextEdit::reject); diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_load_deck_from_website.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_load_deck_from_website.cpp index 04b0db2e3..1c1ada302 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_load_deck_from_website.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_load_deck_from_website.cpp @@ -22,7 +22,6 @@ #include #include #include -#include #include #include #include @@ -34,7 +33,6 @@ class QWidget; -static const QStringList SUPPORTED_PROVIDERS = {"Archidekt", "Deckstats", "ManaBox", "Moxfield", "TappedOut"}; DlgLoadDeckFromWebsite::DlgLoadDeckFromWebsite(QWidget *parent) : QDialog(parent) { nam = NetworkManagerFactory::create(this); @@ -43,7 +41,6 @@ DlgLoadDeckFromWebsite::DlgLoadDeckFromWebsite(QWidget *parent) : QDialog(parent setLayout(layout); instructionLabel = new QLabel(this); - instructionLabel->setWordWrap(true); layout->addWidget(instructionLabel); urlEdit = new QLineEdit(this); @@ -67,8 +64,8 @@ DlgLoadDeckFromWebsite::DlgLoadDeckFromWebsite(QWidget *parent) : QDialog(parent void DlgLoadDeckFromWebsite::retranslateUi() { - instructionLabel->setText(tr("Paste a link to a decklist site here to import it.") + "\n" + - tr("Supported services: %1").arg(SUPPORTED_PROVIDERS.join(", "))); + instructionLabel->setText(tr("Paste a link to a decklist site here to import it.\n(Archidekt, Deckstats, Moxfield, " + "and TappedOut are supported.)")); } bool DlgLoadDeckFromWebsite::testValidUrl() @@ -87,8 +84,8 @@ void DlgLoadDeckFromWebsite::accept() if (!jsonParser && info.provider != DeckProvider::Deckstats && info.provider != DeckProvider::TappedOut) { qCWarning(DlgLoadDeckFromWebsiteLog) << "No parser found for provider"; QMessageBox::warning(this, tr("Load Deck from Website"), - tr("No parser available for this deck provider.") + "\n" + - tr("Supported services: %1").arg(SUPPORTED_PROVIDERS.join(", "))); + tr("No parser available for this deck provider.\n (Archidekt, Deckstats, Moxfield, " + "and TappedOut are supported.)")); QDialog::reject(); return; } @@ -155,7 +152,6 @@ void DlgLoadDeckFromWebsite::accept() "https://archidekt.com/decks/9999999\n" "https://deckstats.net/decks/99999/9999999-your-deck-name/en\n" "https://moxfield.com/decks/XYZxx-XYZ99Yyy-xyzXzzz\n" - "https://manabox.app/decks/XYZxxa-YZ99Yyyx_yzXzzz\n" "https://tappedout.net/mtg-decks/your-deck-name/")); QDialog::reject(); } @@ -168,9 +164,7 @@ QSharedPointer DlgLoadDeckFromWebsite::createParserForProvider( return QSharedPointer(new ArchidektJsonParser()); case DeckProvider::Moxfield: return QSharedPointer(new MoxfieldJsonParser()); - case DeckProvider::Manabox: - return QSharedPointer(new ManaboxJsonParser()); default: return QSharedPointer(nullptr); } -} +} \ No newline at end of file diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_local_game_options.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_local_game_options.cpp index 3ebb5c093..858deb751 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_local_game_options.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_local_game_options.cpp @@ -16,11 +16,8 @@ class QWidget; DlgLocalGameOptions::DlgLocalGameOptions(QWidget *parent) : QDialog(parent) { - setObjectName("localGameDialog"); - numberPlayersLabel = new QLabel(tr("Players:"), this); numberPlayersEdit = new QSpinBox(this); - numberPlayersEdit->setObjectName("numberPlayersEdit"); numberPlayersEdit->setMinimum(1); numberPlayersEdit->setMaximum(8); numberPlayersEdit->setValue(1); @@ -31,12 +28,10 @@ DlgLocalGameOptions::DlgLocalGameOptions(QWidget *parent) : QDialog(parent) generalGrid->addWidget(numberPlayersLabel, 0, 0); generalGrid->addWidget(numberPlayersEdit, 0, 1); generalGroupBox = new QGroupBox(tr("General"), this); - generalGroupBox->setObjectName("generalGroupBox"); generalGroupBox->setLayout(generalGrid); startingLifeTotalLabel = new QLabel(tr("Starting life total:"), this); startingLifeTotalEdit = new QSpinBox(this); - startingLifeTotalEdit->setObjectName("startingLifeTotalEdit"); startingLifeTotalEdit->setMinimum(-999999999); startingLifeTotalEdit->setMaximum(999999999); startingLifeTotalEdit->setValue(20); @@ -47,14 +42,11 @@ DlgLocalGameOptions::DlgLocalGameOptions(QWidget *parent) : QDialog(parent) gameSetupGrid->addWidget(startingLifeTotalLabel, 0, 0); gameSetupGrid->addWidget(startingLifeTotalEdit, 0, 1); gameSetupOptionsGroupBox = new QGroupBox(tr("Game setup options"), this); - gameSetupOptionsGroupBox->setObjectName("gameSetupOptionsGroupBox"); gameSetupOptionsGroupBox->setLayout(gameSetupGrid); rememberSettingsCheckBox = new QCheckBox(tr("Remember settings"), this); - rememberSettingsCheckBox->setObjectName("rememberSettingsCheckBox"); buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this); - buttonBox->setObjectName("buttonBox"); connect(buttonBox, &QDialogButtonBox::accepted, this, &DlgLocalGameOptions::actOK); connect(buttonBox, &QDialogButtonBox::rejected, this, &DlgLocalGameOptions::reject); diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_manage_sets.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_manage_sets.cpp index c6e640a9f..dbfe38e26 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_manage_sets.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_manage_sets.cpp @@ -1,7 +1,7 @@ #include "dlg_manage_sets.h" #include "../../../client/settings/cache_settings.h" -#include "../../theme_icon.h" +#include "../../pixel_map_generator.h" #include "../interface/card_picture_loader/card_picture_loader.h" #include "../interface/widgets/utility/custom_line_edit.h" #include "libcockatrice/card/database/card_database.h" @@ -48,7 +48,6 @@ WndSets::WndSets(QWidget *parent) : QMainWindow(parent) { - setObjectName("manageSetsWindow"); setOrderIsSorted = false; // left toolbar @@ -58,28 +57,28 @@ WndSets::WndSets(QWidget *parent) : QMainWindow(parent) setsEditToolBar->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); aTop = new QAction(QString(), this); - aTop->setIcon(loadThemedIcon(QStringLiteral("icons/arrow_to_top"))); + aTop->setIcon(themePixmap(QStringLiteral("icons/arrow_top_green"))); aTop->setToolTip(tr("Move selected set to the top")); aTop->setEnabled(false); connect(aTop, &QAction::triggered, this, &WndSets::actTop); setsEditToolBar->addAction(aTop); aUp = new QAction(QString(), this); - aUp->setIcon(loadThemedIcon(QStringLiteral("icons/arrow_up"))); + aUp->setIcon(themePixmap(QStringLiteral("icons/arrow_up_green"))); aUp->setToolTip(tr("Move selected set up")); aUp->setEnabled(false); connect(aUp, &QAction::triggered, this, &WndSets::actUp); setsEditToolBar->addAction(aUp); aDown = new QAction(QString(), this); - aDown->setIcon(loadThemedIcon(QStringLiteral("icons/arrow_down"))); + aDown->setIcon(themePixmap(QStringLiteral("icons/arrow_down_green"))); aDown->setToolTip(tr("Move selected set down")); aDown->setEnabled(false); connect(aDown, &QAction::triggered, this, &WndSets::actDown); setsEditToolBar->addAction(aDown); aBottom = new QAction(QString(), this); - aBottom->setIcon(loadThemedIcon(QStringLiteral("icons/arrow_to_bottom"))); + aBottom->setIcon(themePixmap(QStringLiteral("icons/arrow_bottom_green"))); aBottom->setToolTip(tr("Move selected set to the bottom")); aBottom->setEnabled(false); connect(aBottom, &QAction::triggered, this, &WndSets::actBottom); @@ -89,7 +88,7 @@ WndSets::WndSets(QWidget *parent) : QMainWindow(parent) searchField = new LineEditUnfocusable; searchField->setObjectName("searchEdit"); searchField->setPlaceholderText(tr("Search by set name, code, type, or release date")); - searchField->addAction(loadThemedIcon(QStringLiteral("icons/search")), LineEditUnfocusable::LeadingPosition); + searchField->addAction(themePixmap(QStringLiteral("icons/search")), LineEditUnfocusable::LeadingPosition); searchField->setClearButtonEnabled(true); setFocusProxy(searchField); @@ -222,7 +221,6 @@ WndSets::WndSets(QWidget *parent) : QMainWindow(parent) WndSets::~WndSets() { - disconnect(view->header(), &QHeaderView::geometriesChanged, this, &WndSets::saveHeaderState); } void WndSets::closeEvent(QCloseEvent * /*ev*/) diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_my_reports.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_my_reports.cpp index a7387a068..2a2758ba5 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_my_reports.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_my_reports.cpp @@ -1,13 +1,11 @@ #include "dlg_my_reports.h" -#include "../interface/theme_manager.h" #include "../utility/report_utils.h" #include "abstract_client.h" #include "libcockatrice/protocol/pb/response.pb.h" #include "libcockatrice/protocol/pb/serverinfo_report.pb.h" #include -#include #include #include #include @@ -26,6 +24,7 @@ #include #include #include +#include #include #include @@ -70,8 +69,7 @@ DlgMyReports::DlgMyReports(AbstractClient *_client, QWidget *parent) auto *chatGroup = new QGroupBox(tr("Chat Log Context")); chatLogEdit = new QTextEdit; chatLogEdit->setReadOnly(true); - applyChatLogFont(); - connect(themeManager, &ThemeManager::fontsChanged, this, &DlgMyReports::applyChatLogFont); + chatLogEdit->setFont(QtUtils::monospaceFont()); auto *chatLayout = new QVBoxLayout(chatGroup); chatLayout->setContentsMargins(4, 4, 4, 4); chatLayout->addWidget(chatLogEdit); @@ -124,11 +122,6 @@ DlgMyReports::DlgMyReports(AbstractClient *_client, QWidget *parent) refreshList(); } -void DlgMyReports::applyChatLogFont() -{ - chatLogEdit->setFont(report_utils::reportLogFont()); -} - void DlgMyReports::refreshList() { selectedReportIdBeforeRefresh = selectedReportId; diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_my_reports.h b/cockatrice/src/interface/widgets/dialogs/dlg_my_reports.h index 90772f051..dc9f90d2d 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_my_reports.h +++ b/cockatrice/src/interface/widgets/dialogs/dlg_my_reports.h @@ -29,7 +29,6 @@ private slots: void reportDetailsResponse(const Response &response); void addComment(); void addCommentResponse(const Response &response); - void applyChatLogFont(); private: void loadReportDetails(const ServerInfo_Report &report); diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_register.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_register.cpp index 09afee32d..9e32e87d1 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_register.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_register.cpp @@ -1,7 +1,7 @@ #include "dlg_register.h" #include "../../../client/settings/cache_settings.h" -#include "../../theme_icon.h" +#include "../../pixel_map_generator.h" #include "../server/handle_public_servers.h" #include "../server/user/user_info_connection.h" @@ -34,7 +34,7 @@ DlgRegister::DlgRegister(QWidget *parent) : QDialog(parent) previousHosts = new QComboBox(this); btnDeleteServer = new QPushButton(this); - btnDeleteServer->setIcon(loadThemedIcon(QStringLiteral("icons/remove_row"))); + btnDeleteServer->setIcon(themePixmap(QStringLiteral("icons/remove_row"))); btnDeleteServer->setToolTip(tr("Delete the currently selected saved server")); btnDeleteServer->setFixedWidth(30); @@ -42,7 +42,7 @@ DlgRegister::DlgRegister(QWidget *parent) : QDialog(parent) hps = new HandlePublicServers(this); btnRefreshServers = new QPushButton(this); - btnRefreshServers->setIcon(loadThemedIcon(QStringLiteral("icons/sync"))); + btnRefreshServers->setIcon(themePixmap(QStringLiteral("icons/sync"))); btnRefreshServers->setToolTip(tr("Refresh the server list with known public servers")); btnRefreshServers->setFixedWidth(30); diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_report_user.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_report_user.cpp index c7c32469b..3ef45020b 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_report_user.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_report_user.cpp @@ -1,13 +1,10 @@ #include "dlg_report_user.h" -#include "../interface/theme_manager.h" -#include "../utility/report_utils.h" #include "abstract_client.h" #include "libcockatrice/protocol/pb/response.pb.h" #include #include -#include #include #include #include @@ -94,8 +91,7 @@ DlgReportUser::DlgReportUser(AbstractClient *_client, auto *chatLayout = new QVBoxLayout(chatGroup); chatLogEdit = new QTextEdit; chatLogEdit->setReadOnly(true); - applyChatLogFont(); - connect(themeManager, &ThemeManager::fontsChanged, this, &DlgReportUser::applyChatLogFont); + chatLogEdit->setFont(QtUtils::monospaceFont()); if (!_autoChatLog.isEmpty()) { chatLogEdit->setPlainText(_autoChatLog); } else { @@ -124,11 +120,6 @@ DlgReportUser::DlgReportUser(AbstractClient *_client, layout->addWidget(buttonBox); } -void DlgReportUser::applyChatLogFont() -{ - chatLogEdit->setFont(report_utils::reportLogFont()); -} - void DlgReportUser::actSubmit() { const QString description = descriptionEdit->toPlainText().trimmed(); diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_report_user.h b/cockatrice/src/interface/widgets/dialogs/dlg_report_user.h index 0f2fa5080..c7fce055d 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_report_user.h +++ b/cockatrice/src/interface/widgets/dialogs/dlg_report_user.h @@ -27,7 +27,6 @@ public: private slots: void actSubmit(); void reportResponse(const Response &response); - void applyChatLogFont(); private: static QString categoryLabel(const QString &key); diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_settings.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_settings.cpp index 3602e67c6..ab870a2c8 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_settings.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_settings.cpp @@ -73,8 +73,6 @@ DlgSettings::DlgSettings(QWidget *parent) : QDialog(parent) resize(calculateDialogSize(hostSize, available.size(), minimum)); setSizeGripEnabled(true); - setObjectName("settingsDialog"); - connect(&SettingsCache::instance().personal(), &PersonalSettings::langChanged, this, &DlgSettings::updateLanguage); settingsView = new SettingsView(this); @@ -84,7 +82,6 @@ DlgSettings::DlgSettings(QWidget *parent) : QDialog(parent) buttonBox->addStretch(); okButton = new QPushButton; okButton->setDefault(true); - okButton->setObjectName("settingsOkButton"); connect(okButton, &QPushButton::clicked, this, &DlgSettings::close); buttonBox->addWidget(okButton); diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_shared_decks_preview.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_shared_decks_preview.cpp index 49bacd081..d29aee27f 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_shared_decks_preview.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_shared_decks_preview.cpp @@ -2,7 +2,6 @@ #include "../deck_share/shared_deck_preview_widget.h" #include "../general/layout_containers/flow_widget.h" -#include "../interface/theme_manager.h" #include #include @@ -36,14 +35,9 @@ DlgSharedDecksPreview::DlgSharedDecksPreview(QWidget *parent, // QLabels render AutoText and markup would otherwise be shown as rich text. auto *titleLabel = new QLabel(tr("Share: %1").arg((shareName.isEmpty() ? tr("Untitled") : shareName).toHtmlEscaped()), this); - // Family comes from the InterfaceDefault theme role; the +2 size and bold stay caller-side. - auto applyTitleFont = [titleLabel]() { - QFont titleFont = QtUtils::boldFont(themeManager->appFont(AppFont::InterfaceDefault)); - titleFont.setPointSize(titleFont.pointSize() + 2); - titleLabel->setFont(titleFont); - }; - applyTitleFont(); - connect(themeManager, &ThemeManager::fontsChanged, this, applyTitleFont); + QFont titleFont = QtUtils::boldFont(titleLabel->font()); + titleFont.setPointSize(titleFont.pointSize() + 2); + titleLabel->setFont(titleFont); mainLayout->addWidget(titleLabel); if (!serverText.isEmpty()) { diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_tip_of_the_day.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_tip_of_the_day.cpp index c1df0c963..511ff1c7d 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_tip_of_the_day.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_tip_of_the_day.cpp @@ -28,7 +28,6 @@ DlgTipOfTheDay::DlgTipOfTheDay(QWidget *parent) : QDialog(parent) { - setObjectName("tipOfTheDayDialog"); successfulInit = false; QString xmlPath = "theme:tips/tips_of_the_day.xml"; tipDatabase = new TipsOfTheDay(xmlPath, this); diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_turn_order.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_turn_order.cpp deleted file mode 100644 index 8814f89db..000000000 --- a/cockatrice/src/interface/widgets/dialogs/dlg_turn_order.cpp +++ /dev/null @@ -1,136 +0,0 @@ -#include "dlg_turn_order.h" - -#include -#include -#include -#include -#include -#include -#include - -DlgTurnOrder::DlgTurnOrder(const QStringList &_playerNames, QWidget *parent) - : QDialog(parent), randomizeRequested(false) -{ - listWidget = new QListWidget(this); - listWidget->addItems(_playerNames); - listWidget->setCurrentRow(0); - connect(listWidget, &QListWidget::currentRowChanged, this, &DlgTurnOrder::selectionChanged); - - moveUpButton = new QPushButton(this); - connect(moveUpButton, &QPushButton::clicked, this, &DlgTurnOrder::actMoveUp); - moveDownButton = new QPushButton(this); - connect(moveDownButton, &QPushButton::clicked, this, &DlgTurnOrder::actMoveDown); - randomizeButton = new QPushButton(this); - connect(randomizeButton, &QPushButton::clicked, this, &DlgTurnOrder::actRandomize); - - // Shown in place of the list while the server is asked to randomize: the - // order is chosen server side, so there is nothing to preview here. - hintLabel = new QLabel(this); - hintLabel->setWordWrap(true); - hintLabel->setAlignment(Qt::AlignCenter); - - auto *buttonColumn = new QVBoxLayout; - buttonColumn->addWidget(moveUpButton); - buttonColumn->addWidget(moveDownButton); - buttonColumn->addWidget(randomizeButton); - buttonColumn->addStretch(); - - auto *listLayout = new QHBoxLayout; - listLayout->addWidget(listWidget); - listLayout->addWidget(hintLabel, 1); - listLayout->addLayout(buttonColumn); - - auto *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); - connect(buttonBox, &QDialogButtonBox::accepted, this, &DlgTurnOrder::actOk); - connect(buttonBox, &QDialogButtonBox::rejected, this, &DlgTurnOrder::reject); - - auto *mainLayout = new QVBoxLayout; - mainLayout->addLayout(listLayout); - mainLayout->addWidget(buttonBox); - setLayout(mainLayout); - - updateButtons(); - - updateMode(); - setMinimumWidth(300); -} - -void DlgTurnOrder::retranslateUi() -{ - setWindowTitle(tr("Set turn order")); - moveUpButton->setText(tr("Move up")); - moveDownButton->setText(tr("Move down")); - randomizeButton->setText(randomizeRequested ? tr("Set order manually") : tr("Randomize")); - hintLabel->setText(tr("The server will randomize the turn order.")); -} - -QStringList DlgTurnOrder::order() const -{ - QStringList playerNames; - for (int i = 0; i < listWidget->count(); ++i) { - playerNames.append(listWidget->item(i)->text()); - } - return playerNames; -} - -void DlgTurnOrder::moveSelected(int delta) -{ - const int row = listWidget->currentRow(); - const int newRow = row + delta; - if (row < 0 || newRow < 0 || newRow >= listWidget->count()) { - return; - } - - QListWidgetItem *item = listWidget->takeItem(row); - listWidget->insertItem(newRow, item); - listWidget->setCurrentRow(newRow); - updateButtons(); -} - -void DlgTurnOrder::updateMode() -{ - listWidget->setVisible(!randomizeRequested); - hintLabel->setVisible(randomizeRequested); - moveUpButton->setVisible(!randomizeRequested); - moveDownButton->setVisible(!randomizeRequested); - retranslateUi(); -} - -void DlgTurnOrder::updateButtons() -{ - const int row = listWidget->currentRow(); - const int count = listWidget->count(); - const bool manualOrder = !randomizeRequested; - moveUpButton->setEnabled(manualOrder && row > 0); - moveDownButton->setEnabled(manualOrder && row >= 0 && row < count - 1); -} - -void DlgTurnOrder::actMoveUp() -{ - moveSelected(-1); -} - -void DlgTurnOrder::actMoveDown() -{ - moveSelected(1); -} - -void DlgTurnOrder::actRandomize() -{ - // Randomizing is a request to the server, not a local shuffle: showing a - // preview would let the host re-open the dialog until the order looks - // favourable for them. - randomizeRequested = !randomizeRequested; - updateMode(); - updateButtons(); -} - -void DlgTurnOrder::actOk() -{ - accept(); -} - -void DlgTurnOrder::selectionChanged() -{ - updateButtons(); -} diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_turn_order.h b/cockatrice/src/interface/widgets/dialogs/dlg_turn_order.h deleted file mode 100644 index 44d8203b9..000000000 --- a/cockatrice/src/interface/widgets/dialogs/dlg_turn_order.h +++ /dev/null @@ -1,51 +0,0 @@ -/** - * @file dlg_turn_order.h - * @ingroup GameDialogs - */ -//! \todo Document this file. - -#ifndef DLG_TURN_ORDER_H -#define DLG_TURN_ORDER_H - -#include -#include - -class QLabel; -class QListWidget; -class QPushButton; - -class DlgTurnOrder : public QDialog -{ - Q_OBJECT -private: - QListWidget *listWidget; - QLabel *hintLabel; - QPushButton *moveUpButton; - QPushButton *moveDownButton; - QPushButton *randomizeButton; - bool randomizeRequested; - - void moveSelected(int delta); - void updateMode(); - void updateButtons(); - -private slots: - void actMoveUp(); - void actMoveDown(); - void actRandomize(); - void actOk(); - void selectionChanged(); - -public: - explicit DlgTurnOrder(const QStringList &_playerNames, QWidget *parent = nullptr); - - void retranslateUi(); - - [[nodiscard]] bool isRandomizeRequested() const - { - return randomizeRequested; - } - [[nodiscard]] QStringList order() const; -}; - -#endif diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_update.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_update.cpp index 11dcd7e27..ca55c7b41 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_update.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_update.cpp @@ -42,7 +42,6 @@ static const QString UPDATE_INSTALLER_OVERRIDE = "Cockatrice-Update-Override.exe DlgUpdate::DlgUpdate(QWidget *parent) : QDialog(parent) { - setObjectName("updateDialog"); // Handle layout statusLabel = new QLabel(this); diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_view_log.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_view_log.cpp index e75cf352a..d55e800a1 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_view_log.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_view_log.cpp @@ -18,7 +18,6 @@ class QWidget; DlgViewLog::DlgViewLog(QWidget *parent) : QDialog(parent) { - setObjectName("viewLogDialog"); logArea = new QPlainTextEdit; logArea->setReadOnly(true); diff --git a/cockatrice/src/interface/widgets/dialogs/settings_view.cpp b/cockatrice/src/interface/widgets/dialogs/settings_view.cpp index b1a80c86b..d22a8d0b3 100644 --- a/cockatrice/src/interface/widgets/dialogs/settings_view.cpp +++ b/cockatrice/src/interface/widgets/dialogs/settings_view.cpp @@ -6,8 +6,7 @@ #include "settings_view.h" #include "../../../client/settings/cache_settings.h" -#include "../../theme_icon.h" -#include "../interface/theme_manager.h" +#include "../../pixel_map_generator.h" #include "../settings_page/abstract_settings_page.h" #include "../settings_page/appearance_settings_page.h" #include "../settings_page/deck_editor_settings_page.h" @@ -120,8 +119,6 @@ QWidget *fromWidgetId(WidgetId id) SettingsView::SettingsView(QWidget *parent) : QWidget(parent), currentTabIndex(0), searchActive(false) { - setObjectName("settingsView"); - setupUi(); connect(&SettingsCache::instance().personal(), &PersonalSettings::langChanged, this, &SettingsView::retranslateUi); @@ -135,17 +132,15 @@ void SettingsView::setupUi() // Search bar searchEdit = new QLineEdit; searchEdit->setClearButtonEnabled(true); - searchEdit->addAction(loadThemedIcon(QStringLiteral("icons/search")), QLineEdit::LeadingPosition); + searchEdit->addAction(themePixmap(QStringLiteral("icons/search")), QLineEdit::LeadingPosition); searchEdit->installEventFilter(this); connect(searchEdit, &QLineEdit::textChanged, this, &SettingsView::onSearchTextChanged); - searchEdit->setObjectName("settingsSearchEdit"); auto *searchLayout = new QHBoxLayout; searchLayout->addWidget(searchEdit); // Pages stacked widget pagesWidget = new QStackedWidget; - pagesWidget->setObjectName("pagesWidget"); auto *generalPage = new GeneralSettingsPage; auto *appearancePage = new AppearanceSettingsPage; @@ -204,17 +199,12 @@ void SettingsView::setupUi() searchDelegate->setPageIcons(icons); searchResultsView->viewport()->update(); }); - // The search delegate reads the InterfaceDefault role at paint time, so a - // font change only needs a repaint, mirroring the theme change above. - connect(themeManager, &ThemeManager::fontsChanged, searchResultsView, - [this] { searchResultsView->viewport()->update(); }); // Build search index after pages are created buildSearchIndex(); // Pages container (stacked widget + search results overlay) pagesContainer = new QWidget; - pagesContainer->setObjectName("pagesContainer"); auto *containerLayout = new QStackedLayout; containerLayout->setStackingMode(QStackedLayout::StackAll); containerLayout->addWidget(pagesWidget); @@ -254,7 +244,6 @@ void SettingsView::setupUi() void SettingsView::setupNavTree() { navTree = new QTreeWidget; - navTree->setObjectName("navTree"); navTree->setHeaderHidden(true); navTree->setRootIsDecorated(true); navTree->setIconSize(QSize(24, 24)); diff --git a/cockatrice/src/interface/widgets/general/display/banner_widget.cpp b/cockatrice/src/interface/widgets/general/display/banner_widget.cpp index 5cb62db0f..baf46bbba 100644 --- a/cockatrice/src/interface/widgets/general/display/banner_widget.cpp +++ b/cockatrice/src/interface/widgets/general/display/banner_widget.cpp @@ -1,11 +1,9 @@ #include "banner_widget.h" #include "../../../pixel_map_generator.h" -#include "../../../theme_manager.h" #include #include -#include #include #include #include @@ -30,9 +28,7 @@ BannerWidget::BannerWidget(QWidget *parent, const QString &text, Qt::Orientation if (transparency > 50) { textColor = " color: white;"; } - bannerLabel->setStyleSheet(textColor); - applyInterfaceDefaultFont(); - connect(themeManager, &ThemeManager::fontsChanged, this, &BannerWidget::applyInterfaceDefaultFont); + bannerLabel->setStyleSheet("font-size: 24px; font-weight: bold;" + textColor); layout->addWidget(iconLabel); layout->addWidget(bannerLabel); @@ -60,16 +56,6 @@ void BannerWidget::setText(const QString &text) const bannerLabel->setText(text); } -void BannerWidget::applyInterfaceDefaultFont() -{ - // Family comes from the InterfaceDefault theme role; the 24px size and - // bold weight previously carried by the stylesheet stay caller-side. - QFont bannerFont = themeManager->appFont(AppFont::InterfaceDefault); - bannerFont.setPixelSize(24); - bannerFont.setWeight(QFont::Bold); - bannerLabel->setFont(bannerFont); -} - void BannerWidget::setClickable(bool _clickable) { clickable = _clickable; diff --git a/cockatrice/src/interface/widgets/general/display/banner_widget.h b/cockatrice/src/interface/widgets/general/display/banner_widget.h index d46250bdb..06556089b 100644 --- a/cockatrice/src/interface/widgets/general/display/banner_widget.h +++ b/cockatrice/src/interface/widgets/general/display/banner_widget.h @@ -48,7 +48,6 @@ signals: private slots: void toggleBuddyVisibility() const; void updateDropdownIconState() const; - void applyInterfaceDefaultFont(); }; #endif // BANNER_WIDGET_H diff --git a/cockatrice/src/interface/widgets/general/display/charts/bars/bar_chart_widget.cpp b/cockatrice/src/interface/widgets/general/display/charts/bars/bar_chart_widget.cpp index 6bb1dcd62..484ac861d 100644 --- a/cockatrice/src/interface/widgets/general/display/charts/bars/bar_chart_widget.cpp +++ b/cockatrice/src/interface/widgets/general/display/charts/bars/bar_chart_widget.cpp @@ -1,7 +1,5 @@ #include "bar_chart_widget.h" -#include "../../interface/theme_manager.h" - #include #include #include @@ -21,10 +19,6 @@ BarChartWidget::BarChartWidget(QWidget *parent) : QWidget(parent) { setMouseTracking(true); - - // Labels read the InterfaceDefault role at paint time, so a font change - // only needs a repaint. - connect(themeManager, &ThemeManager::fontsChanged, this, [this] { update(); }); } void BarChartWidget::setBars(const QVector &newBars) @@ -152,8 +146,7 @@ void BarChartWidget::paintEvent(QPaintEvent *) // draw label below bar QRect labelRect(left + i * (barWidth + spacing), bottom, barWidth, LABEL_H); - // Family comes from the InterfaceDefault theme role; bold stays caller-side. - QFont f = themeManager->appFont(AppFont::InterfaceDefault); + QFont f = p.font(); f.setBold(true); p.setFont(f); p.setPen(Qt::black); diff --git a/cockatrice/src/interface/widgets/general/display/charts/bars/segmented_bar_widget.cpp b/cockatrice/src/interface/widgets/general/display/charts/bars/segmented_bar_widget.cpp index 359d56772..5a3130964 100644 --- a/cockatrice/src/interface/widgets/general/display/charts/bars/segmented_bar_widget.cpp +++ b/cockatrice/src/interface/widgets/general/display/charts/bars/segmented_bar_widget.cpp @@ -1,7 +1,5 @@ #include "segmented_bar_widget.h" -#include "../../interface/theme_manager.h" - #include #include #include @@ -24,10 +22,6 @@ SegmentedBarWidget::SegmentedBarWidget(QString label, QVector segments, setMinimumWidth(36); setMaximumWidth(50); setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding); - - // The label reads the InterfaceDefault role at paint time, so a font - // change only needs a repaint. - connect(themeManager, &ThemeManager::fontsChanged, this, [this] { update(); }); } QSize SegmentedBarWidget::sizeHint() const @@ -101,8 +95,7 @@ void SegmentedBarWidget::paintEvent(QPaintEvent *) // draw label QRect labelRect(0, h - LABEL_H, w, LABEL_H); - // Family comes from the InterfaceDefault theme role; bold stays caller-side. - QFont f = themeManager->appFont(AppFont::InterfaceDefault); + QFont f = p.font(); f.setBold(true); p.setFont(f); p.setPen(Qt::black); diff --git a/cockatrice/src/interface/widgets/general/display/dynamic_font_size_label.cpp b/cockatrice/src/interface/widgets/general/display/dynamic_font_size_label.cpp index 598c70fe2..0609eb16a 100644 --- a/cockatrice/src/interface/widgets/general/display/dynamic_font_size_label.cpp +++ b/cockatrice/src/interface/widgets/general/display/dynamic_font_size_label.cpp @@ -1,7 +1,5 @@ #include "dynamic_font_size_label.h" -#include "../../interface/theme_manager.h" - #include #include #include @@ -30,13 +28,9 @@ void DynamicFontSizeLabel::paintEvent(QPaintEvent *event) // QElapsedTimer timer; // timer.start(); - // Family follows the InterfaceDefault theme role; the fitted point size - // stays computed from the widget geometry as before. - QFont newFont = themeManager->appFont(AppFont::InterfaceDefault); + QFont newFont = font(); float fontSize = getWidgetMaximumFontSize(this, this->text()); newFont.setPointSizeF(fontSize); - // Preserve the caller's bold emphasis, which the role font does not carry. - newFont.setBold(font().bold()); setFont(newFont); // qDebug() << "Font size set to" << fontSize; @@ -46,10 +40,7 @@ void DynamicFontSizeLabel::paintEvent(QPaintEvent *event) float DynamicFontSizeLabel::getWidgetMaximumFontSize(QWidget *widget, const QString &text) { - // Measure with the InterfaceDefault family so the fit matches the painted font. - QFont font = themeManager->appFont(AppFont::InterfaceDefault); - font.setBold(widget->font().bold()); - font.setPointSizeF(widget->font().pointSizeF()); + QFont font = widget->font(); const QRect widgetRect = widget->contentsRect(); const float widgetWidth = widgetRect.width(); const float widgetHeight = widgetRect.height(); diff --git a/cockatrice/src/interface/widgets/general/display/dynamic_font_size_push_button.cpp b/cockatrice/src/interface/widgets/general/display/dynamic_font_size_push_button.cpp index 9b5f5bdda..25e8ca48f 100644 --- a/cockatrice/src/interface/widgets/general/display/dynamic_font_size_push_button.cpp +++ b/cockatrice/src/interface/widgets/general/display/dynamic_font_size_push_button.cpp @@ -1,6 +1,5 @@ #include "dynamic_font_size_push_button.h" -#include "../../interface/theme_manager.h" #include "dynamic_font_size_label.h" #include @@ -21,12 +20,10 @@ void DynamicFontSizePushButton::paintEvent(QPaintEvent *event) // Call the base class paintEvent to preserve any other painting behavior QPushButton::paintEvent(event); - // Adjust the font size dynamically based on the text. Family follows the - // InterfaceDefault theme role; the fitted size stays computed as before. - QFont newFont = themeManager->appFont(AppFont::InterfaceDefault); + // Adjust the font size dynamically based on the text + QFont newFont = font(); float fontSize = DynamicFontSizeLabel::getWidgetMaximumFontSize(this, this->text()); newFont.setPointSizeF(fontSize); - newFont.setBold(font().bold()); setFont(newFont); // Get painter for custom painting diff --git a/cockatrice/src/interface/widgets/general/home_styled_button.cpp b/cockatrice/src/interface/widgets/general/home_styled_button.cpp index a2ac7912d..7f23827bc 100644 --- a/cockatrice/src/interface/widgets/general/home_styled_button.cpp +++ b/cockatrice/src/interface/widgets/general/home_styled_button.cpp @@ -1,9 +1,6 @@ #include "home_styled_button.h" -#include "../../theme_manager.h" - #include -#include #include #include #include @@ -21,21 +18,9 @@ class QWidget; HomeStyledButton::HomeStyledButton(const QString &text, QPair _gradientColors, QWidget *parent) : QPushButton(text, parent), gradientColors(_gradientColors) { - // Keep the font/style-derived height even when the home page has ample space. - setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); + setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + setMinimumHeight(25); setStyleSheet(generateButtonStylesheet(gradientColors)); - applyInterfaceDefaultFont(); - connect(themeManager, &ThemeManager::fontsChanged, this, &HomeStyledButton::applyInterfaceDefaultFont); -} - -void HomeStyledButton::applyInterfaceDefaultFont() -{ - // Family comes from the InterfaceDefault theme role; the 34px size and - // bold weight previously carried by the stylesheet stay caller-side. - QFont buttonFont = themeManager->appFont(AppFont::InterfaceDefault); - buttonFont.setPixelSize(34); - buttonFont.setWeight(QFont::Bold); - setFont(buttonFont); } void HomeStyledButton::updateStylesheet(const QPair &colors) @@ -65,6 +50,9 @@ QString HomeStyledButton::generateButtonStylesheet(const QPair & return QString(R"( QPushButton { + font-size: 34px; + font-weight: bold; + font-family: sans-serif, "Segoe UI", "Helvetica Neue"; padding: 30px; color: white; border: 2px solid %1; diff --git a/cockatrice/src/interface/widgets/general/home_styled_button.h b/cockatrice/src/interface/widgets/general/home_styled_button.h index 5756db5a1..55ac5550f 100644 --- a/cockatrice/src/interface/widgets/general/home_styled_button.h +++ b/cockatrice/src/interface/widgets/general/home_styled_button.h @@ -24,9 +24,6 @@ public: public slots: void paintEvent(QPaintEvent *event) override; -private slots: - void applyInterfaceDefaultFont(); - private: QPair gradientColors; }; diff --git a/cockatrice/src/interface/widgets/general/home_widget.cpp b/cockatrice/src/interface/widgets/general/home_widget.cpp index db4501efc..5905b2c40 100644 --- a/cockatrice/src/interface/widgets/general/home_widget.cpp +++ b/cockatrice/src/interface/widgets/general/home_widget.cpp @@ -27,11 +27,10 @@ #include #include #include -#include +#include #include #include #include -#include #include #include #include @@ -45,9 +44,9 @@ #include #include #include -#include #include #include +#include #include #include #include @@ -61,7 +60,6 @@ #include #include #include -#include #include #include #include @@ -70,26 +68,20 @@ HomeWidget::HomeWidget(QWidget *parent, TabSupervisor *_tabSupervisor) : QWidget(parent), tabSupervisor(_tabSupervisor), background(themePixmap(QStringLiteral("backgrounds/home"))) { - setObjectName("homeWidget"); + layout = new QGridLayout(this); backgroundSourceCard = new CardInfoPictureArtCropWidget(this); gradientColors = determineButtonColor(); - // The launcher keeps its natural size on a large display and stays centred in the - // viewport. On a small window it scrolls, instead of its combined minimum height - // becoming the minimum height of the entire application. - auto *scrollArea = QtUtils::makeScrollArea(this, Qt::ScrollBarAsNeeded, Qt::ScrollBarAsNeeded); - scrollArea->setObjectName("homeActionsScrollArea"); - scrollArea->setFrameShape(QFrame::NoFrame); - scrollArea->setWidgetResizable(false); - scrollArea->setAlignment(Qt::AlignCenter); - // The home background is painted by this widget, so let it show through the viewport. - scrollArea->viewport()->setAutoFillBackground(false); - scrollArea->setWidget(createButtons()); + layout->addWidget(createButtons(), 1, 1, Qt::AlignVCenter | Qt::AlignHCenter); - auto *layout = new QVBoxLayout(this); - layout->addWidget(scrollArea); + layout->setRowStretch(0, 1); + layout->setRowStretch(2, 1); + layout->setColumnStretch(0, 1); + layout->setColumnStretch(2, 1); + + setLayout(layout); cardChangeTimer = new QTimer(this); connect(cardChangeTimer, &QTimer::timeout, this, &HomeWidget::updateRandomCard); @@ -295,25 +287,22 @@ void HomeWidget::updateButtonsToBackgroundColor() QGroupBox *HomeWidget::createButtons() { QGroupBox *box = new QGroupBox(this); - box->setObjectName("homeButtonsBox"); - // The box only stacks the logo and the actions: no frame, and no background of its - // own, so the home background art stays visible. It never shows a title. The rule is - // load-bearing: QScrollArea::setWidget() turns autoFillBackground on for its widget. - box->setStyleSheet("QGroupBox { background: transparent; border: none; }"); - // Family comes from the InterfaceDefault theme role; the 20px size stays caller-side. - auto applyBoxFont = [box]() { - QFont boxFont = themeManager->appFont(AppFont::InterfaceDefault); - boxFont.setPixelSize(20); - box->setFont(boxFont); - }; - applyBoxFont(); - connect(themeManager, &ThemeManager::fontsChanged, this, applyBoxFont); + box->setStyleSheet(R"( + QGroupBox { + font-size: 20px; + color: white; /* Title text color */ + background: transparent; + } + + QGroupBox::title { + color: white; + subcontrol-origin: margin; + subcontrol-position: top center; /* or top left / right */ + } +)"); + box->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); QVBoxLayout *boxLayout = new QVBoxLayout; boxLayout->setAlignment(Qt::AlignHCenter); - // The scroll area is not resizable, so the box has to size itself: SetFixedSize keeps - // it at its content size and re-fixes it whenever the labels or fonts change, which is - // what makes the scroll area's scroll bars follow the content. - boxLayout->setSizeConstraint(QLayout::SetFixedSize); logoLabel = new QLabel; logoLabel->setAlignment(Qt::AlignCenter); @@ -322,34 +311,27 @@ QGroupBox *HomeWidget::createButtons() boxLayout->addSpacing(25); connectButton = new HomeStyledButton(QString(), gradientColors); - connectButton->setObjectName("homeConnectButton"); boxLayout->addWidget(connectButton); visualDeckEditorButton = new HomeStyledButton(QString(), gradientColors); - visualDeckEditorButton->setObjectName("homeCreateNewDeckButton"); connect(visualDeckEditorButton, &QPushButton::clicked, tabSupervisor, [this] { tabSupervisor->openDeckInNewTab(LoadedDeck()); }); boxLayout->addWidget(visualDeckEditorButton); visualDeckStorageButton = new HomeStyledButton(QString(), gradientColors); - visualDeckStorageButton->setObjectName("homeBrowseDecksButton"); connect(visualDeckStorageButton, &QPushButton::clicked, tabSupervisor, [this] { tabSupervisor->actTabVisualDeckStorage(true); }); boxLayout->addWidget(visualDeckStorageButton); visualDatabaseDisplayButton = new HomeStyledButton(QString(), gradientColors); - visualDatabaseDisplayButton->setObjectName("homeBrowseCardDatabaseButton"); connect(visualDatabaseDisplayButton, &QPushButton::clicked, tabSupervisor, &TabSupervisor::addVisualDatabaseDisplayTab); boxLayout->addWidget(visualDatabaseDisplayButton); edhrecButton = new HomeStyledButton(QString(), gradientColors); - edhrecButton->setObjectName("homeBrowseEdhrecButton"); connect(edhrecButton, &QPushButton::clicked, tabSupervisor, &TabSupervisor::addEdhrecMainTab); boxLayout->addWidget(edhrecButton); archidektButton = new HomeStyledButton(QString(), gradientColors); - archidektButton->setObjectName("homeBrowseArchidektButton"); connect(archidektButton, &QPushButton::clicked, tabSupervisor, &TabSupervisor::addArchidektTab); boxLayout->addWidget(archidektButton); replayButton = new HomeStyledButton(QString(), gradientColors); - replayButton->setObjectName("homeViewReplaysButton"); connect(replayButton, &QPushButton::clicked, tabSupervisor, [this] { tabSupervisor->actTabReplays(true); }); boxLayout->addWidget(replayButton); if (qobject_cast(tabSupervisor->parentWidget())) { @@ -481,9 +463,7 @@ void HomeWidget::paintEvent(QPaintEvent *event) constexpr int margin = 15; constexpr qreal attributionMargin = 4.0; - // Family comes from the InterfaceDefault theme role; the 14px sizes and - // bold weights stay caller-side as before. - QFont attributionFont = themeManager->appFont(AppFont::InterfaceDefault); + QFont attributionFont = painter.font(); attributionFont.setPointSize(14); attributionFont.setBold(true); painter.setFont(attributionFont); @@ -496,7 +476,7 @@ void HomeWidget::paintEvent(QPaintEvent *event) // Card name bubble above the attribution (when enabled). if (!cardName.isEmpty() && SettingsCache::instance().appearance().getHomeTabDisplayCardName()) { - QFont font = themeManager->appFont(AppFont::InterfaceDefault); + QFont font = painter.font(); font.setPointSize(14); font.setBold(true); painter.setFont(font); diff --git a/cockatrice/src/interface/widgets/general/home_widget.h b/cockatrice/src/interface/widgets/general/home_widget.h index fec1db950..aa4745c82 100644 --- a/cockatrice/src/interface/widgets/general/home_widget.h +++ b/cockatrice/src/interface/widgets/general/home_widget.h @@ -16,6 +16,7 @@ #include #include +class QGridLayout; class QLabel; class CardInfoPictureArtCropWidget; class ExactCard; @@ -45,6 +46,7 @@ public slots: void updateConnectButton(const ClientStatus status); private: + QGridLayout *layout; QTimer *cardChangeTimer; TabSupervisor *tabSupervisor; QPixmap background; diff --git a/cockatrice/src/interface/widgets/onboarding/first_run_wizard.cpp b/cockatrice/src/interface/widgets/onboarding/first_run_wizard.cpp index e69687c54..c4298d6b6 100644 --- a/cockatrice/src/interface/widgets/onboarding/first_run_wizard.cpp +++ b/cockatrice/src/interface/widgets/onboarding/first_run_wizard.cpp @@ -1,6 +1,5 @@ #include "first_run_wizard.h" -#include "../interface/theme_manager.h" #include "first_run_wizard_page.h" #include "pages/account_setup_page.h" #include "pages/card_database_setup_page.h" @@ -33,8 +32,9 @@ FirstRunWizard::FirstRunWizard(QWidget *parent) : QDialog(parent) bannerHost = new BannerHost(this); titleLabel = new QLabel(this); - applyInterfaceDefaultFont(); - connect(themeManager, &ThemeManager::fontsChanged, this, &FirstRunWizard::applyInterfaceDefaultFont); + QFont titleFont = QtUtils::boldFont(titleLabel->font()); + titleFont.setPointSizeF(titleFont.pointSizeF() * 1.4); + titleLabel->setFont(titleFont); subtitleLabel = new QLabel(this); subtitleLabel->setWordWrap(true); @@ -154,16 +154,6 @@ void FirstRunWizard::updateChrome() } } -void FirstRunWizard::applyInterfaceDefaultFont() -{ - // Family comes from the InterfaceDefault theme role; the *1.4 size - // multiplier and bold weight stay caller-side as before. - QFont titleFont = themeManager->appFont(AppFont::InterfaceDefault); - titleFont.setPointSizeF(titleFont.pointSizeF() * 1.4); - titleFont.setBold(true); - titleLabel->setFont(titleFont); -} - void FirstRunWizard::goNext() { FirstRunWizardPage *page = pages[currentIndex]; diff --git a/cockatrice/src/interface/widgets/onboarding/first_run_wizard.h b/cockatrice/src/interface/widgets/onboarding/first_run_wizard.h index a5fa3b4bd..812985a9f 100644 --- a/cockatrice/src/interface/widgets/onboarding/first_run_wizard.h +++ b/cockatrice/src/interface/widgets/onboarding/first_run_wizard.h @@ -55,7 +55,6 @@ private slots: void goBack(); void skip(); void updateChrome(); - void applyInterfaceDefaultFont(); private: void addPage(FirstRunWizardPage *page); diff --git a/cockatrice/src/interface/widgets/printing_selector/all_zones_card_amount_widget.cpp b/cockatrice/src/interface/widgets/printing_selector/all_zones_card_amount_widget.cpp index 13a43bd47..bad88ddbb 100644 --- a/cockatrice/src/interface/widgets/printing_selector/all_zones_card_amount_widget.cpp +++ b/cockatrice/src/interface/widgets/printing_selector/all_zones_card_amount_widget.cpp @@ -1,6 +1,5 @@ #include "all_zones_card_amount_widget.h" -#include "../../theme_manager.h" #include "../general/display/shadow_background_label.h" #include "card_amount_widget.h" #include "libcockatrice/card/card_info.h" @@ -68,7 +67,6 @@ AllZonesCardAmountWidget::AllZonesCardAmountWidget(QWidget *parent, buttonBoxSideboard->setVisible(!isToken); connect(cardSizeSlider, &QSlider::valueChanged, this, &AllZonesCardAmountWidget::adjustFontSize); - connect(themeManager, &ThemeManager::fontsChanged, this, &AllZonesCardAmountWidget::applyCardTextFont); QTimer::singleShot(10, this, [this]() { adjustFontSize(this->cardSizeSlider->value()); }); @@ -92,9 +90,8 @@ void AllZonesCardAmountWidget::adjustFontSize(int scalePercentage) int newFontSize = minFontSize + (scalePercentage - basePercentage) * (maxFontSize - minFontSize) / 225; newFontSize = std::clamp(newFontSize, minFontSize, maxFontSize); - // Update the font labels. The family comes from the CardText theme - // role; the slider-driven point size stays as before. - QFont zoneLabelFont = themeManager->appFont(AppFont::CardText); + // Update the font labels + QFont zoneLabelFont = zoneLabelMainboard->font(); zoneLabelFont.setPointSize(newFontSize); zoneLabelMainboard->setFont(zoneLabelFont); zoneLabelSideboard->setFont(zoneLabelFont); @@ -104,11 +101,6 @@ void AllZonesCardAmountWidget::adjustFontSize(int scalePercentage) repaint(); } -void AllZonesCardAmountWidget::applyCardTextFont() -{ - adjustFontSize(cardSizeSlider->value()); -} - void AllZonesCardAmountWidget::setAmounts(int mainboardAmount, int sideboardAmount, int tokensboardAmount) { buttonBoxMainboard->setAmount(mainboardAmount); diff --git a/cockatrice/src/interface/widgets/printing_selector/all_zones_card_amount_widget.h b/cockatrice/src/interface/widgets/printing_selector/all_zones_card_amount_widget.h index 3213cf16b..cefabe293 100644 --- a/cockatrice/src/interface/widgets/printing_selector/all_zones_card_amount_widget.h +++ b/cockatrice/src/interface/widgets/printing_selector/all_zones_card_amount_widget.h @@ -36,9 +36,6 @@ public slots: void adjustFontSize(int scalePercentage); void setAmounts(int mainboardAmount, int sideboardAmount, int tokensboardAmount); -private slots: - void applyCardTextFont(); - private: QVBoxLayout *layout; QSlider *cardSizeSlider; diff --git a/cockatrice/src/interface/widgets/printing_selector/card_amount_widget.cpp b/cockatrice/src/interface/widgets/printing_selector/card_amount_widget.cpp index 7f25ea1f4..eae49d7ef 100644 --- a/cockatrice/src/interface/widgets/printing_selector/card_amount_widget.cpp +++ b/cockatrice/src/interface/widgets/printing_selector/card_amount_widget.cpp @@ -1,6 +1,5 @@ #include "card_amount_widget.h" -#include "../../theme_manager.h" #include "../deck_editor/deck_state_manager.h" #include "../general/display/dynamic_font_size_push_button.h" #include "deck_list_model.h" @@ -78,7 +77,6 @@ CardAmountWidget::CardAmountWidget(QWidget *parent, // Connect slider for dynamic font size adjustment connect(cardSizeSlider, &QSlider::valueChanged, this, &CardAmountWidget::adjustFontSize); - connect(themeManager, &ThemeManager::fontsChanged, this, &CardAmountWidget::applyCardTextFont); } int CardAmountWidget::getAmount() @@ -140,9 +138,8 @@ void CardAmountWidget::adjustFontSize(int scalePercentage) int newFontSize = minFontSize + (scalePercentage - basePercentage) * (maxFontSize - minFontSize) / 225; newFontSize = std::clamp(newFontSize, minFontSize, maxFontSize); - // Update the font for card count label. The family comes from the - // CardText theme role; the slider-driven point size stays as before. - QFont cardCountFont = themeManager->appFont(AppFont::CardText); + // Update the font for card count label + QFont cardCountFont = cardCountInZone->font(); cardCountFont.setPointSize(newFontSize); cardCountInZone->setFont(cardCountFont); @@ -153,11 +150,6 @@ void CardAmountWidget::adjustFontSize(int scalePercentage) repaint(); } -void CardAmountWidget::applyCardTextFont() -{ - adjustFontSize(cardSizeSlider->value()); -} - /** * @brief Updates the card count display in the widget. */ diff --git a/cockatrice/src/interface/widgets/printing_selector/card_amount_widget.h b/cockatrice/src/interface/widgets/printing_selector/card_amount_widget.h index cef872157..6f5889fc7 100644 --- a/cockatrice/src/interface/widgets/printing_selector/card_amount_widget.h +++ b/cockatrice/src/interface/widgets/printing_selector/card_amount_widget.h @@ -67,7 +67,6 @@ private slots: void removePrintingSideboard(); void removePrintingTokensboard(); void adjustFontSize(int scalePercentage); - void applyCardTextFont(); }; #endif // CARD_AMOUNT_WIDGET_H diff --git a/cockatrice/src/interface/widgets/printing_selector/printing_selector_card_overlay_widget.cpp b/cockatrice/src/interface/widgets/printing_selector/printing_selector_card_overlay_widget.cpp index a5177a6bc..94acdbdce 100644 --- a/cockatrice/src/interface/widgets/printing_selector/printing_selector_card_overlay_widget.cpp +++ b/cockatrice/src/interface/widgets/printing_selector/printing_selector_card_overlay_widget.cpp @@ -2,8 +2,6 @@ #include "../../../client/settings/cache_settings.h" #include "../../card_picture_loader/card_picture_loader.h" -#include "../../theme_icon.h" -#include "../../theme_manager.h" #include "../cards/card_info_picture_widget.h" #include "../tabs/abstract_tab_deck_editor.h" #include "all_zones_card_amount_widget.h" @@ -14,19 +12,16 @@ #include #include -#include #include #include #include #include +#include #include #include #include #include #include -#include -#include -#include #include #include #include @@ -41,6 +36,7 @@ #include #include #include +#include #include #include #include @@ -124,12 +120,6 @@ PrintingSelectorCardOverlayWidget::PrintingSelectorCardOverlayWidget(QWidget *pa mainLayout->addWidget(cardInfoPicture); initializePinBadge(); - auto refreshPinBadge = [this] { - initializePinBadge(); - updatePinBadgeVisibility(); - }; - connect(themeManager, &ThemeManager::paletteChanged, this, refreshPinBadge); - connect(themeManager, &ThemeManager::themeChanged, this, refreshPinBadge); // Parent the preview to this overlay so it is destroyed with it (Qt::ToolTip keeps // it a frameless, non-activating top-level window despite the parent). @@ -411,55 +401,10 @@ void PrintingSelectorCardOverlayWidget::customMenu(QPoint point) } /** - * @brief Composes the pin badge: the glyph over a rounded palette backdrop. + * @brief Initializes the pin badge overlay and loads its icon with DPI-aware rasterization. * - * The badge floats on card art rather than on a palette surface, so a bare glyph - * colored WindowText is not enough: a thin dark stroke disappears on dark art in a - * light palette, and a thin light stroke disappears on light art in a dark palette. - * A Window-filled rounded rect with a WindowText outline gives the glyph a solid - * backing, and the outline guarantees the badge edge contrasts with the art even when - * the fill matches it. Both roles follow the palette, like the glyph. - * - * @param logicalSize The badge's size in logical pixels. - * @param devicePixelRatio The label's device pixel ratio, applied to the pixmap so it - * stays crisp on HiDPI displays. - * @param palette The palette the glyph's WindowText resolves against. - */ -static QPixmap composePinBadgePixmap(const QSize &logicalSize, qreal devicePixelRatio, const QPalette &palette) -{ - // Inset the glyph so the backdrop reads as a badge around it, not as a border. - const int inset = qRound(logicalSize.width() * 0.16); - const QSize glyphSize = logicalSize - QSize(inset * 2, inset * 2); - const QPixmap glyph = loadThemedIcon(QStringLiteral("icons/pin")).pixmap(glyphSize, devicePixelRatio); - if (glyph.isNull()) { - // Let the caller fall back to the textual badge instead of drawing an empty panel. - return QPixmap(); - } - - QPixmap badge(logicalSize * devicePixelRatio); - badge.setDevicePixelRatio(devicePixelRatio); - badge.fill(Qt::transparent); - - QPainter painter(&badge); - painter.setRenderHint(QPainter::Antialiasing); - - // Half-pixel inset keeps the 1.5px outline inside the pixmap instead of clipping it. - const QRectF box(QPointF(0.5, 0.5), QSizeF(logicalSize) - QSizeF(1, 1)); - painter.setPen(QPen(palette.color(QPalette::WindowText), 1.5)); - painter.setBrush(palette.color(QPalette::Window)); - painter.drawRoundedRect(box, 8, 8); - painter.drawPixmap(QPointF(inset, inset), glyph); - - return badge; -} - -/** - * @brief Initializes the pin badge overlay and loads a DPI-aware icon pixmap. - * - * The glyph is drawn onto a rounded palette backdrop so the badge stays readable on - * card art, and it is rasterized at the label's device pixel ratio so it stays crisp on - * HiDPI displays. It is re-rendered when the palette or theme changes so the badge - * follows the surrounding UI instead of freezing at its first colors. + * The icon is rasterized once using the label's device pixel ratio so it stays crisp on HiDPI + * displays. The resulting pixmap is cached on the QLabel and simply shown/hidden when needed. * If the SVG cannot be read, a textual fallback badge is created instead. */ void PrintingSelectorCardOverlayWidget::initializePinBadge() @@ -475,13 +420,24 @@ void PrintingSelectorCardOverlayWidget::initializePinBadge() } bool pinLoaded = false; - const QSize targetSize(64, 64); - const QPixmap pinPix = composePinBadgePixmap(targetSize, pinBadge->devicePixelRatioF(), pinBadge->palette()); - if (!pinPix.isNull()) { - pinBadge->setPixmap(pinPix); - pinBadge->setFixedSize(targetSize); - pinBadge->setStyleSheet(QStringLiteral("background: transparent;")); - pinLoaded = true; + QImageReader pinReader(QStringLiteral("theme:icons/pin")); + + if (pinReader.canRead()) { + // Rasterize a 64×64 logical icon so it has a consistent size regardless of card scaling. + const QSize targetSize(64, 64); + const qreal dpr = pinBadge->devicePixelRatioF(); + const QSize rasterSize(qMax(1, qCeil(targetSize.width() * dpr)), qMax(1, qCeil(targetSize.height() * dpr))); + pinReader.setScaledSize(rasterSize); + const QImage pinImage = pinReader.read(); + if (!pinImage.isNull()) { + // Tag the pixmap with the same DPR so Qt renders it at the correct physical size. + QPixmap pinPix = QPixmap::fromImage(pinImage); + pinPix.setDevicePixelRatio(dpr); + pinBadge->setPixmap(pinPix); + pinBadge->setFixedSize(targetSize); + pinBadge->setStyleSheet(QStringLiteral("background: transparent;")); + pinLoaded = true; + } } if (!pinLoaded) { diff --git a/cockatrice/src/interface/widgets/printing_selector/printing_selector_placeholder_widget.cpp b/cockatrice/src/interface/widgets/printing_selector/printing_selector_placeholder_widget.cpp index 225c3e21d..7f9ad66c8 100644 --- a/cockatrice/src/interface/widgets/printing_selector/printing_selector_placeholder_widget.cpp +++ b/cockatrice/src/interface/widgets/printing_selector/printing_selector_placeholder_widget.cpp @@ -1,8 +1,5 @@ #include "printing_selector_placeholder_widget.h" -#include "../interface/theme_manager.h" - -#include #include #include #include @@ -33,12 +30,10 @@ PrintingSelectorPlaceholderWidget::PrintingSelectorPlaceholderWidget(QWidget *pa textLabel->setStyleSheet(R"( QLabel { color: palette(mid); + font-size: 14px; padding: 10px; } )"); - applyInterfaceDefaultFont(); - connect(themeManager, &ThemeManager::fontsChanged, this, - &PrintingSelectorPlaceholderWidget::applyInterfaceDefaultFont); mainLayout->addWidget(imageLabel); mainLayout->addWidget(textLabel); @@ -49,12 +44,4 @@ PrintingSelectorPlaceholderWidget::PrintingSelectorPlaceholderWidget(QWidget *pa void PrintingSelectorPlaceholderWidget::retranslateUi() { textLabel->setText(tr("Select a card to view its available printings")); -} - -void PrintingSelectorPlaceholderWidget::applyInterfaceDefaultFont() -{ - // Family comes from the InterfaceDefault theme role; the 14px size stays caller-side. - QFont placeholderFont = themeManager->appFont(AppFont::InterfaceDefault); - placeholderFont.setPixelSize(14); - textLabel->setFont(placeholderFont); } \ No newline at end of file diff --git a/cockatrice/src/interface/widgets/printing_selector/printing_selector_placeholder_widget.h b/cockatrice/src/interface/widgets/printing_selector/printing_selector_placeholder_widget.h index 9e6e47bcb..6702d325a 100644 --- a/cockatrice/src/interface/widgets/printing_selector/printing_selector_placeholder_widget.h +++ b/cockatrice/src/interface/widgets/printing_selector/printing_selector_placeholder_widget.h @@ -14,9 +14,6 @@ class PrintingSelectorPlaceholderWidget : public QWidget public: explicit PrintingSelectorPlaceholderWidget(QWidget *parent = nullptr); -private slots: - void applyInterfaceDefaultFont(); - private: QVBoxLayout *mainLayout; QLabel *imageLabel; diff --git a/cockatrice/src/interface/widgets/quick_settings/settings_button_widget.cpp b/cockatrice/src/interface/widgets/quick_settings/settings_button_widget.cpp index c6a6d989f..ea0d2db5a 100644 --- a/cockatrice/src/interface/widgets/quick_settings/settings_button_widget.cpp +++ b/cockatrice/src/interface/widgets/quick_settings/settings_button_widget.cpp @@ -1,12 +1,12 @@ #include "settings_button_widget.h" -#include "../../theme_icon.h" +#include "../../pixel_map_generator.h" #include "settings_popup_widget.h" #include #include -#include #include +#include #include #include #include @@ -21,7 +21,7 @@ SettingsButtonWidget::SettingsButtonWidget(QWidget *parent) : QWidget(parent), button(new QToolButton(this)), popup(new SettingsPopupWidget(nullptr)) { - button->setIcon(loadThemedIcon(QStringLiteral("icons/settings"))); + button->setIcon(themePixmap(QStringLiteral("icons/cogwheel"))); button->setCheckable(true); button->setFixedSize(32, 32); connect(button, &QToolButton::clicked, this, &SettingsButtonWidget::togglePopup); @@ -49,9 +49,9 @@ void SettingsButtonWidget::removeSettingsWidget(QWidget *toRemove) const popup->removeSettingsWidget(toRemove); } -void SettingsButtonWidget::setButtonIcon(const QIcon &icon) +void SettingsButtonWidget::setButtonIcon(QPixmap iconMap) { - button->setIcon(icon); + button->setIcon(iconMap); } void SettingsButtonWidget::setButtonText(const QString &text) diff --git a/cockatrice/src/interface/widgets/quick_settings/settings_button_widget.h b/cockatrice/src/interface/widgets/quick_settings/settings_button_widget.h index 56d542d37..015300bf8 100644 --- a/cockatrice/src/interface/widgets/quick_settings/settings_button_widget.h +++ b/cockatrice/src/interface/widgets/quick_settings/settings_button_widget.h @@ -13,7 +13,7 @@ #include class QHBoxLayout; -class QIcon; +class QPixmap; class QToolButton; class SettingsPopupWidget; @@ -28,7 +28,7 @@ public: void addSettingsWidget(QWidget *toAdd) const; void removeSettingsWidget(QWidget *toRemove) const; - void setButtonIcon(const QIcon &icon); + void setButtonIcon(QPixmap iconMap); void setButtonText(const QString &buttonText); void setCompact(bool compact); bool isCompact() const diff --git a/cockatrice/src/interface/widgets/replay/replay_event_index.cpp b/cockatrice/src/interface/widgets/replay/replay_event_index.cpp deleted file mode 100644 index d21671b5a..000000000 --- a/cockatrice/src/interface/widgets/replay/replay_event_index.cpp +++ /dev/null @@ -1,876 +0,0 @@ -#include "replay_event_index.h" - -#include "libcockatrice/protocol/get_pb_extension.h" -#include "libcockatrice/protocol/pb/card_attributes.pb.h" -#include "libcockatrice/protocol/pb/context_concede.pb.h" -#include "libcockatrice/protocol/pb/context_connection_state_changed.pb.h" -#include "libcockatrice/protocol/pb/context_deck_select.pb.h" -#include "libcockatrice/protocol/pb/context_mulligan.pb.h" -#include "libcockatrice/protocol/pb/context_ready_start.pb.h" -#include "libcockatrice/protocol/pb/context_set_sideboard_lock.pb.h" -#include "libcockatrice/protocol/pb/context_undo_draw.pb.h" -#include "libcockatrice/protocol/pb/event_attach_card.pb.h" -#include "libcockatrice/protocol/pb/event_change_zone_properties.pb.h" -#include "libcockatrice/protocol/pb/event_create_counter.pb.h" -#include "libcockatrice/protocol/pb/event_create_token.pb.h" -#include "libcockatrice/protocol/pb/event_del_counter.pb.h" -#include "libcockatrice/protocol/pb/event_destroy_card.pb.h" -#include "libcockatrice/protocol/pb/event_draw_cards.pb.h" -#include "libcockatrice/protocol/pb/event_dump_zone.pb.h" -#include "libcockatrice/protocol/pb/event_flip_card.pb.h" -#include "libcockatrice/protocol/pb/event_game_closed.pb.h" -#include "libcockatrice/protocol/pb/event_game_host_changed.pb.h" -#include "libcockatrice/protocol/pb/event_game_log_notice.pb.h" -#include "libcockatrice/protocol/pb/event_game_say.pb.h" -#include "libcockatrice/protocol/pb/event_game_state_changed.pb.h" -#include "libcockatrice/protocol/pb/event_join.pb.h" -#include "libcockatrice/protocol/pb/event_kicked.pb.h" -#include "libcockatrice/protocol/pb/event_leave.pb.h" -#include "libcockatrice/protocol/pb/event_move_card.pb.h" -#include "libcockatrice/protocol/pb/event_player_properties_changed.pb.h" -#include "libcockatrice/protocol/pb/event_reveal_cards.pb.h" -#include "libcockatrice/protocol/pb/event_reverse_turn.pb.h" -#include "libcockatrice/protocol/pb/event_roll_die.pb.h" -#include "libcockatrice/protocol/pb/event_set_active_phase.pb.h" -#include "libcockatrice/protocol/pb/event_set_active_player.pb.h" -#include "libcockatrice/protocol/pb/event_set_card_attr.pb.h" -#include "libcockatrice/protocol/pb/event_set_card_counter.pb.h" -#include "libcockatrice/protocol/pb/event_set_counter.pb.h" -#include "libcockatrice/protocol/pb/event_shuffle.pb.h" -#include "libcockatrice/protocol/pb/game_event.pb.h" -#include "libcockatrice/protocol/pb/game_event_container.pb.h" -#include "libcockatrice/protocol/pb/game_event_context.pb.h" -#include "libcockatrice/protocol/pb/game_replay.pb.h" -#include "libcockatrice/protocol/pb/serverinfo_card.pb.h" -#include "libcockatrice/protocol/pb/serverinfo_counter.pb.h" -#include "libcockatrice/protocol/pb/serverinfo_player.pb.h" -#include "libcockatrice/protocol/pb/serverinfo_playerproperties.pb.h" -#include "libcockatrice/protocol/pb/serverinfo_zone.pb.h" -#include "libcockatrice/utility/zone_names.h" - -#include -#include -#include -#include -#include -#include - -namespace -{ -struct PendingDetail -{ - QString text; - QString badge; - ReplayEventKind kind = ReplayEventKind::Action; - QString actor; -}; - -QString formatTime(int timeMs) -{ - const int totalSeconds = qMax(0, timeMs) / 1000; - const int hours = totalSeconds / 3600; - const int minutes = (totalSeconds / 60) % 60; - const int seconds = totalSeconds % 60; - - if (hours > 0) { - return QStringLiteral("%1:%2:%3") - .arg(hours) - .arg(minutes, 2, 10, QLatin1Char('0')) - .arg(seconds, 2, 10, QLatin1Char('0')); - } - - return QStringLiteral("%1:%2").arg(minutes, 2, 10, QLatin1Char('0')).arg(seconds, 2, 10, QLatin1Char('0')); -} - -class ReplayEventIndexBuilder -{ - Q_DECLARE_TR_FUNCTIONS(ReplayEventIndex) - -public: - ReplayEventIndexBuilder(const GameReplay *replay, const QList &timeline) : replay(replay), timeline(timeline) - { - } - - QList build() - { - QList entries; - - for (int i = 0; i < replay->event_list_size(); ++i) { - const GameEventContainer &container = replay->event_list(i); - const QList details = describeContainer(container); - if (details.isEmpty()) { - continue; - } - - ReplayEventEntry entry; - entry.eventIndex = i; - entry.timeMs = timeline.value(i, static_cast(container.seconds_elapsed()) * 1000); - entry.timeLabel = formatTime(entry.timeMs); - entry.kind = details.first().kind; - entry.badge = details.first().badge; - if (details.size() > 1) { - entry.badge += QStringLiteral(" +%1").arg(details.size() - 1); - } - - const QString sharedActor = details.first().actor; - const bool mixedActors = std::any_of(details.cbegin(), details.cend(), [&sharedActor](const auto &detail) { - return detail.actor != sharedActor; - }); - - QStringList detailTexts; - for (const PendingDetail &detail : details) { - if (mixedActors && !detail.actor.isEmpty()) { - detailTexts.append(tr("%1: %2").arg(detail.actor, detail.text)); - } else { - detailTexts.append(detail.text); - } - } - entry.actor = mixedActors ? QString() : sharedActor; - entry.detail = detailTexts.join(QStringLiteral(" • ")); - - entries.append(entry); - } - - return entries; - } - -private: - const GameReplay *replay; - const QList &timeline; - - QHash playerNames; - QHash spectators; - QHash cardNames; - QHash counterNames; - - int activePlayerId = -1; - bool gameStarted = false; - bool sawGameState = false; - - static QString cardKey(int playerId, const QString &zone, int cardId) - { - return QStringLiteral("%1/%2/%3").arg(playerId).arg(zone).arg(cardId); - } - - static QString counterKey(int playerId, int counterId) - { - return QStringLiteral("%1/%2").arg(playerId).arg(counterId); - } - - QString playerName(int playerId) const - { - if (playerId < 0) { - return tr("Game"); - } - return playerNames.value(playerId, tr("Player %1").arg(playerId)); - } - - QString actorName(int playerId) const - { - if (playerId < 0) { - return QString(); - } - return playerNames.value(playerId, tr("Player %1").arg(playerId)); - } - - bool isSpectator(int playerId) const - { - return spectators.value(playerId, false); - } - - void rememberPlayer(const ServerInfo_PlayerProperties &properties) - { - const int playerId = properties.player_id(); - if (playerId < 0) { - return; - } - - const QString name = QString::fromStdString(properties.user_info().name()); - if (!name.isEmpty()) { - playerNames.insert(playerId, name); - } - spectators.insert(playerId, properties.spectator()); - } - - void rememberCard(int playerId, const QString &zone, int cardId, const QString &name) - { - if (cardId >= 0 && !name.isEmpty()) { - cardNames.insert(cardKey(playerId, zone, cardId), name); - } - } - - void forgetCard(int playerId, const QString &zone, int cardId) - { - if (cardId >= 0) { - cardNames.remove(cardKey(playerId, zone, cardId)); - } - } - - QString cardName(int playerId, const QString &zone, int cardId) const - { - if (cardId < 0) { - return QString(); - } - return cardNames.value(cardKey(playerId, zone, cardId)); - } - - void rememberCounter(int playerId, const ServerInfo_Counter &counter) - { - const QString name = QString::fromStdString(counter.name()); - if (!name.isEmpty()) { - counterNames.insert(counterKey(playerId, counter.id()), name); - } - } - - QString counterName(int playerId, int counterId) const - { - return counterNames.value(counterKey(playerId, counterId)); - } - - QString zoneName(const QString &zone) const - { - if (zone == ZoneNames::TABLE) { - return tr("battlefield"); - } - if (zone == ZoneNames::GRAVE) { - return tr("graveyard"); - } - if (zone == ZoneNames::EXILE) { - return tr("exile"); - } - if (zone == ZoneNames::HAND) { - return tr("hand"); - } - if (zone == ZoneNames::DECK) { - return tr("library"); - } - if (zone == ZoneNames::SIDEBOARD) { - return tr("sideboard"); - } - if (zone == ZoneNames::STACK) { - return tr("stack"); - } - return zone; - } - - QString zonePhrase(int playerId, const QString &zone, int actorId) const - { - if (zone == ZoneNames::TABLE) { - return tr("the battlefield"); - } - if (zone == ZoneNames::EXILE) { - return tr("exile"); - } - if (zone == ZoneNames::STACK) { - return tr("the stack"); - } - - const QString name = zoneName(zone); - if (playerId < 0 || playerId == actorId) { - return tr("their %1").arg(name); - } - return tr("%1's %2").arg(playerName(playerId), name); - } - - QString formatCardList(const QStringList &names, int total) const - { - // Keep repeats: each entry is a drawn/revealed card, so the list length must agree with the count. - QStringList listedNames; - for (const QString &name : names) { - if (!name.isEmpty()) { - listedNames.append(name); - } - } - - if (listedNames.isEmpty()) { - if (total <= 0) { - return tr("no cards"); - } - return total == 1 ? tr("1 card") : tr("%1 cards").arg(total); - } - - if (total < listedNames.size()) { - total = listedNames.size(); - } - - if (listedNames.size() > 3) { - return tr("%1 and %2 more").arg(listedNames.mid(0, 3).join(QStringLiteral(", "))).arg(total - 3); - } - if (total > listedNames.size()) { - return tr("%1 and %2 more").arg(listedNames.join(QStringLiteral(", "))).arg(total - listedNames.size()); - } - - return listedNames.join(QStringLiteral(", ")); - } - - void updateGameState(const Event_GameStateChanged &event) - { - for (int i = 0; i < event.player_list_size(); ++i) { - const ServerInfo_Player &player = event.player_list(i); - const int playerId = player.properties().player_id(); - rememberPlayer(player.properties()); - - for (int j = 0; j < player.zone_list_size(); ++j) { - const ServerInfo_Zone &zone = player.zone_list(j); - for (int k = 0; k < zone.card_list_size(); ++k) { - const ServerInfo_Card &card = zone.card_list(k); - rememberCard(playerId, QString::fromStdString(zone.name()), card.id(), - QString::fromStdString(card.name())); - } - } - - for (int j = 0; j < player.counter_list_size(); ++j) { - rememberCounter(playerId, player.counter_list(j)); - } - } - - if (event.has_active_player_id()) { - activePlayerId = event.active_player_id(); - } - if (event.has_game_started()) { - gameStarted = event.game_started(); - } - } - - QString describeMoveCard(const Event_MoveCard &event, int actorId, const GameEventContext &context) - { - const int startPlayerId = event.start_player_id() >= 0 ? event.start_player_id() : actorId; - const int targetPlayerId = event.target_player_id() >= 0 ? event.target_player_id() : startPlayerId; - const QString startZone = QString::fromStdString(event.start_zone()); - const QString targetZone = event.has_target_zone() ? QString::fromStdString(event.target_zone()) : startZone; - - QString name; - if (event.has_card_name()) { - name = QString::fromStdString(event.card_name()); - } - if (name.isEmpty()) { - name = cardName(startPlayerId, startZone, event.card_id()); - } - - const bool knownName = !name.isEmpty(); - if (name.isEmpty()) { - name = event.face_down() ? tr("a face-down card") : tr("a card"); - } - - const int newCardId = event.has_new_card_id() ? event.new_card_id() : event.card_id(); - forgetCard(startPlayerId, startZone, event.card_id()); - if (knownName) { - rememberCard(targetPlayerId, targetZone, newCardId, name); - } - - if (startZone == targetZone) { - return tr("moved %1 within %2.").arg(name, zonePhrase(targetPlayerId, targetZone, actorId)); - } - if (context.HasExtension(Context_UndoDraw::ext)) { - return tr("returned %1 to %2.").arg(name, zonePhrase(targetPlayerId, targetZone, actorId)); - } - - return tr("moved %1 from %2 to %3.") - .arg(name, zonePhrase(startPlayerId, startZone, actorId), zonePhrase(targetPlayerId, targetZone, actorId)); - } - - QString describeSetCardAttribute(const Event_SetCardAttr &event, int actorId) - { - const QString zone = QString::fromStdString(event.zone_name()); - QString name; - if (event.has_card_id()) { - name = cardName(actorId, zone, event.card_id()); - } - if (name.isEmpty()) { - name = event.has_card_id() ? tr("a card") : tr("all cards"); - } - - const QString value = QString::fromStdString(event.attr_value()); - switch (event.attribute()) { - case AttrTapped: - return value == QLatin1String("1") ? tr("tapped %1.").arg(name) : tr("untapped %1.").arg(name); - case AttrAttacking: - return value == QLatin1String("1") ? tr("%1 is now attacking.").arg(name) - : tr("%1 is no longer attacking.").arg(name); - case AttrFaceDown: - return value == QLatin1String("1") ? tr("turned %1 face down.").arg(name) - : tr("turned %1 face up.").arg(name); - case AttrColor: - return tr("changed %1's color to %2.").arg(name, value); - case AttrPT: - return tr("set %1's power/toughness to %2.").arg(name, value); - case AttrAnnotation: - return value.isEmpty() ? tr("cleared %1's annotation.").arg(name) - : tr("annotated %1: %2").arg(name, value); - case AttrDoesntUntap: - return value == QLatin1String("1") ? tr("%1 will no longer untap.").arg(name) - : tr("%1 will untap normally again.").arg(name); - } - - return tr("changed %1.").arg(name); - } - - QString badgeForCardAttribute(CardAttribute attribute) const - { - switch (attribute) { - case AttrTapped: - return tr("TAP"); - case AttrAttacking: - return tr("ATTACK"); - case AttrFaceDown: - return tr("FLIP"); - case AttrColor: - return tr("COLOR"); - case AttrPT: - return tr("PT"); - case AttrAnnotation: - return tr("NOTE"); - case AttrDoesntUntap: - return tr("TAP"); - } - return tr("CARD"); - } - - QString describeLeaveReason(Event_Leave::LeaveReason reason) const - { - switch (reason) { - case Event_Leave::USER_KICKED: - return tr("kicked by game host or moderator"); - case Event_Leave::USER_LEFT: - return tr("left the game"); - case Event_Leave::USER_DISCONNECTED: - return tr("disconnected"); - case Event_Leave::OTHER: - default: - return tr("reason unknown"); - } - } - - void addDetail(QList &details, - const QString &text, - const QString &badge, - ReplayEventKind kind, - int actorId, - bool forcedByJudge) const - { - if (text.isEmpty()) { - return; - } - - const QString finalText = forcedByJudge ? tr("%1 (judge)").arg(text) : text; - details.append(PendingDetail{.text = finalText, .badge = badge, .kind = kind, .actor = actorName(actorId)}); - } - - QList describeContainer(const GameEventContainer &container) - { - QList details; - const GameEventContext &context = container.context(); - const bool forcedByJudge = container.has_forced_by_judge(); - const auto contextType = static_cast(getPbExtension(context)); - - for (int i = 0; i < container.event_list_size(); ++i) { - const GameEvent &event = container.event_list(i); - const int eventPlayerId = event.player_id(); - const auto eventType = static_cast(getPbExtension(event)); - - switch (eventType) { - case GameEvent::GAME_STATE_CHANGED: { - const Event_GameStateChanged &state = event.GetExtension(Event_GameStateChanged::ext); - const bool wasStarted = gameStarted; - updateGameState(state); - if (state.game_started() && !wasStarted) { - addDetail(details, tr("Game started."), tr("GAME"), ReplayEventKind::System, -1, forcedByJudge); - } else if (!sawGameState) { - addDetail(details, tr("Game state loaded."), tr("GAME"), ReplayEventKind::System, -1, - forcedByJudge); - } - sawGameState = true; - break; - } - case GameEvent::PLAYER_PROPERTIES_CHANGED: { - const Event_PlayerPropertiesChanged &propertiesEvent = - event.GetExtension(Event_PlayerPropertiesChanged::ext); - const bool hasProperties = propertiesEvent.has_player_properties(); - const ServerInfo_PlayerProperties &properties = propertiesEvent.player_properties(); - // The server only fills in the field that changed, so player_id is absent from these events - // unless they are about a specific player; fall back to the event's own player id. - const bool describesPlayer = hasProperties && properties.has_player_id(); - const int playerId = describesPlayer ? properties.player_id() : eventPlayerId; - if (describesPlayer) { - rememberPlayer(properties); - } - - switch (contextType) { - case GameEventContext::READY_START: - addDetail(details, - properties.ready_start() ? tr("is ready to start.") : tr("is no longer ready."), - tr("READY"), ReplayEventKind::System, playerId, forcedByJudge); - break; - case GameEventContext::CONCEDE: - addDetail(details, tr("conceded the game."), tr("CONCEDE"), ReplayEventKind::System, - playerId, forcedByJudge); - break; - case GameEventContext::UNCONCEDE: - addDetail(details, tr("unconceded the game."), tr("CONCEDE"), ReplayEventKind::System, - playerId, forcedByJudge); - break; - case GameEventContext::DECK_SELECT: { - const Context_DeckSelect &deckSelect = context.GetExtension(Context_DeckSelect::ext); - const int sideboardSize = deckSelect.sideboard_size(); - addDetail(details, - sideboardSize >= 0 - ? tr("selected a deck with %1 sideboard cards.").arg(sideboardSize) - : tr("selected a deck."), - tr("DECK"), ReplayEventKind::System, playerId, forcedByJudge); - break; - } - case GameEventContext::MULLIGAN: { - const Context_Mulligan &mulligan = context.GetExtension(Context_Mulligan::ext); - addDetail(details, - mulligan.number() > 0 ? tr("took a mulligan (%1).").arg(mulligan.number()) - : tr("took a mulligan."), - tr("MULLIGAN"), ReplayEventKind::System, playerId, forcedByJudge); - break; - } - case GameEventContext::SET_SIDEBOARD_LOCK: - addDetail(details, - properties.sideboard_locked() ? tr("locked their sideboard.") - : tr("unlocked their sideboard."), - tr("SIDEBOARD"), ReplayEventKind::System, playerId, forcedByJudge); - break; - case GameEventContext::CONNECTION_STATE_CHANGED: - addDetail(details, - properties.ping_seconds() != -1 ? tr("reconnected to the game.") - : tr("lost connection to the game."), - tr("CONNECTION"), ReplayEventKind::System, playerId, forcedByJudge); - break; - default: - break; - } - break; - } - case GameEvent::JOIN: { - const Event_Join &join = event.GetExtension(Event_Join::ext); - if (join.has_player_properties()) { - rememberPlayer(join.player_properties()); - } - const int playerId = join.player_properties().player_id() >= 0 - ? join.player_properties().player_id() - : eventPlayerId; - addDetail(details, isSpectator(playerId) ? tr("joined as a spectator.") : tr("joined the game."), - tr("JOIN"), ReplayEventKind::System, playerId, forcedByJudge); - break; - } - case GameEvent::LEAVE: { - const Event_Leave &leave = event.GetExtension(Event_Leave::ext); - const QString reason = describeLeaveReason(leave.reason()); - if (eventPlayerId < 0) { - addDetail(details, tr("A player left the game (%1).").arg(reason), tr("LEAVE"), - ReplayEventKind::System, -1, forcedByJudge); - } else { - addDetail(details, - isSpectator(eventPlayerId) ? tr("stopped spectating (%1).").arg(reason) - : tr("left the game (%1).").arg(reason), - tr("LEAVE"), ReplayEventKind::System, eventPlayerId, forcedByJudge); - } - break; - } - case GameEvent::KICKED: - addDetail(details, - eventPlayerId >= 0 ? tr("was kicked from the game.") - : tr("A player was kicked from the game."), - tr("KICK"), ReplayEventKind::System, eventPlayerId, forcedByJudge); - break; - case GameEvent::GAME_CLOSED: - addDetail(details, tr("Game closed."), tr("GAME"), ReplayEventKind::System, -1, forcedByJudge); - break; - case GameEvent::GAME_HOST_CHANGED: - addDetail(details, eventPlayerId >= 0 ? tr("is now the game host.") : tr("The game host changed."), - tr("HOST"), ReplayEventKind::System, eventPlayerId, forcedByJudge); - break; - case GameEvent::GAME_SAY: { - const Event_GameSay &say = event.GetExtension(Event_GameSay::ext); - addDetail(details, QString::fromStdString(say.message()), tr("CHAT"), ReplayEventKind::Chat, - eventPlayerId, forcedByJudge); - break; - } - case GameEvent::SET_ACTIVE_PLAYER: { - const Event_SetActivePlayer &setActivePlayer = event.GetExtension(Event_SetActivePlayer::ext); - activePlayerId = setActivePlayer.active_player_id(); - if (activePlayerId >= 0) { - addDetail(details, tr("begins their turn."), tr("TURN"), ReplayEventKind::Turn, activePlayerId, - forcedByJudge); - } - break; - } - case GameEvent::SET_ACTIVE_PHASE: { - const Event_SetActivePhase &setActivePhase = event.GetExtension(Event_SetActivePhase::ext); - if (setActivePhase.phase() >= 0) { - const QString phaseName = Phases::getPhase(setActivePhase.phase()).getName(); - addDetail(details, tr("entered the %1 phase.").arg(phaseName), tr("PHASE"), - ReplayEventKind::Phase, activePlayerId >= 0 ? activePlayerId : eventPlayerId, - forcedByJudge); - } - break; - } - case GameEvent::REVERSE_TURN: { - const Event_ReverseTurn &reverseTurn = event.GetExtension(Event_ReverseTurn::ext); - addDetail(details, - reverseTurn.reversed() ? tr("reversed the turn order.") : tr("restored the turn order."), - tr("TURN"), ReplayEventKind::Turn, eventPlayerId, forcedByJudge); - break; - } - case GameEvent::MOVE_CARD: { - const Event_MoveCard &moveCard = event.GetExtension(Event_MoveCard::ext); - const QString text = describeMoveCard(moveCard, eventPlayerId, context); - const QString startZone = QString::fromStdString(moveCard.start_zone()); - const QString targetZone = QString::fromStdString(moveCard.target_zone()); - QString badge = tr("MOVE"); - if (context.HasExtension(Context_UndoDraw::ext)) { - badge = tr("UNDO"); - } else if (startZone == ZoneNames::STACK) { - badge = tr("RESOLVE"); - } else if (startZone == ZoneNames::HAND && targetZone == ZoneNames::TABLE) { - badge = tr("PLAY"); - } - addDetail(details, text, badge, ReplayEventKind::Action, eventPlayerId, forcedByJudge); - break; - } - case GameEvent::DRAW_CARDS: { - const Event_DrawCards &drawCards = event.GetExtension(Event_DrawCards::ext); - QStringList names; - for (int j = 0; j < drawCards.cards_size(); ++j) { - const ServerInfo_Card &card = drawCards.cards(j); - const QString name = QString::fromStdString(card.name()); - names.append(name); - rememberCard(eventPlayerId, ZoneNames::HAND, card.id(), name); - } - if (drawCards.number() > 0) { - addDetail(details, tr("drew %1.").arg(formatCardList(names, drawCards.number())), tr("DRAW"), - ReplayEventKind::Action, eventPlayerId, forcedByJudge); - } - break; - } - case GameEvent::REVEAL_CARDS: { - const Event_RevealCards &revealCards = event.GetExtension(Event_RevealCards::ext); - const QString zone = QString::fromStdString(revealCards.zone_name()); - QStringList names; - for (int j = 0; j < revealCards.cards_size(); ++j) { - const ServerInfo_Card &card = revealCards.cards(j); - const QString name = QString::fromStdString(card.name()); - names.append(name); - rememberCard(eventPlayerId, zone, card.id(), name); - } - const int total = - revealCards.number_of_cards() > 0 ? revealCards.number_of_cards() : revealCards.cards_size(); - QString text = - tr("revealed %1 from %2.") - .arg(formatCardList(names, total), zonePhrase(eventPlayerId, zone, eventPlayerId)); - if (revealCards.other_player_id() >= 0 && revealCards.other_player_id() != eventPlayerId) { - text = tr("revealed %1 from %2 to %3.") - .arg(formatCardList(names, total), zonePhrase(eventPlayerId, zone, eventPlayerId), - playerName(revealCards.other_player_id())); - } - addDetail(details, text, tr("REVEAL"), ReplayEventKind::Action, eventPlayerId, forcedByJudge); - break; - } - case GameEvent::SHUFFLE: { - const Event_Shuffle &shuffle = event.GetExtension(Event_Shuffle::ext); - addDetail( - details, - tr("shuffled %1.") - .arg(zonePhrase(eventPlayerId, QString::fromStdString(shuffle.zone_name()), eventPlayerId)), - tr("SHUFFLE"), ReplayEventKind::Action, eventPlayerId, forcedByJudge); - break; - } - case GameEvent::ROLL_DIE: { - const Event_RollDie &rollDie = event.GetExtension(Event_RollDie::ext); - QStringList rolls; - for (int j = 0; j < rollDie.values_size(); ++j) { - rolls.append(QString::number(rollDie.values(j))); - } - if (rolls.isEmpty() && rollDie.value() > 0) { - rolls.append(QString::number(rollDie.value())); - } - const QString sides = rollDie.sides() > 0 ? QString::number(rollDie.sides()) : QStringLiteral("?"); - addDetail(details, tr("rolled a d%1: %2.").arg(sides, rolls.join(QStringLiteral(", "))), tr("DICE"), - ReplayEventKind::Action, eventPlayerId, forcedByJudge); - break; - } - case GameEvent::FLIP_CARD: { - const Event_FlipCard &flipCard = event.GetExtension(Event_FlipCard::ext); - const QString zone = QString::fromStdString(flipCard.zone_name()); - if (flipCard.has_card_name()) { - rememberCard(eventPlayerId, zone, flipCard.card_id(), - QString::fromStdString(flipCard.card_name())); - } - QString name = flipCard.has_card_name() ? QString::fromStdString(flipCard.card_name()) - : cardName(eventPlayerId, zone, flipCard.card_id()); - if (name.isEmpty()) { - name = tr("a card"); - } - addDetail(details, - flipCard.face_down() ? tr("flipped %1 face down.").arg(name) - : tr("flipped %1 face up.").arg(name), - tr("FLIP"), ReplayEventKind::Action, eventPlayerId, forcedByJudge); - break; - } - case GameEvent::DESTROY_CARD: { - const Event_DestroyCard &destroyCard = event.GetExtension(Event_DestroyCard::ext); - const QString zone = QString::fromStdString(destroyCard.zone_name()); - const QString name = cardName(eventPlayerId, zone, destroyCard.card_id()); - forgetCard(eventPlayerId, zone, destroyCard.card_id()); - addDetail(details, tr("destroyed %1.").arg(name.isEmpty() ? tr("a card") : name), tr("DESTROY"), - ReplayEventKind::Action, eventPlayerId, forcedByJudge); - break; - } - case GameEvent::ATTACH_CARD: { - const Event_AttachCard &attachCard = event.GetExtension(Event_AttachCard::ext); - const QString startZone = QString::fromStdString(attachCard.start_zone()); - const QString source = cardName(eventPlayerId, startZone, attachCard.card_id()); - const QString sourceText = source.isEmpty() ? tr("a card") : source; - // An unattach event carries no target at all, so the target fields default to player 0 / no zone - // / card 0; only a present target_player_id means the card was attached to something. - if (!attachCard.has_target_player_id()) { - addDetail(details, tr("unattached %1.").arg(sourceText), tr("ATTACH"), ReplayEventKind::Action, - eventPlayerId, forcedByJudge); - break; - } - const QString targetZone = QString::fromStdString(attachCard.target_zone()); - const int targetPlayerId = - attachCard.target_player_id() >= 0 ? attachCard.target_player_id() : eventPlayerId; - const QString target = cardName(targetPlayerId, targetZone, attachCard.target_card_id()); - addDetail(details, - tr("attached %1 to %2.").arg(sourceText, target.isEmpty() ? tr("a card") : target), - tr("ATTACH"), ReplayEventKind::Action, eventPlayerId, forcedByJudge); - break; - } - case GameEvent::CREATE_TOKEN: { - const Event_CreateToken &createToken = event.GetExtension(Event_CreateToken::ext); - const QString name = QString::fromStdString(createToken.card_name()); - rememberCard(eventPlayerId, QString::fromStdString(createToken.zone_name()), createToken.card_id(), - name); - const QString pt = QString::fromStdString(createToken.pt()); - addDetail( - details, - tr("created a token: %1%2.") - .arg(name.isEmpty() ? tr("token") : name, pt.isEmpty() ? QString() : tr(" (%1)").arg(pt)), - tr("TOKEN"), ReplayEventKind::Action, eventPlayerId, forcedByJudge); - break; - } - case GameEvent::SET_CARD_ATTR: { - const Event_SetCardAttr &setCardAttr = event.GetExtension(Event_SetCardAttr::ext); - addDetail(details, describeSetCardAttribute(setCardAttr, eventPlayerId), - badgeForCardAttribute(setCardAttr.attribute()), ReplayEventKind::Action, eventPlayerId, - forcedByJudge); - break; - } - case GameEvent::SET_CARD_COUNTER: { - const Event_SetCardCounter &setCardCounter = event.GetExtension(Event_SetCardCounter::ext); - const QString name = cardName(eventPlayerId, QString::fromStdString(setCardCounter.zone_name()), - setCardCounter.card_id()); - addDetail( - details, - tr("set a counter on %1 to %2.") - .arg(name.isEmpty() ? tr("a card") : name, QString::number(setCardCounter.counter_value())), - tr("COUNTER"), ReplayEventKind::Action, eventPlayerId, forcedByJudge); - break; - } - case GameEvent::CREATE_COUNTER: { - const Event_CreateCounter &createCounter = event.GetExtension(Event_CreateCounter::ext); - const ServerInfo_Counter &counter = createCounter.counter_info(); - rememberCounter(eventPlayerId, counter); - const QString name = QString::fromStdString(counter.name()); - addDetail(details, - tr("created the %1 counter (%2).") - .arg(name.isEmpty() ? tr("new") : name, QString::number(counter.count())), - tr("COUNTER"), ReplayEventKind::Action, eventPlayerId, forcedByJudge); - break; - } - case GameEvent::SET_COUNTER: { - const Event_SetCounter &setCounter = event.GetExtension(Event_SetCounter::ext); - const QString name = counterName(eventPlayerId, setCounter.counter_id()); - addDetail(details, - tr("set %1 to %2.") - .arg(name.isEmpty() ? tr("a counter") : tr("the %1 counter").arg(name), - QString::number(setCounter.value())), - tr("COUNTER"), ReplayEventKind::Action, eventPlayerId, forcedByJudge); - break; - } - case GameEvent::DEL_COUNTER: { - const Event_DelCounter &delCounter = event.GetExtension(Event_DelCounter::ext); - const QString name = counterName(eventPlayerId, delCounter.counter_id()); - counterNames.remove(counterKey(eventPlayerId, delCounter.counter_id())); - addDetail(details, - tr("removed %1.").arg(name.isEmpty() ? tr("a counter") : tr("the %1 counter").arg(name)), - tr("COUNTER"), ReplayEventKind::Action, eventPlayerId, forcedByJudge); - break; - } - case GameEvent::DUMP_ZONE: { - const Event_DumpZone &dumpZone = event.GetExtension(Event_DumpZone::ext); - const QString zoneOwnerPhrase = zonePhrase( - dumpZone.zone_owner_id(), QString::fromStdString(dumpZone.zone_name()), eventPlayerId); - if (dumpZone.number_cards() < 0) { - addDetail(details, tr("looked at %1.").arg(zoneOwnerPhrase), tr("LOOK"), - ReplayEventKind::Action, eventPlayerId, forcedByJudge); - } else { - addDetail(details, - tr("looked at %1 from %2.") - .arg(formatCardList(QStringList(), dumpZone.number_cards()), zoneOwnerPhrase), - tr("LOOK"), ReplayEventKind::Action, eventPlayerId, forcedByJudge); - } - break; - } - case GameEvent::CHANGE_ZONE_PROPERTIES: { - const Event_ChangeZoneProperties &changeZone = event.GetExtension(Event_ChangeZoneProperties::ext); - const QString zone = QString::fromStdString(changeZone.zone_name()); - QStringList changes; - if (changeZone.always_reveal_top_card()) { - changes.append(tr("top card revealed to all")); - } - if (changeZone.always_look_at_top_card()) { - changes.append(tr("top card visible to its owner")); - } - addDetail( - details, - changes.isEmpty() - ? tr("changed the properties of %1.").arg(zonePhrase(eventPlayerId, zone, eventPlayerId)) - : tr("changed %1: %2.") - .arg(zonePhrase(eventPlayerId, zone, eventPlayerId), - changes.join(QStringLiteral(", "))), - tr("ZONE"), ReplayEventKind::Action, eventPlayerId, forcedByJudge); - break; - } - case GameEvent::GAME_LOG_NOTICE: { - const Event_GameLogNotice ¬ice = event.GetExtension(Event_GameLogNotice::ext); - switch (notice.notice_type()) { - case Event_GameLogNotice::UNDO_DRAW_FAILED: - addDetail(details, tr("failed to undo a draw."), tr("NOTICE"), ReplayEventKind::System, - eventPlayerId, forcedByJudge); - break; - default: - addDetail(details, tr("A game notice was logged."), tr("NOTICE"), ReplayEventKind::System, - -1, forcedByJudge); - break; - } - break; - } - case GameEvent::CREATE_ARROW: - case GameEvent::DELETE_ARROW: - // Arrows are transient visual annotations and would make the history very noisy. - break; - default: - addDetail(details, tr("Unrecognized game event."), tr("EVENT"), ReplayEventKind::Action, - eventPlayerId, forcedByJudge); - break; - } - } - - return details; - } -}; -} // namespace - -QList buildReplayEventEntries(const GameReplay *replay, const QList &timeline) -{ - if (!replay) { - return {}; - } - - return ReplayEventIndexBuilder(replay, timeline).build(); -} diff --git a/cockatrice/src/interface/widgets/replay/replay_event_index.h b/cockatrice/src/interface/widgets/replay/replay_event_index.h deleted file mode 100644 index 40f72a965..000000000 --- a/cockatrice/src/interface/widgets/replay/replay_event_index.h +++ /dev/null @@ -1,43 +0,0 @@ -#ifndef COCKATRICE_REPLAY_EVENT_INDEX_H -#define COCKATRICE_REPLAY_EVENT_INDEX_H - -#include -#include - -class GameReplay; - -enum class ReplayEventKind -{ - Action, - Turn, - Phase, - Chat, - System -}; - -/** - * @brief One user-visible entry in the replay history sidebar. - * - * The event index refers to the corresponding GameEventContainer in the replay. It is used to seek the replay to that - * container. - */ -struct ReplayEventEntry -{ - int eventIndex = -1; - int timeMs = 0; - QString timeLabel; - QString actor; - QString badge; - QString detail; - ReplayEventKind kind = ReplayEventKind::Action; -}; - -/** - * @brief Builds a compact, human-readable index of the events in a replay. - * - * The returned entries copy all the data they need, so the caller does not need to keep the replay object alive after - * this function returns. - */ -QList buildReplayEventEntries(const GameReplay *replay, const QList &timeline); - -#endif // COCKATRICE_REPLAY_EVENT_INDEX_H diff --git a/cockatrice/src/interface/widgets/replay/replay_event_sidebar.cpp b/cockatrice/src/interface/widgets/replay/replay_event_sidebar.cpp deleted file mode 100644 index b5fab2794..000000000 --- a/cockatrice/src/interface/widgets/replay/replay_event_sidebar.cpp +++ /dev/null @@ -1,284 +0,0 @@ -#include "replay_event_sidebar.h" - -#include "replay_event_index.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace -{ -enum ItemDataRole -{ - EventIndexRole = Qt::UserRole + 1, - TimeLabelRole, - KindRole, - ActorRole, - BadgeRole, - DetailRole -}; - -QColor accentForKind(ReplayEventKind kind, const QPalette &palette) -{ - switch (kind) { - case ReplayEventKind::Turn: - return QColor(0x4c, 0xaf, 0x50); - case ReplayEventKind::Phase: - return QColor(0x42, 0xa5, 0xf5); - case ReplayEventKind::Chat: - return QColor(0xff, 0xb3, 0x00); - case ReplayEventKind::System: - return palette.mid().color(); - case ReplayEventKind::Action: - default: - return palette.highlight().color(); - } -} - -class ReplayEventDelegate : public QStyledItemDelegate -{ -public: - explicit ReplayEventDelegate(QObject *parent) : QStyledItemDelegate(parent) - { - } - - void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override - { - painter->save(); - painter->setRenderHint(QPainter::Antialiasing, true); - - const QPalette &palette = option.palette; - const bool selected = option.state.testFlag(QStyle::State_Selected); - const bool hovered = option.state.testFlag(QStyle::State_MouseOver); - const auto kind = static_cast(index.data(KindRole).toInt()); - const QColor accent = accentForKind(kind, palette); - - const QRect card = option.rect.adjusted(4, 2, -4, -2); - QColor cardBackground = selected ? palette.highlight().color() : palette.base().color(); - if (!selected && hovered) { - cardBackground = palette.alternateBase().color(); - } - - painter->setPen(Qt::NoPen); - painter->setBrush(cardBackground); - painter->drawRoundedRect(card, 6, 6); - - QColor borderColor = selected ? palette.highlightedText().color() : palette.mid().color(); - borderColor.setAlpha(selected ? 100 : 130); - painter->setBrush(Qt::NoBrush); - painter->setPen(QPen(borderColor, 1)); - painter->drawRoundedRect(card.adjusted(0, 0, -1, -1), 6, 6); - - QColor accentColor = accent; - if (selected) { - accentColor = palette.highlightedText().color(); - accentColor.setAlpha(210); - } - painter->setPen(Qt::NoPen); - painter->setBrush(accentColor); - painter->drawRoundedRect(QRect(card.left() + 7, card.top() + 7, 3, card.height() - 14), 1.5, 1.5); - - const QColor textColor = selected ? palette.highlightedText().color() : palette.text().color(); - QColor mutedColor = textColor; - mutedColor.setAlpha(170); - - QFont timeFont = option.font; - timeFont.setBold(true); - timeFont.setPointSizeF(qMax(7.0, timeFont.pointSizeF() * 0.9)); - - QFont actorFont = option.font; - actorFont.setBold(true); - - QFont badgeFont = option.font; - badgeFont.setBold(true); - badgeFont.setPointSizeF(qMax(6.5, badgeFont.pointSizeF() * 0.72)); - - QFont detailFont = option.font; - detailFont.setPointSizeF(qMax(7.0, detailFont.pointSizeF() * 0.95)); - - const QFontMetrics timeMetrics(timeFont); - const QFontMetrics actorMetrics(actorFont); - const QFontMetrics badgeMetrics(badgeFont); - const QFontMetrics detailMetrics(detailFont); - - const int contentLeft = card.left() + 15; - const int contentRight = card.right() - 10; - const int topRowTop = card.top() + 6; - const int topRowHeight = qMax(timeMetrics.height(), badgeMetrics.height()) + 2; - - const QString timeText = index.data(TimeLabelRole).toString(); - const QString actorText = index.data(ActorRole).toString(); - const QString badgeText = index.data(BadgeRole).toString(); - const QString detailText = index.data(DetailRole).toString(); - - const int timeWidth = timeMetrics.horizontalAdvance(timeText); - painter->setFont(timeFont); - painter->setPen(mutedColor); - painter->drawText(QRect(contentLeft, topRowTop, timeWidth + 2, topRowHeight), Qt::AlignLeft | Qt::AlignVCenter, - timeText); - - int actorRight = contentRight; - if (!badgeText.isEmpty()) { - const int badgeWidth = badgeMetrics.horizontalAdvance(badgeText) + 10; - const int badgeHeight = badgeMetrics.height() + 2; - const QRect badgeRect(contentRight - badgeWidth, topRowTop + (topRowHeight - badgeHeight) / 2, badgeWidth, - badgeHeight); - - QColor badgeBackground = accentColor; - badgeBackground.setAlpha(selected ? 45 : 32); - painter->setPen(Qt::NoPen); - painter->setBrush(badgeBackground); - painter->drawRoundedRect(badgeRect, 3, 3); - - painter->setFont(badgeFont); - painter->setPen(accentColor); - painter->drawText(badgeRect, Qt::AlignCenter, badgeText); - - actorRight = badgeRect.left() - 8; - } - - const int actorLeft = contentLeft + timeWidth + 10; - if (actorRight > actorLeft) { - const QString elidedActor = - actorMetrics.elidedText(actorText, Qt::ElideRight, qMax(0, actorRight - actorLeft)); - painter->setFont(actorFont); - painter->setPen(textColor); - painter->drawText(QRect(actorLeft, topRowTop, actorRight - actorLeft, topRowHeight), - Qt::AlignLeft | Qt::AlignVCenter, elidedActor); - } - - const int detailTop = topRowTop + topRowHeight + 2; - const int detailHeight = qMax(detailMetrics.height(), card.bottom() - detailTop); - const QString elidedDetail = - detailMetrics.elidedText(detailText, Qt::ElideRight, qMax(0, contentRight - contentLeft)); - painter->setFont(detailFont); - painter->setPen(textColor); - painter->drawText(QRect(contentLeft, detailTop, contentRight - contentLeft, detailHeight), - Qt::AlignLeft | Qt::AlignVCenter, elidedDetail); - - painter->restore(); - } - - QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex & /*index*/) const override - { - const QFontMetrics metrics(option.font); - return {0, qMax(58, metrics.height() * 3 + 10)}; - } -}; -} // namespace - -ReplayEventSidebar::ReplayEventSidebar(QWidget *parent, const GameReplay *replay, const QList &timeline) - : QWidget(parent), eventList(new QListWidget(this)), emptyLabel(new QLabel(this)) -{ - setObjectName("replayEventSidebar"); - - eventList->setObjectName("replayEventList"); - eventList->setItemDelegate(new ReplayEventDelegate(eventList)); - eventList->setFrameShape(QFrame::NoFrame); - eventList->setFocusPolicy(Qt::NoFocus); - eventList->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - eventList->setSelectionMode(QAbstractItemView::SingleSelection); - eventList->setMouseTracking(true); - eventList->setUniformItemSizes(true); - eventList->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel); - - emptyLabel->setAlignment(Qt::AlignCenter); - emptyLabel->setWordWrap(true); - emptyLabel->setMargin(16); - emptyLabel->setForegroundRole(QPalette::PlaceholderText); - - auto *layout = QtUtils::makeLayout(this); - layout->addWidget(eventList); - layout->addWidget(emptyLabel); - - const QList entries = buildReplayEventEntries(replay, timeline); - eventIndices.reserve(entries.size()); - - for (const ReplayEventEntry &entry : entries) { - auto *item = new QListWidgetItem(eventList); - item->setData(EventIndexRole, entry.eventIndex); - item->setData(TimeLabelRole, entry.timeLabel); - item->setData(KindRole, static_cast(entry.kind)); - item->setData(ActorRole, entry.actor); - item->setData(BadgeRole, entry.badge); - item->setData(DetailRole, entry.detail); - const QString tooltip = entry.actor.isEmpty() - ? tr("%1\n%2").arg(entry.timeLabel, entry.detail) - : tr("%1 · %2\n%3").arg(entry.timeLabel, entry.actor, entry.detail); - item->setToolTip(tooltip); - item->setData(Qt::AccessibleTextRole, entry.actor.isEmpty() - ? tr("%1: %2").arg(entry.timeLabel, entry.detail) - : tr("%1, %2: %3").arg(entry.timeLabel, entry.actor, entry.detail)); - eventIndices.append(entry.eventIndex); - } - - const bool hasEvents = !entries.isEmpty(); - eventList->setVisible(hasEvents); - emptyLabel->setVisible(!hasEvents); - emptyLabel->setText(tr("No game events in this replay.")); - - connect(eventList, &QListWidget::itemClicked, this, [this](QListWidgetItem *item) { - if (!item) { - return; - } - - setCurrentRow(eventList->row(item)); - emit eventSelected(item->data(EventIndexRole).toInt()); - }); - - if (hasEvents) { - setCurrentRow(-1); - } -} - -void ReplayEventSidebar::setCurrentEvent(int eventIndex) -{ - if (eventIndices.isEmpty()) { - setCurrentRow(-1); - return; - } - - // The rows hold a subset of the replay's events, in playback order, so the last row at or before eventIndex is the - // one the game state has reached. An eventIndex of -1 (nothing replayed yet) selects no row. - const auto it = std::upper_bound(eventIndices.cbegin(), eventIndices.cend(), eventIndex); - const int row = static_cast(it - eventIndices.cbegin()) - 1; - setCurrentRow(row); -} - -void ReplayEventSidebar::setCurrentRow(int row) -{ - if (row < 0 || row >= eventList->count()) { - row = -1; - } - - if (row >= 0) { - // The view's current index also moves on mouse press and can be deselected by a ctrl-click, - // so the cached row alone cannot tell whether the view is already in the desired state. - const QListWidgetItem *item = eventList->item(row); - if (row == eventList->currentRow() && item && item->isSelected()) { - return; - } - } else if (row == currentRow) { - return; - } - - currentRow = row; - eventList->setCurrentRow(currentRow); - - if (currentRow >= 0) { - QListWidgetItem *item = eventList->item(currentRow); - if (!eventList->viewport()->rect().contains(eventList->visualItemRect(item))) { - eventList->scrollToItem(item, QAbstractItemView::PositionAtCenter); - } - } -} diff --git a/cockatrice/src/interface/widgets/replay/replay_event_sidebar.h b/cockatrice/src/interface/widgets/replay/replay_event_sidebar.h deleted file mode 100644 index ee163356e..000000000 --- a/cockatrice/src/interface/widgets/replay/replay_event_sidebar.h +++ /dev/null @@ -1,50 +0,0 @@ -#ifndef COCKATRICE_REPLAY_EVENT_SIDEBAR_H -#define COCKATRICE_REPLAY_EVENT_SIDEBAR_H - -#include -#include -#include - -class GameReplay; -class QLabel; -class QListWidget; - -/** - * @brief A scrollable list of replay events that can be used to seek the replay. - * - * The sidebar copies the event data it needs at construction time. It does not keep a reference to the replay or to - * the replay manager. - */ -class ReplayEventSidebar : public QWidget -{ - Q_OBJECT - -public: - /** - * @param parent The parent widget - * @param replay The replay to build the event list from - * @param timeline Event index to time in milliseconds, as used by ReplayManager - */ - ReplayEventSidebar(QWidget *parent, const GameReplay *replay, const QList &timeline); - -public slots: - /** - * @brief Highlights the row of the event the game state has reached. - * - * @param eventIndex Index into the replay's event list, or -1 when nothing has been replayed yet - */ - void setCurrentEvent(int eventIndex); - -signals: - void eventSelected(int eventIndex); - -private: - void setCurrentRow(int row); - - QListWidget *eventList; - QLabel *emptyLabel; - QList eventIndices; ///< Replay event index of each row, ascending - int currentRow = -1; -}; - -#endif // COCKATRICE_REPLAY_EVENT_SIDEBAR_H diff --git a/cockatrice/src/interface/widgets/replay/replay_manager.cpp b/cockatrice/src/interface/widgets/replay/replay_manager.cpp index b01d7f9d6..8ba0cb653 100644 --- a/cockatrice/src/interface/widgets/replay/replay_manager.cpp +++ b/cockatrice/src/interface/widgets/replay/replay_manager.cpp @@ -57,7 +57,7 @@ ReplayManager::ReplayManager(QObject *parent, GameReplay *replay) rewindBufferingTimer = new QTimer(this); rewindBufferingTimer->setSingleShot(true); - connect(rewindBufferingTimer, &QTimer::timeout, this, [this] { processRewind(); }); + connect(rewindBufferingTimer, &QTimer::timeout, this, &ReplayManager::processRewind); } ReplayManager::~ReplayManager() @@ -65,7 +65,7 @@ ReplayManager::~ReplayManager() delete replay; } -void ReplayManager::skipToTime(int newTime, bool doRewindBuffering, bool includeEventsAtTime) +void ReplayManager::skipToTime(int newTime, bool doRewindBuffering) { // check boundary conditions if (newTime < 0) { @@ -75,17 +75,15 @@ void ReplayManager::skipToTime(int newTime, bool doRewindBuffering, bool include newTime = maxTime; } - if (!includeEventsAtTime) { - newTime -= newTime % TIMER_INTERVAL_MS; // Time should always be a multiple of the interval - } + newTime -= newTime % TIMER_INTERVAL_MS; // Time should always be a multiple of the interval const bool isBackwardsSkip = newTime < currentProcessedTime; currentVisualTime = newTime; if (isBackwardsSkip) { - handleBackwardsSkip(doRewindBuffering, includeEventsAtTime); + handleBackwardsSkip(doRewindBuffering); } else { - processNewEvents(FORWARD_SKIP, includeEventsAtTime); + processNewEvents(FORWARD_SKIP); } timeChanged(currentVisualTime); @@ -97,7 +95,7 @@ void ReplayManager::skipToTime(int newTime, bool doRewindBuffering, bool include * @param doRewindBuffering When true, if multiple backward skips are made in quick succession, only a single rewind * is processed at the end. When false, the backwards skip will always cause an immediate rewind. */ -void ReplayManager::handleBackwardsSkip(bool doRewindBuffering, bool includeEventsAtTime) +void ReplayManager::handleBackwardsSkip(bool doRewindBuffering) { if (doRewindBuffering) { // We use a one-shot timer to implement the rewind buffering. @@ -107,11 +105,11 @@ void ReplayManager::handleBackwardsSkip(bool doRewindBuffering, bool includeEven rewindBufferingTimer->start(SettingsCache::instance().userInterface().getRewindBufferingMs()); } else { // otherwise, process the rewind immediately - processRewind(includeEventsAtTime); + processRewind(); } } -void ReplayManager::processRewind(bool includeEventsAtTime) +void ReplayManager::processRewind() { // stop any queued-up rewinds rewindBufferingTimer->stop(); @@ -119,7 +117,7 @@ void ReplayManager::processRewind(bool includeEventsAtTime) // process the rewind currentEvent = 0; emit rewound(); - processNewEvents(BACKWARD_SKIP, includeEventsAtTime); + processNewEvents(BACKWARD_SKIP); } void ReplayManager::replayTimerTimeout() @@ -136,13 +134,11 @@ void ReplayManager::replayTimerTimeout() } /** @brief Processes all unprocessed events up to the current time. */ -void ReplayManager::processNewEvents(PlaybackMode playbackMode, bool includeEventsAtTime) +void ReplayManager::processNewEvents(PlaybackMode playbackMode) { currentProcessedTime = currentVisualTime; - while (currentEvent < replayTimeline.size() && - (replayTimeline[currentEvent] < currentProcessedTime || - (includeEventsAtTime && replayTimeline[currentEvent] == currentProcessedTime))) { + while (currentEvent < replayTimeline.size() && replayTimeline[currentEvent] < currentProcessedTime) { EventProcessingOptions options; // backwards skip => always skip reveal windows @@ -240,24 +236,6 @@ void ReplayManager::setTime(int time) skipToTime(time, false); } -void ReplayManager::setTimeToEvent(int eventIndex) -{ - if (eventIndex < 0 || eventIndex >= replayTimeline.size()) { - return; - } - - // Flush a pending buffered rewind before seeking, otherwise its timer fires after this seek and rebuilds the - // replay state from an outdated currentVisualTime, dropping the event we seek to. Stopping the timer instead - // of processing it is not enough: playback ticks advance currentProcessedTime without rewinding currentEvent, - // so the board could still be ahead of the target. - if (rewindBufferingTimer->isActive()) { - processRewind(); - } - - // Unlike a timeline seek, jumping to an event should apply that event immediately. - skipToTime(replayTimeline.at(eventIndex), false, true); -} - void ReplayManager::skipByAmount(int amount) { skipToTime(currentVisualTime + amount, amount < 0); diff --git a/cockatrice/src/interface/widgets/replay/replay_manager.h b/cockatrice/src/interface/widgets/replay/replay_manager.h index e3745778e..3849b86ae 100644 --- a/cockatrice/src/interface/widgets/replay/replay_manager.h +++ b/cockatrice/src/interface/widgets/replay/replay_manager.h @@ -40,10 +40,10 @@ class ReplayManager : public QObject ///< to rewind buffering int currentEvent = 0; ///< current event's index - void skipToTime(int newTime, bool doRewindBuffering, bool includeEventsAtTime = false); - void handleBackwardsSkip(bool doRewindBuffering, bool includeEventsAtTime = false); - void processRewind(bool includeEventsAtTime = false); - void processNewEvents(PlaybackMode playbackMode, bool includeEventsAtTime = false); + void skipToTime(int newTime, bool doRewindBuffering); + void handleBackwardsSkip(bool doRewindBuffering); + void processRewind(); + void processNewEvents(PlaybackMode playbackMode); void handleSkipEmptySection(); private slots: @@ -66,16 +66,6 @@ public: return replayTimeline; } - /** - * @brief Index of the last event the game state has processed. - * - * @return The event index, or -1 while no event has been replayed yet - */ - [[nodiscard]] int getLastProcessedEventIndex() const - { - return currentEvent - 1; - } - void setTimeScaleFactor(qreal _timeScaleFactor); void setSkipEmptySections(bool value); @@ -83,7 +73,6 @@ public slots: void startReplay(); void stopReplay(); void setTime(int time); - void setTimeToEvent(int eventIndex); void skipByAmount(int amount); // use a negative amount to skip backwards signals: diff --git a/cockatrice/src/interface/widgets/replay/replay_widget.cpp b/cockatrice/src/interface/widgets/replay/replay_widget.cpp index e911bc0f9..07b4e72ce 100644 --- a/cockatrice/src/interface/widgets/replay/replay_widget.cpp +++ b/cockatrice/src/interface/widgets/replay/replay_widget.cpp @@ -19,11 +19,6 @@ class GameReplay; -int ReplayWidget::getLastProcessedEventIndex() const -{ - return replayManager->getLastProcessedEventIndex(); -} - ReplayWidget::ReplayWidget(QWidget *parent, GameReplay *replay) : QWidget(parent), replayPlayButton(nullptr), replayFastForwardButton(nullptr), aReplaySkipForward(nullptr), aReplaySkipBackward(nullptr), aReplaySkipForwardBig(nullptr), aReplaySkipBackwardBig(nullptr) @@ -33,7 +28,6 @@ ReplayWidget::ReplayWidget(QWidget *parent, GameReplay *replay) connect(replayManager, &ReplayManager::eventReplayed, this, &ReplayWidget::eventReplayed); connect(replayManager, &ReplayManager::replayFinished, this, &ReplayWidget::replayFinished); connect(replayManager, &ReplayManager::rewound, this, &ReplayWidget::rewound); - connect(replayManager, &ReplayManager::timeChanged, this, &ReplayWidget::timeChanged); // timeline widget timelineWidget = new ReplayTimelineWidget; @@ -102,16 +96,6 @@ ReplayWidget::ReplayWidget(QWidget *parent, GameReplay *replay) refreshShortcuts(); } -const QList &ReplayWidget::getReplayTimeline() const -{ - return replayManager->getReplayTimeline(); -} - -void ReplayWidget::seekToEvent(int eventIndex) -{ - replayManager->setTimeToEvent(eventIndex); -} - void ReplayWidget::replayFinished() { replayPlayButton->setChecked(false); diff --git a/cockatrice/src/interface/widgets/replay/replay_widget.h b/cockatrice/src/interface/widgets/replay/replay_widget.h index cbb676288..0e78442ce 100644 --- a/cockatrice/src/interface/widgets/replay/replay_widget.h +++ b/cockatrice/src/interface/widgets/replay/replay_widget.h @@ -8,7 +8,6 @@ #ifndef REPLAY_WIDGET_H #define REPLAY_WIDGET_H -#include #include #include #include @@ -36,19 +35,8 @@ public: */ ReplayWidget(QWidget *parent, GameReplay *replay); - [[nodiscard]] const QList &getReplayTimeline() const; - - /** - * @brief Index of the last replay event the game state has processed, or -1 before the first event. - */ - [[nodiscard]] int getLastProcessedEventIndex() const; - -public slots: - void seekToEvent(int eventIndex); - signals: void rewound(); - void timeChanged(int time); void eventReplayed(const GameEventContainer &cont, EventProcessingOptions options); private: diff --git a/cockatrice/src/interface/widgets/server/chat_view/chat_view.cpp b/cockatrice/src/interface/widgets/server/chat_view/chat_view.cpp index c57f65352..037a4a1e5 100644 --- a/cockatrice/src/interface/widgets/server/chat_view/chat_view.cpp +++ b/cockatrice/src/interface/widgets/server/chat_view/chat_view.cpp @@ -4,7 +4,6 @@ #include "../../client/sound_engine.h" #include "../../interface/card_localization.h" #include "../../interface/pixel_map_generator.h" -#include "../../interface/theme_manager.h" #include "../../tabs/tab_supervisor.h" #include "../user/user_context_menu.h" #include "../user/user_list_manager.h" @@ -64,9 +63,6 @@ ChatView::ChatView(TabSupervisor *_tabSupervisor, AbstractGame *_game, bool _sho connect(&SettingsCache::instance(), &SettingsCache::themeChanged, this, &ChatView::adjustColorsToPalette); - applyChatFont(); - connect(themeManager, &ThemeManager::fontsChanged, this, &ChatView::applyChatFont); - userContextMenu = new UserContextMenu(tabSupervisor, this, game); connect(userContextMenu, SIGNAL(openMessageDialog(QString, bool)), this, SIGNAL(openMessageDialog(QString, bool))); @@ -112,16 +108,6 @@ void ChatView::adjustColorsToPalette() QTimer::singleShot(0, this, &ChatView::refreshBlockColors); } -void ChatView::applyChatFont() -{ - // Family comes from the ChatLog theme role; the message char formats carry - // no family of their own, so they (and new content) follow the widget and - // document default fonts. Weights/italics stay caller-side as before. - const QFont chatFont = themeManager->appFont(AppFont::ChatLog); - setFont(chatFont); - document()->setDefaultFont(chatFont); -} - void ChatView::refreshBlockColors() { QTextDocument *doc = document(); diff --git a/cockatrice/src/interface/widgets/server/chat_view/chat_view.h b/cockatrice/src/interface/widgets/server/chat_view/chat_view.h index a2e2f33dd..3fa7224e5 100644 --- a/cockatrice/src/interface/widgets/server/chat_view/chat_view.h +++ b/cockatrice/src/interface/widgets/server/chat_view/chat_view.h @@ -111,7 +111,6 @@ private slots: void openLink(const QUrl &link); void actMessageClicked(); void adjustColorsToPalette(); - void applyChatFont(); void refreshBlockColors(); void onScrollBarRangeChanged(); void onScrollBarValueChanged(int value); diff --git a/cockatrice/src/interface/widgets/server/game_selector.cpp b/cockatrice/src/interface/widgets/server/game_selector.cpp index 468095adb..2702a527d 100644 --- a/cockatrice/src/interface/widgets/server/game_selector.cpp +++ b/cockatrice/src/interface/widgets/server/game_selector.cpp @@ -1,7 +1,7 @@ #include "game_selector.h" #include "../../../client/settings/cache_settings.h" -#include "../../theme_icon.h" +#include "../../pixel_map_generator.h" #include "../interface/widgets/dialogs/dlg_create_game.h" #include "../interface/widgets/dialogs/dlg_filter_games.h" #include "../interface/widgets/tabs/tab_room.h" @@ -130,10 +130,10 @@ GameSelector::GameSelector(AbstractClient *_client, } filterButton = new QPushButton; - filterButton->setIcon(loadThemedIcon(QStringLiteral("icons/search"))); + filterButton->setIcon(themePixmap(QStringLiteral("icons/search"))); connect(filterButton, &QPushButton::clicked, this, &GameSelector::actSetFilter); clearFilterButton = new QPushButton; - clearFilterButton->setIcon(loadThemedIcon(QStringLiteral("icons/clearsearch"))); + clearFilterButton->setIcon(themePixmap(QStringLiteral("icons/clearsearch"))); bool filtersSetToDefault = showFilters && gameListProxyModel->areFilterParametersSetToDefaults(); clearFilterButton->setEnabled(!filtersSetToDefault); connect(clearFilterButton, &QPushButton::clicked, this, &GameSelector::actClearFilter); @@ -266,13 +266,9 @@ void GameSelector::actCreate() return; } - // Heap-allocate the dialog: if this selector (or its room tab) is destroyed while the - // dialog is open, e.g. on server disconnect, Qt deletes its children. A stack-allocated - // child would then be deleted by Qt and crash. - auto *dlg = new DlgCreateGame(room, room->getGameTypes(), this); - dlg->setAttribute(Qt::WA_DeleteOnClose); - connect(dlg, &QDialog::finished, this, &GameSelector::updateTitle); - dlg->open(); + DlgCreateGame dlg(room, room->getGameTypes(), this); + dlg.exec(); + updateTitle(); } void GameSelector::checkResponse(const Response &response) @@ -366,9 +362,8 @@ void GameSelector::customContextMenu(const QPoint &point) const ServerInfo_Game &gameInfo = gameListModel->getGame(index.data(Qt::UserRole).toInt()); const QMap &gameTypes = gameListModel->getGameTypes().value(gameInfo.room_id()); - auto *dlg = new DlgCreateGame(gameInfo, gameTypes, this); - dlg->setAttribute(Qt::WA_DeleteOnClose); - dlg->open(); + DlgCreateGame dlg(gameInfo, gameTypes, this); + dlg.exec(); }); QAction copyLink(tr("Cop&y game link")); diff --git a/cockatrice/src/interface/widgets/server/games_model.cpp b/cockatrice/src/interface/widgets/server/games_model.cpp index d33526bd1..315d51d9e 100644 --- a/cockatrice/src/interface/widgets/server/games_model.cpp +++ b/cockatrice/src/interface/widgets/server/games_model.cpp @@ -1,7 +1,6 @@ #include "games_model.h" #include "../../../client/settings/cache_settings.h" -#include "../../theme_icon.h" #include "../interface/pixel_map_generator.h" #include "libcockatrice/protocol/pb/serverinfo_user.pb.h" #include "user/user_list_proxy.h" @@ -86,8 +85,7 @@ const QString GamesModel::getGameCreatedString(const int secs) } GamesModel::GamesModel(const QMap &_rooms, const QMap &_gameTypes, QObject *parent) - : QAbstractTableModel(parent), rooms(_rooms), gameTypes(_gameTypes), - lockIcon(loadThemedIcon(QStringLiteral("icons/lock"))) + : QAbstractTableModel(parent), rooms(_rooms), gameTypes(_gameTypes) { } @@ -187,15 +185,10 @@ QVariant GamesModel::data(const QModelIndex &index, int role) const if (gameentry.share_decklists_on_load()) { result.append(tr("open decklists")); } - if (gameentry.shuffle_players()) { - result.append(tr("random turn order")); - } return result.join(", "); } case Qt::DecorationRole: { - // One engine per model: it is palette- and theme-aware and - // invalidates itself, so a repaint must not build a new one. - return gameentry.with_password() ? lockIcon : QVariant(); + return gameentry.with_password() ? QIcon(LockPixmapGenerator::generatePixmap(13)) : QVariant(); case Qt::TextAlignmentRole: return Qt::AlignLeft; default: diff --git a/cockatrice/src/interface/widgets/server/games_model.h b/cockatrice/src/interface/widgets/server/games_model.h index 62862417d..9bf54b83a 100644 --- a/cockatrice/src/interface/widgets/server/games_model.h +++ b/cockatrice/src/interface/widgets/server/games_model.h @@ -5,7 +5,6 @@ #include "game_type_map.h" #include -#include #include #include #include @@ -33,7 +32,6 @@ private: QList gameList; /**< List of games currently displayed. */ QMap rooms; /**< Map of room IDs to room names. */ QMap gameTypes; /**< Map of room IDs to available game types. */ - QIcon lockIcon; /**< Themed lock for password-protected games. */ static const int NUM_COLS = 8; /**< Number of columns in the table. */ diff --git a/cockatrice/src/interface/widgets/server/remote/remote_replay_list_tree_widget.cpp b/cockatrice/src/interface/widgets/server/remote/remote_replay_list_tree_widget.cpp index c1db6138d..c131c3cbf 100644 --- a/cockatrice/src/interface/widgets/server/remote/remote_replay_list_tree_widget.cpp +++ b/cockatrice/src/interface/widgets/server/remote/remote_replay_list_tree_widget.cpp @@ -1,6 +1,6 @@ #include "remote_replay_list_tree_widget.h" -#include "../../../theme_icon.h" +#include "../../../pixel_map_generator.h" #include "libcockatrice/protocol/pb/response.pb.h" #include "libcockatrice/protocol/pb/serverinfo_replay_match.pb.h" @@ -50,7 +50,7 @@ RemoteReplayList_TreeModel::RemoteReplayList_TreeModel(AbstractClient *_client, QFileIconProvider fip; dirIcon = fip.icon(QFileIconProvider::Folder); fileIcon = fip.icon(QFileIconProvider::File); - lockIcon = loadThemedIcon(QStringLiteral("icons/lock")); + lockIcon = themePixmap(QStringLiteral("icons/lock")); } RemoteReplayList_TreeModel::~RemoteReplayList_TreeModel() diff --git a/cockatrice/src/interface/widgets/server/user/user_card_art_provider.cpp b/cockatrice/src/interface/widgets/server/user/user_card_art_provider.cpp index 6e8fd771a..d84bd5804 100644 --- a/cockatrice/src/interface/widgets/server/user/user_card_art_provider.cpp +++ b/cockatrice/src/interface/widgets/server/user/user_card_art_provider.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -65,6 +66,27 @@ void UserCardArtProvider::requestCardArt(const QString &userName, const QString processQueue(); } +QPixmap UserCardArtProvider::cropCardArt(const QPixmap &fullRes, const ExactCard &card) +{ + QPixmap source = fullRes; + + // Sideways-layout cards (plane, siege/battle, split) store their landscape + // artwork rotated 90° inside a portrait frame. Rotate it upright first so + // the crop below lands on the horizontal art, mirroring the way + // CardInfoPictureWidget displays these cards. + const bool landscape = card.getInfo().getUiAttributes().landscapeOrientation; + source = CardArtUtils::rotateSidewaysLayoutArt(source, card); + + const QSize sz = source.size(); + const int marginX = sz.width() * 0.07; + const int topMargin = landscape ? sz.height() * 0.05 : sz.height() * 0.11; + const int bottomMargin = landscape ? sz.height() * 0.42 : sz.height() * 0.45; + + const QRect artRect(marginX, topMargin, sz.width() - 2 * marginX, sz.height() - topMargin - bottomMargin); + + return source.copy(artRect.intersected(source.rect())); +} + void UserCardArtProvider::insertIntoCache(const QString &key, const QPixmap &pixmap) { if (pixmap.isNull()) { @@ -112,7 +134,7 @@ void UserCardArtProvider::processQueue() // Synchronous hit (already loaded/on disk) if (!fullRes.isNull()) { - insertIntoCache(key, CardArtUtils::cropCardArt(fullRes, card)); + insertIntoCache(key, cropCardArt(fullRes, card)); pending.remove(key); emit cardArtUpdated(userName); @@ -136,7 +158,7 @@ void UserCardArtProvider::processQueue() CardPictureLoader::getPixmap(fullRes, card, QSize(745, 1040)); if (!fullRes.isNull()) { - self->insertIntoCache(key, CardArtUtils::cropCardArt(fullRes, card)); + self->insertIntoCache(key, self->cropCardArt(fullRes, card)); } self->pending.remove(key); @@ -150,4 +172,4 @@ void UserCardArtProvider::processQueue() // Stop here. We'll continue when the async load finishes. return; } -} +} \ No newline at end of file diff --git a/cockatrice/src/interface/widgets/server/user/user_card_art_provider.h b/cockatrice/src/interface/widgets/server/user/user_card_art_provider.h index a7844ca88..ce9225b00 100644 --- a/cockatrice/src/interface/widgets/server/user/user_card_art_provider.h +++ b/cockatrice/src/interface/widgets/server/user/user_card_art_provider.h @@ -10,6 +10,8 @@ #include #include +class ExactCard; + class UserCardArtProvider : public QObject { Q_OBJECT @@ -19,6 +21,7 @@ public: void requestCardArt(const QString &userName, const QString &cardName, const QString &providerId); const QMap &cache() const; + static QPixmap cropCardArt(const QPixmap &fullRes, const ExactCard &card); signals: void cardArtUpdated(const QString &userName); diff --git a/cockatrice/src/interface/widgets/server/user/user_card_settings_dialog.cpp b/cockatrice/src/interface/widgets/server/user/user_card_settings_dialog.cpp index 67fb57e78..f8b57cfc2 100644 --- a/cockatrice/src/interface/widgets/server/user/user_card_settings_dialog.cpp +++ b/cockatrice/src/interface/widgets/server/user/user_card_settings_dialog.cpp @@ -2,7 +2,6 @@ #include "../../../card_picture_loader/card_picture_loader.h" #include "../../cards/art_crop_attribution.h" -#include "../../cards/card_art_utils.h" #include "../../utility/completer_utils.h" #include "card_database_display_model.h" #include "card_database_model.h" @@ -11,6 +10,7 @@ #include "libcockatrice/card/printing/exact_card.h" #include "libcockatrice/card/printing/printing_info.h" #include "libcockatrice/card/set/card_set.h" +#include "user_card_art_provider.h" #include "user_list_painter.h" #include @@ -580,7 +580,7 @@ void UserCardArtSettingsDialog::reloadPreview() return; } - currentPixmap = CardArtUtils::cropCardArt(fullRes, card); + currentPixmap = UserCardArtProvider::cropCardArt(fullRes, card); preview->setPixmap(currentPixmap); preview->setParams(currentParams); diff --git a/cockatrice/src/interface/widgets/server/user/user_games_model.cpp b/cockatrice/src/interface/widgets/server/user/user_games_model.cpp deleted file mode 100644 index b7c728c45..000000000 --- a/cockatrice/src/interface/widgets/server/user/user_games_model.cpp +++ /dev/null @@ -1,149 +0,0 @@ -#include "user_games_model.h" - -#include -#include -#include -#include -#include -#include -#include - -UserGamesModel::UserGamesModel(AbstractClient *_client, int requestTimeoutMs, QObject *parent) - : QAbstractListModel(parent), client(_client) -{ - timeout.setSingleShot(true); - timeout.setInterval(requestTimeoutMs); - connect(&timeout, &QTimer::timeout, this, [this] { - invalidateRequest(); - setState(State::Error); - }); - connected = client->getStatus() == StatusLoggedIn; - connect(client, &AbstractClient::statusChanged, this, [this](ClientStatus status) { - // AbstractClient::setStatus emits this signal while holding the (non-recursive) - // status mutex, which getStatus() would try to lock again. Track the emitted - // value here, and read this cache from canRefresh() for the same reason. - connected = status == StatusLoggedIn; - if (!connected) { - invalidateRequest(); - replaceGames({}); - setState(online ? State::Disconnected : State::Offline); - } else if (online) { - refresh(); - } - }); - connect(client, &QObject::destroyed, this, [this] { - client = nullptr; - connected = false; - invalidateRequest(); - replaceGames({}); - setState(online ? State::Disconnected : State::Offline); - }); -} - -int UserGamesModel::rowCount(const QModelIndex &parent) const -{ - return parent.isValid() ? 0 : games.size(); -} - -QVariant UserGamesModel::data(const QModelIndex &index, int role) const -{ - if (!index.isValid() || index.model() != this || index.column() != 0 || index.row() < 0 || - index.row() >= games.size() || role != GameRole) { - return {}; - } - return QVariant::fromValue(games.at(index.row())); -} - -void UserGamesModel::setUser(const QString &name, bool isOnline) -{ - userName = name; - online = isOnline; - refresh(); -} - -UserGamesModel::State UserGamesModel::getState() const -{ - return state; -} - -bool UserGamesModel::canRefresh() const -{ - return online && !userName.isEmpty() && client && connected && state != State::Loading; -} - -void UserGamesModel::invalidateRequest() -{ - ++requestGeneration; - // The client owns pending commands. Detach our interest without deleting - // an object that may already have moved to the client's thread. - disconnect(responseConnection); - responseConnection = {}; - timeout.stop(); -} - -void UserGamesModel::replaceGames(QList replacement) -{ - beginResetModel(); - games = std::move(replacement); - endResetModel(); -} - -void UserGamesModel::setState(State newState) -{ - state = newState; - emit stateChanged(); -} - -void UserGamesModel::refresh() -{ - invalidateRequest(); - const quint64 request = requestGeneration; - replaceGames({}); - if (!online || userName.isEmpty()) { - setState(State::Offline); - return; - } - if (!client || !connected) { - setState(State::Disconnected); - return; - } - - setState(State::Loading); - // State listeners may select another user or disconnect the client. - if (request != requestGeneration) { - return; - } - - Command_GetGamesOfUser command; - command.set_user_name(userName.toStdString()); - PendingCommand *pending = client->prepareSessionCommand(command); - responseConnection = connect(pending, &PendingCommand::finished, this, - [this, request](const Response &response) { finishRequest(response, request); }); - timeout.start(); - client->sendCommand(pending); -} - -void UserGamesModel::finishRequest(const Response &response, quint64 request) -{ - if (request != requestGeneration) { - return; - } - invalidateRequest(); - const quint64 completion = requestGeneration; - if (response.response_code() != Response::RespOk || !response.HasExtension(Response_GetGamesOfUser::ext)) { - setState(State::Error); - return; - } - - const auto &result = response.GetExtension(Response_GetGamesOfUser::ext); - QList replacement; - replacement.reserve(result.game_list_size()); - for (const auto &game : result.game_list()) { - replacement.append(game); - } - replaceGames(std::move(replacement)); - if (completion != requestGeneration) { - return; - } - setState(games.isEmpty() ? State::Empty : State::Ready); -} diff --git a/cockatrice/src/interface/widgets/server/user/user_games_model.h b/cockatrice/src/interface/widgets/server/user/user_games_model.h deleted file mode 100644 index b8ad71a67..000000000 --- a/cockatrice/src/interface/widgets/server/user/user_games_model.h +++ /dev/null @@ -1,63 +0,0 @@ -#ifndef COCKATRICE_USER_GAMES_MODEL_H -#define COCKATRICE_USER_GAMES_MODEL_H - -#include -#include -#include -#include -#include -#include - -class AbstractClient; -class Response; - -/** Current user's games and the lifetime of the request that supplies them. */ -class UserGamesModel : public QAbstractListModel -{ - Q_OBJECT - -public: - enum class State - { - Offline, - Disconnected, - Loading, - Ready, - Empty, - Error - }; - static constexpr int DefaultRequestTimeoutMs = 15000; - static constexpr int GameRole = Qt::UserRole + 10; - - explicit UserGamesModel(AbstractClient *client, - int requestTimeoutMs = DefaultRequestTimeoutMs, - QObject *parent = nullptr); - int rowCount(const QModelIndex &parent = {}) const override; - QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; - - void setUser(const QString &name, bool isOnline); - void refresh(); - State getState() const; - bool canRefresh() const; - -signals: - void stateChanged(); - -private: - void invalidateRequest(); - void replaceGames(QList replacement); - void finishRequest(const Response &response, quint64 request); - void setState(State newState); - - QPointer client; - QString userName; - bool online = false; - bool connected = false; - State state = State::Offline; - QList games; - quint64 requestGeneration = 0; - QMetaObject::Connection responseConnection; - QTimer timeout; -}; - -#endif // COCKATRICE_USER_GAMES_MODEL_H diff --git a/cockatrice/src/interface/widgets/server/user/user_info_box.cpp b/cockatrice/src/interface/widgets/server/user/user_info_box.cpp index 997001c50..fa7698037 100644 --- a/cockatrice/src/interface/widgets/server/user/user_info_box.cpp +++ b/cockatrice/src/interface/widgets/server/user/user_info_box.cpp @@ -1,7 +1,6 @@ #include "user_info_box.h" #include "../../interface/pixel_map_generator.h" -#include "../../interface/theme_manager.h" #include "../../interface/widgets/dialogs/dlg_edit_avatar.h" #include "../../interface/widgets/dialogs/dlg_edit_password.h" #include "../../interface/widgets/dialogs/dlg_edit_user.h" @@ -40,8 +39,10 @@ UserInfoBox::UserInfoBox(AbstractClient *_client, bool _editable, QWidget *parent, Qt::WindowFlags flags) : QWidget(parent, flags), client(_client), editable(_editable) { - applyInterfaceDefaultFont(); - connect(themeManager, &ThemeManager::fontsChanged, this, &UserInfoBox::applyInterfaceDefaultFont); + QFont nameFont = nameLabel.font(); + nameFont.setBold(true); + nameFont.setPointSizeF(nameFont.pointSizeF() * 1.5); + nameLabel.setFont(nameFont); auto *userIconAndNameLayout = new QHBoxLayout; userIconAndNameLayout->addWidget(&userLevelIcon, 0, Qt::AlignCenter); @@ -90,16 +91,6 @@ UserInfoBox::UserInfoBox(AbstractClient *_client, bool _editable, QWidget *paren retranslateUi(); } -void UserInfoBox::applyInterfaceDefaultFont() -{ - // Family comes from the InterfaceDefault theme role; the *1.5 size - // multiplier and bold weight stay caller-side as before. - QFont nameFont = themeManager->appFont(AppFont::InterfaceDefault); - nameFont.setBold(true); - nameFont.setPointSizeF(nameFont.pointSizeF() * 1.5); - nameLabel.setFont(nameFont); -} - void UserInfoBox::retranslateUi() { realNameLabel1.setText(tr("Real Name:")); diff --git a/cockatrice/src/interface/widgets/server/user/user_info_box.h b/cockatrice/src/interface/widgets/server/user/user_info_box.h index 98919b027..6bc571a37 100644 --- a/cockatrice/src/interface/widgets/server/user/user_info_box.h +++ b/cockatrice/src/interface/widgets/server/user/user_info_box.h @@ -48,7 +48,6 @@ private slots: void processEditResponse(const Response &r); void processPasswordResponse(const Response &r); void processAvatarResponse(const Response &r); - void applyInterfaceDefaultFont(); void actEdit(); void actEditInternal(const Response &r); diff --git a/cockatrice/src/interface/widgets/server/user/user_info_popup.cpp b/cockatrice/src/interface/widgets/server/user/user_info_popup.cpp index 4b4887bc4..efb3b2e31 100644 --- a/cockatrice/src/interface/widgets/server/user/user_info_popup.cpp +++ b/cockatrice/src/interface/widgets/server/user/user_info_popup.cpp @@ -6,9 +6,10 @@ #include "../../interface/widgets/tabs/tab_supervisor.h" #include "libcockatrice/card/database/card_database_querier.h" #include "libcockatrice/card/printing/exact_card.h" +#include "libcockatrice/protocol/pb/response.pb.h" #include "libcockatrice/protocol/pb/serverinfo_game.pb.h" #include "libcockatrice/protocol/pb/serverinfo_user.pb.h" -#include "user_games_model.h" +#include "libcockatrice/protocol/pb/session_commands.pb.h" #include "user_level.h" #include "user_list_painter.h" @@ -33,7 +34,6 @@ #include #include #include -#include #include #include #include @@ -41,6 +41,8 @@ #include #include #include +#include +#include #include #include #include @@ -50,6 +52,8 @@ #include #include #include +#include +#include #include #include #include @@ -138,8 +142,7 @@ public: p->drawEllipse(QRectF(rect.left() + 9, rect.top() + (rect.height() - 8) / 2.0, 8, 8)); // Game title (bold, elided) - // Family comes from the InterfaceDefault theme role; bold stays caller-side. - QFont tf = themeManager->appFont(AppFont::InterfaceDefault); + QFont tf = option.font; tf.setBold(true); p->setFont(tf); p->setPen(pal.color(QPalette::Text)); @@ -151,7 +154,7 @@ public: // Player count const bool full = game.player_count() >= game.max_players(); - p->setFont(themeManager->appFont(AppFont::InterfaceDefault)); + p->setFont(option.font); p->setPen(full ? QColor(249, 115, 22) : pal.color(QPalette::Disabled, QPalette::Text)); p->drawText(QRect(rect.right() - countW - 4, rect.top(), countW, rect.height()), Qt::AlignVCenter | Qt::AlignRight, @@ -320,8 +323,7 @@ void UserInfoHeaderWidget::paintEvent(QPaintEvent *) const int tx = ax + AvatarSize + AvatarToTextGap; const int tw = rect.width() - tx - 8; - // Family comes from the InterfaceDefault theme role; the *1.12 size and bold stay caller-side. - QFont nf = themeManager->appFont(AppFont::InterfaceDefault); + QFont nf = font(); nf.setBold(true); nf.setPointSizeF(nf.pointSizeF() * 1.12); p.setFont(nf); @@ -350,7 +352,7 @@ void UserInfoHeaderWidget::paintEvent(QPaintEvent *) } if (!badge.text.isEmpty()) { - QFont bf = themeManager->appFont(AppFont::InterfaceDefault); + QFont bf = font(); bf.setPointSizeF(bf.pointSizeF() * 0.70); bf.setBold(true); p.setFont(bf); @@ -367,8 +369,7 @@ void UserInfoHeaderWidget::paintEvent(QPaintEvent *) // The painter font at this point depends on whether a badge was drawn // (badge font vs username font), so pin an explicit font for the pill. - // Family follows the InterfaceDefault theme role like the rest of the popup chrome. - p.setFont(themeManager->appFont(AppFont::InterfaceDefault)); + p.setFont(font()); // Only show the attribution when there is actually art on screen: on a // cache miss cardArt is null and the pill would float over the plain @@ -397,7 +398,6 @@ UserInfoPopup::UserInfoPopup(TabSupervisor *_ts, // Restyle the popup chrome when the theme or its color scheme changes. if (themeManager) { connect(themeManager, &ThemeManager::themeChanged, this, &UserInfoPopup::applyTheme); - connect(themeManager, &ThemeManager::fontsChanged, this, &UserInfoPopup::applyTheme); } } @@ -427,7 +427,6 @@ void UserInfoPopup::buildUi() refreshBtn = new QPushButton(QStringLiteral("↻"), this); refreshBtn->setFixedSize(20, 20); refreshBtn->setFlat(true); - refreshBtn->setToolTip(tr("Refresh games")); connect(refreshBtn, &QPushButton::clicked, this, &UserInfoPopup::refreshGames); gh->addWidget(refreshBtn); root->addLayout(gh); @@ -438,8 +437,7 @@ void UserInfoPopup::buildUi() root->addWidget(gamesStatus); // Games list - gamesModel = new UserGamesModel(client, UserGamesModel::DefaultRequestTimeoutMs, this); - connect(gamesModel, &UserGamesModel::stateChanged, this, &UserInfoPopup::updateGamesDisplay); + gamesModel = new QStandardItemModel(this); gamesView = new QListView(this); gamesView->setModel(gamesModel); gamesView->setItemDelegate(new PopupGameDelegate(gamesView)); @@ -477,24 +475,15 @@ void UserInfoPopup::applyTheme() separator->setStyleSheet(QStringLiteral("color:%1; margin: 0 8px;").arg(colorStr(theme.border))); - gamesLabel->setStyleSheet(QStringLiteral("color:%1; background:transparent;").arg(colorStr(theme.subText))); - QFont gamesLabelFont = themeManager->appFont(AppFont::InterfaceDefault); - gamesLabelFont.setPixelSize(11); - gamesLabelFont.setWeight(QFont::Bold); - gamesLabel->setFont(gamesLabelFont); + gamesLabel->setStyleSheet(QStringLiteral("color:%1; font-size:11px; font-weight:bold; background:transparent;") + .arg(colorStr(theme.subText))); - refreshBtn->setStyleSheet(QStringLiteral("QPushButton{color:%1;border:none;background:transparent;}" + refreshBtn->setStyleSheet(QStringLiteral("QPushButton{color:%1;border:none;font-size:14px;background:transparent;}" "QPushButton:hover{color:%2;}") .arg(colorStr(theme.subText), colorStr(theme.text))); - QFont refreshFont = themeManager->appFont(AppFont::InterfaceDefault); - refreshFont.setPixelSize(14); - refreshBtn->setFont(refreshFont); - gamesStatus->setStyleSheet( - QStringLiteral("color:%1; padding:10px; background:transparent;").arg(colorStr(theme.statusText))); - QFont statusFont = themeManager->appFont(AppFont::InterfaceDefault); - statusFont.setPixelSize(11); - gamesStatus->setFont(statusFont); + gamesStatus->setStyleSheet(QStringLiteral("color:%1; font-size:11px; padding:10px; background:transparent;") + .arg(colorStr(theme.statusText))); gamesView->setStyleSheet(QStringLiteral("QListView{background:%1;border:none;}" "QListView::item:selected{background:%2;}") @@ -502,12 +491,9 @@ void UserInfoPopup::applyTheme() closeBtn->setStyleSheet( QStringLiteral("QPushButton{background:%1;color:%2;" - "border:none;border-radius:11px;}" + "border:none;border-radius:11px;font-size:10px;}" "QPushButton:hover{color:%3;background:%4;}") .arg(colorStr(theme.closeBg), colorStr(theme.subText), colorStr(theme.text), colorStr(theme.closeHover))); - QFont closeFont = themeManager->appFont(AppFont::InterfaceDefault); - closeFont.setPixelSize(10); - closeBtn->setFont(closeFont); header->update(); } @@ -522,17 +508,13 @@ static QPushButton *makeBtn(const QString &label, const QString &tip, QWidget *p b->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); b->setStyleSheet(QStringLiteral("QPushButton{" " background:%1;color:%2;border:1px solid %3;" - " border-radius:4px;padding:0 4px;" + " border-radius:4px;font-size:11px;padding:0 4px;" "}" "QPushButton:hover{background:%4;color:%5;}" "QPushButton:pressed{background:%6;}" "QPushButton:disabled{color:%7;border-color:%3;}") .arg(colorStr(t.buttonBg), colorStr(t.text), colorStr(t.buttonBorder), colorStr(t.buttonHover), colorStr(t.text), colorStr(t.buttonPressed), colorStr(t.buttonDisabled))); - // Family comes from the InterfaceDefault theme role; the 11px size stays caller-side. - QFont btnFont = themeManager->appFont(AppFont::InterfaceDefault); - btnFont.setPixelSize(11); - b->setFont(btnFont); return b; } @@ -663,21 +645,13 @@ void UserInfoPopup::rebuildActionButtons(const ServerInfo_User &userInfo, bool o void UserInfoPopup::updateActionButtons(const ServerInfo_User &userInfo, bool online, bool isBuddy, bool isIgnored) { - currentUserInfo = userInfo; - currentBuddy = isBuddy; - currentIgnored = isIgnored; - if (currentOnline != online) { - setOnline(online); - } else { - refreshHeader(); - rebuildActionButtons(userInfo, online, isBuddy, isIgnored); - adjustSize(); - } + rebuildActionButtons(userInfo, online, isBuddy, isIgnored); + adjustSize(); } void UserInfoPopup::onGamesContextMenu(const QPoint &pos) { - const QPersistentModelIndex idx = gamesView->indexAt(pos); + const QModelIndex idx = gamesView->indexAt(pos); if (!idx.isValid()) { return; } @@ -705,7 +679,7 @@ void UserInfoPopup::onGamesContextMenu(const QPoint &pos) } const QAction *chosen = menu.exec(gamesView->viewport()->mapToGlobal(pos)); - if (!chosen || !idx.isValid()) { + if (!chosen) { return; } @@ -742,8 +716,6 @@ void UserInfoPopup::showForUser(const QString &userName, currentUser = userName; currentUserInfo = userInfo; currentOnline = online; - currentBuddy = isBuddy; - currentIgnored = isIgnored; // Header refreshHeader(); @@ -751,71 +723,88 @@ void UserInfoPopup::showForUser(const QString &userName, // Actions rebuildActionButtons(userInfo, online, isBuddy, isIgnored); + // Games list reset + gamesModel->clear(); + gamesView->hide(); + gamesStatus->setText(tr("Loading games…")); + gamesStatus->show(); + // Close button, top right corner, above everything closeBtn->move(PopupWidth - closeBtn->width() - 6, 6); closeBtn->raise(); - gamesModel->setUser(currentUser, currentOnline); adjustSize(); + fetchGames(); } // ── Games fetch ─────────────────────────────────────────────────────────────── void UserInfoPopup::fetchGames() { - gamesModel->refresh(); -} - -void UserInfoPopup::setOnline(bool online) -{ - if (currentOnline == online) { + if (!client || currentUser.isEmpty()) { return; } - currentOnline = online; - refreshHeader(); - rebuildActionButtons(currentUserInfo, online, currentBuddy, currentIgnored); - gamesModel->setUser(currentUser, online); + + Command_GetGamesOfUser cmd; + cmd.set_user_name(currentUser.toStdString()); + + const QString snapshot = currentUser; + PendingCommand *pend = client->prepareSessionCommand(cmd); + connect(pend, &PendingCommand::finished, this, + [this, snapshot](const Response &r) { onGamesReceived(r, snapshot); }); + client->sendCommand(pend); } -void UserInfoPopup::updateGamesDisplay() +void UserInfoPopup::onGamesReceived(const Response &r, const QString &forUser) { - refreshBtn->setEnabled(gamesModel->canRefresh()); - const auto state = gamesModel->getState(); - const bool ready = state == UserGamesModel::State::Ready; - gamesView->setVisible(ready); - gamesStatus->setVisible(!ready); - - switch (state) { - case UserGamesModel::State::Offline: - gamesStatus->setText(tr("User is offline.")); - break; - case UserGamesModel::State::Disconnected: - gamesStatus->setText(tr("Not connected to the server.")); - break; - case UserGamesModel::State::Loading: - gamesStatus->setText(tr("Loading games…")); - break; - case UserGamesModel::State::Empty: - gamesStatus->setText(tr("No active games.")); - break; - case UserGamesModel::State::Error: - gamesStatus->setText(tr("Could not load games.")); - break; - case UserGamesModel::State::Ready: { - // Fit exactly to the number of visible rows, scroll when more than 5. - constexpr int rowH = 38; // must match PopupGameDelegate::sizeHint - constexpr int maxRows = 5; - const int count = gamesModel->rowCount(); - gamesView->setFixedHeight(qMin(count, maxRows) * rowH + 2); - gamesView->setVerticalScrollBarPolicy(count > maxRows ? Qt::ScrollBarAlwaysOn : Qt::ScrollBarAlwaysOff); - break; - } + if (forUser != currentUser) { + return; // stale response, different user showing now } + + gamesModel->clear(); + + if (r.response_code() != Response::RespOk) { + gamesStatus->setText(tr("Could not load games.")); + gamesStatus->show(); + gamesView->hide(); + return; + } + + const auto &resp = r.GetExtension(Response_GetGamesOfUser::ext); + if (resp.game_list_size() == 0) { + gamesStatus->setText(tr("No active games.")); + gamesStatus->show(); + gamesView->hide(); + return; + } + + for (int i = 0; i < resp.game_list_size(); ++i) { + auto *item = new QStandardItem; + item->setData(QVariant::fromValue(resp.game_list(i)), PopupRoles::GameData); + item->setEditable(false); + gamesModel->appendRow(item); + } + + gamesStatus->hide(); + gamesView->show(); + + // Fit exactly to the number of visible rows, scroll when more than 5 + constexpr int rowH = 38; // must match PopupGameDelegate::sizeHint + constexpr int maxRows = 5; + const int count = gamesModel->rowCount(); + const int visible = qMin(count, maxRows); + gamesView->setFixedHeight(visible * rowH + 2); + gamesView->setVerticalScrollBarPolicy(count > maxRows ? Qt::ScrollBarAlwaysOn : Qt::ScrollBarAlwaysOff); + adjustSize(); } void UserInfoPopup::refreshGames() { + gamesModel->clear(); + gamesView->hide(); + gamesStatus->setText(tr("Loading games…")); + gamesStatus->show(); fetchGames(); } diff --git a/cockatrice/src/interface/widgets/server/user/user_info_popup.h b/cockatrice/src/interface/widgets/server/user/user_info_popup.h index 1af4ad0e1..f2a91d025 100644 --- a/cockatrice/src/interface/widgets/server/user/user_info_popup.h +++ b/cockatrice/src/interface/widgets/server/user/user_info_popup.h @@ -1,7 +1,6 @@ #ifndef COCKATRICE_USER_INFO_POPUP_H #define COCKATRICE_USER_INFO_POPUP_H -#include "user_games_model.h" #include "user_list_painter.h" #include @@ -22,13 +21,15 @@ class QPushButton; class TabSupervisor; class QListView; class QPoint; +class QStandardItemModel; +class Response; template class QMap; // ── Roles ───────────────────────────────────────────────────────────────────── namespace PopupRoles { -constexpr int GameData = UserGamesModel::GameRole; +constexpr int GameData = Qt::UserRole + 10; } // Popup theme @@ -142,7 +143,6 @@ public: void showForUser(const QString &userName, const ServerInfo_User &userInfo, bool online, bool isBuddy, bool isIgnored); void fetchGames(); - void setOnline(bool online); [[nodiscard]] QString getCurrentUser() const { @@ -199,7 +199,7 @@ protected: private slots: void refreshGames(); - void updateGamesDisplay(); + void onGamesReceived(const Response &r, const QString &forUser); void onGamesContextMenu(const QPoint &pos); private: @@ -218,8 +218,6 @@ private: QString currentUser; ServerInfo_User currentUserInfo; bool currentOnline = false; - bool currentBuddy = false; - bool currentIgnored = false; std::function gameInviteAvailable; UserInfoHeaderWidget *header; @@ -227,7 +225,7 @@ private: QLabel *gamesLabel; QFrame *separator; QListView *gamesView; - UserGamesModel *gamesModel; + QStandardItemModel *gamesModel; QLabel *gamesStatus; QPushButton *closeBtn; QPushButton *refreshBtn; diff --git a/cockatrice/src/interface/widgets/server/user/user_list_painter.cpp b/cockatrice/src/interface/widgets/server/user/user_list_painter.cpp index acb91351b..08eaaaff7 100644 --- a/cockatrice/src/interface/widgets/server/user/user_list_painter.cpp +++ b/cockatrice/src/interface/widgets/server/user/user_list_painter.cpp @@ -1,7 +1,6 @@ #include "user_list_painter.h" #include "../../interface/pixel_map_generator.h" -#include "../../interface/theme_manager.h" #include "libcockatrice/protocol/pb/serverinfo_user.pb.h" #include @@ -293,9 +292,7 @@ void UserListPainter::drawUserName(QPainter *painter, bool online, const Style &style) { - Q_UNUSED(option); - // Family comes from the InterfaceDefault theme role; bold stays caller-side. - QFont nameFont = themeManager->appFont(AppFont::InterfaceDefault); + QFont nameFont = option.font; nameFont.setBold(true); painter->setFont(nameFont); @@ -354,9 +351,7 @@ void UserListPainter::drawBadges(QPainter *painter, return; } - Q_UNUSED(option); - // Family comes from the InterfaceDefault theme role; the *0.68 size and bold stay caller-side. - QFont badgeFont = themeManager->appFont(AppFont::InterfaceDefault); + QFont badgeFont = option.font; badgeFont.setPointSizeF(badgeFont.pointSizeF() * 0.68); badgeFont.setBold(true); painter->setFont(badgeFont); diff --git a/cockatrice/src/interface/widgets/server/user/user_list_widget.cpp b/cockatrice/src/interface/widgets/server/user/user_list_widget.cpp index 943b4d5de..a20b39b82 100644 --- a/cockatrice/src/interface/widgets/server/user/user_list_widget.cpp +++ b/cockatrice/src/interface/widgets/server/user/user_list_widget.cpp @@ -519,19 +519,6 @@ UserListWidget::UserListWidget(TabSupervisor *_tabSupervisor, userTree->viewport()->update(); userTree->update(); }); - // Divider headers store their font on the item, so re-apply the role - // family here; the painter reads the role live and needs only the - // repaint below. - connect(themeManager, &ThemeManager::fontsChanged, this, [this] { - QFont dividerFont = themeManager->appFont(AppFont::InterfaceDefault); - dividerFont.setBold(true); - for (auto it = sectionItems.constBegin(); it != sectionItems.constEnd(); ++it) { - it.value()->setFont(0, dividerFont); - it.value()->setSizeHint(0, QSize(0, QFontMetrics(dividerFont).height() + 16)); - } - userTree->viewport()->update(); - userTree->update(); - }); } QVBoxLayout *vbox = new QVBoxLayout; @@ -630,14 +617,7 @@ void UserListWidget::bind(UserListManager *mgr) connect(manager, &UserListManager::addedToIgnoreList, this, refreshCurrentPopup); connect(manager, &UserListManager::removedFromIgnoreList, this, refreshIfPopupOpen); connect(manager, &UserListManager::userJoinedOnline, this, refreshCurrentPopup); - connect(manager, &UserListManager::userLeftOnline, this, [this, refreshIfPopupOpen](const QString &name) { - // The online row may already have been removed; the pinned popup must - // still invalidate its games request and display the offline state. - if (userInfoPopup && userInfoPopup->getCurrentUser() == name) { - userInfoPopup->setOnline(false); - } - refreshIfPopupOpen(name); - }); + connect(manager, &UserListManager::userLeftOnline, this, refreshIfPopupOpen); rebuild(); } @@ -1512,7 +1492,7 @@ QTreeWidgetItem *UserListWidget::createSectionItem(Section section) // They act as collapsible section headers once they have focus. divider->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable); - QFont font = themeManager->appFont(AppFont::InterfaceDefault); + QFont font = userTree->font(); font.setBold(true); divider->setFont(0, font); // A little taller than a plain text row so the header reads as a section diff --git a/cockatrice/src/interface/widgets/settings_page/deck_editor_settings_page.cpp b/cockatrice/src/interface/widgets/settings_page/deck_editor_settings_page.cpp index e90603858..31178ba4c 100644 --- a/cockatrice/src/interface/widgets/settings_page/deck_editor_settings_page.cpp +++ b/cockatrice/src/interface/widgets/settings_page/deck_editor_settings_page.cpp @@ -1,14 +1,13 @@ #include "deck_editor_settings_page.h" #include "../../../client/settings/cache_settings.h" -#include "../../theme_icon.h" +#include "../../pixel_map_generator.h" #include "../dialogs/path_safety_warning.h" #include "update/card_spoiler/spoiler_background_updater.h" #include #include #include -#include #include #include #include @@ -62,7 +61,7 @@ DeckEditorSettingsPage::DeckEditorSettingsPage() connect(mpSpoilerPathButton, &QPushButton::clicked, this, &DeckEditorSettingsPage::spoilerPathButtonClicked); updateNowButton = new QPushButton; - updateNowButton->setMinimumWidth(150); + updateNowButton->setFixedWidth(150); connect(updateNowButton, &QPushButton::clicked, this, &DeckEditorSettingsPage::updateSpoilers); // Update the GUI depending on if the box is ticked or not @@ -80,19 +79,19 @@ DeckEditorSettingsPage::DeckEditorSettingsPage() } aAdd = new QAction(this); - aAdd->setIcon(loadThemedIcon(QStringLiteral("icons/increment"))); + aAdd->setIcon(themePixmap(QStringLiteral("icons/increment"))); connect(aAdd, &QAction::triggered, this, &DeckEditorSettingsPage::actAddURL); aEdit = new QAction(this); - aEdit->setIcon(loadThemedIcon(QStringLiteral("icons/pencil"))); + aEdit->setIcon(themePixmap(QStringLiteral("icons/pencil"))); connect(aEdit, &QAction::triggered, this, &DeckEditorSettingsPage::actEditURL); aRemove = new QAction(this); - aRemove->setIcon(loadThemedIcon(QStringLiteral("icons/decrement"))); + aRemove->setIcon(themePixmap(QStringLiteral("icons/decrement"))); connect(aRemove, &QAction::triggered, this, &DeckEditorSettingsPage::actRemoveURL); aRateLimit = new QAction(this); - aRateLimit->setIcon(loadThemedIcon(QStringLiteral("icons/settings"))); + aRateLimit->setIcon(themePixmap(QStringLiteral("icons/cogwheel"))); connect(aRateLimit, &QAction::triggered, this, &DeckEditorSettingsPage::actAdjustRateLimit); auto *urlToolBar = new QToolBar; @@ -121,19 +120,12 @@ DeckEditorSettingsPage::DeckEditorSettingsPage() lpSpoilerGrid->addWidget(&lastUpdatedLabel, 2, 0); lpSpoilerGrid->addWidget(updateNowButton, 2, 1); lpSpoilerGrid->addWidget(&infoOnSpoilersLabel, 3, 0, 1, 3, Qt::AlignTop); - infoOnSpoilersLabel.setWordWrap(true); // On a change to the checkbox, hide/un-hide the other fields connect(&mcDownloadSpoilersCheckBox, &QCheckBox::toggled, &SettingsCache::instance().downloads(), &DownloadSettings::setDownloadSpoilerStatus); connect(&mcDownloadSpoilersCheckBox, &QCheckBox::toggled, this, &DeckEditorSettingsPage::setSpoilersEnabled); - // The updater is application-owned and outlives this page, so a Settings page - // reopened mid-update must observe the operation that is still running. - if (auto *runningUpdater = SpoilerBackgroundUpdater::current()) { - observeSpoilerUpdater(runningUpdater); - } - mpGeneralGroupBox = new QGroupBox; mpGeneralGroupBox->setLayout(lpGeneralGrid); @@ -336,60 +328,20 @@ void DeckEditorSettingsPage::urlListChanged(const QModelIndex &, int, int, const void DeckEditorSettingsPage::updateSpoilers() { - if (spoilerUpdater) { - return; - } - // Application ownership lets either operation finish after settings closes. - // When disabled, the same button retries file removal and the local reload. - SpoilerBackgroundUpdater *sbu; - if (SettingsCache::instance().downloads().getDownloadSpoilersStatus()) { - sbu = new SpoilerBackgroundUpdater(QCoreApplication::instance(), true); - // Only an updater this page creates is retired by deleteLater: observation - // may attach to the stack-allocated launch updater, which must not be deleted. - connect(sbu, &SpoilerBackgroundUpdater::spoilerCheckerDone, sbu, &QObject::deleteLater); - } else { - // deleteSpoilerFile() wires its own deleteLater. - sbu = SpoilerBackgroundUpdater::deleteSpoilerFile(); - } - observeSpoilerUpdater(sbu); -} + // Disable the button so the user can only press it once at a time + updateNowButton->setDisabled(true); + updateNowButton->setText(tr("Updating...")); -void DeckEditorSettingsPage::observeSpoilerUpdater(SpoilerBackgroundUpdater *sbu) -{ - spoilerUpdater = sbu; - unlockSettings(); - connect(sbu, &SpoilerBackgroundUpdater::spoilerCheckerDone, this, [this, sbu] { - if (spoilerUpdater == sbu) { - spoilerUpdater.clear(); - unlockSettings(); - } - }); - connect(sbu, &SpoilerBackgroundUpdater::spoilerUpdateFailed, this, [this, sbu](const QString &message) { - if (spoilerUpdater != sbu) { - return; - } - auto *notice = new QMessageBox(QMessageBox::Warning, tr("Spoiler update"), message, QMessageBox::Ok, this); - notice->setTextFormat(Qt::PlainText); - notice->setAttribute(Qt::WA_DeleteOnClose); - notice->open(); - }); + // Create a new SBU that will act as if the client was just reloaded + auto *sbu = new SpoilerBackgroundUpdater(); + connect(sbu, &SpoilerBackgroundUpdater::spoilerCheckerDone, this, &DeckEditorSettingsPage::unlockSettings); + connect(sbu, &SpoilerBackgroundUpdater::spoilersUpdatedSuccessfully, this, &DeckEditorSettingsPage::unlockSettings); } void DeckEditorSettingsPage::unlockSettings() { - updateNowButton->setEnabled(!spoilerUpdater); - updateNowButton->setText(spoilerUpdater ? tr("Updating...") - : SettingsCache::instance().downloads().getDownloadSpoilersStatus() - ? tr("Update Spoilers") - : tr("Reload Card Database")); - lastUpdatedLabel.setText(tr("Last Change") + ": " + getLastUpdateTime()); - infoOnSpoilersLabel.setText( - SettingsCache::instance().downloads().getDownloadSpoilersStatus() - ? tr("Spoilers download automatically on launch") + "\n" + - tr("Press the button to manually update without relaunching") + "\n\n" + - tr("Updates continue if you close settings") - : tr("Spoilers are off. Reload Card Database removes any downloaded spoiler file and reloads your " - "card data.")); + updateNowButton->setDisabled(false); + updateNowButton->setText(tr("Update Spoilers")); } QString DeckEditorSettingsPage::getLastUpdateTime() @@ -427,17 +379,12 @@ void DeckEditorSettingsPage::setSpoilersEnabled(bool anInput) mpSpoilerSavePathLineEdit->setEnabled(anInput); mpSpoilerPathButton->setEnabled(anInput); lastUpdatedLabel.setEnabled(anInput); - // The hint explains the button in both states, especially the disabled-state - // Reload Card Database action, so keep it readable while spoilers are off. - infoOnSpoilersLabel.setEnabled(true); + updateNowButton->setEnabled(anInput); + infoOnSpoilersLabel.setEnabled(anInput); - // Only react to the user disabling the setting. The constructor calls this to - // initialize the controls, and a leftover spoiler file is recovered through the - // Reload Card Database button rather than an unrequested reload on page open. - if (!anInput && sender() == &mcDownloadSpoilersCheckBox) { - observeSpoilerUpdater(SpoilerBackgroundUpdater::deleteSpoilerFile()); + if (!anInput) { + SpoilerBackgroundUpdater::deleteSpoilerFile(); } - unlockSettings(); } void DeckEditorSettingsPage::retranslateUi() @@ -447,10 +394,13 @@ void DeckEditorSettingsPage::retranslateUi() mcDownloadSpoilersCheckBox.setText(tr("Download Spoilers Automatically")); mcSpoilerSaveLabel.setText(tr("Spoiler Location:")); lastUpdatedLabel.setText(tr("Last Change") + ": " + getLastUpdateTime()); + infoOnSpoilersLabel.setText(tr("Spoilers download automatically on launch") + "\n" + + tr("Press the button to manually update without relaunching") + "\n\n" + + tr("Do not close settings until manual update is complete")); picDownloadCheckBox.setText(tr("Download card pictures on the fly")); urlLinkLabel.setText(QString("%2").arg(WIKI_CUSTOM_PIC_URL).arg(tr("How to add a custom URL"))); resetDownloadURLs.setText(tr("Reset Download URLs")); - unlockSettings(); + updateNowButton->setText(tr("Update Spoilers")); aAdd->setText(tr("Add New URL")); aEdit->setText(tr("Edit URL")); aRemove->setText(tr("Remove URL")); diff --git a/cockatrice/src/interface/widgets/settings_page/deck_editor_settings_page.h b/cockatrice/src/interface/widgets/settings_page/deck_editor_settings_page.h index 8e33e697d..47f840191 100644 --- a/cockatrice/src/interface/widgets/settings_page/deck_editor_settings_page.h +++ b/cockatrice/src/interface/widgets/settings_page/deck_editor_settings_page.h @@ -5,7 +5,6 @@ #include #include -#include #include #include #include @@ -16,7 +15,6 @@ class QLineEdit; class QListWidget; class QListWidgetItem; class QModelIndex; -class SpoilerBackgroundUpdater; class DeckEditorSettingsPage : public AbstractSettingsPage { @@ -56,7 +54,6 @@ private: QLabel infoOnSpoilersLabel; QPushButton *mpSpoilerPathButton; QPushButton *updateNowButton; - QPointer spoilerUpdater; /** @brief Adds a list item for the given URL, storing the raw URL alongside its displayed label. */ QListWidgetItem *addUrlItem(const QString &url); @@ -69,8 +66,6 @@ private: /** @brief Refreshes the displayed label of every URL item after limits or settings change. */ void refreshUrlItems(); - - void observeSpoilerUpdater(SpoilerBackgroundUpdater *updater); }; #endif // COCKATRICE_DECK_EDITOR_SETTINGS_PAGE_H diff --git a/cockatrice/src/interface/widgets/settings_page/messages_settings_page.cpp b/cockatrice/src/interface/widgets/settings_page/messages_settings_page.cpp index 09dd9e9b0..6128a7830 100644 --- a/cockatrice/src/interface/widgets/settings_page/messages_settings_page.cpp +++ b/cockatrice/src/interface/widgets/settings_page/messages_settings_page.cpp @@ -1,7 +1,7 @@ #include "messages_settings_page.h" #include "../../../client/settings/cache_settings.h" -#include "../../theme_icon.h" +#include "../../pixel_map_generator.h" #include "../interface/widgets/utility/get_text_with_max.h" #include "libcockatrice/utility/macros.h" @@ -146,15 +146,15 @@ MessagesSettingsPage::MessagesSettingsPage() } aAdd = new QAction(this); - aAdd->setIcon(loadThemedIcon(QStringLiteral("icons/increment"))); + aAdd->setIcon(themePixmap(QStringLiteral("icons/increment"))); connect(aAdd, &QAction::triggered, this, &MessagesSettingsPage::actAdd); aEdit = new QAction(this); - aEdit->setIcon(loadThemedIcon(QStringLiteral("icons/pencil"))); + aEdit->setIcon(themePixmap(QStringLiteral("icons/pencil"))); connect(aEdit, &QAction::triggered, this, &MessagesSettingsPage::actEdit); aRemove = new QAction(this); - aRemove->setIcon(loadThemedIcon(QStringLiteral("icons/decrement"))); + aRemove->setIcon(themePixmap(QStringLiteral("icons/decrement"))); connect(aRemove, &QAction::triggered, this, &MessagesSettingsPage::actRemove); auto *messageToolBar = new QToolBar; diff --git a/cockatrice/src/interface/widgets/settings_page/settings_search_delegate.cpp b/cockatrice/src/interface/widgets/settings_page/settings_search_delegate.cpp index 67354c9d7..bf35b7557 100644 --- a/cockatrice/src/interface/widgets/settings_page/settings_search_delegate.cpp +++ b/cockatrice/src/interface/widgets/settings_page/settings_search_delegate.cpp @@ -5,7 +5,6 @@ */ #include "settings_search_delegate.h" -#include "../interface/theme_manager.h" #include "settings_search_model.h" #include @@ -89,8 +88,7 @@ void SettingsSearchDelegate::paint(QPainter *painter, QRect textRect = contentRect.adjusted(iconOffset, 0, 0, 0); // Breadcrumb: "Page > Group" - // Family comes from the InterfaceDefault theme role; the -1 size and bold stay caller-side. - QFont breadcrumbFont = themeManager->appFont(AppFont::InterfaceDefault); + QFont breadcrumbFont = option.font; breadcrumbFont.setPointSize(breadcrumbFont.pointSize() - 1); breadcrumbFont.setBold(true); @@ -115,7 +113,7 @@ void SettingsSearchDelegate::paint(QPainter *painter, yPos += 20; // Setting label - QFont labelFont = themeManager->appFont(AppFont::InterfaceDefault); + QFont labelFont = option.font; labelFont.setPointSize(labelFont.pointSize() + 1); labelFont.setBold(isSelected); diff --git a/cockatrice/src/interface/widgets/settings_page/shortcut_settings_page.cpp b/cockatrice/src/interface/widgets/settings_page/shortcut_settings_page.cpp index 83c2ec585..a77c0c2c3 100644 --- a/cockatrice/src/interface/widgets/settings_page/shortcut_settings_page.cpp +++ b/cockatrice/src/interface/widgets/settings_page/shortcut_settings_page.cpp @@ -3,7 +3,7 @@ #include "../../../client/settings/cache_settings.h" #include "../../../client/settings/shortcut_treeview.h" #include "../../../client/settings/shortcuts_settings.h" -#include "../../theme_icon.h" +#include "../../pixel_map_generator.h" #include "../interface/widgets/utility/custom_line_edit.h" #include "../interface/widgets/utility/sequence_edit.h" @@ -58,8 +58,8 @@ ShortcutSettingsPage::ShortcutSettingsPage() btnResetAll = new QPushButton(this); btnClearAll = new QPushButton(this); - btnResetAll->setIcon(loadThemedIcon(QStringLiteral("icons/reload"))); - btnClearAll->setIcon(loadThemedIcon(QStringLiteral("icons/clearsearch"))); + btnResetAll->setIcon(themePixmap(QStringLiteral("icons/update"))); + btnClearAll->setIcon(themePixmap(QStringLiteral("icons/clearsearch"))); // layout auto *_editLayout = new QGridLayout; diff --git a/cockatrice/src/interface/widgets/tabs/api/archidekt/display/archidekt_deck_preview_image_display_widget.cpp b/cockatrice/src/interface/widgets/tabs/api/archidekt/display/archidekt_deck_preview_image_display_widget.cpp index 3720aeb55..97383534f 100644 --- a/cockatrice/src/interface/widgets/tabs/api/archidekt/display/archidekt_deck_preview_image_display_widget.cpp +++ b/cockatrice/src/interface/widgets/tabs/api/archidekt/display/archidekt_deck_preview_image_display_widget.cpp @@ -1,6 +1,5 @@ #include "archidekt_deck_preview_image_display_widget.h" -#include "../../../../../theme_manager.h" #include "../../../../general/display/shadow_background_label.h" #include @@ -18,27 +17,19 @@ ArchidektDeckPreviewImageDisplayWidget::ArchidektDeckPreviewImageDisplayWidget(Q bottomLeftLabel = new ShadowBackgroundLabel(this, ""); bottomRightLabel = new ShadowBackgroundLabel(this, ""); - applyCardTextFont(); - connect(themeManager, &ThemeManager::fontsChanged, this, - &ArchidektDeckPreviewImageDisplayWidget::applyCardTextFont); - - // Raise so labels appear above image - topLeftLabel->raise(); - topRightLabel->raise(); - bottomLeftLabel->raise(); - bottomRightLabel->raise(); -} - -void ArchidektDeckPreviewImageDisplayWidget::applyCardTextFont() -{ - // Family comes from the CardText theme role; size and weight stay caller-side as before. - QFont f = themeManager->appFont(AppFont::CardText); + QFont f; f.setBold(true); f.setPointSize(16); topLeftLabel->setFont(f); topRightLabel->setFont(f); bottomLeftLabel->setFont(f); bottomRightLabel->setFont(f); + + // Raise so labels appear above image + topLeftLabel->raise(); + topRightLabel->raise(); + bottomLeftLabel->raise(); + bottomRightLabel->raise(); } void ArchidektDeckPreviewImageDisplayWidget::setAspectRatio(float ratio) diff --git a/cockatrice/src/interface/widgets/tabs/api/archidekt/display/archidekt_deck_preview_image_display_widget.h b/cockatrice/src/interface/widgets/tabs/api/archidekt/display/archidekt_deck_preview_image_display_widget.h index 41585e851..991f27b6a 100644 --- a/cockatrice/src/interface/widgets/tabs/api/archidekt/display/archidekt_deck_preview_image_display_widget.h +++ b/cockatrice/src/interface/widgets/tabs/api/archidekt/display/archidekt_deck_preview_image_display_widget.h @@ -60,9 +60,6 @@ protected: */ void resizeEvent(QResizeEvent *event) override; -private slots: - void applyCardTextFont(); - private: float aspectRatio = 1.0f; ///< Aspect ratio to maintain for the preview image }; diff --git a/cockatrice/src/interface/widgets/tabs/api/archidekt/tab_archidekt.cpp b/cockatrice/src/interface/widgets/tabs/api/archidekt/tab_archidekt.cpp index ae3a9baef..7f7a2d3fd 100644 --- a/cockatrice/src/interface/widgets/tabs/api/archidekt/tab_archidekt.cpp +++ b/cockatrice/src/interface/widgets/tabs/api/archidekt/tab_archidekt.cpp @@ -1,7 +1,7 @@ #include "tab_archidekt.h" #include "../../../../../client/settings/cache_settings.h" -#include "../../../../theme_icon.h" +#include "../../../../pixel_map_generator.h" #include "../../../cards/additional_info/mana_symbol_widget.h" #include "../../../cards/card_size_widget.h" #include "../../../quick_settings/settings_button_widget.h" @@ -25,7 +25,6 @@ #include #include #include -#include #include #include #include @@ -61,6 +60,8 @@ TabArchidekt::TabArchidekt(TabSupervisor *_tabSupervisor) { // Initialize network networkManager = NetworkManagerFactory::create(this); + networkManager->setRedirectPolicy(QNetworkRequest::ManualRedirectPolicy); + connect(networkManager, &QNetworkAccessManager::finished, this, &TabArchidekt::processApiJson); // Initialize debounce timer searchDebounceTimer = new QTimer(this); @@ -76,48 +77,6 @@ TabArchidekt::TabArchidekt(TabSupervisor *_tabSupervisor) getTopDecks(); } -TabArchidekt::~TabArchidekt() -{ - cancelRequest(); -} - -void TabArchidekt::cancelRequest() -{ - const QPointer previousReply = activeReply; - activeReply = nullptr; - isLoadingMore = false; - if (previousReply) { - disconnect(previousReply, nullptr, this, nullptr); - previousReply->abort(); - previousReply->deleteLater(); - } -} - -void TabArchidekt::startRequest(QUrl url, RequestKind kind, int page) -{ - cancelRequest(); - requestUrl = url; - requestKind = kind; - requestedPage = page; - isLoadingMore = true; - retryButton->hide(); - requestStatusLabel->setText(kind == RequestKind::Deck ? tr("Loading deck…") : tr("Loading decks…")); - requestStatusLabel->show(); - - QNetworkRequest request(url); - request.setHeader(QNetworkRequest::UserAgentHeader, QString("Cockatrice %1").arg(VERSION_STRING)); - auto *reply = networkManager->get(request); - activeReply = reply; - connect(reply, &QNetworkReply::finished, this, [this, reply]() { processApiJson(reply); }); -} - -void TabArchidekt::showRequestError(const QString &error) -{ - requestStatusLabel->setText(tr("Unable to load decks: %1").arg(error)); - requestStatusLabel->show(); - retryButton->show(); -} - void TabArchidekt::initializeUi() { // Main container @@ -235,18 +194,6 @@ void TabArchidekt::initializeUi() mainLayout->addWidget(primaryToolbar); mainLayout->addWidget(secondaryToolbar); - - auto *requestStatusRow = new QHBoxLayout; - requestStatusLabel = new QLabel(container); - requestStatusLabel->setTextFormat(Qt::PlainText); - requestStatusLabel->setWordWrap(true); - retryButton = new QPushButton(tr("Retry"), container); - retryButton->hide(); - connect(retryButton, &QPushButton::clicked, this, - [this]() { startRequest(requestUrl, requestKind, requestedPage); }); - requestStatusRow->addWidget(requestStatusLabel, 1); - requestStatusRow->addWidget(retryButton); - mainLayout->addLayout(requestStatusRow); mainLayout->addWidget(scrollArea); setCentralWidget(container); @@ -282,7 +229,7 @@ void TabArchidekt::setupFilterWidgets() // Format filter (collapsible) formatButton = new SettingsButtonWidget(secondaryToolbar); formatButton->setButtonText(tr("Formats")); - formatButton->setButtonIcon(loadThemedIcon(QStringLiteral("icons/scales"))); + formatButton->setButtonIcon(themePixmap(QStringLiteral("icons/scale_balanced"))); QWidget *formatContainer = new QWidget(secondaryToolbar); QGridLayout *formatLayout = new QGridLayout(formatContainer); @@ -457,7 +404,6 @@ void TabArchidekt::retranslateUi() minDeckSizeLogicCombo->setItemText(0, tr("Exact")); searchButton->setText(tr("Search")); - retryButton->setText(tr("Retry")); settingsButton->setToolTip(tr("Display Settings")); } @@ -571,95 +517,82 @@ QString TabArchidekt::buildSearchUrl() void TabArchidekt::doSearch() { + // Reset to first page on new search + currentPage = 1; + // We're searching, so we'll be in list mode + isListMode = true; // Don't debounce - only called by explicit user actions now doSearchImmediate(); } void TabArchidekt::doSearchImmediate() { - searchDebounceTimer->stop(); - currentPage = 1; - startRequest(QUrl(buildSearchUrl()), RequestKind::Search); + QString url = buildSearchUrl(); + QNetworkRequest req{QUrl(url)}; + req.setHeader(QNetworkRequest::UserAgentHeader, QString("Cockatrice %1").arg(VERSION_STRING)); + networkManager->get(req); } void TabArchidekt::loadNextPage() { - if (activeReply || !isListMode || !listingsWidget || nextPageUrl.isEmpty()) { + if (isLoadingMore) { return; } - // Continue the accepted search, even if the user has edited filters without submitting them. - // Advance currentPage only after this response has been accepted, so failures can retry it. - startRequest(nextPageUrl, RequestKind::NextPage, currentPage + 1); + isLoadingMore = true; + currentPage++; + + QString url = buildSearchUrl(); + QNetworkRequest req{QUrl(url)}; + req.setHeader(QNetworkRequest::UserAgentHeader, QString("Cockatrice %1").arg(VERSION_STRING)); + networkManager->get(req); } void TabArchidekt::actNavigatePage(QString url) { - searchDebounceTimer->stop(); - startRequest(QUrl(url), RequestKind::Deck); + QNetworkRequest request{QUrl(url)}; + request.setHeader(QNetworkRequest::UserAgentHeader, QString("Cockatrice %1").arg(VERSION_STRING)); + networkManager->get(request); } void TabArchidekt::getTopDecks() { - doSearchImmediate(); + currentPage = 1; + QNetworkRequest request{QUrl(buildSearchUrl())}; + request.setHeader(QNetworkRequest::UserAgentHeader, QString("Cockatrice %1").arg(VERSION_STRING)); + networkManager->get(request); } void TabArchidekt::processApiJson(QNetworkReply *reply) { - if (reply != activeReply) { - reply->deleteLater(); - return; - } - activeReply = nullptr; - isLoadingMore = false; - reply->deleteLater(); - if (reply->error() != QNetworkReply::NoError) { - showRequestError(reply->errorString()); + isLoadingMore = false; + reply->deleteLater(); return; } QByteArray responseData = reply->readAll(); - QJsonParseError parseError; - QJsonDocument jsonDoc = QJsonDocument::fromJson(responseData, &parseError); + QJsonDocument jsonDoc = QJsonDocument::fromJson(responseData); - if (parseError.error != QJsonParseError::NoError || !jsonDoc.isObject()) { - showRequestError(tr("Invalid response from Archidekt.")); + if (!jsonDoc.isObject()) { + isLoadingMore = false; + reply->deleteLater(); return; } QJsonObject jsonObj = jsonDoc.object(); - if (requestKind != RequestKind::Deck) { - if (!jsonObj.value("results").isArray() || - (!jsonObj.value("next").isNull() && !jsonObj.value("next").isString())) { - showRequestError(tr("Invalid deck listing from Archidekt.")); - return; - } - for (const auto &value : jsonObj.value("results").toArray()) { - if (!value.isObject() || value.toObject().value("id").toInt() <= 0) { - showRequestError(tr("Invalid deck listing from Archidekt.")); - return; - } - } - currentPage = requestedPage; - const QString next = jsonObj.value("next").toString(); - nextPageUrl = next.isEmpty() ? QUrl() : reply->url().resolved(QUrl(next)); - // An empty or self-repeating page cannot make forward progress. - if (jsonObj.value("results").toArray().isEmpty() || nextPageUrl == reply->url()) { - nextPageUrl.clear(); - } - requestStatusLabel->setText( - currentPage == 1 && jsonObj.value("results").toArray().isEmpty() ? tr("No decks found.") : QString()); - requestStatusLabel->setVisible(!requestStatusLabel->text().isEmpty()); + QString responseUrl = reply->url().toString(); + + if (responseUrl.startsWith("https://archidekt.com/api/decks/v3/")) { processTopDecksResponse(jsonObj); - } else { - if (jsonObj.value("id").toInt() <= 0 || !jsonObj.value("cards").isArray()) { - showRequestError(tr("Invalid deck from Archidekt.")); - return; - } - requestStatusLabel->hide(); + } else if (responseUrl.startsWith("https://archidekt.com/api/decks/")) { processDeckResponse(jsonObj); + } else { + prettyPrintJson(jsonObj, 4); } + + isLoadingMore = false; + reply->deleteLater(); } void TabArchidekt::processTopDecksResponse(QJsonObject reply) @@ -667,12 +600,8 @@ void TabArchidekt::processTopDecksResponse(QJsonObject reply) ArchidektDeckListingApiResponse deckData; deckData.fromJson(reply); - // An accepted listing restores list mode; a failed deck navigation leaves the listing usable. - isListMode = true; - // New search → clear everything - if (requestKind == RequestKind::Search) { - listingsWidget = nullptr; + if (currentPage == 1) { QtUtils::clearLayoutRec(resultsLayout, QtUtils::DeletePolicy::Immediate); listingsWidget = new ArchidektApiResponseDeckListingsDisplayWidget(resultsContainer, deckData, cardSizeSlider); @@ -697,10 +626,8 @@ void TabArchidekt::processDeckResponse(QJsonObject reply) // We're in single deck mode - disable infinite scroll isListMode = false; - nextPageUrl.clear(); // Clear existing results for single deck view - listingsWidget = nullptr; QtUtils::clearLayoutRec(resultsLayout, QtUtils::DeletePolicy::Immediate); auto display = new ArchidektApiResponseDeckDisplayWidget(resultsContainer, deckData, cardSizeSlider); diff --git a/cockatrice/src/interface/widgets/tabs/api/archidekt/tab_archidekt.h b/cockatrice/src/interface/widgets/tabs/api/archidekt/tab_archidekt.h index 81c9502df..c74380b63 100644 --- a/cockatrice/src/interface/widgets/tabs/api/archidekt/tab_archidekt.h +++ b/cockatrice/src/interface/widgets/tabs/api/archidekt/tab_archidekt.h @@ -8,10 +8,8 @@ #include #include #include -#include #include #include -#include #include #include @@ -57,7 +55,6 @@ public: * connects signals and slots, and triggers an initial fetch of top decks. */ explicit TabArchidekt(TabSupervisor *_tabSupervisor); - ~TabArchidekt() override; /** * @brief Update all UI text to reflect the current language or translation @@ -193,17 +190,6 @@ protected: bool eventFilter(QObject *obj, QEvent *event) override; private: - enum class RequestKind - { - Search, - NextPage, - Deck - }; - - void startRequest(QUrl url, RequestKind kind, int page = 1); - void cancelRequest(); - void showRequestError(const QString &error); - /** * @brief Initialize the main UI layout and toolbars * @@ -250,14 +236,7 @@ private: int currentPage; ///< Current page number for infinite scroll bool isLoadingMore; ///< Flag to prevent multiple simultaneous page loads bool isListMode; - QPointer activeReply; - RequestKind requestKind = RequestKind::Search; - QUrl requestUrl; - QUrl nextPageUrl; - int requestedPage = 1; - QPointer listingsWidget; - QLabel *requestStatusLabel; - QPushButton *retryButton; + ArchidektApiResponseDeckListingsDisplayWidget *listingsWidget = nullptr; // --------------------------------------------------------------------- // Layout Containers diff --git a/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_bracket_service.cpp b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_bracket_service.cpp index 7b60ed23c..8e0448faa 100644 --- a/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_bracket_service.cpp +++ b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_bracket_service.cpp @@ -26,11 +26,6 @@ quint64 CommanderBracketService::estimateBracket(const DeckList &deck, QObject * return CommanderSpellbookApiAccessor::instance().estimateBracket(deck, requester); } -void CommanderBracketService::cancelEstimate(quint64 requestId) -{ - CommanderSpellbookApiAccessor::instance().cancelEstimate(requestId); -} - void CommanderBracketService::onEstimateBracketFinished(CommanderSpellbookApiAccessor::RequestId id, QObject *requester, const EstimateBracketResult &result) diff --git a/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_bracket_service.h b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_bracket_service.h index 00debce86..22962cc95 100644 --- a/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_bracket_service.h +++ b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_bracket_service.h @@ -30,7 +30,6 @@ public: static CommanderBracketService &instance(); quint64 estimateBracket(const DeckList &deck, QObject *requester); - void cancelEstimate(quint64 requestId); signals: void estimateFinished(quint64 requestId, QObject *requester, const CommanderBracketEstimate &estimate); diff --git a/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_bracket_widget.cpp b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_bracket_widget.cpp index a0e3f2491..5279165af 100644 --- a/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_bracket_widget.cpp +++ b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_bracket_widget.cpp @@ -1,7 +1,7 @@ #include "commander_bracket_widget.h" #include "../../../../../client/settings/cache_settings.h" -#include "../../../../theme_icon.h" +#include "../../../../pixel_map_generator.h" #include "commander_bracket_service.h" #include "libcockatrice/deck_list/deck_list.h" @@ -28,18 +28,9 @@ template class QSharedPointer; CommanderBracketWidget::CommanderBracketWidget(QWidget *parent) : QWidget(parent) { - autoEstimateTimer = new QTimer(this); - autoEstimateTimer->setSingleShot(true); - connect(autoEstimateTimer, &QTimer::timeout, this, [this]() { - if (SettingsCache::instance().deckEditor().getCommanderSpellbookIntegrationEnabled() == - commanderSpellbookIntegrationEnabledIndexAutomatic) { - requestBracketEstimate(); - } - }); bracketLabel = new QLabel(tr("Bracket:"), this); bracketValueLabel = new QLabel(this); - bracketValueLabel->setTextFormat(Qt::PlainText); bracketValueLabel->setText("-"); bracketValueLabel->setObjectName("bracketValueLabel"); @@ -49,7 +40,7 @@ CommanderBracketWidget::CommanderBracketWidget(QWidget *parent) : QWidget(parent bracketInfoButton->setEnabled(false); bracketRefreshButton = new QToolButton(this); - bracketRefreshButton->setIcon(loadThemedIcon(QStringLiteral("icons/reload"))); + bracketRefreshButton->setIcon(themePixmap(QStringLiteral("icons/reload"))); bracketRefreshButton->setAutoRaise(true); connect(bracketRefreshButton, &QToolButton::clicked, this, &CommanderBracketWidget::requestBracketEstimate); @@ -74,38 +65,18 @@ CommanderBracketWidget::CommanderBracketWidget(QWidget *parent) : QWidget(parent this, &CommanderBracketWidget::maybeAutoEstimateBracket); connect(&SettingsCache::instance().deckEditor(), &DeckEditorSettings::commanderSpellbookIntegrationUseOfficialBracketNamesChanged, this, - &CommanderBracketWidget::displayEstimate); + &CommanderBracketWidget::maybeAutoEstimateBracket); setVisible(false); } -CommanderBracketWidget::~CommanderBracketWidget() -{ - cancelEstimate(); -} - -void CommanderBracketWidget::cancelEstimate() -{ - autoEstimateTimer->stop(); - const quint64 canceledRequest = requestId; - requestId = 0; - CommanderBracketService::instance().cancelEstimate(canceledRequest); - bracketRefreshButton->setEnabled(true); - if (canceledRequest) { - bracketValueLabel->setText("-"); - bracketValueLabel->setToolTip({}); - } -} - void CommanderBracketWidget::setDeck(const QSharedPointer &_deck) { - cancelEstimate(); deck = _deck; - hasEstimate = false; + requestId = 0; // invalidate any in-flight estimate for the previous deck // Reset the displayed bracket bracketValueLabel->setText("-"); - bracketValueLabel->setToolTip({}); bracketInfoButton->setToolTip({}); bracketInfoButton->setEnabled(false); bracketRefreshButton->setEnabled(true); @@ -173,7 +144,7 @@ bool CommanderBracketWidget::promptCommanderSpellbookIntegration() dialog.exec(); // Persist naming choice (if not disabled) - if (clickedButton == enableBtn || clickedButton == automaticBtn) { + if (clickedButton != disableBtn) { bool useOfficial = namingCombo->currentIndex() == 1; SettingsCache::instance().deckEditor().setCommanderSpellbookIntegrationUseOfficialBracketNames(useOfficial); } @@ -205,20 +176,9 @@ void CommanderBracketWidget::updateBracketVisibility(bool visible) void CommanderBracketWidget::requestBracketEstimate() { - const int mode = SettingsCache::instance().deckEditor().getCommanderSpellbookIntegrationEnabled(); - if (!deck || deck->getGameFormat().compare("commander", Qt::CaseInsensitive) != 0 || requestId || - (mode != commanderSpellbookIntegrationEnabledIndexEnabled && - mode != commanderSpellbookIntegrationEnabledIndexAutomatic)) { - return; - } - - autoEstimateTimer->stop(); - hasEstimate = false; bracketRefreshButton->setEnabled(false); bracketInfoButton->setEnabled(false); - bracketInfoButton->setToolTip({}); bracketValueLabel->setText(tr("Calculating…")); - bracketValueLabel->setToolTip({}); requestId = CommanderBracketService::instance().estimateBracket(*deck, this); } @@ -231,35 +191,22 @@ void CommanderBracketWidget::onEstimateBracketFinished(quint64 id, return; } - requestId = 0; - hasEstimate = true; - lastEstimate = result; - displayEstimate(); - bracketRefreshButton->setEnabled(true); -} - -void CommanderBracketWidget::displayEstimate() -{ - if (!hasEstimate) { - bracketInfoButton->setToolTip(tr("Why this bracket?")); - return; - } - BracketExplainer explainer; - lastBracketExplanation = explainer.explain(lastEstimate.rawResult); + lastBracketExplanation = explainer.explain(result.rawResult); // Display bracket bracketValueLabel->setText( SettingsCache::instance().deckEditor().getCommanderSpellbookIntegrationUseOfficialBracketNames() - ? lastEstimate.officialName - : lastEstimate.displayName); + ? result.officialName + : result.displayName); + bracketRefreshButton->setEnabled(true); // Build tooltip QString tooltip; for (const auto §ion : lastBracketExplanation.sections) { - tooltip += "" + section.title.toHtmlEscaped() + "
    "; + tooltip += "" + section.title + "
    "; for (const auto &line : section.bulletPoints) { - tooltip += "• " + line.toHtmlEscaped() + "
    "; + tooltip += "• " + line + "
    "; } tooltip += "
    "; } @@ -268,17 +215,13 @@ void CommanderBracketWidget::displayEstimate() bracketInfoButton->setEnabled(!tooltip.isEmpty()); } -void CommanderBracketWidget::onEstimateBracketError(quint64 id, QObject *requester, const QString &error) +void CommanderBracketWidget::onEstimateBracketError(quint64 id, QObject *requester, const QString & /*error*/) { if (requester != this || id != requestId) { return; } - requestId = 0; - hasEstimate = false; bracketValueLabel->setText("-"); - bracketValueLabel->setToolTip( - QStringLiteral("%1").arg(tr("Unable to estimate bracket: %1").arg(error).toHtmlEscaped())); bracketRefreshButton->setEnabled(true); bracketInfoButton->setToolTip({}); bracketInfoButton->setEnabled(false); @@ -286,15 +229,6 @@ void CommanderBracketWidget::onEstimateBracketError(quint64 id, QObject *request void CommanderBracketWidget::maybeAutoEstimateBracket() { - if (prompting) { - return; - } - autoEstimateTimer->stop(); - if (!deck) { - cancelEstimate(); - updateBracketVisibility(false); - return; - } const QString formatKey = deck->getGameFormat(); const bool isCommander = (formatKey.compare("commander", Qt::CaseInsensitive) == 0); @@ -302,20 +236,17 @@ void CommanderBracketWidget::maybeAutoEstimateBracket() int mode = SettingsCache::instance().deckEditor().getCommanderSpellbookIntegrationEnabled(); if (!isCommander || mode == commanderSpellbookIntegrationEnabledIndexDisabled) { - cancelEstimate(); updateBracketVisibility(false); return; } if (mode == commanderSpellbookIntegrationEnabledIndexUnprompted) { - const auto promptedDeck = deck; + if (prompting) { + return; + } prompting = true; const bool accepted = promptCommanderSpellbookIntegration(); prompting = false; - if (deck != promptedDeck || !deck || deck->getGameFormat().compare("commander", Qt::CaseInsensitive) != 0) { - maybeAutoEstimateBracket(); - return; - } if (!accepted) { updateBracketVisibility(false); return; @@ -330,17 +261,17 @@ void CommanderBracketWidget::maybeAutoEstimateBracket() } // Avoid firing if we already have a result or a request in flight - if (requestId || hasEstimate) { + if (!bracketRefreshButton->isEnabled()) { return; } // Defer to avoid races during init / model rebuild - autoEstimateTimer->start(0); + QTimer::singleShot(0, this, &CommanderBracketWidget::requestBracketEstimate); } void CommanderBracketWidget::retranslateUi() { bracketLabel->setText(tr("Bracket:")); - displayEstimate(); + bracketInfoButton->setToolTip(tr("Why this bracket?")); bracketRefreshButton->setToolTip(tr("Recalculate bracket")); } diff --git a/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_bracket_widget.h b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_bracket_widget.h index 0ef1eedee..f5b2b7958 100644 --- a/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_bracket_widget.h +++ b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_bracket_widget.h @@ -1,7 +1,6 @@ #ifndef COCKATRICE_COMMANDER_BRACKET_WIDGET_H #define COCKATRICE_COMMANDER_BRACKET_WIDGET_H -#include "commander_bracket_service.h" #include "commander_spellbook_bracket_explainer.h" #include @@ -12,7 +11,6 @@ class QLabel; class QToolButton; -class QTimer; struct CommanderBracketEstimate; class DeckList; class QObject; @@ -24,7 +22,6 @@ class CommanderBracketWidget : public QWidget public: explicit CommanderBracketWidget(QWidget *parent = nullptr); - ~CommanderBracketWidget() override; void setDeck(const QSharedPointer &_deck); void retranslateUi(); @@ -38,15 +35,10 @@ private slots: private: bool promptCommanderSpellbookIntegration(); void updateBracketVisibility(bool visible); - void cancelEstimate(); - void displayEstimate(); QSharedPointer deck; bool prompting = false; quint64 requestId = 0; - QTimer *autoEstimateTimer; - bool hasEstimate = false; - CommanderBracketEstimate lastEstimate; QLabel *bracketLabel; QLabel *bracketValueLabel; diff --git a/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_spellbook_api_accessor.cpp b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_spellbook_api_accessor.cpp index 8456e2e21..bd8d9379d 100644 --- a/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_spellbook_api_accessor.cpp +++ b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_spellbook_api_accessor.cpp @@ -9,10 +9,9 @@ #include #include #include -#include #include #include -#include +#include #include class DeckList; @@ -25,18 +24,13 @@ CommanderSpellbookApiAccessor &CommanderSpellbookApiAccessor::instance() return instance; } -CommanderSpellbookApiAccessor::CommanderSpellbookApiAccessor(QObject *parent) - : QObject(parent), network(NetworkManagerFactory::create(this)) +CommanderSpellbookApiAccessor::CommanderSpellbookApiAccessor(QObject *parent) : QObject(parent) { } CommanderSpellbookApiAccessor::RequestId CommanderSpellbookApiAccessor::estimateBracket(const DeckList &deck, QObject *requester) { - if (!requester) { - return 0; - } - CommanderSpellbookDeckRequest deckRequest = CommanderSpellbookDeckRequest::fromDeckList(deck); QJsonDocument doc(deckRequest.toJson()); @@ -46,39 +40,30 @@ CommanderSpellbookApiAccessor::RequestId CommanderSpellbookApiAccessor::estimate req.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); req.setHeader(QNetworkRequest::UserAgentHeader, QString("Cockatrice %1").arg(VERSION_STRING)); - QNetworkReply *reply = network->post(req, body); + QNetworkReply *reply = network.post(req, body); const RequestId id = nextRequestId++; - pendingRequests.insert(id, reply); - const QPointer guardedRequester(requester); - connect(requester, &QObject::destroyed, reply, [this, id]() { cancelEstimate(id); }); - connect(reply, &QNetworkReply::finished, this, [this, reply, id, guardedRequester]() { - if (pendingRequests.take(id) != reply) { - return; - } - reply->deleteLater(); - if (guardedRequester) { - onEstimateReplyFinished(reply, id, guardedRequester); - } - }); + reply->setProperty("requestId", QVariant::fromValue(id)); + reply->setProperty("requester", QVariant::fromValue(requester)); + + connect(reply, &QNetworkReply::finished, this, [this, reply]() { onEstimateReplyFinished(reply); }); return id; } -void CommanderSpellbookApiAccessor::cancelEstimate(RequestId id) +void CommanderSpellbookApiAccessor::onEstimateReplyFinished(QNetworkReply *reply) { - const QPointer reply = pendingRequests.take(id); - if (!reply) { + reply->deleteLater(); + + const RequestId id = reply->property("requestId").toULongLong(); + QObject *requester = reply->property("requester").value(); + + if (!requester) { + // Requester died — silently drop return; } - disconnect(reply, nullptr, this, nullptr); - reply->abort(); - reply->deleteLater(); -} -void CommanderSpellbookApiAccessor::onEstimateReplyFinished(QNetworkReply *reply, RequestId id, QObject *requester) -{ if (reply->error() != QNetworkReply::NoError) { emit estimateBracketError(id, requester, reply->errorString()); return; @@ -93,10 +78,6 @@ void CommanderSpellbookApiAccessor::onEstimateReplyFinished(QNetworkReply *reply } EstimateBracketResult result = EstimateBracketResult::fromJson(doc.object()); - if (result.bracketTag.trimmed().isEmpty()) { - emit estimateBracketError(id, requester, QStringLiteral("Missing bracket in response")); - return; - } emit estimateBracketFinished(id, requester, result); } diff --git a/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_spellbook_api_accessor.h b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_spellbook_api_accessor.h index a1b2936bb..98e7e3710 100644 --- a/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_spellbook_api_accessor.h +++ b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_spellbook_api_accessor.h @@ -1,16 +1,14 @@ #ifndef COCKATRICE_COMMANDER_SPELLBOOK_API_ACCESSOR_H #define COCKATRICE_COMMANDER_SPELLBOOK_API_ACCESSOR_H -#include +#include #include -#include #include #include #include class DeckList; class QNetworkReply; -class QNetworkAccessManager; struct EstimateBracketResult; class CommanderSpellbookApiAccessor final : public QObject @@ -23,7 +21,6 @@ public: using RequestId = quint64; RequestId estimateBracket(const DeckList &deck, QObject *requester); - void cancelEstimate(RequestId id); signals: void estimateBracketFinished(RequestId id, QObject *requester, const EstimateBracketResult &result); @@ -34,10 +31,9 @@ private: explicit CommanderSpellbookApiAccessor(QObject *parent = nullptr); Q_DISABLE_COPY_MOVE(CommanderSpellbookApiAccessor) - void onEstimateReplyFinished(QNetworkReply *reply, RequestId id, QObject *requester); + void onEstimateReplyFinished(QNetworkReply *reply); - QNetworkAccessManager *network; - QHash> pendingRequests; + QNetworkAccessManager network; RequestId nextRequestId = 1; }; diff --git a/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/handle_commander_brackets.cpp b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/handle_commander_brackets.cpp index be490ce37..407f53e99 100644 --- a/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/handle_commander_brackets.cpp +++ b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/handle_commander_brackets.cpp @@ -3,14 +3,11 @@ #include "../../../../../client/settings/cache_settings.h" #include -#include #include -#include #include #include #include #include -#include #include #include #include @@ -25,14 +22,6 @@ HandleCommanderBrackets::HandleCommanderBrackets(QObject *parent) { } -HandleCommanderBrackets::~HandleCommanderBrackets() -{ - if (reply) { - disconnect(reply, nullptr, this, nullptr); - reply->abort(); - } -} - void HandleCommanderBrackets::downloadBracketDefinitions() { if (reply) { @@ -46,49 +35,31 @@ void HandleCommanderBrackets::downloadBracketDefinitions() void HandleCommanderBrackets::actFinishParsingDownloadedData() { - auto *finishedReply = qobject_cast(sender()); - if (!finishedReply || finishedReply != reply) { - return; - } + reply = qobject_cast(sender()); - // Clear the active request before notifying listeners, which may immediately retry. - reply = nullptr; - finishedReply->deleteLater(); + if (reply->error() != QNetworkReply::NoError) { + emit sigBracketDefinitionsDownloadFailed(reply->error()); - if (finishedReply->error() != QNetworkReply::NoError) { - emit sigBracketDefinitionsDownloadFailed(finishedReply->error()); + reply->deleteLater(); return; } QJsonParseError parseError; - auto document = QJsonDocument::fromJson(finishedReply->readAll(), &parseError); + auto document = QJsonDocument::fromJson(reply->readAll(), &parseError); - if (parseError.error != QJsonParseError::NoError || !document.isObject() || - !document.object().value("brackets").isArray() || document.object().value("brackets").toArray().isEmpty()) { + if (parseError.error != QJsonParseError::NoError) { emit sigBracketDefinitionsDownloadFailed(QNetworkReply::UnknownContentError); - return; - } - // Validate the complete replacement before touching the last usable definitions. - // Tags are QSettings group names, so path separators must not create nested groups. - QSet tags; - for (const auto &value : document.object().value("brackets").toArray()) { - const auto definition = value.toObject(); - const auto tag = definition.value("tag").toString(); - if (!value.isObject() || tag.trimmed().isEmpty() || tag.contains('/') || tag.contains('\\') || - tags.contains(tag) || definition.value("officialName").toString().trimmed().isEmpty() || - definition.value("displayName").toString().trimmed().isEmpty() || - !definition.value("explanation").isString()) { - emit sigBracketDefinitionsDownloadFailed(QNetworkReply::UnknownContentError); - return; - } - tags.insert(tag); + reply->deleteLater(); + return; } updateBracketDefinitions(document.toVariant().toMap()); emit sigBracketDefinitionsDownloaded(); + + reply->deleteLater(); } void HandleCommanderBrackets::updateBracketDefinitions(const QVariantMap &jsonMap) diff --git a/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/handle_commander_brackets.h b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/handle_commander_brackets.h index 99b15832c..39bddba05 100644 --- a/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/handle_commander_brackets.h +++ b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/handle_commander_brackets.h @@ -3,7 +3,6 @@ #include #include -#include #include #include @@ -15,7 +14,6 @@ class HandleCommanderBrackets : public QObject public: explicit HandleCommanderBrackets(QObject *parent = nullptr); - ~HandleCommanderBrackets() override; void downloadBracketDefinitions(); @@ -30,7 +28,7 @@ private: void updateBracketDefinitions(const QVariantMap &jsonMap); QNetworkAccessManager *nam; - QPointer reply; + QNetworkReply *reply; }; #endif // COCKATRICE_HANDLE_COMMANDER_BRACKETS_H diff --git a/cockatrice/src/interface/widgets/tabs/api/edhrec/display/cards/edhrec_api_response_card_details_display_widget.cpp b/cockatrice/src/interface/widgets/tabs/api/edhrec/display/cards/edhrec_api_response_card_details_display_widget.cpp index 2b76de973..b2ddace01 100644 --- a/cockatrice/src/interface/widgets/tabs/api/edhrec/display/cards/edhrec_api_response_card_details_display_widget.cpp +++ b/cockatrice/src/interface/widgets/tabs/api/edhrec/display/cards/edhrec_api_response_card_details_display_widget.cpp @@ -1,6 +1,5 @@ #include "edhrec_api_response_card_details_display_widget.h" -#include "../../../../../../theme_manager.h" #include "../../../../../cards/card_info_picture_widget.h" #include "../../../../../cards/card_size_widget.h" #include "../../../../../general/display/background_plate_widget.h" @@ -10,7 +9,6 @@ #include "edhrec_api_response_card_synergy_display_widget.h" #include "libcockatrice/card/database/card_database_querier.h" -#include #include #include #include @@ -34,9 +32,7 @@ EdhrecApiResponseCardDetailsDisplayWidget::EdhrecApiResponseCardDetailsDisplayWi nameLabel = new QLabel(this); nameLabel->setText(toDisplay.name); nameLabel->setAlignment(Qt::AlignHCenter); - applyCardTextFont(); - connect(themeManager, &ThemeManager::fontsChanged, this, - &EdhrecApiResponseCardDetailsDisplayWidget::applyCardTextFont); + nameLabel->setStyleSheet("font-size: 20px; font-weight: bold"); inclusionDisplayWidget = new EdhrecApiResponseCardInclusionDisplayWidget(this, toDisplay); @@ -90,14 +86,3 @@ void EdhrecApiResponseCardDetailsDisplayWidget::actRequestPageNavigation() { emit requestUrl(toDisplay.url); } - -void EdhrecApiResponseCardDetailsDisplayWidget::applyCardTextFont() -{ - // Family comes from the CardText theme role; the 20px size and bold - // weight previously carried by the stylesheet stay caller-side. Pixel - // size, not point size: the stylesheet said 20px, and 20pt is ~27px. - QFont cardFont = themeManager->appFont(AppFont::CardText); - cardFont.setPixelSize(20); - cardFont.setWeight(QFont::Bold); - nameLabel->setFont(cardFont); -} diff --git a/cockatrice/src/interface/widgets/tabs/api/edhrec/display/cards/edhrec_api_response_card_details_display_widget.h b/cockatrice/src/interface/widgets/tabs/api/edhrec/display/cards/edhrec_api_response_card_details_display_widget.h index 8a3c8fba4..d738f4685 100644 --- a/cockatrice/src/interface/widgets/tabs/api/edhrec/display/cards/edhrec_api_response_card_details_display_widget.h +++ b/cockatrice/src/interface/widgets/tabs/api/edhrec/display/cards/edhrec_api_response_card_details_display_widget.h @@ -30,9 +30,6 @@ public slots: signals: void requestUrl(QString url); -private slots: - void applyCardTextFont(); - private: EdhrecApiResponseCardDetails toDisplay; QVBoxLayout *layout; diff --git a/cockatrice/src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_api_response_commander_details_display_widget.cpp b/cockatrice/src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_api_response_commander_details_display_widget.cpp index df0319f88..2ff22f2b3 100644 --- a/cockatrice/src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_api_response_commander_details_display_widget.cpp +++ b/cockatrice/src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_api_response_commander_details_display_widget.cpp @@ -1,6 +1,5 @@ #include "edhrec_api_response_commander_details_display_widget.h" -#include "../../../../../../theme_manager.h" #include "../../../../../cards/card_info_picture_widget.h" #include "../../../../../cards/card_size_widget.h" #include "../../api_response/cards/edhrec_commander_api_response_commander_details.h" @@ -9,7 +8,6 @@ #include "edhrec_commander_api_response_navigation_widget.h" #include "libcockatrice/card/database/card_database_querier.h" -#include #include #include #include @@ -50,16 +48,15 @@ EdhrecCommanderResponseCommanderDetailsDisplayWidget::EdhrecCommanderResponseCom commanderName = new QLabel(this); commanderName->setText(commanderDetails.getName()); commanderName->setAlignment(Qt::AlignCenter); + commanderName->setStyleSheet("font-size: 28px; font-weight: bold"); label = new QLabel(this); label->setAlignment(Qt::AlignCenter); + label->setStyleSheet("font-size: 16px"); salt = new QLabel(this); salt->setAlignment(Qt::AlignCenter); - - applyCardTextFont(); - connect(themeManager, &ThemeManager::fontsChanged, this, - &EdhrecCommanderResponseCommanderDetailsDisplayWidget::applyCardTextFont); + salt->setStyleSheet("font-size: 16px"); cardPricesDisplayWidget = new EdhrecApiResponseCardPricesDisplayWidget(this, commanderDetails.getPrices()); @@ -86,19 +83,3 @@ void EdhrecCommanderResponseCommanderDetailsDisplayWidget::retranslateUi() label->setText(commanderDetails.getLabel()); salt->setText(tr("Salt: ") + QString::number(commanderDetails.getSalt())); } - -void EdhrecCommanderResponseCommanderDetailsDisplayWidget::applyCardTextFont() -{ - // Family comes from the CardText theme role; the sizes and the bold - // weight previously carried by the stylesheets stay caller-side. Pixel - // sizes, not point sizes: the stylesheets said 28px and 16px. - QFont nameFont = themeManager->appFont(AppFont::CardText); - nameFont.setPixelSize(28); - nameFont.setWeight(QFont::Bold); - commanderName->setFont(nameFont); - - QFont detailsFont = themeManager->appFont(AppFont::CardText); - detailsFont.setPixelSize(16); - label->setFont(detailsFont); - salt->setFont(detailsFont); -} diff --git a/cockatrice/src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_api_response_commander_details_display_widget.h b/cockatrice/src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_api_response_commander_details_display_widget.h index 99e6b3d43..197263512 100644 --- a/cockatrice/src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_api_response_commander_details_display_widget.h +++ b/cockatrice/src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_api_response_commander_details_display_widget.h @@ -30,9 +30,6 @@ public: QString baseUrl); void retranslateUi(); -private slots: - void applyCardTextFont(); - private: EdhrecCommanderApiResponseCommanderDetails commanderDetails; QHBoxLayout *layout; diff --git a/cockatrice/src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_bracket_navigation_widget.cpp b/cockatrice/src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_bracket_navigation_widget.cpp index 749476619..b26a7e574 100644 --- a/cockatrice/src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_bracket_navigation_widget.cpp +++ b/cockatrice/src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_bracket_navigation_widget.cpp @@ -1,9 +1,6 @@ #include "edhrec_commander_api_response_bracket_navigation_widget.h" -#include "../../interface/theme_manager.h" - #include -#include #include #include #include @@ -28,7 +25,7 @@ EdhrecCommanderApiResponseBracketNavigationWidget::EdhrecCommanderApiResponseBra QString label = option.isEmpty() ? "All" : option.at(0).toUpper() + option.mid(1); QPushButton *optionButton = new QPushButton(label, this); optionButton->setMinimumHeight(84); - optionButton->setStyleSheet(QString()); + optionButton->setStyleSheet("font-size: 24px"); gameChangerButtons[option] = optionButton; layout->addWidget(optionButton, 2, i); connect(optionButton, &QPushButton::clicked, this, [=, this]() { @@ -38,10 +35,6 @@ EdhrecCommanderApiResponseBracketNavigationWidget::EdhrecCommanderApiResponseBra }); } - applyInterfaceDefaultFont(); - connect(themeManager, &ThemeManager::fontsChanged, this, - &EdhrecCommanderApiResponseBracketNavigationWidget::applyInterfaceDefaultFont); - updateOptionButtonSelection(gameChangerButtons, ""); retranslateUi(); @@ -101,18 +94,8 @@ void EdhrecCommanderApiResponseBracketNavigationWidget::updateOptionButtonSelect const QString &selectedKey) { for (auto it = buttons.begin(); it != buttons.end(); ++it) { - const bool selected = (it.key() == selectedKey); - it.value()->setStyleSheet(selected ? QStringLiteral("background-color: lightblue;") : QString()); - QFont buttonFont = themeManager->appFont(AppFont::InterfaceDefault); - buttonFont.setPixelSize(24); - buttonFont.setWeight(selected ? QFont::Bold : QFont::Normal); - it.value()->setFont(buttonFont); + it.value()->setStyleSheet(it.key() == selectedKey + ? "background-color: lightblue; font-weight: bold; font-size: 24px;" + : "font-size: 24px"); } -} - -void EdhrecCommanderApiResponseBracketNavigationWidget::applyInterfaceDefaultFont() -{ - // Family comes from the InterfaceDefault theme role; the 24px size stays - // caller-side and the selected button keeps its bold emphasis. - updateOptionButtonSelection(gameChangerButtons, selectedGameChanger); } \ No newline at end of file diff --git a/cockatrice/src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_bracket_navigation_widget.h b/cockatrice/src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_bracket_navigation_widget.h index aa4e61ff1..a741b6696 100644 --- a/cockatrice/src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_bracket_navigation_widget.h +++ b/cockatrice/src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_bracket_navigation_widget.h @@ -37,9 +37,6 @@ private: QMap gameChangerButtons; void updateOptionButtonSelection(QMap &buttons, const QString &selectedKey); - -private slots: - void applyInterfaceDefaultFont(); }; #endif // COCKATRICE_EDHREC_COMMANDER_API_RESPONSE_BRACKET_NAVIGATION_WIDGET_H diff --git a/cockatrice/src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_budget_navigation_widget.cpp b/cockatrice/src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_budget_navigation_widget.cpp index 27cd1c51f..48704d010 100644 --- a/cockatrice/src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_budget_navigation_widget.cpp +++ b/cockatrice/src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_budget_navigation_widget.cpp @@ -1,9 +1,6 @@ #include "edhrec_commander_api_response_budget_navigation_widget.h" -#include "../../interface/theme_manager.h" - #include -#include #include #include #include @@ -28,7 +25,7 @@ EdhrecCommanderApiResponseBudgetNavigationWidget::EdhrecCommanderApiResponseBudg QString label = option.isEmpty() ? "Any" : option.at(0).toUpper() + option.mid(1); QPushButton *btn = new QPushButton(label, this); btn->setMinimumHeight(84); - btn->setStyleSheet(QString()); + btn->setStyleSheet("font-size: 24px"); budgetButtons[option] = btn; layout->addWidget(btn, 4, i); connect(btn, &QPushButton::clicked, this, [=, this]() { @@ -38,10 +35,6 @@ EdhrecCommanderApiResponseBudgetNavigationWidget::EdhrecCommanderApiResponseBudg }); } - applyInterfaceDefaultFont(); - connect(themeManager, &ThemeManager::fontsChanged, this, - &EdhrecCommanderApiResponseBudgetNavigationWidget::applyInterfaceDefaultFont); - updateOptionButtonSelection(budgetButtons, ""); retranslateUi(); @@ -103,18 +96,8 @@ void EdhrecCommanderApiResponseBudgetNavigationWidget::updateOptionButtonSelecti const QString &selectedKey) { for (auto it = buttons.begin(); it != buttons.end(); ++it) { - const bool selected = (it.key() == selectedKey); - it.value()->setStyleSheet(selected ? QStringLiteral("background-color: lightblue;") : QString()); - QFont buttonFont = themeManager->appFont(AppFont::InterfaceDefault); - buttonFont.setPixelSize(24); - buttonFont.setWeight(selected ? QFont::Bold : QFont::Normal); - it.value()->setFont(buttonFont); + it.value()->setStyleSheet(it.key() == selectedKey + ? "background-color: lightblue; font-weight: bold; font-size: 24px;" + : "font-size: 24px"); } -} - -void EdhrecCommanderApiResponseBudgetNavigationWidget::applyInterfaceDefaultFont() -{ - // Family comes from the InterfaceDefault theme role; the 24px size stays - // caller-side and the selected button keeps its bold emphasis. - updateOptionButtonSelection(budgetButtons, selectedBudget); } \ No newline at end of file diff --git a/cockatrice/src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_budget_navigation_widget.h b/cockatrice/src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_budget_navigation_widget.h index d727225a5..8013d1d6b 100644 --- a/cockatrice/src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_budget_navigation_widget.h +++ b/cockatrice/src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_budget_navigation_widget.h @@ -37,9 +37,6 @@ private: QMap budgetButtons; void updateOptionButtonSelection(QMap &buttons, const QString &selectedKey); - -private slots: - void applyInterfaceDefaultFont(); }; #endif // COCKATRICE_EDHREC_COMMANDER_API_RESPONSE_BUDGET_NAVIGATION_WIDGET_H diff --git a/cockatrice/src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_navigation_widget.cpp b/cockatrice/src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_navigation_widget.cpp index 57070a5e1..220dcfbda 100644 --- a/cockatrice/src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_navigation_widget.cpp +++ b/cockatrice/src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_navigation_widget.cpp @@ -1,11 +1,9 @@ #include "edhrec_commander_api_response_navigation_widget.h" -#include "../../interface/theme_manager.h" #include "../../tab_edhrec_main.h" #include "edhrec_commander_api_response_bracket_navigation_widget.h" #include "edhrec_commander_api_response_budget_navigation_widget.h" -#include #include #include #include @@ -32,14 +30,10 @@ EdhrecCommanderApiResponseNavigationWidget::EdhrecCommanderApiResponseNavigation comboPushButton = new QPushButton(this); comboPushButton->setMinimumHeight(84); - comboPushButton->setStyleSheet(QString()); + comboPushButton->setStyleSheet("font-size: 24px"); averageDeckPushButton = new QPushButton(this); averageDeckPushButton->setMinimumHeight(84); - averageDeckPushButton->setStyleSheet(QString()); - - applyInterfaceDefaultFont(); - connect(themeManager, &ThemeManager::fontsChanged, this, - &EdhrecCommanderApiResponseNavigationWidget::applyInterfaceDefaultFont); + averageDeckPushButton->setStyleSheet("font-size: 24px"); layout->addWidget(comboPushButton, 0, 0, 1, 1); layout->addWidget(averageDeckPushButton, 0, 1, 1, 1); @@ -67,15 +61,6 @@ void EdhrecCommanderApiResponseNavigationWidget::retranslateUi() averageDeckPushButton->setText(tr("Average Deck")); } -void EdhrecCommanderApiResponseNavigationWidget::applyInterfaceDefaultFont() -{ - // Family comes from the InterfaceDefault theme role; the 24px size stays caller-side. - QFont navFont = themeManager->appFont(AppFont::InterfaceDefault); - navFont.setPixelSize(24); - comboPushButton->setFont(navFont); - averageDeckPushButton->setFont(navFont); -} - QString EdhrecCommanderApiResponseNavigationWidget::addNavigationOptionsToUrl(QString baseUrl) { if (!bracketNavigationWidget->getSelectedGameChanger().isEmpty()) { diff --git a/cockatrice/src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_navigation_widget.h b/cockatrice/src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_navigation_widget.h index 456228554..737558625 100644 --- a/cockatrice/src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_navigation_widget.h +++ b/cockatrice/src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_navigation_widget.h @@ -35,9 +35,6 @@ public slots: signals: void requestUrl(QString url); -private slots: - void applyInterfaceDefaultFont(); - private: QGridLayout *layout; diff --git a/cockatrice/src/interface/widgets/tabs/tab_deck_editor.cpp b/cockatrice/src/interface/widgets/tabs/tab_deck_editor.cpp index e88b2aad6..7ca179cea 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_deck_editor.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_deck_editor.cpp @@ -131,16 +131,10 @@ void TabDeckEditor::loadLayout() LayoutsSettings &layouts = SettingsCache::instance().layouts(); auto layoutState = layouts.getDeckEditorLayoutState(); - // Start with every current dock placed. Saved layouts from older versions - // cannot describe docks introduced later; restoreState leaves those docks - // in their existing positions while restoring the saved docks, including - // their visibility and floating state. - restartLayout(); - if (!layoutState.isEmpty()) { - if (!restoreState(layoutState)) { - // A corrupt or incompatible state must not leave a partial layout. - restartLayout(); - } + if (layoutState.isNull()) { + restartLayout(); + } else { + restoreState(layoutState); restoreGeometry(layouts.getDeckEditorGeometry()); } } diff --git a/cockatrice/src/interface/widgets/tabs/tab_deck_storage.cpp b/cockatrice/src/interface/widgets/tabs/tab_deck_storage.cpp index 8af9d5e6e..3dfdc956b 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_deck_storage.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_deck_storage.cpp @@ -2,7 +2,7 @@ #include "../../../client/settings/cache_settings.h" #include "../../deck_loader/deck_loader.h" -#include "../../theme_icon.h" +#include "../../pixel_map_generator.h" #include "../cards/additional_info/deck_color_identity.h" #include "../deck_share/deck_share_utils.h" #include "../deck_share/share_bar_widget.h" @@ -187,19 +187,19 @@ TabDeckStorage::TabDeckStorage(TabSupervisor *_tabSupervisor, // Left side actions aOpenLocalDeck = new QAction(this); - aOpenLocalDeck->setIcon(loadThemedIcon(QStringLiteral("icons/pencil"))); + aOpenLocalDeck->setIcon(themePixmap(QStringLiteral("icons/pencil"))); connect(aOpenLocalDeck, &QAction::triggered, this, &TabDeckStorage::actOpenLocalDeck); aRenameLocal = new QAction(this); - aRenameLocal->setIcon(loadThemedIcon(QStringLiteral("icons/rename"))); + aRenameLocal->setIcon(themePixmap(QStringLiteral("icons/rename"))); connect(aRenameLocal, &QAction::triggered, this, &TabDeckStorage::actRenameLocal); aUpload = new QAction(this); - aUpload->setIcon(loadThemedIcon(QStringLiteral("icons/arrow_right"))); + aUpload->setIcon(themePixmap(QStringLiteral("icons/arrow_right_green"))); connect(aUpload, &QAction::triggered, this, &TabDeckStorage::actUpload); aNewLocalFolder = new QAction(this); aNewLocalFolder->setIcon(qApp->style()->standardIcon(QStyle::SP_FileDialogNewFolder)); connect(aNewLocalFolder, &QAction::triggered, this, &TabDeckStorage::actNewLocalFolder); aDeleteLocalDeck = new QAction(this); - aDeleteLocalDeck->setIcon(loadThemedIcon(QStringLiteral("icons/remove_row"))); + aDeleteLocalDeck->setIcon(themePixmap(QStringLiteral("icons/remove_row"))); connect(aDeleteLocalDeck, &QAction::triggered, this, &TabDeckStorage::actDeleteLocalDeck); aOpenDecksFolder = new QAction(this); @@ -208,24 +208,24 @@ TabDeckStorage::TabDeckStorage(TabSupervisor *_tabSupervisor, // Right side actions aOpenRemoteDeck = new QAction(this); - aOpenRemoteDeck->setIcon(loadThemedIcon(QStringLiteral("icons/pencil"))); + aOpenRemoteDeck->setIcon(themePixmap(QStringLiteral("icons/pencil"))); connect(aOpenRemoteDeck, &QAction::triggered, this, &TabDeckStorage::actOpenRemoteDeck); aDownload = new QAction(this); - aDownload->setIcon(loadThemedIcon(QStringLiteral("icons/arrow_left"))); + aDownload->setIcon(themePixmap(QStringLiteral("icons/arrow_left_green"))); connect(aDownload, &QAction::triggered, this, &TabDeckStorage::actDownload); aNewFolder = new QAction(this); aNewFolder->setIcon(qApp->style()->standardIcon(QStyle::SP_FileDialogNewFolder)); connect(aNewFolder, &QAction::triggered, this, &TabDeckStorage::actNewFolder); aDeleteRemoteDeck = new QAction(this); - aDeleteRemoteDeck->setIcon(loadThemedIcon(QStringLiteral("icons/remove_row"))); + aDeleteRemoteDeck->setIcon(themePixmap(QStringLiteral("icons/remove_row"))); connect(aDeleteRemoteDeck, &QAction::triggered, this, &TabDeckStorage::actDeleteRemoteDeck); aShareDecks = new QAction(this); - aShareDecks->setIcon(loadThemedIcon(QStringLiteral("icons/share"))); + aShareDecks->setIcon(themePixmap(QStringLiteral("icons/share"))); connect(aShareDecks, &QAction::triggered, this, &TabDeckStorage::actShareDecks); aPublishDeck = new QAction(this); - aPublishDeck->setIcon(loadThemedIcon(QStringLiteral("icons/lock"))); + aPublishDeck->setIcon(QPixmap("theme:icons/lock")); connect(aPublishDeck, &QAction::triggered, this, &TabDeckStorage::actPublishDeck); // Add actions to toolbars diff --git a/cockatrice/src/interface/widgets/tabs/tab_developer.cpp b/cockatrice/src/interface/widgets/tabs/tab_developer.cpp index 024cd46f0..d641a1ea5 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_developer.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_developer.cpp @@ -6,7 +6,6 @@ #include "tab_developer.h" -#include "../interface/theme_manager.h" #include "libcockatrice/protocol/pb/response.pb.h" #include @@ -101,20 +100,6 @@ TabDeveloper::TabDeveloper(TabSupervisor *_tabSupervisor, AbstractClient *_clien central->setLayout(mainLayout); setCentralWidget(central); - // Separator rows store their font on the item; re-apply the role family on - // font changes (value cells keep the default font and need only a repaint). - connect(themeManager, &ThemeManager::fontsChanged, this, [this] { - QFont separatorFont = themeManager->appFont(AppFont::InterfaceDefault); - separatorFont.setBold(true); - for (int row = 0; row < statsTable->rowCount(); ++row) { - auto *labelItem = statsTable->item(row, 0); - if (labelItem && !(labelItem->flags() & Qt::ItemIsSelectable)) { - labelItem->setFont(separatorFont); - } - } - statsTable->viewport()->update(); - }); - retranslateUi(); } @@ -170,8 +155,7 @@ void TabDeveloper::appendSeparatorRow(const QString §ionTitle) const int row = statsTable->rowCount(); statsTable->insertRow(row); auto *labelItem = new QTableWidgetItem(sectionTitle); - // Family comes from the InterfaceDefault theme role; bold stays caller-side. - QFont font = themeManager->appFont(AppFont::InterfaceDefault); + auto font = labelItem->font(); font.setBold(true); labelItem->setFont(font); labelItem->setFlags(labelItem->flags() & ~Qt::ItemIsSelectable); diff --git a/cockatrice/src/interface/widgets/tabs/tab_game.cpp b/cockatrice/src/interface/widgets/tabs/tab_game.cpp index 01d021907..9e724727f 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_game.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_game.cpp @@ -20,14 +20,12 @@ #include "../interface/widgets/cards/card_info_frame_widget.h" #include "../interface/widgets/dialogs/dlg_create_game.h" #include "../interface/widgets/dialogs/dlg_invite_to_game.h" -#include "../interface/widgets/dialogs/dlg_turn_order.h" #include "../interface/widgets/server/game_link.h" #include "../interface/widgets/server/user/user_list_manager.h" #include "../interface/widgets/utility/completer_utils.h" #include "../interface/widgets/utility/line_edit_completer.h" #include "../main.h" #include "../menus/tearoff_menu.h" -#include "../replay/replay_event_sidebar.h" #include "../replay/replay_widget.h" #include "../utility/visibility_change_listener.h" #include "card/card_completer_proxy_model.h" @@ -99,9 +97,8 @@ class Message; } // namespace google TabGame::TabGame(TabSupervisor *_tabSupervisor, GameReplay *_replay) - : Tab(_tabSupervisor), sayLabel(nullptr), sayEdit(nullptr), replayDock(nullptr), historyDock(nullptr) + : Tab(_tabSupervisor), sayLabel(nullptr), sayEdit(nullptr) { - setObjectName("TabGame"); // THIS CTOR IS USED ON REPLAY game = new Replay(this, _replay, tabSupervisor->getIsLocalGame(), &SettingsCache::instance().userInterface(), new SettingsCardPreferenceProvider(this)); @@ -116,11 +113,9 @@ TabGame::TabGame(TabSupervisor *_tabSupervisor, GameReplay *_replay) addDockWidget(Qt::RightDockWidgetArea, cardInfoDock); addDockWidget(Qt::RightDockWidgetArea, playerListDock); addDockWidget(Qt::RightDockWidgetArea, messageLayoutDock); - addDockWidget(Qt::LeftDockWidgetArea, historyDock); addDockWidget(Qt::BottomDockWidgetArea, replayDock); mainWidget = new QStackedWidget(this); - mainWidget->setObjectName("mainWidget"); mainWidget->addWidget(deckViewContainerWidget); mainWidget->addWidget(gamePlayAreaWidget); setCentralWidget(mainWidget); @@ -149,7 +144,6 @@ TabGame::TabGame(TabSupervisor *_tabSupervisor, const QMap &_roomGameTypes) : Tab(_tabSupervisor), userListProxy(_tabSupervisor->getUserListManager()) { - setObjectName("TabGame"); // THIS CTOR IS USED ON GAMES game = new Game(this, tabSupervisor->getIsLocalGame(), _clients, event, _roomGameTypes, &SettingsCache::instance().userInterface(), new SettingsCardPreferenceProvider(this)); @@ -160,14 +154,12 @@ TabGame::TabGame(TabSupervisor *_tabSupervisor, createPlayAreaWidget(); createDeckViewContainerWidget(); replayDock = nullptr; - historyDock = nullptr; addDockWidget(Qt::RightDockWidgetArea, cardInfoDock); addDockWidget(Qt::RightDockWidgetArea, playerListDock); addDockWidget(Qt::RightDockWidgetArea, messageLayoutDock); mainWidget = new QStackedWidget(this); - mainWidget->setObjectName("mainWidget"); mainWidget->addWidget(deckViewContainerWidget); mainWidget->addWidget(gamePlayAreaWidget); mainWidget->setContentsMargins(0, 0, 0, 0); @@ -199,7 +191,6 @@ void TabGame::connectToGameState() { connect(game->getGameState(), &GameState::gameStarted, this, &TabGame::startGame); connect(game->getGameState(), &GameState::gameStopped, this, &TabGame::stopGame); - connect(game->getGameState(), &GameState::hostChanged, this, &TabGame::updateTurnOrderAction); connect(game->getGameState(), &GameState::activePhaseChanged, this, &TabGame::setActivePhase); connect(game->getGameState(), &GameState::activePlayerChanged, this, &TabGame::setActivePlayer); } @@ -268,8 +259,6 @@ void TabGame::connectMessageLogToGameEventHandler() connect(game->getGameEventHandler(), &GameEventHandler::logTurnReversed, messageLog, &MessageLogWidget::logReverseTurn); - connect(game->getGameEventHandler(), &GameEventHandler::logTurnOrderChanged, messageLog, - &MessageLogWidget::logTurnOrderChanged); connect(game->getGameEventHandler(), &GameEventHandler::logConcede, messageLog, &MessageLogWidget::logConcede); connect(game->getGameEventHandler(), &GameEventHandler::logUnconcede, messageLog, &MessageLogWidget::logUnconcede); @@ -356,9 +345,6 @@ void TabGame::retranslateUi() } cardInfoDock->setWindowTitle(tr("Card Info") + (cardInfoDock->isWindow() ? tabText : QString())); messageLayoutDock->setWindowTitle(tr("Messages") + (messageLayoutDock->isWindow() ? tabText : QString())); - if (historyDock) { - historyDock->setWindowTitle(tr("Game History") + (historyDock->isWindow() ? tabText : QString())); - } if (replayDock) { replayDock->setWindowTitle(tr("Replay Timeline") + (replayDock->isWindow() ? tabText : QString())); } @@ -383,9 +369,6 @@ void TabGame::retranslateUi() if (aReverseTurn) { aReverseTurn->setText(tr("Reverse turn order")); } - if (aTurnOrder) { - aTurnOrder->setText(tr("&Set turn order...")); - } if (aRemoveLocalArrows) { aRemoveLocalArrows->setText(tr("&Remove all local arrows")); } @@ -435,9 +418,6 @@ void TabGame::retranslateUi() dockToActions[messageLayoutDock].menu->setTitle(tr("Messages")); dockToActions[playerListDock].menu->setTitle(tr("Player List")); - if (historyDock) { - dockToActions[historyDock].menu->setTitle(tr("Game History")); - } if (replayDock) { dockToActions[replayDock].menu->setTitle(tr("Replay Timeline")); } @@ -570,10 +550,8 @@ void TabGame::adminLockChanged(bool lock) void TabGame::actGameInfo() { - // Heap-allocated so that destroying this tab while the dialog is open cannot delete a stack object. - auto *dlg = new DlgCreateGame(game->getGameMetaInfo()->proto(), game->getGameMetaInfo()->getRoomGameTypes(), this); - dlg->setAttribute(Qt::WA_DeleteOnClose); - dlg->open(); + DlgCreateGame dlg(game->getGameMetaInfo()->proto(), game->getGameMetaInfo()->getRoomGameTypes(), this); + dlg.exec(); } void TabGame::actCopyGameLink() @@ -647,73 +625,6 @@ void TabGame::actConcede() } } -void TabGame::actTurnOrder() -{ - if (!canSetTurnOrder()) { - return; - } - - QStringList playerNames; - const QList seatOrder = game->getGameState()->getSeatOrder(); - const auto &players = game->getPlayerManager()->getPlayers(); - if (!seatOrder.isEmpty()) { - for (int playerId : seatOrder) { - if (PlayerLogic *player = players.value(playerId, nullptr)) { - playerNames.append(player->getPlayerInfo()->getName()); - } - } - // Include any players missing from the tracked order rather than dropping them. - for (auto it = players.constBegin(); it != players.constEnd(); ++it) { - const QString name = it.value()->getPlayerInfo()->getName(); - if (!playerNames.contains(name)) { - playerNames.append(name); - } - } - } else { - for (PlayerLogic *player : players) { - playerNames.append(player->getPlayerInfo()->getName()); - } - } - - DlgTurnOrder dlg(playerNames, this); - if (dlg.exec() != QDialog::Accepted) { - return; - } - - game->getGameEventHandler()->handleSetPlayerOrder(dlg.order(), dlg.isRandomizeRequested()); -} - -/** - * Whether the local user may change the seating order: only while the game has - * not started, and only as the host or a moderator - the server allows both - * (Server_AbstractParticipant::cmdSetPlayerOrder). - */ -bool TabGame::canSetTurnOrder() const -{ - if (game->getGameMetaInfo()->started()) { - return false; - } - if (game->isHost()) { - return true; - } - const ServerInfo_User *userInfo = tabSupervisor->getUserInfo(); - return userInfo != nullptr && (userInfo->user_level() & ServerInfo_User::IsModerator); -} - -/** - * Shows the turn order action only when it can actually change anything: the - * player must be allowed to set the order and the game must not have started. - * Called whenever either condition may have changed (game start/stop, host - * change). - */ -void TabGame::updateTurnOrderAction() -{ - if (aTurnOrder == nullptr) { - return; - } - aTurnOrder->setVisible(canSetTurnOrder()); -} - /** * Confirms the leave game and sends the leave game command, if applicable. * @@ -925,15 +836,6 @@ void TabGame::processPlayerLeave(PlayerLogic *leavingPlayer) } } - // A shuffled local player has their seat changed, so the deck view container - // keyed by their previous id must be cleaned up alongside the leaving player. - int leavingPlayerId = leavingPlayer->getPlayerInfo()->getId(); - if (deckViewContainers.contains(leavingPlayerId)) { - TabbedDeckViewContainer *deckView = deckViewContainers.take(leavingPlayerId); - deckViewContainerLayout->removeWidget(deckView); - deckView->deleteLater(); - } - scene->removePlayer(leavingPlayer); } @@ -1042,7 +944,6 @@ void TabGame::startGame(bool _resuming) aConcede->setText(tr("&Concede")); aConcede->setEnabled(true); } - updateTurnOrderAction(); } void TabGame::stopGame() @@ -1065,7 +966,6 @@ void TabGame::stopGame() aConcede->setText(tr("&Concede")); aConcede->setEnabled(false); } - updateTurnOrderAction(); } void TabGame::closeGame() @@ -1190,9 +1090,6 @@ void TabGame::createMenuItems() connect(aNextTurn, &QAction::triggered, game->getGameEventHandler(), &GameEventHandler::handleNextTurn); aReverseTurn = new QAction(this); connect(aReverseTurn, &QAction::triggered, game->getGameEventHandler(), &GameEventHandler::handleReverseTurn); - aTurnOrder = new QAction(this); - connect(aTurnOrder, &QAction::triggered, this, &TabGame::actTurnOrder); - updateTurnOrderAction(); aRemoveLocalArrows = new QAction(this); connect(aRemoveLocalArrows, &QAction::triggered, this, &TabGame::actRemoveLocalArrows); aRotateViewCW = new QAction(this); @@ -1221,7 +1118,6 @@ void TabGame::createMenuItems() connect(aFocusChat, &QAction::triggered, sayEdit, qOverload<>(&LineEditCompleter::setFocus)); phasesMenu = new TearOffMenu(this); - phasesMenu->setObjectName("phasesMenu"); for (int i = 0; i < phasesToolbar->phaseCount(); ++i) { auto *temp = new QAction(QString(), this); @@ -1232,18 +1128,13 @@ void TabGame::createMenuItems() phasesMenu->addSeparator(); phasesMenu->addAction(aNextPhase); - aNextPhase->setObjectName("aNextPhase"); phasesMenu->addAction(aNextPhaseAction); - aNextPhaseAction->setObjectName("aNextPhaseAction"); gameMenu = new QMenu(this); - gameMenu->setObjectName("gameMenu"); playersSeparator = gameMenu->addSeparator(); gameMenu->addMenu(phasesMenu); gameMenu->addAction(aNextTurn); - aNextTurn->setObjectName("aNextTurn"); gameMenu->addAction(aReverseTurn); - gameMenu->addAction(aTurnOrder); gameMenu->addSeparator(); gameMenu->addAction(aRemoveLocalArrows); gameMenu->addAction(aRotateViewCW); @@ -1272,7 +1163,6 @@ void TabGame::createReplayMenuItems() aNextPhaseAction = nullptr; aNextTurn = nullptr; aReverseTurn = nullptr; - aTurnOrder = nullptr; aRemoveLocalArrows = nullptr; aRotateViewCW = nullptr; aRotateViewCCW = nullptr; @@ -1302,10 +1192,6 @@ void TabGame::createViewMenuItems() registerDockWidget(viewMenu, messageLayoutDock, {250, 200}); registerDockWidget(viewMenu, playerListDock, {250, 50}); - if (historyDock) { - registerDockWidget(viewMenu, historyDock, {280, 400}); - } - if (replayDock) { registerDockWidget(viewMenu, replayDock, {900, 100}); } @@ -1375,12 +1261,6 @@ void TabGame::actResetLayout() addDockWidget(Qt::RightDockWidgetArea, playerListDock); addDockWidget(Qt::RightDockWidgetArea, messageLayoutDock); - if (historyDock) { - historyDock->setVisible(true); - historyDock->setFloating(false); - addDockWidget(Qt::LeftDockWidgetArea, historyDock); - } - if (replayDock) { replayDock->setVisible(true); replayDock->setFloating(false); @@ -1389,9 +1269,6 @@ void TabGame::actResetLayout() cardInfoDock->resize(250, 360); messageLayoutDock->resize(250, 200); playerListDock->resize(250, 50); - if (historyDock) { - historyDock->resize(280, 400); - } replayDock->resize(900, 100); } else { cardInfoDock->resize(250, 360); @@ -1416,7 +1293,6 @@ void TabGame::createPlayAreaWidget(bool bReplay) scene = new GameScene(phasesToolbar, this); connect(game->getPlayerManager(), &PlayerManager::playerConceded, scene, &GameScene::rearrange); connect(game->getPlayerManager(), &PlayerManager::playerCountChanged, scene, &GameScene::rearrange); - connect(game->getGameState(), &GameState::seatOrderChanged, scene, &GameScene::rearrange); connect(scene, &GameScene::arrowDeletionRequested, game->getGameEventHandler(), &GameEventHandler::handleArrowDeletion); connect(game->getGameEventHandler(), &GameEventHandler::arrowDeleted, scene, &GameScene::deleteArrow); @@ -1446,22 +1322,6 @@ void TabGame::createReplayDock(GameReplay *replay) [this](const auto &event, auto options) { game->getGameEventHandler()->processGameEventContainer(event, nullptr, options); }); - - auto *historySidebar = new ReplayEventSidebar(nullptr, replay, replayWidget->getReplayTimeline()); - historyDock = new QDockWidget(this); - historyDock->setObjectName("historyDock"); - historyDock->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetFloatable | - QDockWidget::DockWidgetMovable); - historyDock->setWidget(historySidebar); - historyDock->setFloating(false); - - connect(historySidebar, &ReplayEventSidebar::eventSelected, replayWidget, &ReplayWidget::seekToEvent); - // The sidebar highlights events rather than times, so ask the manager which event the game state has reached. - connect(replayWidget, &ReplayWidget::timeChanged, historySidebar, - [this, historySidebar] { historySidebar->setCurrentEvent(replayWidget->getLastProcessedEventIndex()); }); - - // No event has been replayed yet when the sidebar is connected, so apply the initial state explicitly. - historySidebar->setCurrentEvent(replayWidget->getLastProcessedEventIndex()); } void TabGame::createDeckViewContainerWidget(bool bReplay) diff --git a/cockatrice/src/interface/widgets/tabs/tab_game.h b/cockatrice/src/interface/widgets/tabs/tab_game.h index 142c02999..7bc52202f 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_game.h +++ b/cockatrice/src/interface/widgets/tabs/tab_game.h @@ -50,7 +50,6 @@ class AbstractClient; class GameView; class GameScene; class ReplayWidget; -class ReplayEventSidebar; class CardInfoFrameWidget; class QLabel; class QPushButton; @@ -90,13 +89,12 @@ private: QMap deckViewContainers; QVBoxLayout *deckViewContainerLayout; QWidget *gamePlayAreaWidget, *deckViewContainerWidget; - QDockWidget *cardInfoDock, *messageLayoutDock, *playerListDock, *replayDock, *historyDock; + QDockWidget *cardInfoDock, *messageLayoutDock, *playerListDock, *replayDock; QAction *playersSeparator; QMenu *gameMenu, *viewMenu; TearOffMenu *phasesMenu; QAction *aGameInfo, *aConcede, *aCopyGameLink, *aLeaveGame, *aNextPhase, *aNextPhaseAction, *aNextTurn, - *aReverseTurn, *aTurnOrder, *aRemoveLocalArrows, *aRotateViewCW, *aRotateViewCCW, *aResetLayout, - *aResetReplayLayout; + *aReverseTurn, *aRemoveLocalArrows, *aRotateViewCW, *aRotateViewCCW, *aResetLayout, *aResetReplayLayout; QAction *aFocusChat; QAction *aInviteToGame = nullptr; QList phaseActions; @@ -131,7 +129,6 @@ private: PlayerLogic *setActivePlayer(int id); void setActivePhase(int phase); - bool canSetTurnOrder() const; void createMenuItems(); void createReplayMenuItems(); void createViewMenuItems(); @@ -165,8 +162,6 @@ private slots: void actInviteToGame(); void actConcede(); void actCopyGameLink(); - void actTurnOrder(); - void updateTurnOrderAction(); void actRemoveLocalArrows(); void actRotateViewCW(); void actRotateViewCCW(); diff --git a/cockatrice/src/interface/widgets/tabs/tab_home.cpp b/cockatrice/src/interface/widgets/tabs/tab_home.cpp index 3ad16aa91..af9994fdd 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_home.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_home.cpp @@ -6,7 +6,6 @@ class TabSupervisor; TabHome::TabHome(TabSupervisor *_tabSupervisor, AbstractClient *_client) : Tab(_tabSupervisor), client(_client) { - setObjectName("TabHome"); homeWidget = new HomeWidget(this, tabSupervisor); setCentralWidget(homeWidget); } diff --git a/cockatrice/src/interface/widgets/tabs/tab_public_decks.cpp b/cockatrice/src/interface/widgets/tabs/tab_public_decks.cpp index dfde08f8e..ce37ce440 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_public_decks.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_public_decks.cpp @@ -2,10 +2,8 @@ #include "../../../client/settings/cache_settings.h" #include "../../deck_loader/deck_loader.h" -#include "../../theme_icon.h" #include "../cards/card_size_widget.h" #include "../general/layout_containers/flow_widget.h" -#include "../interface/theme_manager.h" #include "../visual_deck_storage/deck_preview/deck_preview_color_identity_filter_widget.h" #include "../visual_deck_storage/deck_preview/public_deck_preview_widget.h" #include "../visual_deck_storage/remote_public_decks_model.h" @@ -47,25 +45,15 @@ TabPublicDecks::TabPublicDecks(TabSupervisor *_tabSupervisor, AbstractClient *_c cardSize = SettingsCache::instance().cardsDisplay().getVisualDeckStorageCardSize(); titleLabel = new QLabel(tr("Public decks of %1").arg(userName.toHtmlEscaped()), this); - // Family comes from the InterfaceDefault theme role; bold stays caller-side. - auto applyTitleFont = [this]() { - titleLabel->setFont(QtUtils::boldFont(themeManager->appFont(AppFont::InterfaceDefault))); - }; - applyTitleFont(); - connect(themeManager, &ThemeManager::fontsChanged, this, [this, applyTitleFont] { - applyTitleFont(); - // Preview cells paint with the live role font; repaint the grid so the - // new family shows without waiting for a scroll or hover. - flowWidget->update(); - }); + titleLabel->setFont(QtUtils::boldFont(titleLabel->font())); auto *headerLayout = new QHBoxLayout; headerLayout->addWidget(titleLabel); headerLayout->addStretch(1); // Filter/toolbar row, matching the Visual Deck Storage: color identity filter - // first, the search bar stretching in the middle, and the quick-settings button - // at the end. The card size slider lives inside its popup. + // first, the search bar stretching in the middle, and the quick settings + // cogwheel at the end. The card size slider lives inside the cogwheel popup. emptyLabel = new QLabel(tr("This user has not published any decks."), this); emptyLabel->setAlignment(Qt::AlignCenter); emptyLabel->setVisible(false); @@ -80,7 +68,7 @@ TabPublicDecks::TabPublicDecks(TabSupervisor *_tabSupervisor, AbstractClient *_c colorIdentityFilter = new DeckPreviewColorIdentityFilterWidget(this); searchWidget = new VisualDeckStorageSearchWidget(this); refreshButton = new QToolButton(this); - refreshButton->setIcon(loadThemedIcon(QStringLiteral("icons/reload"))); + refreshButton->setIcon(QPixmap("theme:icons/reload")); refreshButton->setFixedSize(32, 32); quickSettingsWidget = new VisualDeckStorageQuickSettingsWidget(this); quickSettingsWidget->setPublicDecksMode(true); diff --git a/cockatrice/src/interface/widgets/tabs/tab_replays.cpp b/cockatrice/src/interface/widgets/tabs/tab_replays.cpp index bd2a69017..d37bff3d8 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_replays.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_replays.cpp @@ -1,7 +1,7 @@ #include "tab_replays.h" #include "../../../client/settings/cache_settings.h" -#include "../../theme_icon.h" +#include "../../pixel_map_generator.h" #include "../interface/widgets/server/remote/remote_replay_list_tree_widget.h" #include "libcockatrice/protocol/pb/commands.pb.h" #include "libcockatrice/protocol/pb/serverinfo_replay.pb.h" @@ -145,17 +145,17 @@ QGroupBox *TabReplays::createLeftLayout() // Left side actions aOpenLocalReplay = new QAction(this); - aOpenLocalReplay->setIcon(loadThemedIcon(QStringLiteral("icons/view"))); + aOpenLocalReplay->setIcon(themePixmap(QStringLiteral("icons/view"))); connect(aOpenLocalReplay, &QAction::triggered, this, &TabReplays::actOpenLocalReplay); connect(localDirView, &QTreeView::doubleClicked, this, &TabReplays::actOpenLocalReplay); aRenameLocal = new QAction(this); - aRenameLocal->setIcon(loadThemedIcon(QStringLiteral("icons/rename"))); + aRenameLocal->setIcon(themePixmap(QStringLiteral("icons/rename"))); connect(aRenameLocal, &QAction::triggered, this, &TabReplays::actRenameLocal); aNewLocalFolder = new QAction(this); aNewLocalFolder->setIcon(qApp->style()->standardIcon(QStyle::SP_FileDialogNewFolder)); connect(aNewLocalFolder, &QAction::triggered, this, &TabReplays::actNewLocalFolder); aDeleteLocalReplay = new QAction(this); - aDeleteLocalReplay->setIcon(loadThemedIcon(QStringLiteral("icons/remove_row"))); + aDeleteLocalReplay->setIcon(themePixmap(QStringLiteral("icons/remove_row"))); connect(aDeleteLocalReplay, &QAction::triggered, this, &TabReplays::actDeleteLocalReplay); aOpenReplaysFolder = new QAction(this); @@ -207,24 +207,24 @@ QGroupBox *TabReplays::createRightLayout() // Right side actions aOpenRemoteReplay = new QAction(this); - aOpenRemoteReplay->setIcon(loadThemedIcon(QStringLiteral("icons/view"))); + aOpenRemoteReplay->setIcon(themePixmap(QStringLiteral("icons/view"))); connect(aOpenRemoteReplay, &QAction::triggered, this, &TabReplays::actOpenRemoteReplay); connect(serverDirView, &QTreeView::doubleClicked, this, &TabReplays::actOpenRemoteReplay); aDownload = new QAction(this); - aDownload->setIcon(loadThemedIcon(QStringLiteral("icons/arrow_left"))); + aDownload->setIcon(themePixmap(QStringLiteral("icons/arrow_left_green"))); connect(aDownload, &QAction::triggered, this, &TabReplays::actDownload); aKeep = new QAction(this); - aKeep->setIcon(loadThemedIcon(QStringLiteral("icons/lock"))); + aKeep->setIcon(themePixmap(QStringLiteral("icons/lock"))); connect(aKeep, &QAction::triggered, this, &TabReplays::actKeepRemoteReplay); aDeleteRemoteReplay = new QAction(this); - aDeleteRemoteReplay->setIcon(loadThemedIcon(QStringLiteral("icons/remove_row"))); + aDeleteRemoteReplay->setIcon(themePixmap(QStringLiteral("icons/remove_row"))); connect(aDeleteRemoteReplay, &QAction::triggered, this, &TabReplays::actDeleteRemoteReplay); aGetReplayCode = new QAction(this); - aGetReplayCode->setIcon(loadThemedIcon(QStringLiteral("icons/share"))); + aGetReplayCode->setIcon(themePixmap(QStringLiteral("icons/share"))); connect(aGetReplayCode, &QAction::triggered, this, &TabReplays::actGetReplayCode); aSubmitReplayCode = new QAction(this); - aSubmitReplayCode->setIcon(loadThemedIcon(QStringLiteral("icons/search"))); + aSubmitReplayCode->setIcon(themePixmap(QStringLiteral("icons/search"))); connect(aSubmitReplayCode, &QAction::triggered, this, &TabReplays::actSubmitReplayCode); // Add actions to toolbars diff --git a/cockatrice/src/interface/widgets/tabs/tab_report.cpp b/cockatrice/src/interface/widgets/tabs/tab_report.cpp index 7986286b3..988c3e7d9 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_report.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_report.cpp @@ -1,6 +1,5 @@ #include "tab_report.h" -#include "../interface/theme_manager.h" #include "../server/user/user_context_menu.h" #include "../utility/report_utils.h" #include "abstract_client.h" @@ -13,7 +12,6 @@ #include #include #include -#include #include #include #include @@ -139,8 +137,7 @@ TabReport::TabReport(TabSupervisor *_tabSupervisor, AbstractClient *_client) : T chatGroup = new QGroupBox; chatLogEdit = new QTextEdit; chatLogEdit->setReadOnly(true); - applyChatLogFont(); - connect(themeManager, &ThemeManager::fontsChanged, this, &TabReport::applyChatLogFont); + chatLogEdit->setFont(QtUtils::monospaceFont()); auto *chatLayout = new QVBoxLayout(chatGroup); chatLayout->setContentsMargins(4, 4, 4, 4); chatLayout->addWidget(chatLogEdit); @@ -272,11 +269,6 @@ TabReport::TabReport(TabSupervisor *_tabSupervisor, AbstractClient *_client) : T refreshList(); } -void TabReport::applyChatLogFont() -{ - chatLogEdit->setFont(report_utils::reportLogFont()); -} - void TabReport::retranslateUi() { userContextMenu->retranslateUi(); diff --git a/cockatrice/src/interface/widgets/tabs/tab_report.h b/cockatrice/src/interface/widgets/tabs/tab_report.h index 43e778f4b..32ebe1a3f 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_report.h +++ b/cockatrice/src/interface/widgets/tabs/tab_report.h @@ -62,7 +62,6 @@ private slots: void statsResponse(const Response &response); void reopenReport(); void reopenResponse(const Response &response); - void applyChatLogFont(); private: int selectedReportId() const; diff --git a/cockatrice/src/interface/widgets/tabs/tab_room.cpp b/cockatrice/src/interface/widgets/tabs/tab_room.cpp index 329732c64..f63b77445 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_room.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_room.cpp @@ -2,7 +2,7 @@ #include "../../../client/settings/cache_settings.h" #include "../../../client/settings/shortcuts_settings.h" -#include "../../theme_icon.h" +#include "../../pixel_map_generator.h" #include "../interface/widgets/dialogs/dlg_settings.h" #include "../interface/widgets/server/chat_view/chat_view.h" #include "../interface/widgets/server/game_link.h" @@ -198,7 +198,7 @@ TabRoom::TabRoom(TabSupervisor *_tabSupervisor, connect(aOpenChatSettings, &QAction::triggered, this, &TabRoom::actOpenChatSettings); auto *chatSettingsButton = new QToolButton; - chatSettingsButton->setIcon(loadThemedIcon(QStringLiteral("icons/settings"))); + chatSettingsButton->setIcon(themePixmap(QStringLiteral("icons/settings"))); chatSettingsButton->setMenu(chatSettingsMenu); chatSettingsButton->setPopupMode(QToolButton::InstantPopup); diff --git a/cockatrice/src/interface/widgets/tabs/tab_supervisor.cpp b/cockatrice/src/interface/widgets/tabs/tab_supervisor.cpp index 45c320852..3316232a4 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_supervisor.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_supervisor.cpp @@ -7,7 +7,6 @@ #include "../../intents/contexts/context_join_room.h" #include "../../intents/intent.h" #include "../../intents/intent_join_server_game.h" -#include "../../theme_icon.h" #include "../interface/pixel_map_generator.h" #include "../interface/widgets/server/game_link.h" #include "../interface/widgets/server/user/user_list_manager.h" @@ -55,7 +54,6 @@ #include #include #include -#include #include #include #include @@ -159,15 +157,6 @@ TabSupervisor::TabSupervisor(AbstractClient *_client, QMenu *tabsMenu, QWidget * tabReplays(nullptr), tabAdmin(nullptr), tabCardArtRules(nullptr), tabLog(nullptr), tabReport(nullptr), tabModeration(nullptr), tabDeveloper(nullptr), isLocalGame(false) { - setObjectName("tabSupervisor"); - - // QTabWidget keeps tab pages in an internal stacked widget. Give it a stable - // name so the GUI tests can address the pages by a readable path - // (tabSupervisor/stackedWidget/). - if (auto *stackedWidget = findChild()) { - stackedWidget->setObjectName("stackedWidget"); - } - setElideMode(Qt::ElideRight); setMovable(true); setIconSize(QSize(15, 15)); @@ -473,9 +462,6 @@ void TabSupervisor::addCloseButtonToTab(Tab *tab, int tabIndex, QAction *manager auto closeSide = static_cast( tabBar()->style()->styleHint(QStyle::SH_TabBar_CloseButtonPosition, nullptr, tabBar())); auto *closeButton = new CloseButton(tab); - // No object name: the tab bar index shifts when tabs move or close, so an - // index-derived name would go stale (or collide). Callers find the button - // through the tab widget instead. if (manager) { // If managed, all close requests should go through the menu action connect(closeButton, &CloseButton::clicked, this, [manager] { checkAndTrigger(manager, false); }); @@ -1361,7 +1347,7 @@ void TabSupervisor::tabUserEvent(bool globalEvent) auto *tab = static_cast(sender()); if (tab != currentWidget()) { tab->setContentsChanged(true); - setTabIcon(indexOf(tab), loadThemedIcon(QStringLiteral("icons/tab_changed"))); + setTabIcon(indexOf(tab), themePixmap(QStringLiteral("icons/tab_changed"))); } if (globalEvent && SettingsCache::instance().userInterface().getNotificationsEnabled()) { QApplication::alert(this); diff --git a/cockatrice/src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual.cpp b/cockatrice/src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual.cpp index 84bf026f7..bf4e15aa6 100644 --- a/cockatrice/src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual.cpp +++ b/cockatrice/src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual.cpp @@ -20,6 +20,7 @@ #include "tab_deck_editor_visual_tab_widget.h" #include +#include #include #include #include @@ -260,25 +261,17 @@ void TabDeckEditorVisual::processMainboardCardClick(const QMouseEvent *event, } } -/** @brief Get a zone name corresponding to keyboard modifiers. */ -static QString zoneForClickModifiers(const Qt::KeyboardModifiers &modifiers) -{ - if (modifiers.testFlag(Qt::ControlModifier)) { - return DECK_ZONE_SIDE; - } else if (modifiers.testFlag(Qt::ShiftModifier)) { - return DECK_ZONE_MAYBEBOARD; - } else { - return DECK_ZONE_MAIN; - } -} - /** @brief Handles clicks on cards in the database display. */ void TabDeckEditorVisual::processDatabaseCardClick(const QMouseEvent *event, const ExactCard &card) { if (event->button() == Qt::LeftButton) { - addCard(card, zoneForClickModifiers(event->modifiers())); + if (QApplication::keyboardModifiers() & Qt::ControlModifier) { + addCard(card, DECK_ZONE_SIDE); + } else { + addCard(card, DECK_ZONE_MAIN); + } } else if (event->button() == Qt::RightButton) { - decrementCard(card, zoneForClickModifiers(event->modifiers())); + decrementCard(card, DECK_ZONE_MAIN); } else if (event->button() == Qt::MiddleButton) { deckDockWidget->actRemoveCard(); } diff --git a/cockatrice/src/interface/widgets/utility/card_completer_delegate.cpp b/cockatrice/src/interface/widgets/utility/card_completer_delegate.cpp index 0090e9d65..a629df43c 100644 --- a/cockatrice/src/interface/widgets/utility/card_completer_delegate.cpp +++ b/cockatrice/src/interface/widgets/utility/card_completer_delegate.cpp @@ -1,6 +1,5 @@ #include "card_completer_delegate.h" -#include "../../interface/theme_manager.h" #include "../../pixel_map_generator.h" #include "../cards/additional_info/mana_cost_widget.h" #include "libcockatrice/card/database/card_database_querier.h" @@ -183,7 +182,7 @@ void CardCompleterDelegate::drawManaSymbol(QPainter *p, QPoint centre, const QSt p->setBrush(Qt::NoBrush); p->drawEllipse(pip.adjusted(1, 1, -1, -1)); - QFont f = QtUtils::boldFont(themeManager->appFont(AppFont::CardText)); + QFont f = QtUtils::boldFont(p->font()); f.setPixelSize(qMax(radius - 1, 7)); p->setFont(f); @@ -430,7 +429,7 @@ void CardCompleterDelegate::paint(QPainter *painter, const QStyleOptionViewItem const QRect nameRect = showInfoBand ? QRect(textLeft, rect.top() + 2, textWidth, 20) : QRect(textLeft, rect.top() + 2, textWidth, rect.height() - 4); - QFont f = themeManager->appFont(AppFont::CardText); + QFont f = option.font; f.setPixelSize(13); f.setBold(true); @@ -452,7 +451,7 @@ void CardCompleterDelegate::paint(QPainter *painter, const QStyleOptionViewItem if (showInfoBand) { const QRect infoRect(textLeft, rect.top() + 22, textWidth, rect.height() - 24); - QFont f = themeManager->appFont(AppFont::CardText); + QFont f = option.font; f.setPixelSize(10); painter->setFont(f); diff --git a/cockatrice/src/interface/widgets/utility/card_completer_styler.cpp b/cockatrice/src/interface/widgets/utility/card_completer_styler.cpp index 1a17bb8be..8f1941412 100644 --- a/cockatrice/src/interface/widgets/utility/card_completer_styler.cpp +++ b/cockatrice/src/interface/widgets/utility/card_completer_styler.cpp @@ -1,6 +1,5 @@ #include "card_completer_styler.h" -#include "../../interface/theme_manager.h" #include "../cards/card_info_picture_enlarged_widget.h" #include "card_completer_delegate.h" #include "libcockatrice/card/database/card_database_querier.h" @@ -74,10 +73,6 @@ CardCompleterStyler::CardCompleterStyler(QCompleter *completer, QObject *parent) popup->viewport()->setMouseTracking(true); - // The delegate reads the CardText role at paint time, so a font change - // only needs a repaint of the popup. - connect(themeManager, &ThemeManager::fontsChanged, popup, [popup] { popup->viewport()->update(); }); - popup->installEventFilter(this); popup->viewport()->installEventFilter(this); diff --git a/cockatrice/src/interface/widgets/utility/report_utils.cpp b/cockatrice/src/interface/widgets/utility/report_utils.cpp index df2400b14..d7fc083c6 100644 --- a/cockatrice/src/interface/widgets/utility/report_utils.cpp +++ b/cockatrice/src/interface/widgets/utility/report_utils.cpp @@ -1,6 +1,5 @@ #include "report_utils.h" -#include "../../theme_manager.h" #include "libcockatrice/protocol/pb/serverinfo_report.pb.h" #include @@ -8,7 +7,6 @@ #include #include #include -#include #include #include #include @@ -21,20 +19,6 @@ namespace report_utils { -QFont reportLogFont() -{ - if (themeManager->hasAppFont(AppFont::ChatLog)) { - return themeManager->appFont(AppFont::ChatLog); - } - QFont monoFont(QStringLiteral("monospace")); - monoFont.setStyleHint(QFont::Monospace); - const int systemPointSize = QFontDatabase::systemFont(QFontDatabase::GeneralFont).pointSize(); - if (systemPointSize > 0) { - monoFont.setPointSize(systemPointSize); - } - return monoFont; -} - namespace { QColor reportStatusColor(const QString &status) diff --git a/cockatrice/src/interface/widgets/utility/report_utils.h b/cockatrice/src/interface/widgets/utility/report_utils.h index 32cc19eac..679989486 100644 --- a/cockatrice/src/interface/widgets/utility/report_utils.h +++ b/cockatrice/src/interface/widgets/utility/report_utils.h @@ -1,7 +1,6 @@ #ifndef REPORT_UTILS_H #define REPORT_UTILS_H -#include #include #include @@ -12,12 +11,6 @@ class ServerInfo_Report; namespace report_utils { -/** @brief Font for the report chat-log views: the theme's ChatLog role when it - * sets one, otherwise the monospace font these views used before the - * role existed. Shared by the three report surfaces so they cannot - * drift apart. */ -QFont reportLogFont(); - QString formatReportCategory(const QString &category); QString formatReportTime(qint64 secondsSinceEpoch); diff --git a/cockatrice/src/interface/widgets/utility/sequence_edit.cpp b/cockatrice/src/interface/widgets/utility/sequence_edit.cpp index b09e6da1e..44e6581e9 100644 --- a/cockatrice/src/interface/widgets/utility/sequence_edit.cpp +++ b/cockatrice/src/interface/widgets/utility/sequence_edit.cpp @@ -2,7 +2,7 @@ #include "../../../client/settings/cache_settings.h" #include "../../../client/settings/shortcuts_settings.h" -#include "../../theme_icon.h" +#include "../../pixel_map_generator.h" #include #include @@ -24,8 +24,8 @@ SequenceEdit::SequenceEdit(const QString &_shortcutName, QWidget *parent) : QWid defaultButton = new QPushButton("", this); lineEdit->setMinimumWidth(70); - clearButton->setIcon(loadThemedIcon(QStringLiteral("icons/clearsearch"))); - defaultButton->setIcon(loadThemedIcon(QStringLiteral("icons/reload"))); + clearButton->setIcon(themePixmap(QStringLiteral("icons/clearsearch"))); + defaultButton->setIcon(themePixmap(QStringLiteral("icons/update"))); auto *layout = QtUtils::makeLayout(this, 1); layout->addWidget(lineEdit); diff --git a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_color_filter_widget.cpp b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_color_filter_widget.cpp index 46330ac46..464d62e4a 100644 --- a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_color_filter_widget.cpp +++ b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_color_filter_widget.cpp @@ -15,23 +15,9 @@ #include #include -namespace -{ - -/** @brief Remove the filters that apply to the given color and attribute from the given model. */ -static void removeFilters(FilterTreeModel *filterModel, QChar color, CardFilter::Attr attr) -{ - for (const CardFilter *filter : filterModel->getFiltersOfType(attr)) { - if (filter->term() == color) { - filterModel->removeFilter(filter); - } - } -} -} // namespace - /** - * This widget provides a graphical control element for the CardFilter::Attr::AttrColor and - * CardFilter::Attr::AttrColorIdentity filters applied to the filter model. + * This widget provides a graphical control element for the CardFilter::Attr::AttrColor filters applied to the filter + * model. * @param parent The Qt Widget that this widget will parent to * @param _filterModel The filter model that this widget will manipulate */ @@ -73,25 +59,11 @@ void VisualDatabaseDisplayColorFilterWidget::retranslateUi() modeComboBox->blockSignals(true); modeComboBox->clear(); - modeComboBox->addItem(tr("Includes"), QVariant::fromValue(FilterMode::Includes)); - modeComboBox->addItem(tr("Excludes"), QVariant::fromValue(FilterMode::Excludes)); - modeComboBox->addItem(tr("Include / Exclude"), QVariant::fromValue(FilterMode::IncludeExclude)); modeComboBox->addItem(tr("Exact match"), QVariant::fromValue(FilterMode::ExactMatch)); - modeComboBox->addItem(tr("Color identity"), QVariant::fromValue(FilterMode::ColorIdentity)); - - modeComboBox->setItemData(0, tr("Cards containing any of the selected colors"), Qt::ToolTipRole); - modeComboBox->setItemData(1, tr("Cards containing none of the selected colors"), Qt::ToolTipRole); - modeComboBox->setItemData(2, tr("Cards containing any of the selected colors and none of the unselected colors"), - Qt::ToolTipRole); - modeComboBox->setItemData(3, tr("Cards being exactly of the selected colors"), Qt::ToolTipRole); - modeComboBox->setItemData(4, tr("Cards whose color identity fits within the selected colors"), Qt::ToolTipRole); - - if (currentMode == FilterMode::Custom) { - addCustomFilterModeItem(); - } + modeComboBox->addItem(tr("Includes"), QVariant::fromValue(FilterMode::Includes)); + modeComboBox->addItem(tr("Include / Exclude"), QVariant::fromValue(FilterMode::IncludeExclude)); modeComboBox->setToolTip(tr("How selected and unselected colors are combined in the filter")); - modeComboBox->setAccessibleName(tr("Color filter mode")); // Restore current mode const int index = modeComboBox->findData(QVariant::fromValue(currentMode)); @@ -102,28 +74,6 @@ void VisualDatabaseDisplayColorFilterWidget::retranslateUi() modeComboBox->blockSignals(false); } -void VisualDatabaseDisplayColorFilterWidget::setFilterMode(FilterMode filterMode) -{ - int index = modeComboBox->findData(QVariant::fromValue(filterMode)); - - if (index < 0 && filterMode == FilterMode::Custom) { - index = modeComboBox->count(); - addCustomFilterModeItem(); - } - - if (index >= 0) { - modeComboBox->setCurrentIndex(index); - } -} - -void VisualDatabaseDisplayColorFilterWidget::addCustomFilterModeItem() -{ - int customIndex = modeComboBox->count(); - modeComboBox->addItem(tr("Custom"), QVariant::fromValue(FilterMode::Custom)); - modeComboBox->setItemData(customIndex, tr("Cards matching an unknown combination of color filters"), - Qt::ToolTipRole); -} - void VisualDatabaseDisplayColorFilterWidget::handleColorToggled(QChar color, bool active) { if (active) { @@ -135,24 +85,74 @@ void VisualDatabaseDisplayColorFilterWidget::handleColorToggled(QChar color, boo void VisualDatabaseDisplayColorFilterWidget::addFilter(QChar color) { - FilterModeInfo info = getFilterModeInfo(currentMode); + QString colorString = color; + QString typeStr; - removeFilters(filterModel, color, info.attr); + // Remove previous filters - if (info.includeType != CardFilter::Type::TypeEnd) { - filterModel->addFilter(new CardFilter(color, info.includeType, info.attr)); + QList allColorFilters = filterModel->getFiltersOfType(CardFilter::Attr::AttrColor); + QList matchingFilters; + + for (const CardFilter *filter : allColorFilters) { + if (filter->term() == color) { + matchingFilters.append(filter); + } + } + + for (const CardFilter *filter : matchingFilters) { + filterModel->removeFilter(filter); + } + + // Add actual filter + + switch (currentMode) { + case FilterMode::ExactMatch: + filterModel->addFilter(new CardFilter(colorString, CardFilter::Type::TypeAnd, CardFilter::Attr::AttrColor)); + break; + + case FilterMode::Includes: + filterModel->addFilter(new CardFilter(colorString, CardFilter::Type::TypeOr, CardFilter::Attr::AttrColor)); + break; + + case FilterMode::IncludeExclude: + filterModel->addFilter(new CardFilter(colorString, CardFilter::Type::TypeOr, CardFilter::Attr::AttrColor)); + break; } } void VisualDatabaseDisplayColorFilterWidget::removeFilter(QChar color) { - FilterModeInfo info = getFilterModeInfo(currentMode); + QString colorString = color; - removeFilters(filterModel, color, info.attr); + // Remove inclusion filters + QList allColorFilters = filterModel->getFiltersOfType(CardFilter::Attr::AttrColor); + QList matchingFilters; - // Add exclusion filters if the mode demands it. - if (info.excludeType != CardFilter::Type::TypeEnd) { - filterModel->addFilter(new CardFilter(color, info.excludeType, info.attr)); + for (const CardFilter *filter : allColorFilters) { + if (filter->term() == color) { + matchingFilters.append(filter); + } + } + + for (const CardFilter *filter : matchingFilters) { + filterModel->removeFilter(filter); + } + + // Add exclusion filters if the mode demands it + switch (currentMode) { + case FilterMode::ExactMatch: + filterModel->addFilter( + new CardFilter(colorString, CardFilter::Type::TypeAndNot, CardFilter::Attr::AttrColor)); + break; + + case FilterMode::IncludeExclude: + filterModel->addFilter( + new CardFilter(colorString, CardFilter::Type::TypeAndNot, CardFilter::Attr::AttrColor)); + break; + + case FilterMode::Includes: + // No exclusion in Includes mode + break; } } @@ -169,7 +169,6 @@ void VisualDatabaseDisplayColorFilterWidget::updateFilterMode() filterModel->filterTree()->blockSignals(true); filterModel->clearFiltersOfType(CardFilter::Attr::AttrColor); - filterModel->clearFiltersOfType(CardFilter::Attr::AttrColorIdentity); const QList manaSymbolWidgets = findChildren(); @@ -212,53 +211,52 @@ QList VisualDatabaseDisplayColorFilterWidget::getActiveColors() void VisualDatabaseDisplayColorFilterWidget::syncWithFilterModel() { - QList filters; - filters.append(filterModel->getFiltersOfType(CardFilter::Attr::AttrColor)); - filters.append(filterModel->getFiltersOfType(CardFilter::Attr::AttrColorIdentity)); + QList allColorFilters = filterModel->getFiltersOfType(CardFilter::Attr::AttrColor); - // Re-derive the mode from the filters in the model, so that a loaded filter set selects the mode that - // produced it (or Custom when none could have). - currentMode = deriveFilterMode(filters, currentMode); + QList manaSymbolWidgets = findChildren(); - // Update the combobox. Add a Custom item only if we have a custom filter mode. - int customIndex = modeComboBox->findData(QVariant::fromValue(FilterMode::Custom)); - modeComboBox->blockSignals(true); - if (currentMode == FilterMode::Custom) { - if (customIndex < 0) { - customIndex = modeComboBox->count(); - addCustomFilterModeItem(); - } - modeComboBox->setCurrentIndex(customIndex); - } else { - if (customIndex >= 0) { - modeComboBox->removeItem(customIndex); - } - int index = modeComboBox->findData(QVariant::fromValue(currentMode)); - if (index >= 0) { - modeComboBox->setCurrentIndex(index); - } - } - modeComboBox->blockSignals(false); - - // Update the mana symbol widgets by looking for the current mode's include or exclude filters. - const FilterModeInfo modeInfo = getFilterModeInfo(currentMode); - - const bool useExclude = - modeInfo.includeType == CardFilter::Type::TypeEnd && modeInfo.excludeType != CardFilter::Type::TypeEnd; - const CardFilter::Type filterType = useExclude ? modeInfo.excludeType : modeInfo.includeType; - - filters = filterModel->getFiltersOfType(modeInfo.attr); - - for (ManaSymbolWidget *manaSymbolWidget : findChildren()) { + for (ManaSymbolWidget *manaSymbolWidget : manaSymbolWidgets) { bool found = false; - for (const CardFilter *filter : filters) { - if (filter->type() == filterType && manaSymbolWidget->getSymbolChar() == filter->term()) { + for (const CardFilter *filter : allColorFilters) { + if (manaSymbolWidget->getSymbolChar() == filter->term()) { + switch (currentMode) { + case FilterMode::ExactMatch: + switch (filter->type()) { + case CardFilter::Type::TypeAnd: + setManaSymbolActive(filter->term().at(0), true); + break; + default: + setManaSymbolActive(filter->term().at(0), false); + break; + } + break; + case FilterMode::Includes: + switch (filter->type()) { + case CardFilter::Type::TypeOr: + setManaSymbolActive(filter->term().at(0), true); + break; + default: + setManaSymbolActive(filter->term().at(0), false); + break; + } + break; + case FilterMode::IncludeExclude: + switch (filter->type()) { + case CardFilter::Type::TypeOr: + setManaSymbolActive(filter->term().at(0), true); + break; + default: + setManaSymbolActive(filter->term().at(0), false); + break; + } + break; + } found = true; - break; } } - manaSymbolWidget->setColorActive(useExclude ? !found : found); - manaSymbolWidget->setDisabled(currentMode == FilterMode::Custom); + if (!found) { + setManaSymbolActive(manaSymbolWidget->getSymbolChar(), false); + } } } diff --git a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_color_filter_widget.h b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_color_filter_widget.h index 561c92d09..184f1f96c 100644 --- a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_color_filter_widget.h +++ b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_color_filter_widget.h @@ -10,8 +10,8 @@ #include #include #include +#include #include -#include #include #include @@ -38,6 +38,15 @@ private: int circleDiameter; }; +enum class FilterMode +{ + ExactMatch, // Only selected colors are included, all others are excluded. + Includes, // Include selected colors (OR condition). + IncludeExclude // Include selected colors (OR) and exclude unselected colors (AND NOT). +}; + +Q_DECLARE_METATYPE(FilterMode) + class VisualDatabaseDisplayColorFilterWidget : public QWidget { Q_OBJECT @@ -45,13 +54,11 @@ class VisualDatabaseDisplayColorFilterWidget : public QWidget public: explicit VisualDatabaseDisplayColorFilterWidget(QWidget *parent, FilterTreeModel *filterModel); void retranslateUi(); - void setFilterMode(FilterMode filterMode); signals: void filterModeChanged(FilterMode filterMode); private slots: - void addCustomFilterModeItem(); void handleColorToggled(QChar color, bool active); void addFilter(QChar color); void removeFilter(QChar color); diff --git a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_filter_toolbar_widget.cpp b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_filter_toolbar_widget.cpp index ecb949b67..5063cce14 100644 --- a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_filter_toolbar_widget.cpp +++ b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_filter_toolbar_widget.cpp @@ -1,6 +1,6 @@ #include "visual_database_display_filter_toolbar_widget.h" -#include "../../theme_icon.h" +#include "../../pixel_map_generator.h" #include "../deck_editor/card_database_view.h" #include "../quick_settings/settings_button_widget.h" #include "card_database_display_model.h" @@ -45,13 +45,13 @@ VisualDatabaseDisplayFilterToolbarWidget::VisualDatabaseDisplayFilterToolbarWidg sortGroupBox = new QGroupBox(this); filterGroupBox = new QGroupBox(this); - auto scalePixmap = [](const QString &prefix) { return loadThemedIcon(prefix).pixmap({20, 20}); }; + auto scalePixmap = [](const QString &fileName) { return QIcon(QPixmap(fileName)).pixmap({20, 20}); }; sortByLabel = new QLabel(this); - sortByLabel->setPixmap(scalePixmap("icons/sort_arrow_down")); + sortByLabel->setPixmap(scalePixmap("theme:icons/sort_arrow_down")); filterByLabel = new QLabel(this); - filterByLabel->setPixmap(scalePixmap("icons/filter")); + filterByLabel->setPixmap(scalePixmap("theme:icons/filter")); sortColumnCombo = new QComboBox(this); sortColumnCombo->setSizeAdjustPolicy(QComboBox::SizeAdjustPolicy::AdjustToContents); @@ -89,22 +89,22 @@ VisualDatabaseDisplayFilterToolbarWidget::VisualDatabaseDisplayFilterToolbarWidg }); quickFilterSaveLoadWidget = new SettingsButtonWidget(this); - quickFilterSaveLoadWidget->setButtonIcon(loadThemedIcon(QStringLiteral("icons/floppy_disk"))); + quickFilterSaveLoadWidget->setButtonIcon(themePixmap(QStringLiteral("icons/floppy_disk"))); quickFilterNameWidget = new SettingsButtonWidget(this); - quickFilterNameWidget->setButtonIcon(loadThemedIcon(QStringLiteral("icons/pen_to_square"))); + quickFilterNameWidget->setButtonIcon(themePixmap(QStringLiteral("icons/pen_to_square"))); quickFilterMainTypeWidget = new SettingsButtonWidget(this); - quickFilterMainTypeWidget->setButtonIcon(loadThemedIcon(QStringLiteral("icons/circle_half_stroke"))); + quickFilterMainTypeWidget->setButtonIcon(themePixmap(QStringLiteral("icons/circle_half_stroke"))); quickFilterSubTypeWidget = new SettingsButtonWidget(this); - quickFilterSubTypeWidget->setButtonIcon(loadThemedIcon(QStringLiteral("icons/dragon"))); + quickFilterSubTypeWidget->setButtonIcon(themePixmap(QStringLiteral("icons/dragon"))); quickFilterSetWidget = new SettingsButtonWidget(this); - quickFilterSetWidget->setButtonIcon(loadThemedIcon(QStringLiteral("icons/scroll"))); + quickFilterSetWidget->setButtonIcon(themePixmap(QStringLiteral("icons/scroll"))); quickFilterFormatLegalityWidget = new SettingsButtonWidget(this); - quickFilterFormatLegalityWidget->setButtonIcon(loadThemedIcon(QStringLiteral("icons/scales"))); + quickFilterFormatLegalityWidget->setButtonIcon(themePixmap(QStringLiteral("icons/scale_balanced"))); retranslateUi(); } diff --git a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_widget.cpp b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_widget.cpp index 6d9b28619..6037418ad 100644 --- a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_widget.cpp +++ b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_widget.cpp @@ -4,9 +4,7 @@ #include "../../../filters/filter_tree_model.h" #include "../../../filters/syntax_help.h" #include "../../key_signals.h" -#include "../../theme_icon.h" -#include "../cards/card_info_picture_widget.h" -#include "../cards/card_info_picture_with_text_overlay_widget.h" +#include "../../pixel_map_generator.h" #include "../cards/card_size_widget.h" #include "../deck_editor/card_database_view.h" #include "../general/layout_containers/flow_widget.h" @@ -81,8 +79,8 @@ VisualDatabaseDisplayWidget::VisualDatabaseDisplayWidget(QWidget *parent, searchEdit->setObjectName("searchEdit"); searchEdit->setPlaceholderText(tr("Search by card name (or search expressions)")); searchEdit->setClearButtonEnabled(true); - searchEdit->addAction(loadThemedIcon(QStringLiteral("icons/search")), QLineEdit::LeadingPosition); - auto help = searchEdit->addAction(loadThemedIcon(QStringLiteral("icons/info")), QLineEdit::TrailingPosition); + searchEdit->addAction(loadColorAdjustedPixmap("theme:icons/search"), QLineEdit::LeadingPosition); + auto help = searchEdit->addAction(themePixmap(QStringLiteral("icons/info")), QLineEdit::TrailingPosition); connect(help, &QAction::triggered, this, [this] { createSearchSyntaxHelpWindow(searchEdit); }); setFocusProxy(searchEdit); @@ -156,11 +154,10 @@ VisualDatabaseDisplayWidget::VisualDatabaseDisplayWidget(QWidget *parent, clearFilterWidget = new QToolButton(); clearFilterWidget->setFixedSize(32, 32); - clearFilterWidget->setIcon(loadThemedIcon(QStringLiteral("icons/delete"))); + clearFilterWidget->setIcon(themePixmap(QStringLiteral("icons/delete"))); connect(clearFilterWidget, &QToolButton::clicked, this, [this] { filterModel->blockSignals(true); filterModel->filterTree()->blockSignals(true); - colorFilterWidget->setFilterMode(FilterMode::Includes); filterModel->clear(); filterModel->blockSignals(false); filterModel->filterTree()->blockSignals(false); diff --git a/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_display_options_widget.cpp b/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_display_options_widget.cpp index f0c054c3a..63ef07835 100644 --- a/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_display_options_widget.cpp +++ b/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_display_options_widget.cpp @@ -1,6 +1,6 @@ #include "visual_deck_display_options_widget.h" -#include "../../theme_icon.h" +#include "../../pixel_map_generator.h" #include "../deck_editor/deck_editor_deck_dock_widget.h" #include "../quick_settings/settings_button_widget.h" #include "../tabs/visual_deck_editor/tab_deck_editor_visual.h" @@ -65,7 +65,7 @@ VisualDeckDisplayOptionsWidget::VisualDeckDisplayOptionsWidget(QWidget *parent) sortByLabel = new QLabel(this); sortCriteriaButton = new SettingsButtonWidget(this); - sortCriteriaButton->setButtonIcon(loadThemedIcon(QStringLiteral("icons/sort_arrow_down"))); + sortCriteriaButton->setButtonIcon(themePixmap(QStringLiteral("icons/sort_arrow_down"))); sortLabel = new QLabel(sortCriteriaButton); sortLabel->setWordWrap(true); @@ -119,11 +119,11 @@ void VisualDeckDisplayOptionsWidget::updateDisplayTypeButton() switch (currentDisplayType) { case DisplayType::Flat: displayTypeButton->setButtonText(tr("Toggle Layout: Flat")); - displayTypeButton->setButtonIcon(loadThemedIcon(QStringLiteral("icons/scroll"))); + displayTypeButton->setButtonIcon(themePixmap(QStringLiteral("icons/scroll"))); break; case DisplayType::Overlap: displayTypeButton->setButtonText(tr("Toggle Layout: Overlap")); - displayTypeButton->setButtonIcon(loadThemedIcon(QStringLiteral("icons/scales"))); + displayTypeButton->setButtonIcon(themePixmap(QStringLiteral("icons/scales"))); break; } } diff --git a/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_editor_placeholder_widget.cpp b/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_editor_placeholder_widget.cpp index d3c087bc6..76c3d0908 100644 --- a/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_editor_placeholder_widget.cpp +++ b/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_editor_placeholder_widget.cpp @@ -1,8 +1,5 @@ #include "visual_deck_editor_placeholder_widget.h" -#include "../interface/theme_manager.h" - -#include #include #include #include @@ -31,12 +28,10 @@ VisualDeckEditorPlaceholderWidget::VisualDeckEditorPlaceholderWidget(QWidget *pa textLabel->setStyleSheet(R"( QLabel { color: palette(mid); + font-size: 14px; padding: 10px; } )"); - applyInterfaceDefaultFont(); - connect(themeManager, &ThemeManager::fontsChanged, this, - &VisualDeckEditorPlaceholderWidget::applyInterfaceDefaultFont); mainLayout->addWidget(imageLabel); mainLayout->addWidget(textLabel); @@ -47,12 +42,4 @@ VisualDeckEditorPlaceholderWidget::VisualDeckEditorPlaceholderWidget(QWidget *pa void VisualDeckEditorPlaceholderWidget::retranslateUi() { textLabel->setText(tr("Add cards using the search bar or database tab to have them appear here")); -} - -void VisualDeckEditorPlaceholderWidget::applyInterfaceDefaultFont() -{ - // Family comes from the InterfaceDefault theme role; the 14px size stays caller-side. - QFont placeholderFont = themeManager->appFont(AppFont::InterfaceDefault); - placeholderFont.setPixelSize(14); - textLabel->setFont(placeholderFont); } \ No newline at end of file diff --git a/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_editor_placeholder_widget.h b/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_editor_placeholder_widget.h index 794be671e..84a3142d8 100644 --- a/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_editor_placeholder_widget.h +++ b/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_editor_placeholder_widget.h @@ -15,9 +15,6 @@ public: explicit VisualDeckEditorPlaceholderWidget(QWidget *parent = nullptr); void retranslateUi(); -private slots: - void applyInterfaceDefaultFont(); - private: QVBoxLayout *mainLayout; QLabel *imageLabel; diff --git a/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_editor_widget.cpp b/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_editor_widget.cpp index 0e0b5e4b4..506ffa2b4 100644 --- a/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_editor_widget.cpp +++ b/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_editor_widget.cpp @@ -1,7 +1,7 @@ #include "visual_deck_editor_widget.h" #include "../../../client/settings/cache_settings.h" -#include "../../theme_icon.h" +#include "../../pixel_map_generator.h" #include "../cards/card_size_widget.h" #include "../cards/deck_card_zone_display_widget.h" #include "../general/layout_containers/flow_widget.h" @@ -144,7 +144,7 @@ void VisualDeckEditorWidget::initializeSearchBarAndCompleter() // Search button functionality searchPushButton = new CompactPushButton(searchContainer); - searchPushButton->setButtonIcon(loadThemedIcon(QStringLiteral("icons/search"))); + searchPushButton->setButtonIcon(themePixmap(QStringLiteral("icons/search"))); connect(searchPushButton, &QPushButton::clicked, this, [this] { addCardFromSearch(); }); searchLayout->addWidget(searchPushButton); diff --git a/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_tag_addition_widget.cpp b/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_tag_addition_widget.cpp index 30e99f401..82ed6191c 100644 --- a/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_tag_addition_widget.cpp +++ b/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_tag_addition_widget.cpp @@ -1,7 +1,5 @@ #include "deck_preview_tag_addition_widget.h" -#include "../../interface/theme_manager.h" - #include #include #include @@ -28,10 +26,6 @@ DeckPreviewTagAdditionWidget::DeckPreviewTagAdditionWidget(QWidget *_parent, QSt // Adjust widget size setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); - - // The tag text reads the InterfaceDefault role at paint time, so a font - // change only needs a repaint. - connect(themeManager, &ThemeManager::fontsChanged, this, [this] { update(); }); } QSize DeckPreviewTagAdditionWidget::sizeHint() const @@ -71,9 +65,8 @@ void DeckPreviewTagAdditionWidget::paintEvent(QPaintEvent *event) painter.setPen(borderPen); painter.drawRect(rect().adjusted(0, 0, -1, -1)); // Adjust for pen width - // Calculate font size based on widget height. Family comes from the - // InterfaceDefault theme role; the height-derived size stays as before. - QFont font = themeManager->appFont(AppFont::InterfaceDefault); + // Calculate font size based on widget height + QFont font = painter.font(); int fontSize = std::max(10, height() / 2); // Ensure a minimum font size of 10 font.setPointSize(fontSize); painter.setFont(font); diff --git a/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_tag_display_widget.cpp b/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_tag_display_widget.cpp index 34809da2e..e62570241 100644 --- a/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_tag_display_widget.cpp +++ b/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_tag_display_widget.cpp @@ -1,7 +1,5 @@ #include "deck_preview_tag_display_widget.h" -#include "../../interface/theme_manager.h" - #include #include #include @@ -43,10 +41,6 @@ DeckPreviewTagDisplayWidget::DeckPreviewTagDisplayWidget(QWidget *parent, const // Adjust widget size setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); - // The tag text reads the InterfaceDefault role at paint time, so a font - // change only needs a repaint. - connect(themeManager, &ThemeManager::fontsChanged, this, [this] { update(); }); - // Connect close button to the remove signal // connect(closeButton, &QPushButton::clicked, this, &DeckPreviewTagDisplayWidget::tagClosed); } @@ -123,9 +117,8 @@ void DeckPreviewTagDisplayWidget::paintEvent(QPaintEvent *event) painter.setPen(borderPen); painter.drawRect(rect().adjusted(0, 0, -1, -1)); - // Calculate font size based on widget height. Family comes from the - // InterfaceDefault theme role; the height-derived size stays as before. - QFont font = themeManager->appFont(AppFont::InterfaceDefault); + // Calculate font size based on widget height + QFont font = painter.font(); int fontSize = std::max(10, height() / 2); // Ensure a minimum font size of 10 font.setPointSize(fontSize); painter.setFont(font); diff --git a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_search_widget.cpp b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_search_widget.cpp index 10a3bf33a..f2669f55d 100644 --- a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_search_widget.cpp +++ b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_search_widget.cpp @@ -1,7 +1,7 @@ #include "visual_deck_storage_search_widget.h" #include "../../../filters/syntax_help.h" -#include "../../theme_icon.h" +#include "../../pixel_map_generator.h" #include #include @@ -27,9 +27,9 @@ VisualDeckStorageSearchWidget::VisualDeckStorageSearchWidget(QWidget *parent) : searchBar = new QLineEdit(this); searchBar->setPlaceholderText(tr("Search by filename (or search expression)")); searchBar->setClearButtonEnabled(true); - searchBar->addAction(loadThemedIcon(QStringLiteral("icons/search")), QLineEdit::LeadingPosition); + searchBar->addAction(loadColorAdjustedPixmap("theme:icons/search"), QLineEdit::LeadingPosition); - auto help = searchBar->addAction(loadThemedIcon(QStringLiteral("icons/info")), QLineEdit::TrailingPosition); + auto help = searchBar->addAction(themePixmap(QStringLiteral("icons/info")), QLineEdit::TrailingPosition); connect(help, &QAction::triggered, this, [this] { createDeckSearchSyntaxHelpWindow(searchBar); }); layout->addWidget(searchBar); diff --git a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_widget.cpp b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_widget.cpp index bbf68adb9..4e1d6293f 100644 --- a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_widget.cpp +++ b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_widget.cpp @@ -1,7 +1,7 @@ #include "visual_deck_storage_widget.h" #include "../../../client/settings/cache_settings.h" -#include "../../theme_icon.h" +#include "../../pixel_map_generator.h" #include "deck_preview/deck_preview_color_identity_filter_widget.h" #include "deck_preview/deck_preview_widget.h" #include "libcockatrice/card/database/card_database.h" @@ -62,12 +62,12 @@ VisualDeckStorageWidget::VisualDeckStorageWidget(QWidget *parent) : QWidget(pare searchWidget = new VisualDeckStorageSearchWidget(this); refreshButton = new QToolButton(this); - refreshButton->setIcon(loadThemedIcon(QStringLiteral("icons/reload"))); + refreshButton->setIcon(themePixmap(QStringLiteral("icons/reload"))); refreshButton->setFixedSize(32, 32); connect(refreshButton, &QPushButton::clicked, this, &VisualDeckStorageWidget::refreshIfPossible); shareButton = new QToolButton(this); - shareButton->setIcon(loadThemedIcon(QStringLiteral("icons/share"))); + shareButton->setIcon(themePixmap(QStringLiteral("icons/share"))); shareButton->setFixedSize(32, 32); shareButton->setVisible(false); connect(shareButton, &QPushButton::clicked, this, &VisualDeckStorageWidget::shareRequested); diff --git a/cockatrice/src/interface/window_main.cpp b/cockatrice/src/interface/window_main.cpp index 715505926..d546676bc 100644 --- a/cockatrice/src/interface/window_main.cpp +++ b/cockatrice/src/interface/window_main.cpp @@ -50,20 +50,11 @@ #include "latency_tracker.h" #include "logger.h" #include "pixel_map_generator.h" -#include "theme_icon.h" #include "version_string.h" #include "widgets/dialogs/dlg_connect.h" #include "widgets/server/handle_public_servers.h" #include "widgets/settings_page/abstract_settings_page.h" #include "widgets/tabs/api/commander_spellbook/handle_commander_brackets.h" -#ifdef WITH_GUI_TEST -#include "../game_graphics/pq_cockatrice_event_player.h" -#include "pqEventObserver.h" -#include "pqEventSource.h" -#include "pqEventTypes.h" -#include "pqTestUtility.h" -#include "recording.h" -#endif #include #include @@ -319,7 +310,6 @@ void MainWindow::actAbout() mb.setIconPixmap( themePixmap(QStringLiteral("cockatrice")).scaled(64, 64, Qt::KeepAspectRatio, Qt::SmoothTransformation)); mb.setTextInteractionFlags(Qt::TextBrowserInteraction); - mb.setObjectName("aboutDialog"); mb.exec(); } @@ -358,24 +348,6 @@ void MainWindow::actOpenSettingsFolder() QtUtils::openPathInFileManager(dir); } -#ifdef WITH_GUI_TEST -void MainWindow::actRecord() -{ - QString filename = QFileDialog::getSaveFileName(this, tr("Test File Name"), QString(), tr("XML Files (*.xml)")); - if (!filename.isEmpty()) { - testUtility->recordTests(filename); - } -} - -void MainWindow::actPlayRecording() -{ - QString filename = QFileDialog::getOpenFileName(this, tr("Test File Name"), QString(), tr("XML Files (*.xml)")); - if (!filename.isEmpty()) { - testUtility->playTests(filename); - } -} -#endif - void MainWindow::retranslateUi() { connectionController->refreshWindowTitle(); @@ -388,10 +360,10 @@ void MainWindow::retranslateUi() aRegister->setText(tr("&Register to server...")); aForgotPassword->setText(tr("&Restore password...")); aSettings->setText(tr("&Settings...")); - aSettings->setIcon(loadThemedIcon(QStringLiteral("icons/settings"))); + aSettings->setIcon(themePixmap(QStringLiteral("icons/settings"))); settingsButton->setToolTip(tr("Settings")); settingsButton->setAccessibleName(tr("Settings")); - settingsButton->setIcon(loadThemedIcon(QStringLiteral("icons/settings"))); + settingsButton->setIcon(themePixmap(QStringLiteral("icons/cogwheel"))); aExit->setText(tr("&Exit")); #if defined(__APPLE__) /* For OSX */ @@ -420,10 +392,6 @@ void MainWindow::retranslateUi() aViewLog->setText(tr("View &Debug Log")); aOpenSettingsFolder->setText(tr("Open Settings Folder")); aFirstRunWizard->setText(tr("Re-run Onboarding Wizard...")); -#ifdef WITH_GUI_TEST - aRecord->setText(tr("&Record Test...")); - aPlayRecording->setText(tr("&Play Test...")); -#endif aShow->setText(tr("Show/Hide")); @@ -433,33 +401,24 @@ void MainWindow::retranslateUi() void MainWindow::createActions() { aConnect = new QAction(this); - aConnect->setObjectName("actionConnect"); connect(aConnect, &QAction::triggered, this, &MainWindow::actConnect); aDisconnect = new QAction(this); - aDisconnect->setObjectName("actionDisconnect"); aDisconnect->setEnabled(false); connect(aDisconnect, &QAction::triggered, this, &MainWindow::actDisconnect); aSinglePlayer = new QAction(this); - aSinglePlayer->setObjectName("actionSinglePlayer"); connect(aSinglePlayer, &QAction::triggered, this, &MainWindow::actSinglePlayer); aWatchReplay = new QAction(this); - aWatchReplay->setObjectName("actionWatchReplay"); connect(aWatchReplay, &QAction::triggered, this, &MainWindow::actWatchReplay); aFullScreen = new QAction(this); - aFullScreen->setObjectName("actionFullScreen"); aFullScreen->setCheckable(true); connect(aFullScreen, &QAction::toggled, this, &MainWindow::actFullScreen); aRegister = new QAction(this); - aRegister->setObjectName("actionRegister"); connect(aRegister, &QAction::triggered, connectionController, &ConnectionController::registerToServer); aForgotPassword = new QAction(this); - aForgotPassword->setObjectName("actionForgotPassword"); connect(aForgotPassword, &QAction::triggered, connectionController, &ConnectionController::forgotPasswordRequest); aSettings = new QAction(this); - aSettings->setObjectName("actionSettings"); connect(aSettings, &QAction::triggered, this, &MainWindow::actSettings); aExit = new QAction(this); - aExit->setObjectName("actionExit"); connect(aExit, &QAction::triggered, this, &MainWindow::actExit); aManageSets = new QAction(QString(), this); @@ -496,12 +455,6 @@ void MainWindow::createActions() connect(aOpenSettingsFolder, &QAction::triggered, this, &MainWindow::actOpenSettingsFolder); aFirstRunWizard = new QAction(this); connect(aFirstRunWizard, &QAction::triggered, this, [this] { runFirstRunWizard(); }); -#ifdef WITH_GUI_TEST - aRecord = new QAction(this); - connect(aRecord, &QAction::triggered, this, &MainWindow::actRecord); - aPlayRecording = new QAction(this); - connect(aPlayRecording, &QAction::triggered, this, &MainWindow::actPlayRecording); -#endif aShow = new QAction(this); connect(aShow, &QAction::triggered, this, &MainWindow::actShow); @@ -542,9 +495,7 @@ void MainWindow::createActions() void MainWindow::createMenus() { - menuBar()->setObjectName("menubar"); cockatriceMenu = menuBar()->addMenu(QString()); - cockatriceMenu->setObjectName("cockatriceMenu"); cockatriceMenu->addAction(aConnect); cockatriceMenu->addAction(aDisconnect); cockatriceMenu->addAction(aRegister); @@ -560,7 +511,6 @@ void MainWindow::createMenus() cockatriceMenu->addAction(aExit); dbMenu = menuBar()->addMenu(QString()); - dbMenu->setObjectName("dbMenu"); dbMenu->addAction(aManageSets); dbMenu->addAction(aEditTokens); dbMenu->addSeparator(); @@ -571,10 +521,8 @@ void MainWindow::createMenus() dbMenu->addAction(aReloadCardDatabase); tabsMenu = menuBar()->addMenu(QString()); - tabsMenu->setObjectName("tabsMenu"); helpMenu = menuBar()->addMenu(QString()); - helpMenu->setObjectName("helpMenu"); helpMenu->addAction(aAbout); helpMenu->addAction(aTips); helpMenu->addSeparator(); @@ -587,19 +535,12 @@ void MainWindow::createMenus() helpMenu->addAction(aOpenSettingsFolder); helpMenu->addSeparator(); helpMenu->addAction(aFirstRunWizard); -#ifdef WITH_GUI_TEST - helpMenu->addSeparator(); - helpMenu->addAction(aRecord); - helpMenu->addAction(aPlayRecording); -#endif } MainWindow::MainWindow(QWidget *parent) - : QMainWindow(parent), localServer(nullptr), bHasActivated(false), askedForDbUpdater(false), noConnect(false), + : QMainWindow(parent), localServer(nullptr), bHasActivated(false), askedForDbUpdater(false), cardUpdateProcess(nullptr), logviewDialog(nullptr) { - setObjectName("MainWindow"); - connect(&SettingsCache::instance().cacheStorage(), &CacheStorageSettings::pixmapCacheSizeChanged, this, &MainWindow::pixmapCacheSizeChanged); pixmapCacheSizeChanged(SettingsCache::instance().cacheStorage().getPixmapCacheSize()); @@ -685,14 +626,6 @@ MainWindow::MainWindow(QWidget *parent) // run startup check async QTimer::singleShot(0, this, &MainWindow::startupConfigCheck); QTimer::singleShot(0, this, &MainWindow::applyStartupDestination); - -#ifdef WITH_GUI_TEST - testUtility = new pqTestUtility(this); - testUtility->setRecordWithDialog(true); - testUtility->addEventObserver("xml", new XMLEventObserver(this)); - testUtility->addEventSource("xml", new XMLEventSource(this)); - testUtility->eventPlayer()->addWidgetEventPlayer(new pqCockatriceEventPlayer(this)); -#endif } void MainWindow::startupConfigCheck() @@ -847,11 +780,6 @@ void MainWindow::runFirstRunWizard(bool firstRun) */ void MainWindow::applyStartupDestination() { - if (noConnect) { - qCInfo(WindowMainStartupAutoconnectLog) << "Startup destination suppressed (--no-connect)"; - return; - } - // An explicit command-line connect takes precedence over the startup destination. if (!connectTo.isEmpty()) { return; @@ -1121,10 +1049,6 @@ void MainWindow::handleCockatriceLink(const QString &url) void MainWindow::attemptStartupAutoConnect() { - if (noConnect) { - qCInfo(WindowMainStartupAutoconnectLog) << "Auto-connect suppressed (--no-connect)"; - return; - } if (startupAutoConnectAttempted || skipStartupAutoConnect) { return; } diff --git a/cockatrice/src/interface/window_main.h b/cockatrice/src/interface/window_main.h index 48d093a38..9e342e4b0 100644 --- a/cockatrice/src/interface/window_main.h +++ b/cockatrice/src/interface/window_main.h @@ -48,7 +48,6 @@ class QAction; class QToolButton; class QWidget; -class pqTestUtility; struct LocalGameOptions; inline Q_LOGGING_CATEGORY(WindowMainLog, "window_main"); @@ -105,11 +104,6 @@ private slots: void actUpdate(); void actViewLog(); void actOpenSettingsFolder(); -#ifdef WITH_GUI_TEST - void actRecord(); - void actPlayRecording(); -#endif - void actShow(); void showWindowIfHidden(); void onUrlChainFinished(bool connected); @@ -191,9 +185,6 @@ private: *aReloadCardDatabase; QAction *aTips, *aUpdate, *aCheckCardUpdates, *aCheckCardUpdatesBackground, *aFirstRunWizard, *aStatusBar, *aViewLog, *aOpenSettingsFolder; -#ifdef WITH_GUI_TEST - QAction *aRecord, *aPlayRecording; -#endif TabSupervisor *tabSupervisor; IntentUrlParser *urlParser; @@ -217,7 +208,6 @@ private: bool skipStartupAutoConnect = false; bool startupAutoConnectAttempted = false; bool firstRunWizardActive = false; - bool noConnect = false; ///< --no-connect: skip every startup connection QProcess *cardUpdateProcess; QByteArray cardUpdateOutputBuffer; QByteArray cardUpdateErrorOutput; ///< diagnostic output collected from the updater's stderr @@ -229,9 +219,6 @@ private: GameReplay *replay; DlgTipOfTheDay *tip; QUrl connectTo; -#ifdef WITH_GUI_TEST - pqTestUtility *testUtility; -#endif public: explicit MainWindow(QWidget *parent = nullptr); @@ -239,16 +226,6 @@ public: { connectTo = QUrl(QString("cockatrice://%1").arg(url)); } - /** - * @brief Suppresses every startup connection: --connect, auto-connect and the - * configured startup destination. Used by the GUI test harness so a - * playback run never dials out. - */ - void setNoConnect(bool value) - { - noConnect = value; - } - // When set, the window's own startup connection (--connect or auto-connect // on first activation) is skipped. Used for activation launches: the intent // chain triggered by a cockatrice:// URL owns the connection, and letting @@ -289,13 +266,6 @@ public: */ void requestInstallerClose(); -#ifdef WITH_GUI_TEST - pqTestUtility *getTestUtility() const - { - return testUtility; - } -#endif - protected: void closeEvent(QCloseEvent *event) override; void changeEvent(QEvent *event) override; diff --git a/cockatrice/src/main.cpp b/cockatrice/src/main.cpp index b4dc0610f..829a8ac42 100644 --- a/cockatrice/src/main.cpp +++ b/cockatrice/src/main.cpp @@ -39,10 +39,6 @@ #include "single_instance_manager.h" #include "version_string.h" -#ifdef WITH_GUI_TEST -#include "pqTestUtility.h" -#endif - #include #include #include @@ -66,12 +62,10 @@ #include #include #include -#include #include #include #include #include -#include #include #include #include @@ -349,16 +343,9 @@ int main(int argc, char *argv[]) parser.addOptions( {{{"c", "connect"}, QCoreApplication::translate("main", "Connect on startup"), "user:pass@host:port"}, {{"d", "debug-output"}, QCoreApplication::translate("main", "Debug to file")}, - {"simulate-crash", QCoreApplication::translate( - "main", "Show the crash-report dialog (saves a mock file when no DSN is configured)")}, - {"no-connect", QCoreApplication::translate("main", "Do not connect to any server on startup")} -#ifdef WITH_GUI_TEST - , - {{"t", "test"}, - QCoreApplication::translate("main", "Play back a QtTesting XML test file and exit"), - "test-file.xml"} -#endif - }); + {"simulate-crash", + QCoreApplication::translate("main", + "Show the crash-report dialog (saves a mock file when no DSN is configured)")}}); parser.process(app); @@ -496,9 +483,6 @@ int main(int argc, char *argv[]) if (parser.isSet("connect")) { ui.setConnectTo(parser.value("connect")); } - if (parser.isSet("no-connect")) { - ui.setNoConnect(true); - } qCInfo(MainLog) << "MainWindow constructor finished"; ui.setWindowIcon(themePixmap(QStringLiteral("cockatrice"))); @@ -539,27 +523,6 @@ int main(int argc, char *argv[]) handleActivation(file); } -#ifdef WITH_GUI_TEST - if (parser.isSet("test")) { - QString testFile = parser.value("test"); - qCInfo(MainLog) << "Playing test file:" << testFile; - QTimer::singleShot(0, &ui, [&ui, testFile]() { - bool success = ui.getTestUtility()->playTests(testFile); - qCInfo(MainLog) << "Test playback" << (success ? "succeeded" : "FAILED"); - qApp->exit(success ? 0 : 1); - - // An event that fails while a modal dialog is open aborts the dispatcher, - // but the dialog's nested event loop can then keep the application alive - // forever. Hard-exit after a grace period so a red test always terminates. - auto *watchdog = QThread::create([success] { - QThread::sleep(10); - ::_Exit(success ? 0 : 1); - }); - watchdog->start(); - }); - } -#endif - // Connect to future file/URL events from other instances QObject::connect(&instance, &SingleInstanceManager::filesReceived, [&handleActivation](const QStringList &files) { for (const QString &file : files) { diff --git a/doc/doxygen/pages/user_documentation/deck_management/editing_decks.md b/doc/doxygen/pages/user_documentation/deck_management/editing_decks.md index 04833282d..37b613461 100644 --- a/doc/doxygen/pages/user_documentation/deck_management/editing_decks.md +++ b/doc/doxygen/pages/user_documentation/deck_management/editing_decks.md @@ -1,7 +1,6 @@ @page editing_decks Editing Decks -- @subpage editing_decks_classic -- @subpage editing_decks_visual + - @subpage editing_decks_classic + - @subpage editing_decks_visual -- @subpage visual_database_display -- @subpage editing_decks_printings + - @subpage editing_decks_printings diff --git a/doc/doxygen/pages/user_documentation/deck_management/importing_decks.md b/doc/doxygen/pages/user_documentation/deck_management/importing_decks.md index fbfb8fe3c..4aab7ad2c 100644 --- a/doc/doxygen/pages/user_documentation/deck_management/importing_decks.md +++ b/doc/doxygen/pages/user_documentation/deck_management/importing_decks.md @@ -70,4 +70,4 @@ confirm. The action will automatically import the deck from the online service without any other required user action. -Currently supported services are Archidekt, Deckstats, ManaBox, Moxfield, and TappedOut. +Currently supported services are Archidekt, Deckstats, Moxfield, and TappedOut. \ No newline at end of file diff --git a/doc/doxygen/pages/user_documentation/deck_management/visual_database_display.md b/doc/doxygen/pages/user_documentation/deck_management/visual_database_display.md deleted file mode 100644 index 097848ab8..000000000 --- a/doc/doxygen/pages/user_documentation/deck_management/visual_database_display.md +++ /dev/null @@ -1,25 +0,0 @@ -@page visual_database_display Visual Database Display - -# Purpose - -The VisualDatabaseDisplay presents all the cards from the enabled sets in the CardDatabase, providing a visual way to find and add cards to your DeckList. - -# Using the Visual Database Display - -The VisualDatabaseDisplay can be opened by itself using the Tabs section in the menu bar at the top of the screen, or by selecting the "Visual Database Display" tab in the visual deck editor. When opened as a part of the visual deck editor, the VisualDatabaseDisplay can be used to add and remove cards from your deck. See @ref editing_decks_visual for more information about modifying your deck. - -By default the cards are displayed as a grid of pictures. However, a table view using card names can be toggled by pressing the Visual/Table button at the top right of the widget. - -# Filtering cards - -The top section of the VisualDatabaseDisplay can be used to filter cards based on various card properties, as well as specifying in which way they should be sorted. Filters can be saved and reused at a later time by using the Save/Load button at the right of this panel. - -# Modifying the Deck list - -When the VisualDatabaseDisplay is opened as part of the visual deck editor, clicking on cards adds or removes them to and from your deck. - -In the picture view, left-clicking on a card will add that card to the main board of your deck, while right-clicking will remove that printing of the card from the main board, if it exists. When clicking cards, modifier keys can be used to add (or remove) the card to zones other than the main board. Holding a Ctrl key on Windows and Linux or a Command key on MacOS adds/removes cards to the sideboard, while the Shift keys add/remove cards to the maybeboard. - -In the table view, double-clicking on a card adds that card to the main board of your deck, while right-click opens the card menu. Holding the Ctrl key on Windows and Linux or the Command key on MacOS when double-clicking adds the card to the sideboard instead. - -Tokens are always added to the token zone, regardless of which view is open or which modifier key is pressed. diff --git a/external/qttesting b/external/qttesting deleted file mode 160000 index c7bb5d372..000000000 --- a/external/qttesting +++ /dev/null @@ -1 +0,0 @@ -Subproject commit c7bb5d372133e94c75cbdc70b04e039b864ea379 diff --git a/libcockatrice_card/libcockatrice/card/card_info.cpp b/libcockatrice_card/libcockatrice/card/card_info.cpp index 36f0cdeb8..90bfd3f44 100644 --- a/libcockatrice_card/libcockatrice/card/card_info.cpp +++ b/libcockatrice_card/libcockatrice/card/card_info.cpp @@ -316,10 +316,6 @@ void CardInfo::setColors(const QString &value) { setProperty(Mtg::Colors, value); } -QString CardInfo::getColorIdentity() const -{ - return getProperty(Mtg::ColorIdentity); -} QString CardInfo::getLoyalty() const { return getProperty(Mtg::Loyalty); diff --git a/libcockatrice_card/libcockatrice/card/card_info.h b/libcockatrice_card/libcockatrice/card/card_info.h index 075b5b264..5452ef98b 100644 --- a/libcockatrice_card/libcockatrice/card/card_info.h +++ b/libcockatrice_card/libcockatrice/card/card_info.h @@ -469,7 +469,6 @@ public: [[nodiscard]] QString getCmc() const; [[nodiscard]] QString getColors() const; void setColors(const QString &value); - [[nodiscard]] QString getColorIdentity() const; [[nodiscard]] QString getLoyalty() const; [[nodiscard]] QString getMainCardType() const; [[nodiscard]] QString getManaCost() const; diff --git a/libcockatrice_card/libcockatrice/card/database/card_database_loader.cpp b/libcockatrice_card/libcockatrice/card/database/card_database_loader.cpp index 7ca5ba433..00b21f88e 100644 --- a/libcockatrice_card/libcockatrice/card/database/card_database_loader.cpp +++ b/libcockatrice_card/libcockatrice/card/database/card_database_loader.cpp @@ -83,7 +83,7 @@ LoadStatus CardDatabaseLoader::loadCardDatabase(const QString &path, CardDatabas auto startTime = QTime::currentTime(); LoadStatus tempLoadStatus = NotLoaded; if (!path.isEmpty()) { - QMutexLocker locker(&loadFromFileMutex); + QMutexLocker locker(loadFromFileMutex); tempLoadStatus = loadFromFile(path, data); } @@ -102,7 +102,7 @@ LoadStatus CardDatabaseLoader::loadCardDatabases(bool ignoreCache) LoadStatus CardDatabaseLoader::doLoadCardDatabases(bool ignoreCache) { - QMutexLocker locker(&reloadDatabaseMutex); + QMutexLocker locker(reloadDatabaseMutex); if (!database) { qCWarning(CardDatabaseLoadingLog) << "Loader has no database pointer"; diff --git a/libcockatrice_card/libcockatrice/card/database/card_database_loader.h b/libcockatrice_card/libcockatrice/card/database/card_database_loader.h index 72aeeff3e..bfbaa0f51 100644 --- a/libcockatrice_card/libcockatrice/card/database/card_database_loader.h +++ b/libcockatrice_card/libcockatrice/card/database/card_database_loader.h @@ -177,8 +177,8 @@ private: ICardSetPriorityController *priorityController; /**< Controller for reconstructing set state. */ QList availableParsers; /**< List of available parsers for different formats. */ - QBasicMutex loadFromFileMutex; /**< Mutex for single-file loading. */ - QBasicMutex reloadDatabaseMutex; /**< Mutex for reloading entire database. */ + QBasicMutex *loadFromFileMutex = new QBasicMutex(); /**< Mutex for single-file loading. */ + QBasicMutex *reloadDatabaseMutex = new QBasicMutex(); /**< Mutex for reloading entire database. */ }; #endif // COCKATRICE_CARD_DATABASE_LOADER_H diff --git a/libcockatrice_card/libcockatrice/card/database/parser/cockatrice_xml_3.cpp b/libcockatrice_card/libcockatrice/card/database/parser/cockatrice_xml_3.cpp index 8d5735db7..d40c1c063 100644 --- a/libcockatrice_card/libcockatrice/card/database/parser/cockatrice_xml_3.cpp +++ b/libcockatrice_card/libcockatrice/card/database/parser/cockatrice_xml_3.cpp @@ -18,7 +18,6 @@ #include #include #include -#include #include #include #include @@ -500,7 +499,7 @@ bool CockatriceXml3Parser::saveToFile(FormatRulesNameMap _formats, { Q_UNUSED(_formats); - QSaveFile file(fileName); + QFile file(fileName); if (!file.open(QIODevice::WriteOnly)) { return false; } @@ -540,5 +539,5 @@ bool CockatriceXml3Parser::saveToFile(FormatRulesNameMap _formats, xml.writeEndElement(); // cockatrice_carddatabase xml.writeEndDocument(); - return !xml.hasError() && file.commit(); + return true; } diff --git a/libcockatrice_card/libcockatrice/card/database/parser/cockatrice_xml_4.cpp b/libcockatrice_card/libcockatrice/card/database/parser/cockatrice_xml_4.cpp index 3965b80f0..3ee1e808e 100644 --- a/libcockatrice_card/libcockatrice/card/database/parser/cockatrice_xml_4.cpp +++ b/libcockatrice_card/libcockatrice/card/database/parser/cockatrice_xml_4.cpp @@ -18,7 +18,6 @@ #include #include #include -#include #include #include #include @@ -683,7 +682,7 @@ bool CockatriceXml4Parser::saveToFile(FormatRulesNameMap _formats, const QString &sourceUrl, const QString &sourceVersion) { - QSaveFile file(fileName); + QFile file(fileName); if (!file.open(QIODevice::WriteOnly)) { return false; } @@ -731,5 +730,5 @@ bool CockatriceXml4Parser::saveToFile(FormatRulesNameMap _formats, xml.writeEndElement(); // cockatrice_carddatabase xml.writeEndDocument(); - return !xml.hasError() && file.commit(); + return true; } diff --git a/libcockatrice_deck_list/libcockatrice/deck_list/deck_list.cpp b/libcockatrice_deck_list/libcockatrice/deck_list/deck_list.cpp index b357b5801..821b53dff 100644 --- a/libcockatrice_deck_list/libcockatrice/deck_list/deck_list.cpp +++ b/libcockatrice_deck_list/libcockatrice/deck_list/deck_list.cpp @@ -340,17 +340,7 @@ bool DeckList::saveToStream_Plain(QTextStream &stream, bool prefixSideboardCards } }; - // Export the main deck first without changing the saved tree's zone order. - forEachCard([&](const auto node, const auto card) { - if (node->getName() == DECK_ZONE_MAIN) { - writeToStream(node, card); - } - }); - forEachCard([&](const auto node, const auto card) { - if (node->getName() != DECK_ZONE_MAIN) { - writeToStream(node, card); - } - }); + forEachCard(writeToStream); return true; } @@ -433,23 +423,8 @@ DecklistCardNode *DeckList::addCard(const QString &cardName, const QString &cardProviderId, bool formatLegal) { - return addCards(cardName, 1, zoneName, position, cardSetName, cardSetCollectorNumber, cardProviderId, formatLegal); -} - -DecklistCardNode *DeckList::addCards(const QString &cardName, - int amount, - const QString &zoneName, - int position, - const QString &cardSetName, - const QString &cardSetCollectorNumber, - const QString &cardProviderId, - bool formatLegal) -{ - if (amount <= 0) { - return nullptr; - } - auto node = tree.addCard(cardName, amount, zoneName, position, cardSetName, cardSetCollectorNumber, cardProviderId, - formatLegal); + auto node = + tree.addCard(cardName, 1, zoneName, position, cardSetName, cardSetCollectorNumber, cardProviderId, formatLegal); refreshDeckHash(); return node; } diff --git a/libcockatrice_deck_list/libcockatrice/deck_list/deck_list.h b/libcockatrice_deck_list/libcockatrice/deck_list/deck_list.h index 9d0f20586..6796e9c33 100644 --- a/libcockatrice_deck_list/libcockatrice/deck_list/deck_list.h +++ b/libcockatrice_deck_list/libcockatrice/deck_list/deck_list.h @@ -284,14 +284,6 @@ public: const QString &cardSetCollectorNumber = QString(), const QString &cardProviderId = QString(), const bool formatLegal = true); - DecklistCardNode *addCards(const QString &cardName, - int amount, - const QString &zoneName, - int position = -1, - const QString &cardSetName = QString(), - const QString &cardSetCollectorNumber = QString(), - const QString &cardProviderId = QString(), - const bool formatLegal = true); ///@} /** @name Deck identity */ diff --git a/libcockatrice_filters/CMakeLists.txt b/libcockatrice_filters/CMakeLists.txt index 1b4303fc9..070c04637 100644 --- a/libcockatrice_filters/CMakeLists.txt +++ b/libcockatrice_filters/CMakeLists.txt @@ -1,10 +1,10 @@ -set(HEADERS libcockatrice/filters/filter_card.h libcockatrice/filters/filter_mode.h - libcockatrice/filters/filter_string.h libcockatrice/filters/filter_tree.h +set(HEADERS libcockatrice/filters/filter_card.h libcockatrice/filters/filter_string.h + libcockatrice/filters/filter_tree.h ) add_library( - libcockatrice_filters STATIC ${HEADERS} libcockatrice/filters/filter_card.cpp libcockatrice/filters/filter_mode.cpp - libcockatrice/filters/filter_string.cpp libcockatrice/filters/filter_tree.cpp + libcockatrice_filters STATIC ${HEADERS} libcockatrice/filters/filter_card.cpp libcockatrice/filters/filter_string.cpp + libcockatrice/filters/filter_tree.cpp ) add_dependencies(libcockatrice_filters libcockatrice_card) diff --git a/libcockatrice_filters/libcockatrice/filters/filter_card.cpp b/libcockatrice_filters/libcockatrice/filters/filter_card.cpp index 8af39c098..26cddd53c 100644 --- a/libcockatrice_filters/libcockatrice/filters/filter_card.cpp +++ b/libcockatrice_filters/libcockatrice/filters/filter_card.cpp @@ -69,8 +69,6 @@ const QString CardFilter::attrName(Attr a) return tr("Type"); case AttrColor: return tr("Color"); - case AttrColorIdentity: - return tr("Color Identity"); case AttrText: return tr("Text"); case AttrSet: diff --git a/libcockatrice_filters/libcockatrice/filters/filter_card.h b/libcockatrice_filters/libcockatrice/filters/filter_card.h index bc613fc9c..dbeffd18f 100644 --- a/libcockatrice_filters/libcockatrice/filters/filter_card.h +++ b/libcockatrice_filters/libcockatrice/filters/filter_card.h @@ -32,7 +32,6 @@ public: { AttrCmc = 0, AttrColor, - AttrColorIdentity, AttrLoyalty, AttrManaCost, AttrName, diff --git a/libcockatrice_filters/libcockatrice/filters/filter_mode.cpp b/libcockatrice_filters/libcockatrice/filters/filter_mode.cpp deleted file mode 100644 index 356ccf608..000000000 --- a/libcockatrice_filters/libcockatrice/filters/filter_mode.cpp +++ /dev/null @@ -1,77 +0,0 @@ -/** - * @file filter_mode.cpp - * @ingroup Filters - */ - -#include "libcockatrice/filters/filter_mode.h" - -#include - -FilterModeInfo getFilterModeInfo(FilterMode filterMode) -{ - switch (filterMode) { - case FilterMode::Includes: - return {CardFilter::Type::TypeOr, CardFilter::Type::TypeEnd, CardFilter::Attr::AttrColor}; - case FilterMode::Excludes: - return {CardFilter::Type::TypeAndNot, CardFilter::Type::TypeEnd, CardFilter::Attr::AttrColor}; - case FilterMode::IncludeExclude: - return {CardFilter::Type::TypeOr, CardFilter::Type::TypeAndNot, CardFilter::Attr::AttrColor}; - case FilterMode::ExactMatch: - return {CardFilter::Type::TypeAnd, CardFilter::Type::TypeAndNot, CardFilter::Attr::AttrColor}; - case FilterMode::ColorIdentity: - return {CardFilter::Type::TypeEnd, CardFilter::Type::TypeAndNot, CardFilter::Attr::AttrColorIdentity}; - case FilterMode::Custom: - case FilterMode::End: - break; - } - return {CardFilter::Type::TypeEnd, CardFilter::Type::TypeEnd, CardFilter::Attr::AttrEnd}; -} - -FilterMode deriveFilterMode(const QList &filters, FilterMode currentMode) -{ - constexpr int modeCount = static_cast(FilterMode::End); - bool modeImpossible[modeCount] = {}; - const QString colors = "WUBRG"; - - for (int i = 0; i < modeCount; ++i) { - const FilterModeInfo modeInfo = getFilterModeInfo(static_cast(i)); - - // Remove the modes that cannot create one of the filters. - for (const CardFilter *filter : filters) { - if (filter->attr() != modeInfo.attr || - (filter->type() != modeInfo.includeType && filter->type() != modeInfo.excludeType)) { - modeImpossible[i] = true; - break; - } - } - - // Modes that have both an include and an exclude filter should always have a filter for each color. - if (modeInfo.includeType != CardFilter::Type::TypeEnd && modeInfo.excludeType != CardFilter::Type::TypeEnd && - !modeImpossible[i]) { - for (const QChar &color : colors) { - int count = 0; - for (const CardFilter *filter : filters) { - count += filter->term() == color; - } - if (count != 1) { - modeImpossible[i] = true; - break; - } - } - } - } - - // Keep the current mode if it can produce the filters, so that a live mode is never swapped for another. - // Switching to and from the custom mode is always possible. - if (!modeImpossible[static_cast(currentMode)] && currentMode != FilterMode::Custom) { - return currentMode; - } - - for (int i = 0; i < modeCount; ++i) { - if (!modeImpossible[i] || i == static_cast(FilterMode::Custom)) { - return static_cast(i); - } - } - - return FilterMode::Custom; -} diff --git a/libcockatrice_filters/libcockatrice/filters/filter_mode.h b/libcockatrice_filters/libcockatrice/filters/filter_mode.h deleted file mode 100644 index f7662d662..000000000 --- a/libcockatrice_filters/libcockatrice/filters/filter_mode.h +++ /dev/null @@ -1,62 +0,0 @@ -/** - * @file filter_mode.h - * @ingroup Filters - */ - -#ifndef LIBCOCKATRICE_FILTERS_FILTER_MODE_H -#define LIBCOCKATRICE_FILTERS_FILTER_MODE_H - -#include "libcockatrice/filters/filter_card.h" - -#include -#include -#include - -/** - * @brief The way selected and unselected colors are combined into card filters. - */ -enum class FilterMode -{ - Includes = 0, // Include selected colors (OR condition). - Excludes, // Exclude selected colors (AND NOT). - IncludeExclude, // Include selected colors (OR) and exclude unselected colors (AND NOT). - ExactMatch, // Only selected colors are included, all others are excluded. - ColorIdentity, // Keep cards whose color identity is a subset of the selected colors (stored as AND NOT for each - // unselected color). - Custom, // The color filters do not match any of the above modes. Not included in the combo box. - End -}; - -Q_DECLARE_METATYPE(FilterMode) - -/** - * @brief The filters a single color contributes in a given mode. - */ -struct FilterModeInfo -{ - // When a color symbol is enabled, add filters of this type. Skipped if equal to CardFilter::Type::TypeEnd. - CardFilter::Type includeType; - - // When a color symbol is disabled, add filters of this type. Skipped if equal to CardFilter::Type::TypeEnd. - CardFilter::Type excludeType; - - // The attribute to filter for when adding the filters. - CardFilter::Attr attr; -}; - -/** @brief Return the filters the given mode contributes for one color. */ -FilterModeInfo getFilterModeInfo(FilterMode filterMode); - -/** - * @brief Determine the mode that produced the given color filters. - * - * Returns @p currentMode unchanged when that mode can still produce @p filters, so that a live mode is never - * replaced by an equally valid one. Otherwise returns the first mode that can produce them, falling back to - * FilterMode::Custom when none can. - * - * @param filters The filters on CardFilter::Attr::AttrColor and CardFilter::Attr::AttrColorIdentity. - * @param currentMode The mode currently selected in the UI. - */ -FilterMode deriveFilterMode(const QList &filters, FilterMode currentMode); - -#endif // LIBCOCKATRICE_FILTERS_FILTER_MODE_H diff --git a/libcockatrice_filters/libcockatrice/filters/filter_string.cpp b/libcockatrice_filters/libcockatrice/filters/filter_string.cpp index 1bcb1646b..081ada824 100644 --- a/libcockatrice_filters/libcockatrice/filters/filter_string.cpp +++ b/libcockatrice_filters/libcockatrice/filters/filter_string.cpp @@ -409,7 +409,7 @@ static void setupParserRules() const bool identity = sv.tokens[0].empty() || sv.tokens[0][0] != 'i'; if (sv.tokens[1][0] == ':') { return [=](const CardData &x) { - const QString match = identity ? x->getColors() : x->getColorIdentity(); + const QString match = identity ? x->getColors() : x->getProperty("coloridentity"); if (parts.contains("m") && match.length() < 2) { return false; } @@ -427,7 +427,7 @@ static void setupParserRules() } return [=](const CardData &x) { - const QString match = identity ? x->getColors() : x->getColorIdentity(); + const QString match = identity ? x->getColors() : x->getProperty("colorIdentity"); if (parts.contains("m") && match.length() < 2) { return false; } diff --git a/libcockatrice_filters/libcockatrice/filters/filter_tree.cpp b/libcockatrice_filters/libcockatrice/filters/filter_tree.cpp index 817c7298d..f12f87181 100644 --- a/libcockatrice_filters/libcockatrice/filters/filter_tree.cpp +++ b/libcockatrice_filters/libcockatrice/filters/filter_tree.cpp @@ -176,24 +176,23 @@ FilterItem::FilterItem(QString trm, FilterItemList *parent) : p(parent), term(st lowercaseTerm = term.toLower(); switch (attr()) { - case CardFilter::AttrColor: - case CardFilter::AttrColorIdentity: { - QString converted = term.trimmed().toLower(); + case CardFilter::AttrColor: { + QString converted = term.trimmed(); - converted.replace("green", "g"); - converted.replace("grn", "g"); - converted.replace("blue", "u"); - converted.replace("blu", "u"); - converted.replace("black", "b"); - converted.replace("blk", "b"); - converted.replace("red", "r"); - converted.replace("white", "w"); - converted.replace("wht", "w"); - converted.replace("colorless", "c"); - converted.replace("colourless", "c"); - converted.replace("none", "c"); + converted.replace("green", "g", Qt::CaseInsensitive); + converted.replace("grn", "g", Qt::CaseInsensitive); + converted.replace("blue", "u", Qt::CaseInsensitive); + converted.replace("blu", "u", Qt::CaseInsensitive); + converted.replace("black", "b", Qt::CaseInsensitive); + converted.replace("blk", "b", Qt::CaseInsensitive); + converted.replace("red", "r", Qt::CaseInsensitive); + converted.replace("white", "w", Qt::CaseInsensitive); + converted.replace("wht", "w", Qt::CaseInsensitive); + converted.replace("colorless", "c", Qt::CaseInsensitive); + converted.replace("colourless", "c", Qt::CaseInsensitive); + converted.replace("none", "c", Qt::CaseInsensitive); - converted.replace(QString(" "), QString("")); + converted.replace(QString(" "), QString(""), Qt::CaseInsensitive); convertedColorTerm = converted; break; @@ -359,17 +358,12 @@ bool FilterItem::acceptSubType(const CardInfoPtr &info) const return subType.contains(term, Qt::CaseInsensitive); } -/** - * @brief Return true when every letter of the given term appears in the given colors string. - * If the term is equal to "c", then only return true if the color string is empty. - * - * @param term The letters which should be included, or "c" - * @param colors The string to check for letters in - */ -static bool matchesColor(const QString &term, const QString &colors) +bool FilterItem::acceptColor(const CardInfoPtr &info) const { + const QString colors = info->getColors(); + // Colorless card filter - if (term == "c" && colors.length() < 1) { + if (convertedColorTerm.toLower() == "c" && colors.length() < 1) { return true; } @@ -378,23 +372,13 @@ static bool matchesColor(const QString &term, const QString &colors) * then we should match all of them to the card's colors */ int match_count = 0; - for (const QChar &color : term) { + for (const QChar &color : convertedColorTerm) { if (colors.contains(color, Qt::CaseInsensitive)) { match_count++; } } - return match_count == term.length(); -} - -bool FilterItem::acceptColor(const CardInfoPtr &info) const -{ - return matchesColor(convertedColorTerm, info->getColors()); -} - -bool FilterItem::acceptColorIdentity(const CardInfoPtr &info) const -{ - return matchesColor(convertedColorTerm, info->getColorIdentity()); + return match_count == convertedColorTerm.length(); } bool FilterItem::acceptText(const CardInfoPtr &info, const CardSearchLanguage &searchLanguage) const @@ -556,8 +540,6 @@ bool FilterItem::acceptCardAttr(const CardInfoPtr &info, return acceptType(info); case CardFilter::AttrColor: return acceptColor(info); - case CardFilter::AttrColorIdentity: - return acceptColorIdentity(info); case CardFilter::AttrText: return acceptText(info, searchLanguage); case CardFilter::AttrSet: diff --git a/libcockatrice_filters/libcockatrice/filters/filter_tree.h b/libcockatrice_filters/libcockatrice/filters/filter_tree.h index f1a5803e5..85a57304d 100644 --- a/libcockatrice_filters/libcockatrice/filters/filter_tree.h +++ b/libcockatrice_filters/libcockatrice/filters/filter_tree.h @@ -244,7 +244,6 @@ public: [[nodiscard]] bool acceptMainType(const CardInfoPtr &info) const; [[nodiscard]] bool acceptSubType(const CardInfoPtr &info) const; [[nodiscard]] bool acceptColor(const CardInfoPtr &info) const; - [[nodiscard]] bool acceptColorIdentity(const CardInfoPtr &info) const; [[nodiscard]] bool acceptText(const CardInfoPtr &info, const CardSearchLanguage &searchLanguage) const; [[nodiscard]] bool acceptSet(const CardInfoPtr &info) const; [[nodiscard]] bool acceptManaCost(const CardInfoPtr &info) const; diff --git a/libcockatrice_game/libcockatrice/game/game_event_handler.cpp b/libcockatrice_game/libcockatrice/game/game_event_handler.cpp index 7ef502400..856475dfa 100644 --- a/libcockatrice_game/libcockatrice/game/game_event_handler.cpp +++ b/libcockatrice_game/libcockatrice/game/game_event_handler.cpp @@ -21,7 +21,6 @@ #include #include #include -#include #include #include #include @@ -39,7 +38,6 @@ #include #include #include -#include #include #include #include @@ -52,7 +50,6 @@ #include #include #include -#include #include #include #include @@ -192,9 +189,6 @@ void GameEventHandler::processGameEventContainer(const GameEventContainer &cont, case GameEvent::REVERSE_TURN: eventReverseTurn(event.GetExtension(Event_ReverseTurn::ext), playerId, context); break; - case GameEvent::PLAYER_ORDER_CHANGED: - eventPlayerOrderChanged(event.GetExtension(Event_PlayerOrderChanged::ext), playerId, context); - break; default: { PlayerLogic *player = game->getPlayerManager()->getPlayers().value(playerId, 0); @@ -221,16 +215,6 @@ void GameEventHandler::handleReverseTurn() sendGameCommand(Command_ReverseTurn()); } -void GameEventHandler::handleSetPlayerOrder(const QList &playerNames, bool randomize) -{ - Command_SetPlayerOrder command; - command.set_randomize(randomize); - for (const QString &name : playerNames) { - command.add_player_names(name.toStdString()); - } - sendGameCommand(command); -} - void GameEventHandler::handleActiveLocalPlayerConceded() { sendGameCommand(Command_Concede()); @@ -393,27 +377,6 @@ void GameEventHandler::eventGameStateChanged(const Event_GameStateChanged &event { const int playerListSize = event.player_list_size(); - if (event.has_local_player_id()) { - game->getPlayerManager()->setLocalPlayerId(event.local_player_id()); - } - - QSet authoritativePlayerIds; - for (int i = 0; i < playerListSize; ++i) { - const ServerInfo_PlayerProperties &prop = event.player_list(i).properties(); - if (!prop.spectator()) { - authoritativePlayerIds.insert(prop.player_id()); - } - } - - // Players whose seats were shuffled away from the previous game are no longer - // part of the authoritative list and must be removed. - for (int stalePlayerId : game->getPlayerManager()->getPlayers().keys()) { - if (!authoritativePlayerIds.contains(stalePlayerId)) { - game->getPlayerManager()->removePlayer(stalePlayerId); - emit playerLeft(stalePlayerId); - } - } - QVector>> opponentDecksToDisplay; for (int i = 0; i < playerListSize; ++i) { @@ -458,19 +421,6 @@ void GameEventHandler::eventGameStateChanged(const Event_GameStateChanged &event emit remotePlayersDecksSelected(opponentDecksToDisplay); - // The server emits player_list in seat order, so record it to lay out the - // table by seating rather than by player id. - QList seatOrder; - for (int i = 0; i < playerListSize; ++i) { - const ServerInfo_PlayerProperties &prop = event.player_list(i).properties(); - if (!prop.spectator()) { - seatOrder.append(prop.player_id()); - } - } - if (!seatOrder.isEmpty()) { - game->getGameState()->setSeatOrder(seatOrder); - } - game->getGameState()->setGameTime(event.seconds_elapsed()); if (event.game_started() && !game->getGameMetaInfo()->started()) { @@ -675,33 +625,6 @@ void GameEventHandler::eventReverseTurn(const Event_ReverseTurn &event, emit logTurnReversed(player, event.reversed()); } -void GameEventHandler::eventPlayerOrderChanged(const Event_PlayerOrderChanged &event, - int /*eventPlayerId*/, - const GameEventContext & /*context*/) -{ - QStringList playerNames; - for (const auto &name : event.player_names()) { - playerNames.append(QString::fromStdString(name)); - } - - // Track the seating by stable player id so the table layout and the turn - // order dialog follow the shuffled order instead of id order. - QList seatOrder; - const auto &players = game->getPlayerManager()->getPlayers(); - for (const QString &name : playerNames) { - for (auto it = players.constBegin(); it != players.constEnd(); ++it) { - if (it.value() && it.value()->getPlayerInfo()->getName() == name) { - seatOrder.append(it.key()); - break; - } - } - } - if (!seatOrder.isEmpty()) { - game->getGameState()->setSeatOrder(seatOrder); - } - emit logTurnOrderChanged(playerNames, event.randomized()); -} - void GameEventHandler::eventGameHostChanged(const Event_GameHostChanged & /*event*/, int eventPlayerId, const GameEventContext & /*context*/) diff --git a/libcockatrice_game/libcockatrice/game/game_event_handler.h b/libcockatrice_game/libcockatrice/game/game_event_handler.h index bfcf355b0..f7b11780b 100644 --- a/libcockatrice_game/libcockatrice/game/game_event_handler.h +++ b/libcockatrice_game/libcockatrice/game/game_event_handler.h @@ -51,7 +51,6 @@ class Event_GameSay; class Event_Kicked; class Event_ReverseTurn; class Event_Ping; -class Event_PlayerOrderChanged; class ServerInfo_PlayerProperties; namespace google { @@ -114,16 +113,6 @@ public: /** @brief Request reversing the current turn order. */ void handleReverseTurn(); - /** - * @brief Request a new seating / turn order. - * - * Only the game host (or a moderator) may do this before the game starts. - * - * @param playerNames Ordered list of player names. Ignored when randomize is true. - * @param randomize Ask the server to shuffle the seats instead. - */ - void handleSetPlayerOrder(const QList &playerNames, bool randomize); - /** @brief Concede the game for the currently active local player. */ void handleActiveLocalPlayerConceded(); @@ -327,10 +316,6 @@ public: /** @brief Handle a turn reversal event. */ void eventReverseTurn(const Event_ReverseTurn &event, int eventPlayerId, const GameEventContext &context); - /** @brief Handle a change of the seating / turn order. */ - void - eventPlayerOrderChanged(const Event_PlayerOrderChanged &event, int eventPlayerId, const GameEventContext &context); - /** @brief Handle ping / latency updates. */ void eventPing(const Event_Ping &event, int eventPlayerId, const GameEventContext &context); @@ -458,7 +443,6 @@ signals: void logLeave(PlayerLogic *player, QString reason); void logKicked(); void logTurnReversed(PlayerLogic *player, bool reversed); - void logTurnOrderChanged(QStringList playerNames, bool randomized); void logGameClosed(); void logActivePlayer(PlayerLogic *activePlayer); void logActivePhaseChanged(int activePhase); diff --git a/libcockatrice_game/libcockatrice/game/game_state.h b/libcockatrice_game/libcockatrice/game/game_state.h index 4d02019ce..55784fb91 100644 --- a/libcockatrice_game/libcockatrice/game/game_state.h +++ b/libcockatrice_game/libcockatrice/game/game_state.h @@ -33,11 +33,7 @@ public: void setHostId(int _hostId) { - if (hostId == _hostId) { - return; - } hostId = _hostId; - emit hostChanged(hostId); } QList getClients() const @@ -116,19 +112,6 @@ public: return hostId; } - void setSeatOrder(const QList &order) - { - if (seatOrder != order) { - seatOrder = order; - emit seatOrderChanged(seatOrder); - } - } - - QList getSeatOrder() const - { - return seatOrder; - } - int getSecondsElapsed() const { return secondsElapsed; @@ -140,8 +123,6 @@ signals: void gameStopped(); void activePhaseChanged(int activePhase); void activePlayerChanged(int playerId); - void hostChanged(int hostId); - void seatOrderChanged(const QList &seatOrder); public slots: void incrementGameTime(); @@ -161,9 +142,6 @@ private: int currentPhase; int activePlayer; bool gameClosed; - //! Player ids in seating (turn) order, first to play first. Empty until the - //! server sends an order, in which case players are laid out in id order. - QList seatOrder; }; #endif // COCKATRICE_GAME_STATE_H diff --git a/libcockatrice_game/libcockatrice/game/player/player_manager.h b/libcockatrice_game/libcockatrice/game/player/player_manager.h index 950574112..c95bbbb96 100644 --- a/libcockatrice_game/libcockatrice/game/player/player_manager.h +++ b/libcockatrice_game/libcockatrice/game/player/player_manager.h @@ -57,11 +57,6 @@ public: return localPlayerId; } - void setLocalPlayerId(int _localPlayerId) - { - localPlayerId = _localPlayerId; - } - [[nodiscard]] const QMap &getPlayers() const { return players; diff --git a/libcockatrice_interfaces/libcockatrice/interfaces/interface_game_settings_provider.h b/libcockatrice_interfaces/libcockatrice/interfaces/interface_game_settings_provider.h index 00763d031..2b1ddf8ec 100644 --- a/libcockatrice_interfaces/libcockatrice/interfaces/interface_game_settings_provider.h +++ b/libcockatrice_interfaces/libcockatrice/interfaces/interface_game_settings_provider.h @@ -20,7 +20,6 @@ public: [[nodiscard]] virtual bool getCreateGameAsSpectator() const = 0; [[nodiscard]] virtual int getDefaultStartingLifeTotal() const = 0; [[nodiscard]] virtual bool getShareDecklistsOnLoad() const = 0; - [[nodiscard]] virtual bool getShufflePlayers() const = 0; [[nodiscard]] virtual bool getRememberGameSettings() const = 0; [[nodiscard]] virtual bool getLocalGameRememberSettings() const = 0; [[nodiscard]] virtual int getLocalGameMaxPlayers() const = 0; diff --git a/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.cpp b/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.cpp index f3c4545d7..617324fe3 100644 --- a/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.cpp +++ b/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.cpp @@ -746,8 +746,7 @@ QModelIndex DeckListModel::nodeToIndex(AbstractDecklistNode *node) const void DeckListModel::sortShadowTree(InnerDecklistNode *node, Qt::SortOrder order) { // The mapping is not needed: fresh shadow nodes have no persistent indices yet. - (void)DeckListModelCustomZones::sortWithCustomZonesLast( - root, node, order, activeGroupCriteria == DeckListModelGroupCriteria::MANA_COST && lastKnownColumn != 0); + (void)DeckListModelCustomZones::sortWithCustomZonesLast(root, node, order); for (int i = node->size() - 1; i >= 0; --i) { if (auto *subNode = dynamic_cast(node->at(i))) { @@ -760,8 +759,7 @@ void DeckListModel::sortHelper(InnerDecklistNode *node, Qt::SortOrder order) { // Sort children (custom zones always sorted after groups within a board) and // use the movement mapping to update the list of persistent indices. - const auto mapping = DeckListModelCustomZones::sortWithCustomZonesLast( - root, node, order, activeGroupCriteria == DeckListModelGroupCriteria::MANA_COST && lastKnownColumn != 0); + const auto mapping = DeckListModelCustomZones::sortWithCustomZonesLast(root, node, order); QModelIndexList from, to; int columns = columnCount(); diff --git a/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model_custom_zones.cpp b/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model_custom_zones.cpp index 523708cbd..419c38b04 100644 --- a/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model_custom_zones.cpp +++ b/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model_custom_zones.cpp @@ -8,8 +8,6 @@ #include #include #include -#include -#include #include namespace DeckListModelCustomZones @@ -113,7 +111,7 @@ QList> plainSort(InnerDecklistNode *node, Qt::SortOrder order) * * @return The (preSortRow, finalRow) mapping covering both the sort and the shift. */ -QList> boardSort(InnerDecklistNode *node, Qt::SortOrder order, bool numericGroups) +QList> boardSort(InnerDecklistNode *node, Qt::SortOrder order) { const QVector> sortResult = node->sort(order); @@ -134,27 +132,6 @@ QList> boardSort(InnerDecklistNode *node, Qt::SortOrder order, b } } - if (numericGroups) { - // Only criteria groups are numeric. Mixing numeric and lexical fallback - // comparisons with custom zone names would not define a strict ordering. - std::sort(groups.begin(), groups.end(), [order](const auto *left, const auto *right) { - const QString leftName = left->getName(); - const QString rightName = right->getName(); - bool leftIsNumber, rightIsNumber; - const double leftValue = leftName.toDouble(&leftIsNumber); - const double rightValue = rightName.toDouble(&rightIsNumber); - leftIsNumber = leftIsNumber && std::isfinite(leftValue); - rightIsNumber = rightIsNumber && std::isfinite(rightValue); - if (leftIsNumber != rightIsNumber) { - return leftIsNumber; - } - if (leftIsNumber && leftValue != rightValue) { - return order == Qt::AscendingOrder ? leftValue < rightValue : leftValue > rightValue; - } - return order == Qt::AscendingOrder ? leftName < rightName : leftName > rightName; - }); - } - QVector ordered = groups + customZones; for (int i = 0; i < ordered.size(); ++i) { node->replace(i, ordered[i]); @@ -170,11 +147,10 @@ QList> boardSort(InnerDecklistNode *node, Qt::SortOrder order, b } // namespace -QList> -sortWithCustomZonesLast(InnerDecklistNode *root, InnerDecklistNode *node, Qt::SortOrder order, bool numericGroups) +QList> sortWithCustomZonesLast(InnerDecklistNode *root, InnerDecklistNode *node, Qt::SortOrder order) { const bool isBoardZone = (node != root) && (node->getParent() == root); - return isBoardZone ? boardSort(node, order, numericGroups) : plainSort(node, order); + return isBoardZone ? boardSort(node, order) : plainSort(node, order); } } // namespace DeckListModelCustomZones diff --git a/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model_custom_zones.h b/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model_custom_zones.h index d3f477450..53bdeaa1e 100644 --- a/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model_custom_zones.h +++ b/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model_custom_zones.h @@ -91,15 +91,10 @@ void mirrorCustomZones(const InnerDecklistNode *deckBoardZone, InnerDecklistNode * @param root Root of the shadow tree (used to classify board zones). * @param node The shadow node whose children are reordered. * @param order Sort order to apply. - * @param numericGroups Compare criteria-group names as mana values. Custom zone - * names and card names retain their normal comparison; nonnumeric groups follow - * numeric groups in either sort direction. * @return A list of (preSortRow, finalRow) pairs describing how each node moved. */ -[[nodiscard]] QList> sortWithCustomZonesLast(InnerDecklistNode *root, - InnerDecklistNode *node, - Qt::SortOrder order, - bool numericGroups = false); +[[nodiscard]] QList> +sortWithCustomZonesLast(InnerDecklistNode *root, InnerDecklistNode *node, Qt::SortOrder order); } // namespace DeckListModelCustomZones diff --git a/libcockatrice_network/libcockatrice/network/client/remote/remote_client.cpp b/libcockatrice_network/libcockatrice/network/client/remote/remote_client.cpp index 636c603c6..faacdc9f7 100644 --- a/libcockatrice_network/libcockatrice/network/client/remote/remote_client.cpp +++ b/libcockatrice_network/libcockatrice/network/client/remote/remote_client.cpp @@ -50,12 +50,11 @@ namespace // The server warns that this account still uses the legacy password hash while this client // holds a scrypt verifier. Sending the verifier would downgrade the proof, so the client // stops; the login only succeeds after the saved verifier is gone and the user types the -// password, which also upgrades the account. Unticking "Save password" drops the verifier -// for the next connection. +// password, which also upgrades the account. constexpr const char *const LOGIN_ABORT_AUTH_DOWNGRADE = QT_TRANSLATE_NOOP( "RemoteClient", "The server still uses an old password format for this account, so the saved login on this computer cannot be " - "used. Untick 'Save password' in the Connect window and log in with your password."); + "used. Untick 'Save password' in the Connect window and connect; when it opens again, type your password."); // The account is still on the legacy hash and only the plaintext password can produce it. constexpr const char *const LOGIN_ABORT_PASSWORD_REQUIRED = QT_TRANSLATE_NOOP("RemoteClient", @@ -138,7 +137,6 @@ RemoteClient::~RemoteClient() void RemoteClient::slotSocketError(QAbstractSocket::SocketError /*error*/) { QString errorString = socket->errorString(); - qCWarning(RemoteClientLog) << "Server connection error:" << errorString; doDisconnectFromServer(); emit socketError(errorString); } @@ -148,7 +146,6 @@ void RemoteClient::slotWebSocketError(QAbstractSocket::SocketError /*error*/) QString errorString = websocket->errorString(); if (getStatus() != ClientStatus::StatusDisconnected) { - qCWarning(RemoteClientLog) << "Server connection error:" << errorString; doDisconnectFromServer(); emit socketError(errorString); } @@ -156,7 +153,6 @@ void RemoteClient::slotWebSocketError(QAbstractSocket::SocketError /*error*/) void RemoteClient::slotConnected() { - qCInfo(RemoteClientLog) << "Server transport connected"; timeRunning = lastDataReceived = 0; timer->start(); @@ -170,12 +166,6 @@ void RemoteClient::slotConnected() void RemoteClient::processServerIdentificationEvent(const Event_ServerIdentification &event) { - // Identification arrives before login, including when protocol versions differ. - // Quote the server-supplied strings in the log and limit their size. - const QString serverName = QString::fromStdString(event.server_name()).left(256); - const QString serverVersion = QString::fromStdString(event.server_version()).left(256); - qCInfo(RemoteClientLog) << "Server identification:" << serverName << "version" << serverVersion - << "protocol version" << event.protocol_version(); if (event.protocol_version() != protocolVersion) { emit protocolVersionMismatch(protocolVersion, event.protocol_version()); setStatus(StatusDisconnecting); @@ -708,8 +698,6 @@ void RemoteClient::sendCommandContainer(const CommandContainer &cont) void RemoteClient::connectToHost(const QString &hostname, unsigned int port) { usingWebSocket = port == 443 || port == 80 || port == 4748 || port == 8080; - qCInfo(RemoteClientLog) << "Connecting to server" << hostname << "port" << port - << (usingWebSocket ? "using WebSocket" : "using TCP"); if (usingWebSocket) { QUrl url(QString("%1://%2:%3/servatrice").arg(port == 443 ? "wss" : "ws").arg(hostname).arg(port)); websocket->open(url); diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/game_config.h b/libcockatrice_network/libcockatrice/network/server/remote/game/game_config.h index 0661bd2aa..baf946632 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/game/game_config.h +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/game_config.h @@ -21,7 +21,6 @@ struct GameConfig bool spectatorsSeeEverything = true; int startingLifeTotal = 20; bool shareDecklistsOnLoad = true; - bool shufflePlayers = false; }; #endif diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_participant.cpp b/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_participant.cpp index dcd14d14c..ff8ea14f3 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_participant.cpp +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_participant.cpp @@ -43,7 +43,6 @@ #include #include #include -#include #include #include #include @@ -435,33 +434,6 @@ Response::ResponseCode Server_AbstractParticipant::cmdReverseTurn(const Command_ return Response::RespOk; } -Response::ResponseCode Server_AbstractParticipant::cmdSetPlayerOrder(const Command_SetPlayerOrder &cmd, - ResponseContainer & /*rc*/, - GameEventStorage & /*ges*/) -{ - if ((game->getHostId() != playerId) && !(userInfo->user_level() & ServerInfo_User::IsModerator)) { - return Response::RespFunctionNotAllowed; - } - - if (game->getGameStarted()) { - return Response::RespContextError; - } - - if (cmd.randomize()) { - game->shufflePlayerSeats(); - } else { - QList playerNames; - for (int i = 0; i < cmd.player_names_size(); ++i) { - playerNames.append(QString::fromStdString(cmd.player_names(i))); - } - game->reorderPlayerSeats(playerNames); - } - - // Clients learn the new seating from the Event_PlayerOrderChanged that - // reseatPlayers broadcasts; no full game state resend is needed for it. - return Response::RespOk; -} - Response::ResponseCode Server_AbstractParticipant::processGameCommand(const GameCommand &command, ResponseContainer &rc, GameEventStorage &ges) { @@ -571,9 +543,6 @@ Server_AbstractParticipant::processGameCommand(const GameCommand &command, Respo case GameCommand::SET_PLAYMAT: return cmdSetPlaymat(command.GetExtension(Command_SetPlaymat::ext), rc, ges); break; - case GameCommand::SET_PLAYER_ORDER: - return cmdSetPlayerOrder(command.GetExtension(Command_SetPlayerOrder::ext), rc, ges); - break; default: return Response::RespInvalidCommand; } diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_participant.h b/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_participant.h index 316b9727c..b6d4a8025 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_participant.h +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_participant.h @@ -53,7 +53,6 @@ class Command_DeckSelect; class Command_SetSideboardLock; class Command_ChangeZoneProperties; class Command_SetPlaymat; -class Command_SetPlayerOrder; class Server_AbstractParticipant : public Server_ArrowTarget, public ServerInfo_User_Container { @@ -90,10 +89,6 @@ public: { return playerId; } - void setPlayerId(int _playerId) - { - playerId = _playerId; - } bool isSpectator() const { return roleInfo.isSpectator; @@ -178,8 +173,6 @@ public: virtual Response::ResponseCode cmdReverseTurn(const Command_ReverseTurn & /*cmd*/, ResponseContainer & /*rc*/, GameEventStorage &ges); virtual Response::ResponseCode - cmdSetPlayerOrder(const Command_SetPlayerOrder &cmd, ResponseContainer &rc, GameEventStorage &ges); - virtual Response::ResponseCode cmdChangeZoneProperties(const Command_ChangeZoneProperties &cmd, ResponseContainer &rc, GameEventStorage &ges); Response::ResponseCode processGameCommand(const GameCommand &command, ResponseContainer &rc, GameEventStorage &ges); diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/server_game.cpp b/libcockatrice_network/libcockatrice/network/server/remote/game/server_game.cpp index e2724523e..49db999f6 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/game/server_game.cpp +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_game.cpp @@ -73,13 +73,11 @@ #include #include #include -#include #include #include #include #include #include -#include #include #include #include @@ -93,8 +91,8 @@ Server_Game::Server_Game(const GameConfig &config, Server_Room *_room) spectatorsAllowed(config.spectatorsAllowed), spectatorsNeedPassword(config.spectatorsNeedPassword), spectatorsCanTalk(config.spectatorsCanTalk), spectatorsSeeEverything(config.spectatorsSeeEverything), startingLifeTotal(config.startingLifeTotal), shareDecklistsOnLoad(config.shareDecklistsOnLoad), - shufflePlayers(config.shufflePlayers), inactivityCounter(0), startTimeOfThisGame(0), secondsElapsed(0), - firstGameStarted(false), turnOrderReversed(false), startTime(QDateTime::currentDateTime()), pingClock(nullptr), + inactivityCounter(0), startTimeOfThisGame(0), secondsElapsed(0), firstGameStarted(false), + turnOrderReversed(false), startTime(QDateTime::currentDateTime()), pingClock(nullptr), deckValidationStrategy(new Server_DefaultDeckValidationStrategy), lifecycleStrategy(new Server_DefaultLifecycleStrategy), matchResultStrategy(new Server_NullMatchResultStrategy), gameMutex() @@ -294,32 +292,6 @@ int Server_Game::getSpectatorCount() const return result; } -QList Server_Game::getParticipantsInSeatOrder() const -{ - // Players in seat order first (fall back to id order for seats predating - // seatOrder), then spectators. Player ids are stable; only the ordering - // changes on shuffle/reorder. - QList ordered; - ordered.reserve(participants.size()); - for (int id : seatOrder) { - auto *participant = participants.value(id, nullptr); - if (participant && !participant->isSpectator() && !ordered.contains(participant)) { - ordered.append(participant); - } - } - for (auto *participant : participants.values()) { - if (!participant->isSpectator() && !ordered.contains(participant)) { - ordered.append(participant); - } - } - for (auto *participant : participants.values()) { - if (participant->isSpectator()) { - ordered.append(participant); - } - } - return ordered; -} - void Server_Game::createGameStateChangedEvent(Event_GameStateChanged *event, Server_AbstractParticipant *recipient, bool omniscient, @@ -334,24 +306,16 @@ void Server_Game::createGameStateChangedEvent(Event_GameStateChanged *event, event->set_game_started(false); } - // Emit the player list in seat order so every client (including old ones - // that ignore Event_PlayerOrderChanged) sees the current seating. Player - // ids are stable; only this ordering changes on shuffle/reorder. - QList ordered = getParticipantsInSeatOrder(); - for (Server_AbstractParticipant *participant : ordered) { + for (Server_AbstractParticipant *participant : participants.values()) { participant->getInfo(event->add_player_list(), recipient, omniscient, withUserInfo); } - - if (recipient != nullptr) { - event->set_local_player_id(recipient->getPlayerId()); - } } void Server_Game::sendGameStateToPlayers() { // game state information for replay and omniscient spectators Event_GameStateChanged omniscientEvent; - createGameStateChangedEvent(&omniscientEvent, nullptr, true, true); + createGameStateChangedEvent(&omniscientEvent, nullptr, true, false); GameEventContainer *replayCont = prepareGameEvent(omniscientEvent, -1); replayCont->set_seconds_elapsed(secondsElapsed - startTimeOfThisGame); @@ -363,7 +327,7 @@ void Server_Game::sendGameStateToPlayers() // the data we used for the replay. All spectators are equal, so we don't need to make a createGameStateChangedEvent // call for each one. Event_GameStateChanged spectatorNormalEvent; - createGameStateChangedEvent(&spectatorNormalEvent, nullptr, false, true); + createGameStateChangedEvent(&spectatorNormalEvent, nullptr, false, false); // send game state info to clients according to their role in the game for (auto *participant : participants.values()) { @@ -376,7 +340,7 @@ void Server_Game::sendGameStateToPlayers() } } else { Event_GameStateChanged event; - createGameStateChangedEvent(&event, participant, participant->isJudge(), true); + createGameStateChangedEvent(&event, participant, participant->isJudge(), false); gec = prepareGameEvent(event, -1); } @@ -416,11 +380,6 @@ void Server_Game::doStartGameIfReady(bool forceStartGame) return; } - if (shufflePlayers && players.size() > 1) { - shufflePlayerSeats(); - players = getPlayers(); - } - for (Server_AbstractPlayer *player : players.values()) { player->setupZones(); } @@ -474,116 +433,6 @@ void Server_Game::startGameIfReady(bool forceStartGame) emit sigStartGameIfReady(forceStartGame); } -void Server_Game::shufflePlayerSeats() -{ - QMutexLocker locker(&gameMutex); - - // Collect players in current seat order (fall back to id order for seats - // predating seatOrder). Spectators keep their seats. - QList players; - for (int id : seatOrder) { - auto *participant = participants.value(id, nullptr); - if (participant && !participant->isSpectator()) { - players.append(participant); - } - } - for (auto *participant : participants.values()) { - if (!participant->isSpectator() && !players.contains(participant)) { - players.append(participant); - } - } - QList spectators; - for (auto *participant : participants.values()) { - if (participant->isSpectator()) { - spectators.append(participant); - } - } - - // Fisher-Yates shuffle of the players. - QList shuffledPlayers; - while (!players.isEmpty()) { - const int index = rng->rand(0, players.size() - 1); - shuffledPlayers.append(players.takeAt(index)); - } - - reseatPlayers(shuffledPlayers + spectators, true); -} - -void Server_Game::reorderPlayerSeats(const QList &orderedNames) -{ - QMutexLocker locker(&gameMutex); - - // Collect players in current seat order (fall back to id order for seats - // predating seatOrder). Spectators keep their seats. - QList players; - for (int id : seatOrder) { - auto *participant = participants.value(id, nullptr); - if (participant && !participant->isSpectator()) { - players.append(participant); - } - } - for (auto *participant : participants.values()) { - if (!participant->isSpectator() && !players.contains(participant)) { - players.append(participant); - } - } - QList spectators; - for (auto *participant : participants.values()) { - if (participant->isSpectator()) { - spectators.append(participant); - } - } - - // Order the players according to the given names, appending any player that - // was not listed (should not happen) in their current seat order. - QList orderedPlayers; - QSet placed; - for (const QString &name : orderedNames) { - for (auto *participant : players) { - if (!placed.contains(participant) && (QString::fromStdString(participant->getUserInfo()->name()) == name)) { - orderedPlayers.append(participant); - placed.insert(participant); - break; - } - } - } - for (auto *participant : players) { - if (!placed.contains(participant)) { - orderedPlayers.append(participant); - } - } - - reseatPlayers(orderedPlayers + spectators, false); -} - -void Server_Game::reseatPlayers(const QList &orderedParticipants, bool randomized) -{ - QMutexLocker locker(&gameMutex); - - // Keep player ids stable and only reorder the seating/turn order. Changing - // ids would break old clients (stale localPlayerId from Event_GameJoined - // duplicates their own seat as an opponent), invalidate the home-server - // per-user game/seat mapping used for ISL command routing, and orphan the - // host id. All of those maps stay valid because ids never change here. - // The current host therefore keeps its id and needs no re-key tracking. - seatOrder.clear(); - for (auto *participant : orderedParticipants) { - if (!participant->isSpectator()) { - seatOrder.append(participant->getPlayerId()); - } - } - - // Broadcast the new seating order so that every client can display it. - Event_PlayerOrderChanged event; - for (auto *participant : orderedParticipants) { - if (!participant->isSpectator()) { - event.add_player_names(QString::fromStdString(participant->getUserInfo()->name()).toStdString()); - } - } - event.set_randomized(randomized); - sendGameEventContainer(prepareGameEvent(event, -1)); -} - void Server_Game::stopGameIfFinished() { QMutexLocker locker(&gameMutex); @@ -714,7 +563,6 @@ void Server_Game::addPlayer(Server_AbstractUserInterface *userInterface, allSpectatorsEver.insert(playerName); } else { allPlayersEver.insert(playerName); - seatOrder.append(newParticipant->getPlayerId()); // if the original creator of the game joins, give them host status back //! \todo Transferring host to spectators has side effects. @@ -744,7 +592,6 @@ void Server_Game::removeParticipant(Server_AbstractParticipant *participant, Eve room->getServer()->removePersistentPlayer(QString::fromStdString(participant->getUserInfo()->name()), room->getId(), gameId, participant->getPlayerId()); participants.remove(participant->getPlayerId()); - seatOrder.removeAll(participant->getPlayerId()); bool spectator = participant->isSpectator(); GameEventStorage ges; @@ -764,20 +611,10 @@ void Server_Game::removeParticipant(Server_AbstractParticipant *participant, Eve participant->prepareDestroy(); if (playerHost) { - // Pass host to the next player in seat order so the game always keeps - // a host while players remain. int newHostId = -1; - for (int id : seatOrder) { - if (participants.contains(id)) { - newHostId = id; - break; - } - } - if (newHostId == -1) { - for (auto *otherPlayer : getPlayers().values()) { - newHostId = otherPlayer->getPlayerId(); - break; - } + for (auto *otherPlayer : getPlayers().values()) { + newHostId = otherPlayer->getPlayerId(); + break; } if (newHostId != -1) { hostId = newHostId; @@ -934,20 +771,7 @@ void Server_Game::nextTurn() } auto players = getPlayers(); - // Advance in seat order, not id order, so shuffles/reorders change the - // turn sequence while ids stay stable. - QList keys; - keys.reserve(players.size()); - for (int id : seatOrder) { - if (players.contains(id)) { - keys.append(id); - } - } - for (int id : players.keys()) { - if (!keys.contains(id)) { - keys.append(id); - } - } + const QList keys = players.keys(); int listPos = -1; if (activePlayer != -1) { listPos = keys.indexOf(activePlayer); @@ -997,7 +821,7 @@ void Server_Game::createGameJoinedEvent(Server_AbstractParticipant *joiningParti event2.set_active_phase(activePhase); bool omniscient = (joiningParticipant->isSpectator() && spectatorsSeeEverything) || joiningParticipant->isJudge(); - for (auto *participant : getParticipantsInSeatOrder()) { + for (auto *participant : participants.values()) { participant->getInfo(event2.add_player_list(), joiningParticipant, omniscient, true); } @@ -1126,7 +950,6 @@ void Server_Game::getInfo(ServerInfo_Game &result) const result.set_spectators_can_chat(spectatorsCanTalk); result.set_spectators_omniscient(spectatorsSeeEverything); result.set_share_decklists_on_load(shareDecklistsOnLoad); - result.set_shuffle_players(shufflePlayers); result.set_spectators_count(getSpectatorCount()); result.set_start_time(startTime.toSecsSinceEpoch()); } diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/server_game.h b/libcockatrice_network/libcockatrice/network/server/remote/game/server_game.h index 23ba96598..363c0ca94 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/game/server_game.h +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_game.h @@ -74,11 +74,6 @@ private: int hostId; ServerInfo_User *creatorInfo; QMap participants; - // Seating/turn order as an ordered list of player ids. Player ids themselves - // are stable for the lifetime of the participant; shuffling or reordering - // only permutes this list so old clients, ISL routes and persistent - // references (all keyed by player id) keep working. - QList seatOrder; QSet allPlayersEver, allSpectatorsEver; bool gameStarted; bool gameClosed; @@ -95,7 +90,6 @@ private: bool spectatorsSeeEverything; int startingLifeTotal; bool shareDecklistsOnLoad; - bool shufflePlayers; int inactivityCounter; int startTimeOfThisGame, secondsElapsed; bool firstGameStarted; @@ -115,8 +109,6 @@ private: Server_AbstractParticipant *recipient, bool omniscient, bool withUserInfo); - QList getParticipantsInSeatOrder() const; - void reseatPlayers(const QList &orderedParticipants, bool randomized); void storeGameInformation(); bool canSeePrivateEventsFor(const Server_AbstractParticipant *participant, int privatePlayerId) const; signals: @@ -157,10 +149,6 @@ public: { return participants; } - QList getSeatOrder() const - { - return seatOrder; - } int getGameId() const { return gameId; @@ -201,10 +189,6 @@ public: { return shareDecklistsOnLoad; } - bool getShufflePlayers() const - { - return shufflePlayers; - } Response::ResponseCode checkJoin(ServerInfo_User *user, const QString &_password, const RoleInfo &roleInfo, bool overrideRestrictions); bool containsUser(const QString &userName) const; @@ -218,8 +202,6 @@ public: bool kickParticipant(int playerId); void startGameIfReady(bool forceStartGame); void stopGameIfFinished(); - void shufflePlayerSeats(); - void reorderPlayerSeats(const QList &orderedNames); int getActivePlayer() const { return activePlayer; diff --git a/libcockatrice_network/libcockatrice/network/server/remote/server_protocolhandler.cpp b/libcockatrice_network/libcockatrice/network/server/remote/server_protocolhandler.cpp index 78b9d955f..f9cadc9aa 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/server_protocolhandler.cpp +++ b/libcockatrice_network/libcockatrice/network/server/remote/server_protocolhandler.cpp @@ -1116,7 +1116,6 @@ Server_ProtocolHandler::cmdCreateGame(const Command_CreateGame &cmd, Server_Room int startingLifeTotal = cmd.has_starting_life_total() ? cmd.starting_life_total() : 20; bool shareDecklistsOnLoad = cmd.has_share_decklists_on_load() ? cmd.share_decklists_on_load() : false; - bool shufflePlayers = cmd.has_shuffle_players() ? cmd.shuffle_players() : false; const int gameId = databaseInterface->getNextGameId(); if (gameId == -1) { @@ -1138,8 +1137,7 @@ Server_ProtocolHandler::cmdCreateGame(const Command_CreateGame &cmd, Server_Room .spectatorsCanTalk = cmd.spectators_can_talk(), .spectatorsSeeEverything = cmd.spectators_see_everything(), .startingLifeTotal = startingLifeTotal, - .shareDecklistsOnLoad = shareDecklistsOnLoad, - .shufflePlayers = shufflePlayers}; + .shareDecklistsOnLoad = shareDecklistsOnLoad}; auto *game = new Server_Game(config, room); diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/CMakeLists.txt b/libcockatrice_protocol/libcockatrice/protocol/pb/CMakeLists.txt index 5fba2631d..059e83096 100644 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/CMakeLists.txt +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/CMakeLists.txt @@ -66,7 +66,6 @@ set(PROTO_FILES command_set_card_attr.proto command_set_card_counter.proto command_set_counter.proto - command_set_player_order.proto command_set_playmat.proto command_set_sideboard_lock.proto command_set_sideboard_plan.proto @@ -111,7 +110,6 @@ set(PROTO_FILES event_list_rooms.proto event_move_card.proto event_notify_user.proto - event_player_order_changed.proto event_player_properties_changed.proto event_remove_from_list.proto event_remove_messages.proto diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/command_set_player_order.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/command_set_player_order.proto deleted file mode 100644 index 97e27038f..000000000 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/command_set_player_order.proto +++ /dev/null @@ -1,15 +0,0 @@ -syntax = "proto2"; -import "game_commands.proto"; -message Command_SetPlayerOrder { - extend GameCommand { - optional Command_SetPlayerOrder ext = 1206; - } - - // The desired seating order, expressed as player names. Only used when - // randomize is false. - repeated string player_names = 1; - - // When set, the server shuffles the seating order instead of using - // player_names. - optional bool randomize = 2; -} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/event_game_state_changed.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/event_game_state_changed.proto index bbf2d0f98..5c8aee3fe 100644 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/event_game_state_changed.proto +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/event_game_state_changed.proto @@ -24,9 +24,4 @@ message Event_GameStateChanged { // the amount of seconds since the game started optional uint32 seconds_elapsed = 5; - - // the id of the player this event is addressed to. Used to re-identify the - // local player after seats have been shuffled. Only set for events sent to - // a specific participant, never for omniscient/replay events. - optional sint32 local_player_id = 6; } diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/event_player_order_changed.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/event_player_order_changed.proto deleted file mode 100644 index d119398b7..000000000 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/event_player_order_changed.proto +++ /dev/null @@ -1,13 +0,0 @@ -syntax = "proto2"; -import "game_event.proto"; -message Event_PlayerOrderChanged { - extend GameEvent { - optional Event_PlayerOrderChanged ext = 2023; - } - - // The new seating order, expressed as player names. - repeated string player_names = 1; - - // Whether the order was obtained by randomizing rather than set explicitly. - optional bool randomized = 2; -} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/game_commands.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/game_commands.proto index eb399ffb9..2e5b88978 100644 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/game_commands.proto +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/game_commands.proto @@ -180,11 +180,6 @@ message GameCommand { /// Server: Server_Player::cmdSetPlaymat /// Client: reflected via player properties changed event SET_PLAYMAT = 1035; - - /// Set the seating (turn) order of the players. - /// Server: Server_AbstractParticipant::cmdSetPlayerOrder - /// Client: reflected via Event_PlayerOrderChanged - SET_PLAYER_ORDER = 1206; } extensions 100 to max; diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/game_event.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/game_event.proto index 573d26453..7d3147701 100644 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/game_event.proto +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/game_event.proto @@ -34,7 +34,6 @@ message GameEvent { CHANGE_ZONE_PROPERTIES = 2020; REVERSE_TURN = 2021; GAME_LOG_NOTICE = 2022; - PLAYER_ORDER_CHANGED = 2023; } optional sint32 player_id = 1 [default = -1]; extensions 100 to max; diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/room_commands.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/room_commands.proto index 3be7e4e64..9d9c6d277 100644 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/room_commands.proto +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/room_commands.proto @@ -78,9 +78,6 @@ message Command_CreateGame { // share decklists with all players when selected optional bool share_decklists_on_load = 14; - - // shuffle the player order (seats) before each game starts - optional bool shuffle_players = 15; } message Command_JoinGame { diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_game.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_game.proto index 39ec79914..9989ae18a 100644 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_game.proto +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_game.proto @@ -48,9 +48,6 @@ message ServerInfo_Game { // decklists are sent to all players when loaded optional bool share_decklists_on_load = 15; - // players are shuffled into new seats before each game starts - optional bool shuffle_players = 16; - // the current player count optional uint32 player_count = 30; diff --git a/libcockatrice_settings/libcockatrice/settings/game_settings.cpp b/libcockatrice_settings/libcockatrice/settings/game_settings.cpp index 89fe7b85e..c8c6463e3 100644 --- a/libcockatrice_settings/libcockatrice/settings/game_settings.cpp +++ b/libcockatrice_settings/libcockatrice/settings/game_settings.cpp @@ -71,11 +71,6 @@ bool GameSettings::getShareDecklistsOnLoad() const return getValue("shareDecklistsOnLoad", "game").toBool(); } -bool GameSettings::getShufflePlayers() const -{ - return getValue("shuffleplayers", "game").toBool(); -} - bool GameSettings::getRememberGameSettings() const { return getValue("rememberGameSettings", "game", QString(), true).toBool(); @@ -156,11 +151,6 @@ void GameSettings::setShareDecklistsOnLoad(bool _shareDecklistsOnLoad) setValue(_shareDecklistsOnLoad, "shareDecklistsOnLoad", "game"); } -void GameSettings::setShufflePlayers(bool _shufflePlayers) -{ - setValue(_shufflePlayers, "shuffleplayers", "game"); -} - void GameSettings::setRememberGameSettings(bool _rememberGameSettings) { setValue(_rememberGameSettings, "rememberGameSettings", "game"); diff --git a/libcockatrice_settings/libcockatrice/settings/game_settings.h b/libcockatrice_settings/libcockatrice/settings/game_settings.h index 45ec8554b..3e64234fc 100644 --- a/libcockatrice_settings/libcockatrice/settings/game_settings.h +++ b/libcockatrice_settings/libcockatrice/settings/game_settings.h @@ -27,7 +27,6 @@ public: [[nodiscard]] bool getCreateGameAsSpectator() const override; [[nodiscard]] int getDefaultStartingLifeTotal() const override; [[nodiscard]] bool getShareDecklistsOnLoad() const override; - [[nodiscard]] bool getShufflePlayers() const override; [[nodiscard]] bool getRememberGameSettings() const override; [[nodiscard]] bool getLocalGameRememberSettings() const override; [[nodiscard]] int getLocalGameMaxPlayers() const override; @@ -45,7 +44,6 @@ public: void setCreateGameAsSpectator(bool _createGameAsSpectator); void setDefaultStartingLifeTotal(int _defaultStartingLifeTotal); void setShareDecklistsOnLoad(bool _shareDecklistsOnLoad); - void setShufflePlayers(bool _shufflePlayers); void setRememberGameSettings(bool _rememberGameSettings); void setLocalGameRememberSettings(bool value); void setLocalGameMaxPlayers(int value); diff --git a/libcockatrice_utility/CMakeLists.txt b/libcockatrice_utility/CMakeLists.txt index 98ad40e69..8d2cb5b92 100644 --- a/libcockatrice_utility/CMakeLists.txt +++ b/libcockatrice_utility/CMakeLists.txt @@ -8,7 +8,6 @@ set(UTILITY_SOURCES libcockatrice/utility/passwordhasher.cpp libcockatrice/utility/report_categories.cpp libcockatrice/utility/server_rate_limiter.cpp - libcockatrice/utility/svg_recolor.cpp libcockatrice/utility/translation_loader.cpp libcockatrice/utility/user_data_backup.cpp libcockatrice/utility/warning_categories.cpp @@ -35,7 +34,6 @@ set(UTILITY_HEADERS libcockatrice/utility/zone_names.h libcockatrice/utility/days_years_between.h libcockatrice/utility/server_rate_limiter.h - libcockatrice/utility/svg_recolor.h libcockatrice/utility/translation_loader.h libcockatrice/utility/user_data_backup.h libcockatrice/utility/warning_categories.h diff --git a/libcockatrice_utility/libcockatrice/utility/svg_recolor.cpp b/libcockatrice_utility/libcockatrice/utility/svg_recolor.cpp deleted file mode 100644 index 041e289fe..000000000 --- a/libcockatrice_utility/libcockatrice/utility/svg_recolor.cpp +++ /dev/null @@ -1,44 +0,0 @@ -#include "svg_recolor.h" - -#include - -namespace -{ -constexpr QByteArrayView SENTINEL = "currentColor"; - -/// Case-insensitive byte search for the sentinel, starting at @p from. -/// QByteArray::indexOf() only matches case-sensitively for QByteArrayView, and -/// its char overload cannot match a word. -qsizetype findSentinel(const QByteArray &bytes, qsizetype from) -{ - // QByteArrayView::sliced() is a view, so the scan does not allocate a copy - // at every byte position (QByteArray::sliced() would). - const QByteArrayView view(bytes); - for (qsizetype i = from; i + SENTINEL.size() <= view.size(); ++i) { - if (view.sliced(i, SENTINEL.size()).compare(SENTINEL, Qt::CaseInsensitive) == 0) { - return i; - } - } - return -1; -} -} // namespace - -QByteArray resolveSvgColorSentinel(const QByteArray &svg, const QByteArray &color) -{ - // The sentinel and the replacement are both ASCII, so stay in bytes: a - // QString round trip would decode the whole document (and mangle bytes that - // are not valid UTF-8) even on the no-op path. - qsizetype pos = findSentinel(svg, 0); - if (pos < 0) { - return svg; - } - - QByteArray resolved = svg; - while (pos >= 0) { - resolved.replace(pos, SENTINEL.size(), color); - // Skip past the inserted color: a caller's color never contains the - // sentinel, and this keeps the scan linear. - pos = findSentinel(resolved, pos + color.size()); - } - return resolved; -} diff --git a/libcockatrice_utility/libcockatrice/utility/svg_recolor.h b/libcockatrice_utility/libcockatrice/utility/svg_recolor.h deleted file mode 100644 index 56d609b87..000000000 --- a/libcockatrice_utility/libcockatrice/utility/svg_recolor.h +++ /dev/null @@ -1,31 +0,0 @@ -/** - * @file svg_recolor.h - * @ingroup Core - */ - -#ifndef SVG_RECOLOR_H -#define SVG_RECOLOR_H - -#include - -/** - * @brief Resolves the `currentColor` sentinel in authored SVG data. - * - * Qt's SVG renderer has no CSS cascade or ambient color context, so the - * sentinel is not resolved by Qt. Callers pass the concrete color they want - * (for example a QPalette role serialized to #RRGGBB) and receive SVG bytes - * that are safe to hand to QSvgRenderer. - * - * Matching is a plain, case-insensitive substring search, not a CSS parse: an - * identifier that merely starts with the sentinel (for example - * `id="currentColorStop"`) is rewritten too. That is acceptable for the assets - * this project authors, and keeps the resolver dependency-free. - * - * @param svg SVG source bytes, expected to be UTF-8. Invalid UTF-8 is preserved - * byte-for-byte, since the resolver never decodes the document. - * @param color Concrete SVG color (for example "#ff00ff"), as ASCII bytes. - * @return The recolored SVG source; the input is left untouched. - */ -QByteArray resolveSvgColorSentinel(const QByteArray &svg, const QByteArray &color); - -#endif // SVG_RECOLOR_H diff --git a/oracle/CMakeLists.txt b/oracle/CMakeLists.txt index 4ca2acd07..587463572 100644 --- a/oracle/CMakeLists.txt +++ b/oracle/CMakeLists.txt @@ -17,7 +17,6 @@ set(ORACLE_WIN32_QM_INSTALL_DIR "translations") # Sources # ------------------------ set(oracle_SOURCES - src/database_writer.cpp src/main.cpp src/oraclewizard.cpp src/oracleimporter.cpp @@ -31,7 +30,6 @@ set(oracle_SOURCES ../cockatrice/src/client/network/update/client/release_channel.cpp ../cockatrice/src/interface/pixel_map_generator.cpp ../cockatrice/src/interface/theme_config.cpp - ../cockatrice/src/interface/theme_icon.cpp ../cockatrice/src/interface/theme_manager.cpp ${VERSION_STRING_CPP} ) diff --git a/oracle/src/database_writer.cpp b/oracle/src/database_writer.cpp deleted file mode 100644 index e80aa853a..000000000 --- a/oracle/src/database_writer.cpp +++ /dev/null @@ -1,52 +0,0 @@ -#include "database_writer.h" - -#include -#include -#include -#include - -namespace OracleDatabase -{ -QString validateXml(const QByteArray &data) -{ - QXmlStreamReader xml(data); - if (!xml.readNextStartElement() || xml.name() != QLatin1String("cockatrice_carddatabase")) { - return QCoreApplication::translate("OracleDatabase", "The download is not a Cockatrice card database."); - } - const auto version = xml.attributes().value("version"); - if (version != QLatin1String("3") && version != QLatin1String("4")) { - return QCoreApplication::translate("OracleDatabase", "The downloaded card database version is not supported."); - } - while (!xml.atEnd()) { - xml.readNext(); - } - if (xml.hasError()) { - return QCoreApplication::translate("OracleDatabase", - "The downloaded card database is incomplete or invalid: %1") - .arg(xml.errorString()); - } - return {}; -} - -bool saveXml(const QString &fileName, const QByteArray &data, QString *error) -{ - const QString validationError = validateXml(data); - if (!validationError.isEmpty()) { - if (error) { - *error = validationError; - } - return false; - } - QSaveFile file(fileName); - if (!file.open(QIODevice::WriteOnly) || file.write(data) != data.size() || !file.commit()) { - if (error) { - *error = file.errorString(); - } - return false; - } - if (error) { - error->clear(); - } - return true; -} -} // namespace OracleDatabase diff --git a/oracle/src/database_writer.h b/oracle/src/database_writer.h deleted file mode 100644 index 173fd23ab..000000000 --- a/oracle/src/database_writer.h +++ /dev/null @@ -1,17 +0,0 @@ -#ifndef ORACLE_DATABASE_WRITER_H -#define ORACLE_DATABASE_WRITER_H - -#include -#include - -namespace OracleDatabase -{ -// Validate the complete downloaded document without allocating a card database. -// Empty string means valid; otherwise returns a diagnostic suitable for the wizard. -QString validateXml(const QByteArray &data); -// Never truncate the installed database: validate, write a sibling temporary file, -// check the complete write and atomically commit it. -bool saveXml(const QString &fileName, const QByteArray &data, QString *error = nullptr); -} // namespace OracleDatabase - -#endif diff --git a/oracle/src/oracleimporter.cpp b/oracle/src/oracleimporter.cpp index c83c962e3..477c54d21 100644 --- a/oracle/src/oracleimporter.cpp +++ b/oracle/src/oracleimporter.cpp @@ -100,10 +100,6 @@ static CardSet::Priority getSetPriority(const QString &setType, const QString &s bool OracleImporter::readSetsFromByteArray(QByteArray data) { - importSuccessful = false; - importError.clear(); - allSets.clear(); - rawSetsData.clear(); const RawJson::ScanProgressCallback progress = progressReporting ? [this]( @@ -768,15 +764,7 @@ int OracleImporter::startImport() { static ICardSetPriorityController *noOpController = new NoopCardSetPriorityController(); - importSuccessful = false; - importError.clear(); - sets.clear(); - cards.clear(); - localizedEntries.clear(); - splitLocalizedTexts.clear(); - if (allSets.isEmpty()) { - return failImport(tr("No set has been selected for import.")); - } + importCancelled.storeRelease(0); // Pre-allocate the cards hash to avoid rehashing during import. Keys are // distinct card names while raw ranges only count printings (AllPrintings @@ -798,7 +786,9 @@ int OracleImporter::startImport() for (const SetToDownload &curSetToParse : allSets) { if (importCancelled.loadAcquire()) { - return failImport(tr("The card database import was cancelled.")); + // The wizard was closed mid-import: stop at the next set boundary so + // the caller can wait for this future without processing every set. + break; } CardSetPtr newSet = CardSet::newInstance(noOpController, curSetToParse.getShortName(), @@ -808,20 +798,30 @@ int OracleImporter::startImport() // parse only this set's slice of the raw document so the whole JSON tree is // never kept in memory at once const RawJson::SetDataRange &rawRange = curSetToParse.getRawRange(); - if (rawRange.start < 0 || rawRange.length <= 0 || rawRange.start > rawSetsData.size() || - rawRange.length > rawSetsData.size() - rawRange.start) { - return failImport(tr("The source data for set %1 is unavailable. Load the sets file again.") - .arg(curSetToParse.getShortName())); + const qsizetype rangeEnd = rawRange.start + rawRange.length; + if (rawRange.start < 0 || rawRange.length <= 0 || rangeEnd > rawSetsData.size()) { + // rawSetsData is cleared by releaseSetData() while SetToDownload copies + // taken from getSets() keep their ranges, and nothing else enforces the + // pairing — so never index past the buffer on stale/mismatched ranges. + qWarning() << "error: out-of-bounds raw range for set" << curSetToParse.getShortName() << "skipping"; + ++setIndex; + emit setIndexChanged(0, setIndex, curSetToParse.getLongName()); + continue; } + // sliced() shares the buffer instead of deep-copying the slice; the largest + // sets in AllPrintings are tens of MB, so the copy is worth avoiding here. const QByteArray setBytes = rawSetsData.sliced(rawRange.start, rawRange.length); QJsonParseError parseError; const QJsonDocument setDoc = QJsonDocument::fromJson(setBytes, &parseError); if (parseError.error != QJsonParseError::NoError) { - return failImport( - tr("Could not import set %1: %2").arg(curSetToParse.getShortName(), parseError.errorString())); - } - if (!setDoc.isObject() || !setDoc.object().value("cards").isArray()) { - return failImport(tr("Set %1 does not contain a card list.").arg(curSetToParse.getShortName())); + qWarning() << "error: parsing card data for set" << curSetToParse.getShortName() << ":" + << parseError.errorString(); + ++setIndex; + // Keep the progress accounting honest: a set that failed to parse + // still advanced the index, so report it (with zero imported cards) + // rather than letting SaveSetsPage's bar stall per failed set. + emit setIndexChanged(0, setIndex, curSetToParse.getLongName()); + continue; } // Only add the set to the database once its slice parsed cleanly; @@ -838,14 +838,7 @@ int OracleImporter::startImport() emit setIndexChanged(numCardsInSet, setIndex, curSetToParse.getLongName()); } - if (importCancelled.loadAcquire()) { - return failImport(tr("The card database import was cancelled.")); - } - if (cards.isEmpty()) { - return failImport(tr("No cards were imported. The existing card database has been kept.")); - } applyLocalizedData(); - importSuccessful = true; emit setIndexChanged(0, setIndex, QString()); @@ -853,23 +846,8 @@ int OracleImporter::startImport() return setIndex; } -int OracleImporter::failImport(const QString &message) -{ - importSuccessful = false; - importError = message; - sets.clear(); - cards.clear(); - localizedEntries.clear(); - splitLocalizedTexts.clear(); - qWarning() << message; - return 0; -} - bool OracleImporter::saveToFile(const QString &fileName, const QString &sourceUrl, const QString &sourceVersion) { - if (!importSuccessful) { - return false; - } CockatriceXml4Parser parser(new NoopCardSetPriorityController()); return parser.saveToFile(createDefaultMagicFormats(), sets, cards, fileName, sourceUrl, sourceVersion); @@ -883,7 +861,6 @@ void OracleImporter::releaseSetData() void OracleImporter::clear() { - prepareImport(); sets.clear(); cards.clear(); allSets.clear(); diff --git a/oracle/src/oracleimporter.h b/oracle/src/oracleimporter.h index 5ebe9cf36..d872e184e 100644 --- a/oracle/src/oracleimporter.h +++ b/oracle/src/oracleimporter.h @@ -198,11 +198,6 @@ private: * the whole import (or racing it). */ QAtomicInt importCancelled; - bool importSuccessful = false; - QString importError; - - // Discard the incomplete in-memory database; the installed file is untouched. - int failImport(const QString &message); /** * The ISO-639 language code whose foreignData is imported; "en" by default. @@ -283,24 +278,7 @@ public: * the wizard can hand over its decompressed buffer without copying it. */ bool readSetsFromByteArray(QByteArray data); - // Returns the complete selected-set count on success, zero on failure or cancellation. int startImport(); - bool hasSuccessfulImport() const - { - return importSuccessful; - } - const QString &getImportError() const - { - return importError; - } - // Call on the owning thread before scheduling a worker, so a cancellation - // arriving before that worker starts is not lost. - void prepareImport() - { - importCancelled.storeRelease(0); - importSuccessful = false; - importError.clear(); - } /** * @brief Requests an in-flight startImport() to stop at the next set boundary. * diff --git a/oracle/src/oraclewizard.cpp b/oracle/src/oraclewizard.cpp index dc8c7ecb3..c33d821a5 100644 --- a/oracle/src/oraclewizard.cpp +++ b/oracle/src/oraclewizard.cpp @@ -1,7 +1,6 @@ #include "oraclewizard.h" #include "client/settings/cache_settings.h" -#include "database_writer.h" #include "main.h" #include "oracleimporter.h" #include "pages.h" @@ -142,13 +141,6 @@ void OracleWizard::reject() } QWizard::reject(); - // A hidden wizard does not close the application's last visible window. - // Stop background refreshes explicitly so the hosting client receives a verdict, - // using the same rule as main(): a reject after the database was written is still - // a successful update, only a reject that wrote nothing counts as a failure. - if (backgroundMode) { - QCoreApplication::exit(hasSavedDatabase() ? 0 : 1); - } } void OracleWizard::runInBackground() @@ -220,10 +212,17 @@ void OracleWizard::disableButtons() bool OracleWizard::saveTokensToFile(const QString &fileName) { - QString error; - if (!OracleDatabase::saveXml(fileName, tokensData, &error)) { - qWarning() << error; + QFile file(fileName); + if (!file.open(QIODevice::WriteOnly)) { + qDebug() << "File open (w) failed for" << fileName; return false; } + + if (file.write(tokensData) == -1) { + qDebug() << "File write (w) failed for" << fileName; + return false; + } + + file.close(); return true; } diff --git a/oracle/src/pages.cpp b/oracle/src/pages.cpp index f5c38be1c..4c664a38c 100644 --- a/oracle/src/pages.cpp +++ b/oracle/src/pages.cpp @@ -1,7 +1,6 @@ #include "pages.h" #include "client/settings/cache_settings.h" -#include "database_writer.h" #include "main.h" #include "oracleimporter.h" #include "oraclewizard.h" @@ -44,7 +43,6 @@ #include #include #include -#include #include #include #include @@ -171,16 +169,7 @@ LoadSetsResult loadSetsData(const QPointer &importer, QByteArray break; } - // Test the first non-space byte in place: trimmed() would duplicate the whole - // payload, which on the plain-XML path is the largest download we handle. - const auto firstNonSpace = std::find_if_not(data.cbegin(), data.cend(), - [](char c) { return c == ' ' || c == '\t' || c == '\n' || c == '\r'; }); - const bool looksLikeXml = firstNonSpace != data.cend() && *firstNonSpace == '<'; - if (looksLikeXml) { - result.errorMessage = OracleDatabase::validateXml(data); - if (!result.errorMessage.isEmpty()) { - return result; - } + if (data.startsWith("<")) { result.ok = true; result.plainXml = true; result.xmlData = std::move(data); @@ -662,10 +651,6 @@ void LoadSetsPage::importFinished() } else if (!result.ok && !result.plainXml) { qWarning() << tr("The file was retrieved successfully, but it does not contain any sets data."); } - if (!result.ok) { - wizard()->reject(); - return; - } emit readyToContinue(); return; } @@ -732,7 +717,7 @@ SaveSetsPage::SaveSetsPage(QWidget *parent) : OracleWizardPage(parent) bool SaveSetsPage::isComplete() const { - return !importActive && importReady; + return !importActive; } void SaveSetsPage::cleanupPage() @@ -745,11 +730,8 @@ void SaveSetsPage::cleanupPage() void SaveSetsPage::initializePage() { - importReady = false; retranslateUi(); if (wizard()->downloadedPlainXml) { - importReady = true; - emit completeChanged(); messageLog->hide(); progressBar->hide(); if (wizard()->backgroundMode) { @@ -775,9 +757,6 @@ void SaveSetsPage::initializePage() emit completeChanged(); const QPointer importer = wizard()->importer; - if (auto *activeImporter = importer.data()) { - activeImporter->prepareImport(); - } importFuture = QtConcurrent::run([importer] { return importer ? importer->startImport() : 0; }); importWatcher.setFuture(importFuture); } @@ -809,28 +788,22 @@ void SaveSetsPage::importFinished() const int setsImported = importWatcher.result(); const QPointer importer = wizard()->importer; - importReady = importer && importer->hasSuccessfulImport() && setsImported == totalSets; - const QString error = importer ? importer->getImportError() : tr("The card database importer is unavailable."); if (importer) { importer->releaseSetData(); } - wizard()->button(QWizard::NextButton)->setEnabled(importReady); - emit completeChanged(); - if (!importReady) { - const QString message = error.isEmpty() ? tr("No set has been imported.") : error; - if (wizard()->backgroundMode) { - qWarning() << message; - wizard()->reject(); - } else { - QMessageBox::critical(this, tr("Error"), message); + if (wizard()->backgroundMode) { + if (setsImported == 0) { + qWarning() << tr("No set has been imported."); } + emit readyToContinue(); return; } progressBar->setValue(progressBar->maximum()); - if (wizard()->backgroundMode) { - emit readyToContinue(); + + if (setsImported == 0) { + QMessageBox::critical(this, tr("Error"), tr("No set has been imported.")); } } @@ -881,18 +854,6 @@ void SaveSetsPage::updateTotalProgress(int cardsImported, int setIndex, const QS bool SaveSetsPage::validatePage() { - if (!isComplete()) { - return false; - } - const auto failSave = [this](const QString &message) { - qWarning() << message; - if (wizard()->backgroundMode) { - wizard()->reject(); - } else { - QMessageBox::critical(this, tr("Error"), message); - } - return false; - }; QString defaultPath = SettingsCache::instance().getCardDatabasePath(); QString windowName = tr("Save card database"); QString fileType = tr("XML; card database (*.xml)"); @@ -911,21 +872,24 @@ bool SaveSetsPage::validatePage() QFileInfo fi(fileName); QDir fileDir(fi.path()); if (!fileDir.exists() && !fileDir.mkpath(fileDir.absolutePath())) { - return failSave(tr("The database directory could not be created: %1").arg(fileDir.absolutePath())); + return false; } if (wizard()->downloadedPlainXml) { - QString error; - if (!OracleDatabase::saveXml(fileName, wizard()->xmlData, &error)) { - return failSave(tr("The file could not be saved to %1: %2").arg(fileName, error)); + QFile file(fileName); + if (!file.open(QIODevice::WriteOnly)) { + qDebug() << "File write (w) failed for" << fileName; + return false; + } + if (file.write(wizard()->xmlData) < 1) { + qDebug() << "File write (w) failed for" << fileName; + return false; } wizard()->xmlData.clear(); } else if (!wizard()->importer->saveToFile(fileName, wizard()->getCardSourceUrl(), wizard()->getCardSourceVersion())) { - // The importer refuses to write a database whose import did not finish, and - // reports the reason there; do not dress an import failure up as a disk failure. - const QString importError = wizard()->importer->getImportError(); - return failSave(importError.isEmpty() ? tr("The file could not be saved to %1").arg(fileName) : importError); + QMessageBox::critical(this, tr("Error"), tr("The file could not be saved to %1").arg(fileName)); + return false; } wizard()->markDatabaseSaved(); diff --git a/oracle/src/pages.h b/oracle/src/pages.h index e29a8e84a..0530010ba 100644 --- a/oracle/src/pages.h +++ b/oracle/src/pages.h @@ -128,7 +128,6 @@ private: QFuture importFuture; int totalSets = 0; bool importActive = false; - bool importReady = false; protected: void initializePage() override; diff --git a/oracle/src/pagetemplates.cpp b/oracle/src/pagetemplates.cpp index 7e5f30bb7..dcc8e070c 100644 --- a/oracle/src/pagetemplates.cpp +++ b/oracle/src/pagetemplates.cpp @@ -1,6 +1,5 @@ #include "pagetemplates.h" -#include "database_writer.h" #include "oraclewizard.h" #include @@ -185,17 +184,9 @@ void SimpleDownloadFilePage::actDownloadFinished() } QNetworkReply::NetworkError errorCode = reply->error(); if (errorCode != QNetworkReply::NoError) { - const QString message = tr("Network error: %1.").arg(reply->errorString()); - reply->deleteLater(); - // A hidden wizard cannot show a modal box: without an explicit stop it would sit - // here forever and the hosting client would wait with no timeout. - if (wizard()->backgroundMode) { - qWarning() << message; - wizard()->reject(); - return; - } - QMessageBox::critical(this, tr("Error"), message); + QMessageBox::critical(this, tr("Error"), tr("Network error: %1.").arg(reply->errorString())); wizard()->enableButtons(); + reply->deleteLater(); return; } @@ -227,15 +218,6 @@ void SimpleDownloadFilePage::actDownloadFinished() bool SimpleDownloadFilePage::saveToFile() { - const auto failSave = [this](const QString &message) { - qWarning() << message; - if (wizard()->backgroundMode) { - wizard()->reject(); - } else { - QMessageBox::critical(this, tr("Error"), message); - } - return false; - }; QString defaultPath = getDefaultSavePath(); QString windowName = getWindowTitle(); QString fileType = getFileType(); @@ -254,11 +236,12 @@ bool SimpleDownloadFilePage::saveToFile() QFileInfo fi(fileName); QDir fileDir(fi.path()); if (!fileDir.exists() && !fileDir.mkpath(fileDir.absolutePath())) { - return failSave(tr("The database directory could not be created: %1").arg(fileDir.absolutePath())); + return false; } if (!internalSaveToFile(fileName)) { - return failSave(tr("The file could not be saved to %1").arg(fileName)); + QMessageBox::critical(this, tr("Error"), tr("The file could not be saved to %1").arg(fileName)); + return false; } // clean saved downloadData @@ -269,10 +252,17 @@ bool SimpleDownloadFilePage::saveToFile() bool SimpleDownloadFilePage::internalSaveToFile(const QString &fileName) { - QString error; - if (!OracleDatabase::saveXml(fileName, downloadData, &error)) { - qWarning() << error; + QFile file(fileName); + if (!file.open(QIODevice::WriteOnly)) { + qDebug() << "File open (w) failed for" << fileName; return false; } + + if (file.write(downloadData) == -1) { + qDebug() << "File write (w) failed for" << fileName; + return false; + } + + file.close(); return true; } diff --git a/oracle/translations/oracle_de.ts b/oracle/translations/oracle_de.ts index 50a5f8e20..faf08d5ce 100644 --- a/oracle/translations/oracle_de.ts +++ b/oracle/translations/oracle_de.ts @@ -137,22 +137,22 @@ Downloading (0 MB) - Herunterladen (0 MB) + Downloading (%1 MB) - Herunterladen (%1 MB) + Parsing file (%1%) - Datei wird verarbeitet (%1%) + Extracting file... - Extrahiere Datei... + @@ -168,7 +168,7 @@ Parsing file (100%) - Datei wird verarbeitet (100%) + @@ -348,7 +348,7 @@ A Cockatrice card database file of %1 MB has been downloaded. - Eine Cockatrice-Kartendatenbankdatei der Größe %1 MB wurde heruntergeladen. + @@ -434,7 +434,7 @@ Downloading (0 MB) Downloading (0MB) - Herunterladen (0 MB) + diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 84b9f02bf..0bb9654a1 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -177,29 +177,7 @@ cockatrice_add_test(NAME test_age_formatting SOURCES test_age_formatting.cpp LIB cockatrice_add_test(NAME user_data_backup_test SOURCES user_data_backup_test.cpp LIBS libcockatrice_utility) cockatrice_add_test(NAME password_hash_test SOURCES password_hash_test.cpp LIBS libcockatrice_utility) cockatrice_add_test(NAME report_categories_test SOURCES report_categories_test.cpp LIBS libcockatrice_utility) -cockatrice_add_test(NAME svg_recolor_test SOURCES svg_recolor_test.cpp LIBS libcockatrice_utility) cockatrice_add_test(NAME warning_categories_test SOURCES warning_categories_test.cpp LIBS libcockatrice_utility) -cockatrice_add_test( - NAME - user_games_model_test - SOURCES - ${CMAKE_SOURCE_DIR}/cockatrice/src/interface/widgets/server/user/user_games_model.cpp - user_games_model_test.cpp - LIBS - libcockatrice_network_client_abstract - INCLUDE_DIRS - ${CMAKE_SOURCE_DIR}/cockatrice/src - ${CMAKE_SOURCE_DIR}/libcockatrice_network -) -cockatrice_add_test( - NAME - spoiler_file_test - SOURCES - ${CMAKE_SOURCE_DIR}/cockatrice/src/client/network/update/card_spoiler/spoiler_file.cpp - spoiler_file_test.cpp - INCLUDE_DIRS - ${CMAKE_SOURCE_DIR}/cockatrice/src -) # The installer's holder probe is Windows-only, but the wording it produces and the # question which programs are the payload's own are plain C++ and are what a stuck user # has to act on, so they are tested everywhere. @@ -230,18 +208,6 @@ cockatrice_add_test( libcockatrice_deck_list libcockatrice_utility ) -cockatrice_add_test( - NAME - card_art_crop_test - SOURCES - ${CMAKE_SOURCE_DIR}/cockatrice/src/interface/widgets/cards/card_art_utils.cpp - ${VERSION_STRING_CPP} - card_art_crop_test.cpp - LIBS - libcockatrice_card - INCLUDE_DIRS - ${CMAKE_SOURCE_DIR}/cockatrice/src -) cockatrice_add_test( NAME server_rate_limiter_test @@ -448,10 +414,18 @@ cockatrice_add_test( ) cockatrice_add_test( NAME - font_config_test + loader_local_matching_test SOURCES - ${CMAKE_SOURCE_DIR}/cockatrice/src/interface/theme_config.cpp - font_config_test.cpp + ${CMAKE_SOURCE_DIR}/cockatrice/src/interface/card_picture_loader/card_picture_loader_local.cpp + ${CMAKE_SOURCE_DIR}/cockatrice/src/client/settings/cache_settings.cpp + ${CMAKE_SOURCE_DIR}/cockatrice/src/client/settings/card_counter_settings.cpp + ${CMAKE_SOURCE_DIR}/cockatrice/src/client/settings/shortcuts_settings.cpp + ${CMAKE_SOURCE_DIR}/cockatrice/src/client/network/update/client/release_channel.cpp + ${VERSION_STRING_CPP} + loader_local_matching_test.cpp + LIBS + libcockatrice_network + libcockatrice_settings INCLUDE_DIRS ${CMAKE_SOURCE_DIR}/cockatrice/src ) @@ -506,19 +480,6 @@ endif() # Guard must match the condition for src/single_instance_manager.cpp in cockatrice/CMakeLists.txt. if(WITH_CLIENT) - cockatrice_add_test( - NAME - moxfield_signature_spells_test - SOURCES - ${VERSION_STRING_CPP} - ${CMAKE_SOURCE_DIR}/cockatrice/src/interface/deck_loader/card_node_function.cpp - moxfield_signature_spells_test.cpp - LIBS - libcockatrice_card - libcockatrice_deck_list - INCLUDE_DIRS - ${CMAKE_SOURCE_DIR}/cockatrice/src - ) cockatrice_add_test( NAME single_instance_manager_test @@ -566,7 +527,6 @@ add_subdirectory(card_zone_algorithms) add_subdirectory(carddatabase) add_subdirectory(deck_list_model) add_subdirectory(deck_list_zones) -add_subdirectory(icon_assets) add_subdirectory(loading_from_clipboard) add_subdirectory(model_contract) add_subdirectory(movecard_tests) @@ -588,46 +548,3 @@ endif() if(WITH_ORACLE OR WITH_CLIENT) add_subdirectory(picture_loader_benchmark) endif() - -# loader_local_matching_test and deck_state_manager_test links libcockatrice_settings, -# which only exists when a client/UI-capable target is being built. -if(WITH_ORACLE OR WITH_CLIENT) - set(SETTINGS_SOURCES - ${CMAKE_SOURCE_DIR}/cockatrice/src/client/network/update/client/release_channel.cpp - ${CMAKE_SOURCE_DIR}/cockatrice/src/client/settings/cache_settings.cpp - ${CMAKE_SOURCE_DIR}/cockatrice/src/client/settings/card_counter_settings.cpp - ${CMAKE_SOURCE_DIR}/cockatrice/src/client/settings/shortcuts_settings.cpp - ${VERSION_STRING_CPP} - ) - set(SETTINGS_LIBS libcockatrice_network libcockatrice_settings) - set(SETTINGS_INCLUDE_DIRS ${CMAKE_SOURCE_DIR}/cockatrice/src) - - cockatrice_add_test( - NAME - loader_local_matching_test - SOURCES - ${CMAKE_SOURCE_DIR}/cockatrice/src/interface/card_picture_loader/card_picture_loader_local.cpp - ${SETTINGS_SOURCES} - loader_local_matching_test.cpp - LIBS - ${SETTINGS_LIBS} - INCLUDE_DIRS - ${SETTINGS_INCLUDE_DIRS} - ) - cockatrice_add_test( - NAME - deck_state_manager_test - SOURCES - ${CMAKE_SOURCE_DIR}/cockatrice/src/interface/widgets/deck_editor/deck_state_manager.cpp - ${SETTINGS_SOURCES} - deck_state_manager_test.cpp - LIBS - libcockatrice_card - libcockatrice_deck_list - libcockatrice_models - libcockatrice_utility - ${SETTINGS_LIBS} - INCLUDE_DIRS - ${SETTINGS_INCLUDE_DIRS} - ) -endif() diff --git a/tests/card_art_crop_test.cpp b/tests/card_art_crop_test.cpp deleted file mode 100644 index 1c4a9ff8d..000000000 --- a/tests/card_art_crop_test.cpp +++ /dev/null @@ -1,92 +0,0 @@ -#include "interface/widgets/cards/card_art_utils.h" - -#include -#include -#include -#include -#include -#include -#include - -namespace -{ -ExactCard makeCard(bool landscape) -{ - CardInfo::UiAttributes attributes; - attributes.landscapeOrientation = landscape; - return ExactCard(CardInfo::newInstance("Artwork", "", false, {}, {}, {}, {}, attributes)); -} - -QPixmap landscapeCardWithFrame() -{ - // Normalized landscape frame: the title reaches 14% down the upright - // image, and the Battle type line starts at 56%. Artwork has two colors - // so the test also detects a wrong or missing rotation. - QImage upright(1400, 1000, QImage::Format_RGB32); - upright.fill(Qt::red); - QPainter painter(&upright); - painter.fillRect(QRect(70, 140, 1260, 420), Qt::green); - painter.fillRect(QRect(700, 140, 630, 420), Qt::blue); - painter.end(); - QTransform storedOrientation; - storedOrientation.rotate(-90); - return QPixmap::fromImage(upright.transformed(storedOrientation)); -} - -TEST(CardArtCrop, LandscapeCropExcludesNameAndRulesBoxesAndRotatesUpright) -{ - const QImage crop = CardArtUtils::cropCardArt(landscapeCardWithFrame(), makeCard(true)).toImage(); - ASSERT_GT(crop.width(), crop.height()); - ASSERT_FALSE(crop.isNull()); - for (int y = 0; y < crop.height(); ++y) { - for (int x = 0; x < crop.width(); ++x) { - EXPECT_EQ(x < crop.width() / 2 ? QColor(Qt::green) : QColor(Qt::blue), crop.pixelColor(x, y)); - } - } -} - -TEST(CardArtCrop, PortraitCropRetainsItsExistingArtworkBounds) -{ - QImage source(745, 1040, QImage::Format_RGB32); - for (int y = 0; y < source.height(); ++y) { - for (int x = 0; x < source.width(); ++x) { - source.setPixelColor(x, y, QColor(x % 256, y % 256, (x + y) % 256)); - } - } - const QPixmap input = QPixmap::fromImage(source); - const QImage crop = CardArtUtils::cropCardArt(input, makeCard(false)).toImage(); - EXPECT_EQ(source.copy(QRect(52, 114, 641, 458)), crop); -} - -TEST(CardArtCrop, DevicePixelRatioDoesNotChangeTheArtworkPixels) -{ - QPixmap source = landscapeCardWithFrame(); - const QPixmap normal = CardArtUtils::cropCardArt(source, makeCard(true)); - source.setDevicePixelRatio(2.0); - const QPixmap highDpi = CardArtUtils::cropCardArt(source, makeCard(true)); - EXPECT_EQ(normal.size(), highDpi.size()); - EXPECT_EQ(2.0, highDpi.devicePixelRatio()); - QImage normalPixels = normal.toImage(); - QImage highDpiPixels = highDpi.toImage(); - normalPixels.setDevicePixelRatio(1.0); - highDpiPixels.setDevicePixelRatio(1.0); - EXPECT_EQ(normalPixels, highDpiPixels); -} - -TEST(CardArtCrop, EmptyAndSmallImagesStayWithinTheirBounds) -{ - EXPECT_TRUE(CardArtUtils::cropCardArt({}, makeCard(true)).isNull()); - QPixmap small(1, 1); - small.fill(Qt::green); - EXPECT_EQ(small.toImage(), CardArtUtils::cropCardArt(small, makeCard(true)).toImage()); - EXPECT_EQ(small.toImage(), CardArtUtils::cropCardArt(small, makeCard(false)).toImage()); -} -} // namespace - -int main(int argc, char **argv) -{ - qputenv("QT_QPA_PLATFORM", "offscreen"); - QApplication app(argc, argv); - ::testing::InitGoogleTest(&argc, argv); - return RUN_ALL_TESTS(); -} diff --git a/tests/carddatabase/CMakeLists.txt b/tests/carddatabase/CMakeLists.txt index 012081fd2..7d83640a1 100644 --- a/tests/carddatabase/CMakeLists.txt +++ b/tests/carddatabase/CMakeLists.txt @@ -21,12 +21,10 @@ cockatrice_add_test( ) # ------------------------ -# Guard to match condition for libcockatrice_filters in the root CMakeLists.txt +# Filter String Test +# (guard must match the condition for libcockatrice_filters in the root CMakeLists.txt) # ------------------------ if(WITH_ORACLE OR WITH_CLIENT) - # ------------------------ - # Filter String Test - # ------------------------ cockatrice_add_test( NAME filter_string_test @@ -38,34 +36,6 @@ if(WITH_ORACLE OR WITH_CLIENT) libcockatrice_filters ) - # ------------------------ - # Filter Tree Test - # ------------------------ - cockatrice_add_test( - NAME - filter_tree_test - SOURCES - ${VERSION_STRING_CPP} - filter_tree_test.cpp - mocks.cpp - LIBS - libcockatrice_filters - ) - - # ------------------------ - # Filter Mode Test - # ------------------------ - cockatrice_add_test( - NAME - filter_mode_test - SOURCES - ${VERSION_STRING_CPP} - filter_mode_test.cpp - mocks.cpp - LIBS - libcockatrice_filters - ) - # ------------------------ # Visual Display Model Test # ------------------------ diff --git a/tests/carddatabase/carddatabase_test.cpp b/tests/carddatabase/carddatabase_test.cpp index 350d9be7e..22cbe9074 100644 --- a/tests/carddatabase/carddatabase_test.cpp +++ b/tests/carddatabase/carddatabase_test.cpp @@ -1,4 +1,3 @@ -#include "../support/scoped_file_size_limit.h" #include "libcockatrice/card/database/card_database.h" #include "libcockatrice/card/database/card_database_loader.h" #include "libcockatrice/card/database/card_database_querier.h" @@ -205,57 +204,6 @@ TEST(CardDatabaseTest, Xml4LocalizedDataRoundTrip) ASSERT_EQ(printings.first().getProperty("manacost"), "1R"); } -#ifdef Q_OS_UNIX -/** - * @brief A failed write keeps the previously installed card database intact. - * - * This is the writer Oracle uses to install a refreshed database, so the truncation - * bug has to be covered here and not only through OracleDatabase::saveXml. - */ -TEST(CardDatabaseTest, Xml4ShortWriteKeepsInstalledFile) -{ - NoopCardSetPriorityController controller; - CardSetPtr set = - CardSet::newInstance(&controller, "TST", "Test Set", "expansion", QDate(), CardSet::PriorityPrimary); - - QHash props; - props["manacost"] = "1R"; - PrintingInfo printing(set, LazyPropertiesHash(props)); - SetToPrintingsMap setsInfo; - setsInfo["TST"].append(printing); - - CardInfo::UiAttributes attributes = {.tableRow = 1}; - CardInfoPtr card = - CardInfo::newInstance("Lightning Bolt", QString(1024, 'x'), false, {}, {}, {}, setsInfo, attributes); - - SetNameMap sets; - sets.insert("TST", set); - CardNameMap cards; - cards.insert("Lightning Bolt", card); - - QTemporaryDir tempDir; - ASSERT_TRUE(tempDir.isValid()); - const QString fileName = tempDir.filePath("cards.xml"); - - QFile installed(fileName); - ASSERT_TRUE(installed.open(QIODevice::WriteOnly)); - ASSERT_EQ(installed.write("installed database"), 18); - installed.close(); - - // A real filesystem write failure: the soft file-size cap makes the writer's own - // flush fail without filling a disk. The cap is scoped rather than forked, because a - // death-test child under RLIMIT_FSIZE is killed by SIGSEGV on macOS. - test_support::ScopedFileSizeLimit fileSizeCap(128); - ASSERT_TRUE(fileSizeCap.isValid()); - - CockatriceXml4Parser writer(&controller); - EXPECT_FALSE(writer.saveToFile({}, sets, cards, fileName)); - - ASSERT_TRUE(installed.open(QIODevice::ReadOnly)); - EXPECT_EQ(installed.readAll(), "installed database"); -} -#endif - /** * @brief Path provider serving a sandboxed copy of the test card database. */ diff --git a/tests/carddatabase/filter_mode_test.cpp b/tests/carddatabase/filter_mode_test.cpp deleted file mode 100644 index 3fec7e25d..000000000 --- a/tests/carddatabase/filter_mode_test.cpp +++ /dev/null @@ -1,147 +0,0 @@ -#include "libcockatrice/filters/filter_mode.h" - -#include - -class FilterModeTest : public ::testing::Test -{ -protected: - ~FilterModeTest() override - { - for (CardFilter *filter : owned) { - delete filter; - } - } - - /** @brief Create a filter owned by the fixture and append it to the set under test. */ - const CardFilter * - add(const QString &term, CardFilter::Type type, CardFilter::Attr attr = CardFilter::Attr::AttrColor) - { - owned.append(new CardFilter(term, type, attr)); - filters.append(owned.last()); - return owned.last(); - } - - QList owned; - QList filters; -}; - -TEST(FilterModeInfoTest, EachModeDescribesItsOwnFilters) -{ - EXPECT_EQ(getFilterModeInfo(FilterMode::Includes).includeType, CardFilter::Type::TypeOr); - EXPECT_EQ(getFilterModeInfo(FilterMode::Includes).excludeType, CardFilter::Type::TypeEnd); - EXPECT_EQ(getFilterModeInfo(FilterMode::Includes).attr, CardFilter::Attr::AttrColor); - - EXPECT_EQ(getFilterModeInfo(FilterMode::Excludes).includeType, CardFilter::Type::TypeAndNot); - EXPECT_EQ(getFilterModeInfo(FilterMode::Excludes).excludeType, CardFilter::Type::TypeEnd); - - EXPECT_EQ(getFilterModeInfo(FilterMode::IncludeExclude).includeType, CardFilter::Type::TypeOr); - EXPECT_EQ(getFilterModeInfo(FilterMode::IncludeExclude).excludeType, CardFilter::Type::TypeAndNot); - - EXPECT_EQ(getFilterModeInfo(FilterMode::ExactMatch).includeType, CardFilter::Type::TypeAnd); - EXPECT_EQ(getFilterModeInfo(FilterMode::ExactMatch).excludeType, CardFilter::Type::TypeAndNot); - - // Color identity only excludes: the selected colors must be the identity, so nothing is included positively. - EXPECT_EQ(getFilterModeInfo(FilterMode::ColorIdentity).includeType, CardFilter::Type::TypeEnd); - EXPECT_EQ(getFilterModeInfo(FilterMode::ColorIdentity).excludeType, CardFilter::Type::TypeAndNot); - EXPECT_EQ(getFilterModeInfo(FilterMode::ColorIdentity).attr, CardFilter::Attr::AttrColorIdentity); - - EXPECT_EQ(getFilterModeInfo(FilterMode::Custom).attr, CardFilter::Attr::AttrEnd); -} - -TEST_F(FilterModeTest, EmptyFilterSetKeepsACompatibleMode) -{ - // Includes and Color identity both allow an empty set, so the live mode is not swapped for another. - EXPECT_EQ(deriveFilterMode(filters, FilterMode::Includes), FilterMode::Includes); - EXPECT_EQ(deriveFilterMode(filters, FilterMode::ColorIdentity), FilterMode::ColorIdentity); -} - -// Include / Exclude and Exact match always hold one filter per color, so an empty set cannot have come from -// them and falls back to the first mode that can describe it. -TEST_F(FilterModeTest, EmptyFilterSetFallsBackWhenTheModeNeedsOneFilterPerColor) -{ - EXPECT_EQ(deriveFilterMode(filters, FilterMode::IncludeExclude), FilterMode::Includes); - EXPECT_EQ(deriveFilterMode(filters, FilterMode::ExactMatch), FilterMode::Includes); -} - -TEST_F(FilterModeTest, IncludesFromOrFilters) -{ - add("W", CardFilter::Type::TypeOr); - - EXPECT_EQ(deriveFilterMode(filters, FilterMode::Includes), FilterMode::Includes); - // A single OR filter cannot come from Excludes, so the first mode that can produce it is selected. - EXPECT_EQ(deriveFilterMode(filters, FilterMode::Excludes), FilterMode::Includes); -} - -TEST_F(FilterModeTest, ExcludesFromAndNotFilters) -{ - add("W", CardFilter::Type::TypeAndNot); - - EXPECT_EQ(deriveFilterMode(filters, FilterMode::Excludes), FilterMode::Excludes); - // Includes needs an OR or an end filter, so an AND NOT filter selects Excludes. - EXPECT_EQ(deriveFilterMode(filters, FilterMode::Includes), FilterMode::Excludes); -} - -TEST_F(FilterModeTest, IncludeExcludeNeedsOneFilterPerColor) -{ - add("W", CardFilter::Type::TypeOr); - for (const QChar color : QString("UBRG")) { - add(QString(color), CardFilter::Type::TypeAndNot); - } - - EXPECT_EQ(deriveFilterMode(filters, FilterMode::IncludeExclude), FilterMode::IncludeExclude); -} - -TEST_F(FilterModeTest, IncludeExcludeRejectsAnIncompleteColorSet) -{ - add("W", CardFilter::Type::TypeOr); - add("U", CardFilter::Type::TypeAndNot); - - EXPECT_EQ(deriveFilterMode(filters, FilterMode::IncludeExclude), FilterMode::Custom); -} - -TEST_F(FilterModeTest, ExactMatchFromAndAndAndNotFilters) -{ - add("W", CardFilter::Type::TypeAnd); - for (const QChar color : QString("UBRG")) { - add(QString(color), CardFilter::Type::TypeAndNot); - } - - EXPECT_EQ(deriveFilterMode(filters, FilterMode::ExactMatch), FilterMode::ExactMatch); -} - -TEST_F(FilterModeTest, ColorIdentityFromIdentityAndNotFilters) -{ - for (const QChar color : QString("BRG")) { - add(QString(color), CardFilter::Type::TypeAndNot, CardFilter::Attr::AttrColorIdentity); - } - - EXPECT_EQ(deriveFilterMode(filters, FilterMode::ColorIdentity), FilterMode::ColorIdentity); -} - -TEST_F(FilterModeTest, MixedAttributesAreCustom) -{ - add("W", CardFilter::Type::TypeOr, CardFilter::Attr::AttrColor); - add("U", CardFilter::Type::TypeAndNot, CardFilter::Attr::AttrColorIdentity); - - EXPECT_EQ(deriveFilterMode(filters, FilterMode::Includes), FilterMode::Custom); -} - -TEST_F(FilterModeTest, CustomLeavesAsSoonAsAModeCanExplainTheFilters) -{ - add("W", CardFilter::Type::TypeOr); - - EXPECT_EQ(deriveFilterMode(filters, FilterMode::Custom), FilterMode::Includes); -} - -// Exact match with no color selected stores one AND NOT filter per color. That is a legitimate Exact match -// state, so the live mode is kept; a mode that cannot produce the set re-derives to Excludes, which produces -// the identical filters. -TEST_F(FilterModeTest, FullyExcludedColorSetIsExactMatchOrExcludes) -{ - for (const QChar color : QString("WUBRG")) { - add(QString(color), CardFilter::Type::TypeAndNot); - } - - EXPECT_EQ(deriveFilterMode(filters, FilterMode::ExactMatch), FilterMode::ExactMatch); - EXPECT_EQ(deriveFilterMode(filters, FilterMode::Includes), FilterMode::Excludes); -} diff --git a/tests/carddatabase/filter_string_test.cpp b/tests/carddatabase/filter_string_test.cpp index 931cb7e0c..e561db590 100644 --- a/tests/carddatabase/filter_string_test.cpp +++ b/tests/carddatabase/filter_string_test.cpp @@ -123,25 +123,6 @@ CardInfoPtr greenIdentityCard() return CardInfo::newInstance("Identity Cat", "", false, {{"colors", "G"}, {"coloridentity", "G"}}, {}, {}, {}, {}); } -// A card whose color identity differs from its colors, so that a query reading the wrong one is visible. -CardInfoPtr splitIdentityCard() -{ - return CardInfo::newInstance("Split Identity", "", false, {{"colors", "G"}, {"coloridentity", "W"}}, {}, {}, {}, - {}); -} - -// "ci!" (exactly these color identities) used to read the camelCase "colorIdentity" property instead of the -// lowercase "coloridentity" key, so it matched the empty string and rejected every card. -TEST_F(CardQuery, ColorIdentityExactReadsTheColorIdentityProperty) -{ - const CardData card = splitIdentityCard(); - ASSERT_TRUE(FilterString("ci!w").check(card)); - ASSERT_FALSE(FilterString("ci!g").check(card)); - // "c!" still reads colors, not identity. - ASSERT_TRUE(FilterString("c!g").check(card)); - ASSERT_FALSE(FilterString("c!w").check(card)); -} - // Whitespace belongs around the separator, never inside an operator token: "c i: g" must not // be read as the color-identity operator "ci" (it is a generic "c" AND a field query on "i"). TEST_F(CardQuery, ColorIdentityOperatorIsNotSplitByWhitespace) diff --git a/tests/carddatabase/filter_tree_test.cpp b/tests/carddatabase/filter_tree_test.cpp deleted file mode 100644 index 8d3706071..000000000 --- a/tests/carddatabase/filter_tree_test.cpp +++ /dev/null @@ -1,204 +0,0 @@ - -#include "libcockatrice/filters/filter_tree.h" - -#include -#include -#include - -namespace -{ -class FilterTreeTest : public ::testing::Test -{ -protected: - void SetUp() override - { - language = CardSearchLanguage{}; - - island = CardInfo::newInstance("Island", "", false, {{"colors", ""}, {"coloridentity", "U"}}, {}, {}, {}, {}); - artifact = - CardInfo::newInstance("Artifact", "", false, {{"colors", ""}, {"coloridentity", ""}}, {}, {}, {}, {}); - truth = CardInfo::newInstance("Truth", "", false, {{"colors", "U"}, {"coloridentity", "U"}}, {}, {}, {}, {}); - notDead = - CardInfo::newInstance("Not Dead", "", false, {{"colors", "B"}, {"coloridentity", "B"}}, {}, {}, {}, {}); - wallOfDenial = CardInfo::newInstance("Wall of Denial", "", false, {{"colors", "WU"}, {"coloridentity", "WU"}}, - {}, {}, {}, {}); - maelstromAngel = CardInfo::newInstance("Maelstrom Archangel", "", false, - {{"colors", "WUBRG"}, {"coloridentity", "WUBRG"}}, {}, {}, {}, {}); - } - - CardSearchLanguage language; - - CardData island; // c/u - CardData artifact; // c - CardData truth; // u - CardData notDead; // b - CardData wallOfDenial; // wu - CardData maelstromAngel; // wubrg -}; -} // namespace - -// ===================================================================================================================== -// acceptColor -// ===================================================================================================================== - -TEST_F(FilterTreeTest, Color) -{ - FilterTree filterTree; - filterTree.termNode(CardFilter::Attr::AttrColor, CardFilter::Type::TypeAnd, "wu"); - ASSERT_FALSE(filterTree.acceptsCard(island, language)); - ASSERT_FALSE(filterTree.acceptsCard(artifact, language)); - ASSERT_FALSE(filterTree.acceptsCard(truth, language)); - ASSERT_FALSE(filterTree.acceptsCard(notDead, language)); - ASSERT_TRUE(filterTree.acceptsCard(wallOfDenial, language)); - ASSERT_TRUE(filterTree.acceptsCard(maelstromAngel, language)); -} - -TEST_F(FilterTreeTest, ColorColorless) -{ - FilterTree filterTree; - filterTree.termNode(CardFilter::Attr::AttrColor, CardFilter::Type::TypeAnd, "c"); - ASSERT_TRUE(filterTree.acceptsCard(island, language)); - ASSERT_TRUE(filterTree.acceptsCard(artifact, language)); - ASSERT_FALSE(filterTree.acceptsCard(truth, language)); - ASSERT_FALSE(filterTree.acceptsCard(notDead, language)); - ASSERT_FALSE(filterTree.acceptsCard(wallOfDenial, language)); - ASSERT_FALSE(filterTree.acceptsCard(maelstromAngel, language)); -} - -TEST_F(FilterTreeTest, ColorNot) -{ - FilterTree filterTree; - filterTree.termNode(CardFilter::Attr::AttrColor, CardFilter::Type::TypeAndNot, "b"); - ASSERT_TRUE(filterTree.acceptsCard(island, language)); - ASSERT_TRUE(filterTree.acceptsCard(artifact, language)); - ASSERT_TRUE(filterTree.acceptsCard(truth, language)); - ASSERT_FALSE(filterTree.acceptsCard(notDead, language)); - ASSERT_TRUE(filterTree.acceptsCard(wallOfDenial, language)); - ASSERT_FALSE(filterTree.acceptsCard(maelstromAngel, language)); -} - -TEST_F(FilterTreeTest, ColorAliases) -{ - FilterTree filterTree; - - filterTree.termNode(CardFilter::Attr::AttrColor, CardFilter::Type::TypeAnd, "green"); - ASSERT_FALSE(filterTree.acceptsCard(truth, language)); - ASSERT_TRUE(filterTree.acceptsCard(maelstromAngel, language)); - filterTree.clear(); - - filterTree.termNode(CardFilter::Attr::AttrColor, CardFilter::Type::TypeAnd, "grn"); - ASSERT_FALSE(filterTree.acceptsCard(truth, language)); - ASSERT_TRUE(filterTree.acceptsCard(maelstromAngel, language)); - filterTree.clear(); - - filterTree.termNode(CardFilter::Attr::AttrColor, CardFilter::Type::TypeAnd, "blue"); - ASSERT_TRUE(filterTree.acceptsCard(truth, language)); - ASSERT_FALSE(filterTree.acceptsCard(notDead, language)); - filterTree.clear(); - - filterTree.termNode(CardFilter::Attr::AttrColor, CardFilter::Type::TypeAnd, "blu"); - ASSERT_TRUE(filterTree.acceptsCard(truth, language)); - ASSERT_FALSE(filterTree.acceptsCard(notDead, language)); - filterTree.clear(); - - filterTree.termNode(CardFilter::Attr::AttrColor, CardFilter::Type::TypeAnd, "black"); - ASSERT_FALSE(filterTree.acceptsCard(truth, language)); - ASSERT_TRUE(filterTree.acceptsCard(notDead, language)); - filterTree.clear(); - - filterTree.termNode(CardFilter::Attr::AttrColor, CardFilter::Type::TypeAnd, "blk"); - ASSERT_FALSE(filterTree.acceptsCard(truth, language)); - ASSERT_TRUE(filterTree.acceptsCard(notDead, language)); - filterTree.clear(); - - filterTree.termNode(CardFilter::Attr::AttrColor, CardFilter::Type::TypeAnd, "red"); - ASSERT_FALSE(filterTree.acceptsCard(truth, language)); - ASSERT_TRUE(filterTree.acceptsCard(maelstromAngel, language)); - filterTree.clear(); - - filterTree.termNode(CardFilter::Attr::AttrColor, CardFilter::Type::TypeAnd, "white"); - ASSERT_FALSE(filterTree.acceptsCard(truth, language)); - ASSERT_TRUE(filterTree.acceptsCard(wallOfDenial, language)); - filterTree.clear(); - - filterTree.termNode(CardFilter::Attr::AttrColor, CardFilter::Type::TypeAnd, "wht"); - ASSERT_FALSE(filterTree.acceptsCard(truth, language)); - ASSERT_TRUE(filterTree.acceptsCard(wallOfDenial, language)); - filterTree.clear(); - - filterTree.termNode(CardFilter::Attr::AttrColor, CardFilter::Type::TypeAnd, "colorless"); - ASSERT_FALSE(filterTree.acceptsCard(truth, language)); - ASSERT_TRUE(filterTree.acceptsCard(artifact, language)); - filterTree.clear(); - - filterTree.termNode(CardFilter::Attr::AttrColor, CardFilter::Type::TypeAnd, "colourless"); - ASSERT_FALSE(filterTree.acceptsCard(truth, language)); - ASSERT_TRUE(filterTree.acceptsCard(artifact, language)); - filterTree.clear(); - - filterTree.termNode(CardFilter::Attr::AttrColor, CardFilter::Type::TypeAnd, "none"); - ASSERT_FALSE(filterTree.acceptsCard(truth, language)); - ASSERT_TRUE(filterTree.acceptsCard(artifact, language)); - filterTree.clear(); -} - -// ===================================================================================================================== -// acceptColorIdentity -// ===================================================================================================================== - -TEST_F(FilterTreeTest, ColorIdentity) -{ - FilterTree filterTree; - filterTree.termNode(CardFilter::Attr::AttrColorIdentity, CardFilter::Type::TypeAnd, "wu"); - ASSERT_FALSE(filterTree.acceptsCard(island, language)); - ASSERT_FALSE(filterTree.acceptsCard(artifact, language)); - ASSERT_FALSE(filterTree.acceptsCard(truth, language)); - ASSERT_FALSE(filterTree.acceptsCard(notDead, language)); - ASSERT_TRUE(filterTree.acceptsCard(wallOfDenial, language)); - ASSERT_TRUE(filterTree.acceptsCard(maelstromAngel, language)); -} - -TEST_F(FilterTreeTest, ColorIdentityColorless) -{ - FilterTree filterTree; - filterTree.termNode(CardFilter::Attr::AttrColorIdentity, CardFilter::Type::TypeAnd, "c"); - ASSERT_FALSE(filterTree.acceptsCard(island, language)); - ASSERT_TRUE(filterTree.acceptsCard(artifact, language)); - ASSERT_FALSE(filterTree.acceptsCard(truth, language)); - ASSERT_FALSE(filterTree.acceptsCard(notDead, language)); - ASSERT_FALSE(filterTree.acceptsCard(wallOfDenial, language)); - ASSERT_FALSE(filterTree.acceptsCard(maelstromAngel, language)); -} - -TEST_F(FilterTreeTest, ColorIdentityNot) -{ - FilterTree filterTree; - filterTree.termNode(CardFilter::Attr::AttrColorIdentity, CardFilter::Type::TypeAndNot, "b"); - ASSERT_TRUE(filterTree.acceptsCard(island, language)); - ASSERT_TRUE(filterTree.acceptsCard(artifact, language)); - ASSERT_TRUE(filterTree.acceptsCard(truth, language)); - ASSERT_FALSE(filterTree.acceptsCard(notDead, language)); - ASSERT_TRUE(filterTree.acceptsCard(wallOfDenial, language)); - ASSERT_FALSE(filterTree.acceptsCard(maelstromAngel, language)); -} - -TEST_F(FilterTreeTest, ColorIdentityMultiple) -{ - // Simulate a WU commander. - FilterTree filterTree; - filterTree.termNode(CardFilter::Attr::AttrColorIdentity, CardFilter::Type::TypeAndNot, "b"); - filterTree.termNode(CardFilter::Attr::AttrColorIdentity, CardFilter::Type::TypeAndNot, "r"); - filterTree.termNode(CardFilter::Attr::AttrColorIdentity, CardFilter::Type::TypeAndNot, "g"); - ASSERT_TRUE(filterTree.acceptsCard(island, language)); - ASSERT_TRUE(filterTree.acceptsCard(artifact, language)); - ASSERT_TRUE(filterTree.acceptsCard(truth, language)); - ASSERT_FALSE(filterTree.acceptsCard(notDead, language)); - ASSERT_TRUE(filterTree.acceptsCard(wallOfDenial, language)); - ASSERT_FALSE(filterTree.acceptsCard(maelstromAngel, language)); -} - -int main(int argc, char **argv) -{ - ::testing::InitGoogleTest(&argc, argv); - return RUN_ALL_TESTS(); -} diff --git a/tests/deck_list_model/deck_list_model_custom_zones_test.cpp b/tests/deck_list_model/deck_list_model_custom_zones_test.cpp index d4e44125c..95df74702 100644 --- a/tests/deck_list_model/deck_list_model_custom_zones_test.cpp +++ b/tests/deck_list_model/deck_list_model_custom_zones_test.cpp @@ -278,37 +278,6 @@ TEST(DeckListModelCustomZones, SortPlainNodeDoesNotReorderCustomZones) EXPECT_EQ(mapping[1].second, 1); } -TEST(DeckListModelCustomZones, ManaGroupsUseNumericOrderWithoutChangingOtherNodes) -{ - InnerDecklistNode root; - auto *board = new InnerDecklistNode(DECK_ZONE_MAIN, &root); - const QStringList original = {"10", "2", "0.5", "1000000", "1", "unknown", "nan"}; - for (const auto &name : original) { - new InnerDecklistNode(name, board); - } - auto *custom = new DecklistModelSubZoneNode("1x", board); - new InnerDecklistNode("10", custom); - new InnerDecklistNode("2", custom); - root.setSortMethod(DeckSortMethod::ByName); - - const auto mapping = DeckListModelCustomZones::sortWithCustomZonesLast(&root, board, Qt::AscendingOrder, true); - EXPECT_EQ(childNames(board), (QStringList{"0.5", "1", "2", "10", "1000000", "nan", "unknown", "1x"})); - QStringList before = original; - before.append("1x"); - for (const auto &move : mapping) { - EXPECT_EQ(board->at(move.second)->getName(), before.at(move.first)); - } - - (void)DeckListModelCustomZones::sortWithCustomZonesLast(&root, board, Qt::DescendingOrder, true); - EXPECT_EQ(childNames(board), (QStringList{"1000000", "10", "2", "1", "0.5", "unknown", "nan", "1x"})); - - // The numeric flag only affects groups directly under a board. - (void)DeckListModelCustomZones::sortWithCustomZonesLast(&root, custom, Qt::AscendingOrder, true); - EXPECT_EQ(childNames(custom), (QStringList{"10", "2"})); - (void)DeckListModelCustomZones::sortWithCustomZonesLast(&root, board, Qt::AscendingOrder); - EXPECT_EQ(childNames(board), (QStringList{"0.5", "1", "10", "1000000", "2", "nan", "unknown", "1x"})); -} - int main(int argc, char **argv) { ::testing::InitGoogleTest(&argc, argv); diff --git a/tests/deck_list_zones/deck_list_zones_test.cpp b/tests/deck_list_zones/deck_list_zones_test.cpp index a67e689cf..1903b70df 100644 --- a/tests/deck_list_zones/deck_list_zones_test.cpp +++ b/tests/deck_list_zones/deck_list_zones_test.cpp @@ -393,28 +393,6 @@ TEST(DeckListZones, MaybeboardCustomZoneCardsAreExcludedFromHash) EXPECT_EQ(deck.getDeckHash(), expected.getDeckHash()); } -TEST(DeckListZones, PlainExportPutsMainFirstWithoutReorderingSavedZones) -{ - DeckList deck; - auto *tree = deck.getTree(); - ASSERT_NE(tree->addCustomZone(DECK_ZONE_SIDE, "Side Tech"), nullptr); - tree->addCard("Side Card", 2, "Side Tech", -1); - tree->addCard("Token Card", 1, DECK_ZONE_TOKENS, -1); - ASSERT_NE(tree->addCustomZone(DECK_ZONE_MAIN, "Removal"), nullptr); - tree->addCard("Main Card", 3, "Removal", -1); - ASSERT_NE(tree->addCustomZone(DECK_ZONE_MAYBEBOARD, "Candidates"), nullptr); - tree->addCard("Maybe Card", 4, "Candidates", -1); - - const QString native = deck.writeToString_Native(); - EXPECT_EQ(deck.writeToString_Plain(true, false), QString("3 Main Card\nSB: 2 Side Card\n1 Token Card\n")); - EXPECT_EQ(deck.writeToString_Plain(false, false), QString("3 Main Card\n2 Side Card\n1 Token Card\n")); - EXPECT_EQ(deck.writeToString_Native(), native); - - DeckList reopened(native); - EXPECT_EQ(reopened.getZoneNodes().first()->getName(), DECK_ZONE_SIDE); - EXPECT_EQ(reopened.writeToString_Plain(true, false), deck.writeToString_Plain(true, false)); -} - int main(int argc, char **argv) { ::testing::InitGoogleTest(&argc, argv); diff --git a/tests/deck_state_manager_test.cpp b/tests/deck_state_manager_test.cpp deleted file mode 100644 index ddd17912c..000000000 --- a/tests/deck_state_manager_test.cpp +++ /dev/null @@ -1,190 +0,0 @@ -/** @file deck_state_manager_test.cpp - * @brief Tests for DeckStateManager operations. - * @ingroup Tests - */ - -#include "libcockatrice/card/database/card_database.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace -{ - -/** - * @brief Assert that the DeckStateManager's deck list contains only the given zone, - * and that that zone only contains the given number of cards, all with the given card name. - * - * @param manager the DeckStateManager - * @param restrictToZones In which zones to look for the zone and cards. Empty means use the entire deck - * @param zone The only zone that should exist in the deck - * @param cardName The name of the only card that should exist in the deck - * @param cardNumber The number of cards that should be in the deck - */ -static void containsExactly(const DeckStateManager &manager, - const QSet &restrictToZones, - QString zone, - QString cardName, - int cardNumber) -{ - QList zones = manager.getDeckList().getZoneNodes(restrictToZones); - ASSERT_EQ(zones.size(), 1); - ASSERT_EQ(zones[0]->getName(), zone); - ASSERT_EQ(zones[0]->size(), 1); - const DecklistCardNode *card = dynamic_cast((*zones[0])[0]); - ASSERT_NE(card, nullptr); - ASSERT_EQ(card->getName(), cardName); - ASSERT_EQ(card->getNumber(), cardNumber); -} - -/** @brief Return CardInfo of a token with the given name. */ -static CardInfoPtr makeToken(QString name) -{ - return CardInfo::newInstance(name, "", true, {}, {}, {}, {}, {}); -} - -// ===================================================================================================================== -// addCard -// ===================================================================================================================== - -TEST(DeckStateManager, AddCards) -{ - DeckStateManager manager; - manager.addCard(ExactCard(CardInfo::newInstance("Lightning Bolt")), DECK_ZONE_MAIN); - containsExactly(manager, {}, DECK_ZONE_MAIN, "Lightning Bolt", 1); - manager.addCard(ExactCard(CardInfo::newInstance("Lightning Bolt")), DECK_ZONE_MAIN); - containsExactly(manager, {}, DECK_ZONE_MAIN, "Lightning Bolt", 2); -} - -TEST(DeckStateManager, AddCardZones) -{ - DeckStateManager manager; - manager.addCard(ExactCard(CardInfo::newInstance("Grizzly Bears")), DECK_ZONE_MAIN); - manager.addCard(ExactCard(CardInfo::newInstance("Lightning Bolt")), DECK_ZONE_SIDE); - manager.addCard(ExactCard(CardInfo::newInstance("Swords to Plowshares")), DECK_ZONE_MAYBEBOARD); - - // Check if the cards got put into the correct zones. - ASSERT_EQ(manager.getDeckList().getZoneNodes().size(), 3); - containsExactly(manager, {DECK_ZONE_MAIN}, DECK_ZONE_MAIN, "Grizzly Bears", 1); - containsExactly(manager, {DECK_ZONE_SIDE}, DECK_ZONE_SIDE, "Lightning Bolt", 1); - containsExactly(manager, {DECK_ZONE_MAYBEBOARD}, DECK_ZONE_MAYBEBOARD, "Swords to Plowshares", 1); -} - -TEST(DeckStateManager, AddTokens) -{ - DeckStateManager manager; - manager.addCard(ExactCard(makeToken("Beast")), DECK_ZONE_MAIN); - manager.addCard(ExactCard(makeToken("Beast")), DECK_ZONE_SIDE); - manager.addCard(ExactCard(makeToken("Beast")), DECK_ZONE_MAYBEBOARD); - manager.addCard(ExactCard(makeToken("Beast")), DECK_ZONE_TOKENS); - - // A token should always be put into the tokens zone. - containsExactly(manager, {}, DECK_ZONE_TOKENS, "Beast", 4); -} - -// ===================================================================================================================== -// decrementCard -// ===================================================================================================================== - -TEST(DeckStateManager, DecrementCards) -{ - DeckStateManager manager; - manager.addCard(ExactCard(CardInfo::newInstance("Lightning Bolt")), DECK_ZONE_MAIN); - manager.addCard(ExactCard(CardInfo::newInstance("Lightning Bolt")), DECK_ZONE_MAIN); - containsExactly(manager, {}, DECK_ZONE_MAIN, "Lightning Bolt", 2); - - // Removing from the sideboard should do nothing, as there is nothing in the side board. - manager.decrementCard(ExactCard(CardInfo::newInstance("Lightning Bolt")), DECK_ZONE_SIDE); - containsExactly(manager, {}, DECK_ZONE_MAIN, "Lightning Bolt", 2); - - // Remove the first card. - manager.decrementCard(ExactCard(CardInfo::newInstance("Lightning Bolt")), DECK_ZONE_MAIN); - containsExactly(manager, {}, DECK_ZONE_MAIN, "Lightning Bolt", 1); - - // Removing the second card should also remove the zone. - manager.decrementCard(ExactCard(CardInfo::newInstance("Lightning Bolt")), DECK_ZONE_MAIN); - ASSERT_EQ(manager.getDeckList().getZoneNodes().size(), 0); -} - -TEST(DeckStateManager, DecrementTokens) -{ - DeckStateManager manager; - manager.addCard(ExactCard(makeToken("Beast")), DECK_ZONE_MAIN); - manager.addCard(ExactCard(makeToken("Beast")), DECK_ZONE_SIDE); - manager.addCard(ExactCard(makeToken("Beast")), DECK_ZONE_TOKENS); - containsExactly(manager, {}, DECK_ZONE_TOKENS, "Beast", 3); - - manager.decrementCard(ExactCard(makeToken("Beast")), DECK_ZONE_TOKENS); - containsExactly(manager, {}, DECK_ZONE_TOKENS, "Beast", 2); - - // Removing tokens from a given zone should remove them from the - // token zone if the given zone does not contain the token. - manager.decrementCard(ExactCard(makeToken("Beast")), DECK_ZONE_SIDE); - containsExactly(manager, {}, DECK_ZONE_TOKENS, "Beast", 1); - - manager.decrementCard(ExactCard(makeToken("Beast")), DECK_ZONE_MAIN); - ASSERT_EQ(manager.getDeckList().getZoneNodes().size(), 0); -} - -TEST(DeckStateManager, DecrementTokenMainboard) -{ - DeckStateManager manager; - - // Add cards to the deck list directly so tokens are not - // automatically added to the token zone. - manager.getDeckListShared()->addCard("Beast", DECK_ZONE_MAIN); - manager.getDeckListShared()->addCard("Beast", DECK_ZONE_TOKENS); - ASSERT_EQ(manager.getDeckList().getZoneNodes().size(), 2); - containsExactly(manager, {DECK_ZONE_MAIN}, DECK_ZONE_MAIN, "Beast", 1); - containsExactly(manager, {DECK_ZONE_TOKENS}, DECK_ZONE_TOKENS, "Beast", 1); - - // decrementCard uses the model to find the card, so we need to rebuild it. - manager.getModel()->rebuildTree(); - - // Tokens should be removed from the main zone first. - manager.decrementCard(ExactCard(makeToken("Beast")), DECK_ZONE_MAIN); - ASSERT_EQ(manager.getDeckList().getZoneNodes({DECK_ZONE_MAIN}).size(), 0); - containsExactly(manager, {DECK_ZONE_TOKENS}, DECK_ZONE_TOKENS, "Beast", 1); - - // If the main zone is empty they can be removed from the token zone. - manager.decrementCard(ExactCard(makeToken("Beast")), DECK_ZONE_MAIN); - ASSERT_EQ(manager.getDeckList().getZoneNodes().size(), 0); -} -} // namespace - -int main(int argc, char **argv) -{ - // Redirect SettingsCache reads/writes (app-data location) away from the real user profile. - QStandardPaths::setTestModeEnabled(true); - QCoreApplication app(argc, argv); - QCoreApplication::setOrganizationName(QStringLiteral("CockatriceTest")); - QCoreApplication::setApplicationName( - QStringLiteral("deck-state-manager-test-%1").arg(QCoreApplication::applicationPid())); - - ::testing::InitGoogleTest(&argc, argv); - - // Add cards to the card database - auto *db = CardDatabaseManager::getInstance(); - db->addCard(CardInfo::newInstance("Lightning Bolt")); - db->addCard(CardInfo::newInstance("Grizzly Bears")); - db->addCard(CardInfo::newInstance("Swords to Plowshares")); - db->addCard(makeToken("Beast")); - - int result = RUN_ALL_TESTS(); - - // Clean up the settings directory in ~/.qttest if the tests ran. - if (!::testing::GTEST_FLAG(list_tests)) { - QDir(SettingsCache::instance().getDataPath()).removeRecursively(); - QDir(SettingsCache::instance().getCachePath()).removeRecursively(); - } - - return result; -} diff --git a/tests/font_config_test.cpp b/tests/font_config_test.cpp deleted file mode 100644 index c8b1fb10d..000000000 --- a/tests/font_config_test.cpp +++ /dev/null @@ -1,195 +0,0 @@ -#include "interface/theme_config.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace -{ -// Writes content to a file inside tmp, creating parents as needed. -QString writeTempFile(QTemporaryDir &tmp, const QString &relativePath, const QString &content) -{ - const QString filePath = QDir(tmp.path()).absoluteFilePath(relativePath); - EXPECT_TRUE(QDir().mkpath(QFileInfo(filePath).absolutePath())); - QFile file(filePath); - EXPECT_TRUE(file.open(QIODevice::WriteOnly | QIODevice::Text)); - QTextStream out(&file); - out << content; - file.close(); - return filePath; -} - -QFont makeFont(const QString &family, int pointSize) -{ - QFont font(family); - font.setPointSize(pointSize); - return font; -} -} // namespace - -TEST(FontConfigTest, FileNameIsFontsToml) -{ - EXPECT_EQ(FontConfig::fileName(), QStringLiteral("fonts.toml")); -} - -TEST(FontConfigTest, EmptyByDefault) -{ - FontConfig cfg; - EXPECT_FALSE(cfg.hasFonts()); - EXPECT_TRUE(cfg.toToml().isEmpty()); - EXPECT_EQ(cfg, FontConfig()); - EXPECT_EQ(cfg, FontConfig::fromFile("/nonexistent/path/fonts.toml")); - EXPECT_EQ(cfg, FontConfig::fromThemeDir(QString())); -} - -TEST(FontConfigTest, RoundTripPreservesFamilyAndSize) -{ - QTemporaryDir tmp; - ASSERT_TRUE(tmp.isValid()); - - FontConfig cfg; - cfg.fonts[AppFont::InterfaceDefault] = makeFont("Inter", 10); - cfg.fonts[AppFont::ChatLog] = makeFont("JetBrains Mono", 9); - cfg.fonts[AppFont::CardText] = makeFont("Noto Serif", 11); - cfg.fonts[AppFont::DeckList] = makeFont("Roboto", 12); - ASSERT_TRUE(cfg.hasFonts()); - - const QString filePath = writeTempFile(tmp, "fonts.toml", cfg.toToml()); - const FontConfig loaded = FontConfig::fromFile(filePath); - - EXPECT_EQ(loaded, cfg); - EXPECT_EQ(loaded.fonts[AppFont::InterfaceDefault].family(), QStringLiteral("Inter")); - EXPECT_EQ(loaded.fonts[AppFont::InterfaceDefault].pointSize(), 10); - EXPECT_EQ(loaded.fonts[AppFont::ChatLog].family(), QStringLiteral("JetBrains Mono")); - EXPECT_EQ(loaded.fonts[AppFont::ChatLog].pointSize(), 9); - EXPECT_EQ(loaded.fonts[AppFont::CardText].family(), QStringLiteral("Noto Serif")); - EXPECT_EQ(loaded.fonts[AppFont::CardText].pointSize(), 11); - EXPECT_EQ(loaded.fonts[AppFont::DeckList].family(), QStringLiteral("Roboto")); - EXPECT_EQ(loaded.fonts[AppFont::DeckList].pointSize(), 12); -} - -TEST(FontConfigTest, RoundTripStripsWeightAndItalic) -{ - QTemporaryDir tmp; - ASSERT_TRUE(tmp.isValid()); - - QFont styled("Inter"); - styled.setPointSize(10); - styled.setBold(true); - styled.setItalic(true); - - FontConfig cfg; - cfg.fonts[AppFont::ChatLog] = styled; - - const QString filePath = writeTempFile(tmp, "fonts.toml", cfg.toToml()); - const FontConfig loaded = FontConfig::fromFile(filePath); - - ASSERT_TRUE(loaded.fonts.contains(AppFont::ChatLog)); - EXPECT_EQ(loaded.fonts[AppFont::ChatLog].family(), QStringLiteral("Inter")); - EXPECT_EQ(loaded.fonts[AppFont::ChatLog].pointSize(), 10); - EXPECT_FALSE(loaded.fonts[AppFont::ChatLog].bold()); - EXPECT_FALSE(loaded.fonts[AppFont::ChatLog].italic()); - EXPECT_EQ(loaded.fonts[AppFont::ChatLog], makeFont("Inter", 10)); -} - -TEST(FontConfigTest, ToTomlFormat) -{ - FontConfig cfg; - cfg.fonts[AppFont::InterfaceDefault] = makeFont("Inter", 10); - - const QString toml = cfg.toToml(); - EXPECT_TRUE(toml.contains(QStringLiteral("[Fonts]"))); - EXPECT_TRUE(toml.contains(QStringLiteral("InterfaceDefault"))); - EXPECT_TRUE(toml.contains(QStringLiteral("\"Inter, 10\""))); -} - -TEST(FontConfigTest, MalformedLinesAreSkipped) -{ - QTemporaryDir tmp; - ASSERT_TRUE(tmp.isValid()); - - const QString filePath = writeTempFile(tmp, "fonts.toml", - "# a comment\n" - "; another comment\n" - "\n" - "StrayBeforeSection = \"Inter, 10\"\n" - "\n" - "[Other]\n" - "ChatLog = \"ShouldBeIgnored, 10\"\n" - "\n" - "[Fonts]\n" - "InterfaceDefault = \"Inter, 10\"\n" - "ChatLog = \"JetBrains Mono\", 9 # inline comment\n" - "CardText = Noto Serif, 11\n" - "AppFont::DeckList = \"Roboto, 12\"\n" - "UnknownRole = \"Inter, 10\"\n" - "QPalette::Window = \"Inter, 10\"\n" - "NoComma = \"Inter\"\n" - "EmptyFamily = \", 10\"\n" - "ZeroSize = \"Inter, 0\"\n" - "NegativeSize = \"Inter, -3\"\n" - "NotANumber = \"Inter, big\"\n" - "NotAnEntry\n"); - - const FontConfig loaded = FontConfig::fromFile(filePath); - - ASSERT_TRUE(loaded.hasFonts()); - EXPECT_EQ(loaded.fonts.size(), 4); - EXPECT_EQ(loaded.fonts[AppFont::InterfaceDefault], makeFont("Inter", 10)); - EXPECT_EQ(loaded.fonts[AppFont::ChatLog], makeFont("JetBrains Mono", 9)); - EXPECT_EQ(loaded.fonts[AppFont::CardText], makeFont("Noto Serif", 11)); - EXPECT_EQ(loaded.fonts[AppFont::DeckList], makeFont("Roboto", 12)); -} - -TEST(FontConfigTest, FromThemeDirReadsFontsToml) -{ - QTemporaryDir tmp; - ASSERT_TRUE(tmp.isValid()); - - const QString themeDir = QDir(tmp.path()).absoluteFilePath("MyTheme"); - ASSERT_TRUE(QDir().mkpath(themeDir)); - - FontConfig cfg; - cfg.fonts[AppFont::CardText] = makeFont("Noto Serif", 11); - writeTempFile(tmp, "MyTheme/fonts.toml", cfg.toToml()); - - const FontConfig loaded = FontConfig::fromThemeDir(themeDir); - EXPECT_EQ(loaded, cfg); - - // A theme directory without fonts.toml loads as empty. - const QString bareDir = QDir(tmp.path()).absoluteFilePath("BareTheme"); - ASSERT_TRUE(QDir().mkpath(bareDir)); - EXPECT_FALSE(FontConfig::fromThemeDir(bareDir).hasFonts()); -} - -TEST(FontConfigTest, InequalityComparesRoles) -{ - FontConfig left; - left.fonts[AppFont::InterfaceDefault] = makeFont("Inter", 10); - - FontConfig right = left; - EXPECT_EQ(left, right); - EXPECT_FALSE(left != right); - - right.fonts[AppFont::InterfaceDefault] = makeFont("Inter", 11); - EXPECT_NE(left, right); - - right = left; - right.fonts[AppFont::ChatLog] = makeFont("Inter", 10); - EXPECT_NE(left, right); -} - -// Every test brings its own main(): GTest is not guaranteed to be found on the build machines, and -// the fallback then links the bare gtest library without gtest_main. -int main(int argc, char **argv) -{ - ::testing::InitGoogleTest(&argc, argv); - return RUN_ALL_TESTS(); -} diff --git a/tests/icon_assets/CMakeLists.txt b/tests/icon_assets/CMakeLists.txt deleted file mode 100644 index da9776f8e..000000000 --- a/tests/icon_assets/CMakeLists.txt +++ /dev/null @@ -1,3 +0,0 @@ -cockatrice_add_test( - NAME icon_assets_test SOURCES icon_assets_test.cpp DEFINITIONS COCKATRICE_SOURCE_DIR="${CMAKE_SOURCE_DIR}" -) diff --git a/tests/icon_assets/icon_assets_test.cpp b/tests/icon_assets/icon_assets_test.cpp deleted file mode 100644 index f0b06d5cc..000000000 --- a/tests/icon_assets/icon_assets_test.cpp +++ /dev/null @@ -1,241 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace -{ -const QString ICONS_RELATIVE_PATH = QStringLiteral("cockatrice/resources/icons"); - -QString iconsDir() -{ - return QDir(QStringLiteral(COCKATRICE_SOURCE_DIR)).absoluteFilePath(ICONS_RELATIVE_PATH); -} - -/** @brief True for content art that is allowed to carry full color. */ -bool isContentArt(const QString &relativePath) -{ - return relativePath.startsWith(QStringLiteral("mana/")); -} - -QString violation(const QString &rule, const QString &relativePath) -{ - return rule + QLatin1Char(' ') + relativePath; -} - -/** - * @brief Reads one SVG and returns true when the root carries a usable viewBox. - * - * Also reports malformed XML and editor-only metadata through the out params. - */ -bool inspectSvg(const QString &absolutePath, - bool &validXml, - bool &hasViewBox, - bool &hasEditorMetadata, - bool &hasPhysicalSize) -{ - validXml = false; - hasViewBox = false; - hasEditorMetadata = false; - hasPhysicalSize = false; - - QFile file(absolutePath); - if (!file.open(QIODevice::ReadOnly)) { - return false; - } - - QXmlStreamReader reader(&file); - bool sawRoot = false; - - while (!reader.atEnd()) { - reader.readNext(); - - if (reader.hasError()) { - return false; - } - - if (!reader.isStartElement()) { - continue; - } - - if (!sawRoot) { - sawRoot = true; - if (reader.name() != QStringLiteral("svg")) { - return false; - } - const QString viewBox = reader.attributes().value(QStringLiteral("viewBox")).toString().simplified(); - hasViewBox = viewBox == QStringLiteral("0 0 24 24"); - hasPhysicalSize = !reader.attributes().value(QStringLiteral("width")).isEmpty() || - !reader.attributes().value(QStringLiteral("height")).isEmpty(); - } - - if (reader.name() == QStringLiteral("metadata") || reader.name() == QStringLiteral("namedview")) { - hasEditorMetadata = true; - } - - for (const QXmlStreamAttribute &attribute : reader.attributes()) { - const QString prefix = attribute.prefix().toString(); - if (prefix == QStringLiteral("inkscape") || prefix == QStringLiteral("sodipodi") || - prefix == QStringLiteral("dc") || prefix == QStringLiteral("cc") || prefix == QStringLiteral("rdf")) { - hasEditorMetadata = true; - } - } - } - - validXml = sawRoot && !reader.hasError(); - return validXml; -} - -bool isAllowedPaintValue(const QString &value) -{ - const QString normalized = value.trimmed().toLower(); - return normalized.isEmpty() || normalized == QStringLiteral("none") || - normalized == QStringLiteral("currentcolor") || normalized == QStringLiteral("inherit") || - normalized == QStringLiteral("context-fill") || normalized == QStringLiteral("context-stroke") || - normalized.startsWith(QStringLiteral("url(")); -} - -QRegularExpression paintValuePattern() -{ - static const QRegularExpression pattern( - QStringLiteral(R"re((?:fill|stroke|stop-color|color)\s*[:=]\s*["']?\s*([^"'\s;>]+))re"), - QRegularExpression::CaseInsensitiveOption); - return pattern; -} - -bool hasHardcodedColor(const QString &absolutePath) -{ - QFile file(absolutePath); - if (!file.open(QIODevice::ReadOnly)) { - return false; - } - - const QString content = QString::fromUtf8(file.readAll()); - auto matches = paintValuePattern().globalMatch(content); - while (matches.hasNext()) { - const QString value = matches.next().captured(1).trimmed(); - if (isAllowedPaintValue(value)) { - continue; - } - const QString normalized = value.toLower(); - if (normalized.startsWith(QLatin1Char('#')) || normalized.contains(QStringLiteral("rgb(")) || - normalized.contains(QStringLiteral("hsl(")) || QColor(value).isValid()) { - return true; - } - } - return false; -} - -bool hasCurrentColorToken(const QString &absolutePath) -{ - QFile file(absolutePath); - if (!file.open(QIODevice::ReadOnly)) { - return false; - } - - const QString content = QString::fromUtf8(file.readAll()); - auto matches = paintValuePattern().globalMatch(content); - while (matches.hasNext()) { - if (matches.next().captured(1).trimmed().compare(QStringLiteral("currentColor"), Qt::CaseInsensitive) == 0) { - return true; - } - } - return false; -} - -bool isSchemeVariant(const QString &fileName) -{ - return fileName.contains(QStringLiteral("-dark."), Qt::CaseInsensitive) || - fileName.contains(QStringLiteral("-light."), Qt::CaseInsensitive); -} - -bool hasColorInFilename(const QString &fileName) -{ - static const QRegularExpression colorSuffix( - QStringLiteral(R"(_(green|blue|red|yellow|black|white|gray|grey)\.svg$)"), - QRegularExpression::CaseInsensitiveOption); - return colorSuffix.match(fileName).hasMatch(); -} - -/** @brief Collects every current violation as "rule relative/path.svg". */ -QSet collectViolations() -{ - QSet violations; - - QDirIterator it(iconsDir(), QDir::Files, QDirIterator::Subdirectories); - while (it.hasNext()) { - it.next(); - const QFileInfo info = it.fileInfo(); - const QString relativePath = QDir(iconsDir()).relativeFilePath(info.absoluteFilePath()); - if (isContentArt(relativePath)) { - continue; - } - if (info.suffix().compare(QStringLiteral("svg"), Qt::CaseInsensitive) != 0) { - violations.insert(violation(QStringLiteral("raster"), relativePath)); - continue; - } - - bool validXml = false; - bool hasViewBox = false; - bool hasEditorMetadata = false; - bool hasPhysicalSize = false; - inspectSvg(info.absoluteFilePath(), validXml, hasViewBox, hasEditorMetadata, hasPhysicalSize); - - if (!validXml) { - violations.insert(violation(QStringLiteral("xml"), relativePath)); - } - if (validXml && !hasViewBox) { - violations.insert(violation(QStringLiteral("viewbox"), relativePath)); - } - if (hasPhysicalSize) { - violations.insert(violation(QStringLiteral("physical-size"), relativePath)); - } - if (hasEditorMetadata) { - violations.insert(violation(QStringLiteral("editor-metadata"), relativePath)); - } - if (hasHardcodedColor(info.absoluteFilePath()) && !isSchemeVariant(info.fileName())) { - violations.insert(violation(QStringLiteral("hardcoded-color"), relativePath)); - } - if (!hasCurrentColorToken(info.absoluteFilePath()) && !isSchemeVariant(info.fileName())) { - violations.insert(violation(QStringLiteral("no-token"), relativePath)); - } - if (hasColorInFilename(info.fileName())) { - violations.insert(violation(QStringLiteral("color-filename"), relativePath)); - } - } - - return violations; -} - -QString formatSet(const QSet &entries) -{ - QStringList lines(entries.begin(), entries.end()); - lines.sort(); - return lines.join(QLatin1Char('\n')); -} - -} // namespace - -/** - * @brief Every UI glyph follows the palette-token rules. - * - * The legacy set was paid down icon by icon, so no baseline grandfathers - * violations: a new violation fails right here. Scheme variants - * (`*-dark.svg`/`*-light.svg`) and mana art are exempt inside collectViolations(). - */ -TEST(IconAssets, AssetsFollowTheRules) -{ - ASSERT_TRUE(QDir::isAbsolutePath(iconsDir())); - ASSERT_TRUE(QDir(iconsDir()).exists()) << "Missing icon directory: " << iconsDir().toStdString(); - - const QSet violations = collectViolations(); - EXPECT_TRUE(violations.isEmpty()) << "Icon rule violations. Fix the icon; do not add exceptions:\n" - << formatSet(violations).toStdString(); -} diff --git a/tests/movecard_tests/CMakeLists.txt b/tests/movecard_tests/CMakeLists.txt index 35d7b44ed..68d8fc84c 100755 --- a/tests/movecard_tests/CMakeLists.txt +++ b/tests/movecard_tests/CMakeLists.txt @@ -9,15 +9,3 @@ cockatrice_add_test( LABELS server ) - -cockatrice_add_test( - NAME - player_order_test - SOURCES - player_order_test.cpp - LIBS - libcockatrice_network_server_remote - libcockatrice_rng - LABELS - server -) diff --git a/tests/movecard_tests/player_order_test.cpp b/tests/movecard_tests/player_order_test.cpp deleted file mode 100644 index 6e398af24..000000000 --- a/tests/movecard_tests/player_order_test.cpp +++ /dev/null @@ -1,268 +0,0 @@ -#include "game/game_config.h" -#include "game/role_info.h" -#include "game/server_abstract_participant.h" -#include "game/server_game.h" -#include "server_abstractuserinterface.h" -#include "server_response_containers.h" -#include "server_room.h" -#include "server_test_helpers.h" - -#include -#include -#include -#include -#include -#include -#include - -RNG_Abstract *rng = nullptr; // this needs to be defined due to other functions in server - -class ScriptedRng : public RNG_Abstract -{ -public: - QList values; - - unsigned int rand(int min, int max) override - { - const unsigned int value = values.takeFirst(); - EXPECT_GE(value, static_cast(min)); - EXPECT_LE(value, static_cast(max)); - return value; - } -}; - -class FakeUserInterface : public Server_AbstractUserInterface -{ -public: - FakeUserInterface(Server *_server, const ServerInfo_User &_userInfo) : Server_AbstractUserInterface(_server) - { - setUserInfo(_userInfo); - } - - int getLastCommandTime() const override - { - return 0; - } - - bool addSaidMessageSize(int) override - { - return true; - } - - void sendProtocolItem(const Response &) override - { - } - - void sendProtocolItem(const SessionEvent &) override - { - } - - void sendProtocolItem(const GameEventContainer &) override - { - } - - void sendProtocolItem(const RoomEvent &) override - { - } -}; - -namespace -{ -QStringList participantNamesInOrder(const Server_Game &game) -{ - QStringList names; - for (int id : game.getSeatOrder()) { - const auto *participant = game.getParticipants().value(id, nullptr); - if (participant) { - names.append(QString::fromStdString(participant->getUserInfo()->name())); - } - } - return names; -} - -QList participantIdsInOrder(const Server_Game &game) -{ - return game.getSeatOrder(); -} -} // namespace - -class PlayerOrderTest : public ::testing::Test -{ -protected: - ServerInfo_User creator; - ServerInfo_User userB; - ServerInfo_User userC; - - PlayerOrderTest() - { - creator.set_name("A"); - creator.set_user_level(ServerInfo_User::IsRegistered); - userB.set_name("B"); - userB.set_user_level(ServerInfo_User::IsRegistered); - userC.set_name("C"); - userC.set_user_level(ServerInfo_User::IsRegistered); - } -}; - -TEST_F(PlayerOrderTest, JoinOrderIsSeatOrder) -{ - FakeServer server; - Server_Room room(0, 0, "", "", "", "", false, "", {}, &server); - FakeUserInterface uiA(&server, creator); - FakeUserInterface uiB(&server, userB); - FakeUserInterface uiC(&server, userC); - GameConfig config{.creatorInfo = creator, - .gameId = 1, - .description = QString(), - .password = QString(), - .maxPlayers = 3, - .gameTypes = QList(), - .onlyBuddies = false, - .onlyRegistered = false, - .spectatorsAllowed = false, - .spectatorsNeedPassword = false, - .spectatorsCanTalk = false, - .spectatorsSeeEverything = false, - .startingLifeTotal = 20, - .shareDecklistsOnLoad = false, - .shufflePlayers = false}; - Server_Game game(config, &room); - ResponseContainer rc(0); - - game.addPlayer(&uiA, rc, RoleInfo{}, false); - game.addPlayer(&uiB, rc, RoleInfo{}, false); - game.addPlayer(&uiC, rc, RoleInfo{}, false); - - EXPECT_EQ(participantNamesInOrder(game), QStringList({"A", "B", "C"})); - EXPECT_EQ(participantIdsInOrder(game), QList({0, 1, 2})); - EXPECT_EQ(game.getHostId(), 0); - EXPECT_EQ(game.getPlayerCount(), 3); -} - -TEST_F(PlayerOrderTest, ReorderPlayerSeats) -{ - FakeServer server; - Server_Room room(0, 0, "", "", "", "", false, "", {}, &server); - FakeUserInterface uiA(&server, creator); - FakeUserInterface uiB(&server, userB); - FakeUserInterface uiC(&server, userC); - GameConfig config{.creatorInfo = creator, - .gameId = 1, - .description = QString(), - .password = QString(), - .maxPlayers = 3, - .gameTypes = QList(), - .onlyBuddies = false, - .onlyRegistered = false, - .spectatorsAllowed = false, - .spectatorsNeedPassword = false, - .spectatorsCanTalk = false, - .spectatorsSeeEverything = false, - .startingLifeTotal = 20, - .shareDecklistsOnLoad = false, - .shufflePlayers = false}; - Server_Game game(config, &room); - ResponseContainer rc(0); - - game.addPlayer(&uiA, rc, RoleInfo{}, false); - game.addPlayer(&uiB, rc, RoleInfo{}, false); - game.addPlayer(&uiC, rc, RoleInfo{}, false); - - game.reorderPlayerSeats({"C", "A", "B"}); - - // player ids are stable; only the seat order changes - EXPECT_EQ(participantNamesInOrder(game), QStringList({"C", "A", "B"})); - EXPECT_EQ(participantIdsInOrder(game), QList({2, 0, 1})); - EXPECT_EQ(game.getParticipants().size(), 3); - - // the host keeps its seat: creator A is still in the game, so host stays 0 - EXPECT_EQ(game.getHostId(), 0); - - // no re-keying means every mapping keyed by player id stays valid - const int gameId = game.getGameId(); - EXPECT_EQ(uiA.getGames().value(gameId).second, 0); - EXPECT_EQ(uiB.getGames().value(gameId).second, 1); - EXPECT_EQ(uiC.getGames().value(gameId).second, 2); -} - -TEST_F(PlayerOrderTest, ReorderPlayerSeatsKeepsPlayerCount) -{ - FakeServer server; - Server_Room room(0, 0, "", "", "", "", false, "", {}, &server); - FakeUserInterface uiA(&server, creator); - FakeUserInterface uiB(&server, userB); - FakeUserInterface uiC(&server, userC); - GameConfig config{.creatorInfo = creator, - .gameId = 1, - .description = QString(), - .password = QString(), - .maxPlayers = 3, - .gameTypes = QList(), - .onlyBuddies = false, - .onlyRegistered = false, - .spectatorsAllowed = false, - .spectatorsNeedPassword = false, - .spectatorsCanTalk = false, - .spectatorsSeeEverything = false, - .startingLifeTotal = 20, - .shareDecklistsOnLoad = false, - .shufflePlayers = false}; - Server_Game game(config, &room); - ResponseContainer rc(0); - - game.addPlayer(&uiA, rc, RoleInfo{}, false); - game.addPlayer(&uiB, rc, RoleInfo{}, false); - game.addPlayer(&uiC, rc, RoleInfo{}, false); - - // listing only a subset appends the remaining players in their current order - game.reorderPlayerSeats({"C"}); - - EXPECT_EQ(participantNamesInOrder(game), QStringList({"C", "A", "B"})); - EXPECT_EQ(participantIdsInOrder(game), QList({2, 0, 1})); - EXPECT_EQ(game.getPlayerCount(), 3); - EXPECT_EQ(game.getHostId(), 0); -} - -TEST_F(PlayerOrderTest, ShufflePlayerSeatsIsAPermutation) -{ - FakeServer server; - Server_Room room(0, 0, "", "", "", "", false, "", {}, &server); - FakeUserInterface uiA(&server, creator); - FakeUserInterface uiB(&server, userB); - FakeUserInterface uiC(&server, userC); - GameConfig config{.creatorInfo = creator, - .gameId = 1, - .description = QString(), - .password = QString(), - .maxPlayers = 3, - .gameTypes = QList(), - .onlyBuddies = false, - .onlyRegistered = false, - .spectatorsAllowed = false, - .spectatorsNeedPassword = false, - .spectatorsCanTalk = false, - .spectatorsSeeEverything = false, - .startingLifeTotal = 20, - .shareDecklistsOnLoad = false, - .shufflePlayers = false}; - Server_Game game(config, &room); - ResponseContainer rc(0); - - game.addPlayer(&uiA, rc, RoleInfo{}, false); - game.addPlayer(&uiB, rc, RoleInfo{}, false); - game.addPlayer(&uiC, rc, RoleInfo{}, false); - - // fisher-yates on [A, B, C]: rand(0, 2) -> 1 (take B), rand(0, 1) -> 0 (take A), rand(0, 0) -> 0 (take C) - auto *scriptedRng = new ScriptedRng; - scriptedRng->values = {1, 0, 0}; - rng = scriptedRng; - - game.shufflePlayerSeats(); - - EXPECT_EQ(participantNamesInOrder(game), QStringList({"B", "A", "C"})); - EXPECT_EQ(participantIdsInOrder(game), QList({1, 0, 2})); - QSet idSet(participantIdsInOrder(game).cbegin(), participantIdsInOrder(game).cend()); - EXPECT_EQ(idSet.size(), 3); // ids are unique - // ids are stable, so the host (creator A, id 0) is unchanged - EXPECT_EQ(game.getHostId(), 0); -} diff --git a/tests/moxfield_signature_spells_test.cpp b/tests/moxfield_signature_spells_test.cpp deleted file mode 100644 index 54346a934..000000000 --- a/tests/moxfield_signature_spells_test.cpp +++ /dev/null @@ -1,84 +0,0 @@ -#include "client/network/parsers/interface_json_deck_parser.h" - -#include -#include -#include -#include - -TEST(MoxfieldImport, SignatureSpellIsAddedToMainWithoutReplacingCommanderBanner) -{ - // Moxfield v2 board objects map IDs to entries with quantity and card. - const auto input = QJsonDocument::fromJson(R"json({ - "name": "Oathbreaker example", "description": "Notes retained", - "mainboard": {"land": {"quantity": 1, "card": {"name": "Island", "set": "abc", "cn": "1"}}}, - "sideboard": {"side": {"quantity": 1, "card": {"name": "Mountain", "set": "abc", "cn": "2"}}}, - "commanders": {"walker": {"quantity": 1, "card": {"name": "Jace", "set": "abc", "cn": "3", "scryfall_id": "walker-id"}}}, - "signatureSpells": {"spell": {"quantity": 1, "card": {"name": "Lightning Bolt", "set": "xyz", "cn": "4", "scryfall_id": "spell-id"}}} - })json") - .object(); - MoxfieldJsonParser parser; - DeckList deck = parser.parse(input); - int mainSpellCount = 0; - int sideSpellCount = 0; - int mainLands = 0; - int sideLands = 0; - deck.forEachCard([&](InnerDecklistNode *zone, DecklistCardNode *card) { - if (card->getName() == "Island" && zone->getName() == DECK_ZONE_MAIN) { - mainLands += card->getNumber(); - } - if (card->getName() == "Mountain" && zone->getName() == DECK_ZONE_SIDE) { - sideLands += card->getNumber(); - } - if (card->getName() != "Lightning Bolt") { - return; - } - if (zone->getName() == DECK_ZONE_MAIN) { - mainSpellCount += card->getNumber(); - EXPECT_EQ(card->getCardSetShortName(), "XYZ"); - EXPECT_EQ(card->getCardCollectorNumber(), "4"); - EXPECT_EQ(card->getCardProviderId(), "spell-id"); - } else if (zone->getName() == DECK_ZONE_SIDE) { - sideSpellCount += card->getNumber(); - } - }); - EXPECT_EQ(mainSpellCount, 1); - EXPECT_EQ(sideSpellCount, 0); - EXPECT_EQ(mainLands, 1); - EXPECT_EQ(sideLands, 1); - EXPECT_EQ(deck.getName(), "Oathbreaker example"); - EXPECT_EQ(deck.getComments(), "Notes retained"); - EXPECT_EQ(deck.getBannerCard().name, "Jace"); - EXPECT_EQ(deck.getBannerCard().providerId, "walker-id"); -} - -TEST(MoxfieldImport, QuantityAwareDeckInsertionRetainsCountAndPrinting) -{ - DeckList deck; - auto *card = deck.addCards("Island", 2, DECK_ZONE_MAIN, -1, "ABC", "4", "id"); - ASSERT_NE(card, nullptr); - EXPECT_EQ(card->getNumber(), 2); - EXPECT_EQ(card->getCardProviderId(), "id"); - EXPECT_EQ(deck.addCards("Island", 0, DECK_ZONE_MAIN), nullptr); -} - -TEST(OnlineImport, ArchidektPreservesNameDescriptionAndCards) -{ - const auto input = QJsonDocument::fromJson(R"json({ - "name": "Online deck", "description": "Deck notes", - "cards": [{"quantity": 2, "card": { - "oracleCard": {"name": "Island"}, "edition": {"editioncode": "abc"}, "collectorNumber": "1" - }}] - })json") - .object(); - ArchidektJsonParser parser; - const DeckList deck = parser.parse(input); - int count = 0; - deck.forEachCard([&](InnerDecklistNode *zone, DecklistCardNode *card) { - if (zone->getName() == DECK_ZONE_MAIN && card->getName() == "Island") { - count += card->getNumber(); - } - }); - EXPECT_EQ(count, 2); - EXPECT_EQ(deck.getName(), "Online deck"); - EXPECT_EQ(deck.getComments(), "Deck notes"); -} diff --git a/tests/oracle/CMakeLists.txt b/tests/oracle/CMakeLists.txt index 1cc1ad952..6958673f7 100644 --- a/tests/oracle/CMakeLists.txt +++ b/tests/oracle/CMakeLists.txt @@ -9,7 +9,6 @@ cockatrice_add_test( ../../oracle/src/oracleimporter.cpp ../../oracle/src/parsehelpers.cpp ../../oracle/src/raw_json_scanner.cpp - ../../oracle/src/database_writer.cpp oracle_importer_test.cpp LIBS libcockatrice_card diff --git a/tests/oracle/oracle_importer_test.cpp b/tests/oracle/oracle_importer_test.cpp index 1b4e2d915..83d77d465 100644 --- a/tests/oracle/oracle_importer_test.cpp +++ b/tests/oracle/oracle_importer_test.cpp @@ -1,14 +1,11 @@ -#include "../../oracle/src/database_writer.h" #include "../../oracle/src/oracleimporter.h" #include "../../oracle/src/raw_json_scanner.h" -#include "../support/scoped_file_size_limit.h" #include "libcockatrice/card/card_info.h" #include "gtest/gtest.h" #include #include #include -#include #include #include #include @@ -23,7 +20,6 @@ #include #include #include -#include #include #include #include @@ -1144,208 +1140,6 @@ TEST_F(OracleImporterTest, StartImportAppliesLocalizedData) ASSERT_EQ(result->getLocalizedText("pt"), "Raio causa 3 de dano a qualquer alvo."); } -namespace -{ -QByteArray refreshSource(const QString &name = "New Card") -{ - QJsonObject card{{"name", name}, {"text", "Rules text."}, {"layout", "normal"}, - {"type", "Artifact"}, {"number", "1"}, {"rarity", "common"}}; - QJsonObject set{{"code", "TST"}, {"name", "Test Set"}, {"type", "expansion"}, {"cards", QJsonArray{card}}}; - return QJsonDocument(QJsonObject{{"data", QJsonObject{{"TST", set}}}}).toJson(QJsonDocument::Compact); -} -} // namespace - -TEST_F(OracleImporterTest, FailedLazyParseKeepsInstalledDatabase) -{ - QTemporaryDir dir; - ASSERT_TRUE(dir.isValid()); - const QString path = dir.filePath("cards.xml"); - QFile original(path); - ASSERT_TRUE(original.open(QIODevice::WriteOnly)); - ASSERT_EQ(original.write("existing database"), 17); - original.close(); - - // The metadata scanner skips numeric values without converting them. Qt's - // per-set parser rejects an overflowing number after selection succeeds. - auto badSet = QJsonDocument::fromJson(refreshSource()).object().value("data").toObject().value("TST").toObject(); - auto goodSet = badSet; - goodSet["code"] = "AAA"; - goodSet["name"] = "A successful first set"; - auto badCard = badSet.value("cards").toArray().first().toObject(); - badCard["number"] = "overflow"; - badSet["cards"] = QJsonArray{badCard}; - QByteArray data = QJsonDocument(QJsonObject{{"data", QJsonObject{{"AAA", goodSet}, {"TST", badSet}}}}) - .toJson(QJsonDocument::Compact); - data.replace("\"number\":\"overflow\"", "\"number\":1e9999"); - ASSERT_TRUE(importer->readSetsFromByteArray(data)); - EXPECT_EQ(importer->startImport(), 0); - EXPECT_FALSE(importer->hasSuccessfulImport()); - EXPECT_TRUE(importer->getImportError().contains("TST")); - EXPECT_TRUE(importer->getCardList().isEmpty()); - EXPECT_FALSE(importer->saveToFile(path, {}, {})); - ASSERT_TRUE(original.open(QIODevice::ReadOnly)); - EXPECT_EQ(original.readAll(), "existing database"); -} - -TEST_F(OracleImporterTest, EmptyImportCannotReplaceInstalledCards) -{ - const QByteArray source = R"({"data":{"TST":{"code":"TST","name":"Test","type":"expansion","cards":[]}}})"; - ASSERT_TRUE(importer->readSetsFromByteArray(source)); - EXPECT_EQ(importer->startImport(), 0); - EXPECT_FALSE(importer->hasSuccessfulImport()); - EXPECT_FALSE(importer->getImportError().isEmpty()); -} - -TEST_F(OracleImporterTest, MissingSourceBytesCannotProduceSuccessfulImport) -{ - ASSERT_TRUE(importer->readSetsFromByteArray(refreshSource())); - const auto selected = importer->getSets(); - importer->releaseSetData(); - importer->getSets() = selected; - EXPECT_EQ(importer->startImport(), 0); - EXPECT_FALSE(importer->hasSuccessfulImport()); - EXPECT_TRUE(importer->getImportError().contains("TST")); -} - -TEST_F(OracleImporterTest, CancellationDuringFinalSetCannotSavePartialDatabase) -{ - ASSERT_TRUE(importer->readSetsFromByteArray(refreshSource())); - QObject::connect( - importer, &OracleImporter::setIndexChanged, importer, - [this](int, int, const QString &name) { - if (!name.isEmpty()) { - importer->cancelImport(); - } - }, - Qt::DirectConnection); - EXPECT_EQ(importer->startImport(), 0); - EXPECT_FALSE(importer->hasSuccessfulImport()); - EXPECT_TRUE(importer->getCardList().isEmpty()); -} - -TEST_F(OracleImporterTest, CancellationBeforeWorkerStartsIsNotLost) -{ - ASSERT_TRUE(importer->readSetsFromByteArray(refreshSource())); - importer->prepareImport(); - importer->cancelImport(); - EXPECT_EQ(importer->startImport(), 0); - EXPECT_FALSE(importer->hasSuccessfulImport()); - importer->prepareImport(); - EXPECT_EQ(importer->startImport(), 1); - EXPECT_TRUE(importer->hasSuccessfulImport()); -} - -TEST_F(OracleImporterTest, NewImportReplacesPreviousInMemoryCards) -{ - ASSERT_TRUE(importer->readSetsFromByteArray(refreshSource("Old Card"))); - ASSERT_EQ(importer->startImport(), 1); - ASSERT_TRUE(importer->readSetsFromByteArray(refreshSource("New Card"))); - EXPECT_FALSE(importer->hasSuccessfulImport()); - ASSERT_EQ(importer->startImport(), 1); - EXPECT_EQ(importer->getCardList().size(), 1); - EXPECT_FALSE(importer->getCardList().contains("Old Card")); - EXPECT_TRUE(importer->getCardList().contains("New Card")); -} - -TEST_F(OracleImporterTest, FailedSourceLoadCannotReusePreviousSuccessfulImport) -{ - ASSERT_TRUE(importer->readSetsFromByteArray(refreshSource())); - ASSERT_EQ(importer->startImport(), 1); - ASSERT_FALSE(importer->readSetsFromByteArray("invalid JSON")); - EXPECT_FALSE(importer->hasSuccessfulImport()); - EXPECT_EQ(importer->startImport(), 0); -} - -TEST_F(OracleImporterTest, SuccessfulRefreshCommitsCompleteXml) -{ - QTemporaryDir dir; - ASSERT_TRUE(dir.isValid()); - const QString path = dir.filePath("cards.xml"); - ASSERT_TRUE(importer->readSetsFromByteArray(refreshSource())); - ASSERT_EQ(importer->startImport(), 1); - importer->releaseSetData(); - ASSERT_TRUE(importer->saveToFile(path, "test-source", "v1")); - QFile file(path); - ASSERT_TRUE(file.open(QIODevice::ReadOnly)); - const QByteArray xml = file.readAll(); - EXPECT_TRUE(xml.contains("New Card")); - EXPECT_TRUE(xml.contains("test-source")); - EXPECT_TRUE(xml.contains("")); -} - -TEST(OracleDatabaseWriter, AcceptsCompleteSupportedVersions) -{ - EXPECT_TRUE(OracleDatabase::validateXml( - "") - .isEmpty()); - EXPECT_TRUE(OracleDatabase::validateXml("") - .isEmpty()); - EXPECT_FALSE( - OracleDatabase::validateXml("") - .isEmpty()); -} - -TEST(OracleDatabaseWriter, InvalidDownloadPreservesInstalledBytes) -{ - QTemporaryDir dir; - ASSERT_TRUE(dir.isValid()); - const QString path = dir.filePath("tokens.xml"); - QFile original(path); - ASSERT_TRUE(original.open(QIODevice::WriteOnly)); - ASSERT_EQ(original.write("installed tokens"), 16); - original.close(); - for (const QByteArray &payload : {QByteArray("server error"), - QByteArray(""), QByteArray()}) { - QString error; - EXPECT_FALSE(OracleDatabase::saveXml(path, payload, &error)); - EXPECT_FALSE(error.isEmpty()); - ASSERT_TRUE(original.open(QIODevice::ReadOnly)); - EXPECT_EQ(original.readAll(), "installed tokens"); - original.close(); - } -} - -TEST(OracleDatabaseWriter, CompleteDownloadReplacesInstalledBytes) -{ - QTemporaryDir dir; - ASSERT_TRUE(dir.isValid()); - const QString path = dir.filePath("cards.xml"); - const QByteArray data(""); - QString error = "previous failure"; - ASSERT_TRUE(OracleDatabase::saveXml(path, data, &error)); - EXPECT_TRUE(error.isEmpty()); - QFile file(path); - ASSERT_TRUE(file.open(QIODevice::ReadOnly)); - EXPECT_EQ(file.readAll(), data); -} - -#ifdef Q_OS_UNIX -TEST(OracleDatabaseWriter, ShortWriteKeepsPreviouslyInstalledDatabase) -{ - QTemporaryDir dir; - ASSERT_TRUE(dir.isValid()); - const QString path = dir.filePath("cards.xml"); - QFile original(path); - ASSERT_TRUE(original.open(QIODevice::WriteOnly)); - ASSERT_EQ(original.write("installed database"), 18); - original.close(); - const QByteArray data = ""; - // A real filesystem write failure: the soft file-size cap makes the writer's own - // flush fail without filling a disk. The cap is scoped rather than forked, because a - // death-test child under RLIMIT_FSIZE is killed by SIGSEGV on macOS. - test_support::ScopedFileSizeLimit fileSizeCap(128); - ASSERT_TRUE(fileSizeCap.isValid()); - - QString error; - EXPECT_FALSE(OracleDatabase::saveXml(path, data, &error)); - EXPECT_FALSE(error.isEmpty()); - - ASSERT_TRUE(original.open(QIODevice::ReadOnly)); - EXPECT_EQ(original.readAll(), "installed database"); -} -#endif - int main(int argc, char **argv) { ::testing::InitGoogleTest(&argc, argv); diff --git a/tests/spoiler_file_test.cpp b/tests/spoiler_file_test.cpp deleted file mode 100644 index ecdc3285f..000000000 --- a/tests/spoiler_file_test.cpp +++ /dev/null @@ -1,135 +0,0 @@ -#include "client/network/update/card_spoiler/spoiler_file.h" -#include "support/scoped_file_size_limit.h" - -#include -#include -#include -#include -#include - -namespace -{ -class SpoilerFileTest : public ::testing::Test -{ -protected: - QTemporaryDir directory; - QString path; - QString error; - const QByteArray original = ""; - - void SetUp() override - { - ASSERT_TRUE(directory.isValid()); - path = directory.filePath("spoiler.xml"); - QFile file(path); - ASSERT_TRUE(file.open(QIODevice::WriteOnly)); - ASSERT_EQ(file.write(original), original.size()); - } - - QByteArray read() - { - QFile file(path); - EXPECT_TRUE(file.open(QIODevice::ReadOnly)); - return file.readAll(); - } - - SpoilerFile::SaveResult save(const QByteArray &data) - { - return SpoilerFile::save(path, data, [] { return true; }, error); - } -}; - -TEST_F(SpoilerFileTest, InvalidDocumentsPreserveExistingFile) -{ - for (const QByteArray &invalid : - {QByteArray(), QByteArray(""), - QByteArray(""), - QByteArray(""), - QByteArray(""), - QByteArray(""), - QByteArray("")}) { - EXPECT_EQ(save(invalid), SpoilerFile::SaveResult::Failed); - EXPECT_FALSE(error.isEmpty()); - EXPECT_EQ(read(), original); - } -} - -TEST_F(SpoilerFileTest, SupportedVersionsAreAccepted) -{ - EXPECT_TRUE(SpoilerFile::validate(original).isEmpty()); - EXPECT_TRUE( - SpoilerFile::validate("").isEmpty()); -} - -TEST_F(SpoilerFileTest, IdenticalFileIsUnchanged) -{ - EXPECT_EQ(save(original), SpoilerFile::SaveResult::Unchanged); - EXPECT_TRUE(error.isEmpty()); - EXPECT_EQ(read(), original); -} - -TEST_F(SpoilerFileTest, CompleteFileHashDetectsChangesAfterLargeCommonPrefix) -{ - QByteArray changed = ""; - ASSERT_EQ(save(changed), SpoilerFile::SaveResult::Written); - changed.replace("", ""); - EXPECT_EQ(save(changed), SpoilerFile::SaveResult::Written); - EXPECT_EQ(read(), changed); - EXPECT_TRUE(error.isEmpty()); -} - -TEST_F(SpoilerFileTest, CancellationBeforeWritePreservesExistingFile) -{ - EXPECT_EQ(SpoilerFile::save(path, original, [] { return false; }, error), SpoilerFile::SaveResult::Failed); - EXPECT_FALSE(error.isEmpty()); - EXPECT_EQ(read(), original); -} - -TEST_F(SpoilerFileTest, CancellationBeforeCommitPreservesExistingFile) -{ - int checks = 0; - EXPECT_EQ(SpoilerFile::save( - path, original + "\n", [&checks] { return ++checks == 1; }, error), - SpoilerFile::SaveResult::Failed); - EXPECT_EQ(checks, 2); - EXPECT_FALSE(error.isEmpty()); - EXPECT_EQ(read(), original); - EXPECT_EQ(QDir(directory.path()).entryList(QDir::Files), QStringList{"spoiler.xml"}); -} - -TEST_F(SpoilerFileTest, MissingParentIsCreated) -{ - path = directory.filePath("nested/spoiler.xml"); - EXPECT_EQ(save(original), SpoilerFile::SaveResult::Written); - EXPECT_EQ(read(), original); -} - -TEST_F(SpoilerFileTest, FileInParentPathReportsFailureWithoutModifyingIt) -{ - EXPECT_EQ(SpoilerFile::save( - QDir(path).filePath("spoiler.xml"), original, [] { return true; }, error), - SpoilerFile::SaveResult::Failed); - EXPECT_FALSE(error.isEmpty()); - EXPECT_EQ(read(), original); -} - -#ifdef Q_OS_UNIX -TEST_F(SpoilerFileTest, ShortWriteLeavesExistingFileIntact) -{ - const QByteArray changed = ""; - // A real short write exercises QSaveFile's buffered flush failure without depending - // on disk capacity. The cap is scoped rather than forked, because a death-test child - // under RLIMIT_FSIZE is killed by SIGSEGV on macOS. - test_support::ScopedFileSizeLimit fileSizeCap(128); - ASSERT_TRUE(fileSizeCap.isValid()); - - EXPECT_EQ(save(changed), SpoilerFile::SaveResult::Failed); - EXPECT_FALSE(error.isEmpty()); - EXPECT_EQ(read(), original); - EXPECT_EQ(QDir(directory.path()).entryList(QDir::Files), QStringList{"spoiler.xml"}); -} -#endif -} // namespace diff --git a/tests/support/scoped_file_size_limit.h b/tests/support/scoped_file_size_limit.h deleted file mode 100644 index 1c1f3d939..000000000 --- a/tests/support/scoped_file_size_limit.h +++ /dev/null @@ -1,83 +0,0 @@ -#ifndef COCKATRICE_TEST_SCOPED_FILE_SIZE_LIMIT_H -#define COCKATRICE_TEST_SCOPED_FILE_SIZE_LIMIT_H - -#include - -#ifdef Q_OS_UNIX - -#include -#include -#include - -namespace test_support -{ -/** - * @brief Caps how large a single file may grow, so a writer's short-write path is reachable. - * - * Lowering RLIMIT_FSIZE makes the next write past the cap fail with EFBIG instead of - * filling a filesystem, which is the only way to fail an install step mid-write without - * depending on free disk space or file ownership. Only the soft limit is lowered, so the - * destructor can hand the process back exactly as it found it: ctest runs one case per - * process, but a whole test binary must not carry the cap into its neighbours. - * - * Do not use this inside a gtest death test. A forked child under RLIMIT_FSIZE is killed - * by SIGSEGV on the macOS runners, while the same case passes on Linux, so the fork is - * what breaks - not the writer. The guard alone isolates the failure just as well. - */ -class ScopedFileSizeLimit -{ -public: - explicit ScopedFileSizeLimit(rlim_t maxBytes) - { - if (getrlimit(RLIMIT_FSIZE, &previousLimit) != 0) { - return; - } - - struct sigaction ignore = {}; - ignore.sa_handler = SIG_IGN; - sigemptyset(&ignore.sa_mask); - if (sigaction(SIGXFSZ, &ignore, &previousAction) != 0) { - return; - } - actionSaved = true; - - struct rlimit capped = previousLimit; - capped.rlim_cur = std::min(maxBytes, previousLimit.rlim_max); - if (setrlimit(RLIMIT_FSIZE, &capped) != 0) { - return; - } - active = true; - } - - ~ScopedFileSizeLimit() - { - if (active) { - setrlimit(RLIMIT_FSIZE, &previousLimit); - } - if (actionSaved) { - sigaction(SIGXFSZ, &previousAction, nullptr); - } - } - - ScopedFileSizeLimit(const ScopedFileSizeLimit &) = delete; - ScopedFileSizeLimit &operator=(const ScopedFileSizeLimit &) = delete; - - /** - * @brief Whether the cap is in force; a test that needs the failure must assert this. - */ - [[nodiscard]] bool isValid() const - { - return active; - } - -private: - bool active = false; - bool actionSaved = false; - struct rlimit previousLimit = {}; - struct sigaction previousAction = {}; -}; -} // namespace test_support - -#endif // Q_OS_UNIX - -#endif // COCKATRICE_TEST_SCOPED_FILE_SIZE_LIMIT_H diff --git a/tests/svg_recolor_test.cpp b/tests/svg_recolor_test.cpp deleted file mode 100644 index abe1e8d1a..000000000 --- a/tests/svg_recolor_test.cpp +++ /dev/null @@ -1,66 +0,0 @@ -#include "libcockatrice/utility/svg_recolor.h" - -#include -#include - -namespace -{ - -TEST(SvgRecolor, ReplacesEverySentinel) -{ - const QByteArray svg = R"()"; - const QByteArray result = resolveSvgColorSentinel(svg, "#ff8800"); - - EXPECT_EQ(result.count("#ff8800"), 2); - EXPECT_FALSE(result.contains("currentColor")); -} - -TEST(SvgRecolor, ReplacesSentinelRegardlessOfCase) -{ - const QByteArray svg = R"()"; - - EXPECT_EQ(resolveSvgColorSentinel(svg, "#ff8800").count("#ff8800"), 2); -} - -TEST(SvgRecolor, LeavesTheRestOfTheDocumentIntact) -{ - const QByteArray svg = - R"()"; - const QByteArray result = resolveSvgColorSentinel(svg, "#123456"); - - EXPECT_TRUE(result.contains(R"(viewBox="0 0 24 24")")); - EXPECT_TRUE(result.contains(R"(d="M0 0h24v24")")); - EXPECT_TRUE(result.contains("an ordinary comment")); -} - -TEST(SvgRecolor, DoesNotModifyTheInput) -{ - const QByteArray svg = R"()"; - const QByteArray original = svg; - - resolveSvgColorSentinel(svg, "#000000"); - - EXPECT_EQ(svg, original); -} - -TEST(SvgRecolor, PreservesBytesThatAreNotValidUtf8) -{ - QByteArray svg = ""; - svg.append("\xff\xfe"); - svg.append(R"()"); - - const QByteArray result = resolveSvgColorSentinel(svg, "#ff8800"); - - EXPECT_TRUE(result.contains("\xff\xfe")); - EXPECT_FALSE(result.contains("currentColor")); - EXPECT_EQ(result.count("#ff8800"), 1); -} - -TEST(SvgRecolor, NoSentinelIsANoOp) -{ - const QByteArray svg = R"()"; - - EXPECT_EQ(resolveSvgColorSentinel(svg, "#ffffff"), svg); -} - -} // namespace diff --git a/tests/theme_migration/CMakeLists.txt b/tests/theme_migration/CMakeLists.txt index 818c1bb16..93493ee5f 100644 --- a/tests/theme_migration/CMakeLists.txt +++ b/tests/theme_migration/CMakeLists.txt @@ -5,14 +5,12 @@ cockatrice_add_test( ${CMAKE_SOURCE_DIR}/cockatrice/src/interface/theme_manager.cpp ${CMAKE_SOURCE_DIR}/cockatrice/src/interface/theme_config.cpp ${CMAKE_SOURCE_DIR}/cockatrice/src/interface/pixel_map_generator.cpp - ${CMAKE_SOURCE_DIR}/cockatrice/src/interface/theme_icon.cpp ${CMAKE_SOURCE_DIR}/cockatrice/src/client/settings/cache_settings.cpp ${CMAKE_SOURCE_DIR}/cockatrice/src/client/settings/card_counter_settings.cpp ${CMAKE_SOURCE_DIR}/cockatrice/src/client/settings/shortcuts_settings.cpp ${CMAKE_SOURCE_DIR}/cockatrice/src/client/network/update/client/release_channel.cpp ${VERSION_STRING_CPP} theme_migration_test.cpp - theme_icon_render_test.cpp LIBS libcockatrice_settings libcockatrice_network @@ -20,6 +18,4 @@ cockatrice_add_test( libcockatrice_protocol INCLUDE_DIRS ${CMAKE_SOURCE_DIR}/cockatrice/src - DEFINITIONS - COCKATRICE_SOURCE_DIR="${CMAKE_SOURCE_DIR}" ) diff --git a/tests/theme_migration/theme_icon_render_test.cpp b/tests/theme_migration/theme_icon_render_test.cpp deleted file mode 100644 index f12db5b16..000000000 --- a/tests/theme_migration/theme_icon_render_test.cpp +++ /dev/null @@ -1,189 +0,0 @@ -#include "interface/theme_icon.h" -#include "interface/theme_manager.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace -{ -/** - * @brief Creates the GUI application QPixmap needs, using the offscreen plugin. - * - * The theme-migration binary is headless; creating this lazily keeps its other - * tests free of a display dependency. - */ -void ensureGuiApplication() -{ - static int argc = 1; - static char name[] = "theme_icon_test"; - static char *argv[] = {name, nullptr}; - // Leaked on purpose: a function-local static QApplication is destroyed before Qt's - // own library statics at exit, and the network module's destructors then run - // against a torn-down QObject layer (SEGV under AddressSanitizer). - static QApplication *app = new QApplication(argc, argv); - Q_UNUSED(app); -} - -QString resourcesDir() -{ - return QDir(QStringLiteral(COCKATRICE_SOURCE_DIR)).absoluteFilePath(QStringLiteral("cockatrice/resources")); -} - -int countOpaquePixels(const QPixmap &pixmap, const QColor &expected) -{ - const QImage image = pixmap.toImage(); - int count = 0; - for (int y = 0; y < image.height(); ++y) { - for (int x = 0; x < image.width(); ++x) { - const QColor pixel = image.pixelColor(x, y); - if (pixel.alpha() > 200 && pixel.red() == expected.red() && pixel.green() == expected.green() && - pixel.blue() == expected.blue()) { - ++count; - } - } - } - return count; -} - -QByteArray svgWith(const QByteArray &colorToken) -{ - return QByteArrayLiteral(R"()"); -} - -/** - * @brief Restores the global theme search paths and palette after each test. - * - * The icon engine reads both process-wide values, so a failing assertion must - * not leak a temporary theme directory or palette into the next test. - */ -class ThemeIconRenderTest : public ::testing::Test -{ -protected: - void SetUp() override - { - qputenv("QT_QPA_PLATFORM", "offscreen"); - ensureGuiApplication(); - savedSearchPaths = QDir::searchPaths(QStringLiteral("theme")); - savedPalette = qApp->palette(); - themeManager = new ThemeManager; - QDir::setSearchPaths(QStringLiteral("theme"), {resourcesDir()}); - } - - void TearDown() override - { - delete themeManager; - themeManager = nullptr; - QDir::setSearchPaths(QStringLiteral("theme"), savedSearchPaths); - qApp->setPalette(savedPalette); - } - - QStringList savedSearchPaths; ///< Search paths captured before the test. - QPalette savedPalette; ///< Application palette captured before the test. -}; - -/** @brief A non-square request must letterbox, not squash, a 24×24 glyph. */ -TEST_F(ThemeIconRenderTest, NonSquareRequestPreservesAspectRatio) -{ - const QIcon icon = loadThemedIcon(QStringLiteral("icons/player")); - ASSERT_FALSE(icon.isNull()); - - EXPECT_EQ(icon.actualSize(QSize(20, 15)), QSize(15, 15)); - - const QPixmap pixmap = icon.pixmap(QSize(20, 15)); - ASSERT_FALSE(pixmap.isNull()); - EXPECT_EQ(pixmap.deviceIndependentSize(), QSizeF(15, 15)); -} - -/** @brief The engine must follow palette flips, icon modes, and DPR. */ -TEST_F(ThemeIconRenderTest, ThemedIconFollowsPaletteModeAndDpr) -{ - QPalette palette = qApp->palette(); - palette.setColor(QPalette::WindowText, QColor(QStringLiteral("#123456"))); - qApp->setPalette(palette); - - const QIcon icon = loadThemedIcon(QStringLiteral("icons/search")); - ASSERT_FALSE(icon.isNull()); - - const QPixmap normal = icon.pixmap(QSize(24, 24), 1.0); - ASSERT_FALSE(normal.isNull()); - EXPECT_EQ(normal.size(), QSize(24, 24)); - EXPECT_DOUBLE_EQ(normal.devicePixelRatio(), 1.0); - EXPECT_GT(countOpaquePixels(normal, QColor(QStringLiteral("#123456"))), 0); - - const QPixmap hidpi = icon.pixmap(QSize(24, 24), 2.0); - ASSERT_FALSE(hidpi.isNull()); - EXPECT_DOUBLE_EQ(hidpi.devicePixelRatio(), 2.0); - EXPECT_EQ(hidpi.deviceIndependentSize(), QSizeF(24, 24)); - - palette.setColor(QPalette::WindowText, QColor(QStringLiteral("#abcdef"))); - qApp->setPalette(palette); - const QPixmap flipped = icon.pixmap(QSize(24, 24), 1.0); - EXPECT_GT(countOpaquePixels(flipped, QColor(QStringLiteral("#abcdef"))), 0); - - QPalette selectedPalette = qApp->palette(); - selectedPalette.setColor(QPalette::HighlightedText, QColor(QStringLiteral("#00ff00"))); - qApp->setPalette(selectedPalette); - const QPixmap selected = icon.pixmap(QSize(24, 24), 1.0, QIcon::Selected, QIcon::Off); - EXPECT_GT(countOpaquePixels(selected, QColor(QStringLiteral("#00ff00"))), 0); -} -} // namespace - -TEST_F(ThemeIconRenderTest, SentinelUsesPaletteColor) -{ - QPalette palette; - palette.setColor(QPalette::WindowText, QColor(Qt::white)); - qApp->setPalette(palette); - - const QPixmap pixmap = loadSentinelSvgPixmap(QStringLiteral("icons/search.svg")); - - ASSERT_FALSE(pixmap.isNull()); - EXPECT_EQ(pixmap.size(), QSize(24, 24)); - EXPECT_GT(countOpaquePixels(pixmap, Qt::white), 0); -} - -TEST_F(ThemeIconRenderTest, NonSentinelSvgReturnsNull) -{ - QTemporaryDir themeDir; - ASSERT_TRUE(themeDir.isValid()); - QFile file(QDir(themeDir.path()).absoluteFilePath(QStringLiteral("plain.svg"))); - ASSERT_TRUE(file.open(QIODevice::WriteOnly)); - file.write(svgWith("#000000")); - file.close(); - QDir::addSearchPath(QStringLiteral("theme"), themeDir.path()); - - EXPECT_TRUE(loadSentinelSvgPixmap(QStringLiteral("plain.svg")).isNull()); - EXPECT_TRUE(loadSentinelSvgPixmap(QStringLiteral("icons/search.png")).isNull()); -} - -TEST_F(ThemeIconRenderTest, LowercaseSentinelIsResolved) -{ - QTemporaryDir themeDir; - ASSERT_TRUE(themeDir.isValid()); - QFile file(QDir(themeDir.path()).absoluteFilePath(QStringLiteral("lower.svg"))); - ASSERT_TRUE(file.open(QIODevice::WriteOnly)); - file.write(svgWith("currentcolor")); - file.close(); - - QDir::addSearchPath(QStringLiteral("theme"), themeDir.path()); - - QPalette palette; - palette.setColor(QPalette::WindowText, QColor(QStringLiteral("#123456"))); - qApp->setPalette(palette); - - const QPixmap pixmap = loadSentinelSvgPixmap(QStringLiteral("lower.svg")); - - ASSERT_FALSE(pixmap.isNull()); - EXPECT_GT(countOpaquePixels(pixmap, QColor(QStringLiteral("#123456"))), 0); -} diff --git a/tests/user_games_model_test.cpp b/tests/user_games_model_test.cpp deleted file mode 100644 index 33c29d6d4..000000000 --- a/tests/user_games_model_test.cpp +++ /dev/null @@ -1,248 +0,0 @@ -#include "interface/widgets/server/user/user_games_model.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -RNG_Abstract *rng = nullptr; // required by the transitive server library; this suite sends no game commands - -namespace -{ -class GamesClient : public AbstractClient -{ -public: - QList sent; - - GamesClient() - { - setStatus(StatusLoggedIn); - } - - ~GamesClient() override - { - qDeleteAll(pendingCommands); - pendingCommands.clear(); - } - - void changeStatus(ClientStatus status) - { - setStatus(status); - } - - void answer(int request, Response response) - { - response.set_cmd_id(sent.at(request).cmd_id()); - ServerMessage message; - message.set_message_type(ServerMessage::RESPONSE); - *message.mutable_response() = response; - processProtocolItem(message); - } - -protected: - void sendCommandContainer(const CommandContainer &command) override - { - sent.append(command); - } -}; - -Response gamesResponse(std::initializer_list ids) -{ - Response response; - response.set_response_code(Response::RespOk); - auto *games = response.MutableExtension(Response_GetGamesOfUser::ext); - for (int id : ids) { - auto *game = games->add_game_list(); - game->set_game_id(id); - game->set_room_id(7); - } - return response; -} - -using State = UserGamesModel::State; - -TEST(UserGamesModel, OfflineUserDoesNotSendARequest) -{ - GamesClient client; - UserGamesModel model(&client); - model.setUser("Alice", false); - model.refresh(); - EXPECT_TRUE(client.sent.isEmpty()); - EXPECT_EQ(State::Offline, model.getState()); - EXPECT_FALSE(model.canRefresh()); - EXPECT_EQ(0, model.rowCount()); -} - -TEST(UserGamesModel, PresenceLossInvalidatesTheOutstandingResponse) -{ - GamesClient client; - UserGamesModel model(&client); - model.setUser("Alice", true); - EXPECT_EQ(State::Loading, model.getState()); - EXPECT_FALSE(model.canRefresh()); - model.setUser("Alice", false); - client.answer(0, gamesResponse({1})); - EXPECT_EQ(State::Offline, model.getState()); - EXPECT_EQ(0, model.rowCount()); - - model.setUser("Alice", true); - client.answer(1, gamesResponse({2})); - EXPECT_EQ(State::Ready, model.getState()); - EXPECT_TRUE(model.canRefresh()); - EXPECT_EQ(2, model.data(model.index(0), UserGamesModel::GameRole).value().game_id()); -} - -TEST(UserGamesModel, SwitchingBackToTheSameUserDoesNotAcceptTheirOldRequest) -{ - GamesClient client; - UserGamesModel model(&client); - model.setUser("Alice", true); - model.setUser("Bob", true); - model.setUser("Alice", true); - client.answer(2, gamesResponse({3})); - client.answer(0, gamesResponse({1})); - client.answer(1, gamesResponse({2})); - EXPECT_EQ(1, model.rowCount()); - EXPECT_EQ(3, model.data(model.index(0), UserGamesModel::GameRole).value().game_id()); -} - -TEST(UserGamesModel, RefreshSupersedesThePreviousRequest) -{ - GamesClient client; - UserGamesModel model(&client); - model.setUser("Alice", true); - model.refresh(); - client.answer(0, gamesResponse({1})); - EXPECT_EQ(State::Loading, model.getState()); - EXPECT_EQ(0, model.rowCount()); - client.answer(1, gamesResponse({2})); - EXPECT_EQ(State::Ready, model.getState()); - EXPECT_EQ(2, model.data(model.index(0), UserGamesModel::GameRole).value().game_id()); -} - -TEST(UserGamesModel, EmptySuccessAndMissingPayloadAreDifferentStates) -{ - GamesClient client; - UserGamesModel model(&client); - model.setUser("Alice", true); - client.answer(0, gamesResponse({})); - EXPECT_EQ(State::Empty, model.getState()); - EXPECT_TRUE(model.canRefresh()); - - model.refresh(); - Response missingPayload; - missingPayload.set_response_code(Response::RespOk); - client.answer(1, missingPayload); - EXPECT_EQ(State::Error, model.getState()); - EXPECT_TRUE(model.canRefresh()); -} - -TEST(UserGamesModel, FailedRequestsCanBeRetried) -{ - GamesClient client; - UserGamesModel model(&client); - model.setUser("Alice", true); - Response error; - error.set_response_code(Response::RespNameNotFound); - client.answer(0, error); - EXPECT_EQ(State::Error, model.getState()); - model.refresh(); - client.answer(1, gamesResponse({4})); - EXPECT_EQ(State::Ready, model.getState()); -} - -TEST(UserGamesModel, DisconnectRejectsLateRepliesAndReconnectFetchesAgain) -{ - GamesClient client; - UserGamesModel model(&client); - model.setUser("Alice", true); - client.changeStatus(StatusDisconnecting); - client.answer(0, gamesResponse({1})); - EXPECT_EQ(State::Disconnected, model.getState()); - EXPECT_FALSE(model.canRefresh()); - model.refresh(); - EXPECT_EQ(1, client.sent.size()); - client.changeStatus(StatusLoggedIn); - client.answer(1, gamesResponse({2})); - EXPECT_EQ(State::Ready, model.getState()); -} - -TEST(UserGamesModel, ClientDestructionClearsGamesAndDisablesRefresh) -{ - auto client = std::make_unique(); - UserGamesModel model(client.get()); - model.setUser("Alice", true); - client->answer(0, gamesResponse({1})); - client.reset(); - EXPECT_EQ(State::Disconnected, model.getState()); - EXPECT_EQ(0, model.rowCount()); - EXPECT_FALSE(model.canRefresh()); - model.refresh(); - EXPECT_EQ(State::Disconnected, model.getState()); -} - -TEST(UserGamesModel, ResetInvalidatesAnOpenGameActionAndPreservesTheModelContract) -{ - GamesClient client; - UserGamesModel model(&client); - QAbstractItemModelTester tester(&model, QAbstractItemModelTester::FailureReportingMode::Fatal); - model.setUser("Alice", true); - client.answer(0, gamesResponse({1, 2, 3})); - QPersistentModelIndex selected(model.index(1)); - EXPECT_TRUE(selected.isValid()); - model.refresh(); - EXPECT_FALSE(selected.isValid()); - EXPECT_EQ(0, model.rowCount()); - client.answer(1, gamesResponse({4})); - EXPECT_EQ(1, model.rowCount()); -} - -TEST(UserGamesModel, StateListenerCanSelectAnotherUserBeforeTheRequestIsSent) -{ - GamesClient client; - UserGamesModel model(&client); - bool switched = false; - QObject::connect(&model, &UserGamesModel::stateChanged, &model, [&] { - if (!switched && model.getState() == State::Loading) { - switched = true; - model.setUser("Bob", false); - } - }); - model.setUser("Alice", true); - EXPECT_TRUE(client.sent.isEmpty()); - EXPECT_EQ(State::Offline, model.getState()); -} - -TEST(UserGamesModel, TimedOutResponseCannotReplaceARetry) -{ - GamesClient client; - UserGamesModel model(&client, 50); - model.setUser("Alice", true); - QSignalSpy changes(&model, &UserGamesModel::stateChanged); - ASSERT_TRUE(changes.wait(1000)); - ASSERT_EQ(State::Error, model.getState()); - EXPECT_TRUE(model.canRefresh()); - model.refresh(); - client.answer(0, gamesResponse({1})); - EXPECT_EQ(State::Loading, model.getState()); - client.answer(1, gamesResponse({2})); - EXPECT_EQ(State::Ready, model.getState()); - EXPECT_EQ(2, model.data(model.index(0), UserGamesModel::GameRole).value().game_id()); -} -} // namespace - -int main(int argc, char **argv) -{ - QCoreApplication app(argc, argv); - ::testing::InitGoogleTest(&argc, argv); - return RUN_ALL_TESTS(); -}