diff --git a/.ci/Arch/Dockerfile b/.ci/Arch/Dockerfile index 4dc2f5e90..b08e568f3 100644 --- a/.ci/Arch/Dockerfile +++ b/.ci/Arch/Dockerfile @@ -1,9 +1,9 @@ -FROM archlinux:latest +from archlinux:latest RUN pacman --sync --refresh --sysupgrade --needed --noconfirm \ + base-devel \ ccache \ cmake \ - gcc \ git \ gtest \ mariadb-libs \ diff --git a/.ci/Debian12/Dockerfile b/.ci/Debian12/Dockerfile index 03f33b38e..4da910dc0 100644 --- a/.ci/Debian12/Dockerfile +++ b/.ci/Debian12/Dockerfile @@ -1,17 +1,16 @@ -FROM debian:12-slim +FROM debian:12 RUN apt-get update && \ DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + build-essential \ ca-certificates \ ccache \ cmake \ - dpkg-dev \ file \ g++ \ git \ libcurl4-openssl-dev \ libgl-dev \ - libgtest-dev \ liblzma-dev \ libmariadb-dev-compat \ libprotobuf-dev \ diff --git a/.ci/Debian13/Dockerfile b/.ci/Debian13/Dockerfile index 8ee8cccff..94f711398 100644 --- a/.ci/Debian13/Dockerfile +++ b/.ci/Debian13/Dockerfile @@ -1,17 +1,16 @@ -FROM debian:13-slim +FROM debian:13 RUN apt-get update && \ DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + build-essential \ ca-certificates \ ccache \ cmake \ - dpkg-dev \ file \ g++ \ git \ libcurl4-openssl-dev \ libgl-dev \ - libgtest-dev \ liblzma-dev \ libmariadb-dev-compat \ libprotobuf-dev \ diff --git a/.ci/Fedora45/Dockerfile b/.ci/Fedora43/Dockerfile similarity index 85% rename from .ci/Fedora45/Dockerfile rename to .ci/Fedora43/Dockerfile index 5527aac87..8aa345651 100644 --- a/.ci/Fedora45/Dockerfile +++ b/.ci/Fedora43/Dockerfile @@ -1,12 +1,11 @@ -FROM fedora:45 +FROM fedora:43 -RUN dnf install -y --nodocs \ +RUN dnf install -y \ ca-certificates \ ccache \ cmake \ gcc-c++ \ git \ - gtest-devel \ libcurl-devel \ mariadb-devel \ ninja-build \ diff --git a/.ci/Fedora44/Dockerfile b/.ci/Fedora44/Dockerfile index d02649b4d..15204fa1a 100644 --- a/.ci/Fedora44/Dockerfile +++ b/.ci/Fedora44/Dockerfile @@ -1,12 +1,11 @@ FROM fedora:44 -RUN dnf install -y --nodocs \ +RUN dnf install -y \ ca-certificates \ ccache \ cmake \ gcc-c++ \ git \ - gtest-devel \ libcurl-devel \ mariadb-devel \ ninja-build \ diff --git a/.ci/Servatrice_Debian12/Dockerfile b/.ci/Servatrice_Debian12/Dockerfile index 8c166a85b..f1a16843a 100644 --- a/.ci/Servatrice_Debian12/Dockerfile +++ b/.ci/Servatrice_Debian12/Dockerfile @@ -1,10 +1,10 @@ -FROM debian:12-slim +FROM debian:12 RUN apt-get update && \ DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + build-essential \ ccache \ cmake \ - dpkg-dev \ file \ g++ \ git \ diff --git a/.ci/Ubuntu24.04/Dockerfile b/.ci/Ubuntu24.04/Dockerfile index 47f927f41..43497dd3b 100644 --- a/.ci/Ubuntu24.04/Dockerfile +++ b/.ci/Ubuntu24.04/Dockerfile @@ -2,16 +2,15 @@ FROM ubuntu:24.04 RUN apt-get update && \ DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + build-essential \ ca-certificates \ ccache \ cmake \ - dpkg-dev \ file \ g++ \ git \ libcurl4-openssl-dev \ libgl-dev \ - libgtest-dev \ liblzma-dev \ libmariadb-dev-compat \ libprotobuf-dev \ diff --git a/.ci/Ubuntu26.04/Dockerfile b/.ci/Ubuntu26.04/Dockerfile index f50a3cec2..4dc861ae9 100644 --- a/.ci/Ubuntu26.04/Dockerfile +++ b/.ci/Ubuntu26.04/Dockerfile @@ -2,17 +2,16 @@ FROM ubuntu:26.04 RUN apt-get update && \ DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + build-essential \ ca-certificates \ ccache \ cmake \ - dpkg-dev \ file \ g++ \ gcovr \ git \ libcurl4-openssl-dev \ libgl-dev \ - libgtest-dev \ liblzma-dev \ libmariadb-dev-compat \ libprotobuf-dev \ diff --git a/.ci/compile.sh b/.ci/compile.sh index c58563f61..569ae9f6b 100755 --- a/.ci/compile.sh +++ b/.ci/compile.sh @@ -20,7 +20,6 @@ # --profile writes an MSBuild performance summary and binary log, Windows only # uses env: BUILDTYPE MAKE_INSTALL MAKE_PACKAGE PACKAGE_TYPE PACKAGE_SUFFIX MAKE_SERVER MAKE_NO_CLIENT MAKE_TEST USE_CCACHE CCACHE_SIZE CCACHE_EVICTION_AGE BUILD_DIR CMAKE_GENERATOR TARGET_MACOS_VERSION MAKE_SANITIZE MAKE_COVERAGE VCPKG_TARGET_MACOS_VERSION BUILD_PARALLEL_LEVEL BUILD_PROFILE # (correspond to args: --debug/--release --install --package --suffix --server --test --sanitize --coverage --ccache --dir --profile) -# boolean env flags are enabled with the value 1 (e.g. MAKE_SERVER=1) # exitcode: 1 for failure, 3 for invalid arguments # Read arguments @@ -139,21 +138,17 @@ done set -e -# Schema version consistency (only relevant to the server) -if [[ ${MAKE_SERVER:-} == "1" ]]; then - ./servatrice/check_schema_version.sh -fi - -# Use default values if unset -if [[ -z $BUILDTYPE ]]; then +# Setup +./servatrice/check_schema_version.sh +if [[ ! $BUILDTYPE ]]; then BUILDTYPE=Release fi -if [[ -z $BUILD_DIR ]]; then +if [[ ! $BUILD_DIR ]]; then BUILD_DIR="build" fi mkdir -p "$BUILD_DIR" -if [[ ${MAKE_COVERAGE:-} == "1" ]]; then +if [[ $MAKE_COVERAGE ]]; then # Resolved while the working directory is still the source root. $0 is relative to the # directory the script was invoked from, so after the cd below it no longer resolves, and # under `set -e` the failed subshell would abort the build rather than just yielding an empty @@ -168,16 +163,16 @@ export CMAKE_POLICY_VERSION_MINIMUM=3.10 # Add cmake flags flags=("-DCMAKE_BUILD_TYPE=$BUILDTYPE") -if [[ ${MAKE_SERVER:-} == "1" ]]; then +if [[ $MAKE_SERVER ]]; then flags+=("-DWITH_SERVER=1") fi -if [[ ${MAKE_NO_CLIENT:-} == "1" ]]; then +if [[ $MAKE_NO_CLIENT ]]; then flags+=("-DWITH_CLIENT=0" "-DWITH_ORACLE=0") fi -if [[ ${MAKE_TEST:-} == "1" ]]; then +if [[ $MAKE_TEST ]]; then flags+=("-DTEST=1") fi -if [[ ${MAKE_SANITIZE:-} == "1" ]]; then +if [[ $MAKE_SANITIZE ]]; then flags+=("-DENABLE_SANITIZERS=ON") else # Every leg states the flag explicitly rather than relying on the CMake @@ -185,20 +180,20 @@ else # never leak libasan/libubsan into a shipped binary. flags+=("-DENABLE_SANITIZERS=OFF") fi -if [[ ${MAKE_COVERAGE:-} == "1" ]]; then +if [[ $MAKE_COVERAGE ]]; then flags+=("-DENABLE_COVERAGE=ON") fi -if [[ ${USE_CCACHE:-} == "1" ]]; then +if [[ $USE_CCACHE ]]; then flags+=("-DUSE_CCACHE=1") - if [[ -n $CCACHE_SIZE ]]; then - # This setting persists after running the script + if [[ $CCACHE_SIZE ]]; then + # note, this setting persists after running the script ccache --max-size "$CCACHE_SIZE" fi fi -if [[ -n $PACKAGE_TYPE ]]; then +if [[ $PACKAGE_TYPE ]]; then flags+=("-DCPACK_GENERATOR=$PACKAGE_TYPE") fi -if [[ ${USE_VCPKG:-} == "1" ]]; then +if [[ $USE_VCPKG ]]; then flags+=("-DUSE_VCPKG=1") flags+=("-DVCPKG_INSTALL_OPTIONS=--x-abi-tools-use-exact-versions") fi @@ -208,11 +203,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 @@ -226,8 +216,7 @@ fi buildflags=(--config "$BUILDTYPE") function ccachestatsverbose() { - ccache --version - # Verbose only works on newer ccache, discard the error + # note, verbose only works on newer ccache, discard the error local got if got="$(ccache --show-stats --verbose 2>/dev/null)"; then echo "$got" @@ -267,44 +256,42 @@ if [[ $RUNNER_OS == macOS ]]; then # this works independent of the qt version as there should be only one version installed on the runner at a time export QTDIR - if [[ -n $TARGET_MACOS_VERSION ]]; then - # CMAKE_OSX_DEPLOYMENT_TARGET is a vanilla CMake flag needed to compile to the target macOS version + if [[ $TARGET_MACOS_VERSION ]]; then + # CMAKE_OSX_DEPLOYMENT_TARGET is a vanilla cmake flag needed to compile to target macOS version flags+=("-DCMAKE_OSX_DEPLOYMENT_TARGET=$TARGET_MACOS_VERSION") - if [[ ${USE_VCPKG:-} == "1" ]]; then - # vcpkg dependencies need a vcpkg triplet file to compile to the target macOS version - # An easy way is to copy the x64-osx.cmake file and modify it - triplets_dir="/tmp/cmake/triplets" - triplet_version="custom-triplet" - triplet_file="$triplets_dir/$triplet_version.cmake" - arch=$(uname -m) - if [[ $arch == x86_64 ]]; then - arch="x64" - fi - mkdir -p "$triplets_dir" - triplet_source="../vcpkg/triplets/$arch-osx.cmake" - if [[ ! -f "$triplet_source" ]]; then - triplet_source="../vcpkg/triplets/community/$arch-osx.cmake" - fi - cp "$triplet_source" "$triplet_file" - # The triplet is what vcpkg hashes into the ABI hash of every port, so two legs that - # agree here reuse each other's binary cache. Targets 14, 15 and 26 on the same Xcode - # produce identical binaries when they share the lowest deployment target, so they all - # build against 14 to keep one vcpkg cache scope instead of three. - # Keep in sync with "vcpkg_scope" and "vcpkg_deployment_target" in desktop-build.yml. - vcpkg_target="${VCPKG_TARGET_MACOS_VERSION:-$TARGET_MACOS_VERSION}" - echo "set(VCPKG_CMAKE_SYSTEM_VERSION $vcpkg_target)" >>"$triplet_file" - echo "set(VCPKG_OSX_DEPLOYMENT_TARGET $vcpkg_target)" >>"$triplet_file" - flags+=("-DVCPKG_OVERLAY_TRIPLETS=$triplets_dir") - flags+=("-DVCPKG_HOST_TRIPLET=$triplet_version") - flags+=("-DVCPKG_TARGET_TRIPLET=$triplet_version") - echo "::group::Generated triplet $triplet_file" - cat "$triplet_file" - echo "::endgroup::" + # vcpkg dependencies need a vcpkg triplet file to compile to the target macOS version + # an easy way is to copy the x64-osx.cmake file and modify it + triplets_dir="/tmp/cmake/triplets" + triplet_version="custom-triplet" + triplet_file="$triplets_dir/$triplet_version.cmake" + arch=$(uname -m) + if [[ $arch == x86_64 ]]; then + arch="x64" fi + mkdir -p "$triplets_dir" + triplet_source="../vcpkg/triplets/$arch-osx.cmake" + if [[ ! -f "$triplet_source" ]]; then + triplet_source="../vcpkg/triplets/community/$arch-osx.cmake" + fi + cp "$triplet_source" "$triplet_file" + # The triplet is what vcpkg hashes into the ABI hash of every port, so two legs that + # agree here reuse each other's binary cache. Targets 14, 15 and 26 on the same Xcode + # produce identical binaries when they share the lowest deployment target, so they all + # build against 14 to keep one vcpkg cache scope instead of three. + # Keep in sync with "vcpkg_scope" and "vcpkg_deployment_target" in desktop-build.yml. + vcpkg_target="${VCPKG_TARGET_MACOS_VERSION:-$TARGET_MACOS_VERSION}" + echo "set(VCPKG_CMAKE_SYSTEM_VERSION $vcpkg_target)" >>"$triplet_file" + echo "set(VCPKG_OSX_DEPLOYMENT_TARGET $vcpkg_target)" >>"$triplet_file" + flags+=("-DVCPKG_OVERLAY_TRIPLETS=$triplets_dir") + flags+=("-DVCPKG_HOST_TRIPLET=$triplet_version") + flags+=("-DVCPKG_TARGET_TRIPLET=$triplet_version") + echo "::group::Generated triplet $triplet_file" + cat "$triplet_file" + echo "::endgroup::" fi - echo "::group::Setup signing certificate" + echo "::group::Signing Certificate" if [[ -n "$MACOS_CERTIFICATE_NAME" ]]; then echo "$MACOS_CERTIFICATE" | base64 --decode >"certificate.p12" security create-keychain -p "$MACOS_CI_KEYCHAIN_PWD" build.keychain @@ -319,13 +306,9 @@ if [[ $RUNNER_OS == macOS ]]; then fi echo "::endgroup::" - if [[ ${MAKE_PACKAGE:-} == "1" ]]; then - # Workaround https://github.com/actions/runner-images/issues/7522: XProtect holds a - # freshly written DMG for a moment ("Resource busy"), and a mount that outlived a - # failed detach stays busy for the rest of the job. Retry every hdiutil call through - # a wrapper that widens the delay so the scan can finish, retries a detach with - # -force, and kills a wedged DiskImages helper instead of letting it hold the job - # until the 100 minute timeout. + if [[ $MAKE_PACKAGE ]]; then + # Workaround https://github.com/actions/runner-images/issues/7522 + # have hdiutil repeat the command 10 times in hope of success. # The wrapper has to exit non-zero once it gives up: CPack reads that status to # decide whether the disk image was created, so a zero exit turns a failed hdiutil # into a much later, misleading "Problem copying the package" error that hides @@ -334,74 +317,17 @@ if [[ $RUNNER_OS == macOS ]]; then # shellcheck disable=SC2016 echo '#!/bin/bash attempts=10 - timeout_seconds=${HDIUTIL_TIMEOUT:-300} - subcommand=$1 - # CPACK_DMG_VOLUME_NAME in CMakeLists.txt; a -volname argument wins when there is one. - volume_name=Cockatrice - arg_count=$# - for (( i = 1; i < arg_count; i++ )); do - if [[ ${!i} == -volname ]] && (( i + 1 <= arg_count )); then - (( i++ )) - volume_name=${!i} - fi - done - - # macOS ships no timeout(1), so a watchdog kills the supervised call. It writes to - # /dev/null because a process holding the output pipe open would make CPack wait for - # the watchdog instead of for hdiutil; it is killed once the call returns. - run_supervised() { - "$@" & - pid=$! - ( sleep "$timeout_seconds" && kill -9 "$pid" ) >/dev/null 2>&1 & - watchdog=$! - wait "$pid" + i=0 + while true; do + hdiutil "$@" && exit 0 rc=$? - kill "$watchdog" 2>/dev/null - wait "$watchdog" 2>/dev/null - return "$rc" - } - - force_detach_stale_mount() { - [[ -d /Volumes/$volume_name ]] || return 0 - echo "hdiutil $subcommand: force-detaching stale /Volumes/$volume_name" >&2 - run_supervised hdiutil detach -force "/Volumes/$volume_name" >/dev/null 2>&1 - return 0 - } - - timeouts=0 - attempt=0 - delay=1 - while (( attempt < attempts )); do - (( attempt++ )) - cmd=(hdiutil "$@") - # A busy volume is what a detach fails on, and -force is what clears it. - if (( attempt > 1 )) && [[ $subcommand == detach ]]; then - cmd=(hdiutil detach -force "${@:2}") + if (( ++i >= attempts )); then + echo "::error::hdiutil $1 failed $i times (last status $rc), giving up" >&2 + exit "$rc" fi - run_supervised "${cmd[@]}" - rc=$? - (( rc == 0 )) && exit 0 - if (( rc == 137 )); then - (( timeouts++ )) - echo "hdiutil $subcommand attempt $attempt/$attempts timed out after ${timeout_seconds}s" >&2 - # A wedged DiskImages helper is not the busy-file flake: two kills are all the - # evidence the loop needs before failing is a better use of the job budget. - if (( timeouts >= 2 )); then - echo "::error::hdiutil $subcommand timed out $timeouts times, giving up" >&2 - exit "$rc" - fi - else - echo "hdiutil $subcommand attempt $attempt/$attempts failed (status $rc), retrying" >&2 - fi - (( attempt < attempts )) || break - if [[ $subcommand == create || $subcommand == attach ]]; then - force_detach_stale_mount - fi - sleep "$delay" - (( delay = delay * 2 > 30 ? 30 : delay * 2 )) - done - echo "::error::hdiutil $subcommand failed $attempt times (last status $rc), giving up" >&2 - exit "$rc"' >"$hdiutil_script" + echo "hdiutil $1 attempt $i/$attempts failed (status $rc), retrying" >&2 + sleep 1 + done' >"$hdiutil_script" chmod +x "$hdiutil_script" flags+=(-DCPACK_COMMAND_HDIUTIL="$hdiutil_script") fi @@ -411,7 +337,7 @@ elif [[ $RUNNER_OS == Windows ]]; then # cmake only hands MSBuild a job count when one is asked for, and without it # MSBuild builds a single node: one project at a time, whatever MTT does inside # it. Ask for a node per core. - if [[ -z $BUILD_PARALLEL_LEVEL ]]; then + if [[ ! $BUILD_PARALLEL_LEVEL ]]; then BUILD_PARALLEL_LEVEL="${NUMBER_OF_PROCESSORS:-1}" fi buildflags+=(--parallel "$BUILD_PARALLEL_LEVEL") @@ -437,20 +363,19 @@ elif [[ $RUNNER_OS == Windows ]]; then fi fi -if [[ ${USE_CCACHE:-} == "1" ]]; then +if [[ $USE_CCACHE ]]; then echo "::group::Show ccache stats" ccachestatsverbose echo "::endgroup::" fi -# Configure CMake -echo "::group::Generate build system" +echo "::group::Configure cmake" cmake --version -echo "Running CMake configuration with the following flags: ${flags[*]}" +echo "Running cmake with flags: ${flags[*]}" cmake .. "${flags[@]}" echo "::endgroup::" -if [[ ${USE_CCACHE:-} == "1" ]]; then +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 @@ -465,10 +390,9 @@ if [[ ${USE_CCACHE:-} == "1" ]]; then ccache --set-config sloppiness=pch_defines,time_macros,include_file_mtime,include_file_ctime fi -# Build echo "::group::Build project" cpuinfo -echo "Running CMake with the following build flags: ${buildflags[*]}" +echo "Running cmake --build with flags: ${buildflags[*]}" cmake --build . "${buildflags[@]}" echo "::endgroup::" @@ -480,16 +404,16 @@ if [[ $BUILD_PROFILE && $RUNNER_OS == Windows && -f msbuild-perf.log ]]; then echo "::endgroup::" fi -if [[ ${USE_CCACHE:-} == "1" ]]; then - if [[ -n $CCACHE_EVICTION_AGE ]]; then - echo "::group::Evict ccache files older than $CCACHE_EVICTION_AGE" +if [[ $USE_CCACHE ]]; then + if [[ $CCACHE_EVICTION_AGE ]]; then + echo "::group::evict ccache files older than $CCACHE_EVICTION_AGE" ccache --evict-older-than "$CCACHE_EVICTION_AGE" echo "::endgroup::" fi echo "::group::Show ccache stats again" ccachestatsverbose echo "::endgroup::" -elif [[ -n $CCACHE_EVICTION_AGE ]]; then +elif [[ $CCACHE_EVICTION_AGE ]]; then echo "::error file=$0::ccache eviction is enabled while ccache is disabled!" fi @@ -506,9 +430,9 @@ if [[ $RUNNER_OS == macOS ]]; then echo "::endgroup::" fi -if [[ ${MAKE_TEST:-} == "1" ]]; then +if [[ $MAKE_TEST ]]; then echo "::group::Run tests" - if [[ ${MAKE_SANITIZE:-} == "1" ]]; then + if [[ $MAKE_SANITIZE ]]; then # detect_leaks=0: Qt and the sanitizers' own runtimes both leak at exit by design, and # LeakSanitizer's default is aggressive enough to bury our real findings under hundreds # of process-teardown reports. Malloc/free misuse and use-after-free are what ASan is for @@ -526,12 +450,11 @@ if [[ ${MAKE_TEST:-} == "1" ]]; then # bound and -j recovers nearly all of the per-process overhead. The test binaries are safe to # run concurrently: the only one that claims a fixed resource is single_instance_manager_test, # which already scopes its local socket name to its own pid so parallel copies cannot collide. - ctest --version ctest -C "$BUILDTYPE" --output-on-failure -j "${CTEST_JOBS:-4}" echo "::endgroup::" fi -if [[ ${MAKE_COVERAGE:-} == "1" ]]; then +if [[ $MAKE_COVERAGE ]]; then # gcovr is not a build dependency and is not available everywhere, so a missing one is reported # and skipped rather than failing an otherwise green build. echo "::group::Coverage report" @@ -556,7 +479,7 @@ if [[ ${MAKE_COVERAGE:-} == "1" ]]; then echo "::endgroup::" fi -if [[ $RUNNER_OS == Windows && ${MAKE_NO_CLIENT:-} != "1" ]]; then +if [[ $RUNNER_OS == Windows && ! $MAKE_NO_CLIENT ]]; then echo "::group::Probe the installer's holder lookup" # The working directory is still the build dir here: the package steps below are what # leave and re-enter it, and that is what made a $BUILD_DIR-prefixed path name @@ -756,21 +679,18 @@ POWERSHELL echo "::endgroup::" fi -# Install -if [[ ${MAKE_INSTALL:-} == "1" ]]; then +if [[ $MAKE_INSTALL ]]; then echo "::group::Install" cmake --build . --target install --config "$BUILDTYPE" echo "::endgroup::" fi -# Package -if [[ ${MAKE_PACKAGE:-} == "1" ]]; then +if [[ $MAKE_PACKAGE ]]; then echo "::group::Create package" - cpack --version cmake --build . --target package --config "$BUILDTYPE" echo "::endgroup::" - if [[ -n $PACKAGE_SUFFIX ]]; then + if [[ $PACKAGE_SUFFIX ]]; then echo "::group::Update package name" cd .. BUILD_DIR="$BUILD_DIR" .ci/name_build.sh "$PACKAGE_SUFFIX" 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/.dockerignore b/.dockerignore index fc07b2b9d..8e0b69332 100644 --- a/.dockerignore +++ b/.dockerignore @@ -2,6 +2,6 @@ build/ .github/ .tx/ cockatrice/ -docs/ +doc/ oracle/ Dockerfile diff --git a/docs/CONTRIBUTING.md b/.github/CONTRIBUTING.md similarity index 100% rename from docs/CONTRIBUTING.md rename to .github/CONTRIBUTING.md diff --git a/.github/workflows/desktop-build.yml b/.github/workflows/desktop-build.yml index 861b28fbb..4ac28d3a5 100644 --- a/.github/workflows/desktop-build.yml +++ b/.github/workflows/desktop-build.yml @@ -15,7 +15,7 @@ on: - '!**.md' - '!.github/**' - '!.tx/**' - - '!docs/**' + - '!doc/**' - '.github/workflows/desktop-build.yml' - 'CMakeLists.txt' - 'vcpkg.json' @@ -28,7 +28,7 @@ on: - '!**.md' - '!.github/**' - '!.tx/**' - - '!docs/**' + - '!doc/**' - '.github/workflows/desktop-build.yml' - 'CMakeLists.txt' - 'vcpkg.json' @@ -47,20 +47,6 @@ concurrency: group: "${{ github.workflow }} @ ${{ github.ref_name }} (${{ github.event_name }})" cancel-in-progress: ${{ github.ref_type != 'tag' }} -# Qt's online installer ships its base package as separate archives. Installing only the -# ones Cockatrice loads drops the full 25+ archive download to this handful (Windows Qt -# cache: 440 MB → 180 MB, macOS thin Qt: ~200 MB → 60 MB). The addons Cockatrice also needs -# (Multimedia, WebSockets, ShaderTools, ImageFormats) are not base archives: they stay in -# each leg's qt_modules. -# -# The list mirrors what cmake/FindQtRuntime.cmake requires, so a component that is missing -# here fails configure with "No suitable version of Qt was found" instead of at runtime: -# qtbase supplies Core, Gui, Widgets, Network, Sql, Xml, Concurrent and PrintSupport; -# qtdeclarative supplies Qml, Quick and QuickWidgets; qtsvg supplies Svg; qttools supplies -# Linguist (lrelease/lupdate); qttranslations ships Qt's own catalogs. -env: - QT_ARCHIVES: qtbase qtdeclarative qtsvg qttools qttranslations - jobs: configure: name: Configure @@ -103,7 +89,7 @@ jobs: TAG: ${{ steps.configure.outputs.tag }} run: .ci/prep_release.sh - - name: "Create or update draft release" + - name: "Create release" if: steps.configure.outputs.tag != null id: create_release shell: bash @@ -115,35 +101,13 @@ jobs: body_path: ${{ steps.prepare.outputs.body_path }} prerelease: ${{ steps.prepare.outputs.is_beta }} run: | - # A release for this tag may already exist: a maintainer can save a draft in - # the GitHub UI before pushing the tag, or an earlier run may have left one. - # Never touch a published release, and never overwrite a draft that already - # has a body (generated notes or a maintainer's edits) - only fill in an - # empty draft. Prints " "; empty when there is none. - release_state="$(gh release view "$tag_name" --json isDraft,body \ - --template '{{.isDraft}} {{len (or .body "")}}' 2>/dev/null || true)" + args=() + [[ $prerelease == yes ]] && args+=(--prerelease) - if [[ -z "$release_state" ]]; then - args=() - [[ $prerelease == yes ]] && args+=(--prerelease) - - gh release create "$tag_name" --verify-tag --draft "${args[@]}" \ - --target "$target" \ - --title "$release_name" \ - --notes-file "$body_path" - elif [[ "$release_state" == "true 0" ]]; then - echo "Filling in the empty draft release $tag_name" - args=(--prerelease=false) - [[ $prerelease == yes ]] && args=(--prerelease) - - gh release edit "$tag_name" "${args[@]}" \ - --title "$release_name" \ - --notes-file "$body_path" - elif [[ "$release_state" == true* ]]; then - echo "::warning::Draft release $tag_name already has a body, leaving it untouched" - else - echo "::warning::Release $tag_name is already published, leaving it untouched" - fi + gh release create "$tag_name" --verify-tag --draft "${args[@]}" \ + --target "$target" \ + --title "$release_name" \ + --notes-file "$body_path" vcpkg-baseline: name: vcpkg baseline @@ -185,8 +149,7 @@ 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: 680 MB working set at -g1 + debug_test: yes # see the note on Fedora 44 below - distro: Servatrice_Debian version: 12 @@ -194,47 +157,46 @@ 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' + ccache_size: 400M # server only, no client to compile - distro: Debian version: 13 package: DEB - ccache_size: 800M # Release plus the suite: 625 MB working set 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 + ccache_size: 850M # the Release build plus the suite it runs - distro: Fedora - version: 45 - variant: release # separate image and ccache from the debug leg above + version: 44 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 + # Arch and Fedora 44 are the two newest toolchains, and the -Wno-error= exceptions + # in CMakeLists.txt exist for exactly their warnings. They keep compiling Debug: + # only the Debug configuration passes -Werror, so these two are the legs where a + # new warning in either compiler still fails the build. + debug_test: yes + ccache_size: 900M # Debug for the suite and Release for the package - distro: Ubuntu version: 26.04 package: DEB - test: skip # Debian 13 covers the uninstrumented suite on pull requests - # 18.1 min, shorter than the Windows leg. The sanitize leg in the nightly - # matrix runs this distro's instrumented suite. - persist_ccache: 'false' + test: skip # The sanitizer leg below runs this distro's suite + + # 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. + # + # 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. + - distro: Ubuntu + version: 26.04 + variant: sanitize # separate image and ccache, same distro + + package: skip + sanitize: yes + ccache_size: 800M # instrumented objects are larger name: ${{ matrix.distro }} ${{ matrix.version }}${{ matrix.variant && format(' {0}', matrix.variant) || '' }} needs: configure @@ -249,71 +211,25 @@ jobs: 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. Measured 2026-11-07, the quota holds 2.97 GB - # of these four ccache entries, 0.92 GB of Qt, 1.24 GB of vcpkg, 0.91 GB of buildkit - # layers (mode=min in docker-release.yml) and 4.26 GB of CodeQL trap caches: one entry - # per analyzed commit, about 1.07 GB each, of which every one but the newest is dead. - # That is 10.30 GB against the 10 GB quota, so every save past this point costs some - # other entry, and least-recently-used order reaches the buildkit layers before the - # trap caches. prune-caches below keeps only the newest trap cache; the live total is - # then 7.1 GB, which is what leaves room for the Windows and macOS compiler caches. + # not against what one leg would like. The quota currently holds about 1.1 GB of + # CodeQL trap, 1.1 GB of vcpkg, 0.7 GB of Qt and 2.1 GB of docker buildkit layers + # (mode=min in docker-release.yml, added here, brings the last one down to roughly + # 0.3 GB - the one input here that is an expectation rather than a measurement, since + # mode=min still writes one layer per pushed binary) plus the compiler caches of the nine legs a pull request runs. The five + # nightly-only legs keep no compiler cache (see PERSIST_CCACHE below), which is what + # makes these per-leg bounds affordable. Accounting for all of it: 6.45G of ccache + # bounds + 3.2G of the fixed entries = 9.65G against the 10G quota. # - # 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 680 MB working set 850M - # Debian 13 Release + suite 625 MB 800M - # Fedora 45 debug Debug + suite 660 MB 850M - # Fedora 45 release Release + package 614 MB 800M - # Windows 10 Release + package 890 MB working set 950M - # macOS 14/15 Release + package 0.5 GB ccache at -g1 700M - # macOS 27 Debug Debug + suite 0.5 GB ccache at -g1 700M - # - # That is 6.35 GB of bounds; what lands in the quota is 5.15 GB of compiler cache, - # 2.97 GB stored by the Linux legs, 0.95 GB by Windows and 1.23 GB by the three macOS - # legs, for 9.3 GB of 10 in total. - # So only legs worth their share get one. A leg keeps no cache when it duplicates - # another leg's coverage (Ubuntu 26.04 behind Debian 13's Release + suite) or finishes - # well inside the Windows leg (Servatrice covers the server alone in 4.7 min). The - # macOS legs compile the client with a different toolchain (Apple clang), and one - # configuration writes only ~0.5 GB, so all three fit beside the Windows entry even - # though a macOS cache served 42% and saved about 32s on a 20 min leg. The four Linux - # legs 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, so Windows sets the - # pull-request wall clock no matter how warm these legs are. That is why the Windows - # leg keeps the largest bound (see build-vcpkg): MSVC has no -g level to shrink, and - # with /Z7 the whole configuration is cacheable, PCH included. - # - # Steady-state master data corrected this on 2026-11-07: Arch 90.1% and Debian 13 - # 96.75% per-run hits at the 850M/800M bounds, Fedora 45 debug 99.18% and release - # 99.2%. The 75%/82% seen the day before were warming runs - the -g1 switch had - # invalidated the cache one commit earlier - not thrashing at the bound. A cold 3G - # run on the same day measured one configuration at 680 MB (Arch) and 625 MB - # (Debian 13), so those bounds already hold the working set with ~170 MB of slack for - # commit-to-commit churn, and raising them buys hit rate the numbers do not show a - # need for. The three macOS entries are 700M each because one configuration wrote a - # 0.5 GB ccache directory at -g1 and level 0 shrinks it further, so the bound holds the - # whole configuration with room to spare. - # - # Re-add a leg only with a bound that holds its whole configuration: a smaller one is - # worse than none. + # A configuration needs roughly 500-850 MB on Linux, so the old uniform 600M bound sat + # below what a leg writes: ccache spent each build deleting the entries the next run + # asked for (measured: 2624-3720 cleanup passes per build, 8% of calls served). Each + # leg states the size it needs instead: one configuration for most, two for Fedora 44 + # (Debug for the suite, Release for the package), the server alone for Servatrice. + # Raising these further means caching fewer legs, since GitHub evicts a whole entry + # when the total is exceeded. # # 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' || '' }} CMAKE_GENERATOR: 'Ninja' NAME: ${{ matrix.distro }}${{ matrix.version }} IMAGE_SUFFIX: ${{ matrix.variant }} @@ -326,13 +242,11 @@ jobs: # 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' }} + # Whether this leg restores and saves its compiler cache. Every leg a pull request + # runs keeps one; the nightly-only legs do not, so that the quota is spent on the + # builds that review a change. A tag build runs both sets, and the nightly legs are + # cold there - accepted, because a tag is rare and only the pull-request legs gate. + PERSIST_CCACHE: 'true' # Shared with build-linux-nightly so the two leg sets run the same steps. steps: &linux-steps @@ -485,14 +399,6 @@ jobs: # comes from the client event itself (COCKATRICE_SENTRY_RELEASE). sentry-cli debug-files upload --org cockatrice --project cockatrice-desktop "${paths[@]}" - # Pull requests and tags only restore; only a push to refs/heads/master writes. A PR save - # would replace the objects the next master build restores, and a PR that compiles a - # different configuration would evict the warm set for everyone. Tags build with full - # DWARF (COCKATRICE_DEBUG_INFO_LEVEL is empty), so a tag's cache would poison the -g1 - # set if it shared the master scope; it does not, because the save is master-only. The - # nightly legs persist nothing at all (PERSIST_CCACHE is false) for the same reason: - # a schedule shares refs/heads/master with the push. - # # 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 @@ -560,7 +466,7 @@ jobs: test: skip # Running tests on all distros is superfluous - distro: Fedora - version: 44 # previous release, still covered by the nightly + version: 43 # previous release, still covered by the nightly package: RPM test: skip # Running tests on all distros is superfluous @@ -571,28 +477,6 @@ jobs: package: DEB test: skip # Running tests on all distros is superfluous - # The one leg that runs the suite under AddressSanitizer and - # UndefinedBehaviorSanitizer. It runs nightly rather than on pull requests: it is - # not on the pull-request critical path (Windows sets that), and its 1.2G cache - # was the largest single entry in the 10 GB quota. Debian 13 and Fedora 45 keep - # covering the uninstrumented suite on pull requests, so nothing is lost there. - # - # It packages nothing. A sanitized Debug build is not something to ship; the - # pull-request Ubuntu 26.04 leg publishes this distro's DEB. - - distro: Ubuntu - version: 26.04 - variant: sanitize # separate image, same distro as the coverage leg below - - package: skip - sanitize: yes - # A sanitizer finding must not redden the nightly run: vcpkg-cache-import only - # imports the archives of a run whose conclusion is success. The job itself - # still reports failure. - allow-failure: yes - # Scratch only (nightly legs persist no cache); sized so the instrumented - # objects, which are larger than the 614 MB measured, do not thrash mid-build. - ccache_size: 1200M - # 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 @@ -617,7 +501,7 @@ jobs: 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 instrumented legs. + # 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' }} @@ -630,16 +514,10 @@ jobs: # 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: ${{ matrix.ccache_size || '600M' }} + 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: @@ -652,7 +530,6 @@ jobs: runner: xcode-27 # https://github.com/actions/runner-images/blob/main/images/macos/xcode-27-arm64-Readme.md ccache_eviction_age: 7d - ccache_size: 700M # Release + package: ~0.4 GB at level 0 cmake_generator: Ninja make_package: 1 override_target: 14 @@ -671,7 +548,6 @@ jobs: runner: xcode-27 # https://github.com/actions/runner-images/blob/main/images/macos/xcode-27-arm64-Readme.md ccache_eviction_age: 7d - ccache_size: 700M # Release + package: ~0.4 GB at level 0 cmake_generator: Ninja make_package: 1 override_target: 15 @@ -689,7 +565,6 @@ jobs: runner: xcode-27 # https://github.com/actions/runner-images/blob/main/images/macos/xcode-27-arm64-Readme.md ccache_eviction_age: 7d - ccache_size: 700M # Debug + suite: ~0.5 GB at level 0 cmake_generator: Ninja qt_version: 6.11.* qt_modules: qtimageformats qtmultimedia qtshadertools qtwebsockets @@ -714,9 +589,6 @@ jobs: qt_version: 6.11.* qt_modules: qtimageformats qtmultimedia qtshadertools qtwebsockets type: Release - ccache_eviction_age: 7d - ccache_size: 950M # Release plus the package: 890 MB working set without /Z7 - use_ccache: 1 # Scope is named after the vs2026 toolchain, not the generator: the # ABI hash covers cl.exe, which is identical, so this leg keeps # restoring the warm archives. Renaming would orphan them. @@ -730,21 +602,14 @@ jobs: runs-on: ${{ matrix.runner }} timeout-minutes: 100 env: - # The Windows leg and all three pull-request macOS legs keep a compiler cache. ccache - # drives MSVC as long as the compiler emits /Z7, which CMakeLists.txt sets for exactly - # this reason, and it drives clang on macOS the same way; both cache the PCH-backed - # targets with the sloppiness that .ci/compile.sh re-asserts. Every leg builds without - # debug information on non-tag refs, so one macOS configuration writes ~0.4-0.5 GB and - # the 700M bound holds it with room for commit-to-commit churn (see build-linux for the - # quota arithmetic). CCACHE_DIR: ${{ github.workspace }}/.cache/ - CCACHE_SIZE: ${{ matrix.ccache_size || '700M' }} - PERSIST_CCACHE: ${{ matrix.persist_ccache || 'true' }} - # MSVC has no -g level, so there is nothing to ask for on Windows: level 0 is the one - # value with a meaning there, and it drops /Z7 rather than lowering a level. Pull-request - # builds carry no debug information, which is most of what a compiler cache stores; a - # tag is the build whose binary is shipped and symbolicated, so it keeps the full set. - COCKATRICE_DEBUG_INFO_LEVEL: ${{ github.ref_type != 'tag' && '0' || '' }} + # macOS configurations compile about twice the TUs of a Linux one, so each of these + # three legs gets a bound above what it has been measured to write. The four macOS + # legs used to be bounded at 600M from the same quota as the Linux ones; the three + # pull-request legs now get 700M each while the nightly Intel leg keeps none, and + # that is what fits (see CCACHE_SIZE in build-linux for the full arithmetic). + CCACHE_SIZE: 700M + PERSIST_CCACHE: 'true' # Shared with build-vcpkg-nightly so the two leg sets run the same steps. steps: &vcpkg-steps @@ -759,31 +624,18 @@ jobs: with: msbuild-architecture: x64 - - name: "[Windows] Setup ccache" - if: matrix.os == 'Windows' && matrix.use_ccache == 1 - shell: bash - run: choco install ccache --no-progress -y - - name: "[macOS] Setup ccache" if: matrix.os == 'macOS' && matrix.use_ccache == 1 run: brew install ccache - - name: "Restore compiler cache (ccache)" - if: matrix.use_ccache == 1 && env.PERSIST_CCACHE == 'true' + - name: "[macOS] Restore compiler cache (ccache)" + if: matrix.os == 'macOS' && matrix.use_ccache == 1 && env.PERSIST_CCACHE == 'true' id: restore_ccache uses: actions/cache/restore@v6 with: - key: ccache-${{ matrix.os }}${{ matrix.target }} + key: ccache-${{ matrix.os }}${{ matrix.override_target }}-${{ matrix.runner }} path: ${{ env.CCACHE_DIR }} - # Every macOS leg that keeps a cache restores the master entry for its own target. - # Measured on master 2026-11-07 (before the debug-information change), one macOS - # configuration wrote a 0.5 GB ccache directory at -g1 against the 700M bound, and the - # restored cache served 42% of calls. Level 0 shrinks that directory to ~0.4 GB, so the - # bound holds the whole configuration and the three entries fit the quota (see the - # sizing note in build-linux). macOS 14 and 15 build the same Release + package - # configuration for two deployment targets, but each is cheap enough to keep its own. - - name: "[macOS] Install aqtinstall" if: matrix.os == 'macOS' run: pipx install aqtinstall @@ -802,7 +654,7 @@ jobs: id: restore_qt uses: actions/cache/restore@v6 with: - key: Qt-${{ steps.resolve_qt_version.outputs.version }}-macOS-${{ matrix.soc }}-${{ matrix.qt_modules }}-${{ env.QT_ARCHIVES }}-thin + key: Qt-${{ steps.resolve_qt_version.outputs.version }}-macOS-${{ matrix.soc }}-${{ matrix.qt_modules }}-thin path: ${{ github.workspace }}/Qt # Using jurplel/install-qt-action to install Qt without using brew @@ -811,7 +663,6 @@ jobs: if: matrix.os == 'macOS' && steps.restore_qt.outputs.cache-hit != 'true' uses: jurplel/install-qt-action@v4 with: - archives: ${{ env.QT_ARCHIVES }} cache: false # cache-key-prefix: Qt dir: ${{ github.workspace }} # thinning script depends on this location @@ -835,7 +686,6 @@ jobs: with: # Qt 6.11.0 only works with aqtinstall directly from git until aqtinstall 3.4 is released aqtsource: git+https://github.com/miurahr/aqtinstall.git - archives: ${{ env.QT_ARCHIVES }} cache: ${{ github.ref == 'refs/heads/master' }} cache-key-prefix: Qt modules: ${{ matrix.qt_modules }} @@ -844,7 +694,7 @@ jobs: - name: "[Windows] Install NSIS" if: matrix.os == 'Windows' shell: bash - run: choco install nsis --no-progress + run: choco install nsis # Ninja invokes cl.exe directly and needs the MSVC environment on PATH. - name: "[Windows] Set up MSVC environment" @@ -924,21 +774,6 @@ jobs: WITH_CRASH_REPORTING: ${{ matrix.type == 'Release' && 'ON' || '' }} run: .ci/compile.sh --server --test --vcpkg - # What the compiler cache has to hold for this leg. ccache stores the objects it - # compiles, so their total is the working set a bound has to fit, and the cache - # directory shows what this build actually wrote. Debug information dominates both, - # which is why the level above is reported next to them. - - name: "Report compiler cache footprint" - if: always() - shell: bash - run: | - echo "=== ccache directory" - du -sh "$CCACHE_DIR" 2>/dev/null || echo "(none)" - echo "=== objects and archives" - find build -type f \( -name '*.o' -o -name '*.obj' -o -name '*.a' -o -name '*.lib' \) -exec du -k {} + 2>/dev/null \ - | awk '{n++; s+=$1} END {printf "%d files, %.2f GB\n", n, s/1048576}' - echo "=== debug info level in use: '${COCKATRICE_DEBUG_INFO_LEVEL}'" - - name: "Install sentry-cli" if: needs.configure.outputs.tag != null && matrix.make_package == '1' # Same as the Linux leg above: the install downloads from Sentry's registry, so a @@ -1123,11 +958,9 @@ jobs: retention-days: 1 # Delete used cache to emulate a ccache update. See https://github.com/actions/cache/issues/342 - - name: "Delete remote compiler cache (ccache)" - if: matrix.use_ccache == 1 && env.PERSIST_CCACHE == 'true' && github.ref == 'refs/heads/master' && steps.restore_ccache.outputs.cache-hit + - 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 continue-on-error: true - # Bash, not the Windows runner's default pwsh, which cannot parse this `if`. - shell: bash env: CACHE_PRIMARY_KEY: ${{ steps.restore_ccache.outputs.cache-primary-key }} GH_TOKEN: ${{ github.token }} @@ -1136,8 +969,8 @@ jobs: echo "Cache deleted successfully" fi - - name: "Cache updated compiler cache (ccache)" - if: matrix.use_ccache == 1 && env.PERSIST_CCACHE == 'true' && github.ref == 'refs/heads/master' + - name: "[macOS] Cache updated compiler cache (ccache)" + if: matrix.os == 'macOS' && matrix.use_ccache == 1 && env.PERSIST_CCACHE == 'true' && github.ref == 'refs/heads/master' uses: actions/cache/save@v6 with: key: ${{ steps.restore_ccache.outputs.cache-primary-key }} @@ -1263,6 +1096,7 @@ jobs: 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 @@ -1271,6 +1105,7 @@ jobs: 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 @@ -1281,6 +1116,12 @@ jobs: 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: @@ -1308,16 +1149,6 @@ jobs: # 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. - # - # codeql.yml's analyze job keeps its extracted database as one entry per analyzed commit, - # keyed codeql-trap----, about 1.07 GB each. Each run - # restores the newest entry by prefix and saves its own, so every run that analyzes a new - # commit makes the previous entry dead, and the action never deletes them: four had - # accumulated by 2026-11-07 (4.26 GB, 40% of the quota). Retiring them cannot wait for - # the age rule, because they are not the oldest entries and so not what least-recently- - # used eviction reaches: the buildkit layers are. Only the newest can be restored again, - # and a miss costs one re-extraction in a job that reports rather than gates, so keeping - # exactly that one is safe. - name: "Delete unusable cache entries" shell: bash env: @@ -1328,10 +1159,9 @@ jobs: 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-Windows10 ccache-macOS14 ccache-macOS15 ccache-macOS27' + # The nightly legs keep none, so their leftover entries are reclaimed here + # instead of waiting out the age rule. + LIVE_CCACHE_KEYS: 'ccache-Arch ccache-Servatrice_Debian12 ccache-Debian13 ccache-Fedora44 ccache-Ubuntu26.04 ccache-Ubuntu26.04sanitize ccache-macOS14-xcode-27 ccache-macOS15-xcode-27 ccache-macOS-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" @@ -1339,19 +1169,15 @@ jobs: cutoff="$(( $(date -u +%s) - 7 * 24 * 3600 ))" live=" ${LIVE_VCPKG_SCOPES} " live_ccache=" ${LIVE_CCACHE_KEYS} " - # The one trap cache a later run can still restore, by prefix. ISO-8601 sorts - # lexicographically, so this is a plain string compare. Entries scoped to another - # ref are deleted by the ref rule below whatever this says. - newest_trap="$(awk -F'\t' '$2 == "refs/heads/master" && $3 ~ /^codeql-trap-/ && $4 > m { m = $4; k = $3 } END { print k }' "$RUNNER_TEMP/caches.tsv")" 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, codeql-trap-* from codeql-action in codeql.yml), - # so that an entry of some other workflow can never be deleted by accident + # come from docker-release.yml), so that an entry of some other workflow can + # never be deleted by accident case "$key" in - vcpkg/* | ccache-* | Qt* | buildkit-* | index-* | codeql-trap-*) ;; + vcpkg/* | ccache-* | Qt* | buildkit-* | index-*) ;; *) continue ;; esac @@ -1359,9 +1185,8 @@ jobs: 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-Fedora45 is a prefix of ccache-Fedora45debug and - # ccache-Fedora45release, and only the latter two are live. + # An exact token compare: ccache-Ubuntu26.04 is a prefix of + # ccache-Ubuntu26.04sanitize, and both keys are live if [[ $live_ccache != *" $key "* ]]; then reason="no pull-request leg keeps it" fi @@ -1372,10 +1197,6 @@ jobs: if [[ $live != *" $scope "* ]]; then reason="no matrix leg uses $scope" fi - elif [[ $key == codeql-trap-* ]]; then - if [[ $key != "$newest_trap" ]]; then - reason="superseded by the newest trap cache" - fi fi if [[ -z $reason ]]; then stamp="$(date -u -d "$accessed" +%s 2>/dev/null || echo 0)" diff --git a/.github/workflows/desktop-lint.yml b/.github/workflows/desktop-lint.yml index f42f26e6a..93ba79464 100644 --- a/.github/workflows/desktop-lint.yml +++ b/.github/workflows/desktop-lint.yml @@ -12,7 +12,7 @@ on: - '!.ci/**' - '!.github/**' - '!.tx/**' - - '!docs/**' + - '!doc/**' - '.ci/lint_cpp.sh' - '.github/workflows/desktop-lint.yml' - '.clang-format' diff --git a/.github/workflows/documentation-build.yml b/.github/workflows/documentation-build.yml index b2f85e267..91ee14e3e 100644 --- a/.github/workflows/documentation-build.yml +++ b/.github/workflows/documentation-build.yml @@ -6,7 +6,7 @@ permissions: on: pull_request: paths: - - 'docs/**' + - 'doc/doxygen/**' - '.github/workflows/documentation-build.yml' - 'Doxyfile' release: @@ -71,7 +71,7 @@ jobs: destination_dir: docs # docs will be available at https://cockatrice.github.io/docs/ external_repository: Cockatrice/cockatrice.github.io publish_branch: master - publish_dir: ./docs-output/html + publish_dir: ./docs/html - name: Link to Documentation Page if: github.event_name == 'release' || github.event_name == 'workflow_dispatch' diff --git a/.gitignore b/.gitignore index ef88b1f58..54939bdcf 100644 --- a/.gitignore +++ b/.gitignore @@ -15,4 +15,4 @@ compile_commands.json .cache .gdb_history cockatrice/resources/config/qtlogging.ini -docs-output/ +docs/ diff --git a/.gitmodules b/.gitmodules index e6f115eb5..e69c8e686 100644 --- a/.gitmodules +++ b/.gitmodules @@ -2,7 +2,7 @@ path = vcpkg url = https://github.com/microsoft/vcpkg.git [submodule "doxygen-awesome-css"] - path = docs/doxygen/theme + path = doc/doxygen/theme url = https://github.com/jothepro/doxygen-awesome-css.git [submodule "external/qttesting"] path = external/qttesting diff --git a/CMakeLists.txt b/CMakeLists.txt index 71f195385..9044bc005 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,11 +1,11 @@ # Cockatrice's main CMakeLists.txt # -# This is basically a wrapper to enable/disable the compilation of the different projects: -# Cockatrice, Oracle, Servatrice, Test +# This is basically a wrapper to enable/disable the compilation +# of the different projects: servatrice, cockatrice, test # This file sets all the variables shared between the projects # like the installation path, compilation flags etc.. -# CMake 3.16 is required for Qt6 and target_precompile_headers() +# 3.16 required for Qt6 and target_precompile_headers() cmake_minimum_required(VERSION 3.16) # Use compiler cache (ccache) @@ -40,20 +40,24 @@ if(WITH_GUI_TEST AND NOT EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/external/qttesting/ endif() # Default to "Release" build type -# A user-provided CMAKE_BUILD_TYPE must be checked before the PROJECT() call -if(NOT CMAKE_BUILD_TYPE) +# User-provided value for CMAKE_BUILD_TYPE must be checked before the PROJECT() call +if(DEFINED CMAKE_BUILD_TYPE) + set(CMAKE_BUILD_TYPE + ${CMAKE_BUILD_TYPE} + CACHE STRING "Type of build" + ) +else() set(CMAKE_BUILD_TYPE Release - CACHE STRING "Build type" + CACHE STRING "Type of build" ) endif() -# ccache drives MSVC too (since 4.7), but only when the compiler writes debug -# information into the object file: /Zi and /ZI keep it in a shared PDB, and ccache -# reports them as unsupported rather than risk a stale one. The MSVC flags below -# therefore use /Z7. MSVC precompiled headers are cached as well, with the same -# sloppiness the other toolchains need. -if(USE_CCACHE) +# ccache does not support MSVC and must not auto-engage on Windows +# (it is installed unintentionally on the Windows CI runner). +# NOTE: this keys off the target OS, so a mingw/Ninja configuration on Windows +# also opts out of ccache even though the GNUCXX branch below supports it. +if(USE_CCACHE AND NOT WIN32) find_program(CCACHE_PROGRAM ccache) if(CCACHE_PROGRAM) # Support Unix Makefiles and Ninja @@ -67,10 +71,10 @@ if(USE_CCACHE) # 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}") - else() - # An explicit opt-in must not disappear silently. - message(STATUS "ccache requested but not found; building without a compiler cache") endif() +elseif(USE_CCACHE AND WIN32) + # An explicit opt-in must not disappear silently on Windows. + message(STATUS "ccache disabled: not supported for the MSVC toolchain on Windows") endif() if(WIN32 OR USE_VCPKG) @@ -104,21 +108,20 @@ endif() # Version can be overriden by git tags, see cmake/getversion.cmake project("Cockatrice" VERSION 3.2.0) -# Set release name if not provided via ENV/CMake var +# Set release name if not provided via env/cmake var if(NOT DEFINED GIT_TAG_RELEASENAME) set(GIT_TAG_RELEASENAME "Reality Check") endif() -# Use C++20 for all targets +# Use c++20 for all targets set(CMAKE_CXX_STANDARD 20 CACHE STRING "C++ ISO Standard" ) -set(CMAKE_CXX_STANDARD_REQUIRED ON) -set(CMAKE_CXX_EXTENSIONS OFF) +set(CMAKE_CXX_STANDARD_REQUIRED True) # Set conventional loops -set(CMAKE_ALLOW_LOOSE_LOOP_CONSTRUCTS ON) +set(CMAKE_ALLOW_LOOSE_LOOP_CONSTRUCTS true) # Search path for cmake modules set(COCKATRICE_CMAKE_PATH "${PROJECT_SOURCE_DIR}/cmake") @@ -131,31 +134,31 @@ include(createversionfile) # Define a proper install path if(UNIX) - if(APPLE) # macOS - # Due to the special bundle structure ignore the prefix eventually set by the user + if(APPLE) + # macOS + # Due to the special bundle structure ignore + # the prefix eventually set by the user. set(CMAKE_INSTALL_PREFIX ${CMAKE_BINARY_DIR}/release) - # Force ccache usage if requested and available - if(USE_CCACHE) - get_property(RULE_LAUNCH_COMPILE GLOBAL PROPERTY RULE_LAUNCH_COMPILE) - if(RULE_LAUNCH_COMPILE) - message(STATUS "Force enabling ccache usage under macOS") + # Force ccache usage if available + get_property(RULE_LAUNCH_COMPILE GLOBAL PROPERTY RULE_LAUNCH_COMPILE) + if(RULE_LAUNCH_COMPILE) + message(STATUS "Force enabling CCache usage under macOS") + # Set up wrapper scripts + configure_file("${COCKATRICE_CMAKE_PATH}/launch-c.in" launch-c) + configure_file("${COCKATRICE_CMAKE_PATH}/launch-cxx.in" launch-cxx) + execute_process(COMMAND chmod a+rx "${CMAKE_BINARY_DIR}/launch-c" "${CMAKE_BINARY_DIR}/launch-cxx") - # Set up wrapper scripts - configure_file("${COCKATRICE_CMAKE_PATH}/launch-c.in" launch-c) - configure_file("${COCKATRICE_CMAKE_PATH}/launch-cxx.in" launch-cxx) - execute_process(COMMAND chmod a+rx "${CMAKE_BINARY_DIR}/launch-c" "${CMAKE_BINARY_DIR}/launch-cxx") - - # Set Xcode project attributes to route compilation through our scripts - set(CMAKE_XCODE_ATTRIBUTE_CC "${CMAKE_BINARY_DIR}/launch-c") - set(CMAKE_XCODE_ATTRIBUTE_CXX "${CMAKE_BINARY_DIR}/launch-cxx") - set(CMAKE_XCODE_ATTRIBUTE_LD "${CMAKE_BINARY_DIR}/launch-c") - set(CMAKE_XCODE_ATTRIBUTE_LDPLUSPLUS "${CMAKE_BINARY_DIR}/launch-cxx") - endif() + # Set Xcode project attributes to route compilation through our scripts + set(CMAKE_XCODE_ATTRIBUTE_CC "${CMAKE_BINARY_DIR}/launch-c") + set(CMAKE_XCODE_ATTRIBUTE_CXX "${CMAKE_BINARY_DIR}/launch-cxx") + set(CMAKE_XCODE_ATTRIBUTE_LD "${CMAKE_BINARY_DIR}/launch-c") + set(CMAKE_XCODE_ATTRIBUTE_LDPLUSPLUS "${CMAKE_BINARY_DIR}/launch-cxx") endif() - else() # Linux / BSD + else() + # Linux / BSD if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) - # Fix package build + #fix package build if(PREFIX) set(CMAKE_INSTALL_PREFIX ${PREFIX}) else() @@ -163,68 +166,47 @@ if(UNIX) endif() endif() endif() -elseif(WIN32) # Windows (including 64-bit) +elseif(WIN32) set(CMAKE_INSTALL_PREFIX ${CMAKE_BINARY_DIR}/rundir/${CMAKE_BUILD_TYPE}) endif() -# Define compiler flags -if(MSVC) # Microsoft Visual C++ compiler - # /EHsc Enable standard C++ exception handling - # /MP Enable parallel compilation - # /permissive- Enable more standards-conforming behavior - # /utf-8 Set source file encoding and execution character set to UTF-8 - # /W4 Enable warning level 4 - # /Zc:__cplusplus Enable correct __cplusplus reporting - # /Z7 Generate debugging information in each object file (ccache-cacheable) - # instead of a compiler-shared PDB, which ccache rejects - set(CMAKE_CXX_FLAGS "/EHsc /MP /permissive- /utf-8 /W4 /Zc:__cplusplus /Z7") - # /O2 Balanced optimization - # /MD Link against the multi-threaded DLL runtime library (Release CRT) - set(CMAKE_CXX_FLAGS_RELEASE "/O2 /MD") - # /Od Disable optimization - # /MDd Link against the multi-threaded Debug DLL runtime library (Debug CRT) +# Define proper compilation flags +if(MSVC) + # Disable Warning C4251, C++20 compatibility, Multi-threaded Builds, Warn Detection, Unwind Semantics, Debug Symbols + set(CMAKE_CXX_FLAGS "/wd4251 /Zc:__cplusplus /std:c++20 /permissive- /W4 /MP /EHsc /Zi") + # Visual Studio: Maximum Optimization, Multi-threaded DLL + set(CMAKE_CXX_FLAGS_RELEASE "/Ox /MD") + # Visual Studio: No Optimization, Multi-threaded Debug DLL set(CMAKE_CXX_FLAGS_DEBUG "/Od /MDd") - # Generate PDBs, even in release builds, so developers can better analyze crash logs - # /DEBUG Enable debug symbols and PDB generation - # /OPT:REF Remove unreferenced functions/data - # /OPT:ICF Fold identical COMDAT functions/data + # Generate PDB, even when in release (So developers can better analyze crash logs) set(CMAKE_EXE_LINKER_FLAGS_RELEASE "${CMAKE_EXE_LINKER_FLAGS_RELEASE} /DEBUG /OPT:REF /OPT:ICF") add_compile_definitions(_SILENCE_STDEXT_ARR_ITERS_DEPRECATION_WARNING) -elseif(CMAKE_CXX_COMPILER_ID STREQUAL "GNU") # GCC compiler +elseif(CMAKE_COMPILER_IS_GNUCXX) + # linux/gcc, bsd/gcc, windows/mingw include(CheckCXXCompilerFlag) - # -O2 Balanced optimization - # -s Remove symbols from the executable - set(CMAKE_CXX_FLAGS_RELEASE "-O2 -s") - - # -ggdb Produce GDB debugging symbols - # -O0 Disable optimization - # -Wall Enable a broad set of useful warnings - # -Wextra Enable a set of extra warnings - # -Werror Treat warnings as compilation errors + set(CMAKE_CXX_FLAGS_RELEASE "-s -O2") if(WARNING_AS_ERROR) set(CMAKE_CXX_FLAGS_DEBUG "-ggdb -O0 -Wall -Wextra -Werror") else() set(CMAKE_CXX_FLAGS_DEBUG "-ggdb -O0 -Wall -Wextra") endif() - # -Wcast-align Catch unsafe pointer casts - # -Wmissing-declarations Catch defined functions without a prior declaration - # -Wno-error=extra Downgrade some -Wextra warnings from errors to warnings - # -Wno-error=delete-non-virtual-dtor Downgrade delete-non-virtual-dtor from error to warning - # -Wno-error=sign-compare Downgrade signed/unsigned comparison from error to warning - # -Wno-error=missing-declarations Downgrade -Wmissing-declarations from error to warning - # -Wno-error=sfinae-incomplete GCC 16+: Qt MOC + protobuf forward decls trigger this + if(APPLE) + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=gnu++20") + endif() + set(ADDITIONAL_DEBUG_FLAGS -Wcast-align -Wmissing-declarations + -Wno-long-long -Wno-error=extra -Wno-error=delete-non-virtual-dtor -Wno-error=sign-compare -Wno-error=missing-declarations - -Wno-error=sfinae-incomplete + -Wno-error=sfinae-incomplete # GCC 16+: Qt MOC + protobuf forward decls trigger this ) foreach(FLAG ${ADDITIONAL_DEBUG_FLAGS}) @@ -236,16 +218,9 @@ elseif(CMAKE_CXX_COMPILER_ID STREQUAL "GNU") # GCC compiler # Reduce compiler I/O by using pipes between stages instead of temp files set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -pipe") -elseif(CMAKE_CXX_COMPILER_ID MATCHES "Clang") # Clang and AppleClang compilers - # -O2 Balanced optimization +else() + # other: osx/llvm, bsd/llvm set(CMAKE_CXX_FLAGS_RELEASE "-O2") - - # -g Include debug information - # -O0 Disable optimization - # -Wall Enable a broad set of useful warnings - # -Wextra Enable a set of extra warnings - # -Werror Treat warnings as compilation errors - # -Wno-unused-parameter Suppress warnings about unused function parameters (common in Qt callbacks) if(WARNING_AS_ERROR) set(CMAKE_CXX_FLAGS_DEBUG "-g -O0 -Wall -Wextra -Werror -Wno-unused-parameter") else() @@ -254,8 +229,6 @@ elseif(CMAKE_CXX_COMPILER_ID MATCHES "Clang") # Clang and AppleClang compilers # Reduce compiler I/O by using pipes between stages instead of temp files set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -pipe") -else() # Unknown compiler - message(WARNING "Unknown C++ compiler: ${CMAKE_CXX_COMPILER_ID}") endif() # Always included, because they define cockatrice_disable_sanitizers_for() and @@ -265,37 +238,9 @@ 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) - # MSVC has no -g level, so /Z7 is either there or it is not. Level 0 is the only value - # that means something here: without /Z7 the objects carry no debug information, which is - # what a cache that has to fit a bound holds. Anything else is the full debug info. - if(COCKATRICE_DEBUG_INFO_LEVEL STREQUAL "0") - string(REPLACE " /Z7" "" CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}") - else() - message(WARNING "COCKATRICE_DEBUG_INFO_LEVEL is ignored on MSVC except 0") - endif() - 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 SFMT, so the RNG compiles without warnings +# 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_compile_definitions(SFMT_MEXP=19937) + add_definitions("-DSFMT_MEXP=19937") endif() find_package(Threads REQUIRED) @@ -348,15 +293,15 @@ if(${Protobuf_VERSION} VERSION_LESS "3.21.0.0" AND NOT EXISTS "${Protobuf_PROTOC message(FATAL_ERROR "No protoc command found!") endif() -# Find OpenSSL -if(WIN32) # Windows (including 64-bit) +#Find OpenSSL +if(WIN32) find_package(OpenSSL REQUIRED) if(OPENSSL_FOUND) include_directories(${OPENSSL_INCLUDE_DIRS}) endif() endif() -# Find Visual C++ Redistributable +#Find VCredist if(MSVC) find_package(VCredistRuntime) endif() @@ -569,10 +514,3 @@ if(Qt6_FOUND AND Qt6_VERSION_MINOR GREATER_EQUAL 3) # Qt6.3+ requires project finalization to support translations qt_finalize_project() endif() - -# Print compiler identification at configuration time -message(STATUS "C++ compiler: ${CMAKE_CXX_COMPILER_ID}") -message(STATUS "C++ compiler version: ${CMAKE_CXX_COMPILER_VERSION}") -message(STATUS "C++ compiler path: ${CMAKE_CXX_COMPILER}") -message(STATUS "C++ standard: ${CMAKE_CXX_STANDARD}") -message(STATUS "C++ extensions: ${CMAKE_CXX_EXTENSIONS}") diff --git a/Doxyfile b/Doxyfile index 0d508ca38..c12ff4a8a 100644 --- a/Doxyfile +++ b/Doxyfile @@ -74,7 +74,7 @@ PROJECT_ICON = cockatrice/resources/cockatrice.png # entered, it will be relative to the location where Doxygen was started. If # left blank the current directory will be used. -OUTPUT_DIRECTORY = docs-output +OUTPUT_DIRECTORY = docs # If the CREATE_SUBDIRS tag is set to YES then Doxygen will create up to 4096 # sub-directories (in 2 levels) under the output directory of each output format @@ -874,7 +874,7 @@ FILE_VERSION_FILTER = # DoxygenLayout.xml, Doxygen will parse it automatically even if the LAYOUT_FILE # tag is left empty. -LAYOUT_FILE = docs/doxygen/DoxygenLayout.xml +LAYOUT_FILE = doc/doxygen/DoxygenLayout.xml # The CITE_BIB_FILES tag can be used to specify one or more bib files containing # the reference definitions. This must be a list of .bib files. The .bib @@ -1091,8 +1091,8 @@ RECURSIVE = YES EXCLUDE = build/ \ cmake/ \ cmake-build-debug/ \ - docs/doxygen/theme/docs/ \ - docs/doxygen/theme/include/ \ + doc/doxygen/theme/docs/ \ + doc/doxygen/theme/include/ \ vcpkg/ # The EXCLUDE_SYMLINKS tag can be used to select whether or not files or @@ -1144,7 +1144,7 @@ EXAMPLE_RECURSIVE = NO # that contain images that are to be included in the documentation (see the # \image command). -IMAGE_PATH = docs/doxygen/images +IMAGE_PATH = doc/doxygen/images # The INPUT_FILTER tag can be used to specify a program that Doxygen should # invoke to filter for each input file. Doxygen will invoke the filter program @@ -1183,7 +1183,7 @@ INPUT_FILTER = # need to set EXTENSION_MAPPING for the extension otherwise the files are not # properly processed by Doxygen. -FILTER_PATTERNS = "*.proto=python docs/doxygen/filters/proto2cpp.py" +FILTER_PATTERNS = "*.proto=python doc/doxygen/filters/proto2cpp.py" # If the FILTER_SOURCE_FILES tag is set to YES, the input filter (if set using # INPUT_FILTER) will also be used to filter the input files that are used for @@ -1414,7 +1414,7 @@ HTML_FILE_EXTENSION = .html # of the possible markers and block names see the documentation. # This tag requires that the tag GENERATE_HTML is set to YES. -HTML_HEADER = docs/doxygen/html/header.html +HTML_HEADER = doc/doxygen/html/header.html # The HTML_FOOTER tag can be used to specify a user-defined HTML footer for each # generated HTML page. If the tag is left blank Doxygen will generate a standard @@ -1424,7 +1424,7 @@ HTML_HEADER = docs/doxygen/html/header.html # that Doxygen normally uses. # This tag requires that the tag GENERATE_HTML is set to YES. -HTML_FOOTER = docs/doxygen/html/footer.html +HTML_FOOTER = doc/doxygen/html/footer.html # The HTML_STYLESHEET tag can be used to specify a user-defined cascading style # sheet that is used by each HTML page. It can be used to fine-tune the look of @@ -1454,9 +1454,9 @@ HTML_STYLESHEET = # documentation. # This tag requires that the tag GENERATE_HTML is set to YES. -HTML_EXTRA_STYLESHEET = docs/doxygen/theme/doxygen-awesome.css \ - docs/doxygen/css/hide_nav_sync.css \ - docs/doxygen/css/cockatrice_docs_style.css +HTML_EXTRA_STYLESHEET = doc/doxygen/theme/doxygen-awesome.css \ + doc/doxygen/css/hide_nav_sync.css \ + doc/doxygen/css/cockatrice_docs_style.css # The HTML_EXTRA_FILES tag can be used to specify one or more extra images or # other source files which should be copied to the HTML output directory. Note @@ -1466,7 +1466,7 @@ HTML_EXTRA_STYLESHEET = docs/doxygen/theme/doxygen-awesome.css \ # files will be copied as-is; there are no commands or markers available. # This tag requires that the tag GENERATE_HTML is set to YES. -HTML_EXTRA_FILES = docs/doxygen/js/graph_toggle.js +HTML_EXTRA_FILES = doc/doxygen/js/graph_toggle.js # The HTML_COLORSTYLE tag can be used to specify if the generated HTML output # should be rendered with a dark or light theme. diff --git a/README.md b/README.md index 03f02e540..fa9272c0f 100644 --- a/README.md +++ b/README.md @@ -84,7 +84,7 @@ Join our [Discord community](https://discord.gg/3Z9yzmA) to connect with other p ### Code -To commit code to the project, please review our [Contribution Guidelines](https://github.com/Cockatrice/Cockatrice/blob/master/docs/CONTRIBUTING.md) first.
+To commit code to the project, please review our [Contribution Guidelines](https://github.com/Cockatrice/Cockatrice/blob/master/.github/CONTRIBUTING.md) first.
We maintain two tags for contributors to easier find issues to potentially work on: - [![Good first issue](https://img.shields.io/github/issues/cockatrice/cockatrice/Good%20First%20Issue)](https://github.com/Cockatrice/Cockatrice/issues?utf8=%E2%9C%93&q=is%3Aopen%20is%3Aissue%20label%3A%22Good%20first%20issue%22%20)
Issues tagged in this way provide a simple way to get started. They don't require much experience to be worked on. @@ -103,7 +103,7 @@ There are various places where useful information for different needs are mainta - [Official Wiki](https://github.com/Cockatrice/Cockatrice/wiki) `Community supported` - [Official Webpage](https://cockatrice.github.io/) - [Official README](https://github.com/Cockatrice/Cockatrice/blob/master/README.md) `This file` -- [Official CONTRIBUTING](https://github.com/Cockatrice/Cockatrice/blob/master/docs/CONTRIBUTING.md) +- [Official CONTRIBUTING](https://github.com/Cockatrice/Cockatrice/blob/master/.github/CONTRIBUTING.md) Cockatrice tries to use the [Google Developer Documentation Style Guide](https://developers.google.com/style/) to ensure consistent documentation. We encourage everybody to improve the documentation by suggesting edits based on this guide. @@ -163,7 +163,7 @@ The following flags (with their non-default values) can be passed to `cmake`: | `-DTEST=1` | Enable regression tests
**Note:** `make test` to run tests, *googletest* will be downloaded if not available | > [!TIP] -> Build code documentation ([Doxygen](https://www.doxygen.nl)) according to our [configuration](https://github.com/Cockatrice/Cockatrice/blob/master/Doxyfile) into `/docs-output`: +> Build code documentation ([Doxygen](https://www.doxygen.nl)) according to our [configuration](https://github.com/Cockatrice/Cockatrice/blob/master/Doxyfile) into `/docs`: > ```bash > doxygen Doxyfile > ``` diff --git a/cockatrice/CMakeLists.txt b/cockatrice/CMakeLists.txt index 984784161..b76783167 100644 --- a/cockatrice/CMakeLists.txt +++ b/cockatrice/CMakeLists.txt @@ -151,7 +151,6 @@ 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 @@ -253,8 +252,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 @@ -291,14 +288,9 @@ set(cockatrice_SOURCES src/interface/widgets/settings_page/storage_settings_page.cpp src/interface/widgets/settings_page/user_interface_settings_page.cpp src/interface/widgets/utility/custom_line_edit.cpp - src/interface/widgets/utility/epoch_format.h src/interface/widgets/utility/get_text_with_max.cpp - src/interface/widgets/utility/pill_painter.cpp - src/interface/widgets/utility/pill_painter.h src/interface/widgets/utility/report_utils.cpp src/interface/widgets/utility/report_utils.h - src/interface/widgets/utility/table_activation.cpp - src/interface/widgets/utility/table_activation.h src/interface/widgets/utility/sequence_edit.cpp src/interface/widgets/utility/visibility_change_listener.cpp src/interface/widgets/utility/visibility_change_listener.h @@ -386,12 +378,9 @@ set(cockatrice_SOURCES src/interface/widgets/tabs/api/edhrec/display/top_tags/edhrec_top_tags_api_response_display_widget.cpp src/interface/widgets/tabs/api/edhrec/tab_edhrec.cpp src/interface/widgets/tabs/api/edhrec/tab_edhrec_main.cpp - src/interface/widgets/tabs/alt_table_delegate.cpp - src/interface/widgets/tabs/report_status_delegate.cpp src/interface/widgets/tabs/tab.cpp src/interface/widgets/tabs/tab_account.cpp src/interface/widgets/tabs/tab_admin.cpp - src/interface/widgets/tabs/tab_alts.cpp src/interface/widgets/tabs/tab_card_art_rules.cpp src/interface/widgets/tabs/tab_deck_editor.cpp src/interface/widgets/tabs/tab_deck_storage.cpp 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/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/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/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/game_graphics/board/arrow_item.cpp b/cockatrice/src/game_graphics/board/arrow_item.cpp index b07a156bd..f7df98ab5 100644 --- a/cockatrice/src/game_graphics/board/arrow_item.cpp +++ b/cockatrice/src/game_graphics/board/arrow_item.cpp @@ -88,7 +88,7 @@ void ArrowItem::onTargetDestroyed() // Drag and attach arrows are never inserted into the arrow registry and // have no server-side counterpart, so no deletion event can clean them // up. Delete them locally when either endpoint is destroyed. - cancel(); + delArrow(); return; } @@ -103,12 +103,6 @@ void ArrowItem::delArrow() deleteLater(); } -void ArrowItem::cancel() -{ - ungrabMouse(); - delArrow(); -} - void ArrowItem::updatePath() { if (!targetItem) { @@ -356,17 +350,6 @@ void ArrowDragItem::addChildArrow(ArrowDragItem *child) childArrows.append(child); } -void ArrowDragItem::cancel() -{ - for (const auto &child : childArrows) { - if (child) { - child->cancel(); - } - } - childArrows.clear(); - ArrowItem::cancel(); -} - void ArrowDragItem::mouseMoveEvent(QGraphicsSceneMouseEvent *event) { if (targetLocked || !startItem) { @@ -410,10 +393,8 @@ void ArrowDragItem::mouseMoveEvent(QGraphicsSceneMouseEvent *event) targetItem ? updatePath() : updatePath(endPos); update(); - for (const auto &child : childArrows) { - if (child) { - child->mouseMoveEvent(event); - } + for (auto *child : childArrows) { + child->mouseMoveEvent(event); } } @@ -422,7 +403,10 @@ void ArrowDragItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) if (!startItem) { // The source card was destroyed while the arrow was being drawn. // Clean up the arrow and its children instead of leaking them. - cancel(); + delArrow(); + for (auto *child : childArrows) { + child->mouseReleaseEvent(event); + } return; } @@ -431,7 +415,7 @@ void ArrowDragItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) // For now, we can safely assume that the start item is always a card. // The target item can be a player as well. if (!startCard) { - cancel(); + delArrow(); return; } @@ -451,7 +435,7 @@ void ArrowDragItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) } else if (auto *targetPlayer = qgraphicsitem_cast(targetItem)) { arrow.targetPlayerId = targetPlayer->getOwner()->getPlayerInfo()->getId(); } else { - cancel(); + delArrow(); return; } @@ -464,12 +448,9 @@ void ArrowDragItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) } delArrow(); - for (const auto &child : childArrows) { - if (child) { - child->mouseReleaseEvent(event); - } + for (auto *child : childArrows) { + child->mouseReleaseEvent(event); } - childArrows.clear(); } // ArrowAttachItem @@ -490,17 +471,6 @@ void ArrowAttachItem::addChildArrow(ArrowAttachItem *child) childArrows.append(child); } -void ArrowAttachItem::cancel() -{ - for (const auto &child : childArrows) { - if (child) { - child->cancel(); - } - } - childArrows.clear(); - ArrowItem::cancel(); -} - void ArrowAttachItem::mouseMoveEvent(QGraphicsSceneMouseEvent *event) { if (targetLocked || !startItem) { @@ -539,10 +509,8 @@ void ArrowAttachItem::mouseMoveEvent(QGraphicsSceneMouseEvent *event) targetItem ? updatePath() : updatePath(endPos); update(); - for (const auto &child : childArrows) { - if (child) { - child->mouseMoveEvent(event); - } + for (auto *child : childArrows) { + child->mouseMoveEvent(event); } } @@ -551,16 +519,17 @@ void ArrowAttachItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) if (!startItem) { // The source card was destroyed while the arrow was being drawn. // Clean up the arrow and its children instead of leaking them. - cancel(); + delArrow(); + for (auto *child : childArrows) { + child->mouseReleaseEvent(event); + } return; } // Attaching could move startItem under the current cursor position, causing all children to retarget to it right // before they are processed. Prevent that. - for (const auto &child : childArrows) { - if (child) { - child->setTargetLocked(true); - } + for (auto *child : childArrows) { + child->setTargetLocked(true); } if (targetItem && targetItem != startItem) { @@ -572,10 +541,7 @@ void ArrowAttachItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) } delArrow(); - for (const auto &child : childArrows) { - if (child) { - child->mouseReleaseEvent(event); - } + for (auto *child : childArrows) { + child->mouseReleaseEvent(event); } - childArrows.clear(); } diff --git a/cockatrice/src/game_graphics/board/arrow_item.h b/cockatrice/src/game_graphics/board/arrow_item.h index c70a1030b..64a48cd14 100644 --- a/cockatrice/src/game_graphics/board/arrow_item.h +++ b/cockatrice/src/game_graphics/board/arrow_item.h @@ -70,8 +70,6 @@ public: void onTargetDestroyed(); void delArrow(); - /// Abandons an in-progress drag arrow, discarding any child arrows with it. - virtual void cancel(); void updatePath(); void updatePath(const QPointF &endPoint); void startDrawAnimation(); @@ -114,13 +112,12 @@ class ArrowDragItem : public ArrowItem private: PlayerLogic *player; int deleteInPhase; - QList> childArrows; + QList childArrows; QMetaObject::Connection positionConnection; public: ArrowDragItem(PlayerLogic *_owner, ArrowTarget *_startItem, const QColor &_color, int _deleteInPhase); void addChildArrow(ArrowDragItem *child); - void cancel() override; protected: void mouseMoveEvent(QGraphicsSceneMouseEvent *event) override; @@ -132,13 +129,12 @@ class ArrowAttachItem : public ArrowItem Q_OBJECT private: PlayerLogic *player; - QList> childArrows; + QList childArrows; QMetaObject::Connection positionConnection; public: explicit ArrowAttachItem(ArrowTarget *_startItem); void addChildArrow(ArrowAttachItem *child); - void cancel() override; protected: void mouseMoveEvent(QGraphicsSceneMouseEvent *event) override; diff --git a/cockatrice/src/game_graphics/board/card_item.cpp b/cockatrice/src/game_graphics/board/card_item.cpp index 02c0427d7..acd0764c9 100644 --- a/cockatrice/src/game_graphics/board/card_item.cpp +++ b/cockatrice/src/game_graphics/board/card_item.cpp @@ -294,13 +294,6 @@ void CardItem::drawArrow(const QColor &arrowColor) return; } - // A held shortcut repeats the action before the previous arrow is placed. - // That arrow has lost the mouse grab to the new one, so it would never - // receive the release that deletes it; discard it (and its children). - if (pendingArrow) { - pendingArrow->cancel(); - } - auto *game = owner->getGame(); PlayerLogic *arrowOwner = game->getPlayerManager()->getActiveLocalPlayer(game->getGameState()->getActivePlayer()); int phase = 0; // 0 means to not set the phase @@ -310,7 +303,6 @@ void CardItem::drawArrow(const QColor &arrowColor) } ArrowDragItem *arrow = new ArrowDragItem(arrowOwner, this, arrowColor, phase); scene()->addItem(arrow); - pendingArrow = arrow; arrow->grabMouse(); for (const auto &item : scene()->selectedItems()) { @@ -334,14 +326,8 @@ void CardItem::drawAttachArrow() return; } - // See drawArrow(): a repeated trigger must not orphan the previous arrow. - if (pendingArrow) { - pendingArrow->cancel(); - } - auto *arrow = new ArrowAttachItem(this); scene()->addItem(arrow); - pendingArrow = arrow; arrow->grabMouse(); for (const auto &item : scene()->selectedItems()) { diff --git a/cockatrice/src/game_graphics/board/card_item.h b/cockatrice/src/game_graphics/board/card_item.h index ae22d9493..01dacc021 100644 --- a/cockatrice/src/game_graphics/board/card_item.h +++ b/cockatrice/src/game_graphics/board/card_item.h @@ -13,13 +13,11 @@ #include #include #include -#include #include #include #include #include -class ArrowItem; class CardDragItem; class ServerInfo_Card; class PlayerLogic; @@ -36,7 +34,6 @@ class CardItem : public AbstractCardItem Q_OBJECT private: CardDragItem *dragItem; - QPointer pendingArrow; void prepareDelete(); void handleClickedToPlay(bool shiftHeld); diff --git a/cockatrice/src/game_graphics/log/message_log_widget.cpp b/cockatrice/src/game_graphics/log/message_log_widget.cpp index 9fe89c04b..8b351e022 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 @@ -64,13 +58,13 @@ MessageLogWidget::getFromStr(CardZoneLogic *zone, QString cardName, int position QString zoneName = zone->getName(); if (zoneName == ZoneNames::TABLE) { - fromStr = tr("from play"); + fromStr = tr(" from play"); } else if (zoneName == ZoneNames::GRAVE) { - fromStr = tr("from their graveyard"); + fromStr = tr(" from their graveyard"); } else if (zoneName == ZoneNames::EXILE) { - fromStr = tr("from exile"); + fromStr = tr(" from exile"); } else if (zoneName == ZoneNames::HAND) { - fromStr = tr("from their hand"); + fromStr = tr(" from their hand"); } else if (zoneName == ZoneNames::DECK) { if (position == 0) { if (cardName.isEmpty()) { @@ -82,9 +76,9 @@ MessageLogWidget::getFromStr(CardZoneLogic *zone, QString cardName, int position cardNameContainsStartZone = true; } else { if (ownerChange) { - fromStr = tr("from the top of %1's library").arg(zone->getPlayer()->getPlayerInfo()->getName()); + fromStr = tr(" from the top of %1's library").arg(zone->getPlayer()->getPlayerInfo()->getName()); } else { - fromStr = tr("from the top of their library"); + fromStr = tr(" from the top of their library"); } } } else if (position == zone->getCards().size()) { @@ -97,41 +91,35 @@ MessageLogWidget::getFromStr(CardZoneLogic *zone, QString cardName, int position cardNameContainsStartZone = true; } else { if (ownerChange) { - fromStr = tr("from the bottom of %1's library").arg(zone->getPlayer()->getPlayerInfo()->getName()); + fromStr = tr(" from the bottom of %1's library").arg(zone->getPlayer()->getPlayerInfo()->getName()); } else { - fromStr = tr("from the bottom of their library"); + fromStr = tr(" from the bottom of their library"); } } } else { if (ownerChange) { - fromStr = tr("from %1's library").arg(zone->getPlayer()->getPlayerInfo()->getName()); + fromStr = tr(" from %1's library").arg(zone->getPlayer()->getPlayerInfo()->getName()); } else { - fromStr = tr("from their library"); + fromStr = tr(" from their library"); } } } else if (zoneName == ZoneNames::SIDEBOARD) { - fromStr = tr("from sideboard"); + fromStr = tr(" from sideboard"); } else if (zoneName == ZoneNames::STACK) { - fromStr = tr("from the stack"); + fromStr = tr(" from the stack"); } else { - fromStr = tr("from custom zone '%1'").arg(zoneName); + fromStr = tr(" from custom zone '%1'").arg(zoneName); } if (!cardNameContainsStartZone) { cardName.clear(); } - // The separator space is added here rather than stored in the tr() string so - // translators never have to preserve an invisible leading blank. - if (!fromStr.isEmpty()) { - fromStr.prepend(' '); - } return {cardName, fromStr}; } void MessageLogWidget::containerProcessingDone() { currentContext = MessageContext_None; - contextJudgeName.clear(); messageSuffix = messagePrefix = QString(); } @@ -867,27 +855,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) @@ -936,6 +905,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..5632f5af8 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); diff --git a/cockatrice/src/game_graphics/player/menu/card_menu.cpp b/cockatrice/src/game_graphics/player/menu/card_menu.cpp index 4f3520814..9f00c0cba 100644 --- a/cockatrice/src/game_graphics/player/menu/card_menu.cpp +++ b/cockatrice/src/game_graphics/player/menu/card_menu.cpp @@ -123,7 +123,7 @@ CardMenu::CardMenu(PlayerGraphicsItem *_player, CardItem *_card, bool _shortcuts aRevealToAll = new QAction(this); - mCardCounters = new QMenu(this); + mCardCounters = new QMenu; // Card counters for (int i = 0; i < 6; ++i) { @@ -225,7 +225,7 @@ void CardMenu::createTableMenu(bool canModifyCard) } addSeparator(); addAction(aClone); - addMenu(new MoveMenu(player, this)); + addMenu(new MoveMenu(player)); addSeparator(); addAction(aAttach); if (card->getAttachedTo()) { @@ -233,7 +233,7 @@ void CardMenu::createTableMenu(bool canModifyCard) } addAction(aDrawArrow); addSeparator(); - addMenu(new PtMenu(player, this)); + addMenu(new PtMenu(player)); addAction(aSetAnnotation); addSeparator(); addAction(aReduceLifeByPower); @@ -275,7 +275,7 @@ void CardMenu::createStackMenu(bool canModifyCard) addAction(aPlayFacedown); addSeparator(); addAction(aClone); - addMenu(new MoveMenu(player, this)); + addMenu(new MoveMenu(player)); addSeparator(); addAction(aAttach); addAction(aDrawArrow); @@ -304,7 +304,7 @@ void CardMenu::createGraveyardOrExileMenu(bool canModifyCard) addAction(aPlayFacedown); addSeparator(); addAction(aClone); - addMenu(new MoveMenu(player, this)); + addMenu(new MoveMenu(player)); addSeparator(); addAction(aAttach); addAction(aDrawArrow); @@ -342,7 +342,7 @@ void CardMenu::createHandOrCustomZoneMenu(bool canModifyCard) addSeparator(); addAction(aClone); - addMenu(new MoveMenu(player, this)); + addMenu(new MoveMenu(player)); // actions that are really wonky when done from deck or sideboard if (card->getZone()->getName() == ZoneNames::HAND) { @@ -366,7 +366,7 @@ void CardMenu::createHandOrCustomZoneMenu(bool canModifyCard) void CardMenu::createZonelessMenu(bool canModifyCard) { if (canModifyCard) { - addMenu(new MoveMenu(player, this)); + addMenu(new MoveMenu(player)); } } @@ -415,7 +415,7 @@ void CardMenu::addRelatedCardView() } addSeparator(); - auto viewRelatedCards = new QMenu(tr("View related cards"), this); + auto viewRelatedCards = new QMenu(tr("View related cards")); addMenu(viewRelatedCards); for (const CardRelation *relatedCard : relatedCards) { QString relatedCardName = relatedCard->getName(); @@ -463,16 +463,16 @@ void CardMenu::addRelatedCardActions() relatedCardName = relatedCard.getName(); // "name" } - QString text; + QString text = tr("Token: "); if (cardRelation->getDoesAttach()) { - text = cardRelation->getDoesTransform() ? tr("Token: Transform into \"%1\"").arg(relatedCardName) - : tr("Token: Attach to \"%1\"").arg(relatedCardName); + text += + tr(cardRelation->getDoesTransform() ? "Transform into " : "Attach to ") + "\"" + relatedCardName + "\""; } else if (cardRelation->getIsVariable()) { - text = tr("Token: X %1").arg(relatedCardName); + text += "X " + relatedCardName; } else if (cardRelation->getDefaultCount() != 1) { - text = tr("Token: %1x %2").arg(cardRelation->getDefaultCount()).arg(relatedCardName); + text += QString::number(cardRelation->getDefaultCount()) + "x " + relatedCardName; } else { - text = tr("Token: %1").arg(relatedCardName); + text += relatedCardName; } if (createRelatedCards == nullptr) { @@ -587,4 +587,4 @@ void CardMenu::setShortcutsActive() aRemoveCounter[i]->setShortcuts(shortcuts.getShortcut("Player/aRC" + colorWords[i])); aSetCounter[i]->setShortcuts(shortcuts.getShortcut("Player/aSC" + colorWords[i])); } -} +} \ No newline at end of file diff --git a/cockatrice/src/game_graphics/player/menu/move_menu.cpp b/cockatrice/src/game_graphics/player/menu/move_menu.cpp index 24bfa3b0c..b81c4619b 100644 --- a/cockatrice/src/game_graphics/player/menu/move_menu.cpp +++ b/cockatrice/src/game_graphics/player/menu/move_menu.cpp @@ -10,7 +10,7 @@ #include #include -MoveMenu::MoveMenu(PlayerGraphicsItem *player, QWidget *parent) : QMenu(tr("Move to"), parent) +MoveMenu::MoveMenu(PlayerGraphicsItem *player) : QMenu(tr("Move to")) { aMoveToTopLibrary = new QAction(this); aMoveToTopLibrary->setData(cmMoveToTopLibrary); diff --git a/cockatrice/src/game_graphics/player/menu/move_menu.h b/cockatrice/src/game_graphics/player/menu/move_menu.h index a2cb304aa..f1f239abd 100644 --- a/cockatrice/src/game_graphics/player/menu/move_menu.h +++ b/cockatrice/src/game_graphics/player/menu/move_menu.h @@ -17,7 +17,7 @@ class MoveMenu : public QMenu Q_OBJECT public: - explicit MoveMenu(PlayerGraphicsItem *player, QWidget *parent); + explicit MoveMenu(PlayerGraphicsItem *player); void setShortcutsActive(); void retranslateUi(); diff --git a/cockatrice/src/game_graphics/player/menu/player_menu.cpp b/cockatrice/src/game_graphics/player/menu/player_menu.cpp index 27949356f..582586ac2 100644 --- a/cockatrice/src/game_graphics/player/menu/player_menu.cpp +++ b/cockatrice/src/game_graphics/player/menu/player_menu.cpp @@ -23,7 +23,6 @@ #include #include #include -#include #include #include #include @@ -71,11 +70,6 @@ PlayerMenu::PlayerMenu(PlayerGraphicsItem *_player) : QObject(_player), player(_ connect(&SettingsCache::instance().shortcuts(), &ShortcutsSettings::shortCutChanged, this, &PlayerMenu::refreshShortcuts); - - // Re-evaluate shortcuts when the game starts or ends, so the lobby only exposes the roll-die shortcut. - connect(player->getLogic()->getGame()->getGameMetaInfo(), &GameMetaInfo::startedChanged, this, - &PlayerMenu::refreshShortcuts); - refreshShortcuts(); retranslateUi(); @@ -150,12 +144,6 @@ void PlayerMenu::refreshShortcuts() void PlayerMenu::setShortcutsActive() { shortcutsActive = true; - - if (!isGameStarted()) { - applyLobbyShortcuts(); - return; - } - for (auto *c : managedComponents) { c->setShortcutsActive(); } @@ -168,24 +156,5 @@ void PlayerMenu::setShortcutsInactive() for (auto *c : managedComponents) { c->setShortcutsInactive(); } - emit shortcutsDeactivated(); -} - -bool PlayerMenu::isGameStarted() const -{ - return player->getLogic()->getGame()->getGameMetaInfo()->started(); -} - -void PlayerMenu::applyLobbyShortcuts() -{ - // Everything stays unbound until the game starts; only the roll-die shortcut is usable in the lobby. - for (auto *c : managedComponents) { - c->setShortcutsInactive(); - } - - if (utilityMenu) { - utilityMenu->setLobbyShortcutsActive(); - } - emit shortcutsDeactivated(); } \ No newline at end of file diff --git a/cockatrice/src/game_graphics/player/menu/player_menu.h b/cockatrice/src/game_graphics/player/menu/player_menu.h index 56de3c918..e21a689de 100644 --- a/cockatrice/src/game_graphics/player/menu/player_menu.h +++ b/cockatrice/src/game_graphics/player/menu/player_menu.h @@ -73,24 +73,17 @@ public: return utilityMenu; } - /** @brief Whether this player currently holds its shortcuts and the game has started. */ [[nodiscard]] bool getShortcutsActive() const { - return shortcutsActive && isGameStarted(); + return shortcutsActive; } - /** @brief Bind keyboard shortcuts. Only the roll-die shortcut is bound before the game starts. */ + /** @brief Bind keyboard shortcuts. Called when this player gains focus. */ void setShortcutsActive(); /** @brief Unbind keyboard shortcuts. Called when this player loses focus. */ void setShortcutsInactive(); private: - /** @brief True once the game has started; false while players are still in the deck-selection lobby. */ - [[nodiscard]] bool isGameStarted() const; - - /** @brief Unbinds everything except the shortcuts that remain usable before the game starts. */ - void applyLobbyShortcuts(); - PlayerGraphicsItem *player; TearOffMenu *playerMenu; QMenu *countersMenu; diff --git a/cockatrice/src/game_graphics/player/menu/pt_menu.cpp b/cockatrice/src/game_graphics/player/menu/pt_menu.cpp index 67e98bd76..e6c65fcfb 100644 --- a/cockatrice/src/game_graphics/player/menu/pt_menu.cpp +++ b/cockatrice/src/game_graphics/player/menu/pt_menu.cpp @@ -9,7 +9,7 @@ #include #include -PtMenu::PtMenu(PlayerGraphicsItem *player, QWidget *parent) : QMenu(tr("Power / toughness"), parent) +PtMenu::PtMenu(PlayerGraphicsItem *player) : QMenu(tr("Power / toughness")) { PlayerActions *playerActions = player->getLogic()->getPlayerActions(); diff --git a/cockatrice/src/game_graphics/player/menu/pt_menu.h b/cockatrice/src/game_graphics/player/menu/pt_menu.h index 987f033f8..aaf17cead 100644 --- a/cockatrice/src/game_graphics/player/menu/pt_menu.h +++ b/cockatrice/src/game_graphics/player/menu/pt_menu.h @@ -18,7 +18,7 @@ class PtMenu : public QMenu Q_OBJECT public: - explicit PtMenu(PlayerGraphicsItem *player, QWidget *parent); + explicit PtMenu(PlayerGraphicsItem *player); void retranslateUi(); void setShortcutsActive(); diff --git a/cockatrice/src/game_graphics/player/menu/utility_menu.cpp b/cockatrice/src/game_graphics/player/menu/utility_menu.cpp index 249fc57e6..3b8affff5 100644 --- a/cockatrice/src/game_graphics/player/menu/utility_menu.cpp +++ b/cockatrice/src/game_graphics/player/menu/utility_menu.cpp @@ -150,13 +150,4 @@ void UtilityMenu::setShortcutsInactive() aCreateAnotherToken->setShortcut(QKeySequence()); aIncrementAllCardCounters->setShortcut(QKeySequence()); } -} - -void UtilityMenu::setLobbyShortcutsActive() -{ - if (!player->getLogic()->getPlayerInfo()->getLocalOrJudge()) { - return; - } - - aRollDie->setShortcuts(SettingsCache::instance().shortcuts().getShortcut("Player/aRollDie")); } \ No newline at end of file diff --git a/cockatrice/src/game_graphics/player/menu/utility_menu.h b/cockatrice/src/game_graphics/player/menu/utility_menu.h index 625c7aa62..009e4aca3 100644 --- a/cockatrice/src/game_graphics/player/menu/utility_menu.h +++ b/cockatrice/src/game_graphics/player/menu/utility_menu.h @@ -32,9 +32,6 @@ public slots: public: explicit UtilityMenu(PlayerGraphicsItem *player, QMenu *playerMenu); - /** @brief Binds only the shortcuts that remain usable before the game starts. */ - void setLobbyShortcutsActive(); - [[nodiscard]] bool createAnotherTokenActionExists() const { return aCreateAnotherToken != nullptr; 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/zones/view_zone.cpp b/cockatrice/src/game_graphics/zones/view_zone.cpp index 1716fc24d..be2e471d4 100644 --- a/cockatrice/src/game_graphics/zones/view_zone.cpp +++ b/cockatrice/src/game_graphics/zones/view_zone.cpp @@ -1,27 +1,21 @@ #include "view_zone.h" #include "../../client/settings/cache_settings.h" -#include "../../interface/theme_manager.h" #include "../board/card_drag_item.h" #include "../board/card_item.h" #include "../card_dimensions.h" -#include "../z_values.h" #include "libcockatrice/card/printing/exact_card.h" #include "libcockatrice/filters/filter_string.h" #include "libcockatrice/protocol/pb/response.pb.h" #include "libcockatrice/protocol/pb/serverinfo_zone.pb.h" #include "libcockatrice/utility/card_ref.h" -#include #include #include #include -#include -#include #include #include #include -#include #include #include #include @@ -40,85 +34,12 @@ class QGraphicsItem; -namespace -{ -constexpr qreal MARKER_HORIZONTAL_PADDING = 4.0; -constexpr qreal MARKER_VERTICAL_PADDING = 2.0; -constexpr qreal MARKER_CORNER_RADIUS = 4.0; -constexpr qreal MARKER_INSET = 2.0; -constexpr int MARKER_FONT_SIZE = 9; -// Vertical pitch of a card column: each card shows this much before the next -// card overlaps it. Marker placement uses it to stay on the visible strip. -constexpr qreal CARD_STACK_OFFSET = CardDimensions::HEIGHT_F / 3.0; - -QFont markerFont() -{ - QFont font = themeManager != nullptr ? themeManager->appFont(AppFont::CardText) : QFont(); - font.setPixelSize(MARKER_FONT_SIZE); - font.setWeight(QFont::Bold); - return font; -} -} // namespace - -LibraryEdgeMarker::LibraryEdgeMarker(QGraphicsItem *parent) : QGraphicsItem(parent) -{ - setAcceptedMouseButtons(Qt::NoButton); - setAcceptHoverEvents(false); -} - -void LibraryEdgeMarker::setLabel(const QString &newLabel, const QString &toolTip) -{ - if (label == newLabel && this->toolTip() == toolTip) { - return; - } - - prepareGeometryChange(); - label = newLabel; - - const QFontMetrics metrics(markerFont()); - markerRect = QRectF(0, 0, metrics.horizontalAdvance(label) + 2 * MARKER_HORIZONTAL_PADDING, - metrics.height() + 2 * MARKER_VERTICAL_PADDING); - - setToolTip(toolTip); - update(); -} - -QRectF LibraryEdgeMarker::boundingRect() const -{ - return markerRect; -} - -void LibraryEdgeMarker::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*option*/, QWidget * /*widget*/) -{ - if (label.isEmpty()) { - return; - } - - painter->save(); - painter->setRenderHint(QPainter::Antialiasing, true); - painter->setFont(markerFont()); - - // ToolTipBase/ToolTipText are the palette's designed background/text pair - // for small overlays, so the chip keeps its contrast in every theme. - const QPalette palette = qApp->palette(); - painter->setPen(QPen(palette.color(QPalette::Mid), 1)); - painter->setBrush(palette.color(QPalette::ToolTipBase)); - painter->drawRoundedRect(markerRect.adjusted(0.5, 0.5, -0.5, -0.5), MARKER_CORNER_RADIUS, MARKER_CORNER_RADIUS); - - painter->setPen(palette.color(QPalette::ToolTipText)); - painter->drawText(markerRect, Qt::AlignCenter, label); - painter->restore(); -} - /** * @param parent the parent QGraphicsWidget containing the reveal zone */ ZoneViewZone::ZoneViewZone(ZoneViewZoneLogic *_logic, QGraphicsItem *parent) : SelectZone(_logic, parent), bRect(QRectF()), minRows(0), groupBy(CardList::NoSort), sortBy(CardList::NoSort) { - libraryEdgeMarker = new LibraryEdgeMarker(this); - libraryEdgeMarker->setVisible(false); - auto *viewLogic = qobject_cast(getLogic()); if (!(viewLogic->getRevealZone() && !viewLogic->getWriteableRevealZone())) { viewLogic->getOriginalZone()->getViews().append(viewLogic); @@ -262,8 +183,6 @@ void ZoneViewZone::reorganizeCards() gridSize = positionCardsForDisplay(cardsToDisplay); } - updateLibraryEdgeMarker(cardsToDisplay); - // determine bounding rect qreal aleft = 0; qreal atop = 0; @@ -313,7 +232,7 @@ ZoneViewZone::GridSize ZoneViewZone::positionCardsForDisplay(CardList &cards, Ca lastColumnProp = columnProp; qreal x = col * CardDimensions::WIDTH_F; - qreal y = row * CARD_STACK_OFFSET; + qreal y = row * CardDimensions::HEIGHT_F / 3; cItem->setPos(HORIZONTAL_PADDING + x, VERTICAL_PADDING + y); cItem->setRealZValue(i); longestRow = qMax(row, longestRow); @@ -342,7 +261,7 @@ ZoneViewZone::GridSize ZoneViewZone::positionCardsForDisplay(CardList &cards, Ca for (int i = 0; i < cardCount; i++) { auto *cItem = qobject_cast(cards.at(i)->parent()); qreal x = (i / rows) * CardDimensions::WIDTH_F; - qreal y = (i % rows) * CARD_STACK_OFFSET; + qreal y = (i % rows) * CardDimensions::HEIGHT_F / 3; cItem->setPos(HORIZONTAL_PADDING + x, VERTICAL_PADDING + y); cItem->setRealZValue(i); } @@ -351,75 +270,6 @@ ZoneViewZone::GridSize ZoneViewZone::positionCardsForDisplay(CardList &cards, Ca } } -void ZoneViewZone::updateLibraryEdgeMarker(const CardList &displayedCards) -{ - auto *viewLogic = qobject_cast(getLogic()); - if (viewLogic == nullptr || !viewLogic->isLibraryEdgeView() || displayedCards.isEmpty()) { - libraryEdgeMarkerAnchorPos = QPointF(); - libraryEdgeMarker->hide(); - return; - } - - const bool topEdge = viewLogic->showsTopOfLibrary(); - CardState *edgeState = topEdge ? displayedCards.first() : displayedCards.last(); - auto *edgeCard = edgeState == nullptr ? nullptr : qobject_cast(edgeState->parent()); - if (edgeCard == nullptr) { - libraryEdgeMarkerAnchorPos = QPointF(); - libraryEdgeMarker->hide(); - return; - } - - libraryEdgeMarkerAnchorPos = edgeCard->pos(); - applyLibraryEdgeMarkerText(); - positionLibraryEdgeMarker(); - - // Above the hovered-card layer: the chip sits inside its card, so hovering - // that card must not let the card repaint over the indicator. - libraryEdgeMarker->setZValue(ZValues::HOVERED_CARD + 0.5); - libraryEdgeMarker->show(); -} - -void ZoneViewZone::positionLibraryEdgeMarker() -{ - // The chip has to stay on the part of its card that is not covered by the - // next card in the column. The top card only shows its top strip, and its - // name is drawn top-left, so the chip goes to the right of that strip. The - // bottom card is fully visible, so the chip goes to its bottom-left, away - // from the bottom-right power/toughness. - const auto *viewLogic = qobject_cast(getLogic()); - const QRectF markerRect = libraryEdgeMarker->boundingRect(); - const bool topEdge = viewLogic != nullptr && viewLogic->showsTopOfLibrary(); - const QPointF offset = topEdge - ? QPointF(CardDimensions::WIDTH_F - markerRect.width() - MARKER_INSET, - CARD_STACK_OFFSET - markerRect.height() - MARKER_INSET) - : QPointF(MARKER_INSET, CardDimensions::HEIGHT_F - markerRect.height() - MARKER_INSET); - libraryEdgeMarker->setPos(libraryEdgeMarkerAnchorPos + offset); -} - -void ZoneViewZone::applyLibraryEdgeMarkerText() -{ - const auto *viewLogic = qobject_cast(getLogic()); - if (viewLogic == nullptr) { - return; - } - - const bool topEdge = viewLogic->showsTopOfLibrary(); - //: Short badge on the card at the top or bottom of a viewed library. The full phrase is in the tooltip. - libraryEdgeMarker->setLabel(topEdge ? tr("Top") : tr("Bottom"), - topEdge ? tr("This card is on top of the library") - : tr("This card is at the bottom of the library")); -} - -void ZoneViewZone::retranslateEdgeMarker() -{ - if (libraryEdgeMarker->isVisible()) { - // The translated label can be a different width, so reposition the chip - // to keep it inside its card. - applyLibraryEdgeMarkerText(); - positionLibraryEdgeMarker(); - } -} - void ZoneViewZone::setFilterString(const QString &_filterString) { const CardsDisplaySettings &cardsDisplay = SettingsCache::instance().cardsDisplay(); diff --git a/cockatrice/src/game_graphics/zones/view_zone.h b/cockatrice/src/game_graphics/zones/view_zone.h index 9593ceeb9..f91b24834 100644 --- a/cockatrice/src/game_graphics/zones/view_zone.h +++ b/cockatrice/src/game_graphics/zones/view_zone.h @@ -9,11 +9,9 @@ #include "select_zone.h" -#include #include #include #include -#include #include #include #include @@ -35,30 +33,6 @@ inline Q_LOGGING_CATEGORY(ViewZoneLog, "view_zone"); class Response; class ServerInfo_Card; class QGraphicsSceneWheelEvent; -class QPainter; -class QStyleOptionGraphicsItem; -class QWidget; - -/** - * A small chip drawn on the card at the edge of a viewed library. - * - * It is a sibling of the card items rather than part of the zone widget, so it - * stays attached to its card when the view reflows into several columns or is - * resized. ZoneViewZone owns and positions it. - */ -class LibraryEdgeMarker : public QGraphicsItem -{ -public: - explicit LibraryEdgeMarker(QGraphicsItem *parent = nullptr); - - void setLabel(const QString &label, const QString &toolTip); - [[nodiscard]] QRectF boundingRect() const override; - void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override; - -private: - QRectF markerRect; - QString label; -}; /** * A CardZone that is a view into another CardZone. @@ -82,8 +56,6 @@ private: FilterString filterString = FilterString(""); CardList::SortOption groupBy, sortBy; bool pileView; - LibraryEdgeMarker *libraryEdgeMarker; - QPointF libraryEdgeMarkerAnchorPos; struct GridSize { @@ -92,9 +64,6 @@ private: }; GridSize positionCardsForDisplay(CardList &cards, CardList::SortOption pileOption = CardList::NoSort); - void updateLibraryEdgeMarker(const CardList &displayedCards); - void applyLibraryEdgeMarkerText(); - void positionLibraryEdgeMarker(); void handleDropEvent(const QList &dragItems, CardZoneLogic *startZone, const QPoint &dropPoint) override; @@ -110,10 +79,6 @@ public: { return optimumRect; } - - /** @brief Re-applies the translated library edge marker text after a language change. */ - void retranslateEdgeMarker(); - public slots: void addToViews(); void removeFromViews(); diff --git a/cockatrice/src/game_graphics/zones/view_zone_widget.cpp b/cockatrice/src/game_graphics/zones/view_zone_widget.cpp index 2efa2fd13..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); }); @@ -294,7 +293,6 @@ void ZoneViewWidget::retranslateUi() shuffleCheckBox.setText(tr("shuffle when closing")); pileViewCheckBox.setText(tr("pile view")); - zone->retranslateEdgeMarker(); } void ZoneViewWidget::stopWindowDrag() diff --git a/cockatrice/src/interface/card_picture_loader/card_picture_loader_worker.cpp b/cockatrice/src/interface/card_picture_loader/card_picture_loader_worker.cpp index 54d2e5cf5..3ab5eb684 100644 --- a/cockatrice/src/interface/card_picture_loader/card_picture_loader_worker.cpp +++ b/cockatrice/src/interface/card_picture_loader/card_picture_loader_worker.cpp @@ -176,7 +176,7 @@ QNetworkReply *CardPictureLoaderWorker::makeRequest(const QUrl &url, CardPicture QNetworkRequest req(url); req.setHeader(QNetworkRequest::UserAgentHeader, QString("Cockatrice %1").arg(VERSION_STRING)); - req.setRawHeader("Accept", "image/avif,image/webp,image/apng,image/*,*/*;q=0.8"); + req.setRawHeader("Accept", "image/avif,image/webp,image/apng,image/,/*;q=0.8"); // Cached entries are served straight from the disk cache even when picture downloads are // enabled: re-fetching an already-cached image would burn the rate limit for nothing. Only a 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/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/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_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..30fde1595 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" @@ -248,25 +248,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); 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/dialogs/dlg_connect.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_connect.cpp index daee5f1f9..853a00828 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" @@ -38,7 +38,7 @@ DlgConnect::DlgConnect(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); @@ -46,7 +46,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); @@ -121,7 +121,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); diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_edit_tokens.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_edit_tokens.cpp index b06481c4b..3845029f7 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" @@ -108,10 +108,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_forgot_password_reset.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_forgot_password_reset.cpp index ca42fbf40..6c1b95b43 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_forgot_password_reset.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_forgot_password_reset.cpp @@ -56,7 +56,6 @@ DlgForgotPasswordReset::DlgForgotPasswordReset(QWidget *parent) : QDialog(parent playernameEdit->setMaxLength(MAX_NAME_LENGTH); playernameLabel->setBuddy(playernameEdit); - //: Code mailed to the player so they can reset their password. Not a game token. tokenLabel = new QLabel(tr("Token:")); tokenEdit = new QLineEdit(); tokenEdit->setMaxLength(MAX_NAME_LENGTH); diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_manage_sets.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_manage_sets.cpp index c6e640a9f..caca5da92 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" @@ -58,28 +58,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 +89,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); diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_my_reports.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_my_reports.cpp index 0f429677a..a7387a068 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_my_reports.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_my_reports.cpp @@ -230,8 +230,7 @@ void DlgMyReports::onSelectionChanged() connect(pend, &PendingCommand::finished, this, &DlgMyReports::reportDetailsResponse); client->sendCommand(pend); - // The cell text is the translated label; the raw status lives in UserRole. - const QString status = table->item(row, COL_STATUS)->data(Qt::UserRole).toString(); + QString status = table->item(row, COL_STATUS)->text(); bool canComment = (status == "open" || status == "assigned"); commentButton->setEnabled(canComment); commentInput->setEnabled(canComment); 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/settings_view.cpp b/cockatrice/src/interface/widgets/dialogs/settings_view.cpp index b1a80c86b..005824995 100644 --- a/cockatrice/src/interface/widgets/dialogs/settings_view.cpp +++ b/cockatrice/src/interface/widgets/dialogs/settings_view.cpp @@ -6,7 +6,7 @@ #include "settings_view.h" #include "../../../client/settings/cache_settings.h" -#include "../../theme_icon.h" +#include "../../pixel_map_generator.h" #include "../interface/theme_manager.h" #include "../settings_page/abstract_settings_page.h" #include "../settings_page/appearance_settings_page.h" @@ -135,7 +135,7 @@ 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"); diff --git a/cockatrice/src/interface/widgets/printing_selector/printing_selector.cpp b/cockatrice/src/interface/widgets/printing_selector/printing_selector.cpp index e21bf8a4e..b8438134c 100644 --- a/cockatrice/src/interface/widgets/printing_selector/printing_selector.cpp +++ b/cockatrice/src/interface/widgets/printing_selector/printing_selector.cpp @@ -21,14 +21,12 @@ #include #include #include -#include #include #include #include #include #include #include -#include #include #include #include @@ -56,9 +54,6 @@ PrintingSelector::PrintingSelector(QWidget *parent, AbstractTabDeckEditor *_deck setLayout(layout); widgetLoadingBufferTimer = new QTimer(this); - deckChangeTimer = new QTimer(this); - deckChangeTimer->setSingleShot(true); - connect(deckChangeTimer, &QTimer::timeout, this, &PrintingSelector::updateDisplay); flowWidget = new FlowWidget(this, Qt::Horizontal, Qt::ScrollBarAlwaysOff, Qt::ScrollBarAsNeeded); @@ -127,36 +122,11 @@ void PrintingSelector::retranslateUi() void PrintingSelector::printingsInDeckChanged() { if (SettingsCache::instance().cardsDisplay().getBumpSetsWithCardsInDeckToTop()) { - displayDirty = true; - if (isVisible()) { - // Coalesce deck changes while the model settles. - deckChangeTimer->start(100); - } else { - widgetLoadingBufferTimer->stop(); - } + // Delay the update to avoid race conditions + QTimer::singleShot(100, this, &PrintingSelector::updateDisplay); } } -void PrintingSelector::showEvent(QShowEvent *event) -{ - QWidget::showEvent(event); - if (displayDirty) { - updateDisplay(); - } -} - -void PrintingSelector::hideEvent(QHideEvent *event) -{ - // A partially built batch must be rebuilt on reveal, not resumed with - // stale printing data after the selected card or deck changes. - if (widgetLoadingBufferTimer->isActive() || deckChangeTimer->isActive()) { - widgetLoadingBufferTimer->stop(); - deckChangeTimer->stop(); - displayDirty = true; - } - QWidget::hideEvent(event); -} - /** * @return A map of uuid to amounts (main, side, tokens). */ @@ -203,8 +173,6 @@ void PrintingSelector::updateCardAmounts() */ void PrintingSelector::updateDisplay() { - deckChangeTimer->stop(); - displayDirty = false; widgetLoadingBufferTimer->stop(); widgetLoadingBufferTimer->deleteLater(); widgetLoadingBufferTimer = new QTimer(this); @@ -231,18 +199,20 @@ void PrintingSelector::updateDisplay() */ void PrintingSelector::setCard(const CardInfoPtr &newCard) { - // Same-name cards may be different database snapshots after a reload. - if (selectedCard == newCard) { + if (newCard.isNull()) { + selectedCard = newCard; + updateDisplay(); + return; + } + + // we don't need to redraw the widget if the card is the same + if (!selectedCard.isNull() && selectedCard->getName() == newCard->getName()) { return; } - widgetLoadingBufferTimer->stop(); - deckChangeTimer->stop(); selectedCard = newCard; if (isVisible()) { updateDisplay(); - } else { - displayDirty = true; } flowWidget->setMinimumSizeToMaxSizeHint(); flowWidget->scrollArea->verticalScrollBar()->setValue(0); diff --git a/cockatrice/src/interface/widgets/printing_selector/printing_selector.h b/cockatrice/src/interface/widgets/printing_selector/printing_selector.h index 7b4af95e0..b9cb27ecd 100644 --- a/cockatrice/src/interface/widgets/printing_selector/printing_selector.h +++ b/cockatrice/src/interface/widgets/printing_selector/printing_selector.h @@ -18,8 +18,6 @@ class QCheckBox; class QHBoxLayout; class QString; class QTimer; -class QShowEvent; -class QHideEvent; class QVBoxLayout; class SettingsButtonWidget; template class QMap; @@ -49,10 +47,6 @@ public: void setCard(const CardInfoPtr &newCard); void getAllSetsForCurrentCard(); -protected: - void showEvent(QShowEvent *event) override; - void hideEvent(QHideEvent *event) override; - public slots: void retranslateUi(); void updateDisplay(); @@ -94,8 +88,6 @@ private: DeckStateManager *deckStateManager; CardInfoPtr selectedCard; QTimer *widgetLoadingBufferTimer; - QTimer *deckChangeTimer; - bool displayDirty = false; int currentIndex = 0; }; 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/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.h b/cockatrice/src/interface/widgets/replay/replay_manager.h index e3745778e..34bae5f9a 100644 --- a/cockatrice/src/interface/widgets/replay/replay_manager.h +++ b/cockatrice/src/interface/widgets/replay/replay_manager.h @@ -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); diff --git a/cockatrice/src/interface/widgets/replay/replay_widget.cpp b/cockatrice/src/interface/widgets/replay/replay_widget.cpp index e911bc0f9..3f0bf35d9 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) diff --git a/cockatrice/src/interface/widgets/replay/replay_widget.h b/cockatrice/src/interface/widgets/replay/replay_widget.h index cbb676288..5d23b358d 100644 --- a/cockatrice/src/interface/widgets/replay/replay_widget.h +++ b/cockatrice/src/interface/widgets/replay/replay_widget.h @@ -38,11 +38,6 @@ public: [[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); diff --git a/cockatrice/src/interface/widgets/server/game_selector.cpp b/cockatrice/src/interface/widgets/server/game_selector.cpp index 9dc7bf1e8..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->exec(); + 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->exec(); + 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..8fed95833 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) { } @@ -193,9 +191,7 @@ QVariant GamesModel::data(const QModelIndex &index, int role) const 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_context_menu.cpp b/cockatrice/src/interface/widgets/server/user/user_context_menu.cpp index f61293013..447e63673 100644 --- a/cockatrice/src/interface/widgets/server/user/user_context_menu.cpp +++ b/cockatrice/src/interface/widgets/server/user/user_context_menu.cpp @@ -83,7 +83,6 @@ UserContextMenu::UserContextMenu(TabSupervisor *_tabSupervisor, QWidget *parent, aDemoteFromDeveloper = new QAction(QString(), this); aGetAdminNotes = new QAction(QString(), this); aInvestigateUser = new QAction(QString(), this); - aInvestigateAlts = new QAction(QString(), this); retranslateUi(); } @@ -112,7 +111,6 @@ void UserContextMenu::retranslateUi() aDemoteFromDeveloper->setText(tr("Demote user from de&veloper")); aGetAdminNotes->setText(tr("View admin notes")); aInvestigateUser->setText(tr("Investigate user")); - aInvestigateAlts->setText(tr("Investigate &alts")); } void UserContextMenu::gamesOfUserReceived(const Response &resp, const CommandContainer &commandContainer) @@ -456,7 +454,6 @@ void UserContextMenu::showContextMenu(const QPoint &pos, menu->addSeparator(); menu->addAction(aGetAdminNotes); menu->addAction(aInvestigateUser); - menu->addAction(aInvestigateAlts); menu->addSeparator(); if (userLevel.testFlag(ServerInfo_User::IsModerator) && @@ -502,7 +499,6 @@ void UserContextMenu::showContextMenu(const QPoint &pos, aBanHistory->setEnabled(anotherUser); aGetAdminNotes->setEnabled(anotherUser); aInvestigateUser->setEnabled(anotherUser); - aInvestigateAlts->setEnabled(anotherUser); aPromoteToMod->setEnabled(anotherUser); aDemoteFromMod->setEnabled(anotherUser); aPromoteToJudge->setEnabled(anotherUser); @@ -557,8 +553,6 @@ void UserContextMenu::showContextMenu(const QPoint &pos, execAdminNotes(userName); } else if (actionClicked == aInvestigateUser) { execInvestigateUser(userName); - } else if (actionClicked == aInvestigateAlts) { - execInvestigateAlts(userName); } else if (actionClicked == aCopyToClipBoard) { QClipboard *clipboard = QGuiApplication::clipboard(); clipboard->setText(deckHash); @@ -751,11 +745,6 @@ void UserContextMenu::execInvestigateUser(const QString &userName) tabSupervisor->openTabModeration(userName); } -void UserContextMenu::execInvestigateAlts(const QString &userName) -{ - tabSupervisor->openTabAlts(userName); -} - void UserContextMenu::execAdjustMod(const QString &userName, bool shouldBeMod) { Command_AdjustMod cmd; diff --git a/cockatrice/src/interface/widgets/server/user/user_context_menu.h b/cockatrice/src/interface/widgets/server/user/user_context_menu.h index 86be5eff2..2536819a4 100644 --- a/cockatrice/src/interface/widgets/server/user/user_context_menu.h +++ b/cockatrice/src/interface/widgets/server/user/user_context_menu.h @@ -54,7 +54,6 @@ private: QAction *aGetAdminNotes; std::function()> gameInviteLinkProvider; QAction *aInvestigateUser; - QAction *aInvestigateAlts; signals: void openMessageDialog(const QString &userName, bool focus); private slots: @@ -128,7 +127,6 @@ public: void execWarnHistory(const QString &userName); void execAdminNotes(const QString &userName); void execInvestigateUser(const QString &userName); - void execInvestigateAlts(const QString &userName); void execAdjustMod(const QString &userName, bool shouldBeMod); void execAdjustJudge(const QString &userName, bool shouldBeJudge); void execAdjustDeveloper(const QString &userName, bool shouldBeDeveloper); diff --git a/cockatrice/src/interface/widgets/settings_page/appearance_settings_page.cpp b/cockatrice/src/interface/widgets/settings_page/appearance_settings_page.cpp index 21c0a79e1..de5f67b32 100644 --- a/cockatrice/src/interface/widgets/settings_page/appearance_settings_page.cpp +++ b/cockatrice/src/interface/widgets/settings_page/appearance_settings_page.cpp @@ -146,7 +146,6 @@ AppearanceSettingsPage::AppearanceSettingsPage() }); homeTabBackgroundShuffleFrequencySpinBox.setRange(0, 3600); - //: Unit following a number of seconds. Keep the leading space unless your language writes number and unit flush. homeTabBackgroundShuffleFrequencySpinBox.setSuffix(tr(" seconds")); homeTabBackgroundShuffleFrequencySpinBox.setValue(settings.appearance().getHomeTabBackgroundShuffleFrequency()); connect(&homeTabBackgroundShuffleFrequencySpinBox, qOverload(&QSpinBox::valueChanged), &settings.appearance(), @@ -676,7 +675,7 @@ void AppearanceSettingsPage::retranslateUi() } handGroupBox->setTitle(tr("Hand layout")); - horizontalHandCheckBox.setText(tr("Display hand horizontally (requires more space)")); + horizontalHandCheckBox.setText(tr("Display hand horizontally (wastes space)")); leftJustifiedHandCheckBox.setText(tr("Enable left justification")); tableGroupBox->setTitle(tr("Table grid layout")); 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..79370f512 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,7 +1,7 @@ #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" @@ -80,19 +80,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; 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 274e55866..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" @@ -88,21 +88,18 @@ MessagesSettingsPage::MessagesSettingsPage() auto *chatGrid = new QGridLayout; chatGrid->addWidget(&chatMentionCheckBox, 0, 0); - chatGrid->addWidget(&mentionColorStringLabel, 0, 1, Qt::AlignRight); + chatGrid->addWidget(&invertMentionForeground, 0, 1); chatGrid->addWidget(mentionColor, 0, 2); - chatGrid->addWidget(&invertMentionForeground, 0, 3); - chatGrid->addWidget(&chatMentionCompleterCheckbox, 1, 0, 1, -1); - chatGrid->addWidget(&ignoreUnregUsersMainChat, 2, 0, 1, -1); - chatGrid->addWidget(&ignoreUnregUserMessages, 3, 0, 1, -1); - chatGrid->addWidget(&ignoreNonBuddyUserMessages, 4, 0, 1, -1); - chatGrid->addWidget(&messagePopups, 5, 0, 1, -1); - chatGrid->addWidget(&mentionPopups, 6, 0, 1, -1); - chatGrid->addWidget(&roomHistory, 7, 0, 1, -1); - chatGrid->addWidget(&ignoreAllPrivateMessagesCheckBox, 8, 0, 1, -1); - chatGrid->addWidget(&useGameTimeCheckBox, 9, 0, 1, -1); - // Expand the first column more than the following ones - chatGrid->setColumnStretch(0, 3); - chatGrid->setColumnStretch(1, 1); + chatGrid->addWidget(&chatMentionCompleterCheckbox, 1, 0); + chatGrid->addWidget(&ignoreUnregUsersMainChat, 2, 0); + chatGrid->addWidget(&hexLabel, 1, 2); + chatGrid->addWidget(&ignoreUnregUserMessages, 3, 0); + chatGrid->addWidget(&ignoreNonBuddyUserMessages, 4, 0); + chatGrid->addWidget(&messagePopups, 5, 0); + chatGrid->addWidget(&mentionPopups, 6, 0); + chatGrid->addWidget(&roomHistory, 7, 0); + chatGrid->addWidget(&ignoreAllPrivateMessagesCheckBox, 8, 0); + chatGrid->addWidget(&useGameTimeCheckBox, 9, 0); chatGroupBox = new QGroupBox; chatGroupBox->setLayout(chatGrid); @@ -112,14 +109,11 @@ MessagesSettingsPage::MessagesSettingsPage() connect(highlightColor, &QLineEdit::textChanged, this, &MessagesSettingsPage::updateHighlightColor); auto *highlightNotice = new QGridLayout; - highlightNotice->addWidget(customAlertString, 0, 0); - highlightNotice->addWidget(&highlightColorStringLabel, 0, 1, Qt::AlignRight); highlightNotice->addWidget(highlightColor, 0, 2); - highlightNotice->addWidget(&invertHighlightForeground, 0, 3); - highlightNotice->addWidget(&customAlertStringLabel, 1, 0, 1, -1); - // Expand the first column more than the following ones - highlightNotice->setColumnStretch(0, 3); - highlightNotice->setColumnStretch(1, 1); + highlightNotice->addWidget(&invertHighlightForeground, 0, 1); + highlightNotice->addWidget(&hexHighlightLabel, 1, 2); + highlightNotice->addWidget(customAlertString, 0, 0); + highlightNotice->addWidget(&customAlertStringLabel, 1, 0); customAlertStringLabel.setBuddy(customAlertString); highlightGroupBox = new QGroupBox; highlightGroupBox->setLayout(highlightNotice); @@ -137,8 +131,10 @@ MessagesSettingsPage::MessagesSettingsPage() auto *channelGrid = new QGridLayout; channelGrid->addWidget(&channelUnreadColorLabel, 0, 0); channelGrid->addWidget(channelUnreadColor, 0, 1); + channelGrid->addWidget(&hexChannelUnreadLabel, 0, 2); channelGrid->addWidget(&channelMentionColorLabel, 1, 0); channelGrid->addWidget(channelMentionColor, 1, 1); + channelGrid->addWidget(&hexChannelMentionLabel, 1, 2); channelGroupBox = new QGroupBox; channelGroupBox->setLayout(channelGrid); @@ -150,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; @@ -335,8 +331,6 @@ void MessagesSettingsPage::retranslateUi() channelGroupBox->setTitle(tr("Room channel tabs")); channelUnreadColorLabel.setText(tr("Unread messages color")); channelMentionColorLabel.setText(tr("Mentioned messages color")); - channelUnreadColor->setPlaceholderText(tr("Enter HEX code")); - channelMentionColor->setPlaceholderText(tr("Enter HEX code")); chatMentionCheckBox.setText(tr("Enable chat mentions")); chatMentionCompleterCheckbox.setText(tr("Enable mention completer")); messageGroupBox->setTitle(tr("In-game message macros")); @@ -347,15 +341,15 @@ void MessagesSettingsPage::retranslateUi() ignoreNonBuddyUserMessages.setText(tr("Ignore private messages sent by non-buddy users")); invertMentionForeground.setText(tr("Invert text color")); invertHighlightForeground.setText(tr("Invert text color")); - mentionColorStringLabel.setText(tr("Color")); - highlightColorStringLabel.setText(tr("Color")); - mentionColor->setPlaceholderText(tr("Enter HEX code")); - highlightColor->setPlaceholderText(tr("Enter HEX code")); messagePopups.setText(tr("Enable desktop notifications for private messages")); mentionPopups.setText(tr("Enable desktop notification for mentions")); roomHistory.setText(tr("Enable room message history on join")); ignoreAllPrivateMessagesCheckBox.setText(tr("Ignore all private messages")); useGameTimeCheckBox.setText(tr("Use game time instead of local time in game logs")); + hexLabel.setText(tr("(Color is hexadecimal)")); + hexHighlightLabel.setText(tr("(Color is hexadecimal)")); + hexChannelUnreadLabel.setText(tr("(Color is hexadecimal)")); + hexChannelMentionLabel.setText(tr("(Color is hexadecimal)")); customAlertStringLabel.setText(tr("Separate words with a space, alphanumeric characters only")); customAlertString->setPlaceholderText(tr("Word1 Word2 Word3")); aAdd->setText(tr("Add New Message")); diff --git a/cockatrice/src/interface/widgets/settings_page/messages_settings_page.h b/cockatrice/src/interface/widgets/settings_page/messages_settings_page.h index 06fabcef2..112ff4e29 100644 --- a/cockatrice/src/interface/widgets/settings_page/messages_settings_page.h +++ b/cockatrice/src/interface/widgets/settings_page/messages_settings_page.h @@ -58,11 +58,13 @@ private: QLineEdit *channelUnreadColor; QLineEdit *channelMentionColor; QLineEdit *customAlertString; + QLabel hexLabel; + QLabel hexHighlightLabel; QLabel channelUnreadColorLabel; QLabel channelMentionColorLabel; + QLabel hexChannelUnreadLabel; + QLabel hexChannelMentionLabel; QLabel customAlertStringLabel; - QLabel mentionColorStringLabel; - QLabel highlightColorStringLabel; QLabel explainMessagesLabel; void storeSettings(); 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/alt_table_delegate.cpp b/cockatrice/src/interface/widgets/tabs/alt_table_delegate.cpp deleted file mode 100644 index 9fb6a7ffe..000000000 --- a/cockatrice/src/interface/widgets/tabs/alt_table_delegate.cpp +++ /dev/null @@ -1,109 +0,0 @@ -#include "alt_table_delegate.h" - -#include "../utility/pill_painter.h" - -#include -#include -#include -#include -#include -#include - -namespace -{ -constexpr int ICON_TEXT_GAP = 4; -} // namespace - -AltTableDelegate::AltTableDelegate(int viaColumn, - int warnsColumn, - int bansColumn, - int sharedColumn, - int activeColumn, - QObject *parent) - : QStyledItemDelegate(parent), viaColumn(viaColumn), warnsColumn(warnsColumn), bansColumn(bansColumn), - sharedColumn(sharedColumn), activeColumn(activeColumn) -{ -} - -void AltTableDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const -{ - const bool selected = (option.state & QStyle::State_Selected) != 0; - - QStyleOptionViewItem opt(option); - initStyleOption(&opt, index); - painter->setFont(opt.font); - QStyle *style = opt.widget != nullptr ? opt.widget->style() : QApplication::style(); - const QString text = index.data(Qt::DisplayRole).toString(); - const QPalette::ColorRole emphasisRole = selected ? QPalette::HighlightedText : QPalette::WindowText; - - if (index.column() == viaColumn) { - bool sharedOk = false; - const unsigned shared = index.sibling(index.row(), sharedColumn).data(Qt::DisplayRole).toUInt(&sharedOk); - const bool ipMatch = text.startsWith(QStringLiteral("ip:")); - // shared_count for an IP match already includes the investigated account, - // so anything above 2 means the address is shared beyond that pair. - const bool dimmed = sharedOk && ipMatch && shared > 2; - const QPalette::ColorRole textRole = (dimmed && !selected) ? QPalette::PlaceholderText : emphasisRole; - - QPalette::ColorRole tintRole = QPalette::NoRole; - if (text == QStringLiteral("email")) { - tintRole = QPalette::Link; - } else if (text.startsWith(QStringLiteral("clientid:"))) { - tintRole = QPalette::Highlight; - } else if (ipMatch) { - tintRole = QPalette::PlaceholderText; - } - if (tintRole != QPalette::NoRole) { - if (!pill_painter::paint(painter, opt, tintRole, textRole, text)) { - QStyledItemDelegate::paint(painter, opt, index); - } - return; - } - QStyledItemDelegate::paint(painter, opt, index); - return; - } - - if (index.column() == warnsColumn || index.column() == bansColumn) { - bool countOk = false; - const int count = text.toInt(&countOk); - if (countOk && count > 0 && !selected) { - const QIcon icon = style->standardIcon(index.column() == bansColumn ? QStyle::SP_MessageBoxCritical - : QStyle::SP_MessageBoxWarning); - const int iconSize = qMax(0, opt.rect.height() - 2 * ICON_TEXT_GAP); - QRect textRect = opt.rect; - if (!icon.isNull() && iconSize > 0) { - const QRect iconRect(opt.rect.left() + ICON_TEXT_GAP, opt.rect.top() + ICON_TEXT_GAP, iconSize, - iconSize); - icon.paint(painter, iconRect); - textRect.setLeft(iconRect.right() + ICON_TEXT_GAP); - } - const QString elided = opt.fontMetrics.elidedText(text, opt.textElideMode, textRect.width()); - style->drawItemText(painter, textRect, Qt::AlignLeft | Qt::AlignVCenter, opt.palette, true, elided, - emphasisRole); - return; - } - } - - if (index.column() == activeColumn) { - const bool active = index.data(Qt::UserRole).toBool(); - const QPalette::ColorRole tintRole = active ? QPalette::Highlight : QPalette::PlaceholderText; - if (!pill_painter::paint(painter, opt, tintRole, emphasisRole, text)) { - QStyledItemDelegate::paint(painter, opt, index); - } - return; - } - - QStyledItemDelegate::paint(painter, opt, index); -} - -QSize AltTableDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const -{ - const QSize hint = QStyledItemDelegate::sizeHint(option, index); - if (index.column() == viaColumn || index.column() == activeColumn) { - return hint + QSize(pill_painter::PAD_X + 2 * ICON_TEXT_GAP, pill_painter::PAD_Y); - } - if (index.column() == warnsColumn || index.column() == bansColumn) { - return hint + QSize(2 * ICON_TEXT_GAP, 0); - } - return hint; -} diff --git a/cockatrice/src/interface/widgets/tabs/alt_table_delegate.h b/cockatrice/src/interface/widgets/tabs/alt_table_delegate.h deleted file mode 100644 index 6cd5c7fad..000000000 --- a/cockatrice/src/interface/widgets/tabs/alt_table_delegate.h +++ /dev/null @@ -1,37 +0,0 @@ -#ifndef ALT_TABLE_DELEGATE_H -#define ALT_TABLE_DELEGATE_H - -#include -#include - -/** - * Badge + icon painting for the User Alts table. The match-evidence column - * renders as a palette-tinted pill per match type (email / client id / IP), and - * an IP shared by more than one other account is dimmed. Warn/ban counts gain - * the style's warning/critical icons when nonzero. The active column renders as - * a palette-tinted pill (Highlight when active, PlaceholderText when inactive, - * from Qt::UserRole). Selected rows and unknown content fall back to default - * painting. - */ -class AltTableDelegate : public QStyledItemDelegate -{ - Q_OBJECT -public: - explicit AltTableDelegate(int viaColumn, - int warnsColumn, - int bansColumn, - int sharedColumn, - int activeColumn, - QObject *parent = nullptr); - void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override; - [[nodiscard]] QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override; - -private: - int viaColumn; - int warnsColumn; - int bansColumn; - int sharedColumn; - int activeColumn; -}; - -#endif // ALT_TABLE_DELEGATE_H 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..9f7369435 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" @@ -282,7 +282,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); 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..9adda008e 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" @@ -49,7 +49,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); diff --git a/cockatrice/src/interface/widgets/tabs/api/edhrec/tab_edhrec_main.h b/cockatrice/src/interface/widgets/tabs/api/edhrec/tab_edhrec_main.h index ce0861ef7..5d0245ed4 100644 --- a/cockatrice/src/interface/widgets/tabs/api/edhrec/tab_edhrec_main.h +++ b/cockatrice/src/interface/widgets/tabs/api/edhrec/tab_edhrec_main.h @@ -38,8 +38,7 @@ public: [[nodiscard]] QString getTabText() const override { auto cardName = cardToQuery.isNull() ? QString() : cardToQuery->getName(); - //: EDHRec is a website name, keep it as written. %1 is the card being looked up. - return tr("EDHRec: %1").arg(cardName); + return tr("EDHRec: ") + cardName; } CardSizeWidget *getCardSizeSlider() const diff --git a/cockatrice/src/interface/widgets/tabs/report_status_delegate.cpp b/cockatrice/src/interface/widgets/tabs/report_status_delegate.cpp deleted file mode 100644 index 361e89605..000000000 --- a/cockatrice/src/interface/widgets/tabs/report_status_delegate.cpp +++ /dev/null @@ -1,54 +0,0 @@ -#include "report_status_delegate.h" - -#include "../utility/pill_painter.h" - -#include -#include -#include -#include -#include - -ReportStatusDelegate::ReportStatusDelegate(QObject *parent) : QStyledItemDelegate(parent) -{ -} - -void ReportStatusDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const -{ - const bool selected = (option.state & QStyle::State_Selected) != 0; - QString status = index.data(Qt::UserRole).toString(); - if (status.isEmpty()) { - status = index.data(Qt::DisplayRole).toString(); - } - - QPalette::ColorRole tintRole = QPalette::NoRole; - if (status == QStringLiteral("open")) { - tintRole = QPalette::Highlight; - } else if (status == QStringLiteral("assigned")) { - tintRole = QPalette::Link; - } else if (status == QStringLiteral("resolved") || status == QStringLiteral("dismissed")) { - tintRole = QPalette::PlaceholderText; - } - - QStyleOptionViewItem opt(option); - initStyleOption(&opt, index); - painter->setFont(opt.font); - - if (tintRole == QPalette::NoRole) { - const QPalette::ColorRole textRole = selected ? QPalette::HighlightedText : QPalette::PlaceholderText; - QStyle *fallbackStyle = opt.widget != nullptr ? opt.widget->style() : QApplication::style(); - fallbackStyle->drawItemText(painter, opt.rect, Qt::AlignLeft | Qt::AlignVCenter, opt.palette, true, opt.text, - textRole); - return; - } - - const QPalette::ColorRole textRole = selected ? QPalette::HighlightedText : QPalette::WindowText; - if (!pill_painter::paint(painter, opt, tintRole, textRole, opt.text.isEmpty() ? status : opt.text)) { - QStyledItemDelegate::paint(painter, opt, index); - } -} - -QSize ReportStatusDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const -{ - const QSize hint = QStyledItemDelegate::sizeHint(option, index); - return hint + QSize(pill_painter::PAD_X, pill_painter::PAD_Y); -} diff --git a/cockatrice/src/interface/widgets/tabs/report_status_delegate.h b/cockatrice/src/interface/widgets/tabs/report_status_delegate.h deleted file mode 100644 index f01f6d3e3..000000000 --- a/cockatrice/src/interface/widgets/tabs/report_status_delegate.h +++ /dev/null @@ -1,22 +0,0 @@ -#ifndef REPORT_STATUS_DELEGATE_H -#define REPORT_STATUS_DELEGATE_H - -#include -#include - -/** - * Status pill for the report queue table. Paints the status text on a - * translucent badge tinted with a palette role per status (open, assigned, - * resolved, dismissed), so the colors always follow the active theme. - * Falls back to default painting for selected rows and unknown statuses. - */ -class ReportStatusDelegate : public QStyledItemDelegate -{ - Q_OBJECT -public: - explicit ReportStatusDelegate(QObject *parent = nullptr); - void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override; - [[nodiscard]] QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override; -}; - -#endif // REPORT_STATUS_DELEGATE_H diff --git a/cockatrice/src/interface/widgets/tabs/tab_alts.cpp b/cockatrice/src/interface/widgets/tabs/tab_alts.cpp deleted file mode 100644 index d2cec3855..000000000 --- a/cockatrice/src/interface/widgets/tabs/tab_alts.cpp +++ /dev/null @@ -1,699 +0,0 @@ -#include "tab_alts.h" - -#include "../utility/epoch_format.h" -#include "../utility/table_activation.h" -#include "abstract_client.h" -#include "alt_table_delegate.h" -#include "libcockatrice/protocol/pb/response.pb.h" -#include "libcockatrice/protocol/pb/response_user_alts.pb.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 - -namespace -{ -constexpr int COL_ALTS_USER = 0; -constexpr int COL_ALTS_VIA = 1; -constexpr int COL_ALTS_DEPTH = 2; -constexpr int COL_ALTS_SHARED = 3; -constexpr int COL_ALTS_IPS = 4; -constexpr int COL_ALTS_EMAIL = 5; -constexpr int COL_ALTS_CLIENTID = 6; -constexpr int COL_ALTS_REGISTERED = 7; -constexpr int COL_ALTS_LAST_LOGIN = 8; -constexpr int COL_ALTS_WARNS = 9; -constexpr int COL_ALTS_BANS = 10; -constexpr int COL_ALTS_ACTIVE = 11; -constexpr int COL_ALTS_COUNT = 12; - -constexpr int COL_WARNS_TIME = 0; -constexpr int COL_WARNS_MODERATOR = 1; -constexpr int COL_WARNS_REASON = 2; -constexpr int COL_WARNS_COUNT = 3; - -constexpr int COL_BANS_TIME = 0; -constexpr int COL_BANS_MODERATOR = 1; -constexpr int COL_BANS_LENGTH = 2; -constexpr int COL_BANS_REASON = 3; -constexpr int COL_BANS_COUNT = 4; - -constexpr int MAX_ALT_DEPTH = 3; -// Starting default only (adjustable 1-100000): shared VPN exits routinely -// cover dozens of users, so 10 hides the obvious VPN noise but keeps -// small shared networks visible. -constexpr int DEFAULT_VPN_THRESHOLD = 10; - -void configureAltTable(QTableWidget *table) -{ - table->setSelectionBehavior(QAbstractItemView::SelectRows); - table->setSelectionMode(QAbstractItemView::SingleSelection); - table->setEditTriggers(QAbstractItemView::NoEditTriggers); - table->verticalHeader()->setVisible(false); - table->setAlternatingRowColors(true); - table->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents); - table->horizontalHeader()->setStretchLastSection(true); -} -} // namespace - -TabAlts::TabAlts(TabSupervisor *_tabSupervisor, AbstractClient *_client, const QString &initialUser) - : Tab(_tabSupervisor), client(_client) -{ - auto *centralWidget = new QWidget(this); - setCentralWidget(centralWidget); - - searchEdit = new QLineEdit; - searchEdit->setClearButtonEnabled(true); - connect(searchEdit, &QLineEdit::returnPressed, this, &TabAlts::investigateUser); - - depthSpin = new QSpinBox; - depthSpin->setMinimum(1); - depthSpin->setMaximum(MAX_ALT_DEPTH); - depthSpin->setValue(1); - depthSpin->setKeyboardTracking(false); - connect(depthSpin, &QSpinBox::valueChanged, this, &TabAlts::onSearchFilterChanged); - - investigateButton = new QPushButton; - connect(investigateButton, &QPushButton::clicked, this, &TabAlts::investigateUser); - - backButton = new QPushButton; - backButton->setEnabled(false); - connect(backButton, &QPushButton::clicked, this, &TabAlts::goBack); - - statusLabel = new QLabel; - trailLabel = new QLabel; - trailLabel->setTextFormat(Qt::RichText); - trailLabel->setTextInteractionFlags(Qt::TextBrowserInteraction); - trailLabel->setOpenExternalLinks(false); - // The trail grows with every hop, so let it wrap instead of forcing the tab - // wider than the window. - trailLabel->setWordWrap(true); - connect(trailLabel, &QLabel::linkActivated, this, [this](const QString &userName) { - if (!userName.isEmpty() && userName != currentUser) { - goToTrailEntry(userName); - } - }); - - auto *topBar = new QHBoxLayout; - topBar->addWidget(backButton); - topBar->addWidget(searchEdit); - topBar->addWidget(depthSpin); - topBar->addWidget(investigateButton); - - auto makeChip = [this](const QString &text) { - auto *chip = new QPushButton(text); - chip->setCheckable(true); - chip->setChecked(true); - connect(chip, &QPushButton::toggled, this, &TabAlts::onSearchFilterChanged); - return chip; - }; - emailChip = makeChip(QString()); - clientIdChip = makeChip(QString()); - ipChip = makeChip(QString()); - vpnLabel = new QLabel; - vpnSpin = new QSpinBox; - vpnSpin->setMinimum(1); - vpnSpin->setMaximum(100000); - vpnSpin->setValue(DEFAULT_VPN_THRESHOLD); - connect(vpnSpin, &QSpinBox::valueChanged, this, &TabAlts::onDisplayFilterChanged); - windowLabel = new QLabel; - monthsSpin = new QSpinBox; - monthsSpin->setMinimum(1); - monthsSpin->setMaximum(24); - monthsSpin->setValue(6); - monthsSpin->setKeyboardTracking(false); - connect(monthsSpin, &QSpinBox::valueChanged, this, &TabAlts::onSearchFilterChanged); - - auto *filterBar = new QHBoxLayout; - filterBar->addWidget(emailChip); - filterBar->addWidget(clientIdChip); - filterBar->addWidget(ipChip); - filterBar->addWidget(vpnLabel); - filterBar->addWidget(vpnSpin); - filterBar->addWidget(windowLabel); - filterBar->addWidget(monthsSpin); - filterBar->addStretch(); - - altsTable = new QTableWidget(0, COL_ALTS_COUNT); - configureAltTable(altsTable); - altsDelegate = - new AltTableDelegate(COL_ALTS_VIA, COL_ALTS_WARNS, COL_ALTS_BANS, COL_ALTS_SHARED, COL_ALTS_ACTIVE, altsTable); - altsTable->setItemDelegateForColumn(COL_ALTS_VIA, altsDelegate); - altsTable->setItemDelegateForColumn(COL_ALTS_WARNS, altsDelegate); - altsTable->setItemDelegateForColumn(COL_ALTS_BANS, altsDelegate); - altsTable->setItemDelegateForColumn(COL_ALTS_ACTIVE, altsDelegate); - connectTableActivation(altsTable, this, [this](const QModelIndex &index) { - if (!index.isValid()) { - return; - } - auto *userItem = altsTable->item(index.row(), COL_ALTS_USER); - if (userItem != nullptr && !userItem->text().isEmpty() && userItem->text() != currentUser) { - investigate(userItem->text()); - } - }); - connect(altsTable, &QTableWidget::itemSelectionChanged, this, &TabAlts::onAltSelected); - - detailNameValue = new QLabel; - detailCountsValue = new QLabel; - detailNotesEdit = new QTextEdit; - detailNotesEdit->setReadOnly(true); - // Compact cap so the warn/ban tables stay visible without scrolling the tab. - detailNotesEdit->setMaximumHeight(60); - warnsTable = new QTableWidget(0, COL_WARNS_COUNT); - configureAltTable(warnsTable); - bansTable = new QTableWidget(0, COL_BANS_COUNT); - configureAltTable(bansTable); - - auto *detailGrid = new QGridLayout; - detailNameLabel = new QLabel; - detailCountsLabel = new QLabel; - detailNotesLabel = new QLabel; - detailGrid->addWidget(detailNameLabel, 0, 0); - detailGrid->addWidget(detailNameValue, 0, 1); - detailGrid->addWidget(detailCountsLabel, 0, 2); - detailGrid->addWidget(detailCountsValue, 0, 3); - detailGrid->addWidget(detailNotesLabel, 1, 0); - detailGrid->addWidget(detailNotesEdit, 1, 1, 1, 3); - - detailsGroup = new QGroupBox; - auto *detailsLayout = new QVBoxLayout(detailsGroup); - detailsLayout->addLayout(detailGrid); - detailsLayout->addWidget(warnsTable, 1); - detailsLayout->addWidget(bansTable, 1); - investigateSelectedButton = new QPushButton; - investigateSelectedButton->setEnabled(false); - connect(investigateSelectedButton, &QPushButton::clicked, this, &TabAlts::investigateSelected); - detailsLayout->addWidget(investigateSelectedButton); - - auto *contentSplitter = new QSplitter(Qt::Horizontal); - contentSplitter->addWidget(altsTable); - contentSplitter->addWidget(detailsGroup); - contentSplitter->setStretchFactor(0, 3); - contentSplitter->setStretchFactor(1, 2); - detailsGroup->setMinimumWidth(260); - - auto *mainLayout = new QVBoxLayout(centralWidget); - mainLayout->addLayout(topBar); - mainLayout->addLayout(filterBar); - mainLayout->addWidget(statusLabel); - mainLayout->addWidget(trailLabel); - mainLayout->addWidget(contentSplitter, 1); - - retranslateUi(); - clearDetails(); - investigate(initialUser); -} - -void TabAlts::retranslateUi() -{ - searchEdit->setPlaceholderText(tr("User name")); - depthSpin->setPrefix(tr("Depth: ")); - depthSpin->setToolTip(tr("Search depth: 1 finds direct matches, higher values follow alts of alts.")); - investigateButton->setText(tr("Investigate")); - backButton->setText(tr("Back")); - backButton->setToolTip(tr("Back to the previously investigated user")); - if (currentUser.isEmpty()) { - statusLabel->setText(tr("Enter a user name to investigate.")); - } - - emailChip->setText(tr("Email")); - emailChip->setToolTip(tr("Show or hide email matches")); - clientIdChip->setText(tr("Client ID")); - clientIdChip->setToolTip(tr("Show or hide client ID matches")); - ipChip->setText(tr("IP")); - ipChip->setToolTip(tr("Show or hide IP address matches")); - vpnLabel->setText(tr("Hide IPs shared by more than")); - vpnSpin->setSuffix(tr(" users")); - windowLabel->setText(tr("Session window")); - monthsSpin->setSuffix(tr(" months")); - monthsSpin->setToolTip(tr("How far back session IPs and client IDs are considered.")); - - altsTable->setHorizontalHeaderLabels({tr("User"), tr("Via"), tr("Depth"), tr("Shared"), tr("IPs"), tr("eMail"), - tr("Client ID"), tr("Registered"), tr("Last login"), tr("Warns"), tr("Bans"), - tr("Active")}); - altsTable->setToolTip(tr("Double-click or press Enter on a row to investigate that account")); - warnsTable->setHorizontalHeaderLabels({tr("Warning time"), tr("Moderator"), tr("Reason")}); - bansTable->setHorizontalHeaderLabels({tr("Ban time"), tr("Moderator"), tr("Length"), tr("Reason")}); - - detailsGroup->setTitle(tr("Selected user")); - detailNameLabel->setText(tr("Name:")); - detailCountsLabel->setText(tr("Counts:")); - detailNotesLabel->setText(tr("Admin notes:")); - investigateSelectedButton->setText(tr("Investigate this user")); - investigateSelectedButton->setToolTip(tr("Open the selected account as a new investigation")); - - if (!lastAlts.isEmpty()) { - renderTable(); - } -} - -QString TabAlts::formatEpoch(quint64 ts) const -{ - return formatEpochTimestamp(ts, tr("Unknown")); -} - -void TabAlts::investigate(const QString &userName) -{ - if (userName.isEmpty() || userName == currentUser) { - return; - } - // Alt matches are symmetric, so revisiting an earlier account is a normal - // double-click. Truncate there instead of pushing it again, so the trail - // stays a single path. - if (backStack.contains(userName)) { - goToTrailEntry(userName); - return; - } - if (!currentUser.isEmpty()) { - backStack << currentUser; - backButton->setEnabled(true); - } - loadUser(userName); -} - -void TabAlts::goToTrailEntry(const QString &userName) -{ - const int index = backStack.indexOf(userName); - if (index < 0) { - investigate(userName); - return; - } - // Clicking an earlier crumb truncates the history there instead of pushing - // another entry, so the trail keeps a single path and Back still works. - backStack.erase(backStack.begin() + index, backStack.end()); - backButton->setEnabled(!backStack.isEmpty()); - loadUser(userName); -} - -void TabAlts::goBack() -{ - if (backStack.isEmpty()) { - return; - } - const QString previousUser = backStack.takeLast(); - backButton->setEnabled(!backStack.isEmpty()); - loadUser(previousUser); -} - -void TabAlts::loadUser(const QString &userName) -{ - currentUser = userName; - searchEdit->setText(userName); - updateTrail(); - investigateUser(); -} - -void TabAlts::updateTrail() -{ - QStringList trail = backStack; - trail << currentUser; - trail.removeAll(QString()); - QStringList links; - for (const QString &name : trail) { - const QString safeName = name.toHtmlEscaped(); - if (name == currentUser) { - links << QStringLiteral("") + safeName + QStringLiteral(""); - } else { - links << QStringLiteral("") + safeName + - QStringLiteral(""); - } - } - trailLabel->setText(links.join(QStringLiteral(" › "))); - trailLabel->setVisible(!links.isEmpty()); -} - -void TabAlts::investigateUser() -{ - const QString userName = searchEdit->text().simplified(); - if (userName.isEmpty()) { - return; - } - if (userName != currentUser && !currentUser.isEmpty()) { - backStack << currentUser; - backButton->setEnabled(true); - } - currentUser = userName; - updateTrail(); - reloadCurrentUser(); -} - -void TabAlts::reloadCurrentUser() -{ - lastAlts.clear(); - selectedUser.clear(); - clearDetails(); - altsTable->setRowCount(0); - statusLabel->setText(tr("Loading alts for %1...").arg(currentUser)); - requestAlts(currentUser, depthSpin->value()); -} - -void TabAlts::requestAlts(const QString &userName, int depth) -{ - Command_GetUserAlts cmd; - cmd.set_user_name(userName.toStdString()); - cmd.set_depth(static_cast(depth)); - cmd.set_include_email(emailChip->isChecked()); - cmd.set_include_clientid(clientIdChip->isChecked()); - cmd.set_include_ip(ipChip->isChecked()); - cmd.set_session_months(static_cast(monthsSpin->value())); - PendingCommand *pend = client->prepareModeratorCommand(cmd); - connect(pend, &PendingCommand::finished, this, [this, userName](const Response &response) { - if (userName == currentUser) { - altsResponse(response); - } - }); - client->sendCommand(pend); -} - -void TabAlts::altsResponse(const Response &response) -{ - lastAlts.clear(); - if (response.response_code() != Response::RespOk) { - statusLabel->setText(tr("Could not load alts for %1.").arg(currentUser)); - return; - } - - const Response_UserAlts &resp = response.GetExtension(Response_UserAlts::ext); - // The server answers RespOk with no rows when the user does not exist. - if (resp.alts_size() == 0) { - statusLabel->setText(tr("User %1 not found.").arg(currentUser)); - return; - } - - for (int i = 0; i < resp.alts_size(); ++i) { - lastAlts << resp.alts(i); - } - renderTable(); -} - -bool TabAlts::rowVisible(const ServerInfo_UserAlt &alt) const -{ - const QString name = QString::fromStdString(alt.user_name()); - if (name == currentUser) { - return true; - } - if (!alt.has_match_reason() || alt.match_reason().empty()) { - return true; - } - const QString reason = QString::fromStdString(alt.match_reason()); - if (reason == QStringLiteral("email")) { - return emailChip->isChecked(); - } - if (reason.startsWith(QStringLiteral("clientid:"))) { - return clientIdChip->isChecked(); - } - if (reason.startsWith(QStringLiteral("ip:"))) { - return ipChip->isChecked() && - (!alt.has_shared_count() || alt.shared_count() <= static_cast(vpnSpin->value())); - } - return true; -} - -void TabAlts::renderTable() -{ - // Blocked so the programmatic refill neither wipes the details panel - // (via the selection-cleared path) nor refetches them on restore. - const QSignalBlocker blocker(altsTable); - altsTable->setRowCount(0); - - int emailCount = 0; - int clientIdCount = 0; - int ipCount = 0; - for (const ServerInfo_UserAlt &alt : lastAlts) { - const QString reason = QString::fromStdString(alt.match_reason()); - if (reason == QStringLiteral("email")) { - ++emailCount; - } else if (reason.startsWith(QStringLiteral("clientid:"))) { - ++clientIdCount; - } else if (reason.startsWith(QStringLiteral("ip:"))) { - ++ipCount; - } - } - // The server omits unchecked match types, so keep the last count while a - // chip is off instead of relabelling it "(0)". - if (emailChip->isChecked()) { - emailChip->setText(tr("Email (%1)").arg(emailCount)); - } - if (clientIdChip->isChecked()) { - clientIdChip->setText(tr("Client ID (%1)").arg(clientIdCount)); - } - if (ipChip->isChecked()) { - ipChip->setText(tr("IP (%1)").arg(ipCount)); - } - - QFont userFont = altsTable->font(); - userFont.setBold(true); - const QColor dimColor = palette().color(QPalette::Disabled, QPalette::Text); - - int hidden = 0; - int row = 0; - for (const ServerInfo_UserAlt &alt : lastAlts) { - if (!rowVisible(alt)) { - ++hidden; - continue; - } - altsTable->insertRow(row); - auto *userItem = new QTableWidgetItem(QString::fromStdString(alt.user_name())); - userItem->setFont(userFont); - altsTable->setItem(row, COL_ALTS_USER, userItem); - altsTable->setItem(row, COL_ALTS_VIA, - new QTableWidgetItem(alt.has_match_reason() && !alt.match_reason().empty() - ? QString::fromStdString(alt.match_reason()) - : QStringLiteral("-"))); - altsTable->setItem(row, COL_ALTS_DEPTH, - new QTableWidgetItem(alt.has_depth() ? QString::number(alt.depth()) : QStringLiteral("-"))); - altsTable->setItem( - row, COL_ALTS_SHARED, - new QTableWidgetItem(alt.has_shared_count() ? QString::number(alt.shared_count()) : QStringLiteral("-"))); - altsTable->setItem( - row, COL_ALTS_IPS, - new QTableWidgetItem(alt.has_ip_count() ? QString::number(alt.ip_count()) : QStringLiteral("-"))); - altsTable->setItem(row, COL_ALTS_EMAIL, new QTableWidgetItem(QString::fromStdString(alt.email()))); - altsTable->setItem(row, COL_ALTS_CLIENTID, new QTableWidgetItem(QString::fromStdString(alt.clientid()))); - altsTable->setItem(row, COL_ALTS_REGISTERED, new QTableWidgetItem(formatEpoch(alt.registration_time()))); - altsTable->setItem(row, COL_ALTS_LAST_LOGIN, new QTableWidgetItem(formatEpoch(alt.last_login()))); - altsTable->setItem(row, COL_ALTS_WARNS, new QTableWidgetItem(QString::number(alt.warn_count()))); - altsTable->setItem(row, COL_ALTS_BANS, new QTableWidgetItem(QString::number(alt.ban_count()))); - auto *activeItem = new QTableWidgetItem(alt.is_active() ? tr("yes") : tr("no")); - activeItem->setData(Qt::UserRole, alt.is_active()); - altsTable->setItem(row, COL_ALTS_ACTIVE, activeItem); - // shared_count already includes the investigated account, so anything - // above 2 means the address is shared beyond that pair. - if (QString::fromStdString(alt.match_reason()).startsWith(QStringLiteral("ip:")) && alt.has_shared_count() && - alt.shared_count() > 2) { - for (int col = 0; col < COL_ALTS_COUNT; ++col) { - if (col == COL_ALTS_VIA) { - continue; - } - if (auto *item = altsTable->item(row, col)) { - item->setForeground(dimColor); - } - } - } - ++row; - } - // The server appends the frontier user as a depth-0 seed row; it is the - // investigated user, not one of their related accounts. - int relatedCount = 0; - for (const ServerInfo_UserAlt &alt : lastAlts) { - if (!alt.has_depth() || alt.depth() > 0) { - ++relatedCount; - } - } - if (hidden == 0) { - statusLabel->setText(tr("%1 account(s) related to %2.").arg(relatedCount).arg(currentUser)); - } else { - statusLabel->setText(tr("%1 shown, %2 hidden by filters (%3 total) related to %4.") - .arg(row) - .arg(hidden) - .arg(relatedCount) - .arg(currentUser)); - } - - if (!selectedUser.isEmpty()) { - bool restored = false; - for (int restoreRow = 0; restoreRow < altsTable->rowCount(); ++restoreRow) { - auto *userItem = altsTable->item(restoreRow, COL_ALTS_USER); - if (userItem != nullptr && userItem->text() == selectedUser) { - altsTable->setCurrentItem(userItem, QItemSelectionModel::Select | QItemSelectionModel::Rows); - restored = true; - break; - } - } - if (!restored) { - selectedUser.clear(); - clearDetails(); - } - } -} - -void TabAlts::onSearchFilterChanged() -{ - if (currentUser.isEmpty()) { - return; - } - // A filter change reloads the user being investigated; it must not switch - // to a name that was typed into the search box but never submitted. - reloadCurrentUser(); -} - -void TabAlts::onDisplayFilterChanged() -{ - if (lastAlts.isEmpty()) { - return; - } - renderTable(); -} - -void TabAlts::investigateSelected() -{ - if (!selectedUser.isEmpty() && selectedUser != currentUser) { - investigate(selectedUser); - } -} - -void TabAlts::onAltSelected() -{ - const auto selected = altsTable->selectedItems(); - if (selected.isEmpty()) { - selectedUser.clear(); - clearDetails(); - return; - } - auto *userItem = altsTable->item(selected.first()->row(), COL_ALTS_USER); - if (userItem == nullptr || userItem->text().isEmpty() || userItem->text() == selectedUser) { - return; - } - selectedUser = userItem->text(); - requestDetails(selectedUser); -} - -void TabAlts::requestDetails(const QString &userName) -{ - detailNameValue->setText(userName); - detailCountsValue->setText(tr("Loading...")); - detailNotesEdit->clear(); - warnsTable->setRowCount(0); - bansTable->setRowCount(0); - investigateSelectedButton->setEnabled(userName != currentUser); - - Command_ReportUserInfo infoCmd; - infoCmd.set_user_name(userName.toStdString()); - PendingCommand *infoPend = client->prepareModeratorCommand(infoCmd); - connect(infoPend, &PendingCommand::finished, this, [this, userName](const Response &response) { - if (userName == selectedUser) { - userDetailsResponse(response); - } - }); - client->sendCommand(infoPend); - - Command_GetWarnHistory warnCmd; - warnCmd.set_user_name(userName.toStdString()); - PendingCommand *warnPend = client->prepareModeratorCommand(warnCmd); - connect(warnPend, &PendingCommand::finished, this, [this, userName](const Response &response) { - if (userName == selectedUser) { - warnHistoryResponse(response); - } - }); - client->sendCommand(warnPend); - - Command_GetBanHistory banCmd; - banCmd.set_user_name(userName.toStdString()); - PendingCommand *banPend = client->prepareModeratorCommand(banCmd); - connect(banPend, &PendingCommand::finished, this, [this, userName](const Response &response) { - if (userName == selectedUser) { - banHistoryResponse(response); - } - }); - client->sendCommand(banPend); -} - -void TabAlts::clearDetails() -{ - detailNameValue->clear(); - detailCountsValue->setText(tr("Select a row to inspect its history.")); - detailNotesEdit->clear(); - warnsTable->setRowCount(0); - bansTable->setRowCount(0); - investigateSelectedButton->setEnabled(false); -} - -void TabAlts::userDetailsResponse(const Response &response) -{ - if (response.response_code() != Response::RespOk) { - detailCountsValue->setText(tr("Error loading user info.")); - return; - } - - const Response_ReportUserInfo &resp = response.GetExtension(Response_ReportUserInfo::ext); - detailCountsValue->setText(tr("Reports: %1 Bans: %2 Warnings: %3") - .arg(resp.total_reports()) - .arg(resp.total_bans()) - .arg(resp.total_warns())); - - if (resp.has_admin_notes() && !resp.admin_notes().empty()) { - detailNotesEdit->setPlainText(QString::fromStdString(resp.admin_notes())); - } else { - detailNotesEdit->setPlainText(tr("(none)")); - } -} - -void TabAlts::warnHistoryResponse(const Response &response) -{ - warnsTable->setRowCount(0); - if (response.response_code() != Response::RespOk) { - return; - } - - const Response_WarnHistory &resp = response.GetExtension(Response_WarnHistory::ext); - warnsTable->setRowCount(resp.warn_list_size()); - for (int i = 0; i < resp.warn_list_size(); ++i) { - const ServerInfo_Warning &warn = resp.warn_list(i); - warnsTable->setItem(i, COL_WARNS_TIME, new QTableWidgetItem(QString::fromStdString(warn.time_of()))); - warnsTable->setItem(i, COL_WARNS_MODERATOR, new QTableWidgetItem(QString::fromStdString(warn.admin_name()))); - warnsTable->setItem(i, COL_WARNS_REASON, new QTableWidgetItem(QString::fromStdString(warn.reason()))); - } -} - -void TabAlts::banHistoryResponse(const Response &response) -{ - bansTable->setRowCount(0); - if (response.response_code() != Response::RespOk) { - return; - } - - const Response_BanHistory &resp = response.GetExtension(Response_BanHistory::ext); - bansTable->setRowCount(resp.ban_list_size()); - for (int i = 0; i < resp.ban_list_size(); ++i) { - const ServerInfo_Ban &ban = resp.ban_list(i); - bansTable->setItem(i, COL_BANS_TIME, new QTableWidgetItem(QString::fromStdString(ban.ban_time()))); - bansTable->setItem(i, COL_BANS_MODERATOR, new QTableWidgetItem(QString::fromStdString(ban.admin_name()))); - bansTable->setItem(i, COL_BANS_LENGTH, new QTableWidgetItem(QString::fromStdString(ban.ban_length()))); - bansTable->setItem(i, COL_BANS_REASON, new QTableWidgetItem(QString::fromStdString(ban.ban_reason()))); - } -} diff --git a/cockatrice/src/interface/widgets/tabs/tab_alts.h b/cockatrice/src/interface/widgets/tabs/tab_alts.h deleted file mode 100644 index 7f6dd4f42..000000000 --- a/cockatrice/src/interface/widgets/tabs/tab_alts.h +++ /dev/null @@ -1,102 +0,0 @@ -#ifndef TAB_ALTS_H -#define TAB_ALTS_H - -#include "tab.h" - -#include -#include -#include -#include -#include - -class AbstractClient; -class AltTableDelegate; -class QGroupBox; -class QLabel; -class QLineEdit; -class QPushButton; -class QSpinBox; -class QTableWidget; -class QTextEdit; -class Response; -class TabSupervisor; - -/** - * Staff alt-graph investigation tool. Shows accounts related to the - * investigated user (shared email, client id, or recent IP address), - * including transitive alts-of-alts, with per-row match evidence. - * Selecting a row loads that account's warnings, bans, and admin notes. - */ -class TabAlts : public Tab -{ - Q_OBJECT -public: - explicit TabAlts(TabSupervisor *_tabSupervisor, AbstractClient *_client, const QString &initialUser = {}); - void retranslateUi() override; - [[nodiscard]] QString getTabText() const override - { - return tr("User Alts"); - } - void investigate(const QString &userName); - -private slots: - void investigateUser(); - void goBack(); - void investigateSelected(); - void altsResponse(const Response &response); - void onSearchFilterChanged(); - void onDisplayFilterChanged(); - void onAltSelected(); - void userDetailsResponse(const Response &response); - void warnHistoryResponse(const Response &response); - void banHistoryResponse(const Response &response); - -private: - void loadUser(const QString &userName); - void goToTrailEntry(const QString &userName); - void updateTrail(); - void reloadCurrentUser(); - void requestAlts(const QString &userName, int depth); - void requestDetails(const QString &userName); - void clearDetails(); - void renderTable(); - [[nodiscard]] bool rowVisible(const ServerInfo_UserAlt &alt) const; - [[nodiscard]] QString formatEpoch(quint64 ts) const; - - AbstractClient *client; - QString currentUser; - QString selectedUser; - QList backStack; - QList lastAlts; - - QLineEdit *searchEdit; - QSpinBox *depthSpin; - QPushButton *investigateButton; - QPushButton *backButton; - QPushButton *investigateSelectedButton; - QLabel *trailLabel; - QLabel *statusLabel; - QTableWidget *altsTable; - AltTableDelegate *altsDelegate; - - QPushButton *emailChip; - QPushButton *clientIdChip; - QPushButton *ipChip; - QLabel *vpnLabel; - QSpinBox *vpnSpin; - QLabel *windowLabel; - QSpinBox *monthsSpin; - - QLabel *detailNameValue; - QLabel *detailCountsValue; - QTextEdit *detailNotesEdit; - QTableWidget *warnsTable; - QTableWidget *bansTable; - - QGroupBox *detailsGroup; - QLabel *detailNameLabel; - QLabel *detailCountsLabel; - QLabel *detailNotesLabel; -}; - -#endif // TAB_ALTS_H 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_game.cpp b/cockatrice/src/interface/widgets/tabs/tab_game.cpp index 5dba1fc51..34d6f8a51 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_game.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_game.cpp @@ -27,7 +27,6 @@ #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,7 +98,7 @@ 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 @@ -116,7 +115,6 @@ 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); @@ -160,7 +158,6 @@ TabGame::TabGame(TabSupervisor *_tabSupervisor, createPlayAreaWidget(); createDeckViewContainerWidget(); replayDock = nullptr; - historyDock = nullptr; addDockWidget(Qt::RightDockWidgetArea, cardInfoDock); addDockWidget(Qt::RightDockWidgetArea, playerListDock); @@ -330,9 +327,6 @@ void TabGame::emitUserEvent() TabGame::~TabGame() { - if (aCardMenu && aCardMenu->menu() && aCardMenu->menu() != emptyCardMenu) { - delete aCardMenu->menu(); - } for (auto &player : game->getPlayerManager()->getPlayers()) { player->clear(); } @@ -359,9 +353,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())); } @@ -438,9 +429,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")); } @@ -573,10 +561,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->exec(); + DlgCreateGame dlg(game->getGameMetaInfo()->proto(), game->getGameMetaInfo()->getRoomGameTypes(), this); + dlg.exec(); } void TabGame::actCopyGameLink() @@ -1161,16 +1147,10 @@ QString TabGame::getTabText() const /** * @param menu The menu to set. Pass in nullptr to set the menu to empty. - * - * The previous card menu is deleted here: QAction::setMenu() does not take ownership, so whoever installs the menu - * (this widget) has to dispose of the one it replaces. The empty placeholder menu is reused instead of deleted. */ void TabGame::setCardMenu(CardMenu *menu) { - if (!aCardMenu || !emptyCardMenu) { - if (menu) { - menu->deleteLater(); - } + if (!aCardMenu) { return; } @@ -1180,11 +1160,10 @@ void TabGame::setCardMenu(CardMenu *menu) gameView->viewport()->update(); } } - - QMenu *previousMenu = aCardMenu->menu(); - aCardMenu->setMenu(menu ? menu : emptyCardMenu); - if (previousMenu && previousMenu != emptyCardMenu && previousMenu != menu) { - previousMenu->deleteLater(); + if (menu) { + aCardMenu->setMenu(menu); + } else { + aCardMenu->setMenu(new QMenu); } } @@ -1268,8 +1247,7 @@ void TabGame::createMenuItems() gameMenu->addSeparator(); - emptyCardMenu = new QMenu(this); - aCardMenu = gameMenu->addMenu(emptyCardMenu); + aCardMenu = gameMenu->addMenu(new QMenu(this)); // Sync the new action with the same state the dock button already shows. updateInviteButtonState(); @@ -1301,7 +1279,6 @@ void TabGame::createReplayMenuItems() gameMenu->addAction(aLeaveGame); aCardMenu = nullptr; - emptyCardMenu = nullptr; addTabMenu(gameMenu); } @@ -1314,10 +1291,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}); } @@ -1387,12 +1360,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); @@ -1401,9 +1368,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); @@ -1458,22 +1422,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 5c2af82b4..412529dc2 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,7 +89,7 @@ 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; @@ -101,8 +100,6 @@ private: QAction *aInviteToGame = nullptr; QList phaseActions; QAction *aCardMenu; - /** @brief Placeholder submenu shown when no card is selected; reused instead of reallocated. */ - QMenu *emptyCardMenu = nullptr; /** * @brief The actions associated with managing a QDockWidget diff --git a/cockatrice/src/interface/widgets/tabs/tab_moderation.cpp b/cockatrice/src/interface/widgets/tabs/tab_moderation.cpp index 8568b0d2e..be1adeef8 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_moderation.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_moderation.cpp @@ -1,14 +1,11 @@ #include "tab_moderation.h" -#include "../utility/epoch_format.h" -#include "../utility/table_activation.h" #include "abstract_client.h" #include "libcockatrice/protocol/pb/admin_commands.pb.h" #include "libcockatrice/protocol/pb/response.pb.h" #include "libcockatrice/protocol/pb/serverinfo_moderator_login.pb.h" #include "libcockatrice/protocol/pb/serverinfo_user_alt.pb.h" #include "libcockatrice/protocol/pb/serverinfo_user_session.pb.h" -#include "tab_supervisor.h" #include #include @@ -24,7 +21,6 @@ #include #include #include -#include #include #include #include @@ -103,67 +99,38 @@ TabModeration::TabModeration(TabSupervisor *_tabSupervisor, AbstractClient *_cli topBar->addWidget(removeBannerButton); userInfoGroup = new QGroupBox; + userInfoNameLabel = new QLabel; userInfoNameValue = new QLabel; - QFont displayFont = userInfoNameValue->font(); - displayFont.setPointSize(displayFont.pointSize() + 6); - displayFont.setBold(true); - userInfoNameValue->setFont(displayFont); + userInfoRegisteredLabel = new QLabel; + userInfoRegisteredValue = new QLabel; + userInfoLastLoginLabel = new QLabel; + userInfoLastLoginValue = new QLabel; + userInfoStatusLabel = new QLabel; userInfoStatusValue = new QLabel; - QFont statusFont = userInfoStatusValue->font(); - statusFont.setBold(true); - userInfoStatusValue->setFont(statusFont); - sublineLabel = new QLabel; - - auto makeCountCard = [](const QString &title, QLabel *&value) { - auto *card = new QGroupBox(title); - value = new QLabel(QStringLiteral("–"), card); - QFont cardFont = value->font(); - cardFont.setPointSize(cardFont.pointSize() + 6); - cardFont.setBold(true); - value->setFont(cardFont); - value->setAlignment(Qt::AlignCenter); - auto *cardLayout = new QVBoxLayout(card); - cardLayout->addWidget(value); - return card; - }; - cardReports = makeCountCard(QString(), cardReportsValue); - cardWarns = makeCountCard(QString(), cardWarnsValue); - cardBans = makeCountCard(QString(), cardBansValue); - - altsCard = new QGroupBox; - altsCountValue = new QLabel(QStringLiteral("–"), altsCard); - QFont altsFont = altsCountValue->font(); - altsFont.setPointSize(altsFont.pointSize() + 6); - altsFont.setBold(true); - altsCountValue->setFont(altsFont); - altsCountValue->setAlignment(Qt::AlignCenter); - openAltsButton = new QPushButton; - connect(openAltsButton, &QPushButton::clicked, this, &TabModeration::openAltsGraph); - auto *altsCardLayout = new QVBoxLayout(altsCard); - altsCardLayout->addWidget(altsCountValue); - altsCardLayout->addWidget(openAltsButton); - - auto *cardsRow = new QHBoxLayout; - cardsRow->addWidget(cardReports); - cardsRow->addWidget(cardWarns); - cardsRow->addWidget(cardBans); - cardsRow->addWidget(altsCard); - + userInfoCountsLabel = new QLabel; + userInfoCountsValue = new QLabel; userInfoNotesLabel = new QLabel; userInfoNotesEdit = new QTextEdit; userInfoNotesEdit->setReadOnly(true); - auto *identityRow = new QHBoxLayout; - identityRow->addWidget(userInfoNameValue); - identityRow->addStretch(); - identityRow->addWidget(userInfoStatusValue); + auto *infoGrid = new QGridLayout; + infoGrid->addWidget(userInfoNameLabel, 0, 0); + infoGrid->addWidget(userInfoNameValue, 0, 1); + infoGrid->addWidget(userInfoRegisteredLabel, 0, 2); + infoGrid->addWidget(userInfoRegisteredValue, 0, 3); + infoGrid->addWidget(userInfoLastLoginLabel, 1, 0); + infoGrid->addWidget(userInfoLastLoginValue, 1, 1); + infoGrid->addWidget(userInfoStatusLabel, 1, 2); + infoGrid->addWidget(userInfoStatusValue, 1, 3); + infoGrid->addWidget(userInfoCountsLabel, 2, 0); + infoGrid->addWidget(userInfoCountsValue, 2, 1, 1, 3); + infoGrid->addWidget(userInfoNotesLabel, 3, 0, Qt::AlignTop); + infoGrid->addWidget(userInfoNotesEdit, 3, 1, 1, 3); + infoGrid->setColumnStretch(1, 1); + infoGrid->setColumnStretch(3, 1); auto *infoLayout = new QVBoxLayout(userInfoGroup); - infoLayout->addLayout(identityRow); - infoLayout->addWidget(sublineLabel); - infoLayout->addLayout(cardsRow); - infoLayout->addWidget(userInfoNotesLabel); - infoLayout->addWidget(userInfoNotesEdit); + infoLayout->addLayout(infoGrid); auto configureTable = [](QTableWidget *table) { table->setSelectionBehavior(QAbstractItemView::SelectRows); @@ -175,27 +142,19 @@ TabModeration::TabModeration(TabSupervisor *_tabSupervisor, AbstractClient *_cli table->horizontalHeader()->setStretchLastSection(true); }; + altsGroup = new QGroupBox; altsTable = new QTableWidget(0, COL_ALTS_COUNT); configureTable(altsTable); - connectTableActivation(altsTable, this, [this](const QModelIndex &index) { - if (!index.isValid()) { - return; - } - auto *userItem = altsTable->item(index.row(), COL_ALTS_USER); - if (userItem != nullptr && !userItem->text().isEmpty()) { - getTabSupervisor()->openTabAlts(userItem->text()); - } - }); - auto *altsPage = new QWidget; - auto *altsLayout = new QVBoxLayout(altsPage); + auto *altsLayout = new QVBoxLayout(altsGroup); altsLayout->addWidget(altsTable); + sessionsGroup = new QGroupBox; sessionsTable = new QTableWidget(0, COL_SESSIONS_COUNT); configureTable(sessionsTable); - auto *sessionsPage = new QWidget; - auto *sessionsLayout = new QVBoxLayout(sessionsPage); + auto *sessionsLayout = new QVBoxLayout(sessionsGroup); sessionsLayout->addWidget(sessionsTable); + staffGroup = new QGroupBox; staffTable = new QTableWidget(0, COL_STAFF_COUNT); configureTable(staffTable); refreshStaffButton = new QPushButton; @@ -203,21 +162,19 @@ TabModeration::TabModeration(TabSupervisor *_tabSupervisor, AbstractClient *_cli auto *staffHeader = new QHBoxLayout; staffHeader->addStretch(); staffHeader->addWidget(refreshStaffButton); - auto *staffPage = new QWidget; - auto *staffLayout = new QVBoxLayout(staffPage); + auto *staffLayout = new QVBoxLayout(staffGroup); staffLayout->addWidget(staffTable); staffLayout->addLayout(staffHeader); - sectionTabs = new QTabWidget; - sectionTabs->addTab(altsPage, QString()); - sectionTabs->addTab(sessionsPage, QString()); - sectionTabs->addTab(staffPage, QString()); - auto *splitter = new QSplitter(Qt::Vertical); splitter->addWidget(userInfoGroup); - splitter->addWidget(sectionTabs); + splitter->addWidget(altsGroup); + splitter->addWidget(sessionsGroup); + splitter->addWidget(staffGroup); splitter->setStretchFactor(0, 1); splitter->setStretchFactor(1, 2); + splitter->setStretchFactor(2, 2); + splitter->setStretchFactor(3, 1); auto *mainLayout = new QVBoxLayout(centralWidget); mainLayout->addLayout(topBar); @@ -239,56 +196,45 @@ void TabModeration::retranslateUi() refreshStaffButton->setText(tr("Refresh")); userInfoGroup->setTitle(tr("User Info")); + userInfoNameLabel->setText(tr("Name:")); + userInfoRegisteredLabel->setText(tr("Registered:")); + userInfoLastLoginLabel->setText(tr("Last login:")); + userInfoStatusLabel->setText(tr("Status:")); + userInfoCountsLabel->setText(tr("Counts:")); userInfoNotesLabel->setText(tr("Admin notes:")); - cardReports->setTitle(tr("Reports")); - cardWarns->setTitle(tr("Warnings")); - cardBans->setTitle(tr("Bans")); - altsCard->setTitle(tr("Alts")); - openAltsButton->setText(tr("Open graph")); - openAltsButton->setToolTip(tr("Open this user in the User Alts tab")); - sectionTabs->setTabText(0, tr("Alts")); - sectionTabs->setTabText(1, tr("Sessions")); - sectionTabs->setTabText(2, tr("Staff Last Logins")); - - if (cachedUserInfoValid) { - applyUserInfo(); - } - if (cachedAltsCount >= 0) { - applyAltsCount(); - } + altsGroup->setTitle(tr("Alts")); + sessionsGroup->setTitle(tr("Sessions")); + staffGroup->setTitle(tr("Staff Last Logins")); altsTable->setHorizontalHeaderLabels({tr("User"), tr("eMail"), tr("Client ID"), tr("Registered"), tr("Last login"), tr("Warns"), tr("Bans"), tr("Active")}); - altsTable->setToolTip(tr("Double-click or press Enter on a row to open it in the User Alts tab")); sessionsTable->setHorizontalHeaderLabels({tr("IP"), tr("Client ID"), tr("Start"), tr("End"), tr("Type")}); staffTable->setHorizontalHeaderLabels({tr("User"), tr("Level"), tr("Last login")}); } QString TabModeration::formatEpoch(quint64 ts) const { - return formatEpochTimestamp(ts, tr("Unknown")); + if (ts == 0) { + return tr("Unknown"); + } + return QDateTime::fromSecsSinceEpoch(ts).toLocalTime().toString("yyyy-MM-dd HH:mm"); } void TabModeration::clearUserData() { currentUser.clear(); - cachedUserInfoValid = false; - cachedAltsCount = -1; userInfoNameValue->clear(); + userInfoRegisteredValue->clear(); + userInfoLastLoginValue->clear(); userInfoStatusValue->clear(); - sublineLabel->clear(); - cardReportsValue->setText(QStringLiteral("–")); - cardWarnsValue->setText(QStringLiteral("–")); - cardBansValue->setText(QStringLiteral("–")); - altsCountValue->setText(QStringLiteral("–")); + userInfoCountsValue->clear(); userInfoNotesEdit->clear(); altsTable->setRowCount(0); sessionsTable->setRowCount(0); resetPasswordButton->setEnabled(false); removeAvatarButton->setEnabled(false); removeBannerButton->setEnabled(false); - openAltsButton->setEnabled(false); } void TabModeration::investigate(const QString &userName) @@ -307,13 +253,9 @@ void TabModeration::investigateUser() return; } currentUser = userName; - cachedUserInfoValid = false; - cachedAltsCount = -1; resetPasswordButton->setEnabled(true); removeAvatarButton->setEnabled(true); removeBannerButton->setEnabled(true); - openAltsButton->setEnabled(true); - altsCountValue->setText(QStringLiteral("…")); altsTable->setRowCount(0); sessionsTable->setRowCount(0); requestUserInfo(userName); @@ -321,22 +263,13 @@ void TabModeration::investigateUser() requestAlts(userName); } -void TabModeration::openAltsGraph() -{ - if (currentUser.isEmpty()) { - return; - } - getTabSupervisor()->openTabAlts(currentUser); -} - void TabModeration::requestUserInfo(const QString &userName) { userInfoNameValue->setText(userName); - sublineLabel->setText(tr("Loading...")); + userInfoRegisteredValue->setText(tr("Loading...")); + userInfoLastLoginValue->setText(tr("Loading...")); userInfoStatusValue->clear(); - cardReportsValue->setText(QStringLiteral("…")); - cardWarnsValue->setText(QStringLiteral("…")); - cardBansValue->setText(QStringLiteral("…")); + userInfoCountsValue->clear(); userInfoNotesEdit->clear(); Command_ReportUserInfo cmd; @@ -349,12 +282,9 @@ void TabModeration::requestUserInfo(const QString &userName) void TabModeration::userInfoResponse(const Response &response) { if (response.response_code() != Response::RespOk) { - cachedUserInfoValid = false; - sublineLabel->clear(); + userInfoRegisteredValue->clear(); + userInfoLastLoginValue->clear(); userInfoStatusValue->setText(tr("Error loading user info.")); - cardReportsValue->setText(QStringLiteral("–")); - cardWarnsValue->setText(QStringLiteral("–")); - cardBansValue->setText(QStringLiteral("–")); return; } @@ -363,28 +293,20 @@ void TabModeration::userInfoResponse(const Response &response) return; } - cachedUserInfo.CopyFrom(resp); - cachedUserInfoValid = true; - applyUserInfo(); -} - -void TabModeration::applyUserInfo() -{ - const Response_ReportUserInfo &resp = cachedUserInfo; - sublineLabel->setText(tr("Registered %1 · Last login %2") - .arg(formatEpoch(resp.registration_time())) - .arg(formatEpoch(resp.last_login()))); + userInfoRegisteredValue->setText(formatEpoch(resp.registration_time())); + userInfoLastLoginValue->setText(formatEpoch(resp.last_login())); QStringList statusParts; statusParts << (resp.is_active() ? tr("active") : tr("inactive")); if (resp.has_is_admin() && resp.is_admin()) { statusParts << tr("admin"); } - userInfoStatusValue->setText(statusParts.join(QStringLiteral(", "))); + userInfoStatusValue->setText(statusParts.join(", ")); - cardReportsValue->setText(QString::number(resp.total_reports())); - cardWarnsValue->setText(QString::number(resp.total_warns())); - cardBansValue->setText(QString::number(resp.total_bans())); + userInfoCountsValue->setText(tr("Reports: %1 Bans: %2 Warnings: %3") + .arg(resp.total_reports()) + .arg(resp.total_bans()) + .arg(resp.total_warns())); if (resp.has_admin_notes() && !resp.admin_notes().empty()) { userInfoNotesEdit->setPlainText(QString::fromStdString(resp.admin_notes())); @@ -393,21 +315,12 @@ void TabModeration::applyUserInfo() } } -void TabModeration::applyAltsCount() -{ - altsCountValue->setText(tr("%1 related").arg(cachedAltsCount)); -} - void TabModeration::requestSessions(const QString &userName) { Command_GetUserSessions cmd; cmd.set_user_name(userName.toStdString()); PendingCommand *pend = client->prepareModeratorCommand(cmd); - connect(pend, &PendingCommand::finished, this, [this, userName](const Response &response) { - if (userName == currentUser) { - sessionsResponse(response); - } - }); + connect(pend, &PendingCommand::finished, this, &TabModeration::sessionsResponse); client->sendCommand(pend); } @@ -439,11 +352,7 @@ void TabModeration::requestAlts(const QString &userName) Command_GetUserAlts cmd; cmd.set_user_name(userName.toStdString()); PendingCommand *pend = client->prepareModeratorCommand(cmd); - connect(pend, &PendingCommand::finished, this, [this, userName](const Response &response) { - if (userName == currentUser) { - altsResponse(response); - } - }); + connect(pend, &PendingCommand::finished, this, &TabModeration::altsResponse); client->sendCommand(pend); } @@ -451,21 +360,10 @@ void TabModeration::altsResponse(const Response &response) { altsTable->setRowCount(0); if (response.response_code() != Response::RespOk) { - cachedAltsCount = -1; - altsCountValue->setText(QStringLiteral("–")); return; } const Response_UserAlts &resp = response.GetExtension(Response_UserAlts::ext); - // The server always includes the frontier user as a depth-0 seed row. - int relatedCount = 0; - for (int i = 0; i < resp.alts_size(); ++i) { - if (!resp.alts(i).has_depth() || resp.alts(i).depth() > 0) { - ++relatedCount; - } - } - cachedAltsCount = relatedCount; - applyAltsCount(); altsTable->setRowCount(resp.alts_size()); for (int i = 0; i < resp.alts_size(); ++i) { const ServerInfo_UserAlt &alt = resp.alts(i); diff --git a/cockatrice/src/interface/widgets/tabs/tab_moderation.h b/cockatrice/src/interface/widgets/tabs/tab_moderation.h index 9ba123493..ce23b5a7a 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_moderation.h +++ b/cockatrice/src/interface/widgets/tabs/tab_moderation.h @@ -5,7 +5,6 @@ #include #include -#include #include class AbstractClient; @@ -14,7 +13,6 @@ class QLabel; class QLineEdit; class QPushButton; class QTableWidget; -class QTabWidget; class QTextEdit; class Response; class TabSupervisor; @@ -38,7 +36,6 @@ public: private slots: void investigateUser(); - void openAltsGraph(); void userInfoResponse(const Response &response); void sessionsResponse(const Response &response); void altsResponse(const Response &response); @@ -56,15 +53,10 @@ private: void requestAlts(const QString &userName); void requestModeratorLogins(); void clearUserData(); - void applyUserInfo(); - void applyAltsCount(); [[nodiscard]] QString formatEpoch(quint64 ts) const; AbstractClient *client; QString currentUser; - Response_ReportUserInfo cachedUserInfo; - bool cachedUserInfoValid = false; - int cachedAltsCount = -1; QLineEdit *searchEdit; QPushButton *investigateButton; @@ -73,25 +65,25 @@ private: QPushButton *removeBannerButton; QGroupBox *userInfoGroup; + QLabel *userInfoNameLabel; QLabel *userInfoNameValue; + QLabel *userInfoRegisteredLabel; + QLabel *userInfoRegisteredValue; + QLabel *userInfoLastLoginLabel; + QLabel *userInfoLastLoginValue; + QLabel *userInfoStatusLabel; QLabel *userInfoStatusValue; - QLabel *sublineLabel; - QGroupBox *cardReports; - QGroupBox *cardWarns; - QGroupBox *cardBans; - QGroupBox *altsCard; - QLabel *cardReportsValue; - QLabel *cardWarnsValue; - QLabel *cardBansValue; - QLabel *altsCountValue; + QLabel *userInfoCountsLabel; + QLabel *userInfoCountsValue; QLabel *userInfoNotesLabel; QTextEdit *userInfoNotesEdit; - QTabWidget *sectionTabs; + QGroupBox *altsGroup; QTableWidget *altsTable; + QGroupBox *sessionsGroup; QTableWidget *sessionsTable; + QGroupBox *staffGroup; QTableWidget *staffTable; - QPushButton *openAltsButton; QPushButton *refreshStaffButton; }; diff --git a/cockatrice/src/interface/widgets/tabs/tab_public_decks.cpp b/cockatrice/src/interface/widgets/tabs/tab_public_decks.cpp index dfde08f8e..dc14b6e4a 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_public_decks.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_public_decks.cpp @@ -2,7 +2,6 @@ #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" @@ -64,8 +63,8 @@ TabPublicDecks::TabPublicDecks(TabSupervisor *_tabSupervisor, AbstractClient *_c 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 +79,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 d934a340b..7986286b3 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_report.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_report.cpp @@ -5,7 +5,6 @@ #include "../utility/report_utils.h" #include "abstract_client.h" #include "libcockatrice/protocol/pb/response.pb.h" -#include "report_status_delegate.h" #include "tab_supervisor.h" #include @@ -25,7 +24,7 @@ #include #include #include -#include +#include #include #include #include @@ -127,43 +126,26 @@ TabReport::TabReport(TabSupervisor *_tabSupervisor, AbstractClient *_client) : T connect(table, &QTableWidget::itemSelectionChanged, this, &TabReport::onSelectionChanged); table->setContextMenuPolicy(Qt::CustomContextMenu); connect(table, &QTableWidget::customContextMenuRequested, this, &TabReport::showTableContextMenu); - table->setItemDelegateForColumn(COL_STATUS, new ReportStatusDelegate(table)); userContextMenu = new UserContextMenu(tabSupervisor, this); connect(userContextMenu, &UserContextMenu::openMessageDialog, this, &TabReport::openMessageDialog); - reportHeaderName = new QLabel; - QFont displayFont = reportHeaderName->font(); - displayFont.setPointSize(displayFont.pointSize() + 6); - displayFont.setBold(true); - reportHeaderName->setFont(displayFont); - reportHeaderStatus = new QLabel; - QFont statusFont = reportHeaderStatus->font(); - statusFont.setBold(true); - reportHeaderStatus->setFont(statusFont); - reportHeaderSubline = new QLabel; - - auto *reportIdentityRow = new QHBoxLayout; - reportIdentityRow->addWidget(reportHeaderName); - reportIdentityRow->addStretch(); - reportIdentityRow->addWidget(reportHeaderStatus); - - auto *descPage = new QWidget; + descGroup = new QGroupBox; descriptionEdit = new QTextEdit; descriptionEdit->setReadOnly(true); - auto *descLayout = new QVBoxLayout(descPage); + auto *descLayout = new QVBoxLayout(descGroup); descLayout->setContentsMargins(4, 4, 4, 4); descLayout->addWidget(descriptionEdit); - auto *chatPage = new QWidget; + chatGroup = new QGroupBox; chatLogEdit = new QTextEdit; chatLogEdit->setReadOnly(true); applyChatLogFont(); connect(themeManager, &ThemeManager::fontsChanged, this, &TabReport::applyChatLogFont); - auto *chatLayout = new QVBoxLayout(chatPage); + auto *chatLayout = new QVBoxLayout(chatGroup); chatLayout->setContentsMargins(4, 4, 4, 4); chatLayout->addWidget(chatLogEdit); - auto *commentsPage = new QWidget; + commentsGroup = new QGroupBox; commentsEdit = new QTextEdit; commentsEdit->setReadOnly(true); auto *commentInputRow = new QHBoxLayout; @@ -174,12 +156,12 @@ TabReport::TabReport(TabSupervisor *_tabSupervisor, AbstractClient *_client) : T connect(commentInput, &QLineEdit::returnPressed, this, &TabReport::addComment); commentInputRow->addWidget(commentInput); commentInputRow->addWidget(commentButton); - auto *commentsLayout = new QVBoxLayout(commentsPage); + auto *commentsLayout = new QVBoxLayout(commentsGroup); commentsLayout->setContentsMargins(4, 4, 4, 4); commentsLayout->addWidget(commentsEdit); commentsLayout->addLayout(commentInputRow); - auto *userPage = new QWidget; + userContextGroup = new QGroupBox; userContextName = new QLabel; userContextAccountAge = new QLabel; userContextReports = new QLabel; @@ -198,7 +180,7 @@ TabReport::TabReport(TabSupervisor *_tabSupervisor, AbstractClient *_client) : T userContextWarnsLabel = new QLabel; userContextNotesLabel = new QLabel; userContextRecentReportsLabel = new QLabel; - auto *ucLayout = new QGridLayout(userPage); + auto *ucLayout = new QGridLayout(userContextGroup); ucLayout->setContentsMargins(8, 8, 8, 8); ucLayout->addWidget(userContextUserLabel, 0, 0); ucLayout->addWidget(userContextName, 0, 1, 1, 3); @@ -214,6 +196,7 @@ TabReport::TabReport(TabSupervisor *_tabSupervisor, AbstractClient *_client) : T ucLayout->addWidget(userContextNotes, 3, 1, 1, 3); ucLayout->addWidget(userContextRecentReportsLabel, 4, 0, 1, 4); ucLayout->addWidget(userContextRecentReports, 5, 0, 1, 4); + userContextGroup->setVisible(false); statsGroup = new QGroupBox; statsGroup->setCheckable(true); @@ -240,11 +223,13 @@ TabReport::TabReport(TabSupervisor *_tabSupervisor, AbstractClient *_client) : T } }); - detailTabs = new QTabWidget; - detailTabs->addTab(descPage, QString()); - detailTabs->addTab(chatPage, QString()); - detailTabs->addTab(commentsPage, QString()); - detailTabs->addTab(userPage, QString()); + detailSplitter = new QSplitter(Qt::Vertical); + detailSplitter->addWidget(descGroup); + detailSplitter->addWidget(chatGroup); + detailSplitter->addWidget(commentsGroup); + detailSplitter->setStretchFactor(0, 1); + detailSplitter->setStretchFactor(1, 1); + detailSplitter->setStretchFactor(2, 2); assignButton = new QPushButton; resolveButton = new QPushButton; @@ -276,14 +261,12 @@ TabReport::TabReport(TabSupervisor *_tabSupervisor, AbstractClient *_client) : T layout->addLayout(topBar); layout->addWidget(statsLabel); layout->addWidget(table, 2); - layout->addLayout(reportIdentityRow); - layout->addWidget(reportHeaderSubline); - layout->addWidget(detailTabs, 2); + layout->addWidget(detailSplitter, 1); + layout->addWidget(userContextGroup); layout->addWidget(statsGroup); layout->addLayout(actionBar); retranslateUi(); - clearReportHeader(); setActionsEnabled(false); refreshList(); @@ -310,18 +293,18 @@ void TabReport::retranslateUi() table->setHorizontalHeaderLabels({tr("#"), tr("Time"), tr("Reporter"), tr("Reported User"), tr("Category"), tr("Game ID"), tr("Status"), tr("Assigned To"), tr("Replay"), tr("Room")}); - detailTabs->setTabText(0, tr("Description")); - detailTabs->setTabText(1, tr("Chat Log Context")); - detailTabs->setTabToolTip(1, tr("Chat log attached by the reporter. It is captured from their client and may be " - "incomplete or edited.")); - detailTabs->setTabText(2, tr("Comments / Thread")); - detailTabs->setTabText(3, tr("Reported User")); + descGroup->setTitle(tr("Description")); + chatGroup->setTitle(tr("Chat Log Context")); + chatGroup->setToolTip( + tr("Chat log attached by the reporter. It is captured from their client and may be incomplete or edited.")); + commentsGroup->setTitle(tr("Comments / Thread")); descriptionEdit->setPlaceholderText(tr("No description.")); chatLogEdit->setPlaceholderText(tr("No chat log attached.")); commentsEdit->setPlaceholderText(tr("No comments yet.")); commentInput->setPlaceholderText(tr("Type a reply...")); commentButton->setText(tr("Send")); + userContextGroup->setTitle(tr("Reported User Context")); userContextUserLabel->setText(tr("User:")); userContextAgeLabel->setText(tr("Account Age:")); userContextReportsLabel->setText(tr("Reports:")); @@ -329,18 +312,10 @@ void TabReport::retranslateUi() userContextWarnsLabel->setText(tr("Warns:")); userContextNotesLabel->setText(tr("Admin Notes:")); userContextRecentReportsLabel->setText(tr("Recent Reports Against User:")); - - if (headerReportId >= 0) { - for (const ServerInfo_Report &r : filteredReports) { - if (r.report_id() == headerReportId) { - updateReportHeader(r); - const QString status = QString::fromStdString(r.status()); - const bool canComment = (status == "open" || status == "assigned"); - commentInput->setPlaceholderText(canComment ? tr("Type a reply...") : tr("This report is closed.")); - break; - } - } - } + userContextAccountAge->setText(QString()); + userContextReports->setText(QString()); + userContextBans->setText(QString()); + userContextWarns->setText(QString()); statsGroup->setTitle(tr("Report Statistics")); @@ -433,8 +408,7 @@ void TabReport::reportListResponse(const Response &response) chatLogEdit->clear(); commentsEdit->clear(); setActionsEnabled(false); - clearReportHeader(); - clearUserContext(); + userContextGroup->setVisible(false); } } @@ -471,14 +445,11 @@ void TabReport::applyFilters() table->setSortingEnabled(false); table->setRowCount(filteredReports.size()); - QFont reportedFont = table->font(); - reportedFont.setBold(true); - for (int row = 0; row < filteredReports.size(); ++row) { const ServerInfo_Report &r = filteredReports[row]; report_utils::fillReportTableRow(table, row, r, COL_ID, COL_TIME, COL_REPORTED, COL_CATEGORY, COL_GAMEID, - COL_STATUS, COL_ASSIGNED, false); + COL_STATUS, COL_ASSIGNED); table->setItem(row, COL_REPORTER, new QTableWidgetItem(QString::fromStdString(r.reporter_name()))); @@ -487,30 +458,12 @@ void TabReport::applyFilters() table->setItem(row, COL_ROOM, new QTableWidgetItem(r.has_room_id() && r.room_id() > 0 ? QString::number(r.room_id()) : "")); - - if (auto *reportedItem = table->item(row, COL_REPORTED)) { - reportedItem->setFont(reportedFont); - } } table->setSortingEnabled(true); table->resizeColumnsToContents(); table->horizontalHeader()->setSectionResizeMode(COL_REPORTED, QHeaderView::Stretch); - if (headerReportId >= 0) { - bool stillVisible = false; - for (const ServerInfo_Report &r : filteredReports) { - if (r.report_id() == headerReportId) { - stillVisible = true; - break; - } - } - if (!stillVisible) { - clearReportHeader(); - clearUserContext(); - } - } - statusLabel->setText(tr("%1 report(s)").arg(filteredReports.size())); } @@ -537,31 +490,6 @@ void TabReport::updateStats() .arg(dismissed)); } -void TabReport::updateReportHeader(const ServerInfo_Report &report) -{ - reportHeaderName->setText(QString::fromStdString(report.reported_user_name())); - reportHeaderStatus->setText(report_utils::formatReportStatus(QString::fromStdString(report.status()))); - - QStringList parts; - parts << QString::fromStdString(report.category()); - parts << QDateTime::fromSecsSinceEpoch(report.report_time()).toLocalTime().toString("yyyy-MM-dd HH:mm"); - if (report.has_room_id() && report.room_id() > 0) { - parts << tr("Room %1").arg(report.room_id()); - } - if (report.game_id() > 0) { - parts << tr("Game %1").arg(report.game_id()); - } - reportHeaderSubline->setText(parts.join(QStringLiteral(" · "))); -} - -void TabReport::clearReportHeader() -{ - headerReportId = -1; - reportHeaderName->setText(tr("Select a report to inspect.")); - reportHeaderStatus->clear(); - reportHeaderSubline->clear(); -} - void TabReport::onSelectionChanged() { const int row = table->currentRow(); @@ -572,8 +500,7 @@ void TabReport::onSelectionChanged() commentInput->clear(); commentButton->setEnabled(false); setActionsEnabled(false); - clearReportHeader(); - clearUserContext(); + userContextGroup->setVisible(false); return; } @@ -582,18 +509,6 @@ void TabReport::onSelectionChanged() updateActionStates(); } -void TabReport::clearUserContext() -{ - lastRequestedUser.clear(); - userContextName->setText(tr("—")); - userContextAccountAge->clear(); - userContextReports->clear(); - userContextBans->clear(); - userContextWarns->clear(); - userContextNotes->setPlainText(tr("Select a report to inspect.")); - userContextRecentReports->clear(); -} - void TabReport::showTableContextMenu(const QPoint &pos) { QTableWidgetItem *clickedItem = table->itemAt(pos); @@ -631,17 +546,12 @@ void TabReport::loadReportDetails(int reportId) for (const ServerInfo_Report &r : filteredReports) { if (r.report_id() == reportId) { descriptionEdit->setPlainText(QString::fromStdString(r.description())); - updateReportHeader(r); - if (headerReportId != reportId) { - headerReportId = reportId; - detailTabs->setCurrentIndex(0); - } QString reportedUser = QString::fromStdString(r.reported_user_name()); if (!reportedUser.isEmpty()) { requestUserInfo(reportedUser); } else { - clearUserContext(); + userContextGroup->setVisible(false); } QString status = QString::fromStdString(r.status()); @@ -961,11 +871,12 @@ void TabReport::addCommentResponse(const Response &response) void TabReport::requestUserInfo(const QString &userName) { if (userName.isEmpty()) { - clearUserContext(); + userContextGroup->setVisible(false); return; } lastRequestedUser = userName; + userContextGroup->setVisible(true); userContextName->setText(userName); userContextAccountAge->setText(tr("Loading...")); userContextReports->clear(); diff --git a/cockatrice/src/interface/widgets/tabs/tab_report.h b/cockatrice/src/interface/widgets/tabs/tab_report.h index aea42648e..43e778f4b 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_report.h +++ b/cockatrice/src/interface/widgets/tabs/tab_report.h @@ -16,8 +16,8 @@ class QGroupBox; class QLabel; class QLineEdit; class QPushButton; +class QSplitter; class QTableWidget; -class QTabWidget; class QTextEdit; class QTimer; class GameReplay; @@ -72,9 +72,6 @@ private: void applyFilters(); void updateStats(); void loadReportDetails(int reportId); - void updateReportHeader(const ServerInfo_Report &report); - void clearReportHeader(); - void clearUserContext(); AbstractClient *client; @@ -84,10 +81,10 @@ private: QPushButton *refreshButton; QLabel *statsLabel; QTableWidget *table; - QLabel *reportHeaderName; - QLabel *reportHeaderStatus; - QLabel *reportHeaderSubline; - QTabWidget *detailTabs; + QSplitter *detailSplitter; + QGroupBox *descGroup; + QGroupBox *chatGroup; + QGroupBox *commentsGroup; QTextEdit *descriptionEdit; QTextEdit *chatLogEdit; QTextEdit *commentsEdit; @@ -103,6 +100,7 @@ private: QTimer *refreshTimer; UserContextMenu *userContextMenu; + QGroupBox *userContextGroup; QLabel *userContextName; QLabel *userContextAccountAge; QLabel *userContextReports; @@ -127,7 +125,6 @@ private: QList allReports; QList filteredReports; - int headerReportId = -1; int detailsRequestedReportId = -1; int detailsRequestSeq = 0; int selectedReportIdBeforeRefresh = -1; 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 7647b8ca8..cf71dd7ed 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" @@ -22,7 +21,6 @@ #include "tab.h" #include "tab_account.h" #include "tab_admin.h" -#include "tab_alts.h" #include "tab_card_art_rules.h" #include "tab_deck_editor.h" #include "tab_deck_storage.h" @@ -158,7 +156,7 @@ TabSupervisor::TabSupervisor(AbstractClient *_client, QMenu *tabsMenu, QWidget * : QTabWidget(parent), userInfo(nullptr), client(_client), tabsMenu(tabsMenu), tabHome(nullptr), tabVisualDeckStorage(nullptr), tabServer(nullptr), tabAccount(nullptr), tabDeckStorage(nullptr), tabReplays(nullptr), tabAdmin(nullptr), tabCardArtRules(nullptr), tabLog(nullptr), tabReport(nullptr), - tabModeration(nullptr), tabAlts(nullptr), tabDeveloper(nullptr), isLocalGame(false) + tabModeration(nullptr), tabDeveloper(nullptr), isLocalGame(false) { setObjectName("tabSupervisor"); @@ -576,7 +574,6 @@ void TabSupervisor::startLocal(const QList &_clients) tabLog = nullptr; tabReport = nullptr; tabModeration = nullptr; - tabAlts = nullptr; tabDeveloper = nullptr; isLocalGame = true; userInfo = new ServerInfo_User; @@ -625,9 +622,6 @@ void TabSupervisor::stop() if (tabModeration) { tabModeration->close(); } - if (tabAlts) { - tabAlts->close(); - } if (tabCardArtRules) { tabCardArtRules->close(); } @@ -936,21 +930,6 @@ void TabSupervisor::openTabModeration(const QString &userName) aTabModeration->setChecked(true); } -void TabSupervisor::openTabAlts(const QString &userName) -{ - if (tabAlts) { - setCurrentWidget(tabAlts); - if (!userName.isEmpty()) { - tabAlts->investigate(userName); - } - return; - } - tabAlts = new TabAlts(this, client, userName); - myAddTab(tabAlts); - setCurrentWidget(tabAlts); - connect(tabAlts, &QObject::destroyed, this, [this] { tabAlts = nullptr; }); -} - void TabSupervisor::actTabDeveloper(bool checked) { if (checked && !tabDeveloper) { @@ -1381,7 +1360,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/tab_supervisor.h b/cockatrice/src/interface/widgets/tabs/tab_supervisor.h index d529c5022..9c7dc48d1 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_supervisor.h +++ b/cockatrice/src/interface/widgets/tabs/tab_supervisor.h @@ -50,7 +50,6 @@ class TabAdmin; class TabMessage; class TabReport; class TabModeration; -class TabAlts; class TabAccount; class TabDeckEditor; class TabDeveloper; @@ -117,7 +116,6 @@ private: TabLog *tabLog; TabReport *tabReport; TabModeration *tabModeration; - TabAlts *tabAlts; TabDeveloper *tabDeveloper; QMap roomTabs; QMap gameTabs; @@ -211,7 +209,6 @@ public slots: /** Joins or spectates a game listed on a user's profile hover card. */ void joinGameFromUserCard(int gameId, int roomId, bool asSpectator); void openTabModeration(const QString &userName = {}); - void openTabAlts(const QString &userName = {}); void switchToFirstAvailableNetworkTab(); void maximizeMainWindow(); void actTabVisualDeckStorage(bool checked); diff --git a/cockatrice/src/interface/widgets/utility/epoch_format.h b/cockatrice/src/interface/widgets/utility/epoch_format.h deleted file mode 100644 index 977053997..000000000 --- a/cockatrice/src/interface/widgets/utility/epoch_format.h +++ /dev/null @@ -1,27 +0,0 @@ -/** - * @file epoch_format.h - * @brief Shared timestamp formatting for the moderation tables. - */ - -#ifndef EPOCH_FORMAT_H -#define EPOCH_FORMAT_H - -#include -#include - -/** - * @brief Format a unix timestamp the way the moderation tabs show dates. - * - * @param ts Seconds since the epoch; 0 means "unknown". - * @param unknownText Label returned for 0, translated by the caller so the - * translation context stays with the tab. - */ -inline QString formatEpochTimestamp(quint64 ts, const QString &unknownText) -{ - if (ts == 0) { - return unknownText; - } - return QDateTime::fromSecsSinceEpoch(ts).toLocalTime().toString(QStringLiteral("yyyy-MM-dd HH:mm")); -} - -#endif // EPOCH_FORMAT_H diff --git a/cockatrice/src/interface/widgets/utility/pill_painter.cpp b/cockatrice/src/interface/widgets/utility/pill_painter.cpp deleted file mode 100644 index 6f317b951..000000000 --- a/cockatrice/src/interface/widgets/utility/pill_painter.cpp +++ /dev/null @@ -1,41 +0,0 @@ -#include "pill_painter.h" - -#include -#include -#include -#include -#include -#include - -namespace -{ -constexpr qreal PILL_ALPHA = 0.30; -} // namespace - -bool pill_painter::paint(QPainter *painter, - const QStyleOptionViewItem &option, - QPalette::ColorRole tintRole, - QPalette::ColorRole textRole, - const QString &text) -{ - const QRect pillRect = option.rect.adjusted(PAD_X / 2, PAD_Y / 2, -PAD_X / 2, -PAD_Y / 2); - if (pillRect.width() <= 0 || pillRect.height() <= 0) { - return false; - } - - if ((option.state & QStyle::State_Selected) == 0) { - painter->save(); - painter->setRenderHint(QPainter::Antialiasing, true); - QColor pill = option.palette.color(tintRole); - pill.setAlphaF(PILL_ALPHA); - painter->setPen(Qt::NoPen); - painter->setBrush(pill); - painter->drawRoundedRect(pillRect, pillRect.height() / 2.0, pillRect.height() / 2.0); - painter->restore(); - } - - QStyle *style = option.widget != nullptr ? option.widget->style() : QApplication::style(); - const QString elided = option.fontMetrics.elidedText(text, option.textElideMode, pillRect.width()); - style->drawItemText(painter, pillRect, Qt::AlignCenter, option.palette, true, elided, textRole); - return true; -} diff --git a/cockatrice/src/interface/widgets/utility/pill_painter.h b/cockatrice/src/interface/widgets/utility/pill_painter.h deleted file mode 100644 index 72e3fe0b4..000000000 --- a/cockatrice/src/interface/widgets/utility/pill_painter.h +++ /dev/null @@ -1,29 +0,0 @@ -#ifndef PILL_PAINTER_H -#define PILL_PAINTER_H - -#include -#include - -class QPainter; -class QStyleOptionViewItem; - -namespace pill_painter -{ -constexpr int PAD_X = 10; -constexpr int PAD_Y = 4; - -/** - * Draws a translucent, palette-tinted rounded pill inside the option's rect and - * paints the text elided and centred on it, so badges follow the active theme. - * Returns false when the cell is too small to hold a pill, leaving the caller to - * fall back to default painting. - */ -bool paint(QPainter *painter, - const QStyleOptionViewItem &option, - QPalette::ColorRole tintRole, - QPalette::ColorRole textRole, - const QString &text); - -} // namespace pill_painter - -#endif // PILL_PAINTER_H diff --git a/cockatrice/src/interface/widgets/utility/report_utils.cpp b/cockatrice/src/interface/widgets/utility/report_utils.cpp index bdbc24f32..df2400b14 100644 --- a/cockatrice/src/interface/widgets/utility/report_utils.cpp +++ b/cockatrice/src/interface/widgets/utility/report_utils.cpp @@ -80,23 +80,6 @@ QString formatReportTime(qint64 secondsSinceEpoch) return dt.toString("yyyy-MM-dd hh:mm"); } -QString formatReportStatus(const QString &status) -{ - if (status == QStringLiteral("open")) { - return QObject::tr("Open"); - } - if (status == QStringLiteral("assigned")) { - return QObject::tr("Assigned"); - } - if (status == QStringLiteral("resolved")) { - return QObject::tr("Resolved"); - } - if (status == QStringLiteral("dismissed")) { - return QObject::tr("Dismissed"); - } - return status; -} - void fillReportTableRow(QTableWidget *table, int row, const ServerInfo_Report &report, @@ -106,8 +89,7 @@ void fillReportTableRow(QTableWidget *table, int categoryColumn, int gameIdColumn, int statusColumn, - int assignedColumn, - bool colorizeStatus) + int assignedColumn) { auto *idItem = new QTableWidgetItem(QString::number(report.report_id())); idItem->setData(Qt::UserRole, report.report_id()); @@ -120,12 +102,8 @@ void fillReportTableRow(QTableWidget *table, table->setItem(row, gameIdColumn, new QTableWidgetItem(report.game_id() > 0 ? QString::number(report.game_id()) : QString())); - const QString rawStatus = QString::fromStdString(report.status()); - auto *statusItem = new QTableWidgetItem(formatReportStatus(rawStatus)); - statusItem->setData(Qt::UserRole, rawStatus); - if (colorizeStatus) { - setReportStatusItemColor(statusItem, rawStatus); - } + auto *statusItem = new QTableWidgetItem(QString::fromStdString(report.status())); + setReportStatusItemColor(statusItem, QString::fromStdString(report.status())); table->setItem(row, statusColumn, statusItem); table->setItem(row, assignedColumn, new QTableWidgetItem(QString::fromStdString(report.assigned_mod_name()))); diff --git a/cockatrice/src/interface/widgets/utility/report_utils.h b/cockatrice/src/interface/widgets/utility/report_utils.h index f81e01369..32cc19eac 100644 --- a/cockatrice/src/interface/widgets/utility/report_utils.h +++ b/cockatrice/src/interface/widgets/utility/report_utils.h @@ -20,7 +20,6 @@ QFont reportLogFont(); QString formatReportCategory(const QString &category); QString formatReportTime(qint64 secondsSinceEpoch); -QString formatReportStatus(const QString &status); void fillReportTableRow(QTableWidget *table, int row, @@ -31,8 +30,7 @@ void fillReportTableRow(QTableWidget *table, int categoryColumn, int gameIdColumn, int statusColumn, - int assignedColumn, - bool colorizeStatus = true); + int assignedColumn); void renderReportDetails(QTextEdit *chatLogEdit, QTextEdit *commentsEdit, 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/utility/table_activation.cpp b/cockatrice/src/interface/widgets/utility/table_activation.cpp deleted file mode 100644 index 5633b9f58..000000000 --- a/cockatrice/src/interface/widgets/utility/table_activation.cpp +++ /dev/null @@ -1,28 +0,0 @@ -#include "table_activation.h" - -#include -#include -#include - -namespace -{ -void addActivationShortcut(QTableWidget *table, - QObject *context, - int key, - const std::function &handler) -{ - auto *shortcut = new QShortcut(QKeySequence(key), table); - // The table's viewport takes focus, so match children as well. - shortcut->setContext(Qt::WidgetWithChildrenShortcut); - QObject::connect(shortcut, &QShortcut::activated, context, [table, handler] { handler(table->currentIndex()); }); -} -} // namespace - -void connectTableActivation(QTableWidget *table, - QObject *context, - const std::function &handler) -{ - QObject::connect(table, &QTableWidget::doubleClicked, context, handler); - addActivationShortcut(table, context, Qt::Key_Return, handler); - addActivationShortcut(table, context, Qt::Key_Enter, handler); -} diff --git a/cockatrice/src/interface/widgets/utility/table_activation.h b/cockatrice/src/interface/widgets/utility/table_activation.h deleted file mode 100644 index f5038bdb0..000000000 --- a/cockatrice/src/interface/widgets/utility/table_activation.h +++ /dev/null @@ -1,32 +0,0 @@ -/** - * @file table_activation.h - * @brief Row activation for tables that must not navigate on a single click. - */ - -#ifndef TABLE_ACTIVATION_H -#define TABLE_ACTIVATION_H - -#include - -class QModelIndex; -class QObject; -class QTableWidget; - -/** - * @brief Call @p handler on double-click or Enter, never on a single click. - * - * QAbstractItemView::activated fires on a single click on platforms where - * SH_ItemView_ActivateItemOnSingleClick is set, so navigation actions must not - * use it. This connects doubleClicked and installs Return/Enter shortcuts that - * forward the current index. - * - * @param table The table whose rows activate. - * @param context Object that owns the connections; the handler is disconnected - * with it. - * @param handler Receives the activated row's index, which may be invalid. - */ -void connectTableActivation(QTableWidget *table, - QObject *context, - const std::function &handler); - -#endif // TABLE_ACTIVATION_H 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_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/visual_deck_storage_model.cpp b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_model.cpp index f9409f5f2..3611a8c1f 100644 --- a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_model.cpp +++ b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_model.cpp @@ -10,7 +10,7 @@ #include #include #include -#include +#include #include #include #include @@ -244,15 +244,15 @@ void VisualDeckStorageModel::startScan() // The scan (directory walk + one stat per file) runs on a worker thread so that // constructing the widget never stalls the UI thread on a large deck folder. - // The continuation is bound to this model as its context object: it runs on the UI thread, - // and Qt drops it if the model is destroyed first, so no result is delivered to a dead model. - QFuture scan = - QtConcurrent::run([currentDeckPath]() -> DeckScanResult { return scanDeckDirectory(currentDeckPath); }); - scan.then(this, [this, generation](const DeckScanResult &result) { + auto *watcher = new QFutureWatcher(this); + connect(watcher, &QFutureWatcher::finished, this, [this, watcher, generation] { + watcher->deleteLater(); + if (generation != scanGeneration) { return; // A newer scan started while this one was running; drop the stale result. } + const DeckScanResult result = watcher->result(); folderPaths = result.folderPaths; if (result.decks.isEmpty()) { @@ -268,6 +268,9 @@ void VisualDeckStorageModel::startScan() beginLoad(row); } }); + + watcher->setFuture( + QtConcurrent::run([currentDeckPath]() -> DeckScanResult { return scanDeckDirectory(currentDeckPath); })); } void VisualDeckStorageModel::beginLoad(int row) @@ -283,7 +286,32 @@ void VisualDeckStorageModel::beginLoad(int row) const DeckFileFormat::Format fmt = DeckFileFormat::getFormatFromName(filePath); const int generation = scanGeneration; - auto load = QtConcurrent::run([filePath, fmt]() -> std::optional { + auto *watcher = new QFutureWatcher>(this); + connect(watcher, &QFutureWatcher>::finished, this, + [this, watcher, filePath, generation] { + watcher->deleteLater(); + + if (generation != scanGeneration) { + return; // The deck list was re-scanned while this load was running; drop the stale result. + } + + // Queue the result and apply a bounded number per event loop turn so that + // finishing hundreds of loads at once cannot stall the UI thread. + const std::optional result = watcher->result(); + PendingDeckLoad pending; + pending.filePath = filePath; + pending.generation = generation; + if (result) { + pending.ok = true; + pending.deck = std::move(result->deck); + pending.lastModified = result->lastModified; + pending.colorIdentity = std::move(result->colorIdentity); + } + pendingLoads.append(std::move(pending)); + schedulePendingLoadDrain(); + }); + + watcher->setFuture(QtConcurrent::run([filePath, fmt]() -> std::optional { std::optional deck = DeckLoader::loadFromFile(filePath, fmt, false); if (!deck) { return std::nullopt; @@ -292,28 +320,7 @@ void VisualDeckStorageModel::beginLoad(int row) // keep the completion handler on the UI thread cheap. const QString colorIdentity = getDeckColorIdentity(deck->deckList, CardDatabaseManager::query()); return DeckLoadResult{std::move(*deck), QFileInfo(filePath).lastModified(), colorIdentity}; - }); - - // Bound to this model as context: runs on the UI thread and is dropped if the model is destroyed first. - load.then(this, [this, filePath, generation](std::optional result) { - if (generation != scanGeneration) { - return; // The deck list was re-scanned while this load was running; drop the stale result. - } - - // Queue the result and apply a bounded number per event loop turn so that - // finishing hundreds of loads at once cannot stall the UI thread. - PendingDeckLoad pending; - pending.filePath = filePath; - pending.generation = generation; - if (result) { - pending.ok = true; - pending.deck = std::move(result->deck); - pending.lastModified = result->lastModified; - pending.colorIdentity = std::move(result->colorIdentity); - } - pendingLoads.append(std::move(pending)); - schedulePendingLoadDrain(); - }); + })); } void VisualDeckStorageModel::schedulePendingLoadDrain() 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..79a802234 100644 --- a/cockatrice/src/interface/window_main.cpp +++ b/cockatrice/src/interface/window_main.cpp @@ -50,7 +50,6 @@ #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" @@ -388,10 +387,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 */ diff --git a/docs/xsd/cards_v3.xsd b/doc/carddatabase_v3/cards.xsd similarity index 100% rename from docs/xsd/cards_v3.xsd rename to doc/carddatabase_v3/cards.xsd diff --git a/docs/xsd/cards.xsd b/doc/carddatabase_v4/cards.xsd similarity index 100% rename from docs/xsd/cards.xsd rename to doc/carddatabase_v4/cards.xsd diff --git a/docs/xsd/deck.xsd b/doc/deck.xsd similarity index 100% rename from docs/xsd/deck.xsd rename to doc/deck.xsd diff --git a/docs/doxygen/DoxygenLayout.xml b/doc/doxygen/DoxygenLayout.xml similarity index 100% rename from docs/doxygen/DoxygenLayout.xml rename to doc/doxygen/DoxygenLayout.xml diff --git a/docs/doxygen/README.md b/doc/doxygen/README.md similarity index 80% rename from docs/doxygen/README.md rename to doc/doxygen/README.md index ca4acc1fc..230422303 100644 --- a/docs/doxygen/README.md +++ b/doc/doxygen/README.md @@ -1,11 +1,11 @@ ## Building Doxygen Documentation We use [Doxygen](https://www.doxygen.nl) for our code documentation. -After installing the tool, you can build a HTML webpage into the `/docs-output` folder according to our [configuration](https://github.com/Cockatrice/Cockatrice/blob/master/Doxyfile): +After installing the tool, you can build a HTML webpage into the `/docs` folder according to our [configuration](https://github.com/Cockatrice/Cockatrice/blob/master/Doxyfile): ```bash doxygen Doxyfile ``` -Open the `index.html` file in the `/docs-output/html/` folder to browse the result locally. +Open the `index.html` file in the `/docs/html/` folder to browse the result locally. There is also an online version we host at the Cockatrice webpage: [cockatrice.github.io/docs](https://cockatrice.github.io/docs) Doxygen's own documenation can be found at [www.doxygen.nl/manual](https://www.doxygen.nl/manual). @@ -25,4 +25,4 @@ FULL_SIDEBAR = NO # Required for doxygen-awesome
-Cockatrice dedicated color adjustments are configured in the [cockatrice_docs_style.css](https://github.com/Cockatrice/Cockatrice/blob/master/docs/doxygen/css/cockatrice_docs_style.css) file that uses & overrides the theme definitions. +Cockatrice dedicated color adjustments are configured in the [cockatrice_docs_style.css](https://github.com/Cockatrice/Cockatrice/blob/master/doc/doxygen/css/cockatrice_docs_style.css) file that uses & overrides the theme definitions. diff --git a/docs/doxygen/css/cockatrice_docs_style.css b/doc/doxygen/css/cockatrice_docs_style.css similarity index 100% rename from docs/doxygen/css/cockatrice_docs_style.css rename to doc/doxygen/css/cockatrice_docs_style.css diff --git a/docs/doxygen/css/doxygen_style_old.css b/doc/doxygen/css/doxygen_style_old.css similarity index 100% rename from docs/doxygen/css/doxygen_style_old.css rename to doc/doxygen/css/doxygen_style_old.css diff --git a/docs/doxygen/css/hide_nav_sync.css b/doc/doxygen/css/hide_nav_sync.css similarity index 100% rename from docs/doxygen/css/hide_nav_sync.css rename to doc/doxygen/css/hide_nav_sync.css diff --git a/docs/doxygen/filters/proto2cpp.py b/doc/doxygen/filters/proto2cpp.py similarity index 100% rename from docs/doxygen/filters/proto2cpp.py rename to doc/doxygen/filters/proto2cpp.py diff --git a/docs/doxygen/groups/doc_groups.dox b/doc/doxygen/groups/doc_groups.dox similarity index 100% rename from docs/doxygen/groups/doc_groups.dox rename to doc/doxygen/groups/doc_groups.dox diff --git a/docs/doxygen/html/footer.html b/doc/doxygen/html/footer.html similarity index 100% rename from docs/doxygen/html/footer.html rename to doc/doxygen/html/footer.html diff --git a/docs/doxygen/html/header.html b/doc/doxygen/html/header.html similarity index 100% rename from docs/doxygen/html/header.html rename to doc/doxygen/html/header.html diff --git a/docs/doxygen/images/cockatrice_logo.svg b/doc/doxygen/images/cockatrice_logo.svg similarity index 100% rename from docs/doxygen/images/cockatrice_logo.svg rename to doc/doxygen/images/cockatrice_logo.svg diff --git a/docs/doxygen/images/screenshots/classic_database_display.png b/doc/doxygen/images/screenshots/classic_database_display.png similarity index 100% rename from docs/doxygen/images/screenshots/classic_database_display.png rename to doc/doxygen/images/screenshots/classic_database_display.png diff --git a/docs/doxygen/images/screenshots/classic_database_display_add_buttons.png b/doc/doxygen/images/screenshots/classic_database_display_add_buttons.png similarity index 100% rename from docs/doxygen/images/screenshots/classic_database_display_add_buttons.png rename to doc/doxygen/images/screenshots/classic_database_display_add_buttons.png diff --git a/docs/doxygen/images/screenshots/classic_deck_editor.png b/doc/doxygen/images/screenshots/classic_deck_editor.png similarity index 100% rename from docs/doxygen/images/screenshots/classic_deck_editor.png rename to doc/doxygen/images/screenshots/classic_deck_editor.png diff --git a/docs/doxygen/images/screenshots/deck_dock_deck_list.png b/doc/doxygen/images/screenshots/deck_dock_deck_list.png similarity index 100% rename from docs/doxygen/images/screenshots/deck_dock_deck_list.png rename to doc/doxygen/images/screenshots/deck_dock_deck_list.png diff --git a/docs/doxygen/images/screenshots/deck_dock_deck_list_buttons.png b/doc/doxygen/images/screenshots/deck_dock_deck_list_buttons.png similarity index 100% rename from docs/doxygen/images/screenshots/deck_dock_deck_list_buttons.png rename to doc/doxygen/images/screenshots/deck_dock_deck_list_buttons.png diff --git a/docs/doxygen/images/screenshots/deck_dock_deck_list_group_by.png b/doc/doxygen/images/screenshots/deck_dock_deck_list_group_by.png similarity index 100% rename from docs/doxygen/images/screenshots/deck_dock_deck_list_group_by.png rename to doc/doxygen/images/screenshots/deck_dock_deck_list_group_by.png diff --git a/docs/doxygen/images/screenshots/deckeditordeckdockwidget.png b/doc/doxygen/images/screenshots/deckeditordeckdockwidget.png similarity index 100% rename from docs/doxygen/images/screenshots/deckeditordeckdockwidget.png rename to doc/doxygen/images/screenshots/deckeditordeckdockwidget.png diff --git a/docs/doxygen/images/screenshots/printing_selector.png b/doc/doxygen/images/screenshots/printing_selector.png similarity index 100% rename from docs/doxygen/images/screenshots/printing_selector.png rename to doc/doxygen/images/screenshots/printing_selector.png diff --git a/docs/doxygen/images/screenshots/printing_selector_disable.png b/doc/doxygen/images/screenshots/printing_selector_disable.png similarity index 100% rename from docs/doxygen/images/screenshots/printing_selector_disable.png rename to doc/doxygen/images/screenshots/printing_selector_disable.png diff --git a/docs/doxygen/images/screenshots/printing_selector_enable.png b/doc/doxygen/images/screenshots/printing_selector_enable.png similarity index 100% rename from docs/doxygen/images/screenshots/printing_selector_enable.png rename to doc/doxygen/images/screenshots/printing_selector_enable.png diff --git a/docs/doxygen/images/screenshots/printing_selector_navigation.png b/doc/doxygen/images/screenshots/printing_selector_navigation.png similarity index 100% rename from docs/doxygen/images/screenshots/printing_selector_navigation.png rename to doc/doxygen/images/screenshots/printing_selector_navigation.png diff --git a/docs/doxygen/images/screenshots/printing_selector_options.png b/doc/doxygen/images/screenshots/printing_selector_options.png similarity index 100% rename from docs/doxygen/images/screenshots/printing_selector_options.png rename to doc/doxygen/images/screenshots/printing_selector_options.png diff --git a/docs/doxygen/images/screenshots/printing_selector_pre_providerid.png b/doc/doxygen/images/screenshots/printing_selector_pre_providerid.png similarity index 100% rename from docs/doxygen/images/screenshots/printing_selector_pre_providerid.png rename to doc/doxygen/images/screenshots/printing_selector_pre_providerid.png diff --git a/docs/doxygen/images/screenshots/release_channel_beta.png b/doc/doxygen/images/screenshots/release_channel_beta.png similarity index 100% rename from docs/doxygen/images/screenshots/release_channel_beta.png rename to doc/doxygen/images/screenshots/release_channel_beta.png diff --git a/docs/doxygen/images/screenshots/vde_deck_analytics.png b/doc/doxygen/images/screenshots/vde_deck_analytics.png similarity index 100% rename from docs/doxygen/images/screenshots/vde_deck_analytics.png rename to doc/doxygen/images/screenshots/vde_deck_analytics.png diff --git a/docs/doxygen/images/screenshots/vde_flat_layout_color_grouped.png b/doc/doxygen/images/screenshots/vde_flat_layout_color_grouped.png similarity index 100% rename from docs/doxygen/images/screenshots/vde_flat_layout_color_grouped.png rename to doc/doxygen/images/screenshots/vde_flat_layout_color_grouped.png diff --git a/docs/doxygen/images/screenshots/vde_flat_layout_type_grouped.png b/doc/doxygen/images/screenshots/vde_flat_layout_type_grouped.png similarity index 100% rename from docs/doxygen/images/screenshots/vde_flat_layout_type_grouped.png rename to doc/doxygen/images/screenshots/vde_flat_layout_type_grouped.png diff --git a/docs/doxygen/images/screenshots/vde_overlap_layout_type_grouped.png b/doc/doxygen/images/screenshots/vde_overlap_layout_type_grouped.png similarity index 100% rename from docs/doxygen/images/screenshots/vde_overlap_layout_type_grouped.png rename to doc/doxygen/images/screenshots/vde_overlap_layout_type_grouped.png diff --git a/docs/doxygen/images/screenshots/vde_sample_hand.png b/doc/doxygen/images/screenshots/vde_sample_hand.png similarity index 100% rename from docs/doxygen/images/screenshots/vde_sample_hand.png rename to doc/doxygen/images/screenshots/vde_sample_hand.png diff --git a/docs/doxygen/js/graph_toggle.js b/doc/doxygen/js/graph_toggle.js similarity index 100% rename from docs/doxygen/js/graph_toggle.js rename to doc/doxygen/js/graph_toggle.js diff --git a/docs/doxygen/pages/developer_documentation/card_database_schema_and_parsing.md b/doc/doxygen/pages/developer_documentation/card_database_schema_and_parsing.md similarity index 81% rename from docs/doxygen/pages/developer_documentation/card_database_schema_and_parsing.md rename to doc/doxygen/pages/developer_documentation/card_database_schema_and_parsing.md index 8679b910b..7b4bdafc3 100644 --- a/docs/doxygen/pages/developer_documentation/card_database_schema_and_parsing.md +++ b/doc/doxygen/pages/developer_documentation/card_database_schema_and_parsing.md @@ -7,8 +7,8 @@ The token file follows the schema of the card database (`tokens.xml`), too. Saved decks (`.xml`) use a different schema. -- [XSD Schema for `Card Databases`](https://github.com/Cockatrice/Cockatrice/blob/master/docs/xsd/cards.xsd) v4 -- [XSD Schema for `Decks`](https://github.com/Cockatrice/Cockatrice/blob/master/docs/xsd/deck.xsd) v1 +- [XSD Schema for `Card Databases`](https://github.com/Cockatrice/Cockatrice/blob/master/doc/carddatabase_v4/cards.xsd) v4 +- [XSD Schema for `Decks`](https://github.com/Cockatrice/Cockatrice/blob/master/doc/deck.xsd) v1 # Card Database Parsing diff --git a/docs/doxygen/pages/developer_documentation/displaying_cards.md b/doc/doxygen/pages/developer_documentation/displaying_cards.md similarity index 100% rename from docs/doxygen/pages/developer_documentation/displaying_cards.md rename to doc/doxygen/pages/developer_documentation/displaying_cards.md diff --git a/docs/doxygen/pages/developer_documentation/index.md b/doc/doxygen/pages/developer_documentation/index.md similarity index 100% rename from docs/doxygen/pages/developer_documentation/index.md rename to doc/doxygen/pages/developer_documentation/index.md diff --git a/docs/doxygen/pages/developer_documentation/loading_card_pictures.md b/doc/doxygen/pages/developer_documentation/loading_card_pictures.md similarity index 100% rename from docs/doxygen/pages/developer_documentation/loading_card_pictures.md rename to doc/doxygen/pages/developer_documentation/loading_card_pictures.md diff --git a/docs/doxygen/pages/developer_documentation/logging.md b/doc/doxygen/pages/developer_documentation/logging.md similarity index 100% rename from docs/doxygen/pages/developer_documentation/logging.md rename to doc/doxygen/pages/developer_documentation/logging.md diff --git a/docs/doxygen/pages/developer_documentation/network/client/game_event_handler.md b/doc/doxygen/pages/developer_documentation/network/client/game_event_handler.md similarity index 100% rename from docs/doxygen/pages/developer_documentation/network/client/game_event_handler.md rename to doc/doxygen/pages/developer_documentation/network/client/game_event_handler.md diff --git a/docs/doxygen/pages/developer_documentation/network/client/index.md b/doc/doxygen/pages/developer_documentation/network/client/index.md similarity index 100% rename from docs/doxygen/pages/developer_documentation/network/client/index.md rename to doc/doxygen/pages/developer_documentation/network/client/index.md diff --git a/docs/doxygen/pages/developer_documentation/network/client/player_event_handler.md b/doc/doxygen/pages/developer_documentation/network/client/player_event_handler.md similarity index 100% rename from docs/doxygen/pages/developer_documentation/network/client/player_event_handler.md rename to doc/doxygen/pages/developer_documentation/network/client/player_event_handler.md diff --git a/docs/doxygen/pages/developer_documentation/network/index.md b/doc/doxygen/pages/developer_documentation/network/index.md similarity index 100% rename from docs/doxygen/pages/developer_documentation/network/index.md rename to doc/doxygen/pages/developer_documentation/network/index.md diff --git a/docs/doxygen/pages/developer_documentation/network/protocol/index.md b/doc/doxygen/pages/developer_documentation/network/protocol/index.md similarity index 100% rename from docs/doxygen/pages/developer_documentation/network/protocol/index.md rename to doc/doxygen/pages/developer_documentation/network/protocol/index.md diff --git a/docs/doxygen/pages/developer_documentation/network/protocol/protocol_command_container.md b/doc/doxygen/pages/developer_documentation/network/protocol/protocol_command_container.md similarity index 100% rename from docs/doxygen/pages/developer_documentation/network/protocol/protocol_command_container.md rename to doc/doxygen/pages/developer_documentation/network/protocol/protocol_command_container.md diff --git a/docs/doxygen/pages/developer_documentation/network/protocol/protocol_game_command.md b/doc/doxygen/pages/developer_documentation/network/protocol/protocol_game_command.md similarity index 100% rename from docs/doxygen/pages/developer_documentation/network/protocol/protocol_game_command.md rename to doc/doxygen/pages/developer_documentation/network/protocol/protocol_game_command.md diff --git a/docs/doxygen/pages/developer_documentation/network/protocol/protocol_overview.md b/doc/doxygen/pages/developer_documentation/network/protocol/protocol_overview.md similarity index 100% rename from docs/doxygen/pages/developer_documentation/network/protocol/protocol_overview.md rename to doc/doxygen/pages/developer_documentation/network/protocol/protocol_overview.md diff --git a/docs/doxygen/pages/developer_documentation/network/protocol/protocol_response.md b/doc/doxygen/pages/developer_documentation/network/protocol/protocol_response.md similarity index 100% rename from docs/doxygen/pages/developer_documentation/network/protocol/protocol_response.md rename to doc/doxygen/pages/developer_documentation/network/protocol/protocol_response.md diff --git a/docs/doxygen/pages/developer_documentation/network/protocol/protocol_server_message.md b/doc/doxygen/pages/developer_documentation/network/protocol/protocol_server_message.md similarity index 100% rename from docs/doxygen/pages/developer_documentation/network/protocol/protocol_server_message.md rename to doc/doxygen/pages/developer_documentation/network/protocol/protocol_server_message.md diff --git a/docs/doxygen/pages/developer_documentation/primer_cards.md b/doc/doxygen/pages/developer_documentation/primer_cards.md similarity index 100% rename from docs/doxygen/pages/developer_documentation/primer_cards.md rename to doc/doxygen/pages/developer_documentation/primer_cards.md diff --git a/docs/doxygen/pages/developer_documentation/querying_the_card_database.md b/doc/doxygen/pages/developer_documentation/querying_the_card_database.md similarity index 100% rename from docs/doxygen/pages/developer_documentation/querying_the_card_database.md rename to doc/doxygen/pages/developer_documentation/querying_the_card_database.md diff --git a/docs/doxygen/pages/index.md b/doc/doxygen/pages/index.md similarity index 100% rename from docs/doxygen/pages/index.md rename to doc/doxygen/pages/index.md diff --git a/docs/doxygen/pages/user_documentation/card_pictures/custom_card_pictures.md b/doc/doxygen/pages/user_documentation/card_pictures/custom_card_pictures.md similarity index 100% rename from docs/doxygen/pages/user_documentation/card_pictures/custom_card_pictures.md rename to doc/doxygen/pages/user_documentation/card_pictures/custom_card_pictures.md diff --git a/docs/doxygen/pages/user_documentation/deck_management/creating_decks.md b/doc/doxygen/pages/user_documentation/deck_management/creating_decks.md similarity index 100% rename from docs/doxygen/pages/user_documentation/deck_management/creating_decks.md rename to doc/doxygen/pages/user_documentation/deck_management/creating_decks.md diff --git a/docs/doxygen/pages/user_documentation/deck_management/editing_decks.md b/doc/doxygen/pages/user_documentation/deck_management/editing_decks.md similarity index 100% rename from docs/doxygen/pages/user_documentation/deck_management/editing_decks.md rename to doc/doxygen/pages/user_documentation/deck_management/editing_decks.md diff --git a/docs/doxygen/pages/user_documentation/deck_management/editing_decks_classic.md b/doc/doxygen/pages/user_documentation/deck_management/editing_decks_classic.md similarity index 100% rename from docs/doxygen/pages/user_documentation/deck_management/editing_decks_classic.md rename to doc/doxygen/pages/user_documentation/deck_management/editing_decks_classic.md diff --git a/docs/doxygen/pages/user_documentation/deck_management/editing_decks_printings.md b/doc/doxygen/pages/user_documentation/deck_management/editing_decks_printings.md similarity index 100% rename from docs/doxygen/pages/user_documentation/deck_management/editing_decks_printings.md rename to doc/doxygen/pages/user_documentation/deck_management/editing_decks_printings.md diff --git a/docs/doxygen/pages/user_documentation/deck_management/editing_decks_visual.md b/doc/doxygen/pages/user_documentation/deck_management/editing_decks_visual.md similarity index 100% rename from docs/doxygen/pages/user_documentation/deck_management/editing_decks_visual.md rename to doc/doxygen/pages/user_documentation/deck_management/editing_decks_visual.md diff --git a/docs/doxygen/pages/user_documentation/deck_management/exporting_decks.md b/doc/doxygen/pages/user_documentation/deck_management/exporting_decks.md similarity index 100% rename from docs/doxygen/pages/user_documentation/deck_management/exporting_decks.md rename to doc/doxygen/pages/user_documentation/deck_management/exporting_decks.md diff --git a/docs/doxygen/pages/user_documentation/deck_management/importing_decks.md b/doc/doxygen/pages/user_documentation/deck_management/importing_decks.md similarity index 100% rename from docs/doxygen/pages/user_documentation/deck_management/importing_decks.md rename to doc/doxygen/pages/user_documentation/deck_management/importing_decks.md diff --git a/docs/doxygen/pages/user_documentation/deck_management/visual_database_display.md b/doc/doxygen/pages/user_documentation/deck_management/visual_database_display.md similarity index 100% rename from docs/doxygen/pages/user_documentation/deck_management/visual_database_display.md rename to doc/doxygen/pages/user_documentation/deck_management/visual_database_display.md diff --git a/docs/doxygen/pages/user_documentation/index.md b/doc/doxygen/pages/user_documentation/index.md similarity index 100% rename from docs/doxygen/pages/user_documentation/index.md rename to doc/doxygen/pages/user_documentation/index.md diff --git a/docs/doxygen/pages/user_documentation/settings/beta.md b/doc/doxygen/pages/user_documentation/settings/beta.md similarity index 100% rename from docs/doxygen/pages/user_documentation/settings/beta.md rename to doc/doxygen/pages/user_documentation/settings/beta.md diff --git a/docs/doxygen/pages/user_documentation/settings/settings_folder.md b/doc/doxygen/pages/user_documentation/settings/settings_folder.md similarity index 100% rename from docs/doxygen/pages/user_documentation/settings/settings_folder.md rename to doc/doxygen/pages/user_documentation/settings/settings_folder.md diff --git a/docs/doxygen/pages/user_documentation/troubleshooting/enabling_debug_logs.md b/doc/doxygen/pages/user_documentation/troubleshooting/enabling_debug_logs.md similarity index 100% rename from docs/doxygen/pages/user_documentation/troubleshooting/enabling_debug_logs.md rename to doc/doxygen/pages/user_documentation/troubleshooting/enabling_debug_logs.md diff --git a/docs/doxygen/pages/user_documentation/troubleshooting/fixing_card_pictures.md b/doc/doxygen/pages/user_documentation/troubleshooting/fixing_card_pictures.md similarity index 100% rename from docs/doxygen/pages/user_documentation/troubleshooting/fixing_card_pictures.md rename to doc/doxygen/pages/user_documentation/troubleshooting/fixing_card_pictures.md diff --git a/docs/doxygen/theme b/doc/doxygen/theme similarity index 100% rename from docs/doxygen/theme rename to doc/doxygen/theme diff --git a/gcovr.cfg b/gcovr.cfg index 7b25dbc8d..ac893b6a4 100644 --- a/gcovr.cfg +++ b/gcovr.cfg @@ -32,7 +32,7 @@ exclude = ^tests/ # Vendored third-party code is not ours to cover and is never going to move the number. exclude = ^vcpkg/ -exclude = ^docs/doxygen/theme/ +exclude = ^doc/doxygen/theme/ exclude = ^libcockatrice_rng/libcockatrice/rng/sfmt/ exclude = ^libcockatrice_utility/libcockatrice/utility/peglib\.h$ exclude = ^oracle/src/lzma/ 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/parser/cockatrice_xml_3.cpp b/libcockatrice_card/libcockatrice/card/database/parser/cockatrice_xml_3.cpp index 99440628a..8d5735db7 100644 --- a/libcockatrice_card/libcockatrice/card/database/parser/cockatrice_xml_3.cpp +++ b/libcockatrice_card/libcockatrice/card/database/parser/cockatrice_xml_3.cpp @@ -33,7 +33,7 @@ class ICardSetPriorityController; #define COCKATRICE_XML3_TAGNAME "cockatrice_carddatabase" #define COCKATRICE_XML3_TAGVER 3 #define COCKATRICE_XML3_SCHEMALOCATION \ - "https://raw.githubusercontent.com/Cockatrice/Cockatrice/master/docs/xsd/cards_v3.xsd" + "https://raw.githubusercontent.com/Cockatrice/Cockatrice/master/doc/carddatabase_v3/cards.xsd" CockatriceXml3Parser::CockatriceXml3Parser(ICardSetPriorityController *_cardSetPriorityController) : ICardDatabaseParser(_cardSetPriorityController) 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 84fc27155..3965b80f0 100644 --- a/libcockatrice_card/libcockatrice/card/database/parser/cockatrice_xml_4.cpp +++ b/libcockatrice_card/libcockatrice/card/database/parser/cockatrice_xml_4.cpp @@ -36,7 +36,7 @@ class ICardSetPriorityController; #define COCKATRICE_XML4_TAGNAME "cockatrice_carddatabase" #define COCKATRICE_XML4_TAGVER 4 #define COCKATRICE_XML4_SCHEMALOCATION \ - "https://raw.githubusercontent.com/Cockatrice/Cockatrice/master/docs/xsd/cards.xsd" + "https://raw.githubusercontent.com/Cockatrice/Cockatrice/master/doc/carddatabase_v4/cards.xsd" CockatriceXml4Parser::CockatriceXml4Parser(ICardSetPriorityController *_cardSetPriorityController) : ICardDatabaseParser(_cardSetPriorityController) 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 f7ca82344..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; } @@ -506,14 +506,6 @@ FilterString::FilterString() _error = "Not initialized"; } -QString FilterString::error() const -{ - if (_errorColumn == 0) { - return _error; - } - return tr("Error at position %1: %2").arg(_errorColumn).arg(_error); -} - FilterString::FilterString(const QString &expr, const CardSearchLanguage &searchLanguage) { QByteArray ba = expr.simplified().toUtf8(); @@ -530,8 +522,7 @@ FilterString::FilterString(const QString &expr, const CardSearchLanguage &search searchLanguageContext = searchLanguage; search.set_logger([&](size_t /*ln*/, size_t col, const std::string &msg) { - _error = QString::fromStdString(msg); - _errorColumn = col; + _error = QString("Error at position %1: %2").arg(col).arg(QString::fromStdString(msg)); }); if (!search.parse(ba.data(), result)) { diff --git a/libcockatrice_filters/libcockatrice/filters/filter_string.h b/libcockatrice_filters/libcockatrice/filters/filter_string.h index 34e2bfb35..2abe95201 100644 --- a/libcockatrice_filters/libcockatrice/filters/filter_string.h +++ b/libcockatrice_filters/libcockatrice/filters/filter_string.h @@ -7,7 +7,6 @@ #ifndef FILTER_STRING_H #define FILTER_STRING_H -#include #include #include #include @@ -34,8 +33,6 @@ typedef AstBase Ast; class FilterString { - Q_DECLARE_TR_FUNCTIONS(FilterString) - public: FilterString(); explicit FilterString(const QString &exp, const CardSearchLanguage &searchLanguage = {}); @@ -48,18 +45,18 @@ public: return result(card); } - [[nodiscard]] bool valid() const + bool valid() { return _error.isEmpty(); } - [[nodiscard]] QString error() const; + QString error() + { + return _error; + } private: - // Untranslated parser message; error() adds the translated prefix, logging uses it as-is. QString _error; - // Column reported by the parser, or 0 when the message carries no position. - size_t _errorColumn = 0; Filter result; }; 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/zones/view_zone_logic.cpp b/libcockatrice_game/libcockatrice/game/zones/view_zone_logic.cpp index f5fcd8786..db1fa32c3 100644 --- a/libcockatrice_game/libcockatrice/game/zones/view_zone_logic.cpp +++ b/libcockatrice_game/libcockatrice/game/zones/view_zone_logic.cpp @@ -7,7 +7,6 @@ #include #include #include -#include class PlayerLogic; class QObject; @@ -37,11 +36,6 @@ bool ZoneViewZoneLogic::getCloseEmptyCardView() const return interfaceSettings ? interfaceSettings->getCloseEmptyCardView() : true; } -bool ZoneViewZoneLogic::isLibraryEdgeView() const -{ - return origZone != nullptr && numberCards > 0 && origZone->getName() == ZoneNames::DECK; -} - /** * Checks if inserting a card at the given position requires an actual new card to be created and added to the view. * Also does any cardId updates that would be required if a card is inserted in that position. diff --git a/libcockatrice_game/libcockatrice/game/zones/view_zone_logic.h b/libcockatrice_game/libcockatrice/game/zones/view_zone_logic.h index ee8156058..a29e543a3 100644 --- a/libcockatrice_game/libcockatrice/game/zones/view_zone_logic.h +++ b/libcockatrice_game/libcockatrice/game/zones/view_zone_logic.h @@ -82,27 +82,6 @@ public: return isReversed; } - /** - * @brief Whether this view is a limited look at one end of a library. - * - * Only scry/surveil-style views of the top or bottom cards of a deck - * qualify. Full-zone views, revealed-card views and views of other zones - * do not, because their first and last cards have no library edge meaning. - */ - [[nodiscard]] bool isLibraryEdgeView() const; - - /** @brief Whether the first card in the view is the top card of the library. */ - [[nodiscard]] bool showsTopOfLibrary() const - { - return isLibraryEdgeView() && !isReversed; - } - - /** @brief Whether the last card in the view is the bottom card of the library. */ - [[nodiscard]] bool showsBottomOfLibrary() const - { - return isLibraryEdgeView() && isReversed; - } - CardZoneLogic *getOriginalZone() const { return origZone; diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/moderator_commands.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/moderator_commands.proto index d03f93667..04cb31d98 100644 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/moderator_commands.proto +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/moderator_commands.proto @@ -169,11 +169,6 @@ message Command_GetUserAlts { optional Command_GetUserAlts ext = 1014; } optional string user_name = 1; - optional uint32 depth = 2 [default = 1]; // BFS depth: 1 = direct matches only, up to 3 for transitive alts-of-alts - optional bool include_email = 3 [default = true]; // match and expand over registration email - optional bool include_clientid = 4 [default = true]; // match and expand over current and historic client ids - optional bool include_ip = 5 [default = true]; // match and expand over recent session IPs - optional uint32 session_months = 6 [default = 6]; // session lookback window in months, 1-24 } message Command_GetModeratorLastLogins { diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_user_alt.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_user_alt.proto index c9ec9aa9a..191dd5063 100644 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_user_alt.proto +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_user_alt.proto @@ -13,9 +13,4 @@ message ServerInfo_UserAlt { optional uint32 warn_count = 6; // number of warnings on record optional uint32 ban_count = 7; // number of bans on record optional bool is_active = 8; // account is not deactivated/banned - optional string match_reason = 9; // why flagged, e.g. "email", "clientid:abc", "ip:1.2.3.4"; empty = seed/legacy - optional uint32 depth = 10; // BFS distance from investigated account; 1 = direct, 2+ = alt-of-alt - optional uint32 shared_count = - 11; // distinct users sharing the matched identifier; high values suggest VPN/shared network - optional uint32 ip_count = 12; // distinct recent session IPs of this account in the search window } 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..f68f51f23 100644 --- a/oracle/CMakeLists.txt +++ b/oracle/CMakeLists.txt @@ -31,7 +31,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/servatrice/check_schema_version.sh b/servatrice/check_schema_version.sh index 8b645b5d2..c4aadf356 100755 --- a/servatrice/check_schema_version.sh +++ b/servatrice/check_schema_version.sh @@ -10,7 +10,7 @@ declare -i schema_ver="${version_line%%)*}" latest_migration="$(ls -1 servatrice/migrations/ | tail -n1)" xtoysql="${latest_migration#servatrice_}" xtoy="${xtoysql%.sql}" -declare -i old_ver="10#${xtoy%_to_*}" # declare as integer with base 10, numbers with a leading 0 are normally interpreted as base 16 +declare -i old_ver="10#${xtoy%_to_*}" #declare as integer with base 10, numbers with a leading 0 are normally interpreted as base 16 declare -i new_ver="10#${xtoy#*_to_}" if ((old_ver >= new_ver)); then diff --git a/servatrice/src/servatrice_database_interface.cpp b/servatrice/src/servatrice_database_interface.cpp index 1327c85d5..a4f184e38 100644 --- a/servatrice/src/servatrice_database_interface.cpp +++ b/servatrice/src/servatrice_database_interface.cpp @@ -34,7 +34,6 @@ #include #include #include -#include #include #include #include @@ -1978,72 +1977,7 @@ QList Servatrice_DatabaseInterface::getUserSessions(cons return results; } -namespace -{ -constexpr int MAX_USER_ALT_DEPTH = 3; -constexpr int MAX_USER_ALTS_PER_LEVEL = 200; -// Bounds per-level work and the size of each dynamic IN-list. -// Identifier-heavy frontiers, e.g. VPN users with hundreds of recent IPs, are -// truncated: this tool is deliberately a first-pass sweep, not an audit. -constexpr int MAX_USER_ALT_IDENTIFIERS = 200; -constexpr int MIN_USER_ALT_MONTHS = 1; -constexpr int MAX_USER_ALT_MONTHS = 24; - -QString altInClause(const QString &prefix, const QList &values) -{ - QStringList placeholders; - placeholders.reserve(values.size()); - for (int i = 0; i < values.size(); ++i) { - placeholders << prefix + QString::number(i); - } - return QStringLiteral("(") + placeholders.join(QStringLiteral(",")) + QStringLiteral(")"); -} - -void altBindValues(QSqlQuery *query, const QString &prefix, const QList &values) -{ - for (int i = 0; i < values.size(); ++i) { - query->bindValue(prefix + QString::number(i), values.at(i)); - } -} - -struct AltCandidate -{ - QString name; - QString email; - QString clientId; - qint64 registrationTime = 0; - qint64 lastLogin = 0; - bool hasLastLogin = false; - int warnCount = 0; - int banCount = 0; - bool isActive = false; -}; - -// Keeps the first-seen order: callers add the strongest evidence first so the -// cap below drops the least relevant identifiers rather than an arbitrary slice. -void appendIdentifier(QList &list, QSet &seen, const QString &value) -{ - if (!value.isEmpty() && !seen.contains(value)) { - seen.insert(value); - list << value; - } -} - -QList cappedIdentifiers(const QList &values) -{ - if (values.size() <= MAX_USER_ALT_IDENTIFIERS) { - return values; - } - return values.mid(0, MAX_USER_ALT_IDENTIFIERS); -} -} // namespace - -QList Servatrice_DatabaseInterface::getUserAlts(const QString &userName, - int depth, - bool useEmail, - bool useClientId, - bool useIp, - int sessionMonths) +QList Servatrice_DatabaseInterface::getUserAlts(const QString &userName) { QList results; @@ -2051,394 +1985,63 @@ QList Servatrice_DatabaseInterface::getUserAlts(const QStrin return results; } - if (depth < 1) { - depth = 1; - } else if (depth > MAX_USER_ALT_DEPTH) { - depth = MAX_USER_ALT_DEPTH; - } - if (sessionMonths < MIN_USER_ALT_MONTHS) { - sessionMonths = MIN_USER_ALT_MONTHS; - } else if (sessionMonths > MAX_USER_ALT_MONTHS) { - sessionMonths = MAX_USER_ALT_MONTHS; - } - - // Interpolated, not bound: placeholders are not allowed in INTERVAL - // expressions, and the clamped integer cannot inject SQL. - const QString sessionWindow = QStringLiteral(" AND start_time >= DATE_SUB(NOW(), INTERVAL ") + - QString::number(sessionMonths) + QStringLiteral(" MONTH)"); - - // Seed must exist; its row anchors the result at depth 0. - QSqlQuery *seedQuery = prepareQuery("SELECT u.name, u.email, u.clientid, UNIX_TIMESTAMP(u.registrationDate), " - "UNIX_TIMESTAMP(a.last_login), " - "(SELECT COUNT(*) FROM {prefix}_warnings w WHERE w.user_id = u.id), " - "(SELECT COUNT(*) FROM {prefix}_bans b WHERE b.user_name = u.name), " - "u.active " - "FROM {prefix}_users u " - "LEFT JOIN {prefix}_user_analytics a ON a.id = u.id " - "WHERE u.name = :user_name"); + // Seed account identifiers used to find related accounts + QSqlQuery *seedQuery = prepareQuery("SELECT email, clientid FROM {prefix}_users WHERE name = :user_name"); seedQuery->bindValue(":user_name", userName); if (!execSqlQuery(seedQuery) || !seedQuery->next()) { return results; } + const QString seedEmail = seedQuery->value(0).toString(); + const QString seedClientId = seedQuery->value(1).toString(); - auto appendAlt = [&results](const AltCandidate &candidate, const QString &matchReason, int matchDepth, - int sharedCount) { - ServerInfo_UserAlt altDetails; - altDetails.set_user_name(candidate.name.toStdString()); - altDetails.set_email(candidate.email.toStdString()); - altDetails.set_clientid(candidate.clientId.toStdString()); - altDetails.set_registration_time(candidate.registrationTime); - if (candidate.hasLastLogin) { - altDetails.set_last_login(candidate.lastLogin); - } - altDetails.set_warn_count(candidate.warnCount); - altDetails.set_ban_count(candidate.banCount); - altDetails.set_is_active(candidate.isActive); - altDetails.set_match_reason(matchReason.toStdString()); - altDetails.set_depth(static_cast(matchDepth)); - altDetails.set_shared_count(static_cast(sharedCount)); - results << altDetails; - }; + QString queryString = "SELECT u.name, u.email, u.clientid, UNIX_TIMESTAMP(u.registrationDate), " + "UNIX_TIMESTAMP(a.last_login), " + "(SELECT COUNT(*) FROM {prefix}_warnings w WHERE w.user_id = u.id), " + "(SELECT COUNT(*) FROM {prefix}_bans b WHERE b.user_name = u.name), " + "u.active " + "FROM {prefix}_users u " + "LEFT JOIN {prefix}_user_analytics a ON a.id = u.id " + "WHERE u.name = :user_name"; + if (!seedEmail.isEmpty()) { + queryString.append(" OR u.email = :seed_email"); + } + if (!seedClientId.isEmpty()) { + queryString.append(" OR u.clientid = :seed_clientid"); + } + queryString.append(" OR u.name IN (SELECT DISTINCT s.user_name FROM {prefix}_sessions s " + "WHERE s.ip_address IN (SELECT DISTINCT s2.ip_address FROM {prefix}_sessions s2 " + "WHERE s2.user_name = :user_name" + " AND s2.start_time >= DATE_SUB(NOW(), INTERVAL 6 MONTH))" + " AND s.start_time >= DATE_SUB(NOW(), INTERVAL 6 MONTH)) " + "ORDER BY u.name LIMIT 200"); - AltCandidate seed{.name = seedQuery->value(0).toString(), - .email = seedQuery->value(1).toString(), - .clientId = seedQuery->value(2).toString(), - .registrationTime = seedQuery->value(3).toLongLong(), - .lastLogin = seedQuery->value(4).toLongLong(), - .hasLastLogin = !seedQuery->value(4).isNull(), - .warnCount = seedQuery->value(5).toInt(), - .banCount = seedQuery->value(6).toInt(), - .isActive = seedQuery->value(7).toBool()}; - appendAlt(seed, QStringLiteral("self"), 0, 1); - - // Identifier lookups are case-insensitive in SQL, so the seed must be keyed by - // its stored casing: otherwise the level-1 query re-lists it as an alt. - QSet visited{seed.name}; - QList frontier{seed.name}; - - // Dynamic IN-lists bypass prepareQuery(): it caches by query text, so every - // distinct list size would leave a prepared statement in the long-lived cache - // and can exhaust MySQL's max_prepared_stmt_count. - const QString dbPrefix = server->getDbPrefix(); - auto prepareDynamicQuery = [&dbPrefix](QSqlQuery &query, const QString &queryText) { - QString text = queryText; - text.replace(QStringLiteral("{prefix}"), dbPrefix); - query.prepare(text); - }; - - for (int level = 1; level <= depth && !frontier.isEmpty(); ++level) { - const QString frontierClause = altInClause(QStringLiteral(":alt_name"), frontier); - - // Current identifiers of the frontier: registration email + client id. - // Kept strongest-first so cappedIdentifiers() drops the least useful ones. - QList emails; - QList clientIds; - QList ips; - QSet emailKeys; - QSet clientIdKeys; - QSet ipKeys; - - if (useEmail || useClientId) { - QSqlQuery frontierQuery(sqlDatabase); - prepareDynamicQuery(frontierQuery, - QStringLiteral("SELECT email, clientid FROM {prefix}_users WHERE name IN ") + - frontierClause); - altBindValues(&frontierQuery, QStringLiteral(":alt_name"), frontier); - if (execSqlQuery(&frontierQuery)) { - while (frontierQuery.next()) { - // Email comparison is case-insensitive in SQL, so normalise the - // keys too or the membership tests below miss real matches. - if (useEmail) { - appendIdentifier(emails, emailKeys, frontierQuery.value(0).toString().toLower()); - } - if (useClientId) { - appendIdentifier(clientIds, clientIdKeys, frontierQuery.value(1).toString()); - } - } - } - } - - // Recent session identifiers: IPs plus historic client ids, most recent - // first so a capped list keeps what the account actually used lately. - if (useIp || useClientId) { - QSqlQuery sessionQuery(sqlDatabase); - prepareDynamicQuery(sessionQuery, - QStringLiteral("SELECT ip_address, clientid, MAX(start_time) AS last_seen " - "FROM {prefix}_sessions WHERE user_name IN ") + - frontierClause + sessionWindow + - QStringLiteral(" GROUP BY ip_address, clientid " - "ORDER BY last_seen DESC")); - altBindValues(&sessionQuery, QStringLiteral(":alt_name"), frontier); - if (execSqlQuery(&sessionQuery)) { - while (sessionQuery.next()) { - if (useIp) { - appendIdentifier(ips, ipKeys, sessionQuery.value(0).toString()); - } - if (useClientId) { - appendIdentifier(clientIds, clientIdKeys, sessionQuery.value(1).toString()); - } - } - } - } - - if (emails.isEmpty() && clientIds.isEmpty() && ips.isEmpty()) { - break; - } - - // Accounts sharing any frontier identifier. Historic client ids are matched - // through past sessions so identifier changes do not hide older alts. - QString candidateString = - QStringLiteral("SELECT u.name, u.email, u.clientid, UNIX_TIMESTAMP(u.registrationDate), " - "UNIX_TIMESTAMP(a.last_login), " - "(SELECT COUNT(*) FROM {prefix}_warnings w WHERE w.user_id = u.id), " - "(SELECT COUNT(*) FROM {prefix}_bans b WHERE b.user_name = u.name), " - "u.active " - "FROM {prefix}_users u " - "LEFT JOIN {prefix}_user_analytics a ON a.id = u.id " - "WHERE "); - QStringList conditions; - const QList emailList = cappedIdentifiers(emails); - const QList clientIdList = cappedIdentifiers(clientIds); - const QList ipList = cappedIdentifiers(ips); - if (!emailList.isEmpty()) { - conditions << QStringLiteral("u.email IN ") + altInClause(QStringLiteral(":alt_email"), emailList); - } - if (!clientIdList.isEmpty()) { - conditions << QStringLiteral("u.clientid IN ") + altInClause(QStringLiteral(":alt_cid"), clientIdList); - } - if (!ipList.isEmpty()) { - conditions << QStringLiteral("u.name IN (SELECT DISTINCT s.user_name FROM {prefix}_sessions s WHERE " - "s.ip_address IN ") + - altInClause(QStringLiteral(":alt_ip"), ipList) + sessionWindow + QStringLiteral(")"); - } - if (!clientIdList.isEmpty()) { - conditions << QStringLiteral("u.name IN (SELECT DISTINCT s.user_name FROM {prefix}_sessions s WHERE " - "s.clientid IN ") + - altInClause(QStringLiteral(":alt_hcid"), clientIdList) + sessionWindow + - QStringLiteral(")"); - } - candidateString += conditions.join(QStringLiteral(" OR ")); - // Already-visited accounts match their own identifiers at every level and - // are dropped below, so widen the row limit by that count: otherwise a - // second level can return only visited rows and no new candidates. - candidateString += - QStringLiteral(" ORDER BY u.name LIMIT ") + QString::number(MAX_USER_ALTS_PER_LEVEL + visited.size()); - - QSqlQuery candidateQuery(sqlDatabase); - prepareDynamicQuery(candidateQuery, candidateString); - altBindValues(&candidateQuery, QStringLiteral(":alt_email"), emailList); - altBindValues(&candidateQuery, QStringLiteral(":alt_cid"), clientIdList); - altBindValues(&candidateQuery, QStringLiteral(":alt_ip"), ipList); - altBindValues(&candidateQuery, QStringLiteral(":alt_hcid"), clientIdList); - if (!execSqlQuery(&candidateQuery)) { - qCWarning(DatabaseInterfaceLog) << "Failed to collect user alt information: SQL Error"; - return results; - } - - QList candidates; - while (candidateQuery.next()) { - AltCandidate candidate; - candidate.name = candidateQuery.value(0).toString(); - if (visited.contains(candidate.name)) { - continue; - } - candidate.email = candidateQuery.value(1).toString(); - candidate.clientId = candidateQuery.value(2).toString(); - candidate.registrationTime = candidateQuery.value(3).toLongLong(); - candidate.hasLastLogin = !candidateQuery.value(4).isNull(); - candidate.lastLogin = candidateQuery.value(4).toLongLong(); - candidate.warnCount = candidateQuery.value(5).toInt(); - candidate.banCount = candidateQuery.value(6).toInt(); - candidate.isActive = candidateQuery.value(7).toBool(); - candidates << candidate; - if (candidates.size() >= MAX_USER_ALTS_PER_LEVEL) { - break; - } - } - - // Resolve session-level evidence (shared IP or historic client id) for - // candidates that do not match on current email or client id. - QMap candidateIp; - QMap candidateHistoricClientId; - QList unexplained; - for (const AltCandidate &candidate : candidates) { - const bool emailMatch = !candidate.email.isEmpty() && emailKeys.contains(candidate.email.toLower()); - const bool clientIdMatch = !candidate.clientId.isEmpty() && clientIdKeys.contains(candidate.clientId); - if (!emailMatch && !clientIdMatch) { - unexplained << candidate.name; - } - } - if (!unexplained.isEmpty() && (!ipKeys.isEmpty() || !clientIdKeys.isEmpty())) { - QString evidenceString = - QStringLiteral("SELECT DISTINCT user_name, ip_address, clientid FROM {prefix}_sessions WHERE " - "user_name IN ") + - altInClause(QStringLiteral(":alt_ev"), unexplained) + QStringLiteral(" AND ("); - QStringList evidenceConditions; - if (!ipList.isEmpty()) { - evidenceConditions << QStringLiteral("ip_address IN ") + - altInClause(QStringLiteral(":alt_eip"), ipList); - } - if (!clientIdList.isEmpty()) { - evidenceConditions << QStringLiteral("clientid IN ") + - altInClause(QStringLiteral(":alt_ecid"), clientIdList); - } - evidenceString += evidenceConditions.join(QStringLiteral(" OR ")); - evidenceString += QStringLiteral(")") + sessionWindow; - - QSqlQuery evidenceQuery(sqlDatabase); - prepareDynamicQuery(evidenceQuery, evidenceString); - altBindValues(&evidenceQuery, QStringLiteral(":alt_ev"), unexplained); - altBindValues(&evidenceQuery, QStringLiteral(":alt_eip"), ipList); - altBindValues(&evidenceQuery, QStringLiteral(":alt_ecid"), clientIdList); - if (execSqlQuery(&evidenceQuery)) { - while (evidenceQuery.next()) { - const QString name = evidenceQuery.value(0).toString(); - const QString ip = evidenceQuery.value(1).toString(); - const QString clientId = evidenceQuery.value(2).toString(); - if (!ip.isEmpty() && ipKeys.contains(ip) && !candidateIp.contains(name)) { - candidateIp.insert(name, ip); - } - if (!clientId.isEmpty() && clientIdKeys.contains(clientId) && - !candidateHistoricClientId.contains(name)) { - candidateHistoricClientId.insert(name, clientId); - } - } - } - } - - // Fan-out counts so moderators can spot VPN/shared-network false positives. - QMap emailCounts; - QMap clientIdCounts; - QMap historicClientIdCounts; - QMap ipCounts; - QSet neededEmails; - QSet neededClientIds; - QSet neededHistoricClientIds; - QSet neededIps; - for (const AltCandidate &candidate : candidates) { - if (!candidate.email.isEmpty() && emailKeys.contains(candidate.email.toLower())) { - neededEmails << candidate.email.toLower(); - } else if (!candidate.clientId.isEmpty() && clientIdKeys.contains(candidate.clientId)) { - neededClientIds << candidate.clientId; - } else if (candidateHistoricClientId.contains(candidate.name)) { - neededHistoricClientIds << candidateHistoricClientId.value(candidate.name); - } else if (candidateIp.contains(candidate.name)) { - neededIps << candidateIp.value(candidate.name); - } - } - if (!neededEmails.isEmpty()) { - const QList neededEmailList = neededEmails.values(); - QSqlQuery countQuery(sqlDatabase); - prepareDynamicQuery(countQuery, QStringLiteral("SELECT email, COUNT(*) FROM {prefix}_users WHERE " - "email IN ") + - altInClause(QStringLiteral(":alt_ce"), neededEmailList) + - QStringLiteral(" GROUP BY email")); - altBindValues(&countQuery, QStringLiteral(":alt_ce"), neededEmailList); - if (execSqlQuery(&countQuery)) { - while (countQuery.next()) { - emailCounts.insert(countQuery.value(0).toString().toLower(), countQuery.value(1).toInt()); - } - } - } - if (!neededClientIds.isEmpty()) { - const QList neededClientIdList = neededClientIds.values(); - QSqlQuery countQuery(sqlDatabase); - prepareDynamicQuery(countQuery, QStringLiteral("SELECT clientid, COUNT(*) FROM {prefix}_users WHERE " - "clientid IN ") + - altInClause(QStringLiteral(":alt_cc"), neededClientIdList) + - QStringLiteral(" GROUP BY clientid")); - altBindValues(&countQuery, QStringLiteral(":alt_cc"), neededClientIdList); - if (execSqlQuery(&countQuery)) { - while (countQuery.next()) { - clientIdCounts.insert(countQuery.value(0).toString(), countQuery.value(1).toInt()); - } - } - } - // Historic client ids are matched through sessions, so their fan-out must - // come from _sessions too: _users only knows each account's current id. - if (!neededHistoricClientIds.isEmpty()) { - const QList neededHistoricClientIdList = neededHistoricClientIds.values(); - QSqlQuery countQuery(sqlDatabase); - prepareDynamicQuery(countQuery, - QStringLiteral("SELECT clientid, COUNT(DISTINCT user_name) FROM {prefix}_sessions " - "WHERE clientid IN ") + - altInClause(QStringLiteral(":alt_hcc"), neededHistoricClientIdList) + - QStringLiteral(" AND start_time >= DATE_SUB(NOW(), INTERVAL 6 MONTH) " - "GROUP BY clientid")); - altBindValues(&countQuery, QStringLiteral(":alt_hcc"), neededHistoricClientIdList); - if (execSqlQuery(&countQuery)) { - while (countQuery.next()) { - historicClientIdCounts.insert(countQuery.value(0).toString(), countQuery.value(1).toInt()); - } - } - } - if (!neededIps.isEmpty()) { - const QList neededIpList = neededIps.values(); - QSqlQuery countQuery(sqlDatabase); - prepareDynamicQuery(countQuery, - QStringLiteral("SELECT ip_address, COUNT(DISTINCT user_name) FROM {prefix}_sessions " - "WHERE ip_address IN ") + - altInClause(QStringLiteral(":alt_ci"), neededIpList) + sessionWindow + - QStringLiteral(" GROUP BY ip_address")); - altBindValues(&countQuery, QStringLiteral(":alt_ci"), neededIpList); - if (execSqlQuery(&countQuery)) { - while (countQuery.next()) { - ipCounts.insert(countQuery.value(0).toString(), countQuery.value(1).toInt()); - } - } - } - - QList nextFrontier; - for (const AltCandidate &candidate : candidates) { - QString matchReason; - int sharedCount = 1; - if (!candidate.email.isEmpty() && emailKeys.contains(candidate.email.toLower())) { - matchReason = QStringLiteral("email"); - sharedCount = qMax(1, emailCounts.value(candidate.email.toLower(), 1)); - } else if (!candidate.clientId.isEmpty() && clientIdKeys.contains(candidate.clientId)) { - matchReason = QStringLiteral("clientid:") + candidate.clientId; - sharedCount = qMax(1, clientIdCounts.value(candidate.clientId, 1)); - } else if (candidateHistoricClientId.contains(candidate.name)) { - const QString historicId = candidateHistoricClientId.value(candidate.name); - matchReason = QStringLiteral("clientid:") + historicId; - sharedCount = qMax(1, historicClientIdCounts.value(historicId, 1)); - } else if (candidateIp.contains(candidate.name)) { - const QString ip = candidateIp.value(candidate.name); - matchReason = QStringLiteral("ip:") + ip; - sharedCount = qMax(1, ipCounts.value(ip, 1)); - } else { - continue; - } - appendAlt(candidate, matchReason, level, sharedCount); - visited.insert(candidate.name); - nextFrontier << candidate.name; - } - frontier = nextFrontier; + QSqlQuery *query = prepareQuery(queryString); + query->bindValue(":user_name", userName); + if (!seedEmail.isEmpty()) { + query->bindValue(":seed_email", seedEmail); + } + if (!seedClientId.isEmpty()) { + query->bindValue(":seed_clientid", seedClientId); } - // Distinct recent IP counts per account: cheap group signal without - // shipping full address histories to the client. - if (!results.isEmpty()) { - QList resultNames; - resultNames.reserve(results.size()); - for (const ServerInfo_UserAlt &alt : results) { - resultNames << QString::fromStdString(alt.user_name()); - } - QSqlQuery *ipCountQuery = prepareQuery( - QStringLiteral("SELECT user_name, COUNT(DISTINCT ip_address) FROM {prefix}_sessions WHERE user_name IN ") + - altInClause(QStringLiteral(":alt_ic"), resultNames) + sessionWindow + - QStringLiteral(" GROUP BY user_name")); - altBindValues(ipCountQuery, QStringLiteral(":alt_ic"), resultNames); - QMap ipCountsByUser; - if (execSqlQuery(ipCountQuery)) { - while (ipCountQuery->next()) { - ipCountsByUser.insert(ipCountQuery->value(0).toString(), ipCountQuery->value(1).toInt()); - } - } - for (ServerInfo_UserAlt &alt : results) { - alt.set_ip_count(static_cast( - ipCountsByUser.value(QString::fromStdString(alt.user_name()), 0))); + if (!execSqlQuery(query)) { + qCWarning(DatabaseInterfaceLog) << "Failed to collect user alt information: SQL Error"; + return results; + } + + while (query->next()) { + ServerInfo_UserAlt altDetails; + altDetails.set_user_name(query->value(0).toString().toStdString()); + altDetails.set_email(query->value(1).toString().toStdString()); + altDetails.set_clientid(query->value(2).toString().toStdString()); + altDetails.set_registration_time(query->value(3).toLongLong()); + if (!query->value(4).isNull()) { + altDetails.set_last_login(query->value(4).toLongLong()); } + altDetails.set_warn_count(query->value(5).toInt()); + altDetails.set_ban_count(query->value(6).toInt()); + altDetails.set_is_active(query->value(7).toBool()); + results << altDetails; } return results; diff --git a/servatrice/src/servatrice_database_interface.h b/servatrice/src/servatrice_database_interface.h index c3cc57ab2..eb74063ac 100644 --- a/servatrice/src/servatrice_database_interface.h +++ b/servatrice/src/servatrice_database_interface.h @@ -224,12 +224,7 @@ public: const QString &beforeTime = QString(), bool *hasMore = nullptr); QList getUserSessions(const QString &userName, int limit); - QList getUserAlts(const QString &userName, - int depth = 1, - bool useEmail = true, - bool useClientId = true, - bool useIp = true, - int sessionMonths = 6); + QList getUserAlts(const QString &userName); QList getModeratorLastLogins(); // Uptime snapshot as recorded by Servatrice::statusUpdate() into the diff --git a/servatrice/src/serversocketinterface.cpp b/servatrice/src/serversocketinterface.cpp index c32ccad49..c38d790b1 100644 --- a/servatrice/src/serversocketinterface.cpp +++ b/servatrice/src/serversocketinterface.cpp @@ -2595,10 +2595,7 @@ Response::ResponseCode AbstractServerSocketInterface::cmdGetUserAlts(const Comma } Response_UserAlts *re = new Response_UserAlts; - const int depth = cmd.has_depth() ? static_cast(cmd.depth()) : 1; - const int sessionMonths = cmd.has_session_months() ? static_cast(cmd.session_months()) : 6; - const QList alts = sqlInterface->getUserAlts( - userName, depth, cmd.include_email(), cmd.include_clientid(), cmd.include_ip(), sessionMonths); + const QList alts = sqlInterface->getUserAlts(userName); for (const ServerInfo_UserAlt &alt : alts) { re->add_alts()->CopyFrom(alt); } diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 84b9f02bf..5b7e06c20 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -177,7 +177,6 @@ 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 @@ -566,7 +565,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) 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..b54205f30 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" @@ -37,6 +36,12 @@ #include #include +#ifdef Q_OS_UNIX +#include +#include +#include +#endif + namespace { @@ -242,14 +247,21 @@ TEST(CardDatabaseTest, Xml4ShortWriteKeepsInstalledFile) 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)); + // A real filesystem write failure, isolated in the death-test child so the + // process-wide file limit never affects other cases or the parent runner. + ASSERT_EXIT( + { + std::signal(SIGXFSZ, SIG_IGN); + struct rlimit limit; + limit.rlim_cur = 128; + limit.rlim_max = 128; + if (setrlimit(RLIMIT_FSIZE, &limit) != 0) { + _exit(2); + } + CockatriceXml4Parser writer(&controller); + _exit(writer.saveToFile({}, sets, cards, fileName) ? 1 : 0); + }, + ::testing::ExitedWithCode(0), ""); ASSERT_TRUE(installed.open(QIODevice::ReadOnly)); EXPECT_EQ(installed.readAll(), "installed 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 88058278f..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) @@ -279,22 +260,6 @@ TEST_F(SetQuery, ComparisonMatchesByReleaseDate) EXPECT_FALSE(FilterString("set -#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/game/card_zone_logic_test.cpp b/tests/game/card_zone_logic_test.cpp index e308d95a6..7f0c39b47 100644 --- a/tests/game/card_zone_logic_test.cpp +++ b/tests/game/card_zone_logic_test.cpp @@ -5,7 +5,6 @@ #include #include #include -#include namespace { @@ -106,30 +105,3 @@ TEST(CardZoneLogicTest, ReorganizeSignalFiresOnAdd) EXPECT_EQ(reorganizeSpy.count(), 1); } - -TEST(CardZoneLogicTest, LibraryEdgeViewIsLimitedToOneEndOfTheLibrary) -{ - HandZoneLogic deck(nullptr, QString::fromLatin1(ZoneNames::DECK), false, true, true); - HandZoneLogic grave(nullptr, QString::fromLatin1(ZoneNames::GRAVE), false, true, true); - - ZoneViewZoneLogic fullDeckView(nullptr, &deck, -1, false, false, false); - EXPECT_FALSE(fullDeckView.isLibraryEdgeView()); - EXPECT_FALSE(fullDeckView.showsTopOfLibrary()); - EXPECT_FALSE(fullDeckView.showsBottomOfLibrary()); - - ZoneViewZoneLogic emptyDeckView(nullptr, &deck, 0, false, false, false); - EXPECT_FALSE(emptyDeckView.isLibraryEdgeView()); - - ZoneViewZoneLogic topView(nullptr, &deck, 3, false, false, false); - EXPECT_TRUE(topView.isLibraryEdgeView()); - EXPECT_TRUE(topView.showsTopOfLibrary()); - EXPECT_FALSE(topView.showsBottomOfLibrary()); - - ZoneViewZoneLogic bottomView(nullptr, &deck, 3, false, false, true); - EXPECT_TRUE(bottomView.isLibraryEdgeView()); - EXPECT_FALSE(bottomView.showsTopOfLibrary()); - EXPECT_TRUE(bottomView.showsBottomOfLibrary()); - - ZoneViewZoneLogic graveView(nullptr, &grave, 3, false, false, false); - EXPECT_FALSE(graveView.isLibraryEdgeView()); -} 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/latency_tracker_test.cpp b/tests/latency_tracker_test.cpp index a8ecf9e8d..a77051799 100644 --- a/tests/latency_tracker_test.cpp +++ b/tests/latency_tracker_test.cpp @@ -3,12 +3,8 @@ #include #include #include -#include #include -// Referenced by other translation units in the server_remote library. -RNG_Abstract *rng = nullptr; - TEST(LatencyTrackerTest, EmptyTrackerYieldsZeroedStats) { LatencyTracker tracker; diff --git a/tests/loader_local_matching_test.cpp b/tests/loader_local_matching_test.cpp index ef6018790..7632e60d4 100644 --- a/tests/loader_local_matching_test.cpp +++ b/tests/loader_local_matching_test.cpp @@ -23,14 +23,10 @@ #include #include #include -#include #include #include #include -// Referenced by other translation units in the server_remote library. -RNG_Abstract *rng = nullptr; - namespace { diff --git a/tests/oracle/oracle_importer_test.cpp b/tests/oracle/oracle_importer_test.cpp index 1b4e2d915..259bae087 100644 --- a/tests/oracle/oracle_importer_test.cpp +++ b/tests/oracle/oracle_importer_test.cpp @@ -1,7 +1,6 @@ #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" @@ -34,6 +33,12 @@ #include #include +#ifdef Q_OS_UNIX +#include +#include +#include +#endif + class OracleImporterTest : public ::testing::Test { protected: @@ -1331,16 +1336,20 @@ TEST(OracleDatabaseWriter, ShortWriteKeepsPreviouslyInstalledDatabase) 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()); - + // A real filesystem write failure, isolated in the death-test child so the + // process-wide file limit never affects other cases or the parent runner. + ASSERT_EXIT( + { + std::signal(SIGXFSZ, SIG_IGN); + struct rlimit limit; + limit.rlim_cur = 128; + limit.rlim_max = 128; + if (setrlimit(RLIMIT_FSIZE, &limit) != 0) { + _exit(2); + } + _exit(OracleDatabase::saveXml(path, data) ? 1 : 0); + }, + ::testing::ExitedWithCode(0), ""); ASSERT_TRUE(original.open(QIODevice::ReadOnly)); EXPECT_EQ(original.readAll(), "installed database"); } diff --git a/tests/server_counter_test.cpp b/tests/server_counter_test.cpp index f894abdbf..492aecf66 100644 --- a/tests/server_counter_test.cpp +++ b/tests/server_counter_test.cpp @@ -8,13 +8,9 @@ #include #include #include -#include #include #include -// Referenced by other translation units in the server_remote library. -RNG_Abstract *rng = nullptr; - TEST(ServerCounter, IncrementDoesNotOverflow) { Server_Counter c(1, "test", color(), 10, std::numeric_limits::max()); diff --git a/tests/spoiler_file_test.cpp b/tests/spoiler_file_test.cpp index ecdc3285f..9944f95bd 100644 --- a/tests/spoiler_file_test.cpp +++ b/tests/spoiler_file_test.cpp @@ -1,5 +1,4 @@ #include "client/network/update/card_spoiler/spoiler_file.h" -#include "support/scoped_file_size_limit.h" #include #include @@ -7,6 +6,12 @@ #include #include +#ifdef Q_OS_UNIX +#include +#include +#include +#endif + namespace { class SpoilerFileTest : public ::testing::Test @@ -118,16 +123,25 @@ TEST_F(SpoilerFileTest, FileInParentPathReportsFailureWithoutModifyingIt) #ifdef Q_OS_UNIX TEST_F(SpoilerFileTest, ShortWriteLeavesExistingFileIntact) { + // Limit only the child process: a real short write exercises QSaveFile's + // buffered flush failure without depending on disk capacity or file ownership. 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_EXIT( + { + struct rlimit limit; + if (getrlimit(RLIMIT_FSIZE, &limit) != 0) { + _exit(2); + } + limit.rlim_cur = 128; + if (setrlimit(RLIMIT_FSIZE, &limit) != 0) { + _exit(3); + } + std::signal(SIGXFSZ, SIG_IGN); + const auto result = save(changed); + _exit(result == SpoilerFile::SaveResult::Failed && !error.isEmpty() ? 0 : 1); + }, + ::testing::ExitedWithCode(0), ""); EXPECT_EQ(read(), original); EXPECT_EQ(QDir(directory.path()).entryList(QDir::Files), QStringList{"spoiler.xml"}); } 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); -}