diff --git a/.ci/Arch/Dockerfile b/.ci/Arch/Dockerfile index b08e568f3..36cf5c4ae 100644 --- a/.ci/Arch/Dockerfile +++ b/.ci/Arch/Dockerfile @@ -8,13 +8,10 @@ RUN pacman --sync --refresh --sysupgrade --needed --noconfirm \ gtest \ mariadb-libs \ ninja \ - openssl \ protobuf \ qt6-base \ - qt6-declarative \ qt6-imageformats \ qt6-multimedia \ - qt6-shadertools \ qt6-svg \ qt6-tools \ qt6-translations \ diff --git a/.ci/Debian12/Dockerfile b/.ci/Debian12/Dockerfile index fc756aac2..202405b84 100644 --- a/.ci/Debian12/Dockerfile +++ b/.ci/Debian12/Dockerfile @@ -15,14 +15,11 @@ RUN apt-get update && \ libprotobuf-dev \ libqt6multimedia6 \ libqt6sql6-mysql \ - libssl-dev \ ninja-build \ protobuf-compiler \ - qt6-declarative-dev \ qt6-image-formats-plugins \ qt6-l10n-tools \ qt6-multimedia-dev \ - qt6-shadertools-dev \ qt6-svg-dev \ qt6-tools-dev \ qt6-tools-dev-tools \ diff --git a/.ci/Debian13/Dockerfile b/.ci/Debian13/Dockerfile index bdecb56df..d7ab6ac86 100644 --- a/.ci/Debian13/Dockerfile +++ b/.ci/Debian13/Dockerfile @@ -16,14 +16,11 @@ RUN apt-get update && \ libprotobuf-dev \ libqt6multimedia6 \ libqt6sql6-mysql \ - libssl-dev \ ninja-build \ protobuf-compiler \ - qt6-declarative-dev \ qt6-image-formats-plugins \ qt6-l10n-tools \ qt6-multimedia-dev \ - qt6-shadertools-dev \ qt6-svg-dev \ qt6-tools-dev \ qt6-tools-dev-tools \ diff --git a/.ci/Fedora43/Dockerfile b/.ci/Fedora43/Dockerfile index 463da5a51..27570cf99 100644 --- a/.ci/Fedora43/Dockerfile +++ b/.ci/Fedora43/Dockerfile @@ -7,9 +7,8 @@ RUN dnf install -y \ git \ mariadb-devel \ ninja-build \ - openssl-devel \ protobuf-devel \ - qt6-{qtdeclarative,qtmultimedia,qtshadertools,qtsvg,qttools,qtwebsockets}-devel \ + qt6-{qttools,qtsvg,qtmultimedia,qtwebsockets}-devel \ qt6-qtimageformats \ rpm-build \ xz-devel \ diff --git a/.ci/Fedora44/Dockerfile b/.ci/Fedora44/Dockerfile index 62238e760..e6c8da7f3 100644 --- a/.ci/Fedora44/Dockerfile +++ b/.ci/Fedora44/Dockerfile @@ -7,9 +7,8 @@ RUN dnf install -y \ git \ mariadb-devel \ ninja-build \ - openssl-devel \ protobuf-devel \ - qt6-{qtdeclarative,qtmultimedia,qtshadertools,qtsvg,qttools,qtwebsockets}-devel \ + qt6-{qttools,qtsvg,qtmultimedia,qtwebsockets}-devel \ qt6-qtimageformats \ rpm-build \ xz-devel \ diff --git a/.ci/Servatrice_Debian12/Dockerfile b/.ci/Servatrice_Debian12/Dockerfile index 321aa7c0f..21f6a036e 100644 --- a/.ci/Servatrice_Debian12/Dockerfile +++ b/.ci/Servatrice_Debian12/Dockerfile @@ -12,7 +12,6 @@ RUN apt-get update && \ libmariadb-dev-compat \ libprotobuf-dev \ libqt6sql6-mysql \ - libssl-dev \ ninja-build \ protobuf-compiler \ qt6-tools-dev \ diff --git a/.ci/Ubuntu24.04/Dockerfile b/.ci/Ubuntu24.04/Dockerfile index 715997474..809b2e43a 100644 --- a/.ci/Ubuntu24.04/Dockerfile +++ b/.ci/Ubuntu24.04/Dockerfile @@ -15,14 +15,11 @@ RUN apt-get update && \ libprotobuf-dev \ libqt6multimedia6 \ libqt6sql6-mysql \ - libssl-dev \ ninja-build \ protobuf-compiler \ - qt6-declarative-dev \ qt6-image-formats-plugins \ qt6-l10n-tools \ qt6-multimedia-dev \ - qt6-shadertools-dev \ qt6-svg-dev \ qt6-tools-dev \ qt6-tools-dev-tools \ diff --git a/.ci/Ubuntu26.04/Dockerfile b/.ci/Ubuntu26.04/Dockerfile index 96dd10763..7b0cd389f 100644 --- a/.ci/Ubuntu26.04/Dockerfile +++ b/.ci/Ubuntu26.04/Dockerfile @@ -16,14 +16,11 @@ RUN apt-get update && \ libprotobuf-dev \ libqt6multimedia6 \ libqt6sql6-mysql \ - libssl-dev \ ninja-build \ protobuf-compiler \ - qt6-declarative-dev \ qt6-image-formats-plugins \ qt6-l10n-tools \ qt6-multimedia-dev \ - qt6-shadertools-dev \ qt6-svg-dev \ qt6-tools-dev \ qt6-tools-dev-tools \ diff --git a/.ci/compile.sh b/.ci/compile.sh index f20432893..8a16d3243 100755 --- a/.ci/compile.sh +++ b/.ci/compile.sh @@ -149,9 +149,6 @@ if [[ $MAKE_TEST ]]; then fi if [[ $USE_CCACHE ]]; then flags+=("-DUSE_CCACHE=1") - # PCH-aware caching is required or ccache refuses to cache any TU that - # consumes a precompiled header, silently recompiling everything on every run. - ccache --set-config sloppiness=pch_defines,time_macros if [[ $CCACHE_SIZE ]]; then # note, this setting persists after running the script ccache --max-size "$CCACHE_SIZE" @@ -327,32 +324,4 @@ if [[ $MAKE_PACKAGE ]]; then BUILD_DIR="$BUILD_DIR" .ci/name_build.sh "$PACKAGE_SUFFIX" echo "::endgroup::" fi - - if [[ $RUNNER_OS == Windows ]]; then - echo "::group::Check installer for build-tree artifacts" - cd "$BUILD_DIR" - package="$(find . -maxdepth 1 -type f -name 'Cockatrice-*.exe' -print -quit)" - if [[ ! $package ]]; then - echo "::error file=$0::Could not find installer to inspect" - exit 1 - fi - seven_zip="$(command -v 7z || true)" - if [[ ! $seven_zip ]]; then - seven_zip="/c/Program Files/7-Zip/7z.exe" - fi - if [[ ! -f $seven_zip ]]; then - echo "::warning file=$0::7-Zip not found, skipping installer content check" - else - echo "Inspecting $package" - # Fail the build if the installer contains any path left behind by the MSBuild or - # Qt AUTOMOC tooling (build-tree artifacts must live in the build dir, not the install) - if "$seven_zip" l "$package" | - grep -E "_autogen|\.dir[\\/]|\.tlog|(^|[\\/])x64[\\/]|(^|[\\/])\.qt[\\/]|(^|[\\/])\.qsb[\\/]|(^|[\\/])\.lupdate[\\/]|CMakeFiles"; then - echo "::error file=$0::Installer contains build-tree artifacts" - exit 1 - fi - echo "Installer content is clean" - fi - echo "::endgroup::" - fi fi diff --git a/.ci/release_template.md b/.ci/release_template.md index 23b475150..ac78a193a 100644 --- a/.ci/release_template.md +++ b/.ci/release_template.md @@ -12,9 +12,9 @@ Available pre-compiled binaries for installation: • Windows 10+ macOS - • macOS 15+ Sequoia - • macOS 14+ Sonoma - • macOS 13+ Ventura (x86) + • macOS 15+ Sequoia Apple M + • macOS 14+ Sonoma Apple M + • macOS 13+ Ventura Intel LinuxUbuntu 26.04 LTS Resolute Racoon @@ -24,10 +24,10 @@ Available pre-compiled binaries for installation: • Fedora 44Fedora 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. +We are also packaged in Arch Linux's official extra repository, courtesy of @FFY00. +General Linux support is available via a flatpak package at Flathub! - We maintain a Docker image for "Servatrice" in GHCR (x86 & ARM). You can docker pull it or use our Docker Compose files! +We provide a Docker image for "Servatrice" in GHCR. You can docker pull it or use our Docker Compose files! diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml deleted file mode 100644 index 75fbc59f1..000000000 --- a/.github/workflows/codeql.yml +++ /dev/null @@ -1,95 +0,0 @@ -# GitHub Docs on Code Scanning: -# https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning -# https://docs.github.com/en/code-security/how-tos/find-and-fix-code-vulnerabilities/manage-your-configuration -# https://docs.github.com/en/code-security/reference/code-scanning/workflow-configuration-options -# -# CodeQL Docs: -# https://codeql.github.com/docs/ - -name: CodeQL - -permissions: - contents: read - security-events: write # needed to post results - -on: - push: - branches: - - master - pull_request: - -# Cancel earlier, unfinished runs of this workflow on the same branch -concurrency: - group: "${{ github.workflow }} @ ${{ github.ref_name }}" - cancel-in-progress: true - -jobs: - analyze: - name: Analyze (${{ matrix.language }}) - runs-on: ubuntu-latest - - strategy: - fail-fast: false - matrix: - include: - # https://codeql.github.com/docs/codeql-overview/supported-languages-and-frameworks/ - - language: cpp - build-mode: manual - - language: actions - build-mode: none - - steps: - - name: "Checkout repository" - uses: actions/checkout@v7 - - - name: "Initialize CodeQL" - uses: github/codeql-action/init@v4 - with: - languages: ${{ matrix.language }} - build-mode: ${{ matrix.build-mode }} - # https://docs.github.com/en/code-security/reference/code-scanning/codeql/codeql-queries/c-cpp-built-in-queries - # https://docs.github.com/en/code-security/reference/code-scanning/codeql/codeql-queries/actions-built-in-queries - queries: security-extended - dependency-caching: true - - - name: "[C++] Install dependencies" - if: matrix.language == 'cpp' && matrix.build-mode == 'manual' - shell: bash - env: - DEBIAN_FRONTEND: noninteractive - run: | - sudo apt-get update - sudo apt-get install -y --no-install-recommends \ - cmake \ - g++ \ - libgl-dev \ - liblzma-dev \ - libmariadb-dev-compat \ - libprotobuf-dev \ - ninja-build \ - protobuf-compiler \ - qt6-declarative-dev \ - qt6-multimedia-dev \ - qt6-shadertools-dev \ - qt6-svg-dev \ - qt6-tools-dev \ - qt6-tools-dev-tools \ - qt6-websockets-dev - -# Minimize dependency install -# Add ccache usage for faster compilation, (install ccache dep, actions/cache step + append DUSE_CCACHE=1 in cmake config, CCACHE env values) - - - name: "[C++] Configure CMake" - if: matrix.language == 'cpp' && matrix.build-mode == 'manual' - shell: bash - run: cmake -S . -B build -G Ninja -DWITH_SERVER=1 -DCMAKE_BUILD_TYPE=Release - - - name: "[C++] Build application" - if: matrix.language == 'cpp' && matrix.build-mode == 'manual' - shell: bash - run: cmake --build build - - - name: "Perform CodeQL Analysis" - uses: github/codeql-action/analyze@v4 - with: - category: "/language:${{ matrix.language }}" diff --git a/.github/workflows/desktop-build.yml b/.github/workflows/desktop-build.yml index bd528f245..f1846ecf6 100644 --- a/.github/workflows/desktop-build.yml +++ b/.github/workflows/desktop-build.yml @@ -3,7 +3,7 @@ name: Build Desktop permissions: actions: write # needed to delete entries in GHA cache (update ccache) attestations: write # needed to persist the attestation. - contents: write # needed for e.g. vcpkg dependency graph updates + contents: write id-token: write # needed for signing certificate in attestation on: @@ -34,7 +34,7 @@ on: - 'vcpkg.json' - 'vcpkg' # needed to match submodule bumps (gitlink) -# Cancel earlier, unfinished runs of this workflow on the same branch (unless on tag --> release) +# Cancel earlier, unfinished runs of this workflow on the same branch (unless on release) concurrency: group: "${{ github.workflow }} @ ${{ github.ref_name }}" cancel-in-progress: ${{ github.ref_type != 'tag' }} @@ -42,7 +42,7 @@ concurrency: jobs: configure: name: Configure - runs-on: ubuntu-slim # https://github.com/actions/runner-images/blob/main/images/ubuntu-slim/ubuntu-slim-Readme.md + runs-on: ubuntu-slim outputs: tag: ${{ steps.configure.outputs.tag }} sha: ${{ steps.configure.outputs.sha }} @@ -146,13 +146,13 @@ jobs: name: ${{ matrix.distro }} ${{ matrix.version }} needs: configure - runs-on: ubuntu-latest # https://github.com/actions/runner-images + runs-on: ubuntu-latest continue-on-error: ${{ matrix.allow-failure == 'yes' }} timeout-minutes: 70 env: CACHE: ${{ github.workspace }}/.cache/${{ matrix.distro }}${{ matrix.version }} # directory for caching docker image and ccache CCACHE_EVICTION_AGE: 7d - CCACHE_SIZE: 600M # space of all repo is 10Gi: https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#usage-limits-and-eviction-policy + CCACHE_SIZE: 550M # space of all repo is 10Gi: https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#usage-limits-and-eviction-policy CMAKE_GENERATOR: 'Ninja' NAME: ${{ matrix.distro }}${{ matrix.version }} @@ -161,11 +161,14 @@ jobs: uses: actions/checkout@v7 - name: "Restore compiler cache (ccache)" - id: restore_ccache + id: ccache_restore uses: actions/cache/restore@v6 + env: + BRANCH_NAME: ${{ github.head_ref || github.ref_name }} with: - key: ccache-${{ matrix.distro }}${{ matrix.version }} + key: ccache-${{ matrix.distro }}${{ matrix.version }}-${{ env.BRANCH_NAME }} path: ${{ env.CACHE }} + restore-keys: ccache-${{ matrix.distro }}${{ matrix.version }}- - name: "Build ${{ matrix.distro }} ${{ matrix.version }} Docker image" shell: bash @@ -176,12 +179,8 @@ jobs: shell: bash run: | source .ci/docker.sh - args=() - [[ $GITHUB_REF == "refs/heads/master" ]] && args+=(--evict-ccache "$CCACHE_EVICTION_AGE") - args+=(--ccache "$CCACHE_SIZE") - args+=(--cmake-generator "$CMAKE_GENERATOR") - - RUN --server --debug --test "${args[@]}" + RUN --server --debug --test --ccache "$CCACHE_SIZE" \ + --cmake-generator "$CMAKE_GENERATOR" - name: "Build release package" id: build @@ -204,10 +203,10 @@ jobs: # Delete used cache to emulate a ccache update. See https://github.com/actions/cache/issues/342 - name: "Delete remote compiler cache (ccache)" - if: github.ref == 'refs/heads/master' && steps.restore_ccache.outputs.cache-hit + if: github.ref == 'refs/heads/master' && steps.ccache_restore.outputs.cache-hit continue-on-error: true env: - CACHE_PRIMARY_KEY: ${{ steps.restore_ccache.outputs.cache-primary-key }} + CACHE_PRIMARY_KEY: ${{ steps.ccache_restore.outputs.cache-primary-key }} GH_TOKEN: ${{ github.token }} run: | if gh cache delete --repo "$GITHUB_REPOSITORY" "$CACHE_PRIMARY_KEY"; then @@ -218,7 +217,7 @@ jobs: if: github.ref == 'refs/heads/master' uses: actions/cache/save@v6 with: - key: ${{ steps.restore_ccache.outputs.cache-primary-key }} + key: ${{ steps.ccache_restore.outputs.cache-primary-key }} path: ${{ env.CACHE }} - name: "Upload artifact" @@ -263,77 +262,74 @@ jobs: matrix: include: - os: macOS - target: 13 # EOL 2025-09-15 - runner: macos-15-intel # https://github.com/actions/runner-images/blob/main/images/macos/macos-15-Readme.md - # macos-26-intel is very slow and fails in CPack during DMG config if not increasing Finder timeout + target: 13 + runner: macos-15-intel ccache_eviction_age: 7d cmake_generator: Ninja make_package: 1 override_target: 13 package_suffix: "-macOS13_Intel" - qt_version: 6.11.* - qt_modules: qtimageformats qtmultimedia qtshadertools qtwebsockets + qt_version: 6.11.0 + qt_modules: qtimageformats qtmultimedia qtwebsockets soc: Intel type: Release use_ccache: 1 - xcode: "26.3" + xcode: "16.4" - os: macOS - target: 14 # EOL 2026-?? - runner: macos-26 # https://github.com/actions/runner-images/blob/main/images/macos/macos-26-arm64-Readme.md + target: 14 + runner: macos-14 ccache_eviction_age: 7d cmake_generator: Ninja make_package: 1 - override_target: 14 package_suffix: "-macOS14" - qt_version: 6.11.* - qt_modules: qtimageformats qtmultimedia qtshadertools qtwebsockets + qt_version: 6.11.0 + qt_modules: qtimageformats qtmultimedia qtwebsockets soc: Apple type: Release use_ccache: 1 - xcode: "26.6" + xcode: "15.4" - os: macOS target: 15 - runner: macos-26 # https://github.com/actions/runner-images/blob/main/images/macos/macos-26-arm64-Readme.md + runner: macos-15 ccache_eviction_age: 7d cmake_generator: Ninja make_package: 1 - override_target: 15 package_suffix: "-macOS15" - qt_version: 6.11.* - qt_modules: qtimageformats qtmultimedia qtshadertools qtwebsockets + qt_version: 6.11.0 + qt_modules: qtimageformats qtmultimedia qtwebsockets soc: Apple type: Release use_ccache: 1 - xcode: "26.6" + xcode: "16.4" - os: macOS - target: 26 - runner: macos-26 # https://github.com/actions/runner-images/blob/main/images/macos/macos-26-arm64-Readme.md + target: 15 + runner: macos-15 ccache_eviction_age: 7d cmake_generator: Ninja - qt_version: 6.11.* - qt_modules: qtimageformats qtmultimedia qtshadertools qtwebsockets + qt_version: 6.11.0 + qt_modules: qtimageformats qtmultimedia qtwebsockets soc: Apple type: Debug use_ccache: 1 - xcode: "26.6" + xcode: "16.4" - os: Windows target: 10 - runner: windows-2025 # https://github.com/actions/runner-images/blob/main/images/windows/Windows2025-VS2026-Readme.md + runner: windows-2025 cmake_generator: "Visual Studio 18 2026" cmake_generator_platform: x64 make_package: 1 package_suffix: "-Win10" - qt_version: 6.11.* - qt_modules: qtimageformats qtmultimedia qtshadertools qtwebsockets + qt_version: 6.11.0 + qt_modules: qtimageformats qtmultimedia qtwebsockets type: Release name: ${{ matrix.os }} ${{ matrix.target }}${{ matrix.soc == 'Intel' && ' Intel' || '' }}${{ matrix.type == 'Debug' && ' Debug' || '' }} @@ -342,7 +338,7 @@ jobs: timeout-minutes: 100 env: CCACHE_DIR: ${{ github.workspace }}/.cache/ - CCACHE_SIZE: 600M # space of all repo is 10Gi: https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#usage-limits-and-eviction-policy + CCACHE_SIZE: 550M # space of all repo is 10Gi: https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#usage-limits-and-eviction-policy steps: - name: "Checkout" @@ -352,6 +348,7 @@ jobs: - name: "[Windows] Add msbuild to PATH" if: matrix.os == 'Windows' + id: add-msbuild uses: microsoft/setup-msbuild@v3 with: msbuild-architecture: x64 @@ -362,54 +359,54 @@ jobs: - name: "[macOS] Restore compiler cache (ccache)" if: matrix.os == 'macOS' && matrix.use_ccache == 1 - id: restore_ccache + id: ccache_restore uses: actions/cache/restore@v6 + env: + BRANCH_NAME: ${{ github.head_ref || github.ref_name }} with: - key: ccache-${{ matrix.runner }}_${{ matrix.override_target }}-Xcode${{ matrix.xcode }} + key: ccache-${{ matrix.runner }}-${{ matrix.soc }}-${{ matrix.type }}-${{ env.BRANCH_NAME }} path: ${{ env.CCACHE_DIR }} + restore-keys: ccache-${{ matrix.runner }}-${{ matrix.soc }}-${{ matrix.type }}- - - name: "[macOS] Install aqtinstall" - if: matrix.os == 'macOS' + - name: "Install aqtinstall" run: pipx install aqtinstall # Resolve given wildcard versions (e.g. Qt 6.6.*) to latest version via aqtinstall to avoid stale caches on new releases - - name: "[macOS] Resolve latest Qt from ${{ matrix.qt_version }} input" - if: matrix.os == 'macOS' + - name: "Resolve latest Qt patch version" env: QT_VERSION: ${{ matrix.qt_version }} id: resolve_qt_version shell: bash run: .ci/resolve_latest_aqt_qt_version.sh "$QT_VERSION" - - name: "[macOS] Restore thin Qt ${{ steps.resolve_qt_version.outputs.version }}" + - name: "[macOS] Restore thin Qt ${{ steps.resolve_qt_version.outputs.version }} libraries" if: matrix.os == 'macOS' id: restore_qt uses: actions/cache/restore@v6 with: - key: Qt-${{ steps.resolve_qt_version.outputs.version }}-macOS-${{ matrix.soc }}-${{ matrix.qt_modules }}-thin + key: thin-qt-macos-${{ matrix.soc }}-${{ steps.resolve_qt_version.outputs.version }} path: ${{ github.workspace }}/Qt # Using jurplel/install-qt-action to install Qt without using brew # Qt build using vcpkg either just fails or takes too long to build - - name: "[macOS] Install fat Qt ${{ matrix.qt_version }}" + - name: "[macOS] Install fat Qt ${{ steps.resolve_qt_version.outputs.version }}" if: matrix.os == 'macOS' && steps.restore_qt.outputs.cache-hit != 'true' uses: jurplel/install-qt-action@v4 with: cache: false - # cache-key-prefix: Qt - dir: ${{ github.workspace }} # thinning script depends on this location + dir: ${{ github.workspace }} modules: ${{ matrix.qt_modules }} - version: ${{ matrix.qt_version }} + version: ${{ steps.resolve_qt_version.outputs.version }} - name: "[macOS] Create thin Qt libraries" if: matrix.os == 'macOS' && steps.restore_qt.outputs.cache-hit != 'true' run: .ci/thin_macos_qtlib.sh - name: "[macOS] Cache thin Qt libraries" - if: matrix.os == 'macOS' && steps.restore_qt.outputs.cache-hit != 'true' && github.ref == 'refs/heads/master' + if: matrix.os == 'macOS' && steps.restore_qt.outputs.cache-hit != 'true' uses: actions/cache/save@v6 with: - key: ${{ steps.restore_qt.outputs.cache-primary-key }} + key: thin-qt-macos-${{ matrix.soc }}-${{ steps.resolve_qt_version.outputs.version }} path: ${{ github.workspace }}/Qt - name: "[Windows] Install Qt ${{ matrix.qt_version }}" @@ -418,10 +415,9 @@ 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 - cache: ${{ github.ref == 'refs/heads/master' }} - cache-key-prefix: Qt + cache: true modules: ${{ matrix.qt_modules }} - version: ${{ matrix.qt_version }} + version: ${{ steps.resolve_qt_version.outputs.version }} - name: "[Windows] Install NSIS" if: matrix.os == 'Windows' @@ -444,7 +440,6 @@ jobs: CMAKE_GENERATOR: ${{ matrix.cmake_generator }} CMAKE_GENERATOR_PLATFORM: ${{ matrix.cmake_generator_platform }} DEVELOPER_DIR: '/Applications/Xcode_${{ matrix.xcode }}.app/Contents/Developer' - GITHUB_TOKEN: ${{ github.token }} # needed for vcpkg dependency graph updates, see VCPKG_FEATURE_FLAGS MACOS_CERTIFICATE: ${{ secrets.PROD_MACOS_CERTIFICATE }} MACOS_CERTIFICATE_NAME: ${{ secrets.PROD_MACOS_CERTIFICATE_NAME }} MACOS_CERTIFICATE_PWD: ${{ secrets.PROD_MACOS_CERTIFICATE_PWD }} @@ -453,28 +448,27 @@ jobs: PACKAGE_SUFFIX: '${{ matrix.package_suffix }}' TARGET_MACOS_VERSION: ${{ matrix.override_target }} USE_CCACHE: ${{ matrix.use_ccache }} - VCPKG_BINARY_SOURCES: "clear;files,${{ steps.vcpkg-cache.outputs.path }},${{ case(github.ref == 'refs/heads/master', 'readwrite', 'read') }}" + VCPKG_BINARY_SOURCES: 'clear;files,${{ steps.vcpkg-cache.outputs.path }},readwrite' VCPKG_DISABLE_METRICS: 1 - VCPKG_FEATURE_FLAGS: dependencygraph run: .ci/compile.sh --server --test --vcpkg # Delete used cache to emulate a ccache update. See https://github.com/actions/cache/issues/342 - name: "[macOS] Delete remote compiler cache (ccache)" - if: matrix.os == 'macOS' && matrix.use_ccache == 1 && github.ref == 'refs/heads/master' && steps.restore_ccache.outputs.cache-hit + if: matrix.os == 'macOS' && matrix.use_ccache == 1 && github.ref == 'refs/heads/master' && steps.ccache_restore.outputs.cache-hit continue-on-error: true env: - CACHE_PRIMARY_KEY: ${{ steps.restore_ccache.outputs.cache-primary-key }} + CACHE_PRIMARY_KEY: ${{ steps.ccache_restore.outputs.cache-primary-key }} GH_TOKEN: ${{ github.token }} run: | if gh cache delete --repo "$GITHUB_REPOSITORY" "$CACHE_PRIMARY_KEY"; then echo "Cache deleted successfully" fi - - name: "[macOS] Cache updated compiler cache (ccache)" + - name: "[macOS] Save updated compiler cache (ccache)" if: matrix.os == 'macOS' && matrix.use_ccache == 1 && github.ref == 'refs/heads/master' uses: actions/cache/save@v6 with: - key: ${{ steps.restore_ccache.outputs.cache-primary-key }} + key: ${{ steps.ccache_restore.outputs.cache-primary-key }} path: ${{ env.CCACHE_DIR }} - name: "[macOS] Sign app bundle" diff --git a/.github/workflows/desktop-lint.yml b/.github/workflows/desktop-lint.yml index 93ba79464..5f31ea59c 100644 --- a/.github/workflows/desktop-lint.yml +++ b/.github/workflows/desktop-lint.yml @@ -1,8 +1,5 @@ name: Code Style (C++) -permissions: - contents: read - on: # Push trigger not needed for linting, we do not allow direct pushes to master pull_request: diff --git a/.github/workflows/docker-release.yml b/.github/workflows/docker-release.yml index 967d94c58..5384c9e64 100644 --- a/.github/workflows/docker-release.yml +++ b/.github/workflows/docker-release.yml @@ -1,8 +1,8 @@ -name: Build Docker +name: Build Docker Image permissions: - contents: read # needed to checkout repo - packages: write # needed for interacting with GHCR + contents: read + packages: write on: push: @@ -13,10 +13,7 @@ on: - master paths: - '.github/workflows/docker-release.yml' - - '.dockerignore' - 'Dockerfile' - - 'docker-compose.yml' - - 'docker-compose.yml.windows' release: types: - released # publishing of stable releases @@ -26,38 +23,36 @@ concurrency: group: "${{ github.workflow }} @ ${{ github.ref_name }}" cancel-in-progress: ${{ github.event_name != 'release' }} -env: - GHCR_IMAGE: ghcr.io/cockatrice/servatrice - OCI_DESCRIPTION: Server for Cockatrice, a cross-platform virtual tabletop for multiplayer card games - OCI_TITLE: Servatrice - OCI_URL: https://cockatrice.github.io/ - jobs: - # Create one platform-specific image and publish its OCI image manifest per matrix job - build: - name: "Servatrice (${{ matrix.label }})" - if: github.repository_owner == 'Cockatrice' - runs-on: ${{ matrix.runner }} - - strategy: - fail-fast: false - matrix: - include: - - label: x86 - platform: linux/amd64 - runner: ubuntu-latest # https://github.com/actions/runner-images - - - label: arm - platform: linux/arm64 - runner: ubuntu-24.04-arm # https://github.com/actions/runner-images/blob/main/images/ubuntu/Ubuntu2404-Arm64-Readme.md, replace with "ubuntu-latest-arm" once available - - env: - CACHE_SCOPE: servatrice-${{ matrix.label }} - + docker: + name: amd64 & arm64 + if: ${{ github.repository_owner == 'Cockatrice' }} + runs-on: ubuntu-latest + steps: - name: "Checkout" uses: actions/checkout@v7 + - name: "Docker metadata" + id: metadata + uses: docker/metadata-action@v6 + env: + DOCKER_METADATA_ANNOTATIONS_LEVELS: index # needed for GHCR + with: + annotations: | + org.opencontainers.image.title=Servatrice + org.opencontainers.image.url=https://cockatrice.github.io/ + org.opencontainers.image.description=Server for Cockatrice, a cross-platform virtual tabletop for multiplayer card games + images: | + ghcr.io/cockatrice/servatrice + labels: | + org.opencontainers.image.title=Servatrice + org.opencontainers.image.url=https://cockatrice.github.io/ + org.opencontainers.image.description=Server for Cockatrice, a cross-platform virtual tabletop for multiplayer card games + + - name: "Set up QEMU" + uses: docker/setup-qemu-action@v4 + - name: "Set up Docker buildx" uses: docker/setup-buildx-action@v4 @@ -66,117 +61,18 @@ jobs: id: login uses: docker/login-action@v4 with: + password: ${{ github.token }} registry: ghcr.io username: ${{ github.actor }} - password: ${{ github.token }} - # Don't push for non-release triggers - - name: "Build image" - if: steps.login.outcome != 'success' + - name: "Build and push Docker image" uses: docker/build-push-action@v7 with: - cache-from: type=gha,scope=${{ env.CACHE_SCOPE }} - cache-to: ${{ case(github.ref == 'refs/heads/master', format('type=gha,mode=max,scope={0}', env.CACHE_SCOPE), '') }} + annotations: ${{ steps.metadata.outputs.annotations }} + cache-from: type=gha,scope=servatrice + cache-to: type=gha,mode=max,scope=servatrice context: . - platforms: ${{ matrix.platform }} - push: false - - # Add OCI labels and push single-platform image by digest (without tags) - - name: "Build image and push by digest" - if: steps.login.outcome == 'success' - id: build - uses: docker/build-push-action@v7 - with: - cache-from: type=gha,scope=${{ env.CACHE_SCOPE }} - cache-to: type=gha,mode=max,scope=${{ env.CACHE_SCOPE }} - context: . - labels: | - org.opencontainers.image.description=${{ env.OCI_DESCRIPTION }} - org.opencontainers.image.title=${{ env.OCI_TITLE }} - org.opencontainers.image.url=${{ env.OCI_URL }} - outputs: type=image,name=${{ env.GHCR_IMAGE }},name-canonical=true,push=true,push-by-digest=true - platforms: ${{ matrix.platform }} - provenance: mode=max # Do not pass secrets as build arguments with this option - sbom: true - - - name: "Export digest" - if: steps.login.outcome == 'success' - env: - DIGEST: ${{ steps.build.outputs.digest }} - run: | - mkdir -p "$RUNNER_TEMP/digests" - touch "$RUNNER_TEMP/digests/${DIGEST#sha256:}" - - - name: "Upload digest" - if: steps.login.outcome == 'success' - uses: actions/upload-artifact@v7 - with: - archive: false - if-no-files-found: error - name: digest-${{ matrix.label }} - path: ${{ runner.temp }}/digests/* - retention-days: 1 - - - # Create an OCI image index from the platform-specific image manifests - index: - name: "Publish multi-platform Servatrice image" - if: github.repository_owner == 'Cockatrice' && github.event_name == 'release' && github.event.release.prerelease == false - needs: build - runs-on: ubuntu-slim # https://github.com/actions/runner-images/blob/main/images/ubuntu-slim/ubuntu-slim-Readme.md - - steps: - - name: "Download digests" - uses: actions/download-artifact@v8 - with: - path: ${{ runner.temp }}/digests - pattern: digest-* - merge-multiple: true - - - name: "Login to GitHub Container Registry (GHCR)" - uses: docker/login-action@v4 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ github.token }} - - - name: "Docker metadata" - id: metadata - uses: docker/metadata-action@v6 - with: - images: ${{ env.GHCR_IMAGE }} - flavor: | - latest=auto - tags: | - type=ref,event=tag # if semver, also: type=semver,pattern={{version}} / {{major}}.{{minor}} - - # Add OCI annotations to image index and publish tags - - name: "Create image index" - env: - DOCKER_TAGS: ${{ steps.metadata.outputs.tags }} - working-directory: ${{ runner.temp }}/digests - run: | - TAG_ARGS=() - while IFS= read -r tag; do - TAG_ARGS+=(--tag "$tag") - done <<< "$DOCKER_TAGS" - - DIGEST_ARGS=() - for digest in *; do - DIGEST_ARGS+=("$GHCR_IMAGE@sha256:$digest") - done - - docker buildx imagetools create \ - --prefer-index=true \ - --annotation "index:org.opencontainers.image.description=$OCI_DESCRIPTION" \ - --annotation "index:org.opencontainers.image.title=$OCI_TITLE" \ - --annotation "index:org.opencontainers.image.url=$OCI_URL" \ - "${TAG_ARGS[@]}" \ - "${DIGEST_ARGS[@]}" - - - name: "Inspect images" - env: - GITHUB_TAG: ${{ github.ref_name }} - run: | - docker buildx imagetools inspect "$GHCR_IMAGE:latest" - docker buildx imagetools inspect "$GHCR_IMAGE:$GITHUB_TAG" + labels: ${{ steps.metadata.outputs.labels }} + platforms: linux/amd64,linux/arm64 + push: ${{ steps.login.outcome == 'success' }} + tags: ${{ steps.metadata.outputs.tags }} diff --git a/.github/workflows/documentation-build.yml b/.github/workflows/documentation-build.yml index 419cbfbfb..4b9ca79ab 100644 --- a/.github/workflows/documentation-build.yml +++ b/.github/workflows/documentation-build.yml @@ -1,8 +1,5 @@ name: Generate Docs -permissions: - contents: read # write permission to the destination repo come from 'deploy_key' - on: pull_request: paths: @@ -14,11 +11,6 @@ on: - published # publishing of stable releases and pre-releases workflow_dispatch: -# Cancel earlier, unfinished runs of this workflow on the same branch (unless on release) -concurrency: - group: "${{ github.workflow }} @ ${{ github.ref_name }}" - cancel-in-progress: ${{ github.event_name != 'release' }} - env: COCKATRICE_REF: ${{ github.ref_name }} # tag name if the commit is tagged, otherwise branch name diff --git a/.github/workflows/translations-pull.yml b/.github/workflows/translations-pull.yml index 71b0b4c22..a3db5f86d 100644 --- a/.github/workflows/translations-pull.yml +++ b/.github/workflows/translations-pull.yml @@ -1,9 +1,5 @@ name: Update Translations -permissions: - contents: read - pull-requests: write - on: pull_request: paths: diff --git a/.github/workflows/translations-push.yml b/.github/workflows/translations-push.yml index 41a7aef40..c4d3f61fb 100644 --- a/.github/workflows/translations-push.yml +++ b/.github/workflows/translations-push.yml @@ -1,9 +1,5 @@ name: Update Translation Source -permissions: - contents: read - pull-requests: write - on: pull_request: paths: diff --git a/CMakeLists.txt b/CMakeLists.txt index 293e25dd9..27fecc979 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -5,23 +5,23 @@ # This file sets all the variables shared between the projects # like the installation path, compilation flags etc.. -# 3.16 required for Qt6 and target_precompile_headers() -cmake_minimum_required(VERSION 3.16) +# cmake 3.16 is required if using qt6 +cmake_minimum_required(VERSION 3.10) -# Use compiler cache (ccache) +# Early detect ccache option(USE_CCACHE "Cache the build results with ccache" ON) # Treat warnings as errors (Debug builds only) option(WARNING_AS_ERROR "Treat warnings as errors in debug builds" ON) # Check for translation updates option(UPDATE_TRANSLATIONS "Update translations on compile" OFF) -# Compile Cockatrice -option(WITH_CLIENT "Build Cockatrice client" ON) -# Compile Oracle -option(WITH_ORACLE "Build Cockatrice card database tool (Oracle)" ON) -# Compile Servatrice -option(WITH_SERVER "Build Cockatrice server (Servatrice)" OFF) +# Compile servatrice +option(WITH_SERVER "build servatrice" OFF) +# Compile cockatrice +option(WITH_CLIENT "build cockatrice" ON) +# Compile oracle +option(WITH_ORACLE "build oracle" ON) # Compile tests -option(TEST "Build tests" OFF) +option(TEST "build tests" OFF) # Use vcpkg regardless of OS option(USE_VCPKG "Use vcpkg regardless of OS" OFF) @@ -39,24 +39,13 @@ else() ) endif() -# 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) +if(USE_CCACHE) find_program(CCACHE_PROGRAM ccache) if(CCACHE_PROGRAM) # Support Unix Makefiles and Ninja set_property(GLOBAL PROPERTY RULE_LAUNCH_COMPILE "${CCACHE_PROGRAM}") - # PCH-aware caching, matching .ci/compile.sh: without this ccache refuses - # to cache any TU that consumes a precompiled header, so every PCH-backed - # target recompiles from scratch on each build. - execute_process(COMMAND ${CCACHE_PROGRAM} --set-config sloppiness=pch_defines,time_macros) message(STATUS "Found CCache ${CCACHE_PROGRAM}") 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) @@ -195,9 +184,6 @@ elseif(CMAKE_COMPILER_IS_GNUCXX) set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} ${FLAG}") endif() endforeach() - - # Reduce compiler I/O by using pipes between stages instead of temp files - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -pipe") else() # other: osx/llvm, bsd/llvm set(CMAKE_CXX_FLAGS_RELEASE "-O2") @@ -206,9 +192,6 @@ else() else() set(CMAKE_CXX_FLAGS_DEBUG "-g -O0 -Wall -Wextra") endif() - - # Reduce compiler I/O by using pipes between stages instead of temp files - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -pipe") endif() # GNU systems need to define the Mersenne exponent for the RNG to compile w/o warning @@ -256,6 +239,11 @@ if(WIN32) find_package(OpenSSL REQUIRED) if(OPENSSL_FOUND) include_directories(${OPENSSL_INCLUDE_DIRS}) + else() + message( + WARNING + "Could not find OpenSSL runtime libraries. They are not required for compiling, but needs to be available at runtime." + ) endif() endif() @@ -293,7 +281,7 @@ if(UNIX) if(CPACK_GENERATOR STREQUAL "RPM") set(CPACK_RPM_PACKAGE_LICENSE "GPLv2") set(CPACK_RPM_MAIN_COMPONENT "cockatrice") - set(CPACK_RPM_PACKAGE_REQUIRES "protobuf, qt6-qtimageformats, qt6-qtmultimedia, qt6-qtsvg, qt6-qttools") + set(CPACK_RPM_PACKAGE_REQUIRES "protobuf, qt6-qttools, qt6-qtsvg, qt6-qtmultimedia, qt6-qtimageformats") set(CPACK_RPM_PACKAGE_GROUP "Amusements/Games") set(CPACK_RPM_PACKAGE_URL "http://github.com/Cockatrice/Cockatrice") # stop directories from making package conflicts @@ -311,7 +299,7 @@ if(UNIX) set(CPACK_DEBIAN_PACKAGE_SHLIBDEPS ON) set(CPACK_DEBIAN_PACKAGE_SECTION "games") set(CPACK_DEBIAN_PACKAGE_HOMEPAGE "http://github.com/Cockatrice/Cockatrice") - set(CPACK_DEBIAN_PACKAGE_DEPENDS "libqt6multimedia6, libqt6svg6, qt6-image-formats-plugins, qt6-qpa-plugins") + set(CPACK_DEBIAN_PACKAGE_DEPENDS "libqt6multimedia6, libqt6svg6, qt6-qpa-plugins, qt6-image-formats-plugins") set(CPACK_DEBIAN_PACKAGE_RECOMMENDS "libqt6sql6-mysql") # for connecting servatrice to a mysql db endif() endif() diff --git a/Dockerfile b/Dockerfile index 7d3deb5fb..7c5c773c9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,47 +3,35 @@ FROM ubuntu:26.04 AS build ARG DEBIAN_FRONTEND=noninteractive -RUN apt-get update \ - && apt-get install -y --no-install-recommends \ - build-essential \ - cmake \ - ninja-build \ - file \ - g++ \ - git \ - libmariadb-dev-compat \ - libprotobuf-dev \ - libqt6sql6-mysql \ - libssl-dev \ - qt6-websockets-dev \ - protobuf-compiler \ - qt6-tools-dev \ - qt6-tools-dev-tools +RUN apt-get update && apt-get install -y --no-install-recommends \ + build-essential \ + cmake \ + file \ + g++ \ + git \ + libmariadb-dev-compat \ + libprotobuf-dev \ + libqt6sql6-mysql \ + qt6-websockets-dev \ + protobuf-compiler \ + qt6-tools-dev \ + qt6-tools-dev-tools WORKDIR /src COPY . . -RUN cmake \ - -S . \ - -B build \ - -G Ninja \ - -DWITH_CLIENT=0 \ - -DWITH_ORACLE=0 \ - -DWITH_SERVER=1 \ - && cmake --build build \ - && cmake --install build +RUN mkdir build && cd build && \ + cmake .. -DWITH_SERVER=1 -DWITH_CLIENT=0 -DWITH_ORACLE=0 && \ + make -j$(nproc) && \ + make install # -------- Runtime Stage (clean) -------- FROM ubuntu:26.04 -ARG DEBIAN_FRONTEND=noninteractive - -RUN apt-get update \ - && apt-get install -y --no-install-recommends \ - libprotobuf32t64 \ - libqt6sql6-mysql \ - libqt6websockets6 \ - libssl3 \ +RUN apt-get update && apt-get install -y --no-install-recommends \ + libprotobuf32t64 \ + libqt6sql6-mysql \ + libqt6websockets6 \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* diff --git a/README.md b/README.md index 5935bb540..f22df461f 100644 --- a/README.md +++ b/README.md @@ -149,15 +149,15 @@ You can then
The following flags (with their non-default values) can be passed to `cmake`: -| Flag | Description | -| -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | -| `-DWITH_SERVER=1` | Build Servatrice server | -| `-DWITH_CLIENT=0` | Don't build Cockatrice client | -| `-DWITH_ORACLE=0` | Don't build Oracle card database tool | -| `-DCMAKE_BUILD_TYPE=Debug` | Compile in debug mode
Enables extra logging output, debug symbols, and much more verbose compiler warnings | -| `-DWARNING_AS_ERROR=0` | Don't treat compilation warnings as errors in debug mode | -| `-DUPDATE_TRANSLATIONS=1` | Configure `make` to update the translation .ts files for new strings in the source code
**Note:** `make clean` will remove the .ts files | -| `-DTEST=1` | Enable regression tests
**Note:** `make test` to run tests, *googletest* will be downloaded if not available | +| Flag | Description | +| --- | --- | +| `-DWITH_SERVER=1` | Build Servatrice server | +| `-DWITH_CLIENT=0` | Don't build Cockatrice client | +| `-DWITH_ORACLE=0` | Don't build Oracle card database tool | +| `-DCMAKE_BUILD_TYPE=Debug` | Compile in debug mode
Enables extra logging output, debug symbols, and much more verbose compiler warnings | +| `-DWARNING_AS_ERROR=0` | Don't treat compilation warnings as errors in debug mode | +| `-DUPDATE_TRANSLATIONS=1` | Configure `make` to update the translation .ts files for new strings in the source code
**Note:** `make clean` will remove the .ts files | +| `-DTEST=1` | Enable regression tests
**Note:** `make test` to run tests, *googletest* will be downloaded if not available | # Run diff --git a/cmake/FindQtRuntime.cmake b/cmake/FindQtRuntime.cmake index 971c9094d..8a3050813 100644 --- a/cmake/FindQtRuntime.cmake +++ b/cmake/FindQtRuntime.cmake @@ -18,17 +18,14 @@ if(WITH_CLIENT) Multimedia Network PrintSupport - ShaderTools Svg WebSockets Widgets Xml - Quick - QuickWidgets ) endif() if(WITH_ORACLE) - set(_ORACLE_NEEDED Concurrent Network Svg Widgets Xml) + set(_ORACLE_NEEDED Concurrent Network Svg Widgets) endif() if(TEST) # Union of Qt modules required across all test targets (independent of application targets). diff --git a/cmake/NSIS.template.in b/cmake/NSIS.template.in index b3cbcece8..5af116470 100644 --- a/cmake/NSIS.template.in +++ b/cmake/NSIS.template.in @@ -387,14 +387,19 @@ SectionEnd Section "un.Application" UnSecApplication SetShellVarContext all + RMDir /r "$INSTDIR\plugins" + RMDir /r "$INSTDIR\sounds" + RMDir /r "$INSTDIR\themes" + RMDir /r "$INSTDIR\translations" + Delete "$INSTDIR\*.exe" + Delete "$INSTDIR\*.dll" + Delete "$INSTDIR\qt.conf" + Delete "$INSTDIR\qdebug.txt" + Delete "$INSTDIR\servatrice.sql" + Delete "$INSTDIR\servatrice.ini.example" + RMDir "$INSTDIR" - ; Remove the entire application directory so any file that is not part of - ; the installed payload (e.g. build-tree artifacts such as *.dir folders, - ; *_autogen and *.tlog files from a build) cannot survive between an - ; uninstall and a fresh reinstall. - RMDir /r "$INSTDIR" - - RMDir /r "$SMPROGRAMS\Cockatrice" + RMDir "$SMPROGRAMS\Cockatrice" DeleteRegKey HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Cockatrice" SectionEnd diff --git a/cmake/pch/qtcore_pch.h b/cmake/pch/qtcore_pch.h deleted file mode 100644 index cc3dd12ee..000000000 --- a/cmake/pch/qtcore_pch.h +++ /dev/null @@ -1,24 +0,0 @@ -/** @file qtcore_pch.h - * @brief Precompiled header for all Qt targets (Qt Core only). - * - * Safe for every target that links Qt Core, including the headless - * Servatrice binary. Keep this header free of any widget/gui types. - */ - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include diff --git a/cmake/pch/qtwidgets_pch.h b/cmake/pch/qtwidgets_pch.h deleted file mode 100644 index 2c63f450e..000000000 --- a/cmake/pch/qtwidgets_pch.h +++ /dev/null @@ -1,30 +0,0 @@ -/** @file qtwidgets_pch.h - * @brief Precompiled header for GUI targets (Cockatrice client, Oracle). - * - * Includes the Qt Core precompiled header plus the heavy Gui, Widgets and - * Network layers that virtually every client translation unit re-parses. - * Do not use on Servatrice (headless, QT_DONT_USE_QTGUI). - */ - -#include "qtcore_pch.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include diff --git a/cockatrice/CMakeLists.txt b/cockatrice/CMakeLists.txt index 9b31310e6..ed8e49f2d 100644 --- a/cockatrice/CMakeLists.txt +++ b/cockatrice/CMakeLists.txt @@ -15,9 +15,6 @@ set(cockatrice_SOURCES src/client/network/update/client/client_update_checker.cpp src/client/network/update/client/release_channel.cpp src/client/network/update/card_spoiler/spoiler_background_updater.cpp - src/client/latency_graph_widget.cpp - src/client/latency_status_widget.cpp - src/client/lag_monitor.cpp src/client/sound_engine.cpp src/client/settings/cache_settings.cpp src/client/settings/card_counter_settings.cpp @@ -39,20 +36,14 @@ set(cockatrice_SOURCES src/interface/widgets/dialogs/dlg_forgot_password_challenge.cpp src/interface/widgets/dialogs/dlg_forgot_password_request.cpp src/interface/widgets/dialogs/dlg_forgot_password_reset.cpp - src/interface/widgets/dialogs/dlg_invite_to_game.cpp src/interface/widgets/dialogs/dlg_load_deck.cpp src/interface/widgets/dialogs/dlg_load_deck_from_clipboard.cpp src/interface/widgets/dialogs/dlg_load_deck_from_website.cpp src/interface/widgets/dialogs/dlg_load_remote_deck.cpp src/interface/widgets/dialogs/dlg_local_game_options.cpp - src/interface/widgets/dialogs/dlg_login_prompt.cpp src/interface/widgets/dialogs/dlg_manage_sets.cpp - src/interface/widgets/dialogs/dlg_my_reports.cpp src/interface/widgets/dialogs/dlg_register.cpp - src/interface/widgets/dialogs/dlg_report_user.cpp src/interface/widgets/dialogs/dlg_select_set_for_cards.cpp - src/interface/widgets/dialogs/dlg_share_deck.cpp - src/interface/widgets/dialogs/dlg_shared_decks_preview.cpp src/interface/widgets/dialogs/dlg_settings.cpp src/interface/widgets/dialogs/dlg_startup_card_check.cpp src/interface/widgets/dialogs/dlg_tip_of_the_day.cpp @@ -60,9 +51,6 @@ set(cockatrice_SOURCES src/interface/widgets/dialogs/dlg_view_log.cpp src/interface/widgets/dialogs/override_printing_warning.cpp src/interface/widgets/dialogs/tip_of_the_day.cpp - src/interface/widgets/deck_share/deck_share_utils.cpp - src/interface/widgets/deck_share/shared_deck_preview_widget.cpp - src/interface/widgets/deck_share/share_bar_widget.cpp src/filters/deck_filter_string.cpp src/filters/filter_builder.cpp src/filters/filter_tree_model.cpp @@ -155,11 +143,7 @@ set(cockatrice_SOURCES src/interface/intents/intent_wait_for_database_load.h src/interface/layouts/flow_layout.cpp src/interface/layouts/overlap_layout.cpp - src/interface/widgets/utility/card_completer_delegate.cpp - src/interface/widgets/utility/card_completer_styler.cpp - src/interface/widgets/utility/completer_utils.cpp src/interface/widgets/utility/line_edit_completer.cpp - src/interface/widgets/utility/reversed_completer_model.cpp src/interface/pixel_map_generator.cpp src/interface/theme_config.cpp src/interface/theme_manager.cpp @@ -169,11 +153,8 @@ set(cockatrice_SOURCES src/interface/palette_editor/palette_grid_widget.cpp src/interface/palette_editor/palette_editor_dialog.cpp src/interface/widgets/cards/additional_info/color_identity_widget.cpp - src/interface/widgets/cards/additional_info/deck_color_identity.cpp src/interface/widgets/cards/additional_info/mana_cost_widget.cpp src/interface/widgets/cards/additional_info/mana_symbol_widget.cpp - src/interface/widgets/cards/art_crop_attribution.cpp - src/interface/widgets/cards/card_art_utils.cpp src/interface/widgets/cards/card_group_display_widgets/card_group_display_widget.cpp src/interface/widgets/cards/card_group_display_widgets/flat_card_group_display_widget.cpp src/interface/widgets/cards/card_group_display_widgets/overlapped_card_group_display_widget.cpp @@ -222,7 +203,6 @@ set(cockatrice_SOURCES src/interface/widgets/deck_editor/deck_editor_printing_selector_dock_widget.cpp src/interface/widgets/deck_editor/deck_list_style_proxy.cpp src/interface/widgets/deck_editor/deck_state_manager.cpp - src/interface/widgets/deck_editor/deck_zone_dialog.cpp src/interface/widgets/deck_editor/printing_disabled_info_widget.cpp src/interface/widgets/general/background_sources.cpp src/interface/widgets/general/display/background_plate_widget.cpp @@ -239,7 +219,6 @@ set(cockatrice_SOURCES src/interface/widgets/general/display/charts/bars/segmented_bar_widget.cpp src/interface/widgets/general/display/charts/pies/color_pie.cpp src/interface/widgets/general/home_styled_button.cpp - src/interface/widgets/general/home_tab_button_color.h src/interface/widgets/general/home_widget.cpp src/interface/widgets/general/layout_containers/flow_widget.cpp src/interface/widgets/general/layout_containers/overlap_control_widget.cpp @@ -255,10 +234,6 @@ set(cockatrice_SOURCES src/interface/widgets/printing_selector/printing_selector_card_selection_widget.cpp src/interface/widgets/printing_selector/printing_selector_card_sorting_widget.cpp src/interface/widgets/printing_selector/set_name_and_collectors_number_display_widget.cpp - src/interface/widgets/playmat/playmat_collection_dialog.cpp - src/interface/widgets/playmat/playmat_collection_dialog.h - src/interface/widgets/playmat/playmat_preview_widget.cpp - 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_manager.cpp @@ -267,7 +242,6 @@ set(cockatrice_SOURCES src/interface/widgets/replay/replay_widget.cpp src/interface/widgets/server/chat_view/chat_view.cpp src/interface/widgets/server/game_filter_configs.cpp - src/interface/widgets/server/game_link.cpp src/interface/widgets/server/game_selector.cpp src/interface/widgets/server/game_selector_quick_filter_toolbar.cpp src/interface/widgets/server/games_model.cpp @@ -280,10 +254,8 @@ set(cockatrice_SOURCES src/interface/widgets/server/user/user_context_menu.cpp src/interface/widgets/server/user/user_info_box.cpp src/interface/widgets/server/user/user_info_connection.cpp - src/interface/widgets/server/user/user_list_dialog.cpp src/interface/widgets/server/user/user_list_manager.cpp src/interface/widgets/server/user/user_list_painter.cpp - src/interface/widgets/server/user/user_list_panel_widget.cpp src/interface/widgets/server/user/user_list_widget.cpp src/interface/widgets/settings_page/abstract_settings_page.cpp src/interface/widgets/settings_page/appearance_settings_page.cpp @@ -298,8 +270,6 @@ set(cockatrice_SOURCES src/interface/widgets/settings_page/user_interface_settings_page.cpp src/interface/widgets/utility/custom_line_edit.cpp src/interface/widgets/utility/get_text_with_max.cpp - src/interface/widgets/utility/report_utils.cpp - src/interface/widgets/utility/report_utils.h src/interface/widgets/utility/sequence_edit.cpp src/interface/widgets/utility/visibility_change_listener.cpp src/interface/widgets/utility/visibility_change_listener.h @@ -324,13 +294,9 @@ set(cockatrice_SOURCES src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_tag_display_widget.cpp src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_tag_item_widget.cpp src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_widget.cpp - src/interface/widgets/visual_deck_storage/deck_preview/public_deck_preview_widget.cpp - src/interface/widgets/visual_deck_storage/remote_public_decks_model.cpp src/interface/widgets/visual_deck_storage/visual_deck_storage_folder_display_widget.cpp - src/interface/widgets/visual_deck_storage/visual_deck_storage_model.cpp src/interface/widgets/visual_deck_storage/visual_deck_storage_quick_settings_widget.cpp src/interface/widgets/visual_deck_storage/visual_deck_storage_search_widget.cpp - src/interface/widgets/visual_deck_storage/visual_deck_storage_sort_filter_proxy_model.cpp src/interface/widgets/visual_deck_storage/visual_deck_storage_sort_widget.cpp src/interface/widgets/visual_deck_storage/visual_deck_storage_tag_filter_widget.cpp src/interface/widgets/visual_deck_storage/visual_deck_storage_widget.cpp @@ -391,14 +357,10 @@ set(cockatrice_SOURCES 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 - src/interface/widgets/tabs/tab_developer.cpp src/interface/widgets/tabs/tab_game.cpp src/interface/widgets/tabs/tab_home.cpp src/interface/widgets/tabs/tab_logs.cpp src/interface/widgets/tabs/tab_message.cpp - src/interface/widgets/tabs/tab_moderation.cpp - src/interface/widgets/tabs/tab_public_decks.cpp - src/interface/widgets/tabs/tab_report.cpp src/interface/widgets/tabs/tab_replays.cpp src/interface/widgets/tabs/tab_room.cpp src/interface/widgets/tabs/tab_server.cpp @@ -412,28 +374,6 @@ set(cockatrice_SOURCES src/interface/widgets/tabs/api/commander_spellbook/commander_bracket_service.cpp src/interface/widgets/tabs/api/commander_spellbook/commander_bracket_widget.cpp src/interface/widgets/tabs/api/commander_spellbook/handle_commander_brackets.cpp - src/interface/widgets/onboarding/banner_shader_config.h - src/interface/widgets/onboarding/brand_colors.h - src/interface/widgets/onboarding/first_run_wizard.cpp - src/interface/widgets/onboarding/first_run_wizard.h - src/interface/widgets/onboarding/first_run_wizard_page.cpp - src/interface/widgets/onboarding/first_run_wizard_page.h - src/interface/widgets/onboarding/pages/account_setup_page.cpp - src/interface/widgets/onboarding/pages/account_setup_page.h - src/interface/widgets/onboarding/pages/card_database_setup_page.cpp - src/interface/widgets/onboarding/pages/card_database_setup_page.h - src/interface/widgets/onboarding/pages/finish_page.cpp - src/interface/widgets/onboarding/pages/finish_page.h - src/interface/widgets/onboarding/pages/preferences_setup_page.cpp - src/interface/widgets/onboarding/pages/preferences_setup_page.h - src/interface/widgets/onboarding/pages/theme_setup_page.cpp - src/interface/widgets/onboarding/pages/theme_setup_page.h - src/interface/widgets/onboarding/pages/welcome_page.cpp - src/interface/widgets/onboarding/pages/welcome_page.h - src/interface/widgets/onboarding/shader_banner_widget.cpp - src/interface/widgets/onboarding/shader_banner_widget.h - src/interface/widgets/onboarding/step_indicator_widget.cpp - src/interface/widgets/onboarding/step_indicator_widget.h src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_bracket_navigation_widget.cpp src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_bracket_navigation_widget.h src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_budget_navigation_widget.cpp @@ -452,10 +392,6 @@ set(cockatrice_SOURCES src/interface/intents/intent_join_server_room.h src/interface/intents/intent_login.cpp src/interface/intents/intent_login.h - src/interface/intents/intent_open_server_room_by_name.cpp - src/interface/intents/intent_open_server_room_by_name.h - src/interface/intents/intent_open_shared_deck.cpp - src/interface/intents/intent_open_shared_deck.h src/interface/intents/url_parser.cpp src/interface/intents/url_parser.h src/interface/widgets/server/user/user_info_popup.cpp @@ -532,31 +468,6 @@ qt6_add_executable( MANUAL_FINALIZATION ) -target_precompile_headers(cockatrice PRIVATE "${CMAKE_SOURCE_DIR}/cmake/pch/qtwidgets_pch.h") - -qt6_add_shaders( - cockatrice - "onboarding_shaders" - PREFIX - "/onboarding/shaders" - BASE - "src/interface/widgets/onboarding/shaders" - FILES - src/interface/widgets/onboarding/shaders/brand_banner.frag - src/interface/widgets/onboarding/shaders/brand_plate.frag -) - -qt6_add_resources( - cockatrice - "onboarding_qml" - PREFIX - "/onboarding/qml" - BASE - "src/interface/widgets/onboarding/qml" - FILES - src/interface/widgets/onboarding/qml/BrandBanner.qml -) - target_link_libraries( cockatrice PUBLIC libcockatrice_card @@ -668,35 +579,18 @@ if(WIN32) set(qtconf_dest_dir .) install( - DIRECTORY "$/" + DIRECTORY "${CMAKE_BINARY_DIR}/${PROJECT_NAME}/${CMAKE_BUILD_TYPE}/" DESTINATION ./ FILES_MATCHING PATTERN "*.dll" - PATTERN "*.pdb" EXCLUDE - PATTERN "*.dir*" EXCLUDE - PATTERN "*_autogen*" EXCLUDE - PATTERN "*.tlog*" EXCLUDE - PATTERN "CMakeFiles*" EXCLUDE - PATTERN "x64*" EXCLUDE - PATTERN ".qt*" EXCLUDE - PATTERN ".qsb*" EXCLUDE - PATTERN ".lupdate*" EXCLUDE ) install( DIRECTORY "${CMAKE_BINARY_DIR}/cockatrice/" DESTINATION ./ FILES_MATCHING + PATTERN "CMakeFiles" EXCLUDE PATTERN "*.ini" - PATTERN "CMakeFiles*" EXCLUDE - PATTERN "*.dir*" EXCLUDE - PATTERN "*_autogen*" EXCLUDE - PATTERN "*.tlog*" EXCLUDE - PATTERN "*.pdb" EXCLUDE - PATTERN "x64*" EXCLUDE - PATTERN ".qt*" EXCLUDE - PATTERN ".qsb*" EXCLUDE - PATTERN ".lupdate*" EXCLUDE ) # Qt plugins: audio, iconengines, imageformats, multimedia, platforms, printsupport, styles, tls @@ -749,6 +643,10 @@ Data = Resources\") " COMPONENT Runtime ) + + if(OPENSSL_FOUND) + install(FILES ${OPENSSL_INCLUDE_DIRS} DESTINATION ./) + endif() endif() if(Qt6LinguistTools_FOUND) diff --git a/cockatrice/cockatrice.qrc b/cockatrice/cockatrice.qrc index 14cf15b2f..9c34929b7 100644 --- a/cockatrice/cockatrice.qrc +++ b/cockatrice/cockatrice.qrc @@ -2,7 +2,6 @@ resources/cardback.svg resources/cockatrice.svg - resources/cockatrice-logo-white.svg resources/hand.svg resources/hr.jpg @@ -63,8 +62,6 @@ resources/icons/mana/W.svg resources/backgrounds/home.png - resources/backgrounds/home-dark.png - resources/backgrounds/home-light.png resources/backgrounds/card_triplet.svg resources/backgrounds/placeholder_printing_selector.svg @@ -365,8 +362,6 @@ resources/usericons/pawn_single.svg resources/usericons/pawn_double.svg - resources/usericons/pawn_dev_single.svg - resources/usericons/pawn_dev_double.svg resources/usericons/pawn_donator_single.svg resources/usericons/pawn_donator_double.svg resources/usericons/pawn_judge_single.svg diff --git a/cockatrice/resources/backgrounds/home-dark.png b/cockatrice/resources/backgrounds/home-dark.png deleted file mode 100644 index 68f48e2c2..000000000 Binary files a/cockatrice/resources/backgrounds/home-dark.png and /dev/null differ diff --git a/cockatrice/resources/backgrounds/home-light.png b/cockatrice/resources/backgrounds/home-light.png deleted file mode 100644 index eaaaba932..000000000 Binary files a/cockatrice/resources/backgrounds/home-light.png and /dev/null differ diff --git a/cockatrice/resources/backgrounds/home.png b/cockatrice/resources/backgrounds/home.png index eaaaba932..68f48e2c2 100644 Binary files a/cockatrice/resources/backgrounds/home.png and b/cockatrice/resources/backgrounds/home.png differ diff --git a/cockatrice/resources/cockatrice-logo-white.svg b/cockatrice/resources/cockatrice-logo-white.svg deleted file mode 100644 index b3b31077f..000000000 --- a/cockatrice/resources/cockatrice-logo-white.svg +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - diff --git a/cockatrice/resources/cockatrice.svg b/cockatrice/resources/cockatrice.svg index 89ba62dcf..d2e22da31 100644 --- a/cockatrice/resources/cockatrice.svg +++ b/cockatrice/resources/cockatrice.svg @@ -2,20 +2,20 @@ + sodipodi:docname="cockatrice.svg"> - - + inkscape:current-layer="g3026" /> + + + + + + + + + + + + + + + + + - - - - - - - - + + + + + + + + + + + + + + @@ -159,7 +199,7 @@ image/svg+xml - + @@ -173,60 +213,170 @@ inkscape:export-xdpi="91.459999" inkscape:export-ydpi="91.459999"> + + + + + + + + + + + + + + diff --git a/cockatrice/resources/countries/cn.svg b/cockatrice/resources/countries/cn.svg index 45b608127..f510cf049 100644 --- a/cockatrice/resources/countries/cn.svg +++ b/cockatrice/resources/countries/cn.svg @@ -52,7 +52,8 @@ id="defs8"> - + diff --git a/cockatrice/resources/help/search.md b/cockatrice/resources/help/search.md index fd0a12507..0c8bdb450 100644 --- a/cockatrice/resources/help/search.md +++ b/cockatrice/resources/help/search.md @@ -52,7 +52,6 @@ In this list of examples below, each entry has an explanation and can be clicked
Edition:
[set:lea](#set:lea) (Cards that appear in Alpha, which has the set code LEA)
[e:lea OR e:leb](#e:lea OR e:leb) (Cards that appear in Alpha or Beta)
-
[e<8ED](#e<8ED) (Cards that appear before 8th edition)
Negate:
[c:wu -c:m](#c:wu -c:m) (Any card that is white or blue, but not multicolored)
diff --git a/cockatrice/resources/usericons/pawn_dev_double.svg b/cockatrice/resources/usericons/pawn_dev_double.svg deleted file mode 100644 index 57ed5c2da..000000000 --- a/cockatrice/resources/usericons/pawn_dev_double.svg +++ /dev/null @@ -1,343 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - image/svg+xml - - - - - - - - - - - diff --git a/cockatrice/resources/usericons/pawn_dev_single.svg b/cockatrice/resources/usericons/pawn_dev_single.svg deleted file mode 100644 index f7c4e7018..000000000 --- a/cockatrice/resources/usericons/pawn_dev_single.svg +++ /dev/null @@ -1,211 +0,0 @@ - - - -image/svg+xml - - diff --git a/cockatrice/src/client/lag_monitor.cpp b/cockatrice/src/client/lag_monitor.cpp deleted file mode 100644 index 383d64644..000000000 --- a/cockatrice/src/client/lag_monitor.cpp +++ /dev/null @@ -1,63 +0,0 @@ -#include "lag_monitor.h" - -#include -#include -#include - -LagMonitor::LagMonitor(QObject *parent) : QObject(parent) -{ - qApp->installEventFilter(this); - - timer = new QTimer(this); - timer->setInterval(TICK_INTERVAL_MS); - connect(timer, &QTimer::timeout, this, &LagMonitor::checkTick); - tickClock.start(); - timer->start(); -} - -QList LagMonitor::recentStalls() const -{ - return stalls; -} - -void LagMonitor::clearStalls() -{ - stalls.clear(); -} - -bool LagMonitor::eventFilter(QObject *obj, QEvent *event) -{ - if (event->type() == QEvent::ApplicationStateChange) { - // The transition may span a suspend or an arbitrary unfocused period; - // discard the gap so it cannot be mistaken for a stall. - tickClock.restart(); - } - return QObject::eventFilter(obj, event); -} - -void LagMonitor::checkTick() -{ - recordGap(tickClock.restart()); -} - -void LagMonitor::recordGap(qint64 gapMs) -{ - if (gapMs <= STALL_THRESHOLD_MS) { - return; - } - - if (gapMs > MAX_PLAUSIBLE_STALL_MS) { - qCDebug(LagMonitorLog, "Ignoring implausible %lld ms gap (likely suspend)", static_cast(gapMs)); - return; - } - - const StallRecord record{.timestampMsSinceEpoch = QDateTime::currentMSecsSinceEpoch(), .durationMs = gapMs}; - - stalls.append(record); - while (stalls.size() > MAX_RECORDED_STALLS) { - stalls.removeFirst(); - } - - qCWarning(LagMonitorLog, "Event loop stalled for %lld ms (threshold: %d ms)", static_cast(gapMs), - STALL_THRESHOLD_MS); -} diff --git a/cockatrice/src/client/lag_monitor.h b/cockatrice/src/client/lag_monitor.h deleted file mode 100644 index 9fdf6b283..000000000 --- a/cockatrice/src/client/lag_monitor.h +++ /dev/null @@ -1,87 +0,0 @@ -/** - * @file lag_monitor.h - * @ingroup Client - */ - -#ifndef LAG_MONITOR_H -#define LAG_MONITOR_H - -#include -#include -#include -#include -#include - -inline Q_LOGGING_CATEGORY(LagMonitorLog, "lag_monitor"); - -class QEvent; -class QTimer; - -/** - * @brief Detects main-thread event loop stalls ("UI freezes") from the inside. - * - * A timer is expected to fire every TICK_INTERVAL_MS of wall time. When the - * observed gap greatly exceeds that interval, some other task blocked the - * event loop for roughly the overshooting duration. This is what separates - * "my client froze" from "the network is lagging" in user reports. - * - * Gaps that span an application state change (suspend, minimize, focus - * loss) are discarded, and implausibly huge gaps are dropped, so operating - * system power events do not fabricate stalls. This handling is load-bearing - * on Windows, where the monotonic clock used by Qt counts sleep time. - * - * Healthy operation costs one timer wakeup per tick and two integer - * comparisons. Allocations happen only when a stall is actually recorded. - */ -class LagMonitor : public QObject -{ - Q_OBJECT - -public: - struct StallRecord - { - qint64 timestampMsSinceEpoch = 0; ///< when the stalled period ended - qint64 durationMs = 0; ///< approximate length of the freeze; measured tick to tick, so it can exceed the true - ///< stall by up to TICK_INTERVAL_MS - }; - - static constexpr int TICK_INTERVAL_MS = 500; - static constexpr int STALL_THRESHOLD_MS = 2000; - static constexpr int MAX_RECORDED_STALLS = 32; - - /// Gaps beyond this are treated as suspend artifacts rather than stalls. - static constexpr qint64 MAX_PLAUSIBLE_STALL_MS = 600000; - - explicit LagMonitor(QObject *parent = nullptr); - - /** - * @brief Stalls recorded during this session, oldest first. - * - * Intended consumers are log output and the diagnostics export. The list - * holds at most MAX_RECORDED_STALLS entries. - */ - QList recentStalls() const; - - void clearStalls(); - - /** - * @brief Feeds a measured tick-to-tick gap through the detection logic. - * - * Split out of checkTick so threshold, plausibility, and trim behavior - * stay unit-testable without real timing. - */ - void recordGap(qint64 gapMs); - -protected: - bool eventFilter(QObject *obj, QEvent *event) override; - -private slots: - void checkTick(); - -private: - QTimer *timer; - QElapsedTimer tickClock; ///< monotonic clock, so wall clock steps do not fabricate stalls - QList stalls; -}; - -#endif diff --git a/cockatrice/src/client/latency_graph_widget.cpp b/cockatrice/src/client/latency_graph_widget.cpp deleted file mode 100644 index 46d6ccdd3..000000000 --- a/cockatrice/src/client/latency_graph_widget.cpp +++ /dev/null @@ -1,51 +0,0 @@ -/** - * @file latency_graph_widget.cpp - * @ingroup Client - */ - -#include "latency_graph_widget.h" - -#include - -LatencyGraphWidget::LatencyGraphWidget(QWidget *parent) : QWidget(parent) -{ -} - -void LatencyGraphWidget::setSamples(const QList &samplesMs) -{ - samples = samplesMs; - update(); -} - -void LatencyGraphWidget::paintEvent(QPaintEvent * /* event */) -{ - if (samples.isEmpty()) { - return; - } - - QPainter painter(this); - - // Heights are relative to the window's own worst sample (floored at - // MinScaleMs) so the shape of the variance stays readable even when every - // value is small. - qint64 heightScaleMs = MinScaleMs; - for (int sample : samples) { - heightScaleMs = qMax(heightScaleMs, static_cast(sample)); - } - - const qreal widthPerBar = static_cast(width()) / samples.size(); - for (int i = 0; i < samples.size(); ++i) { - const qreal heightRatio = qBound(0.0, static_cast(samples.at(i)) / heightScaleMs, 1.0); - const qreal barHeight = heightRatio * height(); - - // Colors follow an absolute quality ramp: a steady good ping stays - // green no matter how uniform the window is. - const qreal colorRatio = qBound(0.0, static_cast(samples.at(i)) / ColorScaleMs, 1.0); - QColor color; - color.setHsv(qRound(120.0 * (1.0 - colorRatio)), 255, 255); - - const QRectF bar(static_cast(i) * widthPerBar + 1.0, static_cast(height()) - barHeight, - qMax(1.0, widthPerBar - 2.0), barHeight); - painter.fillRect(bar, color); - } -} diff --git a/cockatrice/src/client/latency_graph_widget.h b/cockatrice/src/client/latency_graph_widget.h deleted file mode 100644 index 4f6f38f8b..000000000 --- a/cockatrice/src/client/latency_graph_widget.h +++ /dev/null @@ -1,43 +0,0 @@ -/** - * @file latency_graph_widget.h - * @ingroup Client - */ - -#ifndef LATENCY_GRAPH_WIDGET_H -#define LATENCY_GRAPH_WIDGET_H - -#include -#include - -/** - * @brief Bar graph of recent network round-trip samples. - * - * Draws one bar per sample, oldest on the left. Bar height is relative to the - * window's own scale so the shape of the variance stays readable, while bar - * color maps each sample onto an absolute quality ramp (green at rest through - * red at ColorScaleMs) so a steady good ping never looks alarming. Size - * agnostic: the status bar embeds a small instance while the latency detail - * popup shows a large one. - */ -class LatencyGraphWidget : public QWidget -{ - Q_OBJECT -public: - explicit LatencyGraphWidget(QWidget *parent = nullptr); - - /// Sample in milliseconds that maps to a fully red bar. - static constexpr qint64 ColorScaleMs = 500; - - void setSamples(const QList &samplesMs); - -protected: - void paintEvent(QPaintEvent *event) override; - -private: - /// Floor of the vertical scale in milliseconds. Keeps small windows readable. - static constexpr qint64 MinScaleMs = 100; - - QList samples; -}; - -#endif diff --git a/cockatrice/src/client/latency_status_widget.cpp b/cockatrice/src/client/latency_status_widget.cpp deleted file mode 100644 index 779c726eb..000000000 --- a/cockatrice/src/client/latency_status_widget.cpp +++ /dev/null @@ -1,111 +0,0 @@ -/** - * @file latency_status_widget.cpp - * @ingroup Client - */ - -#include "latency_status_widget.h" - -#include "latency_graph_widget.h" - -#include -#include -#include -#include - -LatencyStatusWidget::LatencyStatusWidget(QWidget *parent) : QWidget(parent) -{ - pingLabel = new QLabel(this); - pingLabel->setAccessibleName(tr("Ping")); - - latencyGraph = new LatencyGraphWidget(this); - latencyGraph->setFixedSize(90, 14); - - auto *layout = new QHBoxLayout(this); - layout->setContentsMargins(0, 0, 0, 0); - layout->setSpacing(4); - layout->addWidget(latencyGraph); - layout->addWidget(pingLabel); - - // Clicking anywhere in the area opens the detail view. - for (QObject *child : QList{pingLabel, latencyGraph}) { - child->installEventFilter(this); - } - setCursor(Qt::PointingHandCursor); - - hide(); -} - -void LatencyStatusWidget::updateData(const LatencyTracker::Stats &stats, const QList &samplesMs) -{ - latestSamples = samplesMs; - latencyGraph->setSamples(samplesMs); - if (popup && popup->isVisible() && detailGraph) { - detailGraph->setSamples(samplesMs); - } - - if (stats.sampleCount == 0) { - hide(); - return; - } - - const QString statsStr = statsText(stats); - - pingLabel->setText(tr("Ping: %1 ms").arg(stats.lastMs)); - pingLabel->setToolTip(statsStr); - pingLabel->setAccessibleDescription(statsStr); - if (popup && popup->isVisible() && detailLabel) { - detailLabel->setText(statsStr); - } - show(); -} - -bool LatencyStatusWidget::eventFilter(QObject *watched, QEvent *event) -{ - if ((watched == pingLabel || watched == latencyGraph) && event->type() == QEvent::MouseButtonPress) { - togglePopup(); - return true; - } - return QWidget::eventFilter(watched, event); -} - -void LatencyStatusWidget::togglePopup() -{ - if (!popup) { - popup = new QWidget(this, Qt::Popup | Qt::FramelessWindowHint); - auto *layout = new QVBoxLayout(popup); - layout->setContentsMargins(8, 8, 8, 8); - - detailLabel = new QLabel(popup); - detailLabel->setAccessibleName(tr("Connection latency details")); - detailLabel->setTextInteractionFlags(Qt::TextSelectableByMouse); - - detailGraph = new LatencyGraphWidget(popup); - detailGraph->setFixedSize(280, 80); - - layout->addWidget(detailLabel, 0, Qt::AlignLeft); - layout->addWidget(detailGraph, 0, Qt::AlignHCenter); - } - - if (popup->isVisible()) { - popup->hide(); - return; - } - - // Qt::Popup closes itself on any outside click, so just position and show. - if (latestSamples.isEmpty()) { - return; - } - detailGraph->setSamples(latestSamples); - detailLabel->setText(pingLabel->toolTip()); - popup->adjustSize(); - const QPoint anchor = mapToGlobal(QPoint(width() / 2, 0)); - popup->move(anchor.x() - popup->width() / 2, anchor.y() - popup->height() - 6); - popup->show(); -} - -QString LatencyStatusWidget::statsText(const LatencyTracker::Stats &stats) const -{ - return tr("Connection quality over the last %n sample(s):", "", stats.sampleCount) + "\n" + - tr("Last: %1 ms").arg(stats.lastMs) + "\n" + tr("Median: %1 ms").arg(stats.medianMs) + "\n" + - tr("95th percentile: %1 ms").arg(stats.p95Ms) + "\n" + tr("Maximum: %1 ms").arg(stats.maxMs); -} diff --git a/cockatrice/src/client/latency_status_widget.h b/cockatrice/src/client/latency_status_widget.h deleted file mode 100644 index d9e1d130c..000000000 --- a/cockatrice/src/client/latency_status_widget.h +++ /dev/null @@ -1,50 +0,0 @@ -/** - * @file latency_status_widget.h - * @ingroup Client - */ - -#ifndef LATENCY_STATUS_WIDGET_H -#define LATENCY_STATUS_WIDGET_H - -#include -#include -#include - -class QLabel; -class LatencyGraphWidget; - -/** - * @brief Status bar presentation of server round-trip health. - * - * Combines the textual "Ping" readout with a small LatencyGraphWidget - * sparkline of the rolling sample window. Clicking anywhere in the area opens - * a popup with a larger graph and the numeric statistics. It closes on any - * outside click. Stays hidden while disconnected or before any samples exist. - * Owns all latency display state so MainWindow only needs to forward one - * signal here. - */ -class LatencyStatusWidget : public QWidget -{ - Q_OBJECT -public: - explicit LatencyStatusWidget(QWidget *parent = nullptr); - -public slots: - void updateData(const LatencyTracker::Stats &stats, const QList &samplesMs); - -protected: - bool eventFilter(QObject *watched, QEvent *event) override; - -private: - void togglePopup(); - QString statsText(const LatencyTracker::Stats &stats) const; - - QLabel *pingLabel = nullptr; - LatencyGraphWidget *latencyGraph = nullptr; - QWidget *popup = nullptr; - LatencyGraphWidget *detailGraph = nullptr; - QLabel *detailLabel = nullptr; - QList latestSamples; -}; - -#endif diff --git a/cockatrice/src/client/network/connection_controller/remote_connection_controller.cpp b/cockatrice/src/client/network/connection_controller/remote_connection_controller.cpp index c1598bd25..4e425fb66 100644 --- a/cockatrice/src/client/network/connection_controller/remote_connection_controller.cpp +++ b/cockatrice/src/client/network/connection_controller/remote_connection_controller.cpp @@ -1,6 +1,5 @@ #include "remote_connection_controller.h" -#include "../../../interface/pixel_map_generator.h" #include "../../settings/cache_settings.h" #include "../interface/widgets/dialogs/dlg_connect.h" #include "../interface/widgets/dialogs/dlg_forgot_password_challenge.h" @@ -45,8 +44,6 @@ void ConnectionController::wireClientSignals() connect(remoteClient, &RemoteClient::statusChanged, this, &ConnectionController::onStatusChanged); - connect(remoteClient, &AbstractClient::pingStatsUpdated, this, &ConnectionController::pingStatsUpdated); - connect(remoteClient, &RemoteClient::userInfoChanged, this, &ConnectionController::onUserInfoReceived, Qt::BlockingQueuedConnection); @@ -181,7 +178,7 @@ void ConnectionController::onServerShutdownEvent(const Event_ServerShutdown &eve "games will be lost.\nReason for shutdown: %1", "", event.minutes()) .arg(QString::fromStdString(event.reason()))); - serverShutdownMessageBox.setIconPixmap(themePixmap(QStringLiteral("cockatrice")).scaled(64, 64)); + serverShutdownMessageBox.setIconPixmap(QPixmap("theme:cockatrice").scaled(64, 64)); serverShutdownMessageBox.setText(tr("Scheduled server shutdown")); serverShutdownMessageBox.setWindowModality(Qt::ApplicationModal); serverShutdownMessageBox.setVisible(true); @@ -299,15 +296,6 @@ void ConnectionController::onLoginError(int r, return; } - case Response::RespPasswordChangeRequired: { - QMessageBox::information( - dialogParent, tr("Password Change Required"), - tr("An administrator has reset your password. Please contact your server administrator to obtain " - "your temporary password, then log in and change it via Account -> Change Password.")); - remoteClient->disconnectFromServer(); - return; - } - case Response::RespServerFull: { QMessageBox::critical(dialogParent, tr("Server Full"), tr("The server has reached its maximum user capacity, please check back later.")); diff --git a/cockatrice/src/client/network/connection_controller/remote_connection_controller.h b/cockatrice/src/client/network/connection_controller/remote_connection_controller.h index bae99a3e0..7486bc81a 100644 --- a/cockatrice/src/client/network/connection_controller/remote_connection_controller.h +++ b/cockatrice/src/client/network/connection_controller/remote_connection_controller.h @@ -54,10 +54,6 @@ signals: // action enable/disable logic void statusChanged(ClientStatus status); - // Forwarded from AbstractClient::pingStatsUpdated. See that signal for the - // meaning of the parameters. - void pingStatsUpdated(const LatencyTracker::Stats &stats, const QList &samplesMs); - private slots: // Slots wired directly to RemoteClient signals void onStatusChanged(ClientStatus status); diff --git a/cockatrice/src/client/network/interfaces/deck_stats_interface.cpp b/cockatrice/src/client/network/interfaces/deck_stats_interface.cpp index 42292b2aa..8689a19e9 100644 --- a/cockatrice/src/client/network/interfaces/deck_stats_interface.cpp +++ b/cockatrice/src/client/network/interfaces/deck_stats_interface.cpp @@ -72,7 +72,7 @@ void DeckStatsInterface::copyDeckWithoutTokens(const DeckList &source, DeckList { auto copyIfNotAToken = [&destination](const auto node, const auto card) { CardInfoPtr dbCard = CardDatabaseManager::query()->getCardInfo(card->getName()); - if (dbCard && !dbCard->getIsToken() && node->getName() != DECK_ZONE_MAYBEBOARD) { + if (dbCard && !dbCard->getIsToken()) { DecklistCardNode *addedCard = destination.addCard(card->getName(), node->getName(), -1); addedCard->setNumber(card->getNumber()); } diff --git a/cockatrice/src/client/network/interfaces/tapped_out_interface.cpp b/cockatrice/src/client/network/interfaces/tapped_out_interface.cpp index 627b7fe34..5dc77fa2c 100644 --- a/cockatrice/src/client/network/interfaces/tapped_out_interface.cpp +++ b/cockatrice/src/client/network/interfaces/tapped_out_interface.cpp @@ -99,7 +99,7 @@ void TappedOutInterface::copyDeckSplitMainAndSide(const DeckList &source, DeckLi { auto copyMainOrSide = [&mainboard, &sideboard](const auto node, const auto card) { CardInfoPtr dbCard = CardDatabaseManager::query()->getCardInfo(card->getName()); - if (!dbCard || dbCard->getIsToken() || node->getName() == DECK_ZONE_MAYBEBOARD) { + if (!dbCard || dbCard->getIsToken()) { return; } diff --git a/cockatrice/src/client/settings/shortcuts_settings.h b/cockatrice/src/client/settings/shortcuts_settings.h index f4ebc204e..95155b8d1 100644 --- a/cockatrice/src/client/settings/shortcuts_settings.h +++ b/cockatrice/src/client/settings/shortcuts_settings.h @@ -786,10 +786,6 @@ private: ShortcutGroup::Tabs)}, {"Tabs/aTabLogs", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Logs"), parseSequenceString(""), ShortcutGroup::Tabs)}, - {"Tabs/aTabReport", - ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Report Queue"), parseSequenceString(""), ShortcutGroup::Tabs)}, - {"Tabs/aTabModeration", - ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Moderation"), parseSequenceString(""), ShortcutGroup::Tabs)}, }; }; diff --git a/cockatrice/src/client/sound_engine.cpp b/cockatrice/src/client/sound_engine.cpp index 96cafa3d3..18de2264d 100644 --- a/cockatrice/src/client/sound_engine.cpp +++ b/cockatrice/src/client/sound_engine.cpp @@ -94,7 +94,7 @@ QStringMap &SoundEngine::getAvailableThemes() QDir dir; availableThemes.clear(); - // Load themes from user profile dir + // load themes from user profile dir dir.setPath(SettingsCache::instance().getDataPath() + "/sounds"); @@ -104,7 +104,7 @@ QStringMap &SoundEngine::getAvailableThemes() } } - // Load themes from Cockatrice system dir + // load themes from cockatrice system dir dir.setPath(qApp->applicationDirPath() + #ifdef Q_OS_MAC "/../Resources/sounds" diff --git a/cockatrice/src/filters/deck_filter_string.cpp b/cockatrice/src/filters/deck_filter_string.cpp index a2b7519a8..dd873cfa5 100644 --- a/cockatrice/src/filters/deck_filter_string.cpp +++ b/cockatrice/src/filters/deck_filter_string.cpp @@ -43,12 +43,6 @@ NumericValue <- [0-9]+ static std::once_flag init; -// The peglib parser is a single permanent object, so the rule actions below cannot see -// per-instance state. The card language that the nested [[card name]] search matches -// against is passed through this thread-local context, which is live only while a -// DeckFilterString is being parsed, and copied into the nested FilterString closures. -thread_local CardSearchLanguage deckSearchLanguageContext; - static void setupParserRules() { // plumbing @@ -58,14 +52,18 @@ static void setupParserRules() search["Start"] = passthru; search["QueryPartList"] = [](const peg::SemanticValues &sv) -> DeckFilter { - return [=](const DeckSearchData &data) { - auto matchesFilter = [&data](const std::any &query) { return std::any_cast(query)(data); }; + return [=](const DeckPreviewWidget *deck, const ExtraDeckSearchInfo &info) { + auto matchesFilter = [&deck, &info](const std::any &query) { + return std::any_cast(query)(deck, info); + }; return std::all_of(sv.begin(), sv.end(), matchesFilter); }; }; search["ComplexQueryPart"] = [](const peg::SemanticValues &sv) -> DeckFilter { - return [=](const DeckSearchData &data) { - auto matchesFilter = [&data](const std::any &query) { return std::any_cast(query)(data); }; + return [=](const DeckPreviewWidget *deck, const ExtraDeckSearchInfo &info) { + auto matchesFilter = [&deck, &info](const std::any &query) { + return std::any_cast(query)(deck, info); + }; return std::any_of(sv.begin(), sv.end(), matchesFilter); }; }; @@ -73,7 +71,9 @@ static void setupParserRules() search["QueryPart"] = passthru; search["NotQuery"] = [](const peg::SemanticValues &sv) -> DeckFilter { const auto dependent = std::any_cast(sv[0]); - return [=](const DeckSearchData &data) -> bool { return !dependent(data); }; + return [=](const DeckPreviewWidget *deck, const ExtraDeckSearchInfo &info) -> bool { + return !dependent(deck, info); + }; }; search["String"] = [](const peg::SemanticValues &sv) -> QString { @@ -122,12 +122,12 @@ static void setupParserRules() // actual functionality search["DeckContentQuery"] = [](const peg::SemanticValues &sv) -> DeckFilter { - auto cardFilter = FilterString(std::any_cast(sv[0]), deckSearchLanguageContext); + auto cardFilter = FilterString(std::any_cast(sv[0])); auto numberMatcher = sv.size() > 1 ? std::any_cast(sv[1]) : [](int count) { return count > 0; }; - return [=](const DeckSearchData &data) -> bool { + return [=](const DeckPreviewWidget *deck, const ExtraDeckSearchInfo &) -> bool { int count = 0; - auto cardNodes = data.deck->deckList.getCardNodes(); + auto cardNodes = deck->deckLoader->getDeck().deckList.getCardNodes(); for (auto node : cardNodes) { auto cardInfoPtr = CardDatabaseManager::query()->getCardInfo(node->getName()); if (!cardInfoPtr.isNull() && cardFilter.check(cardInfoPtr)) { @@ -146,53 +146,57 @@ static void setupParserRules() search["DeckNameQuery"] = [](const peg::SemanticValues &sv) -> DeckFilter { auto name = std::any_cast(sv[0]); - return [=](const DeckSearchData &data) { - return data.deck->deckList.getName().contains(name, Qt::CaseInsensitive); + return [=](const DeckPreviewWidget *deck, const ExtraDeckSearchInfo &) { + return deck->deckLoader->getDeck().deckList.getName().contains(name, Qt::CaseInsensitive); }; }; search["FileNameQuery"] = [](const peg::SemanticValues &sv) -> DeckFilter { auto name = std::any_cast(sv[0]); - return [=](const DeckSearchData &data) { - auto filename = QFileInfo(data.filePath).fileName(); + return [=](const DeckPreviewWidget *deck, const ExtraDeckSearchInfo &) { + auto filename = QFileInfo(deck->filePath).fileName(); return filename.contains(name, Qt::CaseInsensitive); }; }; search["PathQuery"] = [](const peg::SemanticValues &sv) -> DeckFilter { auto name = std::any_cast(sv[0]); - return [=](const DeckSearchData &data) { return data.relativeFilePath.contains(name, Qt::CaseInsensitive); }; + return [=](const DeckPreviewWidget *, const ExtraDeckSearchInfo &info) { + return info.relativeFilePath.contains(name, Qt::CaseInsensitive); + }; }; search["FormatQuery"] = [](const peg::SemanticValues &sv) -> DeckFilter { auto format = std::any_cast(sv[0]); - return [=](const DeckSearchData &data) { - auto gameFormat = data.deck->deckList.getGameFormat(); + return [=](const DeckPreviewWidget *deck, const ExtraDeckSearchInfo &) { + auto gameFormat = deck->deckLoader->getDeck().deckList.getGameFormat(); return QString::compare(format, gameFormat, Qt::CaseInsensitive) == 0; }; }; search["CommentQuery"] = [](const peg::SemanticValues &sv) -> DeckFilter { auto value = std::any_cast(sv[0]); - return [=](const DeckSearchData &data) { - auto comments = data.deck->deckList.getComments(); + return [=](const DeckPreviewWidget *deck, const ExtraDeckSearchInfo &) { + auto comments = deck->deckLoader->getDeck().deckList.getComments(); return comments.contains(value, Qt::CaseInsensitive); }; }; search["GenericQuery"] = [](const peg::SemanticValues &sv) -> DeckFilter { auto name = std::any_cast(sv[0]); - return [=](const DeckSearchData &data) { return data.displayName.contains(name, Qt::CaseInsensitive); }; + return [=](const DeckPreviewWidget *deck, const ExtraDeckSearchInfo &) { + return deck->getDisplayName().contains(name, Qt::CaseInsensitive); + }; }; } DeckFilterString::DeckFilterString() { - filter = [](const DeckSearchData &) { return false; }; + filter = [](const DeckPreviewWidget *, const ExtraDeckSearchInfo &) { return false; }; _error = "Not initialized"; } -DeckFilterString::DeckFilterString(const QString &expr, const CardSearchLanguage &searchLanguage) +DeckFilterString::DeckFilterString(const QString &expr) { QByteArray ba = expr.simplified().toUtf8(); @@ -201,18 +205,16 @@ DeckFilterString::DeckFilterString(const QString &expr, const CardSearchLanguage _error = QString(); if (ba.isEmpty()) { - filter = [](const DeckSearchData &) { return true; }; + filter = [](const DeckPreviewWidget *, const ExtraDeckSearchInfo &) { return true; }; return; } - deckSearchLanguageContext = searchLanguage; - search.set_logger([&](size_t /*ln*/, size_t col, const std::string &msg) { _error = QString("Error at position %1: %2").arg(col).arg(QString::fromStdString(msg)); }); if (!search.parse(ba.data(), filter)) { qCInfo(DeckFilterStringLog).nospace() << "DeckFilterString error for " << expr << "; " << qPrintable(_error); - filter = [](const DeckSearchData &) { return false; }; + filter = [](const DeckPreviewWidget *, const ExtraDeckSearchInfo &) { return false; }; } -} +} \ No newline at end of file diff --git a/cockatrice/src/filters/deck_filter_string.h b/cockatrice/src/filters/deck_filter_string.h index 5b1419004..916b629ee 100644 --- a/cockatrice/src/filters/deck_filter_string.h +++ b/cockatrice/src/filters/deck_filter_string.h @@ -7,39 +7,35 @@ #ifndef DECK_FILTER_STRING_H #define DECK_FILTER_STRING_H -#include "../interface/deck_loader/loaded_deck.h" +#include "../interface/widgets/visual_deck_storage/deck_preview/deck_preview_widget.h" #include #include #include -#include inline Q_LOGGING_CATEGORY(DeckFilterStringLog, "deck_filter_string"); /** - * The data a deck search expression is evaluated against. - * - * This is a data view rather than a widget pointer, so the same filter - * expression can be evaluated against a model or a live widget. + * Extra info relevant to filtering that isn't present in the DeckPreviewWidget */ -struct DeckSearchData +struct ExtraDeckSearchInfo { - const LoadedDeck *deck = nullptr; ///< The loaded deck. Must not be null. - QString filePath; ///< Absolute path of the deck file. - QString displayName; ///< Deck name, or the file name if the deck has no name. - QString relativeFilePath; ///< File path relative to the deck folder. + /** + * The relative filepath starting from the deck folder + */ + QString relativeFilePath; }; -typedef std::function DeckFilter; +typedef std::function DeckFilter; class DeckFilterString { public: DeckFilterString(); - explicit DeckFilterString(const QString &expr, const CardSearchLanguage &searchLanguage = {}); - bool check(const DeckSearchData &data) const + explicit DeckFilterString(const QString &expr); + bool check(const DeckPreviewWidget *deck, const ExtraDeckSearchInfo &info) const { - return filter(data); + return filter(deck, info); } [[nodiscard]] bool valid() const diff --git a/cockatrice/src/filters/filter_builder.cpp b/cockatrice/src/filters/filter_builder.cpp index f109fbcd3..785f753e7 100644 --- a/cockatrice/src/filters/filter_builder.cpp +++ b/cockatrice/src/filters/filter_builder.cpp @@ -1,6 +1,5 @@ #include "filter_builder.h" -#include "../interface/pixel_map_generator.h" #include "../interface/widgets/utility/custom_line_edit.h" #include @@ -22,7 +21,7 @@ FilterBuilder::FilterBuilder(QWidget *parent) : QWidget(parent) typeCombo->addItem(CardFilter::typeName(static_cast(i)), QVariant(i)); } - QPushButton *ok = new QPushButton(themePixmap(QStringLiteral("icons/increment")), QString()); + QPushButton *ok = new QPushButton(QPixmap("theme:icons/increment"), QString()); ok->setObjectName("ok"); ok->setMaximumSize(20, 20); diff --git a/cockatrice/src/game/board/counter_state.cpp b/cockatrice/src/game/board/counter_state.cpp index 0970e4272..6da18b662 100644 --- a/cockatrice/src/game/board/counter_state.cpp +++ b/cockatrice/src/game/board/counter_state.cpp @@ -13,12 +13,12 @@ CounterState *CounterState::fromProto(const ServerInfo_Counter &counter, QObject convertColorToQColor(counter.counter_color()), counter.radius(), counter.count(), parent); } -void CounterState::setValue(int newValue, bool skipDamageAnimation) +void CounterState::setValue(int newValue) { if (newValue == value) { return; } int old = value; value = newValue; - emit valueChanged(old, newValue, skipDamageAnimation); + emit valueChanged(old, newValue); } \ No newline at end of file diff --git a/cockatrice/src/game/board/counter_state.h b/cockatrice/src/game/board/counter_state.h index 4c7b34473..0f2f16b55 100644 --- a/cockatrice/src/game/board/counter_state.h +++ b/cockatrice/src/game/board/counter_state.h @@ -35,23 +35,10 @@ public: return value; } - /** - * @brief Set the counter value. - * @param newValue The new value. - * @param skipDamageAnimation When true, valueChanged is emitted with skipDamageAnimation=true, letting views - * suppress damage-related feedback (e.g. battlefield shimmer, life counter flash) for values set during replay - * rewinds. - */ - void setValue(int newValue, bool skipDamageAnimation = false); + void setValue(int newValue); signals: - /** - * @brief Emitted whenever the value changes. - * @param oldValue The previous value. - * @param newValue The new value. - * @param skipDamageAnimation True when the change should not trigger damage/life-change feedback in views. - */ - void valueChanged(int oldValue, int newValue, bool skipDamageAnimation); + void valueChanged(int oldValue, int newValue); private: int id; diff --git a/cockatrice/src/game/game_event_handler.cpp b/cockatrice/src/game/game_event_handler.cpp index f146cdbb4..4a96eebdb 100644 --- a/cockatrice/src/game/game_event_handler.cpp +++ b/cockatrice/src/game/game_event_handler.cpp @@ -229,11 +229,7 @@ void GameEventHandler::handleArrowDeletion(int creatorId, int arrowId) void GameEventHandler::handleArrowDeletionFinished(const Response &response, int creatorId, int arrowId) { - // The server confirms the arrow no longer exists whether it deleted it itself - // (RespOk, followed by an Event_DeleteArrow broadcast) or never had it - // (RespNameNotFound). In both cases the local copy has to go. deleteArrow is - // a no-op if the arrow was already removed by the event broadcast. - if (response.response_code() == Response::RespOk || response.response_code() == Response::RespNameNotFound) { + if (response.response_code() == Response::RespNameNotFound) { emit arrowDeleted(creatorId, arrowId); } } @@ -285,19 +281,12 @@ void GameEventHandler::eventGameStateChanged(const Event_GameStateChanged &event emit playerJoined(prop); } player->processPlayerInfo(playerInfo); - // Extract playmat from player properties for opponent display - if (prop.has_playmat_params()) { - player->setPlaymatFromProperties(prop); - } if (player->getPlayerInfo()->getLocal()) { emit localPlayerDeckSelected(player, playerId, playerInfo); } else { if (!game->getGameMetaInfo()->proto().share_decklists_on_load()) { continue; } - if (!playerInfo.has_deck_list()) { - continue; - } opponentDecksToDisplay.append( qMakePair(playerId, qMakePair(playerName, QString::fromStdString(playerInfo.deck_list())))); @@ -355,11 +344,6 @@ void GameEventHandler::eventPlayerPropertiesChanged(const Event_PlayerProperties const ServerInfo_PlayerProperties &prop = event.player_properties(); emit playerPropertiesChanged(prop, eventPlayerId); - // Update playmat from player properties - if (prop.has_playmat_params()) { - player->setPlaymatFromProperties(prop); - } - const auto contextType = static_cast(getPbExtension(context)); switch (contextType) { case GameEventContext::READY_START: { @@ -430,13 +414,12 @@ void GameEventHandler::eventJoin(const Event_Join &event, int /*eventPlayerId*/, QString playerName = QString::fromStdString(playerInfo.user_info().name()); emit addPlayerToAutoCompleteList(playerName); - PlayerManager *playerManager = game->getPlayerManager(); - if (playerManager->getPlayers().contains(playerId) || playerManager->getSpectators().contains(playerId)) { + if (game->getPlayerManager()->getPlayers().contains(playerId)) { return; } if (playerInfo.spectator()) { - playerManager->addSpectator(playerId, playerInfo); + game->getPlayerManager()->addSpectator(playerId, playerInfo); emit logJoinSpectator(playerName); emit spectatorJoined(playerInfo); } else { diff --git a/cockatrice/src/game/player/event_processing_options.h b/cockatrice/src/game/player/event_processing_options.h index 06238d77e..4c7663789 100644 --- a/cockatrice/src/game/player/event_processing_options.h +++ b/cockatrice/src/game/player/event_processing_options.h @@ -13,8 +13,7 @@ enum EventProcessingOption { SKIP_REVEAL_WINDOW = 0x0001, - SKIP_TAP_ANIMATION = 0x0002, - SKIP_DAMAGE_ANIMATION = 0x0004 + SKIP_TAP_ANIMATION = 0x0002 }; // Wrap it in a QFlags typedef diff --git a/cockatrice/src/game/player/player_event_handler.cpp b/cockatrice/src/game/player/player_event_handler.cpp index 277b8b1d4..bc48298f7 100644 --- a/cockatrice/src/game/player/player_event_handler.cpp +++ b/cockatrice/src/game/player/player_event_handler.cpp @@ -262,15 +262,14 @@ void PlayerEventHandler::eventCreateCounter(const Event_CreateCounter &event) player->addCounter(event.counter_info()); } -void PlayerEventHandler::eventSetCounter(const Event_SetCounter &event, EventProcessingOptions options) +void PlayerEventHandler::eventSetCounter(const Event_SetCounter &event) { CounterState *ctr = player->getCounters().value(event.counter_id(), nullptr); if (!ctr) { return; } int oldValue = ctr->getValue(); - const bool skipDamageAnimation = options.testFlag(SKIP_DAMAGE_ANIMATION); - ctr->setValue(event.value(), skipDamageAnimation); + ctr->setValue(event.value()); emit logSetCounter(player, ctr->getName(), event.value(), oldValue); } @@ -626,7 +625,7 @@ void PlayerEventHandler::processGameEvent(GameEvent::GameEventType type, eventCreateCounter(event.GetExtension(Event_CreateCounter::ext)); break; case GameEvent::SET_COUNTER: - eventSetCounter(event.GetExtension(Event_SetCounter::ext), options); + eventSetCounter(event.GetExtension(Event_SetCounter::ext)); break; case GameEvent::DEL_COUNTER: eventDelCounter(event.GetExtension(Event_DelCounter::ext)); diff --git a/cockatrice/src/game/player/player_event_handler.h b/cockatrice/src/game/player/player_event_handler.h index 300cacd08..48ad85e88 100644 --- a/cockatrice/src/game/player/player_event_handler.h +++ b/cockatrice/src/game/player/player_event_handler.h @@ -153,7 +153,7 @@ public: void eventCreateCounter(const Event_CreateCounter &event); /// Set a player-level counter value. - void eventSetCounter(const Event_SetCounter &event, EventProcessingOptions options); + void eventSetCounter(const Event_SetCounter &event); /// Delete a player-level counter. void eventDelCounter(const Event_DelCounter &event); diff --git a/cockatrice/src/game/player/player_logic.cpp b/cockatrice/src/game/player/player_logic.cpp index 143df5c57..485e2fc5c 100644 --- a/cockatrice/src/game/player/player_logic.cpp +++ b/cockatrice/src/game/player/player_logic.cpp @@ -175,15 +175,7 @@ void PlayerLogic::processPlayerInfo(const ServerInfo_Player &info) const ServerInfo_Card &cardInfo = zoneInfo.card_list(j); auto *card = new CardItem(this); card->processCardInfo(cardInfo); - // Zones without coordinates (hand, piles, stack) preserve the order - // they arrive in on the server in the positions of their cards list. - // The x coordinate of such cards is always 0, so inserting at it - // would reverse the list on reconnect. Append instead. - if (zoneInfo.with_coords()) { - zone->addCard(card, false, cardInfo.x(), cardInfo.y()); - } else { - zone->addCard(card, false, -1); - } + zone->addCard(card, false, cardInfo.x(), cardInfo.y()); } } if (zoneInfo.has_always_reveal_top_card()) { @@ -258,22 +250,6 @@ void PlayerLogic::setDeck(const DeckList &_deck) emit deckChanged(); } -void PlayerLogic::setPlaymatFromProperties(const ServerInfo_PlayerProperties &props) -{ - if (props.has_playmat_params() && !props.playmat_params().card_name().empty()) { - const auto &pp = props.playmat_params(); - remotePlaymatCard = {QString::fromStdString(pp.card_name()), QString::fromStdString(pp.card_provider_id())}; - remotePlaymatParams = {qBound(0.0, pp.margin_pct_l(), 0.95), qBound(0.0, pp.margin_pct_r(), 0.95), - qBound(0.0, pp.vertical_offset(), 1.0), qBound(0.1, pp.zoom(), 4.0)}; - hasRemotePlaymat = true; - } else { - remotePlaymatCard = CardRef{}; - remotePlaymatParams = PlaymatParams{}; - hasRemotePlaymat = false; - } - emit playmatChanged(); -} - CounterState *PlayerLogic::addCounter(const ServerInfo_Counter &counter) { return addCounter(counter.id(), QString::fromStdString(counter.name()), diff --git a/cockatrice/src/game/player/player_logic.h b/cockatrice/src/game/player/player_logic.h index 6923b3afe..a89cb6eed 100644 --- a/cockatrice/src/game/player/player_logic.h +++ b/cockatrice/src/game/player/player_logic.h @@ -17,7 +17,6 @@ #include "../zones/table_zone_logic.h" #include "player_event_handler.h" #include "player_info.h" -#include "player_manager.h" #include #include @@ -73,8 +72,6 @@ signals: const QList &cardList, bool withWritePermission); void deckChanged(); - /** @brief Emitted when the remote playmat (card/params) is updated from player properties. */ - void playmatChanged(); void newCardAdded(AbstractCardItem *card); void requestCardMenuUpdate(const CardItem *card); void counterAdded(CounterState *state); @@ -229,20 +226,6 @@ public: void setZoneId(int _zoneId); - void setPlaymatFromProperties(const ServerInfo_PlayerProperties &props); - const CardRef &getRemotePlaymatCard() const - { - return remotePlaymatCard; - } - const PlaymatParams &getRemotePlaymatParams() const - { - return remotePlaymatParams; - } - bool getHasRemotePlaymat() const - { - return hasRemotePlaymat; - } - private: AbstractGame *game; PlayerInfo *playerInfo; @@ -260,11 +243,6 @@ private: bool dialogSemaphore; QList cardsToDelete; - - // Playmat from player properties (for opponent display) - CardRef remotePlaymatCard; - PlaymatParams remotePlaymatParams; - bool hasRemotePlaymat = false; }; class AnnotationDialog : public QInputDialog diff --git a/cockatrice/src/game/player/player_manager.cpp b/cockatrice/src/game/player/player_manager.cpp index 8486efbeb..6772d3ff1 100644 --- a/cockatrice/src/game/player/player_manager.cpp +++ b/cockatrice/src/game/player/player_manager.cpp @@ -75,14 +75,6 @@ PlayerLogic *PlayerManager::getPlayer(int playerId) const return player; } -void PlayerManager::clearSpectators() -{ - const QList spectatorIds = spectators.keys(); - for (int spectatorId : spectatorIds) { - removeSpectator(spectatorId); - } -} - void PlayerManager::onPlayerConceded(int playerId, bool conceded) { // Everything else cares about this diff --git a/cockatrice/src/game/player/player_manager.h b/cockatrice/src/game/player/player_manager.h index 504e65396..2f8b87af8 100644 --- a/cockatrice/src/game/player/player_manager.h +++ b/cockatrice/src/game/player/player_manager.h @@ -100,9 +100,6 @@ public: emit spectatorRemoved(spectatorId, spectatorInfo); } - /** @brief Remove all spectators, emitting the removal signal for each. */ - void clearSpectators(); - [[nodiscard]] AbstractGame *getGame() const { return game; diff --git a/cockatrice/src/game_graphics/animated_item.h b/cockatrice/src/game_graphics/animated_item.h deleted file mode 100644 index 700e0f62d..000000000 --- a/cockatrice/src/game_graphics/animated_item.h +++ /dev/null @@ -1,26 +0,0 @@ -#ifndef ANIMATED_ITEM_H -#define ANIMATED_ITEM_H - -/** - * @file animated_item.h - * @ingroup GameGraphics - * @brief Interface for scene items driven by GameScene's shared animation timer. - * - * Items that want per-tick animation while a single QBasicTimer runs (instead of - * owning their own QTimer) implement this interface and register with the scene - * via GameScene::registerAnimationItem. - */ - -class IAnimatedItem -{ -public: - virtual ~IAnimatedItem() = default; - - /** - * @brief Advances the item's animation by one timer tick. - * @return true while the animation is still running, false once it has finished. - */ - virtual bool animationEvent() = 0; -}; - -#endif diff --git a/cockatrice/src/game_graphics/board/abstract_card_item.cpp b/cockatrice/src/game_graphics/board/abstract_card_item.cpp index 3969b7d03..e0029ee2d 100644 --- a/cockatrice/src/game_graphics/board/abstract_card_item.cpp +++ b/cockatrice/src/game_graphics/board/abstract_card_item.cpp @@ -1,7 +1,6 @@ #include "abstract_card_item.h" #include "../../client/settings/cache_settings.h" -#include "../../interface/card_localization.h" #include "../../interface/card_picture_loader/card_picture_loader.h" #include "../game_scene.h" #include "../z_values.h" @@ -27,8 +26,6 @@ AbstractCardItem::AbstractCardItem(QGraphicsItem *parent, const CardRef &cardRef connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::displayCardNamesChanged, this, [this] { update(); }); - connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::cardLangChanged, this, - [this] { update(); }); refreshCardInfo(); connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::roundCardCornersChanged, this, @@ -174,7 +171,7 @@ void AbstractCardItem::paintPicture(QPainter *painter, const QSizeF &translatedS if (SettingsCache::instance().debug().getShowCardId()) { prefix = "#" + QString::number(id) + " "; } - nameStr = prefix + CardLocalization::displayName(getCardInfo()); + nameStr = prefix + cardRef.name; } painter->drawText(QRectF(3 * scaleFactor, 3 * scaleFactor, translatedSize.width() - 6 * scaleFactor, translatedSize.height() - 6 * scaleFactor), @@ -308,11 +305,6 @@ void AbstractCardItem::setTapped(bool _tapped, bool canAnimate) } } -bool AbstractCardItem::animationEvent() -{ - return false; -} - void AbstractCardItem::setFaceDown(bool _facedown) { facedown = _facedown; diff --git a/cockatrice/src/game_graphics/board/abstract_card_item.h b/cockatrice/src/game_graphics/board/abstract_card_item.h index 8cbe95282..bdb5f7cf1 100644 --- a/cockatrice/src/game_graphics/board/abstract_card_item.h +++ b/cockatrice/src/game_graphics/board/abstract_card_item.h @@ -7,7 +7,6 @@ #ifndef ABSTRACTCARDITEM_H #define ABSTRACTCARDITEM_H -#include "../animated_item.h" #include "../card_dimensions.h" #include "arrow_target.h" #include "graphics_item_type.h" @@ -17,7 +16,7 @@ class PlayerLogic; -class AbstractCardItem : public ArrowTarget, public IAnimatedItem +class AbstractCardItem : public ArrowTarget { Q_OBJECT protected: @@ -127,9 +126,6 @@ public: emit deleteCardInfoPopup(cardRef.name); } - /** @brief Default: no per-tick animation. Subclasses override to animate. */ - bool animationEvent() override; - protected: void transformPainter(QPainter *painter, const QSizeF &translatedSize, int angle); void mousePressEvent(QGraphicsSceneMouseEvent *event) override; diff --git a/cockatrice/src/game_graphics/board/abstract_counter.cpp b/cockatrice/src/game_graphics/board/abstract_counter.cpp index 4ba04804f..a20fb1b3c 100644 --- a/cockatrice/src/game_graphics/board/abstract_counter.cpp +++ b/cockatrice/src/game_graphics/board/abstract_counter.cpp @@ -29,9 +29,8 @@ AbstractCounter::AbstractCounter(CounterState *state, { setAcceptHoverEvents(true); - connect(state, &CounterState::valueChanged, this, [this](int oldValue, int newValue, bool skipDamageAnimation) { + connect(state, &CounterState::valueChanged, this, [this](int, int newValue) { value = newValue; - onValueChanged(oldValue, newValue, skipDamageAnimation); update(); }); @@ -229,9 +228,3 @@ void AbstractCounterDialog::changeValue(int diff) curValue += diff; setTextValue(QString::number(curValue)); } - -void AbstractCounter::onValueChanged(int /*oldValue*/, int /*newValue*/, bool /*skipDamageAnimation*/) -{ - // Default: no feedback. Subclasses such as PlayerCounter override this to - // flash the counter on meaningful changes (life gain/loss). -} diff --git a/cockatrice/src/game_graphics/board/abstract_counter.h b/cockatrice/src/game_graphics/board/abstract_counter.h index 67b5b4074..b319a722d 100644 --- a/cockatrice/src/game_graphics/board/abstract_counter.h +++ b/cockatrice/src/game_graphics/board/abstract_counter.h @@ -35,14 +35,6 @@ protected: bool hovered = false; bool useNameForShortcut; - /** - * @brief Hook for subclasses that need per-value-change feedback (e.g. life-total flash). - * - * Called whenever the counter's value changes, before the item repaints. - * @param skipDamageAnimation True when damage-related feedback should be suppressed (replay rewinds). - */ - virtual void onValueChanged(int oldValue, int newValue, bool skipDamageAnimation); - void mousePressEvent(QGraphicsSceneMouseEvent *event) override; void hoverEnterEvent(QGraphicsSceneHoverEvent *event) override; void hoverLeaveEvent(QGraphicsSceneHoverEvent *event) override; diff --git a/cockatrice/src/game_graphics/board/arrow_item.cpp b/cockatrice/src/game_graphics/board/arrow_item.cpp index 664d44ecc..ce8967bb5 100644 --- a/cockatrice/src/game_graphics/board/arrow_item.cpp +++ b/cockatrice/src/game_graphics/board/arrow_item.cpp @@ -4,14 +4,12 @@ #include "../../client/settings/cache_settings.h" #include "../../game/player/player_actions.h" #include "../../game/player/player_logic.h" -#include "../game_scene.h" #include "../player/player_target.h" #include "../z_values.h" #include "../zones/card_zone.h" #include "card_item.h" #include -#include #include #include #include @@ -20,27 +18,10 @@ #include #include #include -#include #include #include #include -namespace -{ -constexpr qreal kMinStrokeDurationMs = 200.0; -constexpr qreal kMaxStrokeDurationMs = 450.0; -constexpr qreal kMsPerPixel = 0.8; -constexpr qreal kGlowFadeDurationMs = 120.0; -constexpr qreal kSheenHalfWidth = 14.0; - -/// @brief Ease-out cubic, for a natural "slow in / slow out" reveal. -qreal easeOutCubic(qreal t) -{ - const qreal inverse = 1.0 - t; - return 1.0 - inverse * inverse * inverse; -} -} // namespace - ArrowItem::ArrowItem(QSharedPointer _data, ArrowTarget *_startItem, ArrowTarget *_targetItem) : data(std::move(_data)), startItem(_startItem), targetItem(_targetItem) { @@ -66,23 +47,8 @@ ArrowItem::ArrowItem(QSharedPointer _data, ArrowTarget *_startI } } -ArrowItem::~ArrowItem() -{ - if (auto *scene = qobject_cast(this->scene())) { - scene->unregisterAnimationItem(this); - } -} - void ArrowItem::onTargetDestroyed() { - if (data->id == -1) { - // 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. - delArrow(); - return; - } - emit requestDeletion(data->creatorId, data->id); } @@ -125,21 +91,16 @@ void ArrowItem::updatePath(const QPointF &endPoint) prepareGeometryChange(); if (lineLength < 30) { path = QPainterPath(); - bodyPath = QPainterPath(); - headPath = QPainterPath(); - shaftOutlinePath = QPainterPath(); - centerLine = QPainterPath(); - headBaseFraction = 1.0; } else { QPointF c(lineLength / 2, qTan(phi * M_PI / 180) * lineLength); - centerLine = QPainterPath(); + QPainterPath centerLine; centerLine.moveTo(0, 0); centerLine.quadTo(c, QPointF(lineLength, 0)); - headBaseFraction = 1 - headLength / lineLength; - QPointF arrowBodyEndPoint = centerLine.pointAtPercent(headBaseFraction); - QLineF testLine(arrowBodyEndPoint, centerLine.pointAtPercent(headBaseFraction + 0.001)); + double percentage = 1 - headLength / lineLength; + QPointF arrowBodyEndPoint = centerLine.pointAtPercent(percentage); + QLineF testLine(arrowBodyEndPoint, centerLine.pointAtPercent(percentage + 0.001)); qreal alpha = testLine.angle() - 90; QPointF endPoint1 = arrowBodyEndPoint + arrowWidth / 2 * QPointF(qCos(alpha * M_PI / 180), -qSin(alpha * M_PI / 180)); @@ -150,89 +111,20 @@ void ArrowItem::updatePath(const QPointF &endPoint) QPointF point2 = endPoint2 + (headWidth - arrowWidth) / 2 * QPointF(-qCos(alpha * M_PI / 180), qSin(alpha * M_PI / 180)); - QPointF start1 = -arrowWidth / 2 * QPointF(qCos((phi - 90) * M_PI / 180), qSin((phi - 90) * M_PI / 180)); - QPointF start2 = arrowWidth / 2 * QPointF(qCos((phi - 90) * M_PI / 180), qSin((phi - 90) * M_PI / 180)); - - path = QPainterPath(start1); + path = QPainterPath(-arrowWidth / 2 * QPointF(qCos((phi - 90) * M_PI / 180), qSin((phi - 90) * M_PI / 180))); path.quadTo(c, endPoint1); path.lineTo(point1); path.lineTo(QPointF(lineLength, 0)); path.lineTo(point2); path.lineTo(endPoint2); - path.quadTo(c, start2); - path.lineTo(start1); - - bodyPath = QPainterPath(start1); - bodyPath.quadTo(c, endPoint1); - bodyPath.lineTo(endPoint2); - bodyPath.quadTo(c, start2); - bodyPath.lineTo(start1); - - headPath = QPainterPath(endPoint1); - headPath.lineTo(point1); - headPath.lineTo(QPointF(lineLength, 0)); - headPath.lineTo(point2); - headPath.lineTo(endPoint2); - - shaftOutlinePath = QPainterPath(start1); - shaftOutlinePath.quadTo(c, endPoint1); - shaftOutlinePath.moveTo(endPoint2); - shaftOutlinePath.quadTo(c, start2); - shaftOutlinePath.lineTo(start1); + path.quadTo(c, arrowWidth / 2 * QPointF(qCos((phi - 90) * M_PI / 180), qSin((phi - 90) * M_PI / 180))); + path.lineTo(-arrowWidth / 2 * QPointF(qCos((phi - 90) * M_PI / 180), qSin((phi - 90) * M_PI / 180))); } setPos(startPoint); setTransform(QTransform().rotate(-line.angle())); } -void ArrowItem::startDrawAnimation() -{ - if (!SettingsCache::instance().cardsDisplay().getArrowDrawAnimation() || centerLine.isEmpty()) { - return; - } - - strokeDurationMs = qBound(kMinStrokeDurationMs, centerLine.length() * kMsPerPixel, kMaxStrokeDurationMs); - glowFadeDurationMs = kGlowFadeDurationMs; - // The clock is started on the first animationEvent() tick so that t=0 - // corresponds to the first rendered frame. Starting it here would count - // the time spent before the item's first paint (event-loop delays, bursts - // of arrows created together), making the arrow appear already partway - // drawn when it first shows up. - animationStarted = false; - drawProgress = 0.0; - glowAlpha = 1.0; - update(); - if (auto *scene = qobject_cast(this->scene())) { - scene->registerAnimationItem(this); - } -} - -bool ArrowItem::animationEvent() -{ - if (!animationStarted) { - animationClock.start(); - animationStarted = true; - } - - const qint64 elapsed = animationClock.elapsed(); - if (elapsed >= strokeDurationMs + glowFadeDurationMs) { - drawProgress = 1.0; - glowAlpha = 0.0; - update(); - return false; - } - - if (elapsed < strokeDurationMs) { - drawProgress = easeOutCubic(qBound(0.0, elapsed / strokeDurationMs, 1.0)); - glowAlpha = 1.0; - } else { - drawProgress = 1.0; - glowAlpha = 1.0 - (elapsed - strokeDurationMs) / glowFadeDurationMs; - } - update(); - return true; -} - void ArrowItem::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*option*/, QWidget * /*widget*/) { QColor paintColor(data->color); @@ -241,66 +133,8 @@ void ArrowItem::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*opti } else { paintColor.setAlpha(150); } - - painter->save(); - const QPen outlinePen = painter->pen(); painter->setBrush(paintColor); - - const auto drawShaft = [this, painter, &outlinePen, paintColor]() { - painter->setPen(Qt::NoPen); - painter->drawPath(bodyPath); - painter->setPen(outlinePen); - painter->setBrush(Qt::NoBrush); - painter->drawPath(shaftOutlinePath); - painter->setBrush(paintColor); - }; - - if (drawProgress >= 1.0 || path.isEmpty()) { - painter->drawPath(path); - } else if (drawProgress < headBaseFraction) { - // The reveal edge and the sheen share the same arc-length parameterization, - // so the stroke stays exactly in sync with the trailing sheen. - const qreal revealX = centerLine.pointAtPercent(drawProgress).x(); - QPainterPath clip; - clip.addRect(QRectF(-glowExtent, path.boundingRect().top() - glowExtent, revealX + glowExtent, - path.boundingRect().height() + 2 * glowExtent)); - painter->setClipPath(clip); - drawShaft(); - } else { - // Once the reveal reaches the head base, pop the whole head in with a fade - // instead of slicing the triangle into a growing stub. - drawShaft(); - const qreal headFadeIn = (drawProgress - headBaseFraction) / (1.0 - headBaseFraction); - painter->setOpacity(headFadeIn); - painter->setPen(Qt::NoPen); - painter->drawPath(headPath); - painter->setPen(outlinePen); - painter->setBrush(Qt::NoBrush); - painter->drawPath(headPath); - painter->setOpacity(1.0); - painter->setBrush(paintColor); - } - - if (glowAlpha > 0.0 && !centerLine.isEmpty()) { - // Sweep a bright band across the arrow. Clipping to the - // silhouette keeps it flat against the shaft so it reads as a light reflection. - const qreal anticipation = qMin(1.0, drawProgress / 0.08); - const QPointF sweep = centerLine.pointAtPercent(qMin(drawProgress, 1.0)); - QLinearGradient sheen(sweep.x() - kSheenHalfWidth, 0.0, sweep.x() + kSheenHalfWidth, 0.0); - sheen.setColorAt(0.0, QColor(paintColor.red(), paintColor.green(), paintColor.blue(), 0)); - sheen.setColorAt(0.5, QColor(255, 255, 255, 200)); - sheen.setColorAt(1.0, QColor(paintColor.red(), paintColor.green(), paintColor.blue(), 0)); - painter->save(); - painter->setPen(Qt::NoPen); - painter->setClipPath(path); - painter->setBrush(sheen); - painter->setOpacity(glowAlpha * anticipation); - painter->drawRect(QRectF(sweep.x() - kSheenHalfWidth - glowExtent, path.boundingRect().top() - glowExtent, - (kSheenHalfWidth + glowExtent) * 2.0, - path.boundingRect().height() + glowExtent * 2.0)); - painter->restore(); - } - painter->restore(); + painter->drawPath(path); } void ArrowItem::mousePressEvent(QGraphicsSceneMouseEvent *event) @@ -392,12 +226,6 @@ void ArrowDragItem::mouseMoveEvent(QGraphicsSceneMouseEvent *event) 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. - delArrow(); - for (auto *child : childArrows) { - child->mouseReleaseEvent(event); - } return; } @@ -521,12 +349,6 @@ void ArrowAttachItem::mouseMoveEvent(QGraphicsSceneMouseEvent *event) 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. - delArrow(); - for (auto *child : childArrows) { - child->mouseReleaseEvent(event); - } return; } diff --git a/cockatrice/src/game_graphics/board/arrow_item.h b/cockatrice/src/game_graphics/board/arrow_item.h index 76a2d5d6c..1c306e065 100644 --- a/cockatrice/src/game_graphics/board/arrow_item.h +++ b/cockatrice/src/game_graphics/board/arrow_item.h @@ -2,13 +2,9 @@ #define ARROWITEM_H #include "../../game/board/arrow_data.h" -#include "../animated_item.h" #include "arrow_target.h" -#include "graphics_item_type.h" -#include #include -#include #include #include @@ -16,7 +12,7 @@ class CardItem; class QGraphicsSceneMouseEvent; class PlayerLogic; -class ArrowItem : public QObject, public QGraphicsItem, public IAnimatedItem +class ArrowItem : public QObject, public QGraphicsItem { Q_OBJECT Q_INTERFACES(QGraphicsItem) @@ -25,19 +21,6 @@ signals: private: QPainterPath path; - QPainterPath bodyPath; - QPainterPath headPath; - QPainterPath shaftOutlinePath; - QPainterPath centerLine; - qreal headBaseFraction = 1.0; - QElapsedTimer animationClock; - qreal strokeDurationMs = 0; - qreal glowFadeDurationMs = 0; - qreal drawProgress = 1.0; - qreal glowAlpha = 0.0; - bool animationStarted = false; - - static constexpr qreal glowExtent = 12.0; protected: QSharedPointer data; @@ -49,28 +32,17 @@ protected: void mousePressEvent(QGraphicsSceneMouseEvent *event) override; public: - enum - { - Type = typeArrow - }; - [[nodiscard]] int type() const override - { - return Type; - } ArrowItem(QSharedPointer _data, ArrowTarget *_startItem, ArrowTarget *_targetItem); - ~ArrowItem() override; void onTargetDestroyed(); void delArrow(); void updatePath(); void updatePath(const QPointF &endPoint); - void startDrawAnimation(); - bool animationEvent() override; void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override; [[nodiscard]] QRectF boundingRect() const override { - return path.boundingRect().adjusted(-glowExtent, -glowExtent, glowExtent, glowExtent); + return path.boundingRect(); } [[nodiscard]] QPainterPath shape() const override { @@ -134,4 +106,4 @@ protected: void mouseReleaseEvent(QGraphicsSceneMouseEvent *event) override; }; -#endif +#endif \ No newline at end of file diff --git a/cockatrice/src/game_graphics/board/card_item.cpp b/cockatrice/src/game_graphics/board/card_item.cpp index c2dc455cc..c40c8c214 100644 --- a/cockatrice/src/game_graphics/board/card_item.cpp +++ b/cockatrice/src/game_graphics/board/card_item.cpp @@ -316,7 +316,7 @@ void CardItem::drawAttachArrow() for (const auto &item : scene()->selectedItems()) { CardItem *card = qgraphicsitem_cast(item); - if (card == nullptr || card == this) { + if (card == nullptr) { continue; } if (card->getZone() != state->getZone()) { diff --git a/cockatrice/src/game_graphics/board/card_item.h b/cockatrice/src/game_graphics/board/card_item.h index 2ba43d03d..37f3bab50 100644 --- a/cockatrice/src/game_graphics/board/card_item.h +++ b/cockatrice/src/game_graphics/board/card_item.h @@ -137,7 +137,7 @@ public: void resetState(bool keepAnnotations = false); void processCardInfo(const ServerInfo_Card &_info); - bool animationEvent() override; + bool animationEvent(); CardDragItem *createDragItem(int _id, const QPointF &_pos, const QPointF &_scenePos, bool forceFaceDown); void deleteDragItem(); void drawArrow(const QColor &arrowColor); diff --git a/cockatrice/src/game_graphics/board/graphics_item_type.h b/cockatrice/src/game_graphics/board/graphics_item_type.h index afac7881f..7eac132b0 100644 --- a/cockatrice/src/game_graphics/board/graphics_item_type.h +++ b/cockatrice/src/game_graphics/board/graphics_item_type.h @@ -16,8 +16,7 @@ enum GraphicsItemType typeZone = QGraphicsItem::UserType + 3, typePlayerTarget = QGraphicsItem::UserType + 4, typeDeckViewCardContainer = QGraphicsItem::UserType + 5, - typeOther = QGraphicsItem::UserType + 6, - typeArrow = QGraphicsItem::UserType + 7 + typeOther = QGraphicsItem::UserType + 6 }; #endif // COCKATRICE_GRAPHICS_ITEM_TYPE_H diff --git a/cockatrice/src/game_graphics/deckview/deck_view.cpp b/cockatrice/src/game_graphics/deckview/deck_view.cpp index 1acd02a75..1278737a0 100644 --- a/cockatrice/src/game_graphics/deckview/deck_view.cpp +++ b/cockatrice/src/game_graphics/deckview/deck_view.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include DeckViewCardDragItem::DeckViewCardDragItem(DeckViewCard *_item, @@ -380,10 +381,12 @@ void DeckViewScene::rebuildTree() addItem(container); } - // Cards in custom zones nested under a board are regular board cards in-game. - // They are collected recursively (like every other consumer) and reported with - // the top-level board zone as their origin, so that sideboard plans keep working. - for (auto *currentCard : deck->getCardNodes({currentZone->getName()})) { + for (int j = 0; j < currentZone->size(); j++) { + auto *currentCard = dynamic_cast(currentZone->at(j)); + if (!currentCard) { + continue; + } + for (int k = 0; k < currentCard->getNumber(); ++k) { auto *newCard = new DeckViewCard(container, currentCard->toCardRef(), currentZone->getName()); container->addCard(newCard); diff --git a/cockatrice/src/game_graphics/deckview/deck_view_container.cpp b/cockatrice/src/game_graphics/deckview/deck_view_container.cpp index bc07ac183..69941040f 100644 --- a/cockatrice/src/game_graphics/deckview/deck_view_container.cpp +++ b/cockatrice/src/game_graphics/deckview/deck_view_container.cpp @@ -9,21 +9,17 @@ #include "../../interface/widgets/dialogs/dlg_load_deck_from_website.h" #include "../../interface/widgets/dialogs/dlg_load_remote_deck.h" #include "../../interface/widgets/tabs/tab_game.h" -#include "../../interface/widgets/visual_deck_storage/visual_deck_storage_widget.h" #include "deck_view.h" #include #include #include -#include #include #include -#include #include #include #include #include -#include #include #include @@ -104,9 +100,6 @@ DeckViewContainer::DeckViewContainer(int _playerId, TabGame *parent) connect(&SettingsCache::instance().visualDeckStorage(), &VisualDeckStorageSettings::visualDeckStorageInGameChanged, this, &DeckViewContainer::setVisualDeckStorageExists); - connect(&SettingsCache::instance().userInterface(), &InterfaceSettings::playmatSettingsChanged, this, - &DeckViewContainer::onPlaymatSettingsChanged); - switchToDeckSelectView(); } @@ -284,8 +277,6 @@ void DeckViewContainer::loadDeckFromFile(const QString &filePath) void DeckViewContainer::loadDeckFromDeckList(const DeckList &deck) { - currentDeck = deck; - QString deckString = deck.writeToString_Native(); if (deckString.length() > MAX_FILE_LENGTH) { @@ -298,52 +289,6 @@ void DeckViewContainer::loadDeckFromDeckList(const DeckList &deck) PendingCommand *pend = parentGame->getGame()->getGameEventHandler()->prepareGameCommand(cmd); connect(pend, &PendingCommand::finished, this, &DeckViewContainer::deckSelectFinished); parentGame->getGame()->getGameEventHandler()->sendGameCommand(pend, playerId); - - resolveAndSendPlaymat(); -} - -void DeckViewContainer::resolveAndSendPlaymat() -{ - if (currentDeck.getCardRefList().isEmpty() && currentDeck.getPlaymat().card.isEmpty()) { - return; - } - - const auto &settings = SettingsCache::instance().userInterface(); - const auto fallbackBehavior = static_cast(settings.getPlaymatFallbackBehavior()); - - QList fallbackList = settings.getPlaymatFallbackList(); - - // In random mode with 2+ entries, remove the last-resolved mat to avoid repeats. - if (fallbackBehavior == PlaymatFallbackModeRandom && fallbackList.size() > 1) { - fallbackList.removeAll(lastResolvedPlaymat); - } - - const PlaymatInfo resolved = - resolvePlaymatForDeck(currentDeck, fallbackList, static_cast(settings.getPlaymatMode()), - fallbackBehavior, playmatRotationIndex); - - lastResolvedPlaymat = resolved; - - Command_SetPlaymat playmatCmd; - auto *pp = playmatCmd.mutable_playmat_params(); - pp->set_card_name(resolved.card.name.toStdString()); - pp->set_card_provider_id(resolved.card.providerId.toStdString()); - pp->set_margin_pct_l(resolved.params.marginPctL); - pp->set_margin_pct_r(resolved.params.marginPctR); - pp->set_vertical_offset(resolved.params.verticalOffset); - pp->set_zoom(resolved.params.zoom); - PendingCommand *playmatPend = parentGame->getGame()->getGameEventHandler()->prepareGameCommand(playmatCmd); - parentGame->getGame()->getGameEventHandler()->sendGameCommand(playmatPend, playerId); -} - -void DeckViewContainer::onPlaymatSettingsChanged() -{ - resolveAndSendPlaymat(); -} - -void DeckViewContainer::advancePlaymatRotation() -{ - playmatRotationIndex++; } void DeckViewContainer::loadRemoteDeck() @@ -434,10 +379,6 @@ void DeckViewContainer::sideboardPlanChanged() */ void DeckViewContainer::sendReadyStartCommand(bool ready) { - if (ready) { - resolveAndSendPlaymat(); - } - Command_ReadyStart cmd; cmd.set_ready(ready); parentGame->getGame()->getGameEventHandler()->sendGameCommand(cmd, playerId); @@ -475,7 +416,6 @@ void DeckViewContainer::setSideboardLocked(bool locked) void DeckViewContainer::setDeck(const DeckList &deck) { - currentDeck = deck; deckView->setDeck(deck); switchToDeckLoadedView(); } \ No newline at end of file diff --git a/cockatrice/src/game_graphics/deckview/deck_view_container.h b/cockatrice/src/game_graphics/deckview/deck_view_container.h index b5317c39a..ec024bace 100644 --- a/cockatrice/src/game_graphics/deckview/deck_view_container.h +++ b/cockatrice/src/game_graphics/deckview/deck_view_container.h @@ -57,9 +57,6 @@ private: VisualDeckStorageWidget *visualDeckStorageWidget; TabGame *parentGame; int playerId; - int playmatRotationIndex = 0; ///< Per-match cursor for round-robin playmat mode. - DeckList currentDeck; ///< Cached deck for live settings re-resolution. - PlaymatInfo lastResolvedPlaymat; ///< Tracks last sent playmat to avoid repeats in random mode. void tryCreateVisualDeckStorageWidget(); void sendReadyStartCommand(bool ready); @@ -78,7 +75,6 @@ private slots: void sideboardLockButtonClicked(); void updateSideboardLockButtonText(); void refreshShortcuts(); - void onPlaymatSettingsChanged(); signals: void newCardAdded(AbstractCardItem *card); void notIdle(); @@ -91,8 +87,6 @@ public: void setSideboardLocked(bool locked); void setDeck(const DeckList &deck); void setVisualDeckStorageExists(bool exists); - void advancePlaymatRotation(); - void resolveAndSendPlaymat(); public slots: void loadDeckFromFile(const QString &filePath); diff --git a/cockatrice/src/game_graphics/dialogs/dlg_create_token.cpp b/cockatrice/src/game_graphics/dialogs/dlg_create_token.cpp index 6d8ad0534..b311d2ebd 100644 --- a/cockatrice/src/game_graphics/dialogs/dlg_create_token.cpp +++ b/cockatrice/src/game_graphics/dialogs/dlg_create_token.cpp @@ -16,13 +16,11 @@ #include #include #include -#include #include #include #include #include #include -#include #include #include #include @@ -90,17 +88,6 @@ DlgCreateToken::DlgCreateToken(const QStringList &_predefinedTokens, QWidget *pa cardDatabaseDisplayModel = new TokenDisplayModel(this); cardDatabaseDisplayModel->setSourceModel(cardDatabaseModel); - const auto applyCardSearchLanguage = [this]() { - const CardsDisplaySettings &cardsDisplay = SettingsCache::instance().cardsDisplay(); - cardDatabaseDisplayModel->setSearchLanguage(CardSearchLanguage{ - cardsDisplay.getCardLang(), static_cast(cardsDisplay.getCardSearchLanguage())}); - }; - applyCardSearchLanguage(); - connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::cardLangChanged, this, - applyCardSearchLanguage); - connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::cardSearchLanguageChanged, this, - applyCardSearchLanguage); - chooseTokenFromAllRadioButton = new QRadioButton(tr("Show &all tokens")); connect(chooseTokenFromAllRadioButton, &QRadioButton::toggled, this, &DlgCreateToken::actChooseTokenFromAll); chooseTokenFromDeckRadioButton = new QRadioButton(tr("Show tokens from this &deck")); diff --git a/cockatrice/src/game_graphics/game_scene.cpp b/cockatrice/src/game_graphics/game_scene.cpp index 17af7618b..db2088104 100644 --- a/cockatrice/src/game_graphics/game_scene.cpp +++ b/cockatrice/src/game_graphics/game_scene.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include #include #include @@ -44,19 +45,7 @@ GameScene::GameScene(PhasesToolbar *_phasesToolbar, QObject *parent) GameScene::~GameScene() { - // Sever all destroyed->removeAnimatedItem connections before the members below - // are destroyed: the base QGraphicsScene destructor destroys the remaining items, - // and their destroyed() signals must not reach slots that reference members that - // no longer exist. The connection handle overload is used because the string-based - // disconnect(nullptr, nullptr, this, nullptr) is invalid (the sender must never be - // nullptr) and would otherwise fail to sever these pointer-to-member connections. - for (auto it = animationItemConnections.constBegin(); it != animationItemConnections.constEnd(); ++it) { - QObject::disconnect(*it); - } - animationItemConnections.clear(); - delete animationTimer; - animationTimer = nullptr; // Delete all ArrowItems before QGraphicsScene's base destructor runs. // QGraphicsScene::~QGraphicsScene() destroys items in arbitrary order. @@ -221,12 +210,7 @@ void GameScene::removePlayer(PlayerLogic *player) clearArrowsForPlayer(player->getPlayerInfo()->getId()); - // Closing a view removes it from zoneViews synchronously, so iterate over a - // copy: otherwise a player with several open views (e.g. library and hand) - // only has the first one closed here and the remaining views are left - // pointing at a player that is about to be deleted. - const QList zoneViewCopy = zoneViews; - for (ZoneViewWidget *zone : zoneViewCopy) { + for (ZoneViewWidget *zone : zoneViews) { if (zone->getPlayer() == player) { zone->close(); } @@ -262,27 +246,17 @@ void GameScene::adjustPlayerRotation(int rotationAdjustment) */ void GameScene::rearrange() { - if (rearranging) { - needsReArrange = true; - return; - } - rearranging = true; - do { - needsReArrange = false; + int firstPlayerIndex = 0; + auto playersPlaying = collectActivePlayers(firstPlayerIndex); + playersPlaying = rotatePlayers(playersPlaying, firstPlayerIndex); - int firstPlayerIndex = 0; - auto playersPlaying = collectActivePlayers(firstPlayerIndex); - playersPlaying = rotatePlayers(playersPlaying, firstPlayerIndex); + int columns = determineColumnCount(playersPlaying.size()); + QSizeF sceneSize = computeSceneSizeAndPlayerLayout(playersPlaying, columns); - int columns = determineColumnCount(playersPlaying.size()); - QSizeF sceneSize = computeSceneSizeAndPlayerLayout(playersPlaying, columns); + phasesToolbar->setHeight(sceneSize.height()); + setSceneRect(0, 0, sceneSize.width(), sceneSize.height()); - phasesToolbar->setHeight(sceneSize.height()); - setSceneRect(0, 0, sceneSize.width(), sceneSize.height()); - - processViewSizeChange(viewSize); - } while (needsReArrange); - rearranging = false; + processViewSizeChange(viewSize); } // ---------- View Size ---------- @@ -479,14 +453,8 @@ void GameScene::resizeColumnsAndPlayers(const QList &minWidthByColumn, qr qreal extraWidthPerColumn = (newWidth - minWidth) / playersByColumn.size(); qreal newx = phasesToolbar->getWidth(); - // Snapshot the columns: resizing a player's table can synchronously trigger - // GameScene::rearrange (table width -> sizeChanged -> updateBoundingRect -> - // sizeChanged -> rearrange), and rearrange rebuilds playersByColumn. Iterating - // the live container across that re-entrant call would use invalidated iterators. - const QList> columns = playersByColumn; - - for (int col = 0; col < columns.size(); ++col) { - for (PlayerGraphicsItem *player : columns[col]) { + for (int col = 0; col < playersByColumn.size(); ++col) { + for (PlayerGraphicsItem *player : playersByColumn[col]) { player->processSceneSizeChange(minWidthByColumn[col] + extraWidthPerColumn); player->setPos(newx, player->y()); } @@ -528,7 +496,6 @@ void GameScene::addArrow(QSharedPointer data) auto *arrow = new ArrowItem(data, startCard, targetItem); addItem(arrow); - arrow->startDrawAnimation(); arrowRegistry.insert(data, arrow); connect(arrow, &ArrowItem::requestDeletion, this, &GameScene::requestArrowDeletion); } @@ -669,10 +636,7 @@ CardItem *GameScene::findTopmostCardInZone(const QList &items, */ void GameScene::toggleZoneView(PlayerLogic *player, const QString &zoneName, int numberCards, bool isReversed) { - // Closing a view removes it from zoneViews synchronously, so iterate over a - // copy to make sure every already-open matching view is closed. - const QList zoneViewCopy = zoneViews; - for (auto *view : zoneViewCopy) { + for (auto &view : zoneViews) { ZoneViewZone *temp = view->getZone(); if (temp->getLogic()->getName() == zoneName && temp->getLogic()->getPlayer() == player && qobject_cast(temp->getLogic())->getNumberCards() == numberCards) { @@ -772,53 +736,30 @@ bool GameScene::event(QEvent *event) void GameScene::timerEvent(QTimerEvent * /*event*/) { - QMutableHashIterator i(animatedItems); + QMutableSetIterator i(cardsToAnimate); while (i.hasNext()) { i.next(); if (!i.value()->animationEvent()) { i.remove(); } } - if (animatedItems.isEmpty()) { + if (cardsToAnimate.isEmpty()) { animationTimer->stop(); } } -void GameScene::registerAnimationItem(IAnimatedItem *item) +void GameScene::registerAnimationItem(AbstractCardItem *card) { - auto *object = dynamic_cast(item); - if (!object) { - return; - } - // Guard against duplicate connections using the connection map, not - // animatedItems: the animation timer removes entries from animatedItems when an - // animation completes, but the destroyed->removeAnimatedItem connection must - // persist until the object is destroyed. Relying on animatedItems here would let - // a re-registered item (e.g. a life counter that flashes repeatedly) accumulate - // duplicate destroyed connections, the older ones of which would survive teardown. - if (!animationItemConnections.contains(object)) { - animationItemConnections.insert(object, - connect(object, &QObject::destroyed, this, &GameScene::removeAnimatedItem)); - } - animatedItems.insert(object, item); - if (animationTimer && !animationTimer->isActive()) { + cardsToAnimate.insert(static_cast(card)); + if (!animationTimer->isActive()) { animationTimer->start(10, this); } } -void GameScene::unregisterAnimationItem(IAnimatedItem *item) +void GameScene::unregisterAnimationItem(AbstractCardItem *card) { - animatedItems.remove(dynamic_cast(item)); - if (animationTimer && animatedItems.isEmpty()) { - animationTimer->stop(); - } -} - -void GameScene::removeAnimatedItem(QObject *item) -{ - animatedItems.remove(item); - animationItemConnections.remove(item); - if (animationTimer && animatedItems.isEmpty()) { + cardsToAnimate.remove(static_cast(card)); + if (cardsToAnimate.isEmpty()) { animationTimer->stop(); } } diff --git a/cockatrice/src/game_graphics/game_scene.h b/cockatrice/src/game_graphics/game_scene.h index 859d7a6eb..74e979556 100644 --- a/cockatrice/src/game_graphics/game_scene.h +++ b/cockatrice/src/game_graphics/game_scene.h @@ -4,14 +4,13 @@ #include "../game/arrow_registry.h" #include "../game/board/arrow_data.h" #include "../game/zones/card_zone_logic.h" -#include "animated_item.h" #include "board/arrow_item.h" #include -#include #include #include #include +#include inline Q_LOGGING_CATEGORY(GameSceneLog, "game_scene"); inline Q_LOGGING_CATEGORY(GameScenePlayerAdditionRemovalLog, "game_scene.player_addition_removal"); @@ -25,7 +24,6 @@ class CardItem; class ServerInfo_Card; class PhasesToolbar; class QBasicTimer; -class QObject; /** * @class GameScene @@ -52,13 +50,9 @@ private: QList zoneViews; ///< Active zone view widgets QSize viewSize; ///< Current view size QPointer hoveredCard; ///< Currently hovered card - QBasicTimer *animationTimer; ///< Timer for scene animations - QHash animatedItems; ///< Items currently animating - QHash - animationItemConnections; ///< destroyed->removeAnimatedItem handles per animated item - int playerRotation; ///< Rotation offset for player layout - bool rearranging = false; ///< Guard against re-entrant rearrange - bool needsReArrange = false; ///< Pending rearrange requested during a pass + QBasicTimer *animationTimer; ///< Timer for card animations + QSet cardsToAnimate; ///< Cards currently animating + int playerRotation; ///< Rotation offset for player layout /** * @brief Updates which card is currently hovered based on scene coordinates. @@ -188,24 +182,15 @@ public: /** @brief Updates hovered card highlighting. */ void updateHoveredCard(CardItem *newCard); - /** - * @brief Registers an item for animation updates with the shared scene timer. - * - * The item must inherit QObject; it is unregistered automatically when it is - * destroyed, so it may be deleted mid-animation without a dangling pointer. - */ - void registerAnimationItem(IAnimatedItem *item); + /** @brief Registers a card for animation updates. */ + void registerAnimationItem(AbstractCardItem *card); - /** @brief Unregisters an item from animation updates. */ - void unregisterAnimationItem(IAnimatedItem *item); + /** @brief Unregisters a card from animation updates. */ + void unregisterAnimationItem(AbstractCardItem *card); void startRubberBand(const QPointF &selectionOrigin); void resizeRubberBand(const QPointF &cursorPoint, int selectedCount); void stopRubberBand(); -private slots: - /** @brief Removes a destroyed item from the animation set. */ - void removeAnimatedItem(QObject *item); - public slots: void onCardSelectionChanged(AbstractCardItem *card, bool selected); void onCardRightClicked(AbstractCardItem *card, QPoint screenPos); diff --git a/cockatrice/src/game_graphics/game_view.cpp b/cockatrice/src/game_graphics/game_view.cpp index ed6355157..b768c8317 100644 --- a/cockatrice/src/game_graphics/game_view.cpp +++ b/cockatrice/src/game_graphics/game_view.cpp @@ -114,7 +114,6 @@ void GameView::startRubberBand(const QPointF &_selectionOrigin) } selectionOrigin = _selectionOrigin; - previousBandRect = QRect(); rubberBand->setGeometry(QRect(mapFromScene(selectionOrigin), QSize(0, 0))); rubberBand->show(); } @@ -129,17 +128,7 @@ void GameView::resizeRubberBand(const QPointF &cursorPoint, int selectedCount) QPoint cursor = cursorPoint.toPoint(); QRect rect = QRect(mapFromScene(selectionOrigin), cursor).normalized(); - rubberBand->setGeometry(rect); - if (viewport()) { - // Repaint the union of the previous and current band rects: the vacated - // strip of a child widget is not reliably invalidated on all platforms - // (notably macOS), leaving stale pixels under the selection. - QRect dirty = previousBandRect.isNull() ? rect : previousBandRect.united(rect); - dirty.adjust(-1, -1, 1, 1); - viewport()->update(dirty); - previousBandRect = rect; - } if (!SettingsCache::instance().userInterface().getShowDragSelectionCount()) { dragCountLabel->hide(); @@ -182,13 +171,7 @@ void GameView::stopRubberBand() return; } - // Same rationale as resizeRubberBand: repaint the last known band area - // since hiding a child widget doesn't reliably invalidate its region. rubberBand->hide(); - if (viewport() && !previousBandRect.isNull()) { - viewport()->update(previousBandRect.adjusted(-1, -1, 1, 1)); - previousBandRect = QRect(); - } dragCountLabel->hide(); } diff --git a/cockatrice/src/game_graphics/game_view.h b/cockatrice/src/game_graphics/game_view.h index a23655513..3f6b60dbc 100644 --- a/cockatrice/src/game_graphics/game_view.h +++ b/cockatrice/src/game_graphics/game_view.h @@ -27,7 +27,6 @@ private: QWidget *tallyContainer; QGridLayout *tallyLayout; QPointF selectionOrigin; - QRect previousBandRect; ///< Last rubber-band rect for targeted repaint QList cachedTallyRows; ///< Cached entries to avoid redundant rebuilds QSize rebuildTallyLabels(const QList &entries); diff --git a/cockatrice/src/game_graphics/hand_counter.cpp b/cockatrice/src/game_graphics/hand_counter.cpp index 8dcbcfdaa..35989ff38 100644 --- a/cockatrice/src/game_graphics/hand_counter.cpp +++ b/cockatrice/src/game_graphics/hand_counter.cpp @@ -1,6 +1,5 @@ #include "hand_counter.h" -#include "../interface/pixel_map_generator.h" #include "zones/card_zone.h" #include @@ -33,8 +32,7 @@ void HandCounter::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*op QSize translatedSize = painter->combinedTransform().mapRect(boundingRect()).size().toSize(); QPixmap cachedPixmap; if (!QPixmapCache::find("handCounter" + QString::number(translatedSize.width()), &cachedPixmap)) { - cachedPixmap = - themePixmap(QStringLiteral("hand")).scaled(translatedSize, Qt::KeepAspectRatio, Qt::SmoothTransformation); + cachedPixmap = QPixmap("theme:hand").scaled(translatedSize, Qt::KeepAspectRatio, Qt::SmoothTransformation); QPixmapCache::insert("handCounter" + QString::number(translatedSize.width()), cachedPixmap); } resetPainterTransform(painter); diff --git a/cockatrice/src/game_graphics/player/menu/tally_menu.cpp b/cockatrice/src/game_graphics/player/menu/tally_menu.cpp index 08cb6cac9..7eb3945b3 100644 --- a/cockatrice/src/game_graphics/player/menu/tally_menu.cpp +++ b/cockatrice/src/game_graphics/player/menu/tally_menu.cpp @@ -12,13 +12,11 @@ TallyMenu::TallyMenu() aTallyNone = createTallyAction(TallyType::None); aTallySubtypes = createTallyAction(TallyType::Subtypes); aTallyTotalPower = createTallyAction(TallyType::TotalPower); - aTallyTotalToughness = createTallyAction(TallyType::TotalToughness); addAction(aTallyNone); addSeparator(); addAction(aTallySubtypes); addAction(aTallyTotalPower); - addAction(aTallyTotalToughness); retranslateUi(); } @@ -56,5 +54,4 @@ void TallyMenu::retranslateUi() aTallyNone->setText(tr("None")); aTallySubtypes->setText(tr("Subtypes")); aTallyTotalPower->setText(tr("Total Power")); - aTallyTotalToughness->setText(tr("Total Toughness")); } diff --git a/cockatrice/src/game_graphics/player/menu/tally_menu.h b/cockatrice/src/game_graphics/player/menu/tally_menu.h index 11802fd20..acd1daf67 100644 --- a/cockatrice/src/game_graphics/player/menu/tally_menu.h +++ b/cockatrice/src/game_graphics/player/menu/tally_menu.h @@ -24,7 +24,6 @@ private: QAction *aTallyNone = nullptr; QAction *aTallySubtypes = nullptr; QAction *aTallyTotalPower = nullptr; - QAction *aTallyTotalToughness = nullptr; QAction *createTallyAction(TallyType tallyType); }; diff --git a/cockatrice/src/game_graphics/player/player_graphics_item.cpp b/cockatrice/src/game_graphics/player/player_graphics_item.cpp index 122ab83be..d443853ce 100644 --- a/cockatrice/src/game_graphics/player/player_graphics_item.cpp +++ b/cockatrice/src/game_graphics/player/player_graphics_item.cpp @@ -1,10 +1,6 @@ #include "player_graphics_item.h" #include "../../game/player/player_actions.h" -#include "../../interface/card_picture_loader/card_picture_loader.h" -#include "../../interface/widgets/cards/art_crop_attribution.h" -#include "../../interface/widgets/cards/card_art_utils.h" -#include "../../interface/widgets/playmat/playmat_utils.h" #include "../../interface/widgets/tabs/tab_game.h" #include "../board/abstract_card_item.h" #include "../board/counter_general.h" @@ -17,9 +13,6 @@ #include "player_dialogs.h" #include -#include -#include -#include #include PlayerGraphicsItem::PlayerGraphicsItem(PlayerLogic *_player) : player(_player) @@ -35,10 +28,6 @@ PlayerGraphicsItem::PlayerGraphicsItem(PlayerLogic *_player) : player(_player) connect(player, &PlayerLogic::counterAdded, this, &PlayerGraphicsItem::onCounterAdded); connect(player, &PlayerLogic::counterRemoved, this, &PlayerGraphicsItem::onCounterRemoved); - connect(player, &PlayerLogic::deckChanged, this, &PlayerGraphicsItem::updatePlaymat); - connect(player, &PlayerLogic::playmatChanged, this, &PlayerGraphicsItem::updatePlaymat); - connect(&SettingsCache::instance().userInterface(), &InterfaceSettings::playmatVisibilityChanged, this, - [this](int) { updatePlaymat(); }); playerMenu = new PlayerMenu(this); @@ -78,9 +67,6 @@ PlayerGraphicsItem::PlayerGraphicsItem(PlayerLogic *_player) : player(_player) connect(tableZoneGraphicsItem, &TableZone::sizeChanged, this, &PlayerGraphicsItem::updateBoundingRect); - connect(this, &PlayerGraphicsItem::playmatChanged, tableZoneGraphicsItem, &TableZone::onPlaymatChanged); - connect(this, &PlayerGraphicsItem::playmatChanged, stackZoneGraphicsItem, &StackZone::onPlaymatChanged); - updateBoundingRect(); rearrangeZones(); @@ -126,6 +112,7 @@ void PlayerGraphicsItem::initializeZones() rfgZoneGraphicsItem->setPos(base + QPointF(0, 2 * h + h2 + 10)); tableZoneGraphicsItem = new TableZone(player->getTableZone(), mirrored, this); + connect(tableZoneGraphicsItem, &TableZone::sizeChanged, this, &PlayerGraphicsItem::updateBoundingRect); connect(this, &PlayerGraphicsItem::mirroredChanged, tableZoneGraphicsItem, &TableZone::setMirrored); stackZoneGraphicsItem = @@ -168,61 +155,10 @@ qreal PlayerGraphicsItem::getMinimumWidth() const return result; } -void PlayerGraphicsItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *) +void PlayerGraphicsItem::paint(QPainter * /*painter*/, + const QStyleOptionGraphicsItem * /*option*/, + QWidget * /*widget*/) { - if (!hasPlaymat || playmatPixmap.isNull()) { - return; - } - - // Calculate the combined bounding rect of stack + table zones - QPointF stackPos = stackZoneGraphicsItem->pos(); - QPointF tablePos = tableZoneGraphicsItem->pos(); - QSizeF stackSize = stackZoneGraphicsItem->boundingRect().size(); - QSizeF tableSize = tableZoneGraphicsItem->boundingRect().size(); - - // Combined area: from stack left edge to table right edge - double combinedLeft = qMin(stackPos.x(), tablePos.x()); - double combinedTop = qMin(stackPos.y(), tablePos.y()); - double combinedRight = qMax(stackPos.x() + stackSize.width(), tablePos.x() + tableSize.width()); - double combinedBottom = qMax(stackPos.y() + stackSize.height(), tablePos.y() + tableSize.height()); - - QRectF combinedArea(combinedLeft, combinedTop, combinedRight - combinedLeft, combinedBottom - combinedTop); - - const QRectF srcRect = PlaymatUtils::computeArtSourceRect(playmatPixmap.size(), playmatParams); - const QRectF dstRect = PlaymatUtils::coverFitRect(combinedArea, srcRect.size()); - - painter->save(); - painter->setClipRect(combinedArea); - painter->setRenderHint(QPainter::SmoothPixmapTransform, true); - - // Render from a down-scaled copy of the art so the full-resolution source - // pixmap is never re-sampled at a tiny device size (also much cheaper than - // scaling it on every frame). - const QPixmap scaledPixmap = scaledPlaymatFor(srcRect, painter->worldTransform().mapRect(dstRect).size()); - painter->drawPixmap(dstRect, scaledPixmap, QRectF(scaledPixmap.rect())); - - painter->restore(); - - if (!playmatAttribution.isEmpty()) { - paintArtAttribution(*painter, combinedArea, playmatAttribution, Qt::AlignRight | Qt::AlignBottom, 0.8); - } -} - -QPixmap PlayerGraphicsItem::scaledPlaymatFor(const QRectF &srcRect, const QSizeF &deviceDstSize) -{ - // Bucket the render size so the source pixmap is re-scaled at most once per - // zoom step instead of once per frame. - constexpr int bucketSize = 32; - const QSize target = QSize(qMax(1, qRound(deviceDstSize.width() / bucketSize) * bucketSize), - qMax(1, qRound(deviceDstSize.height() / bucketSize) * bucketSize)) - .boundedTo(srcRect.toAlignedRect().size()); - - if (scaledPlaymatKey != target) { - const QPixmap crop = playmatPixmap.copy(srcRect.toAlignedRect()); - scaledPlaymatPixmap = crop.scaled(target, Qt::KeepAspectRatio, Qt::SmoothTransformation); - scaledPlaymatKey = target; - } - return scaledPlaymatPixmap; } void PlayerGraphicsItem::processSceneSizeChange(int newPlayerWidth) @@ -252,11 +188,6 @@ void PlayerGraphicsItem::onCounterAdded(CounterState *state) AbstractCounter *widget; if (state->getName() == "life") { widget = playerTarget->addCounter(state); - connect(state, &CounterState::valueChanged, this, [this](int oldValue, int newValue, bool skipDamageAnimation) { - if (newValue < oldValue && !skipDamageAnimation) { - tableZoneGraphicsItem->triggerDamageShimmer(); - } - }); } else { widget = new GeneralCounter(state, player, true, this); } @@ -367,100 +298,3 @@ void PlayerGraphicsItem::updateBoundingRect() emit sizeChanged(); } - -void PlayerGraphicsItem::updatePlaymat() -{ - int visibility = SettingsCache::instance().userInterface().getPlaymatVisibility(); - - // "Don't use playmats" — never show - if (visibility == PlaymatVisibilityNone) { - clearPlaymat(); - return; - } - - // "Show own playmat only" — hide playmats for remote players - if (visibility == PlaymatVisibilityOwnOnly && !player->getPlayerInfo()->getLocal()) { - clearPlaymat(); - return; - } - - CardRef playmatCard; - PlaymatParams params; - - if (player->getHasRemotePlaymat()) { - // Prefer the server-confirmed playmat (updated by Command_SetPlaymat). - playmatCard = player->getRemotePlaymatCard(); - params = player->getRemotePlaymatParams(); - } else if (player->getPlayerInfo()->getLocal()) { - // Local player without a server broadcast yet: apply the full - // settings-based resolution chain (mode, fallback list, behavior). - const auto &settings = SettingsCache::instance().userInterface(); - const PlaymatInfo resolved = resolvePlaymatForDeck( - player->getDeck(), settings.getPlaymatFallbackList(), static_cast(settings.getPlaymatMode()), - static_cast(settings.getPlaymatFallbackBehavior()), 0); - playmatCard = resolved.card; - params = resolved.params; - } else { - // Opponent without a server broadcast: use the deck-embedded playmat. - const DeckList &deck = player->getDeck(); - const PlaymatInfo &deckPlaymat = deck.getPlaymat(); - if (!deckPlaymat.card.isEmpty()) { - playmatCard = deckPlaymat.card; - params = deckPlaymat.params; - } - } - - if (playmatCard.isEmpty()) { - clearPlaymat(); - return; - } - - playmatParams = params; - scaledPlaymatKey = QSize(); // the art crop depends on the params, drop any cached scale - - ExactCard card = CardDatabaseManager::query()->getCard(playmatCard); - if (!card) { - clearPlaymat(); - return; - } - - playmatAttribution = buildArtAttribution(card); - - QPixmap fullRes; - CardPictureLoader::getPixmap(fullRes, card, QSize(745, 1040)); - - if (fullRes.isNull()) { - disconnect(playmatPixmapConnection); - CardInfo *cardInfo = card.getCardPtr().data(); - if (cardInfo) { - playmatPixmapConnection = - connect(cardInfo, &CardInfo::pixmapUpdated, this, &PlayerGraphicsItem::onPlaymatPixmapReady); - } - return; - } - - if (!hasPlaymat) { - hasPlaymat = true; - emit playmatChanged(true); - } - playmatPixmap = CardArtUtils::rotateSidewaysLayoutArt(fullRes, card); - update(); -} - -void PlayerGraphicsItem::clearPlaymat() -{ - disconnect(playmatPixmapConnection); - playmatAttribution.clear(); - if (hasPlaymat) { - hasPlaymat = false; - playmatPixmap = QPixmap(); - scaledPlaymatKey = QSize(); - emit playmatChanged(false); - update(); - } -} - -void PlayerGraphicsItem::onPlaymatPixmapReady() -{ - updatePlaymat(); -} diff --git a/cockatrice/src/game_graphics/player/player_graphics_item.h b/cockatrice/src/game_graphics/player/player_graphics_item.h index e5ae59a61..d02234ded 100644 --- a/cockatrice/src/game_graphics/player/player_graphics_item.h +++ b/cockatrice/src/game_graphics/player/player_graphics_item.h @@ -11,7 +11,6 @@ #include "../game_scene.h" #include -#include class HandZone; class PileZone; @@ -127,7 +126,6 @@ signals: void playerCountChanged(); void mirroredChanged(bool isMirrored); void cardInfoRequested(const CardRef &cardRef); - void playmatChanged(bool hasPlaymat); private: PlayerLogic *player; @@ -148,23 +146,9 @@ private: bool mirrored; bool handVisible = false; - QPixmap playmatPixmap; - QPixmap scaledPlaymatPixmap; // down-scaled copy of playmatPixmap for the current render size - QSize scaledPlaymatKey; // size bucket scaledPlaymatPixmap was rendered for - PlaymatParams playmatParams; - QString playmatAttribution; - bool hasPlaymat = false; - QMetaObject::Connection playmatPixmapConnection; - private slots: void updateBoundingRect(); void rearrangeZones(); - void clearPlaymat(); - void updatePlaymat(); - void onPlaymatPixmapReady(); - -private: - QPixmap scaledPlaymatFor(const QRectF &srcRect, const QSizeF &deviceDstSize); }; #endif // COCKATRICE_PLAYER_GRAPHICS_ITEM_H diff --git a/cockatrice/src/game_graphics/player/player_list_widget.cpp b/cockatrice/src/game_graphics/player/player_list_widget.cpp index a7ec2e4a9..4268e1019 100644 --- a/cockatrice/src/game_graphics/player/player_list_widget.cpp +++ b/cockatrice/src/game_graphics/player/player_list_widget.cpp @@ -53,13 +53,13 @@ PlayerListWidget::PlayerListWidget(TabSupervisor *_tabSupervisor, QWidget *parent) : QTreeWidget(parent), tabSupervisor(_tabSupervisor), client(_client), game(_game), gameStarted(false) { - readyIcon = themePixmap(QStringLiteral("icons/ready_start")); - notReadyIcon = themePixmap(QStringLiteral("icons/not_ready_start")); - concededIcon = themePixmap(QStringLiteral("icons/conceded")); + readyIcon = QPixmap("theme:icons/ready_start"); + notReadyIcon = QPixmap("theme:icons/not_ready_start"); + concededIcon = QPixmap("theme:icons/conceded"); playerIcon = loadColorAdjustedPixmap("theme:icons/player"); judgeIcon = loadColorAdjustedPixmap("theme:icons/scales"); spectatorIcon = loadColorAdjustedPixmap("theme:icons/spectator"); - lockIcon = themePixmap(QStringLiteral("icons/lock")); + lockIcon = QPixmap("theme:icons/lock"); if (tabSupervisor) { itemDelegate = new PlayerListItemDelegate(this); @@ -92,11 +92,6 @@ void PlayerListWidget::retranslateUi() void PlayerListWidget::addPlayer(const ServerInfo_PlayerProperties &player) { - if (players.contains(player.player_id())) { - updatePlayerProperties(player); - return; - } - QTreeWidgetItem *newPlayer = new PlayerListTWI; players.insert(player.player_id(), newPlayer); updatePlayerProperties(player); @@ -181,17 +176,6 @@ void PlayerListWidget::removePlayer(int playerId) delete takeTopLevelItem(indexOfTopLevelItem(player)); } -void PlayerListWidget::clearSpectators() -{ - const QList playerIds = players.keys(); - for (int playerId : playerIds) { - QTreeWidgetItem *player = players.value(playerId, 0); - if (player && !player->data(1, Qt::UserRole).toBool()) { - removePlayer(playerId); - } - } -} - void PlayerListWidget::setActivePlayer(int playerId) { QMapIterator i(players); diff --git a/cockatrice/src/game_graphics/player/player_list_widget.h b/cockatrice/src/game_graphics/player/player_list_widget.h index f2f0be5fd..a53cfa989 100644 --- a/cockatrice/src/game_graphics/player/player_list_widget.h +++ b/cockatrice/src/game_graphics/player/player_list_widget.h @@ -66,7 +66,6 @@ public slots: void addPlayer(const ServerInfo_PlayerProperties &player); void removePlayer(int playerId); void updatePlayerProperties(const ServerInfo_PlayerProperties &prop, int playerId = -1); - void clearSpectators(); }; #endif diff --git a/cockatrice/src/game_graphics/player/player_target.cpp b/cockatrice/src/game_graphics/player/player_target.cpp index d6c28370d..567f3d44d 100644 --- a/cockatrice/src/game_graphics/player/player_target.cpp +++ b/cockatrice/src/game_graphics/player/player_target.cpp @@ -1,11 +1,8 @@ #include "player_target.h" -#include "../../client/settings/cache_settings.h" #include "../../game/player/player_logic.h" #include "../../interface/pixel_map_generator.h" -#include "../game_scene.h" -#include #include #include #include @@ -24,24 +21,17 @@ QRectF PlayerCounter::boundingRect() const void PlayerCounter::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*option*/, QWidget * /*widget*/) { - const int radius = 15; - const qreal border = 1.5; - // The box is drawn with a border-wide stroke straddling the path, so the - // visible outline spans [inset, inset + border]. Fills that must not cover - // the outline (e.g. the life-change flash) use a path inset by `border`. - const auto makePath = [](qreal inset) { - QPainterPath path(QPointF(50 - inset, inset)); - path.lineTo(radius, inset); - path.arcTo(inset, inset, 2 * radius, 2 * radius, 90, 90); - path.lineTo(inset, 30 - inset); - path.lineTo(50 - inset, 30 - inset); - path.closeSubpath(); - return path; - }; - QPainterPath path = makePath(border / 2); + const int radius = 8; + const qreal border = 1; + QPainterPath path(QPointF(50 - border / 2, border / 2)); + path.lineTo(radius, border / 2); + path.arcTo(border / 2, border / 2, 2 * radius, 2 * radius, 90, 90); + path.lineTo(border / 2, 30 - border / 2); + path.lineTo(50 - border / 2, 30 - border / 2); + path.closeSubpath(); QPen pen(QColor(100, 100, 100)); - pen.setWidthF(border); + pen.setWidth(border); painter->setPen(pen); painter->setBrush(hovered ? QColor(50, 50, 50, 160) : QColor(0, 0, 0, 160)); @@ -55,53 +45,6 @@ void PlayerCounter::paint(QPainter *painter, const QStyleOptionGraphicsItem * /* painter->setFont(font); painter->setPen(Qt::white); painter->drawText(translatedRect, Qt::AlignCenter, QString::number(value)); - - // Life-change flash: emerald on gain, red on loss, decaying over a few ticks. - if (flashAlpha > 0) { - painter->save(); - QColor flashColor = flashDelta > 0 ? QColor(52, 224, 122) : QColor(239, 68, 68); - flashColor.setAlphaF(0.45 * flashAlpha); - painter->setPen(Qt::NoPen); - painter->setBrush(flashColor); - painter->setOpacity(0.85); - painter->drawPath(makePath(border)); - painter->restore(); - } -} - -void PlayerCounter::onValueChanged(int oldValue, int newValue, bool skipDamageAnimation) -{ - flashDelta = newValue - oldValue; - if (flashDelta == 0) { - return; - } - - if (!SettingsCache::instance().userInterface().getLifeCounterAnimationsEnabled()) { - flashAlpha = 0.0; - return; - } - - if (skipDamageAnimation) { - flashAlpha = 0.0; - return; - } - - flashAlpha = 1.0; - flashClock.start(); - if (scene()) { - static_cast(scene())->registerAnimationItem(this); - } -} - -bool PlayerCounter::animationEvent() -{ - flashAlpha = 1.0 - flashClock.elapsed() / flashDurationMs; - if (flashAlpha <= 0.0) { - flashAlpha = 0.0; - return false; - } - update(); - return true; } PlayerTarget::PlayerTarget(PlayerLogic *_owner, QGraphicsItem *parentItem) @@ -137,18 +80,8 @@ void PlayerTarget::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*o QRectF translatedRect = painter->combinedTransform().mapRect(avatarBoundingRect); QSize translatedSize = translatedRect.size().toSize(); QPixmap cachedPixmap; - // The key must cover everything the generated pawn depends on: the rendered - // size, the user level, and the pixmap being drawn. fullPixmap.cacheKey() is - // 0 for every null pixmap, so the default-pawn branch additionally needs the - // pawn's privlevel (lowercased, matching UserLevelPixmapGenerator) and colors - // in the key — otherwise two players without a custom avatar (and the same - // user level) would share one cached pawn. const QString cacheKey = "avatar" + QString::number(translatedSize.width()) + "_" + - QString::number(translatedSize.height()) + "_" + QString::number(info->user_level()) + - "_" + QString::number(fullPixmap.cacheKey()) + "_" + - QString::fromStdString(info->privlevel()).toLower() + "_" + - QString::fromStdString(info->pawn_colors().left_side()) + "_" + - QString::fromStdString(info->pawn_colors().right_side()); + QString::number(info->user_level()) + "_" + QString::number(fullPixmap.cacheKey()); if (!QPixmapCache::find(cacheKey, &cachedPixmap)) { cachedPixmap = QPixmap(translatedSize.width(), translatedSize.height()); diff --git a/cockatrice/src/game_graphics/player/player_target.h b/cockatrice/src/game_graphics/player/player_target.h index 1d06c6274..67e155660 100644 --- a/cockatrice/src/game_graphics/player/player_target.h +++ b/cockatrice/src/game_graphics/player/player_target.h @@ -7,34 +7,21 @@ #ifndef PLAYERTARGET_H #define PLAYERTARGET_H -#include "../animated_item.h" #include "../board/abstract_counter.h" #include "../board/arrow_target.h" #include "../board/graphics_item_type.h" -#include #include class PlayerLogic; -class PlayerCounter : public AbstractCounter, public IAnimatedItem +class PlayerCounter : public AbstractCounter { Q_OBJECT -protected: - void onValueChanged(int oldValue, int newValue, bool skipDamageAnimation) override; - -private: - static constexpr qreal flashDurationMs = 450.0; - - QElapsedTimer flashClock; - qreal flashAlpha = 0.0; - int flashDelta = 0; - public: PlayerCounter(CounterState *state, PlayerLogic *player, QGraphicsItem *parent); QRectF boundingRect() const override; void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override; - bool animationEvent() override; }; class PlayerTarget : public ArrowTarget diff --git a/cockatrice/src/game_graphics/tally/stats_tally.cpp b/cockatrice/src/game_graphics/tally/stats_tally.cpp index 7e05c3fb1..e7a6621fa 100644 --- a/cockatrice/src/game_graphics/tally/stats_tally.cpp +++ b/cockatrice/src/game_graphics/tally/stats_tally.cpp @@ -34,31 +34,3 @@ QList StatsTally::computeTotalPower(const QList &cards) QString name = QCoreApplication::translate("StatsTally", "Total Power"); return {TallyRow{name, QString::number(total)}}; } - -static int sumToughness(const QList &cards) -{ - int total = 0; - for (auto card : cards) { - QVariantList parsed = CardItem::parsePT(card->getPT()); - if (parsed.size() == 2) { - int toughness = parsed.at(1).toInt(); // toInt will default to 0 if it's not an int - total += qMax(toughness, 0); - } - } - return total; -} - -QList StatsTally::computeTotalToughness(const QList &cards) -{ - // don't bother if none of the cards have pt - bool hasPT = - std::any_of(cards.cbegin(), cards.cend(), [](const CardItem *card) { return !card->getPT().isEmpty(); }); - if (!hasPT) { - return {}; - } - - int total = sumToughness(cards); - - QString name = QCoreApplication::translate("StatsTally", "Total Toughness"); - return {TallyRow{name, QString::number(total)}}; -} diff --git a/cockatrice/src/game_graphics/tally/stats_tally.h b/cockatrice/src/game_graphics/tally/stats_tally.h index e499587eb..4c3d93b56 100644 --- a/cockatrice/src/game_graphics/tally/stats_tally.h +++ b/cockatrice/src/game_graphics/tally/stats_tally.h @@ -16,14 +16,6 @@ namespace StatsTally */ QList computeTotalPower(const QList &cards); -/** - * @brief Sums the toughness of all selected cards - * - * @param cards The list of selected card items to analyze. - * @return A single row containing the total, or an empty list if none of the cards have pt - */ -QList computeTotalToughness(const QList &cards); - } // namespace StatsTally #endif // COCKATRICE_STATS_TALLY_H diff --git a/cockatrice/src/game_graphics/tally/tally.cpp b/cockatrice/src/game_graphics/tally/tally.cpp index 21806ee84..aa2cae024 100644 --- a/cockatrice/src/game_graphics/tally/tally.cpp +++ b/cockatrice/src/game_graphics/tally/tally.cpp @@ -21,8 +21,6 @@ QList Tally::compute(const QList &cards, const TallyType t return SubtypeTally::countSubtypes(cards); case TallyType::TotalPower: return StatsTally::computeTotalPower(cards); - case TallyType::TotalToughness: - return StatsTally::computeTotalToughness(cards); } return {}; } diff --git a/cockatrice/src/game_graphics/tally/tally.h b/cockatrice/src/game_graphics/tally/tally.h index 84c54918f..97406cddb 100644 --- a/cockatrice/src/game_graphics/tally/tally.h +++ b/cockatrice/src/game_graphics/tally/tally.h @@ -21,8 +21,7 @@ enum class TallyType None, Subtypes, TotalPower, - TotalToughness, - MaxValue = TotalToughness // sentinel value + MaxValue = TotalPower // sentinel value }; namespace Tally diff --git a/cockatrice/src/game_graphics/zones/hand_zone.cpp b/cockatrice/src/game_graphics/zones/hand_zone.cpp index 1a8f7a910..b52a4955a 100644 --- a/cockatrice/src/game_graphics/zones/hand_zone.cpp +++ b/cockatrice/src/game_graphics/zones/hand_zone.cpp @@ -41,8 +41,7 @@ void HandZone::handleDropEvent(const QList &dragItems, } } } else { - bool sameZone = startZone == getLogic(); - x = calcDropIndexFromY(dropPoint.y(), !sameZone); + x = calcDropIndexFromY(dropPoint.y()); } Command_MoveCard cmd; diff --git a/cockatrice/src/game_graphics/zones/select_zone.cpp b/cockatrice/src/game_graphics/zones/select_zone.cpp index 470c70fcf..c58c41b92 100644 --- a/cockatrice/src/game_graphics/zones/select_zone.cpp +++ b/cockatrice/src/game_graphics/zones/select_zone.cpp @@ -83,7 +83,7 @@ SelectZone::StackLayoutParams SelectZone::buildStackParams(qreal minOffset) cons return {cardCount, boundingRect().height(), cardHeight, offset, minOffset}; } -int SelectZone::calcDropIndexFromY(qreal dropY, bool allowCountExpand, qreal minOffset) const +int SelectZone::calcDropIndexFromY(qreal dropY, qreal minOffset) const { const auto &cards = getLogic()->getCards(); if (cards.isEmpty()) { @@ -94,8 +94,7 @@ int SelectZone::calcDropIndexFromY(qreal dropY, bool allowCountExpand, qreal min if (effectiveOffset <= 0.0) { return 0; } - int max = allowCountExpand ? params.cardCount : params.cardCount - 1; - return qBound(0, qRound((dropY - start) / effectiveOffset), max); + return qBound(0, qRound((dropY - start) / effectiveOffset), params.cardCount - 1); } void SelectZone::restoreStaleEscapedCards() diff --git a/cockatrice/src/game_graphics/zones/select_zone.h b/cockatrice/src/game_graphics/zones/select_zone.h index b5d3ca37a..7408f29b6 100644 --- a/cockatrice/src/game_graphics/zones/select_zone.h +++ b/cockatrice/src/game_graphics/zones/select_zone.h @@ -104,12 +104,8 @@ protected: /** * @brief Computes the card index at a given y-coordinate within the zone's vertical layout. * Returns 0 if the zone has no cards or the offset is zero. - * - * @param dropY The y-coordinate that the card was dropped at - * @param allowCountExpand If false, clamps the index at the number of cards minus 1 - * @param minOffset Minimum offset to preserve */ - int calcDropIndexFromY(qreal dropY, bool allowCountExpand, qreal minOffset = 0.0) const; + int calcDropIndexFromY(qreal dropY, qreal minOffset = 0.0) const; /** * @brief Positions cards vertically with alternating left/right x-offsets. diff --git a/cockatrice/src/game_graphics/zones/stack_zone.cpp b/cockatrice/src/game_graphics/zones/stack_zone.cpp index ff62097c7..184f96d62 100644 --- a/cockatrice/src/game_graphics/zones/stack_zone.cpp +++ b/cockatrice/src/game_graphics/zones/stack_zone.cpp @@ -31,22 +31,8 @@ QRectF StackZone::boundingRect() const void StackZone::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*option*/, QWidget * /*widget*/) { - if (playmatActive) { - // Subtle overlay to distinguish stack zone from table zone (slightly darker) - painter->fillRect(boundingRect(), QColor(0, 0, 0, 80)); - } else { - QBrush brush = themeManager->getExtraBgBrush(ThemeManager::Stack, getLogic()->getPlayer()->getZoneId()); - painter->fillRect(boundingRect(), brush); - } -} - -void StackZone::onPlaymatChanged(bool active) -{ - playmatActive = active; - // See TableZone::onPlaymatChanged for the rationale. Translucent overlay - // over a dynamic playmat should not be held in the device cache. - setCacheMode(active ? QGraphicsItem::NoCache : QGraphicsItem::DeviceCoordinateCache); - update(); + QBrush brush = themeManager->getExtraBgBrush(ThemeManager::Stack, getLogic()->getPlayer()->getZoneId()); + painter->fillRect(boundingRect(), brush); } void StackZone::handleDropEvent(const QList &dragItems, @@ -57,14 +43,18 @@ void StackZone::handleDropEvent(const QList &dragItems, return; } - bool sameZone = startZone == getLogic(); - int index = calcDropIndexFromY(dropPoint.y(), !sameZone, MIN_CARD_VISIBLE); - if (sameZone) { + const auto &cards = getLogic()->getCards(); + int index; + if (startZone == getLogic()) { + // Reordering within the zone: use drop position + index = calcDropIndexFromY(dropPoint.y(), MIN_CARD_VISIBLE); // Same-zone no-op: don't move a card onto itself - const auto &cards = getLogic()->getCards(); if (!cards.isEmpty() && cards.at(index)->getId() == dragItems.at(0)->getId()) { return; } + } else { + // Coming from another zone: append at end (top of stack, rendered on top) + index = static_cast(cards.size()); } Command_MoveCard cmd; diff --git a/cockatrice/src/game_graphics/zones/stack_zone.h b/cockatrice/src/game_graphics/zones/stack_zone.h index 96b3f96a6..147c3e2fc 100644 --- a/cockatrice/src/game_graphics/zones/stack_zone.h +++ b/cockatrice/src/game_graphics/zones/stack_zone.h @@ -15,13 +15,9 @@ class StackZone : public SelectZone Q_OBJECT private: qreal zoneHeight; - bool playmatActive = false; private slots: void updateBg(); -public slots: - void onPlaymatChanged(bool active); - public: StackZone(StackZoneLogic *_logic, int _zoneHeight, QGraphicsItem *parent); /** @brief Resizes the stack zone height, e.g. when sharing vertical space with the command zone. */ diff --git a/cockatrice/src/game_graphics/zones/table_zone.cpp b/cockatrice/src/game_graphics/zones/table_zone.cpp index 88e9abe6c..4ef01853f 100644 --- a/cockatrice/src/game_graphics/zones/table_zone.cpp +++ b/cockatrice/src/game_graphics/zones/table_zone.cpp @@ -8,7 +8,6 @@ #include "../board/arrow_item.h" #include "../board/card_drag_item.h" #include "../board/card_item.h" -#include "../game_scene.h" #include "../z_values.h" #include @@ -48,31 +47,6 @@ void TableZone::updateBg() update(); } -void TableZone::triggerDamageShimmer() -{ - if (!SettingsCache::instance().userInterface().getBattlefieldFlashEnabled()) { - damageShimmerAlpha = 0.0; - return; - } - - damageShimmerAlpha = 1.0; - shimmerClock.start(); - if (scene()) { - static_cast(scene())->registerAnimationItem(this); - } -} - -bool TableZone::animationEvent() -{ - damageShimmerAlpha = 1.0 - shimmerClock.elapsed() / shimmerDurationMs; - if (damageShimmerAlpha <= 0.0) { - damageShimmerAlpha = 0.0; - return false; - } - update(); - return true; -} - QRectF TableZone::boundingRect() const { return QRectF(0, 0, width, height); @@ -92,43 +66,20 @@ bool TableZone::isInverted() const void TableZone::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*option*/, QWidget * /*widget*/) { - if (playmatActive) { - // Subtle overlay to distinguish table zone from stack zone - painter->fillRect(boundingRect(), QColor(0, 0, 0, 60)); - } else { - QBrush brush = themeManager->getExtraBgBrush(ThemeManager::Table, getLogic()->getPlayer()->getZoneId()); - painter->fillRect(boundingRect(), brush); - } + QBrush brush = themeManager->getExtraBgBrush(ThemeManager::Table, getLogic()->getPlayer()->getZoneId()); + painter->fillRect(boundingRect(), brush); if (active) { paintZoneOutline(painter); } else { // inactive player gets a darker table zone with a semi transparent black mask - // this means if the user provides a custom background or playmat it will fade + // this means if the user provides a custom background it will fade painter->fillRect(boundingRect(), FADE_MASK); } - // Decaying crimson wash from taking damage. - if (damageShimmerAlpha > 0.0) { - QColor shimmerColor(239, 68, 68); - shimmerColor.setAlphaF(0.22 * damageShimmerAlpha); - painter->fillRect(boundingRect(), shimmerColor); - } - paintLandDivider(painter); } -void TableZone::onPlaymatChanged(bool active) -{ - playmatActive = active; - // While a playmat is shown the zone paints a translucent overlay over the - // dynamic playmat behind it. Keep it out of the device cache so the cached - // pixels are never stale relative to the playmat (and to avoid compositing - // artifacts of cached translucent content on some platforms). - setCacheMode(active ? QGraphicsItem::NoCache : QGraphicsItem::DeviceCoordinateCache); - update(); -} - /** Render a soft outline around the edge of the TableZone. diff --git a/cockatrice/src/game_graphics/zones/table_zone.h b/cockatrice/src/game_graphics/zones/table_zone.h index 92915a2ed..0d7e58206 100644 --- a/cockatrice/src/game_graphics/zones/table_zone.h +++ b/cockatrice/src/game_graphics/zones/table_zone.h @@ -8,19 +8,16 @@ #define TABLEZONE_H #include "../../game/zones/table_zone_logic.h" -#include "../animated_item.h" #include "../board/abstract_card_item.h" #include "select_zone.h" -#include - /** * @brief TableZone is the grid based rect where CardItems may be placed. * * It is the main play zone and can be customized with background images. */ //! \todo Refactor methods to make more readable, extract logic to private methods (especially reorganizeCards()). -class TableZone : public SelectZone, public IAnimatedItem +class TableZone : public SelectZone { Q_OBJECT @@ -86,7 +83,6 @@ private: */ bool active = false; bool mirrored = false; - bool playmatActive = false; [[nodiscard]] bool isInverted() const; @@ -96,9 +92,6 @@ private slots: */ void updateBg(); -public slots: - void onPlaymatChanged(bool active); - public slots: /** Reorganizes CardItems in the TableZone @@ -128,16 +121,6 @@ public: */ void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override; - /** - Flashes the table surface after a player loses life. - - Wired up through the life counter so the battlefield glows when life drops. - */ - void triggerDamageShimmer(); - - /** @brief Decays the damage shimmer by one timer tick. */ - bool animationEvent() override; - /** Toggles the selected items as tapped. */ @@ -188,17 +171,8 @@ public: } void setWidth(qreal _width) { - // The width is stored as an int; truncate to match the previous implicit conversion. - const int newWidth = static_cast(_width); - if (width == newWidth) { - return; - } prepareGeometryChange(); - width = newWidth; - // The parent player item's boundingRect (which clips the playmat painting) is - // derived from this zone's size. Without this signal the playmat is cut off at - // the stale boundingRect edge whenever the scene is resized wider. - emit sizeChanged(); + width = _width; } [[nodiscard]] qreal getWidth() const { @@ -211,11 +185,6 @@ public: } private: - static constexpr qreal shimmerDurationMs = 450.0; - - QElapsedTimer shimmerClock; - qreal damageShimmerAlpha = 0.0; - void paintZoneOutline(QPainter *painter); void paintLandDivider(QPainter *painter); diff --git a/cockatrice/src/game_graphics/zones/view_zone.cpp b/cockatrice/src/game_graphics/zones/view_zone.cpp index 5bd5d262f..baf7b8b30 100644 --- a/cockatrice/src/game_graphics/zones/view_zone.cpp +++ b/cockatrice/src/game_graphics/zones/view_zone.cpp @@ -1,6 +1,5 @@ #include "view_zone.h" -#include "../../client/settings/cache_settings.h" #include "../../game/player/player_actions.h" #include "../../game/player/player_logic.h" #include "../../game/zones/view_zone_logic.h" @@ -12,13 +11,11 @@ #include #include #include -#include #include #include #include #include #include -#include /** * @param parent the parent QGraphicsWidget containing the reveal zone @@ -256,10 +253,7 @@ ZoneViewZone::GridSize ZoneViewZone::positionCardsForDisplay(CardList &cards, Ca void ZoneViewZone::setFilterString(const QString &_filterString) { - const CardsDisplaySettings &cardsDisplay = SettingsCache::instance().cardsDisplay(); - filterString = FilterString( - _filterString, CardSearchLanguage{cardsDisplay.getCardLang(), - static_cast(cardsDisplay.getCardSearchLanguage())}); + filterString = FilterString(_filterString); reorganizeCards(); } diff --git a/cockatrice/src/game_graphics/zones/view_zone_widget.cpp b/cockatrice/src/game_graphics/zones/view_zone_widget.cpp index fa6733413..17118e80d 100644 --- a/cockatrice/src/game_graphics/zones/view_zone_widget.cpp +++ b/cockatrice/src/game_graphics/zones/view_zone_widget.cpp @@ -21,7 +21,6 @@ #include #include #include -#include #include namespace @@ -63,7 +62,7 @@ ZoneViewWidget::ZoneViewWidget(PlayerLogic *_player, searchEdit.setPlaceholderText(tr("Search by card name (or search expressions)")); searchEdit.setClearButtonEnabled(true); searchEdit.addAction(loadColorAdjustedPixmap("theme:icons/search"), QLineEdit::LeadingPosition); - auto help = searchEdit.addAction(themePixmap(QStringLiteral("icons/info")), QLineEdit::TrailingPosition); + auto help = searchEdit.addAction(QPixmap("theme:icons/info"), QLineEdit::TrailingPosition); connect(help, &QAction::triggered, this, [this] { createSearchSyntaxHelpWindow(&searchEdit); }); @@ -169,12 +168,6 @@ ZoneViewWidget::ZoneViewWidget(PlayerLogic *_player, } connect(&searchEdit, &QLineEdit::textChanged, zone, &ZoneViewZone::setFilterString); - - const auto applyCardSearchLanguage = [this] { zone->setFilterString(searchEdit.text()); }; - connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::cardLangChanged, this, - applyCardSearchLanguage); - connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::cardSearchLanguageChanged, this, - applyCardSearchLanguage); } setLayout(vbox); @@ -556,7 +549,7 @@ void ZoneViewWidget::initStyleOption(QStyleOption *option) const { QStyleOptionTitleBar *titleBar = qstyleoption_cast(option); if (titleBar) { - titleBar->icon = themePixmap(QStringLiteral("cockatrice")); + titleBar->icon = QPixmap("theme:cockatrice"); } } diff --git a/cockatrice/src/interface/card_localization.h b/cockatrice/src/interface/card_localization.h deleted file mode 100644 index 0bfe4a764..000000000 --- a/cockatrice/src/interface/card_localization.h +++ /dev/null @@ -1,59 +0,0 @@ -#ifndef COCKATRICE_CARD_LOCALIZATION_H -#define COCKATRICE_CARD_LOCALIZATION_H - -#include "../client/settings/cache_settings.h" - -#include -#include -#include - -namespace CardLocalization -{ -/** - * @brief The language code selected for localized card text and images. - */ -inline QString displayLang() -{ - return SettingsCache::instance().cardsDisplay().getCardLang(); -} - -/** - * @brief Card name in the configured display language, falling back to English. - * @param card The card to display. - * @return The localized name, or an empty string for a null card. - */ -inline QString displayName(const CardInfoPtr &card) -{ - return card.isNull() ? QString() : card->getLocalizedName(displayLang()); -} - -/** - * @brief Card rules text in the configured display language, falling back to English. - * @param card The card to display. - * @return The localized text, or an empty string for a null card. - */ -inline QString displayText(const CardInfoPtr &card) -{ - return card.isNull() ? QString() : card->getLocalizedText(displayLang()); -} - -/** - * @brief Card name in the configured display language, falling back to English. - * @param card The card to display. - */ -inline QString displayName(const CardInfo &card) -{ - return card.getLocalizedName(displayLang()); -} - -/** - * @brief Card rules text in the configured display language, falling back to English. - * @param card The card to display. - */ -inline QString displayText(const CardInfo &card) -{ - return card.getLocalizedText(displayLang()); -} -} // namespace CardLocalization - -#endif // COCKATRICE_CARD_LOCALIZATION_H \ No newline at end of file diff --git a/cockatrice/src/interface/card_picture_loader/card_picture_loader.cpp b/cockatrice/src/interface/card_picture_loader/card_picture_loader.cpp index b8a54761a..2f46e7941 100644 --- a/cockatrice/src/interface/card_picture_loader/card_picture_loader.cpp +++ b/cockatrice/src/interface/card_picture_loader/card_picture_loader.cpp @@ -1,14 +1,13 @@ #include "card_picture_loader.h" #include "../../client/settings/cache_settings.h" -#include "../pixel_map_generator.h" #include "card_picture_loader_cache_method.h" #include "card_picture_loader_local_schemes.h" #include #include #include -#include +#include #include #include #include @@ -20,7 +19,6 @@ #include #include #include -#include #include #include #include @@ -38,10 +36,8 @@ CardPictureLoader::CardPictureLoader() : QObject(nullptr) &CardPictureLoader::picsPathChanged); connect(&SettingsCache::instance().downloads(), &DownloadSettings::picDownloadChanged, this, &CardPictureLoader::picDownloadChanged); - connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::cardLangChanged, this, - &CardPictureLoader::cardLangChanged); - qRegisterMetaType("ExactCard"); + qRegisterMetaType(); connect(worker, &CardPictureLoaderWorker::imageLoaded, this, &CardPictureLoader::imageLoaded); statusBar = new CardPictureLoaderStatusBar(nullptr); @@ -66,7 +62,7 @@ void CardPictureLoader::getCardBackPixmap(QPixmap &pixmap, QSize size) QString backCacheKey = "_trice_card_back_" + QString::number(size.width()) + "x" + QString::number(size.height()); if (!QPixmapCache::find(backCacheKey, &pixmap)) { qCDebug(CardPictureLoaderLog) << "PictureLoader: cache miss for" << backCacheKey; - QPixmap tmpPixmap = themePixmap(QStringLiteral("cardback")); + QPixmap tmpPixmap("theme:cardback"); if (tmpPixmap.isNull()) { qCWarning(CardPictureLoaderLog) << "Failed to load 'theme:cardback'! Using fallback pixmap."; @@ -87,7 +83,7 @@ void CardPictureLoader::getCardBackLoadingInProgressPixmap(QPixmap &pixmap, QSiz "_trice_card_back_inprogress_" + QString::number(size.width()) + "x" + QString::number(size.height()); if (!QPixmapCache::find(backCacheKey, &pixmap)) { qCDebug(CardPictureLoaderCardBackCacheFailLog) << "PictureLoader: cache miss for" << backCacheKey; - QPixmap tmpPixmap = themePixmap(QStringLiteral("cardback")); + QPixmap tmpPixmap("theme:cardback"); if (tmpPixmap.isNull()) { qCWarning(CardPictureLoaderLog) << "Failed to load 'theme:cardback' for in-progress state! Using fallback."; @@ -109,7 +105,7 @@ void CardPictureLoader::getCardBackLoadingFailedPixmap(QPixmap &pixmap, QSize si "_trice_card_back_failed_" + QString::number(size.width()) + "x" + QString::number(size.height()); if (!QPixmapCache::find(backCacheKey, &pixmap)) { qCDebug(CardPictureLoaderCardBackCacheFailLog) << "PictureLoader: cache miss for" << backCacheKey; - QPixmap tmpPixmap = themePixmap(QStringLiteral("cardback")); + QPixmap tmpPixmap("theme:cardback"); if (tmpPixmap.isNull()) { qCWarning(CardPictureLoaderLog) << "Failed to load 'theme:cardback' for failed state! Using fallback."; @@ -142,8 +138,7 @@ void CardPictureLoader::getPixmap(QPixmap &pixmap, const ExactCard &card, QSize QPixmap bigPixmap; if (QPixmapCache::find(key, &bigPixmap)) { if (bigPixmap.isNull()) { - // Leave the pixmap null so callers fall back to a solid color - // instead of showing the card back. + getCardBackLoadingFailedPixmap(pixmap, size); QDateTime failedAtTime = getInstance().failedAt.value(key); if (!failedAtTime.isValid() || failedAtTime.addSecs(RETRY_FAILED_CARDS_SECS) < QDateTime::currentDateTime()) { @@ -209,49 +204,7 @@ void CardPictureLoader::imageLoaded(const ExactCard &card, const QImage &image) card.emitPixmapUpdated(); } -void CardPictureLoader::deleteAllLocalOverrides(const ExactCard &card) -{ - const QString picsRoot = SettingsCache::instance().paths().getPicsPath(); - if (picsRoot.isEmpty() || !card) { - return; - } - - QDir baseDir(picsRoot); - if (!baseDir.cd("downloadedPics")) { - return; - } - - const QString name = card.getInfo().getCorrectedName(); - - QString set, collector, uuid; - auto printing = card.getPrinting(); - if (printing.getSet()) { - set = printing.getSet()->getCorrectedShortName(); - collector = printing.getProperty("num"); - uuid = printing.getUuid(); - } - - for (const auto &scheme : CardPictureLoaderLocalSchemes::exportSchemes()) { - QString rel = CardPictureLoaderLocalSchemes::expandPattern(scheme.pattern, name, set, collector, uuid); - - if (rel.isEmpty()) { - continue; - } - - rel += ".png"; - rel = QDir::cleanPath(rel); - - QString fullPath = baseDir.filePath(rel); - - if (QFile::exists(fullPath)) { - QFile::remove(fullPath); - } - } -} - -void CardPictureLoader::saveCardImageToLocalStorage(const ExactCard &card, - const QPixmap &pixmap, - const bool allowOverwrite) +void CardPictureLoader::saveCardImageToLocalStorage(const ExactCard &card, const QPixmap &pixmap) { if (pixmap.isNull() || !card) { return; @@ -311,9 +264,8 @@ void CardPictureLoader::saveCardImageToLocalStorage(const ExactCard &card, QFileInfo outInfo(baseDir.filePath(relativePath)); - // Automatic cache writes (FILESYSTEM_CACHE) must never clobber an explicit user override. - // Only the explicit override paths pass allowOverwrite == true. - if (!allowOverwrite && outInfo.exists()) { + // Do not overwrite existing files + if (outInfo.exists()) { return; } @@ -334,122 +286,6 @@ void CardPictureLoader::saveCardImageToLocalStorage(const ExactCard &card, } } -void CardPictureLoader::installPrintingOverrideOnLoad(const ExactCard &originalCard, const ExactCard &overrideCard) -{ - // Overriding a card with itself is the reset case, not a real override: every code path below - // would re-enter itself through emitPixmapUpdated(). Reject it outright. - if (originalCard == overrideCard) { - return; - } - - CardInfoPtr cardPtr = overrideCard.getCardPtr(); - if (!cardPtr) { - return; - } - - // Heap-allocate so the lambda can capture it before the connection is made - auto *connectionHandle = new QMetaObject::Connection; - - *connectionHandle = - connect(cardPtr.data(), &CardInfo::pixmapUpdated, cardPtr.data(), - [originalCard, overrideCard, connectionHandle, this](const PrintingInfo &printing) { - // All printings share the same CardInfo, so ignore updates triggered by any - // other printing (e.g., the original card re-loading from disk). - if (printing != overrideCard.getPrinting()) { - return; - } - - QPixmap pixmap; - if (QPixmapCache::find(overrideCard.getPixmapCacheKey(), &pixmap) && !pixmap.isNull()) { - // The override art has resolved — persist it and reflect it immediately. - // Retire the connection before emitting so the refresh can't re-enter. - saveCardImageToLocalStorage(originalCard, pixmap, /*allowOverwrite=*/true); - - QObject::disconnect(*connectionHandle); - delete connectionHandle; - - QPixmapCache::clear(); - originalCard.emitPixmapUpdated(); - return; - } - - // The art could not be resolved. Keep the connection armed so a late resolution - // still lands, and surface a visible refusal instead of a silent no-op. An - // override already on disk is left untouched and simply re-displayed. - QPixmapCache::clear(); - if (!hasLocalOverrides(originalCard)) { - QPixmap refusedPixmap; - getCardBackLoadingFailedPixmap(refusedPixmap, QSize(480, 672)); - QPixmapCache::insert(originalCard.getPixmapCacheKey(), refusedPixmap); - } - originalCard.emitPixmapUpdated(); - }); - - // Now enqueue; if the image is already loading (deduplicated in the worker), - // the signal will still fire when it completes - CardPictureLoader::getInstance().worker->enqueueImageLoad(overrideCard); -} - -void CardPictureLoader::installPrintingOverride(const ExactCard &originalCard, const ExactCard &overrideCard) -{ - // Same guard as installPrintingOverrideOnLoad: self-override is the reset case. - if (originalCard == overrideCard) { - return; - } - - QPixmap pixmap; - const QString key = overrideCard.getPixmapCacheKey(); - - if (QPixmapCache::find(key, &pixmap) && !pixmap.isNull()) { - // Already cached — save immediately; the caller refreshes the card. - saveCardImageToLocalStorage(originalCard, pixmap, /*allowOverwrite=*/true); - return; - } - - // Cache miss or previously failed load — enqueue load and wait for the signal. - installPrintingOverrideOnLoad(originalCard, overrideCard); -} - -bool CardPictureLoader::hasLocalOverrides(const ExactCard &card) -{ - const QString picsRoot = SettingsCache::instance().paths().getPicsPath(); - if (picsRoot.isEmpty() || !card) { - return false; - } - - QDir baseDir(picsRoot); - if (!baseDir.cd("downloadedPics")) { - return false; - } - - const QString name = card.getInfo().getCorrectedName(); - - QString set, collector, uuid; - const PrintingInfo printing = card.getPrinting(); - if (printing.getSet()) { - set = printing.getSet()->getCorrectedShortName(); - collector = printing.getProperty("num"); - uuid = printing.getUuid(); - } - - for (const auto &scheme : CardPictureLoaderLocalSchemes::exportSchemes()) { - QString rel = CardPictureLoaderLocalSchemes::expandPattern(scheme.pattern, name, set, collector, uuid); - - if (rel.isEmpty()) { - continue; - } - - rel += ".png"; - rel = QDir::cleanPath(rel); - - if (QFile::exists(baseDir.filePath(rel))) { - return true; - } - } - - return false; -} - void CardPictureLoader::clearPixmapCache() { QPixmapCache::clear(); @@ -489,11 +325,31 @@ void CardPictureLoader::picsPathChanged() QPixmapCache::clear(); } -void CardPictureLoader::cardLangChanged() +bool CardPictureLoader::hasCustomArt() { - // Localized images are fetched via a different URL, but the in-memory - // pixmap cache is keyed by card name/uuid, so drop everything cached - // (including failure timestamps) to force a reload in the new language. - QPixmapCache::clear(); - failedAt.clear(); + auto picsPath = SettingsCache::instance().paths().getPicsPath(); + QDirIterator it(picsPath, QDir::Dirs | QDir::NoDotAndDotDot); + + // Check if there is at least one non-directory file in the pics path, other + // than in the "downloadedPics" subdirectory. + while (it.hasNext()) { +#if (QT_VERSION >= QT_VERSION_CHECK(6, 3, 0)) + QFileInfo dir(it.nextFileInfo()); +#else + // nextFileInfo() is only available in Qt 6.3+, for previous versions, we build + // the QFileInfo from a QString which requires more system calls. + QFileInfo dir(it.next()); +#endif + + if (it.fileName() == "downloadedPics") { + continue; + } + + QDirIterator subIt(it.filePath(), QDir::Files, QDirIterator::Subdirectories | QDirIterator::FollowSymlinks); + if (subIt.hasNext()) { + return true; + } + } + + return false; } diff --git a/cockatrice/src/interface/card_picture_loader/card_picture_loader.h b/cockatrice/src/interface/card_picture_loader/card_picture_loader.h index 0a4934e6d..5c3ac84a3 100644 --- a/cockatrice/src/interface/card_picture_loader/card_picture_loader.h +++ b/cockatrice/src/interface/card_picture_loader/card_picture_loader.h @@ -97,17 +97,10 @@ public: static void cacheCardPixmaps(const QList &cards); /** - * @brief Check if a local override image already exists for the card. - * @param card The card to check. - * @return True if the card has at least one locally stored override image. + * @brief Check if the user has custom card art in the picsPath directory. + * @return True if any custom art exists. */ - static bool hasLocalOverrides(const ExactCard &card); - - /** - * @brief Removes all locally stored override images for the card. - * @param card The card to remove the override images of. - */ - static void deleteAllLocalOverrides(const ExactCard &card); + static bool hasCustomArt(); /** * @brief Clears the in-memory QPixmap cache for all cards. @@ -127,9 +120,7 @@ public slots: * @param image Loaded QImage. */ void imageLoaded(const ExactCard &card, const QImage &image); - void saveCardImageToLocalStorage(const ExactCard &card, const QPixmap &pixmap, bool allowOverwrite = false); - void installPrintingOverride(const ExactCard &originalCard, const ExactCard &overrideCard); - void installPrintingOverrideOnLoad(const ExactCard &originalCard, const ExactCard &overrideCard); + void saveCardImageToLocalStorage(const ExactCard &card, const QPixmap &pixmap); private slots: /** @@ -143,12 +134,6 @@ private slots: * Clears the QPixmap cache to reload images. */ void picsPathChanged(); - - /** - * @brief Triggered when the card language setting changes. - * Clears the in-memory picture caches so images reload in the new language. - */ - void cardLangChanged(); }; #endif diff --git a/cockatrice/src/interface/card_picture_loader/card_picture_loader_local.cpp b/cockatrice/src/interface/card_picture_loader/card_picture_loader_local.cpp index c82fca403..39621839a 100644 --- a/cockatrice/src/interface/card_picture_loader/card_picture_loader_local.cpp +++ b/cockatrice/src/interface/card_picture_loader/card_picture_loader_local.cpp @@ -94,10 +94,6 @@ QImage CardPictureLoaderLocal::tryLoadCardImageFromDisk(const QString &setName, candidatePaths << picsPath + "/downloadedPics/" + setName + "/" + nameVariant; } - // Non-set-folder export schemes (e.g., Name_Set_Collector) write straight into - // downloadedPics/; check there as a fallback so local overrides round-trip. - candidatePaths << picsPath + "/downloadedPics/" + nameVariant; - for (const QString &path : candidatePaths) { QFileInfo fileInfo(path); QDir dir = fileInfo.dir(); @@ -109,8 +105,7 @@ QImage CardPictureLoaderLocal::tryLoadCardImageFromDisk(const QString &setName, QStringList files = dir.entryList(QDir::Files); for (const QString &file : files) { - QFileInfo fi(file); - if (fi.completeBaseName() != baseName) { + if (!file.startsWith(baseName)) { continue; } 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 34092f361..d288236d2 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 @@ -84,8 +84,8 @@ void CardPictureLoaderWorker::queueRequest(const QUrl &url, CardPictureLoaderWor SettingsCache::instance().cacheStorage().getCardPictureLoaderCacheMethod()) == CardPictureLoaderCacheMethod::CacheMethod::NETWORK_CACHE && cache->metaData(url).isValid()) { - // A request that will be served from the disk cache never touches the network and therefore - // doesn't use up any of the rate limit, so it gets to skip the queue. + // If we hit a cached url, we get to make the request for free, since it won't contribute towards the + // rate-limit makeRequest(url, worker); return; } @@ -107,13 +107,10 @@ QNetworkReply *CardPictureLoaderWorker::makeRequest(const QUrl &url, CardPicture req.setHeader(QNetworkRequest::UserAgentHeader, QString("Cockatrice %1").arg(VERSION_STRING)); req.setRawHeader("Accept", "image/avif,image/webp,image/apng,image/,/*;q=0.8"); - // Cached entries are served straight from the disk cache even when picture downloads are - // enabled: re-fetching an already-cached image would burn the rate limit for nothing. Only a - // genuine cache miss goes to the network, and only when downloads are enabled. - bool useNetworkCache = static_cast( - SettingsCache::instance().cacheStorage().getCardPictureLoaderCacheMethod()) == - CardPictureLoaderCacheMethod::CacheMethod::NETWORK_CACHE && - (cache->metaData(url).isValid() || !picDownload); + bool useNetworkCache = + !picDownload && static_cast( + SettingsCache::instance().cacheStorage().getCardPictureLoaderCacheMethod()) == + CardPictureLoaderCacheMethod::CacheMethod::NETWORK_CACHE; req.setAttribute(QNetworkRequest::CacheLoadControlAttribute, useNetworkCache ? QNetworkRequest::AlwaysCache : QNetworkRequest::AlwaysNetwork); diff --git a/cockatrice/src/interface/card_picture_loader/card_picture_to_load.cpp b/cockatrice/src/interface/card_picture_loader/card_picture_to_load.cpp index 4f933207e..5f4ff0bbd 100644 --- a/cockatrice/src/interface/card_picture_loader/card_picture_to_load.cpp +++ b/cockatrice/src/interface/card_picture_loader/card_picture_to_load.cpp @@ -282,15 +282,8 @@ QString CardPictureToLoad::transformUrl(const QString &urlTemplate) const } // language setting - const QString cardLang = SettingsCache::instance().cardsDisplay().getCardLang(); - transformMap["!sflang!"] = cardLang; - - // The localized printing's own id is unknown, so Scryfall must resolve it by - // its translated name (see populateSetUrls); expose that name for the - // `/cards/named` template. - if (cardLang != "en") { - transformMap["!localizedName!"] = card.getInfo().getLocalizedName(cardLang); - } + transformMap["!sflang!"] = QString(QCoreApplication::translate( + "PictureLoader", "en", "code for scryfall's language property, not available for all languages")); QString transformedUrl = urlTemplate; for (const QString &prop : transformMap.keys()) { diff --git a/cockatrice/src/interface/deck_loader/deck_file_format.h b/cockatrice/src/interface/deck_loader/deck_file_format.h index 3a25797ec..995de32c0 100644 --- a/cockatrice/src/interface/deck_loader/deck_file_format.h +++ b/cockatrice/src/interface/deck_loader/deck_file_format.h @@ -17,7 +17,7 @@ enum Format PlainText, /** - * This is Cockatrice's native deck file format, and supports deck metadata such as banner cards and tags. + * This is cockatrice's native deck file format, and supports deck metadata such as banner cards and tags. * Stored as .cod files. */ Cockatrice diff --git a/cockatrice/src/interface/deck_loader/deck_loader.cpp b/cockatrice/src/interface/deck_loader/deck_loader.cpp index f29b4eed2..39a0c1071 100644 --- a/cockatrice/src/interface/deck_loader/deck_loader.cpp +++ b/cockatrice/src/interface/deck_loader/deck_loader.cpp @@ -50,7 +50,7 @@ DeckLoader::loadFromFile(const QString &fileName, DeckFileFormat::Format fmt, bo result = deckList.loadFromFile_Native(&file); if (!result) { qCInfo(DeckLoaderLog) << "Failed to load " << fileName - << "as Cockatrice format; retrying as plain format"; + << "as cockatrice format; retrying as plain format"; file.seek(0); result = deckList.loadFromFile_Plain(&file, CardNameNormalizer()); fmt = DeckFileFormat::PlainText; @@ -375,32 +375,15 @@ void DeckLoader::saveToStream_DeckHeader(QTextStream &out, const DeckList &deckL void DeckLoader::saveToStream_DeckZone(QTextStream &out, const InnerDecklistNode *zoneNode, bool addComments, - bool addSetNameAndNumber, - const QString &boardZoneName) + bool addSetNameAndNumber) { - // Nested sub-zones keep their owning board's identity: the top-level call - // passes no board, so the zone's own name is used; recursive calls carry the - // owning board down so the sideboard marker survives sub-zone nesting. - const QString owningBoardZoneName = boardZoneName.isEmpty() ? zoneNode->getName() : boardZoneName; - // group cards by card type and count the subtotals QMultiMap cardsByType; QMap cardTotalByType; int cardTotal = 0; - QList subZones; for (int j = 0; j < zoneNode->size(); j++) { auto *card = dynamic_cast(zoneNode->at(j)); - if (!card) { - // Cards collected in nested sub-zones are exported by recursion so - // they don't end up invisible in the plain text output. They are - // deferred until after this zone's own header and cards so they read - // as part of this zone's block. - if (auto *subZone = dynamic_cast(zoneNode->at(j))) { - subZones.append(subZone); - } - continue; - } CardInfoPtr info = CardDatabaseManager::query()->getCardInfo(card->getName()); QString cardType = info ? info->getMainCardType() : "unknown"; @@ -428,30 +411,25 @@ void DeckLoader::saveToStream_DeckZone(QTextStream &out, QList cards = cardsByType.values(cardType); - saveToStream_DeckZoneCards(out, cards, addComments, addSetNameAndNumber, owningBoardZoneName); + saveToStream_DeckZoneCards(out, zoneNode, cards, addComments, addSetNameAndNumber); if (addComments) { out << "\n"; } } - - // Nested sub-zones come last, after the parent's own header and cards. - for (const auto *subZone : subZones) { - saveToStream_DeckZone(out, subZone, addComments, addSetNameAndNumber, owningBoardZoneName); - } } void DeckLoader::saveToStream_DeckZoneCards(QTextStream &out, + const InnerDecklistNode *zoneNode, QList cards, bool addComments, - bool addSetNameAndNumber, - const QString &boardZoneName) + bool addSetNameAndNumber) { // QMultiMap sorts values in reverse order for (int i = cards.size() - 1; i >= 0; --i) { DecklistCardNode *card = cards[i]; - if (boardZoneName == DECK_ZONE_SIDE && addComments) { + if (zoneNode->getName() == DECK_ZONE_SIDE && addComments) { out << "SB: "; } @@ -532,26 +510,9 @@ bool DeckLoader::convertToCockatriceFormat(LoadedDeck &deck) void DeckLoader::printDeckListNode(QTextCursor *cursor, const InnerDecklistNode *node) { - if (!node || node->isEmpty()) { - return; - } - const int totalColumns = 2; - // Dispatch children by type instead of trusting a whole-node height: a deck - // node may hold direct cards and nested zones side by side (custom zones), - // and an empty node would previously crash on at(0). - QVector cards; - QVector subZones; - for (int i = 0; i < node->size(); i++) { - if (auto *card = dynamic_cast(node->at(i))) { - cards.append(card); - } else if (auto *zone = dynamic_cast(node->at(i))) { - subZones.append(zone); - } - } - - if (!cards.isEmpty()) { + if (node->height() == 1) { QTextBlockFormat blockFormat; QTextCharFormat charFormat; charFormat.setFontPointSize(11); @@ -562,9 +523,9 @@ void DeckLoader::printDeckListNode(QTextCursor *cursor, const InnerDecklistNode tableFormat.setCellPadding(0); tableFormat.setCellSpacing(0); tableFormat.setBorder(0); - QTextTable *table = cursor->insertTable(cards.size() + 1, totalColumns, tableFormat); - for (int i = 0; i < cards.size(); i++) { - const AbstractDecklistCardNode *card = cards[i]; + QTextTable *table = cursor->insertTable(node->size() + 1, totalColumns, tableFormat); + for (int i = 0; i < node->size(); i++) { + auto *card = dynamic_cast(node->at(i)); QTextCharFormat cellCharFormat; cellCharFormat.setFontPointSize(9); @@ -579,13 +540,7 @@ void DeckLoader::printDeckListNode(QTextCursor *cursor, const InnerDecklistNode cellCursor = cell.firstCursorPosition(); cellCursor.insertText(card->getName()); } - } - - for (const InnerDecklistNode *subZone : subZones) { - if (subZone->isEmpty()) { - continue; - } - + } else if (node->height() == 2) { QTextBlockFormat blockFormat; QTextCharFormat charFormat; charFormat.setFontPointSize(14); @@ -604,8 +559,10 @@ void DeckLoader::printDeckListNode(QTextCursor *cursor, const InnerDecklistNode tableFormat.setColumnWidthConstraints(constraints); QTextTable *table = cursor->insertTable(1, totalColumns, tableFormat); - QTextCursor cellCursor = table->cellAt(0, 0).firstCursorPosition(); - printDeckListNode(&cellCursor, subZone); + for (int i = 0; i < node->size(); i++) { + QTextCursor cellCursor = table->cellAt(0, (i * totalColumns) / node->size()).lastCursorPosition(); + printDeckListNode(&cellCursor, dynamic_cast(node->at(i))); + } } cursor->movePosition(QTextCursor::End); diff --git a/cockatrice/src/interface/deck_loader/deck_loader.h b/cockatrice/src/interface/deck_loader/deck_loader.h index be0df311d..ac23e1ee0 100644 --- a/cockatrice/src/interface/deck_loader/deck_loader.h +++ b/cockatrice/src/interface/deck_loader/deck_loader.h @@ -131,7 +131,7 @@ public: static void printDeckList(QPrinter *printer, const DeckList &deckList); /** - * Converts the given deck's file to the Cockatrice file format. + * Converts the given deck's file to the cockatrice file format. * Uses the lastLoadInfo in the LoadedDeck to determine the current name of the file and where to save to. * @param deck The deck to convert. Should have valid lastLoadInfo. Will update the lastLoadInfo. * @return Whether the conversion succeeded. @@ -159,13 +159,12 @@ private: static void saveToStream_DeckZone(QTextStream &out, const InnerDecklistNode *zoneNode, bool addComments = true, - bool addSetNameAndNumber = true, - const QString &boardZoneName = QString()); + bool addSetNameAndNumber = true); static void saveToStream_DeckZoneCards(QTextStream &out, + const InnerDecklistNode *zoneNode, QList cards, bool addComments = true, - bool addSetNameAndNumber = true, - const QString &boardZoneName = QString()); + bool addSetNameAndNumber = true); }; #endif diff --git a/cockatrice/src/interface/intents/contexts/context_join_game.h b/cockatrice/src/interface/intents/contexts/context_join_game.h index 2e5a88ea2..102e2a520 100644 --- a/cockatrice/src/interface/intents/contexts/context_join_game.h +++ b/cockatrice/src/interface/intents/contexts/context_join_game.h @@ -6,7 +6,6 @@ struct ContextJoinGame { ContextJoinRoom roomContext; int gameId; - bool asSpectator = false; }; #endif // COCKATRICE_CONTEXT_JOIN_GAME_H diff --git a/cockatrice/src/interface/intents/contexts/context_open_deck.h b/cockatrice/src/interface/intents/contexts/context_open_deck.h deleted file mode 100644 index 03dca088e..000000000 --- a/cockatrice/src/interface/intents/contexts/context_open_deck.h +++ /dev/null @@ -1,14 +0,0 @@ -#ifndef COCKATRICE_CONTEXT_OPEN_DECK_H -#define COCKATRICE_CONTEXT_OPEN_DECK_H - -#include "context_connect_to_server.h" - -#include - -struct ContextOpenDeck -{ - ContextConnectToServer serverContext; - QString shareToken; -}; - -#endif // COCKATRICE_CONTEXT_OPEN_DECK_H diff --git a/cockatrice/src/interface/intents/intent.cpp b/cockatrice/src/interface/intents/intent.cpp index db0d13b2c..c02a89f35 100644 --- a/cockatrice/src/interface/intents/intent.cpp +++ b/cockatrice/src/interface/intents/intent.cpp @@ -2,11 +2,10 @@ Intent::Intent(QObject *parent) : QObject(parent) { - // An intent is done as soon as it reports success, failure, or cancellation. - // Deleting it also tears down its dependency chain and disconnects any signal wiring. + // An intent is done as soon as it reports success or failure. Deleting it + // also tears down its dependency chain and disconnects any signal wiring. connect(this, &Intent::finished, this, &QObject::deleteLater); connect(this, &Intent::failed, this, &QObject::deleteLater); - connect(this, &Intent::cancelled, this, &QObject::deleteLater); } Intent::~Intent() = default; @@ -28,7 +27,6 @@ void Intent::runDependency(Intent *dependency) this->execute(); }); connect(dependency, &Intent::failed, this, &Intent::failed); - connect(dependency, &Intent::cancelled, this, &Intent::cancelled); dependency->execute(); } @@ -48,11 +46,3 @@ void Intent::emitFailed(const QString &reason) emit failed(reason); } } - -void Intent::emitCancelled() -{ - if (!completed) { - completed = true; - emit cancelled(); - } -} diff --git a/cockatrice/src/interface/intents/intent.h b/cockatrice/src/interface/intents/intent.h index 5d9fdd3d6..125900ecd 100644 --- a/cockatrice/src/interface/intents/intent.h +++ b/cockatrice/src/interface/intents/intent.h @@ -16,7 +16,6 @@ public: signals: void finished(); void failed(QString reason); - void cancelled(); protected: // --- Subclasses must implement these --- @@ -30,7 +29,6 @@ protected: // Emit the outcome exactly once; ignore late signals after the intent is done. void emitFinished(); void emitFailed(const QString &reason); - void emitCancelled(); private: bool completed = false; diff --git a/cockatrice/src/interface/intents/intent_join_server_game.cpp b/cockatrice/src/interface/intents/intent_join_server_game.cpp index b22b200ed..fb9c4d5ce 100644 --- a/cockatrice/src/interface/intents/intent_join_server_game.cpp +++ b/cockatrice/src/interface/intents/intent_join_server_game.cpp @@ -19,15 +19,13 @@ bool IntentJoinServerGame::checkPrecondition() const if (remoteClient->getStatus() != ClientStatus::StatusLoggedIn) { return false; } - // serverName()/serverPort() reflect the server the client was configured - // to connect to, which may differ from the actual TCP peer (e.g. when - // connecting through a proxy), so compare those configured values. A link - // naming the same host on another port is a different server and must not - // reuse the session there. - if (remoteClient->serverName().compare(context->roomContext.serverContext.hostname, Qt::CaseInsensitive) != 0) { + // peerPort() reflects the actual TCP peer, which may differ from the + // configured server port (e.g. when connecting through a proxy), so only + // the hostname is compared here. + if (remoteClient->peerName() != context->roomContext.serverContext.hostname) { return false; } - if (QString::number(remoteClient->serverPort()) != context->roomContext.serverContext.port) { + if (QString::number(remoteClient->peerPort()) != context->roomContext.serverContext.port) { return false; } @@ -57,7 +55,7 @@ bool IntentJoinServerGame::tryJoinGame(TabRoom *room) return false; } - if (room->getGameSelector()->joinGameById(context->gameId, context->asSpectator)) { + if (room->getGameSelector()->joinGameById(context->gameId)) { emitFinished(); return true; } diff --git a/cockatrice/src/interface/intents/intent_login.cpp b/cockatrice/src/interface/intents/intent_login.cpp index 7beb63e1d..ff871fd03 100644 --- a/cockatrice/src/interface/intents/intent_login.cpp +++ b/cockatrice/src/interface/intents/intent_login.cpp @@ -1,14 +1,9 @@ #include "intent_login.h" #include "../../client/settings/cache_settings.h" -#include "../widgets/dialogs/dlg_login_prompt.h" #include "libcockatrice/settings/servers_settings.h" -#include - -IntentGetLoginCredentials::IntentGetLoginCredentials(ContextConnectToServer *_context, - bool _promptForMissingCredentials) - : Intent(), context(_context), promptForMissingCredentials(_promptForMissingCredentials) +IntentGetLoginCredentials::IntentGetLoginCredentials(ContextConnectToServer *_context) : Intent(), context(_context) { } @@ -34,46 +29,5 @@ void IntentGetLoginCredentials::onPreconditionSatisfied() void IntentGetLoginCredentials::onPreconditionNotSatisfied() { - // MainWindow::applyStartupDestination runs this intent on every launch for - // users whose startup tab is Server / Server Room; keep that path quiet, as - // it was before the link-driven sign-in dialog existed. - if (!promptForMissingCredentials) { - emitFailed(tr("No saved credentials for this server")); - return; - } - - // No credentials saved for the target server: ask the user for them. They - // opt into saving them so later links to the same server connect directly. - const QString serverText = context->hostname + ":" + context->port; - DlgLoginPrompt dialog(serverText); - // ApplicationModal: the dialog has no parent (the intent is not a widget), - // so WindowModal would not actually block any other window. - dialog.setWindowModality(Qt::ApplicationModal); - - if (dialog.exec() != QDialog::Accepted) { - emitCancelled(); - return; - } - - context->username = dialog.username(); - context->password = dialog.password(); - - if (dialog.savePassword() && !context->username.isEmpty()) { - ServersSettings &servers = SettingsCache::instance().servers(); - // The host may already be saved under a friendly name (e.g. a public-server - // list entry) with no credentials; reuse that name instead of overwriting - // it with the raw hostname when addNewServer updates the entry in place. - QString saveName = context->hostname; - const int existingIndex = servers.findServerIndex(context->hostname, context->port); - if (existingIndex >= 0) { - saveName = - servers.getValue(QString("saveName%1").arg(existingIndex), "server", "server_details").toString(); - if (saveName.isEmpty()) { - saveName = context->hostname; - } - } - servers.addNewServer(saveName, context->hostname, context->port, context->username, context->password, true); - } - - emitFinished(); + emitFailed(tr("No saved credentials for this server")); } diff --git a/cockatrice/src/interface/intents/intent_login.h b/cockatrice/src/interface/intents/intent_login.h index 8ffd91a0a..c7fec92b7 100644 --- a/cockatrice/src/interface/intents/intent_login.h +++ b/cockatrice/src/interface/intents/intent_login.h @@ -9,10 +9,7 @@ class IntentGetLoginCredentials : public Intent Q_OBJECT public: - // When promptForMissingCredentials is false (the default) a server without - // saved credentials fails silently; only intent chains from cockatrice:// - // links opt into the interactive sign-in dialog. - explicit IntentGetLoginCredentials(ContextConnectToServer *_context, bool _promptForMissingCredentials = false); + IntentGetLoginCredentials(ContextConnectToServer *_context); protected: bool checkPrecondition() const override; @@ -21,7 +18,6 @@ protected: private: ContextConnectToServer *context; - bool promptForMissingCredentials; }; #endif // COCKATRICE_INTENT_LOGIN_H diff --git a/cockatrice/src/interface/intents/intent_open_server_room_by_name.cpp b/cockatrice/src/interface/intents/intent_open_server_room_by_name.cpp deleted file mode 100644 index d50f509a0..000000000 --- a/cockatrice/src/interface/intents/intent_open_server_room_by_name.cpp +++ /dev/null @@ -1,183 +0,0 @@ -#include "intent_open_server_room_by_name.h" - -#include "../widgets/tabs/tab_room.h" -#include "../widgets/tabs/tab_supervisor.h" -#include "intent_connect_to_server.h" - -#include -#include -#include -#include - -IntentOpenServerRoomByName::IntentOpenServerRoomByName(TabSupervisor *_tabSupervisor, - RemoteClient *_remoteClient, - std::unique_ptr _context, - const QString &_roomName) - : Intent(), tabSupervisor(_tabSupervisor), remoteClient(_remoteClient), context(_context.release()), - roomName(_roomName) -{ - checkTimer.setInterval(250); - connect(&checkTimer, &QTimer::timeout, this, [this]() { - if (selectOpenRoom()) { - checkTimer.stop(); - } - }); -} - -bool IntentOpenServerRoomByName::checkPrecondition() const -{ - if (remoteClient->getStatus() != ClientStatus::StatusLoggedIn) { - return false; - } - // peerPort() reflects the actual TCP peer, which may differ from the - // configured server port (e.g. when connecting through a proxy), so only - // the hostname is compared here. - if (remoteClient->peerName() != context->serverContext.hostname) { - return false; - } - if (QString::number(remoteClient->peerPort()) != context->serverContext.port) { - return false; - } - - return true; -} - -void IntentOpenServerRoomByName::onPreconditionSatisfied() -{ - if (listening) { - return; - } - listening = true; - - if (selectOpenRoom()) { - return; - } - - // The room selector is the component that requests the room list, so the - // server tab must exist for the room to be resolved by name. - if (!tabSupervisor->getTabServer()) { - tabSupervisor->openTabServer(); - } - if (!tabSupervisor->getTabServer()) { - emitFailed(tr("No server tab available")); - return; - } - - connect(remoteClient, &RemoteClient::listRoomsEventReceived, this, &IntentOpenServerRoomByName::processListRooms); - connect(remoteClient, &RemoteClient::statusChanged, this, &IntentOpenServerRoomByName::onClientStatusChanged); - - // The room tab may be opened by our own join, by the room selector's auto-join, or by a - // join that was already in flight. Poll until it shows up. - checkTimer.start(); - - // While no join has been sent yet, keep the room list fresh: the list may have been - // requested before we subscribed to it, or a response may have been dropped during a - // busy login burst. A stale list would otherwise leave the room unresolved forever. - connect(&refreshTimer, &QTimer::timeout, this, [this]() { - if (!joinPending) { - remoteClient->sendCommand(remoteClient->prepareSessionCommand(Command_ListRooms())); - } - }); - refreshTimer.setInterval(5000); - refreshTimer.start(); - - // Last-resort failure for "the room genuinely is not in a fresh list". This must NOT - // fire while a join is in flight: a loaded server may take longer than that to answer - // during a login burst, and killing the intent early would leave the connection - // registered in the room with no tab to display it and every later join attempt - // would then be rejected with RespContextError. - QTimer::singleShot(20000, this, [this]() { - if (!joinPending) { - emitFailed(tr("Timed out while looking for the server room %1").arg(roomName)); - } - }); -} - -void IntentOpenServerRoomByName::onPreconditionNotSatisfied() -{ - runDependency(new IntentConnectToServer(remoteClient, &context->serverContext)); -} - -void IntentOpenServerRoomByName::onClientStatusChanged(ClientStatus status) -{ - if (status != ClientStatus::StatusLoggedIn) { - emitFailed(tr("Disconnected while looking for the server room %1").arg(roomName)); - } -} - -bool IntentOpenServerRoomByName::selectOpenRoom() -{ - const auto &roomTabs = tabSupervisor->getRoomTabs(); - for (auto i = roomTabs.cbegin(), end = roomTabs.cend(); i != end; ++i) { - TabRoom *room = i.value(); - if (room->getRoomName() == roomName) { - tabSupervisor->setCurrentWidget(room); - emitFinished(); - return true; - } - } - return false; -} - -void IntentOpenServerRoomByName::processListRooms(const Event_ListRooms &event) -{ - if (selectOpenRoom()) { - return; - } - - for (int i = 0; i < event.room_list_size(); ++i) { - const ServerInfo_Room &room = event.room_list(i); - if (room.has_name() && QString::fromStdString(room.name()) == roomName) { - openRoom(room); - return; - } - } -} - -void IntentOpenServerRoomByName::openRoom(const ServerInfo_Room &roomInfo) -{ - if (joinPending) { - return; - } - joinPending = true; - - // Rooms flagged auto_join are joined by the room selector automatically. Sending our own - // Command_JoinRoom on top of that would be answered with RespContextError. - if (roomInfo.has_auto_join() && roomInfo.auto_join()) { - return; - } - - Command_JoinRoom cmd; - cmd.set_room_id(roomInfo.room_id()); - PendingCommand *pend = remoteClient->prepareSessionCommand(cmd); - connect(pend, &PendingCommand::finished, this, - [this](const Response &r, const CommandContainer &, const QVariant &) { handleJoinResponse(r); }); - remoteClient->sendCommand(pend); -} - -void IntentOpenServerRoomByName::handleJoinResponse(const Response &response) -{ - switch (response.response_code()) { - case Response::RespOk: { - const Response_JoinRoom &resp = response.GetExtension(Response_JoinRoom::ext); - if (!tabSupervisor->getRoomTabs().contains(resp.room_info().room_id())) { - tabSupervisor->addRoomTab(resp.room_info(), true); - } - emitFinished(); - return; - } - case Response::RespNameNotFound: - emitFailed(tr("Failed to join the server room %1: it doesn't exist on the server.").arg(roomName)); - return; - case Response::RespUserLevelTooLow: - emitFailed(tr("You do not have the required permission to join the server room %1.").arg(roomName)); - return; - case Response::RespContextError: - // The room was already joined by someone else (e.g. the room selector's - // auto-join). It will show up in the room tabs shortly, so keep waiting. - return; - default: - emitFailed(tr("Failed to join the server room %1 due to an unknown error.").arg(roomName)); - return; - } -} diff --git a/cockatrice/src/interface/intents/intent_open_server_room_by_name.h b/cockatrice/src/interface/intents/intent_open_server_room_by_name.h deleted file mode 100644 index 2f9e716af..000000000 --- a/cockatrice/src/interface/intents/intent_open_server_room_by_name.h +++ /dev/null @@ -1,61 +0,0 @@ -#ifndef COCKATRICE_INTENT_OPEN_SERVER_ROOM_BY_NAME_H -#define COCKATRICE_INTENT_OPEN_SERVER_ROOM_BY_NAME_H - -#include "contexts/context_join_room.h" -#include "intent.h" -#include "remote_client.h" - -#include -#include -#include -#include - -class TabRoom; -class TabSupervisor; -class Event_ListRooms; -class ServerInfo_Room; - -/** - * @brief Connects to the configured server and opens a room identified by its name. - * - * Room ids are assigned by the server per session, so the room is resolved by name from the - * room list once the client is logged in. If the room is already open it is simply selected. - * - * The join itself is sent directly through the client instead of `TabServer::joinRoom`, so a - * failed join only fails the intent silently instead of popping a modal error box during - * startup. Success is routed to `TabSupervisor::addRoomTab`, the same tab-creation machinery - * the normal join flow uses. - */ -class IntentOpenServerRoomByName : public Intent -{ - Q_OBJECT - -public: - IntentOpenServerRoomByName(TabSupervisor *_tabSupervisor, - RemoteClient *_remoteClient, - std::unique_ptr _context, - const QString &_roomName); - -protected: - bool checkPrecondition() const override; - void onPreconditionSatisfied() override; - void onPreconditionNotSatisfied() override; - -private: - void processListRooms(const Event_ListRooms &event); - void openRoom(const ServerInfo_Room &roomInfo); - void handleJoinResponse(const Response &response); - void onClientStatusChanged(ClientStatus status); - bool selectOpenRoom(); - - TabSupervisor *tabSupervisor; - RemoteClient *remoteClient; - QScopedPointer context; - QString roomName; - bool listening = false; - bool joinPending = false; - QTimer checkTimer; - QTimer refreshTimer; -}; - -#endif // COCKATRICE_INTENT_OPEN_SERVER_ROOM_BY_NAME_H diff --git a/cockatrice/src/interface/intents/intent_open_shared_deck.cpp b/cockatrice/src/interface/intents/intent_open_shared_deck.cpp deleted file mode 100644 index 016de63e6..000000000 --- a/cockatrice/src/interface/intents/intent_open_shared_deck.cpp +++ /dev/null @@ -1,208 +0,0 @@ -#include "intent_open_shared_deck.h" - -#include "../deck_loader/deck_loader.h" -#include "../widgets/dialogs/dlg_shared_decks_preview.h" -#include "../widgets/tabs/tab_supervisor.h" -#include "intent_connect_to_server.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -IntentOpenSharedDeck::IntentOpenSharedDeck(TabSupervisor *_tabSupervisor, - RemoteClient *_remoteClient, - const CardDatabaseQuerier *_querier, - std::unique_ptr _context) - : Intent(), tabSupervisor(_tabSupervisor), remoteClient(_remoteClient), querier(_querier), - context(_context.release()) -{ - downloadTimer = new QTimer(this); - downloadTimer->setSingleShot(true); - downloadTimer->setInterval(15000); - connect(downloadTimer, &QTimer::timeout, this, &IntentOpenSharedDeck::onDownloadTimeout); -} - -bool IntentOpenSharedDeck::checkPrecondition() const -{ - if (remoteClient->getStatus() != ClientStatus::StatusLoggedIn) { - return false; - } - // serverName()/serverPort() reflect the server the client was configured - // to connect to, which may differ from the actual TCP peer (e.g. when - // connecting through a proxy), so compare those configured values. The - // share token must be resolved against the host the link named — a link to - // the same host on another port is a different server. - if (remoteClient->serverName().compare(context->serverContext.hostname, Qt::CaseInsensitive) != 0) { - return false; - } - return QString::number(remoteClient->serverPort()) == context->serverContext.port; -} - -void IntentOpenSharedDeck::onPreconditionSatisfied() -{ - // Resolve the share token to its items first; a share can contain more than - // one deck, and each item is downloaded by id. Time the round trip like the - // downloads, so a silent server cannot hang the chain forever. - listPhase = true; - downloadTimer->start(); - - Command_DeckShareList cmd; - cmd.set_token(context->shareToken.toStdString()); - - PendingCommand *pend = AbstractClient::prepareSessionCommand(cmd); - connect(pend, &PendingCommand::finished, this, &IntentOpenSharedDeck::listShareFinished); - remoteClient->sendCommand(pend); -} - -void IntentOpenSharedDeck::onPreconditionNotSatisfied() -{ - runDependency(new IntentConnectToServer(remoteClient, &context->serverContext)); -} - -void IntentOpenSharedDeck::listShareFinished(const Response &response, const CommandContainer & /* commandContainer */) -{ - downloadTimer->stop(); - listPhase = false; - - if (response.response_code() != Response::RespOk) { - emitFailed(tr("The shared deck could not be found or has expired")); - return; - } - - const Response_DeckShareList &resp = response.GetExtension(Response_DeckShareList::ext); - if (resp.items_size() == 0) { - emitFailed(tr("The shared deck is empty")); - return; - } - - QList items; - items.reserve(resp.items_size()); - for (const ServerInfo_DeckShareItem &item : resp.items()) { - items.append(item); - itemNames.insert(item.id(), QString::fromStdString(item.name())); - } - - const QString serverText = context->serverContext.hostname + ":" + context->serverContext.port; - - // Ask the user which decks to open before downloading anything. - previewDialog = new DlgSharedDecksPreview(tabSupervisor, querier, QString::fromStdString(resp.name()), - resp.expires_at(), serverText, items); - connect(previewDialog, &DlgSharedDecksPreview::openRequested, this, &IntentOpenSharedDeck::startDownloads); - connect(previewDialog, &DlgSharedDecksPreview::cancelled, this, &IntentOpenSharedDeck::emitCancelled); - connect(previewDialog, &DlgSharedDecksPreview::cancelled, previewDialog, &QWidget::deleteLater); - previewDialog->show(); - previewDialog->raise(); - previewDialog->activateWindow(); -} - -void IntentOpenSharedDeck::startDownloads(const QList &itemIds) -{ - pendingItemIds = itemIds; - totalItems = itemIds.size(); - completedItems = 0; - loadedDecks.clear(); - downloadNextItem(); -} - -void IntentOpenSharedDeck::downloadNextItem() -{ - if (pendingItemIds.isEmpty()) { - finishAll(); - return; - } - - currentItemId = pendingItemIds.takeFirst(); - downloadTimer->start(); - - Command_DeckShareDownload cmd; - cmd.set_token(context->shareToken.toStdString()); - cmd.set_item_id(currentItemId); - - PendingCommand *pend = AbstractClient::prepareSessionCommand(cmd); - connect(pend, &PendingCommand::finished, this, &IntentOpenSharedDeck::downloadShareFinished); - remoteClient->sendCommand(pend); -} - -void IntentOpenSharedDeck::downloadShareFinished(const Response &response, - const CommandContainer & /* commandContainer */) -{ - downloadTimer->stop(); - - QString failureReason; - if (response.response_code() != Response::RespOk) { - failureReason = tr("Failed to download the shared deck"); - } else { - const Response_DeckShareDownload &resp = response.GetExtension(Response_DeckShareDownload::ext); - const QString deckString = QString::fromStdString(resp.deck()); - if (deckString.isEmpty()) { - failureReason = tr("The shared deck is empty"); - } else { - std::optional deckOpt = - DeckLoader::loadFromRemote(deckString, LoadedDeck::LoadInfo::NON_REMOTE_ID); - if (!deckOpt) { - failureReason = tr("The shared deck could not be loaded"); - } else { - loadedDecks.append(deckOpt.value()); - ++completedItems; - previewDialog->setDownloadProgress(completedItems, totalItems, - itemNames.value(currentItemId, tr("Unknown deck"))); - downloadNextItem(); - return; - } - } - } - - onItemFailure(failureReason); -} - -void IntentOpenSharedDeck::onItemFailure(const QString &reason) -{ - downloadTimer->stop(); - - if (loadedDecks.isEmpty()) { - previewDialog->deleteLater(); - emitFailed(reason); - return; - } - - const int downloadedCount = loadedDecks.size(); - const QMessageBox::StandardButton answer = QMessageBox::question( - previewDialog, tr("Open shared decks"), - tr("Could not download the deck \"%1\".\n\n%n deck(s) were already downloaded. Open them?", "", downloadedCount) - .arg(itemNames.value(currentItemId, tr("Unknown deck"))), - QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes); - - if (answer == QMessageBox::Yes) { - finishAll(); - } else { - previewDialog->deleteLater(); - emitCancelled(); - } -} - -void IntentOpenSharedDeck::onDownloadTimeout() -{ - // The list phase has no preview dialog yet to report progress into; fail the - // whole intent instead of letting the shared deck hang in limbo. - if (listPhase) { - emitFailed(tr("Timed out while loading the shared deck")); - return; - } - onItemFailure(tr("Timed out while downloading the shared deck")); -} - -void IntentOpenSharedDeck::finishAll() -{ - previewDialog->deleteLater(); - for (const LoadedDeck &deck : loadedDecks) { - tabSupervisor->openDeckInNewTab(deck); - } - emitFinished(); -} diff --git a/cockatrice/src/interface/intents/intent_open_shared_deck.h b/cockatrice/src/interface/intents/intent_open_shared_deck.h deleted file mode 100644 index 87812bdea..000000000 --- a/cockatrice/src/interface/intents/intent_open_shared_deck.h +++ /dev/null @@ -1,60 +0,0 @@ -#ifndef COCKATRICE_INTENT_OPEN_SHARED_DECK_H -#define COCKATRICE_INTENT_OPEN_SHARED_DECK_H - -#include "contexts/context_open_deck.h" -#include "intent.h" -#include "remote_client.h" - -#include -#include -#include -#include - -class TabSupervisor; -struct LoadedDeck; -class CardDatabaseQuerier; -class DlgSharedDecksPreview; -class QTimer; - -class IntentOpenSharedDeck : public Intent -{ - Q_OBJECT - -public: - IntentOpenSharedDeck(TabSupervisor *_tabSupervisor, - RemoteClient *_remoteClient, - const CardDatabaseQuerier *_querier, - std::unique_ptr _context); - -protected: - bool checkPrecondition() const override; - void onPreconditionSatisfied() override; - void onPreconditionNotSatisfied() override; - -private slots: - void listShareFinished(const Response &response, const CommandContainer &commandContainer); - void downloadShareFinished(const Response &response, const CommandContainer &commandContainer); - void onDownloadTimeout(); - -private: - void startDownloads(const QList &itemIds); - void downloadNextItem(); - void onItemFailure(const QString &reason); - void finishAll(); - - TabSupervisor *tabSupervisor; - RemoteClient *remoteClient; - const CardDatabaseQuerier *querier; - QScopedPointer context; - DlgSharedDecksPreview *previewDialog = nullptr; - QTimer *downloadTimer; - QMap itemNames; - QList pendingItemIds; - QList loadedDecks; - bool listPhase = true; - int currentItemId = 0; - int totalItems = 0; - int completedItems = 0; -}; - -#endif // COCKATRICE_INTENT_OPEN_SHARED_DECK_H diff --git a/cockatrice/src/interface/intents/url_parser.cpp b/cockatrice/src/interface/intents/url_parser.cpp index 707863354..8b5309603 100644 --- a/cockatrice/src/interface/intents/url_parser.cpp +++ b/cockatrice/src/interface/intents/url_parser.cpp @@ -1,28 +1,16 @@ #include "url_parser.h" -#include "../../client/settings/cache_settings.h" -#include "../widgets/tabs/tab_room.h" -#include "../widgets/tabs/tab_supervisor.h" #include "../window_main.h" #include "contexts/context_join_game.h" -#include "contexts/context_open_deck.h" -#include "intent.h" #include "intent_join_server_game.h" #include "intent_login.h" -#include "intent_open_shared_deck.h" #include -#include #include #include #include -#include -#include -#include #include -inline Q_LOGGING_CATEGORY(UrlParserLog, "url_parser"); - IntentUrlParser::IntentUrlParser(QObject *parent, MainWindow *_mainWindow) : QObject(parent), mainWindow(_mainWindow) { } @@ -38,33 +26,16 @@ void IntentUrlParser::handle(const QString &urlStr) const QString action = url.host(); QUrlQuery query(url); - qCDebug(UrlParserLog) << "Parsing intent URL, action:" << action; - - PendingIntentChain chain; - Intent *firstIntent = nullptr; if (action == "joingame") { - firstIntent = createJoinGameIntent(query, chain); + handleJoinGame(query); } else if (action == "opendeck") { - firstIntent = createOpenDeckIntent(query, chain); + // handleOpenDeck(query); } else { qWarning() << "Unknown intent:" << action; } - - if (firstIntent == nullptr) { - // The link was invalid or the user declined the confirm: nothing runs. - // Report the idle state when no other chain is queued so that a startup - // launch (which skipped its own connection for this URL) falls back to it. - if (!chainRunning && pendingChains.isEmpty()) { - emit urlChainFinished(mainWindow->getRemoteClient()->getStatus() == StatusLoggedIn); - } - return; - } - - pendingChains.append(chain); - startNextChain(); } -Intent *IntentUrlParser::createJoinGameIntent(const QUrlQuery &query, PendingIntentChain &chain) +void IntentUrlParser::handleJoinGame(const QUrlQuery &query) { auto showError = [this](const QString &message) { QMessageBox::warning(mainWindow, tr("Open game"), message); }; @@ -75,21 +46,21 @@ Intent *IntentUrlParser::createJoinGameIntent(const QUrlQuery &query, PendingInt if (ctx->roomContext.serverContext.hostname.isEmpty()) { showError(tr("Missing or empty hostname in the game link")); - return nullptr; + return; } bool ok = false; ctx->roomContext.serverContext.port.toUShort(&ok); if (!ok) { showError(tr("Invalid or missing port in the game link")); - return nullptr; + return; } ctx->roomContext.roomId = query.queryItemValue("roomid").toInt(&ok); if (!ok) { showError(tr("Invalid or missing room id in the game link")); - return nullptr; + return; } ok = false; @@ -97,342 +68,22 @@ Intent *IntentUrlParser::createJoinGameIntent(const QUrlQuery &query, PendingInt if (!ok) { showError(tr("Invalid or missing game id in the game link")); - return nullptr; + return; } - const QString gameDescription = query.queryItemValue("game", QUrl::FullyDecoded); - const QString message = generateJoinGameMessage(*ctx, gameDescription); - - const QMessageBox::StandardButton answer = QMessageBox::question( - mainWindow, tr("Join game"), message, QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes); - if (answer != QMessageBox::Yes) { - return nullptr; - } - - RemoteClient *client = mainWindow->getRemoteClient(); - ContextConnectToServer *serverContext = &ctx->roomContext.serverContext; - // The join game intent owns the context and the credential lookup; once the // chain finishes (or fails) it deletes the whole tree. - auto joinGameIntent = new IntentJoinServerGame(mainWindow->getTabSupervisor(), client, std::move(ctx)); + ContextConnectToServer *serverContext = &ctx->roomContext.serverContext; + auto joinGameIntent = + new IntentJoinServerGame(mainWindow->getTabSupervisor(), mainWindow->getRemoteClient(), std::move(ctx)); joinGameIntent->setParent(this); - chain.intents.append(joinGameIntent); + + auto getLoginCredentialsIntent = new IntentGetLoginCredentials(serverContext); + getLoginCredentialsIntent->setParent(joinGameIntent); + + connect(getLoginCredentialsIntent, &Intent::finished, joinGameIntent, &Intent::execute); + connect(getLoginCredentialsIntent, &Intent::failed, joinGameIntent, &Intent::failed); connect(joinGameIntent, &Intent::failed, this, [showError](const QString &reason) { showError(reason); }); - Intent *firstIntent = joinGameIntent; - if (!isConnectedTo(serverContext->hostname, serverContext->port)) { - auto getLoginCredentialsIntent = - new IntentGetLoginCredentials(serverContext, /*promptForMissingCredentials=*/true); - getLoginCredentialsIntent->setParent(joinGameIntent); - chain.intents.insert(0, getLoginCredentialsIntent); - - connect(getLoginCredentialsIntent, &Intent::finished, joinGameIntent, &Intent::execute); - connect(getLoginCredentialsIntent, &Intent::failed, joinGameIntent, &Intent::failed); - connect(getLoginCredentialsIntent, &Intent::cancelled, joinGameIntent, &Intent::cancelled); - firstIntent = getLoginCredentialsIntent; - } - - return firstIntent; -} - -QString IntentUrlParser::generateJoinGameMessage(const ContextJoinGame &context, const QString &gameDescription) -{ - const QString hostname = context.roomContext.serverContext.hostname; - const QString port = context.roomContext.serverContext.port; - const int roomId = context.roomContext.roomId; - const int gameId = context.gameId; - const QString server = QStringLiteral("%1:%2").arg(hostname, port); - - // Prefer the room name over the raw numeric id: it means something to the - // user. The name is only known when we are already connected to the same - // server and sitting in that room — otherwise fall back to a plain prompt. - AbstractClient *client = mainWindow->getTabSupervisor()->getClient(); - const bool sameServer = client != nullptr && client->getStatus() == StatusLoggedIn && - hostname.compare(client->serverName(), Qt::CaseInsensitive) == 0 && - QString::number(client->serverPort()) == port; - TabRoom *roomTab = sameServer ? mainWindow->getTabSupervisor()->getRoomTabs().value(roomId) : nullptr; - - const QString gameIdStr = QString::number(gameId); - // Links built by newer clients embed the game description ("game" item); - // restate it in the confirm so it matches what the chat anchor showed. - // Unknown query items are ignored, so old links without it keep working. - // The multi-arg .arg() overloads replace in a single pass, so a description - // containing "%…" cannot corrupt later placeholders. - // FullyDecoded undoes every %XX escape and must match the chat anchor's - // decode mode, so a description containing "%" reads identically in both. - if (gameDescription.isEmpty()) { - return roomTab ? tr("Join game #%1 in \"%2\" on %3?").arg(gameIdStr, roomTab->getRoomName(), server) - : tr("Join game #%1 on %2?").arg(gameIdStr, server); - } - - return roomTab ? tr("Join game \"%1\" (#%2) in \"%3\" on %4?") - .arg(gameDescription, gameIdStr, roomTab->getRoomName(), server) - : tr("Join game \"%1\" (#%2) on %3?").arg(gameDescription, gameIdStr, server); -} - -Intent *IntentUrlParser::createOpenDeckIntent(const QUrlQuery &query, PendingIntentChain &chain) -{ - auto showError = [this](const QString &message) { - QMessageBox::warning(mainWindow, tr("Open shared deck"), message); - }; - - auto ctx = std::make_unique(); - - ctx->serverContext.hostname = query.queryItemValue("hostname"); - ctx->serverContext.port = query.queryItemValue("port"); - ctx->shareToken = query.queryItemValue("share"); - - qCDebug(UrlParserLog) << "Open-deck intent: host" << ctx->serverContext.hostname << "port" - << ctx->serverContext.port << "token length" << ctx->shareToken.length(); - - if (ctx->serverContext.hostname.isEmpty()) { - showError(tr("Missing or empty hostname in the share link")); - return nullptr; - } - - bool ok = false; - const quint16 port = ctx->serverContext.port.toUShort(&ok); - if (!ok || port == 0) { - showError(tr("Invalid or missing port in the share link")); - return nullptr; - } - - if (ctx->shareToken.isEmpty()) { - showError(tr("Missing or empty share value in the share link")); - return nullptr; - } - - RemoteClient *client = mainWindow->getRemoteClient(); - - // The open deck download needs a connection to the link's server. Ask before - // taking the session anywhere it isn't already, naming the host we would - // connect to. Remember the link's target when it moves us away from a live - // session so a failed or cancelled chain can restore the session it left. - // The hostname is link-supplied and percent-decoded, so escape it: QMessageBox - // renders AutoText, and markup in a hostname would otherwise flip the whole - // prompt to rich text and let a link pad the message the user is shown. - const bool alreadyConnected = isConnectedTo(ctx->serverContext.hostname, ctx->serverContext.port); - if (!alreadyConnected) { - const QString target = - QStringLiteral("%1:%2").arg(ctx->serverContext.hostname.toHtmlEscaped(), ctx->serverContext.port); - - if (client->getStatus() == StatusLoggedIn) { - const QString current = - QStringLiteral("%1:%2").arg(client->serverName(), QString::number(client->serverPort())); - const QMessageBox::StandardButton answer = QMessageBox::question( - mainWindow, tr("Open shared deck"), - tr("Opening this share link connects you to %1 instead of %2.\n\nContinue?").arg(target, current), - QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes); - if (answer != QMessageBox::Yes) { - return nullptr; - } - chain.migrationTargetHost = ctx->serverContext.hostname; - chain.migrationTargetPort = ctx->serverContext.port; - chain.pendingRestore = true; - } else { - // Fresh connection is harmless to wander away from, but a server the - // client has never been configured for deserves a harder warning (no - // by default) so a stray link cannot silently steer the client there. - const bool knownHost = SettingsCache::instance().servers().findHostIndex(ctx->serverContext.hostname) >= 0; - const QMessageBox::StandardButton answer = - knownHost - ? QMessageBox::question(mainWindow, tr("Open shared deck"), - tr("Opening this share link connects you to %1.\n\nContinue?").arg(target)) - : QMessageBox::warning(mainWindow, tr("Open shared deck"), - tr("Opening this share link connects you to %1, a server you have " - "never connected to before.\n\nContinue?") - .arg(target), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - if (answer != QMessageBox::Yes) { - return nullptr; - } - } - } - - ContextConnectToServer *serverContext = &ctx->serverContext; - - // The open deck intent owns the context and the credential lookup; once - // the chain finishes (or fails) it deletes the whole tree. - auto openDeckIntent = - new IntentOpenSharedDeck(mainWindow->getTabSupervisor(), client, CardDatabaseManager::query(), std::move(ctx)); - openDeckIntent->setParent(this); - chain.intents.append(openDeckIntent); - connect(openDeckIntent, &Intent::failed, this, [showError](const QString &reason) { showError(reason); }); - - Intent *firstIntent = openDeckIntent; - if (!isConnectedTo(serverContext->hostname, serverContext->port)) { - auto getLoginCredentialsIntent = - new IntentGetLoginCredentials(serverContext, /*promptForMissingCredentials=*/true); - getLoginCredentialsIntent->setParent(openDeckIntent); - chain.intents.insert(0, getLoginCredentialsIntent); - - connect(getLoginCredentialsIntent, &Intent::finished, openDeckIntent, &Intent::execute); - connect(getLoginCredentialsIntent, &Intent::failed, openDeckIntent, &Intent::failed); - connect(getLoginCredentialsIntent, &Intent::cancelled, openDeckIntent, &Intent::cancelled); - firstIntent = getLoginCredentialsIntent; - } - - return firstIntent; -} - -bool IntentUrlParser::isConnectedTo(const QString &hostname, const QString &port) const -{ - // serverName() reflects the server the client was configured to connect to, - // which may differ from the actual TCP peer (e.g. when connecting through a - // proxy), so compare the configured host and port — exactly what a link - // names. A link to the same host on another port is a different server and - // must not silently reuse an existing session there. - RemoteClient *client = mainWindow->getRemoteClient(); - return client->getStatus() == StatusLoggedIn && client->serverName().compare(hostname, Qt::CaseInsensitive) == 0 && - QString::number(client->serverPort()) == port; -} - -void IntentUrlParser::startNextChain() -{ - if (chainRunning || pendingChains.isEmpty()) { - return; - } - chainRunning = true; - - PendingIntentChain &chain = pendingChains.first(); - if (chain.intents.isEmpty()) { - pendingChains.removeFirst(); - chainRunning = false; - startNextChain(); - return; - } - - // Snapshot the session this chain moves away from now that it actually - // runs. Chains are parsed while earlier ones are still queued, so a capture - // at parse time would follow whichever server the chain before it settled - // on, not the one the user is really on when this link is handled. - if (chain.pendingRestore) { - RemoteClient *client = mainWindow->getRemoteClient(); - chain.previousServerHost = client->serverName(); - chain.previousServerPort = QString::number(client->serverPort()); - } - - // Only the last intent completes the chain; its terminal signal ends the - // whole run. Cancellation of an intermediate intent (e.g. declined login - // prompt) is forwarded onto the last intent in the chain builders above. - Intent *finalIntent = chain.intents.last(); - connect(finalIntent, &Intent::finished, this, [this]() { chainEnded(true); }); - connect(finalIntent, &Intent::failed, this, [this]() { chainEnded(false); }); - connect(finalIntent, &Intent::cancelled, this, [this]() { chainEnded(false); }); - // Backstop: if the final intent is destroyed without emitting a terminal - // signal (e.g. a network error dropped it while running), end the chain so - // later links are not queued and dropped for the rest of the session. - chainBackstopConnection = connect(finalIntent, &QObject::destroyed, this, &IntentUrlParser::onChainIntentDestroyed); - - chain.intents.first()->execute(); -} - -void IntentUrlParser::chainEnded(bool chainSucceeded) -{ - chainRunning = false; - QObject::disconnect(chainBackstopConnection); - - const PendingIntentChain chain = pendingChains.takeFirst(); - - // Only a failed or cancelled chain restores the session the link migrated - // away from; a successful one leaves the user where they are. - if (chain.pendingRestore && !chainSucceeded) { - restorePreviousServer(chain); - } - - startNextChain(); - - // Only report the terminal state once the queue has fully drained, so a - // queued follow-up link keeps the startup fallback out of the picture. - if (!chainRunning && pendingChains.isEmpty()) { - emit urlChainFinished(mainWindow->getRemoteClient()->getStatus() == StatusLoggedIn); - } -} - -void IntentUrlParser::onChainIntentDestroyed() -{ - if (!chainRunning) { - return; - } - qCWarning(UrlParserLog) << "Share-link intent destroyed without a terminal signal; ending its chain"; - chainEnded(false); -} - -void IntentUrlParser::restorePreviousServer(const PendingIntentChain &chain) -{ - if (chain.previousServerHost.isEmpty()) { - return; - } - - RemoteClient *client = mainWindow->getRemoteClient(); - const ClientStatus status = client->getStatus(); - - // A failed/cancelled chain can fire while the client is still settling the - // in-flight connection attempt (wrong password, connect timeout). Only - // decide once the client has settled into logged-in or disconnected; - // deciding mid-connect would strand the user offline from their previous - // server. - if (status == StatusDisconnected || status == StatusLoggedIn) { - restoreToPreviousServer(chain); - return; - } - auto waitConnection = std::make_shared(); - *waitConnection = connect(client, &RemoteClient::statusChanged, this, [this, chain, client, waitConnection]() { - const ClientStatus settled = client->getStatus(); - if (settled == StatusDisconnected || settled == StatusLoggedIn) { - QObject::disconnect(*waitConnection); - restoreToPreviousServer(chain); - } - }); -} - -void IntentUrlParser::restoreToPreviousServer(const PendingIntentChain &chain) -{ - RemoteClient *client = mainWindow->getRemoteClient(); - - // Back on the previous server already → nothing to undo. - if (client->serverName().compare(chain.previousServerHost, Qt::CaseInsensitive) == 0 && - QString::number(client->serverPort()) == chain.previousServerPort) { - return; - } - - // When logged in somewhere, only intervene if that somewhere is the server - // the link moved us to; if the user went elsewhere on their own, leave them. - if (client->getStatus() == StatusLoggedIn) { - const bool onMigrationTarget = - client->serverName().compare(chain.migrationTargetHost, Qt::CaseInsensitive) == 0 && - QString::number(client->serverPort()) == chain.migrationTargetPort; - if (!onMigrationTarget) { - return; - } - - ServersSettings &servers = SettingsCache::instance().servers(); - const int index = servers.findServerIndex(chain.previousServerHost, chain.previousServerPort); - if (index >= 0 && servers.hasLoginData(chain.previousServerHost, chain.previousServerPort)) { - const QString username = - servers.getValue(QString("username%1").arg(index), "server", "server_details").toString(); - const QString password = - servers.getValue(QString("password%1").arg(index), "server", "server_details").toString(); - client->connectToServer(chain.previousServerHost, chain.previousServerPort.toUInt(), username, password); - return; - } - client->disconnectFromServer(); - return; - } - - if (client->getStatus() != StatusDisconnected) { - return; - } - - // The link's connection attempt failed: reconnect to the previous server - // when credentials are saved, otherwise stay offline. - ServersSettings &servers = SettingsCache::instance().servers(); - const int index = servers.findServerIndex(chain.previousServerHost, chain.previousServerPort); - if (index >= 0 && servers.hasLoginData(chain.previousServerHost, chain.previousServerPort)) { - const QString username = - servers.getValue(QString("username%1").arg(index), "server", "server_details").toString(); - const QString password = - servers.getValue(QString("password%1").arg(index), "server", "server_details").toString(); - client->connectToServer(chain.previousServerHost, chain.previousServerPort.toUInt(), username, password); - } + getLoginCredentialsIntent->execute(); } diff --git a/cockatrice/src/interface/intents/url_parser.h b/cockatrice/src/interface/intents/url_parser.h index ea29fed53..bac0e3d25 100644 --- a/cockatrice/src/interface/intents/url_parser.h +++ b/cockatrice/src/interface/intents/url_parser.h @@ -1,46 +1,9 @@ #ifndef COCKATRICE_URL_PARSER_H #define COCKATRICE_URL_PARSER_H - -#include #include #include -class Intent; class MainWindow; -struct ContextJoinGame; - -/** - * @brief One queued intent chain with the session-migration bookkeeping for it. - * - * The restore fields are per-chain on purpose: chains are parsed while earlier - * ones are still queued, so parser-wide state would let one chain's failure - * consume the restore data another chain recorded. - */ -struct PendingIntentChain -{ - QList intents; - - // Snapshot of the session in place when this chain started running, so a - // queued chain follows whichever server the chain before it settled on. - QString previousServerHost; - QString previousServerPort; - - // Recorded at parse time when the user confirmed migrating away from a live - // session to the host/port named by the link. - QString migrationTargetHost; - QString migrationTargetPort; - bool pendingRestore = false; -}; - -/** - * @brief Parses cockatrice:// links and runs them as serialized intent chains. - * - * Links are parsed by action (joingame/opendeck) and translated into an intent - * chain. Chains are queued and run one at a time: a document can hand multiple - * links to the window while an earlier chain still connects, and running two - * connect chains concurrently tears the connection down. urlChainFinished is - * emitted once the queue has fully drained. - */ class IntentUrlParser : public QObject { Q_OBJECT @@ -48,28 +11,10 @@ class IntentUrlParser : public QObject public: IntentUrlParser(QObject *parent, MainWindow *mainWindow); void handle(const QString &urlStr); - -signals: - /** @brief Emitted when the last queued chain ended; carries whether the client is logged in. */ - void urlChainFinished(bool connected); + void handleJoinGame(const QUrlQuery &query); private: - Intent *createJoinGameIntent(const QUrlQuery &query, PendingIntentChain &chain); - Intent *createOpenDeckIntent(const QUrlQuery &query, PendingIntentChain &chain); - QString generateJoinGameMessage(const ContextJoinGame &context, const QString &gameDescription); - [[nodiscard]] bool isConnectedTo(const QString &hostname, const QString &port) const; - void startNextChain(); - void chainEnded(bool chainSucceeded); - void onChainIntentDestroyed(); - void restorePreviousServer(const PendingIntentChain &chain); - void restoreToPreviousServer(const PendingIntentChain &chain); - MainWindow *mainWindow; - QList pendingChains; - bool chainRunning = false; - // Disconnects the destroyed-signal backstop once a chain ends, so an old - // intent's deferred deletion cannot end the chain that runs after it. - QMetaObject::Connection chainBackstopConnection; }; #endif // COCKATRICE_URL_PARSER_H diff --git a/cockatrice/src/interface/palette_editor/palette_editor_dialog.cpp b/cockatrice/src/interface/palette_editor/palette_editor_dialog.cpp index d5a168708..adae6e152 100644 --- a/cockatrice/src/interface/palette_editor/palette_editor_dialog.cpp +++ b/cockatrice/src/interface/palette_editor/palette_editor_dialog.cpp @@ -1,5 +1,6 @@ #include "palette_editor_dialog.h" +#include "../../client/settings/cache_settings.h" #include "../theme_manager.h" #include "palette_generator.h" #include "palette_grid_widget.h" @@ -10,11 +11,31 @@ #include #include #include +#include #include +#include #include +#include #include #include +#include #include +#include + +// Probe whether a directory is truly writable by trying to create and remove a +// temporary file. QFileInfo::isWritable() on a directory is unreliable (notably +// on Windows where UAC VirtualStore can make a system dir appear writable). +static bool isDirReallyWritable(const QString &dirPath) +{ + const QString probe = QDir(dirPath).absoluteFilePath(".cockatrice_write_test"); + QFile f(probe); + if (!f.open(QIODevice::WriteOnly)) { + return false; + } + f.close(); + f.remove(); + return true; +} PaletteEditorDialog::PaletteEditorDialog(const QString &_themeDirPath, const QString &_themeName, QWidget *parent) : QDialog(parent), themeDirPath(_themeDirPath), themeName(_themeName) @@ -25,7 +46,14 @@ PaletteEditorDialog::PaletteEditorDialog(const QString &_themeDirPath, const QSt // Resolve a writable directory for saving. Built-in (Default / Fusion) and // other read-only theme directories must be customised in the user-writable // themes directory; otherwise the write would fail or be lost on upgrade. - saveDir = ThemeManager::writableThemeDir(themeName); + if (!themeDirPath.isEmpty() && isDirReallyWritable(themeDirPath)) { + saveDir = themeDirPath; + } else { + saveDir = QDir(SettingsCache::instance().paths().getThemesPath()).absoluteFilePath(themeName); + if (!QDir().mkpath(saveDir)) { + qWarning() << "Failed to create palette save directory:" << saveDir; + } + } // Load both scheme configs upfront so switching is instant loadSchemes(); @@ -186,7 +214,7 @@ void PaletteEditorDialog::retranslateUi() resetBtn->setToolTip(tr("Discard unsaved edits and restore the last saved palette")); saveBtn->setToolTip(tr("Write palette-%1.toml and reload the theme").arg(loadedScheme.toLower())); - if (saveDir.isEmpty() || !ThemeManager::isDirReallyWritable(saveDir)) { + if (saveDir.isEmpty() || !isDirReallyWritable(saveDir)) { saveBtn->setEnabled(false); saveBtn->setToolTip(tr("Cannot save: this theme has no writable directory")); } @@ -262,35 +290,17 @@ void PaletteEditorDialog::onSave() // Persist every scheme that changed, not just the one on screen. Each scheme // has its own file, so edits to the non-active scheme would otherwise be // silently discarded when the dialog closes. - // - // Save the loaded scheme last so commitPalette's global colour-scheme - // update (ThemeConfig::colorScheme) points at the active scheme. for (auto it = workingConfig.begin(); it != workingConfig.end(); ++it) { - if (it.key() == loadedScheme) { - continue; + const QString &scheme = it.key(); + if (it.value().colors == savedConfig.value(scheme).colors) { + continue; // unchanged — leave the on-disk file alone } - if (it.value() == savedConfig.value(it.key())) { - continue; - } - if (!ThemeManager::commitPalette(saveDir, it.key(), it.value())) { - QMessageBox::warning(this, tr("Save failed"), - tr("Could not write %1 to:\n%2").arg(PaletteConfig::fileName(it.key()), saveDir)); - return; - } - } - // Commit the active scheme last so the global colour scheme matches. - if (workingConfig[loadedScheme] != savedConfig.value(loadedScheme)) { - if (!ThemeManager::commitPalette(saveDir, loadedScheme, workingConfig[loadedScheme])) { + if (!ThemeManager::savePaletteConfig(saveDir, scheme, it.value())) { QMessageBox::warning(this, tr("Save failed"), - tr("Could not write %1 to:\n%2").arg(PaletteConfig::fileName(loadedScheme), saveDir)); + tr("Could not write %1 to:\n%2").arg(PaletteConfig::fileName(scheme), saveDir)); return; } - } else { - // No palette change but scheme may have switched -- still update global config. - ThemeConfig globalCfg = ThemeConfig::fromThemeDir(saveDir); - globalCfg.colorScheme = loadedScheme; - globalCfg.save(saveDir); } // Keep the saved snapshot in sync so Reset behaves correctly afterwards. @@ -298,6 +308,10 @@ void PaletteEditorDialog::onSave() savedConfig[it.key()] = it.value(); } + ThemeConfig globalCfg = ThemeConfig::fromThemeDir(saveDir); + globalCfg.colorScheme = loadedScheme; + globalCfg.save(saveDir); + themeManager->reloadCurrentTheme(); accept(); } diff --git a/cockatrice/src/interface/palette_editor/palette_generator.cpp b/cockatrice/src/interface/palette_editor/palette_generator.cpp index 822e57250..d30dd14f1 100644 --- a/cockatrice/src/interface/palette_editor/palette_generator.cpp +++ b/cockatrice/src/interface/palette_editor/palette_generator.cpp @@ -150,17 +150,6 @@ PaletteConfig fromAccent(const QColor &accent, int intensity, const QString &sch cfg.colors[CG::Disabled][CR::HighlightedText] = disText; cfg.colors[CG::Inactive][CR::HighlightedText] = dark ? Qt::white : Qt::black; - // Accent: same primary hue as Highlight, so palettes derived from a - // QuickSetup accent always carry a matching Accent role. -#if QT_VERSION >= QT_VERSION_CHECK(6, 6, 0) - set3(CR::Accent, hl, disText, hl); -#endif - - // Application role colors: Strong tracks the primary accent, while Soft is - // the lightened, desaturated companion used for button-gradient highlights. - cfg.appColors[AppColor::AccentStrong] = hl; - cfg.appColors[AppColor::AccentSoft] = hsl(accent.lightness() + 60, qRound(accent.hslSaturation() * 70 / 100.0)); - // BrightText QColor bright; if (achromatic) { diff --git a/cockatrice/src/interface/palette_editor/palette_grid_widget.cpp b/cockatrice/src/interface/palette_editor/palette_grid_widget.cpp index 97d28b731..67294cd98 100644 --- a/cockatrice/src/interface/palette_editor/palette_grid_widget.cpp +++ b/cockatrice/src/interface/palette_editor/palette_grid_widget.cpp @@ -1,7 +1,5 @@ #include "palette_grid_widget.h" -#include "../theme_manager.h" - #include #include #include @@ -47,11 +45,6 @@ static const QMap ROLE_DESCRIPTIONS = { {QPalette::Shadow, QT_TR_NOOP("Very dark shadow colour")}, }; -static const QMap APP_ROLE_DESCRIPTIONS = { - {AppColor::AccentStrong, QT_TR_NOOP("Vivid primary accent (e.g. home-tab button gradient start)")}, - {AppColor::AccentSoft, QT_TR_NOOP("Lightened, desaturated accent (e.g. home-tab button gradient end)")}, -}; - PaletteGridWidget::PaletteGridWidget(QWidget *parent) : QWidget(parent) { scroll = new QScrollArea(this); @@ -129,46 +122,6 @@ void PaletteGridWidget::buildGrid(QWidget *host) grid->addWidget(btn, row + 1, col + 1, Qt::AlignHCenter | Qt::AlignVCenter); } } - - // Application color section: one ColorButton per role below the role grid. - // These are not tied to a color group, so a single button spans the row. - QMetaEnum appEnum = QMetaEnum::fromType(); - - const int appHeaderRow = roles.size() + 1; - - auto *appHeader = new QLabel(tr("App colors"), host); - appHeader->setToolTip(tr("Application-specific colors layered on top of the Qt palette")); - QFont appHeaderFont = appHeader->font(); - appHeaderFont.setBold(true); - appHeader->setFont(appHeaderFont); - appHeader->setAutoFillBackground(true); - appHeader->setContentsMargins(4, 4, 4, 4); - grid->addWidget(appHeader, appHeaderRow, 0, 1, 4); - headerLabels.append(appHeader); - - for (int i = 0; i < appEnum.keyCount(); ++i) { - auto role = static_cast(appEnum.value(i)); - const int row = appHeaderRow + 1 + i; - - if (i % 2 == 0) { - for (int col = 0; col < 4; ++col) { - auto *shade = new QWidget(host); - shade->setAutoFillBackground(true); - grid->addWidget(shade, row, col); - rowShadeWidgets.push_back(shade); - } - } - - auto *label = new QLabel(QString(appEnum.valueToKey(role)), host); - label->setToolTip(APP_ROLE_DESCRIPTIONS.value(role, {})); - label->setContentsMargins(4, 2, 8, 2); - grid->addWidget(label, row, 0); - - auto *btn = new ColorButton(host); - connect(btn, &ColorButton::colorChanged, this, [this] { emit paletteChanged(); }); - appColorButtons[role] = btn; - grid->addWidget(btn, row, 1, Qt::AlignHCenter | Qt::AlignVCenter); - } } void PaletteGridWidget::changeEvent(QEvent *e) @@ -213,16 +166,6 @@ void PaletteGridWidget::loadPalette(const PaletteConfig &cfg) colorButtons[group][role]->setColor(color); } } - - QMetaEnum appEnum = QMetaEnum::fromType(); - for (int i = 0; i < appEnum.keyCount(); ++i) { - auto role = static_cast(appEnum.value(i)); - QColor color = cfg.appColors.value(role); - if (!color.isValid()) { - color = themeManager->appColor(role); - } - appColorButtons[role]->setColor(color); - } } PaletteConfig PaletteGridWidget::currentPaletteConfig() const @@ -233,12 +176,5 @@ PaletteConfig PaletteGridWidget::currentPaletteConfig() const cfg.colors[group][role] = colorButtons[group][role]->getColor(); } } - - QMetaEnum appEnum = QMetaEnum::fromType(); - for (int i = 0; i < appEnum.keyCount(); ++i) { - auto role = static_cast(appEnum.value(i)); - cfg.appColors[role] = appColorButtons[role]->getColor(); - } - return cfg; } \ No newline at end of file diff --git a/cockatrice/src/interface/palette_editor/palette_grid_widget.h b/cockatrice/src/interface/palette_editor/palette_grid_widget.h index 77cbf1c62..1a665971a 100644 --- a/cockatrice/src/interface/palette_editor/palette_grid_widget.h +++ b/cockatrice/src/interface/palette_editor/palette_grid_widget.h @@ -31,7 +31,6 @@ private: void refreshChromePalettes(); QMap> colorButtons; - QMap appColorButtons; QScrollArea *scroll; QWidget *gridHost; QVBoxLayout *layout; diff --git a/cockatrice/src/interface/pixel_map_generator.cpp b/cockatrice/src/interface/pixel_map_generator.cpp index b70dc576f..d3b0252a6 100644 --- a/cockatrice/src/interface/pixel_map_generator.cpp +++ b/cockatrice/src/interface/pixel_map_generator.cpp @@ -1,11 +1,8 @@ #include "pixel_map_generator.h" -#include "theme_manager.h" - #include #include #include -#include #include #include #include @@ -17,32 +14,6 @@ #define DEFAULT_COLOR_MODERATOR_RIGHT "#000000"; #define DEFAULT_COLOR_ADMIN "#ff2701"; -/** - * Clamps an svg render size so that rendering does not exceed a multiple of the requested size. - * - * Rendering at the full native size of an svg just to scale it down afterwards wastes memory, - * and canvases with extreme coordinates can exceed Qt's rasterizer coordinate limit which makes - * Qt silently drop shapes from the rendered image. - * - * @param renderSize The size the svg would be rendered at. - * @param requestedSize The size that was actually requested. - * - * @return A size with the aspect ratio of renderSize whose longest side is at most four times - * the longest side of requestedSize. - */ -static QSize capRenderSize(const QSize &renderSize, const QSize &requestedSize) -{ - const int longestRequestedSide = qMax(requestedSize.width(), requestedSize.height()); - if (longestRequestedSide <= 0) { - return renderSize; - } - - const int longestRenderSide = qMax(renderSize.width(), renderSize.height()); - const qreal scale = qMin(1.0, static_cast(longestRequestedSide * 4) / longestRenderSide); - return QSize(qMax(1, static_cast(renderSize.width() * scale)), - qMax(1, static_cast(renderSize.height() * scale))); -} - /** * Loads in an svg from file and scales it without affecting image quality. * @@ -64,9 +35,6 @@ static QPixmap loadSvg(const QString &svgPath, const QSize &size, bool expandOnl // If expandOnly, make sure the pixmap is at least as large as the svg, so that we don't lose any detail. // QIcon.pixmap(size) will automatically scale down the image, but it won't scale it up. QSize pixmapSize = expandOnly ? svgRenderer.defaultSize().expandedTo(size) : size; - if (expandOnly) { - pixmapSize = capRenderSize(pixmapSize, size); - } QPixmap pix(pixmapSize); pix.fill(Qt::transparent); @@ -84,13 +52,7 @@ static QPixmap loadSvg(const QString &svgPath, const QSize &size, bool expandOnl /** * Try to load path image from non-SVG formats, otherwise fall back to SVG. * This is to allow custom themes to support non-SVG format type overrides, since SVG requires custom loading. - * - * The path may already carry the resolved file extension (e.g. via - * ThemeManager::assetPath); such paths are loaded directly. Otherwise a - * format-agnostic lookup probes png, jpg and finally svg. - * - * @param path The path to the file, with no file extension unless the caller - * already resolved it. File formats will be automatically detected. + * @param path The path to the file, with no file extension. File formats will be automatically detected. * @param size The desired size of the pixmap. * @param expandOnly If true, then keep the size of the initial pixmap to at least the size (Only relevant if SVG). * @@ -98,19 +60,6 @@ static QPixmap loadSvg(const QString &svgPath, const QSize &size, bool expandOnl */ static QPixmap tryLoadImage(const QString &path, const QSize &size, bool expandOnly = false) { - if (path.endsWith(QLatin1String(".svg"), Qt::CaseInsensitive)) { - return loadSvg(path, size, expandOnly); - } - if (path.endsWith(QLatin1String(".png"), Qt::CaseInsensitive) || - path.endsWith(QLatin1String(".jpg"), Qt::CaseInsensitive) || - path.endsWith(QLatin1String(".jpeg"), Qt::CaseInsensitive)) { - QPixmap pix(path); - if (!pix.isNull()) { - return pix.scaled(size, Qt::KeepAspectRatio, Qt::SmoothTransformation); - } - return {}; - } - const auto formats = {"png", "jpg"}; QPixmap returnPixmap; @@ -132,8 +81,7 @@ QPixmap PhasePixmapGenerator::generatePixmap(int height, QString name) return pmCache.value(key); } - QPixmap pixmap = tryLoadImage(QStringLiteral("theme:") + themeManager->assetPath(QStringLiteral("phases/") + name), - QSize(height, height)); + QPixmap pixmap = tryLoadImage("theme:phases/" + name, QSize(height, height)); pmCache.insert(key, pixmap); return pixmap; @@ -299,9 +247,7 @@ static QIcon loadAndColorSvg(const QString &iconPath, QSvgRenderer svgRenderer(doc.toByteArray()); - const QSize pixmapSize = - capRenderSize(svgRenderer.defaultSize().expandedTo(QSize(minSize, minSize)), QSize(minSize, minSize)); - QPixmap pix(pixmapSize); + QPixmap pix(svgRenderer.defaultSize().expandedTo(QSize(minSize, minSize))); pix.fill(Qt::transparent); QPainter pixPainter(&pix); @@ -361,10 +307,6 @@ static QString getIconType(const bool isBuddy, const UserLevelFlags &userLevelFl return "pawn_judge"; } - if (userLevelFlags.testFlag(ServerInfo_User::IsDeveloper)) { - return "pawn_dev"; - } - if (!privLevel.isEmpty() && privLevel.toLower() != "none") { return QString("pawn_%1").arg(privLevel.toLower()); } @@ -422,8 +364,7 @@ QPixmap LockPixmapGenerator::generatePixmap(int height) return pmCache.value(key); } - QPixmap pixmap = tryLoadImage(QStringLiteral("theme:") + themeManager->assetPath(QStringLiteral("icons/lock")), - QSize(height, height), true); + QPixmap pixmap = tryLoadImage("theme:icons/lock", QSize(height, height), true); pmCache.insert(key, pixmap); return pixmap; } @@ -438,8 +379,7 @@ QPixmap DropdownIconPixmapGenerator::generatePixmap(int height, bool expanded) } QString name = expanded ? "dropdown_expanded" : "dropdown_collapsed"; - QPixmap pixmap = tryLoadImage(QStringLiteral("theme:") + themeManager->assetPath(QStringLiteral("icons/") + name), - QSize(height, height), true); + QPixmap pixmap = tryLoadImage("theme:icons/" + name, QSize(height, height), true); pmCache.insert(key, pixmap); return pixmap; @@ -447,66 +387,8 @@ QPixmap DropdownIconPixmapGenerator::generatePixmap(int height, bool expanded) QMap DropdownIconPixmapGenerator::pmCache; -namespace -{ -/// Longest side mana symbols are rendered at before being scaled to their final size. -constexpr int MASTER_ICON_SIZE = 128; - -QString manaSymbolCacheKey(const QString &symbol, const QSize &size) -{ - return symbol + QLatin1Char('|') + QString::number(size.width()) + QLatin1Char('x') + - QString::number(size.height()); -} -} // namespace - -const QPixmap &ManaSymbolPixmapGenerator::masterIcon(const QString &symbol) -{ - auto it = masterCache.constFind(symbol); - if (it != masterCache.constEnd()) { - return it.value(); - } - - QImageReader reader("theme:icons/mana/" + symbol); - QSize sourceSize = reader.size(); - if (!sourceSize.isEmpty()) { - sourceSize.scale(QSize(MASTER_ICON_SIZE, MASTER_ICON_SIZE), Qt::KeepAspectRatio); - reader.setScaledSize(sourceSize); - } - const QPixmap rendered = QPixmap::fromImageReader(&reader); - - return masterCache.insert(symbol, rendered).value(); -} - -QPixmap ManaSymbolPixmapGenerator::generatePixmap(const QString &symbol, const QSize &size) -{ - const QString key = manaSymbolCacheKey(symbol, size); - auto it = scaledCache.constFind(key); - if (it != scaledCache.constEnd()) { - return it.value(); - } - - const QPixmap &icon = masterIcon(symbol); - if (icon.isNull()) { - return {}; - } - - QPixmap scaled = icon.scaled(size, Qt::KeepAspectRatio, Qt::SmoothTransformation); - scaledCache.insert(key, scaled); - return scaled; -} - -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(); @@ -517,21 +399,3 @@ QPixmap loadColorAdjustedPixmap(const QString &name) return QPixmap(name); } } - -QPixmap themePixmap(QStringView prefix) -{ - const QString resolved = themeManager->assetPath(prefix); - return QPixmap(QStringLiteral("theme:") + resolved); -} - -void clearPixmapGeneratorCaches() -{ - PhasePixmapGenerator::clear(); - CounterPixmapGenerator::clear(); - PingPixmapGenerator::clear(); - CountryPixmapGenerator::clear(); - UserLevelPixmapGenerator::clear(); - LockPixmapGenerator::clear(); - DropdownIconPixmapGenerator::clear(); - ManaSymbolPixmapGenerator::clear(); -} diff --git a/cockatrice/src/interface/pixel_map_generator.h b/cockatrice/src/interface/pixel_map_generator.h index b6e822fd9..22f44d8db 100644 --- a/cockatrice/src/interface/pixel_map_generator.h +++ b/cockatrice/src/interface/pixel_map_generator.h @@ -7,7 +7,6 @@ #ifndef PIXMAPGENERATOR_H #define PIXMAPGENERATOR_H -#include #include #include #include @@ -126,45 +125,6 @@ public: } }; -class ManaSymbolPixmapGenerator -{ -private: - static QHash masterCache; - static QHash scaledCache; - - /** - * @brief Renders \a symbol once at a fixed moderate size, so repeated scalings never - * re-rasterize the source file (SVG sources can be very expensive to rasterize). - */ - static const QPixmap &masterIcon(const QString &symbol); - -public: - /** - * @brief Returns a smooth-scaled rendering of the given mana symbol icon. - * - * Results are shared between all callers via a process-wide cache keyed by symbol - * and size, so scaling work is done once per distinct combination instead of once - * per widget creation or resize. - */ - static QPixmap generatePixmap(const QString &symbol, const QSize &size); - static void clear() - { - masterCache.clear(); - scaledCache.clear(); - } -}; - QPixmap loadColorAdjustedPixmap(const QString &name); -// Loads a "theme:" asset (with no file extension in prefix), preferring the -// scheme-qualified variant (prefix-dark / prefix-light, resolved via -// ThemeManager::assetPath) and falling back to the plain asset. Callers load -// the returned path directly. Use for scheme-sensitive pixmaps like -// backgrounds, the card back, and the app logo. -QPixmap themePixmap(QStringView prefix); - -// Clears every PixmapGenerator's static cache so scheme variants are -// re-resolved when the active theme or color scheme changes. -void clearPixmapGeneratorCaches(); - #endif diff --git a/cockatrice/src/interface/theme_config.cpp b/cockatrice/src/interface/theme_config.cpp index 8293a82cf..3c43c467d 100644 --- a/cockatrice/src/interface/theme_config.cpp +++ b/cockatrice/src/interface/theme_config.cpp @@ -16,7 +16,7 @@ QString ThemeConfig::toIni() const out += "[Appearance]\n"; out += QString("ColorScheme = %1\n").arg(colorScheme.isEmpty() ? "System" : colorScheme); out += "\n[Style]\n"; - out += QString("Name = %1\n").arg(styleName.isEmpty() ? "System" : styleName); + out += QString("Name = %1\n").arg(styleName.isEmpty() ? "Default" : styleName); return out; } @@ -96,7 +96,7 @@ bool ThemeConfig::save(const QString &themeDirPath) const bool PaletteConfig::hasPalette() const { - return !colors.isEmpty() || !appColors.isEmpty(); + return !colors.isEmpty(); } QString PaletteConfig::toToml() const @@ -133,24 +133,6 @@ QString PaletteConfig::toToml() const out += "\n"; } - if (!appColors.isEmpty()) { - QMetaEnum appEnum = QMetaEnum::fromType(); - - out += "[AppColors]\n"; - - for (auto it = appColors.cbegin(); it != appColors.cend(); ++it) { - const char *roleName = appEnum.valueToKey(it.key()); - - if (!roleName) { - continue; - } - - out += QString("%1 = %2\n").arg(QString(roleName), -20).arg(it.value().name(QColor::HexArgb)); - } - - out += "\n"; - } - return out; } @@ -170,7 +152,6 @@ PaletteConfig PaletteConfig::fromFile(const QString &filePath) } QMetaEnum roleEnum = QMetaEnum::fromType(); - QMetaEnum appEnum = QMetaEnum::fromType(); QString currentSection; QPalette::ColorGroup currentGroup = QPalette::Active; @@ -221,26 +202,6 @@ PaletteConfig PaletteConfig::fromFile(const QString &filePath) } } - QColor color(value); - - if (!color.isValid()) { - continue; - } - - if (currentSection.compare("AppColors", Qt::CaseInsensitive) == 0) { - if (key.startsWith("AppColor::")) { - key = key.mid(10); - } - - int appRoleInt = appEnum.keyToValue(key.toUtf8().constData()); - - if (appRoleInt >= 0) { - cfg.appColors[static_cast(appRoleInt)] = color; - } - - continue; - } - if (!currentSection.startsWith("Palette", Qt::CaseInsensitive)) { continue; } @@ -255,7 +216,11 @@ PaletteConfig PaletteConfig::fromFile(const QString &filePath) continue; } - cfg.colors[currentGroup][static_cast(roleInt)] = color; + QColor color(value); + + if (color.isValid()) { + cfg.colors[currentGroup][static_cast(roleInt)] = color; + } } return cfg; diff --git a/cockatrice/src/interface/theme_config.h b/cockatrice/src/interface/theme_config.h index 567aeccda..07bf55b7a 100644 --- a/cockatrice/src/interface/theme_config.h +++ b/cockatrice/src/interface/theme_config.h @@ -3,25 +3,9 @@ #include #include -#include #include #include -// Application-specific color roles, layered on top of the fixed QPalette role -// set. Stored in the same palette-.toml under an [AppColors] section -// and editable from the palette editor, so theme authors can control colors -// beyond what Qt's palette can express. -namespace AppColor -{ -Q_NAMESPACE -enum Role -{ - AccentStrong, - AccentSoft, -}; -Q_ENUM_NS(Role) -} // namespace AppColor - struct ThemeConfig { QString colorScheme; @@ -37,16 +21,7 @@ struct ThemeConfig struct PaletteConfig { QMap> colors; - QMap appColors; - bool operator==(const PaletteConfig &rhs) const - { - return colors == rhs.colors && appColors == rhs.appColors; - } - bool operator!=(const PaletteConfig &rhs) const - { - return !(*this == rhs); - } bool hasPalette() const; QString toToml() const; diff --git a/cockatrice/src/interface/theme_manager.cpp b/cockatrice/src/interface/theme_manager.cpp index d86ed77f9..518a97bc6 100644 --- a/cockatrice/src/interface/theme_manager.cpp +++ b/cockatrice/src/interface/theme_manager.cpp @@ -1,13 +1,10 @@ #include "theme_manager.h" #include "../../client/settings/cache_settings.h" -#include "pixel_map_generator.h" #include #include #include -#include -#include #include #include #include @@ -22,7 +19,7 @@ #include #include -#define SYSTEM_THEME_NAME "System" +#define NONE_THEME_NAME "Default" #define FUSION_THEME_NAME "Fusion" #define STYLE_CSS_NAME "style.css" #define HANDZONE_BG_NAME "handzone" @@ -93,17 +90,13 @@ struct PaletteColorInfo } } -static QString usableDefaultStyle(const QString &style) -{ - // The Windows 11 native style is broken: when the OS default - // ("System" theme selection) would use it, fall back to the Vista style. - // Explicitly choosing "windows11" in a theme is still honored. - return style.compare("windows11", Qt::CaseInsensitive) == 0 ? QStringLiteral("windowsvista") : style; -} - ThemeManager::ThemeManager(QObject *parent) : QObject(parent) { - defaultStyleName = usableDefaultStyle(qApp->style()->objectName()); + defaultStyleName = qApp->style()->objectName(); + //! \todo Workaround for windows11 style being broken. + if (defaultStyleName == "windows11") { + defaultStyleName = "windowsvista"; + } // Capture the untouched application palette before any theme is applied. defaultPalette = qApp->palette(); ensureThemeDirectoryExists(); @@ -119,20 +112,14 @@ ThemeManager::ThemeManager(QObject *parent) : QObject(parent) void ThemeManager::ensureThemeDirectoryExists() { - auto &settings = SettingsCache::instance(); - - // Migrate the old "Default" theme name to "System" - if (settings.getThemeName() == "Default") { - settings.setThemeName(SYSTEM_THEME_NAME); - } - - if (settings.getThemeName().isEmpty() || !getAvailableThemes().contains(settings.getThemeName())) { + if (SettingsCache::instance().getThemeName().isEmpty() || + !getAvailableThemes().contains(SettingsCache::instance().getThemeName())) { qCInfo(ThemeManagerLog) << "Theme name not set, setting default value"; - settings.setThemeName(FUSION_THEME_NAME); + SettingsCache::instance().setThemeName(NONE_THEME_NAME); } } -bool ThemeManager::isDarkMode(const QString &themeDirPath) const +bool ThemeManager::isDarkMode(const QString &themeDirPath) { ThemeConfig themeConfig = ThemeConfig::fromThemeDir(themeDirPath); if (themeConfig.colorScheme.compare("Dark", Qt::CaseInsensitive) == 0) { @@ -149,74 +136,11 @@ bool ThemeManager::isDarkMode(const QString &themeDirPath) const } } -QString ThemeManager::schemeVariantPath(QStringView prefix) const +bool ThemeManager::isBuiltInTheme() { - static const QStringList formats = {QStringLiteral(".png"), QStringLiteral(".jpg"), QStringLiteral(".jpeg"), - QStringLiteral(".svg")}; - const QString scheme = isDarkMode(currentThemePath) ? QStringLiteral("dark") : QStringLiteral("light"); - const QString variantStem = prefix.toString() + QLatin1Char('-') + scheme; + const auto themeName = SettingsCache::instance().getThemeName(); - for (const QString &format : formats) { - if (QFileInfo::exists(QStringLiteral("theme:") + variantStem + format)) { - return variantStem + format; - } - } - return QString(); -} - -QString ThemeManager::assetPath(QStringView prefix) const -{ - // Probe order mirrors tryLoadImage: a theme may override the default SVG - // with a raster of the same stem, so raster wins over SVG within a stem. - static const QStringList formats = {QStringLiteral(".png"), QStringLiteral(".jpg"), QStringLiteral(".jpeg"), - QStringLiteral(".svg")}; - - auto findExisting = [](const QString &stem) { - for (const QString &format : formats) { - if (QFileInfo::exists(QStringLiteral("theme:") + stem + format)) { - return stem + format; - } - } - return QString(); - }; - - // Prefer the scheme-qualified variant when it exists, else the plain - // asset as the super fallback. Both return the resolved path including - // its file extension so callers can load it directly. - const QString variant = schemeVariantPath(prefix); - if (!variant.isEmpty()) { - return variant; - } - const QString resolvedPlain = findExisting(prefix.toString()); - return resolvedPlain.isEmpty() ? prefix.toString() : resolvedPlain; -} - -// Probe whether a directory is truly writable by trying to create and remove a -// temporary file. QFileInfo::isWritable() on a directory is unreliable (notably -// on Windows where UAC VirtualStore can make a system dir appear writable). -bool ThemeManager::isDirReallyWritable(const QString &dirPath) -{ - const QString probe = QDir(dirPath).absoluteFilePath(".cockatrice_write_test"); - QFile f(probe); - if (!f.open(QIODevice::WriteOnly)) { - return false; - } - f.close(); - f.remove(); - return true; -} - -QString ThemeManager::writableThemeDir(const QString &themeName) -{ - // All theme writes go to the user themes directory regardless of whether - // the resolved (system) theme directory happens to be writable. Even when a - // write would succeed in-place, routing it to the user directory keeps the - // install intact and guarantees changes survive upgrades. - const QString dirPath = QDir(SettingsCache::instance().paths().getThemesPath()).absoluteFilePath(themeName); - if (!QDir().mkpath(dirPath)) { - qWarning() << "Failed to create theme save directory:" << dirPath; - } - return dirPath; + return themeName == NONE_THEME_NAME || themeName == FUSION_THEME_NAME; } // System (read-only) themes location, relative to the application binary. @@ -241,7 +165,9 @@ QStringMap &ThemeManager::getAvailableThemes() // load themes from user profile dir dir.setPath(SettingsCache::instance().paths().getThemesPath()); - availableThemes.insert(SYSTEM_THEME_NAME, dir.absoluteFilePath("System")); + // add default value + availableThemes.insert(NONE_THEME_NAME, dir.absoluteFilePath("Default")); + availableThemes.insert(FUSION_THEME_NAME, dir.absoluteFilePath("Fusion")); for (QString themeName : dir.entryList(QDir::AllDirs | QDir::NoDotAndDotDot, QDir::Name)) { @@ -250,7 +176,7 @@ QStringMap &ThemeManager::getAvailableThemes() } } - // Load themes from Cockatrice system dir + // load themes from cockatrice system dir dir.setPath(systemThemesBasePath()); for (QString themeName : dir.entryList(QDir::AllDirs | QDir::NoDotAndDotDot, QDir::Name)) { @@ -265,7 +191,7 @@ QStringMap &ThemeManager::getAvailableThemes() QBrush ThemeManager::loadBrush(QString fileName, QColor fallbackColor) { QBrush brush; - QPixmap tmp = QPixmap("theme:" + assetPath(QStringLiteral("zones/") + fileName)); + QPixmap tmp = QPixmap("theme:zones/" + fileName); if (tmp.isNull()) { brush.setColor(fallbackColor); brush.setStyle(Qt::SolidPattern); @@ -279,7 +205,7 @@ QBrush ThemeManager::loadBrush(QString fileName, QColor fallbackColor) QBrush ThemeManager::loadExtraBrush(QString fileName, QBrush &fallbackBrush) { QBrush brush; - QPixmap tmp = QPixmap("theme:" + assetPath(QStringLiteral("zones/") + fileName)); + QPixmap tmp = QPixmap("theme:zones/" + fileName); if (tmp.isNull()) { brush = fallbackBrush; @@ -342,22 +268,9 @@ PaletteConfig ThemeManager::loadDefaultPaletteConfig(const QString &themeDirPath return cfg; } -bool ThemeManager::commitPalette(const QString &themeDirPath, const QString &colorScheme, const PaletteConfig &cfg) -{ - if (!savePaletteConfig(themeDirPath, colorScheme, cfg)) { - return false; - } - - ThemeConfig globalCfg = ThemeConfig::fromThemeDir(themeDirPath); - globalCfg.colorScheme = colorScheme; - globalCfg.save(themeDirPath); - - return true; -} - void ThemeManager::setColorScheme(const QString &scheme) { - const QString dirPath = writableThemeDir(SettingsCache::instance().getThemeName()); + const QString dirPath = getAvailableThemes().value(SettingsCache::instance().getThemeName()); ThemeConfig cfg = ThemeConfig::fromThemeDir(dirPath); cfg.colorScheme = scheme; @@ -368,7 +281,7 @@ void ThemeManager::setColorScheme(const QString &scheme) void ThemeManager::setStyleName(const QString &styleName) { - const QString dirPath = writableThemeDir(SettingsCache::instance().getThemeName()); + const QString dirPath = getAvailableThemes().value(SettingsCache::instance().getThemeName()); ThemeConfig cfg = ThemeConfig::fromThemeDir(dirPath); cfg.styleName = styleName; @@ -399,17 +312,17 @@ void ThemeManager::applyStyleAndPalette(const QString &themeName, Q_UNUSED(activeScheme) #endif QString styleName = themeCfg.styleName; - if (styleName.isEmpty() || styleName.compare("System", Qt::CaseInsensitive) == 0) { + if (styleName.isEmpty() || styleName.compare("Default", Qt::CaseInsensitive) == 0) { if (themeName == FUSION_THEME_NAME) { styleName = "Fusion"; } else { - styleName = usableDefaultStyle(defaultStyleName); + styleName = defaultStyleName; } } QStyle *style = QStyleFactory::create(styleName); if (!style) { - style = QStyleFactory::create(usableDefaultStyle(defaultStyleName)); + style = QStyleFactory::create(defaultStyleName); } // Base palette @@ -442,8 +355,6 @@ void ThemeManager::applyStyleAndPalette(const QString &themeName, qApp->setPalette(base); qApp->setStyle(style); - currentAppColors = palCfg.appColors; - // Force every widget to re-polish and repaint immediately rather than // waiting for natural expose events, which produces a patchwork of old // and new colours during a live preview. @@ -456,35 +367,6 @@ void ThemeManager::applyStyleAndPalette(const QString &themeName, style->polish(widget); widget->update(); } - - emit paletteChanged(); -} - -QColor ThemeManager::appColor(AppColor::Role role) const -{ - const auto it = currentAppColors.constFind(role); - if (it != currentAppColors.constEnd()) { - return it.value(); - } - - // QPalette::Accent was introduced in Qt 6.6 and several shipped palettes - // set it to a value barely distinguishable from Window, so it is not a - // reliable accent source. The selection highlight is the stable accent - // (Accent defaults to Highlight when unset), and deriving from it - // unconditionally keeps every Qt version rendering identically. - const QColor accent = qApp->palette().color(QPalette::Active, QPalette::Highlight); - - if (role == AppColor::AccentSoft) { - constexpr int SOFT_SATURATION_PERCENT = 70; - constexpr int SOFT_LIGHTNESS_OFFSET = 60; - - // Light end of the gradient: same hue, softened and lightened - return QColor::fromHsl(qMax(0, accent.hslHue()), - qBound(0, qRound(accent.hslSaturation() * SOFT_SATURATION_PERCENT / 100.0), 255), - qBound(0, accent.lightness() + SOFT_LIGHTNESS_OFFSET, 255)); - } - - return accent; } void ThemeManager::themeChangedSlot() @@ -494,19 +376,9 @@ void ThemeManager::themeChangedSlot() currentThemePath = dirPath; QDir dir(dirPath); - // CSS — prefer the scheme-qualified stylesheet (style-dark.css / - // style-light.css) when present, else the plain style.css as fallback. - if (!dirPath.isEmpty()) { - const QString scheme = isDarkMode(dirPath) ? QStringLiteral("dark") : QStringLiteral("light"); - const QString schemeCss = QFileInfo(QStringLiteral(STYLE_CSS_NAME)).completeBaseName() + QLatin1Char('-') + - scheme + QStringLiteral(".css"); - if (dir.exists(schemeCss)) { - qApp->setStyleSheet("file:///" + dir.absoluteFilePath(schemeCss)); - } else if (dir.exists(STYLE_CSS_NAME)) { - qApp->setStyleSheet("file:///" + dir.absoluteFilePath(STYLE_CSS_NAME)); - } else { - qApp->setStyleSheet(""); - } + // CSS + if (!dirPath.isEmpty() && dir.exists(STYLE_CSS_NAME)) { + qApp->setStyleSheet("file:///" + dir.absoluteFilePath(STYLE_CSS_NAME)); } else { qApp->setStyleSheet(""); } @@ -521,19 +393,8 @@ void ThemeManager::themeChangedSlot() // ── Load palette: custom first, then theme default ──────────────────── PaletteConfig palette = PaletteConfig::fromScheme(dirPath, activeScheme); - const PaletteConfig themeDefault = ThemeManager::loadDefaultPaletteConfig(dirPath, themeName, activeScheme); - if (palette.hasPalette()) { - // A custom palette written before [AppColors] existed carries no app - // colors; merge the theme's shipped defaults so the identity colors - // survive (hasPalette() counts an app-colors-only file as a palette, - // so those are kept wholesale and never reach here empty). - for (auto it = themeDefault.appColors.cbegin(); it != themeDefault.appColors.cend(); ++it) { - if (!palette.appColors.contains(it.key())) { - palette.appColors.insert(it.key(), it.value()); - } - } - } else { - palette = themeDefault; + if (!palette.hasPalette()) { + palette = ThemeManager::loadDefaultPaletteConfig(dirPath, themeName, activeScheme); } applyStyleAndPalette(themeName, themeCfg, palette, activeScheme); @@ -568,7 +429,6 @@ void ThemeManager::themeChangedSlot() } QPixmapCache::clear(); - clearPixmapGeneratorCaches(); emit themeChanged(); } diff --git a/cockatrice/src/interface/theme_manager.h b/cockatrice/src/interface/theme_manager.h index aadb38ee9..861ab838b 100644 --- a/cockatrice/src/interface/theme_manager.h +++ b/cockatrice/src/interface/theme_manager.h @@ -50,7 +50,6 @@ private: QString currentThemePath; std::array brushes; QStringMap availableThemes; - QMap currentAppColors; /* Internal cache for multiple backgrounds */ @@ -66,24 +65,8 @@ protected: const QString &activeScheme); public: - // Resolves the directory to write theme changes to for the given theme - // name. The resolved theme dir (user or system) is used when writable; - // read-only system themes fall back to the user themes directory, creating - // it if needed, so customisations never get lost on upgrade. - static QString writableThemeDir(const QString &themeName); - // Probe whether a directory is truly writable by trying to create and remove - // a temporary file. QFileInfo::isWritable() on a directory is unreliable - // (notably on Windows where UAC VirtualStore can make a system dir appear - // writable). - static bool isDirReallyWritable(const QString &dirPath); - // Explicit color scheme of the theme: theme.cfg's ColorScheme setting - // (Dark/Light), falling back to the OS color scheme when it is "System". - bool isDarkMode(const QString &themeDirPath) const; - // The resolved scheme of the currently active theme. - bool isDarkModeActive() const - { - return isDarkMode(currentThemePath); - } + bool isBuiltInTheme(); + bool isDarkMode(const QString &themeDirPath); QStringMap &getAvailableThemes(); // Returns the path to the currently active theme directory (empty = default) QString getCurrentThemePath() const @@ -97,45 +80,22 @@ public: // Load/save per-scheme palette colors static PaletteConfig loadPaletteConfig(const QString &themeDirPath, const QString &colorScheme); static bool savePaletteConfig(const QString &themeDirPath, const QString &colorScheme, const PaletteConfig &cfg); - // Resolve prefix to a scheme-qualified "theme:" path. Existence is probed - // internally across the formats themes may ship (.png/.jpg/.svg), so - // callers load the returned path directly. Prefers "-" - // when a file exists at that stem, otherwise the plain "" as the - // super fallback. The resolved scheme covers explicit light/dark as well - // as OS-resolved "system". Returns the path with its file extension when a - // match is found; unqualified assets keep working unchanged. - QString assetPath(QStringView prefix) const; - // Like assetPath, but resolves only the scheme-qualified variant - // ("-.") and returns an empty string when no - // variant exists — it never falls back to the plain "" asset. - // Callers that must distinguish "no authored variant" (e.g. to keep a - // legacy runtime fallback alive) should use this instead of assetPath. - QString schemeVariantPath(QStringView prefix) const; // Load the theme's shipped default palette, falling back to the system // theme directory when it is absent from the resolved (user) directory. static PaletteConfig loadDefaultPaletteConfig(const QString &themeDirPath, const QString &themeName, const QString &colorScheme); - /** @brief Writes cfg to disk as the theme's palette-.toml and updates the - * theme's stored colour scheme to match. Shared by PaletteEditorDialog::onSave - * and FirstRunWizard's theme step so the two "generate + keep" paths can't drift. */ - static bool commitPalette(const QString &themeDirPath, const QString &colorScheme, const PaletteConfig &cfg); void setColorScheme(const QString &scheme); void setStyleName(const QString &styleName); void reloadCurrentTheme(); void previewPalette(const PaletteConfig &cfg, const QString &scheme); - // Resolves an application color role: the theme's stored [AppColors] value - // when present, otherwise a palette-accent-derived fallback. - QColor appColor(AppColor::Role role) const; - QBrush &getBgBrush(Role zone); QBrush getExtraBgBrush(Role zone, int zoneId = 0); protected slots: void themeChangedSlot(); signals: void themeChanged(); - void paletteChanged(); }; extern ThemeManager *themeManager; diff --git a/cockatrice/src/interface/widgets/cards/additional_info/color_identity_widget.cpp b/cockatrice/src/interface/widgets/cards/additional_info/color_identity_widget.cpp index 2199faf30..3f0f30a27 100644 --- a/cockatrice/src/interface/widgets/cards/additional_info/color_identity_widget.cpp +++ b/cockatrice/src/interface/widgets/cards/additional_info/color_identity_widget.cpp @@ -41,11 +41,6 @@ void ColorIdentityWidget::populateManaSymbolWidgets() // clear old layout QtUtils::clearLayoutRec(layout); - // The freshly created symbols haven't been sized yet, so force the next resize pass - // to apply the symbol size again. - lastIconSize = -1; - lastWidth = -1; - // populate mana symbols if (SettingsCache::instance().visualDeckStorage().getVisualDeckStorageDrawUnusedColorIdentities()) { for (const QString symbol : fullColorIdentity) { @@ -78,37 +73,20 @@ void ColorIdentityWidget::toggleUnusedVisibility() void ColorIdentityWidget::resizeEvent(QResizeEvent *event) { QWidget::resizeEvent(event); + QList manaSymbols = findChildren(); - const int totalWidth = event->size().width(); - if (totalWidth == lastWidth && lastIconSize != -1) { - return; - } - lastWidth = totalWidth; + if (!manaSymbols.isEmpty()) { + int totalWidth = event->size().width(); + int totalHeight = totalWidth / 6; // Set height to 1/4 of the width + setFixedHeight(totalHeight); - const int totalHeight = qMax(0, totalWidth / 6); // Set height to 1/4 of the width - setFixedHeight(totalHeight); + int spacing = layout->spacing(); + int count = manaSymbols.size(); + int availableWidth = totalWidth - (spacing * (count - 1)); + int iconSize = qMin(availableWidth / count, totalHeight); // Ensure icons fit within the new height - const int count = layout->count(); - if (count == 0) { - return; - } - - const int spacing = layout->spacing(); - const int availableWidth = totalWidth - (spacing * (count - 1)); - const int iconSize = qMin(availableWidth / count, totalHeight); // Ensure icons fit within the new height - - if (iconSize <= 0) { - lastIconSize = iconSize; - return; - } - if (iconSize == lastIconSize) { - return; - } - lastIconSize = iconSize; - - for (int i = 0; i < count; ++i) { - if (auto *w = qobject_cast(layout->itemAt(i)->widget())) { - w->setFixedSize(iconSize, iconSize); + for (ManaSymbolWidget *manaSymbol : manaSymbols) { + manaSymbol->setFixedSize(iconSize, iconSize); } } } diff --git a/cockatrice/src/interface/widgets/cards/additional_info/color_identity_widget.h b/cockatrice/src/interface/widgets/cards/additional_info/color_identity_widget.h index 315ac07d6..f776d4c77 100644 --- a/cockatrice/src/interface/widgets/cards/additional_info/color_identity_widget.h +++ b/cockatrice/src/interface/widgets/cards/additional_info/color_identity_widget.h @@ -30,8 +30,6 @@ public slots: private: QString colorIdentity; QHBoxLayout *layout; - int lastIconSize = -1; ///< The symbol size last applied, to skip redundant resize passes. - int lastWidth = -1; ///< The width last processed, to skip redundant resize passes. }; #endif // COLOR_IDENTITY_WIDGET_H diff --git a/cockatrice/src/interface/widgets/cards/additional_info/deck_color_identity.cpp b/cockatrice/src/interface/widgets/cards/additional_info/deck_color_identity.cpp deleted file mode 100644 index 62b01511e..000000000 --- a/cockatrice/src/interface/widgets/cards/additional_info/deck_color_identity.cpp +++ /dev/null @@ -1,37 +0,0 @@ -#include "deck_color_identity.h" - -#include -#include -#include -#include - -QString getDeckColorIdentity(const DeckList &deck, const CardDatabaseQuerier *db) -{ - const QStringList cardList = deck.getCardList({DECK_ZONE_MAIN, DECK_ZONE_SIDE}); - if (cardList.isEmpty()) { - return {}; - } - - QSet colorSet; // A set to collect unique color symbols (e.g., W, U, B, R, G) - - for (const QString &cardName : cardList) { - CardInfoPtr currentCard = db->getCardInfo(cardName); - if (currentCard) { - const QString colors = currentCard->getColors(); // returns something like "WUB" - for (const QChar &color : colors) { - colorSet.insert(color); - } - } - } - - // Ensure the color identity is in WUBRG order - QString colorIdentity; - const QString wubrgOrder = "WUBRG"; - for (const QChar &color : wubrgOrder) { - if (colorSet.contains(color)) { - colorIdentity.append(color); - } - } - - return colorIdentity; -} diff --git a/cockatrice/src/interface/widgets/cards/additional_info/deck_color_identity.h b/cockatrice/src/interface/widgets/cards/additional_info/deck_color_identity.h deleted file mode 100644 index 04294cb1c..000000000 --- a/cockatrice/src/interface/widgets/cards/additional_info/deck_color_identity.h +++ /dev/null @@ -1,20 +0,0 @@ -#ifndef COCKATRICE_DECK_COLOR_IDENTITY_H -#define COCKATRICE_DECK_COLOR_IDENTITY_H - -#include - -class CardDatabaseQuerier; -class DeckList; - -/** - * @brief Computes the color identity of a deck (e.g. "WUBRG") from the color - * symbols of all cards in the main deck and sideboard, ordered WUBRG. - * - * Shared as a free function so the deck storage previews and the deck share - * dialog compute identities identically. - * - * @param db Card database used to look up card color symbols. - */ -QString getDeckColorIdentity(const DeckList &deck, const CardDatabaseQuerier *db); - -#endif // COCKATRICE_DECK_COLOR_IDENTITY_H diff --git a/cockatrice/src/interface/widgets/cards/additional_info/mana_cost_widget.h b/cockatrice/src/interface/widgets/cards/additional_info/mana_cost_widget.h index 8bd2e9bc0..b2f6b62c0 100644 --- a/cockatrice/src/interface/widgets/cards/additional_info/mana_cost_widget.h +++ b/cockatrice/src/interface/widgets/cards/additional_info/mana_cost_widget.h @@ -17,7 +17,7 @@ class ManaCostWidget : public QWidget public: explicit ManaCostWidget(QWidget *parent, CardInfoPtr card); - static QStringList parseManaCost(const QString &manaString); + QStringList parseManaCost(const QString &manaString); public slots: void resizeEvent(QResizeEvent *event) override; diff --git a/cockatrice/src/interface/widgets/cards/additional_info/mana_symbol_widget.cpp b/cockatrice/src/interface/widgets/cards/additional_info/mana_symbol_widget.cpp index 18011909f..51247da7a 100644 --- a/cockatrice/src/interface/widgets/cards/additional_info/mana_symbol_widget.cpp +++ b/cockatrice/src/interface/widgets/cards/additional_info/mana_symbol_widget.cpp @@ -1,15 +1,15 @@ #include "mana_symbol_widget.h" #include "../../../../client/settings/cache_settings.h" -#include "../../../pixel_map_generator.h" #include #include ManaSymbolWidget::ManaSymbolWidget(QWidget *parent, QString _symbol, bool _isActive, bool _mayBeToggled) - : QLabel(parent), symbol(std::move(_symbol)), isActive(_isActive), mayBeToggled(_mayBeToggled) + : QLabel(parent), symbol(_symbol), isActive(_isActive), mayBeToggled(_mayBeToggled) { - setPixmap(ManaSymbolPixmapGenerator::generatePixmap(symbol, QSize(50, 50))); + loadManaIcon(); + setPixmap(manaIcon.scaled(50, 50, Qt::KeepAspectRatio, Qt::SmoothTransformation)); setMaximumWidth(50); // Initialize opacity effect @@ -64,13 +64,16 @@ void ManaSymbolWidget::mousePressEvent(QMouseEvent *event) void ManaSymbolWidget::resizeEvent(QResizeEvent *event) { QLabel::resizeEvent(event); - const QSize newSize = event->size(); + setPixmap(manaIcon.scaled(event->size(), Qt::KeepAspectRatio, Qt::SmoothTransformation)); +} - // Skip the rescale when the size didn't actually change: layout passes resize these - // widgets repeatedly with identical sizes. - if (newSize.isEmpty() || pixmap().size() == newSize) { - return; +void ManaSymbolWidget::loadManaIcon() +{ + QString filename = "theme:icons/mana/"; + + if (symbol == "W" || symbol == "U" || symbol == "B" || symbol == "R" || symbol == "G") { + filename += symbol; } - setPixmap(ManaSymbolPixmapGenerator::generatePixmap(symbol, newSize)); + manaIcon = QPixmap(filename); } diff --git a/cockatrice/src/interface/widgets/cards/additional_info/mana_symbol_widget.h b/cockatrice/src/interface/widgets/cards/additional_info/mana_symbol_widget.h index 705873dff..0f2d7acd1 100644 --- a/cockatrice/src/interface/widgets/cards/additional_info/mana_symbol_widget.h +++ b/cockatrice/src/interface/widgets/cards/additional_info/mana_symbol_widget.h @@ -33,6 +33,8 @@ public: return symbol[0]; } + void loadManaIcon(); + public slots: void resizeEvent(QResizeEvent *event) override; void mousePressEvent(QMouseEvent *event) override; @@ -42,6 +44,7 @@ signals: private: QString symbol; + QPixmap manaIcon; bool isActive; bool mayBeToggled; QGraphicsOpacityEffect *opacityEffect; diff --git a/cockatrice/src/interface/widgets/cards/art_crop_attribution.cpp b/cockatrice/src/interface/widgets/cards/art_crop_attribution.cpp deleted file mode 100644 index 8ab18ddd9..000000000 --- a/cockatrice/src/interface/widgets/cards/art_crop_attribution.cpp +++ /dev/null @@ -1,61 +0,0 @@ -#include "art_crop_attribution.h" - -#include -#include -#include -#include - -QString buildArtAttribution(const ExactCard &card) -{ - const QString artist = card.getPrinting().getArtist(); - if (artist.isEmpty()) { - return QString(); - } - return QObject::tr("Art: %1").arg(artist); -} - -QRectF paintArtAttribution(QPainter &painter, - const QRectF &rect, - const QString &attribution, - Qt::Alignment anchor, - qreal scale) -{ - if (attribution.isEmpty()) { - return QRectF(); - } - - painter.save(); - - QFont font = painter.font(); - font.setPointSizeF(qMax(6.0, font.pointSizeF() * scale)); - painter.setFont(font); - - const QFontMetrics fm(font); - const qreal maxTextWidth = rect.width() * 0.45; - const QString elided = fm.elidedText(attribution, Qt::ElideRight, qMax(qreal(80.0) * scale, maxTextWidth)); - - const qreal pad = 6.0 * scale; - QRectF captionRect(QPointF(0, 0), QSizeF(fm.horizontalAdvance(elided) + pad * 2.0, fm.height() + pad * 2.0)); - const qreal margin = 4.0 * scale; - if (anchor.testFlag(Qt::AlignLeft)) { - captionRect.moveLeft(rect.left() + margin); - } else { - captionRect.moveRight(rect.right() - margin); - } - if (anchor.testFlag(Qt::AlignTop)) { - captionRect.moveTop(rect.top() + margin); - } else { - captionRect.moveBottom(rect.bottom() - margin); - } - - painter.setPen(Qt::NoPen); - painter.setBrush(QColor(0, 0, 0, 120)); - painter.drawRoundedRect(captionRect, 4, 4); - - painter.setPen(QColor(255, 255, 255, 220)); - painter.drawText(captionRect, Qt::AlignCenter, elided); - - painter.restore(); - - return captionRect; -} diff --git a/cockatrice/src/interface/widgets/cards/art_crop_attribution.h b/cockatrice/src/interface/widgets/cards/art_crop_attribution.h deleted file mode 100644 index 0b3fb3d56..000000000 --- a/cockatrice/src/interface/widgets/cards/art_crop_attribution.h +++ /dev/null @@ -1,41 +0,0 @@ -#ifndef COCKATRICE_ART_CROP_ATTRIBUTION_H -#define COCKATRICE_ART_CROP_ATTRIBUTION_H - -#include - -class ExactCard; -class QPainter; -class QRectF; -class QString; - -/** - * @brief Builds an attribution caption for a cropped card art display. - * - * When a card image's art region is shown cropped (an "art crop"), the artist - * should be credited in the same interface. Returns an empty string when the - * database has no artist data for the card. - * - * @param card The card whose art is being displayed. - * @return Caption such as "Art: John Avon", or empty. - */ -QString buildArtAttribution(const ExactCard &card); - -/** - * @brief Paints an attribution caption in a corner of a rect. - * - * Draws a subtle semi-transparent pill containing the caption, elided to fit. - * - * @param painter Painter to draw with. - * @param rect The area (e.g. the cropped art region) the caption belongs to. - * @param attribution Caption text (see buildArtAttribution()). - * @param anchor Corner of @p rect to pin the pill to (default bottom-right). - * @param scale Size multiplier for the pill (e.g. 0.8 for a smaller pill). - * @return The rect the pill was drawn in, or an empty rect if @p attribution is empty. - */ -QRectF paintArtAttribution(QPainter &painter, - const QRectF &rect, - const QString &attribution, - Qt::Alignment anchor = Qt::AlignRight | Qt::AlignBottom, - qreal scale = 1.0); - -#endif // COCKATRICE_ART_CROP_ATTRIBUTION_H diff --git a/cockatrice/src/interface/widgets/cards/card_art_utils.cpp b/cockatrice/src/interface/widgets/cards/card_art_utils.cpp deleted file mode 100644 index b26b73593..000000000 --- a/cockatrice/src/interface/widgets/cards/card_art_utils.cpp +++ /dev/null @@ -1,18 +0,0 @@ -#include "card_art_utils.h" - -#include -#include - -namespace CardArtUtils -{ -QPixmap rotateSidewaysLayoutArt(const QPixmap &art, const ExactCard &card) -{ - if (!card.getInfo().getUiAttributes().landscapeOrientation) { - return art; - } - - QTransform transform; - transform.rotate(90); - return art.transformed(transform, Qt::SmoothTransformation); -} -} // namespace CardArtUtils \ No newline at end of file diff --git a/cockatrice/src/interface/widgets/cards/card_art_utils.h b/cockatrice/src/interface/widgets/cards/card_art_utils.h deleted file mode 100644 index 5c331a12c..000000000 --- a/cockatrice/src/interface/widgets/cards/card_art_utils.h +++ /dev/null @@ -1,25 +0,0 @@ -#ifndef CARD_ART_UTILS_H -#define CARD_ART_UTILS_H - -#include - -class ExactCard; - -namespace CardArtUtils -{ -/** - * @brief Rotates a card's art upright when its layout shows sideways. - * - * Sideways-layout cards (planes, sieges/battles, split cards) store their - * landscape artwork rotated 90° inside a portrait frame. Art-crop displays, - * playmat art, and the card-info picture must show such art upright before - * sampling or painting. Portrait cards are returned unchanged. - * - * @param art The card pixmap to orient. - * @param card The card describing the art orientation. - * @return @p art rotated 90° clockwise for sideways-layout cards, else @p art. - */ -QPixmap rotateSidewaysLayoutArt(const QPixmap &art, const ExactCard &card); -} // namespace CardArtUtils - -#endif // CARD_ART_UTILS_H \ No newline at end of file diff --git a/cockatrice/src/interface/widgets/cards/card_group_display_widgets/card_group_display_widget.cpp b/cockatrice/src/interface/widgets/cards/card_group_display_widgets/card_group_display_widget.cpp index bfbdd7e42..3f36e559c 100644 --- a/cockatrice/src/interface/widgets/cards/card_group_display_widgets/card_group_display_widget.cpp +++ b/cockatrice/src/interface/widgets/cards/card_group_display_widgets/card_group_display_widget.cpp @@ -174,18 +174,16 @@ void CardGroupDisplayWidget::updateCardDisplays() QModelIndex sourceIndex = proxy.mapToSource(proxyIndex); // 4. persist the source index - addCardWidgets(QPersistentModelIndex(sourceIndex)); - } -} + QPersistentModelIndex persistent(sourceIndex); -void CardGroupDisplayWidget::addCardWidgets(const QPersistentModelIndex &persistent) -{ - // Get the card amount - int cardAmount = persistent.sibling(persistent.row(), DeckListModelColumns::CARD_AMOUNT).data(Qt::EditRole).toInt(); + // Get the card amount + int cardAmount = + sourceIndex.sibling(sourceIndex.row(), DeckListModelColumns::CARD_AMOUNT).data(Qt::EditRole).toInt(); - // Create multiple widgets for the card count - for (int copy = 0; copy < cardAmount; ++copy) { - addToLayout(constructWidgetForIndex(persistent)); + // Create multiple widgets for the card count + for (int copy = 0; copy < cardAmount; ++copy) { + addToLayout(constructWidgetForIndex(persistent)); + } } } diff --git a/cockatrice/src/interface/widgets/cards/card_group_display_widgets/card_group_display_widget.h b/cockatrice/src/interface/widgets/cards/card_group_display_widgets/card_group_display_widget.h index a3bf70981..2308ccf8d 100644 --- a/cockatrice/src/interface/widgets/cards/card_group_display_widgets/card_group_display_widget.h +++ b/cockatrice/src/interface/widgets/cards/card_group_display_widgets/card_group_display_widget.h @@ -35,7 +35,6 @@ public: void onSelectionChanged(const QItemSelection &selected, const QItemSelection &deselected); void refreshSelectionForIndex(const QPersistentModelIndex &persistent); void clearAllDisplayWidgets(); - void addCardWidgets(const QPersistentModelIndex &persistent); DeckListModel *deckListModel; QItemSelectionModel *selectionModel; diff --git a/cockatrice/src/interface/widgets/cards/card_info_picture_enlarged_widget.cpp b/cockatrice/src/interface/widgets/cards/card_info_picture_enlarged_widget.cpp index 40bef3df3..f442fe425 100644 --- a/cockatrice/src/interface/widgets/cards/card_info_picture_enlarged_widget.cpp +++ b/cockatrice/src/interface/widgets/cards/card_info_picture_enlarged_widget.cpp @@ -52,29 +52,12 @@ void CardInfoPictureEnlargedWidget::loadPixmap(const QSize &size) * @param size The desired size for the pixmap. * * Sets the widget's pixmap to the card image and resizes the widget to match the specified size. Triggers a repaint. - * - * When the image is not yet cached, the pixmap is cleared (instead of showing a stale previous card) and the widget - * refreshes automatically once the card image finishes loading. */ void CardInfoPictureEnlargedWidget::setCardPixmap(const ExactCard &_card, const QSize size) { - if (card.getCardPtr()) { - disconnect(card.getCardPtr().data(), nullptr, this, nullptr); - } - card = _card; - - // Clear any previous card's art so we never paint a stale pixmap while the new image loads - enlargedPixmap = QPixmap(); loadPixmap(size); - if (card.getCardPtr()) { - connect(card.getCardPtr().data(), &CardInfo::pixmapUpdated, this, [this]() { - loadPixmap(this->size()); - update(); - }); - } - setFixedSize(size); // Set the widget size to the enlarged size update(); // Trigger a repaint diff --git a/cockatrice/src/interface/widgets/cards/card_info_picture_widget.cpp b/cockatrice/src/interface/widgets/cards/card_info_picture_widget.cpp index 2dd21e78a..79ae087d7 100644 --- a/cockatrice/src/interface/widgets/cards/card_info_picture_widget.cpp +++ b/cockatrice/src/interface/widgets/cards/card_info_picture_widget.cpp @@ -5,7 +5,6 @@ #include "../../../interface/card_picture_loader/card_picture_loader.h" #include "../../../interface/widgets/tabs/tab_supervisor.h" #include "../../window_main.h" -#include "card_art_utils.h" #include #include @@ -74,8 +73,6 @@ CardInfoPictureWidget::CardInfoPictureWidget(QWidget *parent, const bool _hoverT update(); }); - connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::cardLangChanged, this, - &CardInfoPictureWidget::updatePixmap); } /** @@ -196,7 +193,12 @@ void CardInfoPictureWidget::paintEvent(QPaintEvent *event) QPixmap transformedPixmap = resizedPixmap; // Default pixmap if (SettingsCache::instance().cardsDisplay().getAutoRotateSidewaysLayoutCards()) { - transformedPixmap = CardArtUtils::rotateSidewaysLayoutArt(resizedPixmap, exactCard); + if (exactCard.getInfo().getUiAttributes().landscapeOrientation) { + // Rotate pixmap 90 degrees to the left + QTransform transform; + transform.rotate(90); + transformedPixmap = resizedPixmap.transformed(transform, Qt::SmoothTransformation); + } } // Handle DPI scaling diff --git a/cockatrice/src/interface/widgets/cards/card_info_picture_with_text_overlay_widget.cpp b/cockatrice/src/interface/widgets/cards/card_info_picture_with_text_overlay_widget.cpp index 000a88b2f..c5cb59b3b 100644 --- a/cockatrice/src/interface/widgets/cards/card_info_picture_with_text_overlay_widget.cpp +++ b/cockatrice/src/interface/widgets/cards/card_info_picture_with_text_overlay_widget.cpp @@ -133,14 +133,12 @@ void CardInfoPictureWithTextOverlayWidget::paintEvent(QPaintEvent *event) path.addRoundedRect(glowRect, radius, radius); // Soft outer glow - QColor glowColor = palette().color(QPalette::Highlight); - glowColor.setAlpha(80); + QColor glowColor(0, 150, 255, 80); // subtle blu painter.setPen(QPen(glowColor, 6)); painter.drawPath(path); // Thin inner border for crispness - QColor borderColor = palette().color(QPalette::Highlight); - borderColor.setAlpha(200); + QColor borderColor(0, 150, 255, 200); painter.setPen(QPen(borderColor, 2)); painter.drawRoundedRect(pixmapRect, radius, radius); diff --git a/cockatrice/src/interface/widgets/cards/card_info_text_widget.cpp b/cockatrice/src/interface/widgets/cards/card_info_text_widget.cpp index e98c3c02a..c6af5320b 100644 --- a/cockatrice/src/interface/widgets/cards/card_info_text_widget.cpp +++ b/cockatrice/src/interface/widgets/cards/card_info_text_widget.cpp @@ -1,7 +1,6 @@ #include "card_info_text_widget.h" #include "../../../game_graphics/board/card_item.h" -#include "../../card_localization.h" #include #include @@ -11,7 +10,7 @@ #include #include -CardInfoTextWidget::CardInfoTextWidget(QWidget *parent) : QFrame(parent) +CardInfoTextWidget::CardInfoTextWidget(QWidget *parent) : QFrame(parent), info(nullptr) { propsLabel = new QLabel; propsLabel->setOpenExternalLinks(false); @@ -40,12 +39,6 @@ CardInfoTextWidget::CardInfoTextWidget(QWidget *parent) : QFrame(parent) grid->setRowStretch(1, 1); retranslateUi(); - - connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::cardLangChanged, this, [this] { - if (currentCard) { - setCard(currentCard); - } - }); } void CardInfoTextWidget::setTexts(const QString &propsText, const QString &textText) @@ -67,7 +60,7 @@ void CardInfoTextWidget::setCard(const ExactCard &exactCard) QString text = ""; text += QString("") - .arg(tr("Name:"), CardLocalization::displayName(card).toHtmlEscaped()); + .arg(tr("Name:"), card->getName().toHtmlEscaped()); if (!exactCard.getPrinting().isEmpty()) { QString setShort = exactCard.getPrinting().getSet()->getShortName().toHtmlEscaped(); @@ -101,8 +94,7 @@ void CardInfoTextWidget::setCard(const ExactCard &exactCard) } text += "
%1%2
"; - setTexts(text, CardLocalization::displayText(card)); - currentCard = exactCard; + setTexts(text, card->getText()); } void CardInfoTextWidget::setInvalidCardName(const QString &cardName) diff --git a/cockatrice/src/interface/widgets/cards/card_info_text_widget.h b/cockatrice/src/interface/widgets/cards/card_info_text_widget.h index 683be5ab5..a9c29da37 100644 --- a/cockatrice/src/interface/widgets/cards/card_info_text_widget.h +++ b/cockatrice/src/interface/widgets/cards/card_info_text_widget.h @@ -23,7 +23,7 @@ private: QLabel *propsLabel; QScrollArea *propsScroll; QTextEdit *textLabel; - ExactCard currentCard; ///< Last card set, re-rendered when the card language changes. + CardInfoPtr info; void setTexts(const QString &propsText, const QString &textText); public: diff --git a/cockatrice/src/interface/widgets/cards/deck_card_zone_display_widget.cpp b/cockatrice/src/interface/widgets/cards/deck_card_zone_display_widget.cpp index b00d9db1e..eaf3a67b0 100644 --- a/cockatrice/src/interface/widgets/cards/deck_card_zone_display_widget.cpp +++ b/cockatrice/src/interface/widgets/cards/deck_card_zone_display_widget.cpp @@ -5,7 +5,6 @@ #include "libcockatrice/card/database/card_database_manager.h" #include -#include #include DeckCardZoneDisplayWidget::DeckCardZoneDisplayWidget(QWidget *parent, @@ -52,6 +51,11 @@ DeckCardZoneDisplayWidget::DeckCardZoneDisplayWidget(QWidget *parent, // User Interaction // ===================================================================================================================== +void DeckCardZoneDisplayWidget::onClick(QMouseEvent *event, const ExactCard &card) +{ + emit cardClicked(event, card, zoneName); +} + void DeckCardZoneDisplayWidget::onHover(const ExactCard &card) { emit cardHovered(card); @@ -91,18 +95,12 @@ void DeckCardZoneDisplayWidget::constructAppropriateWidget(QPersistentModelIndex } auto categoryName = index.sibling(index.row(), DeckListModelColumns::CARD_NAME).data(Qt::EditRole).toString(); - // Cards in a custom zone belong to that zone, not the board zone, so that - // increment/decrement/swap actions target the custom zone. - const bool isCustomZone = index.data(DeckRoles::IsCustomZoneRole).toBool(); - const QString effectiveZoneName = isCustomZone ? categoryName : zoneName; - const auto routeCardClick = [this, effectiveZoneName](QMouseEvent *event, const ExactCard &card) { - emit cardClicked(event, card, effectiveZoneName); - }; if (displayType == DisplayType::Overlap) { auto *displayWidget = new OverlappedCardGroupDisplayWidget( - cardGroupContainer, deckListModel, selectionModel, index, effectiveZoneName, categoryName, - activeGroupCriteria, activeSortCriteria, subBannerOpacity, cardSizeWidget); - connect(displayWidget, &OverlappedCardGroupDisplayWidget::cardClicked, this, routeCardClick); + cardGroupContainer, deckListModel, selectionModel, index, zoneName, categoryName, activeGroupCriteria, + activeSortCriteria, subBannerOpacity, cardSizeWidget); + connect(displayWidget, &OverlappedCardGroupDisplayWidget::cardClicked, this, + &DeckCardZoneDisplayWidget::onClick); connect(displayWidget, &OverlappedCardGroupDisplayWidget::cardHovered, this, &DeckCardZoneDisplayWidget::onHover); connect(displayWidget, &CardGroupDisplayWidget::cleanupRequested, this, @@ -113,9 +111,9 @@ void DeckCardZoneDisplayWidget::constructAppropriateWidget(QPersistentModelIndex indexToWidgetMap.insert(index, displayWidget); } else if (displayType == DisplayType::Flat) { auto *displayWidget = new FlatCardGroupDisplayWidget(cardGroupContainer, deckListModel, selectionModel, index, - effectiveZoneName, categoryName, activeGroupCriteria, + zoneName, categoryName, activeGroupCriteria, activeSortCriteria, subBannerOpacity, cardSizeWidget); - connect(displayWidget, &FlatCardGroupDisplayWidget::cardClicked, this, routeCardClick); + connect(displayWidget, &FlatCardGroupDisplayWidget::cardClicked, this, &DeckCardZoneDisplayWidget::onClick); connect(displayWidget, &FlatCardGroupDisplayWidget::cardHovered, this, &DeckCardZoneDisplayWidget::onHover); connect(displayWidget, &CardGroupDisplayWidget::cleanupRequested, this, &DeckCardZoneDisplayWidget::cleanupInvalidCardGroup); @@ -128,18 +126,24 @@ void DeckCardZoneDisplayWidget::constructAppropriateWidget(QPersistentModelIndex void DeckCardZoneDisplayWidget::displayCards() { - if (!trackedIndex.isValid()) { - return; - } + QSortFilterProxyModel proxy; + proxy.setSourceModel(deckListModel); + proxy.setSortRole(Qt::EditRole); + proxy.sort(DeckListModelColumns::CARD_NAME, Qt::AscendingOrder); - // Iterate the direct children of the tracked zone, keeping the tree view's row - // order (criteria groups first, then custom zones, both in the model's sort order). - QList rows; - for (int i = 0; i < deckListModel->rowCount(trackedIndex); ++i) { - rows.append(QPersistentModelIndex(deckListModel->index(i, 0, trackedIndex))); - } + // 1. trackedIndex is a source index → map it to proxy space + QModelIndex proxyParent = proxy.mapFromSource(trackedIndex); + + // 2. iterate children under the proxy parent + for (int i = 0; i < proxy.rowCount(proxyParent); ++i) { + QModelIndex proxyIndex = proxy.index(i, 0, proxyParent); + + // 3. map back to source + QModelIndex sourceIndex = proxy.mapToSource(proxyIndex); + + // 4. persist the source index + QPersistentModelIndex persistent(sourceIndex); - for (const QPersistentModelIndex &persistent : rows) { constructAppropriateWidget(persistent); } } diff --git a/cockatrice/src/interface/widgets/cards/deck_card_zone_display_widget.h b/cockatrice/src/interface/widgets/cards/deck_card_zone_display_widget.h index 53f3fa7cf..b426fca30 100644 --- a/cockatrice/src/interface/widgets/cards/deck_card_zone_display_widget.h +++ b/cockatrice/src/interface/widgets/cards/deck_card_zone_display_widget.h @@ -42,6 +42,7 @@ public: void addCardsToOverlapWidget(); public slots: + void onClick(QMouseEvent *event, const ExactCard &card); void onHover(const ExactCard &card); void cleanupInvalidCardGroup(CardGroupDisplayWidget *displayWidget); void constructAppropriateWidget(QPersistentModelIndex index); diff --git a/cockatrice/src/interface/widgets/cards/deck_preview_card_picture_widget.cpp b/cockatrice/src/interface/widgets/cards/deck_preview_card_picture_widget.cpp index 147143e7f..1614836fc 100644 --- a/cockatrice/src/interface/widgets/cards/deck_preview_card_picture_widget.cpp +++ b/cockatrice/src/interface/widgets/cards/deck_preview_card_picture_widget.cpp @@ -27,23 +27,18 @@ DeckPreviewCardPictureWidget::DeckPreviewCardPictureWidget(QWidget *parent, const QColor &textColor, const QColor &outlineColor, const int fontSize, - const Qt::Alignment alignment, - const bool _emitClickImmediately) + const Qt::Alignment alignment) : CardInfoPictureWithTextOverlayWidget(parent, hoverToZoomEnabled, raiseOnEnter, textColor, outlineColor, fontSize, - alignment), - emitClickImmediately(_emitClickImmediately) + alignment) { singleClickTimer = new QTimer(this); singleClickTimer->setSingleShot(true); - connect(singleClickTimer, &QTimer::timeout, this, [this]() { - emit imageClicked(lastMouseEvent, this); - emit imageSingleClicked(); - }); + connect(singleClickTimer, &QTimer::timeout, this, [this]() { emit imageClicked(lastMouseEvent, this); }); connect(&SettingsCache::instance().visualDeckStorage(), &VisualDeckStorageSettings::visualDeckStorageSelectionAnimationChanged, this, &CardInfoPictureWidget::setRaiseOnEnterEnabled); @@ -52,13 +47,8 @@ DeckPreviewCardPictureWidget::DeckPreviewCardPictureWidget(QWidget *parent, void DeckPreviewCardPictureWidget::mousePressEvent(QMouseEvent *event) { if (event->button() == Qt::LeftButton) { - if (emitClickImmediately) { - emit imageClicked(event, this); - emit imageSingleClicked(); - } else { - lastMouseEvent = event; - singleClickTimer->start(QApplication::doubleClickInterval()); - } + lastMouseEvent = event; + singleClickTimer->start(QApplication::doubleClickInterval()); } else { emit imageClicked(event, this); event->accept(); @@ -68,14 +58,7 @@ void DeckPreviewCardPictureWidget::mousePressEvent(QMouseEvent *event) void DeckPreviewCardPictureWidget::mouseDoubleClickEvent(QMouseEvent *event) { if (event->button() == Qt::LeftButton) { - if (emitClickImmediately) { - // Do not report a second single click for the second press of the - // double-click; the consumer maps the double-click to select+open. - lastMouseEvent = event; - emit imageDoubleClicked(event, this); - } else { - singleClickTimer->stop(); // Prevent single-click logic - emit imageDoubleClicked(lastMouseEvent, this); - } + singleClickTimer->stop(); // Prevent single-click logic + emit imageDoubleClicked(lastMouseEvent, this); } } diff --git a/cockatrice/src/interface/widgets/cards/deck_preview_card_picture_widget.h b/cockatrice/src/interface/widgets/cards/deck_preview_card_picture_widget.h index 7571bc256..154e938aa 100644 --- a/cockatrice/src/interface/widgets/cards/deck_preview_card_picture_widget.h +++ b/cockatrice/src/interface/widgets/cards/deck_preview_card_picture_widget.h @@ -20,38 +20,21 @@ class DeckPreviewCardPictureWidget final : public CardInfoPictureWithTextOverlay Q_OBJECT public: - /** - * @brief Constructs a DeckPreviewCardPictureWidget. - * @param parent The parent widget. - * @param hoverToZoomEnabled If this widget will spawn a larger widget when hovered over. - * @param raiseOnEnter If the widget raises its border when the mouse enters. - * @param textColor The color of the overlay text. - * @param outlineColor The color of the outline around the text. - * @param fontSize The font size of the overlay text. - * @param alignment The alignment of the text within the overlay. - * @param emitClickImmediately If true, a left click is reported immediately on click - * instead of after the double-click interval. Use this for selection surfaces - * where reacting to a double-click (select-and-open) would needlessly delay the - * single-click feedback. The double-click signal is still emitted. - */ explicit DeckPreviewCardPictureWidget(QWidget *parent, bool hoverToZoomEnabled = false, bool raiseOnEnter = false, const QColor &textColor = Qt::white, const QColor &outlineColor = Qt::black, int fontSize = 12, - Qt::Alignment alignment = Qt::AlignCenter, - bool _emitClickImmediately = false); + Qt::Alignment alignment = Qt::AlignCenter); signals: void imageClicked(QMouseEvent *event, DeckPreviewCardPictureWidget *instance); - void imageSingleClicked(); void imageDoubleClicked(QMouseEvent *event, DeckPreviewCardPictureWidget *instance); private: QTimer *singleClickTimer; QMouseEvent *lastMouseEvent = nullptr; // Store the last mouse event - bool emitClickImmediately; protected: void mousePressEvent(QMouseEvent *event) override; 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 483fc71b0..089abc5c8 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,5 @@ #include "abstract_analytics_panel_widget.h" -#include "../../pixel_map_generator.h" #include "deck_list_statistics_analyzer.h" #include @@ -21,7 +20,7 @@ AbstractAnalyticsPanelWidget::AbstractAnalyticsPanelWidget(QWidget *parent, Deck // config button configureButton = new QPushButton(this); - configureButton->setIcon(themePixmap(QStringLiteral("icons/cogwheel"))); + configureButton->setIcon(QPixmap("theme: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/card_database_view.cpp b/cockatrice/src/interface/widgets/deck_editor/card_database_view.cpp index 00388a3cd..7c782b074 100644 --- a/cockatrice/src/interface/widgets/deck_editor/card_database_view.cpp +++ b/cockatrice/src/interface/widgets/deck_editor/card_database_view.cpp @@ -90,13 +90,6 @@ void CardDatabaseView::decrementCard(const QString &zoneName) emit cardDecremented(currentCardName(), zoneName); } -void CardDatabaseView::setZoneMenuProvider(const std::function>()> &provider, - const std::function &newZoneHandler) -{ - zoneMenuProvider = provider; - this->newZoneHandler = newZoneHandler; -} - void CardDatabaseView::updateCard(const QModelIndex ¤t, const QModelIndex & /*previous*/) { if (!current.isValid()) { @@ -149,50 +142,6 @@ void CardDatabaseView::openCustomMenu(QPoint point) [this, card] { emit cardAdded(card->getName(), DECK_ZONE_SIDE); }); connect(selectPrinting, &QAction::triggered, this, &CardDatabaseView::selectPrintingClicked); - if (zoneMenuProvider) { - QMenu *addToZoneMenu = menu.addMenu(tr("Add to Zone")); - const auto zoneBoards = zoneMenuProvider(); - for (const QString &boardName : InnerDecklistNode::boardZoneNames()) { - // Boards with zones nest their children so no two menu entries - // share a visible name: "Maindeck ▸ { Maindeck (whole board), … }". - const QStringList customZones = [&zoneBoards, boardName] { - for (const auto &zoneBoard : zoneBoards) { - if (zoneBoard.first == boardName) { - return zoneBoard.second; - } - } - return QStringList(); - }(); - if (customZones.isEmpty()) { - QAction *action = addToZoneMenu->addAction(InnerDecklistNode::visibleNameFromName(boardName)); - connect(action, &QAction::triggered, this, - [this, card, boardName] { emit cardAdded(card->getName(), boardName); }); - } else { - QMenu *boardSubmenu = addToZoneMenu->addMenu(InnerDecklistNode::visibleNameFromName(boardName)); - QAction *wholeBoardAction = boardSubmenu->addAction(InnerDecklistNode::visibleNameFromName(boardName)); - connect(wholeBoardAction, &QAction::triggered, this, - [this, card, boardName] { emit cardAdded(card->getName(), boardName); }); - for (const QString &zoneName : customZones) { - QAction *action = boardSubmenu->addAction(zoneName); - connect(action, &QAction::triggered, this, - [this, card, zoneName] { emit cardAdded(card->getName(), zoneName); }); - } - } - } - - if (newZoneHandler) { - addToZoneMenu->addSeparator(); - - QAction *newZoneAction = addToZoneMenu->addAction(tr("Create &new zone...")); - connect(newZoneAction, &QAction::triggered, this, [this, card] { - const QString zoneName = newZoneHandler(); - if (!zoneName.isEmpty()) { - emit cardAdded(card->getName(), zoneName); - } - }); - } - } - if (canBeCommander(*card)) { QAction *edhRecCommander = menu.addAction(tr("Show on EDHRec (Commander)")); connect(edhRecCommander, &QAction::triggered, this, [this, card] { emit edhrecClicked(card, true); }); diff --git a/cockatrice/src/interface/widgets/deck_editor/card_database_view.h b/cockatrice/src/interface/widgets/deck_editor/card_database_view.h index 668444199..175ec12b9 100644 --- a/cockatrice/src/interface/widgets/deck_editor/card_database_view.h +++ b/cockatrice/src/interface/widgets/deck_editor/card_database_view.h @@ -4,7 +4,6 @@ #include "../../key_signals.h" #include -#include #include class CardDatabaseModel; @@ -20,13 +19,6 @@ class CardDatabaseView : public QTreeView KeySignals searchKeySignals; CardDatabaseDisplayModel *databaseDisplayModel; - /// Provides the custom zones available in the current deck, grouped by board zone. - /// The list contains (board zone name, custom zone names) pairs for every board. - std::function>()> zoneMenuProvider; - /// Handler invoked when the user picks "New zone..." from the add-to-zone menu. - /// Returns the name of the created zone, or an empty string if creation was cancelled. - std::function newZoneHandler; - public: explicit CardDatabaseView(QWidget *parent, CardDatabaseDisplayModel *model); @@ -41,17 +33,6 @@ public: return &searchKeySignals; } - /** - * @brief Sets the provider used to populate the "Add to zone" submenu of the context menu. - * If no provider is set, the submenu is not shown. - * - * @param provider Returns the custom zones of the current deck, grouped by board zone - * @param newZoneHandler Creates a new custom zone and returns its name, or an empty string - * if creation was cancelled. The menu entry is hidden when not provided. - */ - void setZoneMenuProvider(const std::function>()> &provider, - const std::function &newZoneHandler); - signals: void cardChanged(const QString &cardName); diff --git a/cockatrice/src/interface/widgets/deck_editor/deck_editor_card_database_dock_widget.cpp b/cockatrice/src/interface/widgets/deck_editor/deck_editor_card_database_dock_widget.cpp index 6269f0323..2a491de4f 100644 --- a/cockatrice/src/interface/widgets/deck_editor/deck_editor_card_database_dock_widget.cpp +++ b/cockatrice/src/interface/widgets/deck_editor/deck_editor_card_database_dock_widget.cpp @@ -1,12 +1,5 @@ #include "deck_editor_card_database_dock_widget.h" -#include "../../../interface/widgets/tabs/abstract_tab_deck_editor.h" -#include "card_database_view.h" -#include "deck_state_manager.h" -#include "deck_zone_dialog.h" - -#include - DeckEditorCardDatabaseDockWidget::DeckEditorCardDatabaseDockWidget(AbstractTabDeckEditor *parent) : QDockWidget(parent) { setObjectName("databaseDisplayDock"); @@ -22,27 +15,6 @@ void DeckEditorCardDatabaseDockWidget::createDatabaseDisplayDock(AbstractTabDeck { databaseDisplayWidget = new DeckEditorDatabaseDisplayWidget(this, deckEditor->databaseModel); - databaseDisplayWidget->getDatabaseView()->setZoneMenuProvider( - [deckEditor]() -> QList> { - QList> result; - auto *deckListModel = deckEditor->deckStateManager->getModel(); - for (const QString &boardName : InnerDecklistNode::boardZoneNames()) { - result.append({boardName, deckListModel->getCustomZoneNames(boardName)}); - } - return result; - }, - [this, deckEditor]() -> QString { - QString boardName; - const QString zoneName = - DeckZoneDialog::promptForNewZone(this, {}, &boardName, [deckEditor](const QString &candidate) { - return deckEditor->deckStateManager->validateNewZoneName(candidate); - }); - if (!zoneName.isEmpty()) { - deckEditor->deckStateManager->createCustomZone(boardName, zoneName); - } - return zoneName; - }); - auto *frame = new QVBoxLayout; frame->setObjectName("databaseDisplayFrame"); frame->addWidget(databaseDisplayWidget); 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 a83e25f5f..9da821813 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 @@ -11,10 +11,8 @@ #include #include #include -#include #include #include -#include DeckEditorDatabaseDisplayWidget::DeckEditorDatabaseDisplayWidget(QWidget *parent, CardDatabaseModel *databaseModel) : QWidget(parent) @@ -30,7 +28,7 @@ DeckEditorDatabaseDisplayWidget::DeckEditorDatabaseDisplayWidget(QWidget *parent searchEdit->setPlaceholderText(tr("Search by card name (or search expressions)")); searchEdit->setClearButtonEnabled(true); searchEdit->addAction(loadColorAdjustedPixmap("theme:icons/search"), QLineEdit::LeadingPosition); - auto help = searchEdit->addAction(themePixmap(QStringLiteral("icons/info")), QLineEdit::TrailingPosition); + auto help = searchEdit->addAction(QPixmap("theme:icons/info"), QLineEdit::TrailingPosition); setFocusProxy(searchEdit); setFocusPolicy(Qt::ClickFocus); @@ -42,17 +40,6 @@ DeckEditorDatabaseDisplayWidget::DeckEditorDatabaseDisplayWidget(QWidget *parent databaseDisplayModel->setSourceModel(databaseModel); databaseDisplayModel->setFilterKeyColumn(0); - const auto applyCardSearchLanguage = [this]() { - const CardsDisplaySettings &cardsDisplay = SettingsCache::instance().cardsDisplay(); - databaseDisplayModel->setSearchLanguage(CardSearchLanguage{ - cardsDisplay.getCardLang(), static_cast(cardsDisplay.getCardSearchLanguage())}); - }; - applyCardSearchLanguage(); - connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::cardLangChanged, this, - applyCardSearchLanguage); - connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::cardSearchLanguageChanged, this, - applyCardSearchLanguage); - databaseView = new CardDatabaseView(this, databaseDisplayModel); databaseView->setObjectName("databaseView"); databaseView->setFocusProxy(searchEdit); @@ -72,13 +59,13 @@ DeckEditorDatabaseDisplayWidget::DeckEditorDatabaseDisplayWidget(QWidget *parent &DeckEditorDatabaseDisplayWidget::onRelatedCardClicked); aAddCard = new QAction(QString(), this); - aAddCard->setIcon(themePixmap(QStringLiteral("icons/arrow_right_green"))); + aAddCard->setIcon(QPixmap("theme: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(themePixmap(QStringLiteral("icons/arrow_right_blue"))); + aAddCardToSideboard->setIcon(QPixmap("theme: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 4b141e255..fc53b296f 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 @@ -2,26 +2,19 @@ #include "../../../client/settings/cache_settings.h" #include "../../../client/settings/shortcuts_settings.h" -#include "../../pixel_map_generator.h" -#include "../playmat/playmat_settings_dialog.h" #include "../settings_page/user_interface_settings_page.h" #include "../tabs/api/commander_spellbook/commander_bracket_widget.h" #include "deck_list_style_proxy.h" #include "deck_state_manager.h" -#include "deck_zone_dialog.h" #include #include #include #include -#include -#include #include #include #include -#include #include -#include #include #include @@ -193,25 +186,25 @@ void DeckEditorDeckDockWidget::createDeckDock() &DeckEditorDeckDockWidget::applyActiveGroupCriteria); aIncrement = new QAction(QString(), this); - aIncrement->setIcon(themePixmap(QStringLiteral("icons/increment"))); + aIncrement->setIcon(QPixmap("theme:icons/increment")); connect(aIncrement, &QAction::triggered, this, &DeckEditorDeckDockWidget::actIncrementSelection); auto *tbIncrement = new QToolButton(this); tbIncrement->setDefaultAction(aIncrement); aDecrement = new QAction(QString(), this); - aDecrement->setIcon(themePixmap(QStringLiteral("icons/decrement"))); + aDecrement->setIcon(QPixmap("theme:icons/decrement")); connect(aDecrement, &QAction::triggered, this, &DeckEditorDeckDockWidget::actDecrementSelection); auto *tbDecrement = new QToolButton(this); tbDecrement->setDefaultAction(aDecrement); aRemoveCard = new QAction(QString(), this); - aRemoveCard->setIcon(themePixmap(QStringLiteral("icons/remove_row"))); + aRemoveCard->setIcon(QPixmap("theme: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(themePixmap(QStringLiteral("icons/swap"))); + aSwapCard->setIcon(QPixmap("theme:icons/swap")); connect(aSwapCard, &QAction::triggered, this, &DeckEditorDeckDockWidget::actSwapSelection); auto *tbSwapCard = new QToolButton(this); tbSwapCard->setDefaultAction(aSwapCard); @@ -235,18 +228,10 @@ void DeckEditorDeckDockWidget::createDeckDock() upperLayout->addWidget(bannerCardLabel, 4, 0); upperLayout->addWidget(bannerCardComboBox, 4, 1); - playmatLabel = new QLabel(); - playmatLabel->setObjectName("playmatLabel"); - playmatLabel->setText(tr("Playmat")); - playmatSettingsButton = new QPushButton(tr("Edit Playmat...")); - connect(playmatSettingsButton, &QPushButton::clicked, this, &DeckEditorDeckDockWidget::openPlaymatSettings); - upperLayout->addWidget(playmatLabel, 5, 0); - upperLayout->addWidget(playmatSettingsButton, 5, 1); + upperLayout->addWidget(deckTagsDisplayWidget, 5, 1); - upperLayout->addWidget(deckTagsDisplayWidget, 6, 1); - - upperLayout->addWidget(activeGroupCriteriaLabel, 7, 0); - upperLayout->addWidget(activeGroupCriteriaComboBox, 7, 1); + upperLayout->addWidget(activeGroupCriteriaLabel, 6, 0); + upperLayout->addWidget(activeGroupCriteriaComboBox, 6, 1); hashLabel1 = new QLabel(); hashLabel1->setObjectName("hashLabel1"); @@ -345,9 +330,7 @@ ExactCard DeckEditorDeckDockWidget::getCurrentCard() if (!current.isValid()) { return {}; } - // The display role holds the localized card name; the edit role always carries the - // canonical English name needed to look the card up in the database. - const QString cardName = current.siblingAtColumn(DeckListModelColumns::CARD_NAME).data(Qt::EditRole).toString(); + const QString cardName = current.siblingAtColumn(DeckListModelColumns::CARD_NAME).data().toString(); const QString cardProviderID = current.siblingAtColumn(DeckListModelColumns::CARD_PROVIDER_ID).data().toString(); const QModelIndex gparent = current.parent().parent(); @@ -457,35 +440,6 @@ void DeckEditorDeckDockWidget::writeBannerCard(int index) deckStateManager->setBannerCard(bannerCard); } -void DeckEditorDeckDockWidget::openPlaymatSettings() -{ - PlaymatInfo current = deckStateManager->getMetadata().playmat; - - PlaymatSettingsDialog dialog(current.card, current.params, this); - if (dialog.exec() == QDialog::Accepted) { - CardRef newCard = dialog.card(); - PlaymatParams newParams = dialog.params(); - - if (newCard.isEmpty()) { - deckStateManager->setPlaymat(PlaymatInfo{}); - } else { - deckStateManager->setPlaymat({newCard, newParams}); - } - - updatePlaymatLabel(); - } -} - -void DeckEditorDeckDockWidget::updatePlaymatLabel() -{ - CardRef playmat = deckStateManager->getMetadata().playmat.card; - if (playmat.isEmpty()) { - playmatSettingsButton->setText(tr("Edit Playmat...")); - } else { - playmatSettingsButton->setText(tr("Edit Playmat (%1)").arg(playmat.name)); - } -} - void DeckEditorDeckDockWidget::applyActiveGroupCriteria() { getModel()->setActiveGroupCriteria( @@ -520,11 +474,8 @@ void DeckEditorDeckDockWidget::syncBannerCardComboBoxSelectionWithDeck() void DeckEditorDeckDockWidget::setSelectedIndex(const QModelIndex &newCardIndex, bool preserveWidgetFocus) { - const QModelIndex proxyIndex = proxy->mapFromSource(newCardIndex); - deckView->clearSelection(); - deckView->setCurrentIndex(proxyIndex); - deckView->scrollTo(proxyIndex); + deckView->setCurrentIndex(newCardIndex); recursiveExpand(newCardIndex); if (!preserveWidgetFocus) { @@ -546,7 +497,6 @@ void DeckEditorDeckDockWidget::syncDisplayWidgetsToModel() syncBannerCardComboBoxSelectionWithDeck(); updateBannerCardComboBox(); bannerCardComboBox->blockSignals(false); - updatePlaymatLabel(); updateHash(); formatComboBox->blockSignals(true); @@ -781,213 +731,14 @@ void DeckEditorDeckDockWidget::offsetCountAtIndex(const QModelIndex &idx, bool i void DeckEditorDeckDockWidget::decklistCustomMenu(QPoint point) { - const QModelIndex sourceIndex = proxy->mapToSource(deckView->indexAt(point)); - QMenu menu; - const bool isCustomZoneRow = sourceIndex.isValid() && sourceIndex.data(DeckRoles::IsCustomZoneRole).toBool(); - const bool isBoardZoneRow = sourceIndex.isValid() && !isCustomZoneRow && !sourceIndex.parent().isValid(); - const bool isCardRow = - sourceIndex.isValid() && !isCustomZoneRow && !isBoardZoneRow && !getModel()->hasChildren(sourceIndex); - - // Walk the row up to its top-level node to find the hosting board. Cards in - // the tokens board cannot be moved (moveCardToZone bails for it), so the - // move menu is skipped for them. - QString currentBoardName; - QModelIndex board = sourceIndex.parent(); - while (board.isValid() && board.parent().isValid()) { - board = board.parent(); - } - if (board.isValid()) { - currentBoardName = board.siblingAtColumn(DeckListModelColumns::CARD_NAME).data(Qt::EditRole).toString(); - } - - if (isCardRow) { - if (currentBoardName != DECK_ZONE_TOKENS) { - addMoveToZoneMenu(&menu, sourceIndex, currentBoardName); - menu.addSeparator(); - } - } else if (isCustomZoneRow) { - const QString zoneName = - sourceIndex.siblingAtColumn(DeckListModelColumns::CARD_NAME).data(Qt::EditRole).toString(); - - QAction *renameAction = menu.addAction(tr("&Rename zone...")); - connect(renameAction, &QAction::triggered, this, [this, zoneName] { - // The unchanged name must not validate as a duplicate. - const QString newName = - DeckZoneDialog::promptForRename(this, zoneName, [this, zoneName](const QString &candidate) { - return candidate == zoneName ? QString() : deckStateManager->validateNewZoneName(candidate); - }); - if (!newName.isEmpty() && newName != zoneName) { - deckStateManager->renameCustomZone(zoneName, newName); - } - }); - - QMenu *boardMenu = menu.addMenu(tr("Change &board")); - addChangeBoardMenu(boardMenu, zoneName); - - QAction *deleteAction = menu.addAction(tr("&Delete zone")); - const bool zoneHasCards = getModel()->hasChildren(sourceIndex); - deleteAction->setEnabled(!zoneHasCards); - if (zoneHasCards) { - deleteAction->setToolTip(tr("Move or remove all cards first.")); - menu.setToolTipsVisible(true); - } - connect(deleteAction, &QAction::triggered, this, [this, zoneName] { - const auto result = - QMessageBox::warning(this, tr("Delete zone"), tr("Delete the zone \"%1\"?").arg(zoneName), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - if (result == QMessageBox::Yes) { - deckStateManager->removeCustomZone(zoneName); - } - }); - menu.addSeparator(); - } else if (isBoardZoneRow) { - const QString boardName = - sourceIndex.siblingAtColumn(DeckListModelColumns::CARD_NAME).data(Qt::EditRole).toString(); - // Tokens cannot host custom zones, so only offer the action on real boards. - const bool canHostCustomZones = - boardName == DECK_ZONE_MAIN || boardName == DECK_ZONE_SIDE || boardName == DECK_ZONE_MAYBEBOARD; - if (canHostCustomZones) { - addNewZoneAction(&menu, boardName); - menu.addSeparator(); - } - } else if (!sourceIndex.isValid()) { - addNewZoneAction(&menu); - menu.addSeparator(); - } - QAction *selectPrinting = menu.addAction(tr("Select Printing")); connect(selectPrinting, &QAction::triggered, deckEditor, &AbstractTabDeckEditor::showPrintingSelector); menu.exec(deckView->mapToGlobal(point)); } -void DeckEditorDeckDockWidget::addMoveToZoneMenu(QMenu *menu, - const QModelIndex &sourceCardIndex, - const QString ¤tBoardName) -{ - // The card's current *zone*, derived with the same ancestor walk as - // DeckStateManager::moveCardToZone (nearest custom-zone ancestor, else the - // top-level board/zone): a card inside "Removal" under the maindeck lives in - // "Removal", not "main". Comparing against that instead of the board keeps - // the enabled state and the same-zone no-op consistent with the move logic. - QString currentZoneName; - for (QModelIndex ancestor = sourceCardIndex.parent(); ancestor.isValid(); ancestor = ancestor.parent()) { - if (ancestor.data(DeckRoles::IsCustomZoneRole).toBool() || !ancestor.parent().isValid()) { - currentZoneName = ancestor.siblingAtColumn(DeckListModelColumns::CARD_NAME).data(Qt::EditRole).toString(); - break; - } - } - - const auto addMoveAction = [this, sourceCardIndex](QMenu *targetMenu, const QString &targetZoneName, - const QString &label, bool enabled) { - QAction *action = targetMenu->addAction(label); - action->setEnabled(enabled); - if (enabled) { - connect(action, &QAction::triggered, this, [this, sourceCardIndex, targetZoneName] { - deckStateManager->moveCardToZone(sourceCardIndex, targetZoneName); - }); - } - }; - - const auto tree = deckStateManager->getDeckListShared()->getTree(); - - QMenu *moveMenu = menu->addMenu(tr("Move to &zone")); - - for (const QString &boardName : InnerDecklistNode::boardZoneNames()) { - const QString boardLabel = InnerDecklistNode::visibleNameFromName(boardName); - const auto customZones = tree->getCustomZones(boardName); - - // Boards with zones nest their children so no two menu entries share a - // visible name: "Maindeck ▸ { Maindeck (whole board), Removal, … }". - // The board the card already lives on is marked instead of offered. - if (!customZones.isEmpty()) { - QMenu *boardSubmenu = moveMenu->addMenu(boardLabel); - addMoveAction(boardSubmenu, boardName, boardLabel, boardName != currentZoneName); - for (const auto *customZone : customZones) { - addMoveAction(boardSubmenu, customZone->getName(), customZone->getName(), - customZone->getName() != currentZoneName); - } - } else { - addMoveAction(moveMenu, boardName, boardLabel, boardName != currentZoneName); - } - } - - moveMenu->addSeparator(); - - QAction *newZoneAction = moveMenu->addAction(tr("Create new zone and move &here...")); - connect(newZoneAction, &QAction::triggered, this, [this, sourceCardIndex, currentBoardName, currentZoneName] { - // Resolve the card's identity before creating the zone: - // createNewCustomZone rebuilds the model tree, so sourceCardIndex's - // internal pointer is freed by the time it would be used. - const QString cardName = - sourceCardIndex.siblingAtColumn(DeckListModelColumns::CARD_NAME).data(Qt::EditRole).toString(); - const QString providerId = - sourceCardIndex.siblingAtColumn(DeckListModelColumns::CARD_PROVIDER_ID).data(Qt::DisplayRole).toString(); - const QString collectorNumber = sourceCardIndex.siblingAtColumn(DeckListModelColumns::CARD_COLLECTOR_NUMBER) - .data(Qt::DisplayRole) - .toString(); - - const QString zoneName = createNewCustomZone(currentBoardName); - if (!zoneName.isEmpty()) { - // Re-find the card: the old index is no longer safe since rows were - // rebuilt. Mirror DeckStateManager::decrementCard's re-find pattern. - const QModelIndex refreshed = getModel()->findCard(cardName, currentZoneName, providerId, collectorNumber); - if (refreshed.isValid()) { - deckStateManager->moveCardToZone(refreshed, zoneName); - } - } - }); -} - -void DeckEditorDeckDockWidget::addChangeBoardMenu(QMenu *menu, const QString &zoneName) -{ - const auto tree = deckStateManager->getDeckListShared()->getTree(); - for (const QString &boardName : InnerDecklistNode::boardZoneNames()) { - QAction *action = menu->addAction(InnerDecklistNode::visibleNameFromName(boardName)); - - // The board currently holding the zone is marked instead of offered. - // Duplicate names cannot come up through the editor, so this doubles as - // the uniqueness guard for imported decks. - bool holdsTheZone = false; - for (const auto *customZone : tree->getCustomZones(boardName)) { - if (customZone->getName() == zoneName) { - holdsTheZone = true; - break; - } - } - if (holdsTheZone) { - action->setCheckable(true); - action->setChecked(true); - continue; - } - - connect(action, &QAction::triggered, this, - [this, zoneName, boardName] { deckStateManager->moveCustomZone(zoneName, boardName); }); - } -} - -void DeckEditorDeckDockWidget::addNewZoneAction(QMenu *menu, const QString &initialBoardName) -{ - QAction *newZoneAction = menu->addAction(tr("Create &new zone...")); - connect(newZoneAction, &QAction::triggered, this, - [this, initialBoardName] { createNewCustomZone(initialBoardName); }); -} - -QString DeckEditorDeckDockWidget::createNewCustomZone(const QString &initialBoardName) -{ - QString boardName; - const QString zoneName = - DeckZoneDialog::promptForNewZone(this, initialBoardName, &boardName, [this](const QString &candidate) { - return deckStateManager->validateNewZoneName(candidate); - }); - if (!zoneName.isEmpty()) { - deckStateManager->createCustomZone(boardName, zoneName); - } - return zoneName; -} - void DeckEditorDeckDockWidget::refreshShortcuts() { ShortcutsSettings &shortcuts = SettingsCache::instance().shortcuts(); diff --git a/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.h b/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.h index 1e5f4e677..540199f0d 100644 --- a/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.h +++ b/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.h @@ -15,16 +15,11 @@ #include "deck_list_history_manager_widget.h" #include "deck_list_style_proxy.h" -#include -#include #include #include -#include -#include #include #include #include -#include class CommanderBracketWidget; class DeckListModel; @@ -38,8 +33,6 @@ public: DeckListStyleProxy *proxy; QTreeView *deckView; QComboBox *bannerCardComboBox; - QLabel *playmatLabel; - QPushButton *playmatSettingsButton; void createDeckDock(); ExactCard getCurrentCard(); void retranslateUi(); @@ -103,19 +96,12 @@ private: [[nodiscard]] QModelIndexList getSelectedCardNodeSourceIndices() const; void offsetCountAtIndex(const QModelIndex &idx, bool isIncrement); - void addMoveToZoneMenu(QMenu *menu, const QModelIndex &sourceCardIndex, const QString ¤tBoardName); - void addChangeBoardMenu(QMenu *menu, const QString &zoneName); - QString createNewCustomZone(const QString &initialBoardName = {}); - void addNewZoneAction(QMenu *menu, const QString &initialBoardName = {}); - private slots: void decklistCustomMenu(QPoint point); void updateCard(QModelIndex, const QModelIndex ¤t); void writeName(); void writeComments(); void writeBannerCard(int); - void openPlaymatSettings(); - void updatePlaymatLabel(); void applyActiveGroupCriteria(); void setSelectedIndex(const QModelIndex &newCardIndex, bool preserveWidgetFocus); void updateHash(); 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 c6b6e4416..2d4fb60e8 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 @@ -4,7 +4,6 @@ #include "../../../client/settings/shortcuts_settings.h" #include "../../../filters/filter_builder.h" #include "../../../filters/filter_tree_model.h" -#include "../../pixel_map_generator.h" #include #include @@ -43,11 +42,11 @@ void DeckEditorFilterDockWidget::createFiltersDock() connect(filterBuilder, &FilterBuilder::add, filterModel, &FilterTreeModel::addFilter); aClearFilterOne = new QAction(QString(), this); - aClearFilterOne->setIcon(themePixmap(QStringLiteral("icons/decrement"))); + aClearFilterOne->setIcon(QPixmap("theme:icons/decrement")); connect(aClearFilterOne, &QAction::triggered, this, &DeckEditorFilterDockWidget::actClearFilterOne); aClearFilterAll = new QAction(QString(), this); - aClearFilterAll->setIcon(themePixmap(QStringLiteral("icons/clearsearch"))); + aClearFilterAll->setIcon(QPixmap("theme: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 c93f12b34..cef459752 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,5 @@ #include "deck_list_history_manager_widget.h" -#include "../../pixel_map_generator.h" #include "deck_state_manager.h" DeckListHistoryManagerWidget::DeckListHistoryManagerWidget(DeckStateManager *_deckStateManager, @@ -11,7 +10,7 @@ DeckListHistoryManagerWidget::DeckListHistoryManagerWidget(DeckStateManager *_de layout = new QHBoxLayout(this); aUndo = new QAction(QString(), this); - aUndo->setIcon(themePixmap(QStringLiteral("icons/arrow_undo"))); + aUndo->setIcon(QPixmap("theme:icons/arrow_undo")); aUndo->setShortcut(QKeySequence::Undo); aUndo->setShortcutContext(Qt::ApplicationShortcut); connect(aUndo, &QAction::triggered, this, &DeckListHistoryManagerWidget::doUndo); @@ -20,7 +19,7 @@ DeckListHistoryManagerWidget::DeckListHistoryManagerWidget(DeckStateManager *_de undoButton->setDefaultAction(aUndo); aRedo = new QAction(QString(), this); - aRedo->setIcon(themePixmap(QStringLiteral("icons/arrow_redo"))); + aRedo->setIcon(QPixmap("theme:icons/arrow_redo")); aRedo->setShortcut(QKeySequence::Redo); aRedo->setShortcutContext(Qt::ApplicationShortcut); connect(aRedo, &QAction::triggered, this, &DeckListHistoryManagerWidget::doRedo); @@ -32,7 +31,7 @@ DeckListHistoryManagerWidget::DeckListHistoryManagerWidget(DeckStateManager *_de layout->addWidget(redoButton); historyButton = new SettingsButtonWidget(this); - historyButton->setButtonIcon(themePixmap(QStringLiteral("icons/arrow_history"))); + historyButton->setButtonIcon(QPixmap("theme:icons/arrow_history")); historyLabel = new QLabel(this); diff --git a/cockatrice/src/interface/widgets/deck_editor/deck_state_manager.cpp b/cockatrice/src/interface/widgets/deck_editor/deck_state_manager.cpp index cfe989e9f..f8fb450ce 100644 --- a/cockatrice/src/interface/widgets/deck_editor/deck_state_manager.cpp +++ b/cockatrice/src/interface/widgets/deck_editor/deck_state_manager.cpp @@ -1,20 +1,12 @@ #include "deck_state_manager.h" -#include "../../../client/settings/cache_settings.h" - #include #include -#include -#include DeckStateManager::DeckStateManager(QObject *parent) : QObject(parent), deckList(QSharedPointer(new DeckList)), deckListModel(new DeckListModel(this, deckList)), historyManager(new DeckListHistoryManager(this)) { - deckListModel->setDisplayLanguage(SettingsCache::instance().cardsDisplay().getCardLang()); - connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::cardLangChanged, deckListModel, - [this](const QString &lang) { deckListModel->setDisplayLanguage(lang); }); - connect(historyManager, &DeckListHistoryManager::undoRedoStateChanged, this, [this] { setModified(true); emit historyChanged(); @@ -150,19 +142,6 @@ void DeckStateManager::setBannerCard(const CardRef &bannerCard) doMetadataModified(); } -void DeckStateManager::setPlaymat(const PlaymatInfo &playmat) -{ - PlaymatInfo previous = deckList->getPlaymat(); - if (previous == playmat) { - return; - } - - requestHistorySave(tr("Set playmat to %1").arg(playmat.card.name)); - deckList->setPlaymat(playmat); - - doMetadataModified(); -} - void DeckStateManager::setTags(const QStringList &tags) { QStringList previous = deckList->getTags(); @@ -267,10 +246,7 @@ bool DeckStateManager::swapCardAtIndex(const QModelIndex &idx) return false; } - // The display role holds the localized card name; the edit role always carries the - // canonical English name needed to look the card up in the database. - QString displayCardName = idx.siblingAtColumn(DeckListModelColumns::CARD_NAME).data().toString(); - QString cardName = idx.siblingAtColumn(DeckListModelColumns::CARD_NAME).data(Qt::EditRole).toString(); + QString cardName = idx.siblingAtColumn(DeckListModelColumns::CARD_NAME).data().toString(); QString providerId = idx.siblingAtColumn(DeckListModelColumns::CARD_PROVIDER_ID).data().toString(); QModelIndex gparent = idx.parent().parent(); @@ -287,7 +263,7 @@ bool DeckStateManager::swapCardAtIndex(const QModelIndex &idx) QString reason = tr("Moved to %1 1 × \"%2\" (%3)") // .arg(otherZoneName) - .arg(displayCardName) + .arg(cardName) .arg(providerId); return modifyDeck(reason, [&idx, &cardName, &providerId, &otherZoneName](auto model) { @@ -301,8 +277,9 @@ bool DeckStateManager::removeCardAtIndex(const QModelIndex &idx) return false; } - QString reason = - tr("Removed \"%1\" (all copies)").arg(idx.siblingAtColumn(DeckListModelColumns::CARD_NAME).data().toString()); + QString cardName = idx.siblingAtColumn(DeckListModelColumns::CARD_NAME).data().toString(); + + QString reason = tr("Removed \"%1\" (all copies)").arg(cardName); return modifyDeck(reason, [&idx](auto model) { return model->removeRow(idx.row(), idx.parent()); }); } @@ -317,170 +294,6 @@ bool DeckStateManager::decrementCountAtIndex(const QModelIndex &idx) return offsetCountAtIndex(idx, -1); } -bool DeckStateManager::moveCardToZone(const QModelIndex &idx, const QString &targetZoneName) -{ - if (!idx.isValid()) { - return false; - } - - // Only actual card rows can be moved. Group or zone rows report an - // aggregate amount and must never be deleted by this operation. - if (!idx.data(DeckRoles::IsCardRole).toBool()) { - return false; - } - - QString cardName = idx.siblingAtColumn(DeckListModelColumns::CARD_NAME).data(Qt::EditRole).toString(); - QString providerId = idx.siblingAtColumn(DeckListModelColumns::CARD_PROVIDER_ID).data(Qt::DisplayRole).toString(); - int copies = idx.siblingAtColumn(DeckListModelColumns::CARD_AMOUNT).data(Qt::EditRole).toInt(); - - if (copies <= 0) { - return false; - } - - // Tokens only live in the tokens zone and cannot be moved into decks. - CardInfoPtr info = CardDatabaseManager::query()->getCardInfo(cardName); - if (info && info->getIsToken()) { - return false; - } - - // Determine the zone the card currently lives in: the enclosing custom - // zone, or the nearest top-level zone (board zone or legacy zone). - QString currentZoneName; - for (QModelIndex ancestor = idx.parent(); ancestor.isValid(); ancestor = ancestor.parent()) { - bool isCustomZone = ancestor.data(DeckRoles::IsCustomZoneRole).toBool(); - if (isCustomZone || !ancestor.parent().isValid()) { - currentZoneName = ancestor.siblingAtColumn(DeckListModelColumns::CARD_NAME).data(Qt::EditRole).toString(); - break; - } - } - - if (currentZoneName == targetZoneName) { - return false; - } - - QString reason = tr("Moved %1 × \"%2\" (%3) to %4") - .arg(copies) - .arg(cardName) - .arg(providerId) - .arg(InnerDecklistNode::visibleNameFromName(targetZoneName)); - - return modifyDeck(reason, [&idx, &cardName, &providerId, &targetZoneName, copies](auto model) { - if (!model->removeRow(idx.row(), idx.parent())) { - return false; - } - - if (ExactCard card = CardDatabaseManager::query()->getCard({cardName, providerId})) { - for (int i = 0; i < copies; ++i) { - model->addCard(card, targetZoneName); - } - } else { - for (int i = 0; i < copies; ++i) { - model->addPreferredPrintingCard(cardName, targetZoneName, true); - } - } - - return true; - }); -} - -bool DeckStateManager::createCustomZone(const QString &boardZoneName, const QString &zoneName) -{ - const QString trimmedZoneName = zoneName.trimmed(); - if (trimmedZoneName.isEmpty()) { - return false; - } - - QString reason = - tr("Created zone \"%1\" in %2").arg(trimmedZoneName, InnerDecklistNode::visibleNameFromName(boardZoneName)); - - return modifyTree(reason, [&boardZoneName, &trimmedZoneName](DecklistNodeTree *tree) { - return tree->addCustomZone(boardZoneName, trimmedZoneName) != nullptr; - }); -} - -bool DeckStateManager::renameCustomZone(const QString &oldZoneName, const QString &newZoneName) -{ - const QString trimmedNewZoneName = newZoneName.trimmed(); - if (trimmedNewZoneName.isEmpty() || oldZoneName == trimmedNewZoneName) { - return false; - } - - QString reason = tr("Renamed zone \"%1\" to \"%2\"").arg(oldZoneName, trimmedNewZoneName); - - return modifyTree(reason, [&oldZoneName, &trimmedNewZoneName](DecklistNodeTree *tree) { - return tree->renameCustomZone(oldZoneName, trimmedNewZoneName); - }); -} - -bool DeckStateManager::moveCustomZone(const QString &zoneName, const QString &newBoardZoneName) -{ - const auto *tree = deckList->getTree(); - - // Locate the zone through the tree's own lookup, which walks every top-level - // zone (not just the standard boards) and covers the same-board no-op below. - const auto *zone = tree->findCustomZoneByName(zoneName); - if (!zone) { - return false; - } - - // Same-board moves are no-ops and must not pollute the history. - const QString currentBoardName = zone->getParent() ? zone->getParent()->getName() : QString(); - if (currentBoardName == newBoardZoneName) { - return true; - } - - // Zone names are deck-unique among zones created through this manager, so a - // same-named zone on the target board can only come from an imported deck. - // Refuse the move instead of silently stacking same-named zones. - for (const auto *targetZone : tree->getCustomZones(newBoardZoneName)) { - if (targetZone->getName() == zoneName) { - return false; - } - } - - QString reason = - tr("Moved zone \"%1\" to %2").arg(zoneName, InnerDecklistNode::visibleNameFromName(newBoardZoneName)); - - return modifyTree(reason, [&zoneName, &newBoardZoneName](DecklistNodeTree *tree) { - return tree->moveCustomZone(zoneName, newBoardZoneName); - }); -} - -bool DeckStateManager::removeCustomZone(const QString &zoneName) -{ - QString reason = tr("Deleted zone \"%1\"").arg(zoneName); - - return modifyTree(reason, [&zoneName](DecklistNodeTree *tree) { return tree->removeCustomZone(zoneName); }); -} - -QString DeckStateManager::validateNewZoneName(const QString &zoneName) const -{ - if (zoneName.trimmed().isEmpty()) { - return tr("Enter a zone name."); - } - - const QString trimmedZoneName = zoneName.trimmed(); - - // The standard zone names are reserved even before they exist. - if (trimmedZoneName == DECK_ZONE_MAIN || trimmedZoneName == DECK_ZONE_SIDE || - trimmedZoneName == DECK_ZONE_MAYBEBOARD || trimmedZoneName == DECK_ZONE_TOKENS) { - return tr("This name is reserved."); - } - - const auto *tree = deckList->getTree(); - - // Reuse the tree's own uniqueness contract: any top-level zone and any - // custom zone on *every* board claims the name (hasZoneName also reserves - // the standard board names, which we already rejected with a dedicated - // message above). Scanning only the standard boards here would miss a - // custom zone an imported deck carries under `tokens`. - if (tree->hasZoneName(trimmedZoneName)) { - return tr("A zone with this name already exists."); - } - - return {}; -} - bool DeckStateManager::offsetCountAtIndex(const QModelIndex &idx, int offset) { if (!idx.isValid()) { @@ -541,25 +354,6 @@ void DeckStateManager::requestHistorySave(const QString &reason) historyManager->save(deckList->createMemento(reason)); } -bool DeckStateManager::modifyTree(const QString &reason, const std::function &operation) -{ - DeckListMemento memento = deckList->createMemento(reason); - bool success = operation(deckList->getTree()); - - if (success) { - historyManager->save(memento); - deckListModel->rebuildTree(); - deckList->refreshDeckHash(); - emit deckListModel->deckHashChanged(); - // removeCustomZone can drop whole card sets the model never notified - // about (rebuildTree emits no cardNodesChanged), so tell the consumers. - emit deckListModel->cardNodesChanged(); - doCardModified(); - } - - return success; -} - /** * @brief Handles updating state and emitting signals whenever the cards are modified */ diff --git a/cockatrice/src/interface/widgets/deck_editor/deck_state_manager.h b/cockatrice/src/interface/widgets/deck_editor/deck_state_manager.h index 2c8b34a39..6fce6be57 100644 --- a/cockatrice/src/interface/widgets/deck_editor/deck_state_manager.h +++ b/cockatrice/src/interface/widgets/deck_editor/deck_state_manager.h @@ -5,7 +5,6 @@ #include "deck_list_model.h" #include -#include #include class DeckListHistoryManager; @@ -172,7 +171,6 @@ public: void setName(const QString &name); void setComments(const QString &comments); void setBannerCard(const CardRef &bannerCard); - void setPlaymat(const PlaymatInfo &playmat); void setTags(const QStringList &tags); void setFormat(const QString &format); ///@} @@ -237,68 +235,6 @@ public: */ bool decrementCountAtIndex(const QModelIndex &idx); - /** - * @brief Moves all copies of the card at the given index to the given zone. - * No-ops if the index is invalid, not a card node, the card is a token, or the - * card is already in the target zone. - * Saves the operation to history if successful. - * - * @param idx The model index of the card to move - * @param targetZoneName The zone to move the card to (board zone or custom zone name) - * @return Whether the operation was successfully performed - */ - bool moveCardToZone(const QModelIndex &idx, const QString &targetZoneName); - - /** - * @brief Creates a new custom zone nested under a board zone. - * Saves the operation to history if successful. - * - * @param boardZoneName The board zone to nest the custom zone under - * @param zoneName The name of the new custom zone. Gets trimmed and must be - * unique across the deck. - * @return Whether the zone was created - */ - bool createCustomZone(const QString &boardZoneName, const QString &zoneName); - - /** - * @brief Renames a custom zone. - * Saves the operation to history if successful. - * - * @param oldZoneName The current name of the custom zone - * @param newZoneName The new name. Gets trimmed and must be unique across the deck. - * @return Whether the rename succeeded - */ - bool renameCustomZone(const QString &oldZoneName, const QString &newZoneName); - - /** - * @brief Moves a custom zone (and its cards) to a different board zone. - * Same-board moves succeed without creating a history entry. - * Saves the operation to history if successful. - * - * @param zoneName The custom zone to move - * @param newBoardZoneName The board zone to move the custom zone under - * @return Whether the move succeeded - */ - bool moveCustomZone(const QString &zoneName, const QString &newBoardZoneName); - - /** - * @brief Removes a custom zone and all its cards. - * Saves the operation to history if successful. - * - * @param zoneName The custom zone to remove - * @return Whether the zone was removed - */ - bool removeCustomZone(const QString &zoneName); - - /** - * @brief Checks whether a candidate name is usable for a new custom zone. - * - * @param zoneName The candidate name - * @return An empty string when the name is usable, otherwise a user-facing - * error message describing the problem - */ - [[nodiscard]] QString validateNewZoneName(const QString &zoneName) const; - /** * Undoes n steps of the history, setting the decklist state and updating the current step in the historyManager. * @param steps Number of steps to undo. @@ -320,7 +256,6 @@ public slots: private: bool offsetCountAtIndex(const QModelIndex &idx, int offset); - bool modifyTree(const QString &reason, const std::function &operation); void doCardModified(); void doMetadataModified(); diff --git a/cockatrice/src/interface/widgets/deck_editor/deck_zone_dialog.cpp b/cockatrice/src/interface/widgets/deck_editor/deck_zone_dialog.cpp deleted file mode 100644 index 9a0be2570..000000000 --- a/cockatrice/src/interface/widgets/deck_editor/deck_zone_dialog.cpp +++ /dev/null @@ -1,145 +0,0 @@ -#include "deck_zone_dialog.h" - -#include -#include -#include -#include -#include -#include -#include -#include - -DeckZoneDialog::DeckZoneDialog(QWidget *parent, - const QString &initialBoardName, - const std::function &_nameValidator, - bool _allowBoardSelection) - : QDialog(parent), nameValidator(_nameValidator), allowBoardSelection(_allowBoardSelection) -{ - nameLabel = new QLabel(this); - nameEdit = new QLineEdit(this); - nameEdit->setMaxLength(MAX_NAME_LENGTH); - - errorLabel = new QLabel(this); - errorLabel->hide(); - - boardLabel = new QLabel(this); - boardCombo = new QComboBox(this); - for (const QString &boardName : InnerDecklistNode::boardZoneNames()) { - // Use the icon overload explicitly so `boardName` lands in the user data role - // (visible text is applied below in retranslateUi). The two-argument form - // addItem({}, boardName) would be ambiguous and resolve to the icon overload - // with empty user data, yielding empty entries and an empty getBoardName(). - boardCombo->addItem({}, {}, boardName); - } - if (!initialBoardName.isEmpty()) { - int idx = boardCombo->findData(initialBoardName); - if (idx != -1) { - boardCombo->setCurrentIndex(idx); - } - } - - buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this); - buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false); - connect(buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); - connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); - - auto *layout = new QVBoxLayout(this); - layout->addWidget(nameLabel); - layout->addWidget(nameEdit); - layout->addWidget(errorLabel); - if (allowBoardSelection) { - layout->addWidget(boardLabel); - layout->addWidget(boardCombo); - } else { - boardLabel->hide(); - boardCombo->hide(); - } - layout->addWidget(buttonBox); - - retranslateUi(); - - connect(nameEdit, &QLineEdit::textChanged, this, [this] { validateName(); }); - validateName(); - - nameEdit->setFocus(); -} - -QString DeckZoneDialog::getZoneName() const -{ - return nameEdit->text().trimmed(); -} - -QString DeckZoneDialog::getBoardName() const -{ - return boardCombo->currentData().toString(); -} - -void DeckZoneDialog::setZoneName(const QString &zoneName) -{ - nameEdit->setText(zoneName); - nameEdit->selectAll(); -} - -void DeckZoneDialog::changeEvent(QEvent *event) -{ - QDialog::changeEvent(event); - - if (event->type() == QEvent::LanguageChange) { - retranslateUi(); - } -} - -void DeckZoneDialog::retranslateUi() -{ - setWindowTitle(allowBoardSelection ? tr("New zone") : tr("Rename zone")); - - nameLabel->setText(tr("Zone &name:")); - nameLabel->setBuddy(nameEdit); - - boardLabel->setText(tr("&Parent zone:")); - boardLabel->setBuddy(boardCombo); - - for (int i = 0; i < boardCombo->count(); i++) { - boardCombo->setItemText(i, InnerDecklistNode::visibleNameFromName(boardCombo->itemData(i).toString())); - } -} - -void DeckZoneDialog::validateName() -{ - const QString zoneName = nameEdit->text().trimmed(); - QString error; - if (zoneName.isEmpty()) { - error = tr("Enter a zone name."); - } else if (nameValidator) { - error = nameValidator(zoneName); - } - - errorLabel->setText(error); - errorLabel->setVisible(!error.isEmpty()); - buttonBox->button(QDialogButtonBox::Ok)->setEnabled(error.isEmpty()); -} - -QString DeckZoneDialog::promptForNewZone(QWidget *parent, - const QString &initialBoardName, - QString *chosenBoardName, - const std::function &nameValidator) -{ - DeckZoneDialog dialog(parent, initialBoardName, nameValidator); - if (dialog.exec() != QDialog::Accepted) { - return {}; - } - - if (chosenBoardName) { - *chosenBoardName = dialog.getBoardName(); - } - return dialog.getZoneName(); -} - -QString DeckZoneDialog::promptForRename(QWidget *parent, - const QString ¤tZoneName, - const std::function &nameValidator) -{ - DeckZoneDialog dialog(parent, {}, nameValidator, false); - dialog.setZoneName(currentZoneName); - return dialog.exec() == QDialog::Accepted ? dialog.getZoneName() : QString(); -} diff --git a/cockatrice/src/interface/widgets/deck_editor/deck_zone_dialog.h b/cockatrice/src/interface/widgets/deck_editor/deck_zone_dialog.h deleted file mode 100644 index 6f55617a8..000000000 --- a/cockatrice/src/interface/widgets/deck_editor/deck_zone_dialog.h +++ /dev/null @@ -1,123 +0,0 @@ -/** - * @file deck_zone_dialog.h - * @ingroup DeckEditorWidgets - * @brief Shared dialog for creating custom deck zones. - */ - -#ifndef DECK_ZONE_DIALOG_H -#define DECK_ZONE_DIALOG_H - -#include -#include -#include -#include - -class QComboBox; -class QDialogButtonBox; -class QLabel; -class QLineEdit; -class QWidget; - -/** - * @brief Modal dialog asking for the name and parent zone of a new custom deck zone. - * - * Menus construct the dialog transiently around exec(), so validation state only - * ever reflects the name currently typed. - */ -class DeckZoneDialog : public QDialog -{ - Q_OBJECT - -public: - /** - * @brief Constructs the dialog and runs the initial validation pass. - * - * @param parent The parent widget for the dialog - * @param initialBoardName The board zone to preselect in the combo. Unknown names - * fall back to main. - * @param _nameValidator Given the trimmed candidate name, returns an empty string - * when it is usable, otherwise a user-facing error message. May be empty. - * @param _allowBoardSelection When false the parent-zone combo is hidden and the - * dialog acts as a rename prompt for an existing zone. - */ - explicit DeckZoneDialog(QWidget *parent = nullptr, - const QString &initialBoardName = {}, - const std::function &_nameValidator = {}, - bool _allowBoardSelection = true); - - /** - * @brief The trimmed zone name entered by the user. - */ - [[nodiscard]] QString getZoneName() const; - - /** - * @brief The internal name of the board zone selected in the combo. - */ - [[nodiscard]] QString getBoardName() const; - - /** - * @brief Prefills the name field, e.g. with the current name when renaming. - * - * @param zoneName The text to put into the name field, selected for quick editing - */ - void setZoneName(const QString &zoneName); - - /** - * @brief Prompts the user for a new custom zone name and the board zone to nest it under. - * - * Convenience wrapper that runs DeckZoneDialog modally. - * - * @param parent The parent widget for the dialog - * @param initialBoardName The board zone to preselect in the dialog. Unknown names fall - * back to main. - * @param chosenBoardName (out) The internal name of the board zone the user chose - * @param nameValidator Optional validator forwarded to the dialog - * @return The trimmed zone name, or an empty string if the user cancelled - */ - static QString promptForNewZone(QWidget *parent, - const QString &initialBoardName, - QString *chosenBoardName, - const std::function &nameValidator = {}); - - /** - * @brief Prompts the user for a new name for an existing custom zone. - * - * Same inline validation as promptForNewZone, but without a parent-zone picker. - * - * @param parent The parent widget for the dialog - * @param currentZoneName The current name, prefilled for editing - * @param nameValidator Validator deciding whether a candidate name is usable. It sees - * the current name too, so callers wanting to allow unchanged names must - * special-case that themselves. - * @return The trimmed new name, or an empty string if the user cancelled - */ - static QString promptForRename(QWidget *parent, - const QString ¤tZoneName, - const std::function &nameValidator = {}); - -protected: - void changeEvent(QEvent *event) override; - -private: - /** - * @brief Sets every user-visible string. Runs on construction and on runtime - * language changes. - */ - void retranslateUi(); - - /** - * @brief Validates the current input, toggling Ok and the inline error label. - */ - void validateName(); - - QLabel *nameLabel; - QLineEdit *nameEdit; - QLabel *errorLabel; - QLabel *boardLabel; - QComboBox *boardCombo; - QDialogButtonBox *buttonBox; - std::function nameValidator; - bool allowBoardSelection; -}; - -#endif // DECK_ZONE_DIALOG_H diff --git a/cockatrice/src/interface/widgets/deck_share/deck_share_utils.cpp b/cockatrice/src/interface/widgets/deck_share/deck_share_utils.cpp deleted file mode 100644 index 4343f0aab..000000000 --- a/cockatrice/src/interface/widgets/deck_share/deck_share_utils.cpp +++ /dev/null @@ -1,59 +0,0 @@ -#include "deck_share_utils.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace DeckShareUtils -{ - -QString buildShareLink(const AbstractClient *client, const QString &token) -{ - QUrl url; - url.setScheme(QStringLiteral("cockatrice")); - url.setHost(QStringLiteral("opendeck")); - - QUrlQuery query; - query.addQueryItem(QStringLiteral("share"), token); - query.addQueryItem(QStringLiteral("hostname"), client->serverName()); - query.addQueryItem(QStringLiteral("port"), QString::number(client->serverPort())); - url.setQuery(query); - - return url.toString(QUrl::FullyEncoded); -} - -QString copyShareLinkToClipboard(const QString &link) -{ - QGuiApplication::clipboard()->setText(link); - return link; -} - -QString formatShareExpiry(const QDateTime &expiry) -{ - return QLocale().toString(expiry.toLocalTime(), QLocale::ShortFormat); -} - -ShareResponse handleShareResponse(const AbstractClient *client, const Response &response) -{ - const Response_DeckShareCreate &resp = response.GetExtension(Response_DeckShareCreate::ext); - const QString token = QString::fromStdString(resp.token()); - - const QString link = buildShareLink(client, token); - copyShareLinkToClipboard(link); - -#if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0) - const QDateTime expiry = QDateTime::fromSecsSinceEpoch(resp.expires_at(), QTimeZone::UTC); -#else - const QDateTime expiry = QDateTime::fromSecsSinceEpoch(resp.expires_at(), Qt::UTC); -#endif - - return {link, expiry}; -} - -} // namespace DeckShareUtils diff --git a/cockatrice/src/interface/widgets/deck_share/deck_share_utils.h b/cockatrice/src/interface/widgets/deck_share/deck_share_utils.h deleted file mode 100644 index c9f7afa8b..000000000 --- a/cockatrice/src/interface/widgets/deck_share/deck_share_utils.h +++ /dev/null @@ -1,59 +0,0 @@ -/** - * @file deck_share_utils.h - * @ingroup DeckShareWidgets - */ -//! \todo Document this file. - -#ifndef DECK_SHARE_UTILS_H -#define DECK_SHARE_UTILS_H - -#include -#include - -class AbstractClient; -class Response; - -/** - * @brief Shared helpers for creating temporary deck shares. - */ -namespace DeckShareUtils -{ - -/** - * @brief The outcome of a successful share-create response. - */ -struct ShareResponse -{ - QString link; ///< The share link that was copied to the clipboard. - QDateTime expiry; ///< When the share expires (UTC). -}; - -/** - * @brief Builds the cockatrice:// link for a freshly created deck share. - * @param client Used to embed the target server's hostname and port. - * @param token The share token from Response_DeckShareCreate. - */ -QString buildShareLink(const AbstractClient *client, const QString &token); - -/** - * @brief Copies the share link to the clipboard. - * @return The link that was copied. - */ -QString copyShareLinkToClipboard(const QString &link); - -/** - * @brief Formats the expiration timestamp for a share. - */ -QString formatShareExpiry(const QDateTime &expiry); - -/** - * @brief Handles a successful Response_DeckShareCreate: builds the share link, - * copies it to the clipboard, and derives the share expiry. - * @param client Used to embed the target server's hostname and port. - * @param response The successful response carrying the share token and expiry. - */ -ShareResponse handleShareResponse(const AbstractClient *client, const Response &response); - -} // namespace DeckShareUtils - -#endif // DECK_SHARE_UTILS_H diff --git a/cockatrice/src/interface/widgets/deck_share/share_bar_widget.cpp b/cockatrice/src/interface/widgets/deck_share/share_bar_widget.cpp deleted file mode 100644 index 5aca343ca..000000000 --- a/cockatrice/src/interface/widgets/deck_share/share_bar_widget.cpp +++ /dev/null @@ -1,77 +0,0 @@ -#include "share_bar_widget.h" - -#include -#include -#include -#include - -ShareBarWidget::ShareBarWidget(QWidget *parent) : QWidget(parent) -{ - auto *layout = new QHBoxLayout(this); - layout->setContentsMargins(12, 10, 12, 10); - layout->setSpacing(8); - - hintLabel = new QLabel(this); - hintLabel->setWordWrap(true); - - nameEdit = new QLineEdit(this); - nameEdit->setMaximumWidth(260); - - countLabel = new QLabel(this); - - cancelButton = new QPushButton(this); - connect(cancelButton, &QPushButton::clicked, this, &ShareBarWidget::cancelRequested); - - createButton = new QPushButton(this); - createButton->setDefault(true); - connect(createButton, &QPushButton::clicked, this, &ShareBarWidget::createRequested); - - layout->addWidget(hintLabel, 1); - layout->addWidget(nameEdit); - layout->addWidget(countLabel); - layout->addStretch(); - layout->addWidget(cancelButton); - layout->addWidget(createButton); - - setLayout(layout); - - retranslateUi(); -} - -void ShareBarWidget::retranslateUi() -{ - nameEdit->setPlaceholderText(tr("Share name")); - cancelButton->setText(tr("Cancel")); - createButton->setText(tr("Create share link")); -} - -QString ShareBarWidget::name() const -{ - return nameEdit->text().trimmed(); -} - -void ShareBarWidget::setName(const QString &value) -{ - nameEdit->setText(value); -} - -void ShareBarWidget::setCountText(const QString &text) -{ - countLabel->setText(text); -} - -void ShareBarWidget::setHintText(const QString &text, bool visible) -{ - hintLabel->setText(text); - hintLabel->setVisible(visible); -} - -void ShareBarWidget::setCreateEnabled(bool enabled) -{ - createButton->setEnabled(enabled); -} - -void ShareBarWidget::focusName() -{ - nameEdit->setFocus(); -} diff --git a/cockatrice/src/interface/widgets/deck_share/share_bar_widget.h b/cockatrice/src/interface/widgets/deck_share/share_bar_widget.h deleted file mode 100644 index ede9fb41b..000000000 --- a/cockatrice/src/interface/widgets/deck_share/share_bar_widget.h +++ /dev/null @@ -1,63 +0,0 @@ -/** - * @file share_bar_widget.h - * @ingroup DeckShareWidgets - */ -//! \todo Document this file. - -#ifndef SHARE_BAR_WIDGET_H -#define SHARE_BAR_WIDGET_H - -#include - -class QLabel; -class QLineEdit; -class QPushButton; - -/** - * @brief The activated toolbar used to create a temporary deck share. - * - * A single reusable component shared by the local visual deck storage and the - * remote server deck storage tabs, so the share workflow renders identically in - * both places. It owns its own widgets, strings, and layout; the owning tab only - * sets the count/hint text and reacts to the create/cancel signals. - */ -class ShareBarWidget final : public QWidget -{ - Q_OBJECT - -public: - explicit ShareBarWidget(QWidget *parent = nullptr); - - void retranslateUi(); - - /** @return The trimmed name entered by the user. */ - [[nodiscard]] QString name() const; - - /** @brief Resets the name field to the given default. */ - void setName(const QString &name); - - /** @brief Sets the selected-count summary label text. */ - void setCountText(const QString &text); - - /** @brief Sets the explainer hint text, showing it when @p visible is true. */ - void setHintText(const QString &text, bool visible); - - /** @brief Enables or disables the create-share-link button (guards double submission). */ - void setCreateEnabled(bool enabled); - - /** @brief Moves keyboard focus to the name field. */ - void focusName(); - -signals: - void createRequested(); - void cancelRequested(); - -private: - QLabel *hintLabel; - QLineEdit *nameEdit; - QLabel *countLabel; - QPushButton *cancelButton; - QPushButton *createButton; -}; - -#endif // SHARE_BAR_WIDGET_H diff --git a/cockatrice/src/interface/widgets/deck_share/shared_deck_preview_widget.cpp b/cockatrice/src/interface/widgets/deck_share/shared_deck_preview_widget.cpp deleted file mode 100644 index 21ec80e08..000000000 --- a/cockatrice/src/interface/widgets/deck_share/shared_deck_preview_widget.cpp +++ /dev/null @@ -1,140 +0,0 @@ -#include "shared_deck_preview_widget.h" - -#include "../cards/additional_info/color_identity_widget.h" -#include "../cards/deck_preview_card_picture_widget.h" - -#include -#include -#include -#include -#include -#include -#include - -SharedDeckPreviewWidget::SharedDeckPreviewWidget(QWidget *parent, - const CardDatabaseQuerier *querier, - const QString &deckName, - const QString &bannerCardName, - const QString &colorIdentity, - const QString &gameFormat, - const QString &deckToolTip) - : QWidget(parent) -{ - bannerCardDisplayWidget = - new DeckPreviewCardPictureWidget(this, false, false, Qt::white, Qt::black, 12, Qt::AlignCenter, true); - bannerCardDisplayWidget->setScaleFactor(100); - const ExactCard bannerCard = bannerCardName.isEmpty() ? ExactCard() : querier->getCard(CardRef{bannerCardName, {}}); - bannerCardDisplayWidget->setCard(bannerCard); - bannerCardDisplayWidget->setOverlayText(deckName); - setToolTip(deckToolTip.isEmpty() ? deckName : deckToolTip); - setFocusPolicy(Qt::StrongFocus); - setBaseAccessibleName(deckName); - - colorIdentityWidget = new ColorIdentityWidget(this, colorIdentity); - colorIdentityWidget->setVisible(!colorIdentity.isEmpty()); - - // gameFormat is server-supplied and the QLabel renders AutoText, so escape it. - gameFormatLabel = new QLabel(gameFormat.toHtmlEscaped(), this); - gameFormatLabel->setAlignment(Qt::AlignCenter); - gameFormatLabel->setVisible(!gameFormat.isEmpty()); - - selectionCheckBox = new QCheckBox(this); - selectionCheckBox->setToolTip(tr("Select this deck")); - // The tile itself is focusable (Space/Enter toggles); keep the checkbox - // from creating a second tab stop per tile. - selectionCheckBox->setFocusPolicy(Qt::NoFocus); - - // Selection frame reused from the deck-preview selection covenant: a - // palette(highlight) border around the banner card, shown while selected. - selectionFrame = new QFrame(bannerCardDisplayWidget); - selectionFrame->setAttribute(Qt::WA_TransparentForMouseEvents); - selectionFrame->setStyleSheet(QStringLiteral( - "QFrame { border: 2px solid palette(highlight); border-radius: 4px; background: transparent; }")); - selectionFrame->setVisible(false); - - auto *selectionRow = new QHBoxLayout; - selectionRow->addWidget(selectionCheckBox); - selectionRow->addStretch(1); - - auto *layout = new QVBoxLayout(this); - layout->setContentsMargins(0, 0, 0, 0); - layout->addLayout(selectionRow); - layout->addWidget(bannerCardDisplayWidget, 0, Qt::AlignHCenter); - layout->addWidget(colorIdentityWidget, 0, Qt::AlignHCenter); - layout->addWidget(gameFormatLabel, 0, Qt::AlignHCenter); - setLayout(layout); - - connect(selectionCheckBox, &QCheckBox::toggled, this, [this](bool checked) { - updateSelectionVisual(checked); - emit selectionToggled(checked); - }); - connect(bannerCardDisplayWidget, &DeckPreviewCardPictureWidget::imageClicked, this, - &SharedDeckPreviewWidget::toggleSelection); - connect(bannerCardDisplayWidget, &DeckPreviewCardPictureWidget::imageDoubleClicked, this, - &SharedDeckPreviewWidget::activate); -} - -bool SharedDeckPreviewWidget::isSelected() const -{ - return selectionCheckBox->isChecked(); -} - -void SharedDeckPreviewWidget::setSelected(bool selected) -{ - if (isSelected() == selected) { - return; - } - selectionCheckBox->setChecked(selected); -} - -void SharedDeckPreviewWidget::updateSelectionVisual(bool selected) -{ - selectionFrame->setVisible(selected); - selectionFrame->raise(); - if (selected) { - setAccessibleName(baseAccessibleName + tr(" (selected)")); - } else { - setAccessibleName(baseAccessibleName); - } -} - -void SharedDeckPreviewWidget::setBaseAccessibleName(const QString &name) -{ - baseAccessibleName = name; - setAccessibleName(name); -} - -void SharedDeckPreviewWidget::toggleSelection() -{ - setSelected(!isSelected()); -} - -void SharedDeckPreviewWidget::activate() -{ - setSelected(true); - emit activated(); -} - -void SharedDeckPreviewWidget::resizeEvent(QResizeEvent *event) -{ - QWidget::resizeEvent(event); - updateSelectionFrameGeometry(); -} - -void SharedDeckPreviewWidget::updateSelectionFrameGeometry() -{ - if (selectionFrame == nullptr || bannerCardDisplayWidget == nullptr) { - return; - } - selectionFrame->setGeometry(bannerCardDisplayWidget->rect().adjusted(1, 1, -1, -1)); -} - -void SharedDeckPreviewWidget::keyPressEvent(QKeyEvent *event) -{ - if (event->key() == Qt::Key_Space || event->key() == Qt::Key_Return || event->key() == Qt::Key_Enter) { - toggleSelection(); - event->accept(); - return; - } - QWidget::keyPressEvent(event); -} diff --git a/cockatrice/src/interface/widgets/deck_share/shared_deck_preview_widget.h b/cockatrice/src/interface/widgets/deck_share/shared_deck_preview_widget.h deleted file mode 100644 index bd4c4ee5f..000000000 --- a/cockatrice/src/interface/widgets/deck_share/shared_deck_preview_widget.h +++ /dev/null @@ -1,77 +0,0 @@ -/** - * @file shared_deck_preview_widget.h - * @ingroup DeckShareWidgets - */ -//! \todo Document this file. - -#ifndef SHARED_DECK_PREVIEW_WIDGET_H -#define SHARED_DECK_PREVIEW_WIDGET_H - -#include - -class ColorIdentityWidget; -class DeckPreviewCardPictureWidget; -class QCheckBox; -class QFrame; -class QKeyEvent; -class QLabel; -class QResizeEvent; -class CardDatabaseQuerier; - -/** - * @brief A selectable preview tile for a deck that has no local file. - * - * Renders a banner card picture (looked up by name in the card database), the - * deck name, color identity and game format. Used to preview decks shared via a - * cockatrice:// link (metadata from Command_DeckShareList) and the deck - * currently open in the deck editor. - * - * Selection follows the deck-preview covenant: the tile reports its click - * immediately (no double-click interval delay), a palette(highlight) frame - * marks the selected tile, and Space/Enter toggles selection from the keyboard. - * A double click selects the tile and emits activated() so the caller can open - * just that deck. - */ -class SharedDeckPreviewWidget : public QWidget -{ - Q_OBJECT - -public: - explicit SharedDeckPreviewWidget(QWidget *parent, - const CardDatabaseQuerier *querier, - const QString &deckName, - const QString &bannerCardName, - const QString &colorIdentity, - const QString &gameFormat = QString(), - const QString &deckToolTip = QString()); - - [[nodiscard]] bool isSelected() const; - void setSelected(bool selected); - - void setBaseAccessibleName(const QString &name); - -signals: - void selectionToggled(bool selected); - void activated(); - -protected: - void resizeEvent(QResizeEvent *event) override; - void keyPressEvent(QKeyEvent *event) override; - -private slots: - void toggleSelection(); - void activate(); - -private: - void updateSelectionVisual(bool selected); - void updateSelectionFrameGeometry(); - - DeckPreviewCardPictureWidget *bannerCardDisplayWidget; - ColorIdentityWidget *colorIdentityWidget; - QLabel *gameFormatLabel; - QCheckBox *selectionCheckBox; - QFrame *selectionFrame; - QString baseAccessibleName; -}; - -#endif // SHARED_DECK_PREVIEW_WIDGET_H \ No newline at end of file diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_connect.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_connect.cpp index 700012d45..aa8a916f8 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_connect.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_connect.cpp @@ -1,7 +1,6 @@ #include "dlg_connect.h" #include "../../../client/settings/cache_settings.h" -#include "../../pixel_map_generator.h" #include #include @@ -22,7 +21,7 @@ DlgConnect::DlgConnect(QWidget *parent) : QDialog(parent) previousHosts = new QComboBox(this); btnDeleteServer = new QPushButton(this); - btnDeleteServer->setIcon(themePixmap(QStringLiteral("icons/remove_row"))); + btnDeleteServer->setIcon(QPixmap("theme:icons/remove_row")); btnDeleteServer->setToolTip(tr("Delete the currently selected saved server")); btnDeleteServer->setFixedWidth(30); @@ -30,7 +29,7 @@ DlgConnect::DlgConnect(QWidget *parent) : QDialog(parent) hps = new HandlePublicServers(this); btnRefreshServers = new QPushButton(this); - btnRefreshServers->setIcon(themePixmap(QStringLiteral("icons/sync"))); + btnRefreshServers->setIcon(QPixmap("theme:icons/sync")); btnRefreshServers->setToolTip(tr("Refresh the server list with known public servers")); btnRefreshServers->setFixedWidth(30); @@ -100,7 +99,7 @@ DlgConnect::DlgConnect(QWidget *parent) : QDialog(parent) updateDisplayInfo(previousHosts->currentText()); btnForgotPassword = new QPushButton(this); - btnForgotPassword->setIcon(themePixmap(QStringLiteral("icons/forgot_password"))); + btnForgotPassword->setIcon(QPixmap("theme: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_convert_deck_to_cod_format.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_convert_deck_to_cod_format.cpp index a4a31d78d..198fa259b 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_convert_deck_to_cod_format.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_convert_deck_to_cod_format.cpp @@ -1,17 +1,9 @@ #include "dlg_convert_deck_to_cod_format.h" -#include "../../../client/settings/cache_settings.h" -#include "../../deck_loader/deck_loader.h" - #include #include -#include -#include -#include #include -#include #include -#include DialogConvertDeckToCodFormat::DialogConvertDeckToCodFormat(QWidget *parent) : QDialog(parent) { @@ -46,71 +38,3 @@ bool DialogConvertDeckToCodFormat::dontAskAgain() const { return dontAskAgainCheckbox->isChecked(); } - -namespace -{ - -bool confirmOverwriteIfExists(QWidget *parent, const QString &filePath) -{ - QFileInfo fileInfo(filePath); - QString newFileName = QDir::toNativeSeparators(fileInfo.path() + "/" + fileInfo.completeBaseName() + ".cod"); - - if (QFile::exists(newFileName)) { - QMessageBox::StandardButton reply = - QMessageBox::question(parent, QObject::tr("Overwrite Existing File?"), - QObject::tr("A .cod version of this deck already exists. Overwrite it?"), - QMessageBox::Yes | QMessageBox::No); - return reply == QMessageBox::Yes; - } - return true; // Safe to proceed -} - -} // namespace - -bool DialogConvertDeckToCodFormat::promptIfRequired(QWidget *parent, - const QString &filePath, - const std::function &convert) -{ - if (DeckFileFormat::getFormatFromName(filePath) == DeckFileFormat::Cockatrice) { - return true; - } - - // Retrieve saved preference if the prompt is disabled - if (!SettingsCache::instance().visualDeckStorage().getVisualDeckStoragePromptForConversion()) { - if (!SettingsCache::instance().visualDeckStorage().getVisualDeckStorageAlwaysConvert()) { - return false; - } - - if (!confirmOverwriteIfExists(parent, filePath)) { - return false; - } - - return convert(); - } - - // Show the dialog to the user - DialogConvertDeckToCodFormat conversionDialog(parent); - if (conversionDialog.exec() != QDialog::Accepted) { - SettingsCache::instance().visualDeckStorage().setVisualDeckStoragePromptForConversion( - !conversionDialog.dontAskAgain()); - SettingsCache::instance().visualDeckStorage().setVisualDeckStorageAlwaysConvert(false); - - return false; - } - - // Try to convert file - if (!confirmOverwriteIfExists(parent, filePath)) { - return false; - } - - if (!convert()) { - return false; - } - - if (conversionDialog.dontAskAgain()) { - SettingsCache::instance().visualDeckStorage().setVisualDeckStoragePromptForConversion(false); - SettingsCache::instance().visualDeckStorage().setVisualDeckStorageAlwaysConvert(true); - } - - return true; -} diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_convert_deck_to_cod_format.h b/cockatrice/src/interface/widgets/dialogs/dlg_convert_deck_to_cod_format.h index 526582135..6642ad8c6 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_convert_deck_to_cod_format.h +++ b/cockatrice/src/interface/widgets/dialogs/dlg_convert_deck_to_cod_format.h @@ -13,9 +13,6 @@ #include #include #include -#include - -class QWidget; class DialogConvertDeckToCodFormat : public QDialog { @@ -27,21 +24,6 @@ public: [[nodiscard]] bool dontAskAgain() const; - /** - * @brief Checks whether the deck file at \a filePath can store tags. - * - * If the file is not a .cod deck, prompts the user for conversion to the - * Cockatrice format, honoring the saved "always convert / don't ask again" - * preference. On acceptance \a convert is called to perform the conversion. - * - * @param parent The widget to parent the prompt to. - * @param filePath The path of the deck file to check. - * @param convert Called to convert the deck once the user agrees. - * @return true if tags can be stored (no conversion needed, or the conversion - * was performed), false if the user declined to convert. - */ - static bool promptIfRequired(QWidget *parent, const QString &filePath, const std::function &convert); - private: QVBoxLayout *layout; QLabel *label; diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_create_game.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_create_game.cpp index 59f3e033d..2b1e9e8c2 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_create_game.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_create_game.cpp @@ -214,7 +214,6 @@ DlgCreateGame::DlgCreateGame(const ServerInfo_Game &gameInfo, const QMapsetChecked(gameInfo.spectators_need_password()); spectatorsCanTalkCheckBox->setChecked(gameInfo.spectators_can_chat()); spectatorsSeeEverythingCheckBox->setChecked(gameInfo.spectators_omniscient()); - shareDecklistsOnLoadCheckBox->setChecked(gameInfo.share_decklists_on_load()); QSet types; for (int i = 0; i < gameInfo.game_types_size(); ++i) { diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_edit_tokens.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_edit_tokens.cpp index a56aa8e35..f249976c2 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_edit_tokens.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_edit_tokens.cpp @@ -1,6 +1,5 @@ #include "dlg_edit_tokens.h" -#include "../../pixel_map_generator.h" #include "../interface/widgets/utility/get_text_with_max.h" #include @@ -91,10 +90,10 @@ DlgEditTokens::DlgEditTokens(QWidget *parent) : QDialog(parent), currentCard(nul &DlgEditTokens::tokenSelectionChanged); QAction *aAddToken = new QAction(tr("Add token"), this); - aAddToken->setIcon(themePixmap(QStringLiteral("icons/increment"))); + aAddToken->setIcon(QPixmap("theme:icons/increment")); connect(aAddToken, &QAction::triggered, this, &DlgEditTokens::actAddToken); QAction *aRemoveToken = new QAction(tr("Remove token"), this); - aRemoveToken->setIcon(themePixmap(QStringLiteral("icons/decrement"))); + aRemoveToken->setIcon(QPixmap("theme:icons/decrement")); connect(aRemoveToken, &QAction::triggered, this, &DlgEditTokens::actRemoveToken); auto *databaseToolBar = new QToolBar; diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_filter_games.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_filter_games.cpp index 7e174a228..8f498cc2c 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_filter_games.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_filter_games.cpp @@ -30,7 +30,7 @@ DlgFilterGames::DlgFilterGames(const QMap &_allGameTypes, hideFullGames = new QCheckBox(tr("Hide full games")); hideFullGames->setChecked(filters.hideFullGames); - hideGamesThatStarted = new QCheckBox(tr("Hide started games")); + hideGamesThatStarted = new QCheckBox(tr("Hide games that have started")); hideGamesThatStarted->setChecked(filters.hideGamesThatStarted); hidePasswordProtectedGames = new QCheckBox(tr("Hide password protected games")); @@ -57,16 +57,16 @@ DlgFilterGames::DlgFilterGames(const QMap &_allGameTypes, gameNameFilterEdit->setText(filters.gameNameFilter); auto *gameNameFilterLabel = new QLabel(tr("Game &description:")); gameNameFilterLabel->setBuddy(gameNameFilterEdit); - hostNameFilterEdit = new QLineEdit; - hostNameFilterEdit->setText(filters.hostNameFilters.join(", ")); - auto *hostNameFilterLabel = new QLabel(tr("&Host name:")); - hostNameFilterLabel->setBuddy(hostNameFilterEdit); + creatorNameFilterEdit = new QLineEdit; + creatorNameFilterEdit->setText(filters.creatorNameFilters.join(", ")); + auto *creatorNameFilterLabel = new QLabel(tr("&Creator name:")); + creatorNameFilterLabel->setBuddy(creatorNameFilterEdit); auto *generalGrid = new QGridLayout; generalGrid->addWidget(gameNameFilterLabel, 0, 0); generalGrid->addWidget(gameNameFilterEdit, 0, 1); - generalGrid->addWidget(hostNameFilterLabel, 1, 0); - generalGrid->addWidget(hostNameFilterEdit, 1, 1); + generalGrid->addWidget(creatorNameFilterLabel, 1, 0); + generalGrid->addWidget(creatorNameFilterEdit, 1, 1); generalGrid->addWidget(maxGameAgeLabel, 2, 0); generalGrid->addWidget(maxGameAgeComboBox, 2, 1); generalGroupBox = new QGroupBox(tr("General")); @@ -193,7 +193,7 @@ GameFilterConfigs DlgFilterGames::getFilters() const hideNotBuddyCreatedGames->isChecked(), hideOpenDecklistGames->isChecked(), gameNameFilterEdit->text(), - getHostNameFilters(), + getCreatorNameFilters(), getGameTypeFilter(), maxPlayersFilterMinSpinBox->value(), maxPlayersFilterMaxSpinBox->value(), @@ -216,9 +216,9 @@ void DlgFilterGames::toggleSpectatorCheckboxEnabledness(bool spectatorsEnabled) showOnlyIfSpectatorsCanSeeHands->setDisabled(!spectatorsEnabled); } -QStringList DlgFilterGames::getHostNameFilters() const +QStringList DlgFilterGames::getCreatorNameFilters() const { - return hostNameFilterEdit->text().split(",", Qt::SkipEmptyParts); + return creatorNameFilterEdit->text().split(",", Qt::SkipEmptyParts); } QSet DlgFilterGames::getGameTypeFilter() const diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_filter_games.h b/cockatrice/src/interface/widgets/dialogs/dlg_filter_games.h index 1cf822b89..447f9b16c 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_filter_games.h +++ b/cockatrice/src/interface/widgets/dialogs/dlg_filter_games.h @@ -35,7 +35,7 @@ private: QCheckBox *hideNotBuddyCreatedGames; QCheckBox *hideOpenDecklistGames; QLineEdit *gameNameFilterEdit; - QLineEdit *hostNameFilterEdit; + QLineEdit *creatorNameFilterEdit; QMap gameTypeFilterCheckBoxes; QSpinBox *maxPlayersFilterMinSpinBox; QSpinBox *maxPlayersFilterMaxSpinBox; @@ -50,7 +50,7 @@ private: const GamesProxyModel *gamesProxyModel; const QMap gameAgeMap; - [[nodiscard]] QStringList getHostNameFilters() const; + [[nodiscard]] QStringList getCreatorNameFilters() const; [[nodiscard]] QSet getGameTypeFilter() const; [[nodiscard]] QTime getMaxGameAge() const; [[nodiscard]] bool getShowSpectatorPasswordProtected() const; diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_invite_to_game.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_invite_to_game.cpp deleted file mode 100644 index b5451bb2c..000000000 --- a/cockatrice/src/interface/widgets/dialogs/dlg_invite_to_game.cpp +++ /dev/null @@ -1,112 +0,0 @@ -#include "dlg_invite_to_game.h" - -#include "../server/user/user_list_manager.h" -#include "../server/user/user_list_widget.h" -#include "../tabs/tab_supervisor.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -DlgInviteToGame::DlgInviteToGame(TabSupervisor *_tabSupervisor, - const QString &_inviteUrl, - bool _onlyBuddies, - const QStringList &_excludeUserNames, - QWidget *parent) - : QDialog(parent), tabSupervisor(_tabSupervisor), inviteUrl(_inviteUrl), onlyBuddies(_onlyBuddies), - excludeUserNames(_excludeUserNames) -{ - setModal(true); - - searchEdit = new QLineEdit(this); - searchEdit->setClearButtonEnabled(true); - connect(searchEdit, &QLineEdit::textChanged, this, &DlgInviteToGame::searchTextChanged); - - // The embedded list is the real room user list without the hover popup: - // same manager, same delegate/painter, same sections, live via manager - // signals while the modal loop runs. - UserListManager *manager = tabSupervisor->getUserListManager(); - userList = new UserListWidget(tabSupervisor, tabSupervisor->getClient(), UserListWidget::RoomList, this, - /*hasUserInfoPopup=*/false); - userList->setUserFilter([this, manager](const QString &name, bool online) { - return !excludeUserNames.contains(name) && online && !manager->isUserIgnored(name); - }); - if (onlyBuddies) { - userList->setSectioned({UserListWidget::Section::Buddy}); - } else { - userList->setSectioned({UserListWidget::Section::Buddy, UserListWidget::Section::Online}); - } - userList->bind(manager); - userList->rebuild(); - - connect(userList, &UserListWidget::userActivated, this, &DlgInviteToGame::inviteCurrentUser); - connect(userList, &UserListWidget::currentUserChanged, this, [this](const QString &userName) { - currentUserName = userName; - inviteButton->setEnabled(!userName.isEmpty()); - }); - - inviteButton = new QPushButton(this); - inviteButton->setEnabled(false); - inviteButton->setDefault(true); - connect(inviteButton, &QPushButton::clicked, this, [this] { inviteCurrentUser(currentUserName); }); - - cancelButton = new QPushButton(this); - connect(cancelButton, &QPushButton::clicked, this, &QDialog::reject); - - auto *buttonRow = new QHBoxLayout; - buttonRow->addStretch(); - buttonRow->addWidget(inviteButton); - buttonRow->addWidget(cancelButton); - - auto *layout = new QVBoxLayout(this); - layout->addWidget(searchEdit); - layout->addWidget(userList, 1); - layout->addLayout(buttonRow); - - retranslateUi(); - - // Default to a comfortably tall dialog so the list has room to breathe, - // capped by the available screen. No minimum is enforced: small screens - // and manual resizing can go shorter than this. - const QRect availableScreen = QGuiApplication::primaryScreen()->availableGeometry(); - resize(sizeHint().width(), qMin(sizeHint().height() * 3, availableScreen.height() * 4 / 5)); -} - -void DlgInviteToGame::searchTextChanged(const QString &text) -{ - userList->setFilterText(text); -} - -void DlgInviteToGame::inviteCurrentUser(const QString &userName) -{ - if (userName.isEmpty()) { - return; - } - // The invite link carries the game's id and, when the game has one, its - // description (makeGameJoinLink embeds both). Read them back so the prefix - // names the game by description first, then its id — identical to the - // context-menu invite so recipients see one consistent message style. - const QUrl inviteUrlObj(inviteUrl); - const QUrlQuery inviteQuery(inviteUrlObj); - const int gameId = inviteQuery.queryItemValue("gameid").toInt(); - const QString gameDescription = inviteQuery.queryItemValue("game"); - const QString prefix = gameDescription.isEmpty() - ? tr("Join my game (#%1):").arg(gameId) - : tr("Join my game \"%1\" (#%2):").arg(gameDescription).arg(gameId); - tabSupervisor->sendInviteToUser(userName, prefix + " " + inviteUrl); - accept(); -} - -void DlgInviteToGame::retranslateUi() -{ - setWindowTitle(tr("Invite to Game")); - searchEdit->setPlaceholderText(tr("Search users...")); - inviteButton->setText(tr("Invite")); - cancelButton->setText(tr("Cancel")); -} diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_invite_to_game.h b/cockatrice/src/interface/widgets/dialogs/dlg_invite_to_game.h deleted file mode 100644 index bbb589bdb..000000000 --- a/cockatrice/src/interface/widgets/dialogs/dlg_invite_to_game.h +++ /dev/null @@ -1,46 +0,0 @@ -/** - * @file dlg_invite_to_game.h - * @ingroup RoomDialogs - */ -//! \todo Document this file. - -#ifndef DLG_INVITE_TO_GAME_H -#define DLG_INVITE_TO_GAME_H - -#include -#include - -class QLineEdit; -class QPushButton; -class TabSupervisor; -class UserListWidget; - -class DlgInviteToGame : public QDialog -{ - Q_OBJECT -public: - DlgInviteToGame(TabSupervisor *_tabSupervisor, - const QString &_inviteUrl, - bool _onlyBuddies, - const QStringList &_excludeUserNames, - QWidget *parent = nullptr); - -private slots: - void searchTextChanged(const QString &text); - void inviteCurrentUser(const QString &userName); - -private: - TabSupervisor *tabSupervisor; - QString inviteUrl; - bool onlyBuddies; - QStringList excludeUserNames; - QString currentUserName; - QLineEdit *searchEdit; - UserListWidget *userList; - QPushButton *inviteButton; - QPushButton *cancelButton; - - void retranslateUi(); -}; - -#endif diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_login_prompt.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_login_prompt.cpp deleted file mode 100644 index 86647ef26..000000000 --- a/cockatrice/src/interface/widgets/dialogs/dlg_login_prompt.cpp +++ /dev/null @@ -1,50 +0,0 @@ -#include "dlg_login_prompt.h" - -#include -#include -#include -#include -#include -#include - -DlgLoginPrompt::DlgLoginPrompt(const QString &serverText, QWidget *parent) : QDialog(parent) -{ - setWindowTitle(tr("Sign in")); - - auto *mainLayout = new QVBoxLayout(this); - mainLayout->addWidget( - new QLabel(tr("This link requires you to be signed in.\nSign in to %1:").arg(serverText), this)); - - auto *formLayout = new QFormLayout; - usernameEdit = new QLineEdit(this); - passwordEdit = new QLineEdit(this); - passwordEdit->setEchoMode(QLineEdit::Password); - formLayout->addRow(tr("Username:"), usernameEdit); - formLayout->addRow(tr("Password:"), passwordEdit); - mainLayout->addLayout(formLayout); - - savePasswordCheckBox = new QCheckBox(tr("Save password for this server"), this); - mainLayout->addWidget(savePasswordCheckBox); - - auto *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this); - connect(buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); - connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); - mainLayout->addWidget(buttonBox); - - usernameEdit->setFocus(); -} - -QString DlgLoginPrompt::username() const -{ - return usernameEdit->text().trimmed(); -} - -QString DlgLoginPrompt::password() const -{ - return passwordEdit->text(); -} - -bool DlgLoginPrompt::savePassword() const -{ - return savePasswordCheckBox->isChecked(); -} diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_login_prompt.h b/cockatrice/src/interface/widgets/dialogs/dlg_login_prompt.h deleted file mode 100644 index e47924058..000000000 --- a/cockatrice/src/interface/widgets/dialogs/dlg_login_prompt.h +++ /dev/null @@ -1,40 +0,0 @@ -/** - * @file dlg_login_prompt.h - * @ingroup ConnectionDialogs - */ -//! \todo Document this file. - -#ifndef DLG_LOGIN_PROMPT_H -#define DLG_LOGIN_PROMPT_H - -#include - -class QCheckBox; -class QLineEdit; - -/** - * @brief Small sign-in dialog used when a cockatrice:// link needs credentials - * that are not saved for the target server. - * - * The entered name and password are handed to the intent chain; when the user - * opts to save them, they are stored in the server settings so that later links - * to the same server connect seamlessly. - */ -class DlgLoginPrompt : public QDialog -{ - Q_OBJECT - -public: - explicit DlgLoginPrompt(const QString &serverText, QWidget *parent = nullptr); - - [[nodiscard]] QString username() const; - [[nodiscard]] QString password() const; - [[nodiscard]] bool savePassword() const; - -private: - QLineEdit *usernameEdit; - QLineEdit *passwordEdit; - QCheckBox *savePasswordCheckBox; -}; - -#endif // DLG_LOGIN_PROMPT_H diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_manage_sets.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_manage_sets.cpp index 556c270be..7c107eb2f 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_manage_sets.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_manage_sets.cpp @@ -1,7 +1,6 @@ #include "dlg_manage_sets.h" #include "../../../client/settings/cache_settings.h" -#include "../../pixel_map_generator.h" #include "../interface/card_picture_loader/card_picture_loader.h" #include "../interface/widgets/utility/custom_line_edit.h" @@ -36,28 +35,28 @@ WndSets::WndSets(QWidget *parent) : QMainWindow(parent) setsEditToolBar->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); aTop = new QAction(QString(), this); - aTop->setIcon(themePixmap(QStringLiteral("icons/arrow_top_green"))); + aTop->setIcon(QPixmap("theme: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(themePixmap(QStringLiteral("icons/arrow_up_green"))); + aUp->setIcon(QPixmap("theme: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(themePixmap(QStringLiteral("icons/arrow_down_green"))); + aDown->setIcon(QPixmap("theme: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(themePixmap(QStringLiteral("icons/arrow_bottom_green"))); + aBottom->setIcon(QPixmap("theme:icons/arrow_bottom_green")); aBottom->setToolTip(tr("Move selected set to the bottom")); aBottom->setEnabled(false); connect(aBottom, &QAction::triggered, this, &WndSets::actBottom); @@ -67,7 +66,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(themePixmap(QStringLiteral("icons/search")), LineEditUnfocusable::LeadingPosition); + searchField->addAction(QPixmap("theme: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 deleted file mode 100644 index 1af83bfb5..000000000 --- a/cockatrice/src/interface/widgets/dialogs/dlg_my_reports.cpp +++ /dev/null @@ -1,297 +0,0 @@ -#include "dlg_my_reports.h" - -#include "../utility/report_utils.h" -#include "abstract_client.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace -{ -constexpr int COL_ID = 0; -constexpr int COL_TIME = 1; -constexpr int COL_REPORTED = 2; -constexpr int COL_CATEGORY = 3; -constexpr int COL_GAMEID = 4; -constexpr int COL_STATUS = 5; -constexpr int COL_ASSIGNED = 6; -constexpr int COL_COUNT = 7; -} // namespace - -DlgMyReports::DlgMyReports(AbstractClient *_client, QWidget *parent) - : QDialog(parent), client(_client), selectedReportId(-1) -{ - setWindowTitle(tr("My Reports")); - setMinimumSize(800, 500); - - table = new QTableWidget(0, COL_COUNT); - table->setHorizontalHeaderLabels( - {tr("#"), tr("Time"), tr("Reported User"), tr("Category"), tr("Game ID"), tr("Status"), tr("Assigned To")}); - table->setSelectionBehavior(QAbstractItemView::SelectRows); - table->setSelectionMode(QAbstractItemView::SingleSelection); - table->setEditTriggers(QAbstractItemView::NoEditTriggers); - table->setSortingEnabled(true); - table->verticalHeader()->setVisible(false); - table->setAlternatingRowColors(true); - table->horizontalHeader()->setSectionResizeMode(COL_TIME, QHeaderView::ResizeToContents); - table->horizontalHeader()->setSectionResizeMode(COL_REPORTED, QHeaderView::Stretch); - connect(table, &QTableWidget::itemSelectionChanged, this, &DlgMyReports::onSelectionChanged); - - auto *detailsGroup = new QGroupBox(tr("Report Details")); - descriptionEdit = new QTextEdit; - descriptionEdit->setReadOnly(true); - descriptionEdit->setFixedHeight(80); - - auto *chatGroup = new QGroupBox(tr("Chat Log Context")); - chatLogEdit = new QTextEdit; - chatLogEdit->setReadOnly(true); - QFont monoFont("monospace"); - monoFont.setStyleHint(QFont::Monospace); - const int systemPointSize = QFontDatabase::systemFont(QFontDatabase::GeneralFont).pointSize(); - if (systemPointSize > 0) { - monoFont.setPointSize(systemPointSize); - } - chatLogEdit->setFont(monoFont); - auto *chatLayout = new QVBoxLayout(chatGroup); - chatLayout->setContentsMargins(4, 4, 4, 4); - chatLayout->addWidget(chatLogEdit); - - auto *commentsLabel = new QLabel(tr("Comments:")); - commentsEdit = new QTextEdit; - commentsEdit->setReadOnly(true); - commentsEdit->setFixedHeight(120); - - auto *addCommentLabel = new QLabel(tr("Add a comment:")); - commentInput = new QLineEdit; - commentInput->setPlaceholderText(tr("Type your comment here...")); - commentButton = new QPushButton(tr("Send")); - commentButton->setEnabled(false); - connect(commentButton, &QPushButton::clicked, this, &DlgMyReports::addComment); - connect(commentInput, &QLineEdit::returnPressed, this, &DlgMyReports::addComment); - - auto *detailsLayout = new QVBoxLayout(detailsGroup); - detailsLayout->setContentsMargins(4, 4, 4, 4); - detailsLayout->addWidget(descriptionEdit); - detailsLayout->addWidget(chatGroup); - detailsLayout->addWidget(commentsLabel); - detailsLayout->addWidget(commentsEdit); - detailsLayout->addWidget(addCommentLabel); - auto *commentRow = new QHBoxLayout; - commentRow->addWidget(commentInput); - commentRow->addWidget(commentButton); - detailsLayout->addLayout(commentRow); - - closeButton = new QPushButton(tr("Close")); - connect(closeButton, &QPushButton::clicked, this, &QDialog::accept); - - refreshButton = new QPushButton(tr("Refresh")); - connect(refreshButton, &QPushButton::clicked, this, &DlgMyReports::refreshList); - - statusLabel = new QLabel; - - auto *bottomBar = new QHBoxLayout; - bottomBar->addWidget(statusLabel); - bottomBar->addStretch(); - bottomBar->addWidget(refreshButton); - bottomBar->addWidget(closeButton); - - auto *layout = new QVBoxLayout(this); - layout->addWidget(table, 1); - layout->addWidget(detailsGroup); - layout->addLayout(bottomBar); - - setActionsEnabled(false); - refreshList(); -} - -void DlgMyReports::refreshList() -{ - selectedReportIdBeforeRefresh = selectedReportId; - commentDraftBeforeRefresh = commentInput->text(); - statusLabel->setText(tr("Loading...")); - refreshButton->setEnabled(false); - table->setRowCount(0); - currentReports.clear(); - descriptionEdit->clear(); - chatLogEdit->clear(); - commentsEdit->clear(); - commentInput->clear(); - setActionsEnabled(false); - selectedReportId = -1; - - Command_ReportMyList cmd; - - PendingCommand *pend = client->prepareSessionCommand(cmd); - connect(pend, &PendingCommand::finished, this, &DlgMyReports::reportListResponse); - client->sendCommand(pend); -} - -void DlgMyReports::reportListResponse(const Response &response) -{ - refreshButton->setEnabled(true); - - if (response.response_code() != Response::RespOk) { - statusLabel->setText(tr("Failed to load reports.")); - return; - } - - const Response_ReportMyList &resp = response.GetExtension(Response_ReportMyList::ext); - currentReports.clear(); - for (int i = 0; i < resp.reports_size(); ++i) { - currentReports.append(resp.reports(i)); - } - - table->setSortingEnabled(false); - table->setRowCount(currentReports.size()); - - for (int row = 0; row < currentReports.size(); ++row) { - const ServerInfo_Report &r = currentReports[row]; - - report_utils::fillReportTableRow(table, row, r, COL_ID, COL_TIME, COL_REPORTED, COL_CATEGORY, COL_GAMEID, - COL_STATUS, COL_ASSIGNED); - } - - table->setSortingEnabled(true); - table->resizeColumnsToContents(); - table->horizontalHeader()->setSectionResizeMode(COL_REPORTED, QHeaderView::Stretch); - - if (selectedReportIdBeforeRefresh >= 0) { - for (int row = 0; row < table->rowCount(); ++row) { - if (table->item(row, COL_ID) && - table->item(row, COL_ID)->data(Qt::UserRole).toInt() == selectedReportIdBeforeRefresh) { - table->setCurrentCell(row, 0); - break; - } - } - } - - if (commentInput->text().isEmpty()) { - commentInput->setText(commentDraftBeforeRefresh); - } - - statusLabel->setText(tr("%1 report(s)").arg(currentReports.size())); -} - -void DlgMyReports::onSelectionChanged() -{ - const int row = table->currentRow(); - if (row < 0 || !table->item(row, COL_ID)) { - descriptionEdit->clear(); - chatLogEdit->clear(); - commentsEdit->clear(); - commentInput->clear(); - commentButton->setEnabled(false); - selectedReportId = -1; - return; - } - - const int reportId = table->item(row, COL_ID)->data(Qt::UserRole).toInt(); - selectedReportId = reportId; - - for (const ServerInfo_Report &r : currentReports) { - if (r.report_id() == reportId) { - descriptionEdit->setPlainText(QString::fromStdString(r.description())); - break; - } - } - - chatLogEdit->setPlainText(tr("Loading...")); - commentsEdit->setPlainText(tr("Loading...")); - - Command_ReportDetails cmd; - cmd.set_report_id(reportId); - - PendingCommand *pend = client->prepareSessionCommand(cmd); - connect(pend, &PendingCommand::finished, this, &DlgMyReports::reportDetailsResponse); - client->sendCommand(pend); - - QString status = table->item(row, COL_STATUS)->text(); - bool canComment = (status == "open" || status == "assigned"); - commentButton->setEnabled(canComment); - commentInput->setEnabled(canComment); - if (!canComment) { - commentInput->setPlaceholderText(tr("This report is closed.")); - } else { - commentInput->setPlaceholderText(tr("Type your comment here...")); - } -} - -void DlgMyReports::reportDetailsResponse(const Response &response) -{ - if (response.response_code() != Response::RespOk) { - if (selectedReportId == -1) { - return; - } - chatLogEdit->clear(); - commentsEdit->setPlainText(tr("Failed to load report details.")); - return; - } - - const Response_ReportDetails &resp = response.GetExtension(Response_ReportDetails::ext); - const ServerInfo_Report &r = resp.report(); - - if (selectedReportId != r.report_id()) { - return; - } - - loadReportDetails(r); -} - -void DlgMyReports::loadReportDetails(const ServerInfo_Report &report) -{ - report_utils::renderReportDetails(chatLogEdit, commentsEdit, report, tr("No comments yet."), tr("[Moderator]"), - tr("[You]")); -} - -void DlgMyReports::addComment() -{ - if (selectedReportId < 0) { - return; - } - - QString text = commentInput->text().trimmed(); - if (text.isEmpty()) { - return; - } - - commentButton->setEnabled(false); - - Command_ReportAddComment cmd; - cmd.set_report_id(selectedReportId); - cmd.set_comment(text.toStdString()); - - PendingCommand *pend = client->prepareSessionCommand(cmd); - connect(pend, &PendingCommand::finished, this, &DlgMyReports::addCommentResponse); - client->sendCommand(pend); -} - -void DlgMyReports::addCommentResponse(const Response &response) -{ - if (response.response_code() == Response::RespOk) { - commentInput->clear(); - refreshList(); - } else { - commentButton->setEnabled(true); - } -} - -void DlgMyReports::setActionsEnabled(bool enabled) -{ - commentButton->setEnabled(enabled); - commentInput->setEnabled(enabled); -} diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_my_reports.h b/cockatrice/src/interface/widgets/dialogs/dlg_my_reports.h deleted file mode 100644 index 09b1963dd..000000000 --- a/cockatrice/src/interface/widgets/dialogs/dlg_my_reports.h +++ /dev/null @@ -1,52 +0,0 @@ -#ifndef COCKATRICE_DLG_MY_REPORTS_H -#define COCKATRICE_DLG_MY_REPORTS_H - -#include -#include -#include -#include - -class AbstractClient; -class QTableWidget; -class QTextEdit; -class QLineEdit; -class QPushButton; -class QLabel; - -class DlgMyReports : public QDialog -{ - Q_OBJECT -public: - explicit DlgMyReports(AbstractClient *_client, QWidget *parent = nullptr); - -private slots: - void refreshList(); - void reportListResponse(const Response &response); - void onSelectionChanged(); - void reportDetailsResponse(const Response &response); - void addComment(); - void addCommentResponse(const Response &response); - -private: - void loadReportDetails(const ServerInfo_Report &report); - void setActionsEnabled(bool enabled); - - AbstractClient *client; - - QTableWidget *table; - QTextEdit *descriptionEdit; - QTextEdit *chatLogEdit; - QTextEdit *commentsEdit; - QLineEdit *commentInput; - QPushButton *commentButton; - QPushButton *refreshButton; - QPushButton *closeButton; - QLabel *statusLabel; - - QList currentReports; - int selectedReportId; - int selectedReportIdBeforeRefresh = -1; - QString commentDraftBeforeRefresh; -}; - -#endif // COCKATRICE_DLG_MY_REPORTS_H diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_register.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_register.cpp index 6d022861c..fce99a1a7 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_register.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_register.cpp @@ -1,63 +1,18 @@ #include "dlg_register.h" #include "../../../client/settings/cache_settings.h" -#include "../../pixel_map_generator.h" -#include "../server/handle_public_servers.h" -#include "../server/user/user_info_connection.h" -#include +#include #include #include -#include #include #include #include -#include -#include -#include #include #include DlgRegister::DlgRegister(QWidget *parent) : QDialog(parent) { - // ── Server picker ────────────────────────────────────────────────── - previousHostButton = new QRadioButton(tr("Known Hosts"), this); - previousHosts = new QComboBox(this); - - btnDeleteServer = new QPushButton(this); - btnDeleteServer->setIcon(themePixmap(QStringLiteral("icons/remove_row"))); - btnDeleteServer->setToolTip(tr("Delete the currently selected saved server")); - btnDeleteServer->setFixedWidth(30); - - connect(btnDeleteServer, &QPushButton::clicked, this, &DlgRegister::actRemoveSavedServer); - - hps = new HandlePublicServers(this); - btnRefreshServers = new QPushButton(this); - btnRefreshServers->setIcon(themePixmap(QStringLiteral("icons/sync"))); - btnRefreshServers->setToolTip(tr("Refresh the server list with known public servers")); - btnRefreshServers->setFixedWidth(30); - - connect(hps, &HandlePublicServers::sigPublicServersDownloadedSuccessfully, this, [this] { rebuildComboBoxList(); }); - connect(hps, &HandlePublicServers::sigPublicServersDownloadedUnsuccessfully, this, - &DlgRegister::rebuildComboBoxList); - connect(btnRefreshServers, &QPushButton::released, this, &DlgRegister::downloadThePublicServers); - - newHostButton = new QRadioButton(tr("New Host"), this); - - auto *serverPickerRow = new QHBoxLayout; - serverPickerRow->addWidget(previousHosts); - serverPickerRow->addWidget(btnDeleteServer); - serverPickerRow->addWidget(btnRefreshServers); - - auto *serverGroupLayout = new QVBoxLayout; - serverGroupLayout->addWidget(previousHostButton); - serverGroupLayout->addLayout(serverPickerRow); - serverGroupLayout->addWidget(newHostButton); - - auto *serverGroupBox = new QGroupBox(tr("Server")); - serverGroupBox->setLayout(serverGroupLayout); - - // ── Registration fields ──────────────────────────────────────────── ServersSettings &servers = SettingsCache::instance().servers(); infoLabel = new QLabel(tr("Enter your information and the information of the server you'd like to register to.\n" "Your email will be used to verify your account.")); @@ -366,28 +321,26 @@ DlgRegister::DlgRegister(QWidget *parent) : QDialog(parent) realnameEdit->setMaxLength(MAX_NAME_LENGTH); realnameLabel->setBuddy(realnameEdit); - // ── Layout ───────────────────────────────────────────────────────── auto *grid = new QGridLayout; - grid->addWidget(serverGroupBox, 0, 0, 1, 2); - grid->addWidget(infoLabel, 1, 0, 1, 2); - grid->addWidget(hostLabel, 2, 0); - grid->addWidget(hostEdit, 2, 1); - grid->addWidget(portLabel, 3, 0); - grid->addWidget(portEdit, 3, 1); - grid->addWidget(playernameLabel, 4, 0); - grid->addWidget(playernameEdit, 4, 1); - grid->addWidget(passwordLabel, 5, 0); - grid->addWidget(passwordEdit, 5, 1); - grid->addWidget(passwordConfirmationLabel, 6, 0); - grid->addWidget(passwordConfirmationEdit, 6, 1); - grid->addWidget(emailLabel, 7, 0); - grid->addWidget(emailEdit, 7, 1); - grid->addWidget(emailConfirmationLabel, 8, 0); - grid->addWidget(emailConfirmationEdit, 8, 1); - grid->addWidget(countryLabel, 10, 0); - grid->addWidget(countryEdit, 10, 1); - grid->addWidget(realnameLabel, 11, 0); - grid->addWidget(realnameEdit, 11, 1); + grid->addWidget(infoLabel, 0, 0, 1, 2); + grid->addWidget(hostLabel, 1, 0); + grid->addWidget(hostEdit, 1, 1); + grid->addWidget(portLabel, 2, 0); + grid->addWidget(portEdit, 2, 1); + grid->addWidget(playernameLabel, 3, 0); + grid->addWidget(playernameEdit, 3, 1); + grid->addWidget(passwordLabel, 4, 0); + grid->addWidget(passwordEdit, 4, 1); + grid->addWidget(passwordConfirmationLabel, 5, 0); + grid->addWidget(passwordConfirmationEdit, 5, 1); + grid->addWidget(emailLabel, 6, 0); + grid->addWidget(emailEdit, 6, 1); + grid->addWidget(emailConfirmationLabel, 7, 0); + grid->addWidget(emailConfirmationEdit, 7, 1); + grid->addWidget(countryLabel, 9, 0); + grid->addWidget(countryEdit, 9, 1); + grid->addWidget(realnameLabel, 10, 0); + grid->addWidget(realnameEdit, 10, 1); auto *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); connect(buttonBox, &QDialogButtonBox::accepted, this, &DlgRegister::actOk); @@ -399,115 +352,13 @@ DlgRegister::DlgRegister(QWidget *parent) : QDialog(parent) setLayout(mainLayout); setWindowTitle(tr("Register to server")); - setMinimumWidth(360); - - connect(previousHostButton, &QRadioButton::toggled, this, &DlgRegister::previousHostSelected); - connect(newHostButton, &QRadioButton::toggled, this, &DlgRegister::newHostSelected); - connect(previousHosts, &QComboBox::currentTextChanged, this, &DlgRegister::updateDisplayInfo); - - previousHostButton->setChecked(true); - - preRebuildComboBoxList(); -} - -DlgRegister::~DlgRegister() = default; - -void DlgRegister::downloadThePublicServers() -{ - btnRefreshServers->setDisabled(true); - previousHosts->clear(); - previousHosts->addItem(placeHolderText); - hps->downloadPublicServers(); -} - -void DlgRegister::preRebuildComboBoxList() -{ - UserConnection_Information uci; - savedHostList = uci.getServerInfo(); - - if (savedHostList.size() == 1) { - downloadThePublicServers(); - } else { - rebuildComboBoxList(); - } -} - -void DlgRegister::rebuildComboBoxList(int failure) -{ - Q_UNUSED(failure); - - previousHosts->clear(); - - UserConnection_Information uci; - savedHostList = uci.getServerInfo(); - - auto &servers = SettingsCache::instance().servers(); - QString previousHostName = servers.getPrevioushostName(); - - for (const auto &pair : savedHostList) { - const auto &tmp = pair.second; - QString saveName = tmp.getSaveName(); - if (saveName.size()) { - previousHosts->addItem(saveName); - if (saveName.compare(previousHostName) == 0) { - previousHosts->setCurrentIndex(previousHosts->count() - 1); - } - } - } - - btnRefreshServers->setDisabled(false); -} - -void DlgRegister::previousHostSelected(bool state) -{ - if (state) { - previousHosts->setDisabled(false); - btnRefreshServers->setDisabled(false); - hostEdit->setDisabled(true); - portEdit->setDisabled(true); - } -} - -void DlgRegister::newHostSelected(bool state) -{ - if (state) { - previousHosts->setDisabled(true); - btnRefreshServers->setDisabled(true); - hostEdit->setDisabled(false); - hostEdit->clear(); - hostEdit->setPlaceholderText(tr("Server URL")); - portEdit->setDisabled(false); - portEdit->clear(); - portEdit->setPlaceholderText(tr("Communication Port")); - playernameEdit->setDisabled(false); - playernameEdit->clear(); - } else { - // Rebuild the list so the previously selected host's details are - // repopulated (mirrors DlgConnect::newHostSelected). - preRebuildComboBoxList(); - } -} - -void DlgRegister::updateDisplayInfo(const QString &saveName) -{ - if (saveName.isEmpty() || saveName == placeHolderText) { - return; - } - - UserConnection_Information uci; - QStringList _data = uci.getServerInfo(saveName); - - if (_data.size() < 7) { - return; - } - - hostEdit->setText(_data.at(1)); - portEdit->setText(_data.at(2)); - playernameEdit->setText(_data.at(3)); + setFixedHeight(sizeHint().height()); + setMinimumWidth(300); } void DlgRegister::actOk() { + //! \todo This stuff should be using QValidators. if (passwordEdit->text().length() < 8) { QMessageBox::critical(this, tr("Registration Warning"), tr("Your password is too short.")); return; @@ -524,29 +375,5 @@ void DlgRegister::actOk() return; } - ServersSettings &servers = SettingsCache::instance().servers(); - - if (newHostButton->isChecked()) { - // Persist the new host so it shows up in the Connect dialog later. - // The password is never stored: the account is not verified yet. - const QString host = hostEdit->text().trimmed(); - if (!host.isEmpty()) { - servers.addNewServer(host, host, portEdit->text().trimmed(), playernameEdit->text().trimmed(), QString(), - false); - servers.setPrevioushostName(host); - } - } else { - const QString saveName = previousHosts->currentText(); - if (!saveName.isEmpty() && saveName != placeHolderText) { - servers.setPrevioushostName(saveName); - } - } - accept(); } - -void DlgRegister::actRemoveSavedServer() -{ - SettingsCache::instance().servers().removeServer(hostEdit->text()); - previousHosts->removeItem(previousHosts->currentIndex()); -} diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_register.h b/cockatrice/src/interface/widgets/dialogs/dlg_register.h index ce14eb427..abed9ff51 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_register.h +++ b/cockatrice/src/interface/widgets/dialogs/dlg_register.h @@ -1,24 +1,25 @@ +/** + * @file dlg_register.h + * @ingroup AccountDialogs + */ +//! \todo Document this file. + #ifndef DLG_REGISTER_H #define DLG_REGISTER_H #include #include #include -#include -class HandlePublicServers; class QLabel; class QPushButton; -class QRadioButton; -class UserConnection_Information; +class QCheckBox; class DlgRegister : public QDialog { Q_OBJECT public: explicit DlgRegister(QWidget *parent = nullptr); - ~DlgRegister() override; - [[nodiscard]] QString getHost() const { return hostEdit->text(); @@ -47,35 +48,15 @@ public: { return realnameEdit->text(); } - -public slots: - void downloadThePublicServers(); - private slots: void actOk(); - void previousHostSelected(bool state); - void newHostSelected(bool state); - void updateDisplayInfo(const QString &saveName); - void preRebuildComboBoxList(); - void rebuildComboBoxList(int failure = -1); - void actRemoveSavedServer(); private: - QRadioButton *newHostButton; - QRadioButton *previousHostButton; - QComboBox *previousHosts; - QPushButton *btnDeleteServer; - QPushButton *btnRefreshServers; - HandlePublicServers *hps; - QLabel *infoLabel, *hostLabel, *portLabel, *playernameLabel, *passwordLabel, *passwordConfirmationLabel, *emailLabel, *emailConfirmationLabel, *countryLabel, *realnameLabel; QLineEdit *hostEdit, *portEdit, *playernameEdit, *passwordEdit, *passwordConfirmationEdit, *emailEdit, *emailConfirmationEdit, *realnameEdit; QComboBox *countryEdit; - - QMap> savedHostList; - const QString placeHolderText = tr("Downloading..."); }; -#endif // DLG_REGISTER_H +#endif diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_report_user.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_report_user.cpp deleted file mode 100644 index 9519846e2..000000000 --- a/cockatrice/src/interface/widgets/dialogs/dlg_report_user.cpp +++ /dev/null @@ -1,191 +0,0 @@ -#include "dlg_report_user.h" - -#include "abstract_client.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -DlgReportUser::DlgReportUser(AbstractClient *_client, - const QString &_reportedUser, - int _gameId, - const QString &_autoChatLog, - QWidget *parent) - : QDialog(parent), client(_client), reportedUser(_reportedUser), gameId(_gameId) -{ - setWindowTitle(tr("Report User")); - setMinimumWidth(500); - - auto *infoLabel = - new QLabel(tr("Reports are reviewed by moderators. False reports may result in account penalties.")); - infoLabel->setWordWrap(true); - infoLabel->setStyleSheet("color: palette(placeholderText); padding: 5px;"); - - auto *reportGroup = new QGroupBox(tr("Report Details")); - auto *reportGrid = new QGridLayout(reportGroup); - - reportGrid->addWidget(new QLabel(tr("Reported User:")), 0, 0); - reportedUserLabel = new QLabel(reportedUser); - reportedUserLabel->setStyleSheet("font-weight: bold;"); - reportGrid->addWidget(reportedUserLabel, 0, 1); - - reportGrid->addWidget(new QLabel(tr("Game ID:")), 1, 0); - if (gameId >= 0) { - gameIdLabel = new QLabel(QString::number(gameId)); - gameIdLabel->setStyleSheet("font-weight: bold;"); - reportGrid->addWidget(gameIdLabel, 1, 1); - } else { - gameIdEdit = new QLineEdit; - gameIdEdit->setPlaceholderText(tr("(Optional) Enter game ID if available")); - gameIdEdit->setToolTip(tr("If the report is related to a specific game, enter its ID.")); - reportGrid->addWidget(gameIdEdit, 1, 1); - gameIdLabel = nullptr; - } - - auto *categoryGroup = new QGroupBox(tr("Category")); - auto *categoryGrid = new QGridLayout(categoryGroup); - - categoryBox = new QComboBox; - categoryBox->addItem(tr("Cheating / Unsporting behavior"), "cheating"); - categoryBox->setItemData(categoryBox->count() - 1, - tr("Using external tools, card marked manipulation, or exploiting game bugs"), - Qt::ToolTipRole); - categoryBox->addItem(tr("Harassment / Abuse"), "harassment"); - categoryBox->setItemData(categoryBox->count() - 1, tr("Threatening, bullying, or persistent unwanted contact"), - Qt::ToolTipRole); - categoryBox->addItem(tr("Hate speech"), "hate_speech"); - categoryBox->setItemData(categoryBox->count() - 1, - tr("Discriminatory language targeting race, gender, religion, etc."), Qt::ToolTipRole); - categoryBox->addItem(tr("Spam"), "spam"); - categoryBox->setItemData(categoryBox->count() - 1, tr("Repeated unwanted messages or advertisements"), - Qt::ToolTipRole); - categoryBox->addItem(tr("Other"), "other"); - categoryBox->setItemData(categoryBox->count() - 1, tr("Any behavior not covered by the above categories"), - Qt::ToolTipRole); - - categoryGrid->addWidget(new QLabel(tr("Category:")), 0, 0); - categoryGrid->addWidget(categoryBox, 0, 1); - - auto *descGroup = new QGroupBox(tr("Description")); - auto *descLayout = new QVBoxLayout(descGroup); - - descriptionEdit = new QTextEdit; - descriptionEdit->setPlaceholderText( - tr("Please describe what happened. Include dates, game details, or any evidence if available.")); - descriptionEdit->setFixedHeight(120); - descLayout->addWidget(descriptionEdit); - - auto *chatGroup = new QGroupBox(tr("Chat Log Context")); - auto *chatLayout = new QVBoxLayout(chatGroup); - chatLogEdit = new QTextEdit; - chatLogEdit->setReadOnly(true); - QFont monoFont("monospace"); - monoFont.setStyleHint(QFont::Monospace); - const int systemPointSize = QFontDatabase::systemFont(QFontDatabase::GeneralFont).pointSize(); - if (systemPointSize > 0) { - monoFont.setPointSize(systemPointSize); - } - chatLogEdit->setFont(monoFont); - if (!_autoChatLog.isEmpty()) { - chatLogEdit->setPlainText(_autoChatLog); - } else { - chatLogEdit->setPlaceholderText(tr("No chat context available (not triggered from chat).")); - } - chatLogEdit->setFixedHeight(100); - chatLayout->addWidget(chatLogEdit); - - auto *chatNote = new QLabel( - tr("This chat log is captured from your local chat window and may not reflect the full conversation.")); - chatNote->setWordWrap(true); - chatNote->setStyleSheet("color: palette(placeholderText);"); - chatLayout->addWidget(chatNote); - - buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); - buttonBox->button(QDialogButtonBox::Ok)->setText(tr("Submit Report")); - connect(buttonBox, &QDialogButtonBox::accepted, this, &DlgReportUser::actSubmit); - connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); - - auto *layout = new QVBoxLayout(this); - layout->addWidget(infoLabel); - layout->addWidget(reportGroup); - layout->addWidget(categoryGroup); - layout->addWidget(descGroup); - layout->addWidget(chatGroup); - layout->addWidget(buttonBox); -} - -void DlgReportUser::actSubmit() -{ - const QString description = descriptionEdit->toPlainText().trimmed(); - if (description.isEmpty()) { - QMessageBox::warning(this, tr("Missing description"), tr("Please describe what happened before submitting.")); - return; - } - - QMessageBox::StandardButton reply = - QMessageBox::question(this, tr("Confirm Report"), - tr("Submit report against %1 for %2?").arg(reportedUser, categoryBox->currentText()), - QMessageBox::Yes | QMessageBox::No); - - if (reply != QMessageBox::Yes) { - return; - } - - buttonBox->setEnabled(false); - buttonBox->button(QDialogButtonBox::Ok)->setText(tr("Submitting...")); - - Command_Report cmd; - cmd.set_reported_user(reportedUser.toStdString()); - cmd.set_category(categoryBox->currentData().toString().toStdString()); - cmd.set_description(description.toStdString()); - - if (gameId >= 0) { - cmd.set_game_id(gameId); - } else if (gameIdEdit && !gameIdEdit->text().trimmed().isEmpty()) { - bool ok; - int manualGameId = gameIdEdit->text().trimmed().toInt(&ok); - if (ok && manualGameId > 0) { - cmd.set_game_id(manualGameId); - } - } - - const QString chatLog = chatLogEdit->toPlainText().trimmed(); - if (!chatLog.isEmpty()) { - cmd.set_chat_log(chatLog.toStdString()); - } - - PendingCommand *pend = client->prepareSessionCommand(cmd); - connect(pend, &PendingCommand::finished, this, &DlgReportUser::reportResponse); - client->sendCommand(pend); -} - -void DlgReportUser::reportResponse(const Response &response) -{ - buttonBox->setEnabled(true); - buttonBox->button(QDialogButtonBox::Ok)->setText(tr("Submit Report")); - - if (response.response_code() == Response::RespOk) { - QMessageBox::information(this, tr("Report Submitted"), - tr("Your report has been submitted and will be reviewed by a moderator. Thank you.")); - accept(); - } else if (response.response_code() == Response::RespTooManyRequests) { - QMessageBox::warning(this, tr("Submission Failed"), - tr("You have reached the daily report limit. Please try again later.")); - } else if (response.response_code() == Response::RespNameNotFound) { - QMessageBox::warning( - this, tr("Submission Failed"), - tr("The reported user could not be found. Guests (unregistered users) cannot be reported.")); - } else { - QMessageBox::warning(this, tr("Submission Failed"), tr("Failed to submit report. Please try again.")); - } -} diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_report_user.h b/cockatrice/src/interface/widgets/dialogs/dlg_report_user.h deleted file mode 100644 index c59fc5084..000000000 --- a/cockatrice/src/interface/widgets/dialogs/dlg_report_user.h +++ /dev/null @@ -1,42 +0,0 @@ -#ifndef COCKATRICE_DLG_REPORT_USER_H -#define COCKATRICE_DLG_REPORT_USER_H - -#include -#include - -class AbstractClient; -class QComboBox; -class QDialogButtonBox; -class QLineEdit; -class QTextEdit; -class QLabel; - -class DlgReportUser : public QDialog -{ - Q_OBJECT -public: - DlgReportUser(AbstractClient *_client, - const QString &_reportedUser, - int _gameId = -1, - const QString &_autoChatLog = QString(), - QWidget *parent = nullptr); - -private slots: - void actSubmit(); - void reportResponse(const Response &response); - -private: - AbstractClient *client; - QString reportedUser; - int gameId; - - QLabel *reportedUserLabel; - QLabel *gameIdLabel = nullptr; - QLineEdit *gameIdEdit = nullptr; - QComboBox *categoryBox; - QTextEdit *descriptionEdit; - QTextEdit *chatLogEdit; - QDialogButtonBox *buttonBox; -}; - -#endif // COCKATRICE_DLG_REPORT_USER_H diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_settings.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_settings.cpp index 581920dbc..883cfcd03 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_settings.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_settings.cpp @@ -6,7 +6,6 @@ #include "dlg_settings.h" #include "../../../client/settings/cache_settings.h" -#include "../../pixel_map_generator.h" #include "../main.h" #include "../settings_page/appearance_settings_page.h" #include "../settings_page/deck_editor_settings_page.h" @@ -97,7 +96,7 @@ void DlgSettings::setupUi() // Search bar searchEdit = new QLineEdit; searchEdit->setClearButtonEnabled(true); - searchEdit->addAction(themePixmap(QStringLiteral("icons/search")), QLineEdit::LeadingPosition); + searchEdit->addAction(QPixmap("theme:icons/search"), QLineEdit::LeadingPosition); searchEdit->installEventFilter(this); connect(searchEdit, &QLineEdit::textChanged, this, &DlgSettings::onSearchTextChanged); @@ -133,7 +132,7 @@ void DlgSettings::setupUi() pagesWidget->addWidget(makeScrollable(userInterfacePage)); pagesWidget->addWidget(makeScrollable(deckEditorPage)); pagesWidget->addWidget(makeScrollable(storagePage)); - pagesWidget->addWidget(makeScrollable(messagesPage)); + pagesWidget->addWidget(messagesPage); pagesWidget->addWidget(soundPage); pagesWidget->addWidget(shortcutsPage); @@ -416,11 +415,6 @@ void DlgSettings::setTab(int index) } } -AbstractSettingsPage *DlgSettings::page(SettingsPage which) const -{ - return pages.value(static_cast(which)); -} - void DlgSettings::updateLanguage() { qApp->removeTranslator(translator); // NOLINT(cppcoreguidelines-pro-type-static-cast-downcast) @@ -483,13 +477,13 @@ void DlgSettings::closeEvent(QCloseEvent *event) case Invalid: loadErrorMessage = tr("Your card database is invalid.\n\n" "Cockatrice may not function correctly with an invalid database\n\n" - "You may need to rerun Oracle to update your card database.\n\n" + "You may need to rerun oracle to update your card database.\n\n" "Would you like to change your database location setting?"); break; case VersionTooOld: loadErrorMessage = tr("Your card database version is too old.\n\n" "This can cause problems loading card information or images\n\n" - "Usually this can be fixed by rerunning Oracle to to update your card database.\n\n" + "Usually this can be fixed by rerunning oracle to to update your card database.\n\n" "Would you like to change your database location setting?"); break; case NotLoaded: diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_settings.h b/cockatrice/src/interface/widgets/dialogs/dlg_settings.h index 845c0b4d6..b700f7af9 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_settings.h +++ b/cockatrice/src/interface/widgets/dialogs/dlg_settings.h @@ -54,7 +54,6 @@ public: explicit DlgSettings(QWidget *parent = nullptr); void setTab(int index); - AbstractSettingsPage *page(SettingsPage which) const; private slots: void onTabClicked(int index); diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_share_deck.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_share_deck.cpp deleted file mode 100644 index 897d32062..000000000 --- a/cockatrice/src/interface/widgets/dialogs/dlg_share_deck.cpp +++ /dev/null @@ -1,96 +0,0 @@ -#include "dlg_share_deck.h" - -#include "../../../client/settings/cache_settings.h" -#include "../cards/additional_info/deck_color_identity.h" -#include "../deck_share/deck_share_utils.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -DlgShareDeck::DlgShareDeck(AbstractClient *_client, const QSharedPointer &_deck, QWidget *_parent) - : QDialog(_parent), client(_client), deck(_deck), shareTimeoutTimer(new QTimer(this)) -{ - setWindowTitle(tr("Share deck")); - - auto *layout = new QVBoxLayout(this); - - nameEdit = new QLineEdit(this); - nameEdit->setText(tr("Shared deck")); - - auto *form = new QFormLayout; - form->addRow(tr("Share name:"), nameEdit); - layout->addLayout(form); - - auto *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this); - buttonBox->button(QDialogButtonBox::Ok)->setText(tr("Create share link")); - buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel")); - connect(buttonBox, &QDialogButtonBox::accepted, this, &DlgShareDeck::actShare); - connect(buttonBox, &QDialogButtonBox::rejected, this, &DlgShareDeck::reject); - this->buttonBox = buttonBox; - layout->addWidget(buttonBox); - - shareTimeoutTimer->setSingleShot(true); - shareTimeoutTimer->setInterval( - static_cast((static_cast(SettingsCache::instance().network().getTimeOut()) + 1) * - SettingsCache::instance().network().getKeepAlive() * 1000)); - connect(shareTimeoutTimer, &QTimer::timeout, this, &DlgShareDeck::onShareTimeout); -} - -void DlgShareDeck::actShare() -{ - buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false); - - Command_DeckShareCreate cmd; - cmd.set_name(nameEdit->text().trimmed().toStdString()); - if (cmd.name().empty()) { - cmd.set_name(tr("Shared deck").toStdString()); - } - - DeckShareItem *item = cmd.add_items(); - item->set_deck_list(deck->writeToString_Native().toStdString()); - item->set_color_identity(getDeckColorIdentity(*deck, CardDatabaseManager::query()).toStdString()); - - PendingCommand *pend = client->prepareSessionCommand(cmd); - connect(pend, &PendingCommand::finished, this, &DlgShareDeck::shareFinished); - client->sendCommand(pend); - shareTimeoutTimer->start(); -} - -void DlgShareDeck::shareFinished(const Response &response, const CommandContainer & /*commandContainer*/) -{ - shareTimeoutTimer->stop(); - if (response.response_code() != Response::RespOk) { - buttonBox->button(QDialogButtonBox::Ok)->setEnabled(true); - QMessageBox::critical(this, tr("Share deck"), - tr("Failed to create the share link (server response code %1).") - .arg(QString::number(static_cast(response.response_code())))); - return; - } - - const DeckShareUtils::ShareResponse share = DeckShareUtils::handleShareResponse(client, response); - - QMessageBox::information(this, tr("Share deck"), - tr("Share link created and copied to the clipboard:\n\n%1\n\n" - "The share expires on %2.") - .arg(share.link, DeckShareUtils::formatShareExpiry(share.expiry))); - accept(); -} - -void DlgShareDeck::onShareTimeout() -{ - buttonBox->button(QDialogButtonBox::Ok)->setEnabled(true); - QMessageBox::warning(this, tr("Share deck"), tr("The server did not respond in time. Try again.")); -} diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_share_deck.h b/cockatrice/src/interface/widgets/dialogs/dlg_share_deck.h deleted file mode 100644 index 162fa3677..000000000 --- a/cockatrice/src/interface/widgets/dialogs/dlg_share_deck.h +++ /dev/null @@ -1,46 +0,0 @@ -/** - * @file dlg_share_deck.h - * @ingroup Dialogs - */ -//! \todo Document this file. - -#ifndef DLG_SHARE_DECK_H -#define DLG_SHARE_DECK_H - -#include -#include - -class AbstractClient; -class CommandContainer; -class DeckList; -class QDialogButtonBox; -class QLineEdit; -class QTimer; -class Response; - -/** - * @brief Slim dialog to create a temporary share for the deck open in the editor. - * - * Asks for a share name, sends Command_DeckShareCreate for the single inline - * deck, and copies the resulting link to the clipboard. - */ -class DlgShareDeck : public QDialog -{ - Q_OBJECT -public: - DlgShareDeck(AbstractClient *_client, const QSharedPointer &_deck, QWidget *parent = nullptr); - -private slots: - void actShare(); - void shareFinished(const Response &response, const CommandContainer &commandContainer); - void onShareTimeout(); - -private: - AbstractClient *client; - QSharedPointer deck; - QLineEdit *nameEdit; - QDialogButtonBox *buttonBox; - QTimer *shareTimeoutTimer; -}; - -#endif // DLG_SHARE_DECK_H diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_shared_decks_preview.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_shared_decks_preview.cpp deleted file mode 100644 index cd7ecdcdd..000000000 --- a/cockatrice/src/interface/widgets/dialogs/dlg_shared_decks_preview.cpp +++ /dev/null @@ -1,181 +0,0 @@ -#include "dlg_shared_decks_preview.h" - -#include "../deck_share/shared_deck_preview_widget.h" -#include "../general/layout_containers/flow_widget.h" - -#include -#include -#include -#include -#include -#include -#include -#include - -DlgSharedDecksPreview::DlgSharedDecksPreview(QWidget *parent, - const CardDatabaseQuerier *querier, - const QString &shareName, - qint64 expiresAt, - const QString &serverText, - const QList &items) - : QDialog(parent) -{ - setWindowTitle(tr("Open shared decks")); - resize(700, 500); - - auto *mainLayout = new QVBoxLayout(this); - - // shareName and serverText come from the share server, so escape them: the - // QLabels render AutoText and markup would otherwise be shown as rich text. - auto *titleLabel = - new QLabel(tr("Share: %1").arg((shareName.isEmpty() ? tr("Untitled") : shareName).toHtmlEscaped()), this); - QFont titleFont = titleLabel->font(); - titleFont.setBold(true); - titleFont.setPointSize(titleFont.pointSize() + 2); - titleLabel->setFont(titleFont); - mainLayout->addWidget(titleLabel); - - if (!serverText.isEmpty()) { - mainLayout->addWidget(new QLabel(tr("From %1").arg(serverText.toHtmlEscaped()), this)); - } - - if (expiresAt > 0) { - const QString expiryText = QDateTime::fromSecsSinceEpoch(expiresAt).toLocalTime().toString(Qt::TextDate); - mainLayout->addWidget(new QLabel(tr("This share link expires on %1").arg(expiryText), this)); - } - - downloadStatusLabel = new QLabel(this); - downloadStatusLabel->setVisible(false); - mainLayout->addWidget(downloadStatusLabel); - - flowWidget = new FlowWidget(this, Qt::Horizontal, Qt::ScrollBarAlwaysOff, Qt::ScrollBarAsNeeded); - mainLayout->addWidget(flowWidget, 1); - - for (const ServerInfo_DeckShareItem &item : items) { - QStringList tags; - for (const auto &tag : item.tags()) { - tags.append(QString::fromStdString(tag)); - } - - auto *tile = new SharedDeckPreviewWidget( - this, querier, QString::fromStdString(item.name()), QString::fromStdString(item.banner_card()), - QString::fromStdString(item.color_identity()), QString::fromStdString(item.game_format()), tags.join(", ")); - flowWidget->addNavigableWidget(tile); - tiles.append(tile); - itemIds.append(item.id()); - } - - if (tiles.size() == 1) { - tiles.first()->setSelected(true); - } - - auto *buttonBox = new QDialogButtonBox(this); - openSelectedButton = buttonBox->addButton(tr("Open selected"), QDialogButtonBox::AcceptRole); - openAllButton = buttonBox->addButton(tr("Open all"), QDialogButtonBox::ActionRole); - buttonBox->addButton(tr("Cancel"), QDialogButtonBox::RejectRole); - mainLayout->addWidget(buttonBox); - - connect(buttonBox, &QDialogButtonBox::rejected, this, [this]() { - onCancel(); - close(); - }); - - // Esc calls QDialog::reject() directly (which hides the dialog without a - // close event), so route it through the same guarded cancel as the button. - connect(this, &QDialog::rejected, this, [this]() { - onCancel(); - close(); - }); - - connect(openSelectedButton, &QPushButton::clicked, this, &DlgSharedDecksPreview::openSelected); - connect(buttonBox, &QDialogButtonBox::clicked, this, [this, buttonBox](QAbstractButton *button) { - if (buttonBox->buttonRole(button) == QDialogButtonBox::ActionRole) { - openAll(); - } - }); - - for (SharedDeckPreviewWidget *tile : tiles) { - connect(tile, &SharedDeckPreviewWidget::selectionToggled, this, - &DlgSharedDecksPreview::updateOpenSelectedEnabled); - } - for (int i = 0; i < tiles.size(); ++i) { - const int itemId = itemIds.at(i); - // Double-clicking a tile selects it and opens just that deck. - connect(tiles.at(i), &SharedDeckPreviewWidget::activated, this, [this, itemId]() { - resultEmitted = true; - setDownloading(true); - emit openRequested(QList{itemId}); - }); - } - updateOpenSelectedEnabled(); -} - -QList DlgSharedDecksPreview::selectedItemIds() const -{ - QList selectedIds; - for (int i = 0; i < tiles.size(); ++i) { - if (tiles.at(i)->isSelected()) { - selectedIds.append(itemIds.at(i)); - } - } - return selectedIds; -} - -void DlgSharedDecksPreview::openSelected() -{ - const QList selectedIds = selectedItemIds(); - if (selectedIds.isEmpty()) { - return; - } - resultEmitted = true; - setDownloading(true); - emit openRequested(selectedIds); -} - -void DlgSharedDecksPreview::openAll() -{ - resultEmitted = true; - setDownloading(true); - emit openRequested(itemIds); -} - -void DlgSharedDecksPreview::setDownloading(bool downloading) -{ - if (downloadInProgress == downloading) { - return; - } - downloadInProgress = downloading; - downloadStatusLabel->setVisible(downloading); - for (SharedDeckPreviewWidget *tile : tiles) { - tile->setEnabled(!downloading); - } - openSelectedButton->setEnabled(!downloading); - openAllButton->setEnabled(!downloading); -} - -void DlgSharedDecksPreview::setDownloadProgress(int done, int total, const QString ¤tDeckName) -{ - if (!downloadInProgress) { - return; - } - downloadStatusLabel->setText(tr("Downloading deck %1 of %2: %3").arg(done).arg(total).arg(currentDeckName)); -} - -void DlgSharedDecksPreview::updateOpenSelectedEnabled() -{ - openSelectedButton->setEnabled(!selectedItemIds().isEmpty()); -} - -void DlgSharedDecksPreview::onCancel() -{ - if (!resultEmitted || downloadInProgress) { - resultEmitted = true; - emit cancelled(); - } -} - -void DlgSharedDecksPreview::closeEvent(QCloseEvent *event) -{ - onCancel(); - QDialog::closeEvent(event); -} diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_shared_decks_preview.h b/cockatrice/src/interface/widgets/dialogs/dlg_shared_decks_preview.h deleted file mode 100644 index 31820e3a0..000000000 --- a/cockatrice/src/interface/widgets/dialogs/dlg_shared_decks_preview.h +++ /dev/null @@ -1,68 +0,0 @@ -#ifndef COCKATRICE_DLG_SHARED_DECKS_PREVIEW_H -#define COCKATRICE_DLG_SHARED_DECKS_PREVIEW_H - -#include -#include - -class FlowWidget; -class QCloseEvent; -class QLabel; -class QPushButton; -class ServerInfo_DeckShareItem; -class SharedDeckPreviewWidget; -class CardDatabaseQuerier; - -/** - * @brief Non-modal preview of the decks contained in a shared-deck link. - * - * Lets the user pick which of the shared decks to open before anything is - * downloaded. Emits openRequested with the ids of the chosen decks, or - * cancelled when the user closes the dialog without choosing. Once the user - * picks, the dialog switches into a "downloading" state: the tiles and open - * buttons are disabled, a progress label shows the current download and Cancel - * stays functional so the download can be aborted. - */ -class DlgSharedDecksPreview : public QDialog -{ - Q_OBJECT - -public: - explicit DlgSharedDecksPreview(QWidget *parent, - const CardDatabaseQuerier *querier, - const QString &shareName, - qint64 expiresAt, - const QString &serverText, - const QList &items); - - void setDownloadProgress(int done, int total, const QString ¤tDeckName); - -public slots: - void setDownloading(bool downloading); - -signals: - void openRequested(const QList &itemIds); - void cancelled(); - -protected: - void closeEvent(QCloseEvent *event) override; - -private slots: - void openSelected(); - void openAll(); - void updateOpenSelectedEnabled(); - void onCancel(); - -private: - QList selectedItemIds() const; - - FlowWidget *flowWidget; - QList tiles; - QList itemIds; - QPushButton *openSelectedButton; - QPushButton *openAllButton; - QLabel *downloadStatusLabel; - bool resultEmitted = false; - bool downloadInProgress = false; -}; - -#endif // COCKATRICE_DLG_SHARED_DECKS_PREVIEW_H \ No newline at end of file diff --git a/cockatrice/src/interface/widgets/general/display/banner_widget.cpp b/cockatrice/src/interface/widgets/general/display/banner_widget.cpp index 31f384cac..5de5457ea 100644 --- a/cockatrice/src/interface/widgets/general/display/banner_widget.cpp +++ b/cockatrice/src/interface/widgets/general/display/banner_widget.cpp @@ -32,7 +32,6 @@ BannerWidget::BannerWidget(QWidget *parent, const QString &text, Qt::Orientation // Set minimum height for the widget setMinimumHeight(50); - setMaximumHeight(100); connect(this, &BannerWidget::buddyVisibilityChanged, this, &BannerWidget::toggleBuddyVisibility); updateDropdownIconState(); diff --git a/cockatrice/src/interface/widgets/general/home_tab_button_color.h b/cockatrice/src/interface/widgets/general/home_tab_button_color.h deleted file mode 100644 index b45bd7a92..000000000 --- a/cockatrice/src/interface/widgets/general/home_tab_button_color.h +++ /dev/null @@ -1,49 +0,0 @@ -#ifndef COCKATRICE_HOME_TAB_BUTTON_COLOR_H -#define COCKATRICE_HOME_TAB_BUTTON_COLOR_H - -#include - -namespace HomeTabButtonColor -{ - -/** - * @brief Where to get the colors for the home tab buttons from - */ -enum Source -{ - FromThemeColors, ///< Use the theme's identity accent colors - FromBackground, ///< Extract colour from the background image -}; - -struct Entry -{ - Source source; - const char *trKey; ///< key for translation -}; - -inline QList all() -{ - static QList entries = {{FromThemeColors, QT_TR_NOOP("From theme colors")}, - {FromBackground, QT_TR_NOOP("Extract from background")}}; - - return entries; -} - -/** - * Safely converts an int into the corresponding Source. - * - * @param value The int value - * @return The Source. Returns Source::FromThemeColors if the value is not within range - */ -inline Source intToSource(int value) -{ - if (value > FromBackground) { - return FromThemeColors; // default - } - - return static_cast(value); -} - -} // namespace HomeTabButtonColor - -#endif // COCKATRICE_HOME_TAB_BUTTON_COLOR_H diff --git a/cockatrice/src/interface/widgets/general/home_widget.cpp b/cockatrice/src/interface/widgets/general/home_widget.cpp index 648d315f9..8589e3517 100644 --- a/cockatrice/src/interface/widgets/general/home_widget.cpp +++ b/cockatrice/src/interface/widgets/general/home_widget.cpp @@ -2,17 +2,12 @@ #include "../../../client/settings/cache_settings.h" #include "../../../interface/widgets/tabs/tab_supervisor.h" -#include "../../pixel_map_generator.h" #include "../../theme_manager.h" #include "../../window_main.h" -#include "../cards/art_crop_attribution.h" #include "background_sources.h" #include "home_styled_button.h" -#include "home_tab_button_color.h" #include -#include -#include #include #include #include @@ -23,13 +18,13 @@ #include HomeWidget::HomeWidget(QWidget *parent, TabSupervisor *_tabSupervisor) - : QWidget(parent), tabSupervisor(_tabSupervisor), background(themePixmap(QStringLiteral("backgrounds/home"))) + : QWidget(parent), tabSupervisor(_tabSupervisor), background("theme:backgrounds/home"), overlay("theme:cockatrice") { layout = new QGridLayout(this); backgroundSourceCard = new CardInfoPictureArtCropWidget(this); - gradientColors = determineButtonColor(); + gradientColors = extractDominantColors(background); layout->addWidget(createButtons(), 1, 1, Qt::AlignVCenter | Qt::AlignHCenter); @@ -55,27 +50,14 @@ HomeWidget::HomeWidget(QWidget *parent, TabSupervisor *_tabSupervisor) // Lambda is cleaner to read than overloading this connect(&SettingsCache::instance().appearance(), &AppearanceSettings::homeTabDisplayCardNameChanged, this, [this] { repaint(); }); - connect(&SettingsCache::instance().appearance(), &AppearanceSettings::homeTabBackgroundDimChanged, this, - [this] { repaint(); }); connect(&SettingsCache::instance(), &SettingsCache::themeChanged, this, &HomeWidget::initializeBackgroundFromSource); connect(&SettingsCache::instance(), &SettingsCache::themeChanged, this, &HomeWidget::updateButtonsToBackgroundColor); - // Scheme flips (light/dark/system with an OS switch) fire on themeManager, - // not on SettingsCache::themeChanged, so re-resolve the variant background. - connect(themeManager, &ThemeManager::themeChanged, this, &HomeWidget::initializeBackgroundFromSource); - connect(themeManager, &ThemeManager::paletteChanged, this, &HomeWidget::updateButtonsToBackgroundColor); - connect(themeManager, &ThemeManager::paletteChanged, this, &HomeWidget::updateLogoOverlay); - connect(&SettingsCache::instance().appearance(), &AppearanceSettings::homeTabButtonColorChanged, this, - &HomeWidget::updateButtonsToBackgroundColor); } void HomeWidget::initializeBackgroundFromSource() { - // The featured logo is theme/scheme-derived too; reload it alongside the - // background so a theme or appearance switch doesn't leave it stale. - updateLogoOverlay(); - if (CardDatabaseManager::getInstance()->getLoadStatus() != LoadStatus::Ok) { connect(CardDatabaseManager::getInstance(), &CardDatabase::cardDatabaseLoadingFinished, this, &HomeWidget::initializeBackgroundFromSource); @@ -88,7 +70,7 @@ void HomeWidget::initializeBackgroundFromSource() switch (backgroundSourceType) { case BackgroundSources::Theme: cardChangeTimer->stop(); - background = themePixmap(QStringLiteral("backgrounds/home")); + background = QPixmap("theme:backgrounds/home"); backgroundSourceDeck = DeckList(); backgroundSourceCard->setCard(ExactCard()); updateButtonsToBackgroundColor(); @@ -114,26 +96,6 @@ void HomeWidget::loadBackgroundSourceDeck() backgroundSourceDeck = deckOpt.has_value() ? deckOpt.value().deckList : DeckList(); } -static QPair paletteDerivedButtonColors() -{ - return {themeManager->appColor(AppColor::AccentStrong), themeManager->appColor(AppColor::AccentSoft)}; -} - -QPair HomeWidget::determineButtonColor() const -{ - auto colorSource = - HomeTabButtonColor::intToSource(SettingsCache::instance().appearance().getHomeTabButtonColorSourceIndex()); - - switch (colorSource) { - case HomeTabButtonColor::FromThemeColors: - return paletteDerivedButtonColors(); - case HomeTabButtonColor::FromBackground: - return extractDominantColors(background); - } - - return paletteDerivedButtonColors(); -} - void HomeWidget::setRandomCard(ExactCard &newCard) { static constexpr int ATTEMPTS = 10; @@ -208,7 +170,7 @@ void HomeWidget::updateBackgroundProperties() void HomeWidget::updateButtonsToBackgroundColor() { - gradientColors = determineButtonColor(); + gradientColors = extractDominantColors(background); for (HomeStyledButton *button : findChildren()) { button->updateStylesheet(gradientColors); button->update(); @@ -235,10 +197,10 @@ QGroupBox *HomeWidget::createButtons() QVBoxLayout *boxLayout = new QVBoxLayout; boxLayout->setAlignment(Qt::AlignHCenter); - logoLabel = new QLabel; + QLabel *logoLabel = new QLabel; + logoLabel->setPixmap(overlay.scaledToWidth(200, Qt::SmoothTransformation)); logoLabel->setAlignment(Qt::AlignCenter); boxLayout->addWidget(logoLabel); - updateLogoOverlay(); boxLayout->addSpacing(25); connectButton = new HomeStyledButton("Connect/Play", gradientColors); @@ -303,6 +265,11 @@ void HomeWidget::updateConnectButton(const ClientStatus status) QPair HomeWidget::extractDominantColors(const QPixmap &pixmap) { + if (themeManager->isBuiltInTheme() && SettingsCache::instance().appearance().getHomeTabBackgroundSource() == + BackgroundSources::toId(BackgroundSources::Theme)) { + return QPair(QColor::fromRgb(20, 140, 60), QColor::fromRgb(120, 200, 80)); + } + // Step 1: Downscale image for performance QImage image = pixmap.toImage() .scaled(64, 64, Qt::KeepAspectRatio, Qt::SmoothTransformation) @@ -366,19 +333,16 @@ void HomeWidget::paintEvent(QPaintEvent *event) painter.drawPixmap(topLeft, toDraw); } - if (SettingsCache::instance().appearance().getHomeTabBackgroundDim()) { - // Draw translucent black overlay with rounded corners - QRectF overlayRect(5, 5, width() - 10, height() - 10); - QPainterPath roundedRectPath; - roundedRectPath.addRoundedRect(overlayRect, 20, 20); + // Draw translucent black overlay with rounded corners + QRectF overlayRect(5, 5, width() - 10, height() - 10); + QPainterPath roundedRectPath; + roundedRectPath.addRoundedRect(overlayRect, 20, 20); - QColor semiTransparentBlack(0, 0, 0, static_cast(255 * 0.33)); - painter.fillPath(roundedRectPath, semiTransparentBlack); - } + QColor semiTransparentBlack(0, 0, 0, static_cast(255 * 0.33)); + painter.fillPath(roundedRectPath, semiTransparentBlack); - // Card name overlay (above the attribution, bottom-right) + // Card name overlay (bottom-right) QString cardName; - QString attribution; ExactCard card = backgroundSourceCard->getCard(); if (card) { cardName = card.getCardPtr()->getName(); @@ -386,27 +350,8 @@ void HomeWidget::paintEvent(QPaintEvent *event) cardName += " (" + card.getPrinting().getSet()->getCorrectedShortName() + ") " + card.getPrinting().getProperty("num"); } - attribution = buildArtAttribution(card); } - // Scryfall requires artist attribution wherever card art is shown cropped. - // Pin it to the bottom-right corner, using the same font as the card name pill, - // and align its right edge with the card name pill's right edge. - constexpr int margin = 15; - constexpr qreal attributionMargin = 4.0; - - QFont attributionFont = painter.font(); - attributionFont.setPointSize(14); - attributionFont.setBold(true); - painter.setFont(attributionFont); - - // paintArtAttribution insets the pill 4px from the given rect's right edge, - // so nudge the rect's right edge to land exactly on the pill's right edge. - QRectF attributionArea = rect(); - attributionArea.setRight(width() - margin + attributionMargin); - const QRectF attributionRect = paintArtAttribution(painter, attributionArea, attribution); - - // Card name bubble above the attribution (when enabled). if (!cardName.isEmpty() && SettingsCache::instance().appearance().getHomeTabDisplayCardName()) { QFont font = painter.font(); font.setPointSize(14); @@ -415,80 +360,24 @@ void HomeWidget::paintEvent(QPaintEvent *event) QFontMetrics fm(font); constexpr int padding = 10; + constexpr int margin = 15; QRect textRect = fm.boundingRect(cardName); - int bubbleBottom = height() - margin; - if (!attributionRect.isEmpty()) { - bubbleBottom = attributionRect.top() - 6; - } - const QRect nameBubbleRect(width() - textRect.width() - padding * 2 - margin, - bubbleBottom - textRect.height() - padding * 2, textRect.width() + padding * 2, - textRect.height() + padding * 2); + QRect bgRect(width() - textRect.width() - padding * 2 - margin, + height() - textRect.height() - padding * 2 - margin, textRect.width() + padding * 2, + textRect.height() + padding * 2); // Background bubble painter.setPen(Qt::NoPen); painter.setBrush(QColor(0, 0, 0, 160)); - painter.drawRoundedRect(nameBubbleRect, 8, 8); + painter.drawRoundedRect(bgRect, 8, 8); // Text painter.setPen(Qt::white); - painter.drawText(nameBubbleRect.adjusted(padding, padding, -padding, -padding), - Qt::AlignRight | Qt::AlignVCenter, cardName); + painter.drawText(bgRect.adjusted(padding, padding, -padding, -padding), Qt::AlignRight | Qt::AlignVCenter, + cardName); } QWidget::paintEvent(event); } - -void HomeWidget::updateLogoOverlay() -{ - // Emulate cockatrice.svg in Qt rather than rendering the baked-in SVG. - // The SVG has no separate plate: the gradient fills the bird's silhouette - // paths (light #c9fd62/AccentSoft at the top-left, dark #139740/AccentStrong - // toward the bottom-right — the SVG's linearGradient4265-7-8 stops along - // its userSpaceOnUse axis), and the white highlight path - // (cockatrice-logo-white) sits on top. So we paint that gradient clipped to - // the full logo silhouette (the full-color logo's alpha), then overlay the - // white mark. Colours stay fully theme-driven and independent of the static - // greens baked into the SVG. - const QColor strong = themeManager->appColor(AppColor::AccentStrong); - const QColor soft = themeManager->appColor(AppColor::AccentSoft); - - const QPixmap silhouette = themePixmap(QStringLiteral("cockatrice")).scaledToWidth(200, Qt::SmoothTransformation); - const QPixmap whiteMark = - themePixmap(QStringLiteral("cockatrice-logo-white")).scaledToWidth(200, Qt::SmoothTransformation); - if (silhouette.isNull() || whiteMark.isNull()) { - return; - } - - QPixmap composite(silhouette.size()); - composite.fill(Qt::transparent); - - { - QPainter painter(&composite); - painter.setRenderHint(QPainter::Antialiasing); - painter.setRenderHint(QPainter::SmoothPixmapTransform); - - // Recreate cockatrice.svg's own gradient geometry (linearGradient - // 4265-7-8, userSpaceOnUse): light AccentSoft at S=(-8.097,-97.746), - // dark AccentStrong at E=(162.455,295.208), on the SVG's 300x300 - // canvas. Scale those coordinates to this composite's size. - const qreal scale = composite.width() / 300.0; - QLinearGradient gradient(QPointF(-8.097, -97.746) * scale, QPointF(162.455, 295.208) * scale); - gradient.setColorAt(0.0, soft); - gradient.setColorAt(1.0, strong); - painter.fillRect(composite.rect(), gradient); - - // Clip the gradient to the full logo silhouette exactly as the SVG's - // gradient paths are confined to the bird. - painter.setCompositionMode(QPainter::CompositionMode_DestinationIn); - painter.drawPixmap(0, 0, silhouette); - - painter.setCompositionMode(QPainter::CompositionMode_SourceOver); - painter.drawPixmap(0, 0, whiteMark); - } - - if (logoLabel) { - logoLabel->setPixmap(composite); - } -} diff --git a/cockatrice/src/interface/widgets/general/home_widget.h b/cockatrice/src/interface/widgets/general/home_widget.h index 1cadc4a67..90d003aa7 100644 --- a/cockatrice/src/interface/widgets/general/home_widget.h +++ b/cockatrice/src/interface/widgets/general/home_widget.h @@ -15,9 +15,6 @@ #include #include -class QGridLayout; -class QLabel; - class HomeWidget : public QWidget { @@ -26,7 +23,7 @@ class HomeWidget : public QWidget public: HomeWidget(QWidget *parent, TabSupervisor *tabSupervisor); void updateRandomCard(); - static QPair extractDominantColors(const QPixmap &pixmap); + QPair extractDominantColors(const QPixmap &pixmap); public slots: void paintEvent(QPaintEvent *event) override; @@ -44,14 +41,12 @@ private: QPixmap background; CardInfoPictureArtCropWidget *backgroundSourceCard = nullptr; DeckList backgroundSourceDeck; - QLabel *logoLabel = nullptr; + QPixmap overlay; QPair gradientColors; HomeStyledButton *connectButton; void setRandomCard(ExactCard &newCard); void loadBackgroundSourceDeck(); - QPair determineButtonColor() const; - void updateLogoOverlay(); }; #endif // HOME_WIDGET_H diff --git a/cockatrice/src/interface/widgets/general/layout_containers/flow_widget.cpp b/cockatrice/src/interface/widgets/general/layout_containers/flow_widget.cpp index 01c9ac34e..025f457bd 100644 --- a/cockatrice/src/interface/widgets/general/layout_containers/flow_widget.cpp +++ b/cockatrice/src/interface/widgets/general/layout_containers/flow_widget.cpp @@ -7,7 +7,6 @@ #include "flow_widget.h" #include -#include #include #include #include @@ -81,35 +80,13 @@ FlowWidget::FlowWidget(QWidget *parent, /** * @brief Adds a widget to the flow layout within the FlowWidget. * - * Plain widgets are not filtered for arrow keys: intercepting them would steal - * Up/Down/Left/Right from controls that use them (combo boxes, spin boxes - * etc.). Widgets that want keyboard navigation between flow items must be - * added via addNavigableWidget instead. - * * @param widget_to_add The widget to add to the flow layout. */ -void FlowWidget::addWidget(QWidget *widget_to_add) +void FlowWidget::addWidget(QWidget *widget_to_add) const { flowLayout->addWidget(widget_to_add); } -/** - * @brief Adds a widget and routes its arrow keys to FlowWidget focus navigation. - * - * The widget is filtered for arrow-key events so keyboard navigation between - * the flow items keeps working even when the flow sits inside a QScrollArea, - * which swallows arrow keys before they can reach FlowWidget::keyPressEvent. - * Only widgets added through this method are affected; anything that needs its - * own arrow keys should use plain addWidget. - * - * @param widget_to_add The widget to add to the flow layout. - */ -void FlowWidget::addNavigableWidget(QWidget *widget_to_add) -{ - widget_to_add->installEventFilter(this); - flowLayout->addWidget(widget_to_add); -} - void FlowWidget::insertWidgetAtIndex(QWidget *toInsert, int index) { flowLayout->insertWidgetAtIndex(toInsert, index); @@ -200,66 +177,6 @@ QLayoutItem *FlowWidget::itemAt(int index) const return flowLayout->itemAt(index); } -void FlowWidget::keyPressEvent(QKeyEvent *event) -{ - if (moveFocus(event)) { - event->accept(); - return; - } - QWidget::keyPressEvent(event); -} - -bool FlowWidget::eventFilter(QObject *watched, QEvent *event) -{ - if (event->type() == QEvent::KeyPress && moveFocus(static_cast(event))) { - return true; - } - return QWidget::eventFilter(watched, event); -} - -bool FlowWidget::moveFocus(QKeyEvent *event) -{ - // Keyboard navigation between the flow items: arrow keys move focus just - // like clicking the sibling tiles would. Only items that can take keyboard - // focus (e.g. the deck-preview tiles in shared-deck links) are visited. - const bool moveForward = event->key() == Qt::Key_Right || event->key() == Qt::Key_Down; - const bool moveBackward = event->key() == Qt::Key_Left || event->key() == Qt::Key_Up; - if (!moveForward && !moveBackward) { - return false; - } - - QList focusableItems; - for (int i = 0; i < flowLayout->count(); ++i) { - QWidget *item = flowLayout->itemAt(i)->widget(); - if (item != nullptr && (item->focusPolicy() & Qt::TabFocus)) { - focusableItems.append(item); - } - } - - if (focusableItems.isEmpty()) { - return false; - } - - int currentIndex = -1; - for (int i = 0; i < focusableItems.size(); ++i) { - if (focusableItems.at(i)->hasFocus()) { - currentIndex = i; - break; - } - } - - const int delta = moveForward ? 1 : -1; - int nextIndex; - if (currentIndex < 0) { - nextIndex = moveForward ? 0 : focusableItems.size() - 1; - } else { - nextIndex = (currentIndex + delta + focusableItems.size()) % focusableItems.size(); - } - focusableItems.value(nextIndex)->setFocus(); - event->accept(); - return true; -} - int FlowWidget::count() const { return flowLayout->count(); diff --git a/cockatrice/src/interface/widgets/general/layout_containers/flow_widget.h b/cockatrice/src/interface/widgets/general/layout_containers/flow_widget.h index 4d52db3f1..a232336d8 100644 --- a/cockatrice/src/interface/widgets/general/layout_containers/flow_widget.h +++ b/cockatrice/src/interface/widgets/general/layout_containers/flow_widget.h @@ -11,7 +11,6 @@ #include "../../../layouts/flow_layout.h" #include -#include #include #include #include @@ -29,8 +28,7 @@ public: Qt::ScrollBarPolicy horizontalPolicy, Qt::ScrollBarPolicy verticalPolicy); - void addWidget(QWidget *widget_to_add); - void addNavigableWidget(QWidget *widget_to_add); + void addWidget(QWidget *widget_to_add) const; void insertWidgetAtIndex(QWidget *toInsert, int index); void removeWidget(QWidget *widgetToRemove) const; void clearLayout(); @@ -45,15 +43,9 @@ public slots: void setSpacing(int hSpacing, int vSpacing); protected: - bool eventFilter(QObject *watched, QEvent *event) override; void resizeEvent(QResizeEvent *event) override; - void keyPressEvent(QKeyEvent *event) override; private: - /// @brief Moves keyboard focus to an adjacent flow item for an arrow-key event. - /// @return True when the event was an arrow key and was handled. - bool moveFocus(QKeyEvent *event); - Qt::Orientation flowDirection; QHBoxLayout *mainLayout; FlowLayout *flowLayout; diff --git a/cockatrice/src/interface/widgets/menus/deck_editor_menu.cpp b/cockatrice/src/interface/widgets/menus/deck_editor_menu.cpp index 6e27b4e42..f057680be 100644 --- a/cockatrice/src/interface/widgets/menus/deck_editor_menu.cpp +++ b/cockatrice/src/interface/widgets/menus/deck_editor_menu.cpp @@ -28,9 +28,6 @@ DeckEditorMenu::DeckEditorMenu(AbstractTabDeckEditor *parent) : QMenu(parent), d aSaveDeckAs = new QAction(QString(), this); connect(aSaveDeckAs, &QAction::triggered, deckEditor, &AbstractTabDeckEditor::actSaveDeckAs); - aShareDeck = new QAction(QString(), this); - connect(aShareDeck, &QAction::triggered, deckEditor, &AbstractTabDeckEditor::actShareDeck); - aLoadDeckFromClipboard = new QAction(QString(), this); connect(aLoadDeckFromClipboard, &QAction::triggered, deckEditor, &AbstractTabDeckEditor::actLoadDeckFromClipboard); @@ -99,7 +96,6 @@ DeckEditorMenu::DeckEditorMenu(AbstractTabDeckEditor *parent) : QMenu(parent), d addMenu(loadRecentDeckMenu); addAction(aSaveDeck); addAction(aSaveDeckAs); - addAction(aShareDeck); addSeparator(); addAction(aLoadDeckFromClipboard); addMenu(editDeckInClipboardMenu); @@ -124,7 +120,6 @@ void DeckEditorMenu::setSaveStatus(bool newStatus) { aSaveDeck->setEnabled(newStatus); aSaveDeckAs->setEnabled(newStatus); - aShareDeck->setEnabled(newStatus); aSaveDeckToClipboard->setEnabled(newStatus); aSaveDeckToClipboardNoSetInfo->setEnabled(newStatus); aSaveDeckToClipboardRaw->setEnabled(newStatus); @@ -162,7 +157,6 @@ void DeckEditorMenu::retranslateUi() aClearRecents->setText(tr("Clear")); aSaveDeck->setText(tr("&Save deck")); aSaveDeckAs->setText(tr("Save deck &as...")); - aShareDeck->setText(tr("Share deck...")); aLoadDeckFromClipboard->setText(tr("Load deck from cl&ipboard...")); diff --git a/cockatrice/src/interface/widgets/menus/deck_editor_menu.h b/cockatrice/src/interface/widgets/menus/deck_editor_menu.h index 08ce7bba1..eff9257bb 100644 --- a/cockatrice/src/interface/widgets/menus/deck_editor_menu.h +++ b/cockatrice/src/interface/widgets/menus/deck_editor_menu.h @@ -21,8 +21,7 @@ public: QAction *aNewDeck, *aLoadDeck, *aClearRecents, *aSaveDeck, *aSaveDeckAs, *aLoadDeckFromClipboard, *aEditDeckInClipboard, *aEditDeckInClipboardRaw, *aSaveDeckToClipboard, *aSaveDeckToClipboardNoSetInfo, *aSaveDeckToClipboardRaw, *aSaveDeckToClipboardRawNoSetInfo, *aPrintDeck, *aLoadDeckFromWebsite, - *aExportDeckDecklist, *aExportDeckDecklistXyz, *aAnalyzeDeckDeckstats, *aAnalyzeDeckTappedout, *aShareDeck, - *aClose; + *aExportDeckDecklist, *aExportDeckDecklistXyz, *aAnalyzeDeckDeckstats, *aAnalyzeDeckTappedout, *aClose; QMenu *loadRecentDeckMenu, *analyzeDeckMenu, *editDeckInClipboardMenu, *saveDeckToClipboardMenu; void setSaveStatus(bool newStatus); diff --git a/cockatrice/src/interface/widgets/onboarding/banner_shader_config.h b/cockatrice/src/interface/widgets/onboarding/banner_shader_config.h deleted file mode 100644 index 6008044ff..000000000 --- a/cockatrice/src/interface/widgets/onboarding/banner_shader_config.h +++ /dev/null @@ -1,313 +0,0 @@ -#ifndef BANNER_SHADER_CONFIG_H -#define BANNER_SHADER_CONFIG_H - -#include -#include - -/** - * Uniform values fed to brand_banner.frag, exposed to QML as the - * "bannerConfig" context property. - * - * Two independent "banks" (A/B) each carry their own mode/speed/seed so - * BrandBanner.qml can render both simultaneously and crossfade between - * them via opacity -- see frontIsA. The shared palette (colorA/colorB/ - * accent) and clock (time/aspect) apply to both banks identically, since - * only the foreground motif changes between onboarding pages, never the - * brand palette. - * - * Deliberately plain `property` (not `required property`) on the QML side - * -- a required-property shadowing bug bit the home-screen particle - * background before, and there's no reason to reintroduce that risk here. - */ -class BannerShaderConfig : public QObject -{ - Q_OBJECT - Q_PROPERTY(qreal time READ time WRITE setTime NOTIFY timeChanged) - Q_PROPERTY(qreal aspect READ aspect WRITE setAspect NOTIFY aspectChanged) - - Q_PROPERTY(qreal modeA READ modeA WRITE setModeA NOTIFY modeAChanged) - Q_PROPERTY(qreal speedA READ speedA WRITE setSpeedA NOTIFY speedAChanged) - Q_PROPERTY(qreal seedA READ seedA WRITE setSeedA NOTIFY seedAChanged) - - Q_PROPERTY(qreal modeB READ modeB WRITE setModeB NOTIFY modeBChanged) - Q_PROPERTY(qreal speedB READ speedB WRITE setSpeedB NOTIFY speedBChanged) - Q_PROPERTY(qreal seedB READ seedB WRITE setSeedB NOTIFY seedBChanged) - - Q_PROPERTY(bool frontIsA READ frontIsA WRITE setFrontIsA NOTIFY frontIsAChanged) - - Q_PROPERTY(QColor colorA READ colorA WRITE setColorA NOTIFY colorAChanged) - Q_PROPERTY(QColor colorB READ colorB WRITE setColorB NOTIFY colorBChanged) - Q_PROPERTY(QColor accent READ accent WRITE setAccent NOTIFY accentChanged) - Q_PROPERTY(QColor glowColor READ glowColor WRITE setGlowColor NOTIFY glowColorChanged) - Q_PROPERTY(QColor brandStrong READ brandStrong WRITE setBrandStrong NOTIFY brandStrongChanged) - Q_PROPERTY(QColor brandSoft READ brandSoft WRITE setBrandSoft NOTIFY brandSoftChanged) - Q_PROPERTY(qreal vignetteMin READ vignetteMin WRITE setVignetteMin NOTIFY vignetteMinChanged) - - Q_PROPERTY(bool logoVisible READ logoVisible WRITE setLogoVisible NOTIFY logoVisibleChanged) - Q_PROPERTY(qreal logoGlow READ logoGlow WRITE setLogoGlow NOTIFY logoGlowChanged) - -public: - explicit BannerShaderConfig(QObject *parent = nullptr) : QObject(parent) - { - } - - qreal time() const - { - return m_time; - } - void setTime(qreal v) - { - if (v != m_time) { - m_time = v; - emit timeChanged(); - } - } - - qreal aspect() const - { - return m_aspect; - } - void setAspect(qreal v) - { - if (v != m_aspect) { - m_aspect = v; - emit aspectChanged(); - } - } - - qreal modeA() const - { - return m_modeA; - } - void setModeA(qreal v) - { - if (v != m_modeA) { - m_modeA = v; - emit modeAChanged(); - } - } - qreal speedA() const - { - return m_speedA; - } - void setSpeedA(qreal v) - { - if (v != m_speedA) { - m_speedA = v; - emit speedAChanged(); - } - } - qreal seedA() const - { - return m_seedA; - } - void setSeedA(qreal v) - { - if (v != m_seedA) { - m_seedA = v; - emit seedAChanged(); - } - } - - qreal modeB() const - { - return m_modeB; - } - void setModeB(qreal v) - { - if (v != m_modeB) { - m_modeB = v; - emit modeBChanged(); - } - } - qreal speedB() const - { - return m_speedB; - } - void setSpeedB(qreal v) - { - if (v != m_speedB) { - m_speedB = v; - emit speedBChanged(); - } - } - qreal seedB() const - { - return m_seedB; - } - void setSeedB(qreal v) - { - if (v != m_seedB) { - m_seedB = v; - emit seedBChanged(); - } - } - - bool frontIsA() const - { - return m_frontIsA; - } - void setFrontIsA(bool v) - { - if (v != m_frontIsA) { - m_frontIsA = v; - emit frontIsAChanged(); - } - } - - QColor colorA() const - { - return m_colorA; - } - void setColorA(const QColor &c) - { - if (c != m_colorA) { - m_colorA = c; - emit colorAChanged(); - } - } - QColor colorB() const - { - return m_colorB; - } - void setColorB(const QColor &c) - { - if (c != m_colorB) { - m_colorB = c; - emit colorBChanged(); - } - } - QColor accent() const - { - return m_accent; - } - void setAccent(const QColor &c) - { - if (c != m_accent) { - m_accent = c; - emit accentChanged(); - } - } - - QColor glowColor() const - { - return m_glowColor; - } - void setGlowColor(const QColor &c) - { - if (c != m_glowColor) { - m_glowColor = c; - emit glowColorChanged(); - } - } - - QColor brandStrong() const - { - return m_brandStrong; - } - void setBrandStrong(const QColor &c) - { - if (c != m_brandStrong) { - m_brandStrong = c; - emit brandStrongChanged(); - } - } - - QColor brandSoft() const - { - return m_brandSoft; - } - void setBrandSoft(const QColor &c) - { - if (c != m_brandSoft) { - m_brandSoft = c; - emit brandSoftChanged(); - } - } - - qreal vignetteMin() const - { - return m_vignetteMin; - } - void setVignetteMin(qreal v) - { - if (v != m_vignetteMin) { - m_vignetteMin = v; - emit vignetteMinChanged(); - } - } - - bool logoVisible() const - { - return m_logoVisible; - } - void setLogoVisible(bool v) - { - if (v != m_logoVisible) { - m_logoVisible = v; - emit logoVisibleChanged(); - } - } - - qreal logoGlow() const - { - return m_logoGlow; - } - void setLogoGlow(qreal v) - { - if (v != m_logoGlow) { - m_logoGlow = v; - emit logoGlowChanged(); - } - } - -signals: - void timeChanged(); - void aspectChanged(); - void modeAChanged(); - void speedAChanged(); - void seedAChanged(); - void modeBChanged(); - void speedBChanged(); - void seedBChanged(); - void frontIsAChanged(); - void colorAChanged(); - void colorBChanged(); - void accentChanged(); - void glowColorChanged(); - void brandStrongChanged(); - void brandSoftChanged(); - void vignetteMinChanged(); - void logoVisibleChanged(); - void logoGlowChanged(); - -private: - qreal m_time = 0.0; - qreal m_aspect = 16.0 / 9.0; - - qreal m_modeA = 0.0; - qreal m_speedA = 1.0; - qreal m_seedA = 0.0; - - qreal m_modeB = 0.0; - qreal m_speedB = 1.0; - qreal m_seedB = 0.0; - - bool m_frontIsA = true; - - // Curated fallback seed values -- BannerHost overwrites these with - // palette-derived colours (see shader_banner_widget.cpp) before the first - // paint, so they only matter as a safe pre-first-apply default. - QColor m_colorA{0x1A, 0x1A, 0x20}; - QColor m_colorB{0x0E, 0x0E, 0x12}; - QColor m_accent{0x8B, 0xDD, 0x6B}; - QColor m_glowColor{Qt::white}; - QColor m_brandStrong{0x13, 0x97, 0x40}; - QColor m_brandSoft{0xC9, 0xFD, 0x62}; - qreal m_vignetteMin = 0.62; - - bool m_logoVisible = false; - qreal m_logoGlow = 1.0; -}; - -#endif // BANNER_SHADER_CONFIG_H diff --git a/cockatrice/src/interface/widgets/onboarding/brand_colors.h b/cockatrice/src/interface/widgets/onboarding/brand_colors.h deleted file mode 100644 index bf173270b..000000000 --- a/cockatrice/src/interface/widgets/onboarding/brand_colors.h +++ /dev/null @@ -1,15 +0,0 @@ -#ifndef BRAND_COLORS_H -#define BRAND_COLORS_H - -#include - -/** @brief Cockatrice brand green. - * - * Single source of truth for the onboarding brand accent: it backs the - * banner's shader-accent uniform as the curated fallback when the active - * palette resolves no usable Highlight, and it preseads the wizard's - * QuickSetupPanel so a freshly generated palette keeps the brand identity - * until the user picks their own look. */ -inline const QColor kCockatriceBrandGreen(0x8B, 0xDD, 0x6B); - -#endif // BRAND_COLORS_H \ No newline at end of file diff --git a/cockatrice/src/interface/widgets/onboarding/first_run_wizard.cpp b/cockatrice/src/interface/widgets/onboarding/first_run_wizard.cpp deleted file mode 100644 index 4af02fe4f..000000000 --- a/cockatrice/src/interface/widgets/onboarding/first_run_wizard.cpp +++ /dev/null @@ -1,225 +0,0 @@ -#include "first_run_wizard.h" - -#include "first_run_wizard_page.h" -#include "pages/account_setup_page.h" -#include "pages/card_database_setup_page.h" -#include "pages/finish_page.h" -#include "pages/preferences_setup_page.h" -#include "pages/theme_setup_page.h" -#include "pages/welcome_page.h" -#include "shader_banner_widget.h" -#include "step_indicator_widget.h" - -#include -#include -#include -#include -#include -#include -#include -#include - -FirstRunWizard::FirstRunWizard(QWidget *parent) : QDialog(parent) -{ - setWindowFlag(Qt::WindowContextHelpButtonHint, false); - setMinimumSize(640, 490); - resize(720, 550); - - bannerHost = new BannerHost(this); - - titleLabel = new QLabel(this); - QFont titleFont = titleLabel->font(); - titleFont.setPointSizeF(titleFont.pointSizeF() * 1.4); - titleFont.setBold(true); - titleLabel->setFont(titleFont); - - subtitleLabel = new QLabel(this); - subtitleLabel->setWordWrap(true); - - stack = new QStackedWidget(this); - stepIndicator = new StepIndicatorWidget(this); - - backButton = new QPushButton(this); - skipButton = new QPushButton(this); - nextButton = new QPushButton(this); - nextButton->setDefault(true); - - connect(backButton, &QPushButton::clicked, this, &FirstRunWizard::goBack); - connect(skipButton, &QPushButton::clicked, this, &FirstRunWizard::skip); - connect(nextButton, &QPushButton::clicked, this, &FirstRunWizard::goNext); - - auto *headerLayout = new QVBoxLayout; - headerLayout->setContentsMargins(0, 0, 0, 0); - headerLayout->addWidget(bannerHost); - headerLayout->addSpacing(12); - headerLayout->addWidget(titleLabel); - headerLayout->addWidget(subtitleLabel); - - auto *navLayout = new QHBoxLayout; - navLayout->addWidget(backButton); - navLayout->addWidget(skipButton); - navLayout->addStretch(); - navLayout->addWidget(stepIndicator); - navLayout->addStretch(); - navLayout->addWidget(nextButton); - - auto *root = new QVBoxLayout(this); - root->addLayout(headerLayout); - root->addSpacing(8); - root->addWidget(stack, 1); - root->addSpacing(8); - root->addLayout(navLayout); - - auto *welcome = new WelcomePage(this); - auto *cardDb = new CardDatabaseSetupPage(this); - auto *theme = new ThemeSetupPage(this); - auto *account = new AccountSetupPage(this); - auto *prefs = new PreferencesSetupPage(this); - auto *finishPg = new FinishPage(this); - - cardDatabasePage = cardDb; - - connect(cardDb, &CardDatabaseSetupPage::updateRequested, this, &FirstRunWizard::cardDatabaseUpdateRequested); - connect(cardDb, &CardDatabaseSetupPage::manualSetupRequested, this, - &FirstRunWizard::manualCardDatabaseSetupRequested); - connect(account, &AccountSetupPage::registerRequested, this, &FirstRunWizard::registerRequested); - connect(account, &AccountSetupPage::connectRequested, this, &FirstRunWizard::connectRequested); - - connect(cardDb, &CardDatabaseSetupPage::advanceRequested, this, [this] { - if (stack->currentWidget() == cardDatabasePage) { - showPage(currentIndex + 1); - } - }); - - addPage(welcome); - addPage(cardDb); - addPage(theme); - addPage(account); - addPage(prefs); - addPage(finishPg); - - stepIndicator->setStepCount(pages.count()); - retranslateUi(); - showPage(0); -} - -void FirstRunWizard::addPage(FirstRunWizardPage *page) -{ - pages.append(page); - stack->addWidget(page); - connect(page, &FirstRunWizardPage::completeChanged, this, &FirstRunWizard::updateChrome); -} - -void FirstRunWizard::showPage(int index) -{ - if (index < 0 || index >= pages.count()) { - return; - } - currentIndex = index; - stack->setCurrentIndex(index); - pages[index]->initializePage(); - stepIndicator->setCurrentStep(index); - static const QList motifs = { - BannerHost::Motif::Welcome, BannerHost::Motif::CardDatabase, BannerHost::Motif::Theming, - BannerHost::Motif::Account, BannerHost::Motif::Preferences, BannerHost::Motif::Finish, - }; - if (index < motifs.size()) { - bannerHost->setMotif(motifs[index]); - } - titleLabel->setText(pages[index]->stepTitle()); - subtitleLabel->setText(pages[index]->stepSubtitle()); - subtitleLabel->setVisible(!pages[index]->stepSubtitle().isEmpty()); - updateChrome(); -} - -void FirstRunWizard::updateChrome() -{ - if (currentIndex < 0) { - return; - } - FirstRunWizardPage *page = pages[currentIndex]; - const bool isLast = (currentIndex == pages.count() - 1); - - backButton->setVisible(currentIndex > 0); - skipButton->setVisible(page->isSkippable()); - nextButton->setEnabled(page->isComplete()); - - QString customText = page->nextButtonText(); - if (!customText.isEmpty()) { - nextButton->setText(customText); - } else { - nextButton->setText(isLast ? tr("Finish") : tr("Next")); - } -} - -void FirstRunWizard::goNext() -{ - FirstRunWizardPage *page = pages[currentIndex]; - if (!page->validatePage() || !page->handleNextClick()) { - return; - } - if (currentIndex == pages.count() - 1) { - finish(); - return; - } - showPage(currentIndex + 1); -} - -void FirstRunWizard::goBack() -{ - showPage(currentIndex - 1); -} - -void FirstRunWizard::skip() -{ - showPage(currentIndex + 1); -} - -void FirstRunWizard::onCardDatabaseUpdateFinished(bool success) -{ - if (cardDatabasePage) { - cardDatabasePage->onUpdateFinished(success); - } -} - -void FirstRunWizard::onCardDatabaseUpdateProgress(const QString &stage, qint64 done, qint64 total) -{ - if (cardDatabasePage) { - cardDatabasePage->onUpdateProgress(stage, done, total); - } -} - -void FirstRunWizard::finish() -{ - accept(); -} - -void FirstRunWizard::closeEvent(QCloseEvent *event) -{ - // Every step persists its own choice as it's made, so closing early - // isn't destructive -- treat it exactly like reaching the end. - QDialog::closeEvent(event); -} - -void FirstRunWizard::changeEvent(QEvent *event) -{ - if (event->type() == QEvent::LanguageChange) { - retranslateUi(); - } - QDialog::changeEvent(event); -} - -void FirstRunWizard::retranslateUi() -{ - setWindowTitle(tr("Welcome to Cockatrice")); - backButton->setText(tr("Back")); - skipButton->setText(tr("Skip")); - for (FirstRunWizardPage *page : std::as_const(pages)) { - page->retranslateUi(); - } - if (currentIndex >= 0) { - titleLabel->setText(pages[currentIndex]->stepTitle()); - subtitleLabel->setText(pages[currentIndex]->stepSubtitle()); - } - updateChrome(); -} diff --git a/cockatrice/src/interface/widgets/onboarding/first_run_wizard.h b/cockatrice/src/interface/widgets/onboarding/first_run_wizard.h deleted file mode 100644 index 21d7b6e06..000000000 --- a/cockatrice/src/interface/widgets/onboarding/first_run_wizard.h +++ /dev/null @@ -1,74 +0,0 @@ -#ifndef FIRST_RUN_WIZARD_H -#define FIRST_RUN_WIZARD_H - -#include -#include - -class BannerHost; -class FirstRunWizardPage; -class StepIndicatorWidget; -class CardDatabaseSetupPage; -class QLabel; -class QPushButton; -class QStackedWidget; - -/** @brief Polished first-run onboarding flow: card database setup, theme - * selection, server account setup, and a handful of key preferences. - * - * Deliberately ignorant of network/registration/download internals -- - * pages that need them emit request signals for MainWindow to fulfill. - * Every choice is written to SettingsCache as it's made (via the pages - * themselves, same as AppearanceSettingsPage does), so "Skip" or closing - * the window never discards anything already confirmed. */ -class FirstRunWizard : public QDialog -{ - Q_OBJECT - -public: - explicit FirstRunWizard(QWidget *parent = nullptr); - -signals: - void registerRequested(); - void connectRequested(); - void cardDatabaseUpdateRequested(); - void manualCardDatabaseSetupRequested(); - -public slots: - /** @brief Forwarded from MainWindow once the background card database update process exits. */ - void onCardDatabaseUpdateFinished(bool success); - - /** @brief Forwarded from MainWindow while the background card database update process runs. */ - void onCardDatabaseUpdateProgress(const QString &stage, qint64 done, qint64 total); - -protected: - void closeEvent(QCloseEvent *event) override; - void changeEvent(QEvent *event) override; - -private slots: - void goNext(); - void goBack(); - void skip(); - void updateChrome(); - -private: - void addPage(FirstRunWizardPage *page); - void showPage(int index); - void retranslateUi(); - void finish(); - - QStackedWidget *stack; - StepIndicatorWidget *stepIndicator; - BannerHost *bannerHost; - QLabel *titleLabel; - QLabel *subtitleLabel; - QPushButton *backButton; - QPushButton *skipButton; - QPushButton *nextButton; - - CardDatabaseSetupPage *cardDatabasePage = nullptr; - - QList pages; - int currentIndex = -1; -}; - -#endif // FIRST_RUN_WIZARD_H diff --git a/cockatrice/src/interface/widgets/onboarding/first_run_wizard_page.cpp b/cockatrice/src/interface/widgets/onboarding/first_run_wizard_page.cpp deleted file mode 100644 index 6da8958f2..000000000 --- a/cockatrice/src/interface/widgets/onboarding/first_run_wizard_page.cpp +++ /dev/null @@ -1 +0,0 @@ -#include "first_run_wizard_page.h" diff --git a/cockatrice/src/interface/widgets/onboarding/first_run_wizard_page.h b/cockatrice/src/interface/widgets/onboarding/first_run_wizard_page.h deleted file mode 100644 index bdcd123bd..000000000 --- a/cockatrice/src/interface/widgets/onboarding/first_run_wizard_page.h +++ /dev/null @@ -1,75 +0,0 @@ -#ifndef FIRST_RUN_WIZARD_PAGE_H -#define FIRST_RUN_WIZARD_PAGE_H - -#include - -/** @brief Base class for a single step of FirstRunWizard. - * - * QWidget-based rather than QWizardPage-based: FirstRunWizard is a - * QDialog + QStackedWidget shell (not a QWizard) so it can own the - * banner/step-dot chrome that QWizard's native styles don't give us - * consistent control over. Naming mirrors OracleWizardPage for - * familiarity only -- the two hierarchies are unrelated. */ -class FirstRunWizardPage : public QWidget -{ - Q_OBJECT - -public: - explicit FirstRunWizardPage(QWidget *parent = nullptr) : QWidget(parent) - { - } - - /** @brief Called every time the page becomes visible, including navigating back to it. */ - virtual void initializePage() - { - } - - /** @brief Called before advancing past this page. Return false to block navigation; - the page itself is responsible for telling the user why. */ - virtual bool validatePage() - { - return true; - } - - /** @brief Whether Next/Finish should currently be enabled. Pages doing async work - can flip this mid-step; emit completeChanged() when they do. */ - virtual bool isComplete() const - { - return true; - } - - /** @brief Whether the wizard's "Skip" button should be offered on this page. */ - virtual bool isSkippable() const - { - return false; - } - - virtual QString stepTitle() const = 0; - virtual QString stepSubtitle() const - { - return {}; - } - - /** @brief Override to replace the "Next"/"Finish" button text on this page. - Return an empty string to use the default label. */ - virtual QString nextButtonText() const - { - return {}; - } - - /** @brief Called when the user presses the Next button. Return true to allow - advancing to the next page, false to stay on this page (e.g. to - trigger an async action first). */ - virtual bool handleNextClick() - { - return true; - } - - virtual void retranslateUi() = 0; - -signals: - void completeChanged(); - void advanceRequested(); -}; - -#endif // FIRST_RUN_WIZARD_PAGE_H diff --git a/cockatrice/src/interface/widgets/onboarding/pages/account_setup_page.cpp b/cockatrice/src/interface/widgets/onboarding/pages/account_setup_page.cpp deleted file mode 100644 index 2107ea8bf..000000000 --- a/cockatrice/src/interface/widgets/onboarding/pages/account_setup_page.cpp +++ /dev/null @@ -1,56 +0,0 @@ -#include "account_setup_page.h" - -#include -#include -#include - -AccountSetupPage::AccountSetupPage(QWidget *parent) : FirstRunWizardPage(parent) -{ - bodyLabel = new QLabel(this); - bodyLabel->setWordWrap(true); - bodyLabel->setAlignment(Qt::AlignCenter); - - registerButton = new QPushButton(this); - connectButton = new QPushButton(this); - skipHintLabel = new QLabel(this); - skipHintLabel->setWordWrap(true); - skipHintLabel->setAlignment(Qt::AlignCenter); - - connect(registerButton, &QPushButton::clicked, this, &AccountSetupPage::registerRequested); - connect(connectButton, &QPushButton::clicked, this, &AccountSetupPage::connectRequested); - - auto *layout = new QVBoxLayout(this); - layout->addStretch(); - layout->addWidget(bodyLabel); - layout->addSpacing(16); - layout->addWidget(registerButton, 0, Qt::AlignHCenter); - layout->addWidget(connectButton, 0, Qt::AlignHCenter); - layout->addSpacing(16); - layout->addWidget(skipHintLabel); - layout->addStretch(); - - retranslateUi(); -} - -bool AccountSetupPage::isSkippable() const -{ - return true; -} - -QString AccountSetupPage::stepTitle() const -{ - return tr("Join a Server"); -} - -QString AccountSetupPage::stepSubtitle() const -{ - return tr("Optional — you can always do this later from the menu."); -} - -void AccountSetupPage::retranslateUi() -{ - bodyLabel->setText(tr("Playing online needs a server account.")); - registerButton->setText(tr("Register a new account…")); - connectButton->setText(tr("I already have one — Connect…")); - skipHintLabel->setText(tr("Just want to play locally? Skip this and connect whenever you're ready.")); -} \ No newline at end of file diff --git a/cockatrice/src/interface/widgets/onboarding/pages/account_setup_page.h b/cockatrice/src/interface/widgets/onboarding/pages/account_setup_page.h deleted file mode 100644 index 0d9b76699..000000000 --- a/cockatrice/src/interface/widgets/onboarding/pages/account_setup_page.h +++ /dev/null @@ -1,38 +0,0 @@ -#ifndef ACCOUNT_SETUP_PAGE_H -#define ACCOUNT_SETUP_PAGE_H - -#include "../first_run_wizard_page.h" - -class QLabel; -class QPushButton; - -/** @brief First-run account step. Does NOT embed DlgRegister's fields: they exist - * to be handed to ConnectionController's network registration flow, which - * this wizard has no visibility into. Reimplementing the fields here - * without that wiring would look functional and silently do nothing -- - * worse than reuse. So: a friendly landing spot that opens the *existing* - * DlgRegister / connect flow via signals FirstRunWizard forwards. */ -class AccountSetupPage : public FirstRunWizardPage -{ - Q_OBJECT - -public: - explicit AccountSetupPage(QWidget *parent = nullptr); - - bool isSkippable() const override; - QString stepTitle() const override; - QString stepSubtitle() const override; - void retranslateUi() override; - -signals: - void registerRequested(); - void connectRequested(); - -private: - QLabel *bodyLabel; - QPushButton *registerButton; - QPushButton *connectButton; - QLabel *skipHintLabel; -}; - -#endif // ACCOUNT_SETUP_PAGE_H diff --git a/cockatrice/src/interface/widgets/onboarding/pages/card_database_setup_page.cpp b/cockatrice/src/interface/widgets/onboarding/pages/card_database_setup_page.cpp deleted file mode 100644 index 50e8ff63d..000000000 --- a/cockatrice/src/interface/widgets/onboarding/pages/card_database_setup_page.cpp +++ /dev/null @@ -1,334 +0,0 @@ -#include "card_database_setup_page.h" - -#include "../../client/settings/cache_settings.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -CardDatabaseSetupPage::CardDatabaseSetupPage(QWidget *parent) : FirstRunWizardPage(parent) -{ - statusLabel = new QLabel(this); - statusLabel->setWordWrap(true); - statusLabel->setAlignment(Qt::AlignCenter); - - progressBar = new QProgressBar(this); - progressBar->setRange(0, 0); - progressBar->setTextVisible(false); - progressBar->setFixedWidth(280); - - retryButton = new QPushButton(this); - manualButton = new QPushButton(this); - - connect(retryButton, &QPushButton::clicked, this, [this] { - setState(State::Running); - emit updateRequested(); - }); - connect(manualButton, &QPushButton::clicked, this, &CardDatabaseSetupPage::manualSetupRequested); - - // ── Advanced: custom download source ─────────────────────────────── - advancedToggleButton = new QPushButton(this); - advancedToggleButton->setCheckable(true); - advancedToggleButton->setChecked(false); - advancedToggleButton->setFlat(true); - advancedToggleButton->setStyleSheet("QPushButton { text-align: left; padding: 5px 12px; font-weight: bold; }" - "QPushButton:checked { }"); - - advancedPanel = new QWidget(this); - advancedPanel->setVisible(false); - - urlLineEdit = new QLineEdit(advancedPanel); - urlHintLabel = new QLabel(advancedPanel); - urlHintLabel->setWordWrap(true); - - restoreDefaultUrlButton = new QPushButton(advancedPanel); - applyAndRetryButton = new QPushButton(advancedPanel); - - connect(advancedToggleButton, &QPushButton::toggled, this, &CardDatabaseSetupPage::onToggleAdvanced); - connect(restoreDefaultUrlButton, &QPushButton::clicked, this, &CardDatabaseSetupPage::onRestoreDefaultUrl); - connect(applyAndRetryButton, &QPushButton::clicked, this, &CardDatabaseSetupPage::onApplyCustomUrl); - - auto *advancedButtonRow = new QHBoxLayout; - advancedButtonRow->addWidget(restoreDefaultUrlButton); - advancedButtonRow->addStretch(); - advancedButtonRow->addWidget(applyAndRetryButton); - - auto *advancedLayout = new QVBoxLayout(advancedPanel); - advancedLayout->setContentsMargins(12, 4, 12, 4); - advancedLayout->addWidget(urlLineEdit); - advancedLayout->addWidget(urlHintLabel); - advancedLayout->addLayout(advancedButtonRow); - - // ── Startup card update check ─────────────────────────────────────── - auto &upd = SettingsCache::instance().updates(); - - const auto updateBehavior = [this] { - auto &u = SettingsCache::instance().updates(); - int idx = startupBehaviorCombo->currentIndex(); - u.setStartupCardUpdateCheckPromptForUpdate(idx == 1); - u.setStartupCardUpdateCheckAlwaysUpdate(idx == 2); - }; - - startupBehaviorLabel = new QLabel(this); - startupBehaviorCombo = new QComboBox(this); - startupBehaviorCombo->addItem(QString()); // placeholder, filled in retranslateUi - startupBehaviorCombo->addItem(QString()); - startupBehaviorCombo->addItem(QString()); - if (upd.getStartupCardUpdateCheckPromptForUpdate()) { - startupBehaviorCombo->setCurrentIndex(1); - } else if (upd.getStartupCardUpdateCheckAlwaysUpdate()) { - startupBehaviorCombo->setCurrentIndex(2); - } else { - startupBehaviorCombo->setCurrentIndex(0); - } - connect(startupBehaviorCombo, QOverload::of(&QComboBox::currentIndexChanged), this, updateBehavior); - - checkIntervalLabel = new QLabel(this); - checkIntervalSpinBox = new QSpinBox(this); - checkIntervalSpinBox->setMinimum(1); - checkIntervalSpinBox->setMaximum(30); - checkIntervalSpinBox->setValue(upd.getCardUpdateCheckInterval()); - connect(checkIntervalSpinBox, QOverload::of(&QSpinBox::valueChanged), &upd, - &UpdatesSettings::setCardUpdateCheckInterval); - - auto *checkGrid = new QGridLayout; - checkGrid->addWidget(startupBehaviorLabel, 0, 0); - checkGrid->addWidget(startupBehaviorCombo, 0, 1); - checkGrid->addWidget(checkIntervalLabel, 1, 0); - checkGrid->addWidget(checkIntervalSpinBox, 1, 1); - - auto *layout = new QVBoxLayout(this); - layout->addStretch(); - layout->addWidget(statusLabel); - layout->addSpacing(12); - layout->addWidget(progressBar, 0, Qt::AlignHCenter); - layout->addSpacing(12); - layout->addWidget(retryButton, 0, Qt::AlignHCenter); - layout->addWidget(manualButton, 0, Qt::AlignHCenter); - layout->addSpacing(16); - layout->addWidget(advancedToggleButton); - layout->addWidget(advancedPanel); - layout->addSpacing(8); - layout->addLayout(checkGrid); - layout->addStretch(); - - retranslateUi(); -} - -bool CardDatabaseSetupPage::alreadyHaveDatabase() const -{ - return CardDatabaseManager::getInstance()->getCardList().count() > 0; -} - -QString CardDatabaseSetupPage::oracleSettingsFilePath() const -{ - return SettingsCache::instance().getSettingsPath() + "oracle.ini"; -} - -QString CardDatabaseSetupPage::readCustomUrl() const -{ - QSettings oracleSettings(oracleSettingsFilePath(), QSettings::IniFormat); - return oracleSettings.value("allsetsurl").toString(); -} - -void CardDatabaseSetupPage::writeCustomUrl(const QString &url) -{ - QSettings oracleSettings(oracleSettingsFilePath(), QSettings::IniFormat); - if (url.isEmpty()) { - oracleSettings.remove("allsetsurl"); - } else { - oracleSettings.setValue("allsetsurl", url); - } -} - -void CardDatabaseSetupPage::initializePage() -{ - urlLineEdit->setText(readCustomUrl()); - - if (state != State::NotStarted) { - return; - } - - if (alreadyHaveDatabase()) { - setState(State::Succeeded); - return; - } - - // Don't auto-download — wait for the user to press "Download". - setState(State::NotStarted); - statusLabel->setText(tr("Press Download to fetch the card database, or Skip to do it later.")); -} - -void CardDatabaseSetupPage::onUpdateFinished(bool success) -{ - setState(success ? State::Succeeded : State::Failed); - if (success) { - emit advanceRequested(); - } -} - -void CardDatabaseSetupPage::onUpdateProgress(const QString &stage, qint64 done, qint64 total) -{ - if (state != State::Running) { - return; - } - progressBar->setRange(0, total > 0 ? static_cast(qMin(total, INT_MAX)) : 0); - progressBar->setValue(static_cast(qMin(done, INT_MAX))); - if (total > 0) { - const int percent = static_cast((100.0 * done) / total); - if (stage == QLatin1String("download")) { - statusLabel->setText(tr("Downloading the card database (%1%)…").arg(percent)); - } else if (stage == QLatin1String("scan")) { - statusLabel->setText(tr("Parsing the card database (%1%)…").arg(percent)); - } else if (stage == QLatin1String("import")) { - statusLabel->setText(tr("Importing cards (%1%)…").arg(percent)); - } - } -} - -QString CardDatabaseSetupPage::nextButtonText() const -{ - return state == State::NotStarted ? tr("Download") : QString(); -} - -bool CardDatabaseSetupPage::handleNextClick() -{ - if (state == State::NotStarted) { - setState(State::Running); - emit updateRequested(); - return false; - } - return true; -} - -void CardDatabaseSetupPage::onToggleAdvanced(bool open) -{ - advancedToggleButton->setText(open ? tr("▼ Advanced: custom download source") - : tr("▶ Advanced: custom download source")); - advancedPanel->setVisible(open); - - QWidget *wizardWindow = window(); - if (!wizardWindow) { - return; - } - - if (open) { - windowSizeBeforeExpansion = wizardWindow->size(); - QTimer::singleShot(0, this, [wizardWindow] { - wizardWindow->resize(wizardWindow->size().expandedTo(wizardWindow->sizeHint())); - }); - } else { - QTimer::singleShot(0, this, [this, wizardWindow] { - wizardWindow->resize(wizardWindow->size().boundedTo(windowSizeBeforeExpansion)); - }); - } -} - -void CardDatabaseSetupPage::onApplyCustomUrl() -{ - const QString text = urlLineEdit->text().trimmed(); - - if (!text.isEmpty()) { - const QUrl url = QUrl::fromUserInput(text); - if (!url.isValid()) { - QMessageBox::warning(this, tr("Invalid URL"), - tr("That doesn't look like a valid URL. Double-check it and try again, " - "or clear the field to use the default source.")); - return; - } - } - - writeCustomUrl(text); - setState(State::Running); - emit updateRequested(); -} - -void CardDatabaseSetupPage::onRestoreDefaultUrl() -{ - urlLineEdit->clear(); - writeCustomUrl(QString()); -} - -void CardDatabaseSetupPage::setState(State newState) -{ - state = newState; - - progressBar->setVisible(state == State::Running); - retryButton->setVisible(state == State::Failed); - manualButton->setVisible(state == State::Failed); - applyAndRetryButton->setEnabled(state != State::Running); - - switch (state) { - case State::NotStarted: - statusLabel->setText(tr("Press Download to fetch the card database, or Skip to do it later.")); - break; - case State::Running: - statusLabel->setText(tr("Downloading the latest card database…")); - break; - case State::Succeeded: - statusLabel->setText(tr("Card database ready ✓")); - break; - case State::Failed: - statusLabel->setText( - tr("Couldn't download the card database automatically. Check your connection and retry, " - "set it up manually, or skip this for now — you can do it later from the Card Database menu.")); - break; - } - - emit completeChanged(); -} - -bool CardDatabaseSetupPage::isComplete() const -{ - return state != State::Running; -} - -bool CardDatabaseSetupPage::isSkippable() const -{ - return state != State::Succeeded; -} - -QString CardDatabaseSetupPage::stepTitle() const -{ - return tr("Card Database"); -} - -QString CardDatabaseSetupPage::stepSubtitle() const -{ - return tr("Cockatrice needs card data to know what you're playing with."); -} - -void CardDatabaseSetupPage::retranslateUi() -{ - retryButton->setText(tr("Retry")); - manualButton->setText(tr("Set up manually…")); - - onToggleAdvanced(advancedToggleButton->isChecked()); - urlLineEdit->setPlaceholderText(tr("Leave blank to use the default source")); - urlHintLabel->setText(tr("Only change this if you know you need a mirror or a custom card data source.")); - restoreDefaultUrlButton->setText(tr("Restore default")); - applyAndRetryButton->setText(tr("Apply && retry")); - - startupBehaviorLabel->setText(tr("Check for card database updates on startup")); - startupBehaviorCombo->setItemText(0, tr("Don't check")); - startupBehaviorCombo->setItemText(1, tr("Prompt for update")); - startupBehaviorCombo->setItemText(2, tr("Always update in the background")); - - checkIntervalLabel->setText(tr("Check for card database updates every")); - checkIntervalSpinBox->setSuffix(tr(" days")); - - setState(state); -} diff --git a/cockatrice/src/interface/widgets/onboarding/pages/card_database_setup_page.h b/cockatrice/src/interface/widgets/onboarding/pages/card_database_setup_page.h deleted file mode 100644 index 870e759ea..000000000 --- a/cockatrice/src/interface/widgets/onboarding/pages/card_database_setup_page.h +++ /dev/null @@ -1,80 +0,0 @@ -#ifndef CARD_DATABASE_SETUP_PAGE_H -#define CARD_DATABASE_SETUP_PAGE_H - -#include "../first_run_wizard_page.h" - -#include - -class QComboBox; -class QLabel; -class QLineEdit; -class QProgressBar; -class QPushButton; -class QSpinBox; -class QWidget; - -class CardDatabaseSetupPage : public FirstRunWizardPage -{ - Q_OBJECT - -public: - explicit CardDatabaseSetupPage(QWidget *parent = nullptr); - - void initializePage() override; - bool isComplete() const override; - bool isSkippable() const override; - QString stepTitle() const override; - QString stepSubtitle() const override; - QString nextButtonText() const override; - bool handleNextClick() override; - void retranslateUi() override; - - void onUpdateFinished(bool success); - void onUpdateProgress(const QString &stage, qint64 done, qint64 total); - -signals: - void updateRequested(); - void manualSetupRequested(); - -private: - enum class State - { - NotStarted, - Running, - Succeeded, - Failed, - }; - - void setState(State newState); - bool alreadyHaveDatabase() const; - - QString oracleSettingsFilePath() const; - QString readCustomUrl() const; - void writeCustomUrl(const QString &url); - - void onToggleAdvanced(bool open); - void onApplyCustomUrl(); - void onRestoreDefaultUrl(); - - QLabel *statusLabel; - QProgressBar *progressBar; - QPushButton *retryButton; - QPushButton *manualButton; - - QPushButton *advancedToggleButton; - QWidget *advancedPanel; - QLineEdit *urlLineEdit; - QLabel *urlHintLabel; - QPushButton *restoreDefaultUrlButton; - QPushButton *applyAndRetryButton; - - QLabel *startupBehaviorLabel; - QComboBox *startupBehaviorCombo; - QLabel *checkIntervalLabel; - QSpinBox *checkIntervalSpinBox; - - State state = State::NotStarted; - QSize windowSizeBeforeExpansion; -}; - -#endif // CARD_DATABASE_SETUP_PAGE_H diff --git a/cockatrice/src/interface/widgets/onboarding/pages/finish_page.cpp b/cockatrice/src/interface/widgets/onboarding/pages/finish_page.cpp deleted file mode 100644 index 4205fa532..000000000 --- a/cockatrice/src/interface/widgets/onboarding/pages/finish_page.cpp +++ /dev/null @@ -1,30 +0,0 @@ -#include "finish_page.h" - -#include -#include - -FinishPage::FinishPage(QWidget *parent) : FirstRunWizardPage(parent) -{ - bodyLabel = new QLabel(this); - bodyLabel->setWordWrap(true); - bodyLabel->setAlignment(Qt::AlignCenter); - - auto *layout = new QVBoxLayout(this); - layout->addStretch(); - layout->addWidget(bodyLabel); - layout->addStretch(); - - retranslateUi(); -} - -QString FinishPage::stepTitle() const -{ - return tr("You're All Set"); -} - -void FinishPage::retranslateUi() -{ - bodyLabel->setText( - tr("That's everything for now. Jump into Settings any time to change your mind about any of this.\n\n" - "Have fun!")); -} \ No newline at end of file diff --git a/cockatrice/src/interface/widgets/onboarding/pages/finish_page.h b/cockatrice/src/interface/widgets/onboarding/pages/finish_page.h deleted file mode 100644 index 40ebc6ed0..000000000 --- a/cockatrice/src/interface/widgets/onboarding/pages/finish_page.h +++ /dev/null @@ -1,22 +0,0 @@ -#ifndef FINISH_PAGE_H -#define FINISH_PAGE_H - -#include "../first_run_wizard_page.h" - -class QLabel; - -class FinishPage : public FirstRunWizardPage -{ - Q_OBJECT - -public: - explicit FinishPage(QWidget *parent = nullptr); - - QString stepTitle() const override; - void retranslateUi() override; - -private: - QLabel *bodyLabel; -}; - -#endif // FINISH_PAGE_H diff --git a/cockatrice/src/interface/widgets/onboarding/pages/preferences_setup_page.cpp b/cockatrice/src/interface/widgets/onboarding/pages/preferences_setup_page.cpp deleted file mode 100644 index fceb34deb..000000000 --- a/cockatrice/src/interface/widgets/onboarding/pages/preferences_setup_page.cpp +++ /dev/null @@ -1,173 +0,0 @@ -#include "preferences_setup_page.h" - -#include "../../client/settings/cache_settings.h" -#include "../../client/sound_engine.h" -#include "libcockatrice/settings/interface_settings.h" -#include "libcockatrice/settings/sound_settings.h" -#include "libcockatrice/settings/tabs_settings.h" - -#include -#include -#include -#include -#include -#include -#include - -namespace -{ -// The server destinations are omitted: during first run their tabs are not -// open yet, and the wizard offers no way to fill in the server/room details. -QList wizardStartupTabOrder() -{ - return {StartupTabHome, StartupTabVisualDeckStorage, StartupTabDeckStorage, - StartupTabReplays, StartupTabDeckEditor, StartupTabVisualDeckEditor}; -} -} // namespace - -PreferencesSetupPage::PreferencesSetupPage(QWidget *parent) : FirstRunWizardPage(parent) -{ - auto *content = new QWidget; - auto *contentLayout = new QVBoxLayout(content); - - gameplayGroup = new QGroupBox(content); - auto *gameplayLayout = new QVBoxLayout(gameplayGroup); - contentLayout->addWidget(gameplayGroup); - doubleClickToPlayCheckBox = new QCheckBox(gameplayGroup); - horizontalHandCheckBox = new QCheckBox(gameplayGroup); - playToStackCheckBox = new QCheckBox(gameplayGroup); - gameplayLayout->addWidget(doubleClickToPlayCheckBox); - gameplayLayout->addWidget(horizontalHandCheckBox); - gameplayLayout->addWidget(playToStackCheckBox); - - notificationsGroup = new QGroupBox(content); - auto *notificationsLayout = new QVBoxLayout(notificationsGroup); - contentLayout->addWidget(notificationsGroup); - notificationsEnabledCheckBox = new QCheckBox(notificationsGroup); - soundEnabledCheckBox = new QCheckBox(notificationsGroup); - notificationsLayout->addWidget(notificationsEnabledCheckBox); - notificationsLayout->addWidget(soundEnabledCheckBox); - - startupGroup = new QGroupBox(content); - auto *startupForm = new QFormLayout(startupGroup); - contentLayout->addWidget(startupGroup); - startupTabLabel = new QLabel(startupGroup); - startupTabSelector = new QComboBox(startupGroup); - startupTabSelector->setSizeAdjustPolicy(QComboBox::AdjustToContents); - for (StartupTab tab : wizardStartupTabOrder()) { - startupTabSelector->addItem(QString(), tab); // texts set in retranslateUi - } - startupForm->addRow(startupTabLabel, startupTabSelector); - - contentLayout->addStretch(); - - auto *scrollArea = new QScrollArea(this); - scrollArea->setWidget(content); - scrollArea->setWidgetResizable(true); - scrollArea->setFrameShape(QFrame::NoFrame); - - auto *layout = new QVBoxLayout(this); - layout->addWidget(scrollArea); - - SettingsCache &settings = SettingsCache::instance(); - - connect(doubleClickToPlayCheckBox, &QCheckBox::toggled, &settings.userInterface(), - &InterfaceSettings::setDoubleClickToPlay); - connect(horizontalHandCheckBox, &QCheckBox::toggled, &settings.userInterface(), - &InterfaceSettings::setHorizontalHand); - connect(playToStackCheckBox, &QCheckBox::toggled, &settings.userInterface(), &InterfaceSettings::setPlayToStack); - - connect(notificationsEnabledCheckBox, &QCheckBox::toggled, &settings.userInterface(), - &InterfaceSettings::setNotificationsEnabled); - connect(soundEnabledCheckBox, &QCheckBox::toggled, &settings.sound(), &SoundSettings::setSoundEnabled); - connect(soundEnabledCheckBox, &QCheckBox::toggled, soundEngine, &SoundEngine::testSound); - - connect(startupTabSelector, QOverload::of(&QComboBox::currentIndexChanged), this, [this](int index) { - if (index < 0) { - return; - } - SettingsCache::instance().tabs().setStartupTabIndex(startupTabSelector->itemData(index).toInt()); - }); - - retranslateUi(); -} - -void PreferencesSetupPage::initializePage() -{ - SettingsCache &settings = SettingsCache::instance(); - - doubleClickToPlayCheckBox->setChecked(settings.userInterface().getDoubleClickToPlay()); - horizontalHandCheckBox->setChecked(settings.userInterface().getHorizontalHand()); - playToStackCheckBox->setChecked(settings.userInterface().getPlayToStack()); - - notificationsEnabledCheckBox->setChecked(settings.userInterface().getNotificationsEnabled()); - soundEnabledCheckBox->setChecked(settings.sound().getSoundEnabled()); - - startupTabSelector->setCurrentIndex(startupTabSelector->findData(settings.tabs().getStartupTabIndex())); -} - -bool PreferencesSetupPage::isSkippable() const -{ - return true; -} - -QString PreferencesSetupPage::stepTitle() const -{ - return tr("A Few Preferences"); -} - -QString PreferencesSetupPage::stepSubtitle() const -{ - return tr("Defaults are fine — tweak these now or from Settings anytime."); -} - -void PreferencesSetupPage::retranslateUi() -{ - gameplayGroup->setTitle(tr("Gameplay")); - doubleClickToPlayCheckBox->setText(tr("Double-click cards to play them")); - doubleClickToPlayCheckBox->setToolTip(tr("When disabled, a single click plays the selected card onto the table.")); - horizontalHandCheckBox->setText(tr("Display hand horizontally")); - horizontalHandCheckBox->setToolTip( - tr("Shows your hand as a row along the bottom of the table instead of a column beside it.")); - playToStackCheckBox->setText(tr("Play all nonlands onto the stack by default")); - playToStackCheckBox->setToolTip( - tr("Cards you play appear on the stack so other players can respond to them, as in a tabletop game.")); - - notificationsGroup->setTitle(tr("Notifications && Sound")); - notificationsEnabledCheckBox->setText(tr("Show desktop notifications")); - soundEnabledCheckBox->setText(tr("Play sound effects")); - - startupGroup->setTitle(tr("Startup")); - startupTabLabel->setText(tr("Startup tab:")); - const QList tabs = wizardStartupTabOrder(); - for (int i = 0; i < tabs.size(); ++i) { - QString name; - switch (tabs[i]) { - case StartupTabHome: - name = tr("Home"); - break; - case StartupTabVisualDeckStorage: - name = tr("Visual Deck Storage"); - break; - case StartupTabDeckStorage: - name = tr("Deck Storage"); - break; - case StartupTabReplays: - name = tr("Game Replays"); - break; - case StartupTabDeckEditor: - name = tr("Deck Editor"); - break; - case StartupTabVisualDeckEditor: - name = tr("Visual Deck Editor"); - break; - case StartupTabServer: - name = tr("Server"); - break; - case StartupTabServerRoom: - name = tr("Server Room"); - break; - } - startupTabSelector->setItemText(i, name); - } -} diff --git a/cockatrice/src/interface/widgets/onboarding/pages/preferences_setup_page.h b/cockatrice/src/interface/widgets/onboarding/pages/preferences_setup_page.h deleted file mode 100644 index eed1b3aed..000000000 --- a/cockatrice/src/interface/widgets/onboarding/pages/preferences_setup_page.h +++ /dev/null @@ -1,41 +0,0 @@ -#ifndef PREFERENCES_SETUP_PAGE_H -#define PREFERENCES_SETUP_PAGE_H - -#include "../first_run_wizard_page.h" - -class QCheckBox; -class QComboBox; -class QGroupBox; -class QLabel; - -/** @brief A curated subset of settings for the user to adjust. - **/ -class PreferencesSetupPage : public FirstRunWizardPage -{ - Q_OBJECT - -public: - explicit PreferencesSetupPage(QWidget *parent = nullptr); - - void initializePage() override; - bool isSkippable() const override; - QString stepTitle() const override; - QString stepSubtitle() const override; - void retranslateUi() override; - -private: - QGroupBox *gameplayGroup; - QCheckBox *doubleClickToPlayCheckBox; - QCheckBox *horizontalHandCheckBox; - QCheckBox *playToStackCheckBox; - - QGroupBox *notificationsGroup; - QCheckBox *notificationsEnabledCheckBox; - QCheckBox *soundEnabledCheckBox; - - QGroupBox *startupGroup; - QLabel *startupTabLabel; - QComboBox *startupTabSelector; -}; - -#endif // PREFERENCES_SETUP_PAGE_H diff --git a/cockatrice/src/interface/widgets/onboarding/pages/theme_setup_page.cpp b/cockatrice/src/interface/widgets/onboarding/pages/theme_setup_page.cpp deleted file mode 100644 index 797fe4425..000000000 --- a/cockatrice/src/interface/widgets/onboarding/pages/theme_setup_page.cpp +++ /dev/null @@ -1,276 +0,0 @@ -#include "theme_setup_page.h" - -#include "../../client/settings/cache_settings.h" -#include "../../interface/palette_editor/palette_generator.h" -#include "../../interface/palette_editor/quick_setup_panel.h" -#include "../../interface/theme_manager.h" -#include "../../interface/widgets/general/background_sources.h" -#include "libcockatrice/settings/appearance_settings.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace -{ -/** @brief A theme's shipped identity accent, immune to any user- or auto- - * generated palette that may currently be masking appColor(). */ -QColor themeIdentityAccent(const QString &themeDirPath, const QString &themeName) -{ - for (const QString &scheme : {QStringLiteral("Light"), QStringLiteral("Dark")}) { - const PaletteConfig cfg = ThemeManager::loadDefaultPaletteConfig(themeDirPath, themeName, scheme); - if (cfg.appColors.contains(AppColor::AccentStrong)) { - return cfg.appColors.value(AppColor::AccentStrong); - } - const QColor highlight = cfg.colors.value(QPalette::Active).value(QPalette::Highlight); - if (highlight.isValid()) { - return highlight; - } - } - return {}; -} -} // namespace - -ThemeSetupPage::ThemeSetupPage(QWidget *parent) : FirstRunWizardPage(parent) -{ - themeCombo = new QComboBox(this); - schemeCombo = new QComboBox(this); - schemeCombo->addItem(tr("Light"), QStringLiteral("Light")); - schemeCombo->addItem(tr("Dark"), QStringLiteral("Dark")); -#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0) - schemeCombo->addItem(tr("Match system"), QStringLiteral("System")); -#endif - - quickSetupPanel = new QuickSetupPanel(this); - - // Seed the picker from the current theme's own identity accent rather than - // a hardcoded brand green: Plasma seeds violet, Fusion green, etc., and it - // is immune to stale generated palettes that may mask appColor(). This was - // initially a brand-green workaround from before Fusion became the default. - // setAccentColor blocks signals, so this never triggers a generation. - lastSeededTheme = SettingsCache::instance().getThemeName(); - quickSetupPanel->setAccentColor( - themeIdentityAccent(themeManager->getAvailableThemes().value(lastSeededTheme), lastSeededTheme)); - - connect(themeCombo, QOverload::of(&QComboBox::currentIndexChanged), this, &ThemeSetupPage::onThemeChanged); - connect(schemeCombo, QOverload::of(&QComboBox::currentIndexChanged), this, &ThemeSetupPage::onSchemeChanged); - connect(quickSetupPanel, &QuickSetupPanel::valueChanged, this, &ThemeSetupPage::onGenerateFromAccent); - - homeTabBackgroundCombo = new QComboBox(this); - for (const auto &entry : BackgroundSources::all()) { - homeTabBackgroundCombo->addItem(QObject::tr(entry.trKey), QVariant::fromValue(entry.type)); - } - connect(homeTabBackgroundCombo, QOverload::of(&QComboBox::currentIndexChanged), this, - &ThemeSetupPage::onHomeTabBackgroundChanged); - - // Keep the scheme combo honest when the *theme* changes underneath it - // (switching theme reloads that theme's own stored colorScheme), and - // opportunistically seed a palette for themes that ship none at all. - // Mirrors AppearanceSettingsPage's identical listener for the combo-sync - // half of this. - connect(themeManager, &ThemeManager::themeChanged, this, [this] { - const QString newTheme = SettingsCache::instance().getThemeName(); - const QString newDir = themeManager->getAvailableThemes().value(newTheme); - const ThemeConfig cfg = ThemeConfig::fromThemeDir(newDir); - const QString current = cfg.colorScheme; - - schemeCombo->blockSignals(true); - const int idx = schemeCombo->findData(current); - schemeCombo->setCurrentIndex(idx >= 0 ? idx : 0); - schemeCombo->blockSignals(false); - - // Keep the picker's accent in step with the theme's own identity; the - // swatch seeded at construction would otherwise stay stale (e.g. green - // from a previous theme) when the user toggles themes. - if (newTheme != lastSeededTheme) { - lastSeededTheme = newTheme; - quickSetupPanel->setAccentColor(themeIdentityAccent(newDir, newTheme)); - } - - maybeAutoGeneratePalette(); - }); - - auto *form = new QFormLayout; - form->addRow(tr("Theme:"), themeCombo); - form->addRow(tr("Appearance:"), schemeCombo); - form->addRow(tr("Home screen background:"), homeTabBackgroundCombo); - - accentGroup = new QGroupBox(this); - auto *accentLayout = new QVBoxLayout(accentGroup); - accentLayout->addWidget(quickSetupPanel); - - auto *layout = new QVBoxLayout(this); - layout->addLayout(form); - layout->addWidget(accentGroup); - layout->addStretch(); - - retranslateUi(); -} - -void ThemeSetupPage::initializePage() -{ - themeCombo->blockSignals(true); - themeCombo->clear(); - const QString currentTheme = SettingsCache::instance().getThemeName(); - for (const QString &name : themeManager->getAvailableThemes().keys()) { - themeCombo->addItem(name); - } - const int idx = themeCombo->findText(currentTheme); - themeCombo->setCurrentIndex(idx >= 0 ? idx : 0); - themeCombo->blockSignals(false); - - const QString dirPath = themeManager->getAvailableThemes().value(SettingsCache::instance().getThemeName()); - const ThemeConfig cfg = ThemeConfig::fromThemeDir(dirPath); - schemeCombo->blockSignals(true); - const int schemeIdx = schemeCombo->findData(cfg.colorScheme); - schemeCombo->setCurrentIndex(schemeIdx >= 0 ? schemeIdx : 0); - schemeCombo->blockSignals(false); - - homeTabBackgroundCombo->blockSignals(true); - QString homeTabSource = SettingsCache::instance().appearance().getHomeTabBackgroundSource(); - int homeTabIdx = homeTabBackgroundCombo->findData(BackgroundSources::fromId(homeTabSource)); - homeTabBackgroundCombo->setCurrentIndex(homeTabIdx >= 0 ? homeTabIdx : 0); - homeTabBackgroundCombo->blockSignals(false); - - // Opening the page must not touch the running application's palette: - // previews and auto-generation only happen in response to the user - // actually changing a control, never on mere page visibility. - paletteDirty = false; -} - -QString ThemeSetupPage::currentScheme() const -{ - return schemeCombo->currentData().toString(); -} - -QString ThemeSetupPage::resolvedScheme() const -{ - const QString scheme = currentScheme(); - if (scheme.isEmpty() || scheme == QStringLiteral("System")) { - return themeManager->isDarkMode(themeManager->getCurrentThemePath()) ? "Dark" : "Light"; - } - return scheme; -} - -void ThemeSetupPage::onThemeChanged(int index) -{ - if (index < 0) { - return; - } - paletteDirty = false; - SettingsCache::instance().setThemeName(themeCombo->itemText(index)); - // Scheme-combo sync and auto-generation both happen via the - // ThemeManager::themeChanged listener above, triggered by setThemeName. -} - -void ThemeSetupPage::onSchemeChanged() -{ - themeManager->setColorScheme(currentScheme()); -} - -void ThemeSetupPage::onHomeTabBackgroundChanged(int index) -{ - if (index < 0) { - return; - } - auto type = homeTabBackgroundCombo->currentData().value(); - SettingsCache::instance().appearance().setHomeTabBackgroundSource(BackgroundSources::toId(type)); -} - -void ThemeSetupPage::onGenerateFromAccent(const QColor &accent, int intensity) -{ - PaletteConfig cfg = PaletteGenerator::fromAccent(accent, intensity, resolvedScheme()); - themeManager->previewPalette(cfg, resolvedScheme()); - paletteDirty = true; -} - -void ThemeSetupPage::maybeAutoGeneratePalette() -{ - const QString dirPath = themeManager->getAvailableThemes().value(SettingsCache::instance().getThemeName()); - const QString scheme = resolvedScheme(); - - // The theme dir may resolve to the user profile even for built-in themes - // (getAvailableThemes gives the user copy precedence), so consult the - // shipped palette too -- both via loadDefaultPaletteConfig's system fallback. - // Without it, scheme flips regenerate a fresh palette from the picker accent - // and clobber the curated colours the theme explicitly ships. - if (PaletteConfig::fromScheme(dirPath, scheme).hasPalette() || - ThemeManager::loadDefaultPaletteConfig(dirPath, SettingsCache::instance().getThemeName(), scheme) - .hasPalette()) { - return; // theme already has something real to show -- leave it alone - } - - // The theme+scheme combination has nothing saved and nothing shipped, and - // the user just switched to it. Rather than leaving a flat, unstyled look, - // seed one from whatever accent QuickSetupPanel currently holds and mark - // it dirty so it's written to disk if the user moves on. Only ever reached - // through user interaction (theme/scheme change, accent drag) -- never on - // page open. - PaletteConfig generated = - PaletteGenerator::fromAccent(quickSetupPanel->accentColor(), quickSetupPanel->intensity(), scheme); - themeManager->previewPalette(generated, scheme); - paletteDirty = true; -} - -bool ThemeSetupPage::validatePage() -{ - if (paletteDirty) { - const QString scheme = resolvedScheme(); - PaletteConfig cfg = - PaletteGenerator::fromAccent(quickSetupPanel->accentColor(), quickSetupPanel->intensity(), scheme); - if (!ThemeManager::commitPalette(writableThemeDir(), scheme, cfg)) { - QMessageBox::warning(this, tr("Save failed"), - tr("Could not write the theme palette to:\n%1").arg(writableThemeDir())); - return false; - } - themeManager->reloadCurrentTheme(); - } - return true; -} - -QString ThemeSetupPage::writableThemeDir() const -{ - // Built-in themes resolve to the read-only system themes directory; - // palette edits must go to the user themes directory instead, exactly - // as PaletteEditorDialog does. - const QString dirPath = themeManager->getAvailableThemes().value(SettingsCache::instance().getThemeName()); - if (!dirPath.isEmpty()) { - const QString probe = QDir(dirPath).absoluteFilePath(".cockatrice_write_test"); - QFile f(probe); - if (f.open(QIODevice::WriteOnly)) { - f.close(); - f.remove(); - return dirPath; - } - } - return QDir(SettingsCache::instance().paths().getThemesPath()) - .absoluteFilePath(SettingsCache::instance().getThemeName()); -} - -bool ThemeSetupPage::isSkippable() const -{ - return true; -} - -QString ThemeSetupPage::stepTitle() const -{ - return tr("Pick a Look"); -} - -QString ThemeSetupPage::stepSubtitle() const -{ - return tr("You can fine-tune every colour later from Settings → Appearance."); -} - -void ThemeSetupPage::retranslateUi() -{ - accentGroup->setTitle(tr("Accent colour (optional)")); -} \ No newline at end of file diff --git a/cockatrice/src/interface/widgets/onboarding/pages/theme_setup_page.h b/cockatrice/src/interface/widgets/onboarding/pages/theme_setup_page.h deleted file mode 100644 index 16a3c9a5d..000000000 --- a/cockatrice/src/interface/widgets/onboarding/pages/theme_setup_page.h +++ /dev/null @@ -1,61 +0,0 @@ -#ifndef THEME_SETUP_PAGE_H -#define THEME_SETUP_PAGE_H - -#include "../first_run_wizard_page.h" - -class QComboBox; -class QGroupBox; -class QuickSetupPanel; - -/** @brief First-run theme step. Reuses the same building blocks as Appearance - * settings and the Palette Editor (ThemeManager, PaletteConfig, - * PaletteGenerator, and the QuickSetupPanel widget itself) rather than - * reimplementing palette generation or preview here. - * - * Behavior specific to this page (deliberately not pushed down into - * ThemeManager, to avoid changing app-wide behaviour for existing installs): - * - Opening the page never changes the running palette; previews and - * auto-generation only happen when the user actually changes a control. - * - If a theme+scheme the user selects has no saved palette and no shipped - * default, one is generated from the QuickSetupPanel's current accent so - * the preview doesn't fall back to a flat, unstyled look. */ -class ThemeSetupPage : public FirstRunWizardPage -{ - Q_OBJECT - -public: - explicit ThemeSetupPage(QWidget *parent = nullptr); - - void initializePage() override; - bool validatePage() override; - bool isSkippable() const override; - QString stepTitle() const override; - QString stepSubtitle() const override; - void retranslateUi() override; - -private slots: - void onThemeChanged(int index); - void onSchemeChanged(); - void onGenerateFromAccent(const QColor &accent, int intensity); - void onHomeTabBackgroundChanged(int index); - -private: - QString currentScheme() const; - QString resolvedScheme() const; // "System" -> actual Light/Dark - void maybeAutoGeneratePalette(); - QString writableThemeDir() const; - - QComboBox *themeCombo; - QComboBox *schemeCombo; - QGroupBox *accentGroup; - QuickSetupPanel *quickSetupPanel; - - QComboBox *homeTabBackgroundCombo; - - bool paletteDirty = false; - - /// Theme whose identity accent currently seeds the picker. - QString lastSeededTheme; -}; - -#endif // THEME_SETUP_PAGE_H diff --git a/cockatrice/src/interface/widgets/onboarding/pages/welcome_page.cpp b/cockatrice/src/interface/widgets/onboarding/pages/welcome_page.cpp deleted file mode 100644 index 16e0719d2..000000000 --- a/cockatrice/src/interface/widgets/onboarding/pages/welcome_page.cpp +++ /dev/null @@ -1,79 +0,0 @@ -#include "welcome_page.h" - -#include "../../../../main.h" -#include "../../client/settings/cache_settings.h" -#include "../../settings_page/general_settings_page.h" -#include "libcockatrice/settings/personal_settings.h" - -#include -#include -#include -#include -#include -#include -#include - -WelcomePage::WelcomePage(QWidget *parent) : FirstRunWizardPage(parent) -{ - bodyLabel = new QLabel(this); - bodyLabel->setWordWrap(true); - bodyLabel->setAlignment(Qt::AlignCenter); - - languageLabel = new QLabel(this); - langCombo = new QComboBox(this); - for (const QString &code : GeneralSettingsPage::findQmFiles()) { - langCombo->addItem(GeneralSettingsPage::languageName(code), code); - } - - QString current = SettingsCache::instance().personal().getLang(); - if (current.isEmpty()) { - current = QLocale::system().name(); - } - int index = langCombo->findData(current); - if (index < 0) { - index = langCombo->findData(current.section('_', 0, 0)); - } - if (index >= 0) { - langCombo->setCurrentIndex(index); - } - - connect(langCombo, QOverload::of(&QComboBox::currentIndexChanged), this, &WelcomePage::languageChanged); - - auto *languageRow = new QHBoxLayout; - languageRow->addStretch(); - languageRow->addWidget(languageLabel); - languageRow->addWidget(langCombo); - languageRow->addStretch(); - - auto *layout = new QVBoxLayout(this); - layout->addStretch(); - layout->addWidget(bodyLabel); - layout->addStretch(); - layout->addLayout(languageRow); - - retranslateUi(); -} - -void WelcomePage::languageChanged(int index) -{ - if (index < 0) { - return; - } - SettingsCache::instance().personal().setLang(langCombo->itemData(index).toString()); - qApp->removeTranslator(translator); // NOLINT(cppcoreguidelines-pro-type-static-cast-downcast) - installNewTranslator(); -} - -QString WelcomePage::stepTitle() const -{ - return tr("Welcome!"); -} - -void WelcomePage::retranslateUi() -{ - bodyLabel->setText(tr("Let's get you set up. This will only take a minute — " - "we'll grab the card database, pick a look you like, " - "and get you ready to connect to a server.\n\n" - "You can change any of this later from Settings.")); - languageLabel->setText(tr("Language:")); -} diff --git a/cockatrice/src/interface/widgets/onboarding/pages/welcome_page.h b/cockatrice/src/interface/widgets/onboarding/pages/welcome_page.h deleted file mode 100644 index 93b24d83d..000000000 --- a/cockatrice/src/interface/widgets/onboarding/pages/welcome_page.h +++ /dev/null @@ -1,28 +0,0 @@ -#ifndef WELCOME_PAGE_H -#define WELCOME_PAGE_H - -#include "../first_run_wizard_page.h" - -class QComboBox; -class QLabel; - -class WelcomePage : public FirstRunWizardPage -{ - Q_OBJECT - -public: - explicit WelcomePage(QWidget *parent = nullptr); - - QString stepTitle() const override; - void retranslateUi() override; - -private slots: - void languageChanged(int index); - -private: - QLabel *bodyLabel; - QLabel *languageLabel; - QComboBox *langCombo; -}; - -#endif // WELCOME_PAGE_H diff --git a/cockatrice/src/interface/widgets/onboarding/qml/BrandBanner.qml b/cockatrice/src/interface/widgets/onboarding/qml/BrandBanner.qml deleted file mode 100644 index d94e15280..000000000 --- a/cockatrice/src/interface/widgets/onboarding/qml/BrandBanner.qml +++ /dev/null @@ -1,96 +0,0 @@ -import QtQuick - -Item { - id: root - - ShaderEffect { - id: effectA - anchors.fill: parent - opacity: bannerConfig.frontIsA ? 1.0 : 0.0 - Behavior on opacity { NumberAnimation { duration: 450; easing.type: Easing.InOutCubic } } - property real iTime: bannerConfig.time - property real uAspect: bannerConfig.aspect - property real uMode: bannerConfig.modeA - property real uSpeed: bannerConfig.speedA - property real uSeed: bannerConfig.seedA - property vector4d uColorA: Qt.vector4d(bannerConfig.colorA.r, bannerConfig.colorA.g, bannerConfig.colorA.b, 1.0) - property vector4d uColorB: Qt.vector4d(bannerConfig.colorB.r, bannerConfig.colorB.g, bannerConfig.colorB.b, 1.0) - property vector4d uAccent: Qt.vector4d(bannerConfig.accent.r, bannerConfig.accent.g, bannerConfig.accent.b, 1.0) - property vector4d uGlowColor: Qt.vector4d(bannerConfig.glowColor.r, bannerConfig.glowColor.g, bannerConfig.glowColor.b, 1.0) - property real uVignetteMin: bannerConfig.vignetteMin - property real uLogoGlow: bannerConfig.logoGlow - fragmentShader: "qrc:/onboarding/shaders/brand_banner.frag.qsb" - } - - ShaderEffect { - id: effectB - anchors.fill: parent - opacity: bannerConfig.frontIsA ? 0.0 : 1.0 - Behavior on opacity { NumberAnimation { duration: 450; easing.type: Easing.InOutCubic } } - property real iTime: bannerConfig.time - property real uAspect: bannerConfig.aspect - property real uMode: bannerConfig.modeB - property real uSpeed: bannerConfig.speedB - property real uSeed: bannerConfig.seedB - property vector4d uColorA: Qt.vector4d(bannerConfig.colorA.r, bannerConfig.colorA.g, bannerConfig.colorA.b, 1.0) - property vector4d uColorB: Qt.vector4d(bannerConfig.colorB.r, bannerConfig.colorB.g, bannerConfig.colorB.b, 1.0) - property vector4d uAccent: Qt.vector4d(bannerConfig.accent.r, bannerConfig.accent.g, bannerConfig.accent.b, 1.0) - property vector4d uGlowColor: Qt.vector4d(bannerConfig.glowColor.r, bannerConfig.glowColor.g, bannerConfig.glowColor.b, 1.0) - property real uVignetteMin: bannerConfig.vignetteMin - property real uLogoGlow: bannerConfig.logoGlow - fragmentShader: "qrc:/onboarding/shaders/brand_banner.frag.qsb" - } - - // The white logo sits at full opacity on top of the static gradient plate — - // no glow, no breathing. The plate matches home_widget's QPainter composite. - Item { - id: logoHost - visible: bannerConfig.logoVisible - anchors.centerIn: parent - width: root.height * 0.6 - height: width - - // The full-color logo renders beneath the white mark and is consumed as - // a texture (layer.enabled) by the plate shader's silhouette mask, so - // the gradient is clipped to the bird exactly as the SVG's gradient - // paths are. It is never drawn to the screen itself. - Image { - id: silhouetteMask - anchors.fill: parent - source: "qrc:/resources/cockatrice.svg" - sourceSize: Qt.size(256, 256) - fillMode: Image.PreserveAspectFit - smooth: true - visible: false - layer.enabled: true - layer.smooth: true - } - - // The logo's gradient plate, drawn behind the mark: a linear - // AccentSoft (light) -> AccentStrong (dark) sheet along the same - // top-left -> bottom-right userSpaceOnUse axis the baked-in SVG used, - // clipped to the bird silhouette via uSilhouette. The white highlight - // path above is theme independent. Sized to the logo itself — no - // rounded badge, matching home_widget's QPainter composite. Static. - // Small ShaderEffect, Qt 6.4-safe. - ShaderEffect { - id: brandPlate - anchors.fill: parent - property vector4d uStrong: Qt.vector4d(bannerConfig.brandStrong.r, bannerConfig.brandStrong.g, - bannerConfig.brandStrong.b, 1.0) - property vector4d uSoft: Qt.vector4d(bannerConfig.brandSoft.r, bannerConfig.brandSoft.g, - bannerConfig.brandSoft.b, 1.0) - property var uSilhouette: silhouetteMask - fragmentShader: "qrc:/onboarding/shaders/brand_plate.frag.qsb" - } - - Image { - id: logoImage - anchors.fill: parent - source: "qrc:/resources/cockatrice-logo-white.svg" - sourceSize: Qt.size(256, 256) - fillMode: Image.PreserveAspectFit - smooth: true - } - } -} \ No newline at end of file diff --git a/cockatrice/src/interface/widgets/onboarding/shader_banner_widget.cpp b/cockatrice/src/interface/widgets/onboarding/shader_banner_widget.cpp deleted file mode 100644 index 0b9f65783..000000000 --- a/cockatrice/src/interface/widgets/onboarding/shader_banner_widget.cpp +++ /dev/null @@ -1,326 +0,0 @@ -#include "shader_banner_widget.h" - -#include "../../theme_manager.h" -#include "banner_shader_config.h" -#include "brand_colors.h" - -#include -#include -#include -#include -#include -#include -#include - -namespace -{ -// Curated near-black stage -- used only when the active palette resolves no -// usable window colour. Matches the banner's original design (dark and quiet -// so the accent stands out) and satisfies design-plans §2.1's "identity -// survives a bare palette". -constexpr QRgb kFallbackColorA = 0x1A1A20; -constexpr QRgb kFallbackColorB = 0x0E0E12; - -struct SuggestedColors -{ - QColor colorA; - QColor colorB; - QColor accent; - QColor glowColor; - QColor brandStrong; - QColor brandSoft; - qreal vignetteMin = 0.62; -}; - -SuggestedColors suggestedBannerColors() -{ - const QPalette &pal = qApp->palette(); - const QColor window = pal.color(QPalette::Active, QPalette::Window); - // Identity accent: the theme's [AppColors] AccentStrong, which appColor() - // resolves to QPalette::Highlight when a theme doesn't pin AccentStrong. - // Reading bare Highlight ignored curated accent tokens (Plasma's violet - // vs Default's green) whenever a palette didn't set the role itself. - const QColor accentStrong = themeManager->appColor(AppColor::AccentStrong); - if (!window.isValid() || !accentStrong.isValid()) { - return {QColor(kFallbackColorA), - QColor(kFallbackColorB), - kCockatriceBrandGreen, - QColor(Qt::white), - kCockatriceBrandGreen, - QColor(0xC9, 0xFD, 0x62), - 0.62}; - } - - // The theme's brand pair: AccentStrong is the deep green, AccentSoft the - // lime. These two appColors form the logo's "surrounding gradient" (deep - // core grading out to the soft, brand-toned glow) on both the banner and - // the home screen. - const QColor brandStrong = accentStrong; - const QColor brandSoft = themeManager->appColor(AppColor::AccentSoft); - - // Dress the stage for the scheme so the banner never fights the - // surrounding window in either mode. Dark palettes keep the original - // quiet near-black stage (lightness 29 → 16) with the theme's window - // hue; light palettes get a pastel "frosted accent" treatment built from - // the accent hue instead of a plain near-white copy: a coloured wash that - // clearly belongs to the theme. - const qreal luma = 0.299 * window.red() + 0.587 * window.green() + 0.114 * window.blue(); - const bool lightStage = luma > 115.0; - if (lightStage) { - const int hue = accentStrong.hslHue(); - // Achromatic accents (grey) get a neutral near-white stage instead. - const int stageSat = hue < 0 ? 0 : 64; - const int hueSafe = hue < 0 ? 0 : hue; - // Depth is what stops a light stage reading as a washed-out near-white - // copy of the page behind the banner: deepen the lower pastel band and - // raise saturation so the hue is clearly present while staying frosted. - auto pastel = [hueSafe, stageSat](int lightness) { return QColor::fromHsl(hueSafe, stageSat, lightness); }; - auto pastelLower = [hueSafe](int lightness) { return QColor::fromHsl(hueSafe, 76, lightness); }; - // Brightness-lifted accent for additive glows: the raw accent on a - // light stage must be mid-bright to read instead of washing out, so - // lift lightness and saturation together. - const int accentLightness = qBound(158, accentStrong.lightness() + 82, 198); - const int accentSaturation = hue < 0 ? 0 : qMax(accentStrong.hslSaturation(), 180); - const QColor liftedAccent = - hue < 0 ? accentStrong : QColor::fromHsl(hueSafe, accentSaturation, accentLightness); - // The centre glow (and logo tint in QML) uses the deep accent itself: - // a coloured halo/fill behind the logo instead of a white or black one. - return {pastel(214), pastelLower(186), liftedAccent, accentStrong, brandStrong, brandSoft, 0.80}; - } - - // Dark stage: force the window hue down to the banner's curated darkness, - // scaling saturation away so chromatic palettes tint it without going - // muddy. The accent is the bright, brand-driven tone (hue from the accent - // itself, never the -- often grey -- window), and it drives both the - // embers/fog and the logo glow so the mark tints like the light stage. - auto stage = [&window](int lightness) { - const int hue = window.hslHue(); - const int saturation = hue < 0 ? 0 : qBound(0, qRound(window.hslSaturation() * (lightness / 40.0)), 255); - return QColor::fromHsl(hue, saturation, lightness); - }; - const int accentHue = accentStrong.hslHue(); - const int accentHueSafe = accentHue < 0 ? 0 : accentHue; - const int accentLightness = qBound(150, accentStrong.lightness() + 70, 185); - const int accentSaturation = accentHue < 0 ? 0 : qMax(accentStrong.hslSaturation(), 160); - const QColor accent = - accentHue < 0 ? accentStrong : QColor::fromHsl(accentHueSafe, accentSaturation, accentLightness); - return {stage(29), stage(16), accent, accent, brandStrong, brandSoft, 0.62}; -} -} // namespace - -class GradientFallbackWidget : public QWidget -{ -public: - using QWidget::QWidget; - - void setColors(const QColor &a, const QColor &b) - { - if (a != colorA || b != colorB) { - colorA = a; - colorB = b; - } - } - -protected: - void paintEvent(QPaintEvent *) override - { - QPainter painter(this); - QLinearGradient gradient(0, 0, width(), height()); - gradient.setColorAt(0.0, colorA); - gradient.setColorAt(1.0, colorB); - painter.fillRect(rect(), gradient); - } - -private: - QColor colorA{QColor(kFallbackColorA)}; - QColor colorB{QColor(kFallbackColorB)}; -}; - -BannerHost::BannerHost(QWidget *parent) : QWidget(parent) -{ - setFixedHeight(150); - - stack = new QStackedLayout(this); - stack->setContentsMargins(0, 0, 0, 0); - - fallback = new GradientFallbackWidget(this); - stack->addWidget(fallback); - - quickWidget = new QQuickWidget(this); - quickWidget->setResizeMode(QQuickWidget::SizeRootObjectToView); - - config = new BannerShaderConfig(quickWidget->engine()); - quickWidget->rootContext()->setContextProperty("bannerConfig", config); - quickWidget->setSource(QUrl("qrc:/onboarding/qml/BrandBanner.qml")); - - if (quickWidget->status() == QQuickWidget::Error) { - activateFallback(); - } else { - connect(quickWidget, &QQuickWidget::sceneGraphError, this, &BannerHost::onSceneGraphFailed); - stack->addWidget(quickWidget); - stack->setCurrentWidget(quickWidget); - } - - connect(&clock, &QTimer::timeout, this, &BannerHost::tick); - clock.setInterval(16); // ~60fps; the shader itself is cheap, this is just a wall clock - - connect(themeManager, &ThemeManager::themeChanged, this, &BannerHost::applyThemeColors); - applyThemeColors(); - - applyMotifPreset(currentMotif); - updateAspect(); -} - -void BannerHost::activateFallback() -{ - if (usingFallback) { - return; - } - usingFallback = true; - clock.stop(); - stack->setCurrentWidget(fallback); - - if (quickWidget) { - quickWidget->deleteLater(); // takes BannerShaderConfig (parented to its engine) with it - quickWidget = nullptr; - config = nullptr; - } -} - -void BannerHost::onSceneGraphFailed() -{ - activateFallback(); -} - -void BannerHost::setMotif(Motif motif) -{ - currentMotif = motif; - applyMotifPreset(motif); -} - -BannerHost::Preset BannerHost::presetFor(Motif motif) -{ - // speed/seed tuned per motif so e.g. the network "pulse" (Account) reads - // at a deliberately calmer cadence than the data "scan" lines - // (Preferences), even though both come from the same shader. - switch (motif) { - case Motif::Welcome: - return {0.0, 0.6, 0.15}; - case Motif::CardDatabase: - return {1.0, 1.3, 0.42}; - case Motif::Theming: - return {2.0, 1.2, 0.73}; - case Motif::Account: - return {3.0, 0.8, 0.28}; - case Motif::Preferences: - return {4.0, 1.0, 0.61}; - case Motif::Finish: - return {5.0, 1.0, 0.91}; - } - return {0.0, 0.6, 0.15}; -} - -void BannerHost::applyMotifPreset(Motif motif) -{ - if (usingFallback || !config) { - return; - } - - const Preset p = presetFor(motif); - - config->setColorA(bannerColorA); - config->setColorB(bannerColorB); - config->setAccent(bannerAccent); - config->setLogoVisible(motif == Motif::Welcome); - - if (isFirstApply) { - // Nothing on screen yet -- write straight into the front bank, no - // crossfade needed for the very first paint. - config->setModeA(p.mode); - config->setSpeedA(p.speed); - config->setSeedA(p.seed); - config->setFrontIsA(true); - isFirstApply = false; - return; - } - - // Write the new preset into whichever bank is currently hidden, then - // flip which one is front. QML's opacity Behavior does the actual - // crossfade -- BannerHost never animates anything itself. - if (config->frontIsA()) { - config->setModeB(p.mode); - config->setSpeedB(p.speed); - config->setSeedB(p.seed); - config->setFrontIsA(false); - } else { - config->setModeA(p.mode); - config->setSpeedA(p.speed); - config->setSeedA(p.seed); - config->setFrontIsA(true); - } -} - -void BannerHost::updateAspect() -{ - if (config && height() > 0) { - config->setAspect(qreal(width()) / qreal(height())); - } -} - -void BannerHost::resizeEvent(QResizeEvent *event) -{ - QWidget::resizeEvent(event); - updateAspect(); -} - -void BannerHost::showEvent(QShowEvent *event) -{ - QWidget::showEvent(event); - if (!usingFallback) { - elapsed.restart(); - clock.start(); - } -} - -void BannerHost::hideEvent(QHideEvent *event) -{ - QWidget::hideEvent(event); - clock.stop(); -} - -void BannerHost::applyThemeColors() -{ - const SuggestedColors colors = suggestedBannerColors(); - bannerColorA = colors.colorA; - bannerColorB = colors.colorB; - bannerAccent = colors.accent; - - if (usingFallback) { - fallback->setColors(bannerColorA, bannerColorB); - fallback->update(); - } else if (config) { - config->setColorA(bannerColorA); - config->setColorB(bannerColorB); - config->setAccent(bannerAccent); - config->setGlowColor(colors.glowColor); - config->setBrandStrong(colors.brandStrong); - config->setBrandSoft(colors.brandSoft); - config->setVignetteMin(colors.vignetteMin); - } -} - -void BannerHost::tick() -{ - // Palette previews (e.g. accent drags in the wizard's QuickSetupPanel) - // apply qApp->palette() without firing themeChanged, so re-derive here; - // BannerShaderConfig's setters are equality-guarded, so this is a no-op - // unless the colours actually changed. - applyThemeColors(); - if (config) { - qreal t = elapsed.elapsed() / 1000.0; - config->setTime(t); - // Visible breathing for the logo: oscillates between 0.0 and 1.0 - qreal glow = 0.5 + 0.5 * qSin(t * 0.4); - config->setLogoGlow(glow); - } -} diff --git a/cockatrice/src/interface/widgets/onboarding/shader_banner_widget.h b/cockatrice/src/interface/widgets/onboarding/shader_banner_widget.h deleted file mode 100644 index ac47b1141..000000000 --- a/cockatrice/src/interface/widgets/onboarding/shader_banner_widget.h +++ /dev/null @@ -1,91 +0,0 @@ -#ifndef SHADER_BANNER_WIDGET_H -#define SHADER_BANNER_WIDGET_H - -#include -#include -#include -#include - -class BannerShaderConfig; -class QQuickWidget; -class GradientFallbackWidget; -class QStackedLayout; - -/** @brief Onboarding banner: a subtle, looping brand-shader animation, one of six - * per-page "motifs" driving the same prebaked fragment shader - * (onboarding/shaders/brand_banner.frag) with different uniform values, so - * every page feels distinct but unmistakably part of the same family. - * - * Motif switches crossfade smoothly (see BrandBanner.qml's two stacked - * ShaderEffect layers + Behavior on opacity) rather than cutting instantly - * -- BannerHost just writes the new preset into whichever layer is - * currently hidden and flips BannerShaderConfig::frontIsA; QML handles the - * actual animation declaratively. - * - * Falls back to a static two-stop gradient (no shader, no QQuickWidget) if - * the platform's Qt Quick scenegraph can't initialize -- e.g. software - * rendering only, or a CI/VM environment with no GPU -- so onboarding - * never blocks or blanks out over a graphics driver problem. The fallback - * is permanent for the lifetime of this widget once triggered. */ -class BannerHost : public QWidget -{ - Q_OBJECT - -public: - enum class Motif - { - Welcome, - CardDatabase, - Theming, - Account, - Preferences, - Finish, - }; - - explicit BannerHost(QWidget *parent = nullptr); - - void setMotif(Motif motif); - -protected: - void showEvent(QShowEvent *event) override; - void hideEvent(QHideEvent *event) override; - void resizeEvent(QResizeEvent *event) override; - -private slots: - void tick(); - void onSceneGraphFailed(); - void applyThemeColors(); - -private: - struct Preset - { - qreal mode; - qreal speed; - qreal seed; - }; - - static Preset presetFor(Motif motif); - - void applyMotifPreset(Motif motif); - void updateAspect(); - void activateFallback(); - - QStackedLayout *stack; - QQuickWidget *quickWidget = nullptr; - BannerShaderConfig *config = nullptr; - GradientFallbackWidget *fallback = nullptr; - - // Palette-derived banner colours -- the theme's window hue forced down to - // the banner's curated darkness, plus the theme's Highlight as accent. - QColor bannerColorA; - QColor bannerColorB; - QColor bannerAccent; - - QTimer clock; - QElapsedTimer elapsed; - Motif currentMotif = Motif::Welcome; - bool usingFallback = false; - bool isFirstApply = true; -}; - -#endif // SHADER_BANNER_WIDGET_H diff --git a/cockatrice/src/interface/widgets/onboarding/shaders/brand_banner.frag b/cockatrice/src/interface/widgets/onboarding/shaders/brand_banner.frag deleted file mode 100644 index 2bf6d0abf..000000000 --- a/cockatrice/src/interface/widgets/onboarding/shaders/brand_banner.frag +++ /dev/null @@ -1,468 +0,0 @@ -#version 440 - -// ════════════════════════════════════════════════════════════════════════ -// brand_banner.frag -// -// One shader, six motifs (uMode 0..5). All motifs composite over a shared -// backgroundField() whose colour is flow-noise-modulated blend of uColorA -// and uColorB. SDFs operate in aspect-corrected space (ac.x = uv.x * -// uAspect) to preserve shape proportions on the wide banner. -// -// IMPORTANT: the uniform block below must list custom uniforms in EXACTLY -// the order they're declared as properties on each ShaderEffect instance in -// BrandBanner.qml (after the two Qt-supplied members, qt_Matrix/qt_Opacity). -// ════════════════════════════════════════════════════════════════════════ - -layout(location = 0) in vec2 qt_TexCoord0; -layout(location = 0) out vec4 fragColor; - -layout(std140, binding = 0) uniform buf -{ - mat4 qt_Matrix; - float qt_Opacity; - float iTime; - float uAspect; - float uMode; - float uSpeed; - float uSeed; - vec4 uColorA; - vec4 uColorB; - vec4 uAccent; - vec4 uGlowColor; - float uVignetteMin; - float uLogoGlow; -}; - -// ── Primitives ────────────────────────────────────────────────────────── - -float hash21(vec2 p) -{ - p = fract(p * vec2(123.34, 456.21)); - p += dot(p, p + 45.32); - return fract(p.x * p.y); -} - -float valueNoise(vec2 p) -{ - vec2 i = floor(p); - vec2 f = fract(p); - float a = hash21(i); - float b = hash21(i + vec2(1.0, 0.0)); - float c = hash21(i + vec2(0.0, 1.0)); - float d = hash21(i + vec2(1.0, 1.0)); - vec2 u = f * f * (3.0 - 2.0 * f); - return mix(mix(a, b, u.x), mix(c, d, u.x), u.y); -} - -float fbm(vec2 p) -{ - float v = 0.0; - float amp = 0.5; - for (int i = 0; i < 3; i++) { - v += amp * valueNoise(p); - p *= 2.03; - amp *= 0.5; - } - return v; -} - -float flowNoise(vec2 p, float t) -{ - vec2 warp1 = vec2(fbm(p + vec2(0.0, 0.0)), fbm(p + vec2(5.2, 1.3))); - vec2 warp2 = vec2(fbm(p + 4.0 * warp1 + vec2(1.7, 9.2) + t * 0.6), - fbm(p + 4.0 * warp1 + vec2(8.3, 2.8) - t * 0.5)); - return fbm(p + 4.0 * warp2 + t * 0.15); -} - -float bloom(float d, float coreRadius, float haloRadius) -{ - float core = exp(-(d * d) / (coreRadius * coreRadius)); - float halo = exp(-d / haloRadius) * 0.35; - return core + halo; -} - -float roundedBoxSDF(vec2 p, vec2 halfSize, float radius) -{ - vec2 d = abs(p) - halfSize + radius; - return length(max(d, 0.0)) - radius + min(max(d.x, d.y), 0.0); -} - -// Rotated box SDF -- applies 2D rotation to p before evaluating roundedBoxSDF. -float rotatedBoxSDF(vec2 p, vec2 halfSize, float radius, float angle) -{ - float c = cos(angle); - float s = sin(angle); - vec2 rp = vec2(p.x * c - p.y * s, p.x * s + p.y * c); - return roundedBoxSDF(rp, halfSize, radius); -} - -float vignette(vec2 uv) -{ - vec2 c = uv - 0.5; - c.x *= max(uAspect, 0.0001); - return smoothstep(1.0, 0.25, length(c)); -} - -// ── Shared background ─────────────────────────────────────────────────── - -vec3 backgroundField(vec2 uv, float time) -{ - // Diagonal luminance gradient from (0,0) to (1,1) used as blend factor - // between uColorA and uColorB; modulated by flowNoise. - float baseD = smoothstep(0.0, 1.0, uv.y * 0.5 + uv.x * 0.2); - float painted = flowNoise(uv * 1.5, time * 0.04) - 0.5; - baseD = clamp(baseD + painted * 0.12, 0.0, 1.0); - - vec3 col = mix(uColorA.rgb, uColorB.rgb, baseD); - - // Low-frequency fBM noise pushes local colour toward uColorB for depth - float deep = fbm(uv * 1.0 + vec2(37.1, 12.4) + time * 0.015); - col = mix(col, uColorB.rgb, (deep - 0.5) * 0.08); - - // Accent-coloured fog layer: flowNoise peaks above 0.6 contribute accent - float fog = flowNoise(uv * 0.8 + vec2(100.0, 50.0), time * 0.02); - col += uAccent.rgb * max(fog - 0.6, 0.0) * 0.14; - - return col; -} - -// ── Motifs ────────────────────────────────────────────────────────────── - -// Centre bloom, flow-noise shimmer gated to centre, and 48 orbiting ember -// particles that deflect into a tight ring near the centre. -vec3 motifWelcome(vec2 uv, vec3 bg, float t) -{ - vec3 col = bg; - float asp = max(uAspect, 0.001); - vec2 ac = vec2(uv.x * asp, uv.y); - vec2 center = vec2(asp * 0.5, 0.5); - float cDist = length(ac - center); - - // Centre bloom at logo position; intensity scales with uLogoGlow. The - // QML brandGlow halo now supplies the primary logo surround (the two - // brand appColors), so this shader bloom is deliberately kept as a subtle - // ambience rather than a competing glow. - float centreLight = bloom(cDist, 0.08 * asp, 0.40 * asp); - col += uGlowColor.rgb * centreLight * 0.20 * uLogoGlow; - - // Flow-noise shimmer gated by Gaussian mask at centre - float shimmer = flowNoise(ac * 0.8 + vec2(55.0, 33.0), t * 0.05) * 0.5 + 0.5; - float shimmerMask = exp(-(cDist * cDist) / (0.18 * asp * 0.18 * asp)); - col += uGlowColor.rgb * shimmer * shimmerMask * 0.04 * uLogoGlow; - - // 48 ember particles: hash-seeded position, speed, size, brightness. - // Embers within a distance threshold of centre are deflected into an - // orbital ring via tangent displacement perpendicular to the centre vector. - const int EMBERS = 48; - for (int i = 0; i < EMBERS; i++) { - float fi = float(i); - - float baseX = hash21(vec2(fi * 7.31 + uSeed, fi * 3.17)); - float baseY = hash21(vec2(fi * 11.9 + uSeed * 1.4, fi * 5.53)); - - float riseSpeed = 0.025 + hash21(vec2(fi * 1.7, uSeed * 2.1)) * 0.035; - float driftAmp = 0.04 + hash21(vec2(fi * 9.3, uSeed)) * 0.06; - float driftFreq = 0.3 + hash21(vec2(fi * 4.1, uSeed * 3.3)) * 0.5; - - float pX = baseX * asp + sin(t * driftFreq + fi * 1.7) * driftAmp * asp; - float pY = fract(baseY + t * riseSpeed); - - float size = 0.010 + hash21(vec2(fi * 2.9, uSeed * 4.7)) * 0.014; - float bright = 0.18 + hash21(vec2(fi * 6.1, uSeed * 0.9)) * 0.32; - - // Fade out near top/bottom edges - float edgeFade = smoothstep(0.0, 0.12, pY) * smoothstep(1.0, 0.88, pY); - float twinkle = 0.6 + 0.4 * sin(t * (1.2 + fi * 0.37) + fi * 2.9); - - vec2 ePos = vec2(pX, pY); - - // Embers near centre: deflect into orbital ring via tangent displacement - vec2 toCenter = ePos - center; - float distToCenter = length(toCenter); - float ringWeight = smoothstep(0.38 * asp, 0.06 * asp, distToCenter); - - float orbitPhase = t * (0.15 + fi * 0.020) + fi * 2.3; - float orbitAmount = 0.020 + hash21(vec2(fi * 12.3, uSeed * 2.7)) * 0.020; - vec2 tangent = vec2(-toCenter.y, toCenter.x); - vec2 deflected = ePos + tangent * ringWeight * orbitAmount * asp * sin(orbitPhase); - - float pushOut = ringWeight * (0.008 + hash21(vec2(fi * 6.7, uSeed * 1.1)) * 0.012) * asp; - deflected += normalize(toCenter + 0.001) * pushOut; - - float dist = length(ac - deflected); - float intensity = bright * edgeFade * twinkle; - col += uAccent.rgb * bloom(dist, size, size * 4.0) * intensity; - } - - return col; -} - -// 25 card-shaped box SDFs at parallax depths drifting horizontally across -// the banner; each card has a semi-transparent fill, accent outline, and -// card-back diamond pattern. -vec3 motifCardDatabase(vec2 uv, vec3 bg, float t) -{ - vec3 col = bg; - float asp = max(uAspect, 0.001); - vec2 ac = vec2(uv.x * asp, uv.y); - - const int CARDS = 25; - for (int i = 0; i < CARDS; i++) { - float fi = float(i); - - // Parallax depth via hash; used to scale size, speed, brightness - float depth = hash21(vec2(fi * 1.37 + uSeed, fi * 0.91)); - - // Card dimensions in corrected space (portrait: height > width) - float cardH = mix(0.055, 0.15, depth); - cardH *= 0.85 + 0.30 * hash21(vec2(fi * 3.14, uSeed * 2.71)); - float cardW = cardH * 0.71; // 5:7 ratio - - // Horizontal drift; nearer cards (higher depth) move faster - float speed = mix(0.06, 0.18, depth); - float xPhase = hash21(vec2(fi * 7.13, uSeed * 4.37)); - xPhase = fract(xPhase + t * speed); - float x = mix(-1.5, asp + 1.5, xPhase); - - // Vertical position: hash distribution with sinusoidal oscillation - float yBase = hash21(vec2(fi * 2.91, uSeed * 1.63)); - float y = yBase + sin(t * 0.6 + fi * 1.9) * 0.035; - y = clamp(y, cardH + 0.02, 1.0 - cardH - 0.02); - - // Random rotation angle ±4 degrees - float tilt = (hash21(vec2(fi * 5.71, uSeed * 8.29)) - 0.5) * 0.14; - - vec2 p = ac - vec2(x, y); - float d = rotatedBoxSDF(p, vec2(cardW, cardH), cardW * 0.14, tilt); - - // Semi-transparent dark fill - float fill = smoothstep(0.015, -0.005, d); - col = mix(col, uColorB.rgb * 0.60, fill * 0.62); - - // Accent outline - float edge = smoothstep(0.035, 0.0, abs(d)); - col += uAccent.rgb * edge * mix(0.18, 0.50, 1.0 - depth); - - // Card-back diamond: smaller rotated box inset from card edges - float innerD = rotatedBoxSDF(p, vec2(cardW * 0.45, cardH * 0.55), cardW * 0.08, tilt); - float innerEdge = smoothstep(0.012, 0.0, abs(innerD)); - col += uAccent.rgb * innerEdge * fill * 0.12 * (1.0 - depth); - - // Centre dot - float dotDist = length(p); - col += uAccent.rgb * bloom(dotDist, 0.008, 0.02) * fill * 0.15 * (1.0 - depth); - } - return col; -} - -// 4 horizontal bands with multi-frequency sinusoidal warp and pulsing width. -vec3 motifTheming(vec2 uv, vec3 bg, float t) -{ - vec3 col = bg; - - const int BANDS = 4; - for (int i = 0; i < BANDS; i++) { - float fi = float(i); - float yCenter = 0.18 + fi * 0.22; - - // Three summed sinusoids for horizontal undulation - float wave = sin(uv.x * 3.2 + t * 0.5 + fi * 2.1) * 0.08; - wave += sin(uv.x * 7.0 - t * 0.3 + fi * 1.3) * 0.035; - wave += sin(uv.x * 1.6 + t * 0.18 + fi * 3.7) * 0.05; - - float bandDist = abs(uv.y - yCenter - wave); - float bandWidth = 0.04 + sin(t * 0.2 + fi * 0.8) * 0.012; - float band = smoothstep(bandWidth, 0.0, bandDist); - - // Upper bands have higher intensity - float intensity = mix(0.15, 0.38, 1.0 - fi / float(BANDS)); - col += uAccent.rgb * band * intensity; - } - - return col; -} - -// 14 nodes at pseudo-random positions with sinusoidal pulse; edges drawn -// between nodes within a threshold distance; central glow + periodic ring. -vec3 motifAccount(vec2 uv, vec3 bg, float t) -{ - vec3 col = bg; - float asp = max(uAspect, 0.001); - vec2 ac = vec2(uv.x * asp, uv.y); - vec2 center = vec2(asp * 0.5, 0.5); - - const int NODES = 14; - vec2 nodePos[14]; - float nodePulse[14]; - - for (int i = 0; i < NODES; i++) { - float fi = float(i); - - // Hash-seeded position with gentle sinusoidal drift - float nx = hash21(vec2(fi * 3.17 + uSeed, fi * 1.93)) * asp; - float ny = hash21(vec2(fi * 5.41 + uSeed * 1.7, fi * 2.79)); - - float dx = sin(t * 0.12 + fi * 1.7) * 0.08; - float dy = cos(t * 0.09 + fi * 2.3) * 0.04; - vec2 pos = vec2(nx + dx, ny + dy); - nodePos[i] = pos; - - // Per-node pulse phase, normalised to [0, 1] - float pulsePhase = hash21(vec2(fi * 4.31, uSeed * 6.17)); - float pulse = sin(t * 0.8 + pulsePhase * 6.283) * 0.5 + 0.5; - nodePulse[i] = pulse; - - // Node glow via bloom; intensity modulated by pulse - float dist = length(ac - pos); - col += uAccent.rgb * bloom(dist, 0.018, 0.08) * mix(0.30, 0.55, pulse); - } - - // Edges: connect nodes within a radius threshold - float connectDist = asp * 0.22; - for (int i = 0; i < NODES; i++) { - for (int j = i + 1; j < NODES; j++) { - float pairDist = length(nodePos[i] - nodePos[j]); - if (pairDist < connectDist) { - float strength = 1.0 - pairDist / connectDist; - vec2 pa = ac - nodePos[i]; - vec2 ba = nodePos[j] - nodePos[i]; - float h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0); - float lineDist = length(pa - ba * h); - col += uAccent.rgb * smoothstep(0.010, 0.0, lineDist) * strength * 0.14; - } - } - } - - // Central bloom at banner centre - float cDist = length(ac - center); - col += uAccent.rgb * bloom(cDist, 0.04, 0.25) * 0.20; - - // Periodic expanding ring from centre - float ripplePhase = t * 0.4; - float rippleDist = abs(cDist - fract(ripplePhase) * asp * 0.7); - col += uAccent.rgb * smoothstep(0.02, 0.0, rippleDist) * 0.14; - - return col; -} - -// 18x5 toggle-grid of rounded boxes with hash-driven on/off per cell; -// a scanning highlight sweeps L-to-R, brightening cells near the scan line. -vec3 motifPreferences(vec2 uv, vec3 bg, float t) -{ - vec3 col = bg; - - float cols = 18.0; - float rows = 5.0; - vec2 gridUV = uv * vec2(cols, rows); - vec2 cell = fract(gridUV) - 0.5; - vec2 cellId = floor(gridUV); - - // On/off state per cell, hash-seeded for pseudo-randomness - float on = step(0.55, hash21(cellId + uSeed * 10.0)); - - float d = roundedBoxSDF(cell, vec2(0.28, 0.32), 0.06); - - // Filled "on" cells - float cellFill = smoothstep(0.04, -0.02, d); - col += uAccent.rgb * cellFill * on * 0.18; - - // Cell borders (drawn on all cells) - float border = smoothstep(0.025, 0.0, abs(d)); - col += uAccent.rgb * border * 0.06; - - // Scanning highlight: thin line + soft glow sweeping L-to-R - float scanX = fract(t * 0.15); - float scanDist = abs(uv.x - scanX); - float scanLine = smoothstep(0.015, 0.0, scanDist); - col += uAccent.rgb * scanLine * 0.40; - - float scanGlow = smoothstep(0.08, 0.0, scanDist); - col += uAccent.rgb * scanGlow * 0.08; - - // "On" cells near the scan line get extra brightness - float scanProximity = smoothstep(0.12, 0.0, scanDist); - col += uAccent.rgb * cellFill * on * scanProximity * 0.15; - - return col; -} - -// Centre radial bloom with sinusoidal pulse, 4 expanding ring halos with -// outer glow falloff, and 35 rising particles. -vec3 motifFinish(vec2 uv, vec3 bg, float t) -{ - vec3 col = bg; - float asp = max(uAspect, 0.001); - vec2 ac = vec2(uv.x * asp, uv.y); - vec2 center = vec2(asp * 0.5, 0.5); - float cDist = length(ac - center); - - // Centre bloom with sinusoidal pulse modulation - float pulse = 0.65 + 0.35 * sin(t * 0.4); - col += uAccent.rgb * bloom(cDist, 0.12, 0.55) * 0.10 * pulse; - - // 4 expanding rings: radius increases via phase; ring width grows with - // expansion; combined with exponential outer glow falloff - for (int i = 0; i < 4; i++) { - float fi = float(i); - float phase = fract(t * 0.06 + fi * 0.25); - float ringRadius = phase * asp * 0.7; - float ringDist = abs(cDist - ringRadius); - float ringWidth = 0.025 + phase * 0.025; - float ring = smoothstep(ringWidth, 0.0, ringDist); - float outerGlow = exp(-ringDist / (0.03 + phase * 0.02)) * 0.3; - float combined = ring + outerGlow; - float fade = 1.0 - phase * 0.5; - col += uAccent.rgb * combined * fade * 0.15; - } - - // 35 particles rising vertically with sinusoidal horizontal drift; - // each particle uses bloom with edge fade and twinkle animation - const int PARTICLES = 35; - for (int i = 0; i < PARTICLES; i++) { - float fi = float(i); - float baseX = hash21(vec2(fi * 13.7 + uSeed, fi * 7.31)); - float baseY = hash21(vec2(fi * 23.1 + uSeed * 1.9, fi * 11.3)); - - float riseSpeed = 0.04 + hash21(vec2(fi * 3.1, uSeed * 2.7)) * 0.06; - float driftAmp = 0.03 + hash21(vec2(fi * 8.9, uSeed)) * 0.05; - float driftFreq = 0.4 + hash21(vec2(fi * 5.3, uSeed * 4.1)) * 0.6; - - float pX = baseX * asp + sin(t * driftFreq + fi * 2.3) * driftAmp * asp; - float pY = fract(baseY + t * riseSpeed); - - float size = 0.005 + hash21(vec2(fi * 4.7, uSeed * 3.9)) * 0.010; - float bright = 0.12 + hash21(vec2(fi * 7.1, uSeed * 1.3)) * 0.25; - - float edgeFade = smoothstep(0.0, 0.1, pY) * smoothstep(1.0, 0.9, pY); - float twinkle = 0.5 + 0.5 * sin(t * (1.8 + fi * 0.43) + fi * 3.1); - - vec2 pPos = vec2(pX, pY); - float dist = length(ac - pPos); - col += uAccent.rgb * bloom(dist, size, size * 3.5) * bright * edgeFade * twinkle; - } - - return col; -} - -// ── Main ──────────────────────────────────────────────────────────────── - -void main() -{ - vec2 uv = qt_TexCoord0; - float t = iTime * uSpeed; - - vec3 bg = backgroundField(uv, iTime); - - vec3 col; - if (uMode < 0.5) col = motifWelcome(uv, bg, t); - else if (uMode < 1.5) col = motifCardDatabase(uv, bg, t); - else if (uMode < 2.5) col = motifTheming(uv, bg, t); - else if (uMode < 3.5) col = motifAccount(uv, bg, t); - else if (uMode < 4.5) col = motifPreferences(uv, bg, t); - else col = motifFinish(uv, bg, t); - - // Corner vignette; uVignetteMin is scheme-driven (0.62 on dark stages, - // gentler on light ones so near-white corners don't go muddy grey). - col *= mix(uVignetteMin, 1.0, vignette(uv)); - fragColor = vec4(col, 1.0) * qt_Opacity; -} diff --git a/cockatrice/src/interface/widgets/onboarding/shaders/brand_plate.frag b/cockatrice/src/interface/widgets/onboarding/shaders/brand_plate.frag deleted file mode 100644 index 43b798c9f..000000000 --- a/cockatrice/src/interface/widgets/onboarding/shaders/brand_plate.frag +++ /dev/null @@ -1,42 +0,0 @@ -#version 440 - -// The logo's gradient plate, drawn by us rather than the baked-in SVG: a -// linear blend between the two brand appColors (light AccentSoft at the -// top-left grading to dark AccentStrong at the bottom-right, mirroring -// cockatrice.svg's linearGradient4265-7-8 userSpaceOnUse axis), clipped to -// the bird's full silhouette via the full-color logo's alpha (uSilhouette). -// The white highlight path (cockatrice-logo-white) is overlaid in QML on top, -// exactly as the SVG stacks its white path over the gradient paths. Fully -// static: no glow, no breathing — the plate just sits there like the home -// widget's QPainter composite. - -layout(location = 0) in vec2 qt_TexCoord0; -layout(location = 0) out vec4 fragColor; - -layout(std140, binding = 0) uniform buf -{ - mat4 qt_Matrix; - float qt_Opacity; - vec4 uStrong; - vec4 uSoft; -}; - -// The full-color logo's alpha channel acts as the silhouette mask: the -// gradient only appears inside the bird, exactly like the SVG's gradient paths. -layout(binding = 1) uniform sampler2D uSilhouette; - -void main() -{ - // Recreate cockatrice.svg's own gradient geometry (linearGradient4265-7-8, - // userSpaceOnUse): light AccentSoft at the start point S=(-8.097,-97.746), - // dark AccentStrong at the end E=(162.455,295.208), on the SVG's 300x300 - // canvas. Normalized to UV space, V=E-S=(0.5685,1.3098), so - // t = dot(uv - S_norm, V)/|V|^2 with S_norm=(-0.0270,-0.3258). - float t = clamp(dot(qt_TexCoord0 - vec2(-0.02699, -0.32582), vec2(0.56851, 1.30985)) / 2.03891, 0.0, 1.0); - vec3 color = mix(uSoft.rgb, uStrong.rgb, t); - - // Anti-aliased silhouette clip from the full-color logo's alpha. - float alpha = texture(uSilhouette, qt_TexCoord0).a; - - fragColor = vec4(color * alpha, alpha) * qt_Opacity; -} \ No newline at end of file diff --git a/cockatrice/src/interface/widgets/onboarding/step_indicator_widget.cpp b/cockatrice/src/interface/widgets/onboarding/step_indicator_widget.cpp deleted file mode 100644 index d25e8544b..000000000 --- a/cockatrice/src/interface/widgets/onboarding/step_indicator_widget.cpp +++ /dev/null @@ -1,84 +0,0 @@ -#include "step_indicator_widget.h" - -#include -#include - -StepIndicatorWidget::StepIndicatorWidget(QWidget *parent) : QWidget(parent) -{ - setFixedHeight(kDotDiameter + 2 * kVerticalMargin); -} - -void StepIndicatorWidget::setStepCount(int count) -{ - stepCount = qMax(0, count); - currentStep = qBound(0, currentStep, qMax(0, stepCount - 1)); - updateGeometry(); - update(); -} - -void StepIndicatorWidget::setCurrentStep(int index) -{ - if (stepCount == 0) { - return; - } - currentStep = qBound(0, index, stepCount - 1); - update(); -} - -QSize StepIndicatorWidget::sizeHint() const -{ - return minimumSizeHint(); -} - -QSize StepIndicatorWidget::minimumSizeHint() const -{ - if (stepCount == 0) { - return QSize(0, height()); - } - int width = kActiveDotWidth + (stepCount - 1) * kDotDiameter + (stepCount - 1) * kDotSpacing; - return QSize(width, height()); -} - -void StepIndicatorWidget::paintEvent(QPaintEvent * /*event*/) -{ - if (stepCount == 0) { - return; - } - - QPainter painter(this); - painter.setRenderHint(QPainter::Antialiasing); - - const QColor activeColor = palette().color(QPalette::Highlight); - - // QPalette::Mid alpha-blended against a dark Window background reads as - // near-invisible (Mid is itself a dark grey in dark palettes -- see - // PaletteGenerator's satShadeLo/Dark roles). WindowText is guaranteed to - // contrast against Window in any theme by definition, so alpha-blending - // *that* instead keeps the dots visibly dim-but-present in both light and - // dark schemes. Same trick PaletteGenerator uses for placeholder text. - QColor inactiveColor = palette().color(QPalette::WindowText); - inactiveColor.setAlpha(100); - - int totalWidth = 0; - for (int i = 0; i < stepCount; ++i) { - totalWidth += (i == currentStep) ? kActiveDotWidth : kDotDiameter; - if (i > 0) { - totalWidth += kDotSpacing; - } - } - - int x = (width() - totalWidth) / 2; - const int y = height() / 2; - - for (int i = 0; i < stepCount; ++i) { - const bool active = (i == currentStep); - const int dotWidth = active ? kActiveDotWidth : kDotDiameter; - - QPainterPath path; - QRectF rect(x, y - kDotDiameter / 2.0, dotWidth, kDotDiameter); - path.addRoundedRect(rect, kDotDiameter / 2.0, kDotDiameter / 2.0); - painter.fillPath(path, active ? activeColor : inactiveColor); - - x += dotWidth + kDotSpacing; - } -} diff --git a/cockatrice/src/interface/widgets/onboarding/step_indicator_widget.h b/cockatrice/src/interface/widgets/onboarding/step_indicator_widget.h deleted file mode 100644 index 1b85be04f..000000000 --- a/cockatrice/src/interface/widgets/onboarding/step_indicator_widget.h +++ /dev/null @@ -1,34 +0,0 @@ -#ifndef STEP_INDICATOR_WIDGET_H -#define STEP_INDICATOR_WIDGET_H - -#include - -/** @brief Row of dots showing progress through a fixed-length sequence of steps, - * in the style of a mobile/OS setup flow. Purely presentational. */ -class StepIndicatorWidget : public QWidget -{ - Q_OBJECT - -public: - explicit StepIndicatorWidget(QWidget *parent = nullptr); - - void setStepCount(int count); - void setCurrentStep(int index); - - QSize sizeHint() const override; - QSize minimumSizeHint() const override; - -protected: - void paintEvent(QPaintEvent *event) override; - -private: - int stepCount = 0; - int currentStep = 0; - - static constexpr int kDotDiameter = 8; - static constexpr int kActiveDotWidth = 22; - static constexpr int kDotSpacing = 10; - static constexpr int kVerticalMargin = 6; -}; - -#endif // STEP_INDICATOR_WIDGET_H diff --git a/cockatrice/src/interface/widgets/playmat/playmat_collection_dialog.cpp b/cockatrice/src/interface/widgets/playmat/playmat_collection_dialog.cpp deleted file mode 100644 index e970f8413..000000000 --- a/cockatrice/src/interface/widgets/playmat/playmat_collection_dialog.cpp +++ /dev/null @@ -1,188 +0,0 @@ -#include "playmat_collection_dialog.h" - -#include "../../../client/settings/cache_settings.h" -#include "playmat_settings_dialog.h" - -#include -#include -#include -#include -#include -#include -#include -#include - -PlaymatCollectionDialog::PlaymatCollectionDialog(QWidget *parent) : QDialog(parent) -{ - setMinimumWidth(420); - setupUi(); - retranslateUi(); -} - -void PlaymatCollectionDialog::accept() -{ - auto &interfaceSettings = SettingsCache::instance().userInterface(); - interfaceSettings.setPlaymatFallbackList(playmats); - interfaceSettings.setPlaymatFallbackBehavior(modeCombo->currentData().toInt()); - QDialog::accept(); -} - -int PlaymatCollectionDialog::currentRow() const -{ - return playmatList->currentRow(); -} - -void PlaymatCollectionDialog::setupUi() -{ - auto &interfaceSettings = SettingsCache::instance().userInterface(); - playmats = interfaceSettings.getPlaymatFallbackList(); - - playmatList = new QListWidget; - for (const PlaymatInfo &entry : playmats) { - playmatList->addItem(entry.card.name); - } - connect(playmatList, &QListWidget::itemSelectionChanged, this, &PlaymatCollectionDialog::selectionChanged); - connect(playmatList, &QListWidget::itemDoubleClicked, this, [this](QListWidgetItem *) { editPlaymat(); }); - - addButton = new QPushButton; - editButton = new QPushButton; - removeButton = new QPushButton; - moveUpButton = new QPushButton; - moveDownButton = new QPushButton; - - connect(addButton, &QPushButton::clicked, this, &PlaymatCollectionDialog::addPlaymat); - connect(editButton, &QPushButton::clicked, this, &PlaymatCollectionDialog::editPlaymat); - connect(removeButton, &QPushButton::clicked, this, &PlaymatCollectionDialog::removePlaymat); - connect(moveUpButton, &QPushButton::clicked, this, &PlaymatCollectionDialog::movePlaymatUp); - connect(moveDownButton, &QPushButton::clicked, this, &PlaymatCollectionDialog::movePlaymatDown); - - auto *listButtons = new QVBoxLayout; - listButtons->addWidget(addButton); - listButtons->addWidget(editButton); - listButtons->addWidget(removeButton); - listButtons->addWidget(moveUpButton); - listButtons->addWidget(moveDownButton); - listButtons->addStretch(); - - auto *listRow = new QHBoxLayout; - listRow->addWidget(playmatList, 1); - listRow->addLayout(listButtons); - - modeCombo = new QComboBox; - modeCombo->addItem(QString(), PlaymatFallbackModeFixed); - modeCombo->addItem(QString(), PlaymatFallbackModeRoundRobin); - modeCombo->addItem(QString(), PlaymatFallbackModeRandom); - const int modeIndex = modeCombo->findData(interfaceSettings.getPlaymatFallbackBehavior()); - if (modeIndex >= 0) { - modeCombo->setCurrentIndex(modeIndex); - } - - auto *modeRow = new QHBoxLayout; - modeLabel = new QLabel; - modeLabel->setBuddy(modeCombo); - modeRow->addWidget(modeLabel); - modeRow->addWidget(modeCombo, 1); - - auto *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); - connect(buttonBox, &QDialogButtonBox::accepted, this, &PlaymatCollectionDialog::accept); - connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); - - auto *root = new QVBoxLayout; - root->addLayout(listRow); - root->addLayout(modeRow); - root->addWidget(buttonBox); - setLayout(root); - - selectionChanged(); -} - -void PlaymatCollectionDialog::selectionChanged() -{ - const bool hasSelection = playmatList->currentRow() >= 0; - editButton->setEnabled(hasSelection); - removeButton->setEnabled(hasSelection); - moveUpButton->setEnabled(hasSelection && playmatList->currentRow() > 0); - moveDownButton->setEnabled(hasSelection && playmatList->currentRow() < playmatList->count() - 1); -} - -void PlaymatCollectionDialog::addPlaymat() -{ - PlaymatSettingsDialog dialog(CardRef{}, PlaymatParams{}, this); - if (dialog.exec() == QDialog::Accepted) { - const CardRef card = dialog.card(); - if (!card.isEmpty()) { - PlaymatInfo res = {card, dialog.params()}; - playmats.append(res); - playmatList->addItem(res.card.name); - playmatList->setCurrentRow(playmatList->count() - 1); - } - } -} - -void PlaymatCollectionDialog::editPlaymat() -{ - const int row = currentRow(); - if (row < 0) { - return; - } - - const PlaymatInfo ¤t = playmats.at(row); - PlaymatSettingsDialog dialog(current.card, current.params, this); - if (dialog.exec() == QDialog::Accepted) { - const CardRef card = dialog.card(); - if (card.isEmpty()) { - return; // Removal is handled by the Remove button - } - playmats[row] = {card, dialog.params()}; - playmatList->item(row)->setText(card.name); - } -} - -void PlaymatCollectionDialog::removePlaymat() -{ - const int row = currentRow(); - if (row < 0) { - return; - } - playmats.removeAt(row); - delete playmatList->takeItem(row); - selectionChanged(); -} - -void PlaymatCollectionDialog::movePlaymatUp() -{ - const int row = currentRow(); - if (row <= 0) { - return; - } - playmats.swapItemsAt(row, row - 1); - playmatList->insertItem(row - 1, playmatList->takeItem(row)); - playmatList->setCurrentRow(row - 1); - selectionChanged(); -} - -void PlaymatCollectionDialog::movePlaymatDown() -{ - const int row = currentRow(); - if (row < 0 || row >= playmats.size() - 1) { - return; - } - playmats.swapItemsAt(row, row + 1); - playmatList->insertItem(row + 1, playmatList->takeItem(row)); - playmatList->setCurrentRow(row + 1); - selectionChanged(); -} - -void PlaymatCollectionDialog::retranslateUi() -{ - setWindowTitle(tr("Default Playmats")); - addButton->setText(tr("Add...")); - editButton->setText(tr("Edit...")); - removeButton->setText(tr("Remove")); - moveUpButton->setText(tr("Move Up")); - moveDownButton->setText(tr("Move Down")); - modeLabel->setText(tr("List mode:")); - modeCombo->setItemText(0, tr("Fixed (always the first entry)")); - modeCombo->setItemText(1, tr("Round-robin (cycle through entries)")); - modeCombo->setItemText(2, tr("Random (pick one per game)")); -} diff --git a/cockatrice/src/interface/widgets/playmat/playmat_collection_dialog.h b/cockatrice/src/interface/widgets/playmat/playmat_collection_dialog.h deleted file mode 100644 index 167893f8e..000000000 --- a/cockatrice/src/interface/widgets/playmat/playmat_collection_dialog.h +++ /dev/null @@ -1,54 +0,0 @@ -#ifndef COCKATRICE_PLAYMAT_COLLECTION_DIALOG_H -#define COCKATRICE_PLAYMAT_COLLECTION_DIALOG_H - -#include -#include - -class QComboBox; -class QLabel; -class QListWidget; -class QListWidgetItem; -class QPushButton; - -/** - * @brief Dialog for editing the user-level playmat collection. - * - * The collection is the fallback used when a deck has no playmat of its own. - * It supports multiple entries and a pick mode (always first / round-robin / - * random). The dialog edits a working copy and writes it to the settings only - * when accepted. - */ -class PlaymatCollectionDialog : public QDialog -{ - Q_OBJECT - -public: - explicit PlaymatCollectionDialog(QWidget *parent = nullptr); - - void accept() override; - -private slots: - void addPlaymat(); - void editPlaymat(); - void removePlaymat(); - void movePlaymatUp(); - void movePlaymatDown(); - void selectionChanged(); - -private: - void setupUi(); - void retranslateUi(); - int currentRow() const; - - QList playmats; ///< Working copy edited by the dialog. - QListWidget *playmatList; - QComboBox *modeCombo; - QLabel *modeLabel; - QPushButton *addButton; - QPushButton *editButton; - QPushButton *removeButton; - QPushButton *moveUpButton; - QPushButton *moveDownButton; -}; - -#endif // COCKATRICE_PLAYMAT_COLLECTION_DIALOG_H diff --git a/cockatrice/src/interface/widgets/playmat/playmat_preview_widget.cpp b/cockatrice/src/interface/widgets/playmat/playmat_preview_widget.cpp deleted file mode 100644 index 130daadf4..000000000 --- a/cockatrice/src/interface/widgets/playmat/playmat_preview_widget.cpp +++ /dev/null @@ -1,381 +0,0 @@ -#include "playmat_preview_widget.h" - -#include "../cards/art_crop_attribution.h" -#include "playmat_utils.h" - -#include -#include -#include -#include -#include -#include -#include - -namespace -{ -// Mirrors the dialog/proto clamps so gestures can never produce an -// out of range parameter. The zoom FLOOR is dynamic: see -// playmatClampedZoom(), zooming out stops where the sampling window would -// exceed the card itself, so there is no dead range at the bottom end. -constexpr qreal MAX_MARGIN = 0.95; -// Range of in game stack+table aspect ratios worth designing for, derived -// from PlayerGraphicsItem::paint()'s combinedArea = stack ∪ table: -// height = 10 + 30 + 3*102 + 2*30 = 406 (TableZone rows) -// width = 1.5*72 + (20 + 5*72 + 15) = 503 (StackZone + TableZone -// at MIN_WIDTH) -// The area's shape depends on GAME CONTENT (played card columns widen the -// table by ~107 px each), not on the window size. Fresh board ≈ 503/406, -// a table grown to roughly double its minimum width ≈ 2.2. -constexpr qreal MIN_TABLE_ASPECT = 503.0 / 406.0; // fresh board: most generous framing -constexpr qreal MAX_TABLE_ASPECT = 2.2; // well developed, wide table -// Keyboard nudge steps (viewport convention: Down looks further down). -constexpr qreal KEY_PAN_MARGIN_STEP = 0.005; -constexpr qreal KEY_PAN_OFFSET_STEP = 0.01; -constexpr qreal KEY_ZOOM_STEP = 1.05; -constexpr qreal WHEEL_ZOOM_BASE = 1.15; // zoom factor per wheel notch -} // namespace - -PlaymatPreviewWidget::PlaymatPreviewWidget(QWidget *parent) : QWidget(parent) -{ - // The crop is square and drawn contain fit, so the height decides its - // on screen size, keep it generous but let the dialog compress on small - // or high DPI screens - setMinimumSize(400, 180); - QSizePolicy sp(QSizePolicy::Expanding, QSizePolicy::Expanding); - setSizePolicy(sp); - setFocusPolicy(Qt::StrongFocus); - setCursor(Qt::OpenHandCursor); - setAccessibleName(tr("Playmat crop")); - setAccessibleDescription(tr("Zoom %1×").arg(QString::number(params.zoom, 'f', 2))); -} - -void PlaymatPreviewWidget::setPixmap(const QPixmap &pixmap) -{ - sourcePixmap = pixmap; - setCursor(sourcePixmap.isNull() ? Qt::ArrowCursor : Qt::OpenHandCursor); - update(); -} - -void PlaymatPreviewWidget::setParams(const PlaymatParams &p) -{ - params = p; - setAccessibleDescription(tr("Zoom %1×").arg(QString::number(params.zoom, 'f', 2))); - update(); -} - -void PlaymatPreviewWidget::setAttribution(const QString &attribution) -{ - attributionText = attribution; - update(); -} - -QRectF PlaymatPreviewWidget::activePlayArea() const -{ - // The viewport is a frame shaped like a fresh board's stack+table area - // (kMinTableAspect): the most generous framing the game will produce. - // Dimmed strips mark where a wider, developed table crops further. - const QRectF cardRect = QRectF(rect()).adjusted(3, 2, -3, -2); - return PlaymatUtils::aspectFitRect(cardRect.adjusted(6, 4, -4, -4), MIN_TABLE_ASPECT); -} - -qreal PlaymatPreviewWidget::samplingWindowSide() const -{ - if (sourcePixmap.isNull()) { - return 0.0; - } - // Same clamped window the render path uses, gestures and painting must - // never disagree about geometry. - return PlaymatUtils::playmatWindowSide(sourcePixmap.size(), params); -} - -qreal PlaymatPreviewWidget::widgetToSourceScale() const -{ - const qreal cropSide = samplingWindowSide(); - const QRectF area = activePlayArea(); - if (cropSide <= 0.0 || area.isEmpty()) { - return 0.0; - } - // Mirror coverFitRect(): the square crop into the (wider) viewport fills - // its width. - return area.width() / cropSide; -} - -void PlaymatPreviewWidget::applyCropDelta(qreal dMarginL, qreal dMarginR, qreal dOffset, qreal zoomFactor) -{ - PlaymatParams next = params; - if (dMarginL + dMarginR == 0.0) { - // Pure horizontal pan rebalances the margins along their - // sum constant segment. Individual bounds must not break that - // invariant, otherwise repeated corner drags let one margin grow - // without end, collapsing the viewing window and desynchronizing the - // visual zoom from the readout. - const qreal sum = params.marginPctL + params.marginPctR; - const qreal lo = qMax(0.0, sum - MAX_MARGIN); - const qreal hi = qMin(sum, MAX_MARGIN); - next.marginPctL = qBound(lo, params.marginPctL + dMarginL, hi); - next.marginPctR = sum - next.marginPctL; - } else { - next.marginPctL = qBound(0.0, params.marginPctL + dMarginL, MAX_MARGIN); - next.marginPctR = qBound(0.0, params.marginPctR + dMarginR, MAX_MARGIN); - } - next.verticalOffset = qBound(0.0, params.verticalOffset + dOffset, 1.0); - // Clamp through the shared helper so the floor tracks the new margins: - // zooming out stops exactly where the window reaches the card bounds. - next.zoom = params.zoom * zoomFactor; - if (!sourcePixmap.isNull()) { - next.zoom = PlaymatUtils::playmatClampedZoom(sourcePixmap.size(), next); - } - - if (sameCrop(next, params)) { - return; - } - - params = next; - setAccessibleDescription(tr("Zoom %1×").arg(QString::number(params.zoom, 'f', 2))); - update(); - emit paramsEdited(params); -} - -bool PlaymatPreviewWidget::sameCrop(const PlaymatParams &a, const PlaymatParams &b) const -{ - // Exact comparison on purpose: clamped assignments yield identical bits, - // while qFuzzyCompare based equality misbehaves around zero - return a.marginPctL == b.marginPctL && a.marginPctR == b.marginPctR && a.verticalOffset == b.verticalOffset && - a.zoom == b.zoom; -} - -void PlaymatPreviewWidget::restoreSnapshot() -{ - // The snapshot only ever holds values that passed the gesture clamps, - // so it is safe to restore verbatim - if (sameCrop(paramsAtFocusIn, params)) { - return; - } - params = paramsAtFocusIn; - setAccessibleDescription(tr("Zoom %1×").arg(QString::number(params.zoom, 'f', 2))); - update(); - emit paramsEdited(params); -} - -void PlaymatPreviewWidget::paintEvent(QPaintEvent *) -{ - QPainter painter(this); - painter.setRenderHints(QPainter::Antialiasing | QPainter::SmoothPixmapTransform | QPainter::TextAntialiasing); - - const QRect rect = this->rect(); - const QColor accentColor(100, 116, 139); - - // Background - const QRectF cardRect = QRectF(rect).adjusted(3, 2, -3, -2); - QLinearGradient bg(cardRect.topLeft(), cardRect.topRight()); - bg.setColorAt(0, accentColor.darker(320)); - bg.setColorAt(1, QColor(18, 22, 30)); - painter.setPen(Qt::NoPen); - painter.setBrush(bg); - painter.drawRoundedRect(cardRect, 6, 6); - painter.setBrush(accentColor); - painter.drawRoundedRect(QRectF(cardRect.left(), cardRect.top(), 3, cardRect.height()), 2, 2); - - if (sourcePixmap.isNull()) { - painter.setPen(QColor(150, 150, 150)); - painter.drawText(rect, Qt::AlignCenter, tr("No card selected")); - return; - } - - const QRectF playArea = activePlayArea(); - - // Exactly the game's pipeline (player_graphics_item): cover fit the crop - // into the table shaped viewport, centered, so the frame shows precisely - // what a minimum aspect window shows, and dragging moves the art behind - // the fixed frame. - const QRectF srcRect = PlaymatUtils::computeArtSourceRect(sourcePixmap.size(), params); - const QRectF dstRect = PlaymatUtils::coverFitRect(playArea, srcRect.size()); - - painter.setClipRect(playArea.toRect()); - painter.drawPixmap(dstRect, sourcePixmap, srcRect); - - // Wider (developed) tables crop further: mark where a kMaxTableAspect - // board stops. Palette driven so theme authors can recolor the markers. - const qreal wideBandHeight = playArea.height() * (MIN_TABLE_ASPECT / MAX_TABLE_ASPECT); - const qreal stripHeight = (playArea.height() - wideBandHeight) / 2.0; - QColor stripColor = palette().color(QPalette::Window); - stripColor.setAlpha(150); - painter.fillRect(QRectF(playArea.left(), playArea.top(), playArea.width(), stripHeight), stripColor); - painter.fillRect(QRectF(playArea.left(), playArea.bottom() - stripHeight, playArea.width(), stripHeight), - stripColor); - QColor hairlineColor = palette().color(QPalette::Highlight); - hairlineColor.setAlpha(110); - painter.setPen(QPen(hairlineColor, 1)); - painter.drawLine(QPointF(playArea.left(), playArea.top() + stripHeight), - QPointF(playArea.right(), playArea.top() + stripHeight)); - painter.drawLine(QPointF(playArea.left(), playArea.bottom() - stripHeight), - QPointF(playArea.right(), playArea.bottom() - stripHeight)); - - // Draw zone divider: stack is roughly the left portion - const double stackWidthRatio = 0.18; // Stack is about 18% of total play area - const double stackDividerX = playArea.left() + playArea.width() * stackWidthRatio; - - // Subtle semi transparent overlays to distinguish zones - // Stack zone overlay (slightly darker) - QRectF stackOverlay(playArea.left(), playArea.top(), playArea.width() * stackWidthRatio, playArea.height()); - painter.fillRect(stackOverlay, QColor(0, 0, 0, 40)); - - // Table zone overlay (very subtle) - QRectF tableOverlay(stackDividerX, playArea.top(), playArea.width() * (1.0 - stackWidthRatio), playArea.height()); - painter.fillRect(tableOverlay, QColor(0, 0, 0, 20)); - - // Zone divider line - painter.setPen(QPen(QColor(255, 255, 255, 50), 1)); - painter.drawLine(QPointF(stackDividerX, playArea.top()), QPointF(stackDividerX, playArea.bottom())); - - // Land divider line (about 60% down the table area) - const double landDividerY = playArea.top() + playArea.height() * 0.65; - painter.setPen(QPen(QColor(255, 255, 255, 30), 1)); - painter.drawLine(QPointF(stackDividerX, landDividerY), QPointF(playArea.right(), landDividerY)); - painter.setClipping(false); - - // Border around the viewport = boundary of every plausible framing. - painter.setPen(QPen(QColor(70, 80, 95, 120), 1)); - painter.setBrush(Qt::NoBrush); - painter.drawRoundedRect(playArea.adjusted(0, 0, -1, -1), 3, 3); - - // Visible keyboard focus per the focus cursor contract, Tab must show - // where the keys land - if (hasFocus()) { - QPen focusPen(palette().color(QPalette::Highlight), 2); - painter.setPen(focusPen); - painter.drawRoundedRect(playArea.adjusted(-1, -1, 1, 1), 3, 3); - } - - paintArtAttribution(painter, playArea, attributionText, Qt::AlignRight | Qt::AlignBottom, 0.8); - - // Zoom readout so the gesture has a visible, stable counterpart. - QColor ink = palette().color(QPalette::WindowText); - ink.setAlpha(160); - painter.setPen(ink); - painter.drawText(QPointF(playArea.left() + 8, playArea.bottom() - 8), - tr("Zoom %1×").arg(QString::number(params.zoom, 'f', 2))); -} - -void PlaymatPreviewWidget::mousePressEvent(QMouseEvent *event) -{ - if (event->button() != Qt::LeftButton || sourcePixmap.isNull() || samplingWindowSide() <= 0.0) { - QWidget::mousePressEvent(event); - return; - } - lastDragPos = event->pos(); - setCursor(Qt::ClosedHandCursor); - event->accept(); -} - -void PlaymatPreviewWidget::mouseMoveEvent(QMouseEvent *event) -{ - if (!(event->buttons() & Qt::LeftButton) || sourcePixmap.isNull()) { - QWidget::mouseMoveEvent(event); - return; - } - - const QPointF delta = QPointF(event->pos() - lastDragPos); - lastDragPos = event->pos(); - - const qreal scale = widgetToSourceScale(); - // Vertical travel of the SAMPLING window: verticalOffset moves its top - // edge by exactly this much per unit, identical to the render path. - // Windows taller than the art (square/landscape sources zoomed out) - // leave no travel, vertical drags are then boundary no ops. - const qreal travel = static_cast(sourcePixmap.height()) - samplingWindowSide(); - if (scale <= 0.0) { - event->accept(); - return; - } - - // Dragging moves the ART with the cursor, so the viewing window slides the - // other way. Horizontal panning rebalances the margins (their sum, hence - // the window width, stays constant), vertical panning moves the window's - // top edge within its available travel. - const qreal sourceW = sourcePixmap.width(); - const qreal dMargin = -(delta.x() / scale) / sourceW; - const qreal dOffset = travel > 0.5 ? -(delta.y() / scale) / travel : 0.0; - - applyCropDelta(dMargin, -dMargin, dOffset, 1.0); - event->accept(); -} - -void PlaymatPreviewWidget::mouseReleaseEvent(QMouseEvent *event) -{ - if (event->button() == Qt::LeftButton) { - setCursor(sourcePixmap.isNull() ? Qt::ArrowCursor : Qt::OpenHandCursor); - event->accept(); - return; - } - QWidget::mouseReleaseEvent(event); -} - -void PlaymatPreviewWidget::wheelEvent(QWheelEvent *event) -{ - if (sourcePixmap.isNull() || samplingWindowSide() <= 0.0) { - QWidget::wheelEvent(event); - return; - } - const qreal notches = static_cast(event->angleDelta().y()) / 120.0; - if (notches == 0.0) { - event->accept(); - return; - } - applyCropDelta(0.0, 0.0, 0.0, std::pow(WHEEL_ZOOM_BASE, notches)); - event->accept(); -} - -void PlaymatPreviewWidget::keyPressEvent(QKeyEvent *event) -{ - if (sourcePixmap.isNull()) { - QWidget::keyPressEvent(event); - return; - } - - switch (event->key()) { - case Qt::Key_Escape: - if (sameCrop(params, paramsAtFocusIn)) { - // Nothing to undo on this surface, let the event reach the - // dialog so Esc keeps its close meaning there - QWidget::keyPressEvent(event); - return; - } - restoreSnapshot(); - break; - case Qt::Key_Backspace: - restoreSnapshot(); - break; - case Qt::Key_Left: - applyCropDelta(-KEY_PAN_MARGIN_STEP, KEY_PAN_MARGIN_STEP, 0.0, 1.0); - break; - case Qt::Key_Right: - applyCropDelta(KEY_PAN_MARGIN_STEP, -KEY_PAN_MARGIN_STEP, 0.0, 1.0); - break; - case Qt::Key_Up: - applyCropDelta(0.0, 0.0, -KEY_PAN_OFFSET_STEP, 1.0); - break; - case Qt::Key_Down: - applyCropDelta(0.0, 0.0, KEY_PAN_OFFSET_STEP, 1.0); - break; - case Qt::Key_Plus: - case Qt::Key_Equal: - applyCropDelta(0.0, 0.0, 0.0, KEY_ZOOM_STEP); - break; - case Qt::Key_Minus: - applyCropDelta(0.0, 0.0, 0.0, 1.0 / KEY_ZOOM_STEP); - break; - default: - QWidget::keyPressEvent(event); - return; - } - event->accept(); -} - -void PlaymatPreviewWidget::focusInEvent(QFocusEvent *event) -{ - // Snapshot for the Esc or Backspace reset, restoring whatever the user - // had when the surface took focus - paramsAtFocusIn = params; - QWidget::focusInEvent(event); -} diff --git a/cockatrice/src/interface/widgets/playmat/playmat_preview_widget.h b/cockatrice/src/interface/widgets/playmat/playmat_preview_widget.h deleted file mode 100644 index 88b9ec41d..000000000 --- a/cockatrice/src/interface/widgets/playmat/playmat_preview_widget.h +++ /dev/null @@ -1,61 +0,0 @@ -#ifndef COCKATRICE_PLAYMAT_PREVIEW_WIDGET_H -#define COCKATRICE_PLAYMAT_PREVIEW_WIDGET_H - -#include -#include -#include -#include - -/** - * @brief Interactive crop surface showing how a playmat card art will appear - * across the combined table + stack play area. - * - * Renders a fixed frame shaped like a fresh board's stack+table area (the - * most generous framing the game produces): it shows the tallest slice of - * the square crop in normal play, with dimmed strips marking where a wider, - * developed table crops further, exactly the game's own render pipeline. - * The widget doubles as the editor's primary crop control: dragging pans the - * art behind the frame, the wheel zooms, and arrow keys nudge, mirroring - * the stored parameters (margins pan horizontally, verticalOffset - * vertically, zoom scales) so no separate numeric controls are needed. - */ -class PlaymatPreviewWidget : public QWidget -{ - Q_OBJECT - -public: - explicit PlaymatPreviewWidget(QWidget *parent = nullptr); - - void setPixmap(const QPixmap &pixmap); - void setParams(const PlaymatParams ¶ms); - void setAttribution(const QString &attribution); - -signals: - /** @brief Emitted whenever direct manipulation (drag, wheel, keys) changes the crop parameters. */ - void paramsEdited(const PlaymatParams ¶ms); - -protected: - void paintEvent(QPaintEvent *event) override; - void mousePressEvent(QMouseEvent *event) override; - void mouseMoveEvent(QMouseEvent *event) override; - void mouseReleaseEvent(QMouseEvent *event) override; - void wheelEvent(QWheelEvent *event) override; - void keyPressEvent(QKeyEvent *event) override; - void focusInEvent(QFocusEvent *event) override; - -private: - QRectF activePlayArea() const; ///< destination rect used for rendering AND gesture math - qreal samplingWindowSide() const; ///< clamped square window side, shared with the render path - qreal widgetToSourceScale() const; - void applyCropDelta(qreal dMarginL, qreal dMarginR, qreal dOffset, qreal zoomFactor); - bool sameCrop(const PlaymatParams &a, const PlaymatParams &b) const; - void restoreSnapshot(); - - QPixmap sourcePixmap; - PlaymatParams params; - PlaymatParams paramsAtFocusIn; ///< crop as of the latest focus gain, restored by Esc or Backspace - QString attributionText; - QPoint lastDragPos; ///< widget space position of the previous mouse move while panning -}; - -#endif // COCKATRICE_PLAYMAT_PREVIEW_WIDGET_H diff --git a/cockatrice/src/interface/widgets/playmat/playmat_settings_dialog.cpp b/cockatrice/src/interface/widgets/playmat/playmat_settings_dialog.cpp deleted file mode 100644 index 9459c5ea9..000000000 --- a/cockatrice/src/interface/widgets/playmat/playmat_settings_dialog.cpp +++ /dev/null @@ -1,325 +0,0 @@ -#include "playmat_settings_dialog.h" - -#include "../../card_picture_loader/card_picture_loader.h" -#include "../cards/art_crop_attribution.h" -#include "../cards/card_art_utils.h" -#include "../utility/completer_utils.h" -#include "card_database_display_model.h" -#include "card_database_model.h" -#include "playmat_preview_widget.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -PlaymatSettingsDialog::PlaymatSettingsDialog(const CardRef &initialCard, - const PlaymatParams &initialParams, - QWidget *parent) - : QDialog(parent), currentCard(initialCard), currentParams(initialParams) -{ - setMinimumWidth(500); - setupUi(); - - // Seed UI from initial values - if (!initialCard.name.isEmpty()) { - searchBar->setText(initialCard.name); - onCardNameChanged(initialCard.name); - - // onCardNameChanged leaves the printing combo on the first printing in - // the database, which would silently change the deck's stored playmat - // card on accept. Restore the stored printing when it resolves locally. - const int storedPrintingIndex = providerComboBox->findData(initialCard.providerId); - if (storedPrintingIndex != -1) { - providerComboBox->setCurrentIndex(storedPrintingIndex); - } else { - // Stored printing not in the local database: keep it rather than - // silently substituting the first printing. - currentCard.providerId = initialCard.providerId; - reloadPreview(); - } - } - - retranslateUi(); -} - -CardRef PlaymatSettingsDialog::card() const -{ - return currentCard; -} - -PlaymatParams PlaymatSettingsDialog::params() const -{ - return currentParams; -} - -QDoubleSpinBox *PlaymatSettingsDialog::makeSpinBox(double min, double max, double value, double step) -{ - auto *spin = new QDoubleSpinBox; - spin->setRange(min, max); - spin->setSingleStep(step); - spin->setDecimals(3); - spin->setValue(value); - return spin; -} - -void PlaymatSettingsDialog::initializeSearchBar() -{ - searchBar = new QLineEdit; - - cardDatabaseModel = new CardDatabaseModel(CardDatabaseManager::getInstance(), false, this); - cardDatabaseDisplayModel = new CardDatabaseDisplayModel(this); - cardDatabaseDisplayModel->setSourceModel(cardDatabaseModel); - - const CardCompleterSetup cardSetup = createCardCompleter(cardDatabaseDisplayModel, this, 15); - searchModel = cardSetup.searchModel; - proxyModel = cardSetup.proxyModel; - completer = cardSetup.completer; - searchBar->setCompleter(completer); - - connectCardCompleterSearch(searchBar, cardSetup); - - connect(completer, static_cast(&QCompleter::activated), this, - [this](const QString &completion) { - if (searchBar->text() != completion) { - searchBar->setText(completion); - searchBar->setCursorPosition(searchBar->text().length()); - } - onCardNameChanged(completion); - }); - - connect(searchBar, &QLineEdit::returnPressed, this, [this]() { onCardNameChanged(searchBar->text()); }); -} - -void PlaymatSettingsDialog::setupUi() -{ - initializeSearchBar(); - - providerComboBox = new QComboBox; - connect(providerComboBox, &QComboBox::currentIndexChanged, this, [this]() { - currentCard.providerId = providerComboBox->currentData().toString(); - reloadPreview(); - }); - - auto *form = new QFormLayout; - controlsForm = form; - cardNameLabel = new QLabel; - printingLabel = new QLabel; - form->addRow(cardNameLabel, searchBar); - form->addRow(printingLabel, providerComboBox); - - // Numerical editors expose the raw PlaymatParams for precise input. They - // share the same form as the rows above so every field lines up on one - // label column. They stay hidden until requested since the crop surface - // is the primary control. - marginLSpin = makeSpinBox(0.0, 0.95, currentParams.marginPctL, 0.01); - marginRSpin = makeSpinBox(0.0, 0.95, currentParams.marginPctR, 0.01); - verticalOffsetSpin = makeSpinBox(0.0, 1.0, currentParams.verticalOffset, 0.01); - zoomSpin = makeSpinBox(0.1, 4.0, currentParams.zoom, 0.05); - - leftMarginLabel = new QLabel; - rightMarginLabel = new QLabel; - verticalOffsetLabel = new QLabel; - zoomLabel = new QLabel; - - showNumericEditorsCheck = new QCheckBox; - - form->addRow(showNumericEditorsCheck); - form->addRow(leftMarginLabel, marginLSpin); - form->addRow(rightMarginLabel, marginRSpin); - form->addRow(verticalOffsetLabel, verticalOffsetSpin); - form->addRow(zoomLabel, zoomSpin); - - controlsGroup = new QGroupBox; - controlsGroup->setLayout(form); - - preview = new PlaymatPreviewWidget; - preview->setParams(currentParams); - - auto *previewLayout = new QVBoxLayout; - previewLayout->addWidget(preview); - previewCaptionLabel = new QLabel; - previewCaptionLabel->setAlignment(Qt::AlignCenter); - previewCaptionLabel->setWordWrap(true); - previewLayout->addWidget(previewCaptionLabel); - previewGroup = new QGroupBox; - previewGroup->setLayout(previewLayout); - - auto *buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); - removeButton = new QPushButton; - buttons->addButton(removeButton, QDialogButtonBox::ResetRole); - - connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept); - connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject); - connect(removeButton, &QPushButton::clicked, this, [this]() { - currentCard = CardRef{}; // empty signals removal - accept(); - }); - - // The crop surface is the primary control: dragging pans, wheel/keys zoom, - // editing exactly the same stored parameters the numeric fields do. - connect(preview, &PlaymatPreviewWidget::paramsEdited, this, [this](const PlaymatParams &edited) { - currentParams = edited; - - QSignalBlocker blockMarginL(marginLSpin); - QSignalBlocker blockMarginR(marginRSpin); - QSignalBlocker blockOffset(verticalOffsetSpin); - QSignalBlocker blockZoom(zoomSpin); - marginLSpin->setValue(edited.marginPctL); - marginRSpin->setValue(edited.marginPctR); - verticalOffsetSpin->setValue(edited.verticalOffset); - zoomSpin->setValue(edited.zoom); - }); - - connect(showNumericEditorsCheck, &QCheckBox::toggled, this, &PlaymatSettingsDialog::setNumericEditorsVisible); - setNumericEditorsVisible(false); - - connect(marginLSpin, &QDoubleSpinBox::valueChanged, this, &PlaymatSettingsDialog::onParamChanged); - connect(marginRSpin, &QDoubleSpinBox::valueChanged, this, &PlaymatSettingsDialog::onParamChanged); - connect(verticalOffsetSpin, &QDoubleSpinBox::valueChanged, this, &PlaymatSettingsDialog::onParamChanged); - connect(zoomSpin, &QDoubleSpinBox::valueChanged, this, &PlaymatSettingsDialog::onParamChanged); - - // The crop surface leads visually, card selection supports it below. - auto *root = new QVBoxLayout; - root->addWidget(previewGroup); - root->addWidget(controlsGroup); - root->addWidget(buttons); - setLayout(root); -} - -void PlaymatSettingsDialog::populateProviderCombo(const QString &cardName) -{ - providerComboBox->clear(); - - auto card = CardDatabaseManager::query()->getCard({cardName}); - - const auto &sets = card.getInfo().getSets(); - - for (const auto &printings : sets) { - for (const auto &p : printings) { - QString setName = p.getSet()->getLongName(); - QString collector = p.getProperty("num"); - QString uuid = p.getUuid(); - - QString label = setName; - if (!collector.isEmpty()) { - label += " #" + collector; - } - - providerComboBox->addItem(label, uuid); - } - } -} - -void PlaymatSettingsDialog::onCardNameChanged(const QString &name) -{ - if (name.isEmpty()) { - currentPixmap = QPixmap(); - preview->setPixmap(currentPixmap); - return; - } - - const ExactCard card = CardDatabaseManager::query()->getCard({name}); - if (!card) { - currentPixmap = QPixmap(); - preview->setPixmap(currentPixmap); - providerComboBox->clear(); - return; - } - - currentCard.name = name; - - populateProviderCombo(name); - - if (providerComboBox->count() == 0) { - currentPixmap = QPixmap(); - preview->setPixmap(currentPixmap); - currentCard.providerId.clear(); - return; - } - - currentCard.providerId = providerComboBox->currentData().toString(); - reloadPreview(); -} - -void PlaymatSettingsDialog::reloadPreview() -{ - if (currentCard.name.isEmpty()) { - return; - } - - ExactCard card = CardDatabaseManager::query()->getCard({currentCard.name, currentCard.providerId}); - if (!card) { - return; - } - - disconnect(pixmapUpdatedConnection); - - QPixmap fullRes; - CardPictureLoader::getPixmap(fullRes, card, QSize(745, 1040)); - - if (fullRes.isNull()) { - CardInfo *cardInfo = card.getCardPtr().data(); - if (cardInfo) { - pixmapUpdatedConnection = connect(cardInfo, &CardInfo::pixmapUpdated, this, [this]() { reloadPreview(); }); - } - return; - } - - currentPixmap = CardArtUtils::rotateSidewaysLayoutArt(fullRes, card); - preview->setPixmap(currentPixmap); - preview->setParams(currentParams); - preview->setAttribution(buildArtAttribution(card)); -} - -void PlaymatSettingsDialog::onParamChanged() -{ - currentParams.marginPctL = marginLSpin->value(); - currentParams.marginPctR = marginRSpin->value(); - currentParams.verticalOffset = verticalOffsetSpin->value(); - currentParams.zoom = zoomSpin->value(); - preview->setParams(currentParams); -} - -void PlaymatSettingsDialog::setNumericEditorsVisible(bool visible) -{ - controlsForm->setRowVisible(leftMarginLabel, visible); - controlsForm->setRowVisible(rightMarginLabel, visible); - controlsForm->setRowVisible(verticalOffsetLabel, visible); - controlsForm->setRowVisible(zoomLabel, visible); - - // A QDialog never resizes itself when its content requirements change, - // so revealing the editors would squeeze the crop group until the info - // caption ran into the preview. Re-fit the dialog to the new size hint. - adjustSize(); -} - -void PlaymatSettingsDialog::retranslateUi() -{ - setWindowTitle(tr("Playmat Settings")); - searchBar->setPlaceholderText(tr("Type a card name...")); - cardNameLabel->setText(tr("Card name:")); - printingLabel->setText(tr("Printing:")); - showNumericEditorsCheck->setText(tr("Show numerical editors")); - leftMarginLabel->setText(tr("Left margin (%):")); - rightMarginLabel->setText(tr("Right margin (%):")); - verticalOffsetLabel->setText(tr("Vertical offset:")); - zoomLabel->setText(tr("Zoom:")); - controlsGroup->setTitle(tr("Card")); - previewGroup->setTitle(tr("Crop")); - previewCaptionLabel->setText( - tr("Drag to pan, scroll to zoom, arrow keys nudge, plus and minus zoom, Backspace or Esc restores. " - "Dimmed strips mark where a wider table crops further.")); - removeButton->setText(tr("Remove Playmat")); -} diff --git a/cockatrice/src/interface/widgets/playmat/playmat_settings_dialog.h b/cockatrice/src/interface/widgets/playmat/playmat_settings_dialog.h deleted file mode 100644 index 9ef306a5f..000000000 --- a/cockatrice/src/interface/widgets/playmat/playmat_settings_dialog.h +++ /dev/null @@ -1,94 +0,0 @@ -#ifndef COCKATRICE_PLAYMAT_SETTINGS_DIALOG_H -#define COCKATRICE_PLAYMAT_SETTINGS_DIALOG_H - -#include -#include -#include - -class QCheckBox; -class QComboBox; -class QCompleter; -class QDoubleSpinBox; -class QFormLayout; -class QGroupBox; -class QLabel; -class QLineEdit; -class QPushButton; -class QWidget; -class CardDatabaseModel; -class CardDatabaseDisplayModel; -class CardSearchModel; -class CardCompleterProxyModel; -class PlaymatPreviewWidget; - -/** - * @brief Dialog for configuring the playmat card art for a deck. - * - * The crop surface is the primary control: drag to pan the visible art, - * scroll (or +/- keys) to zoom, arrow keys to nudge. Card name and printing - * are selected below. A checkbox reveals optional numerical editors for the - * raw PlaymatParams. These controls edit the same stored PlaymatParams that - * ship in deck files and player properties. - */ -class PlaymatSettingsDialog : public QDialog -{ - Q_OBJECT - -public: - explicit PlaymatSettingsDialog(const CardRef &initialCard = {}, - const PlaymatParams &initialParams = {}, - QWidget *parent = nullptr); - - CardRef card() const; - PlaymatParams params() const; - -private slots: - void onCardNameChanged(const QString &name); - void onParamChanged(); - void reloadPreview(); - -private: - void setupUi(); - void populateProviderCombo(const QString &cardName); - void initializeSearchBar(); - void retranslateUi(); - void setNumericEditorsVisible(bool visible); - QDoubleSpinBox *makeSpinBox(double min, double max, double value, double step); - - QLineEdit *searchBar; - QCompleter *completer; - CardDatabaseModel *cardDatabaseModel; - CardDatabaseDisplayModel *cardDatabaseDisplayModel; - CardSearchModel *searchModel; - CardCompleterProxyModel *proxyModel; - - QComboBox *providerComboBox; - - QMetaObject::Connection pixmapUpdatedConnection; - - QLabel *cardNameLabel; - QLabel *printingLabel; - QLabel *previewCaptionLabel; - QCheckBox *showNumericEditorsCheck; - QFormLayout *controlsForm; - QLabel *leftMarginLabel; - QLabel *rightMarginLabel; - QLabel *verticalOffsetLabel; - QLabel *zoomLabel; - QGroupBox *controlsGroup; - QGroupBox *previewGroup; - QPushButton *removeButton; - - QDoubleSpinBox *marginLSpin; - QDoubleSpinBox *marginRSpin; - QDoubleSpinBox *verticalOffsetSpin; - QDoubleSpinBox *zoomSpin; - - PlaymatPreviewWidget *preview; - - QPixmap currentPixmap; - CardRef currentCard; - PlaymatParams currentParams; -}; - -#endif // COCKATRICE_PLAYMAT_SETTINGS_DIALOG_H diff --git a/cockatrice/src/interface/widgets/playmat/playmat_utils.h b/cockatrice/src/interface/widgets/playmat/playmat_utils.h deleted file mode 100644 index 0691a9637..000000000 --- a/cockatrice/src/interface/widgets/playmat/playmat_utils.h +++ /dev/null @@ -1,140 +0,0 @@ -#ifndef COCKATRICE_PLAYMAT_UTILS_H -#define COCKATRICE_PLAYMAT_UTILS_H - -#include -#include -#include -#include - -namespace PlaymatUtils -{ - -/** @brief Upper bound for zooming into the playmat art. */ -constexpr qreal MAX_ZOOM = 4.0; - -/** - * @brief Width of the outer viewing window: full card width trimmed by the - * horizontal margins. Guarded against margins summing to >= 1. - */ -inline qreal playmatVisibleWidth(const QSize &fullCardSize, const PlaymatParams ¶ms) -{ - const qreal srcW = fullCardSize.width(); - const qreal marginL = params.marginPctL * srcW; - const qreal marginR = params.marginPctR * srcW; - return qMax(0.0, srcW - marginL - marginR); -} - -/** - * @brief Zoom clamped to the range where every step renders differently. - * - * The square sampling window is visibleWidth / zoom, zooming out past - * visibleWidth / min(card width, height) would sample beyond the card itself, - * which both looks broken and makes whole ranges of the parameter dead. The - * floor is therefore derived from the actual image instead of a static value, - * and is shared verbatim by the render path and the editor's gesture math so - * the two can never disagree. - */ -inline qreal playmatClampedZoom(const QSize &fullCardSize, const PlaymatParams ¶ms) -{ - const qreal minDim = qMin(fullCardSize.width(), fullCardSize.height()); - const qreal visibleW = playmatVisibleWidth(fullCardSize, params); - const qreal zoomOutFloor = (minDim > 0.0 && visibleW > 0.0) ? visibleW / minDim : 1.0; - // The floor deliberately bypasses MAX_ZOOM: when the art is much wider - // than tall, keeping the square window inside it requires more than 4x - // zoom-out, and honoring that larger floor keeps side within - // min(card width, height). Zooming IN is still capped at MAX_ZOOM. - return qMin(MAX_ZOOM, qMax(params.zoom, zoomOutFloor)); -} - -/** - * @brief Side of the square sampling window actually rendered for these - * parameters. Never exceeds either card dimension, so the source rect - * always lies within the image (vertical travel remains for panning - * whenever the art is taller than it is wide). - */ -inline qreal playmatWindowSide(const QSize &fullCardSize, const PlaymatParams ¶ms) -{ - const qreal visibleW = playmatVisibleWidth(fullCardSize, params); - if (visibleW <= 0.0) { - return 0.0; - } - return visibleW / playmatClampedZoom(fullCardSize, params); -} - -/** - * @brief Computes the source region of the full resolution card image to use as a playmat. - * - * Parameters are relative to the full card image. horizontal margins trim the - * card borders (shifting them pans the window), verticalOffset places the top - * edge of the sampling window within its available travel, and zoom scales - * into the trimmed span. The result always lies within the card image bounds. - * - * @param fullCardSize Size of the full card image. - * @param params Positioning parameters. - * @return Source rectangle in full card image pixel coordinates. - */ -inline QRectF computeArtSourceRect(const QSize &fullCardSize, const PlaymatParams ¶ms) -{ - const qreal srcW = fullCardSize.width(); - const qreal srcH = fullCardSize.height(); - - // Square sampling window, keeps art unskewed, never exceeds the card on - // either axis thanks to the zoom floor in playmatWindowSide(). - const qreal side = playmatWindowSide(fullCardSize, params); - - // verticalOffset places the TOP edge of the sampling window itself within - // its travel, so the full [0, 1] parameter range is live at every zoom and - // the window can always reach the very top (0.0) and bottom (1.0) of the - // art. - const qreal offset = qBound(0.0, params.verticalOffset, 1.0); - const qreal y = offset * qMax(0.0, srcH - side); - - // Horizontally the sampling window sits centered inside the trimmed span - // (margins pan it), zooming out can make it wider than that span, so it - // is then kept within the image, an edge stop, never an invalid rect. - const qreal outerW = playmatVisibleWidth(fullCardSize, params); - const qreal x = qBound(0.0, params.marginPctL * srcW + (outerW - side) / 2.0, qMax(0.0, srcW - side)); - - return QRectF(x, y, side, side); -} - -/** - * @brief Returns the destination rectangle that fits a source of the given aspect - * ratio into dstArea using "cover" semantics (no distortion, overflows cropped). - * - * @param dstArea Area to fill. - * @param srcSize Size of the source, only its aspect ratio matters. - * @return Destination rectangle centered in dstArea. - */ -inline QRectF coverFitRect(const QRectF &dstArea, const QSizeF &srcSize) -{ - const qreal srcAspect = srcSize.width() / srcSize.height(); - const qreal dstAspect = dstArea.width() / dstArea.height(); - - if (srcAspect > dstAspect) { - const qreal dstW = dstArea.height() * srcAspect; - return QRectF(dstArea.left() + (dstArea.width() - dstW) / 2.0, dstArea.top(), dstW, dstArea.height()); - } - - const qreal dstH = dstArea.width() / srcAspect; - return QRectF(dstArea.left(), dstArea.top() + (dstArea.height() - dstH) / 2.0, dstArea.width(), dstH); -} - -/** - * @brief Fits a rectangle of the given aspect ratio into dstArea, centered, - * touching the constraining dimension ("aspect fit" of the FRAME - * itself, not of a source image). - */ -inline QRectF aspectFitRect(const QRectF &dstArea, qreal aspect) -{ - if (aspect <= 0.0) { - return dstArea; - } - qreal w = qMin(dstArea.width(), dstArea.height() * aspect); - qreal h = w / aspect; - return QRectF(dstArea.left() + (dstArea.width() - w) / 2.0, dstArea.top() + (dstArea.height() - h) / 2.0, w, h); -} - -} // namespace PlaymatUtils - -#endif // COCKATRICE_PLAYMAT_UTILS_H diff --git a/cockatrice/src/interface/widgets/printing_selector/printing_selector_card_overlay_widget.cpp b/cockatrice/src/interface/widgets/printing_selector/printing_selector_card_overlay_widget.cpp index e8816847e..2b9201e6c 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 @@ -1,19 +1,12 @@ #include "printing_selector_card_overlay_widget.h" #include "../../../client/settings/cache_settings.h" -#include "../../card_picture_loader/card_picture_loader.h" -#include "../cards/card_info_picture_widget.h" #include "printing_selector_card_display_widget.h" -#include -#include #include #include #include -#include #include -#include -#include #include #include #include @@ -21,49 +14,6 @@ #include #include -namespace -{ -/** - * @brief Places the preview beside the highlighted action inside the given screen. - * - * Side-aware: hugs the side of the action that has room, aligned with its row, then - * clamps every edge so the preview always lands fully on-screen on first show. - */ -QPoint -previewPositionNear(const QRect &actionRect, const QSize &labelSize, const QRect &screenGeometry, int previewOffset) -{ - const bool rightFits = actionRect.right() + previewOffset + labelSize.width() <= screenGeometry.right(); - const bool leftFits = actionRect.left() - previewOffset - labelSize.width() >= screenGeometry.left(); - - int x; - if (rightFits) { - x = actionRect.right() + previewOffset; - } else if (leftFits) { - x = actionRect.left() - previewOffset - labelSize.width(); - } else { - x = actionRect.left(); - } - x = qMax(screenGeometry.left(), x); - x = qMin(screenGeometry.right() - labelSize.width() + 1, x); - - const bool belowFits = actionRect.bottom() + previewOffset + labelSize.height() <= screenGeometry.bottom(); - const bool aboveFits = actionRect.top() - previewOffset - labelSize.height() >= screenGeometry.top(); - - int y; - if (belowFits) { - y = actionRect.bottom() + previewOffset; - } else if (aboveFits) { - y = actionRect.top() - previewOffset - labelSize.height(); - } else { - y = actionRect.top(); - } - y = qMax(screenGeometry.top(), y); - y = qMin(screenGeometry.bottom() - labelSize.height() + 1, y); - - return {x, y}; -} -} // namespace - /** * @brief Constructs a PrintingSelectorCardOverlayWidget for displaying a card overlay. * @@ -99,31 +49,6 @@ PrintingSelectorCardOverlayWidget::PrintingSelectorCardOverlayWidget(QWidget *pa initializePinBadge(); - // Parent the preview to this overlay so it is destroyed with it (Qt::ToolTip keeps - // it a frameless, non-activating top-level window despite the parent). - cardOverridePreviewLabel = new QLabel(this, Qt::ToolTip); - cardOverridePreviewLabel->setWindowFlag(Qt::FramelessWindowHint); - cardOverridePreviewLabel->setAttribute(Qt::WA_ShowWithoutActivating); - cardOverridePreviewLabel->setScaledContents(true); - cardOverridePreviewLabel->hide(); - - // While the preview is visible, keep it honest: when the hovered printing's art - // resolves (all alternate printings share the root card's CardInfo), redraw it in place. - if (rootCard.getCardPtr()) { - connect(rootCard.getCardPtr().data(), &CardInfo::pixmapUpdated, this, [this] { - if (cardOverridePreviewLabel->isVisible()) { - refreshPreview(); - } - }); - } - - // Alt-Tab / app-inactive must not strand the floating preview. - connect(qApp, &QGuiApplication::applicationStateChanged, this, [this](Qt::ApplicationState state) { - if (state != Qt::ApplicationActive) { - hidePreview(); - } - }); - // Update when this overlay emits cardPreferenceChanged or when size/scale changes connect(this, &PrintingSelectorCardOverlayWidget::cardPreferenceChanged, this, &PrintingSelectorCardOverlayWidget::updatePinBadgeVisibility); @@ -259,23 +184,19 @@ void PrintingSelectorCardOverlayWidget::leaveEvent(QEvent *event) } /** - * @brief Creates and shows the card-overlay context menu. + * @brief Creates and shows a custom context menu when the right mouse button is clicked. * - * The menu includes the card art preference (Pin/Unpin Printing), the Image Overrides - * submenu (Load Custom Image, Clear Custom Image, and one entry per alternate printing with a - * live preview), and the Show Related cards submenu. + * The context menu includes an option to show related cards, which displays a submenu with actions + * for each related card. When an action is triggered, the card information is updated, and the + * printing selector is shown. * - * @param point The local position the menu should pop at. + * @param point The position of the mouse when the right-click occurred. */ void PrintingSelectorCardOverlayWidget::customMenu(QPoint point) { QMenu menu; - hidePreview(); // Clear any preview state left over from a previous menu run. - - // Submenus are owned by the stack-allocated top-level menu (addMenu() does not - // transfer ownership). - auto *preferenceMenu = new QMenu(tr("Preference"), &menu); + auto *preferenceMenu = new QMenu(tr("Preference")); menu.addMenu(preferenceMenu); const auto &preferredProviderId = @@ -297,66 +218,8 @@ void PrintingSelectorCardOverlayWidget::customMenu(QPoint point) }); } - menu.addSeparator(); - - auto *overrideMenu = new QMenu(tr("Image Overrides"), &menu); - - auto *loadCustomAction = overrideMenu->addAction(tr("Load Custom Image...")); - auto *clearOverrideAction = overrideMenu->addAction(tr("Clear Custom Image")); - - // Nothing to clear on a card that has no local override yet. - clearOverrideAction->setEnabled(CardPictureLoader::hasLocalOverrides(rootCard)); - - overrideMenu->addSeparator(); - - const auto &allSets = rootCard.getInfo().getSets(); - - for (const auto &set : allSets) { - for (const auto &printing : set) { - if (printing == rootCard.getPrinting()) { - continue; - } - - // The submenu is already scoped to this card, so the rows lead with set + - // collector; only printings with a distinct display name add their own name. - const CardSetPtr cardSet = printing.getSet(); - if (!cardSet) { - continue; - } - - QString label = tr("%1 %2").arg(cardSet->getCorrectedShortName(), printing.getProperty("num")); - - auto *action = overrideMenu->addAction(label); - - ExactCard overrideCard(rootCard.getCardPtr(), printing); - action->setData(QVariant::fromValue(overrideCard)); - - connect(action, &QAction::triggered, this, [this, overrideCard]() { - CardPictureLoader::getInstance().installPrintingOverride(rootCard, overrideCard); - QPixmapCache::clear(); - rootCard.emitPixmapUpdated(); // refresh the overlay art in place, like the other paths - }); - } - } - - connect(clearOverrideAction, &QAction::triggered, this, [this]() { - CardPictureLoader::deleteAllLocalOverrides(rootCard); - QPixmapCache::clear(); - rootCard.emitPixmapUpdated(); // force UI refresh - }); - - connect(loadCustomAction, &QAction::triggered, this, &PrintingSelectorCardOverlayWidget::loadCustomImage); - - connect(overrideMenu, &QMenu::hovered, this, &PrintingSelectorCardOverlayWidget::showPreviewForAction); - connect(overrideMenu, &QMenu::aboutToHide, this, &PrintingSelectorCardOverlayWidget::hidePreview); - connect(overrideMenu, &QMenu::triggered, this, &PrintingSelectorCardOverlayWidget::hidePreview); - - menu.addMenu(overrideMenu); - - menu.addSeparator(); - // filling out the related cards submenu - auto *relatedMenu = new QMenu(tr("Show Related cards"), &menu); + auto *relatedMenu = new QMenu(tr("Show Related cards")); menu.addMenu(relatedMenu); auto relatedCards = rootCard.getInfo().getAllRelatedCards(); if (relatedCards.isEmpty()) { @@ -371,11 +234,7 @@ void PrintingSelectorCardOverlayWidget::customMenu(QPoint point) }); } } - // The preview anchors itself to this popup's global geometry while it is open, so the - // pointer must stay valid for the whole exec() and be dropped before the stack unwinds. - previewSourceMenu = overrideMenu; menu.exec(this->mapToGlobal(point)); - previewSourceMenu = nullptr; } /** @@ -431,136 +290,3 @@ void PrintingSelectorCardOverlayWidget::initializePinBadge() pinBadge->setVisible(false); pinBadge->raise(); } - -/** - * @brief Asks for an image file and installs it as the card's custom art. - * - * Unreadable files answer with a visible warning instead of a silent no-op. - */ -void PrintingSelectorCardOverlayWidget::loadCustomImage() -{ - QString filePath = QFileDialog::getOpenFileName(this, tr("Select Card Image"), QString(), - tr("Images (*.png *.jpg *.jpeg *.webp)")); - - if (filePath.isEmpty()) { - return; - } - - QPixmap pixmap(filePath); - if (pixmap.isNull()) { - // No silent paths: a file that cannot be read answers visibly instead of a no-op. - QMessageBox::warning(this, tr("Load Custom Image"), tr("The selected file could not be read as an image.")); - return; - } - - CardPictureLoader::getInstance().saveCardImageToLocalStorage(rootCard, pixmap, true); - - QPixmapCache::clear(); - rootCard.emitPixmapUpdated(); -} - -/** - * @brief Shows the hover preview for a highlighted printing entry in the Image Overrides submenu. - * - * QMenu::hovered fires on keyboard highlight too, so the preview appears when arrows walk - * onto a printing entry, not only under the mouse. - * - * Non-printing entries (e.g., Load Custom Image, Clear Custom Image) hide the preview. - * - * @param action The action that was highlighted. - */ -void PrintingSelectorCardOverlayWidget::showPreviewForAction(QAction *action) -{ - if (!action) { - hidePreview(); - return; - } - - const QVariant data = action->data(); - - if (!data.canConvert()) { - hidePreview(); - return; - } - - const ExactCard previewCard = qvariant_cast(data); - if (previewCard.isEmpty()) { - hidePreview(); - return; - } - - hoveredOverrideCard = previewCard; - hoveredOverrideAction = action; - refreshPreview(); -} - -/** - * @brief Renders the hover preview for the currently highlighted printing. - * - * The preview shows the loading placeholder while the art is pending and swaps in the real - * art when it resolves. The label is positioned against its already-resized geometry so the - * first-ever show at the screen's edges stays fully on-screen. - */ -void PrintingSelectorCardOverlayWidget::refreshPreview() -{ - if (hoveredOverrideCard.isEmpty()) { - hidePreview(); - return; - } - - constexpr QSize previewSize(240, 336); - constexpr int previewOffset = 20; - - QPixmap pixmap; - CardPictureLoader::getPixmap(pixmap, hoveredOverrideCard, previewSize); - - if (pixmap.isNull()) { - // Keep the preview honest while loading: show the loading placeholder instead of a void. - // Fetch at the logical size and let the label scale it, so the placeholder matches the - // real art's footprint rather than doubling on HiDPI displays. - CardPictureLoader::getCardBackLoadingInProgressPixmap(pixmap, previewSize); - } - - cardOverridePreviewLabel->setPixmap(pixmap); - // QPixmap::size() is physical pixels; the label layout must use the device-independent size - // so the preview keeps a constant footprint across DPI settings (QScreen geometry is logical). - const QSize labelSize = pixmap.deviceIndependentSize().toSize(); - cardOverridePreviewLabel->resize(labelSize); - - // Anchor the preview to the walked submenu popup rather than QCursor::pos(), which is idle - // under keyboard-only operation: a keyboard-highlighted row must preview at the same place as - // a hovered one. The mouse path is unchanged in effect — the popup sits under the cursor, so - // the preview stays beside the row in both modalities. - const QMenu *popup = previewSourceMenu; - if (!popup || !popup->isVisible() || !hoveredOverrideAction) { - hidePreview(); - return; - } - - const QRect popupGeometry = popup->geometry(); - const QRect actionRectLocal = popup->actionGeometry(hoveredOverrideAction); - const QRect actionRect(popupGeometry.topLeft() + actionRectLocal.topLeft(), actionRectLocal.size()); - - QScreen *screen = QGuiApplication::screenAt(popupGeometry.center()); - if (!screen) { - hidePreview(); - return; - } - const QRect &screenGeometry = screen->geometry(); - - cardOverridePreviewLabel->move(previewPositionNear(actionRect, labelSize, screenGeometry, previewOffset)); - cardOverridePreviewLabel->show(); -} - -/** - * @brief Hides the hover preview and forgets the currently highlighted printing. - */ -void PrintingSelectorCardOverlayWidget::hidePreview() -{ - hoveredOverrideCard = ExactCard(); - hoveredOverrideAction = nullptr; - - if (cardOverridePreviewLabel) { - cardOverridePreviewLabel->hide(); - } -} diff --git a/cockatrice/src/interface/widgets/printing_selector/printing_selector_card_overlay_widget.h b/cockatrice/src/interface/widgets/printing_selector/printing_selector_card_overlay_widget.h index fbcfa9230..228393c9c 100644 --- a/cockatrice/src/interface/widgets/printing_selector/printing_selector_card_overlay_widget.h +++ b/cockatrice/src/interface/widgets/printing_selector/printing_selector_card_overlay_widget.h @@ -13,9 +13,6 @@ #include -class QAction; -class QMenu; - class PrintingSelectorCardOverlayWidget : public QWidget { Q_OBJECT @@ -46,19 +43,11 @@ private slots: private: void initializePinBadge(); - void loadCustomImage(); - void showPreviewForAction(QAction *action); - void refreshPreview(); - void hidePreview(); CardInfoPictureWidget *cardInfoPicture; AllZonesCardAmountWidget *allZonesCardAmountWidget; QLabel *pinBadge = nullptr; AbstractTabDeckEditor *deckEditor; ExactCard rootCard; - QLabel *cardOverridePreviewLabel = nullptr; - ExactCard hoveredOverrideCard; - QMenu *previewSourceMenu = nullptr; - QAction *hoveredOverrideAction = nullptr; }; #endif // PRINTING_SELECTOR_CARD_OVERLAY_WIDGET_H 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 881c3e3ac..9c433ab5a 100644 --- a/cockatrice/src/interface/widgets/quick_settings/settings_button_widget.cpp +++ b/cockatrice/src/interface/widgets/quick_settings/settings_button_widget.cpp @@ -1,7 +1,5 @@ #include "settings_button_widget.h" -#include "../../pixel_map_generator.h" - #include #include #include @@ -10,7 +8,7 @@ SettingsButtonWidget::SettingsButtonWidget(QWidget *parent) : QWidget(parent), button(new QToolButton(this)), popup(new SettingsPopupWidget(nullptr)) { - button->setIcon(themePixmap(QStringLiteral("icons/cogwheel"))); + button->setIcon(QPixmap("theme:icons/cogwheel")); button->setCheckable(true); button->setFixedSize(32, 32); connect(button, &QToolButton::clicked, this, &SettingsButtonWidget::togglePopup); diff --git a/cockatrice/src/interface/widgets/replay/replay_manager.cpp b/cockatrice/src/interface/widgets/replay/replay_manager.cpp index c51b96b6c..c6e7ff1bb 100644 --- a/cockatrice/src/interface/widgets/replay/replay_manager.cpp +++ b/cockatrice/src/interface/widgets/replay/replay_manager.cpp @@ -3,11 +3,9 @@ #include "../../../client/settings/cache_settings.h" #include -#include #include static constexpr int TIMER_INTERVAL_MS = 200; -static constexpr int EMPTY_SECTION_MARGIN_MS = 500; static QList createReplayTimeline(const GameReplay *replay) { @@ -121,10 +119,6 @@ void ReplayManager::replayTimerTimeout() processNewEvents(NORMAL_PLAYBACK); timeChanged(currentVisualTime); - - if (skipEmptySections) { - handleSkipEmptySection(); - } } /** @brief Processes all unprocessed events up to the current time. */ @@ -142,10 +136,8 @@ void ReplayManager::processNewEvents(PlaybackMode playbackMode) } // backwards skip => always skip tap animation - // backwards skip => always skip damage animation (battlefield shimmer / life counter flash) if (playbackMode == BACKWARD_SKIP) { options |= SKIP_TAP_ANIMATION; - options |= SKIP_DAMAGE_ANIMATION; } emit eventReplayed(replay->event_list(currentEvent), options); @@ -157,51 +149,6 @@ void ReplayManager::processNewEvents(PlaybackMode playbackMode) } } -static bool hasMeaningfulEvent(const GameEventContainer &cont) -{ - const int eventListSize = cont.event_list_size(); - for (int i = 0; i < eventListSize; ++i) { - const GameEvent &event = cont.event_list(i); - const auto eventType = static_cast(getPbExtension(event)); - - if (eventType != GameEvent::PLAYER_PROPERTIES_CHANGED) { - return true; - } - } - - return false; -} - -void ReplayManager::handleSkipEmptySection() -{ - if (currentEvent == replayTimeline.size()) { - return; - } - - // find most recent meaningful event - int prevEvent = std::max(0, currentEvent - 1); - for (; prevEvent > 0 && !hasMeaningfulEvent(replay->event_list(prevEvent)); --prevEvent) { - } - - int prevEventTime = replayTimeline.value(prevEvent); - if (currentVisualTime - prevEventTime <= EMPTY_SECTION_MARGIN_MS) { - return; - } - - // find next earliest meaningful event - int nextEvent = currentEvent; - for (; nextEvent < replayTimeline.size() - 1 && !hasMeaningfulEvent(replay->event_list(nextEvent)); ++nextEvent) { - } - - int nextEventTime = replayTimeline.value(nextEvent); - if (nextEventTime - currentVisualTime <= EMPTY_SECTION_MARGIN_MS) { - return; - } - - // skip forward if we're not within margin of either event - skipToTime(nextEventTime - EMPTY_SECTION_MARGIN_MS, false); -} - void ReplayManager::setTimeScaleFactor(qreal _timeScaleFactor) { timeScaleFactor = _timeScaleFactor; @@ -209,11 +156,6 @@ void ReplayManager::setTimeScaleFactor(qreal _timeScaleFactor) replayTimer->setInterval(interval); } -void ReplayManager::setSkipEmptySections(bool value) -{ - skipEmptySections = value; -} - void ReplayManager::startReplay() { replayTimer->start(); diff --git a/cockatrice/src/interface/widgets/replay/replay_manager.h b/cockatrice/src/interface/widgets/replay/replay_manager.h index 81e66824d..16d3591ba 100644 --- a/cockatrice/src/interface/widgets/replay/replay_manager.h +++ b/cockatrice/src/interface/widgets/replay/replay_manager.h @@ -31,7 +31,6 @@ class ReplayManager : public QObject QTimer *rewindBufferingTimer; qreal timeScaleFactor = 1.0; - bool skipEmptySections = false; int currentVisualTime = 0; ///< time currently displayed by the timeline int currentProcessedTime = 0; ///< time that events are currently processed up to. Could differ from visual time due @@ -42,7 +41,6 @@ class ReplayManager : public QObject void handleBackwardsSkip(bool doRewindBuffering); void processRewind(); void processNewEvents(PlaybackMode playbackMode); - void handleSkipEmptySection(); private slots: void replayTimerTimeout(); @@ -65,7 +63,6 @@ public: } void setTimeScaleFactor(qreal _timeScaleFactor); - void setSkipEmptySections(bool value); public slots: void startReplay(); diff --git a/cockatrice/src/interface/widgets/replay/replay_quick_settings_widget.cpp b/cockatrice/src/interface/widgets/replay/replay_quick_settings_widget.cpp index 5d58705d2..08113d2cd 100644 --- a/cockatrice/src/interface/widgets/replay/replay_quick_settings_widget.cpp +++ b/cockatrice/src/interface/widgets/replay/replay_quick_settings_widget.cpp @@ -18,17 +18,12 @@ ReplayQuickSettingsWidget::ReplayQuickSettingsWidget(QWidget *parent) : Settings connect(&fastForwardSpeedBox, qOverload(&QDoubleSpinBox::valueChanged), this, &ReplayQuickSettingsWidget::actUpdateFastForwardSpeed); - skipEmptyCheckBox.setChecked(SettingsCache::instance().userInterface().getSkipEmptySections()); - connect(&skipEmptyCheckBox, &QCheckBox::QT_STATE_CHANGED, this, - &ReplayQuickSettingsWidget::actUpdateSkipEmptySections); - // putting it all together auto *widget = new QWidget; auto *grid = new QGridLayout(widget); grid->setContentsMargins(0, 0, 0, 0); grid->addWidget(&fastForwardSpeedLabel, 0, 0, 1, 1); grid->addWidget(&fastForwardSpeedBox, 0, 1, 1, 1); - grid->addWidget(&skipEmptyCheckBox, 1, 0, 1, 2); this->addSettingsWidget(widget); @@ -41,8 +36,6 @@ void ReplayQuickSettingsWidget::retranslateUi() { fastForwardSpeedLabel.setText(tr("Fast forward speed:")); fastForwardSpeedBox.setSuffix("x"); - - skipEmptyCheckBox.setText(tr("Skip empty sections")); } void ReplayQuickSettingsWidget::actUpdateFastForwardSpeed(qreal value) @@ -50,9 +43,3 @@ void ReplayQuickSettingsWidget::actUpdateFastForwardSpeed(qreal value) SettingsCache::instance().userInterface().setFastForwardSpeed(value); emit fastForwardSpeedChanged(value); } - -void ReplayQuickSettingsWidget::actUpdateSkipEmptySections(QT_STATE_CHANGED_T value) -{ - SettingsCache::instance().userInterface().setSkipEmptySections(value); - emit skipEmptySectionsChanged(value); -} diff --git a/cockatrice/src/interface/widgets/replay/replay_quick_settings_widget.h b/cockatrice/src/interface/widgets/replay/replay_quick_settings_widget.h index a337ea0a6..b88a8b4e3 100644 --- a/cockatrice/src/interface/widgets/replay/replay_quick_settings_widget.h +++ b/cockatrice/src/interface/widgets/replay/replay_quick_settings_widget.h @@ -3,9 +3,7 @@ #include "../../interface/widgets/quick_settings/settings_button_widget.h" -#include #include -#include class ReplayQuickSettingsWidget : public SettingsButtonWidget { @@ -18,17 +16,13 @@ public: signals: void fastForwardSpeedChanged(qreal speed); - void skipEmptySectionsChanged(bool skip); private: QLabel fastForwardSpeedLabel; QDoubleSpinBox fastForwardSpeedBox; - QCheckBox skipEmptyCheckBox; - private slots: void actUpdateFastForwardSpeed(qreal value); - void actUpdateSkipEmptySections(QT_STATE_CHANGED_T value); }; #endif // COCKATRICE_REPLAY_QUICK_SETTINGS_WIDGET_H diff --git a/cockatrice/src/interface/widgets/replay/replay_widget.cpp b/cockatrice/src/interface/widgets/replay/replay_widget.cpp index c92771f8c..fc0110ff1 100644 --- a/cockatrice/src/interface/widgets/replay/replay_widget.cpp +++ b/cockatrice/src/interface/widgets/replay/replay_widget.cpp @@ -2,7 +2,6 @@ #include "../../../client/settings/cache_settings.h" #include "../../../client/settings/shortcuts_settings.h" -#include "../../pixel_map_generator.h" #include "../interface/widgets/tabs/tab_game.h" #include "replay_manager.h" #include "replay_quick_settings_widget.h" @@ -51,15 +50,15 @@ ReplayWidget::ReplayWidget(QWidget *parent, GameReplay *replay) replayPlayButton = new QToolButton; replayPlayButton->setIconSize(QSize(32, 32)); QIcon playButtonIcon = QIcon(); - playButtonIcon.addPixmap(themePixmap(QStringLiteral("replay/start")), QIcon::Normal, QIcon::Off); - playButtonIcon.addPixmap(themePixmap(QStringLiteral("replay/pause")), QIcon::Normal, QIcon::On); + playButtonIcon.addPixmap(QPixmap("theme:replay/start"), QIcon::Normal, QIcon::Off); + playButtonIcon.addPixmap(QPixmap("theme:replay/pause"), QIcon::Normal, QIcon::On); replayPlayButton->setIcon(playButtonIcon); replayPlayButton->setCheckable(true); connect(replayPlayButton, &QToolButton::toggled, this, &ReplayWidget::replayPlayButtonToggled); replayFastForwardButton = new QToolButton; replayFastForwardButton->setIconSize(QSize(32, 32)); - replayFastForwardButton->setIcon(themePixmap(QStringLiteral("replay/fastforward"))); + replayFastForwardButton->setIcon(QPixmap("theme:replay/fastforward")); replayFastForwardButton->setCheckable(true); connect(replayFastForwardButton, &QToolButton::toggled, this, &ReplayWidget::updateTimeScaleFactor); @@ -67,10 +66,6 @@ ReplayWidget::ReplayWidget(QWidget *parent, GameReplay *replay) settingsWidget->setFixedSize(QSize(32, 32)); connect(settingsWidget, &ReplayQuickSettingsWidget::fastForwardSpeedChanged, this, [this] { updateTimeScaleFactor(replayFastForwardButton->isChecked()); }); - connect(settingsWidget, &ReplayQuickSettingsWidget::skipEmptySectionsChanged, replayManager, - &ReplayManager::setSkipEmptySections); - - replayManager->setSkipEmptySections(SettingsCache::instance().userInterface().getSkipEmptySections()); // putting everything together auto replayControlLayout = new QHBoxLayout; diff --git a/cockatrice/src/interface/widgets/server/chat_view/chat_view.cpp b/cockatrice/src/interface/widgets/server/chat_view/chat_view.cpp index 0a287c9ce..e97d25e64 100644 --- a/cockatrice/src/interface/widgets/server/chat_view/chat_view.cpp +++ b/cockatrice/src/interface/widgets/server/chat_view/chat_view.cpp @@ -13,9 +13,6 @@ #include #include #include -#include -#include -#include #include #include #include @@ -51,12 +48,9 @@ ChatView::ChatView(TabSupervisor *_tabSupervisor, AbstractGame *_game, bool _sho viewport()->setCursor(Qt::IBeamCursor); setReadOnly(true); - setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::LinksAccessibleByMouse | Qt::LinksAccessibleByKeyboard); + setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::LinksAccessibleByMouse); setOpenLinks(false); connect(this, &ChatView::anchorClicked, this, &ChatView::openLink); - - connect(verticalScrollBar(), &QScrollBar::rangeChanged, this, &ChatView::onScrollBarRangeChanged); - connect(verticalScrollBar(), &QScrollBar::valueChanged, this, &ChatView::onScrollBarValueChanged); } void ChatView::adjustColorsToPalette() @@ -157,7 +151,7 @@ void ChatView::appendHtml(const QString &html) bool atBottom = verticalScrollBar()->value() >= verticalScrollBar()->maximum(); prepareBlock().insertHtml(html); if (atBottom) { - scrollToBottom(); + verticalScrollBar()->setValue(verticalScrollBar()->maximum()); } } @@ -175,7 +169,7 @@ void ChatView::appendHtmlServerMessage(const QString &html, bool optionalIsBold, prepareBlock().insertHtml(htmlText); if (atBottom) { - scrollToBottom(); + verticalScrollBar()->setValue(verticalScrollBar()->maximum()); } } @@ -221,46 +215,6 @@ void ChatView::appendUrlTag(QTextCursor &cursor, QString url) cursor.setCharFormat(oldFormat); } -void ChatView::appendGameLinkTag(QTextCursor &cursor, const QString &url) -{ - const QUrl gameUrl(url); - const QUrlQuery query(gameUrl); - const QString hostname = query.queryItemValue("hostname"); - // FullyDecoded undoes every %XX escape, so a description that itself - // contains "%" cannot end up displayed as "%25" in the label. - const QString description = query.queryItemValue("game", QUrl::FullyDecoded); - const int gameId = query.queryItemValue("gameid").toInt(); - - QString label; - if (gameId > 0 && !hostname.isEmpty()) { - // Links built before the description was embedded stay readable: the - // id + server fallback below is identical to the old anchor text. - if (!description.isEmpty()) { - // Multi-arg .arg() replaces all placeholders in a single pass, so a - // description containing "%…" cannot corrupt later placeholders. - label = tr("Join game \"%1\" (#%2) on %3").arg(description, QString::number(gameId), hostname); - } else { - label = tr("Join game #%1 on %2").arg(QString::number(gameId), hostname); - } - } else { - label = tr("Join game"); - } - - QTextCharFormat oldFormat = cursor.charFormat(); - QTextCharFormat gameLinkFormat = oldFormat; - gameLinkFormat.setForeground(linkColor); - gameLinkFormat.setFontWeight(QFont::Bold); - gameLinkFormat.setAnchor(true); - gameLinkFormat.setAnchorHref(url); - QColor background = palette().highlight().color(); - background.setAlpha(40); - gameLinkFormat.setBackground(background); - - cursor.setCharFormat(gameLinkFormat); - cursor.insertText(label); - cursor.setCharFormat(oldFormat); -} - void ChatView::appendMessage(QString message, RoomMessageTypeFlags messageType, const ServerInfo_User &userInfo, @@ -273,14 +227,6 @@ void ChatView::appendMessage(QString message, // messageType should be Event_RoomSay::UserMessage though we don't actually check bool isUserMessage = !(userName.toLower() == "servatrice" || userName.isEmpty()); bool sameSender = isUserMessage && userName == lastSender; - - if (isUserMessage) { - chatHistory.append({userName, message, QDateTime::currentDateTime()}); - while (chatHistory.size() > MAX_CHAT_HISTORY) { - chatHistory.removeFirst(); - } - } - QTextCursor cursor = prepareBlock(sameSender); lastSender = userName; @@ -340,27 +286,16 @@ void ChatView::appendMessage(QString message, pos.relativePosition = match.captured(0).length(); // set message start auto before = match.captured(1); auto sentBy = match.captured(2); - - // The user level is not carried in the room chat history, so history - // entries used to render as fixed-level user tags. Resolve online users - // against the user list to turn their history entries into full user - // tags (correct level, name casing and moderation context menu). - QString displayName = sentBy; - // Offline users have no known level; render them as zero-level tags. - QString levelMarker = "0"; - if (const ServerInfo_User *onlineUser = userListProxy->getOnlineUser(sentBy)) { - displayName = QString::fromStdString(onlineUser->name()); - levelMarker = QString::number(onlineUser->user_level()); - } - cursor.insertText(before); // add message timestamp QTextCharFormat senderFormat(defaultFormat); senderFormat.setAnchor(true); - senderFormat.setAnchorHref("user://" + levelMarker + "_" + displayName); + // this underscore is important, it is used to add the user level, but in this case the level is + // unknown, if the name contains an underscore it would split up the name + senderFormat.setAnchorHref("user://_" + sentBy); cursor.setCharFormat(senderFormat); - cursor.insertText(displayName); // add username with href so it shows the menu - userMessagePositions[displayName].append(pos); // save message position - message.remove(0, pos.relativePosition - 2); // do not remove semicolon + cursor.insertText(sentBy); // add username with href so it shows the menu + userMessagePositions[sentBy].append(pos); // save message position + message.remove(0, pos.relativePosition - 2); // do not remove semicolon } } else { //! \todo Remove hardcoded color. @@ -403,36 +338,11 @@ void ChatView::appendMessage(QString message, } } - // ChatHistory messages are only ever sent once per room, right after joining, before the user can - // interact with the view. Always scroll to the bottom so the whole history is visible on join. - if (atBottom || messageType.testFlag(Event_RoomSay::ChatHistory)) { - scrollToBottom(); - } -} - -void ChatView::scrollToBottom() -{ - // The document layout, and therefore the scrollbar range, may be updated asynchronously (e.g. while - // the chat history is loaded into a view that has not been laid out yet). Setting the value once is - // not enough: keep stickToBottom set so any later range change scrolls to the new maximum as well. - stickToBottom = true; - verticalScrollBar()->setValue(verticalScrollBar()->maximum()); -} - -void ChatView::onScrollBarRangeChanged() -{ - if (stickToBottom) { + if (atBottom) { verticalScrollBar()->setValue(verticalScrollBar()->maximum()); } } -void ChatView::onScrollBarValueChanged(int value) -{ - if (value < verticalScrollBar()->maximum()) { - stickToBottom = false; - } -} - void ChatView::checkTag(QTextCursor &cursor, QString &message) { if (message.startsWith("[card]")) { @@ -564,17 +474,6 @@ void ChatView::checkWord(QTextCursor &cursor, QString &message) } } - if (fullWordUpToSpaceOrEnd.startsWith("cockatrice://", Qt::CaseInsensitive)) { - // Only links to a game (cockatrice://joingame) become invite buttons; - // any other cockatrice:// scheme falls through to plain text below. - const QUrl gameLink(fullWordUpToSpaceOrEnd); - if (gameLink.host().compare("joingame", Qt::CaseInsensitive) == 0) { - appendGameLinkTag(cursor, fullWordUpToSpaceOrEnd); - cursor.insertText(rest, defaultFormat); - return; - } - } - // check word mentions for (const QString &word : highlightedWords) { if (fullWordUpToSpaceOrEnd.compare(word, Qt::CaseInsensitive) == 0) { @@ -669,19 +568,6 @@ void ChatView::clearChat() document()->clear(); lastSender = ""; evenNumber = true; - chatHistory.clear(); -} - -QString ChatView::getRecentChatLog(int maxMessages) const -{ - QStringList lines; - int start = qMax(0, chatHistory.size() - maxMessages); - for (int i = start; i < chatHistory.size(); ++i) { - const ChatLogEntry &entry = chatHistory.at(i); - lines.append( - QString("[%1] %2: %3").arg(entry.timestamp.toString("hh:mm:ss")).arg(entry.userName).arg(entry.message)); - } - return lines.join("\n"); } void ChatView::redactMessages(const QString &userName, int amount) @@ -809,11 +695,6 @@ void ChatView::mouseReleaseEvent(QMouseEvent *event) void ChatView::openLink(const QUrl &link) { - if (link.scheme() == "cockatrice") { - emit cockatriceLinkActivated(link.toString(QUrl::FullyEncoded)); - return; - } - if ((link.scheme() == "card") || (link.scheme() == "user")) { return; } diff --git a/cockatrice/src/interface/widgets/server/chat_view/chat_view.h b/cockatrice/src/interface/widgets/server/chat_view/chat_view.h index 671b35163..8d5894613 100644 --- a/cockatrice/src/interface/widgets/server/chat_view/chat_view.h +++ b/cockatrice/src/interface/widgets/server/chat_view/chat_view.h @@ -33,13 +33,6 @@ public: QTextBlock block; }; -struct ChatLogEntry -{ - QString userName; - QString message; - QDateTime timestamp; -}; - class ChatView : public QTextBrowser { Q_OBJECT @@ -67,20 +60,15 @@ private: QStringList highlightedWords; bool evenNumber; bool showTimestamps; - bool stickToBottom = false; HoveredItemType hoveredItemType; QString hoveredContent; QAction *messageClicked; QMap> userMessagePositions; - QList chatHistory; - static constexpr int MAX_CHAT_HISTORY = 200; [[nodiscard]] QTextFragment getFragmentUnderMouse(const QPoint &pos) const; QTextCursor prepareBlock(bool same = false); - void scrollToBottom(); void appendCardTag(QTextCursor &cursor, const QString &cardName); void appendUrlTag(QTextCursor &cursor, QString url); - void appendGameLinkTag(QTextCursor &cursor, const QString &url); static QColor getCustomMentionColor(); static QColor getCustomHighlightColor(); void showSystemPopup(const QString &userName); @@ -100,8 +88,6 @@ private slots: void actMessageClicked(); void adjustColorsToPalette(); void refreshBlockColors(); - void onScrollBarRangeChanged(); - void onScrollBarValueChanged(int value); public: ChatView(TabSupervisor *_tabSupervisor, AbstractGame *_game, bool _showTimestamps, QWidget *parent = nullptr); @@ -116,7 +102,6 @@ public: bool playerBold = false); void clearChat(); void redactMessages(const QString &userName, int amount); - QString getRecentChatLog(int maxMessages = 50) const; protected: void enterEvent(QEnterEvent *event) override; @@ -132,7 +117,6 @@ signals: void addMentionTag(QString mentionTag); void messageClickedSignal(); void showMentionPopup(const QString &userName); - void cockatriceLinkActivated(const QString &url); }; #endif diff --git a/cockatrice/src/interface/widgets/server/game_filter_configs.h b/cockatrice/src/interface/widgets/server/game_filter_configs.h index 5cb048669..0ece7e00c 100644 --- a/cockatrice/src/interface/widgets/server/game_filter_configs.h +++ b/cockatrice/src/interface/widgets/server/game_filter_configs.h @@ -19,7 +19,7 @@ struct GameFilterConfigs bool hideNotBuddyCreatedGames = false; bool hideOpenDecklistGames = false; QString gameNameFilter = ""; - QStringList hostNameFilters = {}; + QStringList creatorNameFilters = {}; QSet gameTypeFilter = {}; int maxPlayersFilterMin = DEFAULT_MAX_PLAYERS_MIN; int maxPlayersFilterMax = DEFAULT_MAX_PLAYERS_MAX; diff --git a/cockatrice/src/interface/widgets/server/game_link.cpp b/cockatrice/src/interface/widgets/server/game_link.cpp deleted file mode 100644 index c866f6571..000000000 --- a/cockatrice/src/interface/widgets/server/game_link.cpp +++ /dev/null @@ -1,23 +0,0 @@ -#include "game_link.h" - -#include -#include - -QString makeGameJoinLink(const QString &hostname, int port, int roomId, int gameId, const QString &description) -{ - QUrl url; - url.setScheme("cockatrice"); - url.setHost("joingame"); - QUrlQuery query; - query.addQueryItem("hostname", hostname); - query.addQueryItem("port", QString::number(port)); - query.addQueryItem("roomid", QString::number(roomId)); - query.addQueryItem("gameid", QString::number(gameId)); - if (!description.isEmpty()) { - // addQueryItem percent-encodes, so arbitrary descriptions (quotes, - // ampersands, non-ASCII…) survive the trip through chat. - query.addQueryItem("game", description); - } - url.setQuery(query); - return url.toString(QUrl::FullyEncoded); -} diff --git a/cockatrice/src/interface/widgets/server/game_link.h b/cockatrice/src/interface/widgets/server/game_link.h deleted file mode 100644 index d57bbd6d2..000000000 --- a/cockatrice/src/interface/widgets/server/game_link.h +++ /dev/null @@ -1,41 +0,0 @@ -/** - * @file game_link.h - * @ingroup UI - * @brief Builds cockatrice://joingame links that let another user join a server game. - */ - -#ifndef GAME_LINK_H -#define GAME_LINK_H - -#include - -/** - * Builds a cockatrice://joingame link for the given server game. The receiver's - * client opens it through the intent chain (connect -> join room -> join game). - * @p description, when non-empty, is embedded in the link as the URL-encoded - * "game" query item so the receiving client can name the game in its confirm - * prompt and chat anchor instead of only its numeric id. Links built without it - * stay valid: the parser and chat renderer fall back to the id alone. - */ -QString -makeGameJoinLink(const QString &hostname, int port, int roomId, int gameId, const QString &description = QString()); - -/** - * One game the inviter is currently in and can invite another user to. - * @p label is meant for display in menus, @p url is the ready-made invite link. - * @p description is the raw game description for building tr()-wrapped invite - * messages (the label already embeds it, but the send sites need the raw value). - * @p onlyBuddies and @p creatorName mirror the server game's room settings so - * callers can gate the invite to the creator's buddies. - */ -struct GameInviteOption -{ - int gameId = 0; - QString label; - QString url; - QString description; - bool onlyBuddies = false; - QString creatorName; -}; - -#endif // GAME_LINK_H diff --git a/cockatrice/src/interface/widgets/server/game_selector.cpp b/cockatrice/src/interface/widgets/server/game_selector.cpp index 659325987..6580f0262 100644 --- a/cockatrice/src/interface/widgets/server/game_selector.cpp +++ b/cockatrice/src/interface/widgets/server/game_selector.cpp @@ -1,6 +1,5 @@ #include "game_selector.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_account.h" @@ -8,7 +7,6 @@ #include "../interface/widgets/tabs/tab_room.h" #include "../interface/widgets/tabs/tab_supervisor.h" #include "../interface/widgets/utility/get_text_with_max.h" -#include "game_link.h" #include "games_model.h" #include "user/user_list_manager.h" @@ -20,6 +18,8 @@ #include #include #include +#include +#include #include #include #include @@ -96,10 +96,10 @@ GameSelector::GameSelector(AbstractClient *_client, } filterButton = new QPushButton; - filterButton->setIcon(themePixmap(QStringLiteral("icons/search"))); + filterButton->setIcon(QPixmap("theme:icons/search")); connect(filterButton, &QPushButton::clicked, this, &GameSelector::actSetFilter); clearFilterButton = new QPushButton; - clearFilterButton->setIcon(themePixmap(QStringLiteral("icons/clearsearch"))); + clearFilterButton->setIcon(QPixmap("theme:icons/clearsearch")); bool filtersSetToDefault = showFilters && gameListProxyModel->areFilterParametersSetToDefaults(); clearFilterButton->setEnabled(!filtersSetToDefault); connect(clearFilterButton, &QPushButton::clicked, this, &GameSelector::actClearFilter); @@ -320,12 +320,19 @@ void GameSelector::customContextMenu(const QPoint &point) dlg.exec(); }); - QAction copyLink(tr("Cop&y game link")); + QAction copyLink(tr("Copy Game Link")); connect(©Link, &QAction::triggered, this, [=, this]() { const ServerInfo_Game &gameInfo = gameListModel->getGame(index.data(Qt::UserRole).toInt()); - QGuiApplication::clipboard()->setText(makeGameJoinLink(client->serverName(), client->serverPort(), - gameInfo.room_id(), gameInfo.game_id(), - QString::fromStdString(gameInfo.description()))); + QUrl url; + url.setScheme("cockatrice"); + url.setHost("joingame"); + QUrlQuery query; + query.addQueryItem("hostname", client->serverName()); + query.addQueryItem("port", QString::number(client->serverPort())); + query.addQueryItem("roomid", QString::number(gameInfo.room_id())); + query.addQueryItem("gameid", QString::number(gameInfo.game_id())); + url.setQuery(query); + QGuiApplication::clipboard()->setText(url.toString(QUrl::FullyEncoded)); }); QMenu menu; @@ -360,40 +367,18 @@ void GameSelector::joinGame(const bool asSpectator, const bool asJudge) return; } - joinGame(gameListModel->getGame(ind.data(Qt::UserRole).toInt()), asSpectator, asJudge); -} - -void GameSelector::joinGame(const ServerInfo_Game &game, const bool asSpectator, const bool asJudge) -{ + const ServerInfo_Game &game = gameListModel->getGame(ind.data(Qt::UserRole).toInt()); if (tabSupervisor->switchToGameTabIfAlreadyExists(game.game_id())) { return; } - bool overrideRestrictions = tabSupervisor->canOverrideGameRestrictions(); - - // Joining a full game without override privileges silently becomes a - // spectator join, so ask first instead of surprising the player. - const bool gameFull = game.player_count() == game.max_players(); - if (gameFull && !asSpectator && !asJudge && !overrideRestrictions) { - const QMessageBox::StandardButton answer = - QMessageBox::question(this, tr("Join game"), tr("The game is full. Join as a spectator instead?"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - if (answer != QMessageBox::Yes) { - return; - } - } - - bool spectator = asSpectator || gameFull; + bool spectator = asSpectator || game.player_count() == game.max_players(); + bool overrideRestrictions = !tabSupervisor->getAdminLocked(); QString password; if (game.with_password() && !(spectator && !game.spectators_need_password()) && !overrideRestrictions) { bool ok; - // Games without a description have no sensible label — fall back to the - // game id so the prompt still tells the user which game they're entering. - const QString gameLabel = QString::fromStdString(game.description()); - const QString prompt = gameLabel.isEmpty() ? tr("Password for game #%1:").arg(game.game_id()) - : tr("Password for \"%1\":").arg(gameLabel); - password = getTextWithMax(this, tr("Join game"), prompt, QLineEdit::Password, QString(), &ok); + password = getTextWithMax(this, tr("Join game"), tr("Password:"), QLineEdit::Password, QString(), &ok); if (!ok) { return; } @@ -419,16 +404,18 @@ void GameSelector::joinGame(const ServerInfo_Game &game, const bool asSpectator, disableButtons(); } -bool GameSelector::joinGameById(const int gameId, const bool asSpectator) +bool GameSelector::joinGameById(int gameId) { - for (int row = 0; row < gameListModel->rowCount(); ++row) { - const ServerInfo_Game &game = gameListModel->getGame(row); - if (game.game_id() != gameId) { - continue; - } + auto *model = gameListView->model(); - joinGame(game, asSpectator); - return true; + for (int row = 0; row < model->rowCount(); ++row) { + QModelIndex idx = model->index(row, 0); + const ServerInfo_Game &game = gameListModel->getGame(idx.data(Qt::UserRole).toInt()); + if (game.game_id() == gameId) { + gameListView->setCurrentIndex(idx); + joinGame(); + return true; + } } qWarning() << "Game" << gameId << "not found"; @@ -462,7 +449,7 @@ void GameSelector::enableButtonsForIndex(const QModelIndex ¤t) } const ServerInfo_Game &game = gameListModel->getGame(current.data(Qt::UserRole).toInt()); - bool overrideRestrictions = tabSupervisor->canOverrideGameRestrictions(); + bool overrideRestrictions = !tabSupervisor->getAdminLocked(); spectateButton->setEnabled(game.spectators_allowed() || overrideRestrictions); joinButton->setEnabled(game.player_count() < game.max_players() || overrideRestrictions); diff --git a/cockatrice/src/interface/widgets/server/game_selector.h b/cockatrice/src/interface/widgets/server/game_selector.h index 9af39cf50..da34d5322 100644 --- a/cockatrice/src/interface/widgets/server/game_selector.h +++ b/cockatrice/src/interface/widgets/server/game_selector.h @@ -171,17 +171,6 @@ private: */ void joinGame(bool asSpectator = false, bool asJudge = false); - /** - * @brief Performs the join or spectate action for a specific game. - * @param game The game to join. - * @param asSpectator True to join as a spectator, false to join as a player. - * @param asJudge True to join as a judge, false to join as a player. - * - * Unlike the selection-based overload, this does not depend on the game being - * visible in the filtered game list. - */ - void joinGame(const ServerInfo_Game &game, bool asSpectator = false, bool asJudge = false); - public: /** * @brief Constructs a GameSelector widget. @@ -213,16 +202,7 @@ public: * @param info The ServerInfo_Game object containing information about the game to update. */ void processGameInfo(const ServerInfo_Game &info); - /** - * @brief Finds a game by ID and joins or spectates it. - * @param gameId The ID of the game to join. - * @param asSpectator True to join as a spectator, false to join as a player. - * @return True if the game was found and joined, false otherwise. - * - * Unlike the selection-based overload, this does not depend on the game - * being visible in the filtered game list. - */ - bool joinGameById(int gameId, bool asSpectator = false); + bool joinGameById(int gameId); }; #endif diff --git a/cockatrice/src/interface/widgets/server/games_model.cpp b/cockatrice/src/interface/widgets/server/games_model.cpp index 127badbb2..8e623d392 100644 --- a/cockatrice/src/interface/widgets/server/games_model.cpp +++ b/cockatrice/src/interface/widgets/server/games_model.cpp @@ -17,27 +17,13 @@ enum GameListColumn ROOM, CREATED, DESCRIPTION, - HOST, + CREATOR, GAME_TYPE, RESTRICTIONS, PLAYERS, SPECTATORS }; -namespace -{ -/** - * @brief Returns the user info of the game's current host, falling back to the creator. - * - * The server only sends host_info once a host transfer has happened, so older - * servers and freshly created games fall back to the original creator. - */ -const ServerInfo_User &getGameHost(const ServerInfo_Game &game) -{ - return game.has_host_info() ? game.host_info() : game.creator_info(); -} -} // namespace - const QString GamesModel::getGameCreatedString(const int secs) { static const QTime zeroTime{0, 0}; @@ -124,16 +110,16 @@ QVariant GamesModel::data(const QModelIndex &index, int role) const default: return QVariant(); } - case HOST: { + case CREATOR: { switch (role) { case SORT_ROLE: case Qt::DisplayRole: - return QString::fromStdString(getGameHost(gameentry).name()); + return QString::fromStdString(gameentry.creator_info().name()); case Qt::DecorationRole: { - const ServerInfo_User &host = getGameHost(gameentry); - return UserLevelPixmapGenerator::generateIcon(13, UserLevelFlags(host.user_level()), - host.pawn_colors(), false, - QString::fromStdString(host.privlevel())); + return UserLevelPixmapGenerator::generateIcon( + 13, UserLevelFlags(gameentry.creator_info().user_level()), + gameentry.creator_info().pawn_colors(), false, + QString::fromStdString(gameentry.creator_info().privlevel())); } default: return QVariant(); @@ -247,8 +233,8 @@ QVariant GamesModel::headerData(int section, Qt::Orientation /*orientation*/, in } case DESCRIPTION: return tr("Description"); - case HOST: - return tr("Host"); + case CREATOR: + return tr("Creator"); case GAME_TYPE: return tr("Type"); case RESTRICTIONS: @@ -285,9 +271,6 @@ void GamesModel::updateGameList(const ServerInfo_Game &game) gameList.removeAt(i); endRemoveRows(); } else { - // MergeFrom concatenates repeated fields instead of replacing them, - // so clear game_types first to avoid duplicated entries. - gameList[i].clear_game_types(); gameList[i].MergeFrom(game); emit dataChanged(index(i, 0), index(i, NUM_COLS - 1)); } @@ -364,7 +347,7 @@ void GamesProxyModel::loadFilterParameters(const QMap &allGameType gameFilters.isHideFullGames(), gameFilters.isHideGamesThatStarted(), gameFilters.isHidePasswordProtectedGames(), gameFilters.isHideNotBuddyCreatedGames(), gameFilters.isHideOpenDecklistGames(), gameFilters.getGameNameFilter(), - gameFilters.getHostNameFilters(), newGameTypeFilter, gameFilters.getMinPlayers(), + gameFilters.getCreatorNameFilters(), newGameTypeFilter, gameFilters.getMinPlayers(), gameFilters.getMaxPlayers(), gameFilters.getMaxGameAge(), gameFilters.isShowOnlyIfSpectatorsCanWatch(), gameFilters.isShowSpectatorPasswordProtected(), gameFilters.isShowOnlyIfSpectatorsCanChat(), gameFilters.isShowOnlyIfSpectatorsCanSeeHands()}); @@ -381,7 +364,7 @@ void GamesProxyModel::saveFilterParameters(const QMap &allGameType gameFilters.setHideNotBuddyCreatedGames(filters.hideNotBuddyCreatedGames); gameFilters.setHideOpenDecklistGames(filters.hideOpenDecklistGames); gameFilters.setGameNameFilter(filters.gameNameFilter); - gameFilters.setHostNameFilters(filters.hostNameFilters); + gameFilters.setCreatorNameFilters(filters.creatorNameFilters); QMapIterator gameTypeIterator(allGameTypes); while (gameTypeIterator.hasNext()) { @@ -426,11 +409,11 @@ bool GamesProxyModel::filterAcceptsRow(int sourceRow) const return false; } if (filters.hideIgnoredUserGames && - userListProxy->isUserIgnored(QString::fromStdString(getGameHost(game).name()))) { + userListProxy->isUserIgnored(QString::fromStdString(game.creator_info().name()))) { return false; } if (filters.hideNotBuddyCreatedGames && - !userListProxy->isUserBuddy(QString::fromStdString(getGameHost(game).name()))) { + !userListProxy->isUserBuddy(QString::fromStdString(game.creator_info().name()))) { return false; } if (filters.hideFullGames && game.player_count() == game.max_players()) { @@ -452,10 +435,10 @@ bool GamesProxyModel::filterAcceptsRow(int sourceRow) const return false; } } - if (!filters.hostNameFilters.isEmpty()) { + if (!filters.creatorNameFilters.isEmpty()) { bool found = false; - for (const auto &hostNameFilter : filters.hostNameFilters) { - if (QString::fromStdString(getGameHost(game).name()).contains(hostNameFilter, Qt::CaseInsensitive)) { + for (const auto &createNameFilter : filters.creatorNameFilters) { + if (QString::fromStdString(game.creator_info().name()).contains(createNameFilter, Qt::CaseInsensitive)) { found = true; } } diff --git a/cockatrice/src/interface/widgets/server/remote/remote_decklist_tree_widget.cpp b/cockatrice/src/interface/widgets/server/remote/remote_decklist_tree_widget.cpp index df9dfbf53..a6add3fca 100644 --- a/cockatrice/src/interface/widgets/server/remote/remote_decklist_tree_widget.cpp +++ b/cockatrice/src/interface/widgets/server/remote/remote_decklist_tree_widget.cpp @@ -113,7 +113,7 @@ int RemoteDeckList_TreeModel::rowCount(const QModelIndex &parent) const int RemoteDeckList_TreeModel::columnCount(const QModelIndex & /*parent*/) const { - return 4; + return 3; } QVariant RemoteDeckList_TreeModel::data(const QModelIndex &index, int role) const @@ -121,7 +121,7 @@ QVariant RemoteDeckList_TreeModel::data(const QModelIndex &index, int role) cons if (!index.isValid()) { return QVariant(); } - if (index.column() >= 4) { + if (index.column() >= 3) { return QVariant(); } @@ -134,29 +134,12 @@ QVariant RemoteDeckList_TreeModel::data(const QModelIndex &index, int role) cons switch (index.column()) { case 0: return node->getName(); - case 3: - // Report the node's own bit, not the inherited effective - // state, so it stays in step with what publishing toggles. - if (node->isPublic()) { - return tr("Public"); - } - return isEffectivelyPublic(node) ? tr("Public (inherited)") : tr("Private"); default: return QVariant(); } } case Qt::DecorationRole: return index.column() == 0 ? dirIcon : QVariant(); - case Qt::ToolTipRole: - if (index.column() == 3) { - if (node->isPublic()) { - return tr("This folder is visible to other users"); - } - return isEffectivelyPublic(node) - ? tr("This folder is private, but a parent folder is public (inherited).") - : tr("This folder is only visible to you"); - } - return QVariant(); default: return QVariant(); } @@ -170,13 +153,6 @@ QVariant RemoteDeckList_TreeModel::data(const QModelIndex &index, int role) cons return file->getId(); case 2: return file->getUploadTime(); - case 3: - // Report the node's own bit, not the inherited effective - // state, so it stays in step with what publishing toggles. - if (file->isPublic()) { - return tr("Public"); - } - return isEffectivelyPublic(file) ? tr("Public (inherited)") : tr("Private"); default: return QVariant(); } @@ -185,16 +161,6 @@ QVariant RemoteDeckList_TreeModel::data(const QModelIndex &index, int role) cons return index.column() == 0 ? fileIcon : QVariant(); case Qt::TextAlignmentRole: return index.column() == 1 ? Qt::AlignRight : Qt::AlignLeft; - case Qt::ToolTipRole: - if (index.column() == 3) { - if (file->isPublic()) { - return tr("This deck is visible to other users"); - } - return isEffectivelyPublic(file) - ? tr("This deck is private, but a parent folder is public (inherited).") - : tr("This deck is only visible to you"); - } - return QVariant(); default: return QVariant(); } @@ -217,8 +183,6 @@ QVariant RemoteDeckList_TreeModel::headerData(int section, Qt::Orientation orien return tr("ID"); case 2: return tr("Upload time"); - case 3: - return tr("Visibility"); default: return QVariant(); } @@ -275,14 +239,13 @@ void RemoteDeckList_TreeModel::addFileToTree(const ServerInfo_DeckStorage_TreeIt time.setSecsSinceEpoch(fileInfo.creation_time()); beginInsertRows(nodeToIndex(parent), parent->size(), parent->size()); - parent->append(new FileNode(QString::fromStdString(file.name()), file.id(), time, parent, fileInfo.is_public())); + parent->append(new FileNode(QString::fromStdString(file.name()), file.id(), time, parent)); endInsertRows(); } void RemoteDeckList_TreeModel::addFolderToTree(const ServerInfo_DeckStorage_TreeItem &folder, DirectoryNode *parent) { DirectoryNode *newItem = addNamedFolderToTree(QString::fromStdString(folder.name()), parent); - newItem->setIsPublic(folder.folder().is_public()); const ServerInfo_DeckStorage_Folder &folderInfo = folder.folder(); const int folderItemsSize = folderInfo.items_size(); for (int i = 0; i < folderItemsSize; ++i) { @@ -322,21 +285,6 @@ void RemoteDeckList_TreeModel::refreshTree() client->sendCommand(pend); } -bool RemoteDeckList_TreeModel::isEffectivelyPublic(const Node *node) const -{ - if (node == nullptr || node == root) { - return false; - } - const Node *current = node; - while (current != nullptr) { - if (current->isPublic()) { - return true; - } - current = current->getParent(); - } - return false; -} - void RemoteDeckList_TreeModel::clearTree() { beginResetModel(); diff --git a/cockatrice/src/interface/widgets/server/remote/remote_decklist_tree_widget.h b/cockatrice/src/interface/widgets/server/remote/remote_decklist_tree_widget.h index 2cf09aff7..3dd91d7a4 100644 --- a/cockatrice/src/interface/widgets/server/remote/remote_decklist_tree_widget.h +++ b/cockatrice/src/interface/widgets/server/remote/remote_decklist_tree_widget.h @@ -27,11 +27,9 @@ public: protected: DirectoryNode *parent; QString name; - bool publicFlag; public: - explicit Node(const QString &_name, DirectoryNode *_parent = nullptr) - : parent(_parent), name(_name), publicFlag(false) + explicit Node(const QString &_name, DirectoryNode *_parent = nullptr) : parent(_parent), name(_name) { } virtual ~Node() = default; @@ -43,14 +41,6 @@ public: { return name; } - [[nodiscard]] bool isPublic() const - { - return publicFlag; - } - void setIsPublic(bool _public) - { - publicFlag = _public; - } }; class DirectoryNode : public Node, public QList { @@ -69,14 +59,9 @@ public: QDateTime uploadTime; public: - FileNode(const QString &_name, - int _id, - const QDateTime &_uploadTime, - DirectoryNode *_parent = nullptr, - bool _isPublic = false) + FileNode(const QString &_name, int _id, const QDateTime &_uploadTime, DirectoryNode *_parent = nullptr) : Node(_name, _parent), id(_id), uploadTime(_uploadTime) { - setIsPublic(_isPublic); } [[nodiscard]] int getId() const { @@ -124,11 +109,6 @@ public: { return root; } - /** - * @brief Whether a node is visible to other users (own flag or inherited - * from any ancestor folder). - */ - [[nodiscard]] bool isEffectivelyPublic(const Node *node) const; void addFileToTree(const ServerInfo_DeckStorage_TreeItem &file, DirectoryNode *parent); void addFolderToTree(const ServerInfo_DeckStorage_TreeItem &folder, DirectoryNode *parent); DirectoryNode *addNamedFolderToTree(const QString &name, DirectoryNode *parent); 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 d62bf81aa..1f034b767 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,7 +1,5 @@ #include "remote_replay_list_tree_widget.h" -#include "../../../pixel_map_generator.h" - #include #include #include @@ -39,7 +37,7 @@ RemoteReplayList_TreeModel::RemoteReplayList_TreeModel(AbstractClient *_client, QFileIconProvider fip; dirIcon = fip.icon(QFileIconProvider::Folder); fileIcon = fip.icon(QFileIconProvider::File); - lockIcon = themePixmap(QStringLiteral("icons/lock")); + lockIcon = QPixmap("theme:icons/lock"); } RemoteReplayList_TreeModel::~RemoteReplayList_TreeModel() diff --git a/cockatrice/src/interface/widgets/server/user/user_card_art_provider.cpp b/cockatrice/src/interface/widgets/server/user/user_card_art_provider.cpp index 2ba745715..67fb4f684 100644 --- a/cockatrice/src/interface/widgets/server/user/user_card_art_provider.cpp +++ b/cockatrice/src/interface/widgets/server/user/user_card_art_provider.cpp @@ -1,7 +1,6 @@ #include "user_card_art_provider.h" #include "../../../card_picture_loader/card_picture_loader.h" -#include "../../cards/card_art_utils.h" #include #include @@ -40,10 +39,7 @@ void UserCardArtProvider::requestCardArt(const QString &userName, const QString const QString key = makeKey(userName, cardName, providerId); - if (pending.contains(key)) { - return; - } - if (cardArtCache.contains(key) && !cardArtCache.value(key).isNull()) { + if (cardArtCache.contains(key) || pending.contains(key)) { return; } @@ -53,33 +49,20 @@ void UserCardArtProvider::requestCardArt(const QString &userName, const QString processQueue(); } -QPixmap UserCardArtProvider::cropCardArt(const QPixmap &fullRes, const ExactCard &card) +QPixmap UserCardArtProvider::cropCardArt(const QPixmap &fullRes) { - QPixmap source = fullRes; - - // Sideways-layout cards (plane, siege/battle, split) store their landscape - // artwork rotated 90° inside a portrait frame. Rotate it upright first so - // the crop below lands on the horizontal art, mirroring the way - // CardInfoPictureWidget displays these cards. - const bool landscape = card.getInfo().getUiAttributes().landscapeOrientation; - source = CardArtUtils::rotateSidewaysLayoutArt(source, card); - - const QSize sz = source.size(); + const QSize sz = fullRes.size(); const int marginX = sz.width() * 0.07; - const int topMargin = landscape ? sz.height() * 0.05 : sz.height() * 0.11; - const int bottomMargin = landscape ? sz.height() * 0.42 : sz.height() * 0.45; + const int topMargin = sz.height() * 0.11; + const int bottomMargin = sz.height() * 0.45; - const QRect artRect(marginX, topMargin, sz.width() - 2 * marginX, sz.height() - topMargin - bottomMargin); + const QRect foilRect(marginX, topMargin, sz.width() - 2 * marginX, sz.height() - topMargin - bottomMargin); - return source.copy(artRect.intersected(source.rect())); + return fullRes.copy(foilRect.intersected(fullRes.rect())); } void UserCardArtProvider::insertIntoCache(const QString &key, const QPixmap &pixmap) { - if (pixmap.isNull()) { - return; - } - if (!cardArtCache.contains(key)) { cacheInsertionOrder.append(key); while (cacheInsertionOrder.size() > MaxCacheEntries) { @@ -121,7 +104,7 @@ void UserCardArtProvider::processQueue() // Synchronous hit (already loaded/on disk) if (!fullRes.isNull()) { - insertIntoCache(key, cropCardArt(fullRes, card)); + insertIntoCache(key, cropCardArt(fullRes)); pending.remove(key); emit cardArtUpdated(userName); @@ -145,7 +128,9 @@ void UserCardArtProvider::processQueue() CardPictureLoader::getPixmap(fullRes, card, QSize(745, 1040)); if (!fullRes.isNull()) { - self->insertIntoCache(key, self->cropCardArt(fullRes, card)); + self->insertIntoCache(key, self->cropCardArt(fullRes)); + } else { + self->insertIntoCache(key, QPixmap()); } self->pending.remove(key); diff --git a/cockatrice/src/interface/widgets/server/user/user_card_art_provider.h b/cockatrice/src/interface/widgets/server/user/user_card_art_provider.h index e8283a891..fb2f37812 100644 --- a/cockatrice/src/interface/widgets/server/user/user_card_art_provider.h +++ b/cockatrice/src/interface/widgets/server/user/user_card_art_provider.h @@ -6,7 +6,6 @@ #include #include #include -#include class UserCardArtProvider : public QObject { @@ -17,7 +16,7 @@ public: void requestCardArt(const QString &userName, const QString &cardName, const QString &providerId); const QMap &cache() const; - static QPixmap cropCardArt(const QPixmap &fullRes, const ExactCard &card); + static QPixmap cropCardArt(const QPixmap &fullRes); signals: void cardArtUpdated(const QString &userName); @@ -27,7 +26,7 @@ public slots: private: bool dbReady = false; - static constexpr int MaxCacheEntries = 1024; + static constexpr int MaxCacheEntries = 300; QList cacheInsertionOrder; // FIFO eviction QMap cardArtCache; QSet pending; diff --git a/cockatrice/src/interface/widgets/server/user/user_card_settings_dialog.cpp b/cockatrice/src/interface/widgets/server/user/user_card_settings_dialog.cpp index d49e3d540..56c9600a0 100644 --- a/cockatrice/src/interface/widgets/server/user/user_card_settings_dialog.cpp +++ b/cockatrice/src/interface/widgets/server/user/user_card_settings_dialog.cpp @@ -1,8 +1,7 @@ #include "user_card_settings_dialog.h" #include "../../../card_picture_loader/card_picture_loader.h" -#include "../../cards/art_crop_attribution.h" -#include "../../utility/completer_utils.h" +#include "card/card_completer_proxy_model.h" #include "card/card_search_model.h" #include "card_database_display_model.h" #include "card_database_model.h" @@ -15,46 +14,19 @@ #include #include #include -#include #include #include -#include #include #include #include #include #include -#include -#include #include -namespace -{ -// Gesture clamps and step sizes for this direct manipulation surface. The -// gesture zoom floor is 1.0: basescale already cover fits the art, so any -// smaller scale would underfill the strip. -constexpr qreal kMinGestureZoom = 1.0; -constexpr qreal kMaxZoom = 4.0; -constexpr qreal kKeyPanOffsetStep = 0.01; -constexpr qreal kKeyZoomStep = 1.05; -constexpr qreal kWheelZoomBase = 1.15; // zoom factor per wheel notch -} // namespace - CardArtPreviewWidget::CardArtPreviewWidget(QWidget *parent) : QWidget(parent) { setMinimumSize(400, 72); setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); - setFocusPolicy(Qt::StrongFocus); - setAccessibleName(tr("Banner preview")); - setAccessibleDescription(tr("Zoom %1×").arg(QString::number(params.zoom, 'f', 2))); -} - -void CardArtPreviewWidget::focusInEvent(QFocusEvent *event) -{ - // Snapshot for the Esc or Backspace reset, restoring whatever the user - // had when the surface took focus - paramsAtFocusIn = params; - QWidget::focusInEvent(event); } void CardArtPreviewWidget::setPixmap(const QPixmap &pixmap) @@ -66,13 +38,6 @@ void CardArtPreviewWidget::setPixmap(const QPixmap &pixmap) void CardArtPreviewWidget::setParams(const CardArtParams &p) { params = p; - setAccessibleDescription(tr("Zoom %1×").arg(QString::number(params.zoom, 'f', 2))); - update(); -} - -void CardArtPreviewWidget::setAttribution(const QString &attribution) -{ - attributionText = attribution; update(); } @@ -95,22 +60,9 @@ void CardArtPreviewWidget::paintEvent(QPaintEvent *) painter.setBrush(accentColor); painter.drawRoundedRect(QRectF(cardRect.left(), cardRect.top(), 3, cardRect.height()), 2, 2); - // Visible keyboard focus per the focus cursor contract, Tab must show - // where the keys land, including in the empty state - const auto paintFocusRing = [&painter, &cardRect, this]() { - if (!hasFocus()) { - return; - } - QPen focusPen(palette().color(QPalette::Highlight), 2); - painter.setPen(focusPen); - painter.setBrush(Qt::NoBrush); - painter.drawRoundedRect(cardRect.adjusted(-1, -1, 1, 1), 6, 6); - }; - if (sourcePixmap.isNull()) { painter.setPen(QColor(150, 150, 150)); painter.drawText(rect, Qt::AlignCenter, tr("No card selected")); - paintFocusRing(); return; } @@ -121,7 +73,7 @@ void CardArtPreviewWidget::paintEvent(QPaintEvent *) &sourcePixmap // direct pixmap ); - // Avatar placeholder so the left margin interaction is visible + // Avatar placeholder so the left-margin interaction is visible const int avatarX = rect.left() + 14; const int avatarY = rect.top() + (rect.height() - 36) / 2; const QRect avatarRect(avatarX, avatarY, 36, 36); @@ -138,202 +90,24 @@ void CardArtPreviewWidget::paintEvent(QPaintEvent *) painter.setPen(QPen(QColor(70, 80, 95), 2)); painter.setBrush(Qt::NoBrush); painter.drawEllipse(avatarRect.adjusted(-1, -1, 1, 1)); - - paintArtAttribution(painter, cardRect, attributionText); - - paintFocusRing(); -} - -qreal CardArtPreviewWidget::bannerTravel() const -{ - if (sourcePixmap.isNull()) { - return 0.0; - } - // Mirror UserListPainter::drawCardArt() exactly: same strip metrics, the - // copy is drawn 1:1, so output pixels equal widget pixels here. - const int cardH = rect().height() - 4; - const int totalW = (rect().right() - 4) - rect().left(); - const int marginL = qRound(totalW * params.marginPctL); - const int marginR = qRound(totalW * params.marginPctR); - const int drawW = totalW - marginL - marginR; - const double basescale = qMax(double(drawW) / sourcePixmap.width(), double(cardH) / sourcePixmap.height()); - // qRound for literal parity with drawCardArt, which rounds the scaled - // height before computing travel - const double scaledH = qRound(sourcePixmap.height() * basescale * params.zoom); - return scaledH - cardH; -} - -void CardArtPreviewWidget::applyCropDelta(qreal dOffset, qreal zoomFactor) -{ - CardArtParams next = params; - next.verticalOffset = qBound(0.0, params.verticalOffset + dOffset, 1.0); - next.zoom = qBound(kMinGestureZoom, params.zoom * zoomFactor, kMaxZoom); - - if (sameCrop(next, params)) { - return; - } - - params = next; - setAccessibleDescription(tr("Zoom %1×").arg(QString::number(params.zoom, 'f', 2))); - update(); - emit paramsEdited(params); -} - -bool CardArtPreviewWidget::sameCrop(const CardArtParams &a, const CardArtParams &b) const -{ - // Exact comparison on purpose: clamped assignments yield identical bits, - // while qFuzzyCompare based equality misbehaves around zero - return a.verticalOffset == b.verticalOffset && a.zoom == b.zoom; -} - -void CardArtPreviewWidget::restoreSnapshot() -{ - // The snapshot only ever holds values that passed the gesture clamps, - // so it is safe to restore verbatim - if (sameCrop(paramsAtFocusIn, params)) { - return; - } - params = paramsAtFocusIn; - setAccessibleDescription(tr("Zoom %1×").arg(QString::number(params.zoom, 'f', 2))); - update(); - emit paramsEdited(params); -} - -void CardArtPreviewWidget::mousePressEvent(QMouseEvent *event) -{ - if (event->button() != Qt::LeftButton || sourcePixmap.isNull() || bannerTravel() <= 0.5) { - QWidget::mousePressEvent(event); - return; - } - dragging = true; - lastDragPos = event->pos(); - setCursor(Qt::ClosedHandCursor); - event->accept(); -} - -void CardArtPreviewWidget::mouseMoveEvent(QMouseEvent *event) -{ - if (!dragging || sourcePixmap.isNull()) { - QWidget::mouseMoveEvent(event); - return; - } - - const qreal dy = event->pos().y() - lastDragPos.y(); - lastDragPos = event->pos(); - - const qreal travel = bannerTravel(); - if (travel <= 0.5) { - event->accept(); - return; - } - - // Dragging moves the ART with the cursor, so the crop window slides the - // other way through the available travel. - applyCropDelta(-dy / travel, 1.0); - event->accept(); -} - -void CardArtPreviewWidget::mouseReleaseEvent(QMouseEvent *event) -{ - if (event->button() == Qt::LeftButton) { - dragging = false; - unsetCursor(); - event->accept(); - return; - } - QWidget::mouseReleaseEvent(event); -} - -void CardArtPreviewWidget::wheelEvent(QWheelEvent *event) -{ - if (sourcePixmap.isNull()) { - QWidget::wheelEvent(event); - return; - } - const qreal notches = static_cast(event->angleDelta().y()) / 120.0; - if (notches == 0.0) { - event->accept(); - return; - } - applyCropDelta(0.0, std::pow(kWheelZoomBase, notches)); - event->accept(); -} - -void CardArtPreviewWidget::keyPressEvent(QKeyEvent *event) -{ - if (sourcePixmap.isNull()) { - QWidget::keyPressEvent(event); - return; - } - - switch (event->key()) { - case Qt::Key_Escape: - if (sameCrop(params, paramsAtFocusIn)) { - // Nothing to undo on this surface, let the event reach the - // dialog so Esc keeps its close meaning there - QWidget::keyPressEvent(event); - return; - } - restoreSnapshot(); - break; - case Qt::Key_Backspace: - restoreSnapshot(); - break; - case Qt::Key_Up: - applyCropDelta(-kKeyPanOffsetStep, 1.0); - break; - case Qt::Key_Down: - applyCropDelta(kKeyPanOffsetStep, 1.0); - break; - case Qt::Key_Plus: - case Qt::Key_Equal: - applyCropDelta(0.0, kKeyZoomStep); - break; - case Qt::Key_Minus: - applyCropDelta(0.0, 1.0 / kKeyZoomStep); - break; - default: - QWidget::keyPressEvent(event); - return; - } - event->accept(); } UserCardArtSettingsDialog::UserCardArtSettingsDialog(const CardArtParams &initial, QWidget *parent) : QDialog(parent), currentParams(initial) { - // Legacy stored banners may carry zoom below the gesture floor or an out - // of range offset. Normalize once on open so the preview renders filled - // and Ok saves a state the gestures can reach again - currentParams.zoom = qBound(kMinGestureZoom, currentParams.zoom, kMaxZoom); - currentParams.verticalOffset = qBound(0.0, currentParams.verticalOffset, 1.0); - + setWindowTitle(tr("Card Art Settings")); setMinimumWidth(500); setupUi(); // Seed UI from initial params - if (!currentParams.cardName.isEmpty()) { - // onCardNameChanged overwrites cardProviderId with the first printing, - // so remember the stored one before it runs - const QString storedProviderId = currentParams.cardProviderId; - searchBar->setText(currentParams.cardName); - onCardNameChanged(currentParams.cardName); - - // onCardNameChanged leaves the printing combo on the first printing in - // the database, which would silently change the stored banner card on - // accept. Restore the stored printing when it resolves locally. - const int storedPrintingIndex = providerComboBox->findData(storedProviderId); - if (storedPrintingIndex != -1) { - providerComboBox->setCurrentIndex(storedPrintingIndex); - } else if (!storedProviderId.isEmpty()) { - // Stored printing not in the local database: keep it rather than - // silently substituting the first printing. - currentParams.cardProviderId = storedProviderId; - reloadPreview(); - } + if (!initial.cardName.isEmpty()) { + searchBar->setText(initial.cardName); + onCardNameChanged(initial.cardName); } - marginLSpin->setValue(currentParams.marginPctL); - marginRSpin->setValue(currentParams.marginPctR); + marginLSpin->setValue(initial.marginPctL); + marginRSpin->setValue(initial.marginPctR); + verticalOffsetSpin->setValue(initial.verticalOffset); + zoomSpin->setValue(initial.zoom); } CardArtParams UserCardArtSettingsDialog::params() const @@ -354,18 +128,34 @@ QDoubleSpinBox *UserCardArtSettingsDialog::makeSpinBox(double min, double max, d void UserCardArtSettingsDialog::initializeSearchBar() { searchBar = new QLineEdit; + searchBar->setPlaceholderText(tr("Type a card name...")); cardDatabaseModel = new CardDatabaseModel(CardDatabaseManager::getInstance(), false, this); cardDatabaseDisplayModel = new CardDatabaseDisplayModel(this); cardDatabaseDisplayModel->setSourceModel(cardDatabaseModel); + searchModel = new CardSearchModel(cardDatabaseDisplayModel, this); - const CardCompleterSetup cardSetup = createCardCompleter(cardDatabaseDisplayModel, this, 15); - searchModel = cardSetup.searchModel; - proxyModel = cardSetup.proxyModel; - completer = cardSetup.completer; + proxyModel = new CardCompleterProxyModel(this); + proxyModel->setSourceModel(searchModel); + proxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive); + proxyModel->setFilterRole(Qt::DisplayRole); + + completer = new QCompleter(proxyModel, this); + completer->setCompletionRole(Qt::DisplayRole); + completer->setCompletionMode(QCompleter::PopupCompletion); + completer->setCaseSensitivity(Qt::CaseInsensitive); + completer->setFilterMode(Qt::MatchContains); + completer->setMaxVisibleItems(15); searchBar->setCompleter(completer); - connectCardCompleterSearch(searchBar, cardSetup); + connect(searchBar, &QLineEdit::textEdited, searchModel, &CardSearchModel::updateSearchResults); + connect(searchBar, &QLineEdit::textEdited, this, [this](const QString &text) { + const QString pattern = ".*" + QRegularExpression::escape(text) + ".*"; + proxyModel->setFilterRegularExpression(QRegularExpression(pattern, QRegularExpression::CaseInsensitiveOption)); + if (!text.isEmpty()) { + completer->complete(); + } + }); connect(completer, static_cast(&QCompleter::activated), this, [this](const QString &completion) { @@ -393,33 +183,29 @@ void UserCardArtSettingsDialog::setupUi() marginLSpin = makeSpinBox(0.0, 0.95, currentParams.marginPctL, 0.01); marginRSpin = makeSpinBox(0.0, 0.95, currentParams.marginPctR, 0.01); + verticalOffsetSpin = makeSpinBox(0.0, 1.0, currentParams.verticalOffset, 0.01); + zoomSpin = makeSpinBox(0.1, 4.0, currentParams.zoom, 0.05); auto *form = new QFormLayout; - cardNameLabel = new QLabel; - printingLabel = new QLabel; - marginLLabel = new QLabel; - marginRLabel = new QLabel; - form->addRow(cardNameLabel, searchBar); - form->addRow(printingLabel, providerComboBox); - form->addRow(marginLLabel, marginLSpin); - form->addRow(marginRLabel, marginRSpin); + form->addRow(tr("Card name:"), searchBar); + form->addRow(tr("Card ProviderId:"), providerComboBox); + form->addRow(tr("Left margin (%):"), marginLSpin); + form->addRow(tr("Right margin (%):"), marginRSpin); + form->addRow(tr("Vertical offset:"), verticalOffsetSpin); + form->addRow(tr("Zoom:"), zoomSpin); - controlsGroup = new QGroupBox; + auto *controlsGroup = new QGroupBox(tr("Parameters")); controlsGroup->setLayout(form); preview = new CardArtPreviewWidget; auto *previewLayout = new QVBoxLayout; previewLayout->addWidget(preview); - previewCaptionLabel = new QLabel; - previewCaptionLabel->setAlignment(Qt::AlignCenter); - previewCaptionLabel->setWordWrap(true); - previewLayout->addWidget(previewCaptionLabel); - previewGroup = new QGroupBox; + auto *previewGroup = new QGroupBox(tr("Preview")); previewGroup->setLayout(previewLayout); auto *buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); - removeBtn = new QPushButton; + auto *removeBtn = new QPushButton(tr("Remove Banner Card")); buttons->addButton(removeBtn, QDialogButtonBox::ResetRole); connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept); @@ -429,38 +215,16 @@ void UserCardArtSettingsDialog::setupUi() accept(); }); - // The banner leads visually, card selection and margins support it below. auto *root = new QVBoxLayout; - root->addWidget(previewGroup); root->addWidget(controlsGroup); + root->addWidget(previewGroup); root->addWidget(buttons); setLayout(root); connect(marginLSpin, &QDoubleSpinBox::valueChanged, this, &UserCardArtSettingsDialog::onParamChanged); connect(marginRSpin, &QDoubleSpinBox::valueChanged, this, &UserCardArtSettingsDialog::onParamChanged); - - // Gestures are the only editors of offset and zoom on this surface. - // Margins stay explicit numeric controls: they trim the strip's - // sides and have no natural drag mapping. - connect(preview, &CardArtPreviewWidget::paramsEdited, this, - [this](const CardArtParams &edited) { currentParams = edited; }); - - retranslateUi(); -} - -void UserCardArtSettingsDialog::retranslateUi() -{ - setWindowTitle(tr("Card Art Settings")); - searchBar->setPlaceholderText(tr("Type a card name...")); - cardNameLabel->setText(tr("Card name:")); - printingLabel->setText(tr("Printing:")); - marginLLabel->setText(tr("Left margin (%):")); - marginRLabel->setText(tr("Right margin (%):")); - controlsGroup->setTitle(tr("Card")); - previewCaptionLabel->setText( - tr("Drag to pan, scroll to zoom, arrow keys nudge, plus and minus zoom, Backspace or Esc restores.")); - previewGroup->setTitle(tr("Banner")); - removeBtn->setText(tr("Remove Banner Card")); + connect(verticalOffsetSpin, &QDoubleSpinBox::valueChanged, this, &UserCardArtSettingsDialog::onParamChanged); + connect(zoomSpin, &QDoubleSpinBox::valueChanged, this, &UserCardArtSettingsDialog::onParamChanged); } void UserCardArtSettingsDialog::populateProviderCombo(const QString &cardName) @@ -510,7 +274,7 @@ void UserCardArtSettingsDialog::onCardNameChanged(const QString &name) populateProviderCombo(name); if (providerComboBox->count() == 0) { - // No printings found for this card, nothing to preview. + // No printings found for this card; nothing to preview. currentPixmap = QPixmap(); preview->setPixmap(currentPixmap); currentParams.cardProviderId.clear(); @@ -540,7 +304,7 @@ void UserCardArtSettingsDialog::reloadPreview() // whichever CardInfo we just asked for, so the preview catches up once // the image actually arrives instead of staying on the placeholder. // - // Disconnect any previous listener first, otherwise switching cards + // Disconnect any previous listener first -- otherwise switching cards // repeatedly stacks up connections to old CardInfo objects, each of // which would still fire reloadPreview() (harmlessly, but wastefully) // whenever ITS art finishes loading later. @@ -550,8 +314,8 @@ void UserCardArtSettingsDialog::reloadPreview() CardPictureLoader::getPixmap(fullRes, card, QSize(745, 1040)); if (fullRes.isNull()) { - // Not loaded yet, wait for the signal instead of giving up. - // card.getCardPtr() is a CardInfoPtr (QSharedPointer), + // Not loaded yet -- wait for the signal instead of giving up. + // card.getCardPtr() is a CardInfoPtr (QSharedPointer); // .data() gives the raw QObject* needed for connect(). CardInfo *cardInfo = card.getCardPtr().data(); if (cardInfo) { @@ -560,19 +324,16 @@ void UserCardArtSettingsDialog::reloadPreview() return; } - currentPixmap = UserCardArtProvider::cropCardArt(fullRes, card); + currentPixmap = UserCardArtProvider::cropCardArt(fullRes); preview->setPixmap(currentPixmap); preview->setParams(currentParams); - - // Only attribute the art once the new pixmap is actually displayed, so a - // cache miss (which keeps the previous pixmap on screen) doesn't pair the - // new card's attribution with the old card's art. - preview->setAttribution(buildArtAttribution(card)); } void UserCardArtSettingsDialog::onParamChanged() { currentParams.marginPctL = marginLSpin->value(); currentParams.marginPctR = marginRSpin->value(); + currentParams.verticalOffset = verticalOffsetSpin->value(); + currentParams.zoom = zoomSpin->value(); preview->setParams(currentParams); -} +} \ No newline at end of file diff --git a/cockatrice/src/interface/widgets/server/user/user_card_settings_dialog.h b/cockatrice/src/interface/widgets/server/user/user_card_settings_dialog.h index 24e14dce5..018043278 100644 --- a/cockatrice/src/interface/widgets/server/user/user_card_settings_dialog.h +++ b/cockatrice/src/interface/widgets/server/user/user_card_settings_dialog.h @@ -8,29 +8,13 @@ #include class QCompleter; -class QFocusEvent; -class QGroupBox; -class QKeyEvent; -class QMouseEvent; class QLineEdit; class QDoubleSpinBox; -class QLabel; -class QPushButton; -class QWheelEvent; class CardDatabaseModel; class CardDatabaseDisplayModel; class CardSearchModel; class CardCompleterProxyModel; -/** - * @brief Interactive preview of the user list banner art. - * - * Renders the banner strip with the given CardArtParams through the same - * UserListPainter::drawCardArt() the live delegate uses, including the - * avatar placeholder and fade masks. Dragging pans the art vertically at - * output scale, the wheel zooms, arrow keys nudge, Backspace or Esc - * restores the parameters as of focus gain. - */ class CardArtPreviewWidget : public QWidget { Q_OBJECT @@ -40,33 +24,13 @@ public: void setPixmap(const QPixmap &pixmap); void setParams(const CardArtParams ¶ms); - void setAttribution(const QString &attribution); - -signals: - /** @brief Emitted whenever direct manipulation (drag, wheel, keys) changes the parameters. */ - void paramsEdited(const CardArtParams ¶ms); protected: void paintEvent(QPaintEvent *event) override; - void mousePressEvent(QMouseEvent *event) override; - void mouseMoveEvent(QMouseEvent *event) override; - void mouseReleaseEvent(QMouseEvent *event) override; - void wheelEvent(QWheelEvent *event) override; - void keyPressEvent(QKeyEvent *event) override; - void focusInEvent(QFocusEvent *event) override; private: - qreal bannerTravel() const; ///< vertical travel of the art behind the strip, in output pixels - void applyCropDelta(qreal dOffset, qreal zoomFactor); - bool sameCrop(const CardArtParams &a, const CardArtParams &b) const; - void restoreSnapshot(); - QPixmap sourcePixmap; CardArtParams params; - CardArtParams paramsAtFocusIn; ///< crop as of the latest focus gain, restored by Esc or Backspace - QString attributionText; - QPoint lastDragPos; ///< widget space position of the previous mouse move while panning - bool dragging{false}; ///< true between an accepted press and its release, guards stale drag positions }; class UserCardArtSettingsDialog : public QDialog @@ -85,7 +49,6 @@ private slots: private: void setupUi(); - void retranslateUi(); void populateProviderCombo(const QString &cardName); void initializeSearchBar(); QDoubleSpinBox *makeSpinBox(double min, double max, double value, double step); @@ -101,20 +64,14 @@ private: QMetaObject::Connection pixmapUpdatedConnection; - QLabel *cardNameLabel; - QLabel *printingLabel; - QLabel *marginLLabel; - QLabel *marginRLabel; - QGroupBox *controlsGroup; - QLabel *previewCaptionLabel; - QGroupBox *previewGroup; - QPushButton *removeBtn; QDoubleSpinBox *marginLSpin; QDoubleSpinBox *marginRSpin; + QDoubleSpinBox *verticalOffsetSpin; + QDoubleSpinBox *zoomSpin; CardArtPreviewWidget *preview; QPixmap currentPixmap; CardArtParams currentParams; }; -#endif // COCKATRICE_USER_CARD_ART_SETTINGS_DIALOG_H +#endif // COCKATRICE_USER_CARD_ART_SETTINGS_DIALOG_H \ No newline at end of file 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 f95ad88e4..5c4a88974 100644 --- a/cockatrice/src/interface/widgets/server/user/user_context_menu.cpp +++ b/cockatrice/src/interface/widgets/server/user/user_context_menu.cpp @@ -1,15 +1,14 @@ #include "user_context_menu.h" -#include "../../dialogs/dlg_report_user.h" #include "../../interface/widgets/tabs/tab_account.h" #include "../../interface/widgets/tabs/tab_game.h" #include "../../interface/widgets/tabs/tab_supervisor.h" #include "../chat_view/chat_view.h" #include "../game_selector.h" #include "user_info_box.h" -#include "user_list_dialog.h" #include "user_list_manager.h" #include "user_list_proxy.h" +#include "user_list_widget.h" #include #include @@ -37,13 +36,11 @@ UserContextMenu::UserContextMenu(TabSupervisor *_tabSupervisor, QWidget *parent, aDetails = new QAction(QString(), this); aChat = new QAction(QString(), this); aShowGames = new QAction(QString(), this); - aViewPublicDecks = new QAction(QString(), this); aAddToBuddyList = new QAction(QString(), this); aRemoveFromBuddyList = new QAction(QString(), this); aAddToIgnoreList = new QAction(QString(), this); aRemoveFromIgnoreList = new QAction(QString(), this); aKick = new QAction(QString(), this); - aReport = new QAction(QString(), this); aWarnUser = new QAction(QString(), this); aWarnHistory = new QAction(QString(), this); aBan = new QAction(QString(), this); @@ -52,10 +49,7 @@ UserContextMenu::UserContextMenu(TabSupervisor *_tabSupervisor, QWidget *parent, aDemoteFromMod = new QAction(QString(), this); aPromoteToJudge = new QAction(QString(), this); aDemoteFromJudge = new QAction(QString(), this); - aPromoteToDeveloper = new QAction(QString(), this); - aDemoteFromDeveloper = new QAction(QString(), this); aGetAdminNotes = new QAction(QString(), this); - aInvestigateUser = new QAction(QString(), this); retranslateUi(); } @@ -65,13 +59,11 @@ void UserContextMenu::retranslateUi() aDetails->setText(tr("User &details")); aChat->setText(tr("Private &chat")); aShowGames->setText(tr("Show this user's &games")); - aViewPublicDecks->setText(tr("View this user's &public decks")); aAddToBuddyList->setText(tr("Add to &buddy list")); aRemoveFromBuddyList->setText(tr("Remove from &buddy list")); aAddToIgnoreList->setText(tr("Add to &ignore list")); aRemoveFromIgnoreList->setText(tr("Remove from &ignore list")); aKick->setText(tr("Kick from &game")); - aReport->setText(tr("Report user")); aWarnUser->setText(tr("Warn user")); aWarnHistory->setText(tr("View user's war&n history")); aBan->setText(tr("Ban from &server")); @@ -80,10 +72,7 @@ void UserContextMenu::retranslateUi() aDemoteFromMod->setText(tr("Dem&ote user from moderator")); aPromoteToJudge->setText(tr("Promote user to &judge")); aDemoteFromJudge->setText(tr("Demote user from judge")); - aPromoteToDeveloper->setText(tr("Promote user to &developer")); - aDemoteFromDeveloper->setText(tr("Demote user from de&veloper")); aGetAdminNotes->setText(tr("View admin notes")); - aInvestigateUser->setText(tr("Investigate user")); } void UserContextMenu::gamesOfUserReceived(const Response &resp, const CommandContainer &commandContainer) @@ -155,8 +144,7 @@ void UserContextMenu::warnUser_processGetWarningsListResponse(const Response &r) if (response.warning_size() > 0) { for (int i = 0; i < response.warning_size(); ++i) { - int startingIl = i < response.warning_il_size() ? response.warning_il(i) : 1; - dlg->addWarningOption(QString::fromStdString(response.warning(i)).simplified(), startingIl); + dlg->addWarningOption(QString::fromStdString(response.warning(i)).simplified()); } } dlg->show(); @@ -274,7 +262,7 @@ void UserContextMenu::adjustMod_processUserResponse(const Response &resp, const const Command_AdjustMod &cmd = commandContainer.admin_command(0).GetExtension(Command_AdjustMod::ext); if (resp.response_code() == Response::RespOk) { - if (cmd.should_be_mod() || cmd.should_be_judge() || cmd.should_be_developer()) { + if (cmd.should_be_mod() || cmd.should_be_judge()) { QMessageBox::information(static_cast(parent()), tr("Success"), tr("Successfully promoted user.")); } else { @@ -282,7 +270,7 @@ void UserContextMenu::adjustMod_processUserResponse(const Response &resp, const } } else { - if (cmd.should_be_mod() || cmd.should_be_judge() || cmd.should_be_developer()) { + if (cmd.should_be_mod() || cmd.should_be_judge()) { QMessageBox::information(static_cast(parent()), tr("Failed"), tr("Failed to promote user.")); } else { QMessageBox::information(static_cast(parent()), tr("Failed"), tr("Failed to demote user.")); @@ -367,7 +355,6 @@ void UserContextMenu::showContextMenu(const QPoint &pos, { QAction *aCopyToClipBoard = nullptr, *aRemoveMessages = nullptr; aUserName->setText(userName); - const bool anotherUser = userName != userListProxy->getOwnUsername(); auto *menu = new QMenu(static_cast(parent())); menu->addAction(aUserName); @@ -378,21 +365,7 @@ void UserContextMenu::showContextMenu(const QPoint &pos, } menu->addAction(aDetails); menu->addAction(aShowGames); - if (userLevel.testFlag(ServerInfo_User::IsRegistered)) { - menu->addAction(aViewPublicDecks); - } menu->addAction(aChat); - const QList inviteOptions = inviteOptionsForUser(userName); - if (!inviteOptions.isEmpty()) { - auto *inviteMenu = new QMenu(tr("&Invite to Game"), menu); - for (const GameInviteOption &option : inviteOptions) { - QAction *inviteAction = inviteMenu->addAction(option.label); - inviteAction->setEnabled(anotherUser && online); - connect(inviteAction, &QAction::triggered, this, - [this, userName, option] { execInvite(userName, option); }); - } - menu->addMenu(inviteMenu); - } if (userLevel.testFlag(ServerInfo_User::IsRegistered) && userListProxy->isOwnUserRegistered()) { menu->addSeparator(); if (userListProxy->isUserBuddy(userName)) { @@ -410,9 +383,6 @@ void UserContextMenu::showContextMenu(const QPoint &pos, aRemoveMessages = new QAction(tr("Remove this user's messages"), this); menu->addAction(aRemoveMessages); } - if (userListProxy->isOwnUserRegistered()) { - menu->addAction(aReport); - } if (game && (game->isHost() || !tabSupervisor->getAdminLocked())) { menu->addSeparator(); menu->addAction(aKick); @@ -426,7 +396,6 @@ void UserContextMenu::showContextMenu(const QPoint &pos, menu->addAction(aBanHistory); menu->addSeparator(); menu->addAction(aGetAdminNotes); - menu->addAction(aInvestigateUser); menu->addSeparator(); if (userLevel.testFlag(ServerInfo_User::IsModerator) && @@ -446,21 +415,11 @@ void UserContextMenu::showContextMenu(const QPoint &pos, (tabSupervisor->getUserInfo()->user_level() & ServerInfo_User::IsAdmin)) { menu->addAction(aPromoteToJudge); } - - if (userLevel.testFlag(ServerInfo_User::IsDeveloper) && - (tabSupervisor->getUserInfo()->user_level() & ServerInfo_User::IsAdmin)) { - menu->addAction(aDemoteFromDeveloper); - - } else if (userLevel.testFlag(ServerInfo_User::IsRegistered) && - (tabSupervisor->getUserInfo()->user_level() & ServerInfo_User::IsAdmin)) { - menu->addAction(aPromoteToDeveloper); - } } + bool anotherUser = userName != userListProxy->getOwnUsername(); aDetails->setEnabled(true); - aChat->setEnabled(anotherUser && online && !userListProxy->isUserIgnored(userName)); + aChat->setEnabled(anotherUser && online); aShowGames->setEnabled(online); - aReport->setEnabled(anotherUser); - aViewPublicDecks->setEnabled(anotherUser); aAddToBuddyList->setEnabled(anotherUser); aRemoveFromBuddyList->setEnabled(anotherUser); aAddToIgnoreList->setEnabled(anotherUser); @@ -471,13 +430,8 @@ void UserContextMenu::showContextMenu(const QPoint &pos, aBan->setEnabled(anotherUser); aBanHistory->setEnabled(anotherUser); aGetAdminNotes->setEnabled(anotherUser); - aInvestigateUser->setEnabled(anotherUser); aPromoteToMod->setEnabled(anotherUser); aDemoteFromMod->setEnabled(anotherUser); - aPromoteToJudge->setEnabled(anotherUser); - aDemoteFromJudge->setEnabled(anotherUser); - aPromoteToDeveloper->setEnabled(anotherUser); - aDemoteFromDeveloper->setEnabled(anotherUser); QAction *actionClicked = menu->exec(pos); if (actionClicked == nullptr) { @@ -487,8 +441,6 @@ void UserContextMenu::showContextMenu(const QPoint &pos, execChat(userName); } else if (actionClicked == aShowGames) { execShowGames(userName); - } else if (actionClicked == aViewPublicDecks) { - execViewPublicDecks(userName); } else if (actionClicked == aAddToBuddyList) { execAddToBuddy(userName); } else if (actionClicked == aRemoveFromBuddyList) { @@ -499,23 +451,12 @@ void UserContextMenu::showContextMenu(const QPoint &pos, execRemoveFromIgnore(userName); } else if (actionClicked == aKick) { execKick(playerId); - } else if (actionClicked == aReport) { - int gameId = game ? game->getGameMetaInfo()->gameId() : -1; - QString autoChatLog; - if (chatView) { - autoChatLog = chatView->getRecentChatLog(50); - } - auto dlgReport = new DlgReportUser(client, userName, gameId, autoChatLog, static_cast(parent())); - dlgReport->setAttribute(Qt::WA_DeleteOnClose); - dlgReport->exec(); } else if (actionClicked == aBan) { execBan(userName); } else if (actionClicked == aPromoteToMod || actionClicked == aDemoteFromMod) { execAdjustMod(userName, actionClicked == aPromoteToMod); } else if (actionClicked == aPromoteToJudge || actionClicked == aDemoteFromJudge) { execAdjustJudge(userName, actionClicked == aPromoteToJudge); - } else if (actionClicked == aPromoteToDeveloper || actionClicked == aDemoteFromDeveloper) { - execAdjustDeveloper(userName, actionClicked == aPromoteToDeveloper); } else if (actionClicked == aBanHistory) { execBanHistory(userName); } else if (actionClicked == aWarnUser) { @@ -524,8 +465,6 @@ void UserContextMenu::showContextMenu(const QPoint &pos, execWarnHistory(userName); } else if (actionClicked == aGetAdminNotes) { execAdminNotes(userName); - } else if (actionClicked == aInvestigateUser) { - execInvestigateUser(userName); } else if (actionClicked == aCopyToClipBoard) { QClipboard *clipboard = QGuiApplication::clipboard(); clipboard->setText(deckHash); @@ -541,60 +480,6 @@ void UserContextMenu::execChat(const QString &userName) emit openMessageDialog(userName, true); } -QList UserContextMenu::inviteOptionsForUser(const QString &userName) const -{ - if (!gameInviteLinkProvider) { - return {}; - } - const QList options = gameInviteLinkProvider(); - QList result; - for (const GameInviteOption &option : options) { - // Buddy-only games accept invites only from their creator, and only to - // users on the creator's buddy list. - if (option.onlyBuddies && - (option.creatorName != userListProxy->getOwnUsername() || !userListProxy->isUserBuddy(userName))) { - continue; - } - result.append(option); - } - return result; -} - -void UserContextMenu::execInvite(const QString &userName) -{ - const QList options = inviteOptionsForUser(userName); - if (options.isEmpty()) { - return; - } - - if (options.size() == 1) { - execInvite(userName, options.first()); - return; - } - - // More than one game in the room — let the user pick which one to invite to. - auto *menu = new QMenu(static_cast(parent())); - for (const GameInviteOption &option : options) { - QAction *action = menu->addAction(option.label); - connect(action, &QAction::triggered, this, [this, userName, option] { execInvite(userName, option); }); - } - menu->setAttribute(Qt::WA_DeleteOnClose); - menu->popup(QCursor::pos()); -} - -void UserContextMenu::execInvite(const QString &userName, const GameInviteOption &option) -{ - // Name the game by description first, then its id — "Join my game 'Magic' - // (#123)" — so a description-less fallback still identifies the game. - // The multi-arg .arg() overloads replace in a single pass, so a description - // containing "%…" cannot corrupt later placeholders. - const QString prefix = - option.description.isEmpty() - ? tr("Join my game (#%1):").arg(option.gameId) - : tr("Join my game \"%1\" (#%2):").arg(option.description, QString::number(option.gameId)); - tabSupervisor->sendInviteToUser(userName, prefix + " " + option.url); -} - void UserContextMenu::execDetails(const QString &userName) { auto *w = new UserInfoBox(client, false, static_cast(parent()), @@ -612,11 +497,6 @@ void UserContextMenu::execShowGames(const QString &userName) client->sendCommand(pend); } -void UserContextMenu::execViewPublicDecks(const QString &userName) -{ - tabSupervisor->openTabPublicDecks(userName); -} - void UserContextMenu::execAddToBuddy(const QString &userName) { Command_AddToList cmd; @@ -638,15 +518,7 @@ void UserContextMenu::execAddToIgnore(const QString &userName) Command_AddToList cmd; cmd.set_list("ignore"); cmd.set_user_name(userName.toStdString()); - PendingCommand *pend = client->prepareSessionCommand(cmd); - connect(pend, &PendingCommand::finished, this, - [this, userName](const Response &response, const CommandContainer &, const QVariant &) { - if (response.response_code() == Response::RespOk) { - QMessageBox::information(static_cast(parent()), tr("Ignore list"), - tr("%1 has been added to your ignore list.").arg(userName)); - } - }); - client->sendCommand(pend); + client->sendCommand(client->prepareSessionCommand(cmd)); } void UserContextMenu::execRemoveFromIgnore(const QString &userName) @@ -715,11 +587,6 @@ void UserContextMenu::execAdminNotes(const QString &userName) client->sendCommand(pend); } -void UserContextMenu::execInvestigateUser(const QString &userName) -{ - tabSupervisor->openTabModeration(userName); -} - void UserContextMenu::execAdjustMod(const QString &userName, bool shouldBeMod) { Command_AdjustMod cmd; @@ -738,14 +605,4 @@ void UserContextMenu::execAdjustJudge(const QString &userName, bool shouldBeJudg PendingCommand *pend = client->prepareAdminCommand(cmd); connect(pend, &PendingCommand::finished, this, &UserContextMenu::adjustMod_processUserResponse); client->sendCommand(pend); -} - -void UserContextMenu::execAdjustDeveloper(const QString &userName, bool shouldBeDeveloper) -{ - Command_AdjustMod cmd; - cmd.set_user_name(userName.toStdString()); - cmd.set_should_be_developer(shouldBeDeveloper); - PendingCommand *pend = client->prepareAdminCommand(cmd); - connect(pend, &PendingCommand::finished, this, &UserContextMenu::adjustMod_processUserResponse); - client->sendCommand(pend); -} +} \ No newline at end of file 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 0922eae94..00fdc51fe 100644 --- a/cockatrice/src/interface/widgets/server/user/user_context_menu.h +++ b/cockatrice/src/interface/widgets/server/user/user_context_menu.h @@ -7,12 +7,9 @@ #ifndef USER_CONTEXT_MENU_H #define USER_CONTEXT_MENU_H -#include "../../interface/widgets/server/game_link.h" - -#include #include -#include #include + class AbstractGame; class UserListProxy; class AbstractClient; @@ -37,20 +34,15 @@ private: QAction *aUserName; QAction *aDetails; QAction *aShowGames; - QAction *aViewPublicDecks; QAction *aChat; QAction *aAddToBuddyList, *aRemoveFromBuddyList; QAction *aAddToIgnoreList, *aRemoveFromIgnoreList; QAction *aKick; - QAction *aReport; QAction *aBan, *aBanHistory; QAction *aPromoteToMod, *aDemoteFromMod; QAction *aPromoteToJudge, *aDemoteFromJudge; - QAction *aPromoteToDeveloper, *aDemoteFromDeveloper; QAction *aWarnUser, *aWarnHistory; QAction *aGetAdminNotes; - std::function()> gameInviteLinkProvider; - QAction *aInvestigateUser; signals: void openMessageDialog(const QString &userName, bool focus); private slots: @@ -88,31 +80,11 @@ public: return userListProxy; } - void setGameInviteLinkProvider(std::function()> provider) - { - gameInviteLinkProvider = std::move(provider); - } - - /** - * The games currently inviteable for @p userName, honoring the room's - * buddy-only setting (the inviter must be the game's creator and the - * target a buddy of theirs). Empty when there is no live provider. - */ - QList inviteOptionsForUser(const QString &userName) const; - - /** Whether at least one invite link is currently available for @p userName. */ - bool hasGameInviteLink(const QString &userName) const - { - return !inviteOptionsForUser(userName).isEmpty(); - } - // Individual action entry points — used by UserInfoPopup to trigger // actions without re-running the full context menu flow. void execChat(const QString &userName); - void execInvite(const QString &userName); void execDetails(const QString &userName); void execShowGames(const QString &userName); - void execViewPublicDecks(const QString &userName); void execAddToBuddy(const QString &userName); void execRemoveFromBuddy(const QString &userName); void execAddToIgnore(const QString &userName); @@ -123,13 +95,8 @@ public: void execBanHistory(const QString &userName); void execWarnHistory(const QString &userName); void execAdminNotes(const QString &userName); - void execInvestigateUser(const QString &userName); void execAdjustMod(const QString &userName, bool shouldBeMod); void execAdjustJudge(const QString &userName, bool shouldBeJudge); - void execAdjustDeveloper(const QString &userName, bool shouldBeDeveloper); - -private: - void execInvite(const QString &userName, const GameInviteOption &option); }; #endif diff --git a/cockatrice/src/interface/widgets/server/user/user_info_box.cpp b/cockatrice/src/interface/widgets/server/user/user_info_box.cpp index 3d89cecf5..416cd42e3 100644 --- a/cockatrice/src/interface/widgets/server/user/user_info_box.cpp +++ b/cockatrice/src/interface/widgets/server/user/user_info_box.cpp @@ -122,8 +122,6 @@ void UserInfoBox::updateInfo(const ServerInfo_User &user) QString userLevelText; if (userLevel.testFlag(ServerInfo_User::IsAdmin)) { userLevelText = tr("Administrator"); - } else if (userLevel.testFlag(ServerInfo_User::IsDeveloper)) { - userLevelText = tr("Developer"); } else if (userLevel.testFlag(ServerInfo_User::IsModerator)) { userLevelText = tr("Moderator"); } else if (userLevel.testFlag(ServerInfo_User::IsRegistered)) { @@ -321,7 +319,6 @@ void UserInfoBox::actBannerCard() if (hasUserInfo && currentUserInfo.has_card_art_params()) { const auto &cap = currentUserInfo.card_art_params(); initial.cardName = QString::fromStdString(cap.card_name()); - initial.cardProviderId = QString::fromStdString(cap.card_provider_id()); initial.marginPctL = cap.margin_pct_l(); initial.marginPctR = cap.margin_pct_r(); initial.verticalOffset = cap.vertical_offset(); diff --git a/cockatrice/src/interface/widgets/server/user/user_info_popup.cpp b/cockatrice/src/interface/widgets/server/user/user_info_popup.cpp index 8be76eea0..edb95f2df 100644 --- a/cockatrice/src/interface/widgets/server/user/user_info_popup.cpp +++ b/cockatrice/src/interface/widgets/server/user/user_info_popup.cpp @@ -1,8 +1,6 @@ #include "user_info_popup.h" -#include "../../cards/art_crop_attribution.h" #include "../../interface/pixel_map_generator.h" -#include "../../interface/theme_manager.h" #include "../../interface/widgets/tabs/tab_supervisor.h" #include "user_list_painter.h" @@ -19,48 +17,11 @@ #include #include #include -#include #include #include #include #include -/// Qt stylesheets accept #aarrggbb, which is QColor::name(QColor::HexArgb). -static QString colorStr(const QColor &color) -{ - return color.name(QColor::HexArgb); -} - -PopupTheme PopupTheme::fromPalette(const QPalette &palette, bool dark) -{ - PopupTheme t; - t.dark = dark; - const QColor window = palette.color(QPalette::Window); - const QColor base = palette.color(QPalette::Base); - const QColor mid = palette.color(QPalette::Mid); - const QColor text = palette.color(QPalette::Text); - const QColor disabledText = palette.color(QPalette::Disabled, QPalette::Text); - const QColor highlight = palette.color(QPalette::Highlight); - - t.bg = window; - t.border = mid; - t.text = text; - t.subText = disabledText; - t.statusText = disabledText; - t.buttonBg = base; - t.buttonBorder = mid; - t.buttonHover = UserListPainter::blend(base, highlight, dark ? 0.30 : 0.12); - t.buttonPressed = UserListPainter::blend(base, highlight, dark ? 0.50 : 0.25); - t.buttonDisabled = disabledText; - t.closeBg = UserListPainter::blend(base, window, 0.5); - t.closeHover = dark ? QColor(200, 50, 50) : UserListPainter::blend(QColor(200, 50, 50), base, 0.45); - t.gamesRow = base; - t.gamesSelected = UserListPainter::blend(base, highlight, dark ? 0.45 : 0.30); - t.gamesSeparator = mid; - t.gamesSeparator.setAlpha(90); - return t; -} - // ── Compact game row delegate ───────────────────────────────────────────────── class PopupGameDelegate : public QStyledItemDelegate @@ -87,14 +48,8 @@ public: const QRect rect = option.rect; const ServerInfo_Game game = var.value(); const bool selected = option.state & QStyle::State_Selected; - const bool dark = themeManager && themeManager->isDarkModeActive(); - // The widget palette can be stale after a runtime theme change, so the - // rows are styled from the application palette (always current). - const QPalette pal = qApp->palette(); - const QColor base = pal.color(QPalette::Base); - const QColor highlight = pal.color(QPalette::Highlight); - p->fillRect(rect, selected ? UserListPainter::blend(base, highlight, dark ? 0.45 : 0.30) : base); + p->fillRect(rect, selected ? QColor(35, 45, 62) : QColor(14, 18, 26)); // State colour dot const QColor dot = game.started() ? QColor(239, 68, 68) @@ -109,7 +64,7 @@ public: QFont tf = option.font; tf.setBold(true); p->setFont(tf); - p->setPen(pal.color(QPalette::Text)); + p->setPen(QColor(205, 215, 230)); const int textX = rect.left() + 26; const int countW = 52; const int titleW = rect.width() - textX - countW - 6; @@ -119,15 +74,13 @@ public: // Player count const bool full = game.player_count() >= game.max_players(); p->setFont(option.font); - p->setPen(full ? QColor(249, 115, 22) : pal.color(QPalette::Disabled, QPalette::Text)); + p->setPen(full ? QColor(249, 115, 22) : QColor(110, 128, 150)); p->drawText(QRect(rect.right() - countW - 4, rect.top(), countW, rect.height()), Qt::AlignVCenter | Qt::AlignRight, QStringLiteral("%1/%2").arg(game.player_count()).arg(game.max_players())); // Row separator - QColor separator = pal.color(QPalette::Mid); - separator.setAlpha(90); - p->setPen(separator); + p->setPen(QColor(24, 32, 44)); p->drawLine(rect.bottomLeft(), rect.bottomRight()); p->restore(); @@ -142,27 +95,17 @@ UserInfoHeaderWidget::UserInfoHeaderWidget(QWidget *parent) : QWidget(parent) setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); } -void UserInfoHeaderWidget::setUserData(const ServerInfo_User &_user, - bool _online, - const QPixmap &_avatar, - const QPixmap &_cardArt, - const CardArtParams &_params) +void UserInfoHeaderWidget::setUserData(const ServerInfo_User &user, + bool online, + const QPixmap &avatar, + const QPixmap &cardArt, + const CardArtParams ¶ms) { - user = _user; - online = _online; - avatar = _avatar; - cardArt = _cardArt; - params = _params; - - attribution.clear(); - if (user.has_card_art_params()) { - const ExactCard card = - CardDatabaseManager::query()->getCard({QString::fromStdString(user.card_art_params().card_name()), - QString::fromStdString(user.card_art_params().card_provider_id())}); - if (card) { - attribution = buildArtAttribution(card); - } - } + m_user = user; + m_online = online; + m_avatar = avatar; + m_cardArt = cardArt; + m_params = params; update(); } @@ -172,48 +115,31 @@ void UserInfoHeaderWidget::paintEvent(QPaintEvent *) p.setRenderHints(QPainter::Antialiasing | QPainter::SmoothPixmapTransform | QPainter::TextAntialiasing); const QRect rect = this->rect(); - const UserLevelFlags level(user.user_level()); - const QString userName = QString::fromStdString(user.name()); - const QString privLevel = QString::fromStdString(user.privlevel()); + const UserLevelFlags level(m_user.user_level()); + const QString userName = QString::fromStdString(m_user.name()); + const QString privLevel = QString::fromStdString(m_user.privlevel()); - const bool dark = themeManager && themeManager->isDarkModeActive(); - const UserListPainter::Style style = UserListPainter::resolveStyle(qApp->palette(), dark); - - // Palette surface - { - QLinearGradient bg(0, 0, rect.width(), 0); - bg.setColorAt(0, style.cardStart); - bg.setColorAt(1, style.cardEnd); - p.fillRect(rect, bg); - } + // Dark base + p.fillRect(rect, QColor(14, 18, 26)); // ── Card art background ─────────────────────────────────────────────────── - if (!cardArt.isNull()) { - // Same DPR normalization as UserListPainter::drawCardArt: the cache - // carries screen scaled pixmaps on HiDPI displays, the math below is - // in raw pixels. - QPixmap art = cardArt; - art.setDevicePixelRatio(1.0); - + if (!m_cardArt.isNull()) { const int w = rect.width(); const int h = rect.height(); - const int mL = qRound(w * params.marginPctL); - const int mR = qRound(w * params.marginPctR); + const int mL = qRound(w * m_params.marginPctL); + const int mR = qRound(w * m_params.marginPctR); const int dW = w - mL - mR; - const double base = qMax(double(dW) / art.width(), double(h) / art.height()); - const double scale = base * params.zoom; - const int sW = qRound(art.width() * scale); - const int sH = qRound(art.height() * scale); + const double base = qMax(double(dW) / m_cardArt.width(), double(h) / m_cardArt.height()); + const double scale = base * m_params.zoom; + const int sW = qRound(m_cardArt.width() * scale); + const int sH = qRound(m_cardArt.height() * scale); - const QPixmap scaled = art.scaled(sW, sH, Qt::IgnoreAspectRatio, Qt::SmoothTransformation); - // Clamp against stored zoom < 1, which can push srcX negative and silently - // underfill the strip with transparent padding - const int safeSrcX = qBound(0, (sW - dW) / 2, qMax(0, sW - dW)); - const int safeSrcY = qBound(0, qRound((sH - h) * params.verticalOffset), qMax(0, sH - h)); + const QPixmap scaled = m_cardArt.scaled(sW, sH, Qt::IgnoreAspectRatio, Qt::SmoothTransformation); + const int srcX = (sW - dW) / 2; + const int srcY = qBound(0, qRound((sH - h) * m_params.verticalOffset), qMax(0, sH - h)); - QImage img = - scaled.copy(safeSrcX, safeSrcY, dW, h).toImage().convertToFormat(QImage::Format_ARGB32_Premultiplied); + QImage img = scaled.copy(srcX, srcY, dW, h).toImage().convertToFormat(QImage::Format_ARGB32_Premultiplied); { QPainter mask(&img); mask.setCompositionMode(QPainter::CompositionMode_DestinationIn); @@ -229,14 +155,12 @@ void UserInfoHeaderWidget::paintEvent(QPaintEvent *) p.setOpacity(1.0); } - // Bottom gradient overlay so avatar and text are always legible. The scrim - // is the palette's Window color so it reads naturally in either scheme. + // Bottom gradient overlay so avatar and text are always legible { - const QColor scrim = qApp->palette().color(QPalette::Window); QLinearGradient ov(0, 0, 0, rect.height()); - ov.setColorAt(0.0, QColor(scrim.red(), scrim.green(), scrim.blue(), 0)); - ov.setColorAt(0.55, QColor(scrim.red(), scrim.green(), scrim.blue(), 110)); - ov.setColorAt(1.0, QColor(scrim.red(), scrim.green(), scrim.blue(), 230)); + ov.setColorAt(0.0, QColor(14, 18, 26, 0)); + ov.setColorAt(0.55, QColor(14, 18, 26, 110)); + ov.setColorAt(1.0, QColor(14, 18, 26, 230)); p.fillRect(rect, ov); } @@ -245,9 +169,6 @@ void UserInfoHeaderWidget::paintEvent(QPaintEvent *) if (level.testFlag(ServerInfo_User::IsAdmin)) { return QColor(245, 158, 11); } - if (level.testFlag(ServerInfo_User::IsDeveloper)) { - return QColor(185, 28, 28); - } if (level.testFlag(ServerInfo_User::IsModerator)) { return QColor(59, 130, 246); } @@ -266,20 +187,20 @@ void UserInfoHeaderWidget::paintEvent(QPaintEvent *) p.save(); p.setClipPath(clip); - if (!avatar.isNull()) { - p.drawPixmap(ar, avatar.scaled(ar.size(), Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation)); + if (!m_avatar.isNull()) { + p.drawPixmap(ar, m_avatar.scaled(ar.size(), Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation)); } else { p.setPen(Qt::NoPen); - p.setBrush(UserListPainter::blend(accent, style.base, dark ? 0.45 : 0.72)); + p.setBrush(accent.darker(200)); p.drawEllipse(ar); const QPixmap pawn = - UserLevelPixmapGenerator::generatePixmap(AvatarPawnSize, level, user.pawn_colors(), false, privLevel); + UserLevelPixmapGenerator::generatePixmap(AvatarPawnSize, level, m_user.pawn_colors(), false, privLevel); p.drawPixmap(ar.center().x() - AvatarPawnSize / 2, ar.center().y() - AvatarPawnSize / 2, pawn); } p.restore(); // Status ring - p.setPen(QPen(online ? QColor(34, 197, 94) : style.ringOffline, 2.5)); + p.setPen(QPen(m_online ? QColor(34, 197, 94) : QColor(70, 80, 95), 2.5)); p.setBrush(Qt::NoBrush); p.drawEllipse(QRectF(ar).adjusted(-1.25, -1.25, 1.25, 1.25)); @@ -291,7 +212,7 @@ void UserInfoHeaderWidget::paintEvent(QPaintEvent *) nf.setBold(true); nf.setPointSizeF(nf.pointSizeF() * 1.12); p.setFont(nf); - p.setPen(online ? style.textOnline : style.textOffline); + p.setPen(m_online ? QColor(220, 228, 240) : QColor(90, 100, 115)); p.drawText(QRect(tx, ay, tw, AvatarSize / 2 + 4), Qt::AlignBottom | Qt::AlignLeft, QFontMetrics(nf).elidedText(userName, Qt::ElideRight, tw)); @@ -303,8 +224,6 @@ void UserInfoHeaderWidget::paintEvent(QPaintEvent *) } badge; if (level.testFlag(ServerInfo_User::IsAdmin)) { badge = {"ADMIN", QColor(245, 158, 11)}; - } else if (level.testFlag(ServerInfo_User::IsDeveloper)) { - badge = {"DEV", QColor(185, 28, 28)}; } else if (level.testFlag(ServerInfo_User::IsModerator)) { badge = {"MOD", QColor(59, 130, 246)}; } else if (level.testFlag(ServerInfo_User::IsJudge)) { @@ -324,184 +243,143 @@ void UserInfoHeaderWidget::paintEvent(QPaintEvent *) const int bw = bfm.horizontalAdvance(badge.text) + 10; const QRect br(tx, ay + AvatarSize / 2 + 6, bw, 15); p.setPen(Qt::NoPen); - p.setBrush(UserListPainter::blend(badge.color, style.base, dark ? 0.55 : 0.78)); + p.setBrush(badge.color.darker(160)); p.drawRoundedRect(br, 3, 3); - p.setPen(dark ? UserListPainter::blend(badge.color, Qt::white, 0.5) - : UserListPainter::blend(badge.color, Qt::black, 0.35)); + p.setPen(badge.color.lighter(150)); p.drawText(br, Qt::AlignCenter, badge.text); } - - // The painter font at this point depends on whether a badge was drawn - // (badge font vs username font), so pin an explicit font for the pill. - p.setFont(font()); - - // Only show the attribution when there is actually art on screen: on a - // cache miss cardArt is null and the pill would float over the plain - // header with no art behind it. - if (!cardArt.isNull()) { - paintArtAttribution(p, rect, attribution, Qt::AlignRight | Qt::AlignBottom, 0.8); - } } // ── UserInfoPopup ───────────────────────────────────────────────────────────── -UserInfoPopup::UserInfoPopup(TabSupervisor *_ts, - AbstractClient *_client, - const QMap *_avatarCache, - const QMap *_cardArtCache, - const QMap *_cardArtParamsMap, +UserInfoPopup::UserInfoPopup(TabSupervisor *ts, + AbstractClient *client, + const QMap *avatarCache, + const QMap *cardArtCache, + const QMap *cardArtParamsMap, QWidget *parent) - : QFrame(parent, Qt::Tool | Qt::FramelessWindowHint), ts(_ts), client(_client), avatarCache(_avatarCache), - cardArtCache(_cardArtCache), cardArtParamsMap(_cardArtParamsMap) + : QFrame(parent, Qt::Tool | Qt::FramelessWindowHint), m_ts(ts), m_client(client), m_avatarCache(avatarCache), + m_cardArtCache(cardArtCache), m_cardArtParamsMap(cardArtParamsMap) { setAttribute(Qt::WA_ShowWithoutActivating); setFixedWidth(PopupWidth); setFrameShape(QFrame::NoFrame); buildUi(); - - // Restyle the popup chrome when the theme or its color scheme changes. - if (themeManager) { - connect(themeManager, &ThemeManager::themeChanged, this, &UserInfoPopup::applyTheme); - } } void UserInfoPopup::buildUi() { + setStyleSheet(QStringLiteral("UserInfoPopup {" + " background:#0e1218;" + " border:1px solid #1e2838;" + " border-radius:8px;" + "}")); + auto *root = new QVBoxLayout(this); root->setContentsMargins(0, 0, 0, 0); root->setSpacing(0); // Header - header = new UserInfoHeaderWidget(this); - root->addWidget(header); + m_header = new UserInfoHeaderWidget(this); + root->addWidget(m_header); - // Action area, rebuilt per user - actionArea = new QWidget(this); - root->addWidget(actionArea); + // Action area — rebuilt per user + m_actionArea = new QWidget(this); + m_actionArea->setStyleSheet(QStringLiteral("background:#0e1218;")); + root->addWidget(m_actionArea); // Thin separator - separator = new QFrame(this); - separator->setFrameShape(QFrame::HLine); - root->addWidget(separator); + auto *sep = new QFrame(this); + sep->setFrameShape(QFrame::HLine); + sep->setStyleSheet(QStringLiteral("color:#1a2434; margin: 0 8px;")); + root->addWidget(sep); // Games header row auto *gh = new QHBoxLayout; gh->setContentsMargins(10, 4, 8, 2); - gamesLabel = new QLabel(tr("Games"), this); - gh->addWidget(gamesLabel); + auto *gl = new QLabel(tr("Games"), this); + gl->setStyleSheet(QStringLiteral("color:#6882a0; font-size:11px; font-weight:bold; background:transparent;")); + gh->addWidget(gl); gh->addStretch(); - refreshBtn = new QPushButton(QStringLiteral("↻"), this); - refreshBtn->setFixedSize(20, 20); - refreshBtn->setFlat(true); - connect(refreshBtn, &QPushButton::clicked, this, &UserInfoPopup::refreshGames); - gh->addWidget(refreshBtn); + m_refreshBtn = new QPushButton(QStringLiteral("↻"), this); + m_refreshBtn->setFixedSize(20, 20); + m_refreshBtn->setFlat(true); + m_refreshBtn->setStyleSheet( + QStringLiteral("QPushButton{color:#6882a0;border:none;font-size:14px;background:transparent;}" + "QPushButton:hover{color:white;}")); + connect(m_refreshBtn, &QPushButton::clicked, this, &UserInfoPopup::refreshGames); + gh->addWidget(m_refreshBtn); root->addLayout(gh); // Status label - gamesStatus = new QLabel(this); - gamesStatus->setAlignment(Qt::AlignCenter); - root->addWidget(gamesStatus); + m_gamesStatus = new QLabel(this); + m_gamesStatus->setAlignment(Qt::AlignCenter); + m_gamesStatus->setStyleSheet( + QStringLiteral("color:#3a4a5e; font-size:11px; padding:10px; background:transparent;")); + root->addWidget(m_gamesStatus); // Games list - gamesModel = new QStandardItemModel(this); - gamesView = new QListView(this); - gamesView->setModel(gamesModel); - gamesView->setItemDelegate(new PopupGameDelegate(gamesView)); - gamesView->setFrameShape(QFrame::NoFrame); - gamesView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - gamesView->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); - gamesView->setMaximumHeight(220); - gamesView->setContextMenuPolicy(Qt::CustomContextMenu); - connect(gamesView, &QListView::customContextMenuRequested, this, &UserInfoPopup::onGamesContextMenu); + m_gamesModel = new QStandardItemModel(this); + m_gamesView = new QListView(this); + m_gamesView->setModel(m_gamesModel); + m_gamesView->setItemDelegate(new PopupGameDelegate(m_gamesView)); + m_gamesView->setFrameShape(QFrame::NoFrame); + m_gamesView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + m_gamesView->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); + m_gamesView->setMaximumHeight(220); + m_gamesView->setStyleSheet(QStringLiteral("QListView{background:#0e1218;border:none;}" + "QListView::item:selected{background:#232e42;}")); + m_gamesView->setContextMenuPolicy(Qt::CustomContextMenu); + connect(m_gamesView, &QListView::customContextMenuRequested, this, &UserInfoPopup::onGamesContextMenu); - root->addWidget(gamesView); + root->addWidget(m_gamesView); - // Close button, positioned absolutely in the top right corner - closeBtn = new QPushButton(QStringLiteral("✕"), this); - closeBtn->setFixedSize(22, 22); - closeBtn->setFlat(true); - connect(closeBtn, &QPushButton::clicked, this, &UserInfoPopup::closeRequested); - - applyTheme(); -} - -void UserInfoPopup::applyTheme() -{ - const bool dark = themeManager && themeManager->isDarkModeActive(); - theme = PopupTheme::fromPalette(qApp->palette(), dark); - - setStyleSheet(QStringLiteral("UserInfoPopup {" - " background:%1;" - " border:1px solid %2;" - " border-radius:8px;" - "}") - .arg(colorStr(theme.bg), colorStr(theme.border))); - - actionArea->setStyleSheet(QStringLiteral("background:%1;").arg(colorStr(theme.bg))); - - separator->setStyleSheet(QStringLiteral("color:%1; margin: 0 8px;").arg(colorStr(theme.border))); - - gamesLabel->setStyleSheet(QStringLiteral("color:%1; font-size:11px; font-weight:bold; background:transparent;") - .arg(colorStr(theme.subText))); - - refreshBtn->setStyleSheet(QStringLiteral("QPushButton{color:%1;border:none;font-size:14px;background:transparent;}" - "QPushButton:hover{color:%2;}") - .arg(colorStr(theme.subText), colorStr(theme.text))); - - gamesStatus->setStyleSheet(QStringLiteral("color:%1; font-size:11px; padding:10px; background:transparent;") - .arg(colorStr(theme.statusText))); - - gamesView->setStyleSheet(QStringLiteral("QListView{background:%1;border:none;}" - "QListView::item:selected{background:%2;}") - .arg(colorStr(theme.gamesRow), colorStr(theme.gamesSelected))); - - closeBtn->setStyleSheet( - QStringLiteral("QPushButton{background:%1;color:%2;" - "border:none;border-radius:11px;font-size:10px;}" - "QPushButton:hover{color:%3;background:%4;}") - .arg(colorStr(theme.closeBg), colorStr(theme.subText), colorStr(theme.text), colorStr(theme.closeHover))); - - header->update(); + // Close button — positioned absolutely in the top-right corner + m_closeBtn = new QPushButton(QStringLiteral("✕"), this); + m_closeBtn->setFixedSize(22, 22); + m_closeBtn->setFlat(true); + m_closeBtn->setStyleSheet(QStringLiteral("QPushButton{background:rgba(14,18,26,180);color:#607080;" + "border:none;border-radius:11px;font-size:10px;}" + "QPushButton:hover{color:white;background:rgba(200,50,50,200);}")); + connect(m_closeBtn, &QPushButton::clicked, this, &UserInfoPopup::closeRequested); } // ── Action button factory ───────────────────────────────────────────────────── -static QPushButton *makeBtn(const QString &label, const QString &tip, QWidget *p, const PopupTheme &t) +static QPushButton *makeBtn(const QString &label, const QString &tip, QWidget *p) { auto *b = new QPushButton(label, p); b->setToolTip(tip); b->setFixedHeight(26); b->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); b->setStyleSheet(QStringLiteral("QPushButton{" - " background:%1;color:%2;border:1px solid %3;" + " background:#192030;color:#b8c8de;border:1px solid #263040;" " border-radius:4px;font-size:11px;padding:0 4px;" "}" - "QPushButton:hover{background:%4;color:%5;}" - "QPushButton:pressed{background:%6;}" - "QPushButton:disabled{color:%7;border-color:%3;}") - .arg(colorStr(t.buttonBg), colorStr(t.text), colorStr(t.buttonBorder), colorStr(t.buttonHover), - colorStr(t.text), colorStr(t.buttonPressed), colorStr(t.buttonDisabled))); + "QPushButton:hover{background:#223050;color:white;}" + "QPushButton:pressed{background:#162030;}" + "QPushButton:disabled{color:#384858;border-color:#192030;}")); return b; } void UserInfoPopup::rebuildActionButtons(const ServerInfo_User &userInfo, bool online, bool isBuddy, bool isIgnored) { // Clear previous contents - delete actionArea->layout(); - const auto old = actionArea->findChildren(QString{}, Qt::FindDirectChildrenOnly); + delete m_actionArea->layout(); + const auto old = m_actionArea->findChildren(QString{}, Qt::FindDirectChildrenOnly); for (auto *w : old) { w->deleteLater(); } const QString name = QString::fromStdString(userInfo.name()); - const auto ownLevel = UserLevelFlags(ts->getUserInfo()->user_level()); - const bool isSelf = (name == QString::fromStdString(ts->getUserInfo()->name())); + const auto ownLevel = UserLevelFlags(m_ts->getUserInfo()->user_level()); + const bool isSelf = (name == QString::fromStdString(m_ts->getUserInfo()->name())); const bool isMod = ownLevel.testFlag(ServerInfo_User::IsModerator); const bool isAdmin = ownLevel.testFlag(ServerInfo_User::IsAdmin); const auto their = UserLevelFlags(userInfo.user_level()); const bool isReg = their.testFlag(ServerInfo_User::IsRegistered); - auto *grid = new QGridLayout(actionArea); + auto *grid = new QGridLayout(m_actionArea); grid->setContentsMargins(8, 6, 8, 6); grid->setSpacing(4); @@ -516,44 +394,37 @@ void UserInfoPopup::rebuildActionButtons(const ServerInfo_User &userInfo, bool o }; // ── Always visible ──────────────────────────────────────────────────────── - auto *chat = makeBtn(tr("Chat"), tr("Open private chat"), actionArea, theme); + auto *chat = makeBtn(tr("Chat"), tr("Open private chat"), m_actionArea); chat->setEnabled(!isSelf && online); connect(chat, &QPushButton::clicked, this, [this, name] { emit chatRequested(name); }); add(chat); - auto *prof = makeBtn(tr("Profile"), tr("View user profile"), actionArea, theme); + auto *prof = makeBtn(tr("Profile"), tr("View user profile"), m_actionArea); connect(prof, &QPushButton::clicked, this, [this, name] { emit detailsRequested(name); }); add(prof); - auto *games = makeBtn(tr("Games"), tr("Show this user's games"), actionArea, theme); + auto *games = makeBtn(tr("Games"), tr("Show this user's games"), m_actionArea); games->setEnabled(!isSelf && online); connect(games, &QPushButton::clicked, this, [this, name] { emit showGamesRequested(name); }); add(games); - // ── Invite (only while the inviter has a joinable game for this user) ──── - if (!isSelf && online && gameInviteAvailable && gameInviteAvailable(name)) { - auto *invite = makeBtn(tr("Invite"), tr("Invite to your game"), actionArea, theme); - connect(invite, &QPushButton::clicked, this, [this, name] { emit inviteRequested(name); }); - add(invite); - } - // ── Buddy / ignore (registered users only) ──────────────────────────────── if (!isSelf && isReg) { if (isBuddy) { - auto *b = makeBtn(tr("− Buddy"), tr("Remove from buddy list"), actionArea, theme); + auto *b = makeBtn(tr("− Buddy"), tr("Remove from buddy list"), m_actionArea); connect(b, &QPushButton::clicked, this, [this, name] { emit removeBuddyRequested(name); }); add(b); } else { - auto *b = makeBtn(tr("+ Buddy"), tr("Add to buddy list"), actionArea, theme); + auto *b = makeBtn(tr("+ Buddy"), tr("Add to buddy list"), m_actionArea); connect(b, &QPushButton::clicked, this, [this, name] { emit addBuddyRequested(name); }); add(b); } if (isIgnored) { - auto *b = makeBtn(tr("− Ignore"), tr("Remove from ignore list"), actionArea, theme); + auto *b = makeBtn(tr("− Ignore"), tr("Remove from ignore list"), m_actionArea); connect(b, &QPushButton::clicked, this, [this, name] { emit removeIgnoreRequested(name); }); add(b); } else { - auto *b = makeBtn(tr("+ Ignore"), tr("Add to ignore list"), actionArea, theme); + auto *b = makeBtn(tr("+ Ignore"), tr("Add to ignore list"), m_actionArea); connect(b, &QPushButton::clicked, this, [this, name] { emit addIgnoreRequested(name); }); add(b); } @@ -566,10 +437,10 @@ void UserInfoPopup::rebuildActionButtons(const ServerInfo_User &userInfo, bool o col = 0; } // start mod section on a fresh row - auto *ban = makeBtn(tr("Ban"), tr("Ban from server"), actionArea, theme); - auto *warn = makeBtn(tr("Warn"), tr("Warn user"), actionArea, theme); - auto *bLog = makeBtn(tr("Ban log"), tr("View ban history"), actionArea, theme); - auto *wLog = makeBtn(tr("Warn log"), tr("View warning history"), actionArea, theme); + auto *ban = makeBtn(tr("Ban"), tr("Ban from server"), m_actionArea); + auto *warn = makeBtn(tr("Warn"), tr("Warn user"), m_actionArea); + auto *bLog = makeBtn(tr("Ban log"), tr("View ban history"), m_actionArea); + auto *wLog = makeBtn(tr("Warn log"), tr("View warning history"), m_actionArea); connect(ban, &QPushButton::clicked, this, [this, name] { emit banRequested(name); }); connect(warn, &QPushButton::clicked, this, [this, name] { emit warnRequested(name); }); connect(bLog, &QPushButton::clicked, this, [this, name] { emit banHistoryRequested(name); }); @@ -582,31 +453,31 @@ void UserInfoPopup::rebuildActionButtons(const ServerInfo_User &userInfo, bool o // ── Admin actions ───────────────────────────────────────────────────────── if (!isSelf && isAdmin) { - auto *notes = makeBtn(tr("Notes"), tr("View admin notes"), actionArea, theme); + auto *notes = makeBtn(tr("Notes"), tr("View admin notes"), m_actionArea); connect(notes, &QPushButton::clicked, this, [this, name] { emit adminNotesRequested(name); }); add(notes); if (their.testFlag(ServerInfo_User::IsModerator)) { - auto *b = makeBtn(tr("− Mod"), tr("Demote from moderator"), actionArea, theme); + auto *b = makeBtn(tr("− Mod"), tr("Demote from moderator"), m_actionArea); connect(b, &QPushButton::clicked, this, [this, name] { emit demoteFromModRequested(name); }); add(b); } else if (isReg) { - auto *b = makeBtn(tr("+ Mod"), tr("Promote to moderator"), actionArea, theme); + auto *b = makeBtn(tr("+ Mod"), tr("Promote to moderator"), m_actionArea); connect(b, &QPushButton::clicked, this, [this, name] { emit promoteToModRequested(name); }); add(b); } if (their.testFlag(ServerInfo_User::IsJudge)) { - auto *b = makeBtn(tr("− Judge"), tr("Demote from judge"), actionArea, theme); + auto *b = makeBtn(tr("− Judge"), tr("Demote from judge"), m_actionArea); connect(b, &QPushButton::clicked, this, [this, name] { emit demoteFromJudgeRequested(name); }); add(b); } else if (isReg) { - auto *b = makeBtn(tr("+ Judge"), tr("Promote to judge"), actionArea, theme); + auto *b = makeBtn(tr("+ Judge"), tr("Promote to judge"), m_actionArea); connect(b, &QPushButton::clicked, this, [this, name] { emit promoteToJudgeRequested(name); }); add(b); } } - actionArea->adjustSize(); + m_actionArea->adjustSize(); } void UserInfoPopup::updateActionButtons(const ServerInfo_User &userInfo, bool online, bool isBuddy, bool isIgnored) @@ -617,7 +488,7 @@ void UserInfoPopup::updateActionButtons(const ServerInfo_User &userInfo, bool on void UserInfoPopup::onGamesContextMenu(const QPoint &pos) { - const QModelIndex idx = gamesView->indexAt(pos); + const QModelIndex idx = m_gamesView->indexAt(pos); if (!idx.isValid()) { return; } @@ -630,9 +501,8 @@ void UserInfoPopup::onGamesContextMenu(const QPoint &pos) QMenu menu(this); menu.setStyleSheet( - QStringLiteral("QMenu{background:%1;color:%2;border:1px solid %3;border-radius:4px;}" - "QMenu::item:selected{background:%4;}") - .arg(colorStr(theme.bg), colorStr(theme.text), colorStr(theme.border), colorStr(theme.buttonHover))); + QStringLiteral("QMenu{background:#12182a;color:#c8d8ec;border:1px solid #1e2838;border-radius:4px;}" + "QMenu::item:selected{background:#223050;}")); const bool canJoin = !game.started() && game.player_count() < game.max_players(); QAction *join = menu.addAction(tr("Join game")); @@ -643,7 +513,7 @@ void UserInfoPopup::onGamesContextMenu(const QPoint &pos) spec = menu.addAction(tr("Spectate")); } - const QAction *chosen = menu.exec(gamesView->viewport()->mapToGlobal(pos)); + const QAction *chosen = menu.exec(m_gamesView->viewport()->mapToGlobal(pos)); if (!chosen) { return; } @@ -657,46 +527,37 @@ void UserInfoPopup::onGamesContextMenu(const QPoint &pos) // ── showForUser ─────────────────────────────────────────────────────────────── -void UserInfoPopup::refreshHeader() -{ - if (currentUser.isEmpty()) { - return; - } - - const QPixmap avatar = avatarCache ? avatarCache->value(currentUser) : QPixmap{}; - const CardArtParams params = (cardArtParamsMap && cardArtParamsMap->contains(currentUser)) - ? cardArtParamsMap->value(currentUser) - : CardArtParams{}; - const QString artKey = currentUser + u'|' + params.cardName + u'|' + params.cardProviderId; - const QPixmap cardArt = (cardArtCache && !params.cardName.isEmpty()) ? cardArtCache->value(artKey) : QPixmap{}; - header->setUserData(currentUserInfo, currentOnline, avatar, cardArt, params); -} - void UserInfoPopup::showForUser(const QString &userName, const ServerInfo_User &userInfo, bool online, bool isBuddy, bool isIgnored) { - currentUser = userName; - currentUserInfo = userInfo; - currentOnline = online; + m_currentUser = userName; + m_currentUserInfo = userInfo; + m_currentOnline = online; // Header - refreshHeader(); + const QPixmap avatar = m_avatarCache ? m_avatarCache->value(userName) : QPixmap{}; + const CardArtParams params = (m_cardArtParamsMap && m_cardArtParamsMap->contains(userName)) + ? m_cardArtParamsMap->value(userName) + : CardArtParams{}; + const QString artKey = userName + u'|' + params.cardName + u'|' + params.cardProviderId; + const QPixmap cardArt = (m_cardArtCache && !params.cardName.isEmpty()) ? m_cardArtCache->value(artKey) : QPixmap{}; + m_header->setUserData(userInfo, online, avatar, cardArt, params); // Actions rebuildActionButtons(userInfo, online, isBuddy, isIgnored); // Games list reset - gamesModel->clear(); - gamesView->hide(); - gamesStatus->setText(tr("Loading games…")); - gamesStatus->show(); + m_gamesModel->clear(); + m_gamesView->hide(); + m_gamesStatus->setText(tr("Loading games…")); + m_gamesStatus->show(); - // Close button, top right corner, above everything - closeBtn->move(PopupWidth - closeBtn->width() - 6, 6); - closeBtn->raise(); + // Close button — top-right corner, above everything + m_closeBtn->move(PopupWidth - m_closeBtn->width() - 6, 6); + m_closeBtn->raise(); adjustSize(); fetchGames(); @@ -706,40 +567,40 @@ void UserInfoPopup::showForUser(const QString &userName, void UserInfoPopup::fetchGames() { - if (!client || currentUser.isEmpty()) { + if (!m_client || m_currentUser.isEmpty()) { return; } Command_GetGamesOfUser cmd; - cmd.set_user_name(currentUser.toStdString()); + cmd.set_user_name(m_currentUser.toStdString()); - const QString snapshot = currentUser; - PendingCommand *pend = client->prepareSessionCommand(cmd); + const QString snapshot = m_currentUser; + PendingCommand *pend = m_client->prepareSessionCommand(cmd); connect(pend, &PendingCommand::finished, this, [this, snapshot](const Response &r) { onGamesReceived(r, snapshot); }); - client->sendCommand(pend); + m_client->sendCommand(pend); } void UserInfoPopup::onGamesReceived(const Response &r, const QString &forUser) { - if (forUser != currentUser) { - return; // stale response, different user showing now + if (forUser != m_currentUser) { + return; // stale response — different user showing now } - gamesModel->clear(); + m_gamesModel->clear(); if (r.response_code() != Response::RespOk) { - gamesStatus->setText(tr("Could not load games.")); - gamesStatus->show(); - gamesView->hide(); + m_gamesStatus->setText(tr("Could not load games.")); + m_gamesStatus->show(); + m_gamesView->hide(); return; } const auto &resp = r.GetExtension(Response_GetGamesOfUser::ext); if (resp.game_list_size() == 0) { - gamesStatus->setText(tr("No active games.")); - gamesStatus->show(); - gamesView->hide(); + m_gamesStatus->setText(tr("No active games.")); + m_gamesStatus->show(); + m_gamesView->hide(); return; } @@ -747,29 +608,29 @@ void UserInfoPopup::onGamesReceived(const Response &r, const QString &forUser) auto *item = new QStandardItem; item->setData(QVariant::fromValue(resp.game_list(i)), PopupRoles::GameData); item->setEditable(false); - gamesModel->appendRow(item); + m_gamesModel->appendRow(item); } - gamesStatus->hide(); - gamesView->show(); + m_gamesStatus->hide(); + m_gamesView->show(); // Fit exactly to the number of visible rows, scroll when more than 5 constexpr int rowH = 38; // must match PopupGameDelegate::sizeHint constexpr int maxRows = 5; - const int count = gamesModel->rowCount(); + const int count = m_gamesModel->rowCount(); const int visible = qMin(count, maxRows); - gamesView->setFixedHeight(visible * rowH + 2); - gamesView->setVerticalScrollBarPolicy(count > maxRows ? Qt::ScrollBarAlwaysOn : Qt::ScrollBarAlwaysOff); + m_gamesView->setFixedHeight(visible * rowH + 2); + m_gamesView->setVerticalScrollBarPolicy(count > maxRows ? Qt::ScrollBarAlwaysOn : Qt::ScrollBarAlwaysOff); adjustSize(); } void UserInfoPopup::refreshGames() { - gamesModel->clear(); - gamesView->hide(); - gamesStatus->setText(tr("Loading games…")); - gamesStatus->show(); + m_gamesModel->clear(); + m_gamesView->hide(); + m_gamesStatus->setText(tr("Loading games…")); + m_gamesStatus->show(); fetchGames(); } @@ -784,4 +645,4 @@ void UserInfoPopup::leaveEvent(QEvent *e) { QFrame::leaveEvent(e); emit mouseLeftPopup(); -} +} \ No newline at end of file diff --git a/cockatrice/src/interface/widgets/server/user/user_info_popup.h b/cockatrice/src/interface/widgets/server/user/user_info_popup.h index ed7320fba..69517093f 100644 --- a/cockatrice/src/interface/widgets/server/user/user_info_popup.h +++ b/cockatrice/src/interface/widgets/server/user/user_info_popup.h @@ -9,7 +9,6 @@ #include #include #include -#include #include #include #include @@ -27,35 +26,6 @@ namespace PopupRoles constexpr int GameData = Qt::UserRole + 10; } -// Popup theme - -/** - * Palette-derived colors for the popup chrome. Both color schemes read from - * the active QPalette so custom palettes are respected. @c dark only tunes the - * blend strengths. - */ -struct PopupTheme -{ - bool dark = false; - QColor bg; - QColor border; - QColor text; - QColor subText; - QColor buttonBg; - QColor buttonBorder; - QColor buttonHover; - QColor buttonPressed; - QColor buttonDisabled; - QColor closeBg; - QColor closeHover; - QColor gamesRow; - QColor gamesSelected; - QColor gamesSeparator; - QColor statusText; - - static PopupTheme fromPalette(const QPalette &palette, bool dark); -}; - // ── Header widget ───────────────────────────────────────────────────────────── /** @@ -81,22 +51,21 @@ class UserInfoHeaderWidget : public QWidget public: explicit UserInfoHeaderWidget(QWidget *parent = nullptr); - void setUserData(const ServerInfo_User &_user, - bool _online, - const QPixmap &_avatar, - const QPixmap &_cardArt, - const CardArtParams &_params); + void setUserData(const ServerInfo_User &user, + bool online, + const QPixmap &avatar, + const QPixmap &cardArt, + const CardArtParams ¶ms); protected: void paintEvent(QPaintEvent *e) override; private: - ServerInfo_User user; - bool online = false; - QPixmap avatar; - QPixmap cardArt; - CardArtParams params; - QString attribution; + ServerInfo_User m_user; + bool m_online = false; + QPixmap m_avatar; + QPixmap m_cardArt; + CardArtParams m_params; }; // ── Main popup ──────────────────────────────────────────────────────────────── @@ -124,11 +93,11 @@ class UserInfoPopup : public QFrame static constexpr int PopupWidth = 316; public: - explicit UserInfoPopup(TabSupervisor *_ts, - AbstractClient *_client, - const QMap *_avatarCache, - const QMap *_cardArtCache, - const QMap *_cardArtParamsMap, + explicit UserInfoPopup(TabSupervisor *tabSupervisor, + AbstractClient *client, + const QMap *avatarCache, + const QMap *cardArtCache, + const QMap *cardArtParamsMap, QWidget *parent); /** @@ -139,28 +108,14 @@ public: showForUser(const QString &userName, const ServerInfo_User &userInfo, bool online, bool isBuddy, bool isIgnored); void fetchGames(); - [[nodiscard]] QString getCurrentUser() const + [[nodiscard]] QString currentUser() const { - return currentUser; + return m_currentUser; } /** Called when buddy/ignore status changes externally while popup is open. */ void updateActionButtons(const ServerInfo_User &userInfo, bool online, bool isBuddy, bool isIgnored); - /** Re-pulls the avatar/card art for the currently shown user (e.g. after it loads). */ - void refreshHeader(); - - /** - * Sets a predicate evaluated on every action-button rebuild. It receives - * the name of the user the popup currently shows; when it returns true an - * "Invite" button is shown. The popup itself never resolves the invite - * link, it just forwards the request. - */ - void setGameInviteAvailable(std::function available) - { - gameInviteAvailable = std::move(available); - } - signals: void mouseEnteredPopup(); void mouseLeftPopup(); @@ -171,7 +126,6 @@ signals: // ── Action signals — connect to UserContextMenu::exec*() ────────────────── void chatRequested(const QString &userName); - void inviteRequested(const QString &userName); void detailsRequested(const QString &userName); void showGamesRequested(const QString &userName); void addBuddyRequested(const QString &userName); @@ -199,31 +153,25 @@ private slots: private: void buildUi(); - void applyTheme(); void rebuildActionButtons(const ServerInfo_User &userInfo, bool online, bool isBuddy, bool isIgnored); - TabSupervisor *ts; - AbstractClient *client; - const QMap *avatarCache; - const QMap *cardArtCache; - const QMap *cardArtParamsMap; + TabSupervisor *m_ts; + AbstractClient *m_client; + const QMap *m_avatarCache; + const QMap *m_cardArtCache; + const QMap *m_cardArtParamsMap; - PopupTheme theme; + QString m_currentUser; + ServerInfo_User m_currentUserInfo; + bool m_currentOnline = false; - QString currentUser; - ServerInfo_User currentUserInfo; - bool currentOnline = false; - std::function gameInviteAvailable; - - UserInfoHeaderWidget *header; - QWidget *actionArea; ///< rebuilt per user - QLabel *gamesLabel; - QFrame *separator; - QListView *gamesView; - QStandardItemModel *gamesModel; - QLabel *gamesStatus; - QPushButton *closeBtn; - QPushButton *refreshBtn; + UserInfoHeaderWidget *m_header; + QWidget *m_actionArea; ///< rebuilt per user + QListView *m_gamesView; + QStandardItemModel *m_gamesModel; + QLabel *m_gamesStatus; + QPushButton *m_closeBtn; + QPushButton *m_refreshBtn; }; #endif // COCKATRICE_USER_INFO_POPUP_H \ No newline at end of file diff --git a/cockatrice/src/interface/widgets/server/user/user_list_dialog.cpp b/cockatrice/src/interface/widgets/server/user/user_list_dialog.cpp deleted file mode 100644 index fc5b520ad..000000000 --- a/cockatrice/src/interface/widgets/server/user/user_list_dialog.cpp +++ /dev/null @@ -1,302 +0,0 @@ -#include "user_list_dialog.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -BanDialog::BanDialog(const ServerInfo_User &info, QWidget *parent) : QDialog(parent) -{ - setAttribute(Qt::WA_DeleteOnClose); - - nameBanCheckBox = new QCheckBox(tr("ban &user name")); - nameBanCheckBox->setChecked(true); - nameBanEdit = new QLineEdit(QString::fromStdString(info.name())); - nameBanEdit->setMaxLength(MAX_NAME_LENGTH); - ipBanCheckBox = new QCheckBox(tr("ban &IP address")); - ipBanCheckBox->setChecked(true); - ipBanEdit = new QLineEdit(QString::fromStdString(info.address())); - ipBanEdit->setMaxLength(MAX_NAME_LENGTH); - idBanCheckBox = new QCheckBox(tr("ban client I&D")); - idBanCheckBox->setChecked(true); - idBanEdit = new QLineEdit(QString::fromStdString(info.clientid())); - idBanEdit->setMaxLength(MAX_NAME_LENGTH); - if (QString::fromStdString(info.clientid()).isEmpty()) { - idBanCheckBox->setChecked(false); - } - - QGridLayout *banTypeGrid = new QGridLayout; - banTypeGrid->addWidget(nameBanCheckBox, 0, 0); - banTypeGrid->addWidget(nameBanEdit, 0, 1); - banTypeGrid->addWidget(ipBanCheckBox, 1, 0); - banTypeGrid->addWidget(ipBanEdit, 1, 1); - banTypeGrid->addWidget(idBanCheckBox, 2, 0); - banTypeGrid->addWidget(idBanEdit, 2, 1); - QGroupBox *banTypeGroupBox = new QGroupBox(tr("Ban type")); - banTypeGroupBox->setLayout(banTypeGrid); - - permanentRadio = new QRadioButton(tr("&permanent ban")); - temporaryRadio = new QRadioButton(tr("&temporary ban")); - temporaryRadio->setChecked(true); - connect(temporaryRadio, &QRadioButton::toggled, this, &BanDialog::enableTemporaryEdits); - daysLabel = new QLabel(tr("&Days:")); - daysEdit = new QSpinBox; - daysEdit->setMinimum(0); - daysEdit->setValue(0); - daysEdit->setMaximum(10000); - daysLabel->setBuddy(daysEdit); - hoursLabel = new QLabel(tr("&Hours:")); - hoursEdit = new QSpinBox; - hoursEdit->setMinimum(0); - hoursEdit->setValue(0); - hoursEdit->setMaximum(24); - hoursLabel->setBuddy(hoursEdit); - minutesLabel = new QLabel(tr("&Minutes:")); - minutesEdit = new QSpinBox; - minutesEdit->setMinimum(0); - minutesEdit->setValue(5); - minutesEdit->setMaximum(60); - minutesLabel->setBuddy(minutesEdit); - QGridLayout *durationLayout = new QGridLayout; - durationLayout->addWidget(permanentRadio, 0, 0, 1, 6); - durationLayout->addWidget(temporaryRadio, 1, 0, 1, 6); - durationLayout->addWidget(daysLabel, 2, 0); - durationLayout->addWidget(daysEdit, 2, 1); - durationLayout->addWidget(hoursLabel, 2, 2); - durationLayout->addWidget(hoursEdit, 2, 3); - durationLayout->addWidget(minutesLabel, 2, 4); - durationLayout->addWidget(minutesEdit, 2, 5); - QGroupBox *durationGroupBox = new QGroupBox(tr("Duration of the ban")); - durationGroupBox->setLayout(durationLayout); - - QLabel *reasonLabel = new QLabel(tr("Please enter the reason for the ban.\n" - "This is only saved for moderators and cannot be seen by the banned person.")); - reasonEdit = new QPlainTextEdit; - - QLabel *visibleReasonLabel = - new QLabel(tr("Please enter the reason for the ban that will be visible to the banned person.")); - visibleReasonEdit = new QPlainTextEdit; - - deleteMessages = new QCheckBox(tr("Redact all messages from this user in all rooms")); - - QPushButton *okButton = new QPushButton(tr("&OK")); - okButton->setAutoDefault(true); - connect(okButton, &QPushButton::clicked, this, &BanDialog::okClicked); - QPushButton *cancelButton = new QPushButton(tr("&Cancel")); - connect(cancelButton, &QPushButton::clicked, this, &BanDialog::reject); - - QHBoxLayout *buttonLayout = new QHBoxLayout; - buttonLayout->addStretch(); - buttonLayout->addWidget(okButton); - buttonLayout->addWidget(cancelButton); - - QVBoxLayout *vbox = new QVBoxLayout; - vbox->addWidget(banTypeGroupBox); - vbox->addWidget(durationGroupBox); - vbox->addWidget(reasonLabel); - vbox->addWidget(reasonEdit); - vbox->addWidget(visibleReasonLabel); - vbox->addWidget(visibleReasonEdit); - vbox->addWidget(deleteMessages); - vbox->addLayout(buttonLayout); - - setLayout(vbox); - setWindowTitle(tr("Ban user from server")); -} - -WarningDialog::WarningDialog(const QString &userName, const QString &clientID, QWidget *parent) : QDialog(parent) -{ - setAttribute(Qt::WA_DeleteOnClose); - descriptionLabel = new QLabel(tr("Which warning would you like to send?")); - nameWarning = new QLineEdit(userName); - nameWarning->setMaxLength(MAX_NAME_LENGTH); - warnClientID = new QLineEdit(clientID); - warnClientID->setMaxLength(MAX_NAME_LENGTH); - warningOption = new QComboBox(); - warningOption->addItem("", ""); - - deleteMessages = new QCheckBox(tr("Redact all messages from this user in all rooms")); - - QPushButton *okButton = new QPushButton(tr("&OK")); - okButton->setAutoDefault(true); - connect(okButton, &QPushButton::clicked, this, &WarningDialog::okClicked); - QPushButton *cancelButton = new QPushButton(tr("&Cancel")); - connect(cancelButton, &QPushButton::clicked, this, &WarningDialog::reject); - - QHBoxLayout *buttonLayout = new QHBoxLayout; - buttonLayout->addStretch(); - buttonLayout->addWidget(okButton); - buttonLayout->addWidget(cancelButton); - - QVBoxLayout *vbox = new QVBoxLayout; - vbox->addWidget(descriptionLabel); - vbox->addWidget(nameWarning); - vbox->addWidget(warningOption); - vbox->addWidget(deleteMessages); - vbox->addLayout(buttonLayout); - setLayout(vbox); - setWindowTitle(tr("Warn user for misconduct")); -} - -void WarningDialog::okClicked() -{ - if (nameWarning->text().simplified().isEmpty()) { - QMessageBox::critical(this, tr("Error"), - tr("User name to send a warning to can not be blank, please specify a user to warn.")); - return; - } - - if (warningOption->currentData().toString().simplified().isEmpty()) { - QMessageBox::critical(this, tr("Error"), - tr("Warning to use can not be blank, please select a valid warning to send.")); - return; - } - - accept(); -} - -QString WarningDialog::getName() const -{ - return nameWarning->text().simplified(); -} - -QString WarningDialog::getWarnID() const -{ - return warnClientID->text().simplified(); -} - -QString WarningDialog::getReason() const -{ - return warningOption->currentData().toString().simplified(); -} - -int WarningDialog::getDeleteMessages() const -{ - return deleteMessages->isChecked() ? -1 : 0; -} - -void WarningDialog::addWarningOption(const QString &warning, int startingIl) -{ - if (startingIl > 1) { - warningOption->addItem(tr("%1 (IL %2)").arg(warning).arg(startingIl), warning); - } else { - warningOption->addItem(warning, warning); - } -} - -void BanDialog::okClicked() -{ - if (!nameBanCheckBox->isChecked() && !ipBanCheckBox->isChecked() && !idBanCheckBox->isChecked()) { - QMessageBox::critical(this, tr("Error"), - tr("You have to select a name-based, IP-based, clientId based, or some combination of " - "the three to place a ban.")); - return; - } - - if (nameBanCheckBox->isChecked()) { - if (nameBanEdit->text().simplified() == "") { - QMessageBox::critical(this, tr("Error"), - tr("You must have a value in the name ban when selecting the name ban checkbox.")); - return; - } - } - - if (ipBanCheckBox->isChecked()) { - if (ipBanEdit->text().simplified() == "") { - QMessageBox::critical(this, tr("Error"), - tr("You must have a value in the ip ban when selecting the ip ban checkbox.")); - return; - } - } - - if (idBanCheckBox->isChecked()) { - if (idBanEdit->text().simplified() == "") { - QMessageBox::critical( - this, tr("Error"), - tr("You must have a value in the clientid ban when selecting the clientid ban checkbox.")); - return; - } - } - - accept(); -} - -void BanDialog::enableTemporaryEdits(bool enabled) -{ - daysLabel->setEnabled(enabled); - daysEdit->setEnabled(enabled); - hoursLabel->setEnabled(enabled); - hoursEdit->setEnabled(enabled); - minutesLabel->setEnabled(enabled); - minutesEdit->setEnabled(enabled); -} - -QString BanDialog::getBanId() const -{ - return idBanCheckBox->isChecked() ? idBanEdit->text() : QString(); -} - -QString BanDialog::getBanName() const -{ - return nameBanCheckBox->isChecked() ? nameBanEdit->text() : QString(); -} - -QString BanDialog::getBanIP() const -{ - return ipBanCheckBox->isChecked() ? ipBanEdit->text() : QString(); -} - -int BanDialog::getMinutes() const -{ - return permanentRadio->isChecked() ? 0 - : (daysEdit->value() * 24 * 60 + hoursEdit->value() * 60 + minutesEdit->value()); -} - -QString BanDialog::getReason() const -{ - return reasonEdit->toPlainText(); -} - -QString BanDialog::getVisibleReason() const -{ - return visibleReasonEdit->toPlainText(); -} - -int BanDialog::getDeleteMessages() const -{ - return deleteMessages->isChecked() ? -1 : 0; -} - -AdminNotesDialog::AdminNotesDialog(const QString &_userName, const QString &_notes, QWidget *_parent) - : QDialog(_parent), userName(_userName) -{ - setAttribute(Qt::WA_DeleteOnClose); - - auto *updateButton = new QPushButton(tr("Update Notes")); - updateButton->setEnabled(false); - connect(updateButton, &QPushButton::clicked, this, &AdminNotesDialog::accept); - - notes = new QPlainTextEdit(_notes); - notes->setMinimumWidth(500); - connect(notes, &QPlainTextEdit::textChanged, this, [=]() { updateButton->setEnabled(true); }); - - auto *vbox = new QVBoxLayout; - vbox->addWidget(notes); - vbox->addWidget(updateButton); - - setLayout(vbox); - setWindowTitle(tr("Admin Notes for %1").arg(_userName)); -} - -QString AdminNotesDialog::getNotes() const -{ - return notes->toPlainText(); -} \ No newline at end of file diff --git a/cockatrice/src/interface/widgets/server/user/user_list_dialog.h b/cockatrice/src/interface/widgets/server/user/user_list_dialog.h deleted file mode 100644 index fa7d5de92..000000000 --- a/cockatrice/src/interface/widgets/server/user/user_list_dialog.h +++ /dev/null @@ -1,79 +0,0 @@ -#ifndef COCKATRICE_USER_LIST_DIALOG_H -#define COCKATRICE_USER_LIST_DIALOG_H - -#include -#include - -class QComboBox; -class QLabel; -class QPlainTextEdit; -class QRadioButton; -class QSpinBox; -class QLineEdit; -class QCheckBox; - -class BanDialog : public QDialog -{ - Q_OBJECT - - QLabel *daysLabel, *hoursLabel, *minutesLabel; - QCheckBox *nameBanCheckBox, *ipBanCheckBox, *idBanCheckBox, *deleteMessages; - QLineEdit *nameBanEdit, *ipBanEdit, *idBanEdit; - QSpinBox *daysEdit, *hoursEdit, *minutesEdit; - QRadioButton *permanentRadio, *temporaryRadio; - QPlainTextEdit *reasonEdit, *visibleReasonEdit; - -private slots: - void okClicked(); - void enableTemporaryEdits(bool enabled); - -public: - explicit BanDialog(const ServerInfo_User &info, QWidget *parent = nullptr); - [[nodiscard]] QString getBanName() const; - [[nodiscard]] QString getBanIP() const; - [[nodiscard]] QString getBanId() const; - [[nodiscard]] int getMinutes() const; - [[nodiscard]] QString getReason() const; - [[nodiscard]] QString getVisibleReason() const; - [[nodiscard]] int getDeleteMessages() const; -}; - -class WarningDialog : public QDialog -{ - Q_OBJECT - - QLabel *descriptionLabel; - QLineEdit *nameWarning; - QComboBox *warningOption; - QLineEdit *warnClientID; - QCheckBox *deleteMessages; - -private slots: - void okClicked(); - -public: - WarningDialog(const QString &userName, const QString &clientID, QWidget *parent = nullptr); - [[nodiscard]] QString getName() const; - [[nodiscard]] QString getWarnID() const; - [[nodiscard]] QString getReason() const; - [[nodiscard]] int getDeleteMessages() const; - void addWarningOption(const QString &warning, int startingIl = 1); -}; - -class AdminNotesDialog : public QDialog -{ - Q_OBJECT - - QString userName; - QPlainTextEdit *notes; - -public: - explicit AdminNotesDialog(const QString &_userName, const QString &_notes, QWidget *_parent = nullptr); - [[nodiscard]] QString getName() const - { - return userName; - } - [[nodiscard]] QString getNotes() const; -}; - -#endif // COCKATRICE_USER_LIST_DIALOG_H diff --git a/cockatrice/src/interface/widgets/server/user/user_list_painter.cpp b/cockatrice/src/interface/widgets/server/user/user_list_painter.cpp index 34a3d6ae1..8891ff268 100644 --- a/cockatrice/src/interface/widgets/server/user/user_list_painter.cpp +++ b/cockatrice/src/interface/widgets/server/user/user_list_painter.cpp @@ -3,7 +3,6 @@ #include "../../interface/pixel_map_generator.h" #include -#include #include #include #include @@ -20,37 +19,12 @@ QSize UserListPainter::sizeHint() return QSize(0, RowHeight); } -UserListPainter::Style UserListPainter::resolveStyle(const QPalette &palette, bool dark) -{ - Style style; - style.dark = dark; - const QColor base = palette.color(QPalette::Base); - const QColor alt = palette.color(QPalette::AlternateBase); - style.cardStart = base; - style.cardEnd = (alt != base) ? alt : palette.color(QPalette::Midlight); - style.base = base; - style.textOnline = palette.color(QPalette::Text); - style.textOffline = palette.color(QPalette::Disabled, QPalette::Text); - style.ringOffline = palette.color(QPalette::Disabled, QPalette::Text); - style.dropShadow = dark; - return style; -} - -QColor UserListPainter::blend(const QColor &a, const QColor &b, qreal t) -{ - const qreal u = 1.0 - t; - return QColor(qRound(a.red() * u + b.red() * t), qRound(a.green() * u + b.green() * t), - qRound(a.blue() * u + b.blue() * t), qRound(a.alpha() * u + b.alpha() * t)); -} - QColor UserListPainter::getAccentColor(const UserLevelFlags &userLevel, bool online) { QColor accentColor; if (userLevel.testFlag(ServerInfo_User::IsAdmin)) { accentColor = QColor(245, 158, 11); - } else if (userLevel.testFlag(ServerInfo_User::IsDeveloper)) { - accentColor = QColor(185, 28, 28); } else if (userLevel.testFlag(ServerInfo_User::IsModerator)) { accentColor = QColor(59, 130, 246); } else if (userLevel.testFlag(ServerInfo_User::IsJudge)) { @@ -85,37 +59,16 @@ int UserListPainter::getCardRight(const QStyleOptionViewItem &option, const QRec void UserListPainter::drawBackground(QPainter *painter, const QRectF &cardRect, const QColor &accentColor, - bool selected, - const Style &style, - bool hasRole) + bool selected) { QLinearGradient bg(cardRect.topLeft(), cardRect.topRight()); - if (style.dark) { - // Dark mode darkens the role color to fit the dark surface and fades - // it into the deep navy surface on the right. The text drop shadow - // keeps the username legible over the colored edge. - bg.setColorAt(0, selected ? accentColor.darker(130) : accentColor.darker(320)); - bg.setColorAt(1, selected ? QColor(40, 48, 60) : QColor(18, 22, 30)); - } else if (hasRole) { - // Light mode pegs the role color on the left at near full strength - // and fades it into the white surface on the right. The tint stays - // bright enough that the dark text remains legible without a shadow. - bg.setColorAt(0, blend(style.cardStart, accentColor, selected ? 0.75 : 0.65)); - bg.setColorAt(1, blend(style.cardEnd, accentColor, selected ? 0.18 : 0.10)); - } else { - // Regular users keep a scaled-down accent tint so the banner card art - // stays legible over a colored backdrop (the pre-branch painter was - // always dark-styled) while the role hierarchy still reads. - bg.setColorAt(0, blend(style.cardStart, accentColor, (selected ? 0.75 : 0.65) * 0.7)); - bg.setColorAt(1, blend(style.cardEnd, accentColor, (selected ? 0.18 : 0.10) * 0.7)); - } + bg.setColorAt(0, selected ? accentColor.darker(130) : accentColor.darker(320)); + bg.setColorAt(1, selected ? QColor(40, 48, 60) : QColor(18, 22, 30)); painter->setPen(Qt::NoPen); painter->setBrush(bg); painter->drawRoundedRect(cardRect, 6, 6); - // The 3px accent bar anchors every row so the banner card art reads as a - // consistent strip in either scheme (pre-branch parity). painter->setBrush(accentColor); painter->drawRoundedRect(QRectF(cardRect.left(), cardRect.top(), 3, cardRect.height()), 2, 2); } @@ -155,12 +108,6 @@ void UserListPainter::drawCardArt(QPainter *painter, return; } - // CardPictureLoader::getPixmap tags its output with the screen's - // devicePixelRatio on HiDPI displays. Every calculation below is in raw - // pixels, so normalize to 1.0 or the crop renders at 1/dpr scale anchored - // to the top left corner of the row. - art.setDevicePixelRatio(1.0); - const int cardH = rect.height() - 4; const int totalW = cardRight - rect.left(); const int marginL = qRound(totalW * params.marginPctL); @@ -178,14 +125,11 @@ void UserListPainter::drawCardArt(QPainter *painter, const int srcX = (scaledW - drawW) / 2; const int srcY = qRound((scaledH - cardH) * params.verticalOffset); - // Clamp so we never copy outside the pixmap bounds. srcX can go negative - // for stored zoom values below 1, which would silently underfill the - // strip with transparent padding. - const int safeSrcX = qBound(0, srcX, qMax(0, scaledW - drawW)); + // Clamp srcY so we never copy outside the pixmap bounds const int safeSrcY = qBound(0, srcY, qMax(0, scaledH - cardH)); QImage img = - scaled.copy(safeSrcX, safeSrcY, drawW, cardH).toImage().convertToFormat(QImage::Format_ARGB32_Premultiplied); + scaled.copy(srcX, safeSrcY, drawW, cardH).toImage().convertToFormat(QImage::Format_ARGB32_Premultiplied); { QPainter mask(&img); @@ -219,8 +163,7 @@ void UserListPainter::drawAvatar(QPainter *painter, const UserLevelFlags &userLevel, const ServerInfo_User &userInfo, const QString &privLevel, - const QMap *avatarCache, - const Style &style) + const QMap *avatarCache) { QPainterPath clipPath; clipPath.addEllipse(avatarRect); @@ -240,7 +183,7 @@ void UserListPainter::drawAvatar(QPainter *painter, } if (!drewAvatar) { - painter->setBrush(blend(accentColor, style.base, style.dark ? 0.45 : 0.72)); + painter->setBrush(accentColor.darker(200)); painter->setPen(Qt::NoPen); painter->drawEllipse(avatarRect); @@ -253,9 +196,9 @@ void UserListPainter::drawAvatar(QPainter *painter, painter->restore(); } -void UserListPainter::drawStatusRing(QPainter *painter, const QRect &avatarRect, bool online, const Style &style) +void UserListPainter::drawStatusRing(QPainter *painter, const QRect &avatarRect, bool online) { - const QColor statusColor = online ? QColor(34, 197, 94) : style.ringOffline; + const QColor statusColor = online ? QColor(34, 197, 94) : QColor(70, 80, 95); painter->setPen(QPen(statusColor, 2)); painter->setBrush(Qt::NoBrush); @@ -269,7 +212,7 @@ void UserListPainter::drawUserName(QPainter *painter, int textX, const QString &userName, bool online, - const Style &style) + bool selected) { QFont nameFont = option.font; nameFont.setBold(true); @@ -278,12 +221,10 @@ void UserListPainter::drawUserName(QPainter *painter, const QRect nameRect(textX, rect.top() + 8, cardRight - textX - 10, 20); const QString elidedName = QFontMetrics(nameFont).elidedText(userName, Qt::ElideRight, cardRight - textX - 10); - if (style.dropShadow) { - painter->setPen(QColor(0, 0, 0, 200)); - painter->drawText(nameRect.translated(1, 1), Qt::AlignVCenter | Qt::AlignLeft, elidedName); - } + painter->setPen(QColor(0, 0, 0, 200)); + painter->drawText(nameRect.translated(1, 1), Qt::AlignVCenter | Qt::AlignLeft, elidedName); - painter->setPen(online ? style.textOnline : style.textOffline); + painter->setPen(online ? (selected ? Qt::white : QColor(226, 232, 240)) : QColor(90, 100, 115)); painter->drawText(nameRect, Qt::AlignVCenter | Qt::AlignLeft, elidedName); } @@ -301,8 +242,6 @@ QList UserListPainter::buildBadges(const UserLevelFlags if (userLevel.testFlag(ServerInfo_User::IsAdmin)) { badges << Badge{"ADMIN", QColor(245, 158, 11)}; - } else if (userLevel.testFlag(ServerInfo_User::IsDeveloper)) { - badges << Badge{"DEV", QColor(185, 28, 28)}; } else if (userLevel.testFlag(ServerInfo_User::IsModerator)) { badges << Badge{"MOD", QColor(59, 130, 246)}; } else if (userLevel.testFlag(ServerInfo_User::IsJudge)) { @@ -323,8 +262,7 @@ void UserListPainter::drawBadges(QPainter *painter, const QRect &rect, int cardRight, const QList &badges, - bool online, - const Style &style) + bool online) { if (badges.isEmpty()) { return; @@ -346,17 +284,15 @@ void UserListPainter::drawBadges(QPainter *painter, int bx = cardRight - 6 - totalBadgeW; for (const Badge &b : badges) { - const QColor col = online ? b.color : blend(b.color, style.base, 0.55); - const QColor surface = blend(col, style.base, style.dark ? 0.55 : 0.78); - const QColor text = style.dark ? blend(col, Qt::white, 0.5) : blend(col, Qt::black, 0.35); + const QColor col = online ? b.color : b.color.darker(180); const int bw = fm.horizontalAdvance(b.text) + 8; const QRect br(bx, rect.top() + 44, bw, 13); painter->setPen(Qt::NoPen); - painter->setBrush(surface); + painter->setBrush(col.darker(online ? 160 : 220)); painter->drawRoundedRect(br, 3, 3); - painter->setPen(text); + painter->setPen(col.lighter(online ? 160 : 100)); painter->drawText(br, Qt::AlignCenter, b.text); bx += bw + 4; @@ -369,19 +305,11 @@ void UserListPainter::paint(QPainter *painter, const ServerInfo_User &userInfo, const QMap *avatarCache, const QMap *cardArtCache, - const QMap *cardArtParamsMap, - bool dark) + const QMap *cardArtParamsMap) { painter->save(); painter->setRenderHints(QPainter::Antialiasing | QPainter::SmoothPixmapTransform | QPainter::TextAntialiasing); - // The delegate supplies the application palette in option.palette, which - // always reflects the active theme. The widget palette can be stale after - // a runtime theme change, so it is only used as a defensive fallback. - const QPalette pal = - option.palette == QPalette() ? (option.widget ? option.widget->palette() : qApp->palette()) : option.palette; - const Style style = resolveStyle(pal, dark); - const QRect rect = option.rect; const bool online = index.data(Qt::UserRole + 1).toBool(); const bool selected = option.state & QStyle::State_Selected; @@ -389,9 +317,6 @@ void UserListPainter::paint(QPainter *painter, const QString userName = QString::fromStdString(userInfo.name()); const QString privLevel = QString::fromStdString(userInfo.privlevel()); const QColor accentColor = getAccentColor(userLevel, online); - const bool hasRole = - userLevel.testFlag(ServerInfo_User::IsAdmin) || userLevel.testFlag(ServerInfo_User::IsDeveloper) || - userLevel.testFlag(ServerInfo_User::IsModerator) || userLevel.testFlag(ServerInfo_User::IsJudge); const QRectF cardRect = QRectF(rect).adjusted(3, 2, -3, -2); const int cardRight = getCardRight(option, rect); @@ -399,19 +324,19 @@ void UserListPainter::paint(QPainter *painter, ? cardArtParamsMap->value(userName) : CardArtParams{}; - drawBackground(painter, cardRect, accentColor, selected, style, hasRole); + drawBackground(painter, cardRect, accentColor, selected); drawCardArt(painter, rect, cardRight, userName, cardArtCache, params); const QRect avatarRect = getAvatarRect(rect); - drawAvatar(painter, avatarRect, userName, accentColor, userLevel, userInfo, privLevel, avatarCache, style); - drawStatusRing(painter, avatarRect, online, style); + drawAvatar(painter, avatarRect, userName, accentColor, userLevel, userInfo, privLevel, avatarCache); + drawStatusRing(painter, avatarRect, online); const int textX = avatarRect.right() + TextSpacing; - drawUserName(painter, option, rect, cardRight, textX, userName, online, style); + drawUserName(painter, option, rect, cardRight, textX, userName, online, selected); drawCountryFlag(painter, rect, textX, userInfo); const QList badges = buildBadges(userLevel, privLevel); - drawBadges(painter, option, rect, cardRight, badges, online, style); + drawBadges(painter, option, rect, cardRight, badges, online); painter->restore(); -} +} \ No newline at end of file diff --git a/cockatrice/src/interface/widgets/server/user/user_list_painter.h b/cockatrice/src/interface/widgets/server/user/user_list_painter.h index 352a01f6b..28cab9675 100644 --- a/cockatrice/src/interface/widgets/server/user/user_list_painter.h +++ b/cockatrice/src/interface/widgets/server/user/user_list_painter.h @@ -6,7 +6,6 @@ #include #include #include -#include #include #include #include @@ -29,36 +28,13 @@ struct CardArtParams class UserListPainter { public: - /** - * Palette-derived surface colors for the current color scheme. Both the - * light and the dark scheme read from the active QPalette so custom - * palettes are respected. @c dark only tunes the blend strengths (and - * whether the name text keeps its drop shadow). - */ - struct Style - { - bool dark = true; - QColor cardStart; ///< row fill, left edge (normal) - QColor cardEnd; ///< row fill, right edge (normal) - QColor base; ///< lightest surface, used for blending accent hues - QColor textOnline; - QColor textOffline; - QColor ringOffline; - bool dropShadow = false; - }; - - static Style resolveStyle(const QPalette &palette, bool dark); - /// Linear interpolation: @p t = 0 returns @p a, @p t = 1 returns @p b. - static QColor blend(const QColor &a, const QColor &b, qreal t); - static void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index, const ServerInfo_User &userInfo, const QMap *avatarCache, const QMap *cardArtCache, - const QMap *cardArtParamsMap, - bool dark); + const QMap *cardArtParamsMap); static QSize sizeHint(); @@ -79,12 +55,7 @@ private: static QColor getAccentColor(const UserLevelFlags &userLevel, bool online); static int getCardRight(const QStyleOptionViewItem &option, const QRect &rect); - static void drawBackground(QPainter *painter, - const QRectF &cardRect, - const QColor &accentColor, - bool selected, - const Style &style, - bool hasRole); + static void drawBackground(QPainter *painter, const QRectF &cardRect, const QColor &accentColor, bool selected); static QRect getAvatarRect(const QRect &rect); static void drawAvatar(QPainter *painter, const QRect &avatarRect, @@ -93,9 +64,8 @@ private: const UserLevelFlags &userLevel, const ServerInfo_User &userInfo, const QString &privLevel, - const QMap *avatarCache, - const Style &style); - static void drawStatusRing(QPainter *painter, const QRect &avatarRect, bool online, const Style &style); + const QMap *avatarCache); + static void drawStatusRing(QPainter *painter, const QRect &avatarRect, bool online); static void drawUserName(QPainter *painter, const QStyleOptionViewItem &option, const QRect &rect, @@ -103,7 +73,7 @@ private: int textX, const QString &userName, bool online, - const Style &style); + bool selected); static void drawCountryFlag(QPainter *painter, const QRect &rect, int textX, const ServerInfo_User &userInfo); static QList buildBadges(const UserLevelFlags &userLevel, const QString &privLevel); static void drawBadges(QPainter *painter, @@ -111,8 +81,7 @@ private: const QRect &rect, int cardRight, const QList &badges, - bool online, - const Style &style); + bool online); }; -#endif // COCKATRICE_USER_LIST_PAINTER_H +#endif // COCKATRICE_USER_LIST_PAINTER_H \ No newline at end of file diff --git a/cockatrice/src/interface/widgets/server/user/user_list_panel_widget.cpp b/cockatrice/src/interface/widgets/server/user/user_list_panel_widget.cpp deleted file mode 100644 index 937058024..000000000 --- a/cockatrice/src/interface/widgets/server/user/user_list_panel_widget.cpp +++ /dev/null @@ -1,88 +0,0 @@ -#include "user_list_panel_widget.h" - -#include "../../../../client/settings/cache_settings.h" -#include "user_list_manager.h" -#include "user_list_widget.h" - -#include -#include -#include -#include - -namespace -{ -// The persisted section keys are the serialization contract with the user's -// settings file, so the values must stay stable across versions. -QString sectionKey(UserListWidget::Section section) -{ - switch (section) { - case UserListWidget::Section::Buddy: - return QStringLiteral("buddy"); - case UserListWidget::Section::Online: - return QStringLiteral("online"); - case UserListWidget::Section::Ignore: - return QStringLiteral("ignore"); - } - return {}; -} -} // namespace - -UserListPanelWidget::UserListPanelWidget(TabSupervisor *_tabSupervisor, AbstractClient *_client, QWidget *parent) - : QWidget(parent) -{ - auto *mainLayout = new QVBoxLayout(this); - mainLayout->setContentsMargins(0, 0, 0, 0); - mainLayout->setSpacing(2); - - searchBar = new QLineEdit(this); - searchBar->setClearButtonEnabled(true); - mainLayout->addWidget(searchBar); - - userList = new UserListWidget(_tabSupervisor, _client, UserListWidget::RoomList, this); - userList->setSectioned( - {UserListWidget::Section::Buddy, UserListWidget::Section::Online, UserListWidget::Section::Ignore}); - mainLayout->addWidget(userList, 1); - - connect(searchBar, &QLineEdit::textChanged, userList, &UserListWidget::setFilterText); - - connect(userList, &UserListWidget::sectionExpanded, this, &UserListPanelWidget::persistExpandedSections); - connect(userList, &UserListWidget::openMessageDialog, this, &UserListPanelWidget::openMessageDialog); - - // Restore the persisted expansion state, then apply it to the tree. - const QStringList expandedSections = SettingsCache::instance().userInterface().getUserListExpandedSections(); - for (const UserListWidget::Section section : userList->getSectionIds()) { - userList->setSectionExpanded(section, expandedSections.contains(sectionKey(section))); - } - - retranslateUi(); -} - -void UserListPanelWidget::bind(UserListManager *manager) -{ - userList->bind(manager); -} - -void UserListPanelWidget::persistExpandedSections(UserListWidget::Section section, bool expanded) -{ - const QString key = sectionKey(section); - QStringList expandedSections = SettingsCache::instance().userInterface().getUserListExpandedSections(); - if (expanded) { - if (!expandedSections.contains(key)) { - expandedSections.append(key); - } - } else { - expandedSections.removeAll(key); - } - SettingsCache::instance().userInterface().setUserListExpandedSections(expandedSections); -} - -void UserListPanelWidget::retranslateUi() -{ - searchBar->setPlaceholderText(tr("Search users...")); - userList->retranslateUi(); -} - -UserListWidget *UserListPanelWidget::getUserList() const -{ - return userList; -} diff --git a/cockatrice/src/interface/widgets/server/user/user_list_panel_widget.h b/cockatrice/src/interface/widgets/server/user/user_list_panel_widget.h deleted file mode 100644 index 7ae14dfcf..000000000 --- a/cockatrice/src/interface/widgets/server/user/user_list_panel_widget.h +++ /dev/null @@ -1,43 +0,0 @@ -/** - * @file user_list_panel_widget.h - * @ingroup Lobby - */ - -#ifndef COCKATRICE_USER_LIST_PANEL_WIDGET_H -#define COCKATRICE_USER_LIST_PANEL_WIDGET_H - -#include "user_list_widget.h" - -#include - -class AbstractClient; -class QLineEdit; -class TabSupervisor; -class UserListManager; - -/** - * A unified user list: a search bar above a single tree whose section headers - * (buddy, online, ignored) are inline dividers. The tree owns the scrolling. - */ -class UserListPanelWidget : public QWidget -{ - Q_OBJECT - -public: - explicit UserListPanelWidget(TabSupervisor *tabSupervisor, AbstractClient *client, QWidget *parent = nullptr); - void bind(UserListManager *manager); - void retranslateUi(); - - [[nodiscard]] UserListWidget *getUserList() const; - -signals: - void openMessageDialog(const QString &userName, bool focus); - -private: - void persistExpandedSections(UserListWidget::Section section, bool expanded); - - QLineEdit *searchBar = nullptr; - UserListWidget *userList = nullptr; -}; - -#endif // COCKATRICE_USER_LIST_PANEL_WIDGET_H diff --git a/cockatrice/src/interface/widgets/server/user/user_list_widget.cpp b/cockatrice/src/interface/widgets/server/user/user_list_widget.cpp index 1bb7c5288..64cbb7b7d 100644 --- a/cockatrice/src/interface/widgets/server/user/user_list_widget.cpp +++ b/cockatrice/src/interface/widgets/server/user/user_list_widget.cpp @@ -1,21 +1,321 @@ #include "user_list_widget.h" #include "../../../../client/settings/cache_settings.h" +#include "../../../card_picture_loader/card_picture_loader.h" #include "../../interface/pixel_map_generator.h" -#include "../../interface/theme_manager.h" +#include "../../interface/widgets/tabs/tab_account.h" #include "../../interface/widgets/tabs/tab_supervisor.h" +#include "../game_selector.h" #include "user_context_menu.h" #include "user_list_painter.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 + +BanDialog::BanDialog(const ServerInfo_User &info, QWidget *parent) : QDialog(parent) +{ + setAttribute(Qt::WA_DeleteOnClose); + + nameBanCheckBox = new QCheckBox(tr("ban &user name")); + nameBanCheckBox->setChecked(true); + nameBanEdit = new QLineEdit(QString::fromStdString(info.name())); + nameBanEdit->setMaxLength(MAX_NAME_LENGTH); + ipBanCheckBox = new QCheckBox(tr("ban &IP address")); + ipBanCheckBox->setChecked(true); + ipBanEdit = new QLineEdit(QString::fromStdString(info.address())); + ipBanEdit->setMaxLength(MAX_NAME_LENGTH); + idBanCheckBox = new QCheckBox(tr("ban client I&D")); + idBanCheckBox->setChecked(true); + idBanEdit = new QLineEdit(QString::fromStdString(info.clientid())); + idBanEdit->setMaxLength(MAX_NAME_LENGTH); + if (QString::fromStdString(info.clientid()).isEmpty()) { + idBanCheckBox->setChecked(false); + } + + QGridLayout *banTypeGrid = new QGridLayout; + banTypeGrid->addWidget(nameBanCheckBox, 0, 0); + banTypeGrid->addWidget(nameBanEdit, 0, 1); + banTypeGrid->addWidget(ipBanCheckBox, 1, 0); + banTypeGrid->addWidget(ipBanEdit, 1, 1); + banTypeGrid->addWidget(idBanCheckBox, 2, 0); + banTypeGrid->addWidget(idBanEdit, 2, 1); + QGroupBox *banTypeGroupBox = new QGroupBox(tr("Ban type")); + banTypeGroupBox->setLayout(banTypeGrid); + + permanentRadio = new QRadioButton(tr("&permanent ban")); + temporaryRadio = new QRadioButton(tr("&temporary ban")); + temporaryRadio->setChecked(true); + connect(temporaryRadio, &QRadioButton::toggled, this, &BanDialog::enableTemporaryEdits); + daysLabel = new QLabel(tr("&Days:")); + daysEdit = new QSpinBox; + daysEdit->setMinimum(0); + daysEdit->setValue(0); + daysEdit->setMaximum(10000); + daysLabel->setBuddy(daysEdit); + hoursLabel = new QLabel(tr("&Hours:")); + hoursEdit = new QSpinBox; + hoursEdit->setMinimum(0); + hoursEdit->setValue(0); + hoursEdit->setMaximum(24); + hoursLabel->setBuddy(hoursEdit); + minutesLabel = new QLabel(tr("&Minutes:")); + minutesEdit = new QSpinBox; + minutesEdit->setMinimum(0); + minutesEdit->setValue(5); + minutesEdit->setMaximum(60); + minutesLabel->setBuddy(minutesEdit); + QGridLayout *durationLayout = new QGridLayout; + durationLayout->addWidget(permanentRadio, 0, 0, 1, 6); + durationLayout->addWidget(temporaryRadio, 1, 0, 1, 6); + durationLayout->addWidget(daysLabel, 2, 0); + durationLayout->addWidget(daysEdit, 2, 1); + durationLayout->addWidget(hoursLabel, 2, 2); + durationLayout->addWidget(hoursEdit, 2, 3); + durationLayout->addWidget(minutesLabel, 2, 4); + durationLayout->addWidget(minutesEdit, 2, 5); + QGroupBox *durationGroupBox = new QGroupBox(tr("Duration of the ban")); + durationGroupBox->setLayout(durationLayout); + + QLabel *reasonLabel = new QLabel(tr("Please enter the reason for the ban.\nThis is only saved for moderators and " + "cannot be seen by the banned person.")); + reasonEdit = new QPlainTextEdit; + + QLabel *visibleReasonLabel = + new QLabel(tr("Please enter the reason for the ban that will be visible to the banned person.")); + visibleReasonEdit = new QPlainTextEdit; + + deleteMessages = new QCheckBox(tr("Redact all messages from this user in all rooms")); + + QPushButton *okButton = new QPushButton(tr("&OK")); + okButton->setAutoDefault(true); + connect(okButton, &QPushButton::clicked, this, &BanDialog::okClicked); + QPushButton *cancelButton = new QPushButton(tr("&Cancel")); + connect(cancelButton, &QPushButton::clicked, this, &BanDialog::reject); + + QHBoxLayout *buttonLayout = new QHBoxLayout; + buttonLayout->addStretch(); + buttonLayout->addWidget(okButton); + buttonLayout->addWidget(cancelButton); + + QVBoxLayout *vbox = new QVBoxLayout; + vbox->addWidget(banTypeGroupBox); + vbox->addWidget(durationGroupBox); + vbox->addWidget(reasonLabel); + vbox->addWidget(reasonEdit); + vbox->addWidget(visibleReasonLabel); + vbox->addWidget(visibleReasonEdit); + vbox->addWidget(deleteMessages); + vbox->addLayout(buttonLayout); + + setLayout(vbox); + setWindowTitle(tr("Ban user from server")); +} + +WarningDialog::WarningDialog(const QString userName, const QString clientID, QWidget *parent) : QDialog(parent) +{ + setAttribute(Qt::WA_DeleteOnClose); + descriptionLabel = new QLabel(tr("Which warning would you like to send?")); + nameWarning = new QLineEdit(userName); + nameWarning->setMaxLength(MAX_NAME_LENGTH); + warnClientID = new QLineEdit(clientID); + warnClientID->setMaxLength(MAX_NAME_LENGTH); + warningOption = new QComboBox(); + warningOption->addItem(""); + + deleteMessages = new QCheckBox(tr("Redact all messages from this user in all rooms")); + + QPushButton *okButton = new QPushButton(tr("&OK")); + okButton->setAutoDefault(true); + connect(okButton, &QPushButton::clicked, this, &WarningDialog::okClicked); + QPushButton *cancelButton = new QPushButton(tr("&Cancel")); + connect(cancelButton, &QPushButton::clicked, this, &WarningDialog::reject); + + QHBoxLayout *buttonLayout = new QHBoxLayout; + buttonLayout->addStretch(); + buttonLayout->addWidget(okButton); + buttonLayout->addWidget(cancelButton); + + QVBoxLayout *vbox = new QVBoxLayout; + vbox->addWidget(descriptionLabel); + vbox->addWidget(nameWarning); + vbox->addWidget(warningOption); + vbox->addWidget(deleteMessages); + vbox->addLayout(buttonLayout); + setLayout(vbox); + setWindowTitle(tr("Warn user for misconduct")); +} + +void WarningDialog::okClicked() +{ + if (nameWarning->text().simplified().isEmpty()) { + QMessageBox::critical(this, tr("Error"), + tr("User name to send a warning to can not be blank, please specify a user to warn.")); + return; + } + + if (warningOption->currentText().simplified().isEmpty()) { + QMessageBox::critical(this, tr("Error"), + tr("Warning to use can not be blank, please select a valid warning to send.")); + return; + } + + accept(); +} + +QString WarningDialog::getName() const +{ + return nameWarning->text().simplified(); +} + +QString WarningDialog::getWarnID() const +{ + return warnClientID->text().simplified(); +} + +QString WarningDialog::getReason() const +{ + return warningOption->currentText().simplified(); +} + +int WarningDialog::getDeleteMessages() const +{ + return deleteMessages->isChecked() ? -1 : 0; +} + +void WarningDialog::addWarningOption(const QString warning) +{ + warningOption->addItem(warning); +} + +void BanDialog::okClicked() +{ + if (!nameBanCheckBox->isChecked() && !ipBanCheckBox->isChecked() && !idBanCheckBox->isChecked()) { + QMessageBox::critical(this, tr("Error"), + tr("You have to select a name-based, IP-based, clientId based, or some combination of " + "the three to place a ban.")); + return; + } + + if (nameBanCheckBox->isChecked()) { + if (nameBanEdit->text().simplified() == "") { + QMessageBox::critical(this, tr("Error"), + tr("You must have a value in the name ban when selecting the name ban checkbox.")); + return; + } + } + + if (ipBanCheckBox->isChecked()) { + if (ipBanEdit->text().simplified() == "") { + QMessageBox::critical(this, tr("Error"), + tr("You must have a value in the ip ban when selecting the ip ban checkbox.")); + return; + } + } + + if (idBanCheckBox->isChecked()) { + if (idBanEdit->text().simplified() == "") { + QMessageBox::critical( + this, tr("Error"), + tr("You must have a value in the clientid ban when selecting the clientid ban checkbox.")); + return; + } + } + + accept(); +} + +void BanDialog::enableTemporaryEdits(bool enabled) +{ + daysLabel->setEnabled(enabled); + daysEdit->setEnabled(enabled); + hoursLabel->setEnabled(enabled); + hoursEdit->setEnabled(enabled); + minutesLabel->setEnabled(enabled); + minutesEdit->setEnabled(enabled); +} + +QString BanDialog::getBanId() const +{ + return idBanCheckBox->isChecked() ? idBanEdit->text() : QString(); +} + +QString BanDialog::getBanName() const +{ + return nameBanCheckBox->isChecked() ? nameBanEdit->text() : QString(); +} + +QString BanDialog::getBanIP() const +{ + return ipBanCheckBox->isChecked() ? ipBanEdit->text() : QString(); +} + +int BanDialog::getMinutes() const +{ + return permanentRadio->isChecked() ? 0 + : (daysEdit->value() * 24 * 60 + hoursEdit->value() * 60 + minutesEdit->value()); +} + +QString BanDialog::getReason() const +{ + return reasonEdit->toPlainText(); +} + +QString BanDialog::getVisibleReason() const +{ + return visibleReasonEdit->toPlainText(); +} + +int BanDialog::getDeleteMessages() const +{ + return deleteMessages->isChecked() ? -1 : 0; +} + +AdminNotesDialog::AdminNotesDialog(const QString &_userName, const QString &_notes, QWidget *_parent) + : QDialog(_parent), userName(_userName) +{ + setAttribute(Qt::WA_DeleteOnClose); + + auto *updateButton = new QPushButton(tr("Update Notes")); + updateButton->setEnabled(false); + connect(updateButton, &QPushButton::clicked, this, &AdminNotesDialog::accept); + + notes = new QPlainTextEdit(_notes); + notes->setMinimumWidth(500); + connect(notes, &QPlainTextEdit::textChanged, this, [=]() { updateButton->setEnabled(true); }); + + auto *vbox = new QVBoxLayout; + vbox->addWidget(notes); + vbox->addWidget(updateButton); + + setLayout(vbox); + setWindowTitle(tr("Admin Notes for %1").arg(_userName)); +} + +QString AdminNotesDialog::getNotes() const +{ + return notes->toPlainText(); +} namespace UserListRoles { @@ -23,16 +323,11 @@ constexpr int Online = Qt::UserRole + 1; constexpr int UserInfo = Qt::UserRole + 2; } // namespace UserListRoles -// Divider items (section headers) in sectioned mode are distinguished from user -// rows (UserListTWI, which uses QTreeWidgetItem::Type) by this item type. -constexpr int SectionItemType = QTreeWidgetItem::UserType + 1; - -UserListItemDelegate::UserListItemDelegate(UserListWidget *owner, - QTreeWidget *tree, +UserListItemDelegate::UserListItemDelegate(QObject *const parent, const QMap *avatarCache, const QMap *cardArtCache, const QMap *cardArtParamsMap) - : QStyledItemDelegate(tree), tree(tree), owner(owner), avatarCache(avatarCache), cardArtCache(cardArtCache), + : QStyledItemDelegate(parent), avatarCache(avatarCache), cardArtCache(cardArtCache), cardArtParamsMap(cardArtParamsMap) { } @@ -45,10 +340,7 @@ bool UserListItemDelegate::editorEvent(QEvent *event, if ((event->type() == QEvent::MouseButtonPress) && index.isValid()) { QMouseEvent *const mouseEvent = static_cast(event); if (mouseEvent->button() == Qt::RightButton) { - // Dialog mode has no context menu: consume the press, show nothing. - if (owner->getHasUserInfoPopup()) { - owner->showContextMenu(mouseEvent->globalPosition().toPoint(), index); - } + static_cast(parent())->showContextMenu(mouseEvent->globalPosition().toPoint(), index); return true; } } @@ -60,129 +352,25 @@ QSize UserListItemDelegate::sizeHint(const QStyleOptionViewItem &option, const Q if (!SettingsCache::instance().appearance().getStyleUserList()) { return QStyledItemDelegate::sizeHint(option, index); } - if (!index.data(UserListRoles::UserInfo).isValid()) { - return QStyledItemDelegate::sizeHint(option, index); // section dividers stay compact - } return UserListPainter::sizeHint(); } void UserListItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const { - const bool styled = SettingsCache::instance().appearance().getStyleUserList(); - // UserInfo/Online are stored on column 0 only. The name lives on column 2, - // so resolve the user data against column 0 no matter which cell is painted. - const QModelIndex userIndex = index.siblingAtColumn(0).isValid() ? index.siblingAtColumn(0) : index; - const QVariant var = userIndex.data(UserListRoles::UserInfo); - - if (styled && var.isValid()) { - // Styled card rows: the tree's cached palette can be stale after a - // runtime theme change, so paint from the application palette (always - // current) instead of option.palette (frozen at the last style switch). - QStyleOptionViewItem opt = option; - opt.palette = qApp->palette(); - - UserListPainter::paint(painter, opt, index, var.value(), avatarCache, cardArtCache, - cardArtParamsMap, themeManager && themeManager->isDarkModeActive()); + if (!SettingsCache::instance().appearance().getStyleUserList()) { + QStyledItemDelegate::paint(painter, option, index); return; } - // Unstyled rows and section dividers are painted manually so every color - // is derived from the current application palette at paint time. The widget - // palette, the view's alternation, and the stored brushes (one per item) - // all go stale after a runtime theme change. - const QPalette appPal = qApp->palette(); - const bool selected = option.state & QStyle::State_Selected; - const bool hovered = option.state & QStyle::State_MouseOver; + const QVariant var = index.data(UserListRoles::UserInfo); - // Row background: selection, hover, zebra striping, plain base. - QColor bg = appPal.color(QPalette::Base); - if (selected) { - bg = appPal.color(QPalette::Highlight); - } else if (hovered) { - bg = UserListPainter::blend(appPal.color(QPalette::Base), appPal.color(QPalette::Highlight), 0.12); - } else if (var.isValid()) { - // Zebra alternation per section. The dividers are top level rows too, - // so the view's own alternation would drift across sections. Count the - // visible user rows back to the previous divider within the same parent - // (sectioned mode stores users as children of the dividers, flat mode - // as top level rows), so the stripe restarts at every divider and at - // the tree top. Hidden filter matches are skipped the same way the view - // skips them, so adjacent visible rows always alternate. - int usersSinceDivider = 0; - const QModelIndex parent = userIndex.parent(); - for (int r = userIndex.row() - 1; r >= 0; --r) { - if (tree->isRowHidden(r, parent)) { - continue; - } - const QModelIndex above = userIndex.model()->index(r, 0, parent); - if (above.isValid() && above.data(UserListRoles::UserInfo).isValid()) { - ++usersSinceDivider; - } else { - break; - } - } - if (usersSinceDivider % 2 == 1) { - bg = appPal.color(QPalette::AlternateBase); - } - } - // Paint the row background. In the column 0 pass the fill spans the full - // viewport width so stripes, hover and selection cover the whole row (the - // name column is content sized in unstyled mode). Later column passes fill - // only their own cell, which is the same color and cannot cover the icons. - QRect bgRect = option.rect; - if (index.column() == 0) { - bgRect = QRect(0, option.rect.top(), tree->viewport()->width(), option.rect.height()); - } - painter->fillRect(bgRect, bg); - - // Text color. - QColor fg = appPal.color(QPalette::Text); - if (selected) { - fg = appPal.color(QPalette::HighlightedText); - } else if (!var.isValid()) { - // Section divider: muted application text color. - fg = appPal.color(QPalette::WindowText); - fg.setAlpha(170); - } else if (index.column() == 2) { - // Name column: online/offline color recomputed at paint time instead - // of trusting the brush stored at login time. - QTreeWidgetItem *item = tree->itemFromIndex(index); - const bool online = item && item->data(0, UserListRoles::Online).toBool(); - if (online) { - fg = appPal.color(QPalette::WindowText); - } else { - fg = (themeManager && themeManager->isDarkModeActive()) - ? QColor(Qt::gray) - : UserListPainter::blend(appPal.color(QPalette::Text), appPal.color(QPalette::Mid), 0.5); - } + if (!var.isValid()) { + QStyledItemDelegate::paint(painter, option, index); + return; } - // Icon (level badge in column 0, country flag in column 1). - QRect textRect = option.rect; - const QIcon icon = index.data(Qt::DecorationRole).value(); - if (!icon.isNull()) { - const QSize iconSize = icon.actualSize(QSize(18, 18)); - const QRect iconRect(option.rect.left() + 2, option.rect.center().y() - iconSize.height() / 2, iconSize.width(), - iconSize.height()); - icon.paint(painter, iconRect); - textRect.setLeft(iconRect.right() + 4); - } - - // Text (name column / divider title), elided to the row width. - painter->save(); - painter->setPen(fg); - const QFont itemFont = index.data(Qt::FontRole).value(); - painter->setFont(itemFont.isCopyOf(QFont()) ? option.font : itemFont); - const QString text = index.data(Qt::DisplayRole).toString(); - const QString elided = painter->fontMetrics().elidedText(text, Qt::ElideRight, textRect.width() - 4); - painter->drawText(textRect.adjusted(2, 0, -2, 0), Qt::AlignLeft | Qt::AlignVCenter, elided); - painter->restore(); - - // Focus indicator for the current item. - if (option.state & QStyle::State_HasFocus) { - painter->setPen(appPal.color(QPalette::Highlight)); - painter->drawRect(option.rect.adjusted(0, 0, -1, -1)); - } + UserListPainter::paint(painter, option, index, var.value(), avatarCache, cardArtCache, + cardArtParamsMap); } UserListTWI::UserListTWI(const ServerInfo_User &_userInfo) : QTreeWidgetItem(Type) @@ -206,10 +394,8 @@ void UserListTWI::setUserInfo(const ServerInfo_User &_userInfo) void UserListTWI::setOnline(bool online) { - // Only the online state is stored here: the delegate derives the - // online/offline text color at paint time from the current application - // palette, so no brush is cached (it would go stale on theme change). setData(0, UserListRoles::Online, online); + setData(2, Qt::ForegroundRole, online ? qApp->palette().brush(QPalette::WindowText) : QBrush(Qt::gray)); } /** @@ -234,11 +420,9 @@ bool UserListTWI::operator<(const QTreeWidgetItem &other) const const auto &lhsUserLevelFlags = UserLevelFlags(data(0, Qt::UserRole).toInt()); const auto &rhsUserLevelFlags = UserLevelFlags(other.data(0, Qt::UserRole).toInt()); - // Admins, Developers & Mods need no additional comparison checks, just to see if they're an admin, a developer - // or a moderator + // Admins & Mods need no additional comparison checks, just to see if they're an admin or a moderator static const QList userLevelWithNoOtherPrefOrder = { - ServerInfo_User_UserLevelFlag_IsAdmin, ServerInfo_User_UserLevelFlag_IsDeveloper, - ServerInfo_User_UserLevelFlag_IsModerator}; + ServerInfo_User_UserLevelFlag_IsAdmin, ServerInfo_User_UserLevelFlag_IsModerator}; for (const auto &userLevelEntry : userLevelWithNoOtherPrefOrder) { if (lhsUserLevelFlags.testFlag(userLevelEntry) && lhsUserLevelFlags.testFlag(userLevelEntry) == rhsUserLevelFlags.testFlag(userLevelEntry)) { @@ -274,14 +458,15 @@ bool UserListTWI::operator<(const QTreeWidgetItem &other) const UserListWidget::UserListWidget(TabSupervisor *_tabSupervisor, AbstractClient *_client, UserListType _type, - QWidget *parent, - bool _hasUserInfoPopup) - : QGroupBox(parent), hasUserInfoPopup(_hasUserInfoPopup), tabSupervisor(_tabSupervisor), client(_client), - type(_type), onlineCount(0) + QWidget *parent) + : QGroupBox(parent), tabSupervisor(_tabSupervisor), client(_client), type(_type), onlineCount(0) { avatarProvider = new UserAvatarProvider(client, this); cardArtProvider = new UserCardArtProvider(this); + itemDelegate = + new UserListItemDelegate(this, &avatarProvider->cache(), &cardArtProvider->cache(), &cardArtParamsMap); + userContextMenu = new UserContextMenu(tabSupervisor, this); connect(userContextMenu, &UserContextMenu::openMessageDialog, this, &UserListWidget::openMessageDialog); @@ -292,8 +477,6 @@ UserListWidget::UserListWidget(TabSupervisor *_tabSupervisor, userTree->setHeaderHidden(true); userTree->setRootIsDecorated(false); userTree->setIconSize(QSize(20, 18)); - itemDelegate = new UserListItemDelegate(this, userTree, &avatarProvider->cache(), &cardArtProvider->cache(), - &cardArtParamsMap); userTree->setItemDelegate(itemDelegate); userTree->setAlternatingRowColors(true); userTree->hideColumn(1); @@ -301,178 +484,79 @@ UserListWidget::UserListWidget(TabSupervisor *_tabSupervisor, userTree->hideColumn(3); connect(userTree, &QTreeWidget::itemActivated, this, &UserListWidget::userClicked); userTree->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - // QTreeWidget enables stretchLastSection by default. Left on, the hidden - // last section absorbs viewport resizes, the Stretch sections never - // redistribute, and the header keeps a stale length past the viewport — - // an invisible horizontal pan range under ScrollBarAlwaysOff. Disable it - // so the explicit resize modes in applyDisplayMode() own the geometry. - userTree->header()->setStretchLastSection(false); + userTree->header()->setStretchLastSection(true); - // Always create timers so callers never segfault on a null deref; - // showPopupForUser / hidePopup already guard against a null userInfoPopup. - showPopupTimer = new QTimer(this); - showPopupTimer->setSingleShot(true); - showPopupTimer->setInterval(280); - connect(showPopupTimer, &QTimer::timeout, this, [this] { - if (hoveredUser.isEmpty()) { - return; - } - // Resolve the row under the cursor again. In sectioned mode a user can - // own several rows (online + buddy), so the popup must anchor to the - // exact hovered row instead of a lookup by name. - const QPoint viewportPos = userTree->viewport()->mapFromGlobal(QCursor::pos()); - QTreeWidgetItem *item = userTree->itemAt(viewportPos); - if (item && item->type() == QTreeWidgetItem::Type && - QString::fromStdString(static_cast(item)->getUserInfo().name()) == hoveredUser) { - showPopupForUser(static_cast(item)); + // ── Hover popup ─────────────────────────────────────────────────────────── + m_userInfoPopup = new UserInfoPopup(tabSupervisor, tabSupervisor->getClient(), &avatarProvider->cache(), + &cardArtProvider->cache(), &cardArtParamsMap, + window()); // parented to main window so it floats above siblings + + m_userInfoPopup->hide(); + m_userInfoPopup->setWindowOpacity(0.0); + m_userInfoPopup->installEventFilter(this); + + connectPopupSignals(); + + m_showPopupTimer = new QTimer(this); + m_showPopupTimer->setSingleShot(true); + m_showPopupTimer->setInterval(280); + connect(m_showPopupTimer, &QTimer::timeout, this, [this] { + if (!m_hoveredUser.isEmpty()) { + showPopupForUser(m_hoveredUser); } }); - hidePopupTimer = new QTimer(this); - hidePopupTimer->setSingleShot(true); - hidePopupTimer->setInterval(160); - connect(hidePopupTimer, &QTimer::timeout, this, [this] { - // The hover ends when the cursor leaves the user row. Empty list - // space, a section divider and anything outside the tree all close - // the popup, while the popup itself keeps it alive. - if (!popupPinned && userInfoPopup && !userInfoPopup->underMouse() && - (hoveredUser.isEmpty() || !userTree->underMouse())) { + m_hidePopupTimer = new QTimer(this); + m_hidePopupTimer->setSingleShot(true); + m_hidePopupTimer->setInterval(160); + connect(m_hidePopupTimer, &QTimer::timeout, this, [this] { + if (!m_popupPinned && !m_userInfoPopup->underMouse() && !userTree->underMouse()) { hidePopup(); } }); - if (hasUserInfoPopup) { - // ── Hover popup ─────────────────────────────────────────────────────── - userInfoPopup = new UserInfoPopup(tabSupervisor, tabSupervisor->getClient(), &avatarProvider->cache(), - &cardArtProvider->cache(), &cardArtParamsMap, - window()); // parented to main window so it floats above siblings - - // The invite availability is scoped to the room this list belongs to, - // and gated on the room's buddy-only setting for the hovered user. - userInfoPopup->setGameInviteAvailable( - [this](const QString &userName) { return userContextMenu->hasGameInviteLink(userName); }); - - userInfoPopup->hide(); - userInfoPopup->setWindowOpacity(0.0); - userInfoPopup->installEventFilter(this); - - connectPopupSignals(); - } - userTree->setMouseTracking(true); userTree->viewport()->setMouseTracking(true); userTree->viewport()->installEventFilter(this); - userTree->installEventFilter(this); // keyboard handling for section dividers - if (hasUserInfoPopup) { - // Clicking anywhere outside the list clears its selection and closes the - // popup. The filter watches all widgets because the press can land on any - // part of the window, on another list or on the popup itself. - qApp->installEventFilter(this); + // Pin on item click + connect(userTree, &QTreeWidget::itemClicked, this, [this](QTreeWidgetItem *item, int) { + if (!SettingsCache::instance().appearance().getStyleUserList()) { + return; + } + const QString name = static_cast(item)->getUserInfo().name().c_str(); + m_popupPinned = false; // reset so showPopupForUser can update + showPopupForUser(name); + m_popupPinned = true; // pin after showing + }); - // Pin on item click - connect(userTree, &QTreeWidget::itemClicked, this, [this](QTreeWidgetItem *item, int) { - // Clicking a section divider toggles it - if (sectioned && item->type() == SectionItemType) { - setExpandedProgrammatically(item, !item->isExpanded()); - handleSectionExpansion(item, item->isExpanded()); - return; - } - if (!SettingsCache::instance().appearance().getStyleUserList()) { - return; - } - if (item->type() != QTreeWidgetItem::Type) { - return; // divider rows have no user popup - } - popupPinned = false; // reset so showPopupForUser can update - showPopupForUser(static_cast(item)); - popupPinned = true; // pin after showing - }); + connect(userTree->selectionModel(), &QItemSelectionModel::selectionChanged, this, + [this](const QItemSelection &sel, const QItemSelection &) { + // if (m_rebuildingTree) return; + if (sel.isEmpty() && m_popupPinned) { + m_popupPinned = false; + hidePopup(); + } + }); - connect(userTree->selectionModel(), &QItemSelectionModel::selectionChanged, this, - [this](const QItemSelection &sel, const QItemSelection &) { - if (sel.isEmpty() && popupPinned) { - popupPinned = false; - hidePopup(); - } - }); + // Hide popup when list scrolls (reference row has moved) + connect(userTree->verticalScrollBar(), &QScrollBar::valueChanged, this, [this] { + m_showPopupTimer->stop(); + hidePopup(true); + }); - // Keyboard selection: the popup is a mouse surface, so keyboard - // navigation shows no floating popup. A pinned (clicked) popup still - // follows the selection so it does not strand on a stale user while - // arrows move the cursor. - connect(userTree, &QTreeWidget::currentItemChanged, this, [this](QTreeWidgetItem *current, QTreeWidgetItem *) { - if (!popupPinned) { - return; // keyboard navigation shows no popup - } - if (!isVisible() || !SettingsCache::instance().appearance().getStyleUserList()) { - return; - } - if (current && current->type() == QTreeWidgetItem::Type) { - showPopupForUser(static_cast(current)); - } else { - popupPinned = false; - hidePopup(); - } - }); + // Forward join requests from popup upward + connect(m_userInfoPopup, &UserInfoPopup::joinGameRequested, this, &UserListWidget::joinGameRequested); - // Section dividers can be collapsed/expanded by the user. Surface those - // changes only from real user interaction. Programmatic expansion is - // applied through setSectionExpanded() / setExpandedProgrammatically(). - connect(userTree, &QTreeWidget::itemExpanded, this, - [this](QTreeWidgetItem *item) { handleSectionExpansion(item, true); }); - connect(userTree, &QTreeWidget::itemCollapsed, this, - [this](QTreeWidgetItem *item) { handleSectionExpansion(item, false); }); - - // Hide popup when list scrolls (reference row has moved) - connect(userTree->verticalScrollBar(), &QScrollBar::valueChanged, this, [this] { - showPopupTimer->stop(); - hidePopup(true); - requestVisibleItemResources(); - }); - - // Forward join requests from popup upward - connect(userInfoPopup, &UserInfoPopup::joinGameRequested, this, &UserListWidget::joinGameRequested); - } else { - // Dialog mode: keyboard selection drives the Invite button. - connect(userTree, &QTreeWidget::currentItemChanged, this, [this](QTreeWidgetItem *current, QTreeWidgetItem *) { - const QString userName = (current && current->type() == QTreeWidgetItem::Type) - ? current->data(2, Qt::UserRole).toString() - : QString(); - emit currentUserChanged(userName); - }); - - // Keep the popup-less scroll path alive for avatar prefetch. - connect(userTree->verticalScrollBar(), &QScrollBar::valueChanged, this, - [this] { requestVisibleItemResources(); }); - } - - // Section dividers can be collapsed/expanded by the user. Surface those - // changes only from real user interaction. Programmatic expansion is - // applied through setSectionExpanded() / setExpandedProgrammatically(). - connect(userTree, &QTreeWidget::itemExpanded, this, - [this](QTreeWidgetItem *item) { handleSectionExpansion(item, true); }); - connect(userTree, &QTreeWidget::itemCollapsed, this, - [this](QTreeWidgetItem *item) { handleSectionExpansion(item, false); }); - - connect(avatarProvider, &UserAvatarProvider::avatarUpdated, this, &UserListWidget::refreshVisibleUserHeader); - connect(cardArtProvider, &UserCardArtProvider::cardArtUpdated, this, &UserListWidget::refreshVisibleUserHeader); + connect(avatarProvider, &UserAvatarProvider::avatarUpdated, this, + [this](const QString &) { userTree->viewport()->update(); }); + connect(cardArtProvider, &UserCardArtProvider::cardArtUpdated, this, + [this](const QString &) { userTree->viewport()->update(); }); connect(&SettingsCache::instance().appearance(), &AppearanceSettings::styleUserListChanged, this, &UserListWidget::applyDisplayMode); applyDisplayMode(); - // The tree's cached palette can go stale after a runtime theme change (Qt - // freezes widget palettes when the style is switched), so rows and dividers - // derive all colors from the application palette at paint time. The theme - // change only needs a repaint to pick them up. - if (themeManager) { - connect(themeManager, &ThemeManager::themeChanged, this, [this] { - userTree->viewport()->update(); - userTree->update(); - }); - } - QVBoxLayout *vbox = new QVBoxLayout; vbox->addWidget(userTree); @@ -481,11 +565,6 @@ UserListWidget::UserListWidget(TabSupervisor *_tabSupervisor, retranslateUi(); } -UserListWidget::~UserListWidget() -{ - qApp->removeEventFilter(this); -} - void UserListWidget::bind(UserListManager *mgr) { manager = mgr; @@ -493,70 +572,50 @@ void UserListWidget::bind(UserListManager *mgr) // ── Full rebuild: disconnect / reconnect / bulk initial load ────────────── connect(manager, &UserListManager::listReset, this, &UserListWidget::rebuild); - if (!sectioned) { - // Online users list (AllUsersList / RoomList) - if (type == AllUsersList || type == RoomList) { - connect(manager, &UserListManager::userJoinedOnline, this, - [this](const ServerInfo_User &user) { processUserInfo(user, true); }); - connect(manager, &UserListManager::userLeftOnline, this, [this](const QString &name) { deleteUser(name); }); - } - - // Buddy list - if (type == BuddyList) { - connect(manager, &UserListManager::addedToBuddyList, this, [this](const ServerInfo_User &user) { - const QString name = QString::fromStdString(user.name()); - processUserInfo(user, manager->getOnlineUser(name) != nullptr); - }); - connect(manager, &UserListManager::removedFromBuddyList, this, - [this](const QString &name) { deleteUser(name); }); - // Track online presence changes for buddies already in the tree - connect(manager, &UserListManager::userJoinedOnline, this, [this](const ServerInfo_User &user) { - const QString name = QString::fromStdString(user.name()); - if (users.contains(name)) { - users[name]->setUserInfo(user); - setUserOnline(name, true); - } - }); - connect(manager, &UserListManager::userLeftOnline, this, [this](const QString &name) { - if (users.contains(name)) { - setUserOnline(name, false); - } - }); - } - - // Ignore list - if (type == IgnoreList) { - connect(manager, &UserListManager::addedToIgnoreList, this, [this](const ServerInfo_User &user) { - const QString name = QString::fromStdString(user.name()); - processUserInfo(user, manager->getOnlineUser(name) != nullptr); - }); - connect(manager, &UserListManager::removedFromIgnoreList, this, - [this](const QString &name) { deleteUser(name); }); - } - } else { - // Sectioned mode: one tree, every source feeds its own section. - // Sections are pure membership views: the "Online" section holds every - // currently online user, the "Buddy"/"Ignore" sections hold those - // lists. A user can therefore appear in several sections at once (an - // online buddy gets one row in each). + // ── Online users list (AllUsersList / RoomList) ─────────────────────────── + if (type == AllUsersList || type == RoomList) { connect(manager, &UserListManager::userJoinedOnline, this, - [this](const ServerInfo_User &user) { handleOnlineChange(user); }); - connect(manager, &UserListManager::userLeftOnline, this, - [this](const QString &name) { handleOnlineChangeLeft(name); }); - connect(manager, &UserListManager::addedToBuddyList, this, - [this](const ServerInfo_User &user) { handleListAdd(Section::Buddy, user); }); + [this](const ServerInfo_User &user) { processUserInfo(user, true); }); + connect(manager, &UserListManager::userLeftOnline, this, [this](const QString &name) { deleteUser(name); }); + } + + // ── Buddy list ──────────────────────────────────────────────────────────── + if (type == BuddyList) { + connect(manager, &UserListManager::addedToBuddyList, this, [this](const ServerInfo_User &user) { + const QString name = QString::fromStdString(user.name()); + processUserInfo(user, manager->getOnlineUser(name) != nullptr); + }); connect(manager, &UserListManager::removedFromBuddyList, this, - [this](const QString &name) { handleListRemove(Section::Buddy, name); }); - connect(manager, &UserListManager::addedToIgnoreList, this, - [this](const ServerInfo_User &user) { handleListAdd(Section::Ignore, user); }); + [this](const QString &name) { deleteUser(name); }); + // Track online presence changes for buddies already in the tree + connect(manager, &UserListManager::userJoinedOnline, this, [this](const ServerInfo_User &user) { + const QString name = QString::fromStdString(user.name()); + if (users.contains(name)) { + users[name]->setUserInfo(user); + setUserOnline(name, true); + } + }); + connect(manager, &UserListManager::userLeftOnline, this, [this](const QString &name) { + if (users.contains(name)) { + setUserOnline(name, false); + } + }); + } + + // ── Ignore list ─────────────────────────────────────────────────────────── + if (type == IgnoreList) { + connect(manager, &UserListManager::addedToIgnoreList, this, [this](const ServerInfo_User &user) { + const QString name = QString::fromStdString(user.name()); + processUserInfo(user, manager->getOnlineUser(name) != nullptr); + }); connect(manager, &UserListManager::removedFromIgnoreList, this, - [this](const QString &name) { handleListRemove(Section::Ignore, name); }); + [this](const QString &name) { deleteUser(name); }); } // ── Popup button refresh ────────────────────────────────────────────────── // Any buddy/ignore mutation while the popup is open refreshes its buttons auto refreshIfPopupOpen = [this](const QString &name) { - if (userInfoPopup && userInfoPopup->isVisible() && userInfoPopup->getCurrentUser() == name) { + if (m_userInfoPopup && m_userInfoPopup->isVisible() && m_userInfoPopup->currentUser() == name) { refreshPopupButtons(name); } }; @@ -574,22 +633,8 @@ void UserListWidget::bind(UserListManager *mgr) rebuild(); } -void UserListWidget::refreshVisibleUserHeader(const QString &name) -{ - userTree->viewport()->update(); - if (!userInfoPopup) { - return; - } - if (userInfoPopup->isVisible() && userInfoPopup->getCurrentUser() == name) { - userInfoPopup->refreshHeader(); - } -} - void UserListWidget::refreshPopupButtons(const QString &userName) { - if (!userInfoPopup) { - return; - } UserListTWI *item = users.value(userName); if (!item) { return; @@ -600,52 +645,29 @@ void UserListWidget::refreshPopupButtons(const QString &userName) const bool isBuddy = proxy->isUserBuddy(userName); const bool isIgn = proxy->isUserIgnored(userName); - userInfoPopup->updateActionButtons(item->getUserInfo(), online, isBuddy, isIgn); - positionPopup(item); // height may have changed, reposition + m_userInfoPopup->updateActionButtons(item->getUserInfo(), online, isBuddy, isIgn); + positionPopup(userName); // height may have changed — reposition } void UserListWidget::hideEvent(QHideEvent *e) { QGroupBox::hideEvent(e); - if (!userInfoPopup) { - return; - } - showPopupTimer->stop(); - hidePopupTimer->stop(); + m_showPopupTimer->stop(); + m_hidePopupTimer->stop(); hidePopup(true); } -void UserListWidget::showEvent(QShowEvent *e) -{ - QGroupBox::showEvent(e); - if (!userInfoPopup) { - return; - } - requestVisibleItemResources(); -} - void UserListWidget::applyDisplayMode() { const bool styled = SettingsCache::instance().appearance().getStyleUserList(); - // Both modes must keep the header length at the viewport width: with - // ScrollBarAlwaysOff a nonzero horizontal range is invisible but still - // pans via trackpad gestures, which reads as janky random drift. if (styled) { userTree->header()->setSectionResizeMode(0, QHeaderView::Stretch); userTree->hideColumn(1); userTree->hideColumn(2); userTree->hideColumn(3); } else { - // Bounded widths instead of ResizeToContents: content sizing measures - // the FULL text width while the delegate elides afterwards, so long - // names widened the header past the viewport. Fixed icon columns plus - // a stretched name column keep the range at zero, eliding trims. - userTree->header()->setSectionResizeMode(0, QHeaderView::Fixed); - userTree->header()->resizeSection(0, 24); - userTree->header()->setSectionResizeMode(1, QHeaderView::Fixed); - userTree->header()->resizeSection(1, 22); - userTree->header()->setSectionResizeMode(2, QHeaderView::Stretch); + userTree->header()->setSectionResizeMode(QHeaderView::ResizeToContents); userTree->showColumn(1); userTree->showColumn(2); userTree->hideColumn(3); @@ -656,126 +678,72 @@ void UserListWidget::applyDisplayMode() void UserListWidget::connectPopupSignals() { - connect(userInfoPopup, &UserInfoPopup::closeRequested, this, [this] { - popupPinned = false; + connect(m_userInfoPopup, &UserInfoPopup::closeRequested, this, [this] { + m_popupPinned = false; hidePopup(true); }); - connect(userInfoPopup, &UserInfoPopup::mouseEnteredPopup, hidePopupTimer, &QTimer::stop); - connect(userInfoPopup, &UserInfoPopup::mouseLeftPopup, this, [this] { - if (!popupPinned) { - hidePopupTimer->start(); + connect(m_userInfoPopup, &UserInfoPopup::mouseEnteredPopup, m_hidePopupTimer, &QTimer::stop); + connect(m_userInfoPopup, &UserInfoPopup::mouseLeftPopup, this, [this] { + if (!m_popupPinned) { + m_hidePopupTimer->start(); } }); // Wire all action signals to UserContextMenu::exec*() - connect(userInfoPopup, &UserInfoPopup::chatRequested, userContextMenu, &UserContextMenu::execChat); - connect(userInfoPopup, &UserInfoPopup::inviteRequested, this, - [this](const QString &userName) { userContextMenu->execInvite(userName); }); - connect(userInfoPopup, &UserInfoPopup::detailsRequested, userContextMenu, &UserContextMenu::execDetails); - connect(userInfoPopup, &UserInfoPopup::showGamesRequested, userContextMenu, &UserContextMenu::execShowGames); - connect(userInfoPopup, &UserInfoPopup::addBuddyRequested, userContextMenu, &UserContextMenu::execAddToBuddy); - connect(userInfoPopup, &UserInfoPopup::removeBuddyRequested, userContextMenu, + connect(m_userInfoPopup, &UserInfoPopup::chatRequested, userContextMenu, &UserContextMenu::execChat); + connect(m_userInfoPopup, &UserInfoPopup::detailsRequested, userContextMenu, &UserContextMenu::execDetails); + connect(m_userInfoPopup, &UserInfoPopup::showGamesRequested, userContextMenu, &UserContextMenu::execShowGames); + connect(m_userInfoPopup, &UserInfoPopup::addBuddyRequested, userContextMenu, &UserContextMenu::execAddToBuddy); + connect(m_userInfoPopup, &UserInfoPopup::removeBuddyRequested, userContextMenu, &UserContextMenu::execRemoveFromBuddy); - connect(userInfoPopup, &UserInfoPopup::addIgnoreRequested, userContextMenu, &UserContextMenu::execAddToIgnore); - connect(userInfoPopup, &UserInfoPopup::removeIgnoreRequested, userContextMenu, + connect(m_userInfoPopup, &UserInfoPopup::addIgnoreRequested, userContextMenu, &UserContextMenu::execAddToIgnore); + connect(m_userInfoPopup, &UserInfoPopup::removeIgnoreRequested, userContextMenu, &UserContextMenu::execRemoveFromIgnore); - connect(userInfoPopup, &UserInfoPopup::banRequested, userContextMenu, &UserContextMenu::execBan); - connect(userInfoPopup, &UserInfoPopup::warnRequested, userContextMenu, &UserContextMenu::execWarn); - connect(userInfoPopup, &UserInfoPopup::banHistoryRequested, userContextMenu, &UserContextMenu::execBanHistory); - connect(userInfoPopup, &UserInfoPopup::warnHistoryRequested, userContextMenu, &UserContextMenu::execWarnHistory); - connect(userInfoPopup, &UserInfoPopup::adminNotesRequested, userContextMenu, &UserContextMenu::execAdminNotes); - connect(userInfoPopup, &UserInfoPopup::promoteToModRequested, this, + connect(m_userInfoPopup, &UserInfoPopup::banRequested, userContextMenu, &UserContextMenu::execBan); + connect(m_userInfoPopup, &UserInfoPopup::warnRequested, userContextMenu, &UserContextMenu::execWarn); + connect(m_userInfoPopup, &UserInfoPopup::banHistoryRequested, userContextMenu, &UserContextMenu::execBanHistory); + connect(m_userInfoPopup, &UserInfoPopup::warnHistoryRequested, userContextMenu, &UserContextMenu::execWarnHistory); + connect(m_userInfoPopup, &UserInfoPopup::adminNotesRequested, userContextMenu, &UserContextMenu::execAdminNotes); + connect(m_userInfoPopup, &UserInfoPopup::promoteToModRequested, this, [this](const QString &n) { userContextMenu->execAdjustMod(n, true); }); - connect(userInfoPopup, &UserInfoPopup::demoteFromModRequested, this, + connect(m_userInfoPopup, &UserInfoPopup::demoteFromModRequested, this, [this](const QString &n) { userContextMenu->execAdjustMod(n, false); }); - connect(userInfoPopup, &UserInfoPopup::promoteToJudgeRequested, this, + connect(m_userInfoPopup, &UserInfoPopup::promoteToJudgeRequested, this, [this](const QString &n) { userContextMenu->execAdjustJudge(n, true); }); - connect(userInfoPopup, &UserInfoPopup::demoteFromJudgeRequested, this, + connect(m_userInfoPopup, &UserInfoPopup::demoteFromJudgeRequested, this, [this](const QString &n) { userContextMenu->execAdjustJudge(n, false); }); } bool UserListWidget::eventFilter(QObject *obj, QEvent *event) { - // A press outside the tree, the popup and any open menu deselects the - // list and closes the popup. The filter is installed application-wide, so - // the target can be any widget in the window or another list. - if (event->type() == QEvent::MouseButtonPress) { - auto *pressTarget = qobject_cast(obj); - if (pressTarget && !isPressInsideListUi(pressTarget)) { - clearSelectionAndClosePopup(); - } - } - - // Keyboard entry to the user context menu: the Menu key (or Shift+F10) - // pops the same menu the right-click shows, anchored to the focused row. - // Divider rows have no menu. Mouse-triggered context events are NOT handled - // here — the delegate's right-press path already pops the menu, and - // handling both would open two menus on one right-click. - if (hasUserInfoPopup && (obj == userTree || obj == userTree->viewport()) && event->type() == QEvent::ContextMenu) { - auto *contextEvent = static_cast(event); - if (contextEvent->reason() == QContextMenuEvent::Keyboard) { - QTreeWidgetItem *current = userTree->currentItem(); - if (current && current->type() == QTreeWidgetItem::Type) { - const QPoint globalPos = userTree->viewport()->mapToGlobal(userTree->visualItemRect(current).center()); - showContextMenu(globalPos, userTree->indexFromItem(current)); - return true; - } - return false; // divider rows: no menu - } - } - - // Keyboard navigation of the section dividers. - // The dividers are selectable so arrow keys land on them. When one is the - // current item, Enter/Space toggle it (like a button) and Left/Right follow - // the tree convention (Left collapses, Right expands). - if (obj == userTree && event->type() == QEvent::KeyPress) { - auto *keyEvent = static_cast(event); - QTreeWidgetItem *current = userTree->currentItem(); - if (sectioned && current && current->type() == SectionItemType) { - const bool toggle = keyEvent->key() == Qt::Key_Return || keyEvent->key() == Qt::Key_Enter || - keyEvent->key() == Qt::Key_Space; - const bool collapse = keyEvent->key() == Qt::Key_Left && current->isExpanded(); - const bool expand = keyEvent->key() == Qt::Key_Right && !current->isExpanded(); - if (toggle || collapse || expand) { - const bool expanded = toggle ? !current->isExpanded() : expand; - setExpandedProgrammatically(current, expanded); - handleSectionExpansion(current, expanded); - return true; - } - } - } - - if (hasUserInfoPopup && obj == userTree->viewport()) { + if (obj == userTree->viewport()) { if (event->type() == QEvent::MouseMove) { if (!SettingsCache::instance().appearance().getStyleUserList()) { return QGroupBox::eventFilter(obj, event); } auto *me = static_cast(event); - QTreeWidgetItem *hoveredItem = userTree->itemAt(me->pos()); - QString hovName; - if (hoveredItem && hoveredItem->type() == QTreeWidgetItem::Type) { - hovName = QString::fromStdString(static_cast(hoveredItem)->getUserInfo().name()); - } + auto *twi = static_cast(userTree->itemAt(me->pos())); + const QString hovName = twi ? QString::fromStdString(twi->getUserInfo().name()) : QString{}; - if (hovName != hoveredUser) { - hoveredUser = hovName; + if (hovName != m_hoveredUser) { + m_hoveredUser = hovName; if (!hovName.isEmpty()) { - hidePopupTimer->stop(); - if (!popupPinned) { - showPopupTimer->start(); + m_hidePopupTimer->stop(); + if (!m_popupPinned) { + m_showPopupTimer->start(); } } else { - showPopupTimer->stop(); - if (!popupPinned) { - hidePopupTimer->start(); + m_showPopupTimer->stop(); + if (!m_popupPinned) { + m_hidePopupTimer->start(); } } } } else if (event->type() == QEvent::Leave) { - hoveredUser.clear(); - showPopupTimer->stop(); - if (!popupPinned) { - hidePopupTimer->start(); + m_hoveredUser.clear(); + m_showPopupTimer->stop(); + if (!m_popupPinned) { + m_hidePopupTimer->start(); } } } @@ -783,76 +751,41 @@ bool UserListWidget::eventFilter(QObject *obj, QEvent *event) return QGroupBox::eventFilter(obj, event); } -void UserListWidget::showPopupForUser(UserListTWI *item) +void UserListWidget::showPopupForUser(const QString &userName) { - if (!userInfoPopup) { - return; - } + UserListTWI *item = users.value(userName); if (!item) { return; } - const QString userName = QString::fromStdString(item->getUserInfo().name()); - avatarProvider->requestAvatar(userName); // ensure the hovered user's avatar is fetched promptly - if (cardArtParamsMap.contains(userName)) { - const CardArtParams ¶ms = cardArtParamsMap.value(userName); - cardArtProvider->requestCardArt(userName, params.cardName, params.cardProviderId); - } - const ServerInfo_User &info = item->getUserInfo(); const bool online = item->data(0, UserListRoles::Online).toBool(); const bool isBuddy = userContextMenu->getUserListProxy()->isUserBuddy(userName); const bool isIgn = userContextMenu->getUserListProxy()->isUserIgnored(userName); - // The popup is already showing this user (e.g. arrow key navigation between - // the online/buddy rows of the same user): just reposition it. - if (userInfoPopup->isVisible() && userInfoPopup->getCurrentUser() == userName) { - positionPopup(item); - return; - } + m_userInfoPopup->showForUser(userName, info, online, isBuddy, isIgn); - // Cancel any pending show/hide so a hover timer that armed before a row - // selected via the keyboard cannot override it, and a pending hide cannot - // kill the popup right after it appears. - showPopupTimer->stop(); - hidePopupTimer->stop(); + // Realize the native window at opacity 0 before positioning so that: + // 1) move() applies to an existing native handle (not overridden by Qt's + // default centering logic on first show) + // 2) adjustSize() inside positionPopup() can measure the final laid-out + // geometry correctly + m_userInfoPopup->setWindowOpacity(0.0); + m_userInfoPopup->show(); + m_userInfoPopup->raise(); - userInfoPopup->showForUser(userName, info, online, isBuddy, isIgn); + positionPopup(userName); // geometry is now accurate; move() sticks - const bool wasVisible = userInfoPopup->isVisible(); - if (!wasVisible) { - // Realize the native window at opacity 0 before positioning so that: - // 1) move() applies to an existing native handle (not overridden by - // Qt's default centering logic on first show) - // 2) adjustSize() inside positionPopup() can measure the final - // laid out geometry correctly - userInfoPopup->setWindowOpacity(0.0); - } - userInfoPopup->show(); - userInfoPopup->raise(); - - positionPopup(item); // geometry is accurate after show, so move() is not overridden - - if (wasVisible) { - // Content swap while already open (hover or arrow key navigation): - // keep the popup opaque instead of flashing through a fade on every - // step. - userInfoPopup->setWindowOpacity(1.0); - return; - } - - auto *fade = new QPropertyAnimation(userInfoPopup, "windowOpacity", userInfoPopup); + auto *fade = new QPropertyAnimation(m_userInfoPopup, "windowOpacity", m_userInfoPopup); fade->setDuration(120); fade->setStartValue(0.0); fade->setEndValue(1.0); fade->start(QAbstractAnimation::DeleteWhenStopped); } -void UserListWidget::positionPopup(UserListTWI *item) +void UserListWidget::positionPopup(const QString &userName) { - if (!userInfoPopup) { - return; - } + UserListTWI *item = users.value(userName); if (!item) { return; } @@ -863,17 +796,12 @@ void UserListWidget::positionPopup(UserListTWI *item) const QPoint vpTL = vp->mapToGlobal(vp->rect().topLeft()); const QPoint vpTR = vp->mapToGlobal(vp->rect().topRight()); - userInfoPopup->adjustSize(); - const int popW = userInfoPopup->width(); - const int popH = userInfoPopup->height(); + m_userInfoPopup->adjustSize(); + const int popW = m_userInfoPopup->width(); + const int popH = m_userInfoPopup->height(); const int margin = 12; - QScreen *activeScreen = QGuiApplication::screenAt(itemTL); - if (!activeScreen) { - activeScreen = window()->screen(); - } - const QRect screen = - activeScreen ? activeScreen->availableGeometry() : QGuiApplication::primaryScreen()->availableGeometry(); + const QRect screen = QGuiApplication::primaryScreen()->availableGeometry(); // ── X: prefer the side with more space ─────────────────────────────────── const int spaceLeft = vpTL.x() - screen.left() - margin; @@ -902,53 +830,31 @@ void UserListWidget::positionPopup(UserListTWI *item) } y = qBound(screen.top() + margin, y, screen.bottom() - popH - margin); - userInfoPopup->move(x, y); + m_userInfoPopup->move(x, y); } void UserListWidget::hidePopup(bool immediate) { - if (!userInfoPopup) { - return; - } - showPopupTimer->stop(); - hidePopupTimer->stop(); - if (!userInfoPopup->isVisible()) { + m_showPopupTimer->stop(); + m_hidePopupTimer->stop(); + if (!m_userInfoPopup->isVisible()) { return; } if (immediate) { - userInfoPopup->hide(); + m_userInfoPopup->hide(); return; } // Fade out - auto *fade = new QPropertyAnimation(userInfoPopup, "windowOpacity", userInfoPopup); + auto *fade = new QPropertyAnimation(m_userInfoPopup, "windowOpacity", m_userInfoPopup); fade->setDuration(100); - fade->setStartValue(userInfoPopup->windowOpacity()); + fade->setStartValue(m_userInfoPopup->windowOpacity()); fade->setEndValue(0.0); - connect(fade, &QPropertyAnimation::finished, userInfoPopup, &QWidget::hide); + connect(fade, &QPropertyAnimation::finished, m_userInfoPopup, &QWidget::hide); fade->start(QAbstractAnimation::DeleteWhenStopped); } -bool UserListWidget::isPressInsideListUi(const QWidget *widget) const -{ - const QWidget *w = widget; - while (w) { - if (w == userTree || w == userInfoPopup || qobject_cast(w)) { - return true; - } - w = w->parentWidget(); - } - return false; -} - -void UserListWidget::clearSelectionAndClosePopup() -{ - popupPinned = false; - hidePopup(true); - userTree->clearSelection(); -} - void UserListWidget::retranslateUi() { userContextMenu->retranslateUi(); @@ -969,100 +875,17 @@ void UserListWidget::retranslateUi() updateCount(); } -void UserListWidget::beginBulkLoad() -{ - bulkLoading = true; -} - -void UserListWidget::endBulkLoad() -{ - bulkLoading = false; - sortItems(); - updateCount(); // divider counts were deferred during the bulk build - requestVisibleItemResources(); - userTree->viewport()->update(); -} - -bool UserListWidget::isItemNearViewport(const UserListTWI *item) const -{ - // Prefetch a full viewport of rows above and below so scrolling never shows - // an unloaded row. - const QRect nearView = - userTree->viewport()->rect().adjusted(0, -userTree->viewport()->height(), 0, userTree->viewport()->height()); - return userTree->visualItemRect(item).intersects(nearView); -} - -void UserListWidget::requestVisibleItemResources() -{ - const auto requestResources = [this](UserListTWI *twi) { - if (!isItemNearViewport(twi)) { - return; - } - const QString userName = QString::fromStdString(twi->getUserInfo().name()); - avatarProvider->requestAvatar(userName); - if (cardArtParamsMap.contains(userName)) { - const CardArtParams ¶ms = cardArtParamsMap.value(userName); - cardArtProvider->requestCardArt(userName, params.cardName, params.cardProviderId); - } - }; - - if (sectioned) { - // Top level items are dividers, user rows hang below them. - for (const Section section : sectionIds) { - QTreeWidgetItem *divider = sectionItems.value(section); - if (!divider) { - continue; - } - for (int i = 0; i < divider->childCount(); ++i) { - requestResources(static_cast(divider->child(i))); - } - } - return; - } - - for (int i = 0; i < userTree->topLevelItemCount(); ++i) { - requestResources(static_cast(userTree->topLevelItem(i))); - } -} - void UserListWidget::rebuild() { userTree->clear(); users.clear(); - sectionUsers.clear(); cardArtParamsMap.clear(); onlineCount = 0; - if (sectioned) { - createSectionItems(); - } - if (!manager) { return; } - if (sectioned) { - // Every source feeds its own section. Users that belong to several - // sources (an online buddy) get one row per section because - // ensureSectionMembership() creates the row when it is missing. - beginBulkLoad(); - const auto &onlineUsers = manager->getAllUsersList(); - for (auto it = onlineUsers.cbegin(); it != onlineUsers.cend(); ++it) { - processUserInfo(Section::Online, it.value(), true); - } - const auto &buddyUsers = manager->getBuddyList(); - for (auto it = buddyUsers.cbegin(); it != buddyUsers.cend(); ++it) { - processUserInfo(Section::Buddy, it.value(), manager->getOnlineUser(it.key()) != nullptr); - } - const auto &ignoreUsers = manager->getIgnoreList(); - for (auto it = ignoreUsers.cbegin(); it != ignoreUsers.cend(); ++it) { - processUserInfo(Section::Ignore, it.value(), manager->getOnlineUser(it.key()) != nullptr); - } - endBulkLoad(); - applyFilter(); - return; - } - const QMap *source = nullptr; switch (type) { @@ -1078,18 +901,19 @@ void UserListWidget::rebuild() break; } - beginBulkLoad(); for (auto it = source->cbegin(); it != source->cend(); ++it) { processUserInfo(it.value(), manager->getOnlineUser(it.key()) != nullptr); } - endBulkLoad(); - applyFilter(); + + sortItems(); } -void UserListWidget::updateCardArtParams(const ServerInfo_User &user, const QString &userName) +void UserListWidget::processUserInfo(const ServerInfo_User &user, bool online) { + const QString userName = QString::fromStdString(user.name()); + // Always update params from the latest ServerInfo_User, whether the - // item is new or existing, so a live server push refreshes the rendering. + // item is new or existing, so a live server-push refreshes the rendering. if (user.has_card_art_params()) { const auto &cap = user.card_art_params(); CardArtParams params; @@ -1101,20 +925,9 @@ void UserListWidget::updateCardArtParams(const ServerInfo_User &user, const QStr params.zoom = cap.zoom(); cardArtParamsMap.insert(userName, params); cardArtProvider->requestCardArt(userName, params.cardName, params.cardProviderId); + } else { + cardArtParamsMap.remove(userName); // clear stale params on removal } - // Intentionally no removal branch: buddy/ignore list copies never carry - // card_art_params (the server omits the column), so a params-less copy here - // means "this snapshot doesn't include it", not "the banner was removed". - // Removing on such copies would wipe banners that the live online list set. - // The map is rebuilt from scratch (clear() + repopulate) on every rebuild, - // which is what actually drops stale entries. -} - -void UserListWidget::processUserInfo(const ServerInfo_User &user, bool online) -{ - const QString userName = QString::fromStdString(user.name()); - - updateCardArtParams(user, userName); UserListTWI *item = users.value(userName); if (item) { @@ -1127,92 +940,37 @@ void UserListWidget::processUserInfo(const ServerInfo_User &user, bool online) ++onlineCount; } updateCount(); - if (!bulkLoading && isItemNearViewport(item)) { - avatarProvider->requestAvatar(userName); - } + avatarProvider->requestAvatar(userName); } item->setOnline(online); - if (!bulkLoading) { - sortItems(); - applyFilter(); - userTree->viewport()->update(); - } -} - -void UserListWidget::processUserInfo(Section section, const ServerInfo_User &user, bool online) -{ - ensureSectionMembership(section, user, online); - if (!bulkLoading) { - sortItems(); - applyFilter(); - userTree->viewport()->update(); - } + sortItems(); + userTree->viewport()->update(); } bool UserListWidget::deleteUser(const QString &userName) { - if (sectioned) { - // The user may own several rows (one per section). Drop them all. - bool removed = false; - const QList
sections = sectionUsers.keys(); // snapshot: maps mutate - for (const Section section : sections) { - removed = dropSectionMembership(section, userName) || removed; - } - if (removed && !bulkLoading) { - sortItems(); - applyFilter(); - userTree->viewport()->update(); - } - return removed; - } - UserListTWI *twi = users.value(userName); if (!twi) { return false; } users.remove(userName); - if (twi->parent()) { - twi->parent()->removeChild(twi); // sectioned mode: rows hang off a divider - } else { - userTree->takeTopLevelItem(userTree->indexOfTopLevelItem(twi)); - } - if (twi->data(0, UserListRoles::Online).toBool()) { + userTree->takeTopLevelItem(userTree->indexOfTopLevelItem(twi)); + if (twi->data(0, Qt::UserRole + 1).toBool()) { --onlineCount; } delete twi; updateCount(); - applyFilter(); return true; } void UserListWidget::setUserOnline(const QString &userName, bool online) { - if (sectioned) { - // The rows in the "Online" section are created/removed by the presence - // handlers. This only keeps the presence flag of the surviving rows - // (e.g. a buddy row after the user went offline) in sync. - for (auto it = sectionUsers.cbegin(); it != sectionUsers.cend(); ++it) { - UserListTWI *item = it.value().value(userName); - if (item) { - item->setOnline(online); - } - } - return; - } - UserListTWI *twi = users.value(userName); if (!twi) { return; } - // No state change: nothing to resort. This also keeps the presence - // broadcasts cheap (userJoinedOnline fires once per online user) when the - // row already carries the right flag. - if (twi->data(0, UserListRoles::Online).toBool() == online) { - return; - } - twi->setOnline(online); if (online) { ++onlineCount; @@ -1220,158 +978,26 @@ void UserListWidget::setUserOnline(const QString &userName, bool online) --onlineCount; } updateCount(); - - // Online users sort above offline users (UserListTWI::operator<), so a - // flag change moves the row. Resort to place the user by the new state. - if (!bulkLoading) { - sortItems(); - applyFilter(); - userTree->viewport()->update(); - } } void UserListWidget::updateCount() { - if (sectioned) { - // The dividers carry the section titles - setTitle(QString()); - for (const Section section : sectionIds) { - updateSectionDivider(section); - } - return; + QString str = titleStr; + if ((type == BuddyList) || (type == IgnoreList)) { + str = str.arg(onlineCount); } - - if (showTitle) { - QString str = titleStr; - if ((type == BuddyList) || (type == IgnoreList)) { - str = str.arg(onlineCount); - } - setTitle(str.arg(userTree->topLevelItemCount())); - } else { - setTitle(QString()); - } -} - -void UserListWidget::setShowTitle(bool _showTitle) -{ - this->showTitle = _showTitle; - updateCount(); -} - -void UserListWidget::setFilterText(const QString &text) -{ - if (filterText == text) { - return; - } - filterText = text; - applyFilter(); -} - -void UserListWidget::applyFilter() -{ - if (sectioned) { - const bool searching = !filterText.isEmpty(); - const QString lower = filterText.toLower(); - for (const Section section : sectionIds) { - QTreeWidgetItem *divider = sectionItems.value(section); - if (!divider) { - continue; - } - int visible = 0; - for (int i = 0; i < divider->childCount(); ++i) { - auto *child = static_cast(divider->child(i)); - const QString name = QString::fromStdString(child->getUserInfo().name()); - const bool passesFilter = - !userFilter || userFilter(name, child->data(0, UserListRoles::Online).toBool()); - const bool match = passesFilter && (!searching || name.toLower().contains(lower)); - child->setHidden(!match); - if (match) { - ++visible; - } - } - if (searching) { - // During a search the sections with matches stay open and empty - // sections disappear entirely. The persisted expansion state is - // untouched and restored when the search is cleared. - divider->setHidden(visible == 0); - setExpandedProgrammatically(divider, visible > 0); - } else { - divider->setHidden(false); - setExpandedProgrammatically(divider, expandedSections.contains(section)); - } - updateSectionDivider(section); - } - requestVisibleItemResources(); - userTree->viewport()->update(); - emit userListChanged(); - return; - } - - if (filterText.isEmpty()) { - for (auto it = users.cbegin(); it != users.cend(); ++it) { - it.value()->setHidden(false); - } - } else { - const QString lower = filterText.toLower(); - for (auto it = users.cbegin(); it != users.cend(); ++it) { - const bool match = QString::fromStdString(it.value()->getUserInfo().name()).toLower().contains(lower); - it.value()->setHidden(!match); - } - } - - requestVisibleItemResources(); - userTree->viewport()->update(); - emit userListChanged(); + setTitle(str.arg(userTree->topLevelItemCount())); } void UserListWidget::userClicked(QTreeWidgetItem *item, int /*column*/) { - if (item->type() != QTreeWidgetItem::Type) { - return; // divider rows open no chat - } - const QString userName = item->data(2, Qt::UserRole).toString(); - if (hasUserInfoPopup) { - emit openMessageDialog(userName, true); - } else { - emit userActivated(userName); - } -} - -int UserListWidget::visibleUserRowCount() const -{ - int count = 0; - if (sectioned) { - for (const Section section : sectionIds) { - QTreeWidgetItem *divider = sectionItems.value(section); - if (!divider || divider->isHidden()) { - continue; - } - for (int i = 0; i < divider->childCount(); ++i) { - if (!divider->child(i)->isHidden()) { - ++count; - } - } - } - return count; - } - for (int i = 0; i < userTree->topLevelItemCount(); ++i) { - QTreeWidgetItem *item = userTree->topLevelItem(i); - if (!item->isHidden() && item->type() == QTreeWidgetItem::Type) { - ++count; - } - } - return count; + emit openMessageDialog(item->data(2, Qt::UserRole).toString(), true); } void UserListWidget::showContextMenu(const QPoint &pos, const QModelIndex &index) { - QTreeWidgetItem *item = userTree->itemFromIndex(index); - if (!item || item->type() != QTreeWidgetItem::Type) { - return; // divider rows have no user menu - } - const auto *userItem = static_cast(item); - const ServerInfo_User &userInfo = userItem->getUserInfo(); - const bool online = userItem->data(0, UserListRoles::Online).toBool(); + const ServerInfo_User &userInfo = static_cast(userTree->topLevelItem(index.row()))->getUserInfo(); + bool online = index.sibling(index.row(), 0).data(Qt::UserRole + 1).toBool(); userContextMenu->showContextMenu(pos, QString::fromStdString(userInfo.name()), UserLevelFlags(userInfo.user_level()), online); @@ -1379,313 +1005,5 @@ void UserListWidget::showContextMenu(const QPoint &pos, const QModelIndex &index void UserListWidget::sortItems() { - if (sectioned) { - // Sorting must stay inside each section so the dividers keep their - // places as top level items. - for (auto it = sectionItems.cbegin(); it != sectionItems.cend(); ++it) { - it.value()->sortChildren(0, Qt::AscendingOrder); - } - return; - } userTree->sortItems(0, Qt::AscendingOrder); } - -// Sectioned mode - -void UserListWidget::setSectioned(const QList
&ids) -{ - if (sectioned || ids.isEmpty()) { - return; - } - - sectioned = true; - sectionIds = ids; - expandedSections.clear(); - for (const Section section : sectionIds) { - expandedSections.insert(section); // everything starts expanded - } - - // The single tree owns scrolling and the dividers carry the section titles, - // so the group box chrome and tree decorations collapse into a flat list. - setFlat(true); - setShowTitle(false); - userTree->setFrameStyle(QFrame::NoFrame); - // No tree branches: the dividers draw their own arrow glyph, so the rows can - // sit flush with the left border. - userTree->setRootIsDecorated(false); - userTree->setIndentation(0); - userTree->setAlternatingRowColors(false); - if (auto *listLayout = layout()) { - listLayout->setContentsMargins(0, 0, 0, 0); - } - - createSectionItems(); - updateCount(); -} - -void UserListWidget::createSectionItems() -{ - sectionItems.clear(); - QSignalBlocker blocker(userTree); // no expansion signals while building - for (const Section section : sectionIds) { - QTreeWidgetItem *divider = createSectionItem(section); - sectionItems.insert(section, divider); - divider->setExpanded(expandedSections.contains(section)); - } -} - -QTreeWidgetItem *UserListWidget::createSectionItem(Section section) -{ - Q_UNUSED(section); - auto *divider = new QTreeWidgetItem(SectionItemType); - // Selectable so keyboard navigation (Up/Down) can land on the dividers. - // They act as collapsible section headers once they have focus. - divider->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable); - - QFont font = userTree->font(); - font.setBold(true); - divider->setFont(0, font); - // A little taller than a plain text row so the header reads as a section - // separator without matching the full user row height. - divider->setSizeHint(0, QSize(0, QFontMetrics(font).height() + 16)); - - userTree->addTopLevelItem(divider); - // QTreeWidgetItem::setFirstColumnSpanned() does nothing while the item is - // detached from the tree (Qt returns early when treeModel() is null), so it - // must be called after addTopLevelItem(). Without the span the divider text - // is confined to column 0 and gets elided in unstyled mode. - divider->setFirstColumnSpanned(true); - return divider; -} - -QString UserListWidget::sectionTitle(Section section) const -{ - switch (section) { - case Section::Buddy: - return tr("Buddies"); - case Section::Online: - return tr("Online"); - case Section::Ignore: - return tr("Ignored"); - } - return {}; -} - -void UserListWidget::updateSectionDivider(Section section) -{ - QTreeWidgetItem *divider = sectionItems.value(section); - if (!divider) { - return; - } - int visible = 0; - int online = 0; - for (int i = 0; i < divider->childCount(); ++i) { - QTreeWidgetItem *child = divider->child(i); - if (!child->isHidden()) { - ++visible; - if (child->data(0, UserListRoles::Online).toBool()) { - ++online; - } - } - } - // The tree draws no branches (rows are flush), so the divider carries its - // own collapse arrow glyph. - const QString arrow = divider->isExpanded() ? QStringLiteral("\u25BE") : QStringLiteral("\u25B8"); - if (section == Section::Buddy) { - // The buddy divider reports how many of the shown buddies are online, - // mirroring the "Buddies online: %1 / %2" title of the non-sectioned - // buddy list. - divider->setText(0, tr("%1 %2 (%3/%4)").arg(arrow, sectionTitle(section)).arg(online).arg(visible)); - } else { - divider->setText(0, tr("%1 %2 (%3)").arg(arrow, sectionTitle(section)).arg(visible)); - } -} - -void UserListWidget::handleSectionExpansion(QTreeWidgetItem *item, bool expanded) -{ - if (!sectioned || item->type() != SectionItemType) { - return; - } - // Reverse lookup. Only three dividers exist, so a linear scan over the - // section map is cheaper than caching the section on each divider. - auto dividerIt = sectionItems.constBegin(); - while (dividerIt != sectionItems.constEnd() && dividerIt.value() != item) { - ++dividerIt; - } - if (dividerIt == sectionItems.constEnd()) { - return; - } - const Section section = dividerIt.key(); - if (expanded) { - expandedSections.insert(section); - } else { - expandedSections.remove(section); - } - updateSectionDivider(section); // the arrow glyph follows the state - emit sectionExpanded(section, expanded); -} - -void UserListWidget::setExpandedProgrammatically(QTreeWidgetItem *item, bool expanded) -{ - QSignalBlocker blocker(userTree); - item->setExpanded(expanded); -} - -void UserListWidget::setSectionExpanded(Section section, bool expanded) -{ - if (!sectioned) { - return; - } - if (expanded) { - expandedSections.insert(section); - } else { - expandedSections.remove(section); - } - QTreeWidgetItem *divider = sectionItems.value(section); - if (!divider) { - return; - } - QSignalBlocker blocker(userTree); - divider->setExpanded(expanded); - updateSectionDivider(section); // the arrow glyph follows the state - userTree->viewport()->update(); -} - -void UserListWidget::handleOnlineChange(const ServerInfo_User &user) -{ - // A user came online: they get a row in the "Online" section, plus (if - // applicable) a row in the buddy/ignore sections, which flip to online. - const QString name = QString::fromStdString(user.name()); - ensureSectionMembership(Section::Online, user, true); - if (manager->isUserBuddy(name)) { - ensureSectionMembership(Section::Buddy, user, true); - } - if (manager->isUserIgnored(name)) { - ensureSectionMembership(Section::Ignore, user, true); - } - finishSectionedMutation(); -} - -void UserListWidget::handleOnlineChangeLeft(const QString &userName) -{ - // The user is no longer online: their "Online" row disappears. Buddies and - // ignored users keep their own section's row, marked offline. A plain user - // has no rows left. - const bool dropped = dropSectionMembership(Section::Online, userName); - const bool kept = manager->isUserBuddy(userName) || manager->isUserIgnored(userName); - if (kept) { - setUserOnline(userName, false); - } - if (dropped || kept) { - finishSectionedMutation(); - } -} - -void UserListWidget::handleListAdd(Section section, const ServerInfo_User &user) -{ - const QString name = QString::fromStdString(user.name()); - const bool online = manager->getOnlineUser(name) != nullptr; - ensureSectionMembership(section, user, online); - if (online) { - // The user belongs to the "Online" section as well. Make sure the row - // exists even if the join event raced ahead of the list mutation. - ensureSectionMembership(Section::Online, user, true); - } - finishSectionedMutation(); -} - -void UserListWidget::handleListRemove(Section section, const QString &userName) -{ - // Only the row of the removed section disappears: an online user keeps - // their "Online" row, and other list memberships keep theirs. - if (dropSectionMembership(section, userName)) { - finishSectionedMutation(); - } -} - -UserListTWI *UserListWidget::ensureSectionMembership(Section section, const ServerInfo_User &user, bool online) -{ - const QString userName = QString::fromStdString(user.name()); - - updateCardArtParams(user, userName); - - // Dialog mode: rows that fail the user filter never exist. applyFilter() - // re-checks the predicate on every pass so a live state change (e.g. the - // user being ignored mid-dialog) hides an already created row. - if (userFilter && !userFilter(userName, online)) { - return nullptr; - } - - QTreeWidgetItem *divider = sectionItems.value(section); - if (!divider) { - return nullptr; - } - - QMap §ionMap = sectionUsers[section]; - UserListTWI *item = sectionMap.value(userName); - if (!item) { - item = new UserListTWI(user); - sectionMap.insert(userName, item); - divider->addChild(item); - if (!users.contains(userName)) { - users.insert(userName, item); // primary row for lookups by name - } - // The divider counts are refreshed once in endBulkLoad(). Calling - // updateCount() per row during a large rebuild would be quadratic. - if (!bulkLoading) { - updateCount(); // a new row changes the divider's count - } - if (!bulkLoading && isItemNearViewport(item)) { - avatarProvider->requestAvatar(userName); - } - } else { - item->setUserInfo(user); - } - item->setOnline(online); - return item; -} - -bool UserListWidget::dropSectionMembership(Section section, const QString &userName) -{ - QMap §ionMap = sectionUsers[section]; - UserListTWI *item = sectionMap.take(userName); - if (!item) { - return false; - } - - if (item->parent()) { - item->parent()->removeChild(item); - } else { - userTree->takeTopLevelItem(userTree->indexOfTopLevelItem(item)); - } - if (users.value(userName) == item) { - // Repoint the primary row at another surviving row, if any. - UserListTWI *replacement = nullptr; - for (auto it = sectionUsers.cbegin(); it != sectionUsers.cend() && !replacement; ++it) { - replacement = it.value().value(userName); - } - if (replacement) { - users.insert(userName, replacement); - } else { - users.remove(userName); - } - } - delete item; - updateCount(); - return true; -} - -void UserListWidget::finishSectionedMutation() -{ - if (bulkLoading) { - return; - } - sortItems(); - applyFilter(); - userTree->viewport()->update(); -} - -void UserListWidget::setGameInviteLinkProvider(std::function()> provider) -{ - userContextMenu->setGameInviteLinkProvider(std::move(provider)); -} diff --git a/cockatrice/src/interface/widgets/server/user/user_list_widget.h b/cockatrice/src/interface/widgets/server/user/user_list_widget.h index 412271160..d70cdfbbd 100644 --- a/cockatrice/src/interface/widgets/server/user/user_list_widget.h +++ b/cockatrice/src/interface/widgets/server/user/user_list_widget.h @@ -8,7 +8,6 @@ #define USERLIST_H #include "../../cards/card_info_picture_art_crop_widget.h" -#include "../../interface/widgets/server/game_link.h" #include "user_avatar_provider.h" #include "user_card_art_provider.h" #include "user_info_popup.h" @@ -16,12 +15,12 @@ #include "user_list_painter.h" #include +#include #include -#include +#include #include #include #include -#include #include #include @@ -29,23 +28,86 @@ class QTreeWidget; class ServerInfo_User; class AbstractClient; class TabSupervisor; +class QLabel; +class QCheckBox; +class QSpinBox; +class QRadioButton; +class QPlainTextEdit; class Response; class CommandContainer; class UserContextMenu; -class UserListWidget; -class QShowEvent; + +class BanDialog : public QDialog +{ + Q_OBJECT +private: + QLabel *daysLabel, *hoursLabel, *minutesLabel; + QCheckBox *nameBanCheckBox, *ipBanCheckBox, *idBanCheckBox, *deleteMessages; + QLineEdit *nameBanEdit, *ipBanEdit, *idBanEdit; + QSpinBox *daysEdit, *hoursEdit, *minutesEdit; + QRadioButton *permanentRadio, *temporaryRadio; + QPlainTextEdit *reasonEdit, *visibleReasonEdit; +private slots: + void okClicked(); + void enableTemporaryEdits(bool enabled); + +public: + explicit BanDialog(const ServerInfo_User &info, QWidget *parent = nullptr); + [[nodiscard]] QString getBanName() const; + [[nodiscard]] QString getBanIP() const; + [[nodiscard]] QString getBanId() const; + [[nodiscard]] int getMinutes() const; + [[nodiscard]] QString getReason() const; + [[nodiscard]] QString getVisibleReason() const; + [[nodiscard]] int getDeleteMessages() const; +}; + +class WarningDialog : public QDialog +{ + Q_OBJECT +private: + QLabel *descriptionLabel; + QLineEdit *nameWarning; + QComboBox *warningOption; + QLineEdit *warnClientID; + QCheckBox *deleteMessages; +private slots: + void okClicked(); + +public: + WarningDialog(const QString userName, const QString clientID, QWidget *parent = nullptr); + [[nodiscard]] QString getName() const; + [[nodiscard]] QString getWarnID() const; + [[nodiscard]] QString getReason() const; + [[nodiscard]] int getDeleteMessages() const; + void addWarningOption(const QString warning); +}; + +class AdminNotesDialog : public QDialog +{ + Q_OBJECT + +private: + QString userName; + QPlainTextEdit *notes; + +public: + explicit AdminNotesDialog(const QString &_userName, const QString &_notes, QWidget *_parent = nullptr); + [[nodiscard]] QString getName() const + { + return userName; + } + [[nodiscard]] QString getNotes() const; +}; class UserListItemDelegate : public QStyledItemDelegate { - QTreeWidget *tree; - UserListWidget *owner; const QMap *avatarCache; const QMap *cardArtCache; const QMap *cardArtParamsMap; public: - explicit UserListItemDelegate(UserListWidget *owner, - QTreeWidget *tree, + explicit UserListItemDelegate(QObject *const parent, const QMap *avatarCache, const QMap *cardArtCache, const QMap *cardArtParamsMap); @@ -84,12 +146,6 @@ public: BuddyList, IgnoreList }; - enum class Section - { - Buddy, - Online, - Ignore - }; private: UserListManager *manager = nullptr; @@ -97,75 +153,30 @@ private: UserCardArtProvider *cardArtProvider = nullptr; QMap cardArtParamsMap; // ── Hover popup ─────────────────────────────────────────────────────────── - UserInfoPopup *userInfoPopup = nullptr; - QTimer *showPopupTimer = nullptr; - QTimer *hidePopupTimer = nullptr; - QString hoveredUser; - bool popupPinned = false; - bool bulkLoading = false; - bool hasUserInfoPopup = true; - std::function userFilter; + UserInfoPopup *m_userInfoPopup = nullptr; + QTimer *m_showPopupTimer = nullptr; + QTimer *m_hidePopupTimer = nullptr; + QString m_hoveredUser; + bool m_popupPinned = false; - /** - * Popup functions are anchored on the row, not the user name. In sectioned - * mode a user can own several rows (online + buddy), and the popup must - * follow the hovered/selected row rather than a lookup by name. - */ - void showPopupForUser(UserListTWI *item); + void showPopupForUser(const QString &userName); void hidePopup(bool immediate = false); - void positionPopup(UserListTWI *item); + void positionPopup(const QString &userName); void connectPopupSignals(); - /** True when @p widget is the tree, the popup or an open menu. */ - bool isPressInsideListUi(const QWidget *widget) const; - void clearSelectionAndClosePopup(); - bool isItemNearViewport(const UserListTWI *item) const; - void requestVisibleItemResources(); - - // Sectioned mode (single tree with inline dividers) - bool sectioned = false; - QList
sectionIds; - QMap sectionItems; - // One row per (section, user): a user that is online AND a buddy appears in - // both the "Online" and the "Buddies" sections, so the same user can own - // several rows, each hanging off its section's divider. - QMap> sectionUsers; - QSet
expandedSections; - void createSectionItems(); - QTreeWidgetItem *createSectionItem(Section section); - [[nodiscard]] QString sectionTitle(Section section) const; - void updateSectionDivider(Section section); - void handleSectionExpansion(QTreeWidgetItem *item, bool expanded); - void setExpandedProgrammatically(QTreeWidgetItem *item, bool expanded); - void handleOnlineChange(const ServerInfo_User &user); - void handleOnlineChangeLeft(const QString &userName); - void handleListAdd(Section section, const ServerInfo_User &user); - void handleListRemove(Section section, const QString &userName); - /** Creates or updates the row for @p user in @p section. */ - UserListTWI *ensureSectionMembership(Section section, const ServerInfo_User &user, bool online); - /** Removes and deletes the row for @p userName in @p section. */ - bool dropSectionMembership(Section section, const QString &userName); - /** Sorts, refilters and repaints after a sectioned mode mutation. */ - void finishSectionedMutation(); - void updateCardArtParams(const ServerInfo_User &user, const QString &userName); - void processUserInfo(Section section, const ServerInfo_User &user, bool online); QMap users; TabSupervisor *tabSupervisor; AbstractClient *client; UserListType type; - QTreeWidget *userTree = nullptr; + QTreeWidget *userTree; UserListItemDelegate *itemDelegate; UserContextMenu *userContextMenu; int onlineCount; QString titleStr; - QString filterText; - bool showTitle = true; void updateCount(); - void applyFilter(); void refreshPopupButtons(const QString &userName); private slots: void userClicked(QTreeWidgetItem *item, int column); - void refreshVisibleUserHeader(const QString &name); signals: void openMessageDialog(const QString &userName, bool focus); void addBuddy(const QString &userName); @@ -173,60 +184,29 @@ signals: void addIgnore(const QString &userName); void removeIgnore(const QString &userName); void joinGameRequested(int gameId, int roomId, bool asSpectator); - void sectionExpanded(Section section, bool expanded); - /** Dialog mode: the user activated (Enter/double-click) the given row. */ - void userActivated(const QString &userName); - /** Dialog mode: the current row changed; empty string means no user row. */ - void currentUserChanged(const QString &userName); - /** The set of visible rows changed (filter, search or a live mutation). */ - void userListChanged(); public: UserListWidget(TabSupervisor *_tabSupervisor, AbstractClient *_client, UserListType _type, - QWidget *parent = nullptr, - bool hasUserInfoPopup = true); - ~UserListWidget() override; + QWidget *parent = nullptr); void bind(UserListManager *mgr); void applyDisplayMode(); - void beginBulkLoad(); - void endBulkLoad(); bool eventFilter(QObject *obj, QEvent *event) override; void retranslateUi(); void rebuild(); void processUserInfo(const ServerInfo_User &user, bool online); bool deleteUser(const QString &userName); void setUserOnline(const QString &userName, bool online); - void setFilterText(const QString &text); - void setShowTitle(bool showTitle); - void setSectioned(const QList
&ids); - void setSectionExpanded(Section section, bool expanded); - /** Dialog mode: rows that fail the predicate are never shown. */ - void setUserFilter(std::function filter) - { - userFilter = std::move(filter); - } - [[nodiscard]] int visibleUserRowCount() const; - [[nodiscard]] bool getHasUserInfoPopup() const - { - return hasUserInfoPopup; - } - [[nodiscard]] const QList
&getSectionIds() const - { - return sectionIds; - } [[nodiscard]] const QMap &getUsers() const { return users; } void showContextMenu(const QPoint &pos, const QModelIndex &index); void sortItems(); - void setGameInviteLinkProvider(std::function()> provider); protected: void hideEvent(QHideEvent *e) override; - void showEvent(QShowEvent *e) override; }; #endif 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 f161f0f19..c470e54fe 100644 --- a/cockatrice/src/interface/widgets/settings_page/appearance_settings_page.cpp +++ b/cockatrice/src/interface/widgets/settings_page/appearance_settings_page.cpp @@ -5,17 +5,13 @@ #include "../../client/settings/card_counter_settings.h" #include "../../palette_editor/palette_editor_dialog.h" #include "../dialogs/override_printing_warning.h" -#include "../general/home_tab_button_color.h" #include "../interface/theme_manager.h" #include "../interface/widgets/general/background_sources.h" -#include "../playmat/playmat_collection_dialog.h" -#include "../playmat/playmat_settings_dialog.h" #include #include #include #include -#include #include #include #include @@ -59,8 +55,8 @@ AppearanceSettingsPage::AppearanceSettingsPage() connect(&schemeCombo, &QComboBox::currentIndexChanged, this, [this] { themeManager->setColorScheme(schemeCombo.currentData().toString()); }); - // Qt widget style; "System" lets the application decide - styleCombo.addItem(tr("System"), QStringLiteral("System")); + // Qt widget style; "Default" lets the application decide + styleCombo.addItem(tr("Default"), QStringLiteral("Default")); for (const QString &key : QStyleFactory::keys()) { styleCombo.addItem(key, key); } @@ -132,18 +128,6 @@ AppearanceSettingsPage::AppearanceSettingsPage() connect(&homeTabDisplayCardNameCheckBox, &QCheckBox::QT_STATE_CHANGED, &settings.appearance(), &AppearanceSettings::setHomeTabDisplayCardName); - homeTabBackgroundDimCheckBox.setChecked(settings.appearance().getHomeTabBackgroundDim()); - connect(&homeTabBackgroundDimCheckBox, &QCheckBox::QT_STATE_CHANGED, &settings.appearance(), - &AppearanceSettings::setHomeTabBackgroundDim); - - for (const auto &entry : HomeTabButtonColor::all()) { - homeTabButtonColorSourceBox.addItem(QObject::tr(entry.trKey)); - } - - homeTabButtonColorSourceBox.setCurrentIndex(settings.appearance().getHomeTabButtonColorSourceIndex()); - connect(&homeTabButtonColorSourceBox, QOverload::of(&QComboBox::currentIndexChanged), &settings.appearance(), - &AppearanceSettings::setHomeTabButtonColorSourceIndex); - updateHomeTabSettingsVisibility(); auto *homeTabGrid = new QGridLayout; @@ -152,55 +136,10 @@ AppearanceSettingsPage::AppearanceSettingsPage() homeTabGrid->addWidget(&homeTabBackgroundShuffleFrequencyLabel, 1, 0); homeTabGrid->addWidget(&homeTabBackgroundShuffleFrequencySpinBox, 1, 1); homeTabGrid->addWidget(&homeTabDisplayCardNameCheckBox, 2, 0, 1, 2); - homeTabGrid->addWidget(&homeTabButtonColorSourceLabel, 3, 0); - homeTabGrid->addWidget(&homeTabButtonColorSourceBox, 3, 1); - homeTabGrid->addWidget(&homeTabBackgroundDimCheckBox, 4, 0, 1, 2); homeTabGroupBox = new QGroupBox; homeTabGroupBox->setLayout(homeTabGrid); - // Playmat settings - playmatVisibilityCombo.addItem(tr("Show all playmats"), PlaymatVisibilityAll); - playmatVisibilityCombo.addItem(tr("Show own playmat only"), PlaymatVisibilityOwnOnly); - playmatVisibilityCombo.addItem(tr("Don't use playmats"), PlaymatVisibilityNone); - int visIdx = playmatVisibilityCombo.findData(settings.userInterface().getPlaymatVisibility()); - if (visIdx >= 0) { - playmatVisibilityCombo.setCurrentIndex(visIdx); - } - connect(&playmatVisibilityCombo, qOverload(&QComboBox::currentIndexChanged), this, [this](int index) { - SettingsCache::instance().userInterface().setPlaymatVisibility(playmatVisibilityCombo.itemData(index).toInt()); - }); - playmatVisibilityLabel.setBuddy(&playmatVisibilityCombo); - - // Playmat mode: Override / Fallback / Deck-only - playmatModeCombo.addItem(tr("Override deck playmat"), PlaymatModeOverrideDeck); - playmatModeCombo.addItem(tr("Fallback if deck has none"), PlaymatModeFallback); - playmatModeCombo.addItem(tr("Deck only, ignore collection"), PlaymatModeDeckOnly); - int modeIdx = playmatModeCombo.findData(settings.userInterface().getPlaymatMode()); - if (modeIdx >= 0) { - playmatModeCombo.setCurrentIndex(modeIdx); - } - connect(&playmatModeCombo, qOverload(&QComboBox::currentIndexChanged), this, [this](int index) { - SettingsCache::instance().userInterface().setPlaymatMode(playmatModeCombo.itemData(index).toInt()); - }); - playmatModeLabel.setBuddy(&playmatModeCombo); - - // User-level playmat settings: fallback collection. - connect(&playmatDefaultEditButton, &QPushButton::clicked, this, - &AppearanceSettingsPage::openPlaymatCollectionDialog); - - auto *playmatGrid = new QGridLayout; - playmatGrid->addWidget(&playmatVisibilityLabel, 0, 0, 1, 1); - playmatGrid->addWidget(&playmatVisibilityCombo, 0, 1, 1, 1); - playmatGrid->addWidget(&playmatModeLabel, 1, 0, 1, 1); - playmatGrid->addWidget(&playmatModeCombo, 1, 1, 1, 1); - playmatGrid->addWidget(&playmatDefaultLabel, 2, 0, 1, 1); - playmatGrid->addWidget(&playmatDefaultEditButton, 2, 1, 1, 1); - - playmatGroupBox = new QGroupBox; - playmatGroupBox->setLayout(playmatGrid); - - // Styling settings styleUserListCheckBox.setChecked(settings.appearance().getStyleUserList()); connect(&styleUserListCheckBox, &QCheckBox::QT_STATE_CHANGED, &settings.appearance(), &AppearanceSettings::setStyleUserList); @@ -306,6 +245,7 @@ AppearanceSettingsPage::AppearanceSettingsPage() cardLayoutGroupBox->setLayout(cardLayoutGrid); // Card counter colors + auto *cardCounterColorsLayout = new QGridLayout; cardCounterColorsLayout->setColumnStretch(1, 1); cardCounterColorsLayout->setColumnStretch(3, 1); @@ -389,7 +329,6 @@ AppearanceSettingsPage::AppearanceSettingsPage() auto *mainLayout = new QVBoxLayout; mainLayout->addWidget(themeGroupBox); mainLayout->addWidget(homeTabGroupBox); - mainLayout->addWidget(playmatGroupBox); mainLayout->addWidget(stylingGroupBox); mainLayout->addWidget(menuGroupBox); mainLayout->addWidget(printingsGroupBox); @@ -436,8 +375,8 @@ void AppearanceSettingsPage::editPalette() void AppearanceSettingsPage::updateHomeTabSettingsVisibility() { - QString sourceId = SettingsCache::instance().appearance().getHomeTabBackgroundSource(); - bool visible = BackgroundSources::fromId(sourceId) != BackgroundSources::Theme; + bool visible = SettingsCache::instance().appearance().getHomeTabBackgroundSource() != + BackgroundSources::toId(BackgroundSources::Theme); homeTabBackgroundShuffleFrequencyLabel.setVisible(visible); homeTabBackgroundShuffleFrequencySpinBox.setVisible(visible); @@ -492,12 +431,6 @@ void AppearanceSettingsPage::cardViewExpandedRowsMaxChanged(int value) } } -void AppearanceSettingsPage::openPlaymatCollectionDialog() -{ - PlaymatCollectionDialog dialog(this); - dialog.exec(); -} - void AppearanceSettingsPage::retranslateUi() { themeGroupBox->setTitle(tr("Theme settings")); @@ -513,18 +446,6 @@ void AppearanceSettingsPage::retranslateUi() homeTabBackgroundShuffleFrequencyLabel.setText(tr("Home tab background shuffle frequency:")); homeTabBackgroundShuffleFrequencySpinBox.setSpecialValueText(tr("Disabled")); homeTabDisplayCardNameCheckBox.setText(tr("Display card name of background in bottom right")); - homeTabBackgroundDimCheckBox.setText(tr("Dim the home tab background")); - homeTabBackgroundDimCheckBox.setToolTip( - tr("Draw a translucent overlay over the home tab background so buttons and text stand out")); - homeTabButtonColorSourceLabel.setText(tr("Home tab button color:")); - homeTabButtonColorSourceBox.setToolTip( - tr("Use the theme's identity accent colors, or extract colors from the background image")); - - playmatGroupBox->setTitle(tr("Playmat settings")); - playmatVisibilityLabel.setText(tr("Playmat visibility:")); - playmatModeLabel.setText(tr("Default collection behavior:")); - playmatDefaultLabel.setText(tr("Default playmat collection:")); - playmatDefaultEditButton.setText(tr("Edit...")); stylingGroupBox->setTitle(tr("Styling settings")); styleUserListCheckBox.setText(tr("Style user list")); @@ -568,4 +489,4 @@ void AppearanceSettingsPage::retranslateUi() tableGroupBox->setTitle(tr("Table grid layout")); invertVerticalCoordinateCheckBox.setText(tr("Invert vertical coordinate")); minPlayersForMultiColumnLayoutLabel.setText(tr("Minimum player count for multi-column layout:")); -} +} \ No newline at end of file diff --git a/cockatrice/src/interface/widgets/settings_page/appearance_settings_page.h b/cockatrice/src/interface/widgets/settings_page/appearance_settings_page.h index bec4cd72f..0b6b6832c 100644 --- a/cockatrice/src/interface/widgets/settings_page/appearance_settings_page.h +++ b/cockatrice/src/interface/widgets/settings_page/appearance_settings_page.h @@ -24,7 +24,6 @@ private slots: void cardViewInitialRowsMaxChanged(int value); void cardViewExpandedRowsMaxChanged(int value); - void openPlaymatCollectionDialog(); private: QLabel themeLabel; @@ -35,65 +34,44 @@ private: QLabel styleComboLabel; QComboBox styleCombo; QPushButton editPaletteButton; - QLabel homeTabBackgroundSourceLabel; QComboBox homeTabBackgroundSourceBox; QLabel homeTabBackgroundShuffleFrequencyLabel; QSpinBox homeTabBackgroundShuffleFrequencySpinBox; QCheckBox homeTabDisplayCardNameCheckBox; - QCheckBox homeTabBackgroundDimCheckBox; - QLabel homeTabButtonColorSourceLabel; - QComboBox homeTabButtonColorSourceBox; - - QLabel playmatVisibilityLabel; - QComboBox playmatVisibilityCombo; - QLabel playmatModeLabel; - QComboBox playmatModeCombo; - QLabel playmatDefaultLabel; - QPushButton playmatDefaultEditButton; - QCheckBox styleUserListCheckBox; - QCheckBox showShortcutsCheckBox; QCheckBox showGameSelectorFilterToolbarCheckBox; - + QLabel minPlayersForMultiColumnLayoutLabel; + QLabel maxFontSizeForCardsLabel; QCheckBox overrideAllCardArtWithPersonalPreferenceCheckBox; QCheckBox bumpSetsWithCardsInDeckToTopCheckBox; - QCheckBox displayCardNamesCheckBox; QCheckBox autoRotateSidewaysLayoutCardsCheckBox; QCheckBox cardScalingCheckBox; QCheckBox roundCardCornersCheckBox; - QLabel maxFontSizeForCardsLabel; - QSpinBox maxFontSizeForCardsEdit; - QLabel verticalCardOverlapPercentLabel; QSpinBox verticalCardOverlapPercentBox; QLabel cardViewInitialRowsMaxLabel; QSpinBox cardViewInitialRowsMaxBox; QLabel cardViewExpandedRowsMaxLabel; QSpinBox cardViewExpandedRowsMaxBox; - - QList cardCounterNames; - QCheckBox horizontalHandCheckBox; QCheckBox leftJustifiedHandCheckBox; - QCheckBox invertVerticalCoordinateCheckBox; - QLabel minPlayersForMultiColumnLayoutLabel; - QSpinBox minPlayersForMultiColumnLayoutEdit; - QGroupBox *themeGroupBox; QGroupBox *homeTabGroupBox; - QGroupBox *playmatGroupBox; QGroupBox *stylingGroupBox; QGroupBox *menuGroupBox; QGroupBox *printingsGroupBox; QGroupBox *cardsGroupBox; QGroupBox *cardLayoutGroupBox; - QGroupBox *cardCountersGroupBox; QGroupBox *handGroupBox; QGroupBox *tableGroupBox; + QGroupBox *cardCountersGroupBox; + QList cardCounterNames; + QSpinBox minPlayersForMultiColumnLayoutEdit; + QSpinBox maxFontSizeForCardsEdit; public: AppearanceSettingsPage(); 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 f3eac05b8..f425afe60 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,6 @@ #include "deck_editor_settings_page.h" #include "../../../client/settings/cache_settings.h" -#include "../../pixel_map_generator.h" #include "update/card_spoiler/spoiler_background_updater.h" #include @@ -54,15 +53,15 @@ DeckEditorSettingsPage::DeckEditorSettingsPage() urlList->addItems(SettingsCache::instance().downloads().getAllURLs()); aAdd = new QAction(this); - aAdd->setIcon(themePixmap(QStringLiteral("icons/increment"))); + aAdd->setIcon(QPixmap("theme:icons/increment")); connect(aAdd, &QAction::triggered, this, &DeckEditorSettingsPage::actAddURL); aEdit = new QAction(this); - aEdit->setIcon(themePixmap(QStringLiteral("icons/pencil"))); + aEdit->setIcon(QPixmap("theme:icons/pencil")); connect(aEdit, &QAction::triggered, this, &DeckEditorSettingsPage::actEditURL); aRemove = new QAction(this); - aRemove->setIcon(themePixmap(QStringLiteral("icons/decrement"))); + aRemove->setIcon(QPixmap("theme:icons/decrement")); connect(aRemove, &QAction::triggered, this, &DeckEditorSettingsPage::actRemoveURL); auto *urlToolBar = new QToolBar; diff --git a/cockatrice/src/interface/widgets/settings_page/general_settings_page.cpp b/cockatrice/src/interface/widgets/settings_page/general_settings_page.cpp index b0fd0e018..91c4943e1 100644 --- a/cockatrice/src/interface/widgets/settings_page/general_settings_page.cpp +++ b/cockatrice/src/interface/widgets/settings_page/general_settings_page.cpp @@ -1,24 +1,16 @@ #include "general_settings_page.h" #include "../../../client/settings/cache_settings.h" -#include "../interface/card_picture_loader/card_picture_loader.h" #include "../main.h" -#include "../server/user/user_info_connection.h" #include "update/client/release_channel.h" #include -#include #include #include #include -#include #include -#include -#include -#include #include #include -#include #include #include @@ -52,40 +44,10 @@ GeneralSettingsPage::GeneralSettingsPage() connect(&languageBox, qOverload(&QComboBox::currentIndexChanged), this, &GeneralSettingsPage::languageBoxChanged); - // card text & images language, independent of the UI language - cardLanguageBox.addItem(tr("English"), "en"); - for (const QString &code : CardLocalization::supportedLanguages()) { - cardLanguageBox.addItem(CardLocalization::languageDisplayName(code), code); - } - const int cardLangIndex = cardLanguageBox.findData(SettingsCache::instance().cardsDisplay().getCardLang()); - cardLanguageBox.setCurrentIndex(cardLangIndex < 0 ? 0 : cardLangIndex); - - connect(&cardLanguageBox, qOverload(&QComboBox::currentIndexChanged), this, - &GeneralSettingsPage::cardLanguageBoxChanged); - - // card search language, independent of the card display language - cardSearchLanguageBox.addItem(""); // texts set in retranslateUi - cardSearchLanguageBox.addItem(""); - cardSearchLanguageBox.addItem(""); - const int cardSearchLanguageIndex = SettingsCache::instance().cardsDisplay().getCardSearchLanguage(); - cardSearchLanguageBox.setCurrentIndex(cardSearchLanguageIndex < 0 ? static_cast(SearchLanguageMode::English) - : cardSearchLanguageIndex); - - connect(&cardSearchLanguageBox, qOverload(&QComboBox::currentIndexChanged), this, - &GeneralSettingsPage::cardSearchLanguageBoxChanged); - auto *languageGrid = new QGridLayout; languageGrid->addWidget(&languageLabel, 0, 0); languageGrid->addWidget(&languageBox, 0, 1); - languageGrid->addWidget(&cardLanguageLabel, 1, 0); - languageGrid->addWidget(&cardLanguageBox, 1, 1); - languageGrid->addWidget(&cardLanguageNoteLabel, 2, 1); - languageGrid->addWidget(&cardSearchLanguageLabel, 3, 0); - languageGrid->addWidget(&cardSearchLanguageBox, 3, 1); - languageGrid->addWidget(&advertiseTranslationPageLabel, 4, 1, Qt::AlignRight); - - cardLanguageNoteLabel.setWordWrap(true); - cardLanguageNoteLabel.setAlignment(Qt::AlignLeft | Qt::AlignVCenter); + languageGrid->addWidget(&advertiseTranslationPageLabel, 1, 1, Qt::AlignRight); languageGroupBox = new QGroupBox; languageGroupBox->setLayout(languageGrid); @@ -159,61 +121,8 @@ GeneralSettingsPage::GeneralSettingsPage() connect(&showTipsOnStartup, &QCheckBox::clicked, &settings.personal(), &PersonalSettings::setShowTipsOnStartup); - // startup destination - for (int i = 0; i < 8; ++i) { - startupTabSelector.addItem(""); // texts set in retranslateUi - } - startupTabSelector.setCurrentIndex(settings.tabs().getStartupTabIndex()); - - connect(&startupTabSelector, qOverload(&QComboBox::currentIndexChanged), &settings.tabs(), - &TabsSettings::setStartupTabIndex); - connect(&startupTabSelector, qOverload(&QComboBox::currentIndexChanged), this, - &GeneralSettingsPage::updateStartupServerControlsVisibility); - - const QString savedHost = settings.tabs().getStartupServerHost(); - const QString savedPort = settings.tabs().getStartupServerPort(); - int startupServerIndex = -1; - UserConnection_Information uci; - for (const auto &savedServer : uci.getServerInfo()) { - const UserConnection_Information &info = savedServer.second; - const QString saveName = info.getSaveName(); - if (saveName.isEmpty()) { - continue; - } - startupServerSelector.addItem(saveName, QVariantList{info.getServer(), info.getPort()}); - if (startupServerIndex == -1 && info.getServer() == savedHost && info.getPort() == savedPort) { - startupServerIndex = startupServerSelector.count() - 1; - } - } - startupServerSelector.setCurrentIndex(startupServerIndex); - - connect(&startupServerSelector, qOverload(&QComboBox::currentIndexChanged), this, [this](int index) { - const QVariantList serverInfo = startupServerSelector.itemData(index).toList(); - if (serverInfo.size() != 2) { - return; - } - TabsSettings &tabs = SettingsCache::instance().tabs(); - tabs.setStartupServerHost(serverInfo[0].toString()); - tabs.setStartupServerPort(serverInfo[1].toString()); - }); - - startupRoomNameEdit = new QLineEdit(settings.tabs().getStartupRoomName()); - // Default (Expanding) would stretch the whole controls column when this row becomes visible, - // so size it like the combo boxes instead: fills the column, never widens it. - startupRoomNameEdit->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); - connect(startupRoomNameEdit, &QLineEdit::editingFinished, this, - [this] { SettingsCache::instance().tabs().setStartupRoomName(startupRoomNameEdit->text().trimmed()); }); - auto *startupGrid = new QGridLayout; startupGrid->addWidget(&showTipsOnStartup, 0, 0, 1, 2); - startupGrid->addWidget(&startupTabLabel, 1, 0); - startupGrid->addWidget(&startupTabSelector, 1, 1); - startupGrid->addWidget(&startupServerLabel, 2, 0); - startupGrid->addWidget(&startupServerSelector, 2, 1); - startupGrid->addWidget(&startupRoomLabel, 3, 0); - startupGrid->addWidget(startupRoomNameEdit, 3, 1); - - updateStartupServerControlsVisibility(); startupGroupBox = new QGroupBox; startupGroupBox->setLayout(startupGrid); @@ -448,102 +357,31 @@ void GeneralSettingsPage::languageBoxChanged(int index) SettingsCache::instance().personal().setLang(languageBox.itemData(index).toString()); } -void GeneralSettingsPage::cardLanguageBoxChanged(int index) -{ - const QString lang = cardLanguageBox.itemData(index).toString(); - SettingsCache::instance().cardsDisplay().setCardLang(lang); - - // Switching to a non-default language only takes effect after the card - // database is re-imported with that language selected; English data is always - // present, so switching back to English needs no prompt. - if (lang == "en") { - return; - } - - // The binary cache does not track the language its entries were imported in, - // and the downloaded pictures were fetched with English art names, so both are - // stale until Oracle re-imports the database in the new language: drop them. - QFile::remove(SettingsCache::instance().getCardDatabasePath() + ".cache"); - CardPictureLoader::clearNetworkCache(); - CardPictureLoader::clearPixmapCache(); - - // Art is resolved by the translated card name for non-English languages, so the - // matching Scryfall URL is added to the top of the download list. It stays - // visible in the deck editor settings, where it can be removed or reordered. - const bool localizedUrlAdded = SettingsCache::instance().downloads().addLocalizedScryfallUrl(); - - QString message = tr("

The card database only contains English card data. To see cards in %1, " - "Oracle must run once with this language selected and re-import the card " - "database.

" - "

The cached database and the downloaded card pictures have been cleared, so a " - "re-import is picked up without stale entries.

") - .arg(cardLanguageBox.itemText(index)); - if (localizedUrlAdded) { - message += tr("

The Scryfall URL that resolves card art by translated name was added to the top of your " - "download list. You can remove or reorder it any time.

"); - } - message += tr("

Run Oracle now?

"); - - const QMessageBox::StandardButton answer = QMessageBox::question( - this, tr("Card text & images language changed"), message, QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes); - - // The answer only controls whether Oracle starts right away; the caches stay - // cleared so the next import or launch rebuilds them in the new language. - if (answer == QMessageBox::Yes) { - emit cardDatabaseUpdateRequested(); - } -} - -void GeneralSettingsPage::cardSearchLanguageBoxChanged(int index) -{ - SettingsCache::instance().cardsDisplay().setCardSearchLanguage(index); -} - -void GeneralSettingsPage::updateStartupServerControlsVisibility() -{ - const int index = startupTabSelector.currentIndex(); - const bool serverNeeded = index == StartupTab::StartupTabServer || index == StartupTab::StartupTabServerRoom; - const bool roomNeeded = index == StartupTab::StartupTabServerRoom; - startupServerLabel.setVisible(serverNeeded); - startupServerSelector.setVisible(serverNeeded); - startupRoomLabel.setVisible(roomNeeded); - startupRoomNameEdit->setVisible(roomNeeded); -} - void GeneralSettingsPage::retranslateUi() { - const auto &settings = SettingsCache::instance(); - languageGroupBox->setTitle(tr("Language settings")); languageLabel.setText(tr("Language:")); - cardLanguageBox.setItemText(0, tr("English")); - cardLanguageLabel.setText(tr("Card text & images language:")); - cardLanguageNoteLabel.setText( - tr("Foreign card names, text and art apply after you update the card database (Oracle).")); - cardSearchLanguageLabel.setText(tr("Language used in card search:")); - cardSearchLanguageBox.setItemText(static_cast(SearchLanguageMode::English), tr("English")); - cardSearchLanguageBox.setItemText(static_cast(SearchLanguageMode::Selected), - tr("Selected card language (untranslated cards still match in English)")); - cardSearchLanguageBox.setItemText(static_cast(SearchLanguageMode::Both), - tr("English and selected card language")); - advertiseTranslationPageLabel.setText( - QString("%2").arg(WIKI_TRANSLATION_FAQ).arg(tr("How to help with translations"))); versionGroupBox->setTitle(tr("Version settings")); + cardDatabaseGroupBox->setTitle(tr("Card database")); + startupGroupBox->setTitle(tr("Startup settings")); + + if (SettingsCache::instance().getIsPortableBuild()) { + pathsGroupBox->setTitle(tr("Paths (editing disabled in portable mode)")); + } else { + pathsGroupBox->setTitle(tr("Paths")); + } + advertiseTranslationPageLabel.setText( + QString("%2").arg(WIKI_TRANSLATION_FAQ).arg(tr("How to help with translations"))); + deckPathLabel.setText(tr("Decks directory:")); + filtersPathLabel.setText(tr("Filters directory:")); + replaysPathLabel.setText(tr("Replays directory:")); + picsPathLabel.setText(tr("Pictures directory:")); + cardDatabasePathLabel.setText(tr("Card database:")); + customCardDatabasePathLabel.setText(tr("Custom database directory:")); + tokenDatabasePathLabel.setText(tr("Token database:")); updateReleaseChannelLabel.setText(tr("Update channel")); startupUpdateCheckCheckBox.setText(tr("Check for client updates on startup")); - updateNotificationCheckBox.setText(tr("Notify if a feature supported by the server is missing in my client")); - newVersionOracleCheckBox.setText(tr("Automatically run Oracle when running a new version of Cockatrice")); - - // We can't change the strings after they're put into the QComboBox, so this is our workaround - int oldIndex = updateReleaseChannelBox.currentIndex(); - updateReleaseChannelBox.clear(); - for (ReleaseChannel *chan : settings.getUpdateReleaseChannels()) { - updateReleaseChannelBox.addItem(tr(chan->getName().toUtf8())); - } - updateReleaseChannelBox.setCurrentIndex(oldIndex); - - cardDatabaseGroupBox->setTitle(tr("Card database")); startupCardUpdateCheckBehaviorLabel.setText(tr("Check for card database updates on startup")); startupCardUpdateCheckBehaviorSelector.setItemText(startupCardUpdateCheckBehaviorIndexNone, tr("Don't check")); startupCardUpdateCheckBehaviorSelector.setItemText(startupCardUpdateCheckBehaviorIndexPrompt, @@ -552,40 +390,24 @@ void GeneralSettingsPage::retranslateUi() tr("Always update in the background")); cardUpdateCheckIntervalLabel.setText(tr("Check for card database updates every")); cardUpdateCheckIntervalSpinBox.setSuffix(tr(" days")); + updateNotificationCheckBox.setText(tr("Notify if a feature supported by the server is missing in my client")); + newVersionOracleCheckBox.setText(tr("Automatically run Oracle when running a new version of Cockatrice")); + showTipsOnStartup.setText(tr("Show tips on startup")); + resetAllPathsButton->setText(tr("Reset all paths")); + + const auto &settings = SettingsCache::instance(); QDate lastCheckDate = settings.updates().getLastCardUpdateCheck(); int daysAgo = lastCheckDate.daysTo(QDate::currentDate()); + lastCardUpdateCheckDateLabel.setText( tr("Last update check on %1 (%2 days ago)").arg(lastCheckDate.toString()).arg(daysAgo)); - startupGroupBox->setTitle(tr("Startup settings")); - showTipsOnStartup.setText(tr("Show tips on startup")); - startupTabLabel.setText(tr("Startup tab:")); - startupTabSelector.setItemText(StartupTab::StartupTabHome, tr("Home")); - startupTabSelector.setItemText(StartupTab::StartupTabVisualDeckStorage, tr("Visual Deck Storage")); - startupTabSelector.setItemText(StartupTab::StartupTabDeckStorage, tr("Deck Storage")); - startupTabSelector.setItemText(StartupTab::StartupTabReplays, tr("Game Replays")); - startupTabSelector.setItemText(StartupTab::StartupTabDeckEditor, tr("Deck Editor")); - startupTabSelector.setItemText(StartupTab::StartupTabVisualDeckEditor, tr("Visual Deck Editor")); - startupTabSelector.setItemText(StartupTab::StartupTabServer, tr("Server")); - startupTabSelector.setItemText(StartupTab::StartupTabServerRoom, tr("Server Room")); - startupTabSelector.setToolTip( - tr("The tab shown when Cockatrice starts. If the chosen tab is not open yet, it is opened.")); - startupServerLabel.setText(tr("Server:")); - startupRoomLabel.setText(tr("Room:")); - startupRoomNameEdit->setPlaceholderText(tr("Room name")); - - if (settings.getIsPortableBuild()) { - pathsGroupBox->setTitle(tr("Paths (editing disabled in portable mode)")); - } else { - pathsGroupBox->setTitle(tr("Paths")); + // We can't change the strings after they're put into the QComboBox, so this is our workaround + int oldIndex = updateReleaseChannelBox.currentIndex(); + updateReleaseChannelBox.clear(); + for (ReleaseChannel *chan : settings.getUpdateReleaseChannels()) { + updateReleaseChannelBox.addItem(tr(chan->getName().toUtf8())); } - deckPathLabel.setText(tr("Decks directory:")); - filtersPathLabel.setText(tr("Filters directory:")); - replaysPathLabel.setText(tr("Replays directory:")); - picsPathLabel.setText(tr("Pictures directory:")); - cardDatabasePathLabel.setText(tr("Card database:")); - customCardDatabasePathLabel.setText(tr("Custom database directory:")); - tokenDatabasePathLabel.setText(tr("Token database:")); - resetAllPathsButton->setText(tr("Reset all paths")); -} + updateReleaseChannelBox.setCurrentIndex(oldIndex); +} \ No newline at end of file diff --git a/cockatrice/src/interface/widgets/settings_page/general_settings_page.h b/cockatrice/src/interface/widgets/settings_page/general_settings_page.h index 07e0ba3a9..8aa39ff65 100644 --- a/cockatrice/src/interface/widgets/settings_page/general_settings_page.h +++ b/cockatrice/src/interface/widgets/settings_page/general_settings_page.h @@ -20,13 +20,6 @@ public: GeneralSettingsPage(); void retranslateUi() override; - static QStringList findQmFiles(); - static QString languageName(const QString &lang); - -signals: - /// Request to re-import the card database with the newly selected card language - void cardDatabaseUpdateRequested(); - private slots: void deckPathButtonClicked(); void filtersPathButtonClicked(); @@ -37,55 +30,17 @@ private slots: void tokenDatabasePathButtonClicked(); void resetAllPathsClicked(); void languageBoxChanged(int index); - void cardLanguageBoxChanged(int index); - void cardSearchLanguageBoxChanged(int index); - void updateStartupServerControlsVisibility(); private: + QStringList findQmFiles(); + QString languageName(const QString &lang); + QGroupBox *languageGroupBox; QGroupBox *versionGroupBox; QGroupBox *cardDatabaseGroupBox; QGroupBox *startupGroupBox; QGroupBox *pathsGroupBox; - QLabel languageLabel; - QComboBox languageBox; - QLabel advertiseTranslationPageLabel; - - QLabel cardLanguageLabel; - QComboBox cardLanguageBox; - QLabel cardLanguageNoteLabel; - - QLabel cardSearchLanguageLabel; - QComboBox cardSearchLanguageBox; - - QLabel updateReleaseChannelLabel; - QComboBox updateReleaseChannelBox; - QCheckBox startupUpdateCheckCheckBox; - QCheckBox updateNotificationCheckBox; - QCheckBox newVersionOracleCheckBox; - - QLabel startupCardUpdateCheckBehaviorLabel; - QComboBox startupCardUpdateCheckBehaviorSelector; - QLabel cardUpdateCheckIntervalLabel; - QSpinBox cardUpdateCheckIntervalSpinBox; - QLabel lastCardUpdateCheckDateLabel; - - QCheckBox showTipsOnStartup; - QLabel startupTabLabel; - QComboBox startupTabSelector; - QLabel startupServerLabel; - QComboBox startupServerSelector; - QLabel startupRoomLabel; - QLineEdit *startupRoomNameEdit; - - QLabel deckPathLabel; - QLabel filtersPathLabel; - QLabel replaysPathLabel; - QLabel picsPathLabel; - QLabel cardDatabasePathLabel; - QLabel customCardDatabasePathLabel; - QLabel tokenDatabasePathLabel; QLineEdit *deckPathEdit; QLineEdit *filtersPathEdit; QLineEdit *replaysPathEdit; @@ -95,6 +50,27 @@ private: QLineEdit *tokenDatabasePathEdit; QPushButton *resetAllPathsButton; QLabel *allPathsResetLabel; + QComboBox languageBox; + QCheckBox startupUpdateCheckCheckBox; + QLabel startupCardUpdateCheckBehaviorLabel; + QComboBox startupCardUpdateCheckBehaviorSelector; + QLabel cardUpdateCheckIntervalLabel; + QSpinBox cardUpdateCheckIntervalSpinBox; + QLabel lastCardUpdateCheckDateLabel; + QCheckBox updateNotificationCheckBox; + QCheckBox newVersionOracleCheckBox; + QComboBox updateReleaseChannelBox; + QLabel languageLabel; + QLabel deckPathLabel; + QLabel filtersPathLabel; + QLabel replaysPathLabel; + QLabel picsPathLabel; + QLabel cardDatabasePathLabel; + QLabel customCardDatabasePathLabel; + QLabel tokenDatabasePathLabel; + QLabel updateReleaseChannelLabel; + QLabel advertiseTranslationPageLabel; + QCheckBox showTipsOnStartup; }; #endif // COCKATRICE_GENERAL_SETTINGS_PAGE_H diff --git a/cockatrice/src/interface/widgets/settings_page/messages_settings_page.cpp b/cockatrice/src/interface/widgets/settings_page/messages_settings_page.cpp index a3b89f8c4..e4f24ab73 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,6 @@ #include "messages_settings_page.h" #include "../../../client/settings/cache_settings.h" -#include "../../pixel_map_generator.h" #include "../interface/widgets/utility/get_text_with_max.h" #include @@ -60,10 +59,6 @@ MessagesSettingsPage::MessagesSettingsPage() connect(&roomHistory, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().chat(), &ChatSettings::setRoomHistory); - ignoreAllPrivateMessagesCheckBox.setChecked(SettingsCache::instance().chat().getIgnoreAllPrivateMessages()); - connect(&ignoreAllPrivateMessagesCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().chat(), - &ChatSettings::setIgnoreAllPrivateMessages); - customAlertString = new QLineEdit(); customAlertString->setText(SettingsCache::instance().chat().getHighlightWords()); connect(customAlertString, &QLineEdit::textChanged, &SettingsCache::instance().chat(), @@ -81,7 +76,6 @@ MessagesSettingsPage::MessagesSettingsPage() chatGrid->addWidget(&messagePopups, 5, 0); chatGrid->addWidget(&mentionPopups, 6, 0); chatGrid->addWidget(&roomHistory, 7, 0); - chatGrid->addWidget(&ignoreAllPrivateMessagesCheckBox, 8, 0); chatGroupBox = new QGroupBox; chatGroupBox->setLayout(chatGrid); @@ -108,15 +102,15 @@ MessagesSettingsPage::MessagesSettingsPage() } aAdd = new QAction(this); - aAdd->setIcon(themePixmap(QStringLiteral("icons/increment"))); + aAdd->setIcon(QPixmap("theme:icons/increment")); connect(aAdd, &QAction::triggered, this, &MessagesSettingsPage::actAdd); aEdit = new QAction(this); - aEdit->setIcon(themePixmap(QStringLiteral("icons/pencil"))); + aEdit->setIcon(QPixmap("theme:icons/pencil")); connect(aEdit, &QAction::triggered, this, &MessagesSettingsPage::actEdit); aRemove = new QAction(this); - aRemove->setIcon(themePixmap(QStringLiteral("icons/decrement"))); + aRemove->setIcon(QPixmap("theme:icons/decrement")); connect(aRemove, &QAction::triggered, this, &MessagesSettingsPage::actRemove); auto *messageToolBar = new QToolBar; @@ -262,7 +256,6 @@ void MessagesSettingsPage::retranslateUi() 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")); hexLabel.setText(tr("(Color is hexadecimal)")); hexHighlightLabel.setText(tr("(Color is hexadecimal)")); customAlertStringLabel.setText(tr("Separate words with a space, alphanumeric characters only")); 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 436ebbad9..e98ae0592 100644 --- a/cockatrice/src/interface/widgets/settings_page/messages_settings_page.h +++ b/cockatrice/src/interface/widgets/settings_page/messages_settings_page.h @@ -40,7 +40,6 @@ private: QCheckBox messagePopups; QCheckBox mentionPopups; QCheckBox roomHistory; - QCheckBox ignoreAllPrivateMessagesCheckBox; QGroupBox *chatGroupBox; QGroupBox *highlightGroupBox; QGroupBox *messageGroupBox; 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 1277d0e49..1f1867f7c 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,6 @@ #include "../../../client/settings/cache_settings.h" #include "../../../client/settings/shortcut_treeview.h" #include "../../../client/settings/shortcuts_settings.h" -#include "../../pixel_map_generator.h" #include "../interface/widgets/utility/custom_line_edit.h" #include "../interface/widgets/utility/sequence_edit.h" @@ -48,8 +47,8 @@ ShortcutSettingsPage::ShortcutSettingsPage() btnResetAll = new QPushButton(this); btnClearAll = new QPushButton(this); - btnResetAll->setIcon(themePixmap(QStringLiteral("icons/update"))); - btnClearAll->setIcon(themePixmap(QStringLiteral("icons/clearsearch"))); + btnResetAll->setIcon(QPixmap("theme:icons/update")); + btnClearAll->setIcon(QPixmap("theme:icons/clearsearch")); // layout auto *_editLayout = new QGridLayout; diff --git a/cockatrice/src/interface/widgets/settings_page/user_interface_settings_page.cpp b/cockatrice/src/interface/widgets/settings_page/user_interface_settings_page.cpp index 2c6e062da..634df0b15 100644 --- a/cockatrice/src/interface/widgets/settings_page/user_interface_settings_page.cpp +++ b/cockatrice/src/interface/widgets/settings_page/user_interface_settings_page.cpp @@ -20,7 +20,26 @@ enum visualDeckStoragePromptForConversionIndex UserInterfaceSettingsPage::UserInterfaceSettingsPage() { - // general settings + // general settings and notification settings + notificationsEnabledCheckBox.setChecked(SettingsCache::instance().userInterface().getNotificationsEnabled()); + connect(¬ificationsEnabledCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().userInterface(), + &InterfaceSettings::setNotificationsEnabled); + connect(¬ificationsEnabledCheckBox, &QCheckBox::QT_STATE_CHANGED, this, + &UserInterfaceSettingsPage::setNotificationEnabled); + + specNotificationsEnabledCheckBox.setChecked( + SettingsCache::instance().userInterface().getSpectatorNotificationsEnabled()); + specNotificationsEnabledCheckBox.setEnabled(SettingsCache::instance().userInterface().getNotificationsEnabled()); + connect(&specNotificationsEnabledCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().userInterface(), + &InterfaceSettings::setSpectatorNotificationsEnabled); + + buddyConnectNotificationsEnabledCheckBox.setChecked( + SettingsCache::instance().userInterface().getBuddyConnectNotificationsEnabled()); + buddyConnectNotificationsEnabledCheckBox.setEnabled( + SettingsCache::instance().userInterface().getNotificationsEnabled()); + connect(&buddyConnectNotificationsEnabledCheckBox, &QCheckBox::QT_STATE_CHANGED, + &SettingsCache::instance().userInterface(), &InterfaceSettings::setBuddyConnectNotificationsEnabled); + doubleClickToPlayCheckBox.setChecked(SettingsCache::instance().userInterface().getDoubleClickToPlay()); connect(&doubleClickToPlayCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().userInterface(), &InterfaceSettings::setDoubleClickToPlay); @@ -84,26 +103,6 @@ UserInterfaceSettingsPage::UserInterfaceSettingsPage() generalGroupBox = new QGroupBox; generalGroupBox->setLayout(generalGrid); - // notification settings - notificationsEnabledCheckBox.setChecked(SettingsCache::instance().userInterface().getNotificationsEnabled()); - connect(¬ificationsEnabledCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().userInterface(), - &InterfaceSettings::setNotificationsEnabled); - connect(¬ificationsEnabledCheckBox, &QCheckBox::QT_STATE_CHANGED, this, - &UserInterfaceSettingsPage::setNotificationEnabled); - - specNotificationsEnabledCheckBox.setChecked( - SettingsCache::instance().userInterface().getSpectatorNotificationsEnabled()); - specNotificationsEnabledCheckBox.setEnabled(SettingsCache::instance().userInterface().getNotificationsEnabled()); - connect(&specNotificationsEnabledCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().userInterface(), - &InterfaceSettings::setSpectatorNotificationsEnabled); - - buddyConnectNotificationsEnabledCheckBox.setChecked( - SettingsCache::instance().userInterface().getBuddyConnectNotificationsEnabled()); - buddyConnectNotificationsEnabledCheckBox.setEnabled( - SettingsCache::instance().userInterface().getNotificationsEnabled()); - connect(&buddyConnectNotificationsEnabledCheckBox, &QCheckBox::QT_STATE_CHANGED, - &SettingsCache::instance().userInterface(), &InterfaceSettings::setBuddyConnectNotificationsEnabled); - auto *notificationsGrid = new QGridLayout; notificationsGrid->addWidget(¬ificationsEnabledCheckBox, 0, 0); notificationsGrid->addWidget(&specNotificationsEnabledCheckBox, 1, 0); @@ -117,29 +116,8 @@ UserInterfaceSettingsPage::UserInterfaceSettingsPage() connect(&tapAnimationCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::setTapAnimation); - arrowDrawAnimationCheckBox.setChecked(SettingsCache::instance().cardsDisplay().getArrowDrawAnimation()); - connect(&arrowDrawAnimationCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().cardsDisplay(), - &CardsDisplaySettings::setArrowDrawAnimation); - - lifeCounterAnimationsCheckBox.setChecked( - SettingsCache::instance().userInterface().getLifeCounterAnimationsEnabled()); - connect(&lifeCounterAnimationsCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().userInterface(), - &InterfaceSettings::setLifeCounterAnimationsEnabled); - - battlefieldFlashCheckBox.setChecked(SettingsCache::instance().userInterface().getBattlefieldFlashEnabled()); - connect(&battlefieldFlashCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().userInterface(), - &InterfaceSettings::setBattlefieldFlashEnabled); - - connect(&enableAllAnimationsButton, &QPushButton::clicked, this, &UserInterfaceSettingsPage::enableAllAnimations); - connect(&disableAllAnimationsButton, &QPushButton::clicked, this, &UserInterfaceSettingsPage::disableAllAnimations); - auto *animationGrid = new QGridLayout; - animationGrid->addWidget(&enableAllAnimationsButton, 0, 0); - animationGrid->addWidget(&disableAllAnimationsButton, 0, 1); - animationGrid->addWidget(&tapAnimationCheckBox, 1, 0); - animationGrid->addWidget(&arrowDrawAnimationCheckBox, 2, 0); - animationGrid->addWidget(&lifeCounterAnimationsCheckBox, 3, 0); - animationGrid->addWidget(&battlefieldFlashCheckBox, 4, 0); + animationGrid->addWidget(&tapAnimationCheckBox, 0, 0); animationGroupBox = new QGroupBox; animationGroupBox->setLayout(animationGrid); @@ -184,13 +162,6 @@ UserInterfaceSettingsPage::UserInterfaceSettingsPage() connect(&defaultDeckEditorTypeSelector, QOverload::of(&QComboBox::currentIndexChanged), &SettingsCache::instance().deckEditor(), &DeckEditorSettings::setDefaultDeckEditorType); - vdeStartupTabSelector.addItem(""); // these will be set in retranslateUI - vdeStartupTabSelector.addItem(""); - vdeStartupTabSelector.addItem(""); - vdeStartupTabSelector.setCurrentIndex(SettingsCache::instance().deckEditor().getVdeStartupTab()); - connect(&vdeStartupTabSelector, QOverload::of(&QComboBox::currentIndexChanged), - &SettingsCache::instance().deckEditor(), &DeckEditorSettings::setVdeStartupTab); - commanderSpellbookIntegrationUseOfficialBracketNamesExplainer.setText("?"); commanderSpellbookIntegrationUseOfficialBracketNamesExplainer.setAutoRaise(true); commanderSpellbookIntegrationUseOfficialBracketNamesExplainer.setEnabled(false); @@ -250,12 +221,10 @@ UserInterfaceSettingsPage::UserInterfaceSettingsPage() deckEditorGrid->addWidget(&visualDeckStoragePromptForConversionSelector, 3, 1); deckEditorGrid->addWidget(&defaultDeckEditorTypeLabel, 4, 0); deckEditorGrid->addWidget(&defaultDeckEditorTypeSelector, 4, 1); - deckEditorGrid->addWidget(&vdeStartupTabLabel, 5, 0); - deckEditorGrid->addWidget(&vdeStartupTabSelector, 5, 1); - deckEditorGrid->addWidget(&commanderSpellbookIntegrationEnabledLabel, 6, 0); - deckEditorGrid->addWidget(&commanderSpellbookIntegrationEnabledSelector, 6, 1); - deckEditorGrid->addWidget(labelWidget, 7, 0); - deckEditorGrid->addWidget(&commanderSpellbookIntegrationBracketNamingSelector, 7, 1); + deckEditorGrid->addWidget(&commanderSpellbookIntegrationEnabledLabel, 5, 0); + deckEditorGrid->addWidget(&commanderSpellbookIntegrationEnabledSelector, 5, 1); + deckEditorGrid->addWidget(labelWidget, 6, 0); + deckEditorGrid->addWidget(&commanderSpellbookIntegrationBracketNamingSelector, 6, 1); deckEditorGroupBox = new QGroupBox; deckEditorGroupBox->setLayout(deckEditorGrid); @@ -299,22 +268,6 @@ void UserInterfaceSettingsPage::setNotificationEnabled(QT_STATE_CHANGED_T i) } } -void UserInterfaceSettingsPage::enableAllAnimations() -{ - tapAnimationCheckBox.setChecked(true); - arrowDrawAnimationCheckBox.setChecked(true); - lifeCounterAnimationsCheckBox.setChecked(true); - battlefieldFlashCheckBox.setChecked(true); -} - -void UserInterfaceSettingsPage::disableAllAnimations() -{ - tapAnimationCheckBox.setChecked(false); - arrowDrawAnimationCheckBox.setChecked(false); - lifeCounterAnimationsCheckBox.setChecked(false); - battlefieldFlashCheckBox.setChecked(false); -} - void UserInterfaceSettingsPage::updateCommanderSpellbookUiState() { const int mode = SettingsCache::instance().deckEditor().getCommanderSpellbookIntegrationEnabled(); @@ -356,15 +309,8 @@ void UserInterfaceSettingsPage::retranslateUi() notificationsEnabledCheckBox.setText(tr("Enable notifications in taskbar")); specNotificationsEnabledCheckBox.setText(tr("Notify in the taskbar for game events while you are spectating")); buddyConnectNotificationsEnabledCheckBox.setText(tr("Notify in the taskbar when users in your buddy list connect")); - animationGroupBox->setTitle(tr("Animation settings")); - enableAllAnimationsButton.setText(tr("&Enable all animations")); - disableAllAnimationsButton.setText(tr("&Disable all animations")); tapAnimationCheckBox.setText(tr("&Tap/untap animation")); - arrowDrawAnimationCheckBox.setText(tr("&Arrow draw animation")); - lifeCounterAnimationsCheckBox.setText(tr("Life counter flash")); - battlefieldFlashCheckBox.setText(tr("Battlefield flash on damage")); - deckEditorGroupBox->setTitle(tr("Deck editor/storage settings")); openDeckInNewTabCheckBox.setText(tr("Open deck in new tab by default")); visualDeckStorageInGameCheckBox.setText(tr("Use visual deck storage in game lobby")); @@ -380,12 +326,6 @@ void UserInterfaceSettingsPage::retranslateUi() defaultDeckEditorTypeLabel.setText(tr("Default deck editor type")); defaultDeckEditorTypeSelector.setItemText(TabSupervisor::ClassicDeckEditor, tr("Classic Deck Editor")); defaultDeckEditorTypeSelector.setItemText(TabSupervisor::VisualDeckEditor, tr("Visual Deck Editor")); - vdeStartupTabLabel.setText(tr("Visual deck editor startup tab")); - vdeStartupTabSelector.setItemText(VdeStartupTabContext, tr("Context")); - vdeStartupTabSelector.setItemText(VdeStartupTabDeckDisplay, tr("Deck display")); - vdeStartupTabSelector.setItemText(VdeStartupTabDatabaseDisplay, tr("Database display")); - vdeStartupTabSelector.setToolTip( - tr("Context mode: New decks open on the database display, existing decks open on the deck view.")); commanderSpellbookIntegrationEnabledLabel.setText( tr("CommanderSpellbook integration to estimate commander bracket")); @@ -400,8 +340,8 @@ void UserInterfaceSettingsPage::retranslateUi() 0, CommanderBracketNames::CommanderSpellbookBracketNames); commanderSpellbookIntegrationBracketNamingSelector.setItemText( 1, CommanderBracketNames::OfficialCommanderBracketNames); - commanderSpellbookIntegrationUseOfficialBracketNamesExplainer.setToolTip(CommanderBracketNames::Explainer); + commanderSpellbookIntegrationUseOfficialBracketNamesExplainer.setToolTip(CommanderBracketNames::Explainer); replayGroupBox->setTitle(tr("Replay settings")); rewindBufferingMsLabel.setText(tr("Buffer time for backwards skip via shortcut:")); rewindBufferingMsBox.setSuffix(" ms"); diff --git a/cockatrice/src/interface/widgets/settings_page/user_interface_settings_page.h b/cockatrice/src/interface/widgets/settings_page/user_interface_settings_page.h index e8a30fb1f..9e6fada69 100644 --- a/cockatrice/src/interface/widgets/settings_page/user_interface_settings_page.h +++ b/cockatrice/src/interface/widgets/settings_page/user_interface_settings_page.h @@ -7,7 +7,6 @@ #include #include #include -#include #include #include #include @@ -18,11 +17,12 @@ class UserInterfaceSettingsPage : public AbstractSettingsPage Q_OBJECT private slots: void setNotificationEnabled(QT_STATE_CHANGED_T); - void enableAllAnimations(); - void disableAllAnimations(); void updateCommanderSpellbookUiState(); private: + QCheckBox notificationsEnabledCheckBox; + QCheckBox specNotificationsEnabledCheckBox; + QCheckBox buddyConnectNotificationsEnabledCheckBox; QCheckBox doubleClickToPlayCheckBox; QCheckBox clickPlaysAllSelectedCheckBox; QCheckBox playToStackCheckBox; @@ -34,18 +34,7 @@ private: QCheckBox showTotalSelectionCountCheckBox; QCheckBox useTearOffMenusCheckBox; QCheckBox keepGameChatFocusCheckBox; - - QCheckBox notificationsEnabledCheckBox; - QCheckBox specNotificationsEnabledCheckBox; - QCheckBox buddyConnectNotificationsEnabledCheckBox; - - QPushButton enableAllAnimationsButton; - QPushButton disableAllAnimationsButton; QCheckBox tapAnimationCheckBox; - QCheckBox arrowDrawAnimationCheckBox; - QCheckBox lifeCounterAnimationsCheckBox; - QCheckBox battlefieldFlashCheckBox; - QCheckBox openDeckInNewTabCheckBox; QLabel visualDeckStoragePromptForConversionLabel; QComboBox visualDeckStoragePromptForConversionSelector; @@ -53,17 +42,13 @@ private: QCheckBox visualDeckStorageSelectionAnimationCheckBox; QLabel defaultDeckEditorTypeLabel; QComboBox defaultDeckEditorTypeSelector; - QLabel vdeStartupTabLabel; - QComboBox vdeStartupTabSelector; QLabel commanderSpellbookIntegrationEnabledLabel; QComboBox commanderSpellbookIntegrationEnabledSelector; QLabel commanderSpellbookIntegrationUseOfficialBracketNamesLabel; QToolButton commanderSpellbookIntegrationUseOfficialBracketNamesExplainer; QComboBox commanderSpellbookIntegrationBracketNamingSelector; - QLabel rewindBufferingMsLabel; QSpinBox rewindBufferingMsBox; - QGroupBox *generalGroupBox; QGroupBox *notificationsGroupBox; QGroupBox *animationGroupBox; diff --git a/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.cpp b/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.cpp index 6423c581b..f80649eba 100644 --- a/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.cpp +++ b/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.cpp @@ -11,7 +11,6 @@ #include "../../../client/settings/cache_settings.h" #include "../../../client/settings/shortcuts_settings.h" -#include "../cards/additional_info/deck_color_identity.h" #include "../client/network/interfaces/deck_stats_interface.h" #include "../client/network/interfaces/tapped_out_interface.h" #include "../deck_editor/deck_state_manager.h" @@ -20,7 +19,6 @@ #include "../interface/widgets/dialogs/dlg_load_deck.h" #include "../interface/widgets/dialogs/dlg_load_deck_from_clipboard.h" #include "../interface/widgets/dialogs/dlg_load_deck_from_website.h" -#include "../interface/widgets/dialogs/dlg_share_deck.h" #include "../utility/visibility_change_listener.h" #include "tab_supervisor.h" @@ -325,7 +323,6 @@ bool AbstractTabDeckEditor::actSaveDeck() Command_DeckUpload cmd; cmd.set_deck_id(static_cast(loadedDeck.lastLoadInfo.remoteDeckId)); cmd.set_deck_list(deckString.toStdString()); - cmd.set_color_identity(getDeckColorIdentity(loadedDeck.deckList, CardDatabaseManager::query()).toStdString()); PendingCommand *pend = AbstractClient::prepareSessionCommand(cmd); connect(pend, &PendingCommand::finished, this, &AbstractTabDeckEditor::saveDeckRemoteFinished); @@ -385,27 +382,6 @@ bool AbstractTabDeckEditor::actSaveDeckAs() return true; } -/** - * @brief Opens the deck share dialog with the current deck preselected. - */ -void AbstractTabDeckEditor::actShareDeck() -{ - AbstractClient *client = tabSupervisor->getServerClient(); - if (client->getStatus() != StatusLoggedIn) { - QMessageBox::information(this, tr("Share deck"), tr("You must be connected to the server to share a deck.")); - return; - } - - const QSharedPointer deck = deckStateManager->getDeckListShared(); - if (deck->isBlankDeck()) { - QMessageBox::information(this, tr("Share deck"), tr("The deck is empty. Add cards before sharing it.")); - return; - } - - DlgShareDeck shareDialog(client, deck, this); - shareDialog.exec(); -} - /** * @brief Callback for remote deck save completion. * @param response Server response. diff --git a/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.h b/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.h index dae8f5b0a..34c585597 100644 --- a/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.h +++ b/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.h @@ -8,7 +8,6 @@ #ifndef TAB_GENERIC_DECK_EDITOR_H #define TAB_GENERIC_DECK_EDITOR_H -#include "../../deck_loader/deck_loader.h" #include "../interface/widgets/deck_editor/deck_editor_card_database_dock_widget.h" #include "../interface/widgets/deck_editor/deck_editor_card_info_dock_widget.h" #include "../interface/widgets/deck_editor/deck_editor_database_display_widget.h" @@ -214,9 +213,6 @@ protected slots: /** @brief Saves the current deck under a new name. */ virtual bool actSaveDeckAs(); - /** @brief Opens the deck share dialog for the current deck. */ - void actShareDeck(); - /** @brief Loads a deck from the clipboard. */ virtual void actLoadDeckFromClipboard(); @@ -267,12 +263,12 @@ protected slots: /** @brief Handles dock close events. */ void closeEvent(QCloseEvent *event) override; +private: /** @brief Sets the deck for this tab. * @param _deck The deck object. */ virtual void setDeck(const LoadedDeck &_deck); -private: /** @brief Helper for editing decks from the clipboard. */ void editDeckInClipboard(bool annotated); diff --git a/cockatrice/src/interface/widgets/tabs/api/archidekt/display/archidekt_api_response_deck_display_widget.cpp b/cockatrice/src/interface/widgets/tabs/api/archidekt/display/archidekt_api_response_deck_display_widget.cpp index 657ef3dbe..66b68d823 100644 --- a/cockatrice/src/interface/widgets/tabs/api/archidekt/display/archidekt_api_response_deck_display_widget.cpp +++ b/cockatrice/src/interface/widgets/tabs/api/archidekt/display/archidekt_api_response_deck_display_widget.cpp @@ -1,6 +1,5 @@ #include "archidekt_api_response_deck_display_widget.h" -#include "../../../../../../client/settings/cache_settings.h" #include "../../../../../deck_loader/card_node_function.h" #include "../../../../../deck_loader/deck_loader.h" #include "../../../../cards/card_size_widget.h" @@ -11,7 +10,6 @@ #include #include -#include ArchidektApiResponseDeckDisplayWidget::ArchidektApiResponseDeckDisplayWidget(QWidget *parent, ArchidektApiResponseDeck _response, @@ -122,9 +120,6 @@ ArchidektApiResponseDeckDisplayWidget::ArchidektApiResponseDeckDisplayWidget(QWi } model = new DeckListModel(this); - model->setDisplayLanguage(SettingsCache::instance().cardsDisplay().getCardLang()); - connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::cardLangChanged, model, - [this](const QString &lang) { model->setDisplayLanguage(lang); }); connect(model, &DeckListModel::modelReset, this, &ArchidektApiResponseDeckDisplayWidget::decklistModelReset); auto decklist = QSharedPointer(new DeckList); 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 888197f53..374d35cdf 100644 --- a/cockatrice/src/interface/widgets/tabs/api/archidekt/tab_archidekt.cpp +++ b/cockatrice/src/interface/widgets/tabs/api/archidekt/tab_archidekt.cpp @@ -1,14 +1,13 @@ #include "tab_archidekt.h" #include "../../../../../client/settings/cache_settings.h" -#include "../../../../pixel_map_generator.h" #include "../../../cards/additional_info/mana_symbol_widget.h" -#include "../../../utility/completer_utils.h" #include "../../tab_supervisor.h" #include "api_response/archidekt_deck_listing_api_response.h" #include "display/archidekt_api_response_deck_display_widget.h" #include "display/archidekt_api_response_deck_listings_display_widget.h" +#include #include #include #include @@ -20,6 +19,7 @@ #include #include #include +#include #include #include #include @@ -214,7 +214,7 @@ void TabArchidekt::setupFilterWidgets() // Format filter (collapsible) formatButton = new SettingsButtonWidget(secondaryToolbar); formatButton->setButtonText(tr("Formats")); - formatButton->setButtonIcon(themePixmap(QStringLiteral("icons/scale_balanced"))); + formatButton->setButtonIcon(QPixmap("theme:icons/scale_balanced")); QWidget *formatContainer = new QWidget(secondaryToolbar); QGridLayout *formatLayout = new QGridLayout(formatContainer); @@ -279,14 +279,41 @@ void TabArchidekt::setupFilterWidgets() auto cardDatabaseModel = new CardDatabaseModel(CardDatabaseManager::getInstance(), false, this); auto displayModel = new CardDatabaseDisplayModel(this); displayModel->setSourceModel(cardDatabaseModel); + auto *searchModel = new CardSearchModel(displayModel, this); - const CardCompleterSetup cardSetup = createCardCompleter(displayModel, this); - cardsField->setCompleter(cardSetup.completer); - commandersField->setCompleter(cardSetup.completer); + auto *proxyModel = new CardCompleterProxyModel(this); + proxyModel->setSourceModel(searchModel); + proxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive); + proxyModel->setFilterRole(Qt::DisplayRole); + + auto *completer = new QCompleter(proxyModel, this); + completer->setCompletionRole(Qt::DisplayRole); + completer->setCompletionMode(QCompleter::PopupCompletion); + completer->setCaseSensitivity(Qt::CaseInsensitive); + completer->setFilterMode(Qt::MatchContains); + completer->setMaxVisibleItems(10); + + cardsField->setCompleter(completer); + commandersField->setCompleter(completer); // Keep autocomplete working for both fields - connectCardCompleterSearch(cardsField, cardSetup); - connectCardCompleterSearch(commandersField, cardSetup); + connect(cardsField, &QLineEdit::textChanged, this, [=](const QString &text) { + searchModel->updateSearchResults(text); + QString pattern = ".*" + QRegularExpression::escape(text) + ".*"; + proxyModel->setFilterRegularExpression(QRegularExpression(pattern, QRegularExpression::CaseInsensitiveOption)); + if (!text.isEmpty()) { + completer->complete(); + } + }); + + connect(commandersField, &QLineEdit::textChanged, this, [=](const QString &text) { + searchModel->updateSearchResults(text); + QString pattern = ".*" + QRegularExpression::escape(text) + ".*"; + proxyModel->setFilterRegularExpression(QRegularExpression(pattern, QRegularExpression::CaseInsensitiveOption)); + if (!text.isEmpty()) { + completer->complete(); + } + }); // Assemble secondary toolbar secondaryToolbarLayout->addWidget(bracketLabel); 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 2cbbedfa4..4f50e38a6 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,6 @@ #include "commander_bracket_widget.h" #include "../../../../../client/settings/cache_settings.h" -#include "../../../../pixel_map_generator.h" #include "commander_bracket_service.h" #include @@ -31,7 +30,7 @@ CommanderBracketWidget::CommanderBracketWidget(QWidget *parent) : QWidget(parent bracketInfoButton->setEnabled(false); bracketRefreshButton = new QToolButton(this); - bracketRefreshButton->setIcon(themePixmap(QStringLiteral("icons/reload"))); + bracketRefreshButton->setIcon(QPixmap("theme: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.cpp b/cockatrice/src/interface/widgets/tabs/api/edhrec/tab_edhrec_main.cpp index 500eab13a..42a689898 100644 --- a/cockatrice/src/interface/widgets/tabs/api/edhrec/tab_edhrec_main.cpp +++ b/cockatrice/src/interface/widgets/tabs/api/edhrec/tab_edhrec_main.cpp @@ -1,7 +1,6 @@ #include "tab_edhrec_main.h" #include "../../../../../client/settings/cache_settings.h" -#include "../../../utility/completer_utils.h" #include "../../tab_supervisor.h" #include "api_response/average_deck/edhrec_average_deck_api_response.h" #include "api_response/commander/edhrec_commander_api_response.h" @@ -13,6 +12,7 @@ #include "display/top_commander/edhrec_top_commanders_api_response_display_widget.h" #include "display/top_tags/edhrec_top_tags_api_response_display_widget.h" +#include #include #include #include @@ -63,12 +63,32 @@ TabEdhRecMain::TabEdhRecMain(TabSupervisor *_tabSupervisor) : Tab(_tabSupervisor auto cardDatabaseModel = new CardDatabaseModel(CardDatabaseManager::getInstance(), false, this); auto displayModel = new CardDatabaseDisplayModel(this); displayModel->setSourceModel(cardDatabaseModel); + auto *searchModel = new CardSearchModel(displayModel, this); - const CardCompleterSetup cardSetup = createCardCompleter(displayModel, this); - searchBar->setCompleter(cardSetup.completer); + auto *proxyModel = new CardCompleterProxyModel(this); + proxyModel->setSourceModel(searchModel); + proxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive); + proxyModel->setFilterRole(Qt::DisplayRole); + + auto *completer = new QCompleter(proxyModel, this); + completer->setCompletionRole(Qt::DisplayRole); + completer->setCompletionMode(QCompleter::PopupCompletion); + completer->setCaseSensitivity(Qt::CaseInsensitive); + completer->setFilterMode(Qt::MatchContains); + completer->setMaxVisibleItems(10); + searchBar->setCompleter(completer); // Update suggestions dynamically - connectCardCompleterSearch(searchBar, cardSetup); + connect(searchBar, &QLineEdit::textChanged, searchModel, &CardSearchModel::updateSearchResults); + connect(searchBar, &QLineEdit::textChanged, this, [=](const QString &text) { + // Ensure substring matching + QString pattern = ".*" + QRegularExpression::escape(text) + ".*"; + proxyModel->setFilterRegularExpression(QRegularExpression(pattern, QRegularExpression::CaseInsensitiveOption)); + + if (!text.isEmpty()) { + completer->complete(); // Force the dropdown to appear + } + }); searchPushButton = new QPushButton(navigationContainer); connect(searchPushButton, &QPushButton::clicked, this, [=, this]() { doSearch(); }); diff --git a/cockatrice/src/interface/widgets/tabs/tab.h b/cockatrice/src/interface/widgets/tabs/tab.h index bddf325e2..6ea1f5077 100644 --- a/cockatrice/src/interface/widgets/tabs/tab.h +++ b/cockatrice/src/interface/widgets/tabs/tab.h @@ -20,7 +20,6 @@ class Tab : public QMainWindow signals: void userEvent(bool globalEvent = true); void tabTextChanged(Tab *tab, const QString &newTabText); - void cockatriceLinkActivated(const QString &url); protected: TabSupervisor *tabSupervisor; diff --git a/cockatrice/src/interface/widgets/tabs/tab_account.cpp b/cockatrice/src/interface/widgets/tabs/tab_account.cpp index dbcf50966..2cc8165e8 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_account.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_account.cpp @@ -1,7 +1,6 @@ #include "tab_account.h" #include "../client/sound_engine.h" -#include "../interface/widgets/dialogs/dlg_my_reports.h" #include "../interface/widgets/server/user/user_info_box.h" #include "../interface/widgets/server/user/user_list_manager.h" #include "../interface/widgets/server/user/user_list_widget.h" @@ -50,11 +49,6 @@ TabAccount::TabAccount(TabSupervisor *_tabSupervisor, AbstractClient *_client, c auto *vbox = new QVBoxLayout; vbox->addWidget(userInfoBox); - - myReportsButton = new QPushButton(tr("My Reports")); - connect(myReportsButton, &QPushButton::clicked, this, &TabAccount::openMyReports); - vbox->addWidget(myReportsButton); - vbox->addWidget(allUsersList); auto *addToBuddyList = new QHBoxLayout; @@ -132,22 +126,15 @@ void TabAccount::addToList(const std::string &listName, const QString &userName) void TabAccount::retranslateUi() { - myReportsButton->setText(tr("My Reports")); allUsersList->retranslateUi(); buddyList->retranslateUi(); ignoreList->retranslateUi(); userInfoBox->retranslateUi(); - - buddyList->setToolTip(tr("Buddies are marked with a star in chat, a sound plays when they join or leave the " - "server, and they can be invited to buddy-only games.")); - ignoreList->setToolTip(tr("Ignored users' chat messages are hidden from you, and they cannot send you private " - "messages or join your games.")); } void TabAccount::processListUsersResponse(const Response &response) { const Response_ListUsers &resp = response.GetExtension(Response_ListUsers::ext); - allUsersList->beginBulkLoad(); for (int i = 0; i < resp.user_list_size(); ++i) { const ServerInfo_User &info = resp.user_list(i); const QString &userName = QString::fromStdString(info.name()); @@ -155,8 +142,8 @@ void TabAccount::processListUsersResponse(const Response &response) ignoreList->setUserOnline(userName, true); buddyList->setUserOnline(userName, true); } - allUsersList->endBulkLoad(); + allUsersList->sortItems(); ignoreList->sortItems(); buddyList->sortItems(); } @@ -201,20 +188,18 @@ void TabAccount::processUserLeftEvent(const Event_UserLeft &event) void TabAccount::buddyListReceived(const QList &_buddyList) { - buddyList->beginBulkLoad(); for (const auto &user : _buddyList) { buddyList->processUserInfo(user, false); } - buddyList->endBulkLoad(); + buddyList->sortItems(); } void TabAccount::ignoreListReceived(const QList &_ignoreList) { - ignoreList->beginBulkLoad(); for (const auto &user : _ignoreList) { ignoreList->processUserInfo(user, false); } - ignoreList->endBulkLoad(); + ignoreList->sortItems(); } void TabAccount::processAddToListEvent(const Event_AddToList &event) @@ -252,10 +237,3 @@ void TabAccount::processRemoveFromListEvent(const Event_RemoveFromList &event) userList->deleteUser(user); } - -void TabAccount::openMyReports() -{ - auto *dlg = new DlgMyReports(client, this); - dlg->setAttribute(Qt::WA_DeleteOnClose); - dlg->exec(); -} diff --git a/cockatrice/src/interface/widgets/tabs/tab_account.h b/cockatrice/src/interface/widgets/tabs/tab_account.h index 68054c7a1..887038ebb 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_account.h +++ b/cockatrice/src/interface/widgets/tabs/tab_account.h @@ -18,7 +18,6 @@ class Event_RemoveFromList; class Event_UserJoined; class Event_UserLeft; class LineEditUnfocusable; -class QPushButton; class Response; class ServerInfo_User; class UserInfoBox; @@ -42,7 +41,6 @@ private slots: void processRemoveFromListEvent(const Event_RemoveFromList &event); void addToIgnoreList(); void addToBuddyList(); - void openMyReports(); private: AbstractClient *client; @@ -52,7 +50,6 @@ private: UserInfoBox *userInfoBox; LineEditUnfocusable *addBuddyEdit; LineEditUnfocusable *addIgnoreEdit; - QPushButton *myReportsButton; void addToList(const std::string &listName, const QString &userName); public: diff --git a/cockatrice/src/interface/widgets/tabs/tab_card_art_rules.cpp b/cockatrice/src/interface/widgets/tabs/tab_card_art_rules.cpp index 999bc3db0..3dc4de15f 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_card_art_rules.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_card_art_rules.cpp @@ -1,6 +1,5 @@ #include "tab_card_art_rules.h" -#include "../utility/completer_utils.h" #include "libcockatrice/card/database/card_database_manager.h" #include @@ -195,14 +194,29 @@ void TabCardArtRules::initSearchBar() cardDbModel = new CardDatabaseModel(CardDatabaseManager::getInstance(), false, this); cardDbDisplayModel = new CardDatabaseDisplayModel(this); cardDbDisplayModel->setSourceModel(cardDbModel); + cardSearchModel = new CardSearchModel(cardDbDisplayModel, this); - const CardCompleterSetup cardSetup = createCardCompleter(cardDbDisplayModel, this, 15); - cardSearchModel = cardSetup.searchModel; - cardProxyModel = cardSetup.proxyModel; - searchCompleter = cardSetup.completer; + cardProxyModel = new CardCompleterProxyModel(this); + cardProxyModel->setSourceModel(cardSearchModel); + cardProxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive); + + searchCompleter = new QCompleter(cardProxyModel, this); + searchCompleter->setCompletionRole(Qt::DisplayRole); + searchCompleter->setCompletionMode(QCompleter::PopupCompletion); + searchCompleter->setCaseSensitivity(Qt::CaseInsensitive); + searchCompleter->setFilterMode(Qt::MatchContains); + searchCompleter->setMaxVisibleItems(15); searchEdit->setCompleter(searchCompleter); - connectCardCompleterSearch(searchEdit, cardSetup); + connect(searchEdit, &QLineEdit::textEdited, cardSearchModel, &CardSearchModel::updateSearchResults); + connect(searchEdit, &QLineEdit::textEdited, this, [this](const QString &text) { + const QString pattern = ".*" + QRegularExpression::escape(text) + ".*"; + cardProxyModel->setFilterRegularExpression( + QRegularExpression(pattern, QRegularExpression::CaseInsensitiveOption)); + if (!text.isEmpty()) { + searchCompleter->complete(); + } + }); connect(searchCompleter, static_cast(&QCompleter::activated), this, [this](const QString &name) { searchEdit->setText(name); }); connect(searchEdit, &QLineEdit::editingFinished, this, diff --git a/cockatrice/src/interface/widgets/tabs/tab_deck_storage.cpp b/cockatrice/src/interface/widgets/tabs/tab_deck_storage.cpp index 62769d0e3..49e42e4cf 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_deck_storage.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_deck_storage.cpp @@ -2,55 +2,35 @@ #include "../../../client/settings/cache_settings.h" #include "../../deck_loader/deck_loader.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" #include "../interface/widgets/server/remote/remote_decklist_tree_widget.h" #include "../interface/widgets/utility/get_text_with_max.h" #include #include -#include #include #include #include #include -#include #include #include -#include #include -#include #include #include #include #include -#include #include #include #include #include #include -#include -#include #include #include #include -#include #include #include -#include #include #include -namespace -{ -// How long to wait after the last visibility change before reading back the -// Public/Private column, in milliseconds. -constexpr int VISIBILITY_REFRESH_DELAY = 500; -} // namespace - TabDeckStorage::TabDeckStorage(TabSupervisor *_tabSupervisor, AbstractClient *_client, const ServerInfo_User *currentUserInfo) @@ -111,34 +91,8 @@ TabDeckStorage::TabDeckStorage(TabSupervisor *_tabSupervisor, serverDirView = new RemoteDeckList_TreeWidget(client); connect(serverDirView, &QTreeView::doubleClicked, this, &TabDeckStorage::actRemoteDoubleClick); - connect(serverDirView->selectionModel(), &QItemSelectionModel::selectionChanged, this, - [this] { onServerSelectionChanged(); }); - - // Share bar for creating a share link from the selected server decks/folders. - shareBar = new ShareBarWidget(this); - connect(shareBar, &ShareBarWidget::createRequested, this, &TabDeckStorage::actShareSelection); - connect(shareBar, &ShareBarWidget::cancelRequested, this, &TabDeckStorage::cancelShareDecks); - shareBar->setVisible(false); - - shareTimeoutTimer = new QTimer(this); - shareTimeoutTimer->setSingleShot(true); - shareTimeoutTimer->setInterval( - static_cast((static_cast(SettingsCache::instance().network().getTimeOut()) + 1) * - SettingsCache::instance().network().getKeepAlive() * 1000)); - connect(shareTimeoutTimer, &QTimer::timeout, this, &TabDeckStorage::onShareFromTreeTimeout); - - // Restartable single-shot refresh for the Public/Private column. It is - // armed with the full network timeout when a publish is sent (so a dropped - // reply still drains once) and re-armed with the short delay every time a - // reply lands, so the drain cannot fire while a slow round trip is still in - // flight. Either way the tree is re-read once things quiet down. - visibilityRefreshTimer = new QTimer(this); - visibilityRefreshTimer->setSingleShot(true); - visibilityRefreshTimer->setInterval(VISIBILITY_REFRESH_DELAY); - connect(visibilityRefreshTimer, &QTimer::timeout, this, &TabDeckStorage::onVisibilityRefreshTimeout); QVBoxLayout *rightVbox = new QVBoxLayout; - rightVbox->addWidget(shareBar); rightVbox->addWidget(serverDirView); rightVbox->addLayout(rightToolBarLayout); rightGroupBox = new QGroupBox; @@ -151,19 +105,19 @@ TabDeckStorage::TabDeckStorage(TabSupervisor *_tabSupervisor, // Left side actions aOpenLocalDeck = new QAction(this); - aOpenLocalDeck->setIcon(themePixmap(QStringLiteral("icons/pencil"))); + aOpenLocalDeck->setIcon(QPixmap("theme:icons/pencil")); connect(aOpenLocalDeck, &QAction::triggered, this, &TabDeckStorage::actOpenLocalDeck); aRenameLocal = new QAction(this); - aRenameLocal->setIcon(themePixmap(QStringLiteral("icons/rename"))); + aRenameLocal->setIcon(QPixmap("theme:icons/rename")); connect(aRenameLocal, &QAction::triggered, this, &TabDeckStorage::actRenameLocal); aUpload = new QAction(this); - aUpload->setIcon(themePixmap(QStringLiteral("icons/arrow_right_green"))); + aUpload->setIcon(QPixmap("theme: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(themePixmap(QStringLiteral("icons/remove_row"))); + aDeleteLocalDeck->setIcon(QPixmap("theme:icons/remove_row")); connect(aDeleteLocalDeck, &QAction::triggered, this, &TabDeckStorage::actDeleteLocalDeck); aOpenDecksFolder = new QAction(this); @@ -172,26 +126,18 @@ TabDeckStorage::TabDeckStorage(TabSupervisor *_tabSupervisor, // Right side actions aOpenRemoteDeck = new QAction(this); - aOpenRemoteDeck->setIcon(themePixmap(QStringLiteral("icons/pencil"))); + aOpenRemoteDeck->setIcon(QPixmap("theme:icons/pencil")); connect(aOpenRemoteDeck, &QAction::triggered, this, &TabDeckStorage::actOpenRemoteDeck); aDownload = new QAction(this); - aDownload->setIcon(themePixmap(QStringLiteral("icons/arrow_left_green"))); + aDownload->setIcon(QPixmap("theme: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(themePixmap(QStringLiteral("icons/remove_row"))); + aDeleteRemoteDeck->setIcon(QPixmap("theme:icons/remove_row")); connect(aDeleteRemoteDeck, &QAction::triggered, this, &TabDeckStorage::actDeleteRemoteDeck); - aShareDecks = new QAction(this); - aShareDecks->setIcon(themePixmap(QStringLiteral("icons/share"))); - connect(aShareDecks, &QAction::triggered, this, &TabDeckStorage::actShareDecks); - - aPublishDeck = new QAction(this); - aPublishDeck->setIcon(QPixmap("theme:icons/lock")); - connect(aPublishDeck, &QAction::triggered, this, &TabDeckStorage::actPublishDeck); - // Add actions to toolbars leftToolBar->addAction(aOpenLocalDeck); leftToolBar->addAction(aRenameLocal); @@ -203,8 +149,6 @@ TabDeckStorage::TabDeckStorage(TabSupervisor *_tabSupervisor, rightToolBar->addAction(aOpenRemoteDeck); rightToolBar->addAction(aDownload); - rightToolBar->addAction(aShareDecks); - rightToolBar->addAction(aPublishDeck); rightToolBar->addAction(aNewFolder); rightToolBar->addAction(aDeleteRemoteDeck); @@ -233,13 +177,7 @@ void TabDeckStorage::retranslateUi() aNewFolder->setText(tr("New folder")); aDeleteLocalDeck->setText(tr("Delete")); aDeleteRemoteDeck->setText(tr("Delete")); - aShareDecks->setText(tr("Share decks")); - aPublishDeck->setText(tr("Publish/unpublish deck")); aOpenDecksFolder->setText(tr("Open decks folder")); - shareBar->retranslateUi(); - if (shareBar->isVisible()) { - onServerSelectionChanged(); - } } QString TabDeckStorage::getTargetPath() const @@ -272,8 +210,6 @@ void TabDeckStorage::handleConnected(const ServerInfo_User &userInfo) void TabDeckStorage::handleConnectionChanged(ClientStatus status) { if (status == StatusDisconnected) { - visibilityRefreshTimer->stop(); - visibilityRefreshStarted = false; setRemoteEnabled(false); } } @@ -283,15 +219,12 @@ void TabDeckStorage::setRemoteEnabled(bool enabled) aUpload->setEnabled(enabled); aOpenRemoteDeck->setEnabled(enabled); aDownload->setEnabled(enabled); - aShareDecks->setEnabled(enabled); - aPublishDeck->setEnabled(enabled); aNewFolder->setEnabled(enabled); aDeleteRemoteDeck->setEnabled(enabled); if (enabled) { serverDirView->refreshTree(); } else { - setShareModeEnabled(false); serverDirView->clearTree(); } } @@ -413,8 +346,6 @@ void TabDeckStorage::uploadDeck(const QString &filePath, const QString &targetPa cmd.set_path(targetPath.toStdString()); cmd.set_deck_list(deckString.toStdString()); - cmd.set_color_identity(getDeckColorIdentity(deck, CardDatabaseManager::query()).toStdString()); - PendingCommand *pend = client->prepareSessionCommand(cmd); connect(pend, &PendingCommand::finished, this, &TabDeckStorage::uploadFinished); client->sendCommand(pend); @@ -694,241 +625,3 @@ void TabDeckStorage::deleteFolderFinished(const Response &response, const Comman serverDirView->removeNode(toDelete); } } - -void TabDeckStorage::actShareDecks() -{ - setShareModeEnabled(true); -} - -void TabDeckStorage::cancelShareDecks() -{ - setShareModeEnabled(false); -} - -void TabDeckStorage::setShareModeEnabled(bool enabled) -{ - shareBar->setVisible(enabled); - if (enabled) { - shareBar->setCreateEnabled(true); - shareBar->setName(tr("Shared decks")); - onServerSelectionChanged(); - shareBar->focusName(); - } else { - // Abandon any in-flight request: otherwise the timer keeps running and a late - // response reports the share as created after the user already backed out. - shareTimeoutTimer->stop(); - shareInFlightSeq = 0; - serverDirView->clearSelection(); - } -} - -void TabDeckStorage::onServerSelectionChanged() -{ - if (!shareBar->isVisible()) { - return; - } - const auto selection = serverDirView->getCurrentSelection(); - int folders = 0; - int files = 0; - for (const auto *node : selection) { - if (dynamic_cast(node)) { - ++folders; - } else { - ++files; - } - } - - QString hint; - if (folders > 1) { - hint = tr("Only one folder can be shared at a time."); - } else if (folders > 0 && files > 0) { - hint = tr("Share either a folder or decks, not both."); - } else if (folders == 0 && files == 0) { - hint = tr("Select folders or decks in the tree to share."); - } - shareBar->setHintText(hint, !hint.isEmpty()); - - QStringList parts; - if (folders > 0) { - parts << tr("%n folder(s)", "", folders); - } - if (files > 0) { - parts << tr("%n deck(s)", "", files); - } - shareBar->setCountText(parts.isEmpty() ? tr("No decks selected") - : tr("Selected: %1").arg(parts.join(QStringLiteral(", ")))); -} - -void TabDeckStorage::actShareSelection() -{ - const auto selection = serverDirView->getCurrentSelection(); - QString sharedFolder; - bool hasFile = false; - bool hasFolder = false; - for (const auto *node : selection) { - if (const auto *dirNode = dynamic_cast(node)) { - hasFolder = true; - if (!sharedFolder.isEmpty()) { - showShareNotice(tr("Only one folder can be shared at a time."), true); - return; - } - sharedFolder = dirNode->getPath(); - } else { - hasFile = true; - } - } - - if (hasFile && hasFolder) { - showShareNotice(tr("Share either a folder or decks, not both."), true); - return; - } - if (hasFolder && sharedFolder.isEmpty()) { - showShareNotice(tr("The root folder cannot be shared."), true); - return; - } - - Command_DeckShareCreate cmd; - cmd.set_name(shareBar->name().toStdString()); - if (cmd.name().empty()) { - cmd.set_name(tr("Shared decks").toStdString()); - } - - if (!sharedFolder.isEmpty()) { - cmd.set_folder_path(sharedFolder.toStdString()); - } else { - for (const auto *node : selection) { - if (const auto *fileNode = dynamic_cast(node)) { - DeckShareItem *item = cmd.add_items(); - item->set_deck_id(fileNode->getId()); - } - } - } - - if (cmd.items_size() == 0 && cmd.folder_path().empty()) { - showShareNotice(tr("Select decks to share."), true); - return; - } - - shareBar->setCreateEnabled(false); - const int seq = ++shareRequestSeq; - shareInFlightSeq = seq; - PendingCommand *pend = client->prepareSessionCommand(cmd); - connect(pend, &PendingCommand::finished, this, - [this, seq](const Response &response, const CommandContainer &commandContainer) { - if (shareInFlightSeq != seq) { - return; // the user cancelled or a newer request superseded this one - } - shareInFlightSeq = 0; - shareFromTreeFinished(response, commandContainer); - }); - client->sendCommand(pend); - shareTimeoutTimer->start(); -} - -void TabDeckStorage::shareFromTreeFinished(const Response &response, const CommandContainer & /*commandContainer*/) -{ - shareTimeoutTimer->stop(); - shareBar->setCreateEnabled(true); - if (response.response_code() != Response::RespOk) { - qWarning() << "failed to create deck share:" << response.response_code(); - showShareNotice(tr("Failed to create the share link (server response code %1).") - .arg(QString::number(static_cast(response.response_code()))), - true); - return; - } - const DeckShareUtils::ShareResponse share = DeckShareUtils::handleShareResponse(client, response); - - showShareNotice( - tr("Share link copied to the clipboard.\nExpires on %1.").arg(DeckShareUtils::formatShareExpiry(share.expiry))); - setShareModeEnabled(false); -} - -void TabDeckStorage::showShareNotice(const QString &message, bool warning) -{ - QMessageBox box(warning ? QMessageBox::Warning : QMessageBox::Information, tr("Share link"), message, - QMessageBox::Ok, this); - box.exec(); -} - -void TabDeckStorage::onShareFromTreeTimeout() -{ - if (shareInFlightSeq == 0) { - return; // share mode was left while the request was still outstanding - } - shareInFlightSeq = 0; - shareBar->setCreateEnabled(true); - showShareNotice(tr("The server did not respond in time. Try again."), true); -} - -void TabDeckStorage::actPublishDeck() -{ - visibilityFailures.clear(); - // Arm the drain with the full network timeout so a lost reply still costs - // one refresh instead of a dead column; each reply shrinks it to the short - // delay below, so a slow round trip is never drained before it lands. - const int visibilityFailSafeDelay = - static_cast((static_cast(SettingsCache::instance().network().getTimeOut()) + 1) * - SettingsCache::instance().network().getKeepAlive() * 1000); - - const auto selection = serverDirView->getCurrentSelection(); - for (const auto *node : selection) { - Command_DeckSetVisibility cmd; - if (const auto *fileNode = dynamic_cast(node)) { - cmd.set_deck_id(fileNode->getId()); - } else if (const auto *dirNode = dynamic_cast(node)) { - const QString path = dirNode->getPath(); - if (path.isEmpty()) { - continue; // the root folder cannot be published - } - cmd.set_folder_path(path.toStdString()); - } else { - continue; - } - // Toggle the node's own visibility bit (what the server persists); the - // effective visibility shown by the column may additionally be inherited - // from a parent folder. - cmd.set_is_public(!node->isPublic()); - - PendingCommand *pend = client->prepareSessionCommand(cmd); - connect(pend, &PendingCommand::finished, this, &TabDeckStorage::setVisibilityFinished); - visibilityRefreshStarted = true; - visibilityRefreshTimer->setInterval(visibilityFailSafeDelay); - visibilityRefreshTimer->start(); - client->sendCommand(pend); - } -} - -void TabDeckStorage::setVisibilityFinished(const Response &r, const CommandContainer & /*commandContainer*/) -{ - if (r.response_code() == Response::RespOk) { - if (visibilityRefreshStarted) { - visibilityRefreshTimer->setInterval(VISIBILITY_REFRESH_DELAY); - visibilityRefreshTimer->start(); - } - return; - } - - // Collect batch failures and surface them once, when publishing quiets - // down, instead of stacking one modal dialog per rejected node. - const QString message = tr("Failed to change deck visibility on server (response code %1).") - .arg(QString::number(static_cast(r.response_code()))); - if (visibilityRefreshStarted) { - visibilityFailures.append(message); - visibilityRefreshTimer->setInterval(VISIBILITY_REFRESH_DELAY); - visibilityRefreshTimer->start(); - } else { - QMessageBox::critical(this, tr("Error"), message); - } -} - -void TabDeckStorage::onVisibilityRefreshTimeout() -{ - visibilityRefreshStarted = false; - if (!visibilityFailures.isEmpty()) { - QMessageBox::critical( - this, tr("Error"), - tr("Failed to change the visibility of %n selected deck(s).", "", visibilityFailures.size())); - visibilityFailures.clear(); - } - serverDirView->refreshTree(); -} diff --git a/cockatrice/src/interface/widgets/tabs/tab_deck_storage.h b/cockatrice/src/interface/widgets/tabs/tab_deck_storage.h index f8d585880..a863e0625 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_deck_storage.h +++ b/cockatrice/src/interface/widgets/tabs/tab_deck_storage.h @@ -11,7 +11,6 @@ #include "../interface/widgets/server/remote/remote_decklist_tree_widget.h" #include "tab.h" -#include #include struct LoadedDeck; @@ -23,10 +22,8 @@ class QToolBar; class QTreeWidget; class QTreeWidgetItem; class QGroupBox; -class QTimer; class CommandContainer; class Response; -class ShareBarWidget; class TabDeckStorage : public Tab { @@ -38,25 +35,14 @@ private: QToolBar *leftToolBar, *rightToolBar; RemoteDeckList_TreeWidget *serverDirView; QGroupBox *leftGroupBox, *rightGroupBox; - ShareBarWidget *shareBar; - QTimer *shareTimeoutTimer; - int shareRequestSeq = 0; - int shareInFlightSeq = 0; QAction *aOpenLocalDeck, *aRenameLocal, *aUpload, *aNewLocalFolder, *aDeleteLocalDeck; QAction *aOpenDecksFolder; - QAction *aOpenRemoteDeck, *aDownload, *aShareDecks, *aPublishDeck, *aNewFolder, *aDeleteRemoteDeck; - bool visibilityRefreshStarted = false; - QTimer *visibilityRefreshTimer; - QStringList visibilityFailures; + QAction *aOpenRemoteDeck, *aDownload, *aNewFolder, *aDeleteRemoteDeck; QString getTargetPath() const; void setRemoteEnabled(bool enabled); - void showShareNotice(const QString &message, bool warning = false); - - void setShareModeEnabled(bool enabled); - void uploadDeck(const QString &filePath, const QString &targetPath); void deleteRemoteDeck(const RemoteDeckList_TreeModel::Node *node); @@ -89,17 +75,6 @@ private slots: void actNewFolder(); void newFolderFinished(const Response &response, const CommandContainer &commandContainer); - void actShareDecks(); - void actShareSelection(); - void cancelShareDecks(); - void onServerSelectionChanged(); - void shareFromTreeFinished(const Response &r, const CommandContainer &commandContainer); - void onShareFromTreeTimeout(); - - void actPublishDeck(); - void setVisibilityFinished(const Response &r, const CommandContainer &commandContainer); - void onVisibilityRefreshTimeout(); - void actDeleteRemoteDeck(); void deleteFolderFinished(const Response &response, const CommandContainer &commandContainer); void deleteDeckFinished(const Response &response, const CommandContainer &commandContainer); diff --git a/cockatrice/src/interface/widgets/tabs/tab_developer.cpp b/cockatrice/src/interface/widgets/tabs/tab_developer.cpp deleted file mode 100644 index 39652ef7d..000000000 --- a/cockatrice/src/interface/widgets/tabs/tab_developer.cpp +++ /dev/null @@ -1,261 +0,0 @@ -/** - * @file tab_developer.cpp - * @ingroup ServerTabs - */ -//! \todo Document this file. - -#include "tab_developer.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -static constexpr int DEFAULT_AUTO_REFRESH_INTERVAL_SECS = 30; - -TabDeveloper::TabDeveloper(TabSupervisor *_tabSupervisor, AbstractClient *_client) - : Tab(_tabSupervisor), client(_client) -{ - statsTable = new QTableWidget(0, 2); - statsTable->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); - statsTable->setEditTriggers(QAbstractItemView::NoEditTriggers); - statsTable->setSelectionBehavior(QAbstractItemView::SelectRows); - statsTable->setSelectionMode(QAbstractItemView::SingleSelection); - statsTable->verticalHeader()->setVisible(false); - statsTable->horizontalHeader()->setSectionResizeMode(0, QHeaderView::Interactive); - statsTable->horizontalHeader()->setSectionResizeMode(1, QHeaderView::Interactive); - statsTable->horizontalHeader()->setStretchLastSection(true); - - commandTable = new QTableWidget(0, 4); - commandTable->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); - commandTable->setEditTriggers(QAbstractItemView::NoEditTriggers); - commandTable->setSelectionBehavior(QAbstractItemView::SelectRows); - commandTable->setSelectionMode(QAbstractItemView::SingleSelection); - commandTable->verticalHeader()->setVisible(false); - commandTable->horizontalHeader()->setSectionResizeMode(0, QHeaderView::Interactive); - commandTable->horizontalHeader()->setSectionResizeMode(1, QHeaderView::Interactive); - commandTable->horizontalHeader()->setSectionResizeMode(2, QHeaderView::Interactive); - commandTable->horizontalHeader()->setSectionResizeMode(3, QHeaderView::Interactive); - - statusLabel = new QLabel; - - autoRefreshCheckBox = new QCheckBox; - autoRefreshCheckBox->setChecked(false); - - refreshIntervalSpinBox = new QSpinBox; - refreshIntervalSpinBox->setRange(5, 3600); - refreshIntervalSpinBox->setValue(DEFAULT_AUTO_REFRESH_INTERVAL_SECS); - refreshIntervalSpinBox->setEnabled(false); - - autoRefreshTimer = new QTimer(this); - connect(autoRefreshTimer, &QTimer::timeout, this, &TabDeveloper::refreshClicked); - connect(autoRefreshCheckBox, &QCheckBox::toggled, this, &TabDeveloper::autoRefreshToggled); - connect(refreshIntervalSpinBox, QOverload::of(&QSpinBox::valueChanged), this, - &TabDeveloper::refreshIntervalChanged); - - refreshButton = new QPushButton; - refreshButton->setAutoDefault(true); - connect(refreshButton, &QPushButton::clicked, this, &TabDeveloper::refreshClicked); - - auto *buttonLayout = new QHBoxLayout; - buttonLayout->addWidget(statusLabel, 1, Qt::AlignLeft); - buttonLayout->addWidget(autoRefreshCheckBox, 0, Qt::AlignRight); - buttonLayout->addWidget(refreshIntervalSpinBox, 0, Qt::AlignRight); - buttonLayout->addWidget(refreshButton, 0, Qt::AlignRight); - - auto *tableLayout = new QHBoxLayout; - tableLayout->addWidget(statsTable, 1); - tableLayout->addWidget(commandTable, 2); - - auto *mainLayout = new QVBoxLayout; - mainLayout->addLayout(tableLayout, 1); - mainLayout->addLayout(buttonLayout); - - auto *central = new QWidget; - central->setLayout(mainLayout); - setCentralWidget(central); - - retranslateUi(); -} - -void TabDeveloper::retranslateUi() -{ - autoRefreshCheckBox->setText(tr("Auto-refresh")); - autoRefreshCheckBox->setToolTip(tr("Automatically request fresh server statistics at a fixed interval.")); - refreshIntervalSpinBox->setSuffix(tr(" s")); - refreshIntervalSpinBox->setToolTip(tr("Seconds between automatic refreshes.")); - refreshButton->setText(tr("Refresh server stats")); - statsTable->setHorizontalHeaderLabels(QString(tr("Statistic;Value")).split(";")); - commandTable->setHorizontalHeaderLabels(QString(tr("Command;Count;Total ms;Avg ms")).split(";")); - if (statsTable->rowCount() == 0) { - statusLabel->clear(); - } -} - -QString TabDeveloper::formatBytes(quint64 bytes) -{ - const quint64 kib = 1024; - const quint64 mib = 1024 * kib; - const quint64 gib = 1024 * mib; - if (bytes >= gib) { - return tr("%1 GiB").arg(QString::number(bytes / static_cast(gib), 'f', 2)); - } - if (bytes >= mib) { - return tr("%1 MiB").arg(QString::number(bytes / static_cast(mib), 'f', 2)); - } - if (bytes >= kib) { - return tr("%1 KiB").arg(QString::number(bytes / static_cast(kib), 'f', 2)); - } - return tr("%1 bytes").arg(bytes); -} - -QString TabDeveloper::formatDurationMs(qint64 ms) -{ - if (ms >= 1000) { - return tr("%1 s").arg(QString::number(ms / 1000.0, 'f', 2)); - } - return tr("%1 ms").arg(ms); -} - -void TabDeveloper::appendStatRow(const QString &name, const QString &value) -{ - const int row = statsTable->rowCount(); - statsTable->insertRow(row); - statsTable->setItem(row, 0, new QTableWidgetItem(name)); - statsTable->setItem(row, 1, new QTableWidgetItem(value)); -} - -void TabDeveloper::appendSeparatorRow(const QString §ionTitle) -{ - const int row = statsTable->rowCount(); - statsTable->insertRow(row); - auto *labelItem = new QTableWidgetItem(sectionTitle); - auto font = labelItem->font(); - font.setBold(true); - labelItem->setFont(font); - labelItem->setFlags(labelItem->flags() & ~Qt::ItemIsSelectable); - statsTable->setItem(row, 0, labelItem); - statsTable->setItem(row, 1, new QTableWidgetItem(QString())); -} - -void TabDeveloper::refreshClicked() -{ - if (requestPending) { - return; - } - requestPending = true; - Command_GetServerStats cmd; - PendingCommand *pend = client->prepareDeveloperCommand(cmd); - connect(pend, &PendingCommand::finished, this, &TabDeveloper::serverStatsResponse); - client->sendCommand(pend); -} - -void TabDeveloper::autoRefreshToggled(bool checked) -{ - refreshIntervalSpinBox->setEnabled(checked); - if (checked) { - refreshIntervalChanged(); - refreshClicked(); - } else { - autoRefreshTimer->stop(); - } -} - -void TabDeveloper::refreshIntervalChanged() -{ - if (autoRefreshCheckBox->isChecked()) { - autoRefreshTimer->start(refreshIntervalSpinBox->value() * 1000); - } -} - -void TabDeveloper::serverStatsResponse(const Response &resp) -{ - requestPending = false; - if (resp.response_code() != Response::RespOk) { - statusLabel->setText(tr("No server statistics available yet.")); - return; - } - - const Response_GetServerStats &response = resp.GetExtension(Response_GetServerStats::ext); - - statsTable->setRowCount(0); - - // Overview section - appendStatRow(tr("Registered users online"), QString::number(response.users_count())); - appendStatRow(tr("Moderators online"), QString::number(response.mods_count())); - appendStatRow(tr("Games running"), QString::number(response.games_count())); - appendStatRow(tr("Traffic sent (last tick)"), formatBytes(response.tx_bytes())); - appendStatRow(tr("Traffic received (last tick)"), formatBytes(response.rx_bytes())); - - const qint64 uptime = static_cast(response.uptime_secs()); - const int days = static_cast(uptime / 86400); - const int hours = static_cast((uptime % 86400) / 3600); - const int minutes = static_cast((uptime % 3600) / 60); - appendStatRow(tr("Server uptime"), days > 0 ? tr("%1d %2h %3m").arg(days).arg(hours).arg(minutes) - : tr("%1h %2m").arg(hours).arg(minutes)); - - const QDateTime snapshotTime = QDateTime::fromSecsSinceEpoch(static_cast(response.timest())); - appendStatRow(tr("Snapshot taken"), snapshotTime.toLocalTime().toString("yyyy-MM-dd HH:mm")); - - // Live metrics section - appendSeparatorRow(tr("Live Metrics")); - appendStatRow(tr("Cards in live games"), QString::number(response.cards_in_games())); - appendStatRow(tr("Total commands processed"), QString::number(response.total_commands())); - - if (response.total_commands() > 0) { - const double avgMs = static_cast(response.total_command_time_ms()) / response.total_commands(); - appendStatRow(tr("Avg command time"), QString::number(avgMs, 'f', 2) + " ms"); - } - appendStatRow(tr("Active command types"), QString::number(response.active_command_types())); - - appendStatRow(tr("Event loop stalls"), QString::number(response.eventloop_stalls_total())); - appendStatRow(tr("Last stall overshoot"), formatDurationMs(response.eventloop_last_stall_ms())); - appendStatRow(tr("Worst stall overshoot"), formatDurationMs(response.eventloop_max_stall_ms())); - - if (response.game_start_count() > 0) { - appendStatRow(tr("Game starts"), QString::number(response.game_start_count())); - const double avgStartMs = static_cast(response.game_start_total_ms()) / response.game_start_count(); - appendStatRow(tr("Avg game start time"), QString::number(avgStartMs, 'f', 1) + " ms"); - } - - // Per-command breakdown table - QList sortedStats(response.command_stats().begin(), response.command_stats().end()); - std::sort(sortedStats.begin(), sortedStats.end(), - [](const auto &a, const auto &b) { return a.total_ms() > b.total_ms(); }); - - commandTable->setRowCount(0); - for (const auto &cs : sortedStats) { - const int row = commandTable->rowCount(); - commandTable->insertRow(row); - commandTable->setItem(row, 0, new QTableWidgetItem(QString::fromStdString(cs.command_name()))); - - auto *countItem = new QTableWidgetItem(QString::number(cs.count())); - countItem->setTextAlignment(Qt::AlignRight | Qt::AlignVCenter); - commandTable->setItem(row, 1, countItem); - - auto *totalItem = new QTableWidgetItem(QString::number(cs.total_ms())); - totalItem->setTextAlignment(Qt::AlignRight | Qt::AlignVCenter); - commandTable->setItem(row, 2, totalItem); - - const double avg = cs.count() > 0 ? static_cast(cs.total_ms()) / cs.count() : 0.0; - auto *avgItem = new QTableWidgetItem(QString::number(avg, 'f', 2)); - avgItem->setTextAlignment(Qt::AlignRight | Qt::AlignVCenter); - commandTable->setItem(row, 3, avgItem); - } - commandTable->resizeColumnsToContents(); - statsTable->resizeColumnsToContents(); - commandTable->resizeColumnsToContents(); - - statusLabel->setText(tr("Updated %1").arg(QDateTime::currentDateTime().toString("yyyy-MM-dd HH:mm"))); -} diff --git a/cockatrice/src/interface/widgets/tabs/tab_developer.h b/cockatrice/src/interface/widgets/tabs/tab_developer.h deleted file mode 100644 index 727a56bcb..000000000 --- a/cockatrice/src/interface/widgets/tabs/tab_developer.h +++ /dev/null @@ -1,55 +0,0 @@ -/** - * @file tab_developer.h - * @ingroup ServerTabs - */ -//! \todo Document this file. - -#ifndef TAB_DEVELOPER_H -#define TAB_DEVELOPER_H - -#include "tab.h" - -class AbstractClient; -class QCheckBox; -class QLabel; -class QPushButton; -class QSpinBox; -class QTableWidget; -class QTimer; -class Response; - -class TabDeveloper : public Tab -{ - Q_OBJECT -private: - AbstractClient *client; - QTableWidget *statsTable; - QTableWidget *commandTable; - QPushButton *refreshButton; - QLabel *statusLabel; - QCheckBox *autoRefreshCheckBox; - QSpinBox *refreshIntervalSpinBox; - QTimer *autoRefreshTimer; - bool requestPending = false; - - void appendStatRow(const QString &name, const QString &value); - void appendSeparatorRow(const QString §ionTitle); - static QString formatBytes(quint64 bytes); - static QString formatDurationMs(qint64 ms); - -private slots: - void refreshClicked(); - void serverStatsResponse(const Response &resp); - void autoRefreshToggled(bool checked); - void refreshIntervalChanged(); - -public: - explicit TabDeveloper(TabSupervisor *_tabSupervisor, AbstractClient *_client); - void retranslateUi() override; - [[nodiscard]] QString getTabText() const override - { - return tr("Developer"); - } -}; - -#endif diff --git a/cockatrice/src/interface/widgets/tabs/tab_game.cpp b/cockatrice/src/interface/widgets/tabs/tab_game.cpp index 035ab1004..7ffcd8a9b 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_game.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_game.cpp @@ -20,23 +20,14 @@ #include "../interface/card_picture_loader/card_picture_loader.h" #include "../interface/widgets/cards/card_info_frame_widget.h" #include "../interface/widgets/dialogs/dlg_create_game.h" -#include "../interface/widgets/dialogs/dlg_invite_to_game.h" -#include "../interface/widgets/server/game_link.h" #include "../interface/widgets/server/user/user_list_manager.h" -#include "../interface/widgets/utility/completer_utils.h" #include "../interface/widgets/utility/line_edit_completer.h" #include "../interface/window_main.h" #include "../main.h" #include "../utility/visibility_change_listener.h" -#include "card/card_completer_proxy_model.h" -#include "card/card_search_model.h" -#include "card_database_display_model.h" -#include "card_database_model.h" #include "tab_supervisor.h" #include -#include -#include #include #include #include @@ -44,12 +35,8 @@ #include #include #include -#include -#include #include -#include #include -#include #include #include #include @@ -266,10 +253,6 @@ void TabGame::resetChatAndPhase() // reset phase markers game->getGameState()->setCurrentPhase(-1); - - // reset spectator state so the replay can rebuild it from the start - game->getPlayerManager()->clearSpectators(); - playerListWidget->clearSpectators(); } void TabGame::emitUserEvent() @@ -303,9 +286,6 @@ void TabGame::retranslateUi() QString tabText = " | " + type + " #" + QString::number(game->getGameMetaInfo()->gameId()); updatePlayerListDockTitle(); - if (inviteButton) { - inviteButton->setText(tr("Invite")); - } cardInfoDock->setWindowTitle(tr("Card Info") + (cardInfoDock->isWindow() ? tabText : QString())); messageLayoutDock->setWindowTitle(tr("Messages") + (messageLayoutDock->isWindow() ? tabText : QString())); if (replayDock) { @@ -344,12 +324,6 @@ void TabGame::retranslateUi() if (aGameInfo) { aGameInfo->setText(tr("Game &information")); } - if (aCopyGameLink) { - aCopyGameLink->setText(tr("Cop&y game link")); - } - if (aInviteToGame) { - aInviteToGame->setText(tr("Invite to Game...")); - } if (aConcede) { if (game->getPlayerManager()->isMainPlayerConceded()) { aConcede->setText(tr("Un&concede")); @@ -517,56 +491,6 @@ void TabGame::actGameInfo() dlg.exec(); } -void TabGame::actCopyGameLink() -{ - const QString link = - makeGameJoinLink(tabSupervisor->getClient()->serverName(), tabSupervisor->getClient()->serverPort(), - game->getGameMetaInfo()->proto().room_id(), game->getGameMetaInfo()->gameId(), - QString::fromStdString(game->getGameMetaInfo()->proto().description())); - QApplication::clipboard()->setText(link); -} - -void TabGame::updateInviteButtonState() -{ - // The dock button stays conservative (pre-start, not full); the menu action - // additionally covers started/full games, which are legitimate spectate - // invites, so it only needs the server-linked + not-closed conditions. - const bool canInvite = !tabSupervisor->getIsLocalGame() && !game->getGameState()->isGameClosed() && - !game->getGameMetaInfo()->started() && - game->getPlayerManager()->getPlayerCount() < game->getGameMetaInfo()->maxPlayers(); - if (inviteButton) { - inviteButton->setVisible(canInvite); - } - if (aInviteToGame) { - aInviteToGame->setEnabled(!tabSupervisor->getIsLocalGame() && !game->getGameState()->isGameClosed()); - } -} - -void TabGame::actInviteToGame() -{ - if (!tabSupervisor || tabSupervisor->getIsLocalGame()) { - return; - } - - GameMetaInfo *metaInfo = game->getGameMetaInfo(); - const QString inviteUrl = makeGameJoinLink( - tabSupervisor->getClient()->serverName(), tabSupervisor->getClient()->serverPort(), metaInfo->proto().room_id(), - metaInfo->gameId(), QString::fromStdString(metaInfo->proto().description())); - - QStringList excludeUserNames; - excludeUserNames << tabSupervisor->getUserListManager()->getOwnUsername(); - for (auto player : game->getPlayerManager()->getPlayers()) { - excludeUserNames << player->getPlayerInfo()->getName(); - } - for (auto it = game->getPlayerManager()->getSpectators().cbegin(); - it != game->getPlayerManager()->getSpectators().cend(); ++it) { - excludeUserNames << QString::fromStdString(it.value().name()); - } - - DlgInviteToGame dlg(tabSupervisor, inviteUrl, metaInfo->proto().only_buddies(), excludeUserNames, this); - dlg.exec(); -} - void TabGame::actConcede() { PlayerLogic *player = game->getPlayerManager()->getActiveLocalPlayer(game->getGameState()->getActivePlayer()); @@ -614,8 +538,7 @@ bool TabGame::leaveGame() void TabGame::actSay() { - if (sayEdit->hasVisibleCompleterPopup()) { - sayEdit->hideCompleterPopups(); + if (completer->popup()->isVisible()) { return; } @@ -635,14 +558,14 @@ void TabGame::addPlayerToAutoCompleteList(QString playerName) { if (sayEdit && !autocompleteUserList.contains(playerName)) { autocompleteUserList << playerName; - mentionModel->setStringList(autocompleteUserList); + sayEdit->setCompletionList(autocompleteUserList); } } void TabGame::removePlayerFromAutoCompleteList(QString playerName) { if (sayEdit && autocompleteUserList.removeOne(playerName)) { - mentionModel->setStringList(autocompleteUserList); + sayEdit->setCompletionList(autocompleteUserList); } } @@ -705,8 +628,8 @@ void TabGame::actRotateViewCCW() void TabGame::actCompleterChanged() { - SettingsCache::instance().chat().getChatMentionCompleter() ? mentionCompleter->setCompletionRole(2) - : mentionCompleter->setCompletionRole(1); + SettingsCache::instance().chat().getChatMentionCompleter() ? completer->setCompletionRole(2) + : completer->setCompletionRole(1); } void TabGame::notifyPlayerJoin(QString playerName) @@ -915,7 +838,6 @@ void TabGame::stopGame() QMapIterator i(deckViewContainers); while (i.hasNext()) { i.next(); - i.value()->playerDeckView->advancePlaymatRotation(); i.value()->show(); } @@ -1056,11 +978,6 @@ void TabGame::createMenuItems() connect(aRotateViewCCW, &QAction::triggered, this, &TabGame::actRotateViewCCW); aGameInfo = new QAction(this); connect(aGameInfo, &QAction::triggered, this, &TabGame::actGameInfo); - aCopyGameLink = new QAction(this); - aCopyGameLink->setEnabled(!tabSupervisor->getIsLocalGame() && !tabSupervisor->getClient()->serverName().isEmpty()); - connect(aCopyGameLink, &QAction::triggered, this, &TabGame::actCopyGameLink); - aInviteToGame = new QAction(this); - connect(aInviteToGame, &QAction::triggered, this, &TabGame::actInviteToGame); aConcede = new QAction(this); connect(aConcede, &QAction::triggered, this, &TabGame::actConcede); if (!game->getGameMetaInfo()->started()) { @@ -1099,8 +1016,6 @@ void TabGame::createMenuItems() gameMenu->addAction(aRotateViewCCW); gameMenu->addSeparator(); gameMenu->addAction(aGameInfo); - gameMenu->addAction(aCopyGameLink); - gameMenu->addAction(aInviteToGame); gameMenu->addAction(aConcede); gameMenu->addAction(aFocusChat); gameMenu->addAction(aLeaveGame); @@ -1109,9 +1024,6 @@ void TabGame::createMenuItems() aCardMenu = gameMenu->addMenu(new QMenu(this)); - // Sync the new action with the same state the dock button already shows. - updateInviteButtonState(); - addTabMenu(gameMenu); } @@ -1126,8 +1038,6 @@ void TabGame::createReplayMenuItems() aRotateViewCCW = nullptr; aResetLayout = nullptr; aGameInfo = nullptr; - aCopyGameLink = nullptr; - aInviteToGame = nullptr; aConcede = nullptr; aFocusChat = nullptr; aLeaveGame = new QAction(this); @@ -1326,29 +1236,11 @@ void TabGame::createPlayerListDock(bool bReplay) } playerListWidget->setFocusPolicy(Qt::NoFocus); - auto *playerListBox = new QWidget(this); - auto *vbox = new QVBoxLayout(playerListBox); - vbox->setContentsMargins(0, 0, 0, 0); - vbox->setSpacing(0); - - vbox->addWidget(playerListWidget); - - if (!bReplay) { - inviteButton = new QPushButton(tr("Invite"), playerListBox); - inviteButton->setVisible(false); - connect(inviteButton, &QPushButton::clicked, this, &TabGame::actInviteToGame); - vbox->addWidget(inviteButton); - - connect(game->getGameMetaInfo(), &GameMetaInfo::startedChanged, this, &TabGame::updateInviteButtonState); - connect(game->getPlayerManager(), &PlayerManager::playerCountChanged, this, &TabGame::updateInviteButtonState); - updateInviteButtonState(); - } - playerListDock = new QDockWidget(this); playerListDock->setObjectName("playerListDock"); playerListDock->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetFloatable | QDockWidget::DockWidgetMovable); - playerListDock->setWidget(playerListBox); + playerListDock->setWidget(playerListWidget); playerListDock->setFloating(false); } @@ -1372,7 +1264,6 @@ void TabGame::createMessageDock(bool bReplay) qOverload(&CardInfoFrameWidget::setCard)); connect(messageLog, &MessageLogWidget::showCardInfoPopup, this, &TabGame::showCardInfoPopup); connect(messageLog, &MessageLogWidget::deleteCardInfoPopup, this, &TabGame::deleteCardInfoPopup); - connect(messageLog, &MessageLogWidget::cockatriceLinkActivated, this, &TabGame::cockatriceLinkActivated); if (!bReplay) { connect(messageLog, &MessageLogWidget::openMessageDialog, this, &TabGame::openMessageDialog); @@ -1390,25 +1281,12 @@ void TabGame::createMessageDock(bool bReplay) sayEdit->setMaxLength(MAX_TEXT_LENGTH); sayLabel->setBuddy(sayEdit); connect(this, &TabGame::chatMessageSent, game->getGameEventHandler(), &GameEventHandler::handleChatMessageSent); - mentionModel = new QStringListModel(autocompleteUserList, sayEdit); - mentionCompleter = createMentionCompleter(mentionModel, sayEdit); - sayEdit->addCompleter(mentionCompleter, CompleterTrigger::Mention); - - auto *cardDatabaseModel = new CardDatabaseModel(CardDatabaseManager::getInstance(), false, sayEdit); - auto *displayModel = new CardDatabaseDisplayModel(sayEdit); - displayModel->setSourceModel(cardDatabaseModel); - const CardCompleterSetup cardSetup = createCardCompleter(displayModel, sayEdit); - sayEdit->addCompleter(cardSetup.completer, CompleterTrigger::Card); - - connect(sayEdit, &LineEditCompleter::cardPartialChanged, this, [this, cardSetup](const QString &text) { - cardSetup.searchModel->updateSearchResults(text); - cardSetup.proxyModel->setFilterRegularExpression( - QRegularExpression(QRegularExpression::escape(text), QRegularExpression::CaseInsensitiveOption)); - if (sayEdit->hasFocus()) { - cardSetup.completer->complete(); - } - }); + completer = new QCompleter(autocompleteUserList, sayEdit); + completer->setCaseSensitivity(Qt::CaseInsensitive); + completer->setMaxVisibleItems(5); + completer->setFilterMode(Qt::MatchStartsWith); + sayEdit->setCompleter(completer); actCompleterChanged(); if (game->getPlayerManager()->isSpectator()) { diff --git a/cockatrice/src/interface/widgets/tabs/tab_game.h b/cockatrice/src/interface/widgets/tabs/tab_game.h index a05b49a9f..9c1406b56 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_game.h +++ b/cockatrice/src/interface/widgets/tabs/tab_game.h @@ -19,7 +19,6 @@ #include #include #include -#include class CardMenu; class ServerInfo_PlayerProperties; @@ -37,7 +36,6 @@ class CardInfoFrameWidget; class QTimer; class QSplitter; class QLabel; -class QPushButton; class QToolButton; class QMenu; class ZoneViewLayout; @@ -63,14 +61,12 @@ private: const UserListProxy *userListProxy; ReplayWidget *replayWidget = nullptr; QStringList gameTypes; - QCompleter *mentionCompleter; - QStringListModel *mentionModel; + QCompleter *completer; QStringList autocompleteUserList; QStackedWidget *mainWidget; CardInfoFrameWidget *cardInfoFrameWidget; PlayerListWidget *playerListWidget; - QPushButton *inviteButton = nullptr; QLabel *timeElapsedLabel; MessageLogWidget *messageLog; QLabel *sayLabel; @@ -85,10 +81,9 @@ private: QAction *playersSeparator; QMenu *gameMenu, *viewMenu; TearOffMenu *phasesMenu; - QAction *aGameInfo, *aConcede, *aCopyGameLink, *aLeaveGame, *aNextPhase, *aNextPhaseAction, *aNextTurn, - *aReverseTurn, *aRemoveLocalArrows, *aRotateViewCW, *aRotateViewCCW, *aResetLayout, *aResetReplayLayout; + QAction *aGameInfo, *aConcede, *aLeaveGame, *aNextPhase, *aNextPhaseAction, *aNextTurn, *aReverseTurn, + *aRemoveLocalArrows, *aRotateViewCW, *aRotateViewCCW, *aResetLayout, *aResetReplayLayout; QAction *aFocusChat; - QAction *aInviteToGame = nullptr; QList phaseActions; QAction *aCardMenu; @@ -131,7 +126,6 @@ private: void createPlayAreaWidget(bool bReplay = false); void createDeckViewContainerWidget(bool bReplay = false); void createReplayDock(GameReplay *replay); - void updateInviteButtonState(); signals: void gameClosing(TabGame *tab); void containerProcessingStarted(const GameEventContext &context); @@ -151,9 +145,7 @@ private slots: void setCardMenu(CardMenu *menu); void actGameInfo(); - void actInviteToGame(); void actConcede(); - void actCopyGameLink(); void actRemoveLocalArrows(); void actRotateViewCW(); void actRotateViewCCW(); diff --git a/cockatrice/src/interface/widgets/tabs/tab_logs.cpp b/cockatrice/src/interface/widgets/tabs/tab_logs.cpp index f73d06b57..e3678a903 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_logs.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_logs.cpp @@ -19,8 +19,7 @@ #include #include -TabLog::TabLog(TabSupervisor *_tabSupervisor, AbstractClient *_client, bool _canUseDeveloperCommands) - : Tab(_tabSupervisor), client(_client), canUseDeveloperCommands(_canUseDeveloperCommands) +TabLog::TabLog(TabSupervisor *_tabSupervisor, AbstractClient *_client) : Tab(_tabSupervisor), client(_client) { roomTable = new QTableWidget(); roomTable->setColumnCount(6); @@ -81,9 +80,7 @@ void TabLog::getClicked() if (!mainRoom->isChecked() && !gameRoom->isChecked() && !privateChat->isChecked()) { mainRoom->setChecked(true); gameRoom->setChecked(true); - if (!canUseDeveloperCommands) { - privateChat->setChecked(true); - } + privateChat->setChecked(true); } if (maximumResults->value() == 0) { @@ -120,15 +117,7 @@ void TabLog::getClicked() }; cmd.set_date_range(dateRange); cmd.set_maximum_results(maximumResults->value()); - - PendingCommand *pend; - if (canUseDeveloperCommands) { - // Developers query logs through the developer command family. - pend = client->prepareDeveloperCommand(cmd); - } else { - pend = client->prepareModeratorCommand(cmd); - } - + PendingCommand *pend = client->prepareModeratorCommand(cmd); connect(pend, &PendingCommand::finished, this, &TabLog::viewLogHistory_processResponse); client->sendCommand(pend); } @@ -182,14 +171,6 @@ void TabLog::createDock() mainRoom = new QCheckBox(tr("Main Room")); gameRoom = new QCheckBox(tr("Game Room")); privateChat = new QCheckBox(tr("Private Chat")); - if (canUseDeveloperCommands) { - // Developers cannot query private conversations. - privateChat->setVisible(false); - // The developer family ignores the IP filter server-side, so showing - // the field would silently unfilter the result by it. Hide it. - labelFindIPAddress->setVisible(false); - findIPAddress->setVisible(false); - } pastDays = new QRadioButton(tr("Past X Days: ")); today = new QRadioButton(tr("Today")); diff --git a/cockatrice/src/interface/widgets/tabs/tab_logs.h b/cockatrice/src/interface/widgets/tabs/tab_logs.h index 8e914ea64..5d164dc92 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_logs.h +++ b/cockatrice/src/interface/widgets/tabs/tab_logs.h @@ -33,7 +33,6 @@ class TabLog : public Tab Q_OBJECT private: AbstractClient *client; - bool canUseDeveloperCommands; QLabel *labelFindUserName, *labelFindIPAddress, *labelFindGameName, *labelFindGameID, *labelMessage, *labelMaximum, *labelDescription; LineEditUnfocusable *findUsername, *findIPAddress, *findGameName, *findGameID, *findMessage; @@ -59,7 +58,7 @@ private slots: void restartLayout(); public: - TabLog(TabSupervisor *_tabSupervisor, AbstractClient *_client, bool _canUseDeveloperCommands = false); + TabLog(TabSupervisor *_tabSupervisor, AbstractClient *_client); ~TabLog() override; void retranslateUi() override; [[nodiscard]] QString getTabText() const override diff --git a/cockatrice/src/interface/widgets/tabs/tab_message.cpp b/cockatrice/src/interface/widgets/tabs/tab_message.cpp index 418843178..9e9dbce1c 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_message.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_message.cpp @@ -23,16 +23,14 @@ TabMessage::TabMessage(TabSupervisor *_tabSupervisor, AbstractClient *_client, const ServerInfo_User &_ownUserInfo, - const ServerInfo_User &_otherUserInfo, - bool _userOnline) + const ServerInfo_User &_otherUserInfo) : Tab(_tabSupervisor), client(_client), ownUserInfo(new ServerInfo_User(_ownUserInfo)), - otherUserInfo(new ServerInfo_User(_otherUserInfo)), userOnline(_userOnline) + otherUserInfo(new ServerInfo_User(_otherUserInfo)), userOnline(true) { chatView = new ChatView(tabSupervisor, 0, true); connect(chatView, &ChatView::showCardInfoPopup, this, &TabMessage::showCardInfoPopup); connect(chatView, &ChatView::deleteCardInfoPopup, this, &TabMessage::deleteCardInfoPopup); connect(chatView, &ChatView::addMentionTag, this, &TabMessage::addMentionTag); - connect(chatView, &ChatView::cockatriceLinkActivated, this, &TabMessage::cockatriceLinkActivated); sayEdit = new LineEditUnfocusable; sayEdit->setMaxLength(MAX_TEXT_LENGTH); connect(sayEdit, &LineEditUnfocusable::returnPressed, this, &TabMessage::sendMessage); @@ -96,64 +94,28 @@ void TabMessage::closeEvent(QCloseEvent *event) event->accept(); } -void TabMessage::sendPrivateMessage(const QString &text) +void TabMessage::sendMessage() { - if (tabSupervisor->getUserListManager()->isUserIgnored(getUserName())) { - chatView->appendMessage(tr("You have ignored %1; your messages are not delivered.") - .arg(QString::fromStdString(otherUserInfo->name()))); + if (sayEdit->text().isEmpty() || !userOnline) { return; } Command_Message cmd; cmd.set_user_name(otherUserInfo->name()); - cmd.set_message(text.toStdString()); + cmd.set_message(sayEdit->text().toStdString()); PendingCommand *pend = client->prepareSessionCommand(cmd); - pend->setExtraData(text); connect(pend, &PendingCommand::finished, this, &TabMessage::messageSent); client->sendCommand(pend); -} -void TabMessage::sendMessage() -{ - if (sayEdit->text().isEmpty()) { - return; - } - - if (!userOnline) { - notifyUserOffline(); - return; - } - - sendPrivateMessage(sayEdit->text()); sayEdit->clear(); } -bool TabMessage::isUserOnline() const -{ - return userOnline; -} - -void TabMessage::sendInviteMessage(const QString &text) -{ - sayEdit->setText(text); - sendMessage(); -} - -void TabMessage::messageSent(const Response &response, - const CommandContainer & /*commandContainer*/, - const QVariant &extraData) +void TabMessage::messageSent(const Response &response) { if (response.response_code() == Response::RespInIgnoreList) { chatView->appendMessage(tr( "This user is ignoring you, they cannot see your messages in main chat and you cannot join their games.")); - } else if (response.response_code() == Response::RespNameNotFound) { - // The recipient went offline before the command reached the server: restore the draft. - userOnline = false; - if (sayEdit->text().isEmpty()) { - sayEdit->setText(extraData.toString()); - } - notifyUserOffline(); } } @@ -213,8 +175,3 @@ void TabMessage::processUserJoined(const ServerInfo_User &_userInfo) userOnline = true; *otherUserInfo = _userInfo; } - -void TabMessage::notifyUserOffline() -{ - chatView->appendMessage(tr("Message not sent — %1 is offline.").arg(QString::fromStdString(otherUserInfo->name()))); -} diff --git a/cockatrice/src/interface/widgets/tabs/tab_message.h b/cockatrice/src/interface/widgets/tabs/tab_message.h index 0e6d66d4c..0472bb061 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_message.h +++ b/cockatrice/src/interface/widgets/tabs/tab_message.h @@ -19,7 +19,6 @@ class LineEditUnfocusable; class Event_UserMessage; class Response; class ServerInfo_User; -class CommandContainer; class TabMessage : public Tab { @@ -40,7 +39,7 @@ signals: void maximizeClient(); private slots: void sendMessage(); - void messageSent(const Response &response, const CommandContainer &commandContainer, const QVariant &extraData); + void messageSent(const Response &response); void addMentionTag(QString mentionTag); void messageClicked(); @@ -51,8 +50,7 @@ public: TabMessage(TabSupervisor *_tabSupervisor, AbstractClient *_client, const ServerInfo_User &_ownUserInfo, - const ServerInfo_User &_otherUserInfo, - bool _userOnline); + const ServerInfo_User &_otherUserInfo); ~TabMessage() override; void retranslateUi() override; void tabActivated() override; @@ -64,14 +62,9 @@ public: void processUserLeft(); void processUserJoined(const ServerInfo_User &_userInfo); - [[nodiscard]] bool isUserOnline() const; - void sendPrivateMessage(const QString &text); - void sendInviteMessage(const QString &text); - private: bool shouldShowSystemPopup(const Event_UserMessage &event); void showSystemPopup(const Event_UserMessage &event); - void notifyUserOffline(); }; #endif diff --git a/cockatrice/src/interface/widgets/tabs/tab_moderation.cpp b/cockatrice/src/interface/widgets/tabs/tab_moderation.cpp deleted file mode 100644 index b9c37f1af..000000000 --- a/cockatrice/src/interface/widgets/tabs/tab_moderation.cpp +++ /dev/null @@ -1,465 +0,0 @@ -#include "tab_moderation.h" - -#include "abstract_client.h" -#include "tab_supervisor.h" - -#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_EMAIL = 1; -constexpr int COL_ALTS_CLIENTID = 2; -constexpr int COL_ALTS_REGISTERED = 3; -constexpr int COL_ALTS_LAST_LOGIN = 4; -constexpr int COL_ALTS_WARNS = 5; -constexpr int COL_ALTS_BANS = 6; -constexpr int COL_ALTS_ACTIVE = 7; -constexpr int COL_ALTS_COUNT = 8; - -constexpr int COL_SESSIONS_IP = 0; -constexpr int COL_SESSIONS_CLIENTID = 1; -constexpr int COL_SESSIONS_START = 2; -constexpr int COL_SESSIONS_END = 3; -constexpr int COL_SESSIONS_TYPE = 4; -constexpr int COL_SESSIONS_COUNT = 5; - -constexpr int COL_STAFF_USER = 0; -constexpr int COL_STAFF_LEVEL = 1; -constexpr int COL_STAFF_LAST_LOGIN = 2; -constexpr int COL_STAFF_COUNT = 3; -} // namespace - -TabModeration::TabModeration(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, &TabModeration::investigateUser); - - investigateButton = new QPushButton; - connect(investigateButton, &QPushButton::clicked, this, &TabModeration::investigateUser); - - resetPasswordButton = new QPushButton; - connect(resetPasswordButton, &QPushButton::clicked, this, &TabModeration::resetPassword); - - removeAvatarButton = new QPushButton; - connect(removeAvatarButton, &QPushButton::clicked, this, &TabModeration::removeAvatar); - - auto *topBar = new QHBoxLayout; - topBar->addWidget(searchEdit); - topBar->addWidget(investigateButton); - topBar->addStretch(); - topBar->addWidget(resetPasswordButton); - topBar->addWidget(removeAvatarButton); - - userInfoGroup = new QGroupBox; - userInfoNameLabel = new QLabel; - userInfoNameValue = new QLabel; - userInfoRegisteredLabel = new QLabel; - userInfoRegisteredValue = new QLabel; - userInfoLastLoginLabel = new QLabel; - userInfoLastLoginValue = new QLabel; - userInfoStatusLabel = new QLabel; - userInfoStatusValue = new QLabel; - userInfoCountsLabel = new QLabel; - userInfoCountsValue = new QLabel; - userInfoNotesLabel = new QLabel; - userInfoNotesEdit = new QTextEdit; - userInfoNotesEdit->setReadOnly(true); - - 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(infoGrid); - - auto configureTable = [](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); - }; - - altsGroup = new QGroupBox; - altsTable = new QTableWidget(0, COL_ALTS_COUNT); - configureTable(altsTable); - auto *altsLayout = new QVBoxLayout(altsGroup); - altsLayout->addWidget(altsTable); - - sessionsGroup = new QGroupBox; - sessionsTable = new QTableWidget(0, COL_SESSIONS_COUNT); - configureTable(sessionsTable); - auto *sessionsLayout = new QVBoxLayout(sessionsGroup); - sessionsLayout->addWidget(sessionsTable); - - staffGroup = new QGroupBox; - staffTable = new QTableWidget(0, COL_STAFF_COUNT); - configureTable(staffTable); - refreshStaffButton = new QPushButton; - connect(refreshStaffButton, &QPushButton::clicked, this, &TabModeration::requestModeratorLogins); - auto *staffHeader = new QHBoxLayout; - staffHeader->addStretch(); - staffHeader->addWidget(refreshStaffButton); - auto *staffLayout = new QVBoxLayout(staffGroup); - staffLayout->addWidget(staffTable); - staffLayout->addLayout(staffHeader); - - auto *splitter = new QSplitter(Qt::Vertical); - splitter->addWidget(userInfoGroup); - 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); - mainLayout->addWidget(splitter); - - retranslateUi(); - clearUserData(); - requestModeratorLogins(); - investigate(initialUser); -} - -void TabModeration::retranslateUi() -{ - searchEdit->setPlaceholderText(tr("User name")); - investigateButton->setText(tr("Investigate")); - resetPasswordButton->setText(tr("Reset Password")); - removeAvatarButton->setText(tr("Remove Avatar")); - 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:")); - - 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")}); - 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 -{ - if (ts == 0) { - return tr("Unknown"); - } - return QDateTime::fromSecsSinceEpoch(ts).toLocalTime().toString("yyyy-MM-dd HH:mm"); -} - -void TabModeration::clearUserData() -{ - currentUser.clear(); - userInfoNameValue->clear(); - userInfoRegisteredValue->clear(); - userInfoLastLoginValue->clear(); - userInfoStatusValue->clear(); - userInfoCountsValue->clear(); - userInfoNotesEdit->clear(); - altsTable->setRowCount(0); - sessionsTable->setRowCount(0); - resetPasswordButton->setEnabled(false); - removeAvatarButton->setEnabled(false); -} - -void TabModeration::investigate(const QString &userName) -{ - if (userName.isEmpty()) { - return; - } - searchEdit->setText(userName); - investigateUser(); -} - -void TabModeration::investigateUser() -{ - const QString userName = searchEdit->text().simplified(); - if (userName.isEmpty()) { - return; - } - currentUser = userName; - resetPasswordButton->setEnabled(true); - removeAvatarButton->setEnabled(true); - altsTable->setRowCount(0); - sessionsTable->setRowCount(0); - requestUserInfo(userName); - requestSessions(userName); - requestAlts(userName); -} - -void TabModeration::requestUserInfo(const QString &userName) -{ - userInfoNameValue->setText(userName); - userInfoRegisteredValue->setText(tr("Loading...")); - userInfoLastLoginValue->setText(tr("Loading...")); - userInfoStatusValue->clear(); - userInfoCountsValue->clear(); - userInfoNotesEdit->clear(); - - Command_ReportUserInfo cmd; - cmd.set_user_name(userName.toStdString()); - PendingCommand *pend = client->prepareModeratorCommand(cmd); - connect(pend, &PendingCommand::finished, this, &TabModeration::userInfoResponse); - client->sendCommand(pend); -} - -void TabModeration::userInfoResponse(const Response &response) -{ - if (response.response_code() != Response::RespOk) { - userInfoRegisteredValue->clear(); - userInfoLastLoginValue->clear(); - userInfoStatusValue->setText(tr("Error loading user info.")); - return; - } - - const Response_ReportUserInfo &resp = response.GetExtension(Response_ReportUserInfo::ext); - if (resp.has_user_name() && QString::fromStdString(resp.user_name()) != currentUser) { - return; - } - - 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(", ")); - - 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())); - } else { - userInfoNotesEdit->setPlainText(tr("(none)")); - } -} - -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, &TabModeration::sessionsResponse); - client->sendCommand(pend); -} - -void TabModeration::sessionsResponse(const Response &response) -{ - sessionsTable->setRowCount(0); - if (response.response_code() != Response::RespOk) { - return; - } - - const Response_UserSessions &resp = response.GetExtension(Response_UserSessions::ext); - sessionsTable->setRowCount(resp.sessions_size()); - for (int i = 0; i < resp.sessions_size(); ++i) { - const ServerInfo_UserSession &session = resp.sessions(i); - sessionsTable->setItem(i, COL_SESSIONS_IP, new QTableWidgetItem(QString::fromStdString(session.ip_address()))); - sessionsTable->setItem(i, COL_SESSIONS_CLIENTID, - new QTableWidgetItem(QString::fromStdString(session.clientid()))); - sessionsTable->setItem(i, COL_SESSIONS_START, new QTableWidgetItem(formatEpoch(session.start_time()))); - sessionsTable->setItem( - i, COL_SESSIONS_END, - new QTableWidgetItem(session.end_time() == 0 ? tr("Active") : formatEpoch(session.end_time()))); - sessionsTable->setItem(i, COL_SESSIONS_TYPE, - new QTableWidgetItem(QString::fromStdString(session.connection_type()))); - } -} - -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, &TabModeration::altsResponse); - client->sendCommand(pend); -} - -void TabModeration::altsResponse(const Response &response) -{ - altsTable->setRowCount(0); - if (response.response_code() != Response::RespOk) { - return; - } - - const Response_UserAlts &resp = response.GetExtension(Response_UserAlts::ext); - altsTable->setRowCount(resp.alts_size()); - for (int i = 0; i < resp.alts_size(); ++i) { - const ServerInfo_UserAlt &alt = resp.alts(i); - altsTable->setItem(i, COL_ALTS_USER, new QTableWidgetItem(QString::fromStdString(alt.user_name()))); - altsTable->setItem(i, COL_ALTS_EMAIL, new QTableWidgetItem(QString::fromStdString(alt.email()))); - altsTable->setItem(i, COL_ALTS_CLIENTID, new QTableWidgetItem(QString::fromStdString(alt.clientid()))); - altsTable->setItem(i, COL_ALTS_REGISTERED, new QTableWidgetItem(formatEpoch(alt.registration_time()))); - altsTable->setItem(i, COL_ALTS_LAST_LOGIN, new QTableWidgetItem(formatEpoch(alt.last_login()))); - altsTable->setItem(i, COL_ALTS_WARNS, new QTableWidgetItem(QString::number(alt.warn_count()))); - altsTable->setItem(i, COL_ALTS_BANS, new QTableWidgetItem(QString::number(alt.ban_count()))); - altsTable->setItem(i, COL_ALTS_ACTIVE, new QTableWidgetItem(alt.is_active() ? tr("yes") : tr("no"))); - } -} - -void TabModeration::requestModeratorLogins() -{ - Command_GetModeratorLastLogins cmd; - PendingCommand *pend = client->prepareModeratorCommand(cmd); - connect(pend, &PendingCommand::finished, this, &TabModeration::moderatorLoginsResponse); - client->sendCommand(pend); -} - -void TabModeration::moderatorLoginsResponse(const Response &response) -{ - staffTable->setRowCount(0); - if (response.response_code() != Response::RespOk) { - return; - } - - const Response_ModeratorLastLogins &resp = response.GetExtension(Response_ModeratorLastLogins::ext); - staffTable->setRowCount(resp.logins_size()); - for (int i = 0; i < resp.logins_size(); ++i) { - const ServerInfo_ModeratorLogin &login = resp.logins(i); - staffTable->setItem(i, COL_STAFF_USER, new QTableWidgetItem(QString::fromStdString(login.user_name()))); - staffTable->setItem(i, COL_STAFF_LAST_LOGIN, new QTableWidgetItem(formatEpoch(login.last_login()))); - - QStringList levels; - if (login.user_level() & ServerInfo_User::IsAdmin) { - levels << tr("Admin"); - } - if (login.user_level() & ServerInfo_User::IsDeveloper) { - levels << tr("Developer"); - } - if (login.user_level() & ServerInfo_User::IsModerator) { - levels << tr("Moderator"); - } - if (login.user_level() & ServerInfo_User::IsJudge) { - levels << tr("Judge"); - } - staffTable->setItem(i, COL_STAFF_LEVEL, new QTableWidgetItem(levels.join(" / "))); - } -} - -void TabModeration::resetPassword() -{ - if (currentUser.isEmpty()) { - return; - } - - QMessageBox::StandardButton choice = - QMessageBox::warning(this, tr("Reset Password"), - tr("Reset the password of %1? A temporary password will be generated and shown to you. " - "The user must change it on their first login.") - .arg(currentUser), - QMessageBox::Ok | QMessageBox::Cancel, QMessageBox::Cancel); - if (choice != QMessageBox::Ok) { - return; - } - - Command_ResetUserPassword cmd; - cmd.set_user_name(currentUser.toStdString()); - PendingCommand *pend = client->prepareModeratorCommand(cmd); - connect(pend, &PendingCommand::finished, this, &TabModeration::resetPasswordResponse); - client->sendCommand(pend); -} - -void TabModeration::resetPasswordResponse(const Response &response) -{ - if (response.response_code() != Response::RespOk) { - QMessageBox::critical(this, tr("Error"), tr("Password reset failed.")); - return; - } - - const Response_ResetUserPassword &resp = response.GetExtension(Response_ResetUserPassword::ext); - QMessageBox::information(this, tr("Password reset"), - tr("Temporary password for %1:\n\n%2\n\nPass it to the user through a secure channel.") - .arg(QString::fromStdString(resp.user_name())) - .arg(QString::fromStdString(resp.temporary_password()))); -} - -void TabModeration::removeAvatar() -{ - if (currentUser.isEmpty()) { - return; - } - - QMessageBox::StandardButton choice = - QMessageBox::warning(this, tr("Remove Avatar"), - tr("Remove the avatar of %1? The user will have to upload a new one.").arg(currentUser), - QMessageBox::Ok | QMessageBox::Cancel, QMessageBox::Cancel); - if (choice != QMessageBox::Ok) { - return; - } - - Command_RemoveUserAvatar cmd; - cmd.set_user_name(currentUser.toStdString()); - PendingCommand *pend = client->prepareModeratorCommand(cmd); - connect(pend, &PendingCommand::finished, this, &TabModeration::removeAvatarResponse); - client->sendCommand(pend); -} - -void TabModeration::removeAvatarResponse(const Response &response) -{ - if (response.response_code() != Response::RespOk) { - QMessageBox::critical(this, tr("Error"), tr("Could not remove the avatar.")); - return; - } - - const Response_RemoveUserAvatar &resp = response.GetExtension(Response_RemoveUserAvatar::ext); - QMessageBox::information(this, tr("Avatar removed"), - tr("The avatar of %1 has been removed.").arg(QString::fromStdString(resp.user_name()))); -} diff --git a/cockatrice/src/interface/widgets/tabs/tab_moderation.h b/cockatrice/src/interface/widgets/tabs/tab_moderation.h deleted file mode 100644 index 0a534992b..000000000 --- a/cockatrice/src/interface/widgets/tabs/tab_moderation.h +++ /dev/null @@ -1,83 +0,0 @@ -#ifndef TAB_MODERATION_H -#define TAB_MODERATION_H - -#include "tab.h" - -#include - -class AbstractClient; -class QGroupBox; -class QLabel; -class QLineEdit; -class QPushButton; -class QTableWidget; -class QTextEdit; - -/** - * Staff investigation tool. Lets moderators look up a user's account data, - * alternate accounts, login sessions, and staff login activity, and offers - * the password-reset and remove-avatar actions. - */ -class TabModeration : public Tab -{ - Q_OBJECT -public: - explicit TabModeration(TabSupervisor *_tabSupervisor, AbstractClient *_client, const QString &initialUser = {}); - void retranslateUi() override; - [[nodiscard]] QString getTabText() const override - { - return tr("Moderation"); - } - void investigate(const QString &userName); - -private slots: - void investigateUser(); - void userInfoResponse(const Response &response); - void sessionsResponse(const Response &response); - void altsResponse(const Response &response); - void moderatorLoginsResponse(const Response &response); - void resetPassword(); - void resetPasswordResponse(const Response &response); - void removeAvatar(); - void removeAvatarResponse(const Response &response); - -private: - void requestUserInfo(const QString &userName); - void requestSessions(const QString &userName); - void requestAlts(const QString &userName); - void requestModeratorLogins(); - void clearUserData(); - [[nodiscard]] QString formatEpoch(quint64 ts) const; - - AbstractClient *client; - QString currentUser; - - QLineEdit *searchEdit; - QPushButton *investigateButton; - QPushButton *resetPasswordButton; - QPushButton *removeAvatarButton; - - QGroupBox *userInfoGroup; - QLabel *userInfoNameLabel; - QLabel *userInfoNameValue; - QLabel *userInfoRegisteredLabel; - QLabel *userInfoRegisteredValue; - QLabel *userInfoLastLoginLabel; - QLabel *userInfoLastLoginValue; - QLabel *userInfoStatusLabel; - QLabel *userInfoStatusValue; - QLabel *userInfoCountsLabel; - QLabel *userInfoCountsValue; - QLabel *userInfoNotesLabel; - QTextEdit *userInfoNotesEdit; - - QGroupBox *altsGroup; - QTableWidget *altsTable; - QGroupBox *sessionsGroup; - QTableWidget *sessionsTable; - QGroupBox *staffGroup; - QTableWidget *staffTable; - QPushButton *refreshStaffButton; -}; - -#endif // TAB_MODERATION_H diff --git a/cockatrice/src/interface/widgets/tabs/tab_public_decks.cpp b/cockatrice/src/interface/widgets/tabs/tab_public_decks.cpp deleted file mode 100644 index 35389d3bf..000000000 --- a/cockatrice/src/interface/widgets/tabs/tab_public_decks.cpp +++ /dev/null @@ -1,251 +0,0 @@ -#include "tab_public_decks.h" - -#include "../../../client/settings/cache_settings.h" -#include "../../deck_loader/deck_loader.h" -#include "../general/layout_containers/flow_widget.h" -#include "../visual_deck_storage/deck_preview/deck_preview_color_identity_filter_widget.h" -#include "../visual_deck_storage/deck_preview/public_deck_preview_widget.h" -#include "../visual_deck_storage/remote_public_decks_model.h" -#include "../visual_deck_storage/visual_deck_storage_quick_settings_widget.h" -#include "../visual_deck_storage/visual_deck_storage_search_widget.h" -#include "../visual_deck_storage/visual_deck_storage_tag_filter_widget.h" -#include "tab_supervisor.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -TabPublicDecks::TabPublicDecks(TabSupervisor *_tabSupervisor, AbstractClient *_client, const QString &_userName) - : Tab(_tabSupervisor), client(_client), userName(_userName) -{ - model = new RemotePublicDecksModel(client, this); - cardSize = SettingsCache::instance().cardsDisplay().getVisualDeckStorageCardSize(); - - titleLabel = new QLabel(tr("Public decks of %1").arg(userName.toHtmlEscaped()), this); - QFont titleFont = titleLabel->font(); - titleFont.setBold(true); - titleLabel->setFont(titleFont); - - auto *headerLayout = new QHBoxLayout; - headerLayout->addWidget(titleLabel); - headerLayout->addStretch(1); - - // Filter/toolbar row, matching the Visual Deck Storage: color identity filter - // first, the search bar stretching in the middle, and the quick settings - // 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); - - statusLabel = new QLabel(this); - statusLabel->setAlignment(Qt::AlignCenter); - statusLabel->setVisible(false); - - flowWidget = new FlowWidget(this, Qt::Horizontal, Qt::ScrollBarAlwaysOff, Qt::ScrollBarAsNeeded); - flowWidget->setSpacing(8, 8); - - colorIdentityFilter = new DeckPreviewColorIdentityFilterWidget(this); - searchWidget = new VisualDeckStorageSearchWidget(this); - refreshButton = new QToolButton(this); - refreshButton->setIcon(QPixmap("theme:icons/reload")); - refreshButton->setFixedSize(32, 32); - quickSettingsWidget = new VisualDeckStorageQuickSettingsWidget(this); - quickSettingsWidget->setPublicDecksMode(true); - - auto *filterLayout = new QHBoxLayout; - filterLayout->addWidget(colorIdentityFilter); - filterLayout->addWidget(searchWidget, 1); - filterLayout->addWidget(refreshButton); - filterLayout->addWidget(quickSettingsWidget); - - tagFilterWidget = new VisualDeckStorageTagFilterWidget(this); - tagFilterWidget->setAllTagsProvider([this] { return model->allTags(); }); - updateTagsVisibility(quickSettingsWidget->getShowTagFilter()); - - auto *layout = new QVBoxLayout; - layout->addLayout(headerLayout); - layout->addLayout(filterLayout); - layout->addWidget(tagFilterWidget); - layout->addWidget(statusLabel); - layout->addWidget(emptyLabel); - layout->addWidget(flowWidget, 1); - - auto *mainWidget = new QWidget(this); - mainWidget->setLayout(layout); - setCentralWidget(mainWidget); - - connect(refreshButton, &QToolButton::clicked, this, [this] { model->refresh(userName); }); - connect(model, &QAbstractItemModel::modelReset, this, &TabPublicDecks::rebuildGrid); - connect(model, &RemotePublicDecksModel::loadingChanged, this, &TabPublicDecks::updateLoadingState); - connect(model, &RemotePublicDecksModel::loadFailed, this, [this](const QString &message) { - lastFailureMessage = message; - statusLabel->setText(message); - statusLabel->setVisible(true); - flowWidget->setVisible(false); - emptyLabel->setVisible(false); - }); - connect(searchWidget, &VisualDeckStorageSearchWidget::searchTextChanged, this, - [this](const QString &text) { model->setSearchText(text); }); - connect(colorIdentityFilter, &DeckPreviewColorIdentityFilterWidget::activeColorsChanged, this, - &TabPublicDecks::updateColorFilter); - connect(colorIdentityFilter, &DeckPreviewColorIdentityFilterWidget::filterModeChanged, this, - &TabPublicDecks::updateColorFilter); - connect(tagFilterWidget, &VisualDeckStorageTagFilterWidget::filterChanged, this, &TabPublicDecks::updateTagFilter); - connect(quickSettingsWidget, &VisualDeckStorageQuickSettingsWidget::cardSizeChanged, this, - &TabPublicDecks::updateCardSize); - connect(quickSettingsWidget, &VisualDeckStorageQuickSettingsWidget::showTagFilterChanged, this, - &TabPublicDecks::updateTagsVisibility); - - retranslateUi(); - model->refresh(userName); -} - -QString TabPublicDecks::getTabText() const -{ - return tr("Public decks of %1").arg(userName); -} - -void TabPublicDecks::retranslateUi() -{ - // The username is another user's data, so escape it for the AutoText QLabel. - titleLabel->setText(tr("Public decks of %1").arg(userName.toHtmlEscaped())); - // The same choice rebuildGrid makes, so a language change does not swap - // the "no match" variant for the "nothing published" one. - emptyLabel->setText(model->totalCount() > 0 ? tr("No decks match your filters.") - : tr("This user has not published any decks.")); - refreshButton->setToolTip(tr("Refresh")); - refreshButton->setAccessibleName(tr("Refresh")); - quickSettingsWidget->setToolTip(tr("Public Decks Settings")); - // Re-show whatever the status label is showing so a language change picks up - // the new language or, for a failure message, at least does not hide it. - if (model->isLoading()) { - updateLoadingState(true); - } else if (!lastFailureMessage.isEmpty()) { - statusLabel->setText(lastFailureMessage); - statusLabel->setVisible(true); - flowWidget->setVisible(false); - emptyLabel->setVisible(false); - } else { - updateLoadingState(false); - } - emit tabTextChanged(this, getTabText()); -} - -bool TabPublicDecks::closeRequest() -{ - emit closing(this); - return Tab::closeRequest(); -} - -void TabPublicDecks::rebuildGrid() -{ - flowWidget->clearLayout(); - - const int count = model->rowCount(); - if (count == 0) { - emptyLabel->setText(model->totalCount() > 0 ? tr("No decks match your filters.") - : tr("This user has not published any decks.")); - } - emptyLabel->setVisible(count == 0); - for (int i = 0; i < count; ++i) { - auto *tile = new PublicDeckPreviewWidget(flowWidget, model->entryAt(i)); - tile->setScaleFactor(cardSize); - connect(tile, &PublicDeckPreviewWidget::openDeckRequested, this, &TabPublicDecks::openDeck); - flowWidget->addWidget(tile); - } - - // The deck set changed, so the tag filter chips are re-gathered from it. - tagFilterWidget->refreshTags(); -} - -void TabPublicDecks::updateColorFilter() -{ - model->setColorFilter(colorIdentityFilter->getFilterMode(), colorIdentityFilter->getActiveColors()); -} - -void TabPublicDecks::updateTagFilter() -{ - const QStringList selectedTags = tagFilterWidget->selectedTags(); - const QStringList excludedTags = tagFilterWidget->excludedTags(); - model->setTagFilter(QSet(selectedTags.cbegin(), selectedTags.cend()), - QSet(excludedTags.cbegin(), excludedTags.cend())); - tagFilterWidget->refreshTags(); -} - -void TabPublicDecks::updateTagsVisibility(bool visible) -{ - tagFilterWidget->setVisible(visible); -} - -void TabPublicDecks::updateLoadingState(bool loading) -{ - if (loading) { - // A new attempt is under way, so the previously shown failure, if any, - // no longer describes the current state. - lastFailureMessage.clear(); - statusLabel->setText(tr("Loading public decks…")); - statusLabel->setVisible(true); - flowWidget->setVisible(false); - emptyLabel->setVisible(false); - } else { - statusLabel->setVisible(false); - flowWidget->setVisible(true); - } -} - -void TabPublicDecks::updateCardSize(int scale) -{ - cardSize = scale; - applyCardSize(scale); -} - -void TabPublicDecks::applyCardSize(int scale) -{ - const auto tiles = flowWidget->findChildren(); - for (PublicDeckPreviewWidget *tile : tiles) { - tile->setScaleFactor(scale); - } - flowWidget->setMinimumSizeToMaxSizeHint(); -} - -void TabPublicDecks::openDeck(int deckId) -{ - Command_DeckDownloadPublic cmd; - cmd.set_deck_id(deckId); - - PendingCommand *pend = client->prepareSessionCommand(cmd); - connect(pend, &PendingCommand::finished, this, &TabPublicDecks::openDeckFinished); - client->sendCommand(pend); -} - -void TabPublicDecks::openDeckFinished(const Response &response, const CommandContainer & /*commandContainer*/) -{ - if (response.response_code() != Response::RespOk) { - QMessageBox::warning(this, tr("Open public deck"), - tr("Failed to open the public deck (server response code %1).") - .arg(QString::number(static_cast(response.response_code())))); - return; - } - - const Response_DeckDownload &resp = response.GetExtension(Response_DeckDownload::ext); - std::optional deckOpt = - DeckLoader::loadFromRemote(QString::fromStdString(resp.deck()), LoadedDeck::LoadInfo::NON_REMOTE_ID); - if (!deckOpt) { - QMessageBox::warning(this, tr("Open public deck"), tr("The public deck could not be parsed.")); - return; - } - - tabSupervisor->openDeckInNewTab(deckOpt.value()); -} diff --git a/cockatrice/src/interface/widgets/tabs/tab_public_decks.h b/cockatrice/src/interface/widgets/tabs/tab_public_decks.h deleted file mode 100644 index 492fdeafa..000000000 --- a/cockatrice/src/interface/widgets/tabs/tab_public_decks.h +++ /dev/null @@ -1,80 +0,0 @@ -/** - * @file tab_public_decks.h - * @ingroup Tabs - */ - -#ifndef TAB_PUBLIC_DECKS_H -#define TAB_PUBLIC_DECKS_H - -#include "tab.h" - -class AbstractClient; -class CommandContainer; -class DeckPreviewColorIdentityFilterWidget; -class FlowWidget; -class PublicDeckPreviewWidget; -class QLabel; -class QToolButton; -class RemotePublicDecksModel; -class Response; -class VisualDeckStorageQuickSettingsWidget; -class VisualDeckStorageSearchWidget; -class VisualDeckStorageTagFilterWidget; - -/** - * @brief A visual grid of the public decks published by another user. - * - * The grid is rendered from the preview metadata the server stores for the - * decks, so browsing costs no downloads; the deck list is fetched via - * Command_DeckDownloadPublic only when the user opens a deck. Multiple users - * can be browsed simultaneously; each gets its own tab. - */ -class TabPublicDecks final : public Tab -{ - Q_OBJECT - -public: - TabPublicDecks(TabSupervisor *tabSupervisor, AbstractClient *client, const QString &userName); - - [[nodiscard]] QString getTabText() const override; - void retranslateUi() override; - bool closeRequest() override; - - [[nodiscard]] QString getUserName() const - { - return userName; - } - -signals: - void closing(TabPublicDecks *tab); - -private slots: - void openDeck(int deckId); - void openDeckFinished(const Response &response, const CommandContainer &commandContainer); - void updateColorFilter(); - void updateTagFilter(); - void updateCardSize(int scale); - void updateTagsVisibility(bool visible); - void updateLoadingState(bool loading); - -private: - void rebuildGrid(); - void applyCardSize(int scale); - - AbstractClient *client; - QString userName; - RemotePublicDecksModel *model; - FlowWidget *flowWidget; - VisualDeckStorageSearchWidget *searchWidget; - DeckPreviewColorIdentityFilterWidget *colorIdentityFilter; - VisualDeckStorageTagFilterWidget *tagFilterWidget; - QToolButton *refreshButton; - VisualDeckStorageQuickSettingsWidget *quickSettingsWidget; - QLabel *titleLabel; - QLabel *statusLabel; - QLabel *emptyLabel; - QString lastFailureMessage; ///< Last load-failure text, re-shown on retranslate. - int cardSize = 100; -}; - -#endif // TAB_PUBLIC_DECKS_H diff --git a/cockatrice/src/interface/widgets/tabs/tab_replays.cpp b/cockatrice/src/interface/widgets/tabs/tab_replays.cpp index ac4b2cbe9..5618604df 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_replays.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_replays.cpp @@ -1,7 +1,6 @@ #include "tab_replays.h" #include "../../../client/settings/cache_settings.h" -#include "../../pixel_map_generator.h" #include "../interface/widgets/server/remote/remote_replay_list_tree_widget.h" #include "tab_game.h" @@ -103,17 +102,17 @@ QGroupBox *TabReplays::createLeftLayout() // Left side actions aOpenLocalReplay = new QAction(this); - aOpenLocalReplay->setIcon(themePixmap(QStringLiteral("icons/view"))); + aOpenLocalReplay->setIcon(QPixmap("theme:icons/view")); connect(aOpenLocalReplay, &QAction::triggered, this, &TabReplays::actOpenLocalReplay); connect(localDirView, &QTreeView::doubleClicked, this, &TabReplays::actOpenLocalReplay); aRenameLocal = new QAction(this); - aRenameLocal->setIcon(themePixmap(QStringLiteral("icons/rename"))); + aRenameLocal->setIcon(QPixmap("theme: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(themePixmap(QStringLiteral("icons/remove_row"))); + aDeleteLocalReplay->setIcon(QPixmap("theme:icons/remove_row")); connect(aDeleteLocalReplay, &QAction::triggered, this, &TabReplays::actDeleteLocalReplay); aOpenReplaysFolder = new QAction(this); @@ -165,24 +164,24 @@ QGroupBox *TabReplays::createRightLayout() // Right side actions aOpenRemoteReplay = new QAction(this); - aOpenRemoteReplay->setIcon(themePixmap(QStringLiteral("icons/view"))); + aOpenRemoteReplay->setIcon(QPixmap("theme:icons/view")); connect(aOpenRemoteReplay, &QAction::triggered, this, &TabReplays::actOpenRemoteReplay); connect(serverDirView, &QTreeView::doubleClicked, this, &TabReplays::actOpenRemoteReplay); aDownload = new QAction(this); - aDownload->setIcon(themePixmap(QStringLiteral("icons/arrow_left_green"))); + aDownload->setIcon(QPixmap("theme:icons/arrow_left_green")); connect(aDownload, &QAction::triggered, this, &TabReplays::actDownload); aKeep = new QAction(this); - aKeep->setIcon(themePixmap(QStringLiteral("icons/lock"))); + aKeep->setIcon(QPixmap("theme:icons/lock")); connect(aKeep, &QAction::triggered, this, &TabReplays::actKeepRemoteReplay); aDeleteRemoteReplay = new QAction(this); - aDeleteRemoteReplay->setIcon(themePixmap(QStringLiteral("icons/remove_row"))); + aDeleteRemoteReplay->setIcon(QPixmap("theme:icons/remove_row")); connect(aDeleteRemoteReplay, &QAction::triggered, this, &TabReplays::actDeleteRemoteReplay); aGetReplayCode = new QAction(this); - aGetReplayCode->setIcon(themePixmap(QStringLiteral("icons/share"))); + aGetReplayCode->setIcon(QPixmap("theme:icons/share")); connect(aGetReplayCode, &QAction::triggered, this, &TabReplays::actGetReplayCode); aSubmitReplayCode = new QAction(this); - aSubmitReplayCode->setIcon(themePixmap(QStringLiteral("icons/search"))); + aSubmitReplayCode->setIcon(QPixmap("theme: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 deleted file mode 100644 index 0b9108f9b..000000000 --- a/cockatrice/src/interface/widgets/tabs/tab_report.cpp +++ /dev/null @@ -1,927 +0,0 @@ -#include "tab_report.h" - -#include "../utility/report_utils.h" -#include "abstract_client.h" -#include "tab_supervisor.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace -{ -constexpr int COL_ID = 0; -constexpr int COL_TIME = 1; -constexpr int COL_REPORTER = 2; -constexpr int COL_REPORTED = 3; -constexpr int COL_CATEGORY = 4; -constexpr int COL_GAMEID = 5; -constexpr int COL_STATUS = 6; -constexpr int COL_ASSIGNED = 7; -constexpr int COL_REPLAY = 8; -constexpr int COL_ROOM = 9; -constexpr int COL_COUNT = 10; -constexpr int REFRESH_INTERVAL_MS = 300000; -} // namespace - -TabReport::TabReport(TabSupervisor *_tabSupervisor, AbstractClient *_client) : Tab(_tabSupervisor), client(_client) -{ - auto *centralWidget = new QWidget(this); - setCentralWidget(centralWidget); - - searchEdit = new QLineEdit; - searchEdit->setClearButtonEnabled(true); - connect(searchEdit, &QLineEdit::textChanged, this, &TabReport::applyFilters); - - statusFilter = new QComboBox; - connect(statusFilter, &QComboBox::currentIndexChanged, this, &TabReport::applyFilters); - - unresolvedOnlyBox = new QCheckBox; - unresolvedOnlyBox->setChecked(true); -#if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0) - connect(unresolvedOnlyBox, &QCheckBox::checkStateChanged, this, &TabReport::refreshList); -#else - connect(unresolvedOnlyBox, &QCheckBox::stateChanged, this, &TabReport::refreshList); -#endif - - refreshButton = new QPushButton; - connect(refreshButton, &QPushButton::clicked, this, &TabReport::refreshList); - - refreshTimer = new QTimer(this); - refreshTimer->setInterval(REFRESH_INTERVAL_MS); - connect(refreshTimer, &QTimer::timeout, this, [this]() { - if (tabSupervisor->currentWidget() == this) { - refreshList(); - } - }); - refreshTimer->start(); - - auto *topBar = new QHBoxLayout; - topBar->addWidget(searchEdit); - topBar->addWidget(statusFilter); - topBar->addWidget(unresolvedOnlyBox); - topBar->addStretch(); - topBar->addWidget(refreshButton); - - statsLabel = new QLabel; - - table = new QTableWidget(0, COL_COUNT); - table->setSelectionBehavior(QAbstractItemView::SelectRows); - table->setSelectionMode(QAbstractItemView::SingleSelection); - table->setEditTriggers(QAbstractItemView::NoEditTriggers); - table->setSortingEnabled(true); - table->verticalHeader()->setVisible(false); - table->setAlternatingRowColors(true); - table->horizontalHeader()->setSectionResizeMode(COL_TIME, QHeaderView::ResizeToContents); - table->horizontalHeader()->setSectionResizeMode(COL_REPORTED, QHeaderView::Stretch); - table->horizontalHeader()->setSectionResizeMode(COL_REPORTER, QHeaderView::ResizeToContents); - connect(table, &QTableWidget::itemSelectionChanged, this, &TabReport::onSelectionChanged); - - descGroup = new QGroupBox; - descriptionEdit = new QTextEdit; - descriptionEdit->setReadOnly(true); - auto *descLayout = new QVBoxLayout(descGroup); - descLayout->setContentsMargins(4, 4, 4, 4); - descLayout->addWidget(descriptionEdit); - - chatGroup = new QGroupBox; - chatLogEdit = new QTextEdit; - chatLogEdit->setReadOnly(true); - QFont monoFont("monospace"); - monoFont.setStyleHint(QFont::Monospace); - const int systemPointSize = QFontDatabase::systemFont(QFontDatabase::GeneralFont).pointSize(); - if (systemPointSize > 0) { - monoFont.setPointSize(systemPointSize); - } - chatLogEdit->setFont(monoFont); - auto *chatLayout = new QVBoxLayout(chatGroup); - chatLayout->setContentsMargins(4, 4, 4, 4); - chatLayout->addWidget(chatLogEdit); - - commentsGroup = new QGroupBox; - commentsEdit = new QTextEdit; - commentsEdit->setReadOnly(true); - auto *commentInputRow = new QHBoxLayout; - commentInput = new QLineEdit; - commentButton = new QPushButton; - commentButton->setEnabled(false); - connect(commentButton, &QPushButton::clicked, this, &TabReport::addComment); - connect(commentInput, &QLineEdit::returnPressed, this, &TabReport::addComment); - commentInputRow->addWidget(commentInput); - commentInputRow->addWidget(commentButton); - auto *commentsLayout = new QVBoxLayout(commentsGroup); - commentsLayout->setContentsMargins(4, 4, 4, 4); - commentsLayout->addWidget(commentsEdit); - commentsLayout->addLayout(commentInputRow); - - userContextGroup = new QGroupBox; - userContextName = new QLabel; - userContextAccountAge = new QLabel; - userContextReports = new QLabel; - userContextBans = new QLabel; - userContextWarns = new QLabel; - userContextNotes = new QTextEdit; - userContextNotes->setReadOnly(true); - userContextNotes->setMaximumHeight(80); - userContextRecentReports = new QTextEdit; - userContextRecentReports->setReadOnly(true); - userContextRecentReports->setMaximumHeight(120); - userContextUserLabel = new QLabel; - userContextAgeLabel = new QLabel; - userContextReportsLabel = new QLabel; - userContextBansLabel = new QLabel; - userContextWarnsLabel = new QLabel; - userContextNotesLabel = new QLabel; - userContextRecentReportsLabel = new QLabel; - auto *ucLayout = new QGridLayout(userContextGroup); - ucLayout->setContentsMargins(8, 8, 8, 8); - ucLayout->addWidget(userContextUserLabel, 0, 0); - ucLayout->addWidget(userContextName, 0, 1, 1, 3); - ucLayout->addWidget(userContextAgeLabel, 1, 0); - ucLayout->addWidget(userContextAccountAge, 1, 1); - ucLayout->addWidget(userContextReportsLabel, 1, 2); - ucLayout->addWidget(userContextReports, 1, 3); - ucLayout->addWidget(userContextBansLabel, 2, 0); - ucLayout->addWidget(userContextBans, 2, 1); - ucLayout->addWidget(userContextWarnsLabel, 2, 2); - ucLayout->addWidget(userContextWarns, 2, 3); - ucLayout->addWidget(userContextNotesLabel, 3, 0, Qt::AlignTop); - 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); - statsTotalLabel = new QLabel; - statsTrendLabel = new QLabel; - statsCategoriesLabel = new QLabel; - statsDetailText = new QTextEdit; - statsDetailText->setReadOnly(true); - statsDetailText->setMaximumHeight(150); - auto *statsContent = new QWidget; - auto *sgLayout = new QVBoxLayout(statsContent); - sgLayout->setContentsMargins(8, 8, 8, 8); - sgLayout->addWidget(statsTotalLabel); - sgLayout->addWidget(statsTrendLabel); - sgLayout->addWidget(statsCategoriesLabel); - sgLayout->addWidget(statsDetailText); - auto *statsGroupLayout = new QVBoxLayout(statsGroup); - statsGroupLayout->setContentsMargins(0, 0, 0, 0); - statsGroupLayout->addWidget(statsContent); - statsGroup->setChecked(true); - connect(statsGroup, &QGroupBox::toggled, this, [this, statsContent](bool checked) { - statsContent->setVisible(checked); - if (checked) { - requestStats(); - } - }); - - 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; - resolveWithNoteButton = new QPushButton; - dismissButton = new QPushButton; - viewReplayButton = new QPushButton; - joinGameButton = new QPushButton; - connect(assignButton, &QPushButton::clicked, this, &TabReport::assignReport); - connect(resolveButton, &QPushButton::clicked, this, [this]() { resolveReport(false, false); }); - connect(resolveWithNoteButton, &QPushButton::clicked, this, [this]() { resolveReport(false, true); }); - connect(dismissButton, &QPushButton::clicked, this, [this]() { resolveReport(true, true); }); - connect(viewReplayButton, &QPushButton::clicked, this, &TabReport::viewReplay); - connect(joinGameButton, &QPushButton::clicked, this, &TabReport::joinGame); - - statusLabel = new QLabel; - - auto *actionBar = new QHBoxLayout; - actionBar->addWidget(assignButton); - actionBar->addWidget(resolveButton); - actionBar->addWidget(resolveWithNoteButton); - actionBar->addWidget(dismissButton); - actionBar->addSpacing(20); - actionBar->addWidget(viewReplayButton); - actionBar->addWidget(joinGameButton); - actionBar->addStretch(); - actionBar->addWidget(statusLabel); - - auto *layout = new QVBoxLayout(centralWidget); - layout->addLayout(topBar); - layout->addWidget(statsLabel); - layout->addWidget(table, 2); - layout->addWidget(detailSplitter, 1); - layout->addWidget(userContextGroup); - layout->addWidget(statsGroup); - layout->addLayout(actionBar); - - retranslateUi(); - - setActionsEnabled(false); - refreshList(); -} - -void TabReport::retranslateUi() -{ - searchEdit->setPlaceholderText(tr("Search by username, category...")); - statusFilter->clear(); - statusFilter->addItem(tr("All Statuses"), ""); - statusFilter->addItem(tr("Open"), "open"); - statusFilter->addItem(tr("Assigned"), "assigned"); - statusFilter->addItem(tr("Resolved"), "resolved"); - statusFilter->addItem(tr("Dismissed"), "dismissed"); - unresolvedOnlyBox->setText(tr("Unresolved only")); - refreshButton->setText(tr("Refresh")); - - table->setHorizontalHeaderLabels({tr("#"), tr("Time"), tr("Reporter"), tr("Reported User"), tr("Category"), - tr("Game ID"), tr("Status"), tr("Assigned To"), tr("Replay"), tr("Room")}); - - 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:")); - userContextBansLabel->setText(tr("Bans:")); - userContextWarnsLabel->setText(tr("Warns:")); - userContextNotesLabel->setText(tr("Admin Notes:")); - userContextRecentReportsLabel->setText(tr("Recent Reports Against User:")); - userContextAccountAge->setText(QString()); - userContextReports->setText(QString()); - userContextBans->setText(QString()); - userContextWarns->setText(QString()); - - statsGroup->setTitle(tr("Report Statistics")); - - assignButton->setText(tr("Assign to Me")); - resolveButton->setText(tr("Resolve")); - resolveWithNoteButton->setText(tr("Resolve with note...")); - dismissButton->setText(tr("Dismiss...")); - viewReplayButton->setText(tr("View Replay")); - joinGameButton->setText(tr("Join Game")); -} - -void TabReport::refreshList() -{ - selectedReportIdBeforeRefresh = selectedReportId(); - commentDraftBeforeRefresh = commentInput->text(); - previousSelectedReportValid = selectedReportInfo(previousSelectedReport); - - refreshButton->setEnabled(false); - statusLabel->setText(tr("Loading...")); - table->setRowCount(0); - allReports.clear(); - filteredReports.clear(); - commentButton->setEnabled(false); - setActionsEnabled(false); - - Command_ReportList cmd; - cmd.set_unresolved_only(unresolvedOnlyBox->isChecked()); - - PendingCommand *pend = client->prepareModeratorCommand(cmd); - connect(pend, &PendingCommand::finished, this, &TabReport::reportListResponse); - client->sendCommand(pend); -} - -void TabReport::reportListResponse(const Response &response) -{ - refreshButton->setEnabled(true); - - if (response.response_code() != Response::RespOk) { - statusLabel->setText(tr("Failed to load reports.")); - return; - } - - const Response_ReportList &resp = response.GetExtension(Response_ReportList::ext); - allReports.clear(); - for (int i = 0; i < resp.reports_size(); ++i) { - allReports.append(resp.reports(i)); - } - - applyFilters(); - updateStats(); - - if (selectedReportIdBeforeRefresh >= 0) { - bool found = false; - for (int row = 0; row < table->rowCount(); ++row) { - if (table->item(row, COL_ID) && - table->item(row, COL_ID)->data(Qt::UserRole).toInt() == selectedReportIdBeforeRefresh) { - found = true; - { - QSignalBlocker blocker(table); - table->setCurrentCell(row, 0); - } - break; - } - } - - if (found) { - bool dataChanged = false; - ServerInfo_Report newReport; - if (!selectedReportInfo(newReport) || !previousSelectedReportValid || - previousSelectedReport.description() != newReport.description() || - previousSelectedReport.status() != newReport.status() || - previousSelectedReport.resolution_note() != newReport.resolution_note() || - previousSelectedReport.assigned_mod_name() != newReport.assigned_mod_name()) { - dataChanged = true; - } - - if (dataChanged || detailsRequestedReportId != selectedReportIdBeforeRefresh) { - loadReportDetails(selectedReportIdBeforeRefresh); - } - updateActionStates(); - } else { - descriptionEdit->clear(); - chatLogEdit->clear(); - commentsEdit->clear(); - setActionsEnabled(false); - userContextGroup->setVisible(false); - } - } - - if (commentInput->text().isEmpty()) { - commentInput->setText(commentDraftBeforeRefresh); - } - - if (statsGroup->isChecked()) { - requestStats(); - } -} - -void TabReport::applyFilters() -{ - filteredReports.clear(); - - const QString searchText = searchEdit->text().toLower(); - const QString statusFilterValue = statusFilter->currentData().toString(); - - for (const ServerInfo_Report &r : allReports) { - bool matchesSearch = searchText.isEmpty() || - QString::fromStdString(r.reporter_name()).toLower().contains(searchText) || - QString::fromStdString(r.reported_user_name()).toLower().contains(searchText) || - QString::fromStdString(r.category()).toLower().contains(searchText) || - QString::fromStdString(r.description()).toLower().contains(searchText); - - bool matchesStatus = statusFilterValue.isEmpty() || QString::fromStdString(r.status()) == statusFilterValue; - - if (matchesSearch && matchesStatus) { - filteredReports.append(r); - } - } - - table->setSortingEnabled(false); - table->setRowCount(filteredReports.size()); - - 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); - - table->setItem(row, COL_REPORTER, new QTableWidgetItem(QString::fromStdString(r.reporter_name()))); - - table->setItem(row, COL_REPLAY, - new QTableWidgetItem(r.has_replay_id() && r.replay_id() > 0 ? tr("Yes") : tr("No"))); - - table->setItem(row, COL_ROOM, - new QTableWidgetItem(r.has_room_id() && r.room_id() > 0 ? QString::number(r.room_id()) : "")); - } - - table->setSortingEnabled(true); - table->resizeColumnsToContents(); - table->horizontalHeader()->setSectionResizeMode(COL_REPORTED, QHeaderView::Stretch); - - statusLabel->setText(tr("%1 report(s)").arg(filteredReports.size())); -} - -void TabReport::updateStats() -{ - int open = 0, assigned = 0, resolved = 0, dismissed = 0; - for (const ServerInfo_Report &r : allReports) { - QString status = QString::fromStdString(r.status()); - if (status == "open") { - open++; - } else if (status == "assigned") { - assigned++; - } else if (status == "resolved") { - resolved++; - } else if (status == "dismissed") { - dismissed++; - } - } - - statsLabel->setText(tr("%1 open | %2 assigned | %3 resolved | %4 dismissed") - .arg(open) - .arg(assigned) - .arg(resolved) - .arg(dismissed)); -} - -void TabReport::onSelectionChanged() -{ - const int row = table->currentRow(); - if (row < 0 || !table->item(row, COL_ID)) { - descriptionEdit->clear(); - chatLogEdit->clear(); - commentsEdit->clear(); - commentInput->clear(); - commentButton->setEnabled(false); - setActionsEnabled(false); - userContextGroup->setVisible(false); - return; - } - - const int reportId = table->item(row, COL_ID)->data(Qt::UserRole).toInt(); - loadReportDetails(reportId); - updateActionStates(); -} - -void TabReport::loadReportDetails(int reportId) -{ - detailsRequestedReportId = reportId; - - for (const ServerInfo_Report &r : filteredReports) { - if (r.report_id() == reportId) { - descriptionEdit->setPlainText(QString::fromStdString(r.description())); - - QString reportedUser = QString::fromStdString(r.reported_user_name()); - if (!reportedUser.isEmpty()) { - requestUserInfo(reportedUser); - } else { - userContextGroup->setVisible(false); - } - - QString status = QString::fromStdString(r.status()); - bool canComment = (status == "open" || status == "assigned"); - commentButton->setEnabled(canComment); - commentInput->setEnabled(canComment); - if (!canComment) { - commentInput->setPlaceholderText(tr("This report is closed.")); - } else { - commentInput->setPlaceholderText(tr("Type a reply...")); - } - break; - } - } - - chatLogEdit->setPlainText(tr("Loading...")); - commentsEdit->setPlainText(tr("Loading...")); - - Command_ReportDetails cmd; - cmd.set_report_id(reportId); - - const int seq = ++detailsRequestSeq; - PendingCommand *pend = client->prepareSessionCommand(cmd); - connect(pend, &PendingCommand::finished, this, [this, seq](const Response &response) { - if (seq != detailsRequestSeq) { - return; - } - reportDetailsResponse(response); - }); - client->sendCommand(pend); -} - -void TabReport::reportDetailsResponse(const Response &response) -{ - if (response.response_code() != Response::RespOk) { - if (selectedReportId() == detailsRequestedReportId) { - chatLogEdit->clear(); - commentsEdit->setPlainText(tr("Failed to load report details.")); - } - return; - } - - const Response_ReportDetails &resp = response.GetExtension(Response_ReportDetails::ext); - const ServerInfo_Report &r = resp.report(); - - if (selectedReportId() != r.report_id()) { - return; - } - - report_utils::renderReportDetails(chatLogEdit, commentsEdit, r, tr("No comments yet."), tr("[Moderator]"), - tr("[Reporter]")); -} - -void TabReport::updateActionStates() -{ - const int row = table->currentRow(); - if (row < 0 || !table->item(row, COL_STATUS)) { - setActionsEnabled(false); - return; - } - - ServerInfo_Report report; - if (!selectedReportInfo(report)) { - setActionsEnabled(false); - return; - } - - const QString status = QString::fromStdString(report.status()); - assignButton->setEnabled(status == "open"); - resolveButton->setEnabled(status == "open" || status == "assigned"); - resolveWithNoteButton->setEnabled(status == "open" || status == "assigned"); - dismissButton->setEnabled(status == "open" || status == "assigned"); - - const bool hasGameId = report.game_id() > 0; - const bool hasReplay = hasGameId && report.has_replay_id() && report.replay_id() > 0; - viewReplayButton->setEnabled(hasReplay); - joinGameButton->setEnabled(hasGameId && report.has_room_id() && report.room_id() > 0); -} - -void TabReport::setActionsEnabled(bool enabled) -{ - assignButton->setEnabled(enabled); - resolveButton->setEnabled(enabled); - resolveWithNoteButton->setEnabled(enabled); - dismissButton->setEnabled(enabled); - viewReplayButton->setEnabled(false); - joinGameButton->setEnabled(false); - commentButton->setEnabled(enabled); - commentInput->setEnabled(enabled); -} - -int TabReport::selectedReportId() const -{ - const int row = table->currentRow(); - if (row < 0 || !table->item(row, COL_ID)) { - return -1; - } - return table->item(row, COL_ID)->data(Qt::UserRole).toInt(); -} - -bool TabReport::selectedReportInfo(ServerInfo_Report &info) const -{ - const int reportId = selectedReportId(); - if (reportId < 0) { - return false; - } - - for (const ServerInfo_Report &r : filteredReports) { - if (r.report_id() == reportId) { - info = r; - return true; - } - } - - return false; -} - -void TabReport::assignReport() -{ - const int reportId = selectedReportId(); - if (reportId < 0) { - return; - } - - setActionsEnabled(false); - statusLabel->setText(tr("Assigning...")); - - Command_ReportAssign cmd; - cmd.set_report_id(reportId); - - PendingCommand *pend = client->prepareModeratorCommand(cmd); - connect(pend, &PendingCommand::finished, this, &TabReport::assignResponse); - client->sendCommand(pend); -} - -void TabReport::assignResponse(const Response &response) -{ - if (response.response_code() == Response::RespOk) { - statusLabel->setText(tr("Assigned.")); - refreshList(); - } else { - statusLabel->setText(tr("Assignment failed.")); - updateActionStates(); - } -} - -void TabReport::resolveReport(bool dismissed, bool promptNote) -{ - const int reportId = selectedReportId(); - if (reportId < 0) { - return; - } - - QString note; - if (promptNote) { - bool ok; - note = QInputDialog::getText(this, dismissed ? tr("Dismiss Report") : tr("Resolve Report"), - dismissed ? tr("Optional note:") : tr("Resolution note (optional):"), - QLineEdit::Normal, QString(), &ok); - if (!ok) { - return; - } - } - - setActionsEnabled(false); - statusLabel->setText(dismissed ? tr("Dismissing...") : tr("Resolving...")); - - Command_ReportResolve cmd; - cmd.set_report_id(reportId); - cmd.set_dismissed(dismissed); - if (!note.isEmpty()) { - cmd.set_resolution_note(note.toStdString()); - } - - PendingCommand *pend = client->prepareModeratorCommand(cmd); - connect(pend, &PendingCommand::finished, this, &TabReport::resolveResponse); - client->sendCommand(pend); -} - -void TabReport::resolveResponse(const Response &response) -{ - if (response.response_code() == Response::RespOk) { - statusLabel->setText(tr("Done.")); - refreshList(); - } else { - statusLabel->setText(tr("Action failed.")); - updateActionStates(); - } -} - -void TabReport::viewReplay() -{ - ServerInfo_Report report; - if (!selectedReportInfo(report) || report.game_id() <= 0) { - return; - } - - setActionsEnabled(false); - statusLabel->setText(tr("Loading replay...")); - - Command_ReplayDownloadByGameId cmd; - cmd.set_game_id(report.game_id()); - - PendingCommand *pend = client->prepareModeratorCommand(cmd); - connect(pend, &PendingCommand::finished, this, &TabReport::viewReplayResponse); - client->sendCommand(pend); -} - -void TabReport::viewReplayResponse(const Response &response) -{ - if (response.response_code() != Response::RespOk) { - statusLabel->setText(tr("No replay available for this game.")); - updateActionStates(); - return; - } - - const Response_ReplayDownloadByGameId &resp = response.GetExtension(Response_ReplayDownloadByGameId::ext); - GameReplay *replay = new GameReplay; - if (replay->ParseFromString(resp.replay_data())) { - emit openReplay(replay); - statusLabel->setText(tr("Replay opened.")); - } else { - delete replay; - statusLabel->setText(tr("Failed to parse replay.")); - } - - updateActionStates(); -} - -void TabReport::joinGame() -{ - ServerInfo_Report report; - if (!selectedReportInfo(report) || report.game_id() <= 0) { - return; - } - - const int roomId = report.has_room_id() ? report.room_id() : -1; - if (roomId <= 0) { - statusLabel->setText(tr("No room recorded for this report, use the replay instead.")); - return; - } - - emit requestJoinGame(report.game_id(), roomId); - statusLabel->setText(tr("Attempting to join game...")); -} - -void TabReport::addComment() -{ - const int reportId = selectedReportId(); - if (reportId < 0) { - return; - } - - QString text = commentInput->text().trimmed(); - if (text.isEmpty()) { - return; - } - - commentButton->setEnabled(false); - - Command_ReportAddComment cmd; - cmd.set_report_id(reportId); - cmd.set_comment(text.toStdString()); - - PendingCommand *pend = client->prepareSessionCommand(cmd); - connect(pend, &PendingCommand::finished, this, &TabReport::addCommentResponse); - client->sendCommand(pend); -} - -void TabReport::addCommentResponse(const Response &response) -{ - if (response.response_code() == Response::RespOk) { - commentInput->clear(); - refreshList(); - } else { - commentButton->setEnabled(true); - statusLabel->setText(tr("Failed to send comment.")); - } -} - -void TabReport::requestUserInfo(const QString &userName) -{ - if (userName.isEmpty()) { - userContextGroup->setVisible(false); - return; - } - - lastRequestedUser = userName; - userContextGroup->setVisible(true); - userContextName->setText(userName); - userContextAccountAge->setText(tr("Loading...")); - userContextReports->clear(); - userContextBans->clear(); - userContextWarns->clear(); - userContextNotes->clear(); - userContextRecentReports->clear(); - - Command_ReportUserInfo cmd; - cmd.set_user_name(userName.toStdString()); - - PendingCommand *pend = client->prepareModeratorCommand(cmd); - connect(pend, &PendingCommand::finished, this, &TabReport::userInfoResponse); - client->sendCommand(pend); -} - -void TabReport::userInfoResponse(const Response &response) -{ - if (response.response_code() != Response::RespOk) { - userContextAccountAge->setText(tr("Error loading user info.")); - return; - } - - const Response_ReportUserInfo &resp = response.GetExtension(Response_ReportUserInfo::ext); - - if (resp.has_user_name() && QString::fromStdString(resp.user_name()) != lastRequestedUser) { - return; - } - - QDateTime regTime = QDateTime::fromSecsSinceEpoch(resp.registration_time()); - qint64 daysSinceReg = regTime.daysTo(QDateTime::currentDateTime()); - userContextAccountAge->setText(tr("%1 days (since %2)").arg(daysSinceReg).arg(regTime.toString("yyyy-MM-dd"))); - - userContextReports->setText(QString::number(resp.total_reports())); - userContextBans->setText(QString::number(resp.total_bans())); - userContextWarns->setText(QString::number(resp.total_warns())); - - if (resp.has_admin_notes() && !resp.admin_notes().empty()) { - userContextNotes->setPlainText(QString::fromStdString(resp.admin_notes())); - } else { - userContextNotes->setPlainText(tr("(none)")); - } - - userContextRecentReports->clear(); - if (resp.recent_reports_size() == 0) { - userContextRecentReports->setPlainText(tr("No previous reports against this user.")); - } else { - for (int i = 0; i < resp.recent_reports_size(); ++i) { - const ServerInfo_Report &r = resp.recent_reports(i); - QDateTime dt = QDateTime::fromSecsSinceEpoch(r.report_time()); - userContextRecentReports->moveCursor(QTextCursor::End); - userContextRecentReports->insertPlainText(QString("[%1] #%2 by %3 [%4]: %5\n") - .arg(dt.toString("yyyy-MM-dd")) - .arg(r.report_id()) - .arg(QString::fromStdString(r.reporter_name())) - .arg(QString::fromStdString(r.status())) - .arg(QString::fromStdString(r.category()))); - } - } -} - -void TabReport::requestStats() -{ - statsTotalLabel->setText(tr("Loading statistics...")); - statsTrendLabel->clear(); - statsCategoriesLabel->clear(); - statsDetailText->clear(); - - Command_ReportStats cmd; - - PendingCommand *pend = client->prepareModeratorCommand(cmd); - connect(pend, &PendingCommand::finished, this, &TabReport::statsResponse); - client->sendCommand(pend); -} - -void TabReport::statsResponse(const Response &response) -{ - if (response.response_code() != Response::RespOk) { - statsTotalLabel->setText(tr("Error loading statistics.")); - return; - } - - const Response_ReportStats &resp = response.GetExtension(Response_ReportStats::ext); - - statsTotalLabel->setText(tr("Total: %1 reports (%2 open, %3 assigned, %4 resolved/dismissed)") - .arg(resp.total_reports()) - .arg(resp.total_pending()) - .arg(resp.total_assigned()) - .arg(resp.total_resolved())); - - statsTrendLabel->setText(tr("Last 24h: %1 | Last 7d: %2 | Last 30d: %3 | Avg resolution: %4h") - .arg(resp.reports_last_24h()) - .arg(resp.reports_last_7d()) - .arg(resp.reports_last_30d()) - .arg(resp.avg_resolution_hours(), 0, 'f', 1)); - - QString weekCompare = - tr("This week: %1 vs last week: %2 (%3%)") - .arg(resp.reports_this_week()) - .arg(resp.reports_last_week()) - .arg(resp.reports_last_week() > 0 ? QString::number(((resp.reports_this_week() - resp.reports_last_week()) * - 100.0 / resp.reports_last_week()), - 'f', 0) - : resp.reports_this_week() > 0 ? "new" - : "0"); - statsTrendLabel->setText(statsTrendLabel->text() + " | " + weekCompare); - - statsCategoriesLabel->setText(tr("By category:")); - statsDetailText->clear(); - - statsDetailText->moveCursor(QTextCursor::End); - statsDetailText->insertPlainText(tr("=== Top Categories ===") + "\n"); - for (int i = 0; i < resp.category_counts_size(); ++i) { - const ReportCategoryCount &cc = resp.category_counts(i); - QString cat = QString::fromStdString(cc.category()); - cat.replace('_', ' '); - cat = cat.left(1).toUpper() + cat.mid(1); - statsDetailText->moveCursor(QTextCursor::End); - statsDetailText->insertPlainText(QString(" %1: %2\n").arg(cat).arg(cc.count())); - } - - statsDetailText->moveCursor(QTextCursor::End); - statsDetailText->insertPlainText("\n" + tr("=== Most Reported Users ===") + "\n"); - for (int i = 0; i < resp.top_reported_users_size(); ++i) { - const ReportTopUser &tu = resp.top_reported_users(i); - statsDetailText->moveCursor(QTextCursor::End); - statsDetailText->insertPlainText( - QString(" %1: %2 reports\n").arg(QString::fromStdString(tu.user_name())).arg(tu.count())); - } - - statsDetailText->moveCursor(QTextCursor::End); - statsDetailText->insertPlainText("\n" + tr("=== Top Reporters ===") + "\n"); - for (int i = 0; i < resp.top_reporters_size(); ++i) { - const ReportTopUser &tu = resp.top_reporters(i); - statsDetailText->moveCursor(QTextCursor::End); - statsDetailText->insertPlainText( - QString(" %1: %2 reports filed\n").arg(QString::fromStdString(tu.user_name())).arg(tu.count())); - } -} diff --git a/cockatrice/src/interface/widgets/tabs/tab_report.h b/cockatrice/src/interface/widgets/tabs/tab_report.h deleted file mode 100644 index b62a99ce2..000000000 --- a/cockatrice/src/interface/widgets/tabs/tab_report.h +++ /dev/null @@ -1,124 +0,0 @@ -#ifndef TAB_REPORT_H -#define TAB_REPORT_H - -#include "tab.h" - -#include -#include -#include - -class AbstractClient; -class QCheckBox; -class QComboBox; -class QGroupBox; -class QLabel; -class QLineEdit; -class QPushButton; -class QSplitter; -class QTableWidget; -class QTextEdit; -class QTimer; -class GameReplay; - -class TabReport : public Tab -{ - Q_OBJECT -public: - TabReport(TabSupervisor *_tabSupervisor, AbstractClient *_client); - void retranslateUi() override; - [[nodiscard]] QString getTabText() const override - { - return tr("Report Queue"); - } - -signals: - void openReplay(GameReplay *replay); - void requestJoinGame(int gameId, int roomId); - -private slots: - void refreshList(); - void reportListResponse(const Response &response); - void assignReport(); - void assignResponse(const Response &response); - void resolveReport(bool dismissed, bool promptNote); - void resolveResponse(const Response &response); - void onSelectionChanged(); - void viewReplay(); - void viewReplayResponse(const Response &response); - void joinGame(); - void addComment(); - void addCommentResponse(const Response &response); - void requestUserInfo(const QString &userName); - void userInfoResponse(const Response &response); - void reportDetailsResponse(const Response &response); - void requestStats(); - void statsResponse(const Response &response); - -private: - int selectedReportId() const; - bool selectedReportInfo(ServerInfo_Report &info) const; - void setActionsEnabled(bool enabled); - void updateActionStates(); - void applyFilters(); - void updateStats(); - void loadReportDetails(int reportId); - - AbstractClient *client; - - QLineEdit *searchEdit; - QComboBox *statusFilter; - QCheckBox *unresolvedOnlyBox; - QPushButton *refreshButton; - QLabel *statsLabel; - QTableWidget *table; - QSplitter *detailSplitter; - QGroupBox *descGroup; - QGroupBox *chatGroup; - QGroupBox *commentsGroup; - QTextEdit *descriptionEdit; - QTextEdit *chatLogEdit; - QTextEdit *commentsEdit; - QLineEdit *commentInput; - QPushButton *commentButton; - QPushButton *assignButton; - QPushButton *resolveButton; - QPushButton *resolveWithNoteButton; - QPushButton *dismissButton; - QPushButton *viewReplayButton; - QPushButton *joinGameButton; - QLabel *statusLabel; - QTimer *refreshTimer; - - QGroupBox *userContextGroup; - QLabel *userContextName; - QLabel *userContextAccountAge; - QLabel *userContextReports; - QLabel *userContextBans; - QLabel *userContextWarns; - QTextEdit *userContextNotes; - QTextEdit *userContextRecentReports; - QLabel *userContextUserLabel; - QLabel *userContextAgeLabel; - QLabel *userContextReportsLabel; - QLabel *userContextBansLabel; - QLabel *userContextWarnsLabel; - QLabel *userContextNotesLabel; - QLabel *userContextRecentReportsLabel; - QString lastRequestedUser; - QGroupBox *statsGroup; - QLabel *statsTotalLabel; - QLabel *statsTrendLabel; - QLabel *statsCategoriesLabel; - QTextEdit *statsDetailText; - - QList allReports; - QList filteredReports; - int detailsRequestedReportId = -1; - int detailsRequestSeq = 0; - int selectedReportIdBeforeRefresh = -1; - QString commentDraftBeforeRefresh; - ServerInfo_Report previousSelectedReport; - bool previousSelectedReportValid = false; -}; - -#endif // TAB_REPORT_H diff --git a/cockatrice/src/interface/widgets/tabs/tab_room.cpp b/cockatrice/src/interface/widgets/tabs/tab_room.cpp index 866324367..5705c184e 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_room.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_room.cpp @@ -2,20 +2,12 @@ #include "../../../client/settings/cache_settings.h" #include "../../../client/settings/shortcuts_settings.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" #include "../interface/widgets/server/game_selector.h" #include "../interface/widgets/server/user/user_list_manager.h" -#include "../interface/widgets/server/user/user_list_panel_widget.h" #include "../interface/widgets/server/user/user_list_widget.h" #include "../main.h" -#include "../utility/completer_utils.h" -#include "card/card_completer_proxy_model.h" -#include "card/card_search_model.h" -#include "card_database_display_model.h" -#include "card_database_model.h" #include "tab_account.h" #include "tab_supervisor.h" @@ -25,15 +17,11 @@ #include #include #include -#include #include -#include #include #include #include #include -#include -#include #include #include #include @@ -64,19 +52,28 @@ TabRoom::TabRoom(TabSupervisor *_tabSupervisor, tempMap.insert(info.room_id(), gameTypes); gameSelector = new GameSelector(client, tabSupervisor, this, QMap(), tempMap, true, true); - userListPanel = new UserListPanelWidget(tabSupervisor, client, this); - userListPanel->bind(tabSupervisor->getUserListManager()); - userList = userListPanel->getUserList(); - connect(userListPanel, &UserListPanelWidget::openMessageDialog, this, &TabRoom::openMessageDialog); + auto *tabs = new QTabWidget(this); - const auto gameInviteLinkProvider = [this]() { return tabSupervisor->getGameInviteLinksForRoom(roomId); }; - userList->setGameInviteLinkProvider(gameInviteLinkProvider); + friendsList = new UserListWidget(tabSupervisor, client, UserListWidget::BuddyList); + friendsList->bind(tabSupervisor->getUserListManager()); + userList = new UserListWidget(tabSupervisor, client, UserListWidget::RoomList); + userList->bind(tabSupervisor->getUserListManager()); + ignoreList = new UserListWidget(tabSupervisor, client, UserListWidget::IgnoreList); + ignoreList->bind(tabSupervisor->getUserListManager()); + + connect(friendsList, SIGNAL(openMessageDialog(const QString &, bool)), this, + SIGNAL(openMessageDialog(const QString &, bool))); + connect(userList, SIGNAL(openMessageDialog(const QString &, bool)), this, + SIGNAL(openMessageDialog(const QString &, bool))); + + tabs->addTab(friendsList, tr("Friends")); + tabs->addTab(userList, tr("Online")); + tabs->addTab(ignoreList, tr("Ignored")); chatView = new ChatView(tabSupervisor, nullptr, true, this); connect(chatView, &ChatView::showMentionPopup, this, &TabRoom::actShowMentionPopup); connect(chatView, &ChatView::messageClickedSignal, this, &TabRoom::focusTab); connect(chatView, &ChatView::openMessageDialog, this, &TabRoom::openMessageDialog); - connect(chatView, &ChatView::cockatriceLinkActivated, this, &TabRoom::cockatriceLinkActivated); connect(chatView, &ChatView::showCardInfoPopup, this, &TabRoom::showCardInfoPopup); connect(chatView, &ChatView::deleteCardInfoPopup, this, &TabRoom::deleteCardInfoPopup); connect(chatView, &ChatView::addMentionTag, this, &TabRoom::addMentionTag); @@ -99,7 +96,7 @@ TabRoom::TabRoom(TabSupervisor *_tabSupervisor, connect(aOpenChatSettings, &QAction::triggered, this, &TabRoom::actOpenChatSettings); auto *chatSettingsButton = new QToolButton; - chatSettingsButton->setIcon(themePixmap(QStringLiteral("icons/settings"))); + chatSettingsButton->setIcon(QPixmap("theme:icons/settings")); chatSettingsButton->setMenu(chatSettingsMenu); chatSettingsButton->setPopupMode(QToolButton::InstantPopup); @@ -121,7 +118,7 @@ TabRoom::TabRoom(TabSupervisor *_tabSupervisor, auto *hbox = new QHBoxLayout; hbox->addWidget(splitter, 3); - hbox->addWidget(userListPanel, 1); + hbox->addWidget(tabs, 1); aLeaveRoom = new QAction(this); connect(aLeaveRoom, &QAction::triggered, this, &TabRoom::closeRequest); @@ -140,27 +137,13 @@ TabRoom::TabRoom(TabSupervisor *_tabSupervisor, gameSelector->processGameInfo(info.game_list(i)); } - mentionModel = new QStringListModel(autocompleteUserList, sayEdit); - mentionCompleter = createMentionCompleter(mentionModel, sayEdit); - sayEdit->addCompleter(mentionCompleter, CompleterTrigger::Mention); - - auto *cardDatabaseModel = new CardDatabaseModel(CardDatabaseManager::getInstance(), false, sayEdit); - auto *displayModel = new CardDatabaseDisplayModel(sayEdit); - displayModel->setSourceModel(cardDatabaseModel); - const CardCompleterSetup cardSetup = createCardCompleter(displayModel, sayEdit); - sayEdit->addCompleter(cardSetup.completer, CompleterTrigger::Card); - - connect(sayEdit, &LineEditCompleter::cardPartialChanged, this, [this, cardSetup](const QString &text) { - cardSetup.searchModel->updateSearchResults(text); - cardSetup.proxyModel->setFilterRegularExpression( - QRegularExpression(QRegularExpression::escape(text), QRegularExpression::CaseInsensitiveOption)); - if (sayEdit->hasFocus()) { - cardSetup.completer->complete(); - } - }); + completer = new QCompleter(autocompleteUserList, sayEdit); + completer->setCaseSensitivity(Qt::CaseInsensitive); + completer->setMaxVisibleItems(5); + completer->setFilterMode(Qt::MatchStartsWith); + sayEdit->setCompleter(completer); actCompleterChanged(); - connect(&SettingsCache::instance().shortcuts(), &ShortcutsSettings::shortCutChanged, this, &TabRoom::refreshShortcuts); refreshShortcuts(); @@ -176,7 +159,7 @@ void TabRoom::retranslateUi() { gameSelector->retranslateUi(); chatView->retranslateUi(); - userListPanel->retranslateUi(); + userList->retranslateUi(); sayLabel->setText(tr("&Say:")); chatGroupBox->setTitle(tr("Chat")); roomMenu->setTitle(tr("&Room")); @@ -230,8 +213,8 @@ void TabRoom::sendMessage() { if (sayEdit->text().isEmpty()) { return; - } else if (sayEdit->hasVisibleCompleterPopup()) { - sayEdit->hideCompleterPopups(); + } else if (completer->popup()->isVisible()) { + completer->popup()->hide(); return; } else { Command_RoomSay cmd; @@ -265,8 +248,8 @@ void TabRoom::actOpenChatSettings() void TabRoom::actCompleterChanged() { - SettingsCache::instance().chat().getChatMentionCompleter() ? mentionCompleter->setCompletionRole(2) - : mentionCompleter->setCompletionRole(1); + SettingsCache::instance().chat().getChatMentionCompleter() ? completer->setCompletionRole(2) + : completer->setCompletionRole(1); } void TabRoom::processRoomEvent(const RoomEvent &event) @@ -302,18 +285,16 @@ void TabRoom::processListGamesEvent(const Event_ListGames &event) void TabRoom::processJoinRoomEvent(const Event_JoinRoom &event) { - QString mention = "@" + QString::fromStdString(event.user_info().name()); - if (!autocompleteUserList.contains(mention)) { - autocompleteUserList << mention; - mentionModel->setStringList(autocompleteUserList); + if (!autocompleteUserList.contains("@" + QString::fromStdString(event.user_info().name()))) { + autocompleteUserList << "@" + QString::fromStdString(event.user_info().name()); + sayEdit->setCompletionList(autocompleteUserList); } } void TabRoom::processLeaveRoomEvent(const Event_LeaveRoom &event) { - QString mention = "@" + QString::fromStdString(event.name()); - autocompleteUserList.removeOne(mention); - mentionModel->setStringList(autocompleteUserList); + autocompleteUserList.removeOne("@" + QString::fromStdString(event.name())); + sayEdit->setCompletionList(autocompleteUserList); } void TabRoom::processRoomSayEvent(const Event_RoomSay &event) diff --git a/cockatrice/src/interface/widgets/tabs/tab_room.h b/cockatrice/src/interface/widgets/tabs/tab_room.h index 7d01d5cf6..2881c25f4 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_room.h +++ b/cockatrice/src/interface/widgets/tabs/tab_room.h @@ -14,7 +14,6 @@ #include #include #include -#include class UserListProxy; class UserListManager; @@ -27,7 +26,6 @@ class Message; } // namespace google class AbstractClient; class UserListWidget; -class UserListPanelWidget; class QLabel; class ChatView; class QPushButton; @@ -58,13 +56,13 @@ private: QMap gameTypes; GameSelector *gameSelector; - UserListPanelWidget *userListPanel; + UserListWidget *friendsList; UserListWidget *userList; + UserListWidget *ignoreList; const UserListProxy *userListProxy; ChatView *chatView; QLabel *sayLabel; LineEditCompleter *sayEdit; - QStringListModel *mentionModel; QGroupBox *chatGroupBox; QMenu *roomMenu; @@ -74,7 +72,7 @@ private: [[nodiscard]] QString sanitizeHtml(QString dirty) const; QStringList autocompleteUserList; - QCompleter *mentionCompleter; + QCompleter *completer; signals: void roomClosing(TabRoom *tab); void openMessageDialog(const QString &userName, bool focus); @@ -114,10 +112,6 @@ public: { return roomId; } - [[nodiscard]] QString getRoomName() const - { - return roomName; - } [[nodiscard]] const QMap &getGameTypes() const { return gameTypes; diff --git a/cockatrice/src/interface/widgets/tabs/tab_server.cpp b/cockatrice/src/interface/widgets/tabs/tab_server.cpp index fca32094c..13a77e957 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_server.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_server.cpp @@ -13,7 +13,6 @@ #include #include #include -#include #include #include @@ -186,37 +185,25 @@ void TabServer::processServerMessageEvent(const Event_ServerMessage &event) void TabServer::joinRoom(int id, bool setCurrent) { TabRoom *room = tabSupervisor->getRoomTabs().value(id); - if (room) { - if (setCurrent) { - tabSupervisor->setCurrentWidget((QWidget *)room); - } + if (!room) { + Command_JoinRoom cmd; + cmd.set_room_id(id); + + PendingCommand *pend = client->prepareSessionCommand(cmd); + pend->setExtraData(setCurrent); + connect(pend, &PendingCommand::finished, this, + [this, id](const Response &r, const CommandContainer &c, const QVariant &v) { + joinRoomFinished(r, c, v, id); + }); + + client->sendCommand(pend); + return; } - auto pendingIt = pendingRoomJoins.find(id); - if (pendingIt != pendingRoomJoins.end()) { - // A join for this room is already in flight: the room tab opens when its response - // arrives. Fold the new request into the pending one so that, for example, clicking - // a room the selector is auto-joining does not send a second Command_JoinRoom - the - // server would reject that duplicate with RespContextError. - if (setCurrent) { - pendingIt.value() = true; - } - return; + if (setCurrent) { + tabSupervisor->setCurrentWidget((QWidget *)room); } - - pendingRoomJoins.insert(id, setCurrent); - - Command_JoinRoom cmd; - cmd.set_room_id(id); - - PendingCommand *pend = client->prepareSessionCommand(cmd); - pend->setExtraData(setCurrent); - connect( - pend, &PendingCommand::finished, this, - [this, id](const Response &r, const CommandContainer &c, const QVariant &v) { joinRoomFinished(r, c, v, id); }); - - client->sendCommand(pend); } void TabServer::joinRoomFinished(const Response &r, @@ -224,72 +211,34 @@ void TabServer::joinRoomFinished(const Response &r, const QVariant &extraData, int roomId) { - const bool setCurrent = pendingRoomJoins.value(roomId, extraData.toBool()); - pendingRoomJoins.remove(roomId); - const bool healedJoin = healedRoomJoins.contains(roomId); - healedRoomJoins.remove(roomId); - switch (r.response_code()) { case Response::RespOk: break; case Response::RespNameNotFound: - if (setCurrent) { - QMessageBox::critical(this, tr("Error"), - tr("Failed to join the server room: it doesn't exist on the server.")); - } + QMessageBox::critical(this, tr("Error"), + tr("Failed to join the server room: it doesn't exist on the server.")); emit roomJoinFailed(roomId); return; case Response::RespContextError: - if (healedJoin) { - // The rejoin below was already answered and the server still rejects the join, so - // the stale-membership heal cannot help: surface the error. The guard was already - // released above so a later user-initiated join may try a fresh heal. - if (setCurrent) { - QMessageBox::critical( - this, tr("Error"), - tr("The server thinks you are in the server room but your client is unable to display it. " - "Try restarting your client.")); - } - emit roomJoinFailed(roomId); - return; - } - // The server already had us registered in the room even though no tab was open, - // usually because two join attempts for the same room overlapped. Leaving and - // rejoining makes the server reply with a fresh RespOk so the tab is displayed - // without requiring a client restart. The guard above covers exactly the rejoin that - // leaveAndRejoinRoom triggers, so a server that keeps replying with RespContextError - // gets one heal attempt per join instead of an endless recursion. - healedRoomJoins.insert(roomId); - leaveAndRejoinRoom(roomId, setCurrent); + QMessageBox::critical( + this, tr("Error"), + tr("The server thinks you are in the server room but your client is unable to display it. " + "Try restarting your client.")); + emit roomJoinFailed(roomId); return; case Response::RespUserLevelTooLow: - if (setCurrent) { - QMessageBox::critical(this, tr("Error"), - tr("You do not have the required permission to join this server room.")); - } + QMessageBox::critical(this, tr("Error"), + tr("You do not have the required permission to join this server room.")); emit roomJoinFailed(roomId); return; default: - if (setCurrent) { - QMessageBox::critical( - this, tr("Error"), - tr("Failed to join the server room due to an unknown error: %1.").arg(r.response_code())); - } + QMessageBox::critical( + this, tr("Error"), + tr("Failed to join the server room due to an unknown error: %1.").arg(r.response_code())); emit roomJoinFailed(roomId); return; } const Response_JoinRoom &resp = r.GetExtension(Response_JoinRoom::ext); - emit roomJoined(resp.room_info(), setCurrent); -} - -void TabServer::leaveAndRejoinRoom(int roomId, bool setCurrent) -{ - // Clear the stale room membership server-side. The leave is sent before the rejoin below, - // so the server no longer considers us a member by the time the join arrives. The leave - // response is intentionally not awaited: commands are processed in send order on the - // connection, and a failed leave (RespNotInRoom) only means the membership was already gone. - client->sendCommand(client->prepareRoomCommand(Command_LeaveRoom(), roomId)); - - joinRoom(roomId, setCurrent); + emit roomJoined(resp.room_info(), extraData.toBool()); } diff --git a/cockatrice/src/interface/widgets/tabs/tab_server.h b/cockatrice/src/interface/widgets/tabs/tab_server.h index 121ff814d..c10b7945b 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_server.h +++ b/cockatrice/src/interface/widgets/tabs/tab_server.h @@ -10,8 +10,6 @@ #include "tab.h" #include -#include -#include #include #include @@ -60,17 +58,10 @@ private slots: int roomId); private: - void leaveAndRejoinRoom(int roomId, bool setCurrent); - AbstractClient *client; RoomSelector *roomSelector; QTextBrowser *serverInfoBox; bool shouldEmitUpdate = false; - /** Room ids with a join command in flight, mapped to whether the tab should be focused once it opens. */ - QHash pendingRoomJoins; - /** Room ids for which a stale-membership heal (leave + rejoin) is currently in flight. Released as soon as the - * rejoin has been answered, so a heal is attempted at most once per join. */ - QSet healedRoomJoins; public: TabServer(TabSupervisor *_tabSupervisor, AbstractClient *_client); diff --git a/cockatrice/src/interface/widgets/tabs/tab_supervisor.cpp b/cockatrice/src/interface/widgets/tabs/tab_supervisor.cpp index c9bda7703..07774770e 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_supervisor.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_supervisor.cpp @@ -2,9 +2,7 @@ #include "../../../client/settings/cache_settings.h" #include "../../../client/settings/shortcuts_settings.h" -#include "../../intents/intent_join_server_game.h" #include "../interface/pixel_map_generator.h" -#include "../interface/widgets/server/game_link.h" #include "../interface/widgets/server/user/user_list_manager.h" #include "../interface/widgets/server/user/user_list_widget.h" #include "../main.h" @@ -15,15 +13,11 @@ #include "tab_card_art_rules.h" #include "tab_deck_editor.h" #include "tab_deck_storage.h" -#include "tab_developer.h" #include "tab_game.h" #include "tab_home.h" #include "tab_logs.h" #include "tab_message.h" -#include "tab_moderation.h" -#include "tab_public_decks.h" #include "tab_replays.h" -#include "tab_report.h" #include "tab_room.h" #include "tab_server.h" #include "tab_visual_database_display.h" @@ -36,7 +30,6 @@ #include #include #include -#include #include #include #include @@ -45,7 +38,6 @@ #include #include #include -#include #include #include #include @@ -118,10 +110,9 @@ void CloseButton::paintEvent(QPaintEvent * /*event*/) } TabSupervisor::TabSupervisor(AbstractClient *_client, QMenu *tabsMenu, QWidget *parent) - : 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), tabDeveloper(nullptr), isLocalGame(false) + : QTabWidget(parent), userInfo(nullptr), client(_client), tabsMenu(tabsMenu), tabVisualDeckStorage(nullptr), + tabServer(nullptr), tabAccount(nullptr), tabDeckStorage(nullptr), tabReplays(nullptr), tabAdmin(nullptr), + tabLog(nullptr), isLocalGame(false) { setElideMode(Qt::ElideRight); setMovable(true); @@ -144,7 +135,6 @@ TabSupervisor::TabSupervisor(AbstractClient *_client, QMenu *tabsMenu, QWidget * connect(client, &AbstractClient::gameJoinedEventReceived, this, &TabSupervisor::gameJoined); connect(client, &AbstractClient::userMessageEventReceived, this, &TabSupervisor::processUserMessageEvent); connect(client, &AbstractClient::maxPingTime, this, &TabSupervisor::updatePingTime); - connect(client, &AbstractClient::pingStatsUpdated, this, &TabSupervisor::updateLatencyTooltip); connect(client, &AbstractClient::notifyUserEventReceived, this, &TabSupervisor::processNotifyUserEvent); // create tabs menu actions @@ -199,18 +189,6 @@ TabSupervisor::TabSupervisor(AbstractClient *_client, QMenu *tabsMenu, QWidget * aTabLog->setCheckable(true); connect(aTabLog, &QAction::triggered, this, &TabSupervisor::actTabLog); - aTabReport = new QAction(this); - aTabReport->setCheckable(true); - connect(aTabReport, &QAction::triggered, this, &TabSupervisor::actTabReport); - - aTabModeration = new QAction(this); - aTabModeration->setCheckable(true); - connect(aTabModeration, &QAction::triggered, this, &TabSupervisor::actTabModeration); - - aTabDeveloper = new QAction(this); - aTabDeveloper->setCheckable(true); - connect(aTabDeveloper, &QAction::triggered, this, &TabSupervisor::actTabDeveloper); - connect(&SettingsCache::instance().shortcuts(), &ShortcutsSettings::shortCutChanged, this, &TabSupervisor::refreshShortcuts); refreshShortcuts(); @@ -250,10 +228,6 @@ void TabSupervisor::retranslateUi() aTabReplays->setText(tr("Game Replays")); aTabAdmin->setText(tr("Administration")); aTabLog->setText(tr("Logs")); - aTabReport->setText(tr("Report Queue")); - aTabModeration->setText(tr("Moderation")); - aTabCardArtRules->setText(tr("Card Art Rules")); - aTabDeveloper->setText(tr("Developer")); // tabs QList tabs; @@ -263,10 +237,6 @@ void TabSupervisor::retranslateUi() tabs.append(tabAdmin); tabs.append(tabAccount); tabs.append(tabLog); - tabs.append(tabReport); - tabs.append(tabModeration); - tabs.append(tabCardArtRules); - tabs.append(tabDeveloper); QMapIterator roomIterator(roomTabs); while (roomIterator.hasNext()) { tabs.append(roomIterator.next().value()); @@ -275,10 +245,6 @@ void TabSupervisor::retranslateUi() while (gameIterator.hasNext()) { tabs.append(gameIterator.next().value()); } - QMapIterator publicDecksIterator(publicDecksTabs); - while (publicDecksIterator.hasNext()) { - tabs.append(publicDecksIterator.next().value()); - } QListIterator replayIterator(replayTabs); while (replayIterator.hasNext()) { tabs.append(replayIterator.next()); @@ -317,8 +283,6 @@ void TabSupervisor::refreshShortcuts() aTabReplays->setShortcuts(shortcuts.getShortcut("Tabs/aTabReplays")); aTabAdmin->setShortcuts(shortcuts.getShortcut("Tabs/aTabAdmin")); aTabLog->setShortcuts(shortcuts.getShortcut("Tabs/aTabLog")); - aTabReport->setShortcuts(shortcuts.getShortcut("Tabs/aTabReport")); - aTabModeration->setShortcuts(shortcuts.getShortcut("Tabs/aTabModeration")); } void TabSupervisor::closeEvent(QCloseEvent *event) @@ -371,12 +335,7 @@ static void checkAndTrigger(QAction *checkableAction, bool checked) } /** - * Opens the always-available tabs, depending on settings, and lands on the configured startup tab. - * - * The startup destination is a request: tabs that were not open before (deck editors, storage - * tabs disabled in the Tabs menu) are opened as part of the startup flow. Destinations that - * require a server connection (Server, Server Room) are handled asynchronously by MainWindow - * through the intent system, since this class has no RemoteClient. + * Opens the always-available tabs, depending on settings. */ void TabSupervisor::initStartupTabs() { @@ -392,42 +351,6 @@ void TabSupervisor::initStartupTabs() if (SettingsCache::instance().tabs().getTabReplaysOpen()) { openTabReplays(); } - - switch (SettingsCache::instance().tabs().getStartupTabIndex()) { - case StartupTab::StartupTabVisualDeckStorage: - if (!tabVisualDeckStorage) { - openTabVisualDeckStorage(); - } - setCurrentWidget(tabVisualDeckStorage); - break; - case StartupTab::StartupTabDeckStorage: - if (!tabDeckStorage) { - openTabDeckStorage(); - } - setCurrentWidget(tabDeckStorage); - break; - case StartupTab::StartupTabReplays: - if (!tabReplays) { - openTabReplays(); - } - setCurrentWidget(tabReplays); - break; - case StartupTab::StartupTabDeckEditor: - addDeckEditorTab(LoadedDeck()); - break; - case StartupTab::StartupTabVisualDeckEditor: - addVisualDeckEditorTab(LoadedDeck()); - break; - case StartupTab::StartupTabServer: - case StartupTab::StartupTabServerRoom: - // Handled asynchronously by MainWindow::applyStartupDestination(); Home stays selected - // until the server connection succeeds. - break; - case StartupTab::StartupTabHome: - default: - setCurrentWidget(tabHome); - break; - } } /** @@ -442,7 +365,6 @@ int TabSupervisor::myAddTab(Tab *tab, QAction *manager) { connect(tab, &TabGame::userEvent, this, &TabSupervisor::tabUserEvent); connect(tab, &TabGame::tabTextChanged, this, &TabSupervisor::updateTabText); - connect(tab, &TabGame::cockatriceLinkActivated, this, &TabSupervisor::cockatriceLinkActivated); QString tabText = tab->getTabText(); int idx = addTab(tab, sanitizeTabName(tabText)); @@ -520,8 +442,6 @@ void TabSupervisor::start(const ServerInfo_User &_userInfo) tabsMenu->addAction(aTabAdmin); tabsMenu->addAction(aTabLog); tabsMenu->addAction(aTabCardArtRules); - tabsMenu->addAction(aTabReport); - tabsMenu->addAction(aTabModeration); if (SettingsCache::instance().tabs().getTabAdminOpen()) { openTabAdmin(); @@ -529,28 +449,7 @@ void TabSupervisor::start(const ServerInfo_User &_userInfo) if (SettingsCache::instance().tabs().getTabLogOpen()) { openTabLog(); } - if (SettingsCache::instance().tabs().getTabReportOpen()) { - openTabReport(); - } - if (SettingsCache::instance().tabs().getTabModerationOpen()) { - openTabModeration(); - } - if (SettingsCache::instance().tabs().getTabCardArtRulesOpen()) { - openTabCardArtRules(); - } - } - - if (userInfo->user_level() & ServerInfo_User::IsDeveloper) { - tabsMenu->addSeparator(); - tabsMenu->addAction(aTabDeveloper); - // Developers without moderation rights get log access through their - // own role. Moderators already have the Logs entry from above. - if (!(userInfo->user_level() & ServerInfo_User::IsModerator)) { - tabsMenu->addAction(aTabLog); - if (SettingsCache::instance().tabs().getTabLogOpen()) { - openTabLog(); - } - } + openTabCardArtRules(); } retranslateUi(); @@ -563,9 +462,6 @@ void TabSupervisor::startLocal(const QList &_clients) tabAccount = nullptr; tabAdmin = nullptr; tabLog = nullptr; - tabReport = nullptr; - tabModeration = nullptr; - tabDeveloper = nullptr; isLocalGame = true; userInfo = new ServerInfo_User; localClients = _clients; @@ -607,18 +503,6 @@ void TabSupervisor::stop() if (tabLog) { tabLog->close(); } - if (tabReport) { - tabReport->close(); - } - if (tabModeration) { - tabModeration->close(); - } - if (tabCardArtRules) { - tabCardArtRules->close(); - } - if (tabDeveloper) { - tabDeveloper->close(); - } } QList tabsToDelete; @@ -631,10 +515,6 @@ void TabSupervisor::stop() tabsToDelete << i.value(); } - for (auto i = publicDecksTabs.cbegin(), end = publicDecksTabs.cend(); i != end; ++i) { - tabsToDelete << i.value(); - } - for (const auto tab : tabsToDelete) { tab->close(); } @@ -681,7 +561,7 @@ void TabSupervisor::actTabVisualDeckStorage(bool checked) void TabSupervisor::openTabVisualDeckStorage() { - tabVisualDeckStorage = new TabDeckStorageVisual(this, client); + tabVisualDeckStorage = new TabDeckStorageVisual(this); myAddTab(tabVisualDeckStorage, aTabVisualDeckStorage); connect(tabVisualDeckStorage, &QObject::destroyed, this, [this] { tabVisualDeckStorage = nullptr; @@ -703,7 +583,6 @@ void TabSupervisor::actTabServer(bool checked) void TabSupervisor::openTabServer() { - SettingsCache::instance().tabs().setTabServerOpen(true); if (tabServer) { return; } @@ -816,7 +695,6 @@ void TabSupervisor::openTabAdmin() void TabSupervisor::actTabCardArtRules(bool checked) { - SettingsCache::instance().tabs().setTabCardArtRulesOpen(checked); if (checked && !tabCardArtRules) { openTabCardArtRules(); setCurrentWidget(tabCardArtRules); @@ -852,13 +730,7 @@ void TabSupervisor::actTabLog(bool checked) void TabSupervisor::openTabLog() { - // Developers query logs through the developer command family, so tell the - // tab which family to use. The moderator family is strictly stronger, so a - // moderator who also holds the developer bit keeps the moderator path — the - // developer bit only selects the (narrowed) developer family on its own. - const bool useDeveloperCommands = (userInfo->user_level() & ServerInfo_User::IsDeveloper) && - !(userInfo->user_level() & ServerInfo_User::IsModerator); - tabLog = new TabLog(this, client, useDeveloperCommands); + tabLog = new TabLog(this, client); myAddTab(tabLog, aTabLog); connect(tabLog, &QObject::destroyed, this, [this] { tabLog = nullptr; @@ -867,80 +739,6 @@ void TabSupervisor::openTabLog() aTabLog->setChecked(true); } -void TabSupervisor::actTabReport(bool checked) -{ - SettingsCache::instance().tabs().setTabReportOpen(checked); - if (checked && !tabReport) { - openTabReport(); - setCurrentWidget(tabReport); - } else if (!checked && tabReport) { - tabReport->closeRequest(); - } -} - -void TabSupervisor::openTabReport() -{ - tabReport = new TabReport(this, client); - myAddTab(tabReport, aTabReport); - connect(tabReport, &TabReport::openReplay, this, &TabSupervisor::openReplay); - connect(tabReport, &TabReport::requestJoinGame, this, &TabSupervisor::joinReportGame); - connect(tabReport, &QObject::destroyed, this, [this] { - tabReport = nullptr; - aTabReport->setChecked(false); - }); - aTabReport->setChecked(true); -} - -void TabSupervisor::actTabModeration(bool checked) -{ - SettingsCache::instance().tabs().setTabModerationOpen(checked); - if (checked && !tabModeration) { - openTabModeration(); - setCurrentWidget(tabModeration); - } else if (!checked && tabModeration) { - tabModeration->closeRequest(); - } -} - -void TabSupervisor::openTabModeration(const QString &userName) -{ - if (tabModeration) { - setCurrentWidget(tabModeration); - if (!userName.isEmpty()) { - tabModeration->investigate(userName); - } - return; - } - tabModeration = new TabModeration(this, client, userName); - myAddTab(tabModeration, aTabModeration); - connect(tabModeration, &QObject::destroyed, this, [this] { - tabModeration = nullptr; - aTabModeration->setChecked(false); - }); - aTabModeration->setChecked(true); -} - -void TabSupervisor::actTabDeveloper(bool checked) -{ - if (checked && !tabDeveloper) { - openTabDeveloper(); - setCurrentWidget(tabDeveloper); - } else if (!checked && tabDeveloper) { - tabDeveloper->closeRequest(); - } -} - -void TabSupervisor::openTabDeveloper() -{ - tabDeveloper = new TabDeveloper(this, client); - myAddTab(tabDeveloper, aTabDeveloper); - connect(tabDeveloper, &QObject::destroyed, this, [this] { - tabDeveloper = nullptr; - aTabDeveloper->setChecked(false); - }); - aTabDeveloper->setChecked(true); -} - void TabSupervisor::updatePingTime(int value, int max) { if (!tabServer) { @@ -953,23 +751,6 @@ void TabSupervisor::updatePingTime(int value, int max) setTabIcon(indexOf(tabServer), QIcon(PingPixmapGenerator::generatePixmap(15, value, max))); } -void TabSupervisor::updateLatencyTooltip(const LatencyTracker::Stats &stats) -{ - if (!tabServer) { - return; - } - - if (stats.sampleCount == 0) { - setTabToolTip(indexOf(tabServer), QString()); - return; - } - - setTabToolTip(indexOf(tabServer), - tr("Connection quality over the last %n sample(s):", "", stats.sampleCount) + "\n" + - tr("Last: %1 ms").arg(stats.lastMs) + "\n" + tr("Median: %1 ms").arg(stats.medianMs) + "\n" + - tr("95th percentile: %1 ms").arg(stats.p95Ms) + "\n" + tr("Maximum: %1 ms").arg(stats.maxMs)); -} - void TabSupervisor::gameJoined(const Event_GameJoined &event) { QMap roomGameTypes; @@ -1028,7 +809,6 @@ void TabSupervisor::addRoomTab(const ServerInfo_Room &info, bool setCurrent) connect(tab, &TabRoom::maximizeClient, this, &TabSupervisor::maximizeMainWindow); connect(tab, &TabRoom::roomClosing, this, &TabSupervisor::roomLeft); connect(tab, &TabRoom::openMessageDialog, this, &TabSupervisor::addMessageTab); - connect(tab, &TabRoom::cockatriceLinkActivated, this, &TabSupervisor::cockatriceLinkActivated); myAddTab(tab); roomTabs.insert(info.room_id(), tab); if (setCurrent) { @@ -1046,30 +826,6 @@ void TabSupervisor::roomLeft(TabRoom *tab) removeTab(indexOf(tab)); } -void TabSupervisor::openTabPublicDecks(const QString &userName) -{ - if (auto *existing = publicDecksTabs.value(userName, nullptr)) { - setCurrentWidget(existing); - return; - } - - auto *tab = new TabPublicDecks(this, client, userName); - connect(tab, &TabPublicDecks::closing, this, &TabSupervisor::publicDecksClosed); - myAddTab(tab); - publicDecksTabs.insert(userName, tab); - setCurrentWidget(tab); -} - -void TabSupervisor::publicDecksClosed(TabPublicDecks *tab) -{ - if (tab == currentWidget()) { - emit setMenu(); - } - - publicDecksTabs.remove(tab->getUserName()); - removeTab(indexOf(tab)); -} - void TabSupervisor::switchToFirstAvailableNetworkTab() { if (!roomTabs.isEmpty()) { @@ -1099,30 +855,6 @@ void TabSupervisor::replayLeft(TabGame *tab) replayTabs.removeOne(tab); } -void TabSupervisor::joinReportGame(const int gameId, const int roomId) -{ - auto *remoteClient = qobject_cast(client); - if (!remoteClient) { - actShowPopup(tr("Report joins are only available on a remote server.")); - return; - } - - auto ctx = std::make_unique(); - ctx->roomContext.serverContext.hostname = remoteClient->peerName(); - ctx->roomContext.serverContext.port = QString::number(remoteClient->peerPort()); - ctx->roomContext.roomId = roomId; - ctx->gameId = gameId; - ctx->asSpectator = true; - - auto *joinGameIntent = new IntentJoinServerGame(this, remoteClient, std::move(ctx)); - joinGameIntent->setParent(this); - connect(joinGameIntent, &Intent::failed, this, [gameId](const QString &reason) { - actShowPopup(tr("Could not join game %1.\n%2").arg(gameId).arg(reason)); - }); - - joinGameIntent->execute(); -} - TabMessage *TabSupervisor::addMessageTab(const QString &receiverName, bool focus) { if (receiverName == QString::fromStdString(userInfo->name())) { @@ -1130,10 +862,8 @@ TabMessage *TabSupervisor::addMessageTab(const QString &receiverName, bool focus } ServerInfo_User otherUser; - bool userOnline = false; if (auto user = userListManager->getOnlineUser(receiverName)) { otherUser = ServerInfo_User(*user); - userOnline = true; } else { otherUser.set_name(receiverName.toStdString()); } @@ -1147,17 +877,9 @@ TabMessage *TabSupervisor::addMessageTab(const QString &receiverName, bool focus return tab; } - if (focus && userListManager->isUserIgnored(receiverName)) { - QMessageBox::information( - this, tr("Ignored user"), - tr("You have ignored %1. Remove them from your ignore list to open a private chat.").arg(receiverName)); - return nullptr; - } - - tab = new TabMessage(this, client, *userInfo, otherUser, userOnline); + tab = new TabMessage(this, client, *userInfo, otherUser); connect(tab, &TabMessage::talkClosing, this, &TabSupervisor::talkLeft); connect(tab, &TabMessage::maximizeClient, this, &TabSupervisor::maximizeMainWindow); - connect(tab, &TabMessage::cockatriceLinkActivated, this, &TabSupervisor::cockatriceLinkActivated); myAddTab(tab); messageTabs.insert(receiverName, tab); if (focus) { @@ -1181,54 +903,6 @@ void TabSupervisor::talkLeft(TabMessage *tab) removeTab(indexOf(tab)); } -QList TabSupervisor::getGameInviteLinksForRoom(int roomId) const -{ - QList options; - if (isLocalGame) { - return options; - } - - // The inviter may be in several games of the same room (hosting one and - // spectating another, for example). Return every game so the caller can - // let the user choose which one to invite to. - for (auto it = gameTabs.cbegin(); it != gameTabs.cend(); ++it) { - TabGame *tab = it.value(); - GameMetaInfo *metaInfo = tab->getGame()->getGameMetaInfo(); - if (metaInfo->proto().room_id() != roomId) { - continue; - } - // A closed game is a dead end — drop it. Started/full games stay - // listed: an invite to them is a legitimate "come spectate" offer. - if (metaInfo->proto().closed()) { - continue; - } - - const int gameId = metaInfo->gameId(); - const QString description = QString::fromStdString(metaInfo->proto().description()); - - GameInviteOption option{ - .gameId = gameId, - .label = - description.isEmpty() ? tr("Game #%1").arg(gameId) : tr("Game #%1 — %2").arg(gameId).arg(description), - .url = makeGameJoinLink(client->serverName(), client->serverPort(), roomId, gameId, description), - .description = description, - .onlyBuddies = metaInfo->proto().only_buddies(), - .creatorName = QString::fromStdString(metaInfo->proto().creator_info().name()), - }; - options.append(option); - } - - return options; -} - -void TabSupervisor::sendInviteToUser(const QString &userName, const QString &inviteText) -{ - TabMessage *tab = addMessageTab(userName, true); - if (tab) { - tab->sendInviteMessage(inviteText); - } -} - /** * Creates a new deck editor tab and loads the deck into it. * Creates either a classic or visual deck editor tab depending on settings @@ -1333,7 +1007,7 @@ void TabSupervisor::tabUserEvent(bool globalEvent) auto *tab = static_cast(sender()); if (tab != currentWidget()) { tab->setContentsChanged(true); - setTabIcon(indexOf(tab), themePixmap(QStringLiteral("icons/tab_changed"))); + setTabIcon(indexOf(tab), QPixmap("theme:icons/tab_changed")); } if (globalEvent && SettingsCache::instance().userInterface().getNotificationsEnabled()) { QApplication::alert(this); @@ -1368,21 +1042,7 @@ void TabSupervisor::processGameEventContainer(const GameEventContainer &cont) void TabSupervisor::processUserMessageEvent(const Event_UserMessage &event) { - // "Ignore all private messages" silences every PM, including messages to - // already-open tabs — unlike the unregistered/non-buddy filters below, - // which only apply when creating a new tab. Messages from moderators/admins - // are exempt to ensure warnings still reach users. QString senderName = QString::fromStdString(event.sender_name()); - if (SettingsCache::instance().chat().getIgnoreAllPrivateMessages()) { - const ServerInfo_User *onlineUserInfo = userListManager->getOnlineUser(senderName); - if (!onlineUserInfo) { - return; - } - const UserLevelFlags userLevel(onlineUserInfo->user_level()); - if (!userLevel.testFlag(ServerInfo_User::IsModerator) && !userLevel.testFlag(ServerInfo_User::IsAdmin)) { - return; - } - } TabMessage *tab = messageTabs.value(senderName); if (!tab) { tab = messageTabs.value(QString::fromStdString(event.receiver_name())); @@ -1484,11 +1144,6 @@ bool TabSupervisor::getAdminLocked() const return tabAdmin->getLocked(); } -bool TabSupervisor::canOverrideGameRestrictions() const -{ - return !getAdminLocked() || (userInfo->user_level() & ServerInfo_User::IsJudge); -} - void TabSupervisor::processNotifyUserEvent(const Event_NotifyUser &event) { @@ -1534,24 +1189,6 @@ void TabSupervisor::processNotifyUserEvent(const Event_NotifyUser &event) } break; } - case Event_NotifyUser::REPORT_RESOLVED: { - QString title = QString::fromStdString(event.custom_title()).simplified(); - QString content = QString::fromStdString(event.custom_content()).trimmed(); - if (!title.isEmpty() && !content.isEmpty()) { - actShowPopup(title + "\n" + content); - QApplication::alert(this); - } - break; - } - case Event_NotifyUser::REPORT_COMMENT: { - QString title = QString::fromStdString(event.custom_title()).simplified(); - QString content = QString::fromStdString(event.custom_content()).trimmed(); - if (!title.isEmpty() && !content.isEmpty()) { - actShowPopup(title + "\n" + content); - QApplication::alert(this); - } - break; - } default:; } } diff --git a/cockatrice/src/interface/widgets/tabs/tab_supervisor.h b/cockatrice/src/interface/widgets/tabs/tab_supervisor.h index 066c84a77..d3c147138 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_supervisor.h +++ b/cockatrice/src/interface/widgets/tabs/tab_supervisor.h @@ -9,7 +9,6 @@ #define TAB_SUPERVISOR_H #include "../../deck_loader/deck_loader.h" -#include "../interface/widgets/server/game_link.h" #include "../interface/widgets/server/user/user_list_proxy.h" #include "abstract_tab_deck_editor.h" #include "api/archidekt/tab_archidekt.h" @@ -24,7 +23,6 @@ #include #include #include -#include class TabCardArtRules; inline Q_LOGGING_CATEGORY(TabSupervisorLog, "tab_supervisor"); @@ -41,13 +39,9 @@ class TabDeckStorage; class TabReplays; class TabAdmin; class TabMessage; -class TabReport; -class TabModeration; class TabAccount; class TabDeckEditor; -class TabDeveloper; class TabLog; -class TabPublicDecks; class RoomEvent; class GameEventContainer; class Event_GameJoined; @@ -108,20 +102,16 @@ private: TabAdmin *tabAdmin; TabCardArtRules *tabCardArtRules; TabLog *tabLog; - TabReport *tabReport; - TabModeration *tabModeration; - TabDeveloper *tabDeveloper; QMap roomTabs; QMap gameTabs; QList replayTabs; QMap messageTabs; - QMap publicDecksTabs; QList deckEditorTabs; bool isLocalGame; QAction *aTabHome, *aTabDeckEditor, *aTabVisualDeckEditor, *aTabEdhRec, *aTabArchidekt, *aTabVisualDeckStorage, *aTabVisualDatabaseDisplay, *aTabServer, *aTabAccount, *aTabDeckStorage, *aTabReplays, *aTabAdmin, - *aTabCardArtRules, *aTabLog, *aTabReport, *aTabModeration, *aTabDeveloper; + *aTabCardArtRules, *aTabLog; int myAddTab(Tab *tab, QAction *manager = nullptr); void addCloseButtonToTab(Tab *tab, int tabIndex, QAction *manager); @@ -154,10 +144,6 @@ public: return userInfo; } [[nodiscard]] AbstractClient *getClient() const; - [[nodiscard]] AbstractClient *getServerClient() const - { - return client; - } [[nodiscard]] UserListManager *getUserListManager() const { return userListManager; @@ -174,10 +160,7 @@ public: { return deckEditorTabs; } - [[nodiscard]] QList getGameInviteLinksForRoom(int roomId) const; - void sendInviteToUser(const QString &userName, const QString &inviteText); [[nodiscard]] bool getAdminLocked() const; - [[nodiscard]] bool canOverrideGameRestrictions() const; void closeEvent(QCloseEvent *event) override; bool switchToGameTabIfAlreadyExists(const int gameId); static void actShowPopup(const QString &message); @@ -186,7 +169,6 @@ signals: void localGameEnded(); void adminLockChanged(bool lock); void showWindowIfHidden(); - void cockatriceLinkActivated(const QString &url); public slots: void openDeckInNewTab(const LoadedDeck &deckToOpen); @@ -197,15 +179,11 @@ public slots: TabArchidekt *addArchidektTab(); TabEdhRec *addEdhrecTab(const CardInfoPtr &cardToQuery, bool isCommander = false); void openReplay(GameReplay *replay); - void joinReportGame(int gameId, int roomId); - void openTabModeration(const QString &userName = {}); void switchToFirstAvailableNetworkTab(); void maximizeMainWindow(); void actTabVisualDeckStorage(bool checked); void actTabReplays(bool checked); void openTabServer(); - void addRoomTab(const ServerInfo_Room &info, bool setCurrent); - void openTabPublicDecks(const QString &userName); private slots: void refreshShortcuts(); @@ -215,9 +193,6 @@ private slots: void actTabDeckStorage(bool checked); void actTabAdmin(bool checked); void actTabLog(bool checked); - void actTabReport(bool checked); - void actTabModeration(bool checked); - void actTabDeveloper(bool checked); void openTabVisualDeckStorage(); void openTabHome(); @@ -228,17 +203,14 @@ private slots: void actTabCardArtRules(bool checked); void openTabCardArtRules(); void openTabLog(); - void openTabReport(); - void openTabDeveloper(); void updateCurrent(int index); void updatePingTime(int value, int max); - void updateLatencyTooltip(const LatencyTracker::Stats &stats); void gameJoined(const Event_GameJoined &event); void localGameJoined(const Event_GameJoined &event); void gameLeft(TabGame *tab); + void addRoomTab(const ServerInfo_Room &info, bool setCurrent); void roomLeft(TabRoom *tab); - void publicDecksClosed(TabPublicDecks *tab); TabMessage *addMessageTab(const QString &userName, bool focus); void replayLeft(TabGame *tab); void processUserLeft(const QString &userName); diff --git a/cockatrice/src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual.cpp b/cockatrice/src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual.cpp index 0f43893d3..0bc927eeb 100644 --- a/cockatrice/src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual.cpp +++ b/cockatrice/src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual.cpp @@ -4,7 +4,6 @@ #include "../../../../client/settings/shortcuts_settings.h" #include "../../cards/card_info_display_widget.h" #include "../../deck_editor/deck_state_manager.h" -#include "../../deck_editor/deck_zone_dialog.h" #include "../../filters/filter_builder.h" #include "../../interface/pixel_map_generator.h" #include "../../interface/widgets/cards/card_info_frame_widget.h" @@ -28,10 +27,10 @@ #include #include #include +#include #include #include #include -#include #include /** @@ -68,6 +67,9 @@ void TabDeckEditorVisual::createCentralFrame() centralFrame = new QVBoxLayout; centralWidget->setLayout(centralFrame); + auto databaseModel = new CardDatabaseModel(CardDatabaseManager::getInstance(), true, this); + databaseModel->setObjectName("databaseModel"); + tabContainer = new TabDeckEditorVisualTabWidget(centralWidget, this, deckStateManager->getModel(), databaseModel); connect(tabContainer, &TabDeckEditorVisualTabWidget::cardChanged, this, @@ -85,7 +87,6 @@ void TabDeckEditorVisual::createCentralFrame() connect(tabContainer, &TabDeckEditorVisualTabWidget::printingSelectorRequested, this, &TabDeckEditorVisual::showPrintingSelector); connect(tabContainer, &TabDeckEditorVisualTabWidget::cardInfoRequested, this, &TabDeckEditorVisual::updateCardInfo); - tabContainer->visualDatabaseDisplay->setNewZoneCreator([this] { return createNewZone(); }); centralFrame->addWidget(tabContainer); setCentralWidget(centralWidget); @@ -101,33 +102,6 @@ void TabDeckEditorVisual::onDeckChanged() tabContainer->sampleHandWidget->setDeckModel(deckStateManager->getModel()); } -/** @brief Sets the deck and selects the startup sub-tab matching the context. */ -void TabDeckEditorVisual::setDeck(const LoadedDeck &_deck) -{ - AbstractTabDeckEditor::setDeck(_deck); - - int startupTab = SettingsCache::instance().deckEditor().getVdeStartupTab(); - if (startupTab == VdeStartupTabContext) { - // New (empty) decks open on the database display so cards can be added - // right away. Existing decks open on the deck view. - startupTab = _deck.isEmpty() ? VdeStartupTabDatabaseDisplay : VdeStartupTabDeckDisplay; - } - - switch (startupTab) { - case VdeStartupTabDatabaseDisplay: - tabContainer->setCurrentIndex(TabDeckEditorVisualTabWidget::TabIndex::VisualDatabaseDisplay); - break; - case VdeStartupTabDeckDisplay: - tabContainer->setCurrentIndex(TabDeckEditorVisualTabWidget::TabIndex::VisualDeckView); - break; - default: - qCWarning(TabSupervisorLog) << "Unknown VdeStartupTab [" << startupTab - << "]; falling back to the deck view"; - tabContainer->setCurrentIndex(TabDeckEditorVisualTabWidget::TabIndex::VisualDeckView); - break; - } -} - /** @brief Creates menus for deck editing and view options, including dock actions. */ void TabDeckEditorVisual::createMenus() { @@ -271,19 +245,6 @@ bool TabDeckEditorVisual::actSaveDeckAs() return result; } -/** @brief Prompts for and creates a new custom deck zone. Returns the name of the created zone. */ -QString TabDeckEditorVisual::createNewZone() -{ - QString boardName; - const QString zoneName = DeckZoneDialog::promptForNewZone(this, {}, &boardName, [this](const QString &candidate) { - return deckStateManager->validateNewZoneName(candidate); - }); - if (!zoneName.isEmpty()) { - deckStateManager->createCustomZone(boardName, zoneName); - } - return zoneName; -} - /** @brief Refreshes keyboard shortcuts for this tab from settings. */ void TabDeckEditorVisual::refreshShortcuts() { diff --git a/cockatrice/src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual.h b/cockatrice/src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual.h index fb09578c4..7d7a3f3a2 100644 --- a/cockatrice/src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual.h +++ b/cockatrice/src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual.h @@ -164,21 +164,6 @@ public slots: * @return true if successful, false otherwise. */ bool actSaveDeckAs() override; - - /** - * @brief Prompts for and creates a new custom deck zone. - * @return The name of the created zone, or an empty string if creation was cancelled. - */ - QString createNewZone(); - -private: - /** - * @brief Sets the deck for this tab and selects the sub-tab to open on - * startup, per the "Visual deck editor startup tab" setting (Context / - * Deck display / Database display). - * @param _deck The deck object. - */ - void setDeck(const LoadedDeck &_deck) override; }; #endif diff --git a/cockatrice/src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual_tab_widget.cpp b/cockatrice/src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual_tab_widget.cpp index 5ccfcc28f..2ee560859 100644 --- a/cockatrice/src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual_tab_widget.cpp +++ b/cockatrice/src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual_tab_widget.cpp @@ -25,8 +25,7 @@ TabDeckEditorVisualTabWidget::TabDeckEditorVisualTabWidget(QWidget *parent, layout = new QVBoxLayout(this); setLayout(layout); - visualDeckView = new VisualDeckEditorWidget(this, deckModel, _deckEditor->deckDockWidget->getSelectionModel(), - _cardDatabaseModel); + visualDeckView = new VisualDeckEditorWidget(this, deckModel, _deckEditor->deckDockWidget->getSelectionModel()); visualDeckView->setObjectName("visualDeckView"); connect(visualDeckView, &VisualDeckEditorWidget::activeCardChanged, this, &TabDeckEditorVisualTabWidget::onCardChanged); diff --git a/cockatrice/src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual_tab_widget.h b/cockatrice/src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual_tab_widget.h index 4f04b51f6..2aabbb26a 100644 --- a/cockatrice/src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual_tab_widget.h +++ b/cockatrice/src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual_tab_widget.h @@ -49,17 +49,6 @@ class TabDeckEditorVisualTabWidget : public QTabWidget Q_OBJECT public: - /** - * @brief Sub-tab order in the container; addNewTab() is called in this order. - */ - enum TabIndex - { - VisualDeckView, - VisualDatabaseDisplay, - DeckAnalytics, - SampleHand, - }; - /** * @brief Construct the tab widget with required models. * @param parent Parent widget. diff --git a/cockatrice/src/interface/widgets/tabs/visual_deck_storage/tab_deck_storage_visual.cpp b/cockatrice/src/interface/widgets/tabs/visual_deck_storage/tab_deck_storage_visual.cpp index 03df76b03..0cbcb641a 100644 --- a/cockatrice/src/interface/widgets/tabs/visual_deck_storage/tab_deck_storage_visual.cpp +++ b/cockatrice/src/interface/widgets/tabs/visual_deck_storage/tab_deck_storage_visual.cpp @@ -1,78 +1,25 @@ #include "tab_deck_storage_visual.h" -#include "../../../../client/settings/cache_settings.h" -#include "../../../deck_loader/deck_loader.h" -#include "../../cards/additional_info/deck_color_identity.h" -#include "../../deck_share/deck_share_utils.h" #include "../../interface/widgets/visual_deck_storage/visual_deck_storage_widget.h" #include "../tab_supervisor.h" #include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include +#include -TabDeckStorageVisual::TabDeckStorageVisual(TabSupervisor *_tabSupervisor, AbstractClient *_client) - : Tab(_tabSupervisor), visualDeckStorageWidget(new VisualDeckStorageWidget(this)), client(_client), - shareTimeoutTimer(new QTimer(this)) +TabDeckStorageVisual::TabDeckStorageVisual(TabSupervisor *_tabSupervisor) + : Tab(_tabSupervisor), visualDeckStorageWidget(new VisualDeckStorageWidget(this)) { connect(this, &TabDeckStorageVisual::openDeckEditor, tabSupervisor, &TabSupervisor::openDeckInNewTab); connect(visualDeckStorageWidget, &VisualDeckStorageWidget::deckLoadRequested, this, &TabDeckStorageVisual::actOpenLocalDeck); connect(visualDeckStorageWidget, &VisualDeckStorageWidget::openDeckEditor, this, &TabDeckStorageVisual::openDeckEditor); - connect(visualDeckStorageWidget, &VisualDeckStorageWidget::shareDeckRequested, this, - &TabDeckStorageVisual::actShareDeck); - connect(visualDeckStorageWidget, &VisualDeckStorageWidget::shareSelectionChanged, this, - &TabDeckStorageVisual::onShareSelectionChanged); - connect(visualDeckStorageWidget, &VisualDeckStorageWidget::shareRequested, this, [this] { - if (shareDeckAvailable) { - enterShareMode(); - } - }); auto *widget = new QWidget(this); auto *layout = new QVBoxLayout(widget); - layout->setContentsMargins(0, 0, 0, 0); - layout->setSpacing(0); widget->setLayout(layout); this->setCentralWidget(widget); layout->addWidget(visualDeckStorageWidget); - - shareBar = new ShareBarWidget(this); - connect(shareBar, &ShareBarWidget::createRequested, this, &TabDeckStorageVisual::actShareSelected); - connect(shareBar, &ShareBarWidget::cancelRequested, this, &TabDeckStorageVisual::exitShareMode); - - layout->insertWidget(0, shareBar); - shareBar->setVisible(false); - - connect(client, &AbstractClient::statusChanged, this, &TabDeckStorageVisual::handleConnectionChanged); - shareDeckAvailable = (client->getStatus() == StatusLoggedIn); - visualDeckStorageWidget->setShareAvailable(shareDeckAvailable); - - shareTimeoutTimer->setSingleShot(true); - shareTimeoutTimer->setInterval( - static_cast((static_cast(SettingsCache::instance().network().getTimeOut()) + 1) * - SettingsCache::instance().network().getKeepAlive() * 1000)); - connect(shareTimeoutTimer, &QTimer::timeout, this, &TabDeckStorageVisual::onShareTimeout); - - retranslateUi(); -} - -void TabDeckStorageVisual::retranslateUi() -{ - visualDeckStorageWidget->retranslateUi(); - shareBar->retranslateUi(); - if (shareBar->isVisible()) { - updateShareHint(); - } } void TabDeckStorageVisual::actOpenLocalDeck(const QString &filePath) @@ -86,144 +33,3 @@ void TabDeckStorageVisual::actOpenLocalDeck(const QString &filePath) emit openDeckEditor(deckOpt.value()); } - -void TabDeckStorageVisual::enterShareMode(const QStringList &preselectFiles) -{ - if (!shareDeckAvailable) { - return; // sharing is gated on being logged in - } - shareBar->setCreateEnabled(true); - visualDeckStorageWidget->setShareSelectable(true); - visualDeckStorageWidget->setShareSelectedFiles(preselectFiles); - shareBar->setName(tr("Shared decks")); - shareBar->setVisible(true); - updateShareHint(); - shareBar->focusName(); -} - -void TabDeckStorageVisual::exitShareMode() -{ - // Abandon any in-flight request: otherwise the timer keeps running and a late - // response reports the share as created after the user already backed out. - shareTimeoutTimer->stop(); - shareInFlightSeq = 0; - visualDeckStorageWidget->setShareSelectable(false); - visualDeckStorageWidget->clearShareSelection(); - shareBar->setVisible(false); -} - -void TabDeckStorageVisual::actShareDeck(const QString &filePath) -{ - if (!shareDeckAvailable) { - QMessageBox::information(this, tr("Share deck"), tr("You must be connected to the server to share a deck.")); - return; - } - enterShareMode({filePath}); -} - -void TabDeckStorageVisual::onShareSelectionChanged() -{ - if (shareBar->isVisible()) { - updateShareHint(); - } -} - -void TabDeckStorageVisual::updateShareHint() -{ - const int count = visualDeckStorageWidget->selectedFilePaths().size(); - shareBar->setCountText(tr("%n deck(s)", "", count)); - if (count == 0) { - shareBar->setHintText(tr("Click deck tiles to select the decks you want to share."), true); - } else if (count == 1) { - shareBar->setHintText(tr("One deck selected. Create the link to share it with other players."), true); - } else { - shareBar->setHintText(tr("%n decks selected. Create the link to share them with other players.", "", count), - true); - } -} - -void TabDeckStorageVisual::actShareSelected() -{ - const QStringList filePaths = visualDeckStorageWidget->selectedFilePaths(); - if (filePaths.isEmpty()) { - QMessageBox::warning(this, tr("Share decks"), tr("Select at least one deck to share.")); - return; - } - - Command_DeckShareCreate cmd; - cmd.set_name(shareBar->name().toStdString()); - if (cmd.name().empty()) { - cmd.set_name(tr("Shared decks").toStdString()); - } - - for (const QString &filePath : filePaths) { - std::optional deckOpt = - DeckLoader::loadFromFile(filePath, DeckFileFormat::getFormatFromName(filePath), true); - if (!deckOpt) { - QMessageBox::warning(this, tr("Share decks"), tr("Unable to load deck file %1").arg(filePath)); - return; - } - DeckShareItem *item = cmd.add_items(); - item->set_deck_list(deckOpt->deckList.writeToString_Native().toStdString()); - item->set_color_identity(getDeckColorIdentity(deckOpt->deckList, CardDatabaseManager::query()).toStdString()); - } - - shareBar->setCreateEnabled(false); - const int seq = ++shareRequestSeq; - shareInFlightSeq = seq; - PendingCommand *pend = client->prepareSessionCommand(cmd); - connect(pend, &PendingCommand::finished, this, - [this, seq](const Response &response, const CommandContainer &commandContainer) { - if (shareInFlightSeq != seq) { - return; // the user cancelled or a newer request superseded this one - } - shareInFlightSeq = 0; - shareFinished(response, commandContainer); - }); - client->sendCommand(pend); - shareTimeoutTimer->start(); -} - -void TabDeckStorageVisual::shareFinished(const Response &response, const CommandContainer & /*commandContainer*/) -{ - shareTimeoutTimer->stop(); - shareBar->setCreateEnabled(true); - if (response.response_code() != Response::RespOk) { - showShareNotice(tr("Failed to create the share link (server response code %1).") - .arg(QString::number(static_cast(response.response_code()))), - true); - return; - } - - const DeckShareUtils::ShareResponse share = DeckShareUtils::handleShareResponse(client, response); - - showShareNotice( - tr("Share link copied to the clipboard.\nExpires on %1.").arg(DeckShareUtils::formatShareExpiry(share.expiry))); - exitShareMode(); -} - -void TabDeckStorageVisual::handleConnectionChanged(ClientStatus status) -{ - shareDeckAvailable = (status == StatusLoggedIn); - visualDeckStorageWidget->setShareAvailable(shareDeckAvailable); - if (!shareDeckAvailable && shareBar->isVisible()) { - exitShareMode(); - } -} - -void TabDeckStorageVisual::showShareNotice(const QString &message, bool warning) -{ - QMessageBox box(warning ? QMessageBox::Warning : QMessageBox::Information, tr("Share link"), message, - QMessageBox::Ok, this); - box.exec(); -} - -void TabDeckStorageVisual::onShareTimeout() -{ - if (shareInFlightSeq == 0) { - return; // share mode was left while the request was still outstanding - } - shareInFlightSeq = 0; - shareBar->setCreateEnabled(true); - showShareNotice(tr("The server did not respond in time. Try again."), true); -} diff --git a/cockatrice/src/interface/widgets/tabs/visual_deck_storage/tab_deck_storage_visual.h b/cockatrice/src/interface/widgets/tabs/visual_deck_storage/tab_deck_storage_visual.h index 7cd4e7d17..d3f64e23d 100644 --- a/cockatrice/src/interface/widgets/tabs/visual_deck_storage/tab_deck_storage_visual.h +++ b/cockatrice/src/interface/widgets/tabs/visual_deck_storage/tab_deck_storage_visual.h @@ -7,19 +7,14 @@ #ifndef TAB_DECK_STORAGE_VISUAL_H #define TAB_DECK_STORAGE_VISUAL_H -#include "../../deck_share/share_bar_widget.h" #include "../tab.h" -#include -#include - struct LoadedDeck; class AbstractClient; class CommandContainer; class DeckPreviewWidget; class QFileSystemModel; class QGroupBox; -class QTimer; class QToolBar; class QTreeView; class QTreeWidget; @@ -31,55 +26,23 @@ class TabDeckStorageVisual final : public Tab { Q_OBJECT public: - explicit TabDeckStorageVisual(TabSupervisor *_tabSupervisor, AbstractClient *_client); - void retranslateUi() override; - + explicit TabDeckStorageVisual(TabSupervisor *_tabSupervisor); + void retranslateUi() override + { + } [[nodiscard]] QString getTabText() const override { return tr("Visual Deck Storage"); } - /** - * @brief Enters share-selection mode, optionally preselecting the given deck files. - */ - void enterShareMode(const QStringList &preselectFiles = {}); - - /** - * @brief Leaves share-selection mode and clears the selection. - */ - void exitShareMode(); - - [[nodiscard]] bool isShareModeActive() const - { - return shareBar->isVisible(); - } - public slots: void actOpenLocalDeck(const QString &filePath); - void actShareDeck(const QString &filePath); signals: void openDeckEditor(const LoadedDeck &deck); -private slots: - void actShareSelected(); - void shareFinished(const Response &response, const CommandContainer &commandContainer); - void onShareTimeout(); - void onShareSelectionChanged(); - void handleConnectionChanged(ClientStatus status); - private: - void showShareNotice(const QString &message, bool warning = false); - void updateShareHint(); - VisualDeckStorageWidget *visualDeckStorageWidget; - - ShareBarWidget *shareBar = nullptr; - AbstractClient *client; - QTimer *shareTimeoutTimer; - int shareRequestSeq = 0; - int shareInFlightSeq = 0; - bool shareDeckAvailable = false; }; #endif diff --git a/cockatrice/src/interface/widgets/utility/card_completer_delegate.cpp b/cockatrice/src/interface/widgets/utility/card_completer_delegate.cpp deleted file mode 100644 index 29bf2263e..000000000 --- a/cockatrice/src/interface/widgets/utility/card_completer_delegate.cpp +++ /dev/null @@ -1,419 +0,0 @@ -#include "card_completer_delegate.h" - -#include "../../pixel_map_generator.h" -#include "../cards/additional_info/mana_cost_widget.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -// --------------------------------------------------------------------------- -// Internal colour helpers -// --------------------------------------------------------------------------- - -namespace -{ - -struct ManaColor -{ - QColor fill; - QColor rim; - QColor text; -}; - -ManaColor manaColor(QChar symbol) -{ - switch (symbol.unicode()) { - case 'W': - return {QColor(248, 248, 246), QColor(190, 180, 160), QColor(80, 70, 50)}; - case 'U': - return {QColor(55, 130, 210), QColor(30, 90, 160), QColor(255, 255, 255)}; - case 'B': - return {QColor(90, 65, 160), QColor(55, 38, 110), QColor(220, 200, 255)}; - case 'R': - return {QColor(210, 55, 55), QColor(150, 30, 30), QColor(255, 255, 255)}; - case 'G': - return {QColor(45, 148, 90), QColor(28, 95, 58), QColor(255, 255, 255)}; - default: - return {QColor(100, 115, 135), QColor(65, 78, 95), QColor(230, 235, 240)}; - } -} - -QColor blend(QColor a, QColor b, qreal t) -{ - return QColor::fromRgbF(a.redF() + (b.redF() - a.redF()) * t, a.greenF() + (b.greenF() - a.greenF()) * t, - a.blueF() + (b.blueF() - a.blueF()) * t, a.alphaF() + (b.alphaF() - a.alphaF()) * t); -} - -} // namespace - -// --------------------------------------------------------------------------- - -QColor CardCompleterDelegate::accentForColors(const QString &colors) -{ - if (colors.isEmpty()) { - return QColor(100, 115, 135); - } - - QSet seen; - for (const QChar c : colors) { - if (QString("WUBRG").contains(c)) { - seen.insert(c); - } - } - - if (seen.size() > 1) { - return QColor(205, 145, 25); - } - - if (seen.isEmpty()) { - return QColor(100, 115, 135); - } - - return manaColor(*seen.begin()).fill; -} - -// --------------------------------------------------------------------------- - -CardCompleterDelegate::CardCompleterDelegate(QObject *parent) : QStyledItemDelegate(parent) -{ - setCodeCache.setMaxCost(64); -} - -// --------------------------------------------------------------------------- -// sizeHint -// --------------------------------------------------------------------------- - -QSize CardCompleterDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const -{ - Q_UNUSED(option) - - if (!index.isValid()) { - return QStyledItemDelegate::sizeHint(option, index); - } - - // Fixed wide rows so the popup has room for name, type line, set and mana - return {480, CardRowHeight}; -} - -// --------------------------------------------------------------------------- -// Mana symbol painting -// --------------------------------------------------------------------------- - -// --------------------------------------------------------------------------- - -void CardCompleterDelegate::drawManaSymbol(QPainter *p, QPoint centre, const QString &symbol, int radius) const -{ - const QRect pip(centre.x() - radius, centre.y() - radius, radius * 2, radius * 2); - - const QPixmap px = ManaSymbolPixmapGenerator::generatePixmap(symbol, QSize(radius * 2, radius * 2)); - - if (!px.isNull()) { - p->drawPixmap(pip, px); - return; - } - - bool isNumeric = false; - const int numVal = symbol.toInt(&isNumeric); - - const QString label = isNumeric ? QString::number(numVal) : symbol; - - const ManaColor mc = - (symbol.length() == 1 && QString("WUBRG").contains(symbol)) ? manaColor(symbol[0]) : manaColor(QChar('X')); - - QPainterPath circle; - circle.addEllipse(pip); - - p->save(); - p->setClipPath(circle); - p->fillPath(circle, mc.fill); - p->restore(); - - p->setPen(QPen(mc.rim, 1.2)); - p->setBrush(Qt::NoBrush); - p->drawEllipse(pip.adjusted(1, 1, -1, -1)); - - QFont f = p->font(); - f.setPixelSize(qMax(radius - 1, 7)); - f.setBold(true); - - p->setFont(f); - p->setPen(mc.text); - p->drawText(pip, Qt::AlignCenter, label); -} - -// --------------------------------------------------------------------------- - -int CardCompleterDelegate::drawManaCost(QPainter *p, const QRect &row, const QString &manaCost, int radius) const -{ - if (manaCost.isEmpty()) { - return row.right(); - } - - const int diam = radius * 2; - - // Split, adventure, aftermath and prepare cards store both halves of the - // cost joined by "//" (e.g. "1W // W"); draw each half as its own group. - static const QRegularExpression splitRegex("\\s*//\\s*"); - - QList parts; - - for (const QString &part : manaCost.split(splitRegex, Qt::SkipEmptyParts)) { - const QStringList symbols = ManaCostWidget::parseManaCost(part); - - if (!symbols.isEmpty()) { - parts.append(symbols); - } - } - - int totalW = 0; - - for (int i = 0; i < parts.size(); ++i) { - if (i > 0) { - totalW += PartGap; - } - - totalW += parts.at(i).size() * diam + qMax(0, parts.at(i).size() - 1) * SymbolSpacing; - } - - const int rightPad = 14; - - int x = row.right() - rightPad - totalW + radius; - - const int cy = row.center().y(); - - for (int i = 0; i < parts.size(); ++i) { - const QStringList &symbols = parts.at(i); - - for (const QString &sym : symbols) { - drawManaSymbol(p, {x, cy}, sym, radius); - x += diam + SymbolSpacing; - } - - if (i < parts.size() - 1) { - x += PartGap - SymbolSpacing; - } - } - - return row.right() - rightPad - totalW - 10; -} - -// --------------------------------------------------------------------------- - -QString CardCompleterDelegate::setCodeForCard(const QSharedPointer &card) const -{ - if (!card) { - return QString(); - } - - const QString name = card->getName(); - - if (setCodeCache.contains(name)) { - return *setCodeCache[name]; - } - - QString code; - - const PrintingInfo printing = CardDatabaseManager::query()->getPreferredPrinting(card); - - if (auto set = printing.getSet()) { - code = set->getShortName(); - } - - auto *cached = new QString(code); - setCodeCache.insert(name, cached); - return code; -} - -// --------------------------------------------------------------------------- -// paint -// --------------------------------------------------------------------------- - -void CardCompleterDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const -{ - if (!index.isValid()) { - QStyledItemDelegate::paint(painter, option, index); - return; - } - - const QRect rect = option.rect; - const QPalette &pal = option.palette; - - const bool selected = option.state & QStyle::State_Selected; - const bool hovered = option.state & QStyle::State_MouseOver; - - painter->save(); - painter->setRenderHints(QPainter::Antialiasing | QPainter::TextAntialiasing | QPainter::SmoothPixmapTransform); - - auto card = index.data(CardSearchModel::CardInfoRole).value>(); - - if (!card) { - painter->fillRect(rect, pal.color(QPalette::Base)); - painter->restore(); - return; - } - - const QString cardName = card->getName(); - const QString manaCost = card->getManaCost(); - const QString typeLine = card->getCardType(); - const QString setCode = setCodeForCard(card); - - const QColor accent = accentForColors(card->getColors()); - - const QColor base = pal.color(QPalette::Base); - const QColor textColor = pal.color(QPalette::Text); - const QColor secondaryColor = pal.color(QPalette::PlaceholderText); - - QColor tinted = blend(base, accent, 0.40); - - if (hovered) { - tinted = blend(tinted, Qt::white, 0.05); - } - - const QRectF cardRect = rect.adjusted(3, 2, -3, -2); - - // ----------------------------------------------------------------------- - // Main card body - // ----------------------------------------------------------------------- - - QPainterPath path; - path.addRoundedRect(cardRect, 7, 7); - - painter->save(); - painter->setClipPath(path); - - QLinearGradient bodyGrad(cardRect.topLeft(), cardRect.bottomLeft()); - - bodyGrad.setColorAt(0.0, blend(tinted, Qt::white, 0.10)); - bodyGrad.setColorAt(1.0, blend(tinted, Qt::black, 0.18)); - - painter->fillPath(path, bodyGrad); - - // ----------------------------------------------------------------------- - // Accent strip - // ----------------------------------------------------------------------- - - QRectF accentRect(cardRect.left(), cardRect.top(), AccentBarWidth, cardRect.height()); - - QLinearGradient accentGrad(accentRect.topLeft(), accentRect.bottomLeft()); - - accentGrad.setColorAt(0.0, blend(accent, Qt::white, 0.20)); - accentGrad.setColorAt(1.0, blend(accent, Qt::black, 0.25)); - - painter->fillRect(accentRect, accentGrad); - - // ----------------------------------------------------------------------- - // Right mana zone - // ----------------------------------------------------------------------- - - const QRectF manaZone(cardRect.right() - 110, cardRect.top(), 110, cardRect.height()); - - QLinearGradient manaGrad(manaZone.topLeft(), manaZone.bottomLeft()); - - manaGrad.setColorAt(0, QColor(0, 0, 0, 18)); - manaGrad.setColorAt(1, QColor(0, 0, 0, 42)); - - painter->fillRect(manaZone, manaGrad); - - painter->restore(); - - // ----------------------------------------------------------------------- - // Border - // ----------------------------------------------------------------------- - - QColor border = blend(accent, Qt::black, 0.45); - - if (hovered) { - border = blend(border, Qt::white, 0.18); - } - - painter->setPen(QPen(border, 1.2)); - painter->drawPath(path); - - // ----------------------------------------------------------------------- - // Selection glow - // ----------------------------------------------------------------------- - - if (selected) { - QColor glow = pal.color(QPalette::Highlight); - glow.setAlpha(30); - - painter->fillPath(path, glow); - - painter->setPen(QPen(pal.color(QPalette::Highlight), 2)); - painter->drawPath(path); - } - - // ----------------------------------------------------------------------- - // Mana cost - // ----------------------------------------------------------------------- - - const int costLeft = drawManaCost(painter, cardRect.toRect(), manaCost, SymbolRadius); - - // ----------------------------------------------------------------------- - // Card name + type line + set code - // ----------------------------------------------------------------------- - - const int textLeft = cardRect.left() + AccentBarWidth + 12; - const int textRight = costLeft - 8; - const int textWidth = qMax(0, textRight - textLeft); - - // ----------------------------------------------------------------------- - // Card name (top band) - // ----------------------------------------------------------------------- - - { - const QRect nameRect(textLeft, rect.top() + 2, textWidth, 20); - - QFont f = option.font; - f.setPixelSize(13); - f.setBold(true); - - painter->setFont(f); - - const QString nameText = QFontMetrics(f).elidedText(cardName, Qt::ElideRight, nameRect.width()); - - painter->setPen(QColor(0, 0, 0, 140)); - painter->drawText(nameRect.translated(0, 1), Qt::AlignLeft | Qt::AlignVCenter, nameText); - - painter->setPen(textColor); - painter->drawText(nameRect, Qt::AlignLeft | Qt::AlignVCenter, nameText); - } - - // ----------------------------------------------------------------------- - // Type line + set code (bottom band) - // ----------------------------------------------------------------------- - - { - const QRect infoRect(textLeft, rect.top() + 22, textWidth, rect.height() - 24); - - QFont f = option.font; - f.setPixelSize(10); - - painter->setFont(f); - - QString infoLine = typeLine; - - if (!setCode.isEmpty()) { - infoLine += " \u00b7 " + setCode; - } - - const QString infoText = QFontMetrics(f).elidedText(infoLine, Qt::ElideRight, infoRect.width()); - - painter->setPen(QColor(0, 0, 0, 120)); - painter->drawText(infoRect.translated(0, 1), Qt::AlignLeft | Qt::AlignVCenter, infoText); - - painter->setPen(secondaryColor); - painter->drawText(infoRect, Qt::AlignLeft | Qt::AlignVCenter, infoText); - } - - painter->restore(); -} diff --git a/cockatrice/src/interface/widgets/utility/card_completer_delegate.h b/cockatrice/src/interface/widgets/utility/card_completer_delegate.h deleted file mode 100644 index 78667f56b..000000000 --- a/cockatrice/src/interface/widgets/utility/card_completer_delegate.h +++ /dev/null @@ -1,57 +0,0 @@ -/** - * @file card_completer_delegate.h - * @ingroup UtilityWidgets - */ -//! \todo Document this file. - -#ifndef CARD_COMPLETER_DELEGATE_H -#define CARD_COMPLETER_DELEGATE_H - -#include -#include -#include -#include - -class CardInfo; - -/** - * @brief Paints styled card completer popup rows. - * - * Each row shows the card name, type line, set code and mana cost pips, - * color-coded by the card's color identity. Card data is read directly from - * the CardSearchModel::CardInfoRole so no extra database lookups are needed. - */ -class CardCompleterDelegate : public QStyledItemDelegate -{ - Q_OBJECT -public: - explicit CardCompleterDelegate(QObject *parent = nullptr); - - void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override; - QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override; - -private: - // Set short codes, resolved once per card name and cached - mutable QCache setCodeCache; - - // Resolve the card's color string ("RG", "W", "", ...) → accent QColor - static QColor accentForColors(const QString &colors); - - // Draw a single mana symbol pip at centre point - void drawManaSymbol(QPainter *p, QPoint centre, const QString &symbol, int radius) const; - - // Draw all mana pips for a cost string like "2RG" or "{2}{R}{G}"; split and - // adventure costs ("1W // W") are drawn as separate groups. Returns the left-most x used - int drawManaCost(QPainter *p, const QRect &row, const QString &manaCost, int radius) const; - - // Resolve the preferred printing's set short code for a card - QString setCodeForCard(const QSharedPointer &card) const; - - static constexpr int CardRowHeight = 40; - static constexpr int AccentBarWidth = 5; - static constexpr int SymbolRadius = 9; - static constexpr int SymbolSpacing = 2; - static constexpr int PartGap = 14; -}; - -#endif // CARD_COMPLETER_DELEGATE_H diff --git a/cockatrice/src/interface/widgets/utility/card_completer_styler.cpp b/cockatrice/src/interface/widgets/utility/card_completer_styler.cpp deleted file mode 100644 index 6794ccbb4..000000000 --- a/cockatrice/src/interface/widgets/utility/card_completer_styler.cpp +++ /dev/null @@ -1,340 +0,0 @@ -#include "card_completer_styler.h" - -#include "../cards/card_info_picture_enlarged_widget.h" -#include "card_completer_delegate.h" -#include "reversed_completer_model.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace -{ -const QSize PreviewSize(300, 419); -const int PreviewMargin = 16; -const int FadeDuration = 120; -} // namespace - -// --------------------------------------------------------------------------- - -void CardCompleterStyler::apply(QCompleter *completer) -{ - if (!completer) { - return; - } - - // The styler lives as long as the completer - new CardCompleterStyler(completer, completer); -} - -// --------------------------------------------------------------------------- - -CardCompleterStyler::CardCompleterStyler(QCompleter *completer, QObject *parent) - : QObject(parent), completer(completer), reversedModel(nullptr), preview(nullptr), above(false) -{ - auto *popup = completer->popup(); - - // Wrap the completer's model so its row order can be reversed when the - // popup is shown above the text edit - QAbstractItemModel *sourceModel = completer->model(); - reversedModel = new ReversedCompleterModel(completer); - reversedModel->setSourceModel(sourceModel); - completer->setModel(reversedModel); - - popup->setItemDelegate(new CardCompleterDelegate(popup)); - - popup->viewport()->setMouseTracking(true); - - popup->installEventFilter(this); - popup->viewport()->installEventFilter(this); - - connect(popup->selectionModel(), &QItemSelectionModel::currentRowChanged, this, - &CardCompleterStyler::updatePreview); - connect(completer, qOverload(&QCompleter::activated), this, &CardCompleterStyler::hidePreview); - connect(completer->completionModel(), &QAbstractItemModel::modelReset, this, - &CardCompleterStyler::onCompletionReset); -} - -CardCompleterStyler::~CardCompleterStyler() -{ - if (preview) { - preview->deleteLater(); - } -} - -// --------------------------------------------------------------------------- - -bool CardCompleterStyler::eventFilter(QObject *obj, QEvent *ev) -{ - auto *popup = completer->popup(); - - if (obj == popup->viewport()) { - if (ev->type() == QEvent::MouseMove) { - updatePreviewFromHover(static_cast(ev)->pos()); - } - } else if (obj == popup) { - switch (ev->type()) { - case QEvent::Show: - case QEvent::Move: - case QEvent::Resize: - updateOrientation(); - reposition(); - break; - case QEvent::Hide: - hidePreview(); - break; - case QEvent::KeyPress: - if (handlePopupKeyPress(static_cast(ev))) { - return true; - } - break; - default: - break; - } - } - - return QObject::eventFilter(obj, ev); -} - -// --------------------------------------------------------------------------- - -bool CardCompleterStyler::handlePopupKeyPress(QKeyEvent *event) -{ - const int key = event->key(); - if (key != Qt::Key_Up && key != Qt::Key_Down) { - return false; - } - - if (!above) { - return false; - } - - auto *popup = completer->popup(); - const int rowCount = completer->completionModel()->rowCount(); - if (rowCount == 0) { - return true; - } - - // With the popup above the text edit the rows are shown in reverse order and - // the closest match is in the bottom-most row. The up arrow then advances - // through the list (towards its end at the top of the popup). - const int currentRow = popup->currentIndex().row(); - const int step = (key == Qt::Key_Up) ? -1 : 1; - const int newRow = qBound(0, currentRow + step, rowCount - 1); - - if (newRow != currentRow) { - popup->setCurrentIndex(completer->completionModel()->index(newRow, completer->completionColumn())); - } - - return true; -} - -// --------------------------------------------------------------------------- - -void CardCompleterStyler::updateOrientation() -{ - above = isPopupAboveWidget(); - reversedModel->setEnabled(above); -} - -// --------------------------------------------------------------------------- - -void CardCompleterStyler::ensureClosestSelected() -{ - auto *popup = completer->popup(); - auto *completionModel = completer->completionModel(); - const int rowCount = completionModel->rowCount(); - if (rowCount == 0) { - return; - } - - const int currentRow = popup->currentIndex().row(); - const int closestRow = rowCount - 1; - - if (currentRow != closestRow) { - popup->setCurrentIndex(completionModel->index(closestRow, completer->completionColumn())); - } -} - -// --------------------------------------------------------------------------- - -void CardCompleterStyler::onCompletionReset() -{ - if (above) { - ensureClosestSelected(); - } -} - -// --------------------------------------------------------------------------- - -bool CardCompleterStyler::isPopupAboveWidget() const -{ - auto *popup = completer->popup(); - QWidget *widget = completer->widget(); - - if (!widget || !popup->isVisible()) { - return false; - } - - const QPoint popupBottom = popup->mapToGlobal(QPoint(0, popup->height())); - const QPoint fieldTop = widget->mapToGlobal(QPoint(0, 0)); - - return popupBottom.y() <= fieldTop.y(); -} - -// --------------------------------------------------------------------------- - -void CardCompleterStyler::updatePreviewFromHover(const QPoint &pos) -{ - const QModelIndex index = completer->popup()->indexAt(pos); - - // Hovering updates the preview but must not change the current completion - if (index.isValid() && index != previewedIndex) { - updatePreview(index); - } -} - -// --------------------------------------------------------------------------- - -void CardCompleterStyler::updatePreview(const QModelIndex &index) -{ - previewedIndex = index; - - if (!index.isValid()) { - hidePreview(); - return; - } - - auto card = index.data(CardSearchModel::CardInfoRole).value>(); - - if (!card) { - hidePreview(); - return; - } - - if (!preview) { - QWidget *contextWindow = completer->widget() ? completer->widget()->window() : nullptr; - - preview = new CardInfoPictureEnlargedWidget(contextWindow); - preview->hide(); - preview->setWindowOpacity(0.0); - } - - const ExactCard exact = CardDatabaseManager::query()->getCard({card->getName()}); - - if (!exact) { - hidePreview(); - return; - } - - preview->setCardPixmap(exact, PreviewSize); - - reposition(); - showPreview(); -} - -// --------------------------------------------------------------------------- - -void CardCompleterStyler::showPreview() -{ - if (!preview) { - return; - } - - stopPreviewFade(); - - preview->show(); - preview->raise(); - - auto *fade = new QPropertyAnimation(preview, "windowOpacity", preview); - - fade->setDuration(FadeDuration); - fade->setStartValue(preview->windowOpacity()); - fade->setEndValue(1.0); - fade->start(QAbstractAnimation::DeleteWhenStopped); -} - -// --------------------------------------------------------------------------- - -void CardCompleterStyler::hidePreview() -{ - if (!preview) { - return; - } - - // Stop any in-flight fade first so a stale one cannot keep the preview visible - stopPreviewFade(); - preview->hide(); -} - -// --------------------------------------------------------------------------- - -void CardCompleterStyler::stopPreviewFade() -{ - if (!preview) { - return; - } - - const auto animations = preview->findChildren(); - for (auto *animation : animations) { - animation->stop(); - } -} - -// --------------------------------------------------------------------------- - -void CardCompleterStyler::reposition() -{ - if (!preview) { - return; - } - - auto *popup = completer->popup(); - - const QRect popupGlobalRect(popup->mapToGlobal(popup->rect().topLeft()), popup->rect().size()); - - int anchorY; - - if (previewedIndex.isValid()) { - const QRect itemRect = popup->visualRect(previewedIndex); - const QPoint itemCenter = popup->viewport()->mapToGlobal(itemRect.center()); - anchorY = itemCenter.y(); - } else { - anchorY = popupGlobalRect.center().y(); - } - - const QScreen *screen = popup->screen(); - const QRect screenGeom = screen ? screen->availableGeometry() : QRect(); - - const int rightX = popupGlobalRect.right() + PreviewMargin; - const int leftX = popupGlobalRect.left() - PreviewMargin - preview->width(); - - int x; - int y = anchorY - preview->height() / 2; - - if (screenGeom.isEmpty()) { - x = rightX; - } else if (rightX + preview->width() <= screenGeom.right()) { - x = rightX; - } else if (leftX >= screenGeom.left()) { - x = leftX; - } else { - x = rightX; - } - - if (!screenGeom.isEmpty()) { - x = qBound(screenGeom.left(), x, qMax(screenGeom.left(), screenGeom.right() - preview->width())); - y = qBound(screenGeom.top(), y, qMax(screenGeom.top(), screenGeom.bottom() - preview->height())); - } - - preview->move(x, y); -} diff --git a/cockatrice/src/interface/widgets/utility/card_completer_styler.h b/cockatrice/src/interface/widgets/utility/card_completer_styler.h deleted file mode 100644 index 616e3fe61..000000000 --- a/cockatrice/src/interface/widgets/utility/card_completer_styler.h +++ /dev/null @@ -1,67 +0,0 @@ -/** - * @file card_completer_styler.h - * @ingroup UtilityWidgets - */ -//! \todo Document this file. - -#ifndef CARD_COMPLETER_STYLER_H -#define CARD_COMPLETER_STYLER_H - -#include -#include - -class CardInfoPictureEnlargedWidget; -class QCompleter; -class QKeyEvent; -class QPoint; -class ReversedCompleterModel; - -/** - * @brief Applies styled row painting and a card-image preview to a card completer. - * - * The completer popup rows are painted by CardCompleterDelegate and the image of - * the currently selected (or hovered) row is shown beside the popup. - */ -class CardCompleterStyler : public QObject -{ - Q_OBJECT -public: - /** - * @brief Styles @p completer and follows its current selection. - * - * The styler is parented to the completer so it lives exactly as long as the - * completer itself. - */ - static void apply(QCompleter *completer); - -protected: - bool eventFilter(QObject *obj, QEvent *ev) override; - -private slots: - void updatePreview(const QModelIndex &index); - void updatePreviewFromHover(const QPoint &pos); - void onCompletionReset(); - -private: - explicit CardCompleterStyler(QCompleter *completer, QObject *parent = nullptr); - ~CardCompleterStyler() override; - - void showPreview(); - void hidePreview(); - void stopPreviewFade(); - void reposition(); - - void updateOrientation(); - void ensureClosestSelected(); - - bool handlePopupKeyPress(QKeyEvent *event); - bool isPopupAboveWidget() const; - - QCompleter *completer; - ReversedCompleterModel *reversedModel; - CardInfoPictureEnlargedWidget *preview; - QModelIndex previewedIndex; - bool above; -}; - -#endif // CARD_COMPLETER_STYLER_H diff --git a/cockatrice/src/interface/widgets/utility/completer_utils.cpp b/cockatrice/src/interface/widgets/utility/completer_utils.cpp deleted file mode 100644 index 23ddb30b0..000000000 --- a/cockatrice/src/interface/widgets/utility/completer_utils.cpp +++ /dev/null @@ -1,72 +0,0 @@ -#include "completer_utils.h" - -#include "../../../client/settings/cache_settings.h" -#include "card_completer_styler.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace -{ -void applyCardSearchLanguage(CardSearchModel *searchModel) -{ - const CardsDisplaySettings &cardsDisplay = SettingsCache::instance().cardsDisplay(); - searchModel->setSearchLanguage(CardSearchLanguage{ - cardsDisplay.getCardLang(), static_cast(cardsDisplay.getCardSearchLanguage())}); -} -} // namespace - -CardCompleterSetup createCardCompleter(CardDatabaseDisplayModel *displayModel, QObject *parent, int maxVisibleItems) -{ - auto *searchModel = new CardSearchModel(displayModel, parent); - applyCardSearchLanguage(searchModel); - - auto *proxyModel = new CardCompleterProxyModel(parent); - proxyModel->setSourceModel(searchModel); - proxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive); - - auto *completer = new QCompleter(proxyModel, parent); - completer->setCompletionRole(Qt::DisplayRole); - completer->setCompletionMode(QCompleter::PopupCompletion); - completer->setCaseSensitivity(Qt::CaseInsensitive); - completer->setFilterMode(Qt::MatchContains); - completer->setMaxVisibleItems(maxVisibleItems); - CardCompleterStyler::apply(completer); - - auto *cardsDisplay = &SettingsCache::instance().cardsDisplay(); - QObject::connect(cardsDisplay, &CardsDisplaySettings::cardLangChanged, searchModel, - [searchModel] { applyCardSearchLanguage(searchModel); }); - QObject::connect(cardsDisplay, &CardsDisplaySettings::cardSearchLanguageChanged, searchModel, - [searchModel] { applyCardSearchLanguage(searchModel); }); - - return {searchModel, proxyModel, completer}; -} - -void connectCardCompleterSearch(QLineEdit *edit, const CardCompleterSetup &setup) -{ - QObject::connect(edit, &QLineEdit::textEdited, setup.searchModel, &CardSearchModel::updateSearchResults); - QObject::connect(edit, &QLineEdit::textEdited, setup.completer, [setup](const QString &text) { - setup.proxyModel->setFilterRegularExpression( - QRegularExpression(QRegularExpression::escape(text), QRegularExpression::CaseInsensitiveOption)); - if (!text.isEmpty()) { - setup.completer->complete(); - } - }); -} - -QCompleter *createMentionCompleter(QStringListModel *model, QObject *parent) -{ - auto *completer = new QCompleter(model, parent); - completer->setCaseSensitivity(Qt::CaseInsensitive); - completer->setMaxVisibleItems(5); - completer->setFilterMode(Qt::MatchStartsWith); - return completer; -} diff --git a/cockatrice/src/interface/widgets/utility/completer_utils.h b/cockatrice/src/interface/widgets/utility/completer_utils.h deleted file mode 100644 index 5d2ddfc30..000000000 --- a/cockatrice/src/interface/widgets/utility/completer_utils.h +++ /dev/null @@ -1,32 +0,0 @@ -/** - * @file completer_utils.h - * @ingroup UtilityWidgets - */ -//! \todo Document this file. - -#ifndef COMPLETER_UTILS_H -#define COMPLETER_UTILS_H - -class CardCompleterProxyModel; -class CardDatabaseDisplayModel; -class CardSearchModel; -class QCompleter; -class QLineEdit; -class QObject; -class QStringListModel; - -struct CardCompleterSetup -{ - CardSearchModel *searchModel; - CardCompleterProxyModel *proxyModel; - QCompleter *completer; -}; - -CardCompleterSetup -createCardCompleter(CardDatabaseDisplayModel *displayModel, QObject *parent, int maxVisibleItems = 10); - -void connectCardCompleterSearch(QLineEdit *edit, const CardCompleterSetup &setup); - -QCompleter *createMentionCompleter(QStringListModel *model, QObject *parent); - -#endif // CARD_COMPLETER_UTILS_H diff --git a/cockatrice/src/interface/widgets/utility/line_edit_completer.cpp b/cockatrice/src/interface/widgets/utility/line_edit_completer.cpp index 3a678029d..13f475a61 100644 --- a/cockatrice/src/interface/widgets/utility/line_edit_completer.cpp +++ b/cockatrice/src/interface/widgets/utility/line_edit_completer.cpp @@ -1,199 +1,135 @@ #include "line_edit_completer.h" #include +#include #include -#include +#include +#include +#include +#include -LineEditCompleter::LineEditCompleter(QWidget *parent) : LineEditUnfocusable(parent) +LineEditCompleter::LineEditCompleter(QWidget *parent) : LineEditUnfocusable(parent), c(nullptr) { } -void LineEditCompleter::addCompleter(QCompleter *c, CompleterTrigger trigger) -{ - c->setWidget(this); - c->setCompletionMode(QCompleter::PopupCompletion); - c->setCaseSensitivity(Qt::CaseInsensitive); - connect(c, qOverload(&QCompleter::activated), this, - qOverload(&LineEditCompleter::insertCompletion)); - - completers.append({c, trigger}); -} - -bool LineEditCompleter::hasVisibleCompleterPopup() const -{ - for (const auto &info : completers) { - if (info.completer->popup()->isVisible()) { - return true; - } - } - return false; -} - -void LineEditCompleter::hideCompleterPopups() -{ - for (const auto &info : completers) { - info.completer->popup()->hide(); - } -} - void LineEditCompleter::focusOutEvent(QFocusEvent *e) { LineEditUnfocusable::focusOutEvent(e); - - // Only commit the highlighted completion when focus moves away via Tab. - // Other focus losses (e.g. the unfocus shortcut / Escape) must simply close - // the popup without inserting anything. - if (e->reason() != Qt::TabFocusReason) { - hideCompleterPopups(); - return; + if (c->popup()->isVisible()) { + // Remove Popup + c->popup()->hide(); + // Truncate the line to last space or whole string + QString textValue = text(); + int lastIndex = textValue.length(); + int lastWordStartIndex = textValue.lastIndexOf(" ") + 1; + int leftShift = qMin(lastIndex, lastWordStartIndex); + setText(textValue.left(leftShift)); + // Insert highlighted line from popup + insert(c->completionModel()->index(c->popup()->currentIndex().row(), 0).data().toString() + " "); + // Set focus back to the textbox since tab was pressed + setFocus(); } - - for (auto &info : completers) { - if (!info.completer->popup()->isVisible()) { - continue; - } - - const QModelIndex currentIndex = info.completer->popup()->currentIndex(); - if (currentIndex.isValid()) { - insertCompletion(info.completer, currentIndex.data().toString()); - } - } - - hideCompleterPopups(); } void LineEditCompleter::keyPressEvent(QKeyEvent *event) { + switch (event->key()) { + case Qt::Key_Return: + case Qt::Key_Enter: + case Qt::Key_Escape: + if (c->popup()->isVisible()) { + event->ignore(); + // Remove Popup + c->popup()->hide(); + // Truncate the line to last space or whole string + QString textValue = text(); + int lastIndexof = qMax(0, textValue.lastIndexOf(" ")); + QString finalString = textValue.left(lastIndexof); + // Add a space if there's a word + if (finalString != "") { + finalString += " "; + } + setText(finalString); + return; + } + break; + case Qt::Key_Space: + if (c->popup()->isVisible()) { + event->ignore(); + // Remove Popup + c->popup()->hide(); + // Truncate the line to last space or whole string + QString textValue = text(); + int lastIndex = textValue.length(); + int lastWordStartIndex = textValue.lastIndexOf(" ") + 1; + int leftShift = qMin(lastIndex, lastWordStartIndex); + setText(textValue.left(leftShift)); + // Insert highlighted line from popup + insert(c->completionModel()->index(c->popup()->currentIndex().row(), 0).data().toString() + " "); + return; + } + break; + default: + break; + } + LineEditUnfocusable::keyPressEvent(event); - - if (event->key() == Qt::Key_Escape) { - hideCompleterPopups(); + // return if the completer is null or if the most recently typed char was '@'. + // Only want the popup AFTER typing the first char of the mention. + if (!c || text().right(1).contains("@")) { + c->popup()->hide(); return; } - QString textValue = text(); - int cursorPos = cursorPosition(); - - CompleterInfo *active = nullptr; - QString prefix; - - for (auto &info : completers) { - bool triggered = false; - switch (info.trigger) { - case CompleterTrigger::Mention: { - int triggerPos = textValue.lastIndexOf("@", cursorPos - 1); - if (triggerPos != -1 && (triggerPos == 0 || textValue[triggerPos - 1].isSpace())) { - triggered = true; - // Keep the "@" so the prefix matches the "@"-prefixed mention model entries. - prefix = textValue.mid(triggerPos, cursorPos - triggerPos); - } - break; - } - case CompleterTrigger::Card: { - int triggerPos = textValue.lastIndexOf("[[", cursorPos - 1); - int closePos = textValue.indexOf("]]", triggerPos + 2); - if (triggerPos != -1 && (closePos == -1 || closePos >= cursorPos)) { - triggered = true; - prefix = textValue.mid(triggerPos + 2, cursorPos - (triggerPos + 2)); - } - break; - } - } - - if (triggered) { - active = &info; - break; - } - } - - if (!active) { - for (auto &info : completers) { - info.completer->popup()->hide(); - } + // Set new completion prefix + c->setCompletionPrefix(cursorWord(text())); + if (c->completionPrefix().length() < 1) { + c->popup()->hide(); return; } - active->completer->setCompletionPrefix(prefix); + // Draw completion box + QRect cr = cursorRect(); + cr.setWidth(c->popup()->sizeHintForColumn(0) + c->popup()->verticalScrollBar()->sizeHint().width()); + c->complete(cr); - switch (active->trigger) { - case CompleterTrigger::Card: - emit cardPartialChanged(prefix); - return; - case CompleterTrigger::Mention: - break; - } - - active->completer->complete(); + // Select first item in the completion popup + QItemSelectionModel *sm = new QItemSelectionModel(c->completionModel()); + c->popup()->setSelectionModel(sm); + sm->select(c->completionModel()->index(0, 0), QItemSelectionModel::ClearAndSelect); + sm->setCurrentIndex(c->completionModel()->index(0, 0), QItemSelectionModel::NoUpdate); } -void LineEditCompleter::insertCompletion(const QString &completion) +QString LineEditCompleter::cursorWord(const QString &line) const { - for (auto &info : completers) { - if (info.completer == sender()) { - insertCompletion(info.completer, completion); - return; - } - } + return line.mid(line.left(cursorPosition()).lastIndexOf(" ") + 1, + cursorPosition() - line.left(cursorPosition()).lastIndexOf(" ") - 1); } -void LineEditCompleter::insertCompletion(QCompleter *completer, const QString &completion) +void LineEditCompleter::insertCompletion(QString arg) { - QString t = text(); - int pos = cursorPosition(); - - for (const auto &info : completers) { - if (info.completer != completer) { - continue; - } - - switch (info.trigger) { - case CompleterTrigger::Card: { - int triggerPos = t.lastIndexOf("[[", pos - 1); - if (triggerPos == -1) { - return; - } - - // If an earlier "[[" is still open it also encloses the cursor, so - // replace from its start. Otherwise completing in text such as - // "[[Opt[[Amok" would leave a stray "[[" behind. - int startPos = triggerPos; - for (int searchFrom = triggerPos; searchFrom > 0;) { - const int earlier = t.lastIndexOf("[[", searchFrom - 1); - if (earlier == -1) { - break; - } - const int earlierClose = t.indexOf("]]", earlier + 2); - if (earlierClose != -1 && earlierClose < pos) { - break; - } - startPos = earlier; - searchFrom = earlier; - } - - // If the cursor sits inside an already-closed [[...]] pair, replace - // the whole construct instead of leaving a duplicate closing bracket - // behind. - int insertEnd = pos; - const int closePos = t.indexOf("]]", startPos + 2); - if (closePos != -1 && closePos >= pos) { - insertEnd = closePos + 2; - } - - QString after = t.mid(insertEnd); - QString replaced = t.left(startPos + 2) + completion + "]] "; - setText(replaced + after); - setCursorPosition(replaced.length()); - return; - } - case CompleterTrigger::Mention: { - int triggerPos = t.lastIndexOf("@", pos - 1); - if (triggerPos == -1) { - return; - } - setText(t.replace(triggerPos, pos - triggerPos, completion + " ")); - setCursorPosition(triggerPos + completion.length() + 1); - return; - } - } - } + QString s_arg = arg + " "; + setText(text().replace(text().left(cursorPosition()).lastIndexOf(" ") + 1, + cursorPosition() - text().left(cursorPosition()).lastIndexOf(" ") - 1, s_arg)); +} + +void LineEditCompleter::setCompleter(QCompleter *completer) +{ + c = completer; + c->setWidget(this); + connect(c, qOverload(&QCompleter::activated), this, &LineEditCompleter::insertCompletion); +} + +void LineEditCompleter::setCompletionList(QStringList completionList) +{ + if (!c || c->popup()->isVisible()) { + return; + } + + QStringListModel *model; + model = (QStringListModel *)(c->model()); + if (model == NULL) { + model = new QStringListModel(); + } + model->setStringList(completionList); } diff --git a/cockatrice/src/interface/widgets/utility/line_edit_completer.h b/cockatrice/src/interface/widgets/utility/line_edit_completer.h index 369b7b553..65fa382ac 100644 --- a/cockatrice/src/interface/widgets/utility/line_edit_completer.h +++ b/cockatrice/src/interface/widgets/utility/line_edit_completer.h @@ -1,7 +1,6 @@ /** * @file line_edit_completer.h * @ingroup UI - * @brief Line edit with support for multiple trigger-based completers, e.g., @mention and [[card]]. */ //! \todo Document this file. @@ -10,49 +9,25 @@ #include "custom_line_edit.h" -#include -#include #include -#include -#include -#include - -enum class CompleterTrigger -{ - Mention, //< "@" prefix, replaced in place with a trailing space. - Card //< "[[" prefix, replaced with the completion plus a closing "]] ". -}; - -struct CompleterInfo -{ - QCompleter *completer; - CompleterTrigger trigger; -}; class LineEditCompleter : public LineEditUnfocusable { Q_OBJECT -signals: - void cardPartialChanged(const QString &partial); - private: - QList completers; - - void insertCompletion(QCompleter *completer, const QString &completion); - + QString cursorWord(const QString &line) const; + QCompleter *c; private slots: - void insertCompletion(const QString &text); + void insertCompletion(QString); protected: - void keyPressEvent(QKeyEvent *event) override; - void focusOutEvent(QFocusEvent *e) override; + void keyPressEvent(QKeyEvent *event); + void focusOutEvent(QFocusEvent *e); public: explicit LineEditCompleter(QWidget *parent = nullptr); - void addCompleter(QCompleter *c, CompleterTrigger trigger); - - bool hasVisibleCompleterPopup() const; - void hideCompleterPopups(); + void setCompleter(QCompleter *); + void setCompletionList(QStringList); }; #endif diff --git a/cockatrice/src/interface/widgets/utility/report_utils.cpp b/cockatrice/src/interface/widgets/utility/report_utils.cpp deleted file mode 100644 index aa4558e5c..000000000 --- a/cockatrice/src/interface/widgets/utility/report_utils.cpp +++ /dev/null @@ -1,119 +0,0 @@ -#include "report_utils.h" - -#include -#include -#include -#include -#include -#include -#include - -namespace report_utils -{ - -namespace -{ -QColor reportStatusColor(const QString &status) -{ - const QColor text = qApp->palette().color(QPalette::Text); - const int luminance = (299 * text.red() + 587 * text.green() + 114 * text.blue()) / 1000; - const bool dark = luminance < 128; - - if (status == "open") { - return dark ? QColor("#e06060") : QColor("#c0392b"); - } - if (status == "assigned") { - return dark ? QColor("#e0a030") : QColor("#d68910"); - } - if (status == "resolved") { - return dark ? QColor("#50c878") : QColor("#1e8449"); - } - if (status == "dismissed") { - return qApp->palette().color(QPalette::PlaceholderText); - } - return QColor(); -} - -void setReportStatusItemColor(QTableWidgetItem *item, const QString &status) -{ - const QColor color = reportStatusColor(status); - if (color.isValid()) { - item->setForeground(QBrush(color)); - } -} -} // namespace - -QString formatReportCategory(const QString &category) -{ - QString result = category; - result.replace('_', ' '); - return result.left(1).toUpper() + result.mid(1); -} - -QString formatReportTime(qint64 secondsSinceEpoch) -{ - const QDateTime dt = QDateTime::fromSecsSinceEpoch(secondsSinceEpoch); - return dt.toString("yyyy-MM-dd hh:mm"); -} - -void fillReportTableRow(QTableWidget *table, - int row, - const ServerInfo_Report &report, - int idColumn, - int timeColumn, - int reportedColumn, - int categoryColumn, - int gameIdColumn, - int statusColumn, - int assignedColumn) -{ - auto *idItem = new QTableWidgetItem(QString::number(report.report_id())); - idItem->setData(Qt::UserRole, report.report_id()); - table->setItem(row, idColumn, idItem); - - table->setItem(row, timeColumn, new QTableWidgetItem(formatReportTime(report.report_time()))); - table->setItem(row, reportedColumn, new QTableWidgetItem(QString::fromStdString(report.reported_user_name()))); - table->setItem(row, categoryColumn, - new QTableWidgetItem(formatReportCategory(QString::fromStdString(report.category())))); - table->setItem(row, gameIdColumn, - new QTableWidgetItem(report.game_id() > 0 ? QString::number(report.game_id()) : QString())); - - 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()))); -} - -void renderReportDetails(QTextEdit *chatLogEdit, - QTextEdit *commentsEdit, - const ServerInfo_Report &report, - const QString &commentsEmptyText, - const QString &moderatorPrefix, - const QString &nonModeratorPrefix) -{ - if (report.has_chat_log() && !report.chat_log().empty()) { - chatLogEdit->setPlainText(QString::fromStdString(report.chat_log())); - } else { - chatLogEdit->clear(); - } - - commentsEdit->clear(); - - if (report.comments_size() == 0) { - commentsEdit->setPlainText(commentsEmptyText); - return; - } - - for (int i = 0; i < report.comments_size(); ++i) { - const ServerInfo_ReportComment &c = report.comments(i); - const QString author = QString::fromStdString(c.author_name()); - const QString text = QString::fromStdString(c.comment_text()); - const QString prefix = c.is_moderator() ? moderatorPrefix : nonModeratorPrefix; - commentsEdit->moveCursor(QTextCursor::End); - commentsEdit->insertPlainText( - QString("[%1] %2 %3:\n%4\n\n").arg(formatReportTime(c.comment_time()), prefix, author, text)); - } -} - -} // namespace report_utils diff --git a/cockatrice/src/interface/widgets/utility/report_utils.h b/cockatrice/src/interface/widgets/utility/report_utils.h deleted file mode 100644 index c6cc96c18..000000000 --- a/cockatrice/src/interface/widgets/utility/report_utils.h +++ /dev/null @@ -1,36 +0,0 @@ -#ifndef REPORT_UTILS_H -#define REPORT_UTILS_H - -#include -#include - -class QTableWidget; -class QTextEdit; - -namespace report_utils -{ - -QString formatReportCategory(const QString &category); -QString formatReportTime(qint64 secondsSinceEpoch); - -void fillReportTableRow(QTableWidget *table, - int row, - const ServerInfo_Report &report, - int idColumn, - int timeColumn, - int reportedColumn, - int categoryColumn, - int gameIdColumn, - int statusColumn, - int assignedColumn); - -void renderReportDetails(QTextEdit *chatLogEdit, - QTextEdit *commentsEdit, - const ServerInfo_Report &report, - const QString &commentsEmptyText, - const QString &moderatorPrefix, - const QString &nonModeratorPrefix); - -} // namespace report_utils - -#endif // REPORT_UTILS_H diff --git a/cockatrice/src/interface/widgets/utility/reversed_completer_model.cpp b/cockatrice/src/interface/widgets/utility/reversed_completer_model.cpp deleted file mode 100644 index 1bc9ba14b..000000000 --- a/cockatrice/src/interface/widgets/utility/reversed_completer_model.cpp +++ /dev/null @@ -1,102 +0,0 @@ -#include "reversed_completer_model.h" - -#include -#include -#include - -void ReversedCompleterModel::setSourceModel(QAbstractItemModel *sourceModel) -{ - if (sourceModel == this->sourceModel()) { - return; - } - - if (QAbstractItemModel *old = this->sourceModel()) { - disconnect(old, nullptr, this, nullptr); - } - - QAbstractProxyModel::setSourceModel(sourceModel); - - if (sourceModel) { - connect(sourceModel, &QAbstractItemModel::modelReset, this, &ReversedCompleterModel::invalidate); - connect(sourceModel, &QAbstractItemModel::rowsInserted, this, &ReversedCompleterModel::invalidate); - connect(sourceModel, &QAbstractItemModel::rowsRemoved, this, &ReversedCompleterModel::invalidate); - connect(sourceModel, &QAbstractItemModel::rowsMoved, this, &ReversedCompleterModel::invalidate); - connect(sourceModel, &QAbstractItemModel::columnsInserted, this, &ReversedCompleterModel::invalidate); - connect(sourceModel, &QAbstractItemModel::columnsRemoved, this, &ReversedCompleterModel::invalidate); - connect(sourceModel, &QAbstractItemModel::dataChanged, this, &ReversedCompleterModel::invalidate); - connect(sourceModel, &QAbstractItemModel::layoutChanged, this, &ReversedCompleterModel::invalidate); - } - - invalidate(); -} - -void ReversedCompleterModel::setEnabled(bool enabled) -{ - if (enabled == isEnabled) { - return; - } - isEnabled = enabled; - invalidate(); -} - -QModelIndex ReversedCompleterModel::mapToSource(const QModelIndex &proxyIndex) const -{ - if (!proxyIndex.isValid() || !sourceModel()) { - return {}; - } - const int sourceRow = isEnabled ? sourceRowCount() - 1 - proxyIndex.row() : proxyIndex.row(); - return sourceModel()->index(sourceRow, proxyIndex.column()); -} - -QModelIndex ReversedCompleterModel::mapFromSource(const QModelIndex &sourceIndex) const -{ - if (!sourceIndex.isValid() || !sourceModel()) { - return {}; - } - const int proxyRow = isEnabled ? sourceRowCount() - 1 - sourceIndex.row() : sourceIndex.row(); - return index(proxyRow, sourceIndex.column()); -} - -QModelIndex ReversedCompleterModel::index(int row, int column, const QModelIndex &parent) const -{ - if (parent.isValid() || !sourceModel() || row < 0 || row >= rowCount() || column < 0 || column >= columnCount()) { - return {}; - } - return createIndex(row, column); -} - -QModelIndex ReversedCompleterModel::parent(const QModelIndex &) const -{ - return {}; -} - -int ReversedCompleterModel::rowCount(const QModelIndex &parent) const -{ - return parent.isValid() || !sourceModel() ? 0 : sourceModel()->rowCount(); -} - -int ReversedCompleterModel::columnCount(const QModelIndex &parent) const -{ - return parent.isValid() || !sourceModel() ? 0 : sourceModel()->columnCount(); -} - -QVariant ReversedCompleterModel::data(const QModelIndex &proxyIndex, int role) const -{ - return sourceModel() ? sourceModel()->data(mapToSource(proxyIndex), role) : QVariant(); -} - -QVariant ReversedCompleterModel::headerData(int section, Qt::Orientation orientation, int role) const -{ - return sourceModel() ? sourceModel()->headerData(section, orientation, role) : QVariant(); -} - -int ReversedCompleterModel::sourceRowCount() const -{ - return sourceModel() ? sourceModel()->rowCount() : 0; -} - -void ReversedCompleterModel::invalidate() -{ - beginResetModel(); - endResetModel(); -} diff --git a/cockatrice/src/interface/widgets/utility/reversed_completer_model.h b/cockatrice/src/interface/widgets/utility/reversed_completer_model.h deleted file mode 100644 index 64233a1b0..000000000 --- a/cockatrice/src/interface/widgets/utility/reversed_completer_model.h +++ /dev/null @@ -1,47 +0,0 @@ -/** - * @file reversed_completer_model.h - * @ingroup UtilityWidgets - */ -//! \todo Document this file. - -#ifndef REVERSED_COMPLETER_MODEL_H -#define REVERSED_COMPLETER_MODEL_H - -#include - -class QAbstractItemModel; - -/** - * @brief A completer model that can present its rows bottom-to-top. - * - * The original row order is kept intact in the source model (row 0 is always the - * closest match). When enabled, the proxy maps the source rows in reverse so the - * popup shows the closest match in the row nearest to the text edit. Any change - * in the source model is forwarded as a full reset, which is all QCompleter - * needs to rebuild its completion list. - */ -class ReversedCompleterModel : public QAbstractProxyModel -{ -public: - using QAbstractProxyModel::QAbstractProxyModel; - - void setSourceModel(QAbstractItemModel *sourceModel) override; - void setEnabled(bool enabled); - - QModelIndex mapToSource(const QModelIndex &proxyIndex) const override; - QModelIndex mapFromSource(const QModelIndex &sourceIndex) const override; - QModelIndex index(int row, int column, const QModelIndex &parent = {}) const override; - QModelIndex parent(const QModelIndex &) const override; - int rowCount(const QModelIndex &parent = {}) const override; - int columnCount(const QModelIndex &parent = {}) const override; - QVariant data(const QModelIndex &proxyIndex, int role = Qt::DisplayRole) const override; - QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; - -private: - int sourceRowCount() const; - void invalidate(); - - bool isEnabled = false; -}; - -#endif // REVERSED_COMPLETER_MODEL_H diff --git a/cockatrice/src/interface/widgets/utility/sequence_edit.cpp b/cockatrice/src/interface/widgets/utility/sequence_edit.cpp index 06561a41d..c6bf289ba 100644 --- a/cockatrice/src/interface/widgets/utility/sequence_edit.cpp +++ b/cockatrice/src/interface/widgets/utility/sequence_edit.cpp @@ -2,7 +2,6 @@ #include "../../../client/settings/cache_settings.h" #include "../../../client/settings/shortcuts_settings.h" -#include "../../pixel_map_generator.h" #include #include @@ -15,8 +14,8 @@ SequenceEdit::SequenceEdit(const QString &_shortcutName, QWidget *parent) : QWid defaultButton = new QPushButton("", this); lineEdit->setMinimumWidth(70); - clearButton->setIcon(themePixmap(QStringLiteral("icons/clearsearch"))); - defaultButton->setIcon(themePixmap(QStringLiteral("icons/update"))); + clearButton->setIcon(QPixmap("theme:icons/clearsearch")); + defaultButton->setIcon(QPixmap("theme:icons/update")); auto *layout = new QHBoxLayout(this); layout->setContentsMargins(0, 0, 0, 0); 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 a59a068dd..62e1bf5ba 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,5 @@ #include "visual_database_display_filter_toolbar_widget.h" -#include "../../pixel_map_generator.h" #include "../deck_editor/card_database_view.h" #include "visual_database_display_widget.h" @@ -61,36 +60,39 @@ VisualDatabaseDisplayFilterToolbarWidget::VisualDatabaseDisplayFilterToolbarWidg }); quickFilterSaveLoadWidget = new SettingsButtonWidget(this); - quickFilterSaveLoadWidget->setButtonIcon(themePixmap(QStringLiteral("icons/floppy_disk"))); + quickFilterSaveLoadWidget->setButtonIcon(QPixmap("theme:icons/floppy_disk")); quickFilterNameWidget = new SettingsButtonWidget(this); - quickFilterNameWidget->setButtonIcon(themePixmap(QStringLiteral("icons/pen_to_square"))); + quickFilterNameWidget->setButtonIcon(QPixmap("theme:icons/pen_to_square")); quickFilterMainTypeWidget = new SettingsButtonWidget(this); - quickFilterMainTypeWidget->setButtonIcon(themePixmap(QStringLiteral("icons/circle_half_stroke"))); + quickFilterMainTypeWidget->setButtonIcon(QPixmap("theme:icons/circle_half_stroke")); quickFilterSubTypeWidget = new SettingsButtonWidget(this); - quickFilterSubTypeWidget->setButtonIcon(themePixmap(QStringLiteral("icons/dragon"))); + quickFilterSubTypeWidget->setButtonIcon(QPixmap("theme:icons/dragon")); quickFilterSetWidget = new SettingsButtonWidget(this); - quickFilterSetWidget->setButtonIcon(themePixmap(QStringLiteral("icons/scroll"))); + quickFilterSetWidget->setButtonIcon(QPixmap("theme:icons/scroll")); quickFilterFormatLegalityWidget = new SettingsButtonWidget(this); - quickFilterFormatLegalityWidget->setButtonIcon(themePixmap(QStringLiteral("icons/scale_balanced"))); + quickFilterFormatLegalityWidget->setButtonIcon(QPixmap("theme:icons/scale_balanced")); retranslateUi(); } void VisualDatabaseDisplayFilterToolbarWidget::initialize() { - auto sortLayout = new QHBoxLayout(sortGroupBox); + // create groupbox layouts + auto sortLayout = new QHBoxLayout(this); sortLayout->setContentsMargins(0, 0, 0, 0); sortLayout->setSpacing(0); + sortGroupBox->setLayout(sortLayout); sortLayout->setAlignment(Qt::AlignLeft); - auto filterLayout = new QHBoxLayout(filterGroupBox); + auto filterLayout = new QHBoxLayout(this); filterLayout->setContentsMargins(0, 0, 0, 0); filterLayout->setSpacing(2); + filterGroupBox->setLayout(filterLayout); filterLayout->setAlignment(Qt::AlignLeft); // create settings widgets 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 a20d56f63..dc98e6940 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 @@ -16,12 +16,10 @@ #include #include -#include #include #include #include #include -#include #include #include @@ -41,17 +39,6 @@ VisualDatabaseDisplayWidget::VisualDatabaseDisplayWidget(QWidget *parent, databaseDisplayModel->setSourceModel(database_model); databaseDisplayModel->setFilterKeyColumn(0); - const auto applyCardSearchLanguage = [this]() { - const CardsDisplaySettings &cardsDisplay = SettingsCache::instance().cardsDisplay(); - databaseDisplayModel->setSearchLanguage(CardSearchLanguage{ - cardsDisplay.getCardLang(), static_cast(cardsDisplay.getCardSearchLanguage())}); - }; - applyCardSearchLanguage(); - connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::cardLangChanged, this, - applyCardSearchLanguage); - connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::cardSearchLanguageChanged, this, - applyCardSearchLanguage); - cards = new QList; connect(databaseDisplayModel, &CardDatabaseDisplayModel::modelDirty, this, &VisualDatabaseDisplayWidget::modelDirty); @@ -77,7 +64,7 @@ VisualDatabaseDisplayWidget::VisualDatabaseDisplayWidget(QWidget *parent, searchEdit->setPlaceholderText(tr("Search by card name (or search expressions)")); searchEdit->setClearButtonEnabled(true); searchEdit->addAction(loadColorAdjustedPixmap("theme:icons/search"), QLineEdit::LeadingPosition); - auto help = searchEdit->addAction(themePixmap(QStringLiteral("icons/info")), QLineEdit::TrailingPosition); + auto help = searchEdit->addAction(QPixmap("theme:icons/info"), QLineEdit::TrailingPosition); connect(help, &QAction::triggered, this, [this] { createSearchSyntaxHelpWindow(searchEdit); }); setFocusProxy(searchEdit); @@ -101,19 +88,6 @@ VisualDatabaseDisplayWidget::VisualDatabaseDisplayWidget(QWidget *parent, databaseView->setItemDelegate(nullptr); databaseView->setVisible(false); - // Without a deck model there is nothing to add cards to, so the zone menu stays hidden. - if (deckListModel) { - databaseView->setZoneMenuProvider( - [deckListModel]() -> QList> { - QList> result; - for (const QString &boardName : InnerDecklistNode::boardZoneNames()) { - result.append({boardName, deckListModel->getCustomZoneNames(boardName)}); - } - return result; - }, - [this] { return newZoneCreator ? newZoneCreator() : QString(); }); - } - searchEdit->setTreeView(databaseView); searchEdit->installEventFilter(databaseView->getKeySignals()); @@ -132,7 +106,7 @@ VisualDatabaseDisplayWidget::VisualDatabaseDisplayWidget(QWidget *parent, clearFilterWidget = new QToolButton(); clearFilterWidget->setFixedSize(32, 32); - clearFilterWidget->setIcon(themePixmap(QStringLiteral("icons/delete"))); + clearFilterWidget->setIcon(QPixmap("theme:icons/delete")); connect(clearFilterWidget, &QToolButton::clicked, this, [this] { filterModel->blockSignals(true); filterModel->filterTree()->blockSignals(true); @@ -166,6 +140,9 @@ void VisualDatabaseDisplayWidget::initialize() { databaseLoadIndicator->setVisible(false); + filterContainer->initialize(); + filterContainer->setVisible(true); + searchContainer->addWidget(colorFilterWidget); searchContainer->addWidget(clearFilterWidget); searchContainer->addWidget(searchEdit); @@ -181,48 +158,17 @@ void VisualDatabaseDisplayWidget::initialize() mainLayout->addWidget(cardSizeWidget); + databaseDisplayModel->setFilterTree(filterModel->filterTree()); + connect(filterModel, &FilterTreeModel::layoutChanged, this, &VisualDatabaseDisplayWidget::onSearchModelChanged); - initializeFilters(); -} + loadCardsTimer = new QTimer(this); + loadCardsTimer->setSingleShot(true); // Ensure it only fires once after the timeout -void VisualDatabaseDisplayWidget::initializeFilters() -{ - if (filtersInitialized || !isVisible() || CardDatabaseManager::getInstance()->getLoadStatus() != LoadStatus::Ok) { - return; - } + connect(loadCardsTimer, &QTimer::timeout, this, [this]() { loadCurrentPage(); }); + loadCardsTimer->start(5000); - filtersInitialized = true; - - // The filter toolbar builds its widgets by iterating the entire card database - // (per-set, per-main-type, per-sub-type and per-format buttons). Building it - // inside showEvent would block the tab switch, so keep it hidden and defer the - // build to the next event loop turn, letting the tab paint first. The toolbar - // then appears one event loop turn later, shifting the grid down by the toolbar - // height -- the intended tradeoff of an responsive tab switch. - filterContainer->setVisible(false); - - QTimer::singleShot(0, this, [this] { - filterContainer->initialize(); - filterContainer->setVisible(true); - - databaseDisplayModel->setFilterTree(filterModel->filterTree()); - - QTimer::singleShot(5000, this, [this] { loadCurrentPage(); }); - - retranslateUi(); - }); -} - -void VisualDatabaseDisplayWidget::showEvent(QShowEvent *event) -{ - QWidget::showEvent(event); - initializeFilters(); -} - -void VisualDatabaseDisplayWidget::setNewZoneCreator(const std::function &creator) -{ - newZoneCreator = creator; + retranslateUi(); } void VisualDatabaseDisplayWidget::retranslateUi() @@ -346,17 +292,9 @@ void VisualDatabaseDisplayWidget::loadCurrentPage() { // Ensure only the initial page is loaded if (currentPage == 0) { - if (!initialLoadScheduled) { - initialLoadScheduled = true; - qCDebug(VisualDatabaseDisplayLog) << "Loading the first page"; - // Defer the first page so the tab switch stays responsive. The card - // grid builds one event loop turn later. This also applies to - // search-driven reloads, which reset currentPage back to 0. - QTimer::singleShot(0, this, [this] { - initialLoadScheduled = false; - populateCards(); - }); - } + // Only load the first page initially + qCDebug(VisualDatabaseDisplayLog) << "Loading the first page"; + populateCards(); } else if (nearEndOfPage()) { // If not the first page, just load the next page and append to the flow widget loadNextPage(); diff --git a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_widget.h b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_widget.h index d161ce362..a383e8ead 100644 --- a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_widget.h +++ b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_widget.h @@ -22,7 +22,6 @@ #include #include #include -#include #include #include #include @@ -47,12 +46,6 @@ public: void sortCardList(const QStringList &properties, Qt::SortOrder order) const; void setDeckList(const DeckList &new_deck_list_model); - /** - * @brief Sets the callback used to create a custom zone from the add-to-zone menu. - * The callback returns the name of the created zone, or an empty string if creation was cancelled. - */ - void setNewZoneCreator(const std::function &creator); - CardDatabaseDisplayModel *getDatabaseDisplayModel() { return databaseDisplayModel; @@ -113,7 +106,6 @@ private: VisualDatabaseDisplayFilterToolbarWidget *filterContainer; CardDatabaseDisplayModel *databaseDisplayModel; CardDatabaseView *databaseView; - std::function newZoneCreator; QList *cards; QVBoxLayout *mainLayout; QScrollArea *scrollArea; @@ -123,20 +115,17 @@ private: OverlapControlWidget *overlapControlWidget; CardSizeWidget *cardSizeWidget; QTimer *debounceTimer; + QTimer *loadCardsTimer; int debounceTime = 300; // in Ms int currentPage = 0; // Current page index int cardsPerPage = 100; // Number of cards per page - bool filtersInitialized = false; - bool initialLoadScheduled = false; - void initializeFilters(); void highlightAllSearchEdit(); bool nearEndOfPage() const; protected: void resizeEvent(QResizeEvent *event) override; - void showEvent(QShowEvent *event) override; }; #endif // VISUAL_DATABASE_DISPLAY_WIDGET_H 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 28abe480e..f44c9c3ef 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,5 @@ #include "visual_deck_display_options_widget.h" -#include "../../pixel_map_generator.h" #include "../tabs/visual_deck_editor/tab_deck_editor_visual.h" #include @@ -48,7 +47,7 @@ VisualDeckDisplayOptionsWidget::VisualDeckDisplayOptionsWidget(QWidget *parent) sortByLabel = new QLabel(this); sortCriteriaButton = new SettingsButtonWidget(this); - sortCriteriaButton->setButtonIcon(themePixmap(QStringLiteral("icons/sort_arrow_down"))); + sortCriteriaButton->setButtonIcon(QPixmap("theme:icons/sort_arrow_down")); sortLabel = new QLabel(sortCriteriaButton); sortLabel->setWordWrap(true); @@ -93,7 +92,7 @@ void VisualDeckDisplayOptionsWidget::retranslateUi() sortLabel->setText(tr("Click and drag to change the sort order within the groups")); sortCriteriaButton->setToolTip(tr("Configure how cards are sorted within their groups")); displayTypeButton->setButtonText(tr("Toggle Layout: Overlap")); - displayTypeButton->setButtonIcon(themePixmap(QStringLiteral("icons/scales"))); + displayTypeButton->setButtonIcon(QPixmap("theme:icons/scales")); displayTypeButton->setToolTip( tr("Change how cards are displayed within zones (i.e. overlapped or fully visible.)")); } @@ -118,11 +117,11 @@ void VisualDeckDisplayOptionsWidget::updateDisplayType() switch (currentDisplayType) { case DisplayType::Flat: displayTypeButton->setButtonText(tr("Toggle Layout: Flat")); - displayTypeButton->setButtonIcon(themePixmap(QStringLiteral("icons/scroll"))); + displayTypeButton->setButtonIcon(QPixmap("theme:icons/scroll")); break; case DisplayType::Overlap: displayTypeButton->setButtonText(tr("Toggle Layout: Overlap")); - displayTypeButton->setButtonIcon(themePixmap(QStringLiteral("icons/scales"))); + displayTypeButton->setButtonIcon(QPixmap("theme:icons/scales")); break; } emit displayTypeChanged(currentDisplayType); 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 064fbed5d..e5bcb2fd3 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 @@ -4,14 +4,12 @@ #include "../../../main.h" #include "../../deck_loader/deck_loader.h" #include "../../layouts/overlap_layout.h" -#include "../../pixel_map_generator.h" #include "../cards/card_info_picture_with_text_overlay_widget.h" #include "../cards/deck_card_zone_display_widget.h" #include "../general/layout_containers/flow_widget.h" #include "../tabs/visual_deck_editor/tab_deck_editor_visual.h" #include "../tabs/visual_deck_editor/tab_deck_editor_visual_tab_widget.h" #include "../utility/compact_push_button.h" -#include "../utility/completer_utils.h" #include "visual_deck_display_options_widget.h" #include @@ -23,6 +21,8 @@ #include #include #include +#include +#include #include #include #include @@ -30,10 +30,8 @@ VisualDeckEditorWidget::VisualDeckEditorWidget(QWidget *parent, DeckListModel *_deckListModel, - QItemSelectionModel *_selectionModel, - CardDatabaseModel *_cardDatabaseModel) - : QWidget(parent), deckListModel(_deckListModel), selectionModel(_selectionModel), - cardDatabaseModel(_cardDatabaseModel) + QItemSelectionModel *_selectionModel) + : QWidget(parent), deckListModel(_deckListModel), selectionModel(_selectionModel) { // The Main Widget and Main Layout, which contain a single Widget: The Scroll Area setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); @@ -97,19 +95,35 @@ void VisualDeckEditorWidget::initializeSearchBarAndCompleter() setFocusProxy(searchBar); setFocusPolicy(Qt::ClickFocus); - if (!cardDatabaseModel) { - cardDatabaseModel = new CardDatabaseModel(CardDatabaseManager::getInstance(), false, this); - } + cardDatabaseModel = new CardDatabaseModel(CardDatabaseManager::getInstance(), false, this); cardDatabaseDisplayModel = new CardDatabaseDisplayModel(this); cardDatabaseDisplayModel->setSourceModel(cardDatabaseModel); + CardSearchModel *searchModel = new CardSearchModel(cardDatabaseDisplayModel, this); - const CardCompleterSetup cardSetup = createCardCompleter(cardDatabaseDisplayModel, this, 15); - proxyModel = cardSetup.proxyModel; - completer = cardSetup.completer; + proxyModel = new CardCompleterProxyModel(this); + proxyModel->setSourceModel(searchModel); + proxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive); + proxyModel->setFilterRole(Qt::DisplayRole); + + completer = new QCompleter(proxyModel, this); + completer->setCompletionRole(Qt::DisplayRole); + completer->setCompletionMode(QCompleter::PopupCompletion); + completer->setCaseSensitivity(Qt::CaseInsensitive); + completer->setFilterMode(Qt::MatchContains); + completer->setMaxVisibleItems(15); searchBar->setCompleter(completer); // Update suggestions dynamically - connectCardCompleterSearch(searchBar, cardSetup); + connect(searchBar, &QLineEdit::textEdited, searchModel, &CardSearchModel::updateSearchResults); + connect(searchBar, &QLineEdit::textEdited, this, [=, this](const QString &text) { + // Ensure substring matching + QString pattern = ".*" + QRegularExpression::escape(text) + ".*"; + proxyModel->setFilterRegularExpression(QRegularExpression(pattern, QRegularExpression::CaseInsensitiveOption)); + + if (!text.isEmpty()) { + completer->complete(); // Force the dropdown to appear + } + }); connect(completer, static_cast(&QCompleter::activated), this, [=, this](const QString &completion) { @@ -132,7 +146,7 @@ void VisualDeckEditorWidget::initializeSearchBarAndCompleter() // Search button functionality searchPushButton = new CompactPushButton(searchContainer); - searchPushButton->setButtonIcon(themePixmap(QStringLiteral("icons/search"))); + searchPushButton->setButtonIcon(QPixmap("theme:icons/search")); connect(searchPushButton, &QPushButton::clicked, this, [=, this]() { ExactCard card = CardDatabaseManager::query()->getCard({searchBar->text()}); if (card) { diff --git a/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_editor_widget.h b/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_editor_widget.h index ac0d07efd..da02b5c1f 100644 --- a/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_editor_widget.h +++ b/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_editor_widget.h @@ -38,10 +38,7 @@ class VisualDeckEditorWidget : public QWidget Q_OBJECT public: - explicit VisualDeckEditorWidget(QWidget *parent, - DeckListModel *deckListModel, - QItemSelectionModel *selectionModel, - CardDatabaseModel *_cardDatabaseModel = nullptr); + explicit VisualDeckEditorWidget(QWidget *parent, DeckListModel *deckListModel, QItemSelectionModel *selectionModel); void retranslateUi(); void updateCompactMode(); void clearAllDisplayWidgets(); diff --git a/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_color_identity_filter_widget.cpp b/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_color_identity_filter_widget.cpp index d1a780c38..575632724 100644 --- a/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_color_identity_filter_widget.cpp +++ b/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_color_identity_filter_widget.cpp @@ -1,10 +1,11 @@ #include "deck_preview_color_identity_filter_widget.h" #include "../../cards/additional_info/mana_symbol_widget.h" +#include "deck_preview_widget.h" -#include +#include -DeckPreviewColorIdentityFilterWidget::DeckPreviewColorIdentityFilterWidget(QWidget *parent) +DeckPreviewColorIdentityFilterWidget::DeckPreviewColorIdentityFilterWidget(VisualDeckStorageWidget *parent) : QWidget(parent), layout(new QHBoxLayout(this)) { setLayout(layout); @@ -27,10 +28,15 @@ DeckPreviewColorIdentityFilterWidget::DeckPreviewColorIdentityFilterWidget(QWidg } toggleButton = new QPushButton(this); + toggleButton->setCheckable(true); layout->addWidget(toggleButton); - // Connect the button's clicked signal - connect(toggleButton, &QPushButton::clicked, this, &DeckPreviewColorIdentityFilterWidget::updateFilterMode); + // Connect the button's toggled signal + connect(toggleButton, &QPushButton::toggled, this, &DeckPreviewColorIdentityFilterWidget::updateFilterMode); + connect(this, &DeckPreviewColorIdentityFilterWidget::activeColorsChanged, parent, + &VisualDeckStorageWidget::updateColorFilter); + connect(this, &DeckPreviewColorIdentityFilterWidget::filterModeChanged, parent, + &VisualDeckStorageWidget::updateColorFilter); // Call retranslateUi to set the initial text retranslateUi(); @@ -39,55 +45,76 @@ DeckPreviewColorIdentityFilterWidget::DeckPreviewColorIdentityFilterWidget(QWidg void DeckPreviewColorIdentityFilterWidget::retranslateUi() { // Set the toggle button text based on the current mode - switch (filterMode) { - case VisualDeckStorageSortFilterProxyModel::FilterMode::ExactMatch: - toggleButton->setText(tr("Mode: Exact Match")); - break; - case VisualDeckStorageSortFilterProxyModel::FilterMode::Includes: - toggleButton->setText(tr("Mode: Includes")); - break; - case VisualDeckStorageSortFilterProxyModel::FilterMode::Excludes: - toggleButton->setText(tr("Mode: Excludes")); - break; - } + toggleButton->setText(exactMatchMode ? tr("Mode: Exact Match") : tr("Mode: Includes")); toggleButton->setToolTip(tr("Color identity filter mode (AND/OR/NOT conjunctions of filters)")); } -/** - * @brief The colors that are currently toggled on. - */ -QSet DeckPreviewColorIdentityFilterWidget::getActiveColors() const -{ - QSet activeColorSet; - for (auto it = activeColors.constBegin(); it != activeColors.constEnd(); ++it) { - if (it.value()) { - activeColorSet.insert(it.key()); - } - } - return activeColorSet; -} - void DeckPreviewColorIdentityFilterWidget::handleColorToggled(QChar color, bool active) { activeColors[color] = active; emit activeColorsChanged(); } -void DeckPreviewColorIdentityFilterWidget::updateFilterMode() +void DeckPreviewColorIdentityFilterWidget::updateFilterMode(bool checked) { - // Cycle through the modes - switch (filterMode) { - case VisualDeckStorageSortFilterProxyModel::FilterMode::ExactMatch: - filterMode = VisualDeckStorageSortFilterProxyModel::FilterMode::Includes; - break; - case VisualDeckStorageSortFilterProxyModel::FilterMode::Includes: - filterMode = VisualDeckStorageSortFilterProxyModel::FilterMode::Excludes; - break; - case VisualDeckStorageSortFilterProxyModel::FilterMode::Excludes: - filterMode = VisualDeckStorageSortFilterProxyModel::FilterMode::ExactMatch; + exactMatchMode = checked; // Toggle between modes + retranslateUi(); // Update the button text + emit filterModeChanged(exactMatchMode); +} + +void DeckPreviewColorIdentityFilterWidget::filterWidgets(QList widgets) +{ + // Check if no colors are active + bool noColorsActive = true; + for (auto it = activeColors.constBegin(); it != activeColors.constEnd(); ++it) { + if (it.value()) { + noColorsActive = false; break; + } } - retranslateUi(); // Update the button text - emit filterModeChanged(filterMode); + // If no colors are active, return the unfiltered list of widgets + if (noColorsActive) { + for (DeckPreviewWidget *previewWidget : widgets) { + previewWidget->filteredByColor = false; + } + } + + for (const auto &widget : widgets) { + QString colorIdentity = widget->getColorIdentity(); + + bool matchesFilter = true; + if (exactMatchMode) { + // Exact match mode: active colors must exactly match colorIdentity + + // Create a set of active colors + QSet activeColorSet; + for (auto it = activeColors.constBegin(); it != activeColors.constEnd(); ++it) { + if (it.value()) { + activeColorSet.insert(it.key().toUpper()); // Use uppercase for uniformity + } + } + + // Create a set of colors from the color identity string + QSet colorIdentitySet; + for (const QChar &color : colorIdentity) { + colorIdentitySet.insert(color.toUpper()); // Ensure case uniformity + } + + // Compare the sets: the sets must match exactly + if (activeColorSet != colorIdentitySet) { + matchesFilter = false; + } + } else { + // Includes mode: colorIdentity must contain all active colors + for (auto it = activeColors.constBegin(); it != activeColors.constEnd(); ++it) { + if (it.value() && !colorIdentity.contains(it.key())) { + matchesFilter = false; + break; + } + } + } + + widget->filteredByColor = !matchesFilter; + } } diff --git a/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_color_identity_filter_widget.h b/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_color_identity_filter_widget.h index d54984bcf..0207e2ee2 100644 --- a/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_color_identity_filter_widget.h +++ b/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_color_identity_filter_widget.h @@ -2,60 +2,42 @@ * @file deck_preview_color_identity_filter_widget.h * @ingroup VisualDeckPreviewWidgets */ +//! \todo Document this file. #ifndef DECK_PREVIEW_COLOR_IDENTITY_FILTER_WIDGET_H #define DECK_PREVIEW_COLOR_IDENTITY_FILTER_WIDGET_H -#include "../visual_deck_storage_sort_filter_proxy_model.h" +#include "../visual_deck_storage_widget.h" #include -#include #include -#include #include +class DeckPreviewWidget; +class VisualDeckStorageWidget; + class DeckPreviewColorIdentityFilterWidget : public QWidget { Q_OBJECT public: - explicit DeckPreviewColorIdentityFilterWidget(QWidget *parent = nullptr); + explicit DeckPreviewColorIdentityFilterWidget(VisualDeckStorageWidget *parent); void retranslateUi(); - - /** - * @brief The currently active color identity filter mode. - */ - [[nodiscard]] VisualDeckStorageSortFilterProxyModel::FilterMode getFilterMode() const - { - return filterMode; - } - - /** - * @brief The colors that are currently toggled on. - */ - [[nodiscard]] QSet getActiveColors() const; + void filterWidgets(QList widgets); signals: - /** - * Emitted when the set of active colors changed due to user interaction. - */ + void filterModeChanged(bool exactMatchMode); void activeColorsChanged(); - /** - * Emitted when the user cycles the color identity filter mode. - * @param mode The new filter mode. - */ - void filterModeChanged(VisualDeckStorageSortFilterProxyModel::FilterMode mode); - private slots: void handleColorToggled(QChar color, bool active); - void updateFilterMode(); + void updateFilterMode(bool checked); private: QHBoxLayout *layout; QPushButton *toggleButton; QMap activeColors; - VisualDeckStorageSortFilterProxyModel::FilterMode filterMode = VisualDeckStorageSortFilterProxyModel::Includes; + bool exactMatchMode = false; // Default to "includes" mode }; #endif // DECK_PREVIEW_COLOR_IDENTITY_FILTER_WIDGET_H diff --git a/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_deck_tags_display_widget.cpp b/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_deck_tags_display_widget.cpp index 41d87ccb1..b45f61be7 100644 --- a/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_deck_tags_display_widget.cpp +++ b/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_deck_tags_display_widget.cpp @@ -1,15 +1,19 @@ #include "deck_preview_deck_tags_display_widget.h" #include "../../../../client/settings/cache_settings.h" -#include "../../../deck_loader/deck_loader.h" +#include "../../../../interface/widgets/dialogs/dlg_convert_deck_to_cod_format.h" +#include "../../../../interface/widgets/tabs/tab_deck_editor.h" #include "../../general/layout_containers/flow_widget.h" #include "deck_preview_tag_addition_widget.h" #include "deck_preview_tag_dialog.h" #include "deck_preview_tag_display_widget.h" +#include "deck_preview_widget.h" #include #include +#include #include +#include DeckPreviewDeckTagsDisplayWidget::DeckPreviewDeckTagsDisplayWidget(QWidget *_parent, const QStringList &_tags) : QWidget(_parent), currentTags(_tags) @@ -50,16 +54,6 @@ void DeckPreviewDeckTagsDisplayWidget::refreshTags() flowWidget->addWidget(tagAdditionWidget); } -void DeckPreviewDeckTagsDisplayWidget::setKnownTagsProvider(const std::function &provider) -{ - knownTagsProvider = provider; -} - -void DeckPreviewDeckTagsDisplayWidget::setConversionPromptHandler(const std::function &handler) -{ - conversionPromptHandler = handler; -} - /** * Gets the filepath of all files (no directories) in target directory and all subdirectories */ @@ -98,13 +92,93 @@ static QStringList findAllKnownTags() void DeckPreviewDeckTagsDisplayWidget::openTagEditDlg() { - // The deck editor path has no conversion prompt; the VDS path registers one. - if (conversionPromptHandler && !conversionPromptHandler()) { - return; + if (qobject_cast(parentWidget())) { + // If we're the child of a DeckPreviewWidget, then we need to handle conversion + auto *deckPreviewWidget = qobject_cast(parentWidget()); + + bool canAddTags = promptFileConversionIfRequired(deckPreviewWidget); + + if (canAddTags) { + QStringList knownTags = deckPreviewWidget->visualDeckStorageWidget->tagFilterWidget->getAllKnownTags(); + execTagDialog(knownTags); + } + } else { + // If we're the child of an AbstractTabDeckEditor, then we don't bother with conversion + QStringList knownTags = findAllKnownTags(); + execTagDialog(knownTags); + } +} + +static bool confirmOverwriteIfExists(QWidget *parent, const QString &filePath) +{ + QFileInfo fileInfo(filePath); + QString newFileName = QDir::toNativeSeparators(fileInfo.path() + "/" + fileInfo.completeBaseName() + ".cod"); + + if (QFile::exists(newFileName)) { + QMessageBox::StandardButton reply = + QMessageBox::question(parent, QObject::tr("Overwrite Existing File?"), + QObject::tr("A .cod version of this deck already exists. Overwrite it?"), + QMessageBox::Yes | QMessageBox::No); + return reply == QMessageBox::Yes; + } + return true; // Safe to proceed +} + +static void convertFileToCockatriceFormat(DeckPreviewWidget *deckPreviewWidget) +{ + DeckLoader::convertToCockatriceFormat(deckPreviewWidget->deckLoader->getDeck()); + deckPreviewWidget->filePath = deckPreviewWidget->deckLoader->getDeck().lastLoadInfo.fileName; + deckPreviewWidget->refreshBannerCardText(); +} + +/** + * Checks if the deck's file format supports tags. + * If not, then prompt the user for file conversion. + * @return whether the resulting file can support adding tags + */ +bool DeckPreviewDeckTagsDisplayWidget::promptFileConversionIfRequired(DeckPreviewWidget *deckPreviewWidget) +{ + if (DeckFileFormat::getFormatFromName(deckPreviewWidget->filePath) == DeckFileFormat::Cockatrice) { + return true; } - const QStringList knownTags = knownTagsProvider ? knownTagsProvider() : findAllKnownTags(); - execTagDialog(knownTags); + // Retrieve saved preference if the prompt is disabled + if (!SettingsCache::instance().visualDeckStorage().getVisualDeckStoragePromptForConversion()) { + if (!SettingsCache::instance().visualDeckStorage().getVisualDeckStorageAlwaysConvert()) { + return false; + } + + if (!confirmOverwriteIfExists(this, deckPreviewWidget->filePath)) { + return false; + } + + convertFileToCockatriceFormat(deckPreviewWidget); + return true; + } + + // Show the dialog to the user + DialogConvertDeckToCodFormat conversionDialog(parentWidget()); + if (conversionDialog.exec() != QDialog::Accepted) { + SettingsCache::instance().visualDeckStorage().setVisualDeckStoragePromptForConversion( + !conversionDialog.dontAskAgain()); + SettingsCache::instance().visualDeckStorage().setVisualDeckStorageAlwaysConvert(false); + + return false; + } + + // Try to convert file + if (!confirmOverwriteIfExists(this, deckPreviewWidget->filePath)) { + return false; + } + + convertFileToCockatriceFormat(deckPreviewWidget); + + if (conversionDialog.dontAskAgain()) { + SettingsCache::instance().visualDeckStorage().setVisualDeckStoragePromptForConversion(false); + SettingsCache::instance().visualDeckStorage().setVisualDeckStorageAlwaysConvert(true); + } + + return true; } void DeckPreviewDeckTagsDisplayWidget::execTagDialog(const QStringList &knownTags) @@ -117,4 +191,4 @@ void DeckPreviewDeckTagsDisplayWidget::execTagDialog(const QStringList &knownTag emit tagsChanged(updatedTags); } } -} +} \ No newline at end of file diff --git a/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_deck_tags_display_widget.h b/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_deck_tags_display_widget.h index 64bd5aa1a..4bd7915cd 100644 --- a/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_deck_tags_display_widget.h +++ b/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_deck_tags_display_widget.h @@ -2,53 +2,41 @@ * @file deck_preview_deck_tags_display_widget.h * @ingroup VisualDeckPreviewWidgets */ +//! \todo Document this file. #ifndef DECK_PREVIEW_DECK_TAGS_DISPLAY_WIDGET_H #define DECK_PREVIEW_DECK_TAGS_DISPLAY_WIDGET_H -#include +#include "../../../deck_loader/deck_loader.h" +#include "deck_preview_widget.h" + #include -#include - -class FlowWidget; +class DeckPreviewWidget; class DeckPreviewDeckTagsDisplayWidget : public QWidget { Q_OBJECT QStringList currentTags; FlowWidget *flowWidget; - std::function knownTagsProvider; - std::function conversionPromptHandler; public: explicit DeckPreviewDeckTagsDisplayWidget(QWidget *_parent, const QStringList &_tags = {}); void setTags(const QStringList &_tags); void refreshTags(); - /** - * @brief Sets a provider for the tags shown in the edit dialog. - * Defaults to scanning all deck files in the deck folder. - */ - void setKnownTagsProvider(const std::function &provider); - - /** - * @brief Sets a handler run before opening the tag dialog. Returning false - * cancels the dialog. Defaults to no handler (the deck editor path). - */ - void setConversionPromptHandler(const std::function &handler); - public slots: void openTagEditDlg(); +private: + bool promptFileConversionIfRequired(DeckPreviewWidget *deckPreviewWidget); + void execTagDialog(const QStringList &knownTags); + signals: /** * Emitted when the tags have changed due to user interaction. * @param tags The new list of tags. */ void tagsChanged(const QStringList &tags); - -private: - void execTagDialog(const QStringList &knownTags); }; #endif // DECK_PREVIEW_DECK_TAGS_DISPLAY_WIDGET_H diff --git a/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_tag_display_widget.cpp b/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_tag_display_widget.cpp index a7fef5031..db466b77a 100644 --- a/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_tag_display_widget.cpp +++ b/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_tag_display_widget.cpp @@ -48,7 +48,6 @@ QSize DeckPreviewTagDisplayWidget::sizeHint() const void DeckPreviewTagDisplayWidget::mousePressEvent(QMouseEvent *event) { - const TagState previousState = state; switch (event->button()) { case Qt::LeftButton: setState(state != TagState::Selected ? TagState::Selected : TagState::NotSelected); @@ -63,12 +62,7 @@ void DeckPreviewTagDisplayWidget::mousePressEvent(QMouseEvent *event) break; } - // Only announce a change when the state was actually toggled, so a click that falls - // through the switch (e.g. a button the widget does not react to) does not drive a - // full tag-filter update and layout pass for nothing. - if (state != previousState) { - emit tagClicked(); - } + emit tagClicked(); QWidget::mousePressEvent(event); } diff --git a/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_tag_display_widget.h b/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_tag_display_widget.h index df2a6b404..980741c5d 100644 --- a/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_tag_display_widget.h +++ b/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_tag_display_widget.h @@ -48,10 +48,7 @@ public: signals: /** - * @brief Emitted when a click toggles the chip's selection/exclusion state. - * - * Not emitted for clicks that leave the state unchanged. Connected handlers use - * this as the trigger to update filters built from selectedTags()/excludedTags(). + * @brief Emitted when the tag is clicked. */ void tagClicked(); diff --git a/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_widget.cpp b/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_widget.cpp index 53fe32314..ffe954308 100644 --- a/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_widget.cpp +++ b/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_widget.cpp @@ -1,70 +1,47 @@ #include "deck_preview_widget.h" #include "../../../../client/settings/cache_settings.h" -#include "../../../../interface/widgets/dialogs/dlg_convert_deck_to_cod_format.h" -#include "../../../deck_loader/deck_loader.h" #include "../../cards/additional_info/color_identity_widget.h" #include "../../cards/deck_preview_card_picture_widget.h" -#include "../visual_deck_storage_quick_settings_widget.h" -#include "../visual_deck_storage_tag_filter_widget.h" -#include "../visual_deck_storage_widget.h" #include "deck_preview_deck_tags_display_widget.h" +#include +#include #include -#include #include -#include #include #include #include #include #include #include -#include #include #include DeckPreviewWidget::DeckPreviewWidget(QWidget *_parent, VisualDeckStorageWidget *_visualDeckStorageWidget, - VisualDeckStorageModel *_model, const QString &_filePath) - : QWidget(_parent), filePath(_filePath), visualDeckStorageWidget(_visualDeckStorageWidget), model(_model) + : QWidget(_parent), visualDeckStorageWidget(_visualDeckStorageWidget), filePath(_filePath), + colorIdentityWidget(nullptr), deckTagsDisplayWidget(nullptr) { layout = new QVBoxLayout(this); setLayout(layout); - auto *pictureWidget = + deckLoader = new DeckLoader(this); + connect(deckLoader, &DeckLoader::loadFinished, this, &DeckPreviewWidget::initializeUi); + //! \todo Batch tag refresh: count finished deck loads and refresh tags once all decks are loaded. + // Currently expensive: refreshes on each individual deck load instead of once at the end. + connect(deckLoader, &DeckLoader::loadFinished, visualDeckStorageWidget->tagFilterWidget, + &VisualDeckStorageTagFilterWidget::refreshTags); + deckLoader->loadFromFileAsync(filePath, DeckFileFormat::getFormatFromName(filePath), false); + + bannerCardDisplayWidget = new DeckPreviewCardPictureWidget(this, false, visualDeckStorageWidget->deckPreviewSelectionAnimationEnabled); - pictureWidget->setFontSize(24); - connect(pictureWidget, &DeckPreviewCardPictureWidget::imageClicked, this, &DeckPreviewWidget::imageClickedEvent); - connect(pictureWidget, &DeckPreviewCardPictureWidget::imageSingleClicked, this, - &DeckPreviewWidget::imageSingleClicked); - connect(pictureWidget, &DeckPreviewCardPictureWidget::imageDoubleClicked, this, + + connect(bannerCardDisplayWidget, &DeckPreviewCardPictureWidget::imageClicked, this, + &DeckPreviewWidget::imageClickedEvent); + connect(bannerCardDisplayWidget, &DeckPreviewCardPictureWidget::imageDoubleClicked, this, &DeckPreviewWidget::imageDoubleClickedEvent); - bannerCardDisplayWidget = pictureWidget; - - colorIdentityWidget = new ColorIdentityWidget(this); - deckTagsDisplayWidget = new DeckPreviewDeckTagsDisplayWidget(this); - connect(deckTagsDisplayWidget, &DeckPreviewDeckTagsDisplayWidget::tagsChanged, this, &DeckPreviewWidget::setTags); - deckTagsDisplayWidget->setKnownTagsProvider( - [this] { return visualDeckStorageWidget->tagFilterWidget->getAllKnownTags(); }); - deckTagsDisplayWidget->setConversionPromptHandler([this] { return promptFileConversionIfRequired(); }); - - bannerCardLabel = new QLabel(this); - bannerCardLabel->setObjectName("bannerCardLabel"); - bannerCardComboBox = new QComboBox(this); - bannerCardComboBox->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); - bannerCardComboBox->setObjectName("bannerCardComboBox"); - bannerCardComboBox->installEventFilter(new NoScrollFilter(bannerCardComboBox)); - connect(bannerCardComboBox, QOverload::of(&QComboBox::currentIndexChanged), this, - &DeckPreviewWidget::setBannerCard); - - // Apply the initial visibility settings and keep them in sync while they change. - updateColorIdentityVisibility( - SettingsCache::instance().visualDeckStorage().getVisualDeckStorageShowColorIdentity()); - updateBannerCardComboBoxVisibility( - SettingsCache::instance().visualDeckStorage().getVisualDeckStorageShowBannerCardComboBox()); - updateTagsVisibility(SettingsCache::instance().visualDeckStorage().getVisualDeckStorageShowTagsOnDeckPreviews()); connect(&SettingsCache::instance().visualDeckStorage(), &VisualDeckStorageSettings::visualDeckStorageShowColorIdentityChanged, this, @@ -79,38 +56,6 @@ DeckPreviewWidget::DeckPreviewWidget(QWidget *_parent, &DeckPreviewWidget::refreshBannerCardToolTip); layout->addWidget(bannerCardDisplayWidget); - layout->addWidget(colorIdentityWidget); - layout->addWidget(deckTagsDisplayWidget); - layout->addWidget(bannerCardLabel); - layout->addWidget(bannerCardComboBox); - - // Only re-sync when this widget's own row changed. Without the row check, every - // finished deck load would trigger a full resync (card db lookup + combo rebuild) - // in every preview widget. - connect(model, &QAbstractItemModel::dataChanged, this, - [this](const QModelIndex &topLeft, const QModelIndex &bottomRight) { - const int r = row(); - if (r >= topLeft.row() && r <= bottomRight.row()) { - syncFromModel(); - } - }); - - retranslateUi(); - syncFromModel(); - - // resizeEvent clamps every child to the picture's width, so collect them once here - // to keep the resize handler from searching the widget tree on every layout pass. - fixedWidthChildren = {bannerCardDisplayWidget, colorIdentityWidget, deckTagsDisplayWidget, bannerCardLabel, - bannerCardComboBox}; - - // Child of the banner widget so the frame tracks the banner's selection animation - // (which animates the banner's position) instead of staying at a stale static offset. - selectionFrame = new QFrame(bannerCardDisplayWidget); - selectionFrame->setAttribute(Qt::WA_TransparentForMouseEvents); - selectionFrame->setStyleSheet(QStringLiteral( - "QFrame { border: 2px solid palette(highlight); border-radius: 4px; background: transparent; }")); - selectionFrame->setVisible(false); - bannerCardDisplayWidget->installEventFilter(this); } void DeckPreviewWidget::retranslateUi() @@ -124,72 +69,9 @@ void DeckPreviewWidget::resizeEvent(QResizeEvent *event) if (bannerCardDisplayWidget == nullptr) { return; } - - const int width = bannerCardDisplayWidget->width(); - if (width == lastKnownBannerWidth) { - return; - } - lastKnownBannerWidth = width; - - for (QWidget *widget : fixedWidthChildren) { - widget->setMaximumWidth(width); - } - updateSelectionFrameGeometry(); -} - -bool DeckPreviewWidget::eventFilter(QObject *watched, QEvent *event) -{ - if (watched == bannerCardDisplayWidget && (event->type() == QEvent::Resize || event->type() == QEvent::Move)) { - updateSelectionFrameGeometry(); - } - return QWidget::eventFilter(watched, event); -} - -void DeckPreviewWidget::setShareSelectable(bool selectable) -{ - shareSelectable = selectable; - if (!selectable) { - setShareSelected(false); - } - updateSelectionStyle(); -} - -void DeckPreviewWidget::setShareSelected(bool selected) -{ - if (shareSelected == selected) { - return; - } - shareSelected = selected; - updateSelectionStyle(); - emit shareSelectionToggled(selected); -} - -bool DeckPreviewWidget::isShareSelected() const -{ - return shareSelected; -} - -bool DeckPreviewWidget::isShareSelectable() const -{ - return shareSelectable; -} - -void DeckPreviewWidget::updateSelectionFrameGeometry() -{ - if (selectionFrame == nullptr || bannerCardDisplayWidget == nullptr) { - return; - } - // Frame is a child of the banner, so it is positioned in banner coordinates and - // tracks the banner's selection animation automatically. A small inset keeps the - // highlight visible around the card art without occluding it. - selectionFrame->setGeometry(bannerCardDisplayWidget->rect().adjusted(1, 1, -1, -1)); - selectionFrame->raise(); -} - -void DeckPreviewWidget::updateSelectionStyle() -{ - if (selectionFrame != nullptr) { - selectionFrame->setVisible(shareSelectable && isShareSelected()); + QList widgets = findChildren(); + for (QWidget *widget : widgets) { + widget->setMaximumWidth(bannerCardDisplayWidget->width()); } } @@ -197,28 +79,83 @@ void DeckPreviewWidget::enterEvent(QEnterEvent *event) { QWidget::enterEvent(event); - // Don't do reloads until the deck has actually been loaded once. - reloadIfModified(); -} - -/** - * @brief The row of this deck in the source model, or -1 if it no longer exists. - */ -int DeckPreviewWidget::row() const -{ - return model->rowForFilePath(filePath); -} - -/** - * @brief The display name is given by the deck name, or the filename if the deck name is not set. - */ -QString DeckPreviewWidget::getDisplayName() const -{ - const int r = row(); - if (r == -1) { - return {}; + // don't do reloads until widgets have been created + if (bannerCardComboBox != nullptr) { + reloadIfModified(); } - return model->dataForRow(r).displayName; +} + +/** + * @brief Sets the lastModifiedTime to the value given by the file. + */ +void DeckPreviewWidget::updateLastModifiedTime() +{ + QFileInfo fileInfo(filePath); + lastModifiedTime = fileInfo.lastModified(); +} + +/** + * @brief Writes the current contents of the deck to file. Updates the lastModifiedTime afterward. + */ +void DeckPreviewWidget::writeDeckToFile() +{ + DeckLoader::saveToFile(deckLoader->getDeck()); + updateLastModifiedTime(); +} + +void DeckPreviewWidget::initializeUi(const bool deckLoadSuccess) +{ + if (!deckLoadSuccess) { + return; + } + + QFileInfo fileInfo(filePath); + lastModifiedTime = fileInfo.lastModified(); + + bannerCardDisplayWidget->setFontSize(24); + setFilePath(deckLoader->getDeck().lastLoadInfo.fileName); + + colorIdentityWidget = new ColorIdentityWidget(this); + deckTagsDisplayWidget = new DeckPreviewDeckTagsDisplayWidget(this); + connect(deckTagsDisplayWidget, &DeckPreviewDeckTagsDisplayWidget::tagsChanged, this, &DeckPreviewWidget::setTags); + + bannerCardLabel = new QLabel(this); + bannerCardLabel->setObjectName("bannerCardLabel"); + bannerCardComboBox = new QComboBox(this); + bannerCardComboBox->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); + bannerCardComboBox->setObjectName("bannerCardComboBox"); + bannerCardComboBox->installEventFilter(new NoScrollFilter(bannerCardComboBox)); + connect(bannerCardComboBox, QOverload::of(&QComboBox::currentIndexChanged), this, + &DeckPreviewWidget::setBannerCard); + + updateColorIdentityVisibility( + SettingsCache::instance().visualDeckStorage().getVisualDeckStorageShowColorIdentity()); + updateBannerCardComboBoxVisibility( + SettingsCache::instance().visualDeckStorage().getVisualDeckStorageShowBannerCardComboBox()); + updateTagsVisibility(SettingsCache::instance().visualDeckStorage().getVisualDeckStorageShowTagsOnDeckPreviews()); + + layout->addWidget(colorIdentityWidget); + layout->addWidget(deckTagsDisplayWidget); + layout->addWidget(bannerCardLabel); + layout->addWidget(bannerCardComboBox); + + retranslateUi(); + resyncWidgets(); +} + +/** + * @brief Syncs the contents of the child widgets with the current deck. + */ +void DeckPreviewWidget::resyncWidgets() +{ + auto bannerCardRef = deckLoader->getDeck().deckList.getBannerCard(); + auto bannerCard = bannerCardRef.name.isEmpty() ? ExactCard() : CardDatabaseManager::query()->getCard(bannerCardRef); + + bannerCardDisplayWidget->setCard(bannerCard); + refreshBannerCardText(); + updateBannerCardComboBox(bannerCardRef.name); + colorIdentityWidget->setColorIdentity(getColorIdentity()); + deckTagsDisplayWidget->setTags(deckLoader->getDeck().deckList.getTags()); } /** @@ -226,36 +163,33 @@ QString DeckPreviewWidget::getDisplayName() const */ void DeckPreviewWidget::reloadIfModified() { - const int r = row(); - if (r == -1 || !model->dataForRow(r).loadSucceeded) { + QFileInfo fileInfo(filePath); + QDateTime newLastModifiedTime = fileInfo.lastModified(); + + if (!newLastModifiedTime.isValid() || newLastModifiedTime <= lastModifiedTime) { return; } - model->reloadIfModified(r); + bool success = deckLoader->reload(); + + if (success) { + fileInfo.refresh(); + lastModifiedTime = fileInfo.lastModified(); + resyncWidgets(); + } } -/** - * @brief Syncs the contents of the child widgets with the current row's data. - */ -void DeckPreviewWidget::syncFromModel() +void DeckPreviewWidget::updateVisibility() { - const int r = row(); - if (r == -1) { - return; + setHidden(!checkVisibility()); +} + +bool DeckPreviewWidget::checkVisibility() const +{ + if (filteredBySearch || filteredByColor || filteredByTags) { + return false; } - - const DeckPreviewData &data = model->dataForRow(r); - filePath = data.filePath; - - const CardRef bannerCardRef = data.deck.deckList.getBannerCard(); - const ExactCard bannerCard = - bannerCardRef.name.isEmpty() ? ExactCard() : CardDatabaseManager::query()->getCard(bannerCardRef); - - bannerCardDisplayWidget->setCard(bannerCard); - refreshBannerCardText(); - updateBannerCardComboBox(bannerCardRef.name); - colorIdentityWidget->setColorIdentity(data.colorIdentity); - deckTagsDisplayWidget->setTags(data.tags); + return true; } void DeckPreviewWidget::updateColorIdentityVisibility(bool visible) @@ -295,6 +229,51 @@ void DeckPreviewWidget::updateTagsVisibility(bool visible) } } +QString DeckPreviewWidget::getColorIdentity() +{ + QStringList cardList = deckLoader->getDeck().deckList.getCardList({DECK_ZONE_MAIN, DECK_ZONE_SIDE}); + if (cardList.isEmpty()) { + return {}; + } + + QSet colorSet; // A set to collect unique color symbols (e.g., W, U, B, R, G) + + for (const QString &cardName : cardList) { + CardInfoPtr currentCard = CardDatabaseManager::query()->getCardInfo(cardName); + if (currentCard) { + QString colors = currentCard->getColors(); // Assuming this returns something like "WUB" + for (const QChar &color : colors) { + colorSet.insert(color); + } + } + } + + // Ensure the color identity is in WUBRG order + QString colorIdentity; + const QString wubrgOrder = "WUBRG"; + for (const QChar &color : wubrgOrder) { + if (colorSet.contains(color)) { + colorIdentity.append(color); + } + } + + return colorIdentity; +} + +/** + * The display name is given by the deck name, or the filename if the deck name is not set. + */ +QString DeckPreviewWidget::getDisplayName() const +{ + QString deckName = deckLoader->getDeck().deckList.getName(); + return !deckName.isEmpty() ? deckName : QFileInfo(deckLoader->getDeck().lastLoadInfo.fileName).fileName(); +} + +void DeckPreviewWidget::setFilePath(const QString &_filePath) +{ + filePath = _filePath; +} + /** * Refreshes the banner card text. * This also calls `refreshBannerCardToolTip`, since those two often need to be updated together. @@ -331,15 +310,11 @@ void DeckPreviewWidget::updateBannerCardComboBox(const QString ¤tText) // Prepare the new items with deduplication QSet> bannerCardSet; - const int r = row(); - if (r != -1) { - const DeckList &deckList = model->dataForRow(r).deck.deckList; - const QList cardsInDeck = deckList.getCardNodes(); + QList cardsInDeck = deckLoader->getDeck().deckList.getCardNodes(); - for (auto currentCard : cardsInDeck) { - for (int k = 0; k < currentCard->getNumber(); ++k) { - bannerCardSet.insert(QPair(currentCard->getName(), currentCard->getCardProviderId())); - } + for (auto currentCard : cardsInDeck) { + for (int k = 0; k < currentCard->getNumber(); ++k) { + bannerCardSet.insert(QPair(currentCard->getName(), currentCard->getCardProviderId())); } } @@ -352,16 +327,16 @@ void DeckPreviewWidget::updateBannerCardComboBox(const QString ¤tText) // This is *slightly* more performant than using addItem in a loop. - QStandardItemModel *comboModel = new QStandardItemModel(pairList.size(), 1, bannerCardComboBox); + QStandardItemModel *model = new QStandardItemModel(pairList.size(), 1, bannerCardComboBox); int row = 0; for (const auto &pair : pairList) { QStandardItem *item = new QStandardItem(pair.first); item->setData(QVariant::fromValue(pair), Qt::UserRole); - comboModel->setItem(row++, 0, item); + model->setItem(row++, 0, item); } - bannerCardComboBox->setModel(comboModel); + bannerCardComboBox->setModel(model); // Try to restore the previous selection by finding the currentText int restoredIndex = bannerCardComboBox->findText(currentText); @@ -369,9 +344,7 @@ void DeckPreviewWidget::updateBannerCardComboBox(const QString ¤tText) bannerCardComboBox->setCurrentIndex(restoredIndex); } else { // Add a placeholder "-" and set it as the current selection - const QString currentBannerCardName = - r == -1 ? QString() : model->dataForRow(r).deck.deckList.getBannerCard().name; - int bannerIndex = bannerCardComboBox->findText(currentBannerCardName); + int bannerIndex = bannerCardComboBox->findText(deckLoader->getDeck().deckList.getBannerCard().name); if (bannerIndex != -1) { bannerCardComboBox->setCurrentIndex(bannerIndex); } else { @@ -389,11 +362,8 @@ void DeckPreviewWidget::setBannerCard(int /* changedIndex */) { auto [name, id] = bannerCardComboBox->currentData().value>(); CardRef cardRef = {name, id}; - const int r = row(); - if (r == -1) { - return; - } - model->setBannerCard(r, cardRef); + deckLoader->getDeck().deckList.setBannerCard(cardRef); + writeDeckToFile(); bannerCardDisplayWidget->setCard(CardDatabaseManager::query()->getCard(cardRef)); } @@ -406,46 +376,26 @@ void DeckPreviewWidget::imageClickedEvent(QMouseEvent *event, DeckPreviewCardPic } } -void DeckPreviewWidget::imageSingleClicked() -{ - if (isShareSelectable()) { - setShareSelected(!isShareSelected()); - } -} - void DeckPreviewWidget::imageDoubleClickedEvent(QMouseEvent *event, DeckPreviewCardPictureWidget *instance) { Q_UNUSED(event); Q_UNUSED(instance); - if (isShareSelectable()) { - return; // in share mode a double click would just toggle a single selection - } emit deckLoadRequested(filePath); } void DeckPreviewWidget::setTags(const QStringList &tags) { - const int r = row(); - if (r != -1) { - model->setTags(r, tags); - } + deckLoader->getDeck().deckList.setTags(tags); + writeDeckToFile(); } QMenu *DeckPreviewWidget::createRightClickMenu() { - const int r = row(); - auto *menu = new QMenu(this); menu->setAttribute(Qt::WA_DeleteOnClose); - connect(menu->addAction(tr("Open in deck editor")), &QAction::triggered, this, [this, r] { - if (r != -1) { - emit openDeckEditor(model->deckForRow(r)); - } - }); - - connect(menu->addAction(tr("Share deck...")), &QAction::triggered, this, - [this] { emit shareDeckRequested(filePath); }); + connect(menu->addAction(tr("Open in deck editor")), &QAction::triggered, this, + [this] { emit openDeckEditor(deckLoader->getDeck()); }); connect(menu->addAction(tr("Edit Tags")), &QAction::triggered, deckTagsDisplayWidget, &DeckPreviewDeckTagsDisplayWidget::openTagEditDlg); @@ -458,26 +408,14 @@ QMenu *DeckPreviewWidget::createRightClickMenu() auto saveToClipboardMenu = menu->addMenu(tr("Save Deck to Clipboard")); - connect(saveToClipboardMenu->addAction(tr("Annotated")), &QAction::triggered, this, [this, r] { - if (r != -1) { - DeckLoader::saveToClipboard(model->dataForRow(r).deck.deckList, true, true); - } - }); - connect(saveToClipboardMenu->addAction(tr("Annotated (No set info)")), &QAction::triggered, this, [this, r] { - if (r != -1) { - DeckLoader::saveToClipboard(model->dataForRow(r).deck.deckList, true, false); - } - }); - connect(saveToClipboardMenu->addAction(tr("Not Annotated")), &QAction::triggered, this, [this, r] { - if (r != -1) { - DeckLoader::saveToClipboard(model->dataForRow(r).deck.deckList, false, true); - } - }); - connect(saveToClipboardMenu->addAction(tr("Not Annotated (No set info)")), &QAction::triggered, this, [this, r] { - if (r != -1) { - DeckLoader::saveToClipboard(model->dataForRow(r).deck.deckList, false, false); - } - }); + connect(saveToClipboardMenu->addAction(tr("Annotated")), &QAction::triggered, this, + [this] { DeckLoader::saveToClipboard(deckLoader->getDeck().deckList, true, true); }); + connect(saveToClipboardMenu->addAction(tr("Annotated (No set info)")), &QAction::triggered, this, + [this] { DeckLoader::saveToClipboard(deckLoader->getDeck().deckList, true, false); }); + connect(saveToClipboardMenu->addAction(tr("Not Annotated")), &QAction::triggered, this, + [this] { DeckLoader::saveToClipboard(deckLoader->getDeck().deckList, false, true); }); + connect(saveToClipboardMenu->addAction(tr("Not Annotated (No set info)")), &QAction::triggered, this, + [this] { DeckLoader::saveToClipboard(deckLoader->getDeck().deckList, false, false); }); menu->addSeparator(); @@ -512,58 +450,57 @@ void DeckPreviewWidget::addSetBannerCardMenu(QMenu *menu) void DeckPreviewWidget::actRenameDeck() { - const int r = row(); - if (r == -1) { - return; - } - // read input - const QString oldName = model->dataForRow(r).deckName; + const QString oldName = deckLoader->getDeck().deckList.getName(); bool ok; - QString newName = QInputDialog::getText(this, tr("Rename deck"), tr("New name:"), QLineEdit::Normal, oldName, &ok); + QString newName = QInputDialog::getText(this, "Rename deck", tr("New name:"), QLineEdit::Normal, oldName, &ok); if (!ok || oldName == newName) { return; } // write change - model->renameDeck(r, newName); + deckLoader->getDeck().deckList.setName(newName); + writeDeckToFile(); - // The banner card text updates via the model's dataChanged signal. + // update VDS + refreshBannerCardText(); } void DeckPreviewWidget::actRenameFile() { - const int r = row(); - if (r == -1) { - return; - } - // read input const auto info = QFileInfo(filePath); const QString oldName = info.baseName(); bool ok; - QString newName = QInputDialog::getText(this, tr("Rename file"), tr("New name:"), QLineEdit::Normal, oldName, &ok); + QString newName = QInputDialog::getText(this, "Rename file", tr("New name:"), QLineEdit::Normal, oldName, &ok); if (!ok || newName.isEmpty() || oldName == newName) { return; } - // write change - if (!model->renameFile(r, newName)) { - QMessageBox::critical(this, tr("Error"), tr("Rename failed")); + QString newFileName = newName; + if (!info.suffix().isEmpty()) { + newFileName += "." + info.suffix(); } - // The file path and banner card text update via the model's signals. + // write change + const QString newFilePath = QFileInfo(info.dir(), newFileName).filePath(); + if (!QFile::rename(info.filePath(), newFilePath)) { + QMessageBox::critical(this, tr("Error"), tr("Rename failed")); + return; + } + + deckLoader->getDeck().lastLoadInfo.fileName = newFilePath; + setFilePath(newFilePath); + + // update VDS + updateLastModifiedTime(); + refreshBannerCardText(); } void DeckPreviewWidget::actDeleteFile() { - const int r = row(); - if (r == -1) { - return; - } - // read input auto res = QMessageBox::warning(this, tr("Delete file"), tr("Are you sure you want to delete the selected file?"), QMessageBox::Yes | QMessageBox::No); @@ -572,22 +509,11 @@ void DeckPreviewWidget::actDeleteFile() } // write change - if (!model->deleteFile(r)) { + if (!QFile::remove(QFileInfo(filePath).filePath())) { QMessageBox::critical(this, tr("Error"), tr("Delete failed")); + return; } - // The folder widget removes this preview once the row is gone. -} - -/** - * Checks if the deck's file format supports tags. - * If not, then prompt the user for file conversion. - * @return whether the resulting file can support adding tags - */ -bool DeckPreviewWidget::promptFileConversionIfRequired() -{ - return DialogConvertDeckToCodFormat::promptIfRequired(this, filePath, [this] { - model->convertToCockatriceFormat(row()); - return true; - }); + // update VDS + this->deleteLater(); } diff --git a/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_widget.h b/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_widget.h index f4909eda8..de66c194b 100644 --- a/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_widget.h +++ b/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_widget.h @@ -2,12 +2,16 @@ * @file deck_preview_widget.h * @ingroup VisualDeckPreviewWidgets */ +//! \todo Document this file. #ifndef DECK_PREVIEW_WIDGET_H #define DECK_PREVIEW_WIDGET_H +#include "../../../deck_loader/deck_loader.h" +#include "../../cards/additional_info/color_identity_widget.h" #include "../../cards/deck_preview_card_picture_widget.h" -#include "../visual_deck_storage_model.h" +#include "../visual_deck_storage_widget.h" +#include "deck_preview_deck_tags_display_widget.h" #include #include @@ -16,100 +20,72 @@ #include #include -class QEnterEvent; -class QFrame; -class QLabel; class QMenu; -class QMouseEvent; -class ColorIdentityWidget; -class DeckPreviewCardPictureWidget; -class DeckPreviewDeckTagsDisplayWidget; -class VisualDeckStorageModel; class VisualDeckStorageWidget; +class DeckPreviewDeckTagsDisplayWidget; class DeckPreviewWidget final : public QWidget { Q_OBJECT public: - explicit DeckPreviewWidget(QWidget *parent, + explicit DeckPreviewWidget(QWidget *_parent, VisualDeckStorageWidget *_visualDeckStorageWidget, - VisualDeckStorageModel *_model, const QString &_filePath); void retranslateUi(); + QString getColorIdentity(); + [[nodiscard]] QString getDisplayName() const; - /** - * @brief The banner card picture; the parent widget wires its size to the card size setting. - */ - DeckPreviewCardPictureWidget *bannerCardDisplayWidget; - - /** @brief The path of the deck file backing this preview. */ + VisualDeckStorageWidget *visualDeckStorageWidget; + QVBoxLayout *layout; QString filePath; - - void setShareSelectable(bool selectable); - void setShareSelected(bool selected); - [[nodiscard]] bool isShareSelected() const; - [[nodiscard]] bool isShareSelectable() const; + QDateTime lastModifiedTime; + DeckLoader *deckLoader; + DeckPreviewCardPictureWidget *bannerCardDisplayWidget = nullptr; + ColorIdentityWidget *colorIdentityWidget = nullptr; + DeckPreviewDeckTagsDisplayWidget *deckTagsDisplayWidget = nullptr; + QLabel *bannerCardLabel = nullptr; + QComboBox *bannerCardComboBox = nullptr; + bool filteredBySearch = false; + bool filteredByColor = false; + bool filteredByTags = false; + [[nodiscard]] bool checkVisibility() const; signals: void deckLoadRequested(const QString &filePath); void openDeckEditor(const LoadedDeck &deck); - void shareDeckRequested(const QString &filePath); - void shareSelectionToggled(bool selected); public slots: - /** - * @brief Re-reads the row's data from the model and syncs every child widget. - * Connected to the model's dataChanged signal. - */ - void syncFromModel(); - - /** - * @brief Reloads the deck file if its modification time is newer than the stored one. - */ - void reloadIfModified(); + void setFilePath(const QString &filePath); + void refreshBannerCardText(); void refreshBannerCardToolTip(); + void updateBannerCardComboBox(const QString ¤tText); + void setBannerCard(int); + void imageClickedEvent(QMouseEvent *event, DeckPreviewCardPictureWidget *instance); + void imageDoubleClickedEvent(QMouseEvent *event, DeckPreviewCardPictureWidget *instance); + void initializeUi(bool deckLoadSuccess); + void resyncWidgets(); + void reloadIfModified(); + void updateVisibility(); void updateColorIdentityVisibility(bool visible); void updateBannerCardComboBoxVisibility(bool visible); void updateTagsVisibility(bool visible); - void setBannerCard(int); - void setTags(const QStringList &tags); + void resizeEvent(QResizeEvent *event) override; protected: void enterEvent(QEnterEvent *event) override; - void resizeEvent(QResizeEvent *event) override; - bool eventFilter(QObject *watched, QEvent *event) override; private: - [[nodiscard]] int row() const; - [[nodiscard]] QString getDisplayName() const; - void refreshBannerCardText(); - void updateBannerCardComboBox(const QString ¤tText); - bool promptFileConversionIfRequired(); + void updateLastModifiedTime(); + void writeDeckToFile(); QMenu *createRightClickMenu(); void addSetBannerCardMenu(QMenu *menu); - void imageClickedEvent(QMouseEvent *event, DeckPreviewCardPictureWidget *instance); - void imageSingleClicked(); - void imageDoubleClickedEvent(QMouseEvent *event, DeckPreviewCardPictureWidget *instance); + +private slots: + void setTags(const QStringList &tags); void actRenameDeck(); void actRenameFile(); void actDeleteFile(); - - VisualDeckStorageWidget *visualDeckStorageWidget; - VisualDeckStorageModel *model; - QVBoxLayout *layout; - ColorIdentityWidget *colorIdentityWidget; - DeckPreviewDeckTagsDisplayWidget *deckTagsDisplayWidget; - QLabel *bannerCardLabel; - QComboBox *bannerCardComboBox; - QList fixedWidthChildren; ///< Children clamped to the picture width on resize. - int lastKnownBannerWidth = -1; ///< The picture width last applied to the children. - QFrame *selectionFrame = nullptr; - bool shareSelectable = false; - bool shareSelected = false; - - void updateSelectionStyle(); - void updateSelectionFrameGeometry(); }; class NoScrollFilter : public QObject diff --git a/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/public_deck_preview_widget.cpp b/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/public_deck_preview_widget.cpp deleted file mode 100644 index b49d6f9f4..000000000 --- a/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/public_deck_preview_widget.cpp +++ /dev/null @@ -1,167 +0,0 @@ -#include "public_deck_preview_widget.h" - -#include "../../../../client/settings/cache_settings.h" -#include "../../cards/additional_info/color_identity_widget.h" -#include "../../cards/deck_preview_card_picture_widget.h" -#include "../../general/layout_containers/flow_widget.h" -#include "deck_preview_tag_display_widget.h" - -#include -#include -#include -#include -#include -#include -#include - -PublicDeckPreviewWidget::PublicDeckPreviewWidget(QWidget *parent, const RemotePublicDecksModel::DeckEntry &entry) - : QWidget(parent) -{ - bannerCardDisplayWidget = new DeckPreviewCardPictureWidget(this); - bannerCardDisplayWidget->setFontSize(24); - - // The whole tile is a single focusable, keyboard-operable control: Tab lands - // on it and Space/Enter opens the deck, mirroring the shared-deck preview tile. - setFocusPolicy(Qt::StrongFocus); - - uploadTimeLabel = new QLabel(this); - uploadTimeLabel->setAlignment(Qt::AlignHCenter); - - colorIdentityWidget = new ColorIdentityWidget(this); - - tagsFlowWidget = new FlowWidget(this, Qt::Horizontal, Qt::ScrollBarAlwaysOff, Qt::ScrollBarAsNeeded); - tagsFlowWidget->setSpacing(3, 3); - - auto *layout = new QVBoxLayout(this); - layout->setContentsMargins(0, 0, 0, 0); - layout->addWidget(bannerCardDisplayWidget); - layout->addWidget(uploadTimeLabel); - layout->addWidget(colorIdentityWidget); - layout->addWidget(tagsFlowWidget); - setLayout(layout); - - connect(&SettingsCache::instance().visualDeckStorage(), - &VisualDeckStorageSettings::visualDeckStorageShowColorIdentityChanged, this, - &PublicDeckPreviewWidget::updateColorIdentityVisibility); - connect(&SettingsCache::instance().visualDeckStorage(), - &VisualDeckStorageSettings::visualDeckStorageShowTagsOnDeckPreviewsChanged, this, - &PublicDeckPreviewWidget::updateTagsVisibility); - connect(&SettingsCache::instance().visualDeckStorage(), - &VisualDeckStorageSettings::visualDeckStorageShowUploadTimeChanged, this, - &PublicDeckPreviewWidget::updateUploadTimeVisibility); - - setEntry(entry); - - connect(bannerCardDisplayWidget, &DeckPreviewCardPictureWidget::imageClicked, this, - &PublicDeckPreviewWidget::imageClickedEvent); - connect(bannerCardDisplayWidget, &DeckPreviewCardPictureWidget::imageDoubleClicked, this, - &PublicDeckPreviewWidget::imageDoubleClickedEvent); - - // resizeEvent clamps every child to the banner picture's width, so collect them - // once here to keep the resize handler from searching the widget tree on every pass. - fixedWidthChildren = {bannerCardDisplayWidget, uploadTimeLabel, colorIdentityWidget, tagsFlowWidget}; -} - -void PublicDeckPreviewWidget::resizeEvent(QResizeEvent *event) -{ - QWidget::resizeEvent(event); - if (bannerCardDisplayWidget == nullptr) { - return; - } - - const int width = bannerCardDisplayWidget->width(); - if (width == lastKnownBannerWidth) { - return; - } - lastKnownBannerWidth = width; - - for (QWidget *widget : fixedWidthChildren) { - widget->setMaximumWidth(width); - } -} - -void PublicDeckPreviewWidget::setEntry(const RemotePublicDecksModel::DeckEntry &entry) -{ - deckId = entry.id; - - hasColorIdentity = !entry.colorIdentity.isEmpty(); - colorIdentityWidget->setColorIdentity(entry.colorIdentity); - updateColorIdentityVisibility(); - - const ExactCard bannerCard = - entry.bannerCardName.isEmpty() - ? ExactCard() - : CardDatabaseManager::query()->getCard(CardRef{entry.bannerCardName, entry.bannerCardProvider}); - bannerCardDisplayWidget->setCard(bannerCard); - - // The deck name is the overlay text on the banner, like the local preview. - bannerCardDisplayWidget->setOverlayText(entry.name); - // The deck name comes from another user's record, and Qt tooltips are - // rendered as AutoText, so escape and bound it to keep it readable text - // (the overlay painted onto the banner is already a plain painter draw). - setToolTip(entry.name.left(200).toHtmlEscaped()); - setBaseAccessibleName(entry.name); - - tagsFlowWidget->clearLayout(); - for (const QString &tag : entry.tags) { - auto *chip = new DeckPreviewTagDisplayWidget(tagsFlowWidget, tag); - chip->setAttribute(Qt::WA_TransparentForMouseEvents); - tagsFlowWidget->addWidget(chip); - } - hasTags = !entry.tags.isEmpty(); - updateTagsVisibility(); - - uploadTimeLabel->setText(tr("Uploaded %1").arg(entry.uploadTime.toString(Qt::TextDate))); - hasUploadTime = !entry.uploadTime.isNull(); - updateUploadTimeVisibility(); -} - -void PublicDeckPreviewWidget::updateColorIdentityVisibility() -{ - colorIdentityWidget->setVisible( - hasColorIdentity && SettingsCache::instance().visualDeckStorage().getVisualDeckStorageShowColorIdentity()); -} - -void PublicDeckPreviewWidget::updateTagsVisibility() -{ - tagsFlowWidget->setVisible( - hasTags && SettingsCache::instance().visualDeckStorage().getVisualDeckStorageShowTagsOnDeckPreviews()); -} - -void PublicDeckPreviewWidget::updateUploadTimeVisibility() -{ - uploadTimeLabel->setVisible(hasUploadTime && - SettingsCache::instance().visualDeckStorage().getVisualDeckStorageShowUploadTime()); -} - -void PublicDeckPreviewWidget::keyPressEvent(QKeyEvent *event) -{ - if (event->key() == Qt::Key_Space || event->key() == Qt::Key_Enter || event->key() == Qt::Key_Return) { - event->accept(); - emit openDeckRequested(deckId); - return; - } - QWidget::keyPressEvent(event); -} - -void PublicDeckPreviewWidget::setBaseAccessibleName(const QString &name) -{ - baseAccessibleName = name; - setAccessibleName(name); -} - -void PublicDeckPreviewWidget::setScaleFactor(int scale) -{ - bannerCardDisplayWidget->setScaleFactor(scale); -} - -void PublicDeckPreviewWidget::imageClickedEvent(QMouseEvent * /*event*/, DeckPreviewCardPictureWidget * /*instance*/) -{ - // Reserved: clicking could show a card popup for the banner card. -} - -void PublicDeckPreviewWidget::imageDoubleClickedEvent(QMouseEvent * /*event*/, - DeckPreviewCardPictureWidget * /*instance*/) -{ - emit openDeckRequested(deckId); -} diff --git a/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/public_deck_preview_widget.h b/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/public_deck_preview_widget.h deleted file mode 100644 index a0e5dd43a..000000000 --- a/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/public_deck_preview_widget.h +++ /dev/null @@ -1,75 +0,0 @@ -/** - * @file public_deck_preview_widget.h - * @ingroup VisualDeckPreviewWidgets - */ - -#ifndef PUBLIC_DECK_PREVIEW_WIDGET_H -#define PUBLIC_DECK_PREVIEW_WIDGET_H - -#include "../remote_public_decks_model.h" - -#include -#include -#include - -class ColorIdentityWidget; -class DeckPreviewCardPictureWidget; -class FlowWidget; -class QKeyEvent; -class QLabel; -class QMouseEvent; -class QResizeEvent; - -/** - * @brief A preview tile for a public deck published by another user. - * - * Renders the banner card picture (looked up by name/provider in the card - * database) with the deck name overlaid, the color identity, the deck's tags - * (read-only) and its upload time, all from the metadata the server stores for - * the deck, so no deck list is downloaded until the user actually opens the - * deck. Double-clicking the banner requests opening it. - */ -class PublicDeckPreviewWidget final : public QWidget -{ - Q_OBJECT - -public: - explicit PublicDeckPreviewWidget(QWidget *parent, const RemotePublicDecksModel::DeckEntry &entry); - - void setEntry(const RemotePublicDecksModel::DeckEntry &entry); - - /** @brief Sets the accessible name announced to assistive technologies. */ - void setBaseAccessibleName(const QString &name); - - /** @brief Scales the banner card picture, mirroring the Visual Deck Storage. */ - void setScaleFactor(int scale); - -signals: - void openDeckRequested(int deckId); - -protected: - void resizeEvent(QResizeEvent *event) override; - void keyPressEvent(QKeyEvent *event) override; - -private slots: - void imageClickedEvent(QMouseEvent *event, DeckPreviewCardPictureWidget *instance); - void imageDoubleClickedEvent(QMouseEvent *event, DeckPreviewCardPictureWidget *instance); - void updateColorIdentityVisibility(); - void updateTagsVisibility(); - void updateUploadTimeVisibility(); - -private: - int deckId = 0; - QString baseAccessibleName; - bool hasColorIdentity = false; - bool hasTags = false; - bool hasUploadTime = false; - int lastKnownBannerWidth = 0; - QList fixedWidthChildren; - DeckPreviewCardPictureWidget *bannerCardDisplayWidget; - ColorIdentityWidget *colorIdentityWidget; - FlowWidget *tagsFlowWidget; - QLabel *uploadTimeLabel; -}; - -#endif // PUBLIC_DECK_PREVIEW_WIDGET_H diff --git a/cockatrice/src/interface/widgets/visual_deck_storage/remote_public_decks_model.cpp b/cockatrice/src/interface/widgets/visual_deck_storage/remote_public_decks_model.cpp deleted file mode 100644 index a1cabd729..000000000 --- a/cockatrice/src/interface/widgets/visual_deck_storage/remote_public_decks_model.cpp +++ /dev/null @@ -1,220 +0,0 @@ -#include "remote_public_decks_model.h" - -#include "../../../client/settings/cache_settings.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -RemotePublicDecksModel::RemotePublicDecksModel(AbstractClient *_client, QObject *parent) - : QAbstractListModel(parent), client(_client) -{ - // The ping sweep can drop a pending command without ever emitting finished, - // so loading must not be a latch: time it out and clear it when the client - // goes away, or the tab is stuck on the loading state for the session. - loadingTimeoutTimer = new QTimer(this); - loadingTimeoutTimer->setSingleShot(true); - loadingTimeoutTimer->setInterval( - static_cast((static_cast(SettingsCache::instance().network().getTimeOut()) + 1) * - SettingsCache::instance().network().getKeepAlive() * 1000)); - connect(loadingTimeoutTimer, &QTimer::timeout, this, &RemotePublicDecksModel::onLoadingTimeout); - connect(client, &AbstractClient::statusChanged, this, [this](ClientStatus status) { - if (status == StatusDisconnected) { - loadingTimeoutTimer->stop(); - setLoading(false); - } - }); -} - -int RemotePublicDecksModel::rowCount(const QModelIndex &parent) const -{ - return parent.isValid() ? 0 : visibleIndices.size(); -} - -QVariant RemotePublicDecksModel::data(const QModelIndex &index, int role) const -{ - if (!index.isValid() || index.row() < 0 || index.row() >= visibleIndices.size()) { - return QVariant(); - } - if (role == Qt::DisplayRole || role == Qt::ToolTipRole) { - return decks.at(visibleIndices.at(index.row())).name; - } - return QVariant(); -} - -RemotePublicDecksModel::DeckEntry RemotePublicDecksModel::entryAt(int row) const -{ - if (row < 0 || row >= visibleIndices.size()) { - return DeckEntry{}; - } - return decks.at(visibleIndices.at(row)); -} - -void RemotePublicDecksModel::setSearchText(const QString &text) -{ - searchText = text.trimmed(); - rebuildVisibleIndices(); -} - -void RemotePublicDecksModel::setColorFilter(VisualDeckStorageSortFilterProxyModel::FilterMode mode, - const QSet &colors) -{ - colorFilterMode = mode; - activeColors = colors; - rebuildVisibleIndices(); -} - -void RemotePublicDecksModel::setTagFilter(const QSet &selected, const QSet &excluded) -{ - includedTags = selected; - excludedTags = excluded; - rebuildVisibleIndices(); -} - -QSet RemotePublicDecksModel::allTags() const -{ - QSet all; - for (const DeckEntry &entry : decks) { - all.unite(QSet(entry.tags.cbegin(), entry.tags.cend())); - } - return all; -} - -void RemotePublicDecksModel::rebuildVisibleIndices() -{ - QList newIndices; - newIndices.reserve(decks.size()); - for (int row = 0; row < decks.size(); ++row) { - const DeckEntry &entry = decks.at(row); - - if (!searchText.isEmpty() && !entry.name.contains(searchText, Qt::CaseInsensitive)) { - continue; - } - - if (!activeColors.isEmpty()) { - const QString &identity = entry.colorIdentity; - if (!colorIdentityMatches(colorFilterMode, activeColors, identity)) { - continue; - } - } - - if (!includedTags.isEmpty()) { - const QSet entryTags(entry.tags.cbegin(), entry.tags.cend()); - bool hasAll = std::all_of(includedTags.begin(), includedTags.end(), - [&entryTags](const QString &tag) { return entryTags.contains(tag); }); - if (!hasAll) { - continue; - } - } - - if (!excludedTags.isEmpty() && std::any_of(excludedTags.begin(), excludedTags.end(), - [&entry](const QString &tag) { return entry.tags.contains(tag); })) { - continue; - } - - newIndices.append(row); - } - - beginResetModel(); - visibleIndices = newIndices; - endResetModel(); -} - -void RemotePublicDecksModel::refresh(const QString &userName) -{ - if (loading) { - return; - } - // Every refresh captures its own request id so a reply that lands after its - // loading timeout (the reverse of the ping sweep dropping the command) is - // recognised as stale: it must not stop the newer request's timer or paint - // the grid with out-of-date data. - const int seq = ++requestSequence; - setLoading(true); - loadingTimeoutTimer->start(); - Command_DeckListOtherUser cmd; - cmd.set_user_name(userName.toStdString()); - PendingCommand *pend = client->prepareSessionCommand(cmd); - connect(pend, &PendingCommand::finished, this, - [this, seq](const Response &response, const CommandContainer &commandContainer) { - if (seq != requestSequence) { - return; // a newer refresh superseded this one - } - decksReceived(response, commandContainer); - }); - client->sendCommand(pend); -} - -void RemotePublicDecksModel::onLoadingTimeout() -{ - setLoading(false); - emit loadFailed(tr("The server did not respond in time. Try again.")); -} - -void RemotePublicDecksModel::clear() -{ - decks.clear(); - rebuildVisibleIndices(); -} - -void RemotePublicDecksModel::setLoading(bool value) -{ - if (loading == value) { - return; - } - loading = value; - emit loadingChanged(loading); -} - -void RemotePublicDecksModel::decksReceived(const Response &response, const CommandContainer & /*commandContainer*/) -{ - setLoading(false); - loadingTimeoutTimer->stop(); - if (response.response_code() != Response::RespOk) { - emit loadFailed(tr("Failed to load the user's public decks (server response code %1).") - .arg(QString::number(static_cast(response.response_code())))); - return; - } - - const Response_DeckList &resp = response.GetExtension(Response_DeckList::ext); - decks.clear(); - addFolder(resp.root()); - rebuildVisibleIndices(); -} - -void RemotePublicDecksModel::addFolder(const ServerInfo_DeckStorage_Folder &folder) -{ - const int itemCount = folder.items_size(); - for (int i = 0; i < itemCount; ++i) { - addTreeItem(folder.items(i)); - } -} - -void RemotePublicDecksModel::addTreeItem(const ServerInfo_DeckStorage_TreeItem &item) -{ - if (item.has_folder()) { - addFolder(item.folder()); - return; - } - - const ServerInfo_DeckStorage_File &file = item.file(); - DeckEntry entry; - entry.id = item.id(); - entry.name = QString::fromStdString(item.name()); - entry.uploadTime = QDateTime::fromSecsSinceEpoch(file.creation_time()); - entry.bannerCardName = QString::fromStdString(file.banner_card_name()); - entry.bannerCardProvider = QString::fromStdString(file.banner_card_provider()); - entry.colorIdentity = QString::fromStdString(file.color_identity()); - QStringList tags; - for (const auto &tag : file.tags()) { - tags.append(QString::fromStdString(tag)); - } - entry.tags = tags; - decks.append(entry); -} diff --git a/cockatrice/src/interface/widgets/visual_deck_storage/remote_public_decks_model.h b/cockatrice/src/interface/widgets/visual_deck_storage/remote_public_decks_model.h deleted file mode 100644 index eeb606442..000000000 --- a/cockatrice/src/interface/widgets/visual_deck_storage/remote_public_decks_model.h +++ /dev/null @@ -1,129 +0,0 @@ -/** - * @file remote_public_decks_model.h - * @ingroup DeckStorageWidgets - */ - -#ifndef REMOTE_PUBLIC_DECKS_MODEL_H -#define REMOTE_PUBLIC_DECKS_MODEL_H - -#include "visual_deck_storage_sort_filter_proxy_model.h" - -#include -#include -#include -#include -#include - -class AbstractClient; -class CommandContainer; -class QTimer; -class Response; -class ServerInfo_DeckStorage_Folder; -class ServerInfo_DeckStorage_TreeItem; - -/** - * @brief Flat, read-only list of the public decks published by another user. - * - * Fetches the target user's public decks via Command_DeckListOtherUser and - * flattens the response tree into entries carrying the preview metadata stored - * on the server (banner card name/provider and color identity). No deck list is - * downloaded until the user actually opens a deck. - * - * Name and color-identity filtering is applied against this metadata, mirroring - * the Visual Deck Storage's filter semantics, so the grid can be narrowed like - * the local deck storage. - */ -class RemotePublicDecksModel : public QAbstractListModel -{ - Q_OBJECT - -public: - struct DeckEntry - { - int id = 0; - QString name; - QDateTime uploadTime; - QString bannerCardName; - QString bannerCardProvider; - QString colorIdentity; - QStringList tags; - }; - - /** - * @brief The color identity filter mode, shared with the Visual Deck Storage. - */ - using FilterMode = VisualDeckStorageSortFilterProxyModel::FilterMode; - - explicit RemotePublicDecksModel(AbstractClient *client, QObject *parent = nullptr); - - [[nodiscard]] int rowCount(const QModelIndex &parent = QModelIndex()) const override; - [[nodiscard]] QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; - - /** @brief Fetches the public decks of another user, replacing the current contents. */ - void refresh(const QString &userName); - void clear(); - - /** @brief Sets a case-insensitive substring filter on the deck name. */ - void setSearchText(const QString &text); - - /** @brief Sets the active color identity filter and mode. */ - void setColorFilter(FilterMode mode, const QSet &colors); - - /** @brief Filters decks by required (`selected`) and forbidden (`excluded`) tags. */ - void setTagFilter(const QSet &selected, const QSet &excluded); - - /** @brief All tags present across all loaded decks, for building filter chips. */ - [[nodiscard]] QSet allTags() const; - - /** @brief The number of decks after filtering. */ - [[nodiscard]] int filteredCount() const - { - return visibleIndices.size(); - } - - /** @brief The number of decks before filtering. */ - [[nodiscard]] int totalCount() const - { - return decks.size(); - } - - /** @brief True while a refresh request is in flight and the grid has no data yet. */ - [[nodiscard]] bool isLoading() const - { - return loading; - } - - [[nodiscard]] DeckEntry entryAt(int row) const; - -signals: - /** @brief Emitted when a refresh starts, completes, or fails (see loading()). */ - void loadingChanged(bool loading); - - /** @brief Emitted when the last refresh failed; contains a user-facing message. */ - void loadFailed(const QString &message); - -private slots: - void decksReceived(const Response &response, const CommandContainer &commandContainer); - void onLoadingTimeout(); - -private: - void addFolder(const ServerInfo_DeckStorage_Folder &folder); - void addTreeItem(const ServerInfo_DeckStorage_TreeItem &item); - void rebuildVisibleIndices(); - void setLoading(bool value); - - AbstractClient *client; - QTimer *loadingTimeoutTimer; - QList decks; - QList visibleIndices; ///< Row indices into `decks` that pass the current filters. - bool loading = false; - int requestSequence = 0; ///< Monotonically increases per refresh; only the newest request may update the grid. - - QString searchText; - VisualDeckStorageSortFilterProxyModel::FilterMode colorFilterMode = VisualDeckStorageSortFilterProxyModel::Includes; - QSet activeColors; - QSet includedTags; - QSet excludedTags; -}; - -#endif // REMOTE_PUBLIC_DECKS_MODEL_H diff --git a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_folder_display_widget.cpp b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_folder_display_widget.cpp index 911f3dee9..cc7f07871 100644 --- a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_folder_display_widget.cpp +++ b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_folder_display_widget.cpp @@ -1,27 +1,20 @@ #include "visual_deck_storage_folder_display_widget.h" -#include "../cards/card_info_picture_widget.h" -#include "../general/display/banner_widget.h" -#include "../general/layout_containers/flow_widget.h" +#include "../../../client/settings/cache_settings.h" #include "deck_preview/deck_preview_widget.h" -#include "visual_deck_storage_model.h" -#include "visual_deck_storage_quick_settings_widget.h" -#include "visual_deck_storage_sort_filter_proxy_model.h" #include "visual_deck_storage_widget.h" -#include -#include -#include -#include +#include +#include +#include VisualDeckStorageFolderDisplayWidget::VisualDeckStorageFolderDisplayWidget( QWidget *parent, VisualDeckStorageWidget *_visualDeckStorageWidget, - const QString &_folderPath, + QString _filePath, bool canBeHidden, bool _showFolders) - : QWidget(parent), showFolders(_showFolders), folderPath(_folderPath), - visualDeckStorageWidget(_visualDeckStorageWidget) + : QWidget(parent), showFolders(_showFolders), visualDeckStorageWidget(_visualDeckStorageWidget), filePath(_filePath) { layout = new QVBoxLayout(this); setLayout(layout); @@ -29,9 +22,6 @@ VisualDeckStorageFolderDisplayWidget::VisualDeckStorageFolderDisplayWidget( header = new BannerWidget(this, ""); header->setClickable(canBeHidden); header->setHidden(!showFolders); - - const QString bannerText = folderPath.isEmpty() ? tr("Deck Storage") : folderPath; - header->setText(bannerText); layout->addWidget(header); container = new QWidget(this); @@ -45,305 +35,192 @@ VisualDeckStorageFolderDisplayWidget::VisualDeckStorageFolderDisplayWidget( flowWidget = new FlowWidget(this, Qt::Horizontal, Qt::ScrollBarAlwaysOff, Qt::ScrollBarAlwaysOff); containerLayout->addWidget(flowWidget); - auto *proxy = visualDeckStorageWidget->proxyModel(); - // A burst of proxy changes (one dataChanged per finished deck load, plus the filter - // invalidations) coalesces into a single reconcile, so a scan of many decks doesn't - // rebuild the flow layout once per deck. - connect(proxy, &QAbstractItemModel::modelReset, this, &VisualDeckStorageFolderDisplayWidget::scheduleReconcile); - connect(proxy, &QAbstractItemModel::rowsInserted, this, &VisualDeckStorageFolderDisplayWidget::scheduleReconcile); - connect(proxy, &QAbstractItemModel::rowsRemoved, this, &VisualDeckStorageFolderDisplayWidget::scheduleReconcile); - connect(proxy, &QAbstractItemModel::dataChanged, this, &VisualDeckStorageFolderDisplayWidget::scheduleReconcile); - connect(proxy, &QAbstractItemModel::layoutChanged, this, &VisualDeckStorageFolderDisplayWidget::scheduleReconcile); + createWidgetsForFiles(); + createWidgetsForFolders(); - reconcileTimer = new QTimer(this); - reconcileTimer->setSingleShot(true); - reconcileTimer->setInterval(150); - connect(reconcileTimer, &QTimer::timeout, this, &VisualDeckStorageFolderDisplayWidget::reconcile); - - // Building the whole folder subtree synchronously here would stall the ui thread on large - // collections, so the first reconcile runs as a chunked pass on later event loop turns. - scheduleReconcile(); + refreshUi(); } -void VisualDeckStorageFolderDisplayWidget::scheduleReconcile() +void VisualDeckStorageFolderDisplayWidget::refreshUi() { - if (deckPassActive) { - // The active pass may be scanning stale model state, so restart it from a clean - // slate once the current chunk yields. - deckPassRestartRequested = true; - return; + QString bannerText = tr("Deck Storage"); + QString deckPath = SettingsCache::instance().paths().getDeckPath(); + if (filePath != deckPath) { + QString relativePath = filePath; + + if (filePath.startsWith(deckPath)) { + relativePath = filePath.mid(deckPath.length()); // Remove the deckPath prefix + if (relativePath.startsWith('/')) { + relativePath.remove(0, 1); // Remove leading '/' if it exists + } + } + + bannerText = relativePath; } - reconcileTimer->start(); + header->setText(bannerText); } /** - * @brief Starts a new chunked scan of the source model, yielding to the event loop between chunks. - */ -void VisualDeckStorageFolderDisplayWidget::reconcile() -{ - beginDeckPass(); -} - -void VisualDeckStorageFolderDisplayWidget::beginDeckPass() -{ - deckPassActive = true; - deckPassRestartRequested = false; - deckPassRow = 0; - visibleDeckCount = 0; - deckPassPresentPaths.clear(); - - continueDeckPass(); -} - -void VisualDeckStorageFolderDisplayWidget::continueDeckPass() -{ - if (!deckPassActive) { - return; - } - - QElapsedTimer passTimer; - passTimer.start(); - - auto *proxy = visualDeckStorageWidget->proxyModel(); - const int proxyRowCount = proxy->rowCount(); - - // Scan rows of this folder, creating missing previews, until the time budget for this - // event loop turn runs out. The rest continues on the next turn. - while (deckPassRow < proxyRowCount) { - const int row = deckPassRow++; - const QModelIndex index = proxy->index(row, 0); - if (showFolders && index.data(VisualDeckStorageRoles::FolderPathRole).toString() != folderPath) { - continue; - } - const QString filePath = index.data(VisualDeckStorageRoles::FilePathRole).toString(); - deckPassPresentPaths.insert(filePath); - - DeckPreviewWidget *deckPreviewWidget = deckWidgets.value(filePath, nullptr); - if (!deckPreviewWidget) { - deckPreviewWidget = createDeckPreviewWidget(filePath); - flowWidget->addWidget(deckPreviewWidget); - } - - const bool matches = index.data(VisualDeckStorageRoles::FilterMatchRole).toBool(); - deckPreviewWidget->setVisible(matches); - if (!matches) { - // A deck that no longer matches the filters is dropped from the selection so its - // highlight cannot linger on an invisible preview or be counted in the share. - deckPreviewWidget->setShareSelected(false); - } - if (matches) { - ++visibleDeckCount; - } - - if (passTimer.elapsed() >= DECK_PASS_TIME_BUDGET_MS) { - break; - } - } - - if (deckPassRestartRequested) { - beginDeckPass(); - return; - } - - if (deckPassRow < proxyRowCount) { - QMetaObject::invokeMethod(this, &VisualDeckStorageFolderDisplayWidget::continueDeckPass, Qt::QueuedConnection); - return; - } - - finishDeckPass(); -} - -void VisualDeckStorageFolderDisplayWidget::finishDeckPass() -{ - auto *proxy = visualDeckStorageWidget->proxyModel(); - - // Drop previews of decks that no longer exist in the model. - for (auto it = deckWidgets.begin(); it != deckWidgets.end();) { - if (!deckPassPresentPaths.contains(it.key())) { - flowWidget->removeWidget(it.value()); - it.value()->deleteLater(); - it = deckWidgets.erase(it); - } else { - ++it; - } - } - - // Order the flow layout like the proxy sorts its rows. Filtered-out decks stay part of - // the layout, hidden in their sorted place until a filter lets them through again. - QStringList orderedFilePaths; - orderedFilePaths.reserve(proxy->rowCount()); - for (int proxyRow = 0; proxyRow < proxy->rowCount(); ++proxyRow) { - const QString filePath = proxy->index(proxyRow, 0).data(VisualDeckStorageRoles::FilePathRole).toString(); - if (deckWidgets.contains(filePath)) { - orderedFilePaths.append(filePath); - } - } - - // Re-add all widgets so the flow layout order matches the proxy order. Skipped when the - // order is unchanged so that data-only updates don't invalidate the flow layout. - if (orderedFilePaths != lastOrderedFilePaths) { - for (const QString &filePath : orderedFilePaths) { - flowWidget->removeWidget(deckWidgets.value(filePath)); - } - for (const QString &filePath : orderedFilePaths) { - flowWidget->addWidget(deckWidgets.value(filePath)); - } - lastOrderedFilePaths = orderedFilePaths; - } - - createSubFolderWidgets(); - - // Mark completion before evaluating visibility so this pass's own numbers decide whether - // the folder has content. The flag only guards evaluations made *during* a build. - deckPassActive = false; - initialPassCompleted = true; - - refreshVisibility(); -} - -/** - * @brief Creates a deck preview widget and wires it up to the storage widget. + * Gets all files in the directory that have an accepted decklist file extension * - * @param filePath The absolute path of the deck file to preview. + * @param filePath The directory to search through + * @param recursive Whether to search through subdirectories */ -DeckPreviewWidget *VisualDeckStorageFolderDisplayWidget::createDeckPreviewWidget(const QString &filePath) +static QStringList getAllFiles(const QString &filePath, bool recursive) { - auto *deckPreviewWidget = - new DeckPreviewWidget(flowWidget, visualDeckStorageWidget, visualDeckStorageWidget->model(), filePath); - connect(deckPreviewWidget, &DeckPreviewWidget::deckLoadRequested, visualDeckStorageWidget, - &VisualDeckStorageWidget::deckLoadRequested); - connect(deckPreviewWidget, &DeckPreviewWidget::openDeckEditor, visualDeckStorageWidget, - &VisualDeckStorageWidget::openDeckEditor); - connect(deckPreviewWidget, &DeckPreviewWidget::shareDeckRequested, visualDeckStorageWidget, - &VisualDeckStorageWidget::shareDeckRequested); - connect(deckPreviewWidget, &DeckPreviewWidget::shareSelectionToggled, visualDeckStorageWidget, - &VisualDeckStorageWidget::shareSelectionChanged); - deckPreviewWidget->setShareSelectable(visualDeckStorageWidget->isShareSelectable()); - connect(visualDeckStorageWidget->settings(), &VisualDeckStorageQuickSettingsWidget::cardSizeChanged, - deckPreviewWidget->bannerCardDisplayWidget, &CardInfoPictureWidget::setScaleFactor); - deckPreviewWidget->bannerCardDisplayWidget->setScaleFactor(visualDeckStorageWidget->settings()->getCardSize()); - deckWidgets.insert(filePath, deckPreviewWidget); - return deckPreviewWidget; + QStringList allFiles; + + // QDirIterator with QDir::Files ensures only files are listed (no directories) + auto flags = + recursive ? QDirIterator::Subdirectories | QDirIterator::FollowSymlinks : QDirIterator::NoIteratorFlags; + QDirIterator it(filePath, DeckLoader::ACCEPTED_FILE_EXTENSIONS, QDir::Files, flags); + + while (it.hasNext()) { + allFiles << it.next(); // Add each file path to the list + } + + return allFiles; } -void VisualDeckStorageFolderDisplayWidget::setShareSelectable(bool selectable) +void VisualDeckStorageFolderDisplayWidget::createWidgetsForFiles() { - const auto previews = flowWidget->findChildren(); - for (DeckPreviewWidget *preview : previews) { - preview->setShareSelectable(selectable); + QList allDecks; + for (const QString &file : getAllFiles(filePath, !showFolders)) { + auto *display = new DeckPreviewWidget(flowWidget, visualDeckStorageWidget, file); + + connect(display, &DeckPreviewWidget::deckLoadRequested, visualDeckStorageWidget, + &VisualDeckStorageWidget::deckLoadRequested); + connect(display, &DeckPreviewWidget::openDeckEditor, visualDeckStorageWidget, + &VisualDeckStorageWidget::openDeckEditor); + connect(visualDeckStorageWidget->settings(), &VisualDeckStorageQuickSettingsWidget::cardSizeChanged, + display->bannerCardDisplayWidget, &CardInfoPictureWidget::setScaleFactor); + display->bannerCardDisplayWidget->setScaleFactor(visualDeckStorageWidget->settings()->getCardSize()); + allDecks.append(display); } - const auto subFolders = findChildren(); - for (VisualDeckStorageFolderDisplayWidget *subFolder : subFolders) { - subFolder->setShareSelectable(selectable); + + flowWidget->clearLayout(); // Clear existing widgets in the flow layout + + for (DeckPreviewWidget *deck : allDecks) { + flowWidget->addWidget(deck); } } /** - * @brief Creates, removes and keeps in sync the subfolder widgets of this folder. + * Updates the visibility of this folder and all its DeckPreviewWidgets * - * Only direct child folders are created here; each child manages its own - * children, mirroring the folder tree on disk. + * @param recursive Also update the visibility of all subfolders and their DeckPreviewWidgets. */ -void VisualDeckStorageFolderDisplayWidget::createSubFolderWidgets() +void VisualDeckStorageFolderDisplayWidget::updateVisibility(bool recursive) +{ + bool atLeastOneWidgetVisible = checkVisibility(); + if (atLeastOneWidgetVisible) { + setVisible(true); + for (DeckPreviewWidget *display : flowWidget->findChildren()) { + display->updateVisibility(); + } + if (recursive) { + for (auto *subFolder : findChildren()) { + subFolder->updateVisibility(false); + } + } + } else { + setVisible(false); + } +} + +bool VisualDeckStorageFolderDisplayWidget::checkVisibility() +{ + bool atLeastOneWidgetVisible = false; + if (flowWidget) { + // Iterate through all DeckPreviewWidgets + for (DeckPreviewWidget *display : flowWidget->findChildren()) { + if (display->checkVisibility()) { + atLeastOneWidgetVisible = true; + } + } + } + for (VisualDeckStorageFolderDisplayWidget *subFolder : findChildren()) { + if (subFolder->checkVisibility()) { + atLeastOneWidgetVisible = true; + } + } + return atLeastOneWidgetVisible; +} + +static QStringList getAllSubFolders(const QString &filePath) +{ + QStringList allFolders; + + // QDirIterator with QDir::Files ensures only files are listed (no directories) + QDirIterator it(filePath, QDir::Dirs | QDir::NoDotAndDotDot); + + while (it.hasNext()) { + allFolders << it.next(); // Add each file path to the list + } + + return allFolders; +} + +void VisualDeckStorageFolderDisplayWidget::createWidgetsForFolders() { if (!showFolders) { return; } - const QStringList children = childFolderPaths(); - - for (auto it = subFolderWidgets.begin(); it != subFolderWidgets.end();) { - if (!children.contains(it.key())) { - containerLayout->removeWidget(it.value()); - it.value()->deleteLater(); - it = subFolderWidgets.erase(it); - } else { - ++it; - } - } - - for (const QString &child : children) { - if (subFolderWidgets.contains(child)) { - continue; - } - - auto *subFolderWidget = - new VisualDeckStorageFolderDisplayWidget(this, visualDeckStorageWidget, child, true, showFolders); - connect(subFolderWidget, &VisualDeckStorageFolderDisplayWidget::contentVisibilityChanged, this, - &VisualDeckStorageFolderDisplayWidget::refreshVisibility); - containerLayout->addWidget(subFolderWidget); - subFolderWidgets.insert(child, subFolderWidget); + for (const QString &dir : getAllSubFolders(filePath)) { + auto *display = new VisualDeckStorageFolderDisplayWidget(this, visualDeckStorageWidget, dir, true, showFolders); + containerLayout->addWidget(display); } } void VisualDeckStorageFolderDisplayWidget::updateShowFolders(bool enabled) { showFolders = enabled; - header->setHidden(!showFolders); if (!showFolders) { - for (auto it = subFolderWidgets.begin(); it != subFolderWidgets.end(); ++it) { - containerLayout->removeWidget(it.value()); - it.value()->deleteLater(); - } - subFolderWidgets.clear(); + flattenFolderStructure(); + } else { + // if setting was switched from disabled to enabled, we assume that there aren't any existing subfolders + createWidgetsForFiles(); + createWidgetsForFolders(); } - scheduleReconcile(); + header->setHidden(!showFolders); } /** - * @brief Hides the folder when it contains nothing visible, and reports the change upward. + * Steals all DeckPreviewWidgets from this widget's nested subfolders, and deletes those subfolders */ -void VisualDeckStorageFolderDisplayWidget::refreshVisibility() +void VisualDeckStorageFolderDisplayWidget::flattenFolderStructure() { - const bool shouldBeVisible = hasContent(); - if (isHidden() == !shouldBeVisible) { - return; + for (auto *subFolder : findChildren()) { + // steal all DeckPreviewWidgets from the subfolder + for (auto *deck : subFolder->getFlowWidget()->findChildren()) { + flowWidget->addWidget(deck); + } + + // delete the subfolder + subFolder->deleteLater(); } - setHidden(!shouldBeVisible); - emit contentVisibilityChanged(); } -/** - * @brief Whether this folder shows any deck previews or has any visible subfolder. - * - * While the first pass is still building, the folder counts as having content so it - * doesn't flicker or hide prematurely before its previews have been created. - */ -bool VisualDeckStorageFolderDisplayWidget::hasContent() const +QStringList VisualDeckStorageFolderDisplayWidget::gatherAllTagsFromFlowWidget() const { - if (!initialPassCompleted || visibleDeckCount > 0) { - return true; - } + QStringList allTags; - for (VisualDeckStorageFolderDisplayWidget *subFolderWidget : subFolderWidgets) { - if (subFolderWidget->hasContent()) { - return true; + if (flowWidget) { + // Iterate through all DeckPreviewWidgets + for (DeckPreviewWidget *display : flowWidget->findChildren()) { + // Get tags from each DeckPreviewWidget + QStringList tags = display->deckLoader->getDeck().deckList.getTags(); + + // Add tags to the list while avoiding duplicates + allTags.append(tags); } } - return false; -} + // Remove duplicates by calling 'removeDuplicates' + allTags.removeDuplicates(); -/** - * @brief The direct child folder paths of this folder, sorted by name. - */ -QStringList VisualDeckStorageFolderDisplayWidget::childFolderPaths() const -{ - QStringList children; - const QString prefix = folderPath.isEmpty() ? QString() : folderPath + "/"; - - for (const QString &candidate : visualDeckStorageWidget->model()->getFolderPaths()) { - if (!candidate.startsWith(prefix)) { - continue; - } - const QString rest = candidate.mid(prefix.length()); - if (rest.isEmpty() || rest.contains('/')) { - continue; - } - children.append(candidate); - } - - return children; -} + return allTags; +} \ No newline at end of file diff --git a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_folder_display_widget.h b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_folder_display_widget.h index 5a81457b0..a5e3be212 100644 --- a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_folder_display_widget.h +++ b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_folder_display_widget.h @@ -1,106 +1,49 @@ /** * @file visual_deck_storage_folder_display_widget.h * @ingroup VisualDeckStorageWidgets - * @brief Renders the decks of one folder of the Visual Deck Storage. - * - * This is a pure view: it keeps one persistent DeckPreviewWidget alive per deck - * in its folder, and shows or hides those widgets according to each row's - * FilterMatchRole in the VisualDeckStorageSortFilterProxyModel. Subfolders are - * shown as nested VisualDeckStorageFolderDisplayWidgets when the "show folders" - * setting is enabled. - * - * Reconciling runs as a time-budgeted chunked pass that yields to the event loop - * between chunks, so scanning a large collection never stalls the ui thread. */ +//! \todo Document this file. #ifndef VISUAL_DECK_STORAGE_FOLDER_DISPLAY_WIDGET_H #define VISUAL_DECK_STORAGE_FOLDER_DISPLAY_WIDGET_H -#include -#include -#include -#include +#include "../general/display/banner_widget.h" +#include "../general/layout_containers/flow_widget.h" -class BannerWidget; -class DeckPreviewWidget; -class FlowWidget; -class QTimer; -class QVBoxLayout; class VisualDeckStorageWidget; - class VisualDeckStorageFolderDisplayWidget : public QWidget { Q_OBJECT public: VisualDeckStorageFolderDisplayWidget(QWidget *parent, - VisualDeckStorageWidget *visualDeckStorageWidget, - const QString &folderPath, + VisualDeckStorageWidget *_visualDeckStorageWidget, + QString _filePath, bool canBeHidden, - bool showFolders); + bool _showFolders); + void refreshUi(); + void createWidgetsForFiles(); + void createWidgetsForFolders(); + void flattenFolderStructure(); + [[nodiscard]] QStringList gatherAllTagsFromFlowWidget() const; + [[nodiscard]] FlowWidget *getFlowWidget() const + { + return flowWidget; + } public slots: - /** - * @brief Starts a new chunked reconcile pass that re-reads the proxy and rebuilds - * the deck previews and subfolder widgets to match. - */ - void reconcile(); - - /** - * @brief Coalesces proxy change signals (a burst of deck loads or filter - * invalidations) into a single reconcile on the next event loop turn. - */ - void scheduleReconcile(); + void updateVisibility(bool recursive = true); + bool checkVisibility(); void updateShowFolders(bool enabled); - void setShareSelectable(bool selectable); - -signals: - /** - * @brief Emitted whenever this folder's visible content changes, so parent - * folders can re-evaluate their own visibility. - */ - void contentVisibilityChanged(); private: - void beginDeckPass(); - void continueDeckPass(); - void finishDeckPass(); - [[nodiscard]] DeckPreviewWidget *createDeckPreviewWidget(const QString &filePath); - void createSubFolderWidgets(); - void refreshVisibility(); - [[nodiscard]] bool hasContent() const; - [[nodiscard]] QStringList childFolderPaths() const; - - /** - * @brief The maximum time in milliseconds spent creating deck previews per event loop turn. - * - * Creating all previews of a large folder at once blocks the ui thread for hundreds of - * milliseconds, so the pass is split into chunks that yield to the event loop instead. - */ - static constexpr int DECK_PASS_TIME_BUDGET_MS = 20; - bool showFolders; - QString folderPath; ///< Path relative to the deck folder, empty for the root folder. - int visibleDeckCount = 0; ///< The number of this folder's deck previews not filtered out. QVBoxLayout *layout; + VisualDeckStorageWidget *visualDeckStorageWidget; + QString filePath; + BannerWidget *header; QWidget *container; QVBoxLayout *containerLayout; FlowWidget *flowWidget; - BannerWidget *header; - VisualDeckStorageWidget *visualDeckStorageWidget; - QHash deckWidgets; ///< Deck file path -> preview widget. - QHash subFolderWidgets; ///< Folder path -> subfolder widget. - QTimer *reconcileTimer = nullptr; ///< Coalesces proxy change bursts. - QStringList lastOrderedFilePaths; ///< The deck order last applied to the flow layout. - - /// Whether a chunked reconcile pass is currently running. - bool deckPassActive = false; - /// Set when the model changes mid-pass. Discards progress and restarts the scan once - /// the current chunk finishes so the pass always converges on the latest model state. - bool deckPassRestartRequested = false; - /// Whether the first reconcile pass has run to completion at least once. - bool initialPassCompleted = false; - int deckPassRow = 0; ///< Next proxy row to scan in the active pass. - QSet deckPassPresentPaths; ///< File paths seen so far in the active pass. }; #endif // VISUAL_DECK_STORAGE_FOLDER_DISPLAY_WIDGET_H 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 deleted file mode 100644 index daca55fc8..000000000 --- a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_model.cpp +++ /dev/null @@ -1,560 +0,0 @@ -#include "visual_deck_storage_model.h" - -#include "../../deck_loader/deck_loader.h" -#include "../cards/additional_info/deck_color_identity.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace -{ -/** - * @brief The result of a background directory scan: the deck rows in scan order - * plus the sorted list of subfolder paths. - */ -struct DeckScanResult -{ - QList decks; ///< Deck rows in scan order. - QStringList folderPaths; ///< Sorted list of subfolder paths. -}; - -/** - * @brief The result of a background deck file load: the parsed deck plus the - * file's modification time, so the disk stat happens off the UI thread. - */ -struct DeckLoadResult -{ - LoadedDeck deck; ///< The parsed deck. - QDateTime lastModified; ///< File modification time at load. - QString colorIdentity; ///< WUBRG color identity, computed off the UI thread. -}; - -/** - * @brief How long per event loop turn the pending-load drain applies finished loads. - * - * Applying a load updates the row and wakes up the proxies and views, which is - * not free. A time budget instead of a fixed count lets fast machines apply - * many loads in one turn while keeping the ui thread responsive everywhere. - */ -constexpr int LOAD_DRAIN_TIME_BUDGET_MS = 8; - -/** - * @brief The path of \a path relative to the deck root, or empty if \a path - * is not below it. - */ -QString relativePathFromDeckRoot(const QString &path, const QString &deckPath) -{ - if (!path.startsWith(deckPath)) { - return {}; - } - QString relativePath = path.mid(deckPath.length()); - if (relativePath.startsWith('/')) { - relativePath.remove(0, 1); - } - return relativePath; -} - -/** - * @brief The path of \a filePath relative to \a deckPath, or the bare file name - * if \a filePath is not below \a deckPath. - */ -QString relativeFilePathFor(const QString &filePath, const QString &deckPath) -{ - if (filePath.startsWith(deckPath)) { - return filePath.mid(deckPath.length()); - } - - return QFileInfo(filePath).fileName(); -} - -/** - * @brief The directory of \a filePath relative to \a deckPath, or empty if the - * file sits directly in the deck root. - */ -QString folderPathFor(const QString &filePath, const QString &deckPath) -{ - return relativePathFromDeckRoot(QFileInfo(filePath).absolutePath(), deckPath); -} - -/** - * @brief Scans a deck directory on a worker thread, returning discovered deck - * files and subfolder paths. - */ -DeckScanResult scanDeckDirectory(const QString &deckPath) -{ - DeckScanResult result; - - QDirIterator fileIt(deckPath, DeckLoader::ACCEPTED_FILE_EXTENSIONS, QDir::Files, - QDirIterator::Subdirectories | QDirIterator::FollowSymlinks); - while (fileIt.hasNext()) { - const QString filePath = fileIt.next(); - DeckPreviewData data; - data.filePath = filePath; - data.relativeFilePath = relativeFilePathFor(filePath, deckPath); - data.folderPath = folderPathFor(filePath, deckPath); - data.lastModified = QFileInfo(filePath).lastModified(); - result.decks.append(std::move(data)); - } - - QSet seenFolders; - QDirIterator folderIt(deckPath, QDir::Dirs | QDir::NoDotAndDotDot, - QDirIterator::Subdirectories | QDirIterator::FollowSymlinks); - while (folderIt.hasNext()) { - const QString folderPath = relativePathFromDeckRoot(folderIt.next(), deckPath); - if (!folderPath.isEmpty() && !seenFolders.contains(folderPath)) { - seenFolders.insert(folderPath); - result.folderPaths.append(folderPath); - } - } - result.folderPaths.sort(); - - return result; -} -} // namespace - -VisualDeckStorageModel::VisualDeckStorageModel(QObject *parent) : QAbstractListModel(parent) -{ -} - -int VisualDeckStorageModel::rowCount(const QModelIndex &parent) const -{ - return parent.isValid() ? 0 : decks.size(); -} - -QVariant VisualDeckStorageModel::data(const QModelIndex &index, int role) const -{ - if (!index.isValid() || index.row() < 0 || index.row() >= decks.size()) { - return {}; - } - - const DeckPreviewData &data = decks.at(index.row()); - switch (role) { - case Qt::DisplayRole: - case VisualDeckStorageRoles::DisplayNameRole: - return data.displayName; - case VisualDeckStorageRoles::FilePathRole: - return data.filePath; - case VisualDeckStorageRoles::RelativeFilePathRole: - return data.relativeFilePath; - case VisualDeckStorageRoles::FolderPathRole: - return data.folderPath; - case VisualDeckStorageRoles::TagsRole: - return data.tags; - case VisualDeckStorageRoles::ColorIdentityRole: - return data.colorIdentity; - case VisualDeckStorageRoles::LastModifiedRole: - return data.lastModified; - case VisualDeckStorageRoles::LastLoadedRole: - return data.lastLoaded; - case VisualDeckStorageRoles::BannerCardNameRole: - return data.bannerCard.name; - case VisualDeckStorageRoles::BannerCardProviderIdRole: - return data.bannerCard.providerId; - default: - return {}; - } -} - -void VisualDeckStorageModel::setDeckPath(const QString &path) -{ - QString cleanedPath = QDir::cleanPath(path); - if (cleanedPath == ".") { - cleanedPath.clear(); - } - deckPath = cleanedPath; - startScan(); -} - -void VisualDeckStorageModel::refresh() -{ - startScan(); -} - -const DeckPreviewData &VisualDeckStorageModel::dataForRow(int row) const -{ - static const DeckPreviewData emptyData; - if (row < 0 || row >= decks.size()) { - return emptyData; - } - return decks.at(row); -} - -const LoadedDeck &VisualDeckStorageModel::deckForRow(int row) const -{ - return dataForRow(row).deck; -} - -int VisualDeckStorageModel::rowForFilePath(const QString &filePath) const -{ - return rowByFilePath.value(filePath, -1); -} - -/** - * @brief Rebuilds the file path -> row index from scratch after bulk changes. - */ -void VisualDeckStorageModel::reindexFilePaths() -{ - rowByFilePath.clear(); - for (int row = 0; row < decks.size(); ++row) { - const QString &filePath = decks.at(row).filePath; - // First row wins, mirroring what a linear scan would return for duplicates. - if (!rowByFilePath.contains(filePath)) { - rowByFilePath.insert(filePath, row); - } - } -} - -void VisualDeckStorageModel::startScan() -{ - ++scanGeneration; - beginResetModel(); - decks.clear(); - rowByFilePath.clear(); - folderPaths.clear(); - pendingLoads.clear(); - endResetModel(); - - if (deckPath.isEmpty()) { - return; - } - - const QString currentDeckPath = deckPath; - const int generation = scanGeneration; - - // 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. - 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()) { - return; - } - - beginInsertRows(QModelIndex(), 0, result.decks.size() - 1); - decks = result.decks; - reindexFilePaths(); - endInsertRows(); - - for (int row = 0; row < decks.size(); ++row) { - beginLoad(row); - } - }); - - watcher->setFuture( - QtConcurrent::run([currentDeckPath]() -> DeckScanResult { return scanDeckDirectory(currentDeckPath); })); -} - -void VisualDeckStorageModel::beginLoad(int row) -{ - if (row < 0 || row >= decks.size() || decks.at(row).loadInProgress) { - return; - } - - DeckPreviewData &data = decks[row]; - data.loadInProgress = true; - - const QString filePath = data.filePath; - const DeckFileFormat::Format fmt = DeckFileFormat::getFormatFromName(filePath); - const int generation = scanGeneration; - - 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; - } - // Color identity walks every card through the database, so compute it here to - // 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}; - })); -} - -void VisualDeckStorageModel::schedulePendingLoadDrain() -{ - if (drainScheduled) { - return; - } - drainScheduled = true; - QMetaObject::invokeMethod(this, &VisualDeckStorageModel::drainPendingLoads, Qt::QueuedConnection); -} - -void VisualDeckStorageModel::drainPendingLoads() -{ - drainScheduled = false; - - QElapsedTimer timer; - timer.start(); - - while (!pendingLoads.isEmpty()) { - PendingDeckLoad pending = pendingLoads.takeFirst(); - - if (pending.generation != scanGeneration) { - continue; - } - - const int row = rowForFilePath(pending.filePath); - if (row == -1) { - continue; - } - - DeckPreviewData &data = decks[row]; - data.loadInProgress = false; - - if (!pending.ok) { - continue; // Leave the row unloaded so it stays visible without deck data. - } - - data.deck = std::move(pending.deck); - data.loadSucceeded = true; - data.lastModified = pending.lastModified; - recomputeDeckMetadata(data, false); - data.colorIdentity = std::move(pending.colorIdentity); - - emit dataChanged(index(row), index(row)); - emit deckLoaded(row); - - // Checked after applying at least one load, so a single slow application - // still makes progress instead of starving the queue. - if (timer.elapsed() >= LOAD_DRAIN_TIME_BUDGET_MS) { - break; - } - } - - if (!pendingLoads.isEmpty()) { - schedulePendingLoadDrain(); - } -} - -/** - * @brief Recomputes all derived metadata of a row from its loaded deck. - */ -void VisualDeckStorageModel::recomputeDeckMetadata(DeckPreviewData &data, bool recomputeColorIdentity) -{ - const DeckList &deckList = data.deck.deckList; - - data.deckName = deckList.getName(); - data.displayName = !data.deckName.isEmpty() ? data.deckName : QFileInfo(data.deck.lastLoadInfo.fileName).fileName(); - data.tags = deckList.getTags(); - data.lastLoaded = QDateTime::fromString(deckList.getLastLoadedTimestamp()); - data.bannerCard = deckList.getBannerCard(); - if (recomputeColorIdentity) { - data.colorIdentity = getDeckColorIdentity(data.deck.deckList, CardDatabaseManager::query()); - } -} - -void VisualDeckStorageModel::setFilePathForRow(int row, const QString &newFilePath) -{ - if (row < 0 || row >= decks.size()) { - return; - } - - DeckPreviewData &data = decks[row]; - rowByFilePath.remove(data.filePath); - data.filePath = newFilePath; - data.relativeFilePath = relativeFilePathFor(newFilePath, deckPath); - data.folderPath = folderPathFor(newFilePath, deckPath); - if (!rowByFilePath.contains(newFilePath)) { - rowByFilePath.insert(newFilePath, row); - } -} - -bool VisualDeckStorageModel::renameDeck(int row, const QString &newName) -{ - if (row < 0 || row >= decks.size() || decks.at(row).deck.isEmpty()) { - return false; - } - - DeckPreviewData &data = decks[row]; - data.deck.deckList.setName(newName); - if (!DeckLoader::saveToFile(data.deck)) { - return false; - } - - recomputeDeckMetadata(data); - emit dataChanged(index(row), index(row), {VisualDeckStorageRoles::DisplayNameRole}); - return true; -} - -bool VisualDeckStorageModel::renameFile(int row, const QString &newBaseName) -{ - if (row < 0 || row >= decks.size() || newBaseName.isEmpty()) { - return false; - } - - DeckPreviewData &data = decks[row]; - const QFileInfo info(data.filePath); - if (newBaseName == info.baseName()) { - return false; - } - - QString newFileName = newBaseName; - if (!info.suffix().isEmpty()) { - newFileName += "." + info.suffix(); - } - - const QString newFilePath = QFileInfo(info.dir(), newFileName).filePath(); - if (!QFile::rename(info.filePath(), newFilePath)) { - return false; - } - - const QString oldFilePath = data.filePath; - data.deck.lastLoadInfo.fileName = newFilePath; - setFilePathForRow(row, newFilePath); - data.lastModified = QFileInfo(newFilePath).lastModified(); - - emit dataChanged(index(row), index(row)); - emit deckFilePathChanged(oldFilePath, newFilePath); - return true; -} - -bool VisualDeckStorageModel::deleteFile(int row) -{ - if (row < 0 || row >= decks.size()) { - return false; - } - - const QString filePath = decks.at(row).filePath; - if (!QFile::remove(QFileInfo(filePath).filePath())) { - return false; - } - - beginRemoveRows(QModelIndex(), row, row); - rowByFilePath.remove(filePath); - decks.removeAt(row); - // Rows after the deleted one shift down by one. - for (auto it = rowByFilePath.begin(); it != rowByFilePath.end(); ++it) { - if (it.value() > row) { - --it.value(); - } - } - endRemoveRows(); - return true; -} - -bool VisualDeckStorageModel::setTags(int row, const QStringList &tags) -{ - if (row < 0 || row >= decks.size() || decks.at(row).deck.isEmpty()) { - return false; - } - - DeckPreviewData &data = decks[row]; - data.deck.deckList.setTags(tags); - if (!DeckLoader::saveToFile(data.deck)) { - return false; - } - - data.tags = tags; - emit dataChanged(index(row), index(row), {VisualDeckStorageRoles::TagsRole}); - return true; -} - -bool VisualDeckStorageModel::setBannerCard(int row, const CardRef &cardRef) -{ - if (row < 0 || row >= decks.size() || decks.at(row).deck.isEmpty()) { - return false; - } - - DeckPreviewData &data = decks[row]; - data.deck.deckList.setBannerCard(cardRef); - if (!DeckLoader::saveToFile(data.deck)) { - return false; - } - - data.bannerCard = cardRef; - emit dataChanged(index(row), index(row), - {VisualDeckStorageRoles::BannerCardNameRole, VisualDeckStorageRoles::BannerCardProviderIdRole}); - return true; -} - -bool VisualDeckStorageModel::convertToCockatriceFormat(int row) -{ - if (row < 0 || row >= decks.size() || decks.at(row).deck.isEmpty()) { - return false; - } - - DeckPreviewData &data = decks[row]; - const QString oldFilePath = data.filePath; - if (!DeckLoader::convertToCockatriceFormat(data.deck)) { - return false; - } - - setFilePathForRow(row, data.deck.lastLoadInfo.fileName); - data.lastModified = QFileInfo(data.filePath).lastModified(); - recomputeDeckMetadata(data); - - emit dataChanged(index(row), index(row)); - if (oldFilePath != data.filePath) { - emit deckFilePathChanged(oldFilePath, data.filePath); - } - return true; -} - -bool VisualDeckStorageModel::reloadIfModified(int row) -{ - if (row < 0 || row >= decks.size()) { - return false; - } - - DeckPreviewData &data = decks[row]; - QFileInfo fileInfo(data.filePath); - const QDateTime newLastModified = fileInfo.lastModified(); - if (!newLastModified.isValid() || newLastModified <= data.lastModified) { - return false; - } - - std::optional result = - DeckLoader::loadFromFile(data.filePath, DeckFileFormat::getFormatFromName(data.filePath), false); - if (!result) { - return false; - } - - data.deck = *result; - data.loadSucceeded = true; - data.lastModified = fileInfo.lastModified(); - recomputeDeckMetadata(data); - - emit dataChanged(index(row), index(row)); - emit deckLoaded(row); - return true; -} diff --git a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_model.h b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_model.h deleted file mode 100644 index 330205356..000000000 --- a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_model.h +++ /dev/null @@ -1,188 +0,0 @@ -/** - * @file visual_deck_storage_model.h - * @ingroup VisualDeckStorageWidgets - * @brief Source model for the Visual Deck Storage: the deck files on disk. - * - * The model owns the deck metadata (name, tags, color identity, banner card, - * modification times) and the parsed deck list, loading each deck file in the - * background. Views read through the roles or the direct accessors, and all - * mutations (rename, tags, banner card, delete, conversion) go through this - * class so the view layer never touches the filesystem directly. - */ - -#ifndef VISUAL_DECK_STORAGE_MODEL_H -#define VISUAL_DECK_STORAGE_MODEL_H - -#include "../../deck_loader/loaded_deck.h" - -#include -#include -#include -#include -#include -#include -#include - -namespace VisualDeckStorageRoles -{ -/** - * @brief Custom roles exposed by the VisualDeckStorageModel. - */ -enum -{ - FilePathRole = Qt::UserRole + 1, /**< Absolute file path of the deck. */ - RelativeFilePathRole, /**< File path relative to the deck folder. */ - FolderPathRole, /**< Directory of the deck relative to the deck folder ("" for root). */ - DisplayNameRole, /**< Deck name, or the file name if the deck has no name. */ - TagsRole, /**< The deck's tags. */ - ColorIdentityRole, /**< The deck's color identity (WUBRG order). */ - LastModifiedRole, /**< QDateTime of the deck file's last modification. */ - LastLoadedRole, /**< QDateTime when the deck was last loaded from the file. */ - BannerCardNameRole, /**< Name of the deck's banner card. */ - BannerCardProviderIdRole, /**< Provider id of the deck's banner card. */ - /** - * @brief Whether the row passes the proxy's current search / tag / color filters. - * - * Not served by this model, but by VisualDeckStorageSortFilterProxyModel on top - * of it. Declared here so every role read through a proxy index stays unique. - */ - FilterMatchRole -}; -} // namespace VisualDeckStorageRoles - -/** - * @brief One deck file as seen by the Visual Deck Storage. - * - * The metadata is computed once when the deck loads and refreshed on reloads - * and mutations, so filters and sorts never re-read the file from disk. - */ -struct DeckPreviewData -{ - QString filePath; ///< Absolute file path. - QString relativeFilePath; ///< File path relative to the deck folder. - QString folderPath; ///< Directory relative to the deck folder ("" for the deck folder itself). - QString deckName; ///< The deck name as stored in the file (may be empty). - QString displayName; ///< Deck name, or the file name if the deck has no name. - QStringList tags; ///< The deck's tags. - QString colorIdentity; ///< The deck's color identity in WUBRG order. - QDateTime lastModified; ///< File modification time at last check. - QDateTime lastLoaded; ///< When the deck was last loaded from the file. - CardRef bannerCard; ///< The deck's banner card (name + provider id). - LoadedDeck deck; ///< The parsed deck; empty until the file has been loaded. - bool loadSucceeded = false; ///< Whether the deck file finished loading successfully. - bool loadInProgress = false; ///< Whether the deck file is currently being loaded. -}; - -/** - * @brief One finished background deck load that has not been applied to the model yet. - */ -struct PendingDeckLoad -{ - QString filePath; ///< Identifies the row the result belongs to. - int generation; ///< Scan generation the load was started in. - bool ok = false; ///< Whether the file parsed successfully. - LoadedDeck deck; ///< The parsed deck, valid when ok. - QDateTime lastModified; ///< File modification time at load, valid when ok. - QString colorIdentity; ///< WUBRG color identity computed off the UI thread, valid when ok. -}; - -/** - * @brief The list model backing the Visual Deck Storage widget tree. - * - * Rows are in filesystem scan order; ordering and filtering are handled by - * VisualDeckStorageSortFilterProxyModel on top of this model. The proxy keeps - * every row and exposes each row's filter result through its FilterMatchRole. - */ -class VisualDeckStorageModel : public QAbstractListModel -{ - Q_OBJECT -public: - explicit VisualDeckStorageModel(QObject *parent = nullptr); - - /// @name Qt model overrides - ///@{ - [[nodiscard]] int rowCount(const QModelIndex &parent = QModelIndex()) const override; - [[nodiscard]] QVariant data(const QModelIndex &index, int role) const override; - ///@} - - /** - * @brief Sets the folder to scan for deck files and starts (re)loading. - * Clears the model immediately (modelReset), then populates it asynchronously - * as the background scan discovers deck files. - */ - void setDeckPath(const QString &path); - - /** - * @brief Re-scans the current deck folder, reloading every deck file. - */ - void refresh(); - - [[nodiscard]] QString getDeckPath() const - { - return deckPath; - } - - /** - * @brief The relative paths of all subdirectories of the deck folder, one level at a time. - * Used by the view to build the folder tree. Sorted for deterministic order. - */ - [[nodiscard]] QStringList getFolderPaths() const - { - return folderPaths; - } - - /// @name Data accessors - ///@{ - [[nodiscard]] const DeckPreviewData &dataForRow(int row) const; - [[nodiscard]] const LoadedDeck &deckForRow(int row) const; - [[nodiscard]] int rowForFilePath(const QString &filePath) const; - ///@} - - /// @name Mutations (persist to disk and update the row) - ///@{ - bool renameDeck(int row, const QString &newName); - bool renameFile(int row, const QString &newBaseName); - bool deleteFile(int row); - bool setTags(int row, const QStringList &tags); - bool setBannerCard(int row, const CardRef &cardRef); - bool convertToCockatriceFormat(int row); - bool reloadIfModified(int row); - ///@} - -signals: - /** - * @brief Emitted when a deck file finishes loading. - * @param row The row of the deck that finished loading. - */ - void deckLoaded(int row); - - /** - * @brief Emitted when a deck's file path changes (rename file, conversion). - * @param oldFilePath The previous file path. - * @param newFilePath The new file path. - */ - void deckFilePathChanged(const QString &oldFilePath, const QString &newFilePath); - -private: - void startScan(); - void beginLoad(int row); - static void recomputeDeckMetadata(DeckPreviewData &data, bool recomputeColorIdentity = true); - void reindexFilePaths(); - void schedulePendingLoadDrain(); - -private slots: - void drainPendingLoads(); - -private: - void setFilePathForRow(int row, const QString &newFilePath); - - QString deckPath; - QList decks; - QHash rowByFilePath; ///< Maps each deck's file path to its row for O(1) lookups. - QStringList folderPaths; ///< All subdirectories of the deck folder, sorted. - int scanGeneration = 0; ///< Bumped on every scan so stale results are ignored. - QVector pendingLoads; ///< Finished background loads waiting to be applied. - bool drainScheduled = false; ///< Whether a queued drain pass is already pending. -}; - -#endif // VISUAL_DECK_STORAGE_MODEL_H diff --git a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_quick_settings_widget.cpp b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_quick_settings_widget.cpp index c19ac0fec..cce3ff6ce 100644 --- a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_quick_settings_widget.cpp +++ b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_quick_settings_widget.cpp @@ -1,7 +1,6 @@ #include "visual_deck_storage_quick_settings_widget.h" #include "../../../client/settings/cache_settings.h" -#include "../cards/card_size_widget.h" #include "visual_deck_storage_widget.h" #include @@ -50,15 +49,6 @@ VisualDeckStorageQuickSettingsWidget::VisualDeckStorageQuickSettingsWidget(QWidg &SettingsCache::instance().visualDeckStorage(), &VisualDeckStorageSettings::setVisualDeckStorageShowTagsOnDeckPreviews); - // show upload time on DeckPreviewWidget checkbox - showUploadTimeCheckBox = new QCheckBox(this); - showUploadTimeCheckBox->setChecked( - SettingsCache::instance().visualDeckStorage().getVisualDeckStorageShowUploadTime()); - connect(showUploadTimeCheckBox, &QCheckBox::QT_STATE_CHANGED, this, - &VisualDeckStorageQuickSettingsWidget::showUploadTimeChanged); - connect(showUploadTimeCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().visualDeckStorage(), - &VisualDeckStorageSettings::setVisualDeckStorageShowUploadTime); - // show banner card selector checkbox showBannerCardComboBoxCheckBox = new QCheckBox(this); showBannerCardComboBoxCheckBox->setChecked( @@ -103,7 +93,7 @@ VisualDeckStorageQuickSettingsWidget::VisualDeckStorageQuickSettingsWidget(QWidg unusedColorIdentityOpacityLayout->addWidget(unusedColorIdentitiesOpacitySpinBox); // tooltip selector - deckPreviewTooltipWidget = new QWidget(this); + auto deckPreviewTooltipWidget = new QWidget(this); deckPreviewTooltipLabel = new QLabel(deckPreviewTooltipWidget); deckPreviewTooltipComboBox = new QComboBox(deckPreviewTooltipWidget); @@ -137,7 +127,6 @@ VisualDeckStorageQuickSettingsWidget::VisualDeckStorageQuickSettingsWidget(QWidg this->addSettingsWidget(showTagFilterCheckBox); this->addSettingsWidget(showColorIdentityCheckBox); this->addSettingsWidget(showTagsOnDeckPreviewsCheckBox); - this->addSettingsWidget(showUploadTimeCheckBox); this->addSettingsWidget(showBannerCardComboBoxCheckBox); this->addSettingsWidget(drawUnusedColorIdentitiesCheckBox); this->addSettingsWidget(unusedColorIdentityOpacityWidget); @@ -155,7 +144,6 @@ void VisualDeckStorageQuickSettingsWidget::retranslateUi() showTagFilterCheckBox->setText(tr("Show Tag Filter")); showColorIdentityCheckBox->setText(tr("Show Color Identity")); showTagsOnDeckPreviewsCheckBox->setText(tr("Show Tags On Deck Previews")); - showUploadTimeCheckBox->setText(tr("Show Upload Time")); showBannerCardComboBoxCheckBox->setText(tr("Show Banner Card Selection Option")); drawUnusedColorIdentitiesCheckBox->setText(tr("Draw unused Color Identities")); unusedColorIdentitiesOpacityLabel->setText(tr("Unused Color Identities Opacity")); @@ -166,14 +154,6 @@ void VisualDeckStorageQuickSettingsWidget::retranslateUi() deckPreviewTooltipComboBox->setItemText(1, tr("Filepath")); } -void VisualDeckStorageQuickSettingsWidget::setPublicDecksMode(bool enabled) -{ - const bool hidden = enabled; - showFoldersCheckBox->setVisible(!hidden); - showBannerCardComboBoxCheckBox->setVisible(!hidden); - deckPreviewTooltipWidget->setVisible(!hidden); -} - bool VisualDeckStorageQuickSettingsWidget::getShowFolders() const { return showFoldersCheckBox->isChecked(); @@ -204,11 +184,6 @@ bool VisualDeckStorageQuickSettingsWidget::getShowTagsOnDeckPreviews() const return showTagsOnDeckPreviewsCheckBox->isChecked(); } -bool VisualDeckStorageQuickSettingsWidget::getShowUploadTime() const -{ - return showUploadTimeCheckBox->isChecked(); -} - int VisualDeckStorageQuickSettingsWidget::getUnusedColorIdentitiesOpacity() const { return unusedColorIdentitiesOpacitySpinBox->value(); diff --git a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_quick_settings_widget.h b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_quick_settings_widget.h index fc250cb20..ea4330a15 100644 --- a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_quick_settings_widget.h +++ b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_quick_settings_widget.h @@ -27,12 +27,10 @@ class VisualDeckStorageQuickSettingsWidget : public SettingsButtonWidget QCheckBox *showBannerCardComboBoxCheckBox; QCheckBox *showTagFilterCheckBox; QCheckBox *showTagsOnDeckPreviewsCheckBox; - QCheckBox *showUploadTimeCheckBox; QLabel *unusedColorIdentitiesOpacityLabel; QSpinBox *unusedColorIdentitiesOpacitySpinBox; QLabel *deckPreviewTooltipLabel; QComboBox *deckPreviewTooltipComboBox; - QWidget *deckPreviewTooltipWidget; CardSizeWidget *cardSizeWidget; public: @@ -48,15 +46,6 @@ public: explicit VisualDeckStorageQuickSettingsWidget(QWidget *parent = nullptr); - /** - * @brief Hides the controls that do not apply to the public decks tab. - * - * The public decks tab reuses this widget for its quick settings menu but - * has no folders, banner selection or per-deck tooltip, so those controls - * are hidden while every shared key keeps syncing with SettingsCache. - */ - void setPublicDecksMode(bool enabled); - void retranslateUi(); [[nodiscard]] bool getShowFolders() const; @@ -65,7 +54,6 @@ public: [[nodiscard]] bool getShowBannerCardComboBox() const; [[nodiscard]] bool getShowTagFilter() const; [[nodiscard]] bool getShowTagsOnDeckPreviews() const; - [[nodiscard]] bool getShowUploadTime() const; [[nodiscard]] int getUnusedColorIdentitiesOpacity() const; [[nodiscard]] TooltipType getDeckPreviewTooltip() const; [[nodiscard]] int getCardSize() const; @@ -77,7 +65,6 @@ signals: void showBannerCardComboBoxChanged(bool enabled); void showTagFilterChanged(bool enabled); void showTagsOnDeckPreviewsChanged(bool enabled); - void showUploadTimeChanged(bool enabled); void unusedColorIdentitiesOpacityChanged(int opacity); void deckPreviewTooltipChanged(TooltipType tooltip); void cardSizeChanged(int scale); 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 406527893..07ee55105 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,20 +1,23 @@ #include "visual_deck_storage_search_widget.h" +#include "../../../client/settings/cache_settings.h" +#include "../../../filters/deck_filter_string.h" #include "../../../filters/syntax_help.h" #include "../../pixel_map_generator.h" #include -#include +#include +#include /** - * @brief Constructs a search bar for filtering decks in the Visual Deck Storage. + * @brief Constructs a PrintingSelectorCardSearchWidget for searching cards by set name or set code. * - * Provides a search bar that allows users to search decks by filename or search - * expression, with a debounced timer to trigger the search after the user stops typing. + * This widget provides a search bar that allows users to search for cards by either their set name + * or set code. It uses a debounced timer to trigger the search action after the user stops typing. * - * @param parent The parent widget. + * @param parent The parent PrintingSelector widget that will handle the search results. */ -VisualDeckStorageSearchWidget::VisualDeckStorageSearchWidget(QWidget *parent) : QWidget(parent) +VisualDeckStorageSearchWidget::VisualDeckStorageSearchWidget(VisualDeckStorageWidget *parent) : parent(parent) { layout = new QHBoxLayout(this); layout->setContentsMargins(0, 0, 0, 0); @@ -25,7 +28,7 @@ VisualDeckStorageSearchWidget::VisualDeckStorageSearchWidget(QWidget *parent) : searchBar->setClearButtonEnabled(true); searchBar->addAction(loadColorAdjustedPixmap("theme:icons/search"), QLineEdit::LeadingPosition); - auto help = searchBar->addAction(themePixmap(QStringLiteral("icons/info")), QLineEdit::TrailingPosition); + auto help = searchBar->addAction(QPixmap("theme:icons/info"), QLineEdit::TrailingPosition); connect(help, &QAction::triggered, this, [this] { createDeckSearchSyntaxHelpWindow(searchBar); }); layout->addWidget(searchBar); @@ -37,5 +40,43 @@ VisualDeckStorageSearchWidget::VisualDeckStorageSearchWidget(QWidget *parent) : searchDebounceTimer->start(300); // 300ms debounce }); - connect(searchDebounceTimer, &QTimer::timeout, this, [this] { emit searchTextChanged(searchBar->text()); }); + connect(searchDebounceTimer, &QTimer::timeout, parent, &VisualDeckStorageWidget::updateSearchFilter); +} + +/** + * @brief Retrieves the current text in the search bar. + * + * @return The text entered by the user in the search bar. + */ +QString VisualDeckStorageSearchWidget::getSearchText() +{ + return searchBar->text(); +} + +/** + * Converts the filepath into a relative filepath starting from the deck folder. + * If the file isn't in the deck folder, then this will just return the filename. + * + * @param filePath The filepath to convert into a relative filepath + */ +static QString toRelativeFilepath(const QString &filePath) +{ + QString deckPath = SettingsCache::instance().paths().getDeckPath(); + if (filePath.startsWith(deckPath)) { + return filePath.mid(deckPath.length()); + } + + QFileInfo fileInfo(filePath); + QString fileName = fileInfo.fileName(); + return fileName; +} + +void VisualDeckStorageSearchWidget::filterWidgets(QList widgets, const QString &searchText) +{ + auto filterString = DeckFilterString(searchText); + + for (auto widget : widgets) { + QString relativeFilePath = toRelativeFilepath(widget->filePath); + widget->filteredBySearch = !filterString.check(widget, {relativeFilePath}); + } } diff --git a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_search_widget.h b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_search_widget.h index 7769ea911..2f3d81aeb 100644 --- a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_search_widget.h +++ b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_search_widget.h @@ -2,32 +2,30 @@ * @file visual_deck_storage_search_widget.h * @ingroup VisualDeckStorageWidgets */ +//! \todo Document this file. #ifndef VISUAL_DECK_STORAGE_SEARCH_WIDGET_H #define VISUAL_DECK_STORAGE_SEARCH_WIDGET_H +#include "deck_preview/deck_preview_widget.h" + #include #include #include -class QTimer; - +class VisualDeckStorageWidget; class VisualDeckStorageSearchWidget : public QWidget { Q_OBJECT public: - explicit VisualDeckStorageSearchWidget(QWidget *parent); - -signals: - /** - * Emitted once the debounce timer fires after the user stopped typing. - * @param text The current contents of the search bar. - */ - void searchTextChanged(const QString &text); + explicit VisualDeckStorageSearchWidget(VisualDeckStorageWidget *parent); + QString getSearchText(); + void filterWidgets(QList widgets, const QString &searchText); private: QHBoxLayout *layout; + VisualDeckStorageWidget *parent; QLineEdit *searchBar; QTimer *searchDebounceTimer; }; diff --git a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_sort_filter_proxy_model.cpp b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_sort_filter_proxy_model.cpp deleted file mode 100644 index 0ad6ebcb2..000000000 --- a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_sort_filter_proxy_model.cpp +++ /dev/null @@ -1,299 +0,0 @@ -#include "visual_deck_storage_sort_filter_proxy_model.h" - -#include "../../../client/settings/cache_settings.h" -#include "../../filters/deck_filter_string.h" - -#include -#include -#include -#include - -VisualDeckStorageSortFilterProxyModel::VisualDeckStorageSortFilterProxyModel(QObject *parent) - : QSortFilterProxyModel(parent) -{ - setDynamicSortFilter(false); -} - -bool colorIdentityMatches(VisualDeckStorageSortFilterProxyModel::FilterMode mode, - const QSet &colors, - const QString &identity) -{ - switch (mode) { - case VisualDeckStorageSortFilterProxyModel::ExactMatch: { - QSet activeColorSet; - for (const QChar &color : colors) { - activeColorSet.insert(color.toUpper()); - } - - QSet colorIdentitySet; - for (const QChar &color : identity) { - colorIdentitySet.insert(color.toUpper()); - } - - return activeColorSet == colorIdentitySet; - } - case VisualDeckStorageSortFilterProxyModel::Includes: - return std::all_of(colors.begin(), colors.end(), - [&identity](const QChar &color) { return identity.contains(color); }); - case VisualDeckStorageSortFilterProxyModel::Excludes: - return std::none_of(colors.begin(), colors.end(), - [&identity](const QChar &color) { return identity.contains(color); }); - } - return false; -} - -void VisualDeckStorageSortFilterProxyModel::setSourceModel(QAbstractItemModel *model) -{ - if (QAbstractItemModel *oldModel = sourceModel()) { - disconnect(oldModel, &QAbstractItemModel::modelReset, this, - &VisualDeckStorageSortFilterProxyModel::resizeMatchLists); - disconnect(oldModel, &QAbstractItemModel::rowsInserted, this, - &VisualDeckStorageSortFilterProxyModel::resizeMatchLists); - disconnect(oldModel, &QAbstractItemModel::rowsRemoved, this, - &VisualDeckStorageSortFilterProxyModel::resizeMatchLists); - } - - QSortFilterProxyModel::setSourceModel(model); - - if (model) { - connect(model, &QAbstractItemModel::modelReset, this, &VisualDeckStorageSortFilterProxyModel::resizeMatchLists); - connect(model, &QAbstractItemModel::rowsInserted, this, - &VisualDeckStorageSortFilterProxyModel::resizeMatchLists); - connect(model, &QAbstractItemModel::rowsRemoved, this, - &VisualDeckStorageSortFilterProxyModel::resizeMatchLists); - } - - resizeMatchLists(); -} - -void VisualDeckStorageSortFilterProxyModel::setSearchText(const QString &text) -{ - if (searchText == text) { - return; - } - - searchText = text; - updateSearchMatches(); - invalidate(); -} - -void VisualDeckStorageSortFilterProxyModel::setTagFilter(const QSet &newSelectedTags, - const QSet &newExcludedTags) -{ - if (selectedTags == newSelectedTags && excludedTags == newExcludedTags) { - return; - } - - selectedTags = newSelectedTags; - excludedTags = newExcludedTags; - updateTagMatches(); - invalidate(); -} - -void VisualDeckStorageSortFilterProxyModel::setColorFilter(FilterMode mode, const QSet &colors) -{ - if (colorFilterMode == mode && activeColors == colors) { - return; - } - - colorFilterMode = mode; - activeColors = colors; - updateColorMatches(); - invalidate(); -} - -void VisualDeckStorageSortFilterProxyModel::setSortOrder(SortOrder order) -{ - // No equality guard: the initial reapply (with the default order) must still - // trigger sort(0), since without a sort the proxy would show scan order. - sortOrder = order; - sort(0); -} - -void VisualDeckStorageSortFilterProxyModel::reapplyFilters() -{ - const QList oldSearchMatches = searchMatches; - const QList oldTagMatches = tagMatches; - const QList oldColorMatches = colorMatches; - - updateSearchMatches(); - updateTagMatches(); - updateColorMatches(); - - if (searchMatches != oldSearchMatches || tagMatches != oldTagMatches || colorMatches != oldColorMatches) { - invalidate(); - } - - if (sortOrder == ByName || sortOrder == ByLastLoaded) { - // These orders depend on data that only becomes available when a deck finishes loading. - sort(0); - } -} - -void VisualDeckStorageSortFilterProxyModel::resort() -{ - sort(0); -} - -QVariant VisualDeckStorageSortFilterProxyModel::data(const QModelIndex &index, int role) const -{ - if (role == VisualDeckStorageRoles::FilterMatchRole) { - if (!index.isValid()) { - return true; - } - const QModelIndex sourceIndex = mapToSource(index); - return rowMatches(sourceIndex.row()); - } - - return QSortFilterProxyModel::data(index, role); -} - -bool VisualDeckStorageSortFilterProxyModel::rowMatches(int sourceRow) const -{ - // If the match lists aren't sized to the current model yet, don't hide anything. - if (sourceRow < 0 || sourceRow >= searchMatches.size() || sourceRow >= tagMatches.size() || - sourceRow >= colorMatches.size()) { - return true; - } - - return searchMatches.at(sourceRow) && tagMatches.at(sourceRow) && colorMatches.at(sourceRow); -} - -bool VisualDeckStorageSortFilterProxyModel::filterAcceptsRow(int /*sourceRow*/, - const QModelIndex & /*sourceParent*/) const -{ - // Rows are never dropped: the filter result is exposed per row through - // FilterMatchRole, so views can keep their widgets alive and just hide them. - return true; -} - -bool VisualDeckStorageSortFilterProxyModel::lessThan(const QModelIndex &left, const QModelIndex &right) const -{ - const auto *source = deckSourceModel(); - if (!source) { - return false; - } - - const DeckPreviewData &leftData = source->dataForRow(left.row()); - const DeckPreviewData &rightData = source->dataForRow(right.row()); - - switch (sortOrder) { - case ByName: - return leftData.deckName < rightData.deckName; - case Alphabetical: - return QString::localeAwareCompare(QFileInfo(leftData.filePath).fileName(), - QFileInfo(rightData.filePath).fileName()) < 0; - case ByLastModified: - return leftData.lastModified > rightData.lastModified; - case ByLastLoaded: - return leftData.lastLoaded > rightData.lastLoaded; - } - - return false; -} - -void VisualDeckStorageSortFilterProxyModel::resizeMatchLists() -{ - const int count = sourceModel() ? sourceModel()->rowCount() : 0; - searchMatches.resize(count); - searchMatches.fill(true); - tagMatches.resize(count); - tagMatches.fill(true); - colorMatches.resize(count); - colorMatches.fill(true); -} - -void VisualDeckStorageSortFilterProxyModel::updateSearchMatches() -{ - const auto *source = deckSourceModel(); - if (!source) { - searchMatches.clear(); - return; - } - - const int count = source->rowCount(); - searchMatches.resize(count); - if (searchText.isEmpty()) { - searchMatches.fill(true); - return; - } - - const auto &cardsDisplay = SettingsCache::instance().cardsDisplay(); - DeckFilterString filterString( - searchText, CardSearchLanguage{cardsDisplay.getCardLang(), - static_cast(cardsDisplay.getCardSearchLanguage())}); - for (int row = 0; row < count; ++row) { - const DeckPreviewData &data = source->dataForRow(row); - - // isEmpty() is intentional: if a deck fails to load, loadInProgress becomes false - // but the deck remains empty. Using loadInProgress alone would pass failed decks - // to DeckFilterString::check, which requires a non-empty deck. - if (data.deck.isEmpty()) { - searchMatches[row] = true; - continue; - } - - DeckSearchData searchData{ - .deck = &data.deck, - .filePath = data.filePath, - .displayName = data.displayName, - .relativeFilePath = data.relativeFilePath, - }; - searchMatches[row] = filterString.check(searchData); - } -} - -void VisualDeckStorageSortFilterProxyModel::updateTagMatches() -{ - const auto *source = deckSourceModel(); - if (!source) { - tagMatches.clear(); - return; - } - - const int count = source->rowCount(); - tagMatches.resize(count); - - if (selectedTags.isEmpty() && excludedTags.isEmpty()) { - tagMatches.fill(true); - return; - } - - for (int row = 0; row < count; ++row) { - const QStringList deckTags = source->dataForRow(row).tags; - - const bool hasAllSelected = std::all_of(selectedTags.begin(), selectedTags.end(), - [&deckTags](const QString &tag) { return deckTags.contains(tag); }); - const bool hasAnyExcluded = std::any_of(excludedTags.begin(), excludedTags.end(), - [&deckTags](const QString &tag) { return deckTags.contains(tag); }); - - tagMatches[row] = hasAllSelected && !hasAnyExcluded; - } -} - -void VisualDeckStorageSortFilterProxyModel::updateColorMatches() -{ - const auto *source = deckSourceModel(); - if (!source) { - colorMatches.clear(); - return; - } - - const int count = source->rowCount(); - colorMatches.resize(count); - - if (activeColors.isEmpty()) { - colorMatches.fill(true); - return; - } - - for (int row = 0; row < count; ++row) { - const QString colorIdentity = source->dataForRow(row).colorIdentity; - colorMatches[row] = colorIdentityMatches(colorFilterMode, activeColors, colorIdentity); - } -} - -const VisualDeckStorageModel *VisualDeckStorageSortFilterProxyModel::deckSourceModel() const -{ - return qobject_cast(sourceModel()); -} diff --git a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_sort_filter_proxy_model.h b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_sort_filter_proxy_model.h deleted file mode 100644 index a6c40a2d7..000000000 --- a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_sort_filter_proxy_model.h +++ /dev/null @@ -1,115 +0,0 @@ -/** - * @file visual_deck_storage_sort_filter_proxy_model.h - * @ingroup VisualDeckStorageWidgets - * @brief Sorting and filtering proxy on top of VisualDeckStorageModel. - * - * Owns all search / tag / color filter state and the sort order. Filtering is - * evaluated against the model's data (never against widgets), so it can run - * before any view exists and re-evaluate whenever deck data finishes loading. - * - * Rows are never removed by filtering. Instead, every row carries the - * FilterMatchRole, which views read to show or hide their widgets while keeping - * them alive; all rows stay in the proxy so they keep their sorted position. - */ - -#ifndef VISUAL_DECK_STORAGE_SORT_FILTER_PROXY_MODEL_H -#define VISUAL_DECK_STORAGE_SORT_FILTER_PROXY_MODEL_H - -#include "visual_deck_storage_model.h" - -#include -#include -#include - -class VisualDeckStorageSortFilterProxyModel : public QSortFilterProxyModel -{ - Q_OBJECT -public: - /** - * @brief The order in which decks are sorted. Values must match the - * entries of the sort widget's combo box and the stored settings value. - */ - enum SortOrder - { - ByName, - Alphabetical, - ByLastModified, - ByLastLoaded, - }; - Q_ENUM(SortOrder) - - /** - * @brief How the color identity filter is applied. - */ - enum FilterMode - { - ExactMatch, - Includes, - Excludes - }; - Q_ENUM(FilterMode) - - explicit VisualDeckStorageSortFilterProxyModel(QObject *parent = nullptr); - - [[nodiscard]] QVariant data(const QModelIndex &index, int role) const override; - - void setSourceModel(QAbstractItemModel *model) override; - - /// @name Filter input setters (each re-evaluates the affected matches) - ///@{ - void setSearchText(const QString &text); - void setTagFilter(const QSet &newSelectedTags, const QSet &newExcludedTags); - void setColorFilter(FilterMode mode, const QSet &colors); - ///@} - - /** - * @brief Sets the sort order and applies it immediately. - */ - void setSortOrder(SortOrder order); - - /** - * @brief Re-evaluates all matches against the current model data and - * re-applies filtering and sorting. Called after deck data changes. - */ - void reapplyFilters(); - - /** - * @brief Re-applies the current sort order without touching the filters. - */ - void resort(); - -protected: - bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override; - bool lessThan(const QModelIndex &left, const QModelIndex &right) const override; - -private: - [[nodiscard]] bool rowMatches(int sourceRow) const; - void resizeMatchLists(); - void updateSearchMatches(); - void updateTagMatches(); - void updateColorMatches(); - [[nodiscard]] const VisualDeckStorageModel *deckSourceModel() const; - - QString searchText; - QSet selectedTags; - QSet excludedTags; - FilterMode colorFilterMode = ExactMatch; - QSet activeColors; - SortOrder sortOrder = Alphabetical; - - QList searchMatches; ///< Per-row search match, sized like the source model. - QList tagMatches; ///< Per-row tag match. - QList colorMatches; ///< Per-row color identity match. -}; - -/** - * @brief Whether an identity string matches the active color-identity filter. - * - * The single source of truth for the color identity matching rule, shared by - * the Visual Deck Storage proxy and the remote public decks model. - */ -[[nodiscard]] bool colorIdentityMatches(VisualDeckStorageSortFilterProxyModel::FilterMode mode, - const QSet &colors, - const QString &identity); - -#endif // VISUAL_DECK_STORAGE_SORT_FILTER_PROXY_MODEL_H diff --git a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_sort_widget.cpp b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_sort_widget.cpp index 6289a4941..e4eedb078 100644 --- a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_sort_widget.cpp +++ b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_sort_widget.cpp @@ -1,11 +1,20 @@ #include "visual_deck_storage_sort_widget.h" #include "../../../client/settings/cache_settings.h" -#include "visual_deck_storage_widget.h" +#include #include -VisualDeckStorageSortWidget::VisualDeckStorageSortWidget(VisualDeckStorageWidget *parent) : QWidget(parent) +/** + * @brief Constructs a PrintingSelectorCardSortWidget for searching cards by set name or set code. + * + * This widget provides a search bar that allows users to search for cards by either their set name + * or set code. It uses a debounced timer to trigger the search action after the user stops typing. + * + * @param parent The parent PrintingSelector widget that will handle the search results. + */ +VisualDeckStorageSortWidget::VisualDeckStorageSortWidget(VisualDeckStorageWidget *parent) + : parent(parent), sortOrder(Alphabetical) { layout = new QHBoxLayout(this); layout->setContentsMargins(0, 0, 0, 0); @@ -21,10 +30,12 @@ VisualDeckStorageSortWidget::VisualDeckStorageSortWidget(VisualDeckStorageWidget // Set the current sort order sortComboBox->setCurrentIndex(SettingsCache::instance().visualDeckStorage().getVisualDeckStorageSortingOrder()); + sortOrder = static_cast(sortComboBox->currentIndex()); - // Connect sorting change signal to persist the order and refresh the file list + // Connect sorting change signal to refresh the file list connect(sortComboBox, QOverload::of(&QComboBox::currentIndexChanged), this, &VisualDeckStorageSortWidget::updateSortOrder); + connect(this, &VisualDeckStorageSortWidget::sortOrderChanged, parent, &VisualDeckStorageWidget::updateSortOrder); } void VisualDeckStorageSortWidget::retranslateUi() @@ -36,13 +47,10 @@ void VisualDeckStorageSortWidget::retranslateUi() // Clear and repopulate the ComboBox with translated items sortComboBox->clear(); - sortComboBox->addItem(tr("Sort Alphabetically (Deck Name)"), - VisualDeckStorageSortFilterProxyModel::SortOrder::ByName); - sortComboBox->addItem(tr("Sort Alphabetically (Filename)"), - VisualDeckStorageSortFilterProxyModel::SortOrder::Alphabetical); - sortComboBox->addItem(tr("Sort by Last Modified"), - VisualDeckStorageSortFilterProxyModel::SortOrder::ByLastModified); - sortComboBox->addItem(tr("Sort by Last Loaded"), VisualDeckStorageSortFilterProxyModel::SortOrder::ByLastLoaded); + sortComboBox->addItem(tr("Sort Alphabetically (Deck Name)"), ByName); + sortComboBox->addItem(tr("Sort Alphabetically (Filename)"), Alphabetical); + sortComboBox->addItem(tr("Sort by Last Modified"), ByLastModified); + sortComboBox->addItem(tr("Sort by Last Loaded"), ByLastLoaded); // Restore the current index sortComboBox->setCurrentIndex(oldIndex); @@ -51,13 +59,60 @@ void VisualDeckStorageSortWidget::retranslateUi() sortComboBox->blockSignals(false); } -VisualDeckStorageSortFilterProxyModel::SortOrder VisualDeckStorageSortWidget::currentSortOrder() const -{ - return static_cast(sortComboBox->currentIndex()); -} - void VisualDeckStorageSortWidget::updateSortOrder() { + sortOrder = static_cast(sortComboBox->currentIndex()); SettingsCache::instance().visualDeckStorage().setVisualDeckStorageSortingOrder(sortComboBox->currentIndex()); emit sortOrderChanged(); } + +void VisualDeckStorageSortWidget::sortFolder(VisualDeckStorageFolderDisplayWidget *folderWidget) +{ + auto children = + folderWidget->getFlowWidget()->findChildren(QString(), Qt::FindChildOption::FindDirectChildrenOnly); + for (auto widget : children) { + auto deckPreviewWidgets = + widget->findChildren(QString(), Qt::FindChildOption::FindDirectChildrenOnly); + auto newOrder = filterFiles(deckPreviewWidgets); + for (DeckPreviewWidget *previewWidget : newOrder) { + folderWidget->getFlowWidget()->removeWidget(previewWidget); + } + for (DeckPreviewWidget *previewWidget : newOrder) { + folderWidget->getFlowWidget()->addWidget(previewWidget); + } + } +} + +QList VisualDeckStorageSortWidget::filterFiles(QList widgets) +{ + // Sort the widgets list based on the current sort order + std::sort(widgets.begin(), widgets.end(), [this](DeckPreviewWidget *widget1, DeckPreviewWidget *widget2) { + if (!widget1 || !widget2) { + return false; // Handle null pointers gracefully + } + + QFileInfo info1(widget1->filePath); + QFileInfo info2(widget2->filePath); + + switch (sortOrder) { + case ByName: + return widget1->deckLoader->getDeck().deckList.getName() < + widget2->deckLoader->getDeck().deckList.getName(); + case Alphabetical: + return QString::localeAwareCompare(info1.fileName(), info2.fileName()) <= 0; + case ByLastModified: + return info1.lastModified() > info2.lastModified(); + case ByLastLoaded: { + QDateTime time1 = + QDateTime::fromString(widget1->deckLoader->getDeck().deckList.getLastLoadedTimestamp()); + QDateTime time2 = + QDateTime::fromString(widget2->deckLoader->getDeck().deckList.getLastLoadedTimestamp()); + return time1 > time2; + } + } + + return false; // Default case, no sorting applied + }); + + return widgets; +} diff --git a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_sort_widget.h b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_sort_widget.h index 633924d84..24eddba33 100644 --- a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_sort_widget.h +++ b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_sort_widget.h @@ -2,17 +2,19 @@ * @file visual_deck_storage_sort_widget.h * @ingroup VisualDeckStorageWidgets */ +//! \todo Document this file. #ifndef VISUAL_DECK_STORAGE_SORT_WIDGET_H #define VISUAL_DECK_STORAGE_SORT_WIDGET_H -#include "visual_deck_storage_sort_filter_proxy_model.h" +#include "visual_deck_storage_widget.h" #include #include #include class VisualDeckStorageWidget; +class VisualDeckStorageFolderDisplayWidget; class VisualDeckStorageSortWidget : public QWidget { Q_OBJECT @@ -20,23 +22,25 @@ class VisualDeckStorageSortWidget : public QWidget public: explicit VisualDeckStorageSortWidget(VisualDeckStorageWidget *parent); void retranslateUi(); - - /** - * @brief The currently selected sort order. - */ - [[nodiscard]] VisualDeckStorageSortFilterProxyModel::SortOrder currentSortOrder() const; + void updateSortOrder(); + void sortFolder(VisualDeckStorageFolderDisplayWidget *folderWidget); + QString getSearchText(); + QList filterFiles(QList widgets); signals: - /** - * @brief Emitted when the user picks a different sort order. - */ void sortOrderChanged(); -private slots: - void updateSortOrder(); - private: + enum SortOrder + { + ByName, + Alphabetical, + ByLastModified, + ByLastLoaded, + }; QHBoxLayout *layout; + VisualDeckStorageWidget *parent; + SortOrder sortOrder; // Current sorting option QComboBox *sortComboBox; }; diff --git a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_tag_filter_widget.cpp b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_tag_filter_widget.cpp index 6f954b01b..c4c8d18a8 100644 --- a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_tag_filter_widget.cpp +++ b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_tag_filter_widget.cpp @@ -2,10 +2,12 @@ #include "../general/layout_containers/flow_widget.h" #include "deck_preview/deck_preview_tag_display_widget.h" +#include "visual_deck_storage_widget.h" #include -VisualDeckStorageTagFilterWidget::VisualDeckStorageTagFilterWidget(QWidget *parent) : QWidget(parent) +VisualDeckStorageTagFilterWidget::VisualDeckStorageTagFilterWidget(VisualDeckStorageWidget *_parent) + : QWidget(_parent), parent(_parent) { setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); @@ -16,99 +18,148 @@ VisualDeckStorageTagFilterWidget::VisualDeckStorageTagFilterWidget(QWidget *pare setFixedHeight(100); - flowWidget = new FlowWidget(this, Qt::Horizontal, Qt::ScrollBarAlwaysOff, Qt::ScrollBarAsNeeded); + auto *flowWidget = new FlowWidget(this, Qt::Horizontal, Qt::ScrollBarAlwaysOff, Qt::ScrollBarAsNeeded); layout->addWidget(flowWidget); } -void VisualDeckStorageTagFilterWidget::setAllTagsProvider(const std::function()> &provider) -{ - allTagsProvider = provider; -} - void VisualDeckStorageTagFilterWidget::showEvent(QShowEvent *event) { QWidget::showEvent(event); refreshTags(); } +void VisualDeckStorageTagFilterWidget::filterDecksBySelectedTags(const QList &deckPreviews) const +{ + QStringList selectedTags; + QStringList excludedTags; + + // Collect selected and excluded tags + for (DeckPreviewTagDisplayWidget *tagWidget : findChildren()) { + switch (tagWidget->getState()) { + case TagState::Selected: + selectedTags.append(tagWidget->getTagName()); + break; + case TagState::Excluded: + excludedTags.append(tagWidget->getTagName()); + break; + default: + break; + } + } + + // If no tags are selected or excluded, show all + if (selectedTags.isEmpty() && excludedTags.isEmpty()) { + for (DeckPreviewWidget *deckPreview : deckPreviews) { + deckPreview->filteredByTags = false; + } + return; + } + + for (DeckPreviewWidget *deckPreview : deckPreviews) { + QStringList deckTags = deckPreview->deckLoader->getDeck().deckList.getTags(); + + bool hasAllSelected = std::all_of(selectedTags.begin(), selectedTags.end(), + [&deckTags](const QString &tag) { return deckTags.contains(tag); }); + + bool hasAnyExcluded = std::any_of(excludedTags.begin(), excludedTags.end(), + [&deckTags](const QString &tag) { return deckTags.contains(tag); }); + + // Filter out if any excluded tag is present or if any selected tag is missing + deckPreview->filteredByTags = !(hasAllSelected && !hasAnyExcluded); + } +} + void VisualDeckStorageTagFilterWidget::refreshTags() { - const QSet allTags = allTagsProvider ? allTagsProvider() : QSet(); + QSet allTags = gatherAllTags(); + removeTagsNotInList(allTags); + addTagsIfNotPresent(allTags); + sortTags(); +} + +void VisualDeckStorageTagFilterWidget::removeTagsNotInList(const QSet &tags) +{ + auto *flowWidget = findChild(); - // Existing chips survive if their tag is still part of the deck set, or if the chip - // is currently selected/excluded. Everything else is dropped. Dropped chips must NOT - // be re-added to the layout afterwards: they are reparented to nullptr and scheduled - // for a deferred delete (QWidget::setParent(nullptr) also hides them), and the flow - // layout would keep a dangling reference to them once the deletion runs on the next - // event-loop cycle. - QList chips; for (DeckPreviewTagDisplayWidget *tagWidget : findChildren()) { - if (tagWidget->getState() != TagState::NotSelected || allTags.contains(tagWidget->getTagName())) { - chips.append(tagWidget); - } else { + const QString &tagName = tagWidget->getTagName(); + + // Keep the tag widget if it is either selected or excluded + if (!tags.contains(tagName) && tagWidget->getState() == TagState::NotSelected) { flowWidget->removeWidget(tagWidget); - tagWidget->setParent(nullptr); tagWidget->deleteLater(); } } +} - // Add chips for tags that are not shown yet. - QSet existingTags; - for (DeckPreviewTagDisplayWidget *tagWidget : chips) { - existingTags.insert(tagWidget->getTagName()); +void VisualDeckStorageTagFilterWidget::addTagsIfNotPresent(const QSet &tags) +{ + for (const QString &tag : tags) { + addTagIfNotPresent(tag); } - for (const QString &tag : allTags) { - if (!existingTags.contains(tag)) { - auto *newTagWidget = new DeckPreviewTagDisplayWidget(this, tag); - connect(newTagWidget, &DeckPreviewTagDisplayWidget::tagClicked, this, - &VisualDeckStorageTagFilterWidget::filterChanged); - flowWidget->addWidget(newTagWidget); - chips.append(newTagWidget); +} + +void VisualDeckStorageTagFilterWidget::addTagIfNotPresent(const QString &tag) +{ + // Check if the tag already exists in the flow widget + bool tagExists = false; + for (DeckPreviewTagDisplayWidget *tagWidget : findChildren()) { + if (tagWidget->getTagName() == tag) { + tagExists = true; + break; } } - // Sort, but skip the full remove/re-add when the order already matches the layout. - // FlowWidget inherits QLayout::removeWidget's linear scan, so rebuilding an unchanged - // order would be quadratic plus a full relayout on every chip click and load batch. - std::sort(chips.begin(), chips.end(), [](DeckPreviewTagDisplayWidget *a, DeckPreviewTagDisplayWidget *b) { - const QString aName = a->getTagName(); - const QString bName = b->getTagName(); - const int compared = aName.compare(bName, Qt::CaseInsensitive); - return compared != 0 ? compared < 0 : aName < bName; - }); - if (chips == currentChipOrder) { + // If the tag doesn't exist, add a new DeckPreviewTagDisplayWidget + if (!tagExists) { + auto *newTagWidget = new DeckPreviewTagDisplayWidget(this, tag); + connect(newTagWidget, &DeckPreviewTagDisplayWidget::tagClicked, parent, + &VisualDeckStorageWidget::updateTagFilter); + connect(newTagWidget, &DeckPreviewTagDisplayWidget::tagClicked, this, + &VisualDeckStorageTagFilterWidget::refreshTags); + auto *flowWidget = findChild(); + flowWidget->addWidget(newTagWidget); + } +} + +void VisualDeckStorageTagFilterWidget::sortTags() +{ + auto *flowWidget = findChild(); + if (!flowWidget) { return; } - for (DeckPreviewTagDisplayWidget *tagWidget : chips) { + + // Get all tag widgets + QList tagWidgets = findChildren(); + + // Sort widgets by tag name + std::sort(tagWidgets.begin(), tagWidgets.end(), [](DeckPreviewTagDisplayWidget *a, DeckPreviewTagDisplayWidget *b) { + return a->getTagName().toLower() < b->getTagName().toLower(); + }); + + // Clear and re-add widgets in sorted order + for (DeckPreviewTagDisplayWidget *tagWidget : tagWidgets) { flowWidget->removeWidget(tagWidget); } - for (DeckPreviewTagDisplayWidget *tagWidget : chips) { + for (DeckPreviewTagDisplayWidget *tagWidget : tagWidgets) { flowWidget->addWidget(tagWidget); } - currentChipOrder = chips; } -QStringList VisualDeckStorageTagFilterWidget::selectedTags() const +QSet VisualDeckStorageTagFilterWidget::gatherAllTags() const { - QStringList selected; - for (DeckPreviewTagDisplayWidget *tagWidget : findChildren()) { - if (tagWidget->getState() == TagState::Selected) { - selected.append(tagWidget->getTagName()); - } - } - return selected; -} + QSet allTags; + QList deckWidgets = parent->findChildren(); -QStringList VisualDeckStorageTagFilterWidget::excludedTags() const -{ - QStringList excluded; - for (DeckPreviewTagDisplayWidget *tagWidget : findChildren()) { - if (tagWidget->getState() == TagState::Excluded) { - excluded.append(tagWidget->getTagName()); + for (DeckPreviewWidget *widget : deckWidgets) { + if (widget->checkVisibility()) { + for (const QString &tag : widget->deckLoader->getDeck().deckList.getTags()) { + allTags.insert(tag); + } } } - return excluded; + return allTags; } QStringList VisualDeckStorageTagFilterWidget::getAllKnownTags() const diff --git a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_tag_filter_widget.h b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_tag_filter_widget.h index 5e3cb398c..3290c9e9a 100644 --- a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_tag_filter_widget.h +++ b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_tag_filter_widget.h @@ -2,58 +2,34 @@ * @file visual_deck_storage_tag_filter_widget.h * @ingroup VisualDeckStorageWidgets */ +//! \todo Document this file. #ifndef VISUAL_DECK_STORAGE_TAG_FILTER_WIDGET_H #define VISUAL_DECK_STORAGE_TAG_FILTER_WIDGET_H -#include -#include +#include "deck_preview/deck_preview_widget.h" + #include -#include - -class DeckPreviewTagDisplayWidget; -class FlowWidget; +class VisualDeckStorageWidget; class VisualDeckStorageTagFilterWidget : public QWidget { Q_OBJECT - FlowWidget *flowWidget; - std::function()> allTagsProvider; - QList currentChipOrder; + VisualDeckStorageWidget *parent; + + [[nodiscard]] QSet gatherAllTags() const; + void removeTagsNotInList(const QSet &tags); + void addTagsIfNotPresent(const QSet &tags); + void addTagIfNotPresent(const QString &tag); + void sortTags(); public: - explicit VisualDeckStorageTagFilterWidget(QWidget *parent = nullptr); + explicit VisualDeckStorageTagFilterWidget(VisualDeckStorageWidget *_parent); [[nodiscard]] QStringList getAllKnownTags() const; - - /** - * @brief Sets a provider for the full set of tags to draw chips from. - */ - void setAllTagsProvider(const std::function()> &provider); - - /** - * @brief The tags currently in "selected" state. - */ - [[nodiscard]] QStringList selectedTags() const; - - /** - * @brief The tags currently in "excluded" state. - */ - [[nodiscard]] QStringList excludedTags() const; - -signals: - /** - * Emitted when a chip's selection or exclusion state changes. - * - * The chip only emits when its state actually changed, so this fires once per - * effective toggle rather than on every click. - */ - void filterChanged(); + void filterDecksBySelectedTags(const QList &deckPreviews) const; public slots: - /** - * @brief Rebuilds the tag chips from the currently available tags. - */ void refreshTags(); void showEvent(QShowEvent *event) override; }; 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 c7218f195..4b4dee55b 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,32 +1,25 @@ #include "visual_deck_storage_widget.h" #include "../../../client/settings/cache_settings.h" -#include "../../pixel_map_generator.h" #include "../quick_settings/settings_button_widget.h" -#include "deck_preview/deck_preview_color_identity_filter_widget.h" #include "deck_preview/deck_preview_widget.h" #include "visual_deck_storage_folder_display_widget.h" -#include "visual_deck_storage_quick_settings_widget.h" #include "visual_deck_storage_search_widget.h" #include "visual_deck_storage_sort_widget.h" #include "visual_deck_storage_tag_filter_widget.h" -#include -#include +#include +#include +#include #include #include -#include #include #include -VisualDeckStorageWidget::VisualDeckStorageWidget(QWidget *parent) : QWidget(parent) +VisualDeckStorageWidget::VisualDeckStorageWidget(QWidget *parent) : QWidget(parent), folderWidget(nullptr) { - // The model and proxy own all deck data, sorting and filtering. The view widgets below - // only display the proxy's rows and their FilterMatchRole, so nothing touches the - // filesystem outside the model. - storageModel = new VisualDeckStorageModel(this); - storageProxyModel = new VisualDeckStorageSortFilterProxyModel(this); - storageProxyModel->setSourceModel(storageModel); + deckListModel = new DeckListModel(this); + deckListModel->setObjectName("visualDeckModel"); layout = new QVBoxLayout(this); layout->setSpacing(0); @@ -45,16 +38,10 @@ VisualDeckStorageWidget::VisualDeckStorageWidget(QWidget *parent) : QWidget(pare searchWidget = new VisualDeckStorageSearchWidget(this); refreshButton = new QToolButton(this); - refreshButton->setIcon(themePixmap(QStringLiteral("icons/reload"))); + refreshButton->setIcon(QPixmap("theme:icons/reload")); refreshButton->setFixedSize(32, 32); connect(refreshButton, &QPushButton::clicked, this, &VisualDeckStorageWidget::refreshIfPossible); - shareButton = new QToolButton(this); - shareButton->setIcon(themePixmap(QStringLiteral("icons/share"))); - shareButton->setFixedSize(32, 32); - shareButton->setVisible(false); - connect(shareButton, &QPushButton::clicked, this, &VisualDeckStorageWidget::shareRequested); - quickSettingsWidget = new VisualDeckStorageQuickSettingsWidget(this); connect(quickSettingsWidget, &VisualDeckStorageQuickSettingsWidget::showFoldersChanged, this, &VisualDeckStorageWidget::updateShowFolders); @@ -65,14 +52,10 @@ VisualDeckStorageWidget::VisualDeckStorageWidget(QWidget *parent) : QWidget(pare searchAndSortLayout->addWidget(sortWidget); searchAndSortLayout->addWidget(searchWidget); searchAndSortLayout->addWidget(refreshButton); - searchAndSortLayout->addWidget(shareButton); searchAndSortLayout->addWidget(quickSettingsWidget); // tag filter box tagFilterWidget = new VisualDeckStorageTagFilterWidget(this); - tagFilterWidget->setAllTagsProvider([this] { return gatherVisibleTags(); }); - connect(tagFilterWidget, &VisualDeckStorageTagFilterWidget::filterChanged, this, - &VisualDeckStorageWidget::updateTagFilter); updateTagsVisibility(SettingsCache::instance().visualDeckStorage().getVisualDeckStorageShowTagFilter()); deckPreviewSelectionAnimationEnabled = @@ -92,40 +75,6 @@ VisualDeckStorageWidget::VisualDeckStorageWidget(QWidget *parent) : QWidget(pare layout->addWidget(tagFilterWidget); layout->addWidget(scrollArea); - // The deck data changed (a load finished, or a mutation happened): re-evaluate the filters, - // since search/tag/color matches are computed against the data. Debounced so that a burst of - // load completions triggers a single re-application instead of one O(n) pass per deck. - refreshTimer = new QTimer(this); - refreshTimer->setSingleShot(true); - refreshTimer->setInterval(150); - connect(refreshTimer, &QTimer::timeout, this, [this] { - storageProxyModel->reapplyFilters(); - // A batch of decks finished loading: re-gather the tag chips from the visible decks once - // the burst settles instead of on every individual load. - tagFilterWidget->refreshTags(); - }); - connect(storageModel, &QAbstractItemModel::dataChanged, this, [this] { refreshTimer->start(); }); - connect(storageModel, &VisualDeckStorageModel::deckLoaded, this, [this] { refreshTimer->start(); }); - // A deck's file path changed: re-apply the sort, since orders like "filename" depend on it. - connect(storageModel, &VisualDeckStorageModel::deckFilePathChanged, this, [this] { storageProxyModel->resort(); }); - connect(sortWidget, &VisualDeckStorageSortWidget::sortOrderChanged, this, - &VisualDeckStorageWidget::updateSortOrder); - // The filter widgets only own their ui state. Pushing it into the proxy model - // happens here, so the children stay decoupled from the model layer. - connect(deckPreviewColorIdentityFilterWidget, &DeckPreviewColorIdentityFilterWidget::activeColorsChanged, this, - &VisualDeckStorageWidget::updateColorFilter); - connect(deckPreviewColorIdentityFilterWidget, &DeckPreviewColorIdentityFilterWidget::filterModeChanged, this, - &VisualDeckStorageWidget::updateColorFilter); - connect(searchWidget, &VisualDeckStorageSearchWidget::searchTextChanged, this, - &VisualDeckStorageWidget::updateSearchFilter); - - // The deck content search matches card names in the configured card language; - // re-run it whenever that setting changes so active searches follow immediately. - CardsDisplaySettings *cardsDisplay = &SettingsCache::instance().cardsDisplay(); - const auto reapplySearchForLanguage = [this] { storageProxyModel->reapplyFilters(); }; - connect(cardsDisplay, &CardsDisplaySettings::cardLangChanged, this, reapplySearchForLanguage); - connect(cardsDisplay, &CardsDisplaySettings::cardSearchLanguageChanged, this, reapplySearchForLanguage); - connect(CardDatabaseManager::getInstance(), &CardDatabase::cardDatabaseLoadingFinished, this, &VisualDeckStorageWidget::createRootFolderWidget); @@ -173,69 +122,7 @@ void VisualDeckStorageWidget::retranslateUi() databaseLoadIndicator->setText(tr("Loading database ...")); refreshButton->setToolTip(tr("Refresh loaded files")); - shareButton->setToolTip(tr("Select decks to share")); quickSettingsWidget->setToolTip(tr("Visual Deck Storage Settings")); - - sortWidget->retranslateUi(); -} - -void VisualDeckStorageWidget::setShareSelectable(bool selectable) -{ - if (shareSelectable == selectable) { - return; - } - shareSelectable = selectable; - if (folderWidget != nullptr) { - folderWidget->setShareSelectable(selectable); - } - emit shareSelectionChanged(); -} - -bool VisualDeckStorageWidget::isShareSelectable() const -{ - return shareSelectable; -} - -QStringList VisualDeckStorageWidget::selectedFilePaths() const -{ - QStringList selectedPaths; - if (folderWidget != nullptr) { - const auto previews = folderWidget->findChildren(); - for (DeckPreviewWidget *preview : previews) { - // Filtered-out previews stay alive hidden in their sorted place, so only decks the - // user can actually see are part of the share. - if (preview->isVisible() && preview->isShareSelected()) { - selectedPaths.append(preview->filePath); - } - } - } - return selectedPaths; -} - -void VisualDeckStorageWidget::clearShareSelection() -{ - if (folderWidget != nullptr) { - const auto previews = folderWidget->findChildren(); - for (DeckPreviewWidget *preview : previews) { - preview->setShareSelected(false); - } - } -} - -void VisualDeckStorageWidget::setShareAvailable(bool available) -{ - shareButton->setVisible(available); - shareButton->setEnabled(available); -} - -void VisualDeckStorageWidget::setShareSelectedFiles(const QStringList &paths) -{ - if (folderWidget != nullptr) { - const auto previews = folderWidget->findChildren(); - for (DeckPreviewWidget *preview : previews) { - preview->setShareSelected(paths.contains(preview->filePath)); - } - } } /** @@ -247,88 +134,72 @@ const VisualDeckStorageQuickSettingsWidget *VisualDeckStorageWidget::settings() } /** - * Reapplies all sort and filter options by updating the proxy model. + * Reapplies all sort and filter options by calling the appropriate update methods. */ void VisualDeckStorageWidget::reapplySortAndFilters() { - storageProxyModel->setSortOrder(sortWidget->currentSortOrder()); - storageProxyModel->reapplyFilters(); + updateSortOrder(); + updateTagFilter(); + updateColorFilter(); + updateSearchFilter(); } -/** - * @brief Scans the deck folder and rebuilds the folder tree of deck previews. - */ void VisualDeckStorageWidget::createRootFolderWidget() { - storageModel->setDeckPath(SettingsCache::instance().paths().getDeckPath()); - - folderWidget = - new VisualDeckStorageFolderDisplayWidget(this, this, QString(), false, quickSettingsWidget->getShowFolders()); + folderWidget = new VisualDeckStorageFolderDisplayWidget(this, this, SettingsCache::instance().paths().getDeckPath(), + false, quickSettingsWidget->getShowFolders()); scrollArea->setWidget(folderWidget); // this automatically destroys the old folderWidget scrollArea->widget()->setMaximumWidth(scrollArea->viewport()->width()); scrollArea->widget()->adjustSize(); - // Sort and filter runs against the model data, so it is safe to apply immediately. - reapplySortAndFilters(); + /* We have to schedule a QTimer here so that the sorting logic doesn't try to access widgets that haven't been + * processed by the event loop yet. Otherwise, deck sorting will intermittently segfault on some systems. + */ + QTimer::singleShot(0, this, &VisualDeckStorageWidget::reapplySortAndFilters); } void VisualDeckStorageWidget::updateShowFolders(bool enabled) { if (folderWidget) { folderWidget->updateShowFolders(enabled); + QTimer::singleShot(0, this, &VisualDeckStorageWidget::reapplySortAndFilters); } } void VisualDeckStorageWidget::updateSortOrder() { - storageProxyModel->setSortOrder(sortWidget->currentSortOrder()); + if (folderWidget) { + sortWidget->sortFolder(folderWidget); + for (VisualDeckStorageFolderDisplayWidget *subFolderWidget : + folderWidget->findChildren()) { + sortWidget->sortFolder(subFolderWidget); + } + } } void VisualDeckStorageWidget::updateTagFilter() { - const QStringList selected = tagFilterWidget->selectedTags(); - const QStringList excluded = tagFilterWidget->excludedTags(); - storageProxyModel->setTagFilter(QSet(selected.cbegin(), selected.cend()), - QSet(excluded.cbegin(), excluded.cend())); - // The visible deck set changed, so the chips are re-gathered from it. - tagFilterWidget->refreshTags(); -} - -/** - * @brief The tags of all decks currently accepted by the proxy model. - */ -QSet VisualDeckStorageWidget::gatherVisibleTags() const -{ - QSet allTags; - for (int proxyRow = 0; proxyRow < storageProxyModel->rowCount(); ++proxyRow) { - const QModelIndex index = storageProxyModel->index(proxyRow, 0); - if (!index.data(VisualDeckStorageRoles::FilterMatchRole).toBool()) { - continue; - } - const QStringList deckTags = index.data(VisualDeckStorageRoles::TagsRole).toStringList(); - for (const QString &tag : deckTags) { - allTags.insert(tag); - } + if (folderWidget) { + tagFilterWidget->filterDecksBySelectedTags(folderWidget->findChildren()); + folderWidget->updateVisibility(); } - return allTags; } -/** - * Pushes the color identity filter widget's state into the proxy model. - */ void VisualDeckStorageWidget::updateColorFilter() { - storageProxyModel->setColorFilter(deckPreviewColorIdentityFilterWidget->getFilterMode(), - deckPreviewColorIdentityFilterWidget->getActiveColors()); + if (folderWidget) { + deckPreviewColorIdentityFilterWidget->filterWidgets(folderWidget->findChildren()); + folderWidget->updateVisibility(); + } } -/** - * Pushes the search bar's text into the proxy model. - */ -void VisualDeckStorageWidget::updateSearchFilter(const QString &text) +void VisualDeckStorageWidget::updateSearchFilter() { - storageProxyModel->setSearchText(text); + if (folderWidget) { + searchWidget->filterWidgets(folderWidget->findChildren(), searchWidget->getSearchText()); + folderWidget->updateVisibility(); + } } void VisualDeckStorageWidget::updateTagsVisibility(const bool visible) @@ -344,4 +215,4 @@ void VisualDeckStorageWidget::updateTagsVisibility(const bool visible) void VisualDeckStorageWidget::updateSelectionAnimationEnabled(const bool enabled) { deckPreviewSelectionAnimationEnabled = enabled; -} +} \ No newline at end of file diff --git a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_widget.h b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_widget.h index dfb715bea..c3c0ae91b 100644 --- a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_widget.h +++ b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_widget.h @@ -2,30 +2,29 @@ * @file visual_deck_storage_widget.h * @ingroup VisualDeckStorageWidgets */ +//! \todo Document this file. #ifndef VISUAL_DECK_STORAGE_WIDGET_H #define VISUAL_DECK_STORAGE_WIDGET_H -#include "visual_deck_storage_model.h" -#include "visual_deck_storage_sort_filter_proxy_model.h" +#include "../../deck_loader/deck_loader.h" +#include "../cards/card_size_widget.h" +#include "../quick_settings/settings_button_widget.h" +#include "deck_preview/deck_preview_color_identity_filter_widget.h" +#include "visual_deck_storage_folder_display_widget.h" +#include "visual_deck_storage_quick_settings_widget.h" +#include "visual_deck_storage_search_widget.h" +#include "visual_deck_storage_sort_widget.h" +#include "visual_deck_storage_tag_filter_widget.h" -#include -#include -#include -#include -#include +#include -class QLabel; -class QResizeEvent; -class QShowEvent; -class QTimer; -class DeckPreviewColorIdentityFilterWidget; -class VisualDeckStorageFolderDisplayWidget; -class VisualDeckStorageQuickSettingsWidget; +class QSpinBox; class VisualDeckStorageSearchWidget; class VisualDeckStorageSortWidget; class VisualDeckStorageTagFilterWidget; - +class VisualDeckStorageFolderDisplayWidget; +class DeckPreviewColorIdentityFilterWidget; class VisualDeckStorageWidget final : public QWidget { Q_OBJECT @@ -33,71 +32,44 @@ public: explicit VisualDeckStorageWidget(QWidget *parent); void refreshIfPossible(); void retranslateUi(); - void setShareSelectable(bool selectable); - [[nodiscard]] bool isShareSelectable() const; - [[nodiscard]] QStringList selectedFilePaths() const; - void setShareSelectedFiles(const QStringList &paths); - void clearShareSelection(); - void setShareAvailable(bool available); VisualDeckStorageTagFilterWidget *tagFilterWidget; bool deckPreviewSelectionAnimationEnabled; [[nodiscard]] const VisualDeckStorageQuickSettingsWidget *settings() const; - [[nodiscard]] VisualDeckStorageModel *model() const - { - return storageModel; - } - [[nodiscard]] VisualDeckStorageSortFilterProxyModel *proxyModel() const - { - return storageProxyModel; - } public slots: - /** - * @brief Starts scanning the deck folder and rebuilds the folder tree and previews. - */ - void createRootFolderWidget(); + void createRootFolderWidget(); // Refresh the display of cards based on the current sorting option void updateShowFolders(bool enabled); + void updateTagFilter(); + void updateColorFilter(); + void updateSearchFilter(); void updateTagsVisibility(bool visible); void updateSelectionAnimationEnabled(bool enabled); void updateSortOrder(); - void updateTagFilter(); - void updateColorFilter(); - void updateSearchFilter(const QString &text); - -signals: - void deckLoadRequested(const QString &filePath); - void openDeckEditor(const LoadedDeck &deck); - void shareDeckRequested(const QString &filePath); - void shareSelectionChanged(); - void shareRequested(); - -protected: void resizeEvent(QResizeEvent *event) override; void showEvent(QShowEvent *event) override; -private: - void reapplySortAndFilters(); - [[nodiscard]] QSet gatherVisibleTags() const; +signals: + void bannerCardsRefreshed(); + void deckLoadRequested(const QString &filePath); + void openDeckEditor(const LoadedDeck &deck); private: QVBoxLayout *layout; QWidget *searchAndSortContainer; QHBoxLayout *searchAndSortLayout; + DeckListModel *deckListModel; QLabel *databaseLoadIndicator; VisualDeckStorageSortWidget *sortWidget; VisualDeckStorageSearchWidget *searchWidget; DeckPreviewColorIdentityFilterWidget *deckPreviewColorIdentityFilterWidget; QToolButton *refreshButton; - QToolButton *shareButton; VisualDeckStorageQuickSettingsWidget *quickSettingsWidget; QScrollArea *scrollArea; - VisualDeckStorageFolderDisplayWidget *folderWidget = nullptr; - VisualDeckStorageModel *storageModel = nullptr; - VisualDeckStorageSortFilterProxyModel *storageProxyModel = nullptr; - QTimer *refreshTimer = nullptr; ///< Coalesces the re-apply/refresh burst following a batch of deck loads. - bool shareSelectable = false; + VisualDeckStorageFolderDisplayWidget *folderWidget; + + void reapplySortAndFilters(); }; #endif // VISUAL_DECK_STORAGE_WIDGET_H diff --git a/cockatrice/src/interface/window_main.cpp b/cockatrice/src/interface/window_main.cpp index 595d38d5b..44e188760 100644 --- a/cockatrice/src/interface/window_main.cpp +++ b/cockatrice/src/interface/window_main.cpp @@ -19,7 +19,6 @@ ***************************************************************************/ #include "window_main.h" -#include "../client/latency_status_widget.h" #include "../client/network/update/client/client_update_checker.h" #include "../client/network/update/client/release_channel.h" #include "../client/settings/cache_settings.h" @@ -32,20 +31,10 @@ #include "../interface/widgets/dialogs/dlg_tip_of_the_day.h" #include "../interface/widgets/dialogs/dlg_update.h" #include "../interface/widgets/dialogs/dlg_view_log.h" -#include "../interface/widgets/onboarding/first_run_wizard.h" -#include "../interface/widgets/settings_page/general_settings_page.h" #include "../interface/widgets/tabs/tab_game.h" -#include "../interface/widgets/tabs/tab_server.h" #include "../interface/widgets/tabs/tab_supervisor.h" #include "../main.h" -#include "intents/contexts/context_connect_to_server.h" -#include "intents/contexts/context_join_room.h" -#include "intents/intent_connect_to_server.h" -#include "intents/intent_login.h" -#include "intents/intent_open_server_room_by_name.h" -#include "intents/url_parser.h" #include "logger.h" -#include "pixel_map_generator.h" #include "version_string.h" #include "widgets/dialogs/dlg_connect.h" #include "widgets/server/handle_public_servers.h" @@ -60,7 +49,6 @@ #include #include #include -#include #include #include #include @@ -89,12 +77,11 @@ #include #include #include -#include #include #define GITHUB_PAGES_URL "https://cockatrice.github.io" -#define GITHUB_CONTRIBUTORS_URL "https://github.com/Cockatrice/Cockatrice/graphs/contributors" -#define GITHUB_CONTRIBUTE_URL "https://github.com/Cockatrice/Cockatrice#" +#define GITHUB_CONTRIBUTORS_URL "https://github.com/Cockatrice/Cockatrice/graphs/contributors?type=c" +#define GITHUB_CONTRIBUTE_URL "https://github.com/Cockatrice/Cockatrice#cockatrice" #define GITHUB_TRANSIFEX_TRANSLATORS_URL "https://github.com/Cockatrice/Cockatrice/wiki/Translator-Hall-of-Fame" #define GITHUB_TRANSLATOR_FAQ_URL "https://github.com/Cockatrice/Cockatrice/wiki/Translation-FAQ" #define GITHUB_ISSUES_URL "https://github.com/Cockatrice/Cockatrice/issues" @@ -247,8 +234,6 @@ void MainWindow::actFullScreen(bool checked) void MainWindow::actSettings() { DlgSettings dlg(this); - auto *generalPage = qobject_cast(dlg.page(DlgSettings::GeneralPage)); - connect(generalPage, &GeneralSettingsPage::cardDatabaseUpdateRequested, this, &MainWindow::actCheckCardUpdates); dlg.exec(); } @@ -275,8 +260,7 @@ void MainWindow::actAbout() GITHUB_TROUBLESHOOTING_URL + "'>" + tr("Troubleshooting") + "
" + "" + tr("F.A.Q.") + "
"), QMessageBox::Ok, this); - mb.setIconPixmap( - themePixmap(QStringLiteral("cockatrice")).scaled(64, 64, Qt::KeepAspectRatio, Qt::SmoothTransformation)); + mb.setIconPixmap(QPixmap("theme:cockatrice").scaled(64, 64, Qt::KeepAspectRatio, Qt::SmoothTransformation)); mb.setTextInteractionFlags(Qt::TextBrowserInteraction); mb.exec(); } @@ -328,7 +312,7 @@ void MainWindow::retranslateUi() aRegister->setText(tr("&Register to server...")); aForgotPassword->setText(tr("&Restore password...")); aSettings->setText(tr("&Settings...")); - aSettings->setIcon(themePixmap(QStringLiteral("icons/settings"))); + aSettings->setIcon(QPixmap("theme:icons/settings")); aExit->setText(tr("&Exit")); #if defined(__APPLE__) /* For OSX */ @@ -356,7 +340,6 @@ void MainWindow::retranslateUi() aStatusBar->setText(tr("Show Status Bar")); aViewLog->setText(tr("View &Debug Log")); aOpenSettingsFolder->setText(tr("Open Settings Folder")); - aFirstRunWizard->setText(tr("Re-run Onboarding Wizard...")); aShow->setText(tr("Show/Hide")); @@ -418,8 +401,6 @@ void MainWindow::createActions() connect(aViewLog, &QAction::triggered, this, &MainWindow::actViewLog); aOpenSettingsFolder = new QAction(this); connect(aOpenSettingsFolder, &QAction::triggered, this, &MainWindow::actOpenSettingsFolder); - aFirstRunWizard = new QAction(this); - connect(aFirstRunWizard, &QAction::triggered, this, [this] { runFirstRunWizard(); }); aShow = new QAction(this); connect(aShow, &QAction::triggered, this, &MainWindow::actShow); @@ -498,8 +479,6 @@ void MainWindow::createMenus() helpMenu->addAction(aStatusBar); helpMenu->addAction(aViewLog); helpMenu->addAction(aOpenSettingsFolder); - helpMenu->addSeparator(); - helpMenu->addAction(aFirstRunWizard); } MainWindow::MainWindow(QWidget *parent) @@ -511,8 +490,6 @@ MainWindow::MainWindow(QWidget *parent) pixmapCacheSizeChanged(SettingsCache::instance().cacheStorage().getPixmapCacheSize()); connectionController = new ConnectionController(this, this); - urlParser = new IntentUrlParser(this, this); - connect(urlParser, &IntentUrlParser::urlChainFinished, this, &MainWindow::onUrlChainFinished); createActions(); createMenus(); @@ -524,7 +501,6 @@ MainWindow::MainWindow(QWidget *parent) connect(tabSupervisor, &TabSupervisor::setMenu, this, &MainWindow::updateTabMenu); connect(tabSupervisor, &TabSupervisor::localGameEnded, this, &MainWindow::localGameEnded); connect(tabSupervisor, &TabSupervisor::showWindowIfHidden, this, &MainWindow::showWindowIfHidden); - connect(tabSupervisor, &TabSupervisor::cockatriceLinkActivated, this, &MainWindow::handleCockatriceLink); connect(connectionController, &ConnectionController::tabSupervisorStartRequested, tabSupervisor, &TabSupervisor::start); connect(connectionController, &ConnectionController::tabSupervisorStopRequested, tabSupervisor, @@ -549,12 +525,6 @@ MainWindow::MainWindow(QWidget *parent) [this](bool show) { statusBar()->setVisible(show); }); statusBar()->setVisible(SettingsCache::instance().userInterface().getShowStatusBar()); - latencyStatus = new LatencyStatusWidget(this); - statusBar()->addPermanentWidget(latencyStatus); - - connect(connectionController, &ConnectionController::pingStatsUpdated, latencyStatus, - &LatencyStatusWidget::updateData); - connect(&SettingsCache::instance().shortcuts(), &ShortcutsSettings::shortCutChanged, this, &MainWindow::refreshShortcuts); refreshShortcuts(); @@ -570,7 +540,6 @@ MainWindow::MainWindow(QWidget *parent) // run startup check async QTimer::singleShot(0, this, &MainWindow::startupConfigCheck); - QTimer::singleShot(0, this, &MainWindow::applyStartupDestination); } void MainWindow::startupConfigCheck() @@ -597,10 +566,9 @@ void MainWindow::startupConfigCheck() // no config found, 99% new clean install qCInfo(WindowMainStartupVersionLog) << "Startup: old client version empty, assuming first start after clean install"; + alertForcedOracleRun(VERSION_STRING, false); SettingsCache::instance().downloads().resetToDefaultURLs(); // populate the download urls SettingsCache::instance().network().setClientVersion(VERSION_STRING); - actCheckServerUpdates(); - runFirstRunWizard(); if (QString(VERSION_STRING).contains("custom", Qt::CaseInsensitive)) { SettingsCache::instance().updates().setCheckUpdatesOnStartup(false); @@ -680,105 +648,6 @@ void MainWindow::startupConfigCheck() } } -void MainWindow::runFirstRunWizard() -{ - auto *wizard = new FirstRunWizard(this); - wizard->setAttribute(Qt::WA_DeleteOnClose); - - connect(wizard, &FirstRunWizard::cardDatabaseUpdateRequested, this, &MainWindow::actCheckCardUpdatesBackground); - connect(wizard, &FirstRunWizard::manualCardDatabaseSetupRequested, this, &MainWindow::actCheckCardUpdates); - connect(this, &MainWindow::cardDatabaseUpdateFinished, wizard, &FirstRunWizard::onCardDatabaseUpdateFinished); - connect(this, &MainWindow::cardDatabaseUpdateProgress, wizard, &FirstRunWizard::onCardDatabaseUpdateProgress); - connect(wizard, &FirstRunWizard::registerRequested, connectionController, &ConnectionController::registerToServer); - connect(wizard, &FirstRunWizard::connectRequested, connectionController, &ConnectionController::connectToServer); - - wizard->setModal(true); - wizard->show(); -} - -/** - * Drives the server-based startup destinations (Server lobby, Server Room) through the intent - * system: fetch saved credentials, connect to the configured server, then land on the Lobby or - * join the configured room by name. - */ -void MainWindow::applyStartupDestination() -{ - // An explicit command-line connect takes precedence over the startup destination. - if (!connectTo.isEmpty()) { - return; - } - - // A cockatrice:// link owns the startup connection while its chain runs; - // connecting here would race (and tear down) the link's own connection. - if (skipStartupAutoConnect) { - return; - } - - const int destination = SettingsCache::instance().tabs().getStartupTabIndex(); - if (destination != StartupTab::StartupTabServer && destination != StartupTab::StartupTabServerRoom) { - return; - } - - const QString host = SettingsCache::instance().tabs().getStartupServerHost(); - const QString port = SettingsCache::instance().tabs().getStartupServerPort(); - if (host.isEmpty() || port.isEmpty()) { - qCWarning(WindowMainStartupLog) << "Startup destination needs a configured server"; - return; - } - - auto serverContext = std::make_shared(); - serverContext->hostname = host; - serverContext->port = port; - - auto *credentials = new IntentGetLoginCredentials(serverContext.get()); - auto *connector = new IntentConnectToServer(getRemoteClient(), serverContext.get()); - - connect(credentials, &Intent::finished, connector, &Intent::execute); - connect(credentials, &Intent::failed, this, &MainWindow::startupDestinationFailed); - connect(credentials, &Intent::cancelled, this, [this]() { startupDestinationFailed(tr("Sign-in cancelled")); }); - connect(connector, &Intent::finished, this, - [this, destination, serverContext]() { onStartupDestinationConnected(destination, *serverContext); }); - connect(connector, &Intent::failed, this, &MainWindow::startupDestinationFailed); - - credentials->execute(); -} - -void MainWindow::onStartupDestinationConnected(int destination, const ContextConnectToServer &serverContext) -{ - // The server tab must exist: it is what requests the room list. - if (!tabSupervisor->getTabServer()) { - tabSupervisor->openTabServer(); - } - - if (destination == StartupTab::StartupTabServerRoom) { - auto roomContext = std::make_unique(); - roomContext->serverContext = serverContext; - auto *roomIntent = new IntentOpenServerRoomByName(tabSupervisor, getRemoteClient(), std::move(roomContext), - SettingsCache::instance().tabs().getStartupRoomName()); - roomIntent->setParent(this); - connect(roomIntent, &Intent::failed, this, &MainWindow::startupDestinationFailed); - roomIntent->execute(); - return; - } - - if (tabSupervisor->getTabServer()) { - tabSupervisor->setCurrentWidget(tabSupervisor->getTabServer()); - } else { - qCWarning(WindowMainStartupLog) << "Startup destination: server tab could not be opened"; - } -} - -void MainWindow::startupDestinationFailed(const QString &reason) -{ - qCWarning(WindowMainStartupLog) << "Startup destination failed:" << reason; -} - -bool MainWindow::startupDestinationConnectsToServer() const -{ - const int destination = SettingsCache::instance().tabs().getStartupTabIndex(); - return destination == StartupTab::StartupTabServer || destination == StartupTab::StartupTabServerRoom; -} - void MainWindow::alertForcedOracleRun(const QString &version, bool isUpdate) { if (isUpdate) { @@ -830,7 +699,7 @@ void MainWindow::createTrayIcon() trayIcon = new QSystemTrayIcon(this); trayIcon->setContextMenu(trayIconMenu); - trayIcon->setIcon(themePixmap(QStringLiteral("cockatrice"))); + trayIcon->setIcon(QPixmap("theme:cockatrice")); trayIcon->show(); } @@ -857,17 +726,6 @@ void MainWindow::closeEvent(QCloseEvent *event) } bClosingDown = true; - if (cardUpdateProcess && cardUpdateProcess->state() != QProcess::NotRunning) { - if (QMessageBox::question(this, tr("Are you sure?"), - tr("A card database update is still running. Quitting now will cancel it.\n" - "Are you sure you want to quit?"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No) == QMessageBox::No) { - event->ignore(); - bClosingDown = false; - return; - } - } - if (!tabSupervisor->close()) { event->ignore(); bClosingDown = false; @@ -887,7 +745,17 @@ void MainWindow::changeEvent(QEvent *event) } else if (event->type() == QEvent::ActivationChange) { if (isActiveWindow() && !bHasActivated) { bHasActivated = true; - attemptStartupAutoConnect(); + if (!connectTo.isEmpty()) { + qCInfo(WindowMainStartupAutoconnectLog) << "Command line connect to " << connectTo; + connectionController->connectToServerDirect(connectTo.host(), connectTo.port(), connectTo.userName(), + connectTo.password()); + } else if (SettingsCache::instance().servers().getAutoConnect() && + !SettingsCache::instance().debug().getLocalGameOnStartup()) { + qCInfo(WindowMainStartupAutoconnectLog) << "Attempting auto-connect..."; + DlgConnect dlg(this); + connectionController->connectToServerDirect(dlg.getHost(), static_cast(dlg.getPort()), + dlg.getPlayerName(), dlg.getPassword()); + } } } @@ -908,64 +776,6 @@ void MainWindow::showWindowIfHidden() show(); } -void MainWindow::handleCockatriceLink(const QString &url) -{ - urlParser->handle(url); -} - -void MainWindow::attemptStartupAutoConnect() -{ - if (startupAutoConnectAttempted || skipStartupAutoConnect) { - return; - } - startupAutoConnectAttempted = true; - - if (!connectTo.isEmpty()) { - qCInfo(WindowMainStartupAutoconnectLog) << "Command line connect to " << connectTo; - connectionController->connectToServerDirect(connectTo.host(), connectTo.port(), connectTo.userName(), - connectTo.password()); - } else if (SettingsCache::instance().servers().getAutoConnect() && - !SettingsCache::instance().debug().getLocalGameOnStartup() && !startupDestinationConnectsToServer()) { - qCInfo(WindowMainStartupAutoconnectLog) << "Attempting auto-connect..."; - DlgConnect dlg(this); - connectionController->connectToServerDirect(dlg.getHost(), static_cast(dlg.getPort()), - dlg.getPlayerName(), dlg.getPassword()); - } -} - -void MainWindow::onUrlChainFinished(bool connected) -{ - // A cockatrice:// link owns the startup connection while it runs. When its - // chain ended without connecting (declined, invalid, offline), fall back to - // the startup connection so the activation launch still behaves like a - // normal launch. - if (connected) { - // The launch link connected, so the startup fallback has served its - // purpose: drop the skip so a later mid-session link that ends declined - // or offline cannot silently fire auto-connect or applyStartupDestination - // again. - skipStartupAutoConnect = false; - return; - } - - if (!skipStartupAutoConnect || getRemoteClient()->getStatus() != StatusDisconnected) { - return; - } - - if (startupDestinationConnectsToServer()) { - // Users whose startup tab is a Server / Server Room connect through the - // startup destination, not through auto-connect; retry that instead. - qCInfo(WindowMainStartupAutoconnectLog) << "URL chain ended without a connection; retrying startup destination"; - skipStartupAutoConnect = false; - applyStartupDestination(); - return; - } - - qCInfo(WindowMainStartupAutoconnectLog) << "URL chain ended without a connection; retrying startup connect"; - skipStartupAutoConnect = false; - attemptStartupAutoConnect(); -} - void MainWindow::cardDatabaseLoadingFailed() { if (askedForDbUpdater) { @@ -1103,7 +913,7 @@ void MainWindow::createCardUpdateProcess(bool background) if (dir.exists(binaryName)) { updaterCmd = dir.absoluteFilePath(binaryName); - } else { // try and find the directory Oracle is stored in the build directory + } else { // try and find the directory oracle is stored in the build directory QDir findLocalDir(dir); findLocalDir.cdUp(); findLocalDir.cd(getCardUpdaterBinaryName()); @@ -1117,57 +927,19 @@ void MainWindow::createCardUpdateProcess(bool background) QMessageBox::warning(this, tr("Error"), tr("Unable to run the card database updater: ") + dir.absoluteFilePath(binaryName)); exitCardDatabaseUpdate(); - emit cardDatabaseUpdateFinished(false); return; } if (!background) { cardUpdateProcess->start(updaterCmd, QStringList()); } else { - cardUpdateOutputBuffer.clear(); - connect(cardUpdateProcess, &QProcess::readyReadStandardOutput, this, &MainWindow::cardUpdateProgressOutput); cardUpdateProcess->start(updaterCmd, QStringList("-b")); statusBar()->showMessage(tr("Card database update running.")); } } -void MainWindow::cardUpdateProgressOutput() -{ - if (!cardUpdateProcess) { - return; - } - cardUpdateOutputBuffer.append(cardUpdateProcess->readAllStandardOutput()); - while (true) { - const int newline = cardUpdateOutputBuffer.indexOf('\n'); - if (newline < 0) { - break; - } - const QByteArray line = cardUpdateOutputBuffer.left(newline).trimmed(); - cardUpdateOutputBuffer.remove(0, newline + 1); - // Protocol emitted by `oracle -b`: "PROGRESS " - if (!line.startsWith("PROGRESS ")) { - continue; - } - const QList parts = line.split(' '); - if (parts.size() != 4) { - continue; - } - bool doneOk = false; - bool totalOk = false; - const qint64 done = parts.at(2).toLongLong(&doneOk); - const qint64 total = parts.at(3).toLongLong(&totalOk); - if (!doneOk || !totalOk || done < 0 || total < 0) { - continue; - } - emit cardDatabaseUpdateProgress(QString::fromLatin1(parts.at(1)), done, total); - } -} - void MainWindow::exitCardDatabaseUpdate() { - if (!cardUpdateProcess) { - return; - } cardUpdateProcess->deleteLater(); cardUpdateProcess = nullptr; statusBar()->clearMessage(); @@ -1205,19 +977,14 @@ void MainWindow::cardUpdateError(QProcess::ProcessError err) exitCardDatabaseUpdate(); QMessageBox::warning(this, tr("Error"), tr("The card database updater exited with an error:\n%1").arg(error)); - emit cardDatabaseUpdateFinished(false); } -void MainWindow::cardUpdateFinished(int exitCode, QProcess::ExitStatus exitStatus) +void MainWindow::cardUpdateFinished(int, QProcess::ExitStatus exitStatus) { - cardUpdateProgressOutput(); // drain any progress lines not yet parsed - - const bool success = (exitStatus == QProcess::NormalExit) && (exitCode == 0); if (exitStatus == QProcess::NormalExit) { SettingsCache::instance().updates().setLastCardUpdateCheck(QDateTime::currentDateTime().date()); } exitCardDatabaseUpdate(); - emit cardDatabaseUpdateFinished(success); } void MainWindow::actCheckServerUpdates() diff --git a/cockatrice/src/interface/window_main.h b/cockatrice/src/interface/window_main.h index 9e45a4c3e..fa6c79915 100644 --- a/cockatrice/src/interface/window_main.h +++ b/cockatrice/src/interface/window_main.h @@ -25,7 +25,6 @@ #ifndef WINDOW_H #define WINDOW_H -#include "../client/lag_monitor.h" #include "connection_controller/remote_connection_controller.h" #include "widgets/dialogs/dlg_local_game_options.h" @@ -50,29 +49,16 @@ class GameReplay; class HandlePublicServers; class LocalClient; class LocalServer; -class QLabel; -class LatencyStatusWidget; class QThread; class RemoteClient; class ServerInfo_User; class TabSupervisor; class WndSets; class DlgTipOfTheDay; -struct ContextConnectToServer; -class IntentUrlParser; class MainWindow : public QMainWindow { Q_OBJECT -signals: - /** @brief Emitted after the background card-database update subprocess exits. */ - void cardDatabaseUpdateFinished(bool success); - - /** @brief Emitted while the background card-database update subprocess runs. - * @p stage is one of "download", "scan" or "import"; @p done/@p total - * are byte counts for the first two stages and set indices for "import". */ - void cardDatabaseUpdateProgress(const QString &stage, qint64 done, qint64 total); - public slots: void actCheckCardUpdates(); void actCheckCardUpdatesBackground(); @@ -80,7 +66,6 @@ public slots: void actCheckClientUpdates(); void actConnect(); void actExit(); - void handleCockatriceLink(const QString &url); private slots: void updateTabMenu(const QList &newMenuList); void statusChanged(ClientStatus _status); @@ -98,11 +83,9 @@ private slots: void actOpenSettingsFolder(); void actShow(); void showWindowIfHidden(); - void onUrlChainFinished(bool connected); void cardUpdateError(QProcess::ProcessError err); void cardUpdateFinished(int exitCode, QProcess::ExitStatus exitStatus); - void cardUpdateProgressOutput(); void refreshShortcuts(); void cardDatabaseLoadingFailed(); void cardDatabaseNewSetsFound(int numUnknownSets, QStringList unknownSetsNames); @@ -122,13 +105,6 @@ private slots: void startupConfigCheck(); void alertForcedOracleRun(const QString &version, bool isUpdate); - void applyStartupDestination(); - void onStartupDestinationConnected(int destination, const ContextConnectToServer &serverContext); - void startupDestinationFailed(const QString &reason); - [[nodiscard]] bool startupDestinationConnectsToServer() const; - - void attemptStartupAutoConnect(); - private: static const QString appName; static const QStringList fileNameFilters; @@ -138,9 +114,6 @@ private: void createTrayIcon(); int getNextCustomSetPrefix(QDir dataDir); - - void runFirstRunWizard(); - inline QString getCardUpdaterBinaryName() { return "oracle"; @@ -156,21 +129,15 @@ private: QAction *aConnect, *aDisconnect, *aRegister, *aForgotPassword, *aSinglePlayer, *aWatchReplay, *aFullScreen; QAction *aManageSets, *aEditTokens, *aOpenCustomFolder, *aOpenCustomsetsFolder, *aAddCustomSet, *aReloadCardDatabase; - QAction *aTips, *aUpdate, *aCheckCardUpdates, *aCheckCardUpdatesBackground, *aFirstRunWizard, *aStatusBar, - *aViewLog, *aOpenSettingsFolder; + QAction *aTips, *aUpdate, *aCheckCardUpdates, *aCheckCardUpdatesBackground, *aStatusBar, *aViewLog, + *aOpenSettingsFolder; TabSupervisor *tabSupervisor; - IntentUrlParser *urlParser; WndSets *wndSets; ConnectionController *connectionController; LocalServer *localServer; - LagMonitor lagMonitor; ///< watches the main thread for event loop stalls - LatencyStatusWidget *latencyStatus = nullptr; ///< status bar widget with live round-trip stats and history graph bool bHasActivated, askedForDbUpdater; - bool skipStartupAutoConnect = false; - bool startupAutoConnectAttempted = false; QProcess *cardUpdateProcess; - QByteArray cardUpdateOutputBuffer; DlgViewLog *logviewDialog; GameReplay *replay; DlgTipOfTheDay *tip; @@ -182,16 +149,6 @@ public: { connectTo = QUrl(QString("cockatrice://%1").arg(url)); } - // When set, the window's own startup connection (--connect or auto-connect - // on first activation) is skipped. Used for activation launches: the intent - // chain triggered by a cockatrice:// URL owns the connection, and letting - // auto-connect race against it caused two connectToServer calls to tear - // each other down. onUrlChainFinished() clears this and retries the startup - // connection when the link's chain ended without connecting. - void setSkipStartupAutoConnect(bool skip) - { - skipStartupAutoConnect = skip; - } ~MainWindow() override; RemoteClient *getRemoteClient() const diff --git a/cockatrice/src/main.cpp b/cockatrice/src/main.cpp index b9c30d1ad..814da9808 100644 --- a/cockatrice/src/main.cpp +++ b/cockatrice/src/main.cpp @@ -26,6 +26,7 @@ #include "client/url_scheme_event_filter.h" #include "database/interface/settings_card_preference_provider.h" #include "interface/intents/intent_open_local_deck.h" +#include "interface/intents/url_parser.h" #include "interface/logger.h" #include "interface/pixel_map_generator.h" #include "interface/theme_manager.h" @@ -44,8 +45,6 @@ #include #include #include -#include -#include #include #include #include @@ -54,7 +53,6 @@ #include #include #include -#include QTranslator *translator, *qtTranslator; RNG_Abstract *rng; @@ -178,18 +176,6 @@ QString const generateClientID() return strClientID; } -static QString redactActivationUrl(const QString &url) -{ - // Activation URLs carry secrets in their query string (e.g. the deck share - // token); log only the scheme and the action (cockatrice://opendeck), never - // the parameters. - if (!url.startsWith(QStringLiteral("cockatrice://"))) { - return url; - } - const QUrl parsed(url); - return parsed.scheme() + "://" + parsed.host(); -} - int main(int argc, char *argv[]) { #ifdef Q_OS_WIN @@ -244,7 +230,7 @@ int main(int argc, char *argv[]) // These values are only used by the settings loader/saver // Wrong or outdated values are kept to not break things QCoreApplication::setOrganizationName("Cockatrice"); - QCoreApplication::setOrganizationDomain("cockatrice.github.io"); + QCoreApplication::setOrganizationDomain("cockatrice.de"); QCoreApplication::setApplicationName("Cockatrice"); QCoreApplication::setApplicationVersion(VERSION_STRING); @@ -263,7 +249,7 @@ int main(int argc, char *argv[]) // Command-line parser QCommandLineParser parser; - parser.setApplicationDescription("Cockatrice Client"); + parser.setApplicationDescription("Cockatrice"); parser.addHelpOption(); parser.addVersionOption(); @@ -286,17 +272,10 @@ int main(int argc, char *argv[]) SingleInstanceManager instance; if (hasActivationFiles) { - QStringList redactedFiles; - redactedFiles.reserve(startupFiles.size()); - for (const QString &file : startupFiles) { - redactedFiles.append(redactActivationUrl(file)); - } - qCInfo(MainLog) << "Activation launch, files:" << redactedFiles; // Activation launch: hand off to the primary instance if one is // running, otherwise become the primary ourselves. Do this before // constructing the main window so a hand-off exits cheaply. if (!instance.tryRun(startupFiles)) { - qInfo() << "Handed off to a running instance, exiting"; // Sent successfully → exit return 0; } @@ -313,7 +292,7 @@ int main(int argc, char *argv[]) } } - rng = new RNG_SFMT(CryptoUtil::randomUInt64()); + rng = new RNG_SFMT; themeManager = new ThemeManager; soundEngine = new SoundEngine; @@ -345,30 +324,10 @@ int main(int argc, char *argv[]) MainWindow ui; - // A URL launch must own the connection: the intent chain triggered by the - // URL connects to the server named in the URL, so the window's own startup - // auto-connect must not race against it (two connectToServer calls tear - // each other down via doDisconnectFromServer). - bool hasUrlActivation = std::any_of(startupFiles.begin(), startupFiles.end(), [](const QString &file) { - return file.startsWith(QStringLiteral("cockatrice://")); - }); -#ifdef Q_OS_MAC - // On macOS the launch can arrive through the URL scheme instead of as a - // positional argument (captured in pendingMacUrls); count those too or the - // window would auto-connect into the link's own connection attempt. - hasUrlActivation = hasUrlActivation || - std::any_of(pendingMacUrls.cbegin(), pendingMacUrls.cend(), - [](const QString &url) { return url.startsWith(QStringLiteral("cockatrice://")); }); -#endif - ui.setSkipStartupAutoConnect(hasUrlActivation); - auto handleActivation = [&ui](const QString &file) { if (file.startsWith("cockatrice://")) { - qCInfo(MainLog) << "Handling URL activation:" << redactActivationUrl(file); - // Route through the window's persistent url parser: it serializes - // link chains so activations handed over while another chain is - // still connecting do not connect concurrently. - ui.handleCockatriceLink(file); + auto urlParser = new IntentUrlParser(&ui, &ui); + urlParser->handle(file); } else if (QFileInfo(file).exists()) { auto openDeckIntent = new IntentOpenLocalDeck(ui.getTabSupervisor(), file); QObject::connect(openDeckIntent, &Intent::failed, &ui, [&ui](const QString &reason) { @@ -389,9 +348,9 @@ int main(int argc, char *argv[]) } qCInfo(MainLog) << "MainWindow constructor finished"; - ui.setWindowIcon(themePixmap(QStringLiteral("cockatrice"))); - // Set name of the app desktop file; used by wayland to load the window icon - QGuiApplication::setDesktopFileName("Cockatrice"); + ui.setWindowIcon(QPixmap("theme:cockatrice")); + // set name of the app desktop file; used by wayland to load the window icon + QGuiApplication::setDesktopFileName("cockatrice"); SettingsCache::instance().network().setClientID(generateClientID()); @@ -431,7 +390,6 @@ int main(int argc, char *argv[]) PingPixmapGenerator::clear(); CountryPixmapGenerator::clear(); UserLevelPixmapGenerator::clear(); - ManaSymbolPixmapGenerator::clear(); return ret; } diff --git a/cockatrice/src/single_instance_manager.cpp b/cockatrice/src/single_instance_manager.cpp index eff0e6586..aca23160c 100644 --- a/cockatrice/src/single_instance_manager.cpp +++ b/cockatrice/src/single_instance_manager.cpp @@ -2,14 +2,6 @@ #include -namespace -{ -// Sent by the primary instance after it has read a forwarded payload. Without -// an acknowledgment, a second instance cannot tell a live primary apart from a -// stale socket left behind by a process that is still shutting down. -const QByteArray ACK_MESSAGE = QByteArrayLiteral("COCKATRICE_ACK"); -} // namespace - SingleInstanceManager::SingleInstanceManager(QObject *parent) : QObject(parent) { } @@ -28,15 +20,9 @@ bool SingleInstanceManager::tryRun(const QStringList &filesToSend) } serverName = QStringLiteral("CockatriceSingleInstance-%1").arg(userName); - // Hand off to an already-running primary instance if one exists. Never steal - // the socket of a busy primary: it is alive and will act on the payload. - switch (forwardToPrimary(filesToSend)) { - case ForwardResult::Delivered: - return false; - case ForwardResult::PrimaryBusy: - return false; - case ForwardResult::NoPrimary: - break; + // Hand off to an already-running primary instance if one exists. + if (forwardToPrimary(filesToSend)) { + return false; } // No primary instance is currently reachable, so become the primary. @@ -49,18 +35,12 @@ bool SingleInstanceManager::tryRun(const QStringList &filesToSend) // Another instance may have started while we were probing; hand off to it // instead of stealing its socket. - switch (forwardToPrimary(filesToSend)) { - case ForwardResult::Delivered: - return false; - case ForwardResult::PrimaryBusy: - return false; - case ForwardResult::NoPrimary: - break; + if (forwardToPrimary(filesToSend)) { + return false; } - // The socket is stale (left over by a crashed instance), so no primary is - // holding it: remove it and retry. If that still fails, another instance - // just took the name. + // The socket is stale (left over by a crashed instance): remove it and + // retry. If that still fails, another instance just took the name. QLocalServer::removeServer(serverName); if (server->listen(serverName)) { return true; @@ -70,12 +50,12 @@ bool SingleInstanceManager::tryRun(const QStringList &filesToSend) return false; } -SingleInstanceManager::ForwardResult SingleInstanceManager::forwardToPrimary(const QStringList &filesToSend) +bool SingleInstanceManager::forwardToPrimary(const QStringList &filesToSend) { QLocalSocket socket; socket.connectToServer(serverName); if (!socket.waitForConnected(200)) { - return ForwardResult::NoPrimary; + return false; } // Serialize payload with length prefix @@ -92,23 +72,7 @@ SingleInstanceManager::ForwardResult SingleInstanceManager::forwardToPrimary(con socket.flush(); socket.waitForBytesWritten(1000); - // A plain launch has nothing for the primary to act on, so there is nothing - // to acknowledge. Waiting here would block the new instance for seconds if - // the primary is busy in a modal dialog, so only the activation path (which - // needs the ACK to avoid stealing a live primary's socket) waits below. - if (filesToSend.isEmpty()) { - return ForwardResult::Delivered; - } - - // Only report a successful hand-off once the primary has acknowledged that - // it actually read the payload. A socket that connects but is still working - // on an earlier payload is alive but busy, not dead: give it more room - // before giving up, so a slow handler does not make a live primary look - // dead (which would lead to stealing its socket). - if (!socket.waitForReadyRead(1000) && !socket.waitForReadyRead(4000)) { - return ForwardResult::PrimaryBusy; - } - return socket.readAll() == ACK_MESSAGE ? ForwardResult::Delivered : ForwardResult::PrimaryBusy; + return true; } void SingleInstanceManager::handleNewConnection() @@ -147,23 +111,12 @@ void SingleInstanceManager::handleNewConnection() QStringList files; payloadStream >> files; - // Acknowledge receipt as soon as the payload is parsed, before the - // primary starts handling it. The handlers run synchronously and can - // take longer than the sender's readiness timeout (e.g. a modal - // confirmation box), which would otherwise make a live primary look - // dead and cause duplicate handling. - socket->write(ACK_MESSAGE); - socket->flush(); + emit filesReceived(files); - // Drop the payload from the buffer before handling it: the handlers - // run synchronously and can spin a nested event loop (e.g. a modal - // dialog) that re-reads this socket, which would re-parse and re-emit - // the same files. + // Reset buffer (single message use-case) buffer->clear(); *expectedSize = 0; - emit filesReceived(files); - socket->disconnectFromServer(); return; } diff --git a/cockatrice/src/single_instance_manager.h b/cockatrice/src/single_instance_manager.h index d3884a60f..55bff0e80 100644 --- a/cockatrice/src/single_instance_manager.h +++ b/cockatrice/src/single_instance_manager.h @@ -23,14 +23,7 @@ private slots: void handleNewConnection(); private: - enum class ForwardResult - { - Delivered, // a live primary acknowledged the payload - NoPrimary, // no connectable primary socket exists - PrimaryBusy // a primary exists but did not acknowledge in time - }; - - ForwardResult forwardToPrimary(const QStringList &filesToSend); + bool forwardToPrimary(const QStringList &filesToSend); QString serverName; QLocalServer *server = nullptr; diff --git a/cockatrice/themes/CMakeLists.txt b/cockatrice/themes/CMakeLists.txt index 70344d95a..577977551 100644 --- a/cockatrice/themes/CMakeLists.txt +++ b/cockatrice/themes/CMakeLists.txt @@ -2,7 +2,7 @@ # # add themes subfolders -set(defthemes Fabric Fusion Leather Plasma VelvetMarble System) +set(defthemes Default Fabric Fusion Leather Plasma VelvetMarble) if(UNIX) if(APPLE) diff --git a/cockatrice/themes/System/palette-default-dark.toml b/cockatrice/themes/Default/palette-default-dark.toml similarity index 96% rename from cockatrice/themes/System/palette-default-dark.toml rename to cockatrice/themes/Default/palette-default-dark.toml index e101935b4..3ee174a2f 100644 --- a/cockatrice/themes/System/palette-default-dark.toml +++ b/cockatrice/themes/Default/palette-default-dark.toml @@ -61,7 +61,3 @@ ToolTipBase = #ffffffdc ToolTipText = #ff000000 PlaceholderText = #6effffff - -[AppColors] -AccentStrong = #ff148c3c -AccentSoft = #ff78c850 diff --git a/cockatrice/themes/System/theme.cfg b/cockatrice/themes/Default/theme.cfg similarity index 73% rename from cockatrice/themes/System/theme.cfg rename to cockatrice/themes/Default/theme.cfg index 4b756a5e8..d2016a238 100644 --- a/cockatrice/themes/System/theme.cfg +++ b/cockatrice/themes/Default/theme.cfg @@ -2,4 +2,4 @@ ColorScheme = Light [Style] -Name = System +Name = Default diff --git a/cockatrice/themes/Fabric/backgrounds/home-dark.png b/cockatrice/themes/Fabric/backgrounds/home-dark.png deleted file mode 100644 index 8d9514626..000000000 Binary files a/cockatrice/themes/Fabric/backgrounds/home-dark.png and /dev/null differ diff --git a/cockatrice/themes/Fabric/backgrounds/home-light.png b/cockatrice/themes/Fabric/backgrounds/home-light.png deleted file mode 100644 index 78f51a686..000000000 Binary files a/cockatrice/themes/Fabric/backgrounds/home-light.png and /dev/null differ diff --git a/cockatrice/themes/Fabric/palette-default-dark.toml b/cockatrice/themes/Fabric/palette-default-dark.toml deleted file mode 100644 index cad18921a..000000000 --- a/cockatrice/themes/Fabric/palette-default-dark.toml +++ /dev/null @@ -1,74 +0,0 @@ -# Fabric — dark identity palette (navy-cloth chrome) -[Palette] -WindowText = #e8ecf2 -Button = #2b374a -Light = #3a4759 -Midlight = #333f50 -Dark = #141a23 -Mid = #252c38 -Text = #e8ecf2 -BrightText = #8ec2ff -ButtonText = #f2f4f8 -Base = #222c3d -Window = #18202e -Shadow = #0b0f15 -Highlight = #4a82e6 -HighlightedText = #ffffff -Link = #7fb0f2 -LinkVisited = #a98ad9 -AlternateBase = #1d2636 -ToolTipBase = #2b3546 -ToolTipText = #f2f4f8 -PlaceholderText = #6ee8ecf2 -Accent = #4a82e6 - -[Palette.Disabled] -WindowText = #9d9d9d -Button = #18202e -Light = #3a4759 -Midlight = #333f50 -Dark = #141a23 -Mid = #252c38 -Text = #9d9d9d -BrightText = #8ec2ff -ButtonText = #787878 -Base = #18202e -Window = #18202e -Shadow = #0b0f15 -Highlight = #222d40 -HighlightedText = #9d9d9d -Link = #308cc6 -LinkVisited = #b450ff -AlternateBase = #1d2636 -ToolTipBase = #2b3546 -ToolTipText = #f2f4f8 -PlaceholderText = #6ee8ecf2 -Accent = #9d9d9d - -[Palette.Inactive] -WindowText = #e8ecf2 -Button = #2b374a -Light = #3a4759 -Midlight = #333f50 -Dark = #141a23 -Mid = #252c38 -Text = #e8ecf2 -BrightText = #8ec2ff -ButtonText = #f2f4f8 -Base = #222c3d -Window = #18202e -Shadow = #0b0f15 -Highlight = #222d40 -HighlightedText = #ffffff -Link = #7fb0f2 -LinkVisited = #a98ad9 -AlternateBase = #1d2636 -ToolTipBase = #2b3546 -ToolTipText = #f2f4f8 -PlaceholderText = #6ee8ecf2 -Accent = #4a82e6 - -[AppColors] -AccentStrong = #3a6fd6 -AccentSoft = #8fb8f2 - diff --git a/cockatrice/themes/Fabric/palette-default-light.toml b/cockatrice/themes/Fabric/palette-default-light.toml deleted file mode 100644 index f5151af3e..000000000 --- a/cockatrice/themes/Fabric/palette-default-light.toml +++ /dev/null @@ -1,74 +0,0 @@ -# Fabric — light identity palette (navy-cloth chrome) -[Palette] -WindowText = #1c2330 -Button = #c8d6ea -Light = #ffffff -Midlight = #b9c6dc -Dark = #8fa0bd -Mid = #a3b3cd -Text = #1c2330 -BrightText = #1d4fa0 -ButtonText = #141c29 -Base = #f6f8fc -Window = #dbe3ef -Shadow = #5a6a85 -Highlight = #2f6fd6 -HighlightedText = #ffffff -Link = #2555b0 -LinkVisited = #6a4bb8 -AlternateBase = #e9eef7 -ToolTipBase = #e9eff8 -ToolTipText = #141c29 -PlaceholderText = #7a1c2330 -Accent = #2f6fd6 - -[Palette.Disabled] -WindowText = #787878 -Button = #dbe3ef -Light = #ffffff -Midlight = #b9c6dc -Dark = #8fa0bd -Mid = #a3b3cd -Text = #787878 -BrightText = #1d4fa0 -ButtonText = #969696 -Base = #dbe3ef -Window = #dbe3ef -Shadow = #5a6a85 -Highlight = #cdd8e9 -HighlightedText = #787878 -Link = #0000ff -LinkVisited = #ff00ff -AlternateBase = #e9eef7 -ToolTipBase = #e9eff8 -ToolTipText = #141c29 -PlaceholderText = #7a1c2330 -Accent = #787878 - -[Palette.Inactive] -WindowText = #1c2330 -Button = #c8d6ea -Light = #ffffff -Midlight = #b9c6dc -Dark = #8fa0bd -Mid = #a3b3cd -Text = #1c2330 -BrightText = #1d4fa0 -ButtonText = #141c29 -Base = #f6f8fc -Window = #dbe3ef -Shadow = #5a6a85 -Highlight = #cdd8e9 -HighlightedText = #000000 -Link = #2555b0 -LinkVisited = #6a4bb8 -AlternateBase = #e9eef7 -ToolTipBase = #e9eff8 -ToolTipText = #141c29 -PlaceholderText = #7a1c2330 -Accent = #2f6fd6 - -[AppColors] -AccentStrong = #3a6fd6 -AccentSoft = #8fb8f2 - diff --git a/cockatrice/themes/Fabric/theme.cfg b/cockatrice/themes/Fabric/theme.cfg deleted file mode 100644 index 55b916e71..000000000 --- a/cockatrice/themes/Fabric/theme.cfg +++ /dev/null @@ -1,5 +0,0 @@ -[Appearance] -ColorScheme = System - -[Style] -Name = Fusion \ No newline at end of file diff --git a/cockatrice/themes/Fabric/zones/handzone-light.png b/cockatrice/themes/Fabric/zones/handzone-light.png deleted file mode 100644 index a87f9e292..000000000 Binary files a/cockatrice/themes/Fabric/zones/handzone-light.png and /dev/null differ diff --git a/cockatrice/themes/Fabric/zones/handzone.png b/cockatrice/themes/Fabric/zones/handzone.png index a5b72286c..25d22b070 100644 Binary files a/cockatrice/themes/Fabric/zones/handzone.png and b/cockatrice/themes/Fabric/zones/handzone.png differ diff --git a/cockatrice/themes/Fabric/zones/playerzone-light.png b/cockatrice/themes/Fabric/zones/playerzone-light.png deleted file mode 100644 index 05da3ea9b..000000000 Binary files a/cockatrice/themes/Fabric/zones/playerzone-light.png and /dev/null differ diff --git a/cockatrice/themes/Fabric/zones/playerzone.png b/cockatrice/themes/Fabric/zones/playerzone.png index 7fa977249..728fccdfa 100644 Binary files a/cockatrice/themes/Fabric/zones/playerzone.png and b/cockatrice/themes/Fabric/zones/playerzone.png differ diff --git a/cockatrice/themes/Fabric/zones/stackzone-light.png b/cockatrice/themes/Fabric/zones/stackzone-light.png deleted file mode 100644 index dcd6dc045..000000000 Binary files a/cockatrice/themes/Fabric/zones/stackzone-light.png and /dev/null differ diff --git a/cockatrice/themes/Fabric/zones/stackzone.png b/cockatrice/themes/Fabric/zones/stackzone.png index 9046aa049..89e2080f8 100644 Binary files a/cockatrice/themes/Fabric/zones/stackzone.png and b/cockatrice/themes/Fabric/zones/stackzone.png differ diff --git a/cockatrice/themes/Fabric/zones/tablezone-light.png b/cockatrice/themes/Fabric/zones/tablezone-light.png deleted file mode 100644 index cb297e7e1..000000000 Binary files a/cockatrice/themes/Fabric/zones/tablezone-light.png and /dev/null differ diff --git a/cockatrice/themes/Fabric/zones/tablezone.png b/cockatrice/themes/Fabric/zones/tablezone.png index 8048a4cc1..6a2ce68e1 100644 Binary files a/cockatrice/themes/Fabric/zones/tablezone.png and b/cockatrice/themes/Fabric/zones/tablezone.png differ diff --git a/cockatrice/themes/Fusion/palette-default-dark.toml b/cockatrice/themes/Fusion/palette-default-dark.toml index b180e9b63..c1d83a4cd 100644 --- a/cockatrice/themes/Fusion/palette-default-dark.toml +++ b/cockatrice/themes/Fusion/palette-default-dark.toml @@ -11,15 +11,15 @@ ButtonText = #ffffffff Base = #ff2d2d2d Window = #ff1e1e1e Shadow = #ff000000 -Highlight = #ff139740 +Highlight = #ff148c3c HighlightedText = #ffffffff -Link = #ffc9fd62 -LinkVisited = #ff9ad43e +Link = #ff00f652 +LinkVisited = #ff00d346 AlternateBase = #ff353535 ToolTipBase = #ff3c3c3c ToolTipText = #ffd4d4d4 PlaceholderText = #80ffffff -Accent = #ffc9fd62 +Accent = #ff00d346 [Palette.Disabled] WindowText = #ff9d9d9d @@ -34,7 +34,7 @@ ButtonText = #ff9d9d9d Base = #ff1e1e1e Window = #ff1e1e1e Shadow = #ff000000 -Highlight = #ff139740 +Highlight = #ff148c3c HighlightedText = #ffffffff Link = #ff308cc6 LinkVisited = #ffff00ff @@ -59,15 +59,11 @@ Window = #ff1e1e1e Shadow = #ff000000 Highlight = #ff1e1e1e HighlightedText = #ffffffff -Link = #ffc9fd62 -LinkVisited = #ff9ad43e +Link = #ff00f652 +LinkVisited = #ff00d346 AlternateBase = #ff353535 ToolTipBase = #ff3c3c3c ToolTipText = #ffd4d4d4 PlaceholderText = #80ffffff Accent = #ff1e1e1e - -[AppColors] -AccentStrong = #ff139740 -AccentSoft = #ffc9fd62 diff --git a/cockatrice/themes/Fusion/palette-default-light.toml b/cockatrice/themes/Fusion/palette-default-light.toml index 5a1163a2b..86c41be78 100644 --- a/cockatrice/themes/Fusion/palette-default-light.toml +++ b/cockatrice/themes/Fusion/palette-default-light.toml @@ -11,15 +11,15 @@ ButtonText = #ff000000 Base = #ffffffff Window = #fff0f0f0 Shadow = #ff696969 -Highlight = #ff139740 +Highlight = #ff148c3c HighlightedText = #ffffffff -Link = #ff0e6b31 -LinkVisited = #ff0a521f +Link = #ff0d5f28 +LinkVisited = #ff08401b AlternateBase = #ffe9e7e3 ToolTipBase = #ffffffdc ToolTipText = #ff000000 PlaceholderText = #80000000 -Accent = #ff139740 +Accent = #ff107532 [Palette.Disabled] WindowText = #ff787878 @@ -34,7 +34,7 @@ ButtonText = #ff787878 Base = #fff0f0f0 Window = #fff0f0f0 Shadow = #ff000000 -Highlight = #ff139740 +Highlight = #ff148c3c HighlightedText = #ffffffff Link = #ff0000ff LinkVisited = #ffff00ff @@ -59,15 +59,11 @@ Window = #fff0f0f0 Shadow = #ff696969 Highlight = #fff0f0f0 HighlightedText = #ff000000 -Link = #ff0e6b31 -LinkVisited = #ff0a521f +Link = #ff0d5f28 +LinkVisited = #ff08401b AlternateBase = #ffe9e7e3 ToolTipBase = #ffffffdc ToolTipText = #ff000000 PlaceholderText = #80000000 Accent = #fff0f0f0 - -[AppColors] -AccentStrong = #ff139740 -AccentSoft = #ffc9fd62 diff --git a/cockatrice/themes/Leather/backgrounds/home-dark.png b/cockatrice/themes/Leather/backgrounds/home-dark.png deleted file mode 100644 index 20d16a27f..000000000 Binary files a/cockatrice/themes/Leather/backgrounds/home-dark.png and /dev/null differ diff --git a/cockatrice/themes/Leather/backgrounds/home-light.png b/cockatrice/themes/Leather/backgrounds/home-light.png deleted file mode 100644 index ee1da361d..000000000 Binary files a/cockatrice/themes/Leather/backgrounds/home-light.png and /dev/null differ diff --git a/cockatrice/themes/Leather/palette-default-dark.toml b/cockatrice/themes/Leather/palette-default-dark.toml deleted file mode 100644 index b0db0e90a..000000000 --- a/cockatrice/themes/Leather/palette-default-dark.toml +++ /dev/null @@ -1,74 +0,0 @@ -# Leather — dark identity palette (black-brown chrome with brass accents) -[Palette] -WindowText = #eee9e2 -Button = #332d26 -Light = #4a4239 -Midlight = #3d3730 -Dark = #131009 -Mid = #26221c -Text = #eee9e2 -BrightText = #e3c27e -ButtonText = #f5f1ea -Base = #26221d -Window = #1d1a16 -Shadow = #0d0b07 -Highlight = #4a5f8f -HighlightedText = #ffffff -Link = #cfa263 -LinkVisited = #9a7db8 -AlternateBase = #211d19 -ToolTipBase = #37302a -ToolTipText = #f5f1ea -PlaceholderText = #6eeee9e2 -Accent = #c9995a - -[Palette.Disabled] -WindowText = #9d9d9d -Button = #1d1a16 -Light = #4a4239 -Midlight = #3d3730 -Dark = #131009 -Mid = #26221c -Text = #9d9d9d -BrightText = #e3c27e -ButtonText = #787878 -Base = #1d1a16 -Window = #1d1a16 -Shadow = #0d0b07 -Highlight = #2d2822 -HighlightedText = #9d9d9d -Link = #308cc6 -LinkVisited = #b450ff -AlternateBase = #211d19 -ToolTipBase = #37302a -ToolTipText = #f5f1ea -PlaceholderText = #6eeee9e2 -Accent = #9d9d9d - -[Palette.Inactive] -WindowText = #eee9e2 -Button = #332d26 -Light = #4a4239 -Midlight = #3d3730 -Dark = #131009 -Mid = #26221c -Text = #eee9e2 -BrightText = #e3c27e -ButtonText = #f5f1ea -Base = #26221d -Window = #1d1a16 -Shadow = #0d0b07 -Highlight = #2d2822 -HighlightedText = #ffffff -Link = #cfa263 -LinkVisited = #9a7db8 -AlternateBase = #211d19 -ToolTipBase = #37302a -ToolTipText = #f5f1ea -PlaceholderText = #6eeee9e2 -Accent = #c9995a - -[AppColors] -AccentStrong = #b7823b -AccentSoft = #e4c58f - diff --git a/cockatrice/themes/Leather/palette-default-light.toml b/cockatrice/themes/Leather/palette-default-light.toml deleted file mode 100644 index ff122db76..000000000 --- a/cockatrice/themes/Leather/palette-default-light.toml +++ /dev/null @@ -1,74 +0,0 @@ -# Leather — light identity palette (black-brown chrome with brass accents) -[Palette] -WindowText = #2a2114 -Button = #dfcbb0 -Light = #ffffff -Midlight = #c6b08c -Dark = #a18a64 -Mid = #b29a74 -Text = #2a2114 -BrightText = #7a531c -ButtonText = #1f1710 -Base = #fdf8ee -Window = #f0e2cb -Shadow = #5d4d33 -Highlight = #34508c -HighlightedText = #ffffff -Link = #8a5e1e -LinkVisited = #6b5190 -AlternateBase = #f5ebd7 -ToolTipBase = #fff7e8 -ToolTipText = #1f1710 -PlaceholderText = #7a2a2114 -Accent = #a5712f - -[Palette.Disabled] -WindowText = #787878 -Button = #f0e2cb -Light = #ffffff -Midlight = #c6b08c -Dark = #a18a64 -Mid = #b29a74 -Text = #787878 -BrightText = #7a531c -ButtonText = #969696 -Base = #f0e2cb -Window = #f0e2cb -Shadow = #5d4d33 -Highlight = #ecd9bb -HighlightedText = #787878 -Link = #0000ff -LinkVisited = #ff00ff -AlternateBase = #f5ebd7 -ToolTipBase = #fff7e8 -ToolTipText = #1f1710 -PlaceholderText = #7a2a2114 -Accent = #787878 - -[Palette.Inactive] -WindowText = #2a2114 -Button = #dfcbb0 -Light = #ffffff -Midlight = #c6b08c -Dark = #a18a64 -Mid = #b29a74 -Text = #2a2114 -BrightText = #7a531c -ButtonText = #1f1710 -Base = #fdf8ee -Window = #f0e2cb -Shadow = #5d4d33 -Highlight = #ecd9bb -HighlightedText = #000000 -Link = #8a5e1e -LinkVisited = #6b5190 -AlternateBase = #f5ebd7 -ToolTipBase = #fff7e8 -ToolTipText = #1f1710 -PlaceholderText = #7a2a2114 -Accent = #a5712f - -[AppColors] -AccentStrong = #b7823b -AccentSoft = #e4c58f - diff --git a/cockatrice/themes/Leather/theme.cfg b/cockatrice/themes/Leather/theme.cfg deleted file mode 100644 index 55b916e71..000000000 --- a/cockatrice/themes/Leather/theme.cfg +++ /dev/null @@ -1,5 +0,0 @@ -[Appearance] -ColorScheme = System - -[Style] -Name = Fusion \ No newline at end of file diff --git a/cockatrice/themes/Leather/zones/handzone-light.png b/cockatrice/themes/Leather/zones/handzone-light.png deleted file mode 100644 index b2c9bba34..000000000 Binary files a/cockatrice/themes/Leather/zones/handzone-light.png and /dev/null differ diff --git a/cockatrice/themes/Leather/zones/handzone.png b/cockatrice/themes/Leather/zones/handzone.png index 5f803df74..aba879683 100644 Binary files a/cockatrice/themes/Leather/zones/handzone.png and b/cockatrice/themes/Leather/zones/handzone.png differ diff --git a/cockatrice/themes/Leather/zones/playerzone-light.png b/cockatrice/themes/Leather/zones/playerzone-light.png deleted file mode 100644 index a26f96a5e..000000000 Binary files a/cockatrice/themes/Leather/zones/playerzone-light.png and /dev/null differ diff --git a/cockatrice/themes/Leather/zones/playerzone.png b/cockatrice/themes/Leather/zones/playerzone.png index cccbb3fd3..4f42ee41d 100644 Binary files a/cockatrice/themes/Leather/zones/playerzone.png and b/cockatrice/themes/Leather/zones/playerzone.png differ diff --git a/cockatrice/themes/Leather/zones/stackzone-light.png b/cockatrice/themes/Leather/zones/stackzone-light.png deleted file mode 100644 index 117c738f7..000000000 Binary files a/cockatrice/themes/Leather/zones/stackzone-light.png and /dev/null differ diff --git a/cockatrice/themes/Leather/zones/stackzone.png b/cockatrice/themes/Leather/zones/stackzone.png index d9cff892b..c02e2284a 100644 Binary files a/cockatrice/themes/Leather/zones/stackzone.png and b/cockatrice/themes/Leather/zones/stackzone.png differ diff --git a/cockatrice/themes/Leather/zones/tablezone-light.png b/cockatrice/themes/Leather/zones/tablezone-light.png deleted file mode 100644 index 912cbfb94..000000000 Binary files a/cockatrice/themes/Leather/zones/tablezone-light.png and /dev/null differ diff --git a/cockatrice/themes/Leather/zones/tablezone.png b/cockatrice/themes/Leather/zones/tablezone.png index 8af4c853d..152f4f7e9 100644 Binary files a/cockatrice/themes/Leather/zones/tablezone.png and b/cockatrice/themes/Leather/zones/tablezone.png differ diff --git a/cockatrice/themes/Plasma/backgrounds/home-dark.png b/cockatrice/themes/Plasma/backgrounds/home-dark.png deleted file mode 100644 index ce1469e2b..000000000 Binary files a/cockatrice/themes/Plasma/backgrounds/home-dark.png and /dev/null differ diff --git a/cockatrice/themes/Plasma/backgrounds/home-light.png b/cockatrice/themes/Plasma/backgrounds/home-light.png deleted file mode 100644 index 2e9000bd6..000000000 Binary files a/cockatrice/themes/Plasma/backgrounds/home-light.png and /dev/null differ diff --git a/cockatrice/themes/Plasma/palette-default-dark.toml b/cockatrice/themes/Plasma/palette-default-dark.toml deleted file mode 100644 index 222456f4f..000000000 --- a/cockatrice/themes/Plasma/palette-default-dark.toml +++ /dev/null @@ -1,74 +0,0 @@ -# Plasma — dark identity palette (electric violet chrome with cyan sparks) -[Palette] -WindowText = #eeebff -Button = #2a2440 -Light = #453d63 -Midlight = #383252 -Dark = #0e0c16 -Mid = #211e31 -Text = #eeebff -BrightText = #7fd7ff -ButtonText = #f6f4ff -Base = #1e1a2e -Window = #151220 -Shadow = #08070e -Highlight = #6a4df0 -HighlightedText = #ffffff -Link = #37c7e8 -LinkVisited = #9b7aff -AlternateBase = #1a1726 -ToolTipBase = #2c2745 -ToolTipText = #f6f4ff -PlaceholderText = #6eeeebff -Accent = #6a4df0 - -[Palette.Disabled] -WindowText = #9d9d9d -Button = #151220 -Light = #453d63 -Midlight = #383252 -Dark = #0e0c16 -Mid = #211e31 -Text = #9d9d9d -BrightText = #7fd7ff -ButtonText = #787878 -Base = #151220 -Window = #151220 -Shadow = #08070e -Highlight = #211c32 -HighlightedText = #9d9d9d -Link = #308cc6 -LinkVisited = #b450ff -AlternateBase = #1a1726 -ToolTipBase = #2c2745 -ToolTipText = #f6f4ff -PlaceholderText = #6eeeebff -Accent = #9d9d9d - -[Palette.Inactive] -WindowText = #eeebff -Button = #2a2440 -Light = #453d63 -Midlight = #383252 -Dark = #0e0c16 -Mid = #211e31 -Text = #eeebff -BrightText = #7fd7ff -ButtonText = #f6f4ff -Base = #1e1a2e -Window = #151220 -Shadow = #08070e -Highlight = #211c32 -HighlightedText = #ffffff -Link = #37c7e8 -LinkVisited = #9b7aff -AlternateBase = #1a1726 -ToolTipBase = #2c2745 -ToolTipText = #f6f4ff -PlaceholderText = #6eeeebff -Accent = #6a4df0 - -[AppColors] -AccentStrong = #5b3ee0 -AccentSoft = #a28bf7 - diff --git a/cockatrice/themes/Plasma/palette-default-light.toml b/cockatrice/themes/Plasma/palette-default-light.toml deleted file mode 100644 index d54db31d2..000000000 --- a/cockatrice/themes/Plasma/palette-default-light.toml +++ /dev/null @@ -1,74 +0,0 @@ -# Plasma — light identity palette (electric violet chrome with cyan sparks) -[Palette] -WindowText = #1d1830 -Button = #cfc6f0 -Light = #ffffff -Midlight = #b7adde -Dark = #9589c8 -Mid = #a49ad2 -Text = #1d1830 -BrightText = #10406e -ButtonText = #120d26 -Base = #f9f7ff -Window = #e3dcf7 -Shadow = #554c85 -Highlight = #5a3ee2 -HighlightedText = #ffffff -Link = #0f8fb5 -LinkVisited = #6a45d6 -AlternateBase = #ece8fa -ToolTipBase = #f5f3ff -ToolTipText = #120d26 -PlaceholderText = #7a1d1830 -Accent = #5a3ee2 - -[Palette.Disabled] -WindowText = #787878 -Button = #e3dcf7 -Light = #ffffff -Midlight = #b7adde -Dark = #9589c8 -Mid = #a49ad2 -Text = #787878 -BrightText = #10406e -ButtonText = #969696 -Base = #e3dcf7 -Window = #e3dcf7 -Shadow = #554c85 -Highlight = #d6ccf3 -HighlightedText = #787878 -Link = #0000ff -LinkVisited = #ff00ff -AlternateBase = #ece8fa -ToolTipBase = #f5f3ff -ToolTipText = #120d26 -PlaceholderText = #7a1d1830 -Accent = #787878 - -[Palette.Inactive] -WindowText = #1d1830 -Button = #cfc6f0 -Light = #ffffff -Midlight = #b7adde -Dark = #9589c8 -Mid = #a49ad2 -Text = #1d1830 -BrightText = #10406e -ButtonText = #120d26 -Base = #f9f7ff -Window = #e3dcf7 -Shadow = #554c85 -Highlight = #d6ccf3 -HighlightedText = #000000 -Link = #0f8fb5 -LinkVisited = #6a45d6 -AlternateBase = #ece8fa -ToolTipBase = #f5f3ff -ToolTipText = #120d26 -PlaceholderText = #7a1d1830 -Accent = #5a3ee2 - -[AppColors] -AccentStrong = #5b3ee0 -AccentSoft = #a28bf7 - diff --git a/cockatrice/themes/Plasma/theme.cfg b/cockatrice/themes/Plasma/theme.cfg deleted file mode 100644 index 55b916e71..000000000 --- a/cockatrice/themes/Plasma/theme.cfg +++ /dev/null @@ -1,5 +0,0 @@ -[Appearance] -ColorScheme = System - -[Style] -Name = Fusion \ No newline at end of file diff --git a/cockatrice/themes/Plasma/zones/handzone-light.png b/cockatrice/themes/Plasma/zones/handzone-light.png deleted file mode 100644 index 89fa7e6d4..000000000 Binary files a/cockatrice/themes/Plasma/zones/handzone-light.png and /dev/null differ diff --git a/cockatrice/themes/Plasma/zones/handzone.png b/cockatrice/themes/Plasma/zones/handzone.png index 82debb405..543571d8a 100644 Binary files a/cockatrice/themes/Plasma/zones/handzone.png and b/cockatrice/themes/Plasma/zones/handzone.png differ diff --git a/cockatrice/themes/Plasma/zones/playerzone-light.png b/cockatrice/themes/Plasma/zones/playerzone-light.png deleted file mode 100644 index 9034818f2..000000000 Binary files a/cockatrice/themes/Plasma/zones/playerzone-light.png and /dev/null differ diff --git a/cockatrice/themes/Plasma/zones/playerzone.png b/cockatrice/themes/Plasma/zones/playerzone.png index 4623524f6..eebcd1f84 100644 Binary files a/cockatrice/themes/Plasma/zones/playerzone.png and b/cockatrice/themes/Plasma/zones/playerzone.png differ diff --git a/cockatrice/themes/Plasma/zones/stackzone-light.png b/cockatrice/themes/Plasma/zones/stackzone-light.png deleted file mode 100644 index 2607b3389..000000000 Binary files a/cockatrice/themes/Plasma/zones/stackzone-light.png and /dev/null differ diff --git a/cockatrice/themes/Plasma/zones/stackzone.png b/cockatrice/themes/Plasma/zones/stackzone.png index 144d991f3..1845c7091 100644 Binary files a/cockatrice/themes/Plasma/zones/stackzone.png and b/cockatrice/themes/Plasma/zones/stackzone.png differ diff --git a/cockatrice/themes/Plasma/zones/tablezone-light.png b/cockatrice/themes/Plasma/zones/tablezone-light.png deleted file mode 100644 index dcf2ef95e..000000000 Binary files a/cockatrice/themes/Plasma/zones/tablezone-light.png and /dev/null differ diff --git a/cockatrice/themes/Plasma/zones/tablezone.png b/cockatrice/themes/Plasma/zones/tablezone.png index 4dc36ec2d..8f998d4bb 100644 Binary files a/cockatrice/themes/Plasma/zones/tablezone.png and b/cockatrice/themes/Plasma/zones/tablezone.png differ diff --git a/cockatrice/themes/System/palette-default-light.toml b/cockatrice/themes/System/palette-default-light.toml deleted file mode 100644 index 14c215cdf..000000000 --- a/cockatrice/themes/System/palette-default-light.toml +++ /dev/null @@ -1,67 +0,0 @@ -[Palette] -WindowText = #ff000000 -Button = #fff0f0f0 -Light = #ffffffff -Midlight = #ffe3e3e3 -Dark = #ffa0a0a0 -Mid = #ffa0a0a0 -Text = #ff000000 -BrightText = #ffffffff -ButtonText = #ff000000 -Base = #ffffffff -Window = #fff0f0f0 -Shadow = #ff696969 -HighlightedText = #ffffffff -Link = #ff0d5f28 -LinkVisited = #ff08401b -AlternateBase = #ffe9e7e3 -ToolTipBase = #ffffffdc -ToolTipText = #ff000000 -PlaceholderText = #80000000 - -[Palette.Disabled] -WindowText = #ff787878 -Button = #fff0f0f0 -Light = #ffffffff -Midlight = #fff7f7f7 -Dark = #ffa0a0a0 -Mid = #ffa0a0a0 -Text = #ff787878 -BrightText = #ffffffff -ButtonText = #ff787878 -Base = #fff0f0f0 -Window = #fff0f0f0 -Shadow = #ff000000 -HighlightedText = #ffffffff -Link = #ff0000ff -LinkVisited = #ffff00ff -AlternateBase = #fff7f7f7 -ToolTipBase = #ffffffdc -ToolTipText = #ff000000 -PlaceholderText = #80000000 - -[Palette.Inactive] -WindowText = #ff000000 -Button = #fff0f0f0 -Light = #ffffffff -Midlight = #ffe3e3e3 -Dark = #ffa0a0a0 -Mid = #ffa0a0a0 -Text = #ff000000 -BrightText = #ffffffff -ButtonText = #ff000000 -Base = #ffffffff -Window = #fff0f0f0 -Shadow = #ff696969 -HighlightedText = #ff000000 -Link = #ff0d5f28 -LinkVisited = #ff08401b -AlternateBase = #ffe9e7e3 -ToolTipBase = #ffffffdc -ToolTipText = #ff000000 -PlaceholderText = #80000000 - - -[AppColors] -AccentStrong = #ff148c3c -AccentSoft = #ff78c850 \ No newline at end of file diff --git a/cockatrice/themes/VelvetMarble/backgrounds/home-dark.png b/cockatrice/themes/VelvetMarble/backgrounds/home-dark.png deleted file mode 100644 index 34738090f..000000000 Binary files a/cockatrice/themes/VelvetMarble/backgrounds/home-dark.png and /dev/null differ diff --git a/cockatrice/themes/VelvetMarble/backgrounds/home-light.png b/cockatrice/themes/VelvetMarble/backgrounds/home-light.png deleted file mode 100644 index de783e086..000000000 Binary files a/cockatrice/themes/VelvetMarble/backgrounds/home-light.png and /dev/null differ diff --git a/cockatrice/themes/VelvetMarble/palette-default-dark.toml b/cockatrice/themes/VelvetMarble/palette-default-dark.toml deleted file mode 100644 index 496943931..000000000 --- a/cockatrice/themes/VelvetMarble/palette-default-dark.toml +++ /dev/null @@ -1,74 +0,0 @@ -# VelvetMarble — dark identity palette (charcoal-velvet chrome with slate marble) -[Palette] -WindowText = #e8eaee -Button = #262a31 -Light = #3a3f47 -Midlight = #31363d -Dark = #0e1013 -Mid = #1f2329 -Text = #e8eaee -BrightText = #c3d8f2 -ButtonText = #f2f3f6 -Base = #1b1e23 -Window = #131519 -Shadow = #08090b -Highlight = #8fa6c0 -HighlightedText = #101318 -Link = #a9c6e8 -LinkVisited = #b9a8d9 -AlternateBase = #171a1f -ToolTipBase = #2d323a -ToolTipText = #f2f3f6 -PlaceholderText = #6ee8eaee -Accent = #8fa6c0 - -[Palette.Disabled] -WindowText = #9d9d9d -Button = #131519 -Light = #3a3f47 -Midlight = #31363d -Dark = #0e1013 -Mid = #1f2329 -Text = #9d9d9d -BrightText = #c3d8f2 -ButtonText = #787878 -Base = #131519 -Window = #131519 -Shadow = #08090b -Highlight = #1f2229 -HighlightedText = #9d9d9d -Link = #308cc6 -LinkVisited = #b450ff -AlternateBase = #171a1f -ToolTipBase = #2d323a -ToolTipText = #f2f3f6 -PlaceholderText = #6ee8eaee -Accent = #9d9d9d - -[Palette.Inactive] -WindowText = #e8eaee -Button = #262a31 -Light = #3a3f47 -Midlight = #31363d -Dark = #0e1013 -Mid = #1f2329 -Text = #e8eaee -BrightText = #c3d8f2 -ButtonText = #f2f3f6 -Base = #1b1e23 -Window = #131519 -Shadow = #08090b -Highlight = #1f2229 -HighlightedText = #ffffff -Link = #a9c6e8 -LinkVisited = #b9a8d9 -AlternateBase = #171a1f -ToolTipBase = #2d323a -ToolTipText = #f2f3f6 -PlaceholderText = #6ee8eaee -Accent = #8fa6c0 - -[AppColors] -AccentStrong = #54687e -AccentSoft = #9db0c4 - diff --git a/cockatrice/themes/VelvetMarble/palette-default-light.toml b/cockatrice/themes/VelvetMarble/palette-default-light.toml deleted file mode 100644 index 5323fd320..000000000 --- a/cockatrice/themes/VelvetMarble/palette-default-light.toml +++ /dev/null @@ -1,74 +0,0 @@ -# VelvetMarble — light identity palette (charcoal-velvet chrome with slate marble) -[Palette] -WindowText = #1b1d21 -Button = #c7d1dd -Light = #ffffff -Midlight = #b3becb -Dark = #93a0b1 -Mid = #a1adbc -Text = #1b1d21 -BrightText = #26465f -ButtonText = #121418 -Base = #f7f9fb -Window = #d9e0ea -Shadow = #57626f -Highlight = #54687e -HighlightedText = #ffffff -Link = #2e4d6b -LinkVisited = #5d4a78 -AlternateBase = #e6ebf2 -ToolTipBase = #f2f4f6 -ToolTipText = #121418 -PlaceholderText = #7a1b1d21 -Accent = #54687e - -[Palette.Disabled] -WindowText = #787878 -Button = #d9e0ea -Light = #ffffff -Midlight = #b3becb -Dark = #93a0b1 -Mid = #a1adbc -Text = #787878 -BrightText = #26465f -ButtonText = #969696 -Base = #d9e0ea -Window = #d9e0ea -Shadow = #57626f -Highlight = #ccd5e3 -HighlightedText = #787878 -Link = #0000ff -LinkVisited = #ff00ff -AlternateBase = #e6ebf2 -ToolTipBase = #f2f4f6 -ToolTipText = #121418 -PlaceholderText = #7a1b1d21 -Accent = #787878 - -[Palette.Inactive] -WindowText = #1b1d21 -Button = #c7d1dd -Light = #ffffff -Midlight = #b3becb -Dark = #93a0b1 -Mid = #a1adbc -Text = #1b1d21 -BrightText = #26465f -ButtonText = #121418 -Base = #f7f9fb -Window = #d9e0ea -Shadow = #57626f -Highlight = #ccd5e3 -HighlightedText = #000000 -Link = #2e4d6b -LinkVisited = #5d4a78 -AlternateBase = #e6ebf2 -ToolTipBase = #f2f4f6 -ToolTipText = #121418 -PlaceholderText = #7a1b1d21 -Accent = #54687e - -[AppColors] -AccentStrong = #54687e -AccentSoft = #9db0c4 - diff --git a/cockatrice/themes/VelvetMarble/theme.cfg b/cockatrice/themes/VelvetMarble/theme.cfg deleted file mode 100644 index 55b916e71..000000000 --- a/cockatrice/themes/VelvetMarble/theme.cfg +++ /dev/null @@ -1,5 +0,0 @@ -[Appearance] -ColorScheme = System - -[Style] -Name = Fusion \ No newline at end of file diff --git a/cockatrice/themes/VelvetMarble/zones/handzone-light.png b/cockatrice/themes/VelvetMarble/zones/handzone-light.png deleted file mode 100644 index b21e78e61..000000000 Binary files a/cockatrice/themes/VelvetMarble/zones/handzone-light.png and /dev/null differ diff --git a/cockatrice/themes/VelvetMarble/zones/handzone.jpg b/cockatrice/themes/VelvetMarble/zones/handzone.jpg new file mode 100644 index 000000000..2ec9b37fe Binary files /dev/null and b/cockatrice/themes/VelvetMarble/zones/handzone.jpg differ diff --git a/cockatrice/themes/VelvetMarble/zones/handzone.png b/cockatrice/themes/VelvetMarble/zones/handzone.png deleted file mode 100644 index 3e2b7ebea..000000000 Binary files a/cockatrice/themes/VelvetMarble/zones/handzone.png and /dev/null differ diff --git a/cockatrice/themes/VelvetMarble/zones/playerzone-light.png b/cockatrice/themes/VelvetMarble/zones/playerzone-light.png deleted file mode 100644 index 630aac6eb..000000000 Binary files a/cockatrice/themes/VelvetMarble/zones/playerzone-light.png and /dev/null differ diff --git a/cockatrice/themes/VelvetMarble/zones/playerzone.jpg b/cockatrice/themes/VelvetMarble/zones/playerzone.jpg new file mode 100644 index 000000000..dd13cab78 Binary files /dev/null and b/cockatrice/themes/VelvetMarble/zones/playerzone.jpg differ diff --git a/cockatrice/themes/VelvetMarble/zones/playerzone.png b/cockatrice/themes/VelvetMarble/zones/playerzone.png deleted file mode 100644 index dcf21ba38..000000000 Binary files a/cockatrice/themes/VelvetMarble/zones/playerzone.png and /dev/null differ diff --git a/cockatrice/themes/VelvetMarble/zones/stackzone-light.png b/cockatrice/themes/VelvetMarble/zones/stackzone-light.png deleted file mode 100644 index 62027827c..000000000 Binary files a/cockatrice/themes/VelvetMarble/zones/stackzone-light.png and /dev/null differ diff --git a/cockatrice/themes/VelvetMarble/zones/stackzone.jpg b/cockatrice/themes/VelvetMarble/zones/stackzone.jpg new file mode 100644 index 000000000..b63aa0902 Binary files /dev/null and b/cockatrice/themes/VelvetMarble/zones/stackzone.jpg differ diff --git a/cockatrice/themes/VelvetMarble/zones/stackzone.png b/cockatrice/themes/VelvetMarble/zones/stackzone.png deleted file mode 100644 index a47f7bf9d..000000000 Binary files a/cockatrice/themes/VelvetMarble/zones/stackzone.png and /dev/null differ diff --git a/cockatrice/themes/VelvetMarble/zones/tablezone-light.png b/cockatrice/themes/VelvetMarble/zones/tablezone-light.png deleted file mode 100644 index 5a9014f9d..000000000 Binary files a/cockatrice/themes/VelvetMarble/zones/tablezone-light.png and /dev/null differ diff --git a/cockatrice/themes/VelvetMarble/zones/tablezone.jpg b/cockatrice/themes/VelvetMarble/zones/tablezone.jpg new file mode 100644 index 000000000..9f511491a Binary files /dev/null and b/cockatrice/themes/VelvetMarble/zones/tablezone.jpg differ diff --git a/cockatrice/themes/VelvetMarble/zones/tablezone.png b/cockatrice/themes/VelvetMarble/zones/tablezone.png deleted file mode 100644 index 1523d0821..000000000 Binary files a/cockatrice/themes/VelvetMarble/zones/tablezone.png and /dev/null differ diff --git a/doc/carddatabase_v4/cards.xsd b/doc/carddatabase_v4/cards.xsd index 82aed32ab..59ca3e560 100644 --- a/doc/carddatabase_v4/cards.xsd +++ b/doc/carddatabase_v4/cards.xsd @@ -35,13 +35,6 @@ - - - - - - - @@ -56,13 +49,6 @@ - - - - - - - diff --git a/doc/doxygen/extra-pages/developer_documentation/loading_card_pictures.md b/doc/doxygen/extra-pages/developer_documentation/loading_card_pictures.md index c4c335b00..b606f9e4b 100644 --- a/doc/doxygen/extra-pages/developer_documentation/loading_card_pictures.md +++ b/doc/doxygen/extra-pages/developer_documentation/loading_card_pictures.md @@ -33,178 +33,20 @@ issue a load request, which will first look for local images on-disk and then co found, use the stored binary data from the network cache to populate the in-memory pixmap cache under the card's cache key. If it is not found, it will then proceed with issuing a network request. -The size of both of these caches can be configured by the user on the "Storage" settings page. +The size of both of these caches can be configured by the user in the "Card Sources" settings page. # PixmapCacheKeys and ProviderIDs -Every card picture that is loaded ends up in the QPixmapCache under a key that identifies the exact printing it belongs -to. The key is produced by ExactCard::getPixmapCacheKey() and has the following shape: - -```text -card__ -``` - -For example, the _Example Card_ printing with provider ID `0b23cdc8-d413-4fb1-8470-474221b10fe2` is stored -under `card_Example Card_0b23cdc8-d413-4fb1-8470-474221b10fe2`. If the printing has no provider ID, the key -drops the suffix and falls back to `card_`. - -The **provider ID** is the Scryfall UUID of the printing. Oracle maps the `scryfallId` of every printing to the `uuid` -property when building the card database, and deck files persist it as the `uuid` attribute of each card entry. Because -the provider ID is part of the pixmap cache key, two different printings of the same card never share a cache entry. -This is exactly what allows the printing selector and exact-card lookups to display the picture of the precise printing -a card was added as. - -The base key holds the full-size image. When a widget asks for a scaled version, the scaled pixmap is stored under an -additional key of the form `_x`, with the size adjusted for the device pixel ratio of the screen, -so each widget size is only ever scaled once. - -The cache key is also used for bookkeeping outside of the pixmap cache itself: - -- CardPictureLoaderWorker keeps a set of keys that are currently being loaded so the same card is never queued twice. -- CardPictureLoader tracks, per key, the last time loading failed. A failed load stores a NULL pixmap under the key; as - long as that marker is present, subsequent requests for the card show the "failed" card back and are only re-queued - after the retry interval of 300 seconds has passed. -- When the CardInfo of a loaded card is destroyed, its cache entries and failure markers are removed. +TODO # The Redirect Cache -Many picture URLs - in particular the Gatherer and Scryfall URLs from the default set of templates - redirect to a CDN -or to a different host. To avoid following the same redirect for every single card, CardPictureLoaderWorker remembers -redirects and applies them without an extra network round trip. - -The redirect cache is a hash map from original URL to redirect URL plus timestamp. It is persisted to a `cache.ini` -file (Qt's INI format, under the `redirects` array) inside the redirect cache directory -(`SettingsCache::instance().paths().getRedirectCachePath()`, i.e. `/redirects/`). The cache is loaded when the worker -starts, pruned of entries older than the configured TTL, and written back to disk when the application quits. - -Entries are added whenever a network reply reports a redirection (see below) and are consulted before any request is -made: both CardPictureLoaderWorker::queueRequest() and CardPictureLoaderWorker::makeRequest() check for a cached -redirect first and jump straight to the final URL. - -The TTL is the "Redirect Cache TTL" setting on the "Storage" settings page and defaults to 30 days. Lowering it makes -Cockatrice re-resolve redirects sooner, which can help when a download URL changed its redirect target. - -Because Cockatrice tracks redirects itself, the QNetworkAccessManager is configured with Qt's `ManualRedirectPolicy`. -Redirects found in a reply are handled manually: - -- A recursive redirect (a URL redirecting to itself) is treated as a failed load. -- Otherwise the redirect is recorded in the redirect cache and the request is re-issued against the target URL. -- A successful reply with one of the redirect status codes 301, 302, 303, 305, 307 or 308 is handled the same way. - -Clearing the network cache (CardPictureLoader::clearNetworkCache()) also clears the redirect cache. +TODO # Local Image Loading -Before any network request is issued, CardPictureLoaderWorker hands the ExactCard to CardPictureLoaderLocal, which -tries to find a matching picture on disk. If a local picture is found, it is used and no network request is made. - -CardPictureLoaderLocal searches three locations: - -- The **CUSTOM folder** (`/CUSTOM/`). Every file in it is indexed recursively by its base name - (both `baseName` and `completeBaseName`, so a file named `ExampleCard.jpg` is indexed as `ExampleCard`). The index is rebuilt - every 10 seconds, so new files are picked up without restarting the - client (changing the configured pictures directory only reassigns the search paths; the next timer tick rebuilds the index). -- The **set-named subfolders** of the pictures directory: `//` and - `/downloadedPics//`. -- The **root of the `downloadedPics` folder** (`/downloadedPics/`). The export naming schemes without a - set-folder part write their files straight into `downloadedPics/`, so this is where flat-scheme downloads and the - local overrides described below are matched. - -For each candidate folder, the loader generates file-name variants from the card's corrected name, set code, collector -number and provider ID using the import naming schemes (Card Name + Provider ID, Card Name + Set + Collector, -Set + Collector + Card Name, Card Name + Set, Card Name), each tried with both `_` and `-` as separator. A file is -accepted when its name without the extension *equals* the variant exactly - the extension itself is free - and the -first variant that yields a readable image wins. For example, the file `Example Card_EXM_43.png` in the `EXM` set -folder matches the card with corrected name `Example Card`, set code `EXM` and collector number `43`. - -\attention The file-name variants use the *corrected* card name, so split cards are stored under their joined name: the -"Example // Card" card is matched by a file named `ExampleCard.*`. - -The naming schemes are also documented in the user-facing page @ref custom_card_pictures, which additionally covers -the CUSTOM folder workflow, `picurl` and download URL templates. - -When the filesystem cache method is selected on the "Storage" settings page, downloaded images are additionally written -into `/downloadedPics/` using the configured export naming scheme (as `.png` files). The two export -schemes with a set-folder part (`Set Folder / Name + Provider ID` and `Set Folder / Name + Set Name + Collector`) write -into `downloadedPics//`; the three flat schemes write directly into `downloadedPics/`. Automatic cache writes -never overwrite an existing file, so a provider outage can permanently leave an outdated image in that folder until it -is deleted manually - the user-facing troubleshooting guide @ref fixing_card_pictures covers how to do this. Explicit -image overrides (see below) are the exception and always overwrite. - -# Local Image Overrides - -Beyond the generic on-disk lookup above, individual printings can be given explicit artwork that wins over every other -source without touching the CUSTOM folder or any download URL. This is the "Image Overrides" submenu of the context menu -that opens when you right-click a card in the deck editor's printing selector. - -- **Load Custom Image...** asks for a picture file and installs it for the card through - CardPictureLoader::saveCardImageToLocalStorage() with `allowOverwrite == true`. -- **One entry per alternate printing** (labeled ` `): selecting one hands the card to - CardPictureLoader::installPrintingOverride(), which resolves that printing's artwork - enqueueing a load and waiting - for the `CardInfo::pixmapUpdated` signal if it is not cached yet - and persists it for the card. -- **Clear Custom Image** calls CardPictureLoader::deleteAllLocalOverrides() to remove every stored override image of the - card, after which normal resolution resumes. The entry is only enabled while CardPictureLoader::hasLocalOverrides() - reports at least one stored file. - -Overrides are stored as `.png` files in `downloadedPics/` under the export naming scheme configured on the "Storage" -settings page - which is exactly why the local matcher also looks into the `downloadedPics/` root (see above). They are -written with `allowOverwrite == true`, so an override always replaces whatever the filesystem cache previously saved for -that spelling; only *automatic* cache writes are prevented from clobbering it. Overriding a card with its own current -printing is a no-op (the UI omits it from the menu), and an override whose artwork fails to resolve surfaces the -"failed" card back instead of a silent no-op while any override already on disk is left in place and re-displayed. +TODO # URL Generation and Resolution -When no local image is available and downloading is enabled, the network loader starts working through a list of -candidate URLs. This list is managed by CardPictureToLoad and is built in two steps. - -First, CardPictureToLoad::extractSetsSorted() collects all sets the card has printings in and sorts them by set -priority. Unless the user disabled per-printing art ("Override all card art with personal set preference (Pre-ProviderID -change behavior)"), the set that -matches the requested printing's provider ID is moved to the front, so the exact printing is always attempted first. - -For each set, CardPictureToLoad::populateSetUrls() builds an ordered URL list: - -1. A custom URL defined for that printing via the `picurl` property in the card database, if present. -2. The configured download URL templates, in priority order (Deck Editor → "URL Download Priority"). - -URL templates are transformed into concrete URLs by CardPictureToLoad::transformUrl(), which substitutes reference -points. `!name!`, `!setcode!` and friends substitute card and printing data, while the `!set:!` and -`!prop:!` reference points resolve a property of the printing or of the card respectively. The canonical list -of all reference points with examples, including the `_fill_with_` and `_substr_` modifiers, lives in -@ref custom_card_pictures. - -The `!set:...!` and `!prop:...!` reference points also support two modifiers: - -- `_fill_with_` pads the value with the given text, right-aligned, e.g. `!set:num_fill_with_000!` turns collector - number `1` into `001`. If the value is longer than the fill text, the template is invalidated. -- `_substr__` extracts a substring, e.g. `!set:num_substr_2_2!` takes two characters starting at the - third. If the substring would extend past the end of the value, the template is invalidated. - -Substituted values are percent-encoded. If a template asks for a property the card or printing does not have (or one of -the modifiers invalidates it), the template yields no URL and is skipped; the next template is tried instead. - -\attention Custom URLs should start with `http://` or `https://`. The scheme is not validated before the URL is handed -to QNetworkAccessManager, so a template without an absolute scheme may silently fail to download; prefer HTTPS where the -provider allows it. - -The resolution order is: for the current set, try each URL in the list; when all URLs for a set are exhausted, move to -the next set; when every set is exhausted, the load fails. A failed load is reported through the NULL-pixmap mechanism -described in the PixmapCacheKeys and ProviderIDs section above. - -Several mechanisms influence the resolution process: - -- **Rate limiting.** The worker allows roughly 10 requests per second globally. A server that answers with HTTP 429 - gets its per-host allowance halved; the first 429 for a host is waited out (honoring the `Retry-After` header if - present) and the same URL retried, while a second 429 makes the loader fall through to the other configured sources. - When all sources are exhausted the request is deferred with some random jitter and retried once the back-off expires. -- **Redirects.** Replies with a redirect status (301, 302, 303, 305, 307, 308) are followed and recorded in the - redirect cache as described in the Redirect Cache section above. -- **Blacklisted images.** Gatherer returns the card back image for cards it does not know. A few known MD5 hashes of - that image are blacklisted, so such a "successful" download is treated as not found instead of being shown. -- **WebP.** Images detected as WebP (RIFF/WEBP header) are decoded through QMovie instead of QImageReader. -- **Downloads disabled.** When "Download card pictures on the fly" is disabled and the network cache method is active, - requests use Qt's `AlwaysCache` policy so that only previously cached images are served. - -A user-facing reference for writing download URL templates, including more worked examples, is available at -@ref custom_card_pictures. +TODO \ No newline at end of file diff --git a/doc/doxygen/extra-pages/user_documentation/card_pictures/custom_card_pictures.md b/doc/doxygen/extra-pages/user_documentation/card_pictures/custom_card_pictures.md deleted file mode 100644 index 3f1079661..000000000 --- a/doc/doxygen/extra-pages/user_documentation/card_pictures/custom_card_pictures.md +++ /dev/null @@ -1,135 +0,0 @@ -@page custom_card_pictures Custom Card Pictures - -There are four ways to make Cockatrice use custom artwork for your cards: - -- Placing image files in the **CUSTOM pictures folder**. -- Providing a **custom card database** that points each printing at a picture URL via the `picurl` property. -- Writing your **own download URL templates**. -- Setting an **image override** for a single card from inside the deck editor. - -Each of these is described below. If pictures are missing or wrong, see @ref fixing_card_pictures instead. - -# Custom Pictures Folder (CUSTOM) - -Any image file placed in the CUSTOM folder is used as the card picture, and no download is attempted for cards that -match a file there. - -- The folder is `/CUSTOM/`. The pictures directory is configured on the 'General' settings tab, - under 'Directories' → 'Pictures directory'. -- Any image format Qt can decode is accepted (PNG, JPG/JPEG, WebP, GIF, BMP, ...); the file extension is not filtered, - so even an extension-less file is picked up if the decoder recognizes its content. -- Files are indexed by their name, so you can organize them into subfolders freely. -- New or changed files are picked up automatically within a few seconds — no client restart is required. - -The file name must match the card using one of the naming schemes below. Both `_` and `-` are accepted as separators, -and the file extension is ignored when matching: - -| Scheme | Example file name | -| --------------------------- | ------------------------------------------------------- | -| Card Name | `Example Card.png` | -| Card Name + Set | `Example Card_DDL.png` | -| Card Name + Set + Collector | `Example Card_DDL_43.png` | -| Set + Collector + Card Name | `DDL_43_Example Card.png` | -| Card Name + Provider ID | `Example Card_0b23cdc8-d413-4fb1-8470-474221b10fe2.png` | - -The name used for matching is the *corrected* card name. Correction removes the split-card separator ` // ` and the -characters reserved in Windows file names (`* < > : " \ ?` and control characters), and turns `/` into a space, so the -"Example // Card" card is matched by a file named `ExampleCard.png`, not `Example // Card.png`. Most other punctuation -(commas, apostrophes, `!`, ...) is left untouched. - -\attention A file in the CUSTOM folder always wins over downloaded pictures, even if it is the wrong image. Delete the -file if you want to see the downloaded artwork again. - -The naming conventions are the same as those recognized in the set-named subfolders and in `downloadedPics`, and are -documented for developers in @ref loading_card_pictures. - -# Custom Card Database (picurl) - -If you maintain your own card database (see the -[Custom Cards & Sets](https://github.com/Cockatrice/Cockatrice/wiki/Custom-Cards-&-Sets) wiki), each printing's `` -tag can carry a `picurl` attribute containing a full URL for that printing's picture: - -```xml - -``` - -Cockatrice tries this URL **before** the configured download URL templates, so it is the most direct way to provide -custom artwork for a specific printing. - -- The URL should start with `http://` or `https://`; the scheme is not validated, so make sure it is absolute or the - download may silently fail. -- When you change a `picurl` for a card whose picture was already downloaded and cached, delete the stored images - (Storage tab → 'Delete Saved Images' / 'Delete Cached Images') so Cockatrice fetches the new URL. - -# Custom Download URL Templates - -The built-in download URLs are templates: Cockatrice replaces reference points in the URL with information about the -card and its printing. You can write your own templates in 'Cockatrice → Settings' (Ctrl + Shift + P by default), on -the 'Deck Editor' tab, in the 'URL Download Priority' section. - -The following reference points are available: - -| Reference point | Description | Example | -| ------------------------------- | ------------------------------- | -------------- | -| `!name!` | Card name | `Example Card` | -| `!name_lower!` | Card name, lower case | `example card` | -| `!corrected_name!` | Corrected card name | `ExampleCard` (instead of "Example // Card") | -| `!corrected_name_lower!` | Corrected card name, lower case | `examplecard` | -| `!sflang!` | Scryfall language code for the current client language; defaults to English when the language has no localized images | `en`, `zhs` | -| `!setcode!` / `!setcode_lower!` | Set code | `EXM` / `exm` | -| `!setname!` / `!setname_lower!` | Full set name | `Exemplary Set` / `exemplary set` | -| `!set:!` | A property of this printing, e.g. `muid` (Gatherer multiverse ID), `uuid` (Scryfall UUID), `num` (collector number), `rarity` | `373549` | -| `!prop:!` | A property of the card, e.g. `side` (front/back), `colors`, `cmc`, `coloridentity`, `type`, `pt`, and the format legality statuses | `front` | - -The `!set:...!` and `!prop:...!` reference points support two modifiers: - -- `_fill_with_` pads the value with the given text, right-aligned, e.g. `!set:num_fill_with_000!` turns collector - number `1` into `001`. If the value is longer than the fill text, the template is skipped. -- `_substr__` extracts a substring, e.g. `!set:num_substr_2_2!` takes two characters starting at the - third. If the substring would extend past the end of the value, the template is skipped. - -Substituted values are URL-encoded. A template that asks for a property the card or printing does not have is skipped, -and the next template in the list is tried instead. - -\attention Custom URLs should start with `http://` or `https://`. As with `picurl`, the scheme is not validated before -the URL is handed to QNetworkAccessManager, so use an absolute URL or the download may silently fail. - -Some working examples: - -```text -https://cards.scryfall.io/large/!prop:side!/!set:uuid_substr_0_1!/!set:uuid_substr_1_1!/!set:uuid!.jpg -https://api.scryfall.com/cards/!set:uuid!?format=image&face=!prop:side! -https://api.scryfall.com/cards/multiverse/!set:muid!?format=image -https://gatherer.wizards.com/Handlers/Image.ashx?multiverseid=!set:muid!&type=card -https://gatherer.wizards.com/Handlers/Image.ashx?name=!name!&type=card -``` - -See the [Custom Picture Download URLs](https://github.com/Cockatrice/Cockatrice/wiki/Custom-Picture-Download-URLs) -wiki for more examples and ideas. - -\attention Keep in mind that templates using `!name!` or `!set:muid!` resolve by name or multiverse ID, not by the -exact printing. Only the Scryfall `!set:uuid!` templates always return the exact printing requested. See -@ref fixing_card_pictures for more on this. - -# Image Overrides - -The quickest way to give one card custom art is an image override: right-click the card in the deck editor's printing -selector and open the **Image Overrides** submenu of the context menu. - -- **Load Custom Image...** — choose a picture file (the dialog suggests PNG, JPG/JPEG and WebP); it becomes that card's - artwork immediately. -- **One entry per alternate printing** of the card, labeled ` ` (hovering an entry previews that - printing's artwork). Selecting one makes the card use that exact printing's picture, so e.g. a basic land can be shown - with any of its artworks. -- **Clear Custom Image** — removes the stored override and returns the card to normal resolution. It is only available - while the card has a stored override. - -Overrides are stored as `.png` files in `/downloadedPics/`, under the "Naming scheme" configured on -the Storage settings page, and are matched the same way as downloaded images. Because local files are checked before any -URL is requested, an override always wins over downloaded artwork and `picurl` for that card. The override exists only on -the machine it was created on - it is not part of the deck file - so a card with a stored override shows normally on -another computer. - -\attention If you also keep a matching file in the CUSTOM folder, that file is matched before the override. When you -change an override, use **Clear Custom Image** so the stored `.png` is replaced; manually deleting the file in -`downloadedPics/` has the same effect. diff --git a/doc/doxygen/extra-pages/user_documentation/index.md b/doc/doxygen/extra-pages/user_documentation/index.md index b55d00fcd..d7e9d529d 100644 --- a/doc/doxygen/extra-pages/user_documentation/index.md +++ b/doc/doxygen/extra-pages/user_documentation/index.md @@ -11,15 +11,6 @@ - @subpage beta_release -## Card Pictures - -- @subpage custom_card_pictures - -## Troubleshooting - -- @subpage fixing_card_pictures -- @subpage enabling_debug_logs - ## Syntax Help - @subpage search_syntax_help diff --git a/doc/doxygen/extra-pages/user_documentation/troubleshooting/enabling_debug_logs.md b/doc/doxygen/extra-pages/user_documentation/troubleshooting/enabling_debug_logs.md deleted file mode 100644 index 7e4196a50..000000000 --- a/doc/doxygen/extra-pages/user_documentation/troubleshooting/enabling_debug_logs.md +++ /dev/null @@ -1,140 +0,0 @@ -@page enabling_debug_logs Enabling Debug Logs - -Cockatrice ships with a "diagnostics mode" that prints detailed debug messages about what the client is doing. This is -extremely useful when asking for help, because it shows exactly what happened: which URL the card picture loader tried, -whether it found or missed a file on disk, whether a download succeeded or failed, which redirects were followed, and -much more. Each of these messages belongs to a category, and you can enable or disable categories individually. - -Don't worry, this sounds more technical than it is. You only need to do two things: create one small text file, and tell -Cockatrice where it is. There are no installation steps and you can undo everything later (see [When you are -done](#when-you-are-done)). - -# Step 1: Create the file - -Open a plain text editor (Notepad on Windows, TextEdit on macOS, or any text editor on Linux) and paste the following -content: - -```ini -[Rules] -# The default log level is info -*.debug = false - -# Turn on debug level logs for the card picture loader and all its sub categories -card_picture_loader.* = true -``` - -Save the file with the exact name `qtlogging.ini` in a place you can find again, for example your Documents folder. - -\attention The file name matters: it must be `qtlogging.ini`, not `qtlogging.ini.txt`. If your text editor adds a -`.txt` extension automatically, you need to stop it from doing so (see below). On macOS, TextEdit must be switched to -plain text mode first via 'Format → Make Plain Text'. - -The file contains one rule per line. The `*.debug = false` rule turns off debug messages everywhere by default, and the -`card_picture_loader.* = true` line then re-enables them for the card picture loader. The `.*` at the end means "this -category and all of its sub categories". To enable a different category instead, just replace that line with the -category name of your choice, for example `card_database.loading = true` or `window_main.startup = true`. - -# Step 2: Tell Cockatrice where the file is - -Cockatrice does not know about the file yet. You have to point it there by setting an environment variable called -`QT_LOGGING_CONF` to the full location of your file. How to do this depends on your operating system: - -**Windows** - -1. Press the Windows key, type "environment variables", and open "Edit the system environment variables". -2. Click "Environment Variables...", then under "User variables" click "New...". -3. Set "Variable name" to `QT_LOGGING_CONF` and "Variable value" to the full path of your file, for example - `C:\Users\YourName\Documents\qtlogging.ini`. -4. Confirm all dialogs, then close and reopen Cockatrice. - -Alternatively, if Cockatrice is installed in a folder you can write to, you can simply place the `qtlogging.ini` file -directly next to the Cockatrice executable (in the same folder as `cockatrice.exe`) and skip the environment variable -altogether. Note that this copy may be replaced when you update the client. - -**macOS** - -Open the Terminal app (it is in 'Applications → Utilities') and run the following two commands, replacing the path -with the full location of your file: - -```shell -launchctl setenv QT_LOGGING_CONF /path/to/qtlogging.ini -open -a Cockatrice -``` - -You can also use `export QT_LOGGING_CONF=/path/to/qtlogging.ini` to set the variable for the current terminal session. - -The setting stays active until you log out or restart your Mac. If you have multiple users on the same Mac, be aware -that this setting only applies to your user account. - -**Linux** - -For a quick test, open a terminal and start Cockatrice with the file on the command line, replacing the path with the -full location of your file: - -```shell -QT_LOGGING_CONF=/path/to/qtlogging.ini cockatrice -``` - -If this works and you want it to apply every time you start Cockatrice, add the following line to your `~/.profile` -file and log in again: - -```shell -export QT_LOGGING_CONF="/path/to/qtlogging.ini" -``` - -# Step 3: See the logs - -Now that debug logging is enabled, open Cockatrice and trigger the behavior you are investigating, for example by -opening a deck, reloading the card database, or starting a game. - -The easiest way to see the logs is to use the built-in log viewer inside Cockatrice itself: open 'Help → View Debug -Log'. A window appears that shows the log messages live and keeps the most recent entries. It even has a 'Copy to -clipboard' button so you can paste the output into a bug report or a Discord message. This works the same on every -operating system. - -If you prefer to capture everything to a file instead, start Cockatrice with the `--debug-output` option: - -```shell -cockatrice --debug-output -``` - -Cockatrice then writes the full log to a file called `qdebug.txt` in the folder it was started from. - -# Which categories are available? - -Every message Cockatrice logs belongs to a category. The following table lists the most useful ones for troubleshooting, -grouped by area. Enable a category by adding a line like `category = true` to your `qtlogging.ini` file (or use a `.*` -suffix, e.g. `card_picture_loader.*`, to include all sub categories). - -| What you want to see | Categories | -| ------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | -| Card picture loading (URLs, local file hits/misses, downloads, redirects) | `card_picture_loader.*` | -| Card database loading and parsing | `card_database`, `card_database.loading`, `card_database.loading.success_or_failure`, `cockatrice_xml.*` | -| Card, set, and deck information | `card_info`, `card_list`, `deck_loader` | -| Startup sequence and update checks | `window_main.startup.*`, `release_channel`, `spoiler_background_updater` | -| User interface and themes | `theme_manager`, `sound_engine`, `flow_layout`, `flow_widget.*`, `pixel_map_generator`, `card_info_picture_widget` | -| Networking and servers | `local_client`, `remote_client`, `tapped_out_interface`, `servers_settings` | -| In-game logic | `player`, `game_scene.*`, `card_zone.*`, `view_zone`, `game_event_handler` | -| Dialogs and tabs | `dlg_settings`, `dlg_update`, `dlg_tip_of_the_day`, `tab_game`, `tab_message`, `tab_supervisor` | -| Settings and shortcuts | `settings_cache`, `shortcuts_settings` | -| Deck and card filtering | `filter_string`, `deck_filter_string`, `syntax_help` | - -For example, to investigate why a card database update seems to fail, enable the card database categories: - -```ini -[Rules] -*.debug = false - -card_database = true -card_database.loading = true -card_database.loading.success_or_failure = true -cockatrice_xml.* = true -``` - -# When you are done - -To turn the diagnostics back off, just reverse what you did: remove the `QT_LOGGING_CONF` environment variable (or -unset it again via `launchctl unsetenv QT_LOGGING_CONF` on macOS) and/or delete the `qtlogging.ini` file, then restart -Cockatrice. Leaving it on is harmless, but the extra logging can make the client slightly slower. - -For the full details on how Cockatrice logging works, including the complete list of categories, see @ref logging. diff --git a/doc/doxygen/extra-pages/user_documentation/troubleshooting/fixing_card_pictures.md b/doc/doxygen/extra-pages/user_documentation/troubleshooting/fixing_card_pictures.md deleted file mode 100644 index 7f761c020..000000000 --- a/doc/doxygen/extra-pages/user_documentation/troubleshooting/fixing_card_pictures.md +++ /dev/null @@ -1,118 +0,0 @@ -@page fixing_card_pictures Fixing Card Pictures - -This guide collects the common causes for card pictures not showing up, showing the wrong printing, or showing the -card back instead of the artwork, and how to fix them. - -Work through the steps in order. In most cases the problem is caused by an outdated client, misconfigured download -URLs, stale local images, or a corrupted cache. - -# Update Your Client - -Picture handling bugs are fixed on a regular basis, so the first thing to try is updating your client. - -Use the update check in 'Help → Check for Updates' (or look for the update prompt shown on startup). - -If a fix has not yet made it into the latest stable release, it may already be available in the beta release. The beta -ships very frequently and usually receives a follow-up fix within a day or two if something breaks. - -See @subpage beta_release for instructions on how to switch to the beta channel. - -# Check Your Download URLs - -Card pictures are downloaded from a list of URL templates. Each template is tried in order until one produces a valid -image, so the order matters: URLs at the top of the list are tried first. - -The list can be found in 'Cockatrice → Settings' (or Ctrl + Shift + P by default), on the 'Deck Editor' tab, in the -'URL Download Priority' section. Make sure 'Download card pictures on the fly' is enabled and that the list contains -valid URLs. If you suspect the list has been modified or corrupted, press 'Reset Download URLs' to restore the -defaults. - -For information on how to add your own custom URL templates, see the 'How to add a custom URL' link in the same -settings section, or @ref custom_card_pictures for a full reference of the URL reference points, the CUSTOM -pictures folder, and custom card databases. - -# Check Your Local Picture Folder - -Before any network request is made, Cockatrice looks for local image files. If a matching file is found on disk it is -shown instead of anything downloaded, even if it is the wrong image. - -The pictures directory is configured on the 'General' settings tab, under 'Directories' → 'Pictures directory'. -Cockatrice checks the following locations, in order: - -- The custom pictures folder (recursively indexed by file name). -- `//` -- `/downloadedPics//` -- `/downloadedPics/` (for export naming schemes without a set folder) - -A file only matches when its name without the extension equals one of the recognized scheme patterns exactly. - -The following import naming schemes are recognized (using both `_` and `-` as separators). The canonical table with -concrete example file names is on @ref custom_card_pictures: - -| Scheme | Pattern | -| --------------------------- | -------------------------- | -| Card Name + Provider ID | `{name}_{providerId}` | -| Card Name + Set + Collector | `{name}_{set}_{collector}` | -| Set + Collector + Card Name | `{set}_{collector}_{name}` | -| Card Name + Set | `{name}_{set}` | -| Card Name | `{name}` | - -If a picture you downloaded or placed manually is wrong, stale, or corrupted, delete the offending file. Pay special -attention to the `downloadedPics` subfolder: this is where the filesystem caching method writes downloaded images, and -after a provider outage it can permanently contain the wrong printing until you delete it manually. - -If a card persistently shows artwork you assigned yourself, you may have an **image override** set for it. Right-click -the card in the deck editor's printing selector and use 'Image Overrides' → 'Clear Custom Image' to remove it (or -delete the stored `.png` in `downloadedPics/`). See @ref custom_card_pictures for details. - -See @ref loading_card_pictures for details on how local images are loaded. - -# Clear Caches - -Cockatrice caches card pictures in three places. All of them can be managed on the 'Storage' settings tab: - -- **Network cache** — downloaded images stored on disk. Press 'Delete Cached Images' to clear it. -- **Filesystem / image backup** — downloaded images written directly to `downloadedPics`. Press 'Delete Saved Images' - to clear it. -- **In-memory (pixmap) cache** — images currently held in RAM. Press 'Clear In-Memory Images' to clear it. - -If a provider outage caused the wrong pictures to be downloaded and cached, clearing the network cache (and the -'Delete Saved Images' button if you use the filesystem caching method) will force Cockatrice to download the correct -images again. The redirect cache TTL (also on the Storage tab) controls how long previously seen redirects for -download URLs are remembered; lowering it can help if a URL used to redirect somewhere else. - -# Restart the Client - -After updating the client, changing the download URLs, moving or deleting local image files, or clearing caches, it is -recommended to restart Cockatrice so that all changes are fully picked up. - -# Check Logs - -Before changing any settings, check the existing logs first. Rate limit errors and most download errors are already -logged at warn level, so you may find the cause without enabling debug mode. - -Open 'Help → View Debug Log' and look for error or warning messages related to card picture loading. If you need more -detail than the default log level provides, see below. - -# Enable Picture Loader Debug Logs - -If the steps above did not solve the problem, you can turn on a "diagnostics mode" that prints what the picture loader -is actually doing: which URL it is trying, whether it found or missed a file on disk, whether the download succeeded or -failed, and which redirects it followed. This information is extremely useful when asking for help. - -See @subpage enabling_debug_logs for a step-by-step guide on how to enable the logs, including instructions -for Windows, macOS, and Linux. - -# Provider Accuracy - -Cards in Cockatrice are identified by a provider ID, which is the Scryfall UUID of a specific printing. Decks store -this ID, which is why the exact printing a card was added as can be looked up again. - -The Scryfall URL templates built into Cockatrice use this provider ID directly (`!set:uuid!`), so they always download -the exact printing that was requested. - -The Gatherer URL templates, on the other hand, do **not** use the provider ID. They resolve pictures by multiverse ID -(`!set:muid!`) or by card name (`!name!`) only. As a result they may return a different printing than the one the -provider ID refers to, or no picture at all for cards Gatherer does not know. If you need pictures to match the exact -printing of a card, make sure the Scryfall URLs are at the top of your download URL priority list and consider removing -or demoting the Gatherer URLs. diff --git a/format.sh b/format.sh index 9e3a6069b..83dee9e28 100755 --- a/format.sh +++ b/format.sh @@ -11,18 +11,17 @@ set -o pipefail # go to the project root directory, this file should be located in the project root directory olddir="$PWD" cd "${BASH_SOURCE%/*}/" || exit 2 # could not find path, this could happen with special links etc. -phys_root="$(pwd -P)" # defaults include=("cockatrice/src" \ libcockatrice_* \ "oracle/src" \ "servatrice/src" \ -"cmake/pch" \ "tests") exclude=("libcockatrice_rng/libcockatrice/rng/sfmt/" \ "libcockatrice_utility/libcockatrice/utility/peglib.h" \ "oracle/src/lzma/" \ +"oracle/src/qt-json/" \ "oracle/src/zip/" \ "servatrice/src/smtp/") exts=("cpp" "h" "proto") @@ -169,14 +168,14 @@ EOM echo "error in parsing arguments of $0: $1 is an unrecognized option" >&2 exit 2 # input error fi - if [[ ! $1 ]] || next_dir=$(cd "$olddir" && cd -- "$1" && pwd -P); then + if [[ ! $1 ]] || next_dir=$(cd "$olddir" && cd -- "$1" && pwd); then if ! [[ $set_include ]]; then include=() # remove default includes set_include=1 fi if [[ $1 ]]; then - if [[ "$next_dir" != "$phys_root" && "$next_dir" != "$phys_root"/* ]]; then - echo "error in parsing arguments of $0: $next_dir is not in $phys_root" >&2 + if [[ $next_dir != $PWD/* ]]; then + echo "error in parsing arguments of $0: $next_dir is not in $PWD" >&2 exit 2 # input error fi include+=("$next_dir") diff --git a/libcockatrice_card/CMakeLists.txt b/libcockatrice_card/CMakeLists.txt index 15388ecfc..7d3d47eea 100644 --- a/libcockatrice_card/CMakeLists.txt +++ b/libcockatrice_card/CMakeLists.txt @@ -5,8 +5,6 @@ set(CMAKE_AUTORCC ON) set(HEADERS libcockatrice/card/card_info.h libcockatrice/card/card_info_comparator.h - libcockatrice/card/card_localization.h - libcockatrice/card/lazy_properties_hash.h libcockatrice/card/database/card_database.h libcockatrice/card/database/card_database_loader.h libcockatrice/card/database/card_database_manager.h @@ -28,8 +26,6 @@ add_library( ${MOC_SOURCES} libcockatrice/card/card_info.cpp libcockatrice/card/card_info_comparator.cpp - libcockatrice/card/card_localization.cpp - libcockatrice/card/lazy_properties_hash.cpp libcockatrice/card/database/card_database.cpp libcockatrice/card/database/card_database_cache.cpp libcockatrice/card/database/card_database_loader.cpp diff --git a/libcockatrice_card/libcockatrice/card/card_info.cpp b/libcockatrice_card/libcockatrice/card/card_info.cpp index 56c737793..f03503550 100644 --- a/libcockatrice_card/libcockatrice/card/card_info.cpp +++ b/libcockatrice_card/libcockatrice/card/card_info.cpp @@ -5,7 +5,10 @@ #include "relation/card_relation.h" #include "set/card_set.h" +#include #include +#include +#include #include #include #include @@ -18,36 +21,64 @@ class CardInfo; using CardInfoPtr = QSharedPointer; +namespace +{ +QByteArray serializeProperties(const QHash &props) +{ + QByteArray blob; + QDataStream out(&blob, QIODevice::WriteOnly); + out.setVersion(QDataStream::Qt_6_4); + out << props; + return blob; +} +} // namespace + +void CardInfo::ensurePropertiesLoaded() const +{ + QMutexLocker lock(&propertiesMutex); + if (propertiesLoaded) { + return; + } + if (!propertiesBlob.isEmpty()) { + QDataStream in(propertiesBlob); + in.setVersion(QDataStream::Qt_6_4); + in >> propertiesCache; + } + propertiesLoaded = true; +} + const QHash &CardInfo::getPropertiesHash() const { - return properties.getProperties(); + ensurePropertiesLoaded(); + return propertiesCache; } void CardInfo::setProperty(const QString &_name, const QString &_value) { - bool changed = properties.insert(_name, _value); - if (!changed) { + ensurePropertiesLoaded(); + if (propertiesCache.value(_name) == _value) { return; } - + propertiesCache.insert(_name, _value); + propertiesBlob = serializeProperties(propertiesCache); emit cardInfoChanged(smartThis); } CardInfo::CardInfo(const QString &_name, const QString &_text, bool _isToken, - const QHash &_properties, + QHash _properties, const QList &_relatedCards, const QList &_reverseRelatedCards, SetToPrintingsMap _sets, - const UiAttributes _uiAttributes, - QMap _localizedNames, - QMap _localizedTexts) - : name(_name), text(_text), isToken(_isToken), localizedNames(std::move(_localizedNames)), - localizedTexts(std::move(_localizedTexts)), properties(LazyPropertiesHash(_properties)), - relatedCards(_relatedCards), reverseRelatedCards(_reverseRelatedCards), setsToPrintings(std::move(_sets)), - uiAttributes(_uiAttributes) + const UiAttributes _uiAttributes) + : name(_name), text(_text), isToken(_isToken), relatedCards(_relatedCards), + reverseRelatedCards(_reverseRelatedCards), setsToPrintings(std::move(_sets)), uiAttributes(_uiAttributes) { + propertiesCache = std::move(_properties); + propertiesBlob = serializeProperties(propertiesCache); + propertiesLoaded = true; + simpleName = CardInfo::simplifyName(name); refreshCachedSets(); @@ -56,18 +87,15 @@ CardInfo::CardInfo(const QString &_name, CardInfo::CardInfo(const QString &_name, const QString &_text, bool _isToken, - const QByteArray &_propertiesBlob, + QByteArray _propertiesBlob, const QList &_relatedCards, const QList &_reverseRelatedCards, SetToPrintingsMap _sets, const UiAttributes _uiAttributes, QString _simpleName, - QSet _altNames, - QMap _localizedNames, - QMap _localizedTexts) + QSet _altNames) : name(_name), simpleName(std::move(_simpleName)), text(_text), isToken(_isToken), - localizedNames(std::move(_localizedNames)), localizedTexts(std::move(_localizedTexts)), - properties(LazyPropertiesHash(_propertiesBlob)), relatedCards(_relatedCards), + propertiesBlob(std::move(_propertiesBlob)), relatedCards(_relatedCards), reverseRelatedCards(_reverseRelatedCards), setsToPrintings(std::move(_sets)), uiAttributes(_uiAttributes), altNames(std::move(_altNames)) { @@ -85,16 +113,14 @@ CardInfoPtr CardInfo::newInstance(const QString &_name) CardInfoPtr CardInfo::newInstance(const QString &_name, const QString &_text, bool _isToken, - const QHash &_properties, + QHash _properties, const QList &_relatedCards, const QList &_reverseRelatedCards, SetToPrintingsMap _sets, - const UiAttributes _uiAttributes, - QMap _localizedNames, - QMap _localizedTexts) + const UiAttributes _uiAttributes) { - CardInfoPtr ptr(new CardInfo(_name, _text, _isToken, _properties, _relatedCards, _reverseRelatedCards, _sets, - _uiAttributes, std::move(_localizedNames), std::move(_localizedTexts))); + CardInfoPtr ptr(new CardInfo(_name, _text, _isToken, std::move(_properties), _relatedCards, _reverseRelatedCards, + _sets, _uiAttributes)); ptr->setSmartPointer(ptr); for (const auto &printings : _sets) { @@ -117,13 +143,11 @@ CardInfoPtr CardInfo::newInstance(const QString &_name, const UiAttributes _uiAttributes, QString _simpleName, QSet _altNames, - bool _appendToSets, - QMap _localizedNames, - QMap _localizedTexts) + bool _appendToSets) { CardInfoPtr ptr(new CardInfo(_name, _text, _isToken, std::move(_propertiesBlob), _relatedCards, _reverseRelatedCards, _sets, _uiAttributes, std::move(_simpleName), - std::move(_altNames), std::move(_localizedNames), std::move(_localizedTexts))); + std::move(_altNames))); ptr->setSmartPointer(ptr); if (_appendToSets) { @@ -179,13 +203,15 @@ void CardInfo::addToSet(const CardSetPtr &_set, const PrintingInfo &_info) void CardInfo::combineLegalities(const QHash &props) { + ensurePropertiesLoaded(); QHashIterator it(props); while (it.hasNext()) { it.next(); if (it.key().startsWith("format-")) { - properties.insert(it.key(), it.value()); + propertiesCache.insert(it.key(), it.value()); } } + propertiesBlob = serializeProperties(propertiesCache); emit cardInfoChanged(smartThis); } diff --git a/libcockatrice_card/libcockatrice/card/card_info.h b/libcockatrice_card/libcockatrice/card/card_info.h index 17894cec5..a5c208893 100644 --- a/libcockatrice_card/libcockatrice/card/card_info.h +++ b/libcockatrice_card/libcockatrice/card/card_info.h @@ -2,7 +2,6 @@ #define CARD_INFO_H #include "format/format_legality_rules.h" -#include "lazy_properties_hash.h" #include "printing/printing_info.h" #include @@ -76,11 +75,19 @@ private: QString simpleName; ///< Simplified name for fuzzy matching. QString text; ///< Text description or rules text of the card. bool isToken; ///< Whether this card is a token or not. + // Properties are stored as a pre-serialized blob (cheap to load) and the + // QHash is materialized on first query, so database load avoids + // constructing thousands of QStrings per card. + mutable QByteArray propertiesBlob; ///< Serialized properties (load form). + mutable QHash propertiesCache; ///< Materialized properties (query form). + mutable bool propertiesLoaded = false; ///< Whether propertiesCache is valid. + mutable QMutex propertiesMutex; ///< Guards lazy materialization. - QMap localizedNames; ///< Localized card names, keyed by language code. - QMap localizedTexts; ///< Localized rules text, keyed by language code. - - LazyPropertiesHash properties; ///< Key-value store of dynamic card properties. + /** + * @brief Materializes propertiesCache from propertiesBlob if not already done. + * Safe to call from const getters (members are mutable). + */ + void ensurePropertiesLoaded() const; QList relatedCards; ///< Forward references to related cards. QList reverseRelatedCards; ///< Cards that refer back to this card. @@ -103,19 +110,15 @@ public: * @param _reverseRelatedCards Backward references to related cards. * @param _sets Map of set names to printing information. * @param _uiAttributes Attributes that affect display and game logic - * @param _localizedNames Localized card names, keyed by language code. - * @param _localizedTexts Localized rules text, keyed by language code. */ explicit CardInfo(const QString &_name, const QString &_text, bool _isToken, - const QHash &_properties, + QHash _properties, const QList &_relatedCards, const QList &_reverseRelatedCards, SetToPrintingsMap _sets, - UiAttributes _uiAttributes, - QMap _localizedNames = {}, - QMap _localizedTexts = {}); + UiAttributes _uiAttributes); /** * @brief Constructs a CardInfo from a cache snapshot with precomputed derived @@ -137,21 +140,17 @@ public: * @param _uiAttributes Attributes that affect display and game logic. * @param _simpleName Precomputed simplified name. * @param _altNames Precomputed alternate names. - * @param _localizedNames Localized card names, keyed by language code. - * @param _localizedTexts Localized rules text, keyed by language code. */ explicit CardInfo(const QString &_name, const QString &_text, bool _isToken, - const QByteArray &_propertiesBlob, + QByteArray _propertiesBlob, const QList &_relatedCards, const QList &_reverseRelatedCards, SetToPrintingsMap _sets, UiAttributes _uiAttributes, QString _simpleName, - QSet _altNames, - QMap _localizedNames = {}, - QMap _localizedTexts = {}); + QSet _altNames); /** * @brief Copy constructor for CardInfo. @@ -162,8 +161,7 @@ public: */ CardInfo(const CardInfo &other) : QObject(other.parent()), name(other.name), simpleName(other.simpleName), text(other.text), - isToken(other.isToken), localizedNames(other.localizedNames), localizedTexts(other.localizedTexts), - properties(other.properties), relatedCards(other.relatedCards), + isToken(other.isToken), propertiesBlob(other.propertiesBlob), relatedCards(other.relatedCards), reverseRelatedCards(other.reverseRelatedCards), reverseRelatedCardsToMe(other.reverseRelatedCardsToMe), setsToPrintings(other.setsToPrintings), uiAttributes(other.uiAttributes), setsNames(other.setsNames), altNames(other.altNames) @@ -191,20 +189,16 @@ public: * @param _reverseRelatedCards Reverse relationships. * @param _sets Printing information per set. * @param _uiAttributes Attributes that affect display and game logic - * @param _localizedNames Localized card names, keyed by language code. - * @param _localizedTexts Localized rules text, keyed by language code. * @return Shared pointer to the new CardInfo instance. */ static CardInfoPtr newInstance(const QString &_name, const QString &_text, bool _isToken, - const QHash &_properties, + QHash _properties, const QList &_relatedCards, const QList &_reverseRelatedCards, SetToPrintingsMap _sets, - UiAttributes _uiAttributes, - QMap _localizedNames = {}, - QMap _localizedTexts = {}); + UiAttributes _uiAttributes); /** * @brief Creates a new instance from a cache snapshot with precomputed @@ -224,8 +218,6 @@ public: * its CardSets. Pass false when building cards in parallel so the * (non-thread-safe) set membership is populated in a later * single-threaded pass. - * @param _localizedNames Localized card names, keyed by language code. - * @param _localizedTexts Localized rules text, keyed by language code. * @return Shared pointer to the new CardInfo instance. */ static CardInfoPtr newInstance(const QString &_name, @@ -238,9 +230,7 @@ public: UiAttributes _uiAttributes, QString _simpleName, QSet _altNames, - bool _appendToSets = true, - QMap _localizedNames = {}, - QMap _localizedTexts = {}); + bool _appendToSets = true); /** * @brief Clones the current CardInfo instance. @@ -290,96 +280,6 @@ public: text = _text; emit cardInfoChanged(smartThis); } - - /** - * @brief Returns the card name in the given language, falling back to the - * English name when no localization is available. - * - * @param lang Language code (e.g. "de", "ja", "zhs"). - * @return The localized name, or the English name as fallback. - */ - [[nodiscard]] const QString &getLocalizedName(const QString &lang) const - { - const auto it = localizedNames.constFind(lang); - return it != localizedNames.constEnd() ? it.value() : name; - } - - /** - * @brief Returns the rules text in the given language, falling back to the - * English text when no localization is available. - * - * @param lang Language code (e.g. "de", "ja", "zhs"). - * @return The localized text, or the English text as fallback. - */ - [[nodiscard]] const QString &getLocalizedText(const QString &lang) const - { - const auto it = localizedTexts.constFind(lang); - return it != localizedTexts.constEnd() ? it.value() : text; - } - - /** - * @brief Returns the localized card names keyed by language code. - * - * Only languages that have an entry are present; there is no English - * fallback in this map. - */ - [[nodiscard]] const QMap &getLocalizedNames() const - { - return localizedNames; - } - - /** - * @brief Returns the localized rules text keyed by language code. - * - * Only languages that have an entry are present; there is no English - * fallback in this map. - */ - [[nodiscard]] const QMap &getLocalizedTexts() const - { - return localizedTexts; - } - - /** - * @brief Sets the card name for the given language. - * - * @param lang Language code. - * @param _localizedName The localized card name. - */ - void setLocalizedName(const QString &lang, const QString &_localizedName) - { - if (localizedNames.value(lang) == _localizedName) { - return; - } - localizedNames.insert(lang, _localizedName); - emit cardInfoChanged(smartThis); - } - - /** - * @brief Sets the rules text for the given language. - * - * @param lang Language code. - * @param _localizedText The localized rules text. - */ - void setLocalizedText(const QString &lang, const QString &_localizedText) - { - if (localizedTexts.value(lang) == _localizedText) { - return; - } - localizedTexts.insert(lang, _localizedText); - emit cardInfoChanged(smartThis); - } - - /** - * @brief Returns the language codes for which this card has a localized - * name or rules text. - */ - [[nodiscard]] QStringList localizationLanguages() const - { - QStringList languages = localizedNames.keys(); - languages.append(localizedTexts.keys()); - languages.removeDuplicates(); - return languages; - } [[nodiscard]] bool getIsToken() const { return isToken; diff --git a/libcockatrice_card/libcockatrice/card/card_localization.cpp b/libcockatrice_card/libcockatrice/card/card_localization.cpp deleted file mode 100644 index 03c6c659b..000000000 --- a/libcockatrice_card/libcockatrice/card/card_localization.cpp +++ /dev/null @@ -1,38 +0,0 @@ -#include "card_localization.h" - -#include -#include -#include - -namespace CardLocalization -{ -const QStringList &supportedLanguages() -{ - static const QStringList languages = {"cs", "de", "es", "fr", "it", "ja", "ko", "pt", "ru", "zhs", "zht", "he"}; - return languages; -} - -QString languageDisplayName(const QString &lang) -{ - static const QHash displayNames = { - {"cs", "Česky (Czech)"}, - {"de", "Deutsch (German)"}, - {"es", "Español (Spanish)"}, - {"fr", "Français (French)"}, - {"it", "Italiano (Italian)"}, - {"ja", "日本語 (Japanese)"}, - {"ko", "한국어 (Korean)"}, - {"pt", "Português (Portuguese)"}, - {"ru", "Русский (Russian)"}, - {"he", "עברית (Hebrew)"}, - {"zhs", "简体中文 (Chinese Simplified)"}, - {"zht", "繁體中文 (Chinese Traditional)"}, - }; - const QString displayName = displayNames.value(lang); - if (!displayName.isEmpty()) { - return displayName; - } - const QString nativeName = QLocale(lang).nativeLanguageName(); - return nativeName.isEmpty() ? lang : nativeName; -} -} // namespace CardLocalization \ No newline at end of file diff --git a/libcockatrice_card/libcockatrice/card/card_localization.h b/libcockatrice_card/libcockatrice/card/card_localization.h deleted file mode 100644 index 1bde506cb..000000000 --- a/libcockatrice_card/libcockatrice/card/card_localization.h +++ /dev/null @@ -1,79 +0,0 @@ -#ifndef CARD_LOCALIZATION_H -#define CARD_LOCALIZATION_H - -#include -#include - -/** - * @brief The card languages card search should run against. - */ -enum class SearchLanguageMode -{ - English, ///< Only search the English card names and texts. - Selected, ///< Search the selected card language (untranslated cards still match in English). - Both ///< Search both the English and the selected card language names and texts. -}; - -/** - * @brief The card language and matching mode searches run against. - * - * Bundles the card language code configured in the settings with the - * SearchLanguageMode, so entry points take one value instead of two related - * parameters. - */ -struct CardSearchLanguage -{ - QString language; ///< Card language code (e.g. "de"); empty means the English fallback. - SearchLanguageMode mode = SearchLanguageMode::English; ///< How the language participates in the search. - - /** - * @brief Whether only the English card data is searched. - * - * @return True when no card language is selected or English itself is selected. - */ - [[nodiscard]] bool isEnglishOnly() const - { - return language.isEmpty() || language == QLatin1String("en"); - } - - bool operator==(const CardSearchLanguage &) const = default; - bool operator!=(const CardSearchLanguage &) const = default; -}; - -/** - * @namespace CardLocalization - * @ingroup Cards - * - * @brief Shared language metadata for localized card text and images. - * - * Lists the language codes Cockatrice can display localized card data for and - * provides human-readable names. The list is shared between Oracle (which - * imports the selected language's card data) and the client settings UI (which - * offers the language choice). - */ -namespace CardLocalization -{ -/** - * @brief Language codes for which localized card data can be imported/displayed. - * - * Matches the languages Scryfall can serve localized card images for. "en" is - * always available as the default/fallback and is not listed here. - * - * @return The list of supported language codes. - */ -[[nodiscard]] const QStringList &supportedLanguages(); - -/** - * @brief Human-readable name for a language code. - * - * Follows the same "native name (English name)" format the UI language list - * uses (e.g. "日本語 (Japanese)"), so the English fallback is always visible. - * - * @param lang Language code (e.g. "de", "ja", "zhs"). - * @return The language's native name with its English name in parentheses, or - * the code itself if it cannot be resolved. - */ -[[nodiscard]] QString languageDisplayName(const QString &lang); -} // namespace CardLocalization - -#endif // CARD_LOCALIZATION_H \ No newline at end of file diff --git a/libcockatrice_card/libcockatrice/card/database/card_database_cache.cpp b/libcockatrice_card/libcockatrice/card/database/card_database_cache.cpp index 3165ff871..3985889b7 100644 --- a/libcockatrice_card/libcockatrice/card/database/card_database_cache.cpp +++ b/libcockatrice_card/libcockatrice/card/database/card_database_cache.cpp @@ -17,7 +17,7 @@ namespace { constexpr quint32 CACHE_MAGIC = 0x43445243; // "CDRC" -constexpr quint32 CACHE_VERSION = 3; +constexpr quint32 CACHE_VERSION = 2; // ---- Primitives ----------------------------------------------------------- @@ -71,35 +71,6 @@ QDate readDate(QDataStream &in) return d; } -void writeStringMap(QDataStream &out, const QMap &map) -{ - out << static_cast(map.size()); - for (auto it = map.constBegin(); it != map.constEnd(); ++it) { - writeString(out, it.key()); - writeString(out, it.value()); - } -} - -QMap readStringMap(QDataStream &in) -{ - QMap map; - quint32 count = 0; - in >> count; - if (in.status() != QDataStream::Ok) { - return map; - } - for (quint32 i = 0; i < count; ++i) { - QString key = readString(in); - QString value = readString(in); - if (in.status() != QDataStream::Ok) { - map.clear(); - return map; - } - map.insert(key, value); - } - return map; -} - // ---- CardRelation ---------------------------------------------------------- void writeRelation(QDataStream &out, const CardRelation *rel) @@ -148,7 +119,7 @@ PrintingInfo readPrinting(QDataStream &in, const SetNameMap &sets) QByteArray propsBlob = readHashBlob(in); auto set = sets.value(setName); - return PrintingInfo(set, LazyPropertiesHash(propsBlob)); + return PrintingInfo(set, propsBlob); } // ---- CardSet --------------------------------------------------------------- @@ -224,10 +195,6 @@ void writeCard(QDataStream &out, const CardInfoPtr &card) for (const CardRelation *rel : reverse) { writeRelation(out, rel); } - - // localized card data - writeStringMap(out, card->getLocalizedNames()); - writeStringMap(out, card->getLocalizedTexts()); } CardInfoPtr readCard(QDataStream &in, const SetNameMap &sets) @@ -301,19 +268,8 @@ CardInfoPtr readCard(QDataStream &in, const SetNameMap &sets) reverse.append(readRelation(in)); } - const QMap localizedNames = readStringMap(in); - if (in.status() != QDataStream::Ok) { - return nullptr; - } - const QMap localizedTexts = readStringMap(in); - if (in.status() != QDataStream::Ok) { - return nullptr; - } - - CardInfoPtr card = CardInfo::newInstance(name, text, isToken, propertiesBlob, related, reverse, cardSets, ui, - simpleName, altNames, false, localizedNames, localizedTexts); - - return card; + return CardInfo::newInstance(name, text, isToken, propertiesBlob, related, reverse, cardSets, ui, simpleName, + altNames, false); } // ---- FormatRules ----------------------------------------------------------- diff --git a/libcockatrice_card/libcockatrice/card/database/card_database_querier.cpp b/libcockatrice_card/libcockatrice/card/database/card_database_querier.cpp index 5b9c5a4b5..174943333 100644 --- a/libcockatrice_card/libcockatrice/card/database/card_database_querier.cpp +++ b/libcockatrice_card/libcockatrice/card/database/card_database_querier.cpp @@ -12,17 +12,6 @@ CardDatabaseQuerier::CardDatabaseQuerier(QObject *_parent, const ICardPreferenceProvider *prefs) : QObject(_parent), db(_db), prefs(prefs) { - // Invalidate the cached count maps whenever the database contents change. - connect(db, &CardDatabase::cardAdded, this, &CardDatabaseQuerier::invalidateCaches); - connect(db, &CardDatabase::cardRemoved, this, &CardDatabaseQuerier::invalidateCaches); - connect(db, &CardDatabase::cardDatabaseReset, this, &CardDatabaseQuerier::invalidateCaches); -} - -void CardDatabaseQuerier::invalidateCaches() -{ - mainCardTypeCountsCache.clear(); - subCardTypeCountsCache.clear(); - formatsCountCache.clear(); } /** @@ -315,43 +304,44 @@ QString CardDatabaseQuerier::getPreferredPrintingProviderId(const QString &cardN QStringList CardDatabaseQuerier::getAllMainCardTypes() const { - return getAllMainCardTypesWithCount().keys(); + QSet types; + for (const auto &card : db->cards.values()) { + types.insert(card->getMainCardType()); + } + return types.values(); } QMap CardDatabaseQuerier::getAllMainCardTypesWithCount() const { - // An empty cache is always recomputed correctly: a database with no cards - // produces an empty map, so the cache is only ever empty when it needs a - // (trivially cheap) rebuild. - if (mainCardTypeCountsCache.isEmpty()) { - for (const auto &card : db->cards.values()) { - QString type = card->getMainCardType(); - mainCardTypeCountsCache[type]++; - } + QMap typeCounts; + + for (const auto &card : db->cards.values()) { + QString type = card->getMainCardType(); + typeCounts[type]++; } - return mainCardTypeCountsCache; + return typeCounts; } QMap CardDatabaseQuerier::getAllSubCardTypesWithCount() const { - if (subCardTypeCountsCache.isEmpty()) { - for (const auto &card : db->cards.values()) { - QString type = card->getCardType(); + QMap typeCounts; - QStringList parts = type.split(" — "); + for (const auto &card : db->cards.values()) { + QString type = card->getCardType(); - if (parts.size() > 1) { // Ensure there are subtypes - QStringList subtypes = parts[1].split(" ", Qt::SkipEmptyParts); + QStringList parts = type.split(" — "); - for (const QString &subtype : subtypes) { - subCardTypeCountsCache[subtype]++; - } + if (parts.size() > 1) { // Ensure there are subtypes + QStringList subtypes = parts[1].split(" ", Qt::SkipEmptyParts); + + for (const QString &subtype : subtypes) { + typeCounts[subtype]++; } } } - return subCardTypeCountsCache; + return typeCounts; } FormatRulesPtr CardDatabaseQuerier::getFormat(const QString &formatName) const @@ -361,18 +351,18 @@ FormatRulesPtr CardDatabaseQuerier::getFormat(const QString &formatName) const QMap CardDatabaseQuerier::getAllFormatsWithCount() const { - if (formatsCountCache.isEmpty()) { - for (const auto &card : db->cards.values()) { - QStringList allProps = card->getProperties(); + QMap formatCounts; - for (const QString &prop : allProps) { - if (prop.startsWith("format-")) { - QString formatName = prop.mid(QStringLiteral("format-").size()); - formatsCountCache[formatName]++; - } + for (const auto &card : db->cards.values()) { + QStringList allProps = card->getProperties(); + + for (const QString &prop : allProps) { + if (prop.startsWith("format-")) { + QString formatName = prop.mid(QStringLiteral("format-").size()); + formatCounts[formatName]++; } } } - return formatsCountCache; + return formatCounts; } diff --git a/libcockatrice_card/libcockatrice/card/database/card_database_querier.h b/libcockatrice_card/libcockatrice/card/database/card_database_querier.h index f195a8170..ff8d7958b 100644 --- a/libcockatrice_card/libcockatrice/card/database/card_database_querier.h +++ b/libcockatrice_card/libcockatrice/card/database/card_database_querier.h @@ -220,16 +220,6 @@ public: private: const CardDatabase *db; //!< Card database used for all lookups. const ICardPreferenceProvider *prefs; //!< Preference provider for preferred printings. - - // Count maps are expensive to compute (they iterate the whole database) and are - // queried every time a filter widget is built, so cache them and invalidate on - // any database mutation. Only the main thread reads or writes these. - mutable QMap mainCardTypeCountsCache; - mutable QMap subCardTypeCountsCache; - mutable QMap formatsCountCache; - -private slots: - void invalidateCaches(); }; #endif // COCKATRICE_CARD_DATABASE_QUERIER_H diff --git a/libcockatrice_card/libcockatrice/card/database/parser/cockatrice_xml_3.cpp b/libcockatrice_card/libcockatrice/card/database/parser/cockatrice_xml_3.cpp index 64202ab21..f3aac7809 100644 --- a/libcockatrice_card/libcockatrice/card/database/parser/cockatrice_xml_3.cpp +++ b/libcockatrice_card/libcockatrice/card/database/parser/cockatrice_xml_3.cpp @@ -248,7 +248,7 @@ void CockatriceXml3Parser::loadCardsFromXml(QXmlStreamReader &xml) if (attrs.hasAttribute("rarity")) { printingProps.insert("rarity", attrs.value("rarity").toString()); } - PrintingInfo setInfo(set, LazyPropertiesHash(printingProps)); + PrintingInfo setInfo(set, printingProps); _sets[setName].append(setInfo); } // related cards 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 19e972a7a..8649bbfaf 100644 --- a/libcockatrice_card/libcockatrice/card/database/parser/cockatrice_xml_4.cpp +++ b/libcockatrice_card/libcockatrice/card/database/parser/cockatrice_xml_4.cpp @@ -273,8 +273,6 @@ void CockatriceXml4Parser::loadCardsFromXml(QXmlStreamReader &xml) QString name = QString(""); QString text = QString(""); QHash properties; - QMap localizedNames; - QMap localizedTexts; QList relatedCards, reverseRelatedCards; auto _sets = SetToPrintingsMap(); int tableRow = 0; @@ -300,44 +298,6 @@ void CockatriceXml4Parser::loadCardsFromXml(QXmlStreamReader &xml) // generic properties } else if (xmlName == "prop") { properties = loadCardPropertiesFromXml(xml); - // localized card data - } else if (xmlName == "localizations") { - while (!xml.atEnd()) { - if (xml.readNextStartElement()) { - const QString elementName = xml.name().toString(); - if (elementName == "localization") { - const QString lang = xml.attributes().value("lang").toString(); - QString localizedName; - QString localizedText; - while (!xml.atEnd()) { - if (xml.readNext() == QXmlStreamReader::EndElement) { - break; - } - if (xml.isStartElement()) { - const QString childName = xml.name().toString(); - QString value = xml.readElementText(QXmlStreamReader::IncludeChildElements); - if (childName == "name") { - localizedName = value; - } else if (childName == "text") { - localizedText = value; - } - } - } - if (!lang.isEmpty()) { - if (!localizedName.isEmpty()) { - localizedNames.insert(lang, localizedName); - } - if (!localizedText.isEmpty()) { - localizedTexts.insert(lang, localizedText); - } - } - } else { - xml.skipCurrentElement(); - } - } else { - break; - } - } // positioning info } else if (xmlName == "tablerow") { tableRow = xml.readElementText(QXmlStreamReader::IncludeChildElements).toInt(); @@ -362,7 +322,7 @@ void CockatriceXml4Parser::loadCardsFromXml(QXmlStreamReader &xml) } printingProps.insert(attrName, attr.value().toString()); } - PrintingInfo printingInfo(set, LazyPropertiesHash(printingProps)); + PrintingInfo printingInfo(set, printingProps); // This is very much a hack and not the right place to // put this check, as it requires a reload of Cockatrice @@ -439,9 +399,8 @@ void CockatriceXml4Parser::loadCardsFromXml(QXmlStreamReader &xml) .landscapeOrientation = landscapeOrientation, .tableRow = tableRow, .upsideDownArt = upsideDown}; - CardInfoPtr newCard = - CardInfo::newInstance(name, text, isToken, properties, relatedCards, reverseRelatedCards, _sets, - attributes, std::move(localizedNames), std::move(localizedTexts)); + CardInfoPtr newCard = CardInfo::newInstance(name, text, isToken, properties, relatedCards, + reverseRelatedCards, _sets, attributes); if (targetData) { // Mirror CardDatabase::addCard: if a card with this name already // exists, merge the new printings into it instead of replacing. @@ -558,26 +517,6 @@ static QXmlStreamWriter &operator<<(QXmlStreamWriter &xml, const CardInfoPtr &in } xml.writeEndElement(); - // localized card data - const QStringList localizedLanguages = info->localizationLanguages(); - if (!localizedLanguages.isEmpty()) { - xml.writeStartElement("localizations"); - const QMap &localizedNames = info->getLocalizedNames(); - const QMap &localizedTexts = info->getLocalizedTexts(); - for (const QString &lang : localizedLanguages) { - xml.writeStartElement("localization"); - xml.writeAttribute("lang", lang); - if (localizedNames.contains(lang)) { - xml.writeTextElement("name", localizedNames.value(lang)); - } - if (localizedTexts.contains(lang)) { - xml.writeTextElement("text", localizedTexts.value(lang)); - } - xml.writeEndElement(); - } - xml.writeEndElement(); - } - // sets for (const auto &printings : info->getSets()) { for (const PrintingInfo &set : printings) { diff --git a/libcockatrice_card/libcockatrice/card/lazy_properties_hash.cpp b/libcockatrice_card/libcockatrice/card/lazy_properties_hash.cpp deleted file mode 100644 index 4d903a3bd..000000000 --- a/libcockatrice_card/libcockatrice/card/lazy_properties_hash.cpp +++ /dev/null @@ -1,95 +0,0 @@ -#include "lazy_properties_hash.h" - -#include - -LazyPropertiesHash::LazyPropertiesHash() : isMaterialized(true) -{ -} - -LazyPropertiesHash::LazyPropertiesHash(const QByteArray &blob) : blob(blob) -{ -} - -LazyPropertiesHash::LazyPropertiesHash(const QHash &properties) - : properties(properties), isMaterialized(true) -{ -} - -LazyPropertiesHash::LazyPropertiesHash(const LazyPropertiesHash &other) -{ - // since we do not allow dematerialization, we only need to lock if not materialized yet - if (other.isMaterialized) { - blob = other.blob; - properties = other.properties; - isMaterialized = true; - } else { - QMutexLocker lock(&other.propertiesMutex); - blob = other.blob; - properties = other.properties; - isMaterialized = false; - } -} - -LazyPropertiesHash &LazyPropertiesHash::operator=(const LazyPropertiesHash &other) -{ - if (this == &other) { - return *this; - } - - // since we do not allow dematerialization, we only need to lock if not materialized yet - if (other.isMaterialized) { - blob = other.blob; - properties = other.properties; - isMaterialized = true; - } else { - QMutexLocker lock(&other.propertiesMutex); - blob = other.blob; - properties = other.properties; - isMaterialized = false; - } - - return *this; -} - -void LazyPropertiesHash::ensureMaterialized() const -{ - QMutexLocker lock(&propertiesMutex); - - if (isMaterialized) { - return; - } - - if (!blob.isEmpty()) { - QDataStream in(blob); - in.setVersion(QDataStream::Qt_6_4); - in >> properties; - } - - blob.clear(); - - isMaterialized = true; -} - -QString LazyPropertiesHash::value(const QString &key) const -{ - ensureMaterialized(); - return properties.value(key); -} - -bool LazyPropertiesHash::insert(const QString &key, const QString &value) -{ - ensureMaterialized(); - - if (value == properties.value(key)) { - return false; - } - - properties.insert(key, value); - return true; -} - -const QHash &LazyPropertiesHash::getProperties() const -{ - ensureMaterialized(); - return properties; -} diff --git a/libcockatrice_card/libcockatrice/card/lazy_properties_hash.h b/libcockatrice_card/libcockatrice/card/lazy_properties_hash.h deleted file mode 100644 index 0ed8c215b..000000000 --- a/libcockatrice_card/libcockatrice/card/lazy_properties_hash.h +++ /dev/null @@ -1,74 +0,0 @@ -#ifndef COCKATRICE_LAZY_PROPERTIES_HASH_H -#define COCKATRICE_LAZY_PROPERTIES_HASH_H - -#include -#include - -/** - * @brief A property map that can lazily deserialize blobs to avoid loading overhead. - * - * Properties are stored as a pre-serialized blob (cheap to load), and the QString is materialized on the first query, - * so the database load avoids constructing thousands of QString per card. - * - * Once the properties are materialized, it cannot be unmaterialized. - * If you want to reset the properties to an unmaterialized state, you should create a new LazyPropertiesHash. - */ -class LazyPropertiesHash -{ - - mutable QByteArray blob; ///< Serialized properties (load form). - mutable QHash properties; ///< Materialized properties (query form). - mutable QMutex propertiesMutex; ///< Guards lazy materialization. - mutable bool isMaterialized = false; ///< Whether propertiesCache is valid. - - /** - * @brief Materializes properties from blob if not already done. Clears blob afterward. - * Safe to call from const getters (members are mutable). - */ - void ensureMaterialized() const; - -public: - /** - * @brief Default constructor. - */ - LazyPropertiesHash(); - - /** - * @brief Creates an unmaterialized LazyPropertiesHash - * @param blob The pre-serialized blob - */ - explicit LazyPropertiesHash(const QByteArray &blob); - - /** - * @brief Creates an already-materialized LazyPropertiesHash - * @param properties The properties - */ - explicit LazyPropertiesHash(const QHash &properties); - - // Override copy constructor and copy-assignment because mutex isn't copiable - LazyPropertiesHash(const LazyPropertiesHash &other); - LazyPropertiesHash &operator=(const LazyPropertiesHash &other); - - /** - * @brief Gets the value from the materialized properties hash - * @param key The key - * @return The value, or an empty string if the key is not present - */ - QString value(const QString &key) const; - - /** - * @brief Inserts a value into the materialized properties hash - * @param key The key - * @param value The value to insert - * @return True if a new value was inserted; false if the new value is the same as the existing value - */ - bool insert(const QString &key, const QString &value); - - /** - * @brief Gets a view of the materialized properties hash. - * @return The properties hash - */ - const QHash &getProperties() const; -}; - -#endif // COCKATRICE_LAZY_PROPERTIES_HASH_H diff --git a/libcockatrice_card/libcockatrice/card/printing/printing_info.cpp b/libcockatrice_card/libcockatrice/card/printing/printing_info.cpp index 3d185583a..49086f8b7 100644 --- a/libcockatrice_card/libcockatrice/card/printing/printing_info.cpp +++ b/libcockatrice_card/libcockatrice/card/printing/printing_info.cpp @@ -5,14 +5,37 @@ #include #include -PrintingInfo::PrintingInfo(const CardSetPtr &_set, const LazyPropertiesHash &_properties) - : set(_set), properties(_properties) +PrintingInfo::PrintingInfo(const CardSetPtr &_set, const QHash &_properties) + : set(_set), propertiesCache(_properties), propertiesLoaded(true) { } +PrintingInfo::PrintingInfo(const CardSetPtr &_set, const QByteArray &_blob) : set(_set), propertiesBlob(_blob) +{ +} + +void PrintingInfo::ensurePropertiesLoaded() const +{ + QMutexLocker lock(propertiesMutex.data()); + if (propertiesLoaded) { + return; + } + propertiesCache.clear(); + if (!propertiesBlob.isEmpty()) { + QDataStream in(propertiesBlob); + in.setVersion(QDataStream::Qt_6_4); + in >> propertiesCache; + } + propertiesLoaded = true; +} + void PrintingInfo::setProperty(const QString &_name, const QString &_value) { - properties.insert(_name, _value); + ensurePropertiesLoaded(); + if (propertiesCache.value(_name) == _value) { + return; + } + propertiesCache.insert(_name, _value); } /** diff --git a/libcockatrice_card/libcockatrice/card/printing/printing_info.h b/libcockatrice_card/libcockatrice/card/printing/printing_info.h index 974c7bb95..70093b686 100644 --- a/libcockatrice_card/libcockatrice/card/printing/printing_info.h +++ b/libcockatrice_card/libcockatrice/card/printing/printing_info.h @@ -2,11 +2,12 @@ #define COCKATRICE_PRINTING_INFO_H #include "../set/card_set.h" -#include "libcockatrice/card/lazy_properties_hash.h" #include #include +#include #include +#include class PrintingInfo; @@ -33,7 +34,15 @@ public: * @param _set The set this printing belongs to (defaults to null). * @param _properties The printing properties (defaults to empty) */ - explicit PrintingInfo(const CardSetPtr &_set = nullptr, const LazyPropertiesHash &_properties = {}); + explicit PrintingInfo(const CardSetPtr &_set = nullptr, const QHash &_properties = {}); + + /** + * @brief Constructs a PrintingInfo associated with a specific set. + * + * @param _set The set this printing belongs to (defaults to null). + * @param _blob The serialized properties (as written by the cache writer). + */ + explicit PrintingInfo(const CardSetPtr &_set, const QByteArray &_blob); /** * @brief Destroys the PrintingInfo. @@ -67,8 +76,18 @@ public: } private: - CardSetPtr set; ///< The set this variation belongs to. - LazyPropertiesHash properties; ///< Key-value store for variation-specific attributes. + CardSetPtr set; ///< The set this variation belongs to. + + // Properties are stored as a pre-serialized blob (cheap to load) and the + // QHash is materialized on first query. This avoids constructing + // thousands of QStrings per card at database-load time. + mutable QByteArray propertiesBlob; ///< Serialized properties (load form). + mutable QHash propertiesCache; ///< Materialized properties (query form). + mutable bool propertiesLoaded = false; ///< Whether propertiesCache is valid. + mutable QSharedPointer propertiesMutex = + QSharedPointer::create(); ///< Guards lazy materialization. + + void ensurePropertiesLoaded() const; public: /** @@ -93,7 +112,8 @@ public: [[nodiscard]] const QHash &getPropertiesHash() const { - return properties.getProperties(); + ensurePropertiesLoaded(); + return propertiesCache; } /** @@ -104,7 +124,7 @@ public: */ [[nodiscard]] QString getProperty(const QString &propertyName) const { - return properties.value(propertyName); + return getPropertiesHash().value(propertyName); } /** @@ -130,18 +150,6 @@ public: * @return The flavorName, or empty if it isn't present. */ [[nodiscard]] QString getFlavorName() const; - - /** - * @brief Returns the artist name credited for this printing's artwork. - * - * Requires a card database generated with artist data. - * - * @return The artist name, or empty if it isn't present. - */ - [[nodiscard]] QString getArtist() const - { - return getProperty("artist"); - } }; #endif // COCKATRICE_PRINTING_INFO_H diff --git a/libcockatrice_deck_list/CMakeLists.txt b/libcockatrice_deck_list/CMakeLists.txt index 0c487466c..33494b9a9 100644 --- a/libcockatrice_deck_list/CMakeLists.txt +++ b/libcockatrice_deck_list/CMakeLists.txt @@ -11,8 +11,6 @@ set(HEADERS libcockatrice/deck_list/deck_list_history_manager.h libcockatrice/deck_list/deck_list_node_tree.h libcockatrice/deck_list/deck_list_memento.h - libcockatrice/deck_list/deck_list_plain_text_parser.h - libcockatrice/deck_list/playmat_resolver.h libcockatrice/deck_list/sideboard_plan.h ) @@ -28,8 +26,6 @@ add_library( libcockatrice/deck_list/deck_list.cpp libcockatrice/deck_list/deck_list_history_manager.cpp libcockatrice/deck_list/deck_list_node_tree.cpp - libcockatrice/deck_list/deck_list_plain_text_parser.cpp - libcockatrice/deck_list/playmat_resolver.cpp libcockatrice/deck_list/sideboard_plan.cpp ) @@ -37,7 +33,4 @@ add_dependencies(libcockatrice_deck_list libcockatrice_protocol) target_include_directories(libcockatrice_deck_list PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) -target_link_libraries( - libcockatrice_deck_list PUBLIC libcockatrice_interfaces libcockatrice_protocol libcockatrice_utility - ${QT_CORE_MODULE} -) +target_link_libraries(libcockatrice_deck_list PUBLIC libcockatrice_protocol libcockatrice_utility ${QT_CORE_MODULE}) diff --git a/libcockatrice_deck_list/libcockatrice/deck_list/deck_list.cpp b/libcockatrice_deck_list/libcockatrice/deck_list/deck_list.cpp index 90f63c09d..d41713302 100644 --- a/libcockatrice_deck_list/libcockatrice/deck_list/deck_list.cpp +++ b/libcockatrice_deck_list/libcockatrice/deck_list/deck_list.cpp @@ -1,7 +1,6 @@ #include "deck_list.h" #include "deck_list_memento.h" -#include "deck_list_plain_text_parser.h" #include "tree/abstract_deck_list_node.h" #include "tree/deck_list_card_node.h" #include "tree/inner_deck_list_node.h" @@ -9,136 +8,24 @@ #include #include #include +#include #include #include #include +#if QT_VERSION < 0x050600 +// qHash on QRegularExpression was added in 5.6, FIX IT +uint qHash(const QRegularExpression &key, uint seed) noexcept +{ + return qHash(key.pattern(), seed); // call qHash on pattern QString instead +} +#endif + static const QString CURRENT_SIDEBOARD_PLAN_KEY = ""; -/** - * @brief Parses a floating point XML attribute into a clamped playmat parameter. - * - * Falls back to @p fallback when the attribute is missing or malformed, so - * malformed deck files cannot produce degenerate art rectangles (e.g. a zoom - * of 0 dividing by zero). - * - * @param valueString Raw attribute text. - * @param fallback Value used when the text cannot be parsed. - * @param min Lower clamp bound. - * @param max Upper clamp bound. - * @return The parsed value clamped to [min, max], or @p fallback. - */ -static double parseClampedParam(const QString &valueString, double fallback, double min, double max) -{ - bool ok = false; - const double value = valueString.toDouble(&ok); - if (!ok) { - return fallback; - } - return qBound(min, value, max); -} - -/** - * @brief Reads a `bannerCard` element from the XML stream. - * - * @param xml Reader positioned at the element. - * @return The referenced card. - */ -static CardRef readBannerCard(QXmlStreamReader *xml) -{ - QString providerId = xml->attributes().value("providerId").toString(); - QString cardName = xml->readElementText(); - return {cardName, providerId}; -} - -/** - * @brief Reads a `playmatCard` element from the XML stream. - * - * Attribute values are read before readElementText consumes the element, and - * the params are clamped to the same ranges as the settings dialog and the - * remote player-properties path so malformed deck files cannot produce - * degenerate art rectangles (e.g. a zoom of 0 dividing by zero). - * - * @param xml Reader positioned at the element. - * @return The referenced card plus its clamped positioning parameters. - */ -static PlaymatInfo readPlaymatCard(QXmlStreamReader *xml) -{ - QString providerId = xml->attributes().value("providerId").toString(); - QString marginLStr = xml->attributes().value("marginPctL").toString(); - QString marginRStr = xml->attributes().value("marginPctR").toString(); - QString vOffStr = xml->attributes().value("verticalOffset").toString(); - QString zoomStr = xml->attributes().value("zoom").toString(); - QString cardName = xml->readElementText(); - - return { - .card = {cardName, providerId}, - .params = {.marginPctL = parseClampedParam(marginLStr, 0.07, 0.0, 0.95), - .marginPctR = parseClampedParam(marginRStr, 0.07, 0.0, 0.95), - .verticalOffset = parseClampedParam(vOffStr, 0.33, 0.0, 1.0), - .zoom = parseClampedParam(zoomStr, 1.0, 0.1, 4.0)}, - }; -} - bool DeckList::Metadata::isEmpty() const { - return name.isEmpty() && comments.isEmpty() && bannerCard.isEmpty() && tags.isEmpty() && playmat.card.isEmpty(); -} - -bool DeckList::Metadata::readElement(QXmlStreamReader *xml, const QString &childName) -{ - if (childName == "lastLoadedTimestamp") { - lastLoadedTimestamp = xml->readElementText(); - } else if (childName == "deckname") { - name = xml->readElementText(); - } else if (childName == "format") { - gameFormat = xml->readElementText(); - } else if (childName == "comments") { - comments = xml->readElementText(); - } else if (childName == "bannerCard") { - bannerCard = readBannerCard(xml); - } else if (childName == "playmatCard") { - playmat = readPlaymatCard(xml); - } else if (childName == "tags") { - tags.clear(); // Clear existing tags - while (xml->readNextStartElement()) { - if (xml->name().toString() == "tag") { - tags.append(xml->readElementText()); - } - } - } else { - return false; - } - return true; -} - -void DeckList::Metadata::write(QXmlStreamWriter *xml) const -{ - xml->writeTextElement("lastLoadedTimestamp", lastLoadedTimestamp); - xml->writeTextElement("deckname", name); - xml->writeTextElement("format", gameFormat); - xml->writeStartElement("bannerCard"); - xml->writeAttribute("providerId", bannerCard.providerId); - xml->writeCharacters(bannerCard.name); - xml->writeEndElement(); - if (!playmat.card.isEmpty()) { - xml->writeStartElement("playmatCard"); - xml->writeAttribute("providerId", playmat.card.providerId); - xml->writeAttribute("marginPctL", QString::number(playmat.params.marginPctL, 'f', 4)); - xml->writeAttribute("marginPctR", QString::number(playmat.params.marginPctR, 'f', 4)); - xml->writeAttribute("verticalOffset", QString::number(playmat.params.verticalOffset, 'f', 4)); - xml->writeAttribute("zoom", QString::number(playmat.params.zoom, 'f', 4)); - xml->writeCharacters(playmat.card.name); - xml->writeEndElement(); - } - xml->writeTextElement("comments", comments); - - // Write tags - xml->writeStartElement("tags"); - for (const QString &tag : tags) { - xml->writeTextElement("tag", tag); - } - xml->writeEndElement(); + return name.isEmpty() && comments.isEmpty() && bannerCard.isEmpty() && tags.isEmpty(); } DeckList::DeckList() @@ -175,10 +62,26 @@ bool DeckList::readElement(QXmlStreamReader *xml) { const QString childName = xml->name().toString(); if (xml->isStartElement()) { - if (metadata.readElement(xml, childName)) { - return true; - } - if (childName == "zone") { + if (childName == "lastLoadedTimestamp") { + metadata.lastLoadedTimestamp = xml->readElementText(); + } else if (childName == "deckname") { + metadata.name = xml->readElementText(); + } else if (childName == "format") { + metadata.gameFormat = xml->readElementText(); + } else if (childName == "comments") { + metadata.comments = xml->readElementText(); + } else if (childName == "bannerCard") { + QString providerId = xml->attributes().value("providerId").toString(); + QString cardName = xml->readElementText(); + metadata.bannerCard = {cardName, providerId}; + } else if (childName == "tags") { + metadata.tags.clear(); // Clear existing tags + while (xml->readNextStartElement()) { + if (xml->name().toString() == "tag") { + metadata.tags.append(xml->readElementText()); + } + } + } else if (childName == "zone") { tree.readZoneElement(xml); } else if (childName == "sideboard_plan") { SideboardPlan newSideboardPlan; @@ -192,12 +95,31 @@ bool DeckList::readElement(QXmlStreamReader *xml) return true; } +static void writeMetadata(QXmlStreamWriter *xml, const DeckList::Metadata &metadata) +{ + xml->writeTextElement("lastLoadedTimestamp", metadata.lastLoadedTimestamp); + xml->writeTextElement("deckname", metadata.name); + xml->writeTextElement("format", metadata.gameFormat); + xml->writeStartElement("bannerCard"); + xml->writeAttribute("providerId", metadata.bannerCard.providerId); + xml->writeCharacters(metadata.bannerCard.name); + xml->writeEndElement(); + xml->writeTextElement("comments", metadata.comments); + + // Write tags + xml->writeStartElement("tags"); + for (const QString &tag : metadata.tags) { + xml->writeTextElement("tag", tag); + } + xml->writeEndElement(); +} + void DeckList::write(QXmlStreamWriter *xml) const { xml->writeStartElement("cockatrice_deck"); xml->writeAttribute("version", "1"); - metadata.write(xml); + writeMetadata(xml, metadata); // Write zones tree.write(xml); @@ -210,27 +132,6 @@ void DeckList::write(QXmlStreamWriter *xml) const xml->writeEndElement(); // Close "cockatrice_deck" } -bool DeckList::seekToNextElement(QXmlStreamReader *xml) -{ - while (!xml->atEnd()) { - xml->readNext(); - if (xml->isStartElement()) { - return true; - } - } - return false; -} - -void DeckList::readDeckBody(QXmlStreamReader *xml) -{ - while (!xml->atEnd()) { - xml->readNext(); - if (!readElement(xml)) { - break; - } - } -} - bool DeckList::loadFromXml(QXmlStreamReader *xml) { if (xml->error()) { @@ -239,11 +140,19 @@ bool DeckList::loadFromXml(QXmlStreamReader *xml) } cleanList(); - while (seekToNextElement(xml)) { - if (xml->name().toString() != "cockatrice_deck") { - return false; + while (!xml->atEnd()) { + xml->readNext(); + if (xml->isStartElement()) { + if (xml->name().toString() != "cockatrice_deck") { + return false; + } + while (!xml->atEnd()) { + xml->readNext(); + if (!readElement(xml)) { + break; + } + } } - readDeckBody(xml); } refreshDeckHash(); if (xml->error()) { @@ -299,12 +208,160 @@ bool DeckList::loadFromStream_Plain(QTextStream &in, bool preserveMetadata, const std::function &cardNameNormalizer) { - if (!preserveMetadata) { - metadata = {}; + const QRegularExpression reCardLine(R"(^\s*[\w\[\(\{].*$)", QRegularExpression::UseUnicodePropertiesOption); + const QRegularExpression reEmpty("^\\s*$"); + const QRegularExpression reComment(R"([\w\[\(\{].*$)", QRegularExpression::UseUnicodePropertiesOption); + const QRegularExpression reSBMark("^\\s*sb:\\s*(.+)", QRegularExpression::CaseInsensitiveOption); + const QRegularExpression reSBComment("^sideboard\\b.*$", QRegularExpression::CaseInsensitiveOption); + const QRegularExpression reDeckComment("^((main)?deck(list)?|mainboard)\\b", + QRegularExpression::CaseInsensitiveOption); + + // Regex for advanced card parsing + const QRegularExpression reMultiplier(R"(^[xX\(\[]*(\d+)[xX\*\)\]]* ?(.+))"); + + // Regex for extracting set code and collector number with attached symbols + const QRegularExpression reHyphenFormat(R"(\((\w{3,})\)\s+(\w{3,})-(\d+[^\w\s]*))"); + const QRegularExpression reRegularFormat(R"(\((\w{3,})\)\s+(\d+[^\w\s]*))"); + + cleanList(preserveMetadata); + + auto inputs = in.readAll().trimmed().split('\n'); + auto max_line = inputs.size(); + + // Start at the first empty line before the first card line + auto deckStart = inputs.indexOf(reCardLine); + if (deckStart == -1) { + if (inputs.indexOf(reComment) == -1) { + return false; // Input is empty + } + deckStart = max_line; + } else { + deckStart = inputs.lastIndexOf(reEmpty, deckStart); + if (deckStart == -1) { + deckStart = 0; + } } - bool ok = DeckListPlainText::parse(in, cardNameNormalizer, metadata, tree); + + // find sideboard position, if marks are used this won't be needed + int sBStart = -1; + if (inputs.indexOf(reSBMark, deckStart) == -1) { + sBStart = inputs.indexOf(reSBComment, deckStart); + if (sBStart == -1) { + sBStart = inputs.indexOf(reEmpty, deckStart + 1); + if (sBStart == -1) { + sBStart = max_line; + } + auto nextCard = inputs.indexOf(reCardLine, sBStart + 1); + if (inputs.indexOf(reEmpty, nextCard + 1) != -1) { + sBStart = max_line; + } + } + } + + int index = 0; + QRegularExpressionMatch match; + + // Parse name and comments + while (index < deckStart) { + const auto ¤t = inputs.at(index++); + if (!current.contains(reEmpty)) { + match = reComment.match(current); + metadata.name = match.captured(); + break; + } + } + while (index < deckStart) { + const auto ¤t = inputs.at(index++); + if (!current.contains(reEmpty)) { + match = reComment.match(current); + metadata.comments += match.captured() + '\n'; + } + } + metadata.comments.chop(1); + + // Discard empty lines + while (index < max_line && inputs.at(index).contains(reEmpty)) { + ++index; + } + + // Discard line if it starts with deck or mainboard, all cards until the sideboard starts are in the mainboard + if (inputs.at(index).contains(reDeckComment)) { + ++index; + } + + // Parse decklist + for (; index < max_line; ++index) { + // check if line is a card + match = reCardLine.match(inputs.at(index)); + if (!match.hasMatch()) { + continue; + } + + QString cardName = match.captured().simplified(); + bool sideboard = false; + + // Sideboard detection + if (sBStart < 0) { + match = reSBMark.match(cardName); + if (match.hasMatch()) { + sideboard = true; + cardName = match.captured(1); + } + } else { + if (index == sBStart) { + continue; + } + sideboard = index > sBStart; + } + + // Extract set code, collector number, and foil + QString setCode; + QString collectorNumber; + bool isFoil = false; + + // Check for foil status at the end of the card name + if (cardName.endsWith("*F*", Qt::CaseInsensitive)) { + isFoil = true; + cardName.chop(3); // Remove the "*F*" from the card name + } + Q_UNUSED(isFoil); + + // Attempt to match the hyphen-separated format (PLST-2094) + match = reHyphenFormat.match(cardName); + if (match.hasMatch()) { + setCode = match.captured(2).toUpper(); + collectorNumber = match.captured(3); + cardName = cardName.left(match.capturedStart()).trimmed(); + } else { + // Attempt to match the regular format (PLST) 2094 + match = reRegularFormat.match(cardName); + if (match.hasMatch()) { + setCode = match.captured(1).toUpper(); + collectorNumber = match.captured(2); + cardName = cardName.left(match.capturedStart()).trimmed(); + } + } + + // check if a specific amount is mentioned + int amount = 1; + match = reMultiplier.match(cardName); + if (match.hasMatch()) { + amount = match.captured(1).toInt(); + cardName = match.captured(2); + } + + // Normalize the card name + cardName = cardNameNormalizer(cardName); + + // Determine the zone (mainboard/sideboard) + QString zoneName = sideboard ? DECK_ZONE_SIDE : DECK_ZONE_MAIN; + + // make new entry in decklist + tree.addCard(cardName, amount, zoneName, -1, setCode, collectorNumber); + } + refreshDeckHash(); - return ok; + return true; } bool DeckList::loadFromFile_Plain(QIODevice *device, const std::function &cardNameNormalizer) @@ -316,10 +373,6 @@ bool DeckList::loadFromFile_Plain(QIODevice *device, const std::functiongetName() == DECK_ZONE_MAYBEBOARD) { - return; - } if (prefixSideboardCards && node->getName() == DECK_ZONE_SIDE) { stream << "SB: "; } diff --git a/libcockatrice_deck_list/libcockatrice/deck_list/deck_list.h b/libcockatrice_deck_list/libcockatrice/deck_list/deck_list.h index 199d3a9a8..e792c85dc 100644 --- a/libcockatrice_deck_list/libcockatrice/deck_list/deck_list.h +++ b/libcockatrice_deck_list/libcockatrice/deck_list/deck_list.h @@ -18,7 +18,7 @@ #include #include #include -#include +#include class AbstractDecklistNode; class DecklistCardNode; @@ -70,29 +70,11 @@ public: CardRef bannerCard; ///< Optional representative card for the deck. QStringList tags; ///< User-defined tags for deck classification. QString lastLoadedTimestamp; ///< Timestamp string of last load. - PlaymatInfo playmat; ///< Optional playmat background for table+stack zones. /** * @brief Checks if all values (except for lastLoadedTimestamp) in the metadata is empty. */ bool isEmpty() const; - - /** - * @brief Reads a single deck metadata element from a Cockatrice deck XML stream. - * - * @param xml Reader positioned at the element. - * @param childName Name of the current element. - * @return true if a metadata element was consumed, false if @p childName is - * not a metadata element. - */ - bool readElement(QXmlStreamReader *xml, const QString &childName); - - /** - * @brief Writes the deck metadata section of a Cockatrice deck XML file. - * - * @param xml Writer to append the metadata elements to. - */ - void write(QXmlStreamWriter *xml) const; }; private: @@ -106,22 +88,6 @@ private: */ mutable QString cachedDeckHash; - /** @name XML load helpers */ - ///@{ - /** - * @brief Advances to the next element in the XML stream. - * @param xml Reader to advance past non-element tokens. - * @return true when a start element was reached, false at end of stream. - */ - bool seekToNextElement(QXmlStreamReader *xml); - - /** - * @brief Reads the contents of a `cockatrice_deck` element into this deck. - * @param xml Reader positioned at the deck element, stopped at its end. - */ - void readDeckBody(QXmlStreamReader *xml); - ///@} - public: /** @name Metadata setters */ ///@{ @@ -149,10 +115,6 @@ public: { metadata.bannerCard = _bannerCard; } - void setPlaymat(const PlaymatInfo &_playmat = {}) - { - metadata.playmat = _playmat; - } void setLastLoadedTimestamp(const QString &_lastLoadedTimestamp = QString()) { metadata.lastLoadedTimestamp = _lastLoadedTimestamp; @@ -208,10 +170,6 @@ public: { return metadata.bannerCard; } - PlaymatInfo getPlaymat() const - { - return metadata.playmat; - } QString getLastLoadedTimestamp() const { return metadata.lastLoadedTimestamp; diff --git a/libcockatrice_deck_list/libcockatrice/deck_list/deck_list_history_manager.cpp b/libcockatrice_deck_list/libcockatrice/deck_list/deck_list_history_manager.cpp index b6d0687b4..acf4707ab 100644 --- a/libcockatrice_deck_list/libcockatrice/deck_list/deck_list_history_manager.cpp +++ b/libcockatrice_deck_list/libcockatrice/deck_list/deck_list_history_manager.cpp @@ -14,32 +14,42 @@ void DeckListHistoryManager::clear() emit undoRedoStateChanged(); } -void DeckListHistoryManager::restoreAndSwap(QStack &source, - QStack &target, - DeckList *deck) +void DeckListHistoryManager::undo(DeckList *deck) { - if (source.isEmpty()) { + if (undoStack.isEmpty()) { return; } - // The reason is read before the source is popped. - const QString reason = source.top().getReason(); + // Peek at the memento we are going to restore + const DeckListMemento &mementoToRestore = undoStack.top(); - // Save the current state so the opposite direction can return to it. - target.push(deck->createMemento(reason)); + // Save current state for redo + DeckListMemento currentState = deck->createMemento(mementoToRestore.getReason()); + redoStack.push(currentState); - // Apply the state we are moving to. - deck->restoreMemento(source.pop()); + // Pop the last state from undo stack and restore it + DeckListMemento memento = undoStack.pop(); + deck->restoreMemento(memento); emit undoRedoStateChanged(); } -void DeckListHistoryManager::undo(DeckList *deck) -{ - restoreAndSwap(undoStack, redoStack, deck); -} - void DeckListHistoryManager::redo(DeckList *deck) { - restoreAndSwap(redoStack, undoStack, deck); + if (redoStack.isEmpty()) { + return; + } + + // Peek at the memento we are going to restore + const DeckListMemento &mementoToRestore = redoStack.top(); + + // Save current state for undo + DeckListMemento currentState = deck->createMemento(mementoToRestore.getReason()); + undoStack.push(currentState); + + // Pop the next state from redo stack and restore it + DeckListMemento memento = redoStack.pop(); + deck->restoreMemento(memento); + + emit undoRedoStateChanged(); } diff --git a/libcockatrice_deck_list/libcockatrice/deck_list/deck_list_history_manager.h b/libcockatrice_deck_list/libcockatrice/deck_list/deck_list_history_manager.h index 6acdb199e..e6bd27e2d 100644 --- a/libcockatrice_deck_list/libcockatrice/deck_list/deck_list_history_manager.h +++ b/libcockatrice_deck_list/libcockatrice/deck_list/deck_list_history_manager.h @@ -47,13 +47,6 @@ public: } private: - /** - * @brief Moves one state from @p source to @p target, applying it to @p deck. - * - * Used by both undo (undoStack -> redoStack) and redo (redoStack -> undoStack). - */ - void restoreAndSwap(QStack &source, QStack &target, DeckList *deck); - QStack undoStack; QStack redoStack; }; diff --git a/libcockatrice_deck_list/libcockatrice/deck_list/deck_list_node_tree.cpp b/libcockatrice_deck_list/libcockatrice/deck_list/deck_list_node_tree.cpp index 66f228d19..196416cde 100644 --- a/libcockatrice_deck_list/libcockatrice/deck_list/deck_list_node_tree.cpp +++ b/libcockatrice_deck_list/libcockatrice/deck_list/deck_list_node_tree.cpp @@ -5,86 +5,6 @@ #include #include -static constexpr int MAX_DECK_SIZE = 1e5; - -namespace -{ - -/** - * @brief Expands card nodes into one lowercase name entry per copy. - * - * @param nodes The card nodes to expand. - * @param prefix Optional prefix prepended to every entry (e.g. "SB:" for the sideboard). - * @return One entry per copy, in node order. - */ -QStringList cardNodesToCopies(const QList &nodes, const QString &prefix = {}) -{ - QStringList result; - for (auto node : nodes) { - for (int i = 0; i < node->getNumber(); ++i) { - result.append(prefix + node->getName().toLower()); - } - } - return result; -} - -/** - * @brief Packs the first five bytes of a SHA-1 digest into a compact base-32 number. - * - * The bytes are placed in most-significant-byte-first order (byte 0 shifted by 32, - * byte 4 unshifted) so decks that only differ in their low-order hash bytes still - * produce distinct identifiers. - * - * @return The 8-character base-32 representation of the packed number. - */ -QString encodeDeckHash(const QByteArray &digest) -{ - quint64 number = 0; - for (int i = 0; i < 5; ++i) { - number |= static_cast(static_cast(digest[i])) << (32 - 8 * i); - } - return QString::number(number, 32).rightJustified(8, '0'); -} - -/** - * @brief Collects every card node in @p node's subtree, in tree order. - * - * @return The collected card nodes. - */ -QList collectCardsRecursive(const InnerDecklistNode *node) -{ - QList result; - for (int i = 0; i < node->size(); i++) { - if (auto *card = dynamic_cast(node->at(i))) { - result.append(card); - } else if (auto *inner = dynamic_cast(node->at(i))) { - result.append(collectCardsRecursive(inner)); - } - } - return result; -} - -/** - * @brief Invokes @p func on every card in @p node's subtree. - * - * Cards nested in custom zones are reported with their top-level @p boardZone - * so that callers can classify cards by board (main/side/maybeboard/tokens). - */ -void forEachCardInNode(InnerDecklistNode *boardZone, - InnerDecklistNode *node, - const std::function &func) -{ - for (int i = 0; i < node->size(); i++) { - if (auto *card = dynamic_cast(node->at(i))) { - func(boardZone, card); - } else if (auto *inner = dynamic_cast(node->at(i))) { - forEachCardInNode(boardZone, inner, func); - } - } -} - -} // namespace - DecklistNodeTree::DecklistNodeTree() : root(new InnerDecklistNode()) { } @@ -122,7 +42,12 @@ QList DecklistNodeTree::getCardNodes(const QSet result; for (auto *zoneNode : getZoneNodes(restrictToZones)) { - result.append(collectCardsRecursive(zoneNode)); + for (auto *cardNode : *zoneNode) { + auto *cardCardNode = dynamic_cast(cardNode); + if (cardCardNode) { + result.append(cardCardNode); + } + } } return result; @@ -150,11 +75,25 @@ QString DecklistNodeTree::computeDeckHash() const auto mainDeckNodes = getCardNodes({DECK_ZONE_MAIN}); auto sideDeckNodes = getCardNodes({DECK_ZONE_SIDE}); - QStringList cardList = cardNodesToCopies(mainDeckNodes) + cardNodesToCopies(sideDeckNodes, "SB:"); + static auto nodesToCardList = [](const QList &nodes, const QString &prefix = {}) { + QStringList result; + for (auto node : nodes) { + for (int i = 0; i < node->getNumber(); ++i) { + result.append(prefix + node->getName().toLower()); + } + } + return result; + }; + + QStringList cardList = nodesToCardList(mainDeckNodes) + nodesToCardList(sideDeckNodes, "SB:"); cardList.sort(); QByteArray deckHashArray = QCryptographicHash::hash(cardList.join(";").toUtf8(), QCryptographicHash::Sha1); - return encodeDeckHash(deckHashArray); + quint64 number = (((quint64)(unsigned char)deckHashArray[0]) << 32) + + (((quint64)(unsigned char)deckHashArray[1]) << 24) + + (((quint64)(unsigned char)deckHashArray[2] << 16)) + + (((quint64)(unsigned char)deckHashArray[3]) << 8) + (quint64)(unsigned char)deckHashArray[4]; + return QString::number(number, 32).rightJustified(8, '0'); } void DecklistNodeTree::write(QXmlStreamWriter *xml) const @@ -168,7 +107,7 @@ void DecklistNodeTree::readZoneElement(QXmlStreamReader *xml) { QString zoneName = xml->attributes().value("name").toString(); InnerDecklistNode *newZone = getZoneObjFromName(zoneName); - totalCards += newZone->readElement(xml, MAX_DECK_SIZE - totalCards); + newZone->readElement(xml); } DecklistCardNode *DecklistNodeTree::addCard(const QString &cardName, @@ -180,8 +119,6 @@ DecklistCardNode *DecklistNodeTree::addCard(const QString &cardName, const QString &cardProviderId, const bool formatLegal) { - amount = qMin(amount, MAX_DECK_SIZE - totalCards); - totalCards += amount; auto *zoneNode = getZoneObjFromName(zoneName); auto *node = new DecklistCardNode(cardName, amount, zoneNode, position, cardSetName, cardSetCollectorNumber, cardProviderId, formatLegal); @@ -201,7 +138,11 @@ bool DecklistNodeTree::deleteNode(AbstractDecklistNode *node, InnerDecklistNode int index = rootNode->indexOf(node); if (index != -1) { delete rootNode->takeAt(index); - pruneEmptyBoardZone(rootNode); + + if (rootNode->empty()) { + deleteNode(rootNode, rootNode->getParent()); + } + return true; } @@ -217,175 +158,30 @@ bool DecklistNodeTree::deleteNode(AbstractDecklistNode *node, InnerDecklistNode return false; } -void DecklistNodeTree::pruneEmptyBoardZone(InnerDecklistNode *container) -{ - if (container->isEmpty() && container->getParent() == root) { - deleteNode(container, container->getParent()); - } -} - void DecklistNodeTree::forEachCard(const std::function &func) const { + // Support for this is only possible if the internal structure + // doesn't get more complicated. for (int i = 0; i < root->size(); i++) { - if (auto *zone = dynamic_cast(root->at(i))) { - forEachCardInNode(zone, zone, func); + InnerDecklistNode *node = dynamic_cast(root->at(i)); + for (int j = 0; j < node->size(); j++) { + DecklistCardNode *card = dynamic_cast(node->at(j)); + func(node, card); } } } /** * Gets the InnerDecklistNode that is the root node for the given zone, creating a new node if it doesn't exist. - * - * Top-level zones take precedence, then deck-unique custom zones nested under boards - * are resolved. Unknown names create a new top-level zone (legacy behavior). */ -InnerDecklistNode *DecklistNodeTree::getZoneObjFromName(const QString &zoneName) +InnerDecklistNode *DecklistNodeTree::getZoneObjFromName(const QString &zoneName) const { for (int i = 0; i < root->size(); i++) { auto *node = dynamic_cast(root->at(i)); - if (node && node->getName() == zoneName) { + if (node->getName() == zoneName) { return node; } } - if (auto *customZone = findCustomZoneByName(zoneName)) { - return customZone; - } - return new InnerDecklistNode(zoneName, root); } - -InnerDecklistNode *DecklistNodeTree::findBoardZone(const QString &boardZoneName) const -{ - return dynamic_cast(root->findChild(boardZoneName)); -} - -InnerDecklistNode *DecklistNodeTree::findOrCreateBoardZone(const QString &boardZoneName) -{ - auto *boardZone = findBoardZone(boardZoneName); - if (!boardZone && - (boardZoneName == DECK_ZONE_MAYBEBOARD || boardZoneName == DECK_ZONE_MAIN || boardZoneName == DECK_ZONE_SIDE)) { - // The boards are lazy zones: they only exist once cards or custom zones need them. - boardZone = new InnerDecklistNode(boardZoneName, root); - } - return boardZone; -} - -InnerDecklistNode *DecklistNodeTree::addCustomZone(const QString &boardZoneName, const QString &zoneName) -{ - if (hasZoneName(zoneName)) { - return nullptr; - } - - auto *boardZone = findOrCreateBoardZone(boardZoneName); - - if (!boardZone) { - return nullptr; - } - - return new InnerDecklistNode(zoneName, boardZone); -} - -bool DecklistNodeTree::renameCustomZone(const QString &oldZoneName, const QString &newZoneName) -{ - if (hasZoneName(newZoneName)) { - return false; - } - - auto *zone = findCustomZoneByName(oldZoneName); - if (!zone) { - return false; - } - - zone->setName(newZoneName); - return true; -} - -bool DecklistNodeTree::moveCustomZone(const QString &zoneName, const QString &newBoardZoneName) -{ - auto *zone = findCustomZoneByName(zoneName); - if (!zone) { - return false; - } - - auto *currentBoardZone = zone->getParent(); - if (currentBoardZone && currentBoardZone->getName() == newBoardZoneName) { - return true; - } - - auto *newBoardZone = findOrCreateBoardZone(newBoardZoneName); - if (!newBoardZone) { - return false; - } - - currentBoardZone->removeOne(zone); - newBoardZone->append(zone); - zone->setParent(newBoardZone); - return true; -} - -bool DecklistNodeTree::removeCustomZone(const QString &zoneName) -{ - auto *zone = findCustomZoneByName(zoneName); - if (!zone) { - return false; - } - - // Detach and delete without pruning the board zone. - auto *boardZone = zone->getParent(); - boardZone->removeOne(zone); - delete zone; - return true; -} - -QList DecklistNodeTree::getCustomZones(const QString &boardZoneName) const -{ - QList result; - - auto *boardZone = findBoardZone(boardZoneName); - if (!boardZone) { - return result; - } - - for (int i = 0; i < boardZone->size(); i++) { - if (auto *customZone = dynamic_cast(boardZone->at(i))) { - result.append(customZone); - } - } - - return result; -} - -InnerDecklistNode *DecklistNodeTree::findCustomZoneByName(const QString &zoneName) const -{ - for (int i = 0; i < root->size(); i++) { - auto *boardZone = dynamic_cast(root->at(i)); - if (!boardZone) { - continue; - } - - for (int j = 0; j < boardZone->size(); j++) { - auto *customZone = dynamic_cast(boardZone->at(j)); - if (customZone && customZone->getName() == zoneName) { - return customZone; - } - } - } - - return nullptr; -} - -bool DecklistNodeTree::hasZoneName(const QString &zoneName) const -{ - // The standard zones are reserved names even before they are created lazily. - if (zoneName == DECK_ZONE_MAIN || zoneName == DECK_ZONE_SIDE || zoneName == DECK_ZONE_MAYBEBOARD || - zoneName == DECK_ZONE_TOKENS) { - return true; - } - - if (root->findChild(zoneName)) { - return true; - } - - return findCustomZoneByName(zoneName) != nullptr; -} diff --git a/libcockatrice_deck_list/libcockatrice/deck_list/deck_list_node_tree.h b/libcockatrice_deck_list/libcockatrice/deck_list/deck_list_node_tree.h index 2c66b34ff..8ef0b18a5 100644 --- a/libcockatrice_deck_list/libcockatrice/deck_list/deck_list_node_tree.h +++ b/libcockatrice_deck_list/libcockatrice/deck_list/deck_list_node_tree.h @@ -10,7 +10,6 @@ class DecklistNodeTree { InnerDecklistNode *root; ///< Root of the deck tree (zones + cards). - int totalCards = 0; public: /** @brief Constructs an empty DecklistNodeTree. */ @@ -79,82 +78,15 @@ public: bool deleteNode(AbstractDecklistNode *node, InnerDecklistNode *rootNode = nullptr); /** - * @brief Creates a new custom zone nested under a board zone. + * @brief Apply a function to every card in the deck tree. This can modify the cards. * - * Custom zone names must be unique across the whole deck so that cards can be - * added to a custom zone without specifying its board zone. - * - * @param boardZoneName Name of the board zone (e.g. DECK_ZONE_MAIN). - * @param zoneName Name of the custom zone. - * @return The created zone node, or nullptr if the name is already in use. - */ - InnerDecklistNode *addCustomZone(const QString &boardZoneName, const QString &zoneName); - - /** - * @brief Renames a custom zone. - * @return true on success, false if the zone was not found or the new name is taken. - */ - bool renameCustomZone(const QString &oldZoneName, const QString &newZoneName); - - /** - * @brief Moves a custom zone (and all its cards) to another board zone. - * @return true on success, false if the zone or the new board zone was not found. - */ - bool moveCustomZone(const QString &zoneName, const QString &newBoardZoneName); - - /** - * @brief Removes a custom zone and all its cards. - * @return true if the zone was found and removed. - */ - bool removeCustomZone(const QString &zoneName); - - /** - * @brief Gets all custom zones nested under a board zone. - * @param boardZoneName Name of the board zone. - * @return The custom zones, in insertion order. - */ - QList getCustomZones(const QString &boardZoneName) const; - - /** - * @brief Checks whether a zone name is taken anywhere in the deck. - * - * Covers the standard board names and any top-level or nested custom zone. - * @param zoneName The checked name. - * @return true if the name is reserved or already in use. - */ - bool hasZoneName(const QString &zoneName) const; - - /** - * @brief Finds a custom zone anywhere in the deck by name. - * - * Walks the children of every top-level zone, so a zone nested under any - * board (and not just the standard ones) is found. - * @param zoneName The zone name to find. - * @return The matching zone node, or nullptr if none exists. - */ - InnerDecklistNode *findCustomZoneByName(const QString &zoneName) const; - - /** - * @brief Applies a function to every card in the deck tree. This can modify the cards. - * - * @param func Function taking (top-level board zone node, card node). Cards nested - * in custom zones are reported with their board zone. + * @param func Function taking (zone node, card node). */ void forEachCard(const std::function &func) const; private: // Helpers for traversing the tree - InnerDecklistNode *getZoneObjFromName(const QString &zoneName); - InnerDecklistNode *findBoardZone(const QString &boardZoneName) const; - InnerDecklistNode *findOrCreateBoardZone(const QString &boardZoneName); - - /** - * @brief Recursively removes @p container when it is an empty board zone. - * - * Empty custom zones are kept while empty board zones get pruned, so a - * board zone disappears once its last card or custom zone goes away. - */ - void pruneEmptyBoardZone(InnerDecklistNode *container); + InnerDecklistNode *getZoneObjFromName(const QString &zoneName) const; }; #endif // COCKATRICE_DECKLIST_NODE_TREE_H diff --git a/libcockatrice_deck_list/libcockatrice/deck_list/deck_list_plain_text_parser.cpp b/libcockatrice_deck_list/libcockatrice/deck_list/deck_list_plain_text_parser.cpp deleted file mode 100644 index de50d743b..000000000 --- a/libcockatrice_deck_list/libcockatrice/deck_list/deck_list_plain_text_parser.cpp +++ /dev/null @@ -1,172 +0,0 @@ -#include "deck_list_plain_text_parser.h" - -#include "deck_list_node_tree.h" -#include "tree/inner_deck_list_node.h" - -#include -#include - -namespace DeckListPlainText -{ - -bool parse(QTextStream &in, - const std::function &cardNameNormalizer, - DeckList::Metadata &metadata, - DecklistNodeTree &tree) -{ - tree.clear(); - - static const QRegularExpression reCardLine(R"(^\s*[\w\[\(\{].*$)", QRegularExpression::UseUnicodePropertiesOption); - static const QRegularExpression reEmpty("^\\s*$"); - static const QRegularExpression reComment(R"([\w\[\(\{].*$)", QRegularExpression::UseUnicodePropertiesOption); - static const QRegularExpression reSBMark("^\\s*sb:\\s*(.+)", QRegularExpression::CaseInsensitiveOption); - static const QRegularExpression reSBComment("^sideboard\\b.*$", QRegularExpression::CaseInsensitiveOption); - static const QRegularExpression reDeckComment("^((main)?deck(list)?|mainboard)\\b", - QRegularExpression::CaseInsensitiveOption); - - // Regex for advanced card parsing - static const QRegularExpression reMultiplier(R"(^[xX\(\[]*(\d+)[xX\*\)\]]* ?(.+))"); - - // Regex for extracting set code and collector number with attached symbols - static const QRegularExpression reHyphenFormat(R"(\((\w{3,})\)\s+(\w{3,})-(\d+[^\w\s]*))"); - static const QRegularExpression reRegularFormat(R"(\((\w{3,})\)\s+(\d+[^\w\s]*))"); - - auto inputs = in.readAll().trimmed().split('\n'); - auto max_line = inputs.size(); - - // Start at the first empty line before the first card line - auto deckStart = inputs.indexOf(reCardLine); - if (deckStart == -1) { - if (inputs.indexOf(reComment) == -1) { - return false; // Input is empty - } - deckStart = max_line; - } else { - deckStart = inputs.lastIndexOf(reEmpty, deckStart); - if (deckStart == -1) { - deckStart = 0; - } - } - - // find sideboard position, if marks are used this won't be needed - int sBStart = -1; - if (inputs.indexOf(reSBMark, deckStart) == -1) { - sBStart = inputs.indexOf(reSBComment, deckStart); - if (sBStart == -1) { - sBStart = inputs.indexOf(reEmpty, deckStart + 1); - if (sBStart == -1) { - sBStart = max_line; - } - auto nextCard = inputs.indexOf(reCardLine, sBStart + 1); - if (inputs.indexOf(reEmpty, nextCard + 1) != -1) { - sBStart = max_line; - } - } - } - - int index = 0; - QRegularExpressionMatch match; - - // Parse name and comments - while (index < deckStart) { - const auto ¤t = inputs.at(index++); - if (!current.contains(reEmpty)) { - match = reComment.match(current); - metadata.name = match.captured(); - break; - } - } - while (index < deckStart) { - const auto ¤t = inputs.at(index++); - if (!current.contains(reEmpty)) { - match = reComment.match(current); - metadata.comments += match.captured() + '\n'; - } - } - metadata.comments.chop(1); - - // Discard empty lines - while (index < max_line && inputs.at(index).contains(reEmpty)) { - ++index; - } - - // Discard line if it starts with deck or mainboard, all cards until the sideboard starts are in the mainboard - if (inputs.at(index).contains(reDeckComment)) { - ++index; - } - - // Parse decklist - for (; index < max_line; ++index) { - // check if line is a card - match = reCardLine.match(inputs.at(index)); - if (!match.hasMatch()) { - continue; - } - - QString cardName = match.captured().simplified(); - bool sideboard = false; - - // Sideboard detection - if (sBStart < 0) { - match = reSBMark.match(cardName); - if (match.hasMatch()) { - sideboard = true; - cardName = match.captured(1); - } - } else { - if (index == sBStart) { - continue; - } - sideboard = index > sBStart; - } - - // Extract set code, collector number, and foil - QString setCode; - QString collectorNumber; - bool isFoil = false; - - // Check for foil status at the end of the card name - if (cardName.endsWith("*F*", Qt::CaseInsensitive)) { - isFoil = true; - cardName.chop(3); // Remove the "*F*" from the card name - } - Q_UNUSED(isFoil); - - // Attempt to match the hyphen-separated format (PLST-2094) - match = reHyphenFormat.match(cardName); - if (match.hasMatch()) { - setCode = match.captured(2).toUpper(); - collectorNumber = match.captured(3); - cardName = cardName.left(match.capturedStart()).trimmed(); - } else { - // Attempt to match the regular format (PLST) 2094 - match = reRegularFormat.match(cardName); - if (match.hasMatch()) { - setCode = match.captured(1).toUpper(); - collectorNumber = match.captured(2); - cardName = cardName.left(match.capturedStart()).trimmed(); - } - } - - // check if a specific amount is mentioned - int amount = 1; - match = reMultiplier.match(cardName); - if (match.hasMatch()) { - amount = match.captured(1).toInt(); - cardName = match.captured(2); - } - - // Normalize the card name - cardName = cardNameNormalizer(cardName); - - // Determine the zone (mainboard/sideboard) - QString zoneName = sideboard ? DECK_ZONE_SIDE : DECK_ZONE_MAIN; - - // make new entry in decklist - tree.addCard(cardName, amount, zoneName, -1, setCode, collectorNumber); - } - - return true; -} - -} // namespace DeckListPlainText \ No newline at end of file diff --git a/libcockatrice_deck_list/libcockatrice/deck_list/deck_list_plain_text_parser.h b/libcockatrice_deck_list/libcockatrice/deck_list/deck_list_plain_text_parser.h deleted file mode 100644 index e0f456a18..000000000 --- a/libcockatrice_deck_list/libcockatrice/deck_list/deck_list_plain_text_parser.h +++ /dev/null @@ -1,33 +0,0 @@ -#ifndef COCKATRICE_DECK_LIST_PLAIN_TEXT_PARSER_H -#define COCKATRICE_DECK_LIST_PLAIN_TEXT_PARSER_H - -#include "deck_list.h" - -#include -#include - -class QTextStream; - -namespace DeckListPlainText -{ - -/** - * @brief Parses a plain-text deck list into a tree and its metadata. - * - * Clears the tree first, then fills both from the text. - * - * @param in The text to load - * @param cardNameNormalizer Function that takes the parsed card name string - * in the text and returns the name to store - * @param metadata Deck metadata written by the parser - * @param tree Deck tree the parser adds cards to - * @return False if the input was empty, true otherwise. - */ -bool parse(QTextStream &in, - const std::function &cardNameNormalizer, - DeckList::Metadata &metadata, - DecklistNodeTree &tree); - -} // namespace DeckListPlainText - -#endif // COCKATRICE_DECK_LIST_PLAIN_TEXT_PARSER_H \ No newline at end of file diff --git a/libcockatrice_deck_list/libcockatrice/deck_list/playmat_resolver.cpp b/libcockatrice_deck_list/libcockatrice/deck_list/playmat_resolver.cpp deleted file mode 100644 index fd72fea0b..000000000 --- a/libcockatrice_deck_list/libcockatrice/deck_list/playmat_resolver.cpp +++ /dev/null @@ -1,54 +0,0 @@ -#include "playmat_resolver.h" - -#include - -PlaymatInfo resolveEffectivePlaymat(const DeckList &deck, - const PlaymatInfo &force, - const QList &fallbackList, - PlaymatFallbackMode fallbackMode, - int rotationIndex) -{ - if (!force.card.isEmpty()) { - return force; - } - - const PlaymatInfo &deckPlaymat = deck.getPlaymat(); - if (!deckPlaymat.card.isEmpty()) { - return deckPlaymat; - } - - if (fallbackList.isEmpty()) { - return {}; - } - - switch (fallbackMode) { - case PlaymatFallbackModeFixed: - return fallbackList.first(); - case PlaymatFallbackModeRoundRobin: - return fallbackList.at(rotationIndex % fallbackList.size()); - case PlaymatFallbackModeRandom: - return fallbackList.at(QRandomGenerator::global()->bounded(fallbackList.size())); - } - - return {}; -} - -PlaymatInfo resolvePlaymatForDeck(const DeckList &deck, - const QList &fallbackList, - PlaymatMode mode, - PlaymatFallbackMode fallbackBehavior, - int rotationIndex) -{ - switch (mode) { - case PlaymatModeOverrideDeck: { - const DeckList emptyDeck; - return resolveEffectivePlaymat(emptyDeck, {}, fallbackList, fallbackBehavior, rotationIndex); - } - case PlaymatModeFallback: - return resolveEffectivePlaymat(deck, {}, fallbackList, fallbackBehavior, rotationIndex); - case PlaymatModeDeckOnly: - return deck.getPlaymat(); - } - - return {}; -} diff --git a/libcockatrice_deck_list/libcockatrice/deck_list/playmat_resolver.h b/libcockatrice_deck_list/libcockatrice/deck_list/playmat_resolver.h deleted file mode 100644 index 59bc449c8..000000000 --- a/libcockatrice_deck_list/libcockatrice/deck_list/playmat_resolver.h +++ /dev/null @@ -1,47 +0,0 @@ -#ifndef COCKATRICE_PLAYMAT_RESOLVER_H -#define COCKATRICE_PLAYMAT_RESOLVER_H - -#include "deck_list.h" - -#include - -/** - * @brief Resolves the effective playmat for a deck per the resolution chain: - * force override > deck-configured playmat > fallback list > none. - * - * @param deck The deck to resolve a playmat for. - * @param force An optional user-level override; wins over everything. Pass an - * empty @ref PlaymatInfo::card to skip it. - * @param fallbackList User-level fallback playmats, consulted only when the - * deck has no configured playmat. - * @param fallbackMode How @p fallbackList is consulted (ignored when empty). - * @param rotationIndex In/out cursor for @c PlaymatFallbackModeRoundRobin; - * advanced once per call. Unused for the other modes. - * @return The effective playmat; an empty @ref PlaymatInfo::card when - * nothing in the chain resolves. - */ -PlaymatInfo resolveEffectivePlaymat(const DeckList &deck, - const PlaymatInfo &force, - const QList &fallbackList, - PlaymatFallbackMode fallbackMode, - int rotationIndex); - -/** - * @brief Resolves the playmat to display for a deck according to the user's - * collection mode (@ref PlaymatMode), combining the deck with the - * given fallback list. - * - * @param deck The deck to resolve a playmat for. - * @param fallbackList User-level fallback playmats. - * @param mode How the collection interacts with the deck-configured playmat. - * @param fallbackBehavior How @p fallbackList is picked from. - * @param rotationIndex Cursor for @c PlaymatFallbackModeRoundRobin. - * @return The effective playmat; an empty @ref PlaymatInfo::card when nothing resolves. - */ -PlaymatInfo resolvePlaymatForDeck(const DeckList &deck, - const QList &fallbackList, - PlaymatMode mode, - PlaymatFallbackMode fallbackBehavior, - int rotationIndex); - -#endif // COCKATRICE_PLAYMAT_RESOLVER_H diff --git a/libcockatrice_deck_list/libcockatrice/deck_list/sideboard_plan.cpp b/libcockatrice_deck_list/libcockatrice/deck_list/sideboard_plan.cpp index 855062c18..a76fed619 100644 --- a/libcockatrice_deck_list/libcockatrice/deck_list/sideboard_plan.cpp +++ b/libcockatrice_deck_list/libcockatrice/deck_list/sideboard_plan.cpp @@ -2,32 +2,6 @@ #include -namespace -{ - -void readMoveCardToZone(QXmlStreamReader *xml, QList &moveList) -{ - MoveCard_ToZone move; - while (!xml->atEnd()) { - xml->readNext(); - const QString childName = xml->name().toString(); - if (xml->isStartElement()) { - if (childName == "card_name") { - move.set_card_name(xml->readElementText().toStdString()); - } else if (childName == "start_zone") { - move.set_start_zone(xml->readElementText().toStdString()); - } else if (childName == "target_zone") { - move.set_target_zone(xml->readElementText().toStdString()); - } - } else if (xml->isEndElement() && (childName == "move_card_to_zone")) { - moveList.append(move); - return; - } - } -} - -} // namespace - SideboardPlan::SideboardPlan(const QString &_name, const QList &_moveList) : name(_name), moveList(_moveList) { @@ -47,7 +21,23 @@ bool SideboardPlan::readElement(QXmlStreamReader *xml) if (childName == "name") { name = xml->readElementText(); } else if (childName == "move_card_to_zone") { - readMoveCardToZone(xml, moveList); + MoveCard_ToZone m; + while (!xml->atEnd()) { + xml->readNext(); + const QString childName2 = xml->name().toString(); + if (xml->isStartElement()) { + if (childName2 == "card_name") { + m.set_card_name(xml->readElementText().toStdString()); + } else if (childName2 == "start_zone") { + m.set_start_zone(xml->readElementText().toStdString()); + } else if (childName2 == "target_zone") { + m.set_target_zone(xml->readElementText().toStdString()); + } + } else if (xml->isEndElement() && (childName2 == "move_card_to_zone")) { + moveList.append(m); + break; + } + } } } else if (xml->isEndElement() && (childName == "sideboard_plan")) { return true; diff --git a/libcockatrice_deck_list/libcockatrice/deck_list/tree/abstract_deck_list_card_node.cpp b/libcockatrice_deck_list/libcockatrice/deck_list/tree/abstract_deck_list_card_node.cpp index 685238d53..705dfae4c 100644 --- a/libcockatrice_deck_list/libcockatrice/deck_list/tree/abstract_deck_list_card_node.cpp +++ b/libcockatrice_deck_list/libcockatrice/deck_list/tree/abstract_deck_list_card_node.cpp @@ -34,6 +34,17 @@ bool AbstractDecklistCardNode::compareName(AbstractDecklistNode *other) const } } +bool AbstractDecklistCardNode::readElement(QXmlStreamReader *xml) +{ + while (!xml->atEnd()) { + xml->readNext(); + if (xml->isEndElement() && xml->name().toString() == "card") { + return false; + } + } + return true; +} + void AbstractDecklistCardNode::writeElement(QXmlStreamWriter *xml) { xml->writeEmptyElement("card"); @@ -49,4 +60,4 @@ void AbstractDecklistCardNode::writeElement(QXmlStreamWriter *xml) if (!getCardProviderId().isEmpty()) { xml->writeAttribute("uuid", getCardProviderId()); } -} +} \ No newline at end of file diff --git a/libcockatrice_deck_list/libcockatrice/deck_list/tree/abstract_deck_list_card_node.h b/libcockatrice_deck_list/libcockatrice/deck_list/tree/abstract_deck_list_card_node.h index 0942a4601..df903a168 100644 --- a/libcockatrice_deck_list/libcockatrice/deck_list/tree/abstract_deck_list_card_node.h +++ b/libcockatrice_deck_list/libcockatrice/deck_list/tree/abstract_deck_list_card_node.h @@ -134,6 +134,15 @@ public: */ bool compareName(AbstractDecklistNode *other) const; + /** + * @brief Deserialize this node’s properties from XML. + * @param xml QXmlStreamReader positioned at the element. + * @return true if parsing succeeded. + * + * This supports loading deck files from Cockatrice’s XML format. + */ + bool readElement(QXmlStreamReader *xml) override; + /** * @brief Serialize this node’s properties to XML. * @param xml Writer to append this node’s XML element. diff --git a/libcockatrice_deck_list/libcockatrice/deck_list/tree/abstract_deck_list_node.h b/libcockatrice_deck_list/libcockatrice/deck_list/tree/abstract_deck_list_node.h index 38d1d44b8..a39f0e7b2 100644 --- a/libcockatrice_deck_list/libcockatrice/deck_list/tree/abstract_deck_list_node.h +++ b/libcockatrice_deck_list/libcockatrice/deck_list/tree/abstract_deck_list_node.h @@ -142,12 +142,6 @@ public: return parent; } - /** @param newParent Reparent this node. The new parent takes ownership. */ - void setParent(InnerDecklistNode *newParent) - { - parent = newParent; - } - /** * @brief Compute the depth of this node in the tree. * @return Distance from the root (root = 0, children = 1, etc.). @@ -179,10 +173,11 @@ public: /** * @name XML serialization - * This method supports writing this node and its children to the + * These methods support reading and writing decks from/to * Cockatrice deck XML format. * @{ */ + virtual bool readElement(QXmlStreamReader *xml) = 0; virtual void writeElement(QXmlStreamWriter *xml) = 0; /// @} }; diff --git a/libcockatrice_deck_list/libcockatrice/deck_list/tree/inner_deck_list_node.cpp b/libcockatrice_deck_list/libcockatrice/deck_list/tree/inner_deck_list_node.cpp index 86f7f5363..602ea6aec 100644 --- a/libcockatrice_deck_list/libcockatrice/deck_list/tree/inner_deck_list_node.cpp +++ b/libcockatrice_deck_list/libcockatrice/deck_list/tree/inner_deck_list_node.cpp @@ -28,8 +28,6 @@ QString InnerDecklistNode::visibleNameFromName(const QString &_name) return QObject::tr("Sideboard"); } else if (_name == DECK_ZONE_TOKENS) { return QObject::tr("Tokens"); - } else if (_name == DECK_ZONE_MAYBEBOARD) { - return QObject::tr("Maybeboard"); } else { return _name; } @@ -43,13 +41,6 @@ void InnerDecklistNode::setSortMethod(DeckSortMethod method) } } -const QList &InnerDecklistNode::boardZoneNames() -{ - static const QList names = {QString(DECK_ZONE_MAIN), QString(DECK_ZONE_SIDE), - QString(DECK_ZONE_MAYBEBOARD)}; - return names; -} - QString InnerDecklistNode::getVisibleName() const { return visibleNameFromName(name); @@ -94,9 +85,6 @@ AbstractDecklistNode *InnerDecklistNode::findCardChildByNameProviderIdAndNumber( int InnerDecklistNode::height() const { - if (isEmpty()) { - return 1; - } return at(0)->height() + 1; } @@ -151,35 +139,27 @@ bool InnerDecklistNode::compareName(AbstractDecklistNode *other) const } } -int InnerDecklistNode::readCardElement(QXmlStreamReader *xml, int remainingBudget) +bool InnerDecklistNode::readElement(QXmlStreamReader *xml) { - const int amount = qMin(xml->attributes().value("number").toString().toInt(), remainingBudget); - new DecklistCardNode(xml->attributes().value("name").toString(), amount, this, -1, - xml->attributes().value("setShortName").toString(), - xml->attributes().value("collectorNumber").toString(), - xml->attributes().value("uuid").toString()); - return amount; -} - -int InnerDecklistNode::readElement(QXmlStreamReader *xml, int limit) -{ - int totalCards = 0; while (!xml->atEnd()) { xml->readNext(); const QString childName = xml->name().toString(); - const int remainingBudget = limit - totalCards; if (xml->isStartElement()) { if (childName == "zone") { auto *newZone = new InnerDecklistNode(xml->attributes().value("name").toString(), this); - totalCards += newZone->readElement(xml, remainingBudget); + newZone->readElement(xml); } else if (childName == "card") { - totalCards += readCardElement(xml, remainingBudget); + auto *newCard = new DecklistCardNode( + xml->attributes().value("name").toString(), xml->attributes().value("number").toString().toInt(), + this, -1, xml->attributes().value("setShortName").toString(), + xml->attributes().value("collectorNumber").toString(), xml->attributes().value("uuid").toString()); + newCard->readElement(xml); } } else if (xml->isEndElement() && (childName == "zone")) { - return totalCards; + return false; } } - return totalCards; + return true; } void InnerDecklistNode::writeElement(QXmlStreamWriter *xml) @@ -192,35 +172,31 @@ void InnerDecklistNode::writeElement(QXmlStreamWriter *xml) xml->writeEndElement(); // zone } -QVector> InnerDecklistNode::indexedSnapshot() const -{ - QVector> snapshot(size()); - for (int i = size() - 1; i >= 0; --i) { - snapshot[i].first = i; - snapshot[i].second = at(i); - } - return snapshot; -} - -QVector> InnerDecklistNode::applySortedOrder(const QVector> &sorted) -{ - QVector> result(size()); - for (int i = size() - 1; i >= 0; --i) { - result[i].first = sorted[i].first; - result[i].second = i; - replace(i, sorted[i].second); - } - return result; -} - QVector> InnerDecklistNode::sort(Qt::SortOrder order) { - auto snapshot = indexedSnapshot(); + QVector> result(size()); + // Initialize temporary list with contents of current list + QVector> tempList(size()); + for (int i = size() - 1; i >= 0; --i) { + tempList[i].first = i; + tempList[i].second = at(i); + } + + // Sort temporary list auto cmp = [order](const auto &a, const auto &b) { return (order == Qt::AscendingOrder) ? (b.second->compare(a.second)) : (a.second->compare(b.second)); }; - std::sort(snapshot.begin(), snapshot.end(), cmp); - return applySortedOrder(snapshot); -} + std::sort(tempList.begin(), tempList.end(), cmp); + + // Map old indexes to new indexes and + // copy temporary list to the current one + for (int i = size() - 1; i >= 0; --i) { + result[i].first = tempList[i].first; + result[i].second = i; + replace(i, tempList[i].second); + } + + return result; +} \ No newline at end of file diff --git a/libcockatrice_deck_list/libcockatrice/deck_list/tree/inner_deck_list_node.h b/libcockatrice_deck_list/libcockatrice/deck_list/tree/inner_deck_list_node.h index 8404d7116..81c37dffa 100644 --- a/libcockatrice_deck_list/libcockatrice/deck_list/tree/inner_deck_list_node.h +++ b/libcockatrice_deck_list/libcockatrice/deck_list/tree/inner_deck_list_node.h @@ -18,17 +18,12 @@ #include "abstract_deck_list_node.h" -#include -#include - /** @brief Constant for the "main" deck zone name. */ #define DECK_ZONE_MAIN "main" /** @brief Constant for the "sideboard" zone name. */ #define DECK_ZONE_SIDE "side" /** @brief Constant for the "tokens" zone name. */ #define DECK_ZONE_TOKENS "tokens" -/** @brief Constant for the "maybeboard" zone name. */ -#define DECK_ZONE_MAYBEBOARD "maybeboard" /** * @class InnerDecklistNode @@ -121,13 +116,6 @@ public: */ static QString visibleNameFromName(const QString &_name); - /** - * @brief The standard board zone names, in display order. - * - * @return main, side and maybeboard. - */ - static const QList &boardZoneNames(); - /** * @brief Get this node’s display-friendly name. * @return Human-readable name (zone/group name). @@ -223,45 +211,18 @@ public: */ QVector> sort(Qt::SortOrder order = Qt::AscendingOrder); -private: - /** - * @brief Snapshots the current children as (old index, node) pairs. - */ - QVector> indexedSnapshot() const; - - /** - * @brief Replaces this node's children with @p sorted and maps old indexes to new ones. - * - * @return A list of (old index, new index) pairs for each reordered child. - */ - QVector> applySortedOrder(const QVector> &sorted); - -public: /** * @brief Deserialize this node and its children from XML. * @param xml Reader positioned at this element. - * @param limit The maximum amount of cards to read - * @return the amount of cards found + * @return true if parsing succeeded. */ - int readElement(QXmlStreamReader *xml, int limit); + bool readElement(QXmlStreamReader *xml) override; /** * @brief Serialize this node and its children to XML. * @param xml Writer to append elements to. */ void writeElement(QXmlStreamWriter *xml) override; - -private: - /** - * @brief Reads a single `card` element and appends it to this node. - * - * The card's quantity is capped at @p remainingBudget so a malicious or - * oversized deck file cannot push the total card count past the deck size - * limit. - * - * @return The amount of cards actually added. - */ - int readCardElement(QXmlStreamReader *xml, int remainingBudget); }; #endif // COCKATRICE_INNER_DECK_LIST_NODE_H diff --git a/libcockatrice_filters/libcockatrice/filters/filter_string.cpp b/libcockatrice_filters/libcockatrice/filters/filter_string.cpp index df0c70ae7..25e8e97db 100644 --- a/libcockatrice_filters/libcockatrice/filters/filter_string.cpp +++ b/libcockatrice_filters/libcockatrice/filters/filter_string.cpp @@ -5,7 +5,6 @@ #include #include #include -#include #include static peg::parser search(R"( @@ -20,8 +19,7 @@ SomewhatComplexQueryPart <- [(] QueryPartList [)] / QueryPart QueryPart <- NotQuery / SetQuery / RarityQuery / CMCQuery / FormatQuery / PowerQuery / ToughnessQuery / ColorQuery / TypeQuery / OracleQuery / FieldQuery / GenericQuery NotQuery <- ('NOT' ws/'-') SomewhatComplexQueryPart -SetQuery <- ('e'/'set') SetQueryValue -SetQueryValue <- ([=:] FlexStringValue) / (<[!][=]?> FlexStringValue) / SetExpression +SetQuery <- ('e'/'set') [:] FlexStringValue OracleQuery <- 'o' [:] MatcherString @@ -66,8 +64,6 @@ RegexMatcherString <- ('\\/' / !'/' .)+ FlexStringValue <- CompactStringSet / String / [(] StringList [)] CompactStringSet <- StringListString ([,+] StringListString)+ -SetExpression <- NumericOperator ws? String - NumericExpression <- NumericOperator ws? NumericValue NumericOperator <- [=:] / <[> NumericValue <- [0-9]+ @@ -75,36 +71,6 @@ NumericValue <- [0-9]+ static std::once_flag init; -// The peglib parser rules (and therefore their rule actions) are set up once per -// process, so a rule action cannot capture per-instance state. The card language -// plain-text name and text queries search in is therefore handed to the GenericQuery -// and OracleQuery rule actions through this thread-local context, which is live only -// while a FilterString is being parsed. The rule actions copy it into the filter -// closures they produce, so card evaluation never reads process-global state. -thread_local CardSearchLanguage searchLanguageContext; - -namespace -{ -bool matchesInSearchLanguage(const QString &english, - const QString &localized, - const CardSearchLanguage &searchLanguage, - const StringMatcher &matcher) -{ - if (searchLanguage.mode == SearchLanguageMode::English) { - return matcher(english); - } - - if (searchLanguage.mode == SearchLanguageMode::Both) { - if (!searchLanguage.isEnglishOnly() && matcher(localized)) { - return true; - } - return matcher(english); - } - - return searchLanguage.isEnglishOnly() ? matcher(english) : matcher(localized); -} -} // namespace - static void setupParserRules() { auto passthru = [](const peg::SemanticValues &sv) -> Filter { @@ -134,35 +100,13 @@ static void setupParserRules() const auto matcher = std::any_cast(sv[0]); return [=](const CardData &x) -> bool { return matcher(x->getCardType()); }; }; - search["SetQueryValue"] = [](const peg::SemanticValues &sv) -> Filter { - if (sv.choice() == 0) { - auto matcher = std::any_cast(sv[0]); - return [=](const CardData &x) -> bool { - QList sets = x->getSets().keys(); - - auto matchesSet = [&matcher](const QString &set) { return matcher(set); }; - return std::any_of(sets.begin(), sets.end(), matchesSet); - }; - } - if (sv.choice() == 1) { - auto matcher = std::any_cast(sv[0]); - return [=](const CardData &x) -> bool { - QList sets = x->getSets().keys(); - - auto matchesSet = [&matcher](const QString &set) { return matcher(set); }; - return std::none_of(sets.begin(), sets.end(), matchesSet); - }; - } - - auto matcher = std::any_cast(sv[0]); + search["SetQuery"] = [](const peg::SemanticValues &sv) -> Filter { + auto matcher = std::any_cast(sv[0]); return [=](const CardData &x) -> bool { - const auto &sets = x->getSets().values(); - auto matchesSet = [&](const PrintingInfo &printing) { - return printing.getSet()->getEnabled() && matcher(printing.getSet()->getReleaseDate().toJulianDay()); - }; - return std::any_of(sets.begin(), sets.end(), [&](const auto &printings) { - return std::any_of(printings.begin(), printings.end(), matchesSet); - }); + QList sets = x->getSets().keys(); + + auto matchesSet = [&matcher](const QString &set) { return matcher(set); }; + return std::any_of(sets.begin(), sets.end(), matchesSet); }; }; search["Rarity"] = [](const peg::SemanticValues &sv) -> QString { @@ -303,54 +247,40 @@ static void setupParserRules() return QString::fromStdString(std::string(sv.sv())); }; - search["NumericOperator"] = [](const peg::SemanticValues &sv) -> NumberComparer { - const auto op = QString::fromStdString(std::string(sv.sv())); + search["NumericExpression"] = [](const peg::SemanticValues &sv) -> NumberMatcher { + const auto arg = std::any_cast(sv[1]); + const auto op = std::any_cast(sv[0]); if (op == ">") { - return [=](const int s, const int arg) { return s > arg; }; + return [=](const int s) { return s > arg; }; } if (op == ">=") { - return [=](const int s, const int arg) { return s >= arg; }; + return [=](const int s) { return s >= arg; }; } if (op == "<") { - return [=](const int s, const int arg) { return s < arg; }; + return [=](const int s) { return s < arg; }; } if (op == "<=") { - return [=](const int s, const int arg) { return s <= arg; }; + return [=](const int s) { return s <= arg; }; } if (op == "=") { - return [=](const int s, const int arg) { return s == arg; }; + return [=](const int s) { return s == arg; }; } if (op == ":") { - return [=](const int s, const int arg) { return s == arg; }; + return [=](const int s) { return s == arg; }; } if (op == "!=") { - return [=](const int s, const int arg) { return s != arg; }; + return [=](const int s) { return s != arg; }; } - return [](int, int) { return false; }; + return [](int) { return false; }; }; search["NumericValue"] = [](const peg::SemanticValues &sv) -> int { return QString::fromStdString(std::string(sv.sv())).toInt(); }; - search["NumericExpression"] = [](const peg::SemanticValues &sv) -> NumberMatcher { - const auto comparer = std::any_cast(sv[0]); - const auto arg = std::any_cast(sv[1]); - return [=](int s) { return comparer(s, arg); }; - }; - - search["SetExpression"] = [](const peg::SemanticValues &sv) -> NumberMatcher { - const auto comparer = std::any_cast(sv[0]); - const auto setCode = std::any_cast(sv[1]); - const auto allSets = CardDatabaseManager::getInstance()->getSetList(); - for (auto &set : allSets) { - if (set->getShortName() == setCode) { - const int releaseDate = set->getReleaseDate().toJulianDay(); - return [=](int s) { return comparer(s, releaseDate); }; - } - } - return [](int) { return false; }; + search["NumericOperator"] = [](const peg::SemanticValues &sv) -> QString { + return QString::fromStdString(std::string(sv.sv())); }; search["NormalMatcher"] = [](const peg::SemanticValues &sv) -> StringMatcher { @@ -373,11 +303,7 @@ static void setupParserRules() search["OracleQuery"] = [](const peg::SemanticValues &sv) -> Filter { const auto matcher = std::any_cast(sv[0]); - const CardSearchLanguage searchLanguage = searchLanguageContext; - return [=](const CardData &x) { - return matchesInSearchLanguage(x->getText(), x->getLocalizedText(searchLanguage.language), searchLanguage, - matcher); - }; + return [=](const CardData &x) { return matcher(x->getText()); }; }; search["ColorQuery"] = [](const peg::SemanticValues &sv) -> Filter { @@ -454,11 +380,7 @@ static void setupParserRules() }; search["GenericQuery"] = [](const peg::SemanticValues &sv) -> Filter { const auto matcher = std::any_cast(sv[0]); - const CardSearchLanguage searchLanguage = searchLanguageContext; - return [=](const CardData &x) { - return matchesInSearchLanguage(x->getName(), x->getLocalizedName(searchLanguage.language), searchLanguage, - matcher); - }; + return [=](const CardData &x) { return matcher(x->getName()); }; }; search["Color"] = [](const peg::SemanticValues &sv) -> char { return "WUBRGU"[sv.choice()]; }; @@ -473,7 +395,7 @@ FilterString::FilterString() _error = "Not initialized"; } -FilterString::FilterString(const QString &expr, const CardSearchLanguage &searchLanguage) +FilterString::FilterString(const QString &expr) { QByteArray ba = expr.simplified().toUtf8(); @@ -486,8 +408,6 @@ FilterString::FilterString(const QString &expr, const CardSearchLanguage &search return; } - searchLanguageContext = searchLanguage; - search.set_logger([&](size_t /*ln*/, size_t col, const std::string &msg) { _error = QString("Error at position %1: %2").arg(col).arg(QString::fromStdString(msg)); }); diff --git a/libcockatrice_filters/libcockatrice/filters/filter_string.h b/libcockatrice_filters/libcockatrice/filters/filter_string.h index 015df0cf4..71a99f7b5 100644 --- a/libcockatrice_filters/libcockatrice/filters/filter_string.h +++ b/libcockatrice_filters/libcockatrice/filters/filter_string.h @@ -14,7 +14,6 @@ #include #include #include -#include #include inline Q_LOGGING_CATEGORY(FilterStringLog, "filter_string"); @@ -23,7 +22,6 @@ typedef CardInfoPtr CardData; typedef std::function Filter; typedef std::function StringMatcher; typedef std::function NumberMatcher; -typedef std::function NumberComparer; namespace peg { @@ -36,7 +34,7 @@ class FilterString { public: FilterString(); - explicit FilterString(const QString &exp, const CardSearchLanguage &searchLanguage = {}); + explicit FilterString(const QString &exp); [[nodiscard]] bool check(const CardData &card) const { if (card.isNull()) { diff --git a/libcockatrice_filters/libcockatrice/filters/filter_tree.cpp b/libcockatrice_filters/libcockatrice/filters/filter_tree.cpp index a5d91d9d3..8502db50b 100644 --- a/libcockatrice_filters/libcockatrice/filters/filter_tree.cpp +++ b/libcockatrice_filters/libcockatrice/filters/filter_tree.cpp @@ -100,16 +100,14 @@ FilterTreeNode *FilterItemList::termNode(const QString &term) return childNodes.at(i); } -bool FilterItemList::testTypeAnd(const CardInfoPtr info, - CardFilter::Attr attr, - const CardSearchLanguage &searchLanguage) const +bool FilterItemList::testTypeAnd(const CardInfoPtr info, CardFilter::Attr attr) const { for (auto i = childNodes.constBegin(); i != childNodes.constEnd(); i++) { if (!(*i)->isEnabled()) { continue; } - if (!(*i)->acceptCardAttr(info, attr, searchLanguage)) { + if (!(*i)->acceptCardAttr(info, attr)) { return false; } } @@ -117,17 +115,13 @@ bool FilterItemList::testTypeAnd(const CardInfoPtr info, return true; } -bool FilterItemList::testTypeAndNot(const CardInfoPtr info, - CardFilter::Attr attr, - const CardSearchLanguage &searchLanguage) const +bool FilterItemList::testTypeAndNot(const CardInfoPtr info, CardFilter::Attr attr) const { // if any one in the list is true, return false - return !testTypeOr(info, attr, searchLanguage); + return !testTypeOr(info, attr); } -bool FilterItemList::testTypeOr(const CardInfoPtr info, - CardFilter::Attr attr, - const CardSearchLanguage &searchLanguage) const +bool FilterItemList::testTypeOr(const CardInfoPtr info, CardFilter::Attr attr) const { bool noChildEnabledChild = true; @@ -140,7 +134,7 @@ bool FilterItemList::testTypeOr(const CardInfoPtr info, noChildEnabledChild = false; } - if ((*i)->acceptCardAttr(info, attr, searchLanguage)) { + if ((*i)->acceptCardAttr(info, attr)) { return true; } } @@ -148,58 +142,20 @@ bool FilterItemList::testTypeOr(const CardInfoPtr info, return noChildEnabledChild; } -bool FilterItemList::testTypeOrNot(const CardInfoPtr info, - CardFilter::Attr attr, - const CardSearchLanguage &searchLanguage) const +bool FilterItemList::testTypeOrNot(const CardInfoPtr info, CardFilter::Attr attr) const { // if any one in the list is false, return true - return !testTypeAnd(info, attr, searchLanguage); + return !testTypeAnd(info, attr); } -bool FilterItem::acceptName(const CardInfoPtr info, const CardSearchLanguage &searchLanguage) const +bool FilterItem::acceptName(const CardInfoPtr info) const { - const QString &englishName = info->getName(); - const QString &localizedName = info->getLocalizedName(searchLanguage.language); - - switch (searchLanguage.mode) { - case SearchLanguageMode::English: - return englishName.contains(term, Qt::CaseInsensitive); - case SearchLanguageMode::Both: - if (englishName.contains(term, Qt::CaseInsensitive)) { - return true; - } - return !searchLanguage.isEnglishOnly() && localizedName.contains(term, Qt::CaseInsensitive); - case SearchLanguageMode::Selected: - if (searchLanguage.isEnglishOnly()) { - return englishName.contains(term, Qt::CaseInsensitive); - } - return localizedName.contains(term, Qt::CaseInsensitive); - } - - return false; + return info->getName().contains(term, Qt::CaseInsensitive); } -bool FilterItem::acceptNameExact(const CardInfoPtr info, const CardSearchLanguage &searchLanguage) const +bool FilterItem::acceptNameExact(const CardInfoPtr info) const { - const QString &englishName = info->getName(); - const QString &localizedName = info->getLocalizedName(searchLanguage.language); - - switch (searchLanguage.mode) { - case SearchLanguageMode::English: - return englishName == term; - case SearchLanguageMode::Both: - if (englishName == term) { - return true; - } - return !searchLanguage.isEnglishOnly() && localizedName == term; - case SearchLanguageMode::Selected: - if (searchLanguage.isEnglishOnly()) { - return englishName == term; - } - return localizedName == term; - } - - return false; + return info->getName() == term; } bool FilterItem::acceptType(const CardInfoPtr info) const @@ -257,27 +213,9 @@ bool FilterItem::acceptColor(const CardInfoPtr info) const return match_count == converted_term.length(); } -bool FilterItem::acceptText(const CardInfoPtr info, const CardSearchLanguage &searchLanguage) const +bool FilterItem::acceptText(const CardInfoPtr info) const { - const QString &englishText = info->getText(); - const QString &localizedText = info->getLocalizedText(searchLanguage.language); - - switch (searchLanguage.mode) { - case SearchLanguageMode::English: - return englishText.contains(term, Qt::CaseInsensitive); - case SearchLanguageMode::Both: - if (englishText.contains(term, Qt::CaseInsensitive)) { - return true; - } - return !searchLanguage.isEnglishOnly() && localizedText.contains(term, Qt::CaseInsensitive); - case SearchLanguageMode::Selected: - if (searchLanguage.isEnglishOnly()) { - return englishText.contains(term, Qt::CaseInsensitive); - } - return localizedText.contains(term, Qt::CaseInsensitive); - } - - return false; + return info->getText().contains(term, Qt::CaseInsensitive); } bool FilterItem::acceptSet(const CardInfoPtr info) const @@ -464,21 +402,19 @@ bool FilterItem::relationCheck(int cardInfo) const return result; } -bool FilterItem::acceptCardAttr(const CardInfoPtr info, - CardFilter::Attr attr, - const CardSearchLanguage &searchLanguage) const +bool FilterItem::acceptCardAttr(const CardInfoPtr info, CardFilter::Attr attr) const { switch (attr) { case CardFilter::AttrName: - return acceptName(info, searchLanguage); + return acceptName(info); case CardFilter::AttrNameExact: - return acceptNameExact(info, searchLanguage); + return acceptNameExact(info); case CardFilter::AttrType: return acceptType(info); case CardFilter::AttrColor: return acceptColor(info); case CardFilter::AttrText: - return acceptText(info, searchLanguage); + return acceptText(info); case CardFilter::AttrSet: return acceptSet(info); case CardFilter::AttrManaCost: @@ -548,18 +484,18 @@ FilterTreeNode *FilterTree::termNode(const CardFilter *f) return termNode(f->attr(), f->type(), f->term()); } -bool FilterTree::testAttr(const CardInfoPtr info, const LogicMap *lm, const CardSearchLanguage &searchLanguage) const +bool FilterTree::testAttr(const CardInfoPtr info, const LogicMap *lm) const { const FilterItemList *fil; bool status = true; fil = lm->findTypeList(CardFilter::TypeAnd); - if (fil && fil->isEnabled() && !fil->testTypeAnd(info, lm->attr, searchLanguage)) { + if (fil && fil->isEnabled() && !fil->testTypeAnd(info, lm->attr)) { return false; } fil = lm->findTypeList(CardFilter::TypeAndNot); - if (fil && fil->isEnabled() && !fil->testTypeAndNot(info, lm->attr, searchLanguage)) { + if (fil && fil->isEnabled() && !fil->testTypeAndNot(info, lm->attr)) { return false; } @@ -568,23 +504,23 @@ bool FilterTree::testAttr(const CardInfoPtr info, const LogicMap *lm, const Card status = false; // if this is true we can return because it is OR'd with the OrNot list - if (fil->testTypeOr(info, lm->attr, searchLanguage)) { + if (fil->testTypeOr(info, lm->attr)) { return true; } } fil = lm->findTypeList(CardFilter::TypeOrNot); - if (fil && fil->isEnabled() && fil->testTypeOrNot(info, lm->attr, searchLanguage)) { + if (fil && fil->isEnabled() && fil->testTypeOrNot(info, lm->attr)) { return true; } return status; } -bool FilterTree::acceptsCard(const CardInfoPtr info, const CardSearchLanguage &searchLanguage) const +bool FilterTree::acceptsCard(const CardInfoPtr info) const { for (auto i = childNodes.constBegin(); i != childNodes.constEnd(); i++) { - if ((*i)->isEnabled() && !testAttr(info, *i, searchLanguage)) { + if ((*i)->isEnabled() && !testAttr(info, *i)) { return false; } } diff --git a/libcockatrice_filters/libcockatrice/filters/filter_tree.h b/libcockatrice_filters/libcockatrice/filters/filter_tree.h index dd47a1ebc..aac1777e0 100644 --- a/libcockatrice_filters/libcockatrice/filters/filter_tree.h +++ b/libcockatrice_filters/libcockatrice/filters/filter_tree.h @@ -11,7 +11,6 @@ #include #include -#include #include #include @@ -168,14 +167,10 @@ public: return CardFilter::typeName(type); } - [[nodiscard]] bool - testTypeAnd(CardInfoPtr info, CardFilter::Attr attr, const CardSearchLanguage &searchLanguage) const; - [[nodiscard]] bool - testTypeAndNot(CardInfoPtr info, CardFilter::Attr attr, const CardSearchLanguage &searchLanguage) const; - [[nodiscard]] bool - testTypeOr(CardInfoPtr info, CardFilter::Attr attr, const CardSearchLanguage &searchLanguage) const; - [[nodiscard]] bool - testTypeOrNot(CardInfoPtr info, CardFilter::Attr attr, const CardSearchLanguage &searchLanguage) const; + [[nodiscard]] bool testTypeAnd(CardInfoPtr info, CardFilter::Attr attr) const; + [[nodiscard]] bool testTypeAndNot(CardInfoPtr info, CardFilter::Attr attr) const; + [[nodiscard]] bool testTypeOr(CardInfoPtr info, CardFilter::Attr attr) const; + [[nodiscard]] bool testTypeOrNot(CardInfoPtr info, CardFilter::Attr attr) const; }; class FilterItem : public FilterTreeNode @@ -212,21 +207,20 @@ public: return true; } - [[nodiscard]] bool acceptName(CardInfoPtr info, const CardSearchLanguage &searchLanguage) const; - [[nodiscard]] bool acceptNameExact(CardInfoPtr info, const CardSearchLanguage &searchLanguage) const; + [[nodiscard]] bool acceptName(CardInfoPtr info) const; + [[nodiscard]] bool acceptNameExact(CardInfoPtr info) const; [[nodiscard]] bool acceptType(CardInfoPtr info) const; [[nodiscard]] bool acceptMainType(CardInfoPtr info) const; [[nodiscard]] bool acceptSubType(CardInfoPtr info) const; [[nodiscard]] bool acceptColor(CardInfoPtr info) const; - [[nodiscard]] bool acceptText(CardInfoPtr info, const CardSearchLanguage &searchLanguage) const; + [[nodiscard]] bool acceptText(CardInfoPtr info) const; [[nodiscard]] bool acceptSet(CardInfoPtr info) const; [[nodiscard]] bool acceptManaCost(CardInfoPtr info) const; [[nodiscard]] bool acceptCmc(CardInfoPtr info) const; [[nodiscard]] bool acceptPowerToughness(CardInfoPtr info, CardFilter::Attr attr) const; [[nodiscard]] bool acceptLoyalty(CardInfoPtr info) const; [[nodiscard]] bool acceptRarity(CardInfoPtr info) const; - [[nodiscard]] bool - acceptCardAttr(CardInfoPtr info, CardFilter::Attr attr, const CardSearchLanguage &searchLanguage) const; + [[nodiscard]] bool acceptCardAttr(CardInfoPtr info, CardFilter::Attr attr) const; [[nodiscard]] bool acceptFormat(CardInfoPtr info) const; [[nodiscard]] bool relationCheck(int cardInfo) const; }; @@ -246,7 +240,7 @@ private: LogicMap *attrLogicMap(CardFilter::Attr attr); FilterItemList *attrTypeList(CardFilter::Attr attr, CardFilter::Type type); - bool testAttr(CardInfoPtr info, const LogicMap *lm, const CardSearchLanguage &searchLanguage) const; + bool testAttr(CardInfoPtr info, const LogicMap *lm) const; void nodeChanged() const override { @@ -285,7 +279,7 @@ public: return 0; } - [[nodiscard]] bool acceptsCard(CardInfoPtr info, const CardSearchLanguage &searchLanguage) const; + [[nodiscard]] bool acceptsCard(CardInfoPtr info) const; void removeFiltersByAttr(CardFilter::Attr filterType); void removeFilter(const CardFilter *toRemove); void clear(); diff --git a/libcockatrice_interfaces/CMakeLists.txt b/libcockatrice_interfaces/CMakeLists.txt index f53c41807..f606f6207 100644 --- a/libcockatrice_interfaces/CMakeLists.txt +++ b/libcockatrice_interfaces/CMakeLists.txt @@ -30,4 +30,4 @@ add_library(libcockatrice_interfaces STATIC ${MOC_SOURCES}) target_include_directories(libcockatrice_interfaces PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) -target_link_libraries(libcockatrice_interfaces PUBLIC libcockatrice_utility ${QT_CORE_MODULE}) +target_link_libraries(libcockatrice_interfaces PUBLIC ${QT_CORE_MODULE}) diff --git a/libcockatrice_interfaces/libcockatrice/interfaces/interface_cards_display_settings_provider.h b/libcockatrice_interfaces/libcockatrice/interfaces/interface_cards_display_settings_provider.h index 900f51f2b..3ee3d2aef 100644 --- a/libcockatrice_interfaces/libcockatrice/interfaces/interface_cards_display_settings_provider.h +++ b/libcockatrice_interfaces/libcockatrice/interfaces/interface_cards_display_settings_provider.h @@ -1,8 +1,6 @@ #ifndef COCKATRICE_INTERFACE_CARDS_DISPLAY_SETTINGS_PROVIDER_H #define COCKATRICE_INTERFACE_CARDS_DISPLAY_SETTINGS_PROVIDER_H -#include - class ICardsDisplaySettingsProvider { public: @@ -17,7 +15,6 @@ public: [[nodiscard]] virtual bool getIncludeRebalancedCards() const = 0; [[nodiscard]] virtual bool getPrintingSelectorNavigationButtonsVisible() const = 0; [[nodiscard]] virtual bool getTapAnimation() const = 0; - [[nodiscard]] virtual bool getArrowDrawAnimation() const = 0; [[nodiscard]] virtual bool getAutoRotateSidewaysLayoutCards() const = 0; [[nodiscard]] virtual bool getScaleCards() const = 0; [[nodiscard]] virtual int getStackCardOverlapPercent() const = 0; @@ -28,8 +25,6 @@ public: [[nodiscard]] virtual int getEDHRecCardSize() const = 0; [[nodiscard]] virtual int getArchidektPreviewSize() const = 0; [[nodiscard]] virtual int getSampleHandSize() const = 0; - [[nodiscard]] virtual QString getCardLang() const = 0; - [[nodiscard]] virtual int getCardSearchLanguage() const = 0; }; #endif // COCKATRICE_INTERFACE_CARDS_DISPLAY_SETTINGS_PROVIDER_H diff --git a/libcockatrice_interfaces/libcockatrice/interfaces/interface_chat_settings_provider.h b/libcockatrice_interfaces/libcockatrice/interfaces/interface_chat_settings_provider.h index cdf2da5eb..cd9ad29e1 100644 --- a/libcockatrice_interfaces/libcockatrice/interfaces/interface_chat_settings_provider.h +++ b/libcockatrice_interfaces/libcockatrice/interfaces/interface_chat_settings_provider.h @@ -20,7 +20,6 @@ public: [[nodiscard]] virtual bool getShowMessagePopup() const = 0; [[nodiscard]] virtual bool getShowMentionPopup() const = 0; [[nodiscard]] virtual bool getRoomHistory() const = 0; - [[nodiscard]] virtual bool getIgnoreAllPrivateMessages() const = 0; [[nodiscard]] virtual QString getHighlightWords() const = 0; }; diff --git a/libcockatrice_interfaces/libcockatrice/interfaces/interface_interface_settings_provider.h b/libcockatrice_interfaces/libcockatrice/interfaces/interface_interface_settings_provider.h index bc2118cb3..07e60b28f 100644 --- a/libcockatrice_interfaces/libcockatrice/interfaces/interface_interface_settings_provider.h +++ b/libcockatrice_interfaces/libcockatrice/interfaces/interface_interface_settings_provider.h @@ -1,40 +1,7 @@ #ifndef COCKATRICE_INTERFACE_INTERFACE_SETTINGS_PROVIDER_H #define COCKATRICE_INTERFACE_INTERFACE_SETTINGS_PROVIDER_H -#include #include -#include -#include - -/** - * @brief Whether playmats are rendered in-game, and for whom. - */ -enum PlaymatVisibility -{ - PlaymatVisibilityNone = 0, ///< Don't use playmats. - PlaymatVisibilityOwnOnly = 1, ///< Show the local player's playmat only. - PlaymatVisibilityAll = 2 ///< Show playmats for all players. -}; - -/** - * @brief How the user-level playmat collection interacts with the deck-configured playmat. - */ -enum PlaymatMode -{ - PlaymatModeOverrideDeck = 0, ///< Always use the collection, ignoring any deck-configured playmat. - PlaymatModeFallback = 1, ///< Prefer the deck-configured playmat; fall back to the collection when absent. - PlaymatModeDeckOnly = 2 ///< Use only the deck-configured playmat, ignoring the collection. -}; - -/** - * @brief How the user-level fallback playmat list is consulted when a deck has no playmat configured. - */ -enum PlaymatFallbackMode -{ - PlaymatFallbackModeFixed = 0, ///< Always use the first entry of the fallback list. - PlaymatFallbackModeRoundRobin = 1, ///< Cycle through the list, advancing one entry per resolution. - PlaymatFallbackModeRandom = 2 ///< Pick a random entry per resolution. -}; class IInterfaceSettingsProvider { @@ -64,7 +31,6 @@ public: [[nodiscard]] virtual int getMinPlayersForMultiColumnLayout() const = 0; [[nodiscard]] virtual int getRewindBufferingMs() const = 0; [[nodiscard]] virtual qreal getFastForwardSpeed() const = 0; - [[nodiscard]] virtual bool getSkipEmptySections() const = 0; [[nodiscard]] virtual bool getLeftJustified() const = 0; [[nodiscard]] virtual int getZoneViewGroupByIndex() const = 0; [[nodiscard]] virtual int getZoneViewSortByIndex() const = 0; @@ -72,24 +38,6 @@ public: [[nodiscard]] virtual bool getShowStatusBar() const = 0; [[nodiscard]] virtual bool getShowShortcuts() const = 0; [[nodiscard]] virtual bool getShowGameSelectorFilterToolbar() const = 0; - [[nodiscard]] virtual bool getLifeCounterAnimationsEnabled() const = 0; - [[nodiscard]] virtual bool getBattlefieldFlashEnabled() const = 0; - [[nodiscard]] virtual QStringList getUserListExpandedSections() const = 0; - - /** @brief Who gets playmats rendered: @ref PlaymatVisibility. */ - [[nodiscard]] virtual int getPlaymatVisibility() const = 0; - - /** @brief User-level playmat collection. Used either as a forced playmat - * (mode == @ref PlaymatModeOverrideDeck) or as a fallback when a deck has none - * (mode == @ref PlaymatModeFallback). */ - [[nodiscard]] virtual QList getPlaymatFallbackList() const = 0; - - /** @brief How the fallback list is applied: @ref PlaymatMode. */ - [[nodiscard]] virtual int getPlaymatMode() const = 0; - - /** @brief How the fallback list is picked from when mode is @ref PlaymatModeFallback: - * @ref PlaymatFallbackMode. */ - [[nodiscard]] virtual int getPlaymatFallbackBehavior() const = 0; }; #endif // COCKATRICE_INTERFACE_INTERFACE_SETTINGS_PROVIDER_H diff --git a/libcockatrice_interfaces/libcockatrice/interfaces/interface_tabs_settings_provider.h b/libcockatrice_interfaces/libcockatrice/interfaces/interface_tabs_settings_provider.h index 054c4cd72..4403de569 100644 --- a/libcockatrice_interfaces/libcockatrice/interfaces/interface_tabs_settings_provider.h +++ b/libcockatrice_interfaces/libcockatrice/interfaces/interface_tabs_settings_provider.h @@ -1,17 +1,11 @@ #ifndef COCKATRICE_INTERFACE_TABS_SETTINGS_PROVIDER_H #define COCKATRICE_INTERFACE_TABS_SETTINGS_PROVIDER_H -#include - class ITabsSettingsProvider { public: virtual ~ITabsSettingsProvider() = default; - [[nodiscard]] virtual int getStartupTabIndex() const = 0; - [[nodiscard]] virtual QString getStartupServerHost() const = 0; - [[nodiscard]] virtual QString getStartupServerPort() const = 0; - [[nodiscard]] virtual QString getStartupRoomName() const = 0; [[nodiscard]] virtual bool getTabVisualDeckStorageOpen() const = 0; [[nodiscard]] virtual bool getTabServerOpen() const = 0; [[nodiscard]] virtual bool getTabAccountOpen() const = 0; @@ -19,9 +13,6 @@ public: [[nodiscard]] virtual bool getTabReplaysOpen() const = 0; [[nodiscard]] virtual bool getTabAdminOpen() const = 0; [[nodiscard]] virtual bool getTabLogOpen() const = 0; - [[nodiscard]] virtual bool getTabReportOpen() const = 0; - [[nodiscard]] virtual bool getTabModerationOpen() const = 0; - [[nodiscard]] virtual bool getTabCardArtRulesOpen() const = 0; }; #endif // COCKATRICE_INTERFACE_TABS_SETTINGS_PROVIDER_H diff --git a/libcockatrice_models/libcockatrice/models/database/card/card_completer_proxy_model.cpp b/libcockatrice_models/libcockatrice/models/database/card/card_completer_proxy_model.cpp index f31463b50..387eb454f 100644 --- a/libcockatrice_models/libcockatrice/models/database/card/card_completer_proxy_model.cpp +++ b/libcockatrice_models/libcockatrice/models/database/card/card_completer_proxy_model.cpp @@ -6,10 +6,6 @@ CardCompleterProxyModel::CardCompleterProxyModel(QObject *parent) : QSortFilterP bool CardCompleterProxyModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const { - if (!sourceModel()) { - return false; - } - if (filterRegularExpression().pattern().isEmpty()) { return true; } @@ -17,5 +13,6 @@ bool CardCompleterProxyModel::filterAcceptsRow(int sourceRow, const QModelIndex QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent); QString data = index.data(Qt::DisplayRole).toString(); + // Ensure substring matching return data.contains(filterRegularExpression()); } diff --git a/libcockatrice_models/libcockatrice/models/database/card/card_search_model.cpp b/libcockatrice_models/libcockatrice/models/database/card/card_search_model.cpp index 3a734ed37..d1fbbac2f 100644 --- a/libcockatrice_models/libcockatrice/models/database/card/card_search_model.cpp +++ b/libcockatrice_models/libcockatrice/models/database/card/card_search_model.cpp @@ -23,14 +23,8 @@ QVariant CardSearchModel::data(const QModelIndex &index, int role) const return QVariant(); } - const SearchResult &result = searchResults.at(index.row()); - if (role == Qt::DisplayRole) { - return result.card->getName(); - } - - if (role == CardInfoRole) { - return QVariant::fromValue(result.card); + return searchResults.at(index.row()).card->getName(); } return QVariant(); @@ -42,90 +36,43 @@ void CardSearchModel::updateSearchResults(const QString &query) searchResults.clear(); if (query.isEmpty() || !sourceModel) { - endResetModel(); return; } - CardDatabaseModel *sourceDbModel = qobject_cast(sourceModel->sourceModel()); - if (!sourceDbModel) { - endResetModel(); - return; - } + // Set the filter for the display model + sourceModel->setCardName(query); - const QString lowerQuery = query.toLower(); + // Collect matching cards and compute Levenshtein distance + for (int i = 0; i < sourceModel->rowCount(); ++i) { + QModelIndex modelIndex = sourceModel->index(i, 0); + QModelIndex sourceIndex = sourceModel->mapToSource(modelIndex); + CardDatabaseModel *sourceDbModel = qobject_cast(sourceModel->sourceModel()); - QList prefixMatches; - QList containsMatches; + if (!sourceDbModel || !sourceIndex.isValid()) { + return; + } + + CardInfoPtr card = sourceDbModel->getCard(sourceIndex.row()); - // Iterate the raw database model directly so results are always complete and fresh - const int rowCount = sourceDbModel->rowCount(); - for (int i = 0; i < rowCount; ++i) { - CardInfoPtr card = sourceDbModel->getCard(i); if (!card) { continue; } - // The completer suggestions match against the same languages the card - // search uses, so typing a localized name finds the card. In Both mode - // either language can match. - for (const QString &matchName : searchableNames(card)) { - const QString lowerName = matchName.toLower(); - if (!lowerName.contains(lowerQuery)) { - continue; - } - - const int distance = levenshteinDistance(lowerQuery, lowerName); - - if (lowerName.startsWith(lowerQuery)) { - prefixMatches.append({card, distance}); - } else { - containsMatches.append({card, distance}); - } - break; - } + int distance = levenshteinDistance(query.toLower(), card->getName().toLower()); + searchResults.append({card, distance}); } - auto sortByDistanceThenLength = [this](const SearchResult &a, const SearchResult &b) { - if (a.distance != b.distance) { - return a.distance < b.distance; - } - return sortableName(a.card).size() < sortableName(b.card).size(); - }; + // Sort by Levenshtein distance (lower distance = better match) + std::sort(searchResults.begin(), searchResults.end(), + [](const SearchResult &a, const SearchResult &b) { return a.distance < b.distance; }); - std::sort(prefixMatches.begin(), prefixMatches.end(), sortByDistanceThenLength); - std::sort(containsMatches.begin(), containsMatches.end(), sortByDistanceThenLength); - - // Prefix matches always come first, then contains-only matches - searchResults.reserve(prefixMatches.size() + containsMatches.size()); - searchResults.append(prefixMatches); - searchResults.append(containsMatches); - - // Keep only the top 10 results + // Keep only the top 5 results if (searchResults.size() > 10) { searchResults = searchResults.mid(0, 10); } + emit dataChanged(index(0, 0), index(rowCount() - 1, 0)); + emit layoutChanged(); + endResetModel(); } - -QStringList CardSearchModel::searchableNames(const CardInfoPtr &card) const -{ - if (searchLanguage.isEnglishOnly()) { - return {card->getName()}; - } - - const QString localizedName = card->getLocalizedName(searchLanguage.language); - if (searchLanguage.mode == SearchLanguageMode::Selected) { - return {localizedName}; - } - - return {card->getName(), localizedName}; -} - -QString CardSearchModel::sortableName(const CardInfoPtr &card) const -{ - if (searchLanguage.isEnglishOnly()) { - return card->getName(); - } - return card->getLocalizedName(searchLanguage.language); -} diff --git a/libcockatrice_models/libcockatrice/models/database/card/card_search_model.h b/libcockatrice_models/libcockatrice/models/database/card/card_search_model.h index b877a9385..bc4be7a0e 100644 --- a/libcockatrice_models/libcockatrice/models/database/card/card_search_model.h +++ b/libcockatrice_models/libcockatrice/models/database/card/card_search_model.h @@ -15,11 +15,6 @@ class CardSearchModel : public QAbstractListModel { Q_OBJECT public: - enum CardSearchRoles - { - CardInfoRole = Qt::UserRole + 1, - }; - explicit CardSearchModel(CardDatabaseDisplayModel *sourceModel, QObject *parent = nullptr); [[nodiscard]] int rowCount(const QModelIndex &parent = QModelIndex()) const override; @@ -27,14 +22,6 @@ public: void updateSearchResults(const QString &query); // Update results based on input - void setSearchLanguage(const CardSearchLanguage &searchLang) - { - if (searchLanguage == searchLang) { - return; - } - searchLanguage = searchLang; - } - private: struct SearchResult { @@ -42,15 +29,8 @@ private: int distance; }; - /** @brief The names a card is searched by with the current search language. */ - [[nodiscard]] QStringList searchableNames(const CardInfoPtr &card) const; - - /** @brief The name used to break distance ties when sorting suggestions. */ - [[nodiscard]] QString sortableName(const CardInfoPtr &card) const; - CardDatabaseDisplayModel *sourceModel; QList searchResults; - CardSearchLanguage searchLanguage; }; #endif // CARD_SEARCH_MODEL_H diff --git a/libcockatrice_models/libcockatrice/models/database/card_database_display_model.cpp b/libcockatrice_models/libcockatrice/models/database/card_database_display_model.cpp index 7f89677c8..724ee61f2 100644 --- a/libcockatrice_models/libcockatrice/models/database/card_database_display_model.cpp +++ b/libcockatrice_models/libcockatrice/models/database/card_database_display_model.cpp @@ -179,7 +179,7 @@ bool CardDatabaseDisplayModel::filterAcceptsRow(int sourceRow, const QModelIndex } if (filterString != nullptr) { - if (filterTree != nullptr && !filterTree->acceptsCard(info, searchLanguage)) { + if (filterTree != nullptr && !filterTree->acceptsCard(info)) { return false; } return filterString->check(info); @@ -190,14 +190,8 @@ bool CardDatabaseDisplayModel::filterAcceptsRow(int sourceRow, const QModelIndex bool CardDatabaseDisplayModel::rowMatchesCardName(CardInfoPtr info) const { - if (!cardName.isEmpty()) { - const bool matchesEnglish = info->getName().contains(cardName, Qt::CaseInsensitive); - const bool matchesLocalized = - !searchLanguage.isEnglishOnly() && - info->getLocalizedName(searchLanguage.language).contains(cardName, Qt::CaseInsensitive); - if (!matchesEnglish && !matchesLocalized) { - return false; - } + if (!cardName.isEmpty() && !info->getName().contains(cardName, Qt::CaseInsensitive)) { + return false; } if (!cardNameSet.isEmpty() && !cardNameSet.contains(info->getName())) { @@ -205,7 +199,7 @@ bool CardDatabaseDisplayModel::rowMatchesCardName(CardInfoPtr info) const } if (filterTree != nullptr) { - return filterTree->acceptsCard(info, searchLanguage); + return filterTree->acceptsCard(info); } return true; @@ -241,28 +235,6 @@ void CardDatabaseDisplayModel::setFilterTree(FilterTree *_filterTree) invalidate(); } -void CardDatabaseDisplayModel::setStringFilter(const QString &_src) -{ - searchText = _src; - delete filterString; - filterString = new FilterString(_src, searchLanguage); - dirty(); -} - -void CardDatabaseDisplayModel::setSearchLanguage(const CardSearchLanguage &searchLang) -{ - if (searchLanguage == searchLang) { - return; - } - - searchLanguage = searchLang; - - if (filterString != nullptr) { - setStringFilter(searchText); - } - dirty(); -} - void CardDatabaseDisplayModel::filterTreeChanged() { invalidate(); diff --git a/libcockatrice_models/libcockatrice/models/database/card_database_display_model.h b/libcockatrice_models/libcockatrice/models/database/card_database_display_model.h index e6aadd8c6..c3145c356 100644 --- a/libcockatrice_models/libcockatrice/models/database/card_database_display_model.h +++ b/libcockatrice_models/libcockatrice/models/database/card_database_display_model.h @@ -10,7 +10,6 @@ #include #include -#include #include class FilterTree; @@ -33,8 +32,6 @@ private: FilterString *filterString; int loadedRowCount; QTimer dirtyTimer; - CardSearchLanguage searchLanguage; - QString searchText; /** The translation table that will be used for sanitizeCardName. */ static QMap characterTranslation; @@ -58,13 +55,17 @@ public: cardName = sanitizeCardName(_cardName, characterTranslation); dirty(); } - void setStringFilter(const QString &_src); + void setStringFilter(const QString &_src) + { + delete filterString; + filterString = new FilterString(_src); + dirty(); + } void setCardNameSet(const QSet &_cardNameSet) { cardNameSet = _cardNameSet; dirty(); } - void setSearchLanguage(const CardSearchLanguage &searchLang); void dirty() { diff --git a/libcockatrice_models/libcockatrice/models/database/card_database_model.cpp b/libcockatrice_models/libcockatrice/models/database/card_database_model.cpp index ecd26a9f8..7b248a982 100644 --- a/libcockatrice_models/libcockatrice/models/database/card_database_model.cpp +++ b/libcockatrice_models/libcockatrice/models/database/card_database_model.cpp @@ -111,50 +111,19 @@ bool CardDatabaseModel::checkCardHasAtLeastOneEnabledSet(const CardInfoPtr &card void CardDatabaseModel::cardDatabaseEnabledSetsChanged() { - // Build the new card list in a single pass. - QList newCardList; - newCardList.reserve(cardList.size()); + // remove all the cards no more present in at least one enabled set for (const CardInfoPtr &card : cardList) { - if (checkCardHasAtLeastOneEnabledSet(card)) { - newCardList.append(card); + if (!checkCardHasAtLeastOneEnabledSet(card)) { + cardRemoved(card); } } + + // re-check all the card currently not shown, maybe their part of a newly-enabled set for (const CardInfoPtr &card : db->getCardList()) { - if (!cardListSet.contains(card) && checkCardHasAtLeastOneEnabledSet(card)) { - newCardList.append(card); + if (!cardListSet.contains(card)) { + cardAdded(card); } } - - if (newCardList == cardList) { - return; - } - - // Rebuild the whole list inside a single model reset instead of emitting - // per-card insert/remove notifications. With tens of thousands of cards the - // per-card path is the dominant cost of constructing a CardDatabaseModel. - QSet oldCardListSet = cardListSet; - QSet newCardListSet(newCardList.begin(), newCardList.end()); - - beginResetModel(); - - // Disconnect cards that are no longer shown. - for (const CardInfoPtr &card : cardList) { - if (!newCardListSet.contains(card)) { - disconnect(card.data(), nullptr, this, nullptr); - } - } - - cardList = newCardList; - cardListSet = newCardListSet; - - // Connect cards that are now shown for the first time. - for (const CardInfoPtr &card : cardList) { - if (!oldCardListSet.contains(card)) { - connect(card.data(), &CardInfo::cardInfoChanged, this, &CardDatabaseModel::cardInfoChanged); - } - } - - endResetModel(); } void CardDatabaseModel::cardAdded(const CardInfoPtr &card) diff --git a/libcockatrice_models/libcockatrice/models/deck_list/CMakeLists.txt b/libcockatrice_models/libcockatrice/models/deck_list/CMakeLists.txt index a6ab2a204..d4aee3686 100644 --- a/libcockatrice_models/libcockatrice/models/deck_list/CMakeLists.txt +++ b/libcockatrice_models/libcockatrice/models/deck_list/CMakeLists.txt @@ -7,8 +7,7 @@ set(HEADERS deck_list_model.h deck_list_sort_filter_proxy_model.h) qt6_wrap_cpp(MOC_SOURCES ${HEADERS}) add_library( - libcockatrice_models_deck_list STATIC ${MOC_SOURCES} deck_list_model.cpp deck_list_model_custom_zones.cpp - deck_list_sort_filter_proxy_model.cpp + libcockatrice_models_deck_list STATIC ${MOC_SOURCES} deck_list_model.cpp deck_list_sort_filter_proxy_model.cpp ) target_include_directories(libcockatrice_models_deck_list PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) diff --git a/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.cpp b/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.cpp index a5628a844..9b43281c1 100644 --- a/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.cpp +++ b/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.cpp @@ -28,16 +28,6 @@ DeckListModel::~DeckListModel() delete root; } -void DeckListModel::setDisplayLanguage(const QString &lang) -{ - if (displayLang == lang) { - return; - } - displayLang = lang; - emit layoutAboutToBeChanged(); - emit layoutChanged(); -} - /** * @brief Extract the value from the card that is used for the group criteria. * @param info Pointer to card information. @@ -76,8 +66,7 @@ void DeckListModel::rebuildTree() for (int j = 0; j < currentZone->size(); j++) { auto *currentCard = dynamic_cast(currentZone->at(j)); - // Non-card children are custom zones; they are mirrored in a single - // pass below so each is mirrored exactly once. + //! \todo Better sanity checking. if (currentCard == nullptr) { continue; } @@ -93,19 +82,8 @@ void DeckListModel::rebuildTree() new DecklistModelCardNode(currentCard, groupNode); } - - // Custom zones nested under the board zone are mirrored as-is, with their - // cards as direct children (no further grouping). - DeckListModelCustomZones::mirrorCustomZones(currentZone, node); } - // The shadow tree was built in deck file order. Apply the active sort while - // the reset is still open so every consumer (tree view and visual editor) - // sees the canonical order from the start. sortShadowTree emits no signals, - // which is only valid before endResetModel closes the reset. - root->setSortMethod(lastKnownColumn == 0 ? DeckSortMethod::ByNumber : DeckSortMethod::ByName); - sortShadowTree(root, lastKnownOrder); - endResetModel(); refreshCardFormatLegalities(); @@ -176,9 +154,6 @@ QVariant DeckListModel::data(const QModelIndex &index, int role) const case DeckRoles::IsLegalRole: return true; - case DeckRoles::IsCustomZoneRole: - return DeckListModelCustomZones::isCustomZone(group); - default: return {}; } @@ -191,15 +166,8 @@ QVariant DeckListModel::data(const QModelIndex &index, int role) const switch (index.column()) { case DeckListModelColumns::CARD_AMOUNT: return card->getNumber(); - case DeckListModelColumns::CARD_NAME: { - if (role == Qt::DisplayRole) { - CardInfoPtr info = CardDatabaseManager::query()->getCardInfo(card->getName()); - if (info) { - return info->getLocalizedName(displayLang); - } - } + case DeckListModelColumns::CARD_NAME: return card->getName(); - } case DeckListModelColumns::CARD_SET: return card->getCardSetShortName(); case DeckListModelColumns::CARD_COLLECTOR_NUMBER: @@ -222,10 +190,6 @@ QVariant DeckListModel::data(const QModelIndex &index, int role) const return card->getFormatLegality(); } - case DeckRoles::IsCustomZoneRole: { - return false; - } - default: { return {}; } @@ -363,13 +327,6 @@ bool DeckListModel::removeRows(int row, int count, const QModelIndex &parent) return false; } - // Custom zone rows are managed through the deck tree, never removed as model rows. - for (int i = 0; i < count; i++) { - if (DeckListModelCustomZones::isCustomZone(node->at(row + i))) { - return false; - } - } - beginRemoveRows(parent, row, row + count - 1); for (int i = 0; i < count; i++) { AbstractDecklistNode *toDelete = node->takeAt(row); @@ -380,8 +337,7 @@ bool DeckListModel::removeRows(int row, int count, const QModelIndex &parent) } endRemoveRows(); - // Empty criteria groups get pruned, but custom zones stay until explicitly deleted. - if (node->empty() && (node != root) && !DeckListModelCustomZones::isCustomZone(node)) { + if (node->empty() && (node != root)) { removeRows(parent.row(), 1, parent.parent()); } else { emitRecursiveUpdates(parent); @@ -395,8 +351,7 @@ bool DeckListModel::removeRows(int row, int count, const QModelIndex &parent) InnerDecklistNode *DeckListModel::createNodeIfNeeded(const QString &name, InnerDecklistNode *parent) { - // Group lookups must not resolve a mirrored custom zone that shares the name. - auto *newNode = DeckListModelCustomZones::findGroupChild(parent, name); + auto *newNode = dynamic_cast(parent->findChild(name)); if (!newNode) { beginInsertRows(nodeToIndex(parent), parent->size(), parent->size()); newNode = new InnerDecklistNode(name, parent); @@ -410,44 +365,24 @@ DecklistModelCardNode *DeckListModel::findCardNode(const QString &cardName, const QString &providerId, const QString &cardNumber) const { + InnerDecklistNode *zoneNode = dynamic_cast(root->findChild(zoneName)); + if (!zoneNode) { + return nullptr; + } + CardInfoPtr info = CardDatabaseManager::query()->getCardInfo(cardName); if (!info) { return nullptr; } - // 1. Board zone lookup: search the criteria groups, then the custom zones - // nested under the board. - if (auto *zoneNode = dynamic_cast(root->findChild(zoneName))) { - QString groupCriteria = extractGroupCriteriaValue(info, activeGroupCriteria); - if (auto *groupNode = DeckListModelCustomZones::findGroupChild(zoneNode, groupCriteria)) { - if (auto *card = dynamic_cast( - groupNode->findCardChildByNameProviderIdAndNumber(cardName, providerId, cardNumber))) { - return card; - } - } - - for (auto *child : *zoneNode) { - if (!DeckListModelCustomZones::isCustomZone(child)) { - continue; - } - auto *customZone = dynamic_cast(child); - if (!customZone) { - continue; - } - if (auto *card = dynamic_cast( - customZone->findCardChildByNameProviderIdAndNumber(cardName, providerId, cardNumber))) { - return card; - } - } + QString groupCriteria = extractGroupCriteriaValue(info, activeGroupCriteria); + InnerDecklistNode *groupNode = dynamic_cast(zoneNode->findChild(groupCriteria)); + if (!groupNode) { + return nullptr; } - // 2. Custom zone lookup by name (custom zone names are deck-unique). - if (auto *customZone = DeckListModelCustomZones::findSubZoneByName(root, zoneName)) { - return dynamic_cast( - customZone->findCardChildByNameProviderIdAndNumber(cardName, providerId, cardNumber)); - } - - return nullptr; + return dynamic_cast( + groupNode->findCardChildByNameProviderIdAndNumber(cardName, providerId, cardNumber)); } QModelIndex DeckListModel::findCard(const QString &cardName, @@ -488,95 +423,29 @@ QModelIndex DeckListModel::addCard(const ExactCard &card, const QString &zoneNam return {}; } + InnerDecklistNode *zoneNode = createNodeIfNeeded(zoneName, root); + CardInfoPtr cardInfo = card.getCardPtr(); PrintingInfo printingInfo = card.getPrinting(); - InnerDecklistNode *cardParent = nullptr; + QString groupCriteria = extractGroupCriteriaValue(cardInfo, activeGroupCriteria); + InnerDecklistNode *groupNode = createNodeIfNeeded(groupCriteria, zoneNode); - auto *boardNode = dynamic_cast(root->findChild(zoneName)); - auto *customZoneNode = boardNode ? nullptr : DeckListModelCustomZones::findSubZoneByName(root, zoneName); - - // Mirroring flattens nested deck sub-zones into shadow rows, so a shadow row - // index is only usable as a deck-tree position while both sides have the same - // direct-children shape. When they diverge, the card is appended to the deck - // zone instead of being written out of range. - InnerDecklistNode *deckCardParent = nullptr; - bool customZoneNeedsAppend = false; - - if (boardNode) { - // Board zone: cards are grouped by the active criteria. - QString groupCriteria = extractGroupCriteriaValue(cardInfo, activeGroupCriteria); - cardParent = createNodeIfNeeded(groupCriteria, boardNode); - } else if (customZoneNode) { - // Custom zone: cards live flat inside the zone. - cardParent = customZoneNode; - auto *listRoot = deckList->getTree()->getRoot(); - for (int i = 0; i < listRoot->size(); ++i) { - auto *boardZone = dynamic_cast(listRoot->at(i)); - if (!boardZone) { - continue; - } - deckCardParent = dynamic_cast(boardZone->findChild(zoneName)); - if (deckCardParent) { - break; - } - } - // A deck custom zone holding nested sub-zones mirrors with flattened rows, - // so a shadow row index does not map onto its direct children. - if (deckCardParent) { - for (int i = 0; i < deckCardParent->size(); ++i) { - if (dynamic_cast(deckCardParent->at(i))) { - customZoneNeedsAppend = true; - break; - } - } - } - } else { - // Not present in the shadow tree. The deck tree may still hold a custom - // zone that has not been mirrored (callers can add a zone and then a - // card without a rebuild). Check before falling back to creating a - // top-level zone the deck does not actually have. - auto *listRoot = deckList->getTree()->getRoot(); - bool hasDeckZone = false; - for (int i = 0; i < listRoot->size(); ++i) { - if (auto *boardZone = dynamic_cast(listRoot->at(i))) { - // Only real zones count: a card sitting directly under the board - // shares the name comparison but is not a zone, and treating it as - // one would recurse forever without mirroring anything. - if (dynamic_cast(boardZone->findChild(zoneName))) { - hasDeckZone = true; - break; - } - } - } - - if (hasDeckZone) { - rebuildTree(); - return addCard(card, zoneName); - } - - // Unknown zone: create a top-level zone (legacy behavior). - QString groupCriteria = extractGroupCriteriaValue(cardInfo, activeGroupCriteria); - auto *newZone = createNodeIfNeeded(zoneName, root); - cardParent = createNodeIfNeeded(groupCriteria, newZone); - } - - const QModelIndex parentIndex = nodeToIndex(cardParent); - auto *cardNode = dynamic_cast(cardParent->findCardChildByNameProviderIdAndNumber( + const QModelIndex parentIndex = nodeToIndex(groupNode); + auto *cardNode = dynamic_cast(groupNode->findCardChildByNameProviderIdAndNumber( card.getName(), printingInfo.getUuid(), printingInfo.getProperty("num"))); const auto cardSetName = printingInfo.getSet().isNull() ? "" : printingInfo.getSet()->getCorrectedShortName(); bool cardNodeAdded = false; if (!cardNode) { // Determine the correct index - int insertRow = findSortedInsertRow(cardParent, cardInfo); - int deckInsertRow = customZoneNeedsAppend ? -1 : insertRow; + int insertRow = findSortedInsertRow(groupNode, cardInfo); - auto *decklistCard = deckList->addCard(cardInfo->getName(), zoneName, deckInsertRow, cardSetName, + auto *decklistCard = deckList->addCard(cardInfo->getName(), zoneName, insertRow, cardSetName, printingInfo.getProperty("num"), printingInfo.getProperty("uuid")); beginInsertRows(parentIndex, insertRow, insertRow); - cardNode = new DecklistModelCardNode(decklistCard, cardParent, insertRow); + cardNode = new DecklistModelCardNode(decklistCard, groupNode, insertRow); endInsertRows(); cardNodeAdded = true; @@ -707,41 +576,21 @@ QModelIndex DeckListModel::nodeToIndex(AbstractDecklistNode *node) const return createIndex(node->getParent()->indexOf(node), 0, node); } -/** - * @brief Sorts a freshly built shadow subtree without emitting model signals. - * - * Used by rebuildTree while the model reset is still open (emitting layout - * changes during a reset is invalid). Reorders every node just like - * sortHelper does, but ignores the movement mapping because there are no - * persistent indices established yet. - */ -void DeckListModel::sortShadowTree(InnerDecklistNode *node, Qt::SortOrder order) -{ - // The mapping is not needed: fresh shadow nodes have no persistent indices yet. - (void)DeckListModelCustomZones::sortWithCustomZonesLast(root, node, order); - - for (int i = node->size() - 1; i >= 0; --i) { - if (auto *subNode = dynamic_cast(node->at(i))) { - sortShadowTree(subNode, order); - } - } -} - void DeckListModel::sortHelper(InnerDecklistNode *node, Qt::SortOrder order) { - // Sort children (custom zones always sorted after groups within a board) and - // use the movement mapping to update the list of persistent indices. - const auto mapping = DeckListModelCustomZones::sortWithCustomZonesLast(root, node, order); + // Sort children of node and save the information needed to + // update the list of persistent indexes. + QVector> sortResult = node->sort(order); QModelIndexList from, to; int columns = columnCount(); - for (const auto &move : mapping) { - const int preSortRow = move.first; - const int finalRow = move.second; - AbstractDecklistNode *temp = node->at(finalRow); + for (int i = sortResult.size() - 1; i >= 0; --i) { + const int fromRow = sortResult[i].first; + const int toRow = sortResult[i].second; + AbstractDecklistNode *temp = node->at(toRow); for (int j = 0; j < columns; ++j) { - from << createIndex(preSortRow, j, temp); - to << createIndex(finalRow, j, temp); + from << createIndex(fromRow, j, temp); + to << createIndex(toRow, j, temp); } } changePersistentIndexList(from, to); @@ -855,15 +704,6 @@ QList DeckListModel::getZones() const return zones; } -QStringList DeckListModel::getCustomZoneNames(const QString &boardZoneName) const -{ - QStringList zoneNames; - for (const auto *customZone : deckList->getTree()->getCustomZones(boardZoneName)) { - zoneNames.append(customZone->getName()); - } - return zoneNames; -} - static int maxAllowedForLegality(const FormatRules &format, const QString &legality) { for (const AllowedCount &c : format.allowedCounts) { diff --git a/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.h b/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.h index ce6d7f8cb..209ec8c42 100644 --- a/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.h +++ b/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.h @@ -1,8 +1,6 @@ #ifndef DECKLISTMODEL_H #define DECKLISTMODEL_H -#include "deck_list_model_custom_zones.h" - #include <../../../../libcockatrice_deck_list/libcockatrice/deck_list/tree/abstract_deck_list_card_node.h> #include <../../../../libcockatrice_deck_list/libcockatrice/deck_list/tree/deck_list_card_node.h> #include @@ -32,8 +30,7 @@ enum { IsCardRole = Qt::UserRole + 1, /**< Indicates whether the item represents a card. */ DepthRole, /**< Depth level within the deck's grouping hierarchy. */ - IsLegalRole, /**< Whether the card is legal in the current deck format. */ - IsCustomZoneRole /**< Whether the item represents a custom zone nested under a board zone. */ + IsLegalRole /**< Whether the card is legal in the current deck format. */ }; } // namespace DeckRoles @@ -287,16 +284,6 @@ public: explicit DeckListModel(QObject *parent, const QSharedPointer &deckList); ~DeckListModel() override; - /** - * @brief Selects the language code for localized card names in the display role. - * - * The model never reads global settings itself; callers wire this to the card - * language setting (including reacting to its changes) rather than the model - * querying it. - * @param lang Language code; "en" shows the canonical English names. - */ - void setDisplayLanguage(const QString &lang); - /** * @brief Returns the root index of the model. * @return QModelIndex representing the root node. @@ -404,21 +391,12 @@ public: */ [[nodiscard]] QList getZones() const; - /** - * @brief Gets the names of the custom zones nested under the given board zone. - * - * @param boardZoneName The board zone to query (main/side/maybeboard) - * @return The custom zone names, in deck order - */ - [[nodiscard]] QStringList getCustomZoneNames(const QString &boardZoneName) const; - private: QSharedPointer deckList; /**< Pointer to the decklist providing the underlying data. */ InnerDecklistNode *root; /**< Root node of the model tree. */ DeckListModelGroupCriteria::Type activeGroupCriteria = DeckListModelGroupCriteria::MAIN_TYPE; int lastKnownColumn; /**< Last column used for sorting. */ Qt::SortOrder lastKnownOrder; /**< Last known sort order. */ - QString displayLang = "en"; /**< Language code for localized card names in the display role. */ InnerDecklistNode *createNodeIfNeeded(const QString &name, InnerDecklistNode *parent); QModelIndex nodeToIndex(AbstractDecklistNode *node) const; @@ -449,7 +427,6 @@ private: void emitRecursiveUpdates(const QModelIndex &index); void sortHelper(InnerDecklistNode *node, Qt::SortOrder order); - void sortShadowTree(InnerDecklistNode *node, Qt::SortOrder order); template T getNode(const QModelIndex &index) const { diff --git a/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model_custom_zones.cpp b/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model_custom_zones.cpp deleted file mode 100644 index 1dc745e63..000000000 --- a/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model_custom_zones.cpp +++ /dev/null @@ -1,152 +0,0 @@ -#include "deck_list_model_custom_zones.h" - -#include "deck_list_model.h" - -#include <../../../../libcockatrice_deck_list/libcockatrice/deck_list/tree/deck_list_card_node.h> -#include -#include - -namespace DeckListModelCustomZones -{ - -bool isCustomZone(const AbstractDecklistNode *node) -{ - return dynamic_cast(node) != nullptr; -} - -namespace -{ - -/** - * @brief Flattens every card under @p zone into @p shadowZone, preserving order. - * - * Custom zones mirror as a single row level: cards nested in sub-zones of any - * depth are added as direct children of the mirrored zone so no card is left - * without a model row. - */ -void flattenCards(const InnerDecklistNode *zone, InnerDecklistNode *shadowZone) -{ - for (int k = 0; k < zone->size(); k++) { - if (auto *zoneCard = dynamic_cast(zone->at(k))) { - new DecklistModelCardNode(zoneCard, shadowZone); - } else if (auto *subZone = dynamic_cast(zone->at(k))) { - flattenCards(subZone, shadowZone); - } - } -} - -} // namespace - -void mirrorCustomZones(const InnerDecklistNode *deckBoardZone, InnerDecklistNode *shadowBoardZone) -{ - for (int j = 0; j < deckBoardZone->size(); j++) { - auto *customZone = dynamic_cast(deckBoardZone->at(j)); - if (!customZone) { - continue; - } - - auto *shadowZone = new DecklistModelSubZoneNode(customZone->getName(), shadowBoardZone); - flattenCards(customZone, shadowZone); - } -} - -InnerDecklistNode *findGroupChild(InnerDecklistNode *parent, const QString &name) -{ - for (int i = 0; i < parent->size(); i++) { - AbstractDecklistNode *child = parent->at(i); - if (isCustomZone(child)) { - continue; - } - auto *group = dynamic_cast(child); - if (group && group->getName() == name) { - return group; - } - } - return nullptr; -} - -DecklistModelSubZoneNode *findSubZoneByName(InnerDecklistNode *root, const QString &zoneName) -{ - for (int i = 0; i < root->size(); i++) { - auto *boardZone = dynamic_cast(root->at(i)); - if (!boardZone) { - continue; - } - - for (int j = 0; j < boardZone->size(); j++) { - auto *customZone = dynamic_cast(boardZone->at(j)); - if (customZone && customZone->getName() == zoneName) { - return customZone; - } - } - } - - return nullptr; -} - -namespace -{ - -/** - * @brief Sorts a node's children and returns the (preSortRow, finalRow) mapping. - */ -QList> plainSort(InnerDecklistNode *node, Qt::SortOrder order) -{ - const QVector> sortResult = node->sort(order); - - QList> mapping; - mapping.reserve(node->size()); - for (int i = 0; i < node->size(); ++i) { - mapping.append({sortResult[i].first, i}); - } - return mapping; -} - -/** - * @brief Sorts a board zone's children, then stably moves custom zones to the end. - * - * @return The (preSortRow, finalRow) mapping covering both the sort and the shift. - */ -QList> boardSort(InnerDecklistNode *node, Qt::SortOrder order) -{ - const QVector> sortResult = node->sort(order); - - QVector groups; - QVector customZones; - QHash preSortRowOf; - - groups.reserve(node->size()); - customZones.reserve(node->size()); - - for (int i = 0; i < node->size(); ++i) { - AbstractDecklistNode *child = node->at(i); - preSortRowOf.insert(child, sortResult[i].first); - if (isCustomZone(child)) { - customZones.append(child); - } else { - groups.append(child); - } - } - - QVector ordered = groups + customZones; - for (int i = 0; i < ordered.size(); ++i) { - node->replace(i, ordered[i]); - } - - QList> mapping; - mapping.reserve(ordered.size()); - for (int i = 0; i < ordered.size(); ++i) { - mapping.append({preSortRowOf.value(ordered[i]), i}); - } - return mapping; -} - -} // namespace - -QList> sortWithCustomZonesLast(InnerDecklistNode *root, InnerDecklistNode *node, Qt::SortOrder order) -{ - const bool isBoardZone = (node != root) && (node->getParent() == root); - return isBoardZone ? boardSort(node, order) : plainSort(node, order); -} - -} // namespace DeckListModelCustomZones diff --git a/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model_custom_zones.h b/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model_custom_zones.h deleted file mode 100644 index 518a9e1d2..000000000 --- a/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model_custom_zones.h +++ /dev/null @@ -1,98 +0,0 @@ -#ifndef DECK_LIST_MODEL_CUSTOM_ZONES_H -#define DECK_LIST_MODEL_CUSTOM_ZONES_H - -#include <../../../../libcockatrice_deck_list/libcockatrice/deck_list/tree/inner_deck_list_node.h> -#include -#include -#include - -/** - * @class DecklistModelSubZoneNode - * @ingroup DeckModels - * @brief Model node representing a custom zone nested under a board zone. - * - * Custom zones group cards by user-defined names (e.g. "Removal", "Utility") - * inside a board zone. They are mirrored from the underlying deck tree so that - * they can be told apart from criteria group nodes by type. - */ -class DecklistModelSubZoneNode : public InnerDecklistNode -{ -public: - using InnerDecklistNode::InnerDecklistNode; -}; - -/** - * @namespace DeckListModelCustomZones - * @ingroup DeckModels - * @brief Tree-level helpers for the deck list model's custom-zone shadow nodes. - * - * The deck list model keeps a second "shadow" tree of InnerDecklistNode that - * mirrors the canonical deck tree for grouping and sorting. Custom zones add a - * layer of bookkeeping to that shadow tree: they must be mirrored alongside - * criteria groups, always sort after the groups within a board, and be - * resolvable by deck-unique name. - * - * This namespace centralizes every "what is / where is a custom zone" decision - * so the model itself only wires the results into Qt model signals. - */ -namespace DeckListModelCustomZones -{ - -/** - * @brief Whether the given node is a custom zone (as opposed to a criteria group). - */ -[[nodiscard]] bool isCustomZone(const AbstractDecklistNode *node); - -/** - * @brief Finds a criteria-group child of @p parent by name, skipping custom zones. - * - * The shadow tree keeps criteria groups and mirrored custom zones as siblings - * under a board zone, and `InnerDecklistNode::findChild` matches both by name. - * Group lookups must not resolve a custom zone that happens to share the group - * name (e.g. a zone called "Creature"), so this searches only non-custom - * children. - * - * @param parent The shadow node whose children are searched. - * @param name The group name to find. - * @return The matching group node, or nullptr if none exists. - */ -[[nodiscard]] InnerDecklistNode *findGroupChild(InnerDecklistNode *parent, const QString &name); - -/** - * @brief Mirrors the custom zones of a deck board zone into its shadow board node. - * - * Each custom zone becomes a DecklistModelSubZoneNode under @p shadowBoardZone - * with its cards as direct (un-grouped) children. - * - * @param deckBoardZone The board zone in the canonical deck tree. - * @param shadowBoardZone The matching board zone in the model's shadow tree. - */ -void mirrorCustomZones(const InnerDecklistNode *deckBoardZone, InnerDecklistNode *shadowBoardZone); - -/** - * @brief Finds a custom zone in the shadow tree by deck-unique name. - * @param root Root of the shadow tree. - * @param zoneName The custom zone name to find. - * @return The matching custom zone node, or nullptr if not found. - */ -[[nodiscard]] DecklistModelSubZoneNode *findSubZoneByName(InnerDecklistNode *root, const QString &zoneName); - -/** - * @brief Sorts a shadow node's children, keeping a board's custom zones last. - * - * Sorting alone would interleave custom zones with criteria groups by name, but - * custom zones must always stay after the groups within a board, regardless of - * name. This applies the sort and, for board zones, stably moves the custom - * zones to the end. - * - * @param root Root of the shadow tree (used to classify board zones). - * @param node The shadow node whose children are reordered. - * @param order Sort order to apply. - * @return A list of (preSortRow, finalRow) pairs describing how each node moved. - */ -[[nodiscard]] QList> -sortWithCustomZonesLast(InnerDecklistNode *root, InnerDecklistNode *node, Qt::SortOrder order); - -} // namespace DeckListModelCustomZones - -#endif // DECK_LIST_MODEL_CUSTOM_ZONES_H diff --git a/libcockatrice_network/libcockatrice/network/client/abstract/CMakeLists.txt b/libcockatrice_network/libcockatrice/network/client/abstract/CMakeLists.txt index 6fba8d629..c4a8e4648 100644 --- a/libcockatrice_network/libcockatrice/network/client/abstract/CMakeLists.txt +++ b/libcockatrice_network/libcockatrice/network/client/abstract/CMakeLists.txt @@ -2,9 +2,9 @@ set(CMAKE_AUTOMOC ON) set(CMAKE_AUTOUIC ON) set(CMAKE_AUTORCC ON) -set(HEADERS abstract_client.h latency_tracker.h) +set(HEADERS abstract_client.h) -set(SOURCES abstract_client.cpp latency_tracker.cpp) +set(SOURCES abstract_client.cpp) qt6_wrap_cpp(MOC_SOURCES ${HEADERS}) diff --git a/libcockatrice_network/libcockatrice/network/client/abstract/abstract_client.cpp b/libcockatrice_network/libcockatrice/network/client/abstract/abstract_client.cpp index 687d93666..916f4351b 100644 --- a/libcockatrice_network/libcockatrice/network/client/abstract/abstract_client.cpp +++ b/libcockatrice_network/libcockatrice/network/client/abstract/abstract_client.cpp @@ -1,7 +1,6 @@ #include "abstract_client.h" #include -#include #include #include #include @@ -29,7 +28,6 @@ AbstractClient::AbstractClient(QObject *parent) qRegisterMetaType("Response"); qRegisterMetaType("Response::ResponseCode"); qRegisterMetaType("ClientStatus"); - qRegisterMetaType("LatencyTracker::Stats"); qRegisterMetaType("RoomEvent"); qRegisterMetaType("GameEventContainer"); qRegisterMetaType("Event_ServerIdentification"); @@ -73,8 +71,6 @@ void AbstractClient::processProtocolItem(const ServerMessage &item) } pendingCommands.remove(cmdId); - recordLatency(*pend); - pend->processResponse(response); pend->deleteLater(); break; @@ -165,62 +161,9 @@ void AbstractClient::queuePendingCommand(PendingCommand *pend) pendingCommands.insert(cmdId, pend); - pend->startTiming(); sendCommandContainer(pend->getCommandContainer()); } -namespace -{ -constexpr int STATS_EMIT_INTERVAL_MS = 1000; -// Game actions are what players perceive as lag. Surface unusually slow ones -// without requiring debug logging to be enabled. -constexpr qint64 SLOW_GAME_COMMAND_WARN_MS = 1500; -} // namespace - -void AbstractClient::recordLatency(PendingCommand &pend) -{ - const qint64 elapsed = pend.elapsedMs(); - if (elapsed < 0) { - return; - } - - latencyTracker.addSample(elapsed); - - if (AbstractClientLog().isDebugEnabled()) { - qCDebug(AbstractClientLog).noquote() - << "command RTT:" << elapsed << "ms (cmd_id" << pend.getCommandContainer().cmd_id() << ")"; - } - - if (elapsed >= SLOW_GAME_COMMAND_WARN_MS && pend.getCommandContainer().game_command_size() > 0) { - qCWarning(AbstractClientLog).noquote() - << "slow game command round trip:" << elapsed << "ms | " << getSafeDebugString(pend.getCommandContainer()); - } - - // Emit aggregated stats at most once per StatsEmitIntervalMs so that the - // per-command hot path stays free of signal traffic. The keepalive ping - // guarantees a fresh sample roughly every second while connected. - if (!statsEmitClockStarted || statsEmitClock.elapsed() >= STATS_EMIT_INTERVAL_MS) { - statsEmitClock.start(); - statsEmitClockStarted = true; - const LatencyTracker::Stats stats = latencyTracker.stats(); - - QList samples; - samples.reserve(stats.sampleCount); - for (qint64 sample : latencyTracker.recentSamples()) { - samples.append(static_cast(sample)); - } - - emit pingStatsUpdated(stats, samples); - } -} - -void AbstractClient::clearLatencyStats() -{ - latencyTracker.clear(); - statsEmitClockStarted = false; - emit pingStatsUpdated(LatencyTracker::Stats{}, {}); -} - PendingCommand *AbstractClient::prepareSessionCommand(const ::google::protobuf::Message &cmd) { CommandContainer cont; @@ -253,24 +196,3 @@ PendingCommand *AbstractClient::prepareAdminCommand(const ::google::protobuf::Me c->GetReflection()->MutableMessage(c, cmd.GetDescriptor()->FindExtensionByName("ext"))->CopyFrom(cmd); return new PendingCommand(cont); } - -PendingCommand *AbstractClient::prepareDeveloperCommand(const ::google::protobuf::Message &cmd) -{ - CommandContainer cont; - DeveloperCommand *c = cont.add_developer_command(); - // A developer command message may also be usable through other command - // families, so select the extension scoped to DeveloperCommand rather than - // guessing by name. - const ::google::protobuf::Descriptor *cmdDescriptor = cmd.GetDescriptor(); - const ::google::protobuf::Descriptor *developerDescriptor = DeveloperCommand::descriptor(); - const ::google::protobuf::FieldDescriptor *developerExtension = nullptr; - for (int i = 0; i < cmdDescriptor->extension_count(); ++i) { - if (cmdDescriptor->extension(i)->containing_type() == developerDescriptor) { - developerExtension = cmdDescriptor->extension(i); - break; - } - } - Q_ASSERT(developerExtension != nullptr); - c->GetReflection()->MutableMessage(c, developerExtension)->CopyFrom(cmd); - return new PendingCommand(cont); -} diff --git a/libcockatrice_network/libcockatrice/network/client/abstract/abstract_client.h b/libcockatrice_network/libcockatrice/network/client/abstract/abstract_client.h index af22a5c9d..982aa6bf3 100644 --- a/libcockatrice_network/libcockatrice/network/client/abstract/abstract_client.h +++ b/libcockatrice_network/libcockatrice/network/client/abstract/abstract_client.h @@ -7,17 +7,11 @@ #ifndef ABSTRACTCLIENT_H #define ABSTRACTCLIENT_H -#include "latency_tracker.h" - -#include -#include #include #include #include #include -inline Q_LOGGING_CATEGORY(AbstractClientLog, "abstract_client"); - class PendingCommand; class CommandContainer; class RoomEvent; @@ -60,18 +54,6 @@ signals: void statusChanged(ClientStatus _status); void maxPingTime(int seconds, int maxSeconds); - /** - * @brief Aggregated round-trip statistics and a chronological snapshot of - * the rolling window, emitted at most once per second. - * - * All values in the stats struct are in milliseconds; sampleCount is the - * number of samples currently in the rolling window. The samples list is - * ordered oldest first so graphs can redraw without polling the tracker - * across threads. Emitted from the client thread. The connection to UI - * objects is automatically queued across threads. - */ - void pingStatsUpdated(const LatencyTracker::Stats &stats, const QList &samplesMs); - // Room events void roomEventReceived(const RoomEvent &event); // Game events @@ -103,11 +85,6 @@ private: int nextCmdId; mutable QMutex clientMutex; ClientStatus status; - LatencyTracker latencyTracker; - QElapsedTimer statsEmitClock; - bool statsEmitClockStarted = false; - - void recordLatency(PendingCommand &pend); private slots: void queuePendingCommand(PendingCommand *pend); protected slots: @@ -136,16 +113,6 @@ public: void sendCommand(const CommandContainer &cont); void sendCommand(PendingCommand *pend); - /** - * @brief Drops all recorded round-trip samples and resets the stats - * emission throttle, emitting zeroed stats so that UI listeners can - * clear their display. - * - * Must be called from the client thread (as RemoteClient's disconnect - * path does). The tracker is deliberately lock-free. - */ - void clearLatencyStats(); - bool getServerSupportsPasswordHash() const { return serverSupportsPasswordHash; @@ -173,7 +140,6 @@ public: static PendingCommand *prepareRoomCommand(const ::google::protobuf::Message &cmd, int roomId); static PendingCommand *prepareModeratorCommand(const ::google::protobuf::Message &cmd); static PendingCommand *prepareAdminCommand(const ::google::protobuf::Message &cmd); - static PendingCommand *prepareDeveloperCommand(const ::google::protobuf::Message &cmd); QMap clientFeatures; }; diff --git a/libcockatrice_network/libcockatrice/network/client/abstract/latency_tracker.cpp b/libcockatrice_network/libcockatrice/network/client/abstract/latency_tracker.cpp deleted file mode 100644 index 98b353fdf..000000000 --- a/libcockatrice_network/libcockatrice/network/client/abstract/latency_tracker.cpp +++ /dev/null @@ -1,60 +0,0 @@ -#include "latency_tracker.h" - -#include -#include - -void LatencyTracker::addSample(qint64 ms) -{ - samples[static_cast(head)] = ms; - head = (head + 1) % WindowSize; - if (count < WindowSize) { - ++count; - } -} - -QList LatencyTracker::recentSamples() const -{ - QList result; - result.reserve(count); - for (int i = count; i > 0; --i) { - const int index = (head + WindowSize - i) % WindowSize; - result.append(samples[static_cast(index)]); - } - return result; -} - -LatencyTracker::Stats LatencyTracker::stats() const -{ - if (count == 0) { - return {}; - } - - QList sorted(samples.cbegin(), samples.cbegin() + count); - std::sort(sorted.begin(), sorted.end()); - - Stats s; - s.sampleCount = count; - s.lastMs = samples[static_cast((head + WindowSize - 1) % WindowSize)]; - s.maxMs = sorted.last(); - - const int n = count; - if (n % 2 == 1) { - s.medianMs = sorted[n / 2]; - } else { - s.medianMs = (sorted[n / 2 - 1] + sorted[n / 2]) / 2; - } - - // Nearest-rank percentile: smallest value in the list such that at least - // 95% of the samples are <= it. - const int p95Index = qMax(0, qCeil(0.95 * static_cast(n)) - 1); - s.p95Ms = sorted[p95Index]; - - return s; -} - -void LatencyTracker::clear() -{ - samples.fill(0); - head = 0; - count = 0; -} diff --git a/libcockatrice_network/libcockatrice/network/client/abstract/latency_tracker.h b/libcockatrice_network/libcockatrice/network/client/abstract/latency_tracker.h deleted file mode 100644 index 75f18ac0c..000000000 --- a/libcockatrice_network/libcockatrice/network/client/abstract/latency_tracker.h +++ /dev/null @@ -1,51 +0,0 @@ -/** - * @file latency_tracker.h - * @ingroup Client - */ - -#ifndef LATENCY_TRACKER_H -#define LATENCY_TRACKER_H - -#include -#include -#include - -/** - * @brief Fixed-capacity rolling window of network round-trip time samples. - * - * The hot path (addSample) is a single array store and is intentionally free of - * allocations, locks, or signal emissions so that recording one sample per - * completed command cannot affect gameplay performance. Aggregate statistics - * are only computed on demand in stats(), which callers should throttle. - */ -class LatencyTracker -{ -public: - static constexpr int WindowSize = 64; - - struct Stats - { - qint64 lastMs = 0; ///< most recently added sample - qint64 medianMs = 0; ///< median over the current window - qint64 p95Ms = 0; ///< 95th percentile over the current window - qint64 maxMs = 0; ///< maximum over the current window - int sampleCount = 0; ///< number of samples currently in the window - }; - - void addSample(qint64 ms); - Stats stats() const; - - /// Snapshot of the current window in chronological order (oldest first). - QList recentSamples() const; - - void clear(); - -private: - std::array samples{}; - int head = 0; ///< index where the next sample will be written - int count = 0; ///< number of valid samples, capped at WindowSize -}; - -Q_DECLARE_METATYPE(LatencyTracker::Stats) - -#endif diff --git a/libcockatrice_network/libcockatrice/network/client/remote/remote_client.cpp b/libcockatrice_network/libcockatrice/network/client/remote/remote_client.cpp index 53608db65..7e20f2722 100644 --- a/libcockatrice_network/libcockatrice/network/client/remote/remote_client.cpp +++ b/libcockatrice_network/libcockatrice/network/client/remote/remote_client.cpp @@ -543,7 +543,6 @@ void RemoteClient::doDisconnectFromServer() delete i; } pendingCommands.clear(); - clearLatencyStats(); setStatus(StatusDisconnected); if (websocket->isValid()) { diff --git a/libcockatrice_network/libcockatrice/network/server/remote/CMakeLists.txt b/libcockatrice_network/libcockatrice/network/server/remote/CMakeLists.txt index e11a962d1..9fb63c221 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/CMakeLists.txt +++ b/libcockatrice_network/libcockatrice/network/server/remote/CMakeLists.txt @@ -10,12 +10,7 @@ set(HEADERS game/server_card.h game/server_cardzone.h game/server_counter.h - game/game_config.h - game/server_deck_validation_strategy.h game/server_game.h - game/server_game_lifecycle_strategy.h - game/server_match_result_strategy.h - game/server_match_game_factory.h game/server_player.h game/server_spectator.h server.h diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/game_config.h b/libcockatrice_network/libcockatrice/network/server/remote/game/game_config.h deleted file mode 100644 index baf946632..000000000 --- a/libcockatrice_network/libcockatrice/network/server/remote/game/game_config.h +++ /dev/null @@ -1,26 +0,0 @@ -#ifndef GAME_CONFIG_H -#define GAME_CONFIG_H - -#include -#include -#include - -struct GameConfig -{ - ServerInfo_User creatorInfo; - int gameId = -1; - QString description; - QString password; - int maxPlayers = 2; - QList gameTypes; - bool onlyBuddies = false; - bool onlyRegistered = false; - bool spectatorsAllowed = false; - bool spectatorsNeedPassword = false; - bool spectatorsCanTalk = true; - bool spectatorsSeeEverything = true; - int startingLifeTotal = 20; - bool shareDecklistsOnLoad = true; -}; - -#endif diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_participant.cpp b/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_participant.cpp index a6280ffa1..5c0fdf944 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_participant.cpp +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_participant.cpp @@ -37,7 +37,6 @@ #include #include #include -#include #include #include #include @@ -145,13 +144,6 @@ Response::ResponseCode Server_AbstractParticipant::cmdSetSideboardLock(const Com return Response::RespFunctionNotAllowed; } -Response::ResponseCode Server_AbstractParticipant::cmdSetPlaymat(const Command_SetPlaymat & /*cmd*/, - ResponseContainer & /*rc*/, - GameEventStorage & /*ges*/) -{ - return Response::RespFunctionNotAllowed; -} - Response::ResponseCode Server_AbstractParticipant::cmdConcede(const Command_Concede & /*cmd*/, ResponseContainer & /*rc*/, GameEventStorage & /*ges*/) @@ -533,9 +525,6 @@ Server_AbstractParticipant::processGameCommand(const GameCommand &command, Respo case GameCommand::REVERSE_TURN: return cmdReverseTurn(command.GetExtension(Command_ReverseTurn::ext), rc, ges); break; - case GameCommand::SET_PLAYMAT: - return cmdSetPlaymat(command.GetExtension(Command_SetPlaymat::ext), rc, ges); - break; default: return Response::RespInvalidCommand; } diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_participant.h b/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_participant.h index c78ae78c1..a24fa5799 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_participant.h +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_participant.h @@ -52,7 +52,6 @@ class Command_SetSideboardPlan; class Command_DeckSelect; class Command_SetSideboardLock; class Command_ChangeZoneProperties; -class Command_SetPlaymat; class Server_AbstractParticipant : public Server_ArrowTarget, public ServerInfo_User_Container { @@ -125,8 +124,6 @@ public: cmdSetSideboardPlan(const Command_SetSideboardPlan &cmd, ResponseContainer &rc, GameEventStorage &ges); virtual Response::ResponseCode cmdSetSideboardLock(const Command_SetSideboardLock &cmd, ResponseContainer &rc, GameEventStorage &ges); - virtual Response::ResponseCode - cmdSetPlaymat(const Command_SetPlaymat &cmd, ResponseContainer &rc, GameEventStorage &ges); virtual Response::ResponseCode cmdGameSay(const Command_GameSay &cmd, ResponseContainer &rc, GameEventStorage &ges); virtual Response::ResponseCode cmdShuffle(const Command_Shuffle &cmd, ResponseContainer &rc, GameEventStorage &ges); virtual Response::ResponseCode diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_player.cpp b/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_player.cpp index 6b4101a99..5bf27eebb 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_player.cpp +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_player.cpp @@ -66,15 +66,6 @@ Server_AbstractPlayer::Server_AbstractPlayer(Server_Game *_game, Server_AbstractPlayer::~Server_AbstractPlayer() = default; -int Server_AbstractPlayer::getCardCount() const -{ - int result = 0; - for (auto *zone : zones) { - result += zone->getCards().size(); - } - return result; -} - void Server_AbstractPlayer::prepareDestroy() { delete deck; @@ -1640,9 +1631,7 @@ void Server_AbstractPlayer::getInfo(ServerInfo_Player *info, { getProperties(*info->mutable_properties(), withUserInfo); - // Deck lists are only shared with other players when the game is in Open Decklists mode, - // so a player joining an open lobby can see every deck that was loaded before they joined. - if (deck && (recipient == this || game->getShareDecklistsOnLoad())) { + if (deck) { info->set_deck_list(deck->writeToString_Native().toStdString()); } @@ -1662,13 +1651,5 @@ void Server_AbstractPlayer::getPlayerProperties(ServerInfo_PlayerProperties &res result.set_ready_start(readyStart); if (deck) { result.set_deck_hash(deck->getDeckHash().toStdString()); - const auto &playmat = deck->getPlaymat(); - auto *playmatParams = result.mutable_playmat_params(); - playmatParams->set_card_name(playmat.card.name.toStdString()); - playmatParams->set_card_provider_id(playmat.card.providerId.toStdString()); - playmatParams->set_margin_pct_l(playmat.params.marginPctL); - playmatParams->set_margin_pct_r(playmat.params.marginPctR); - playmatParams->set_vertical_offset(playmat.params.verticalOffset); - playmatParams->set_zoom(playmat.params.zoom); } } diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_player.h b/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_player.h index 4cc79c5fe..85fbc0557 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_player.h +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_player.h @@ -43,8 +43,6 @@ public: Server_AbstractUserInterface *_handler); ~Server_AbstractPlayer() override; void prepareDestroy() override; - /// Total cards across all of this player's zones. The caller must hold the game's mutex. - int getCardCount() const; const DeckList *getDeckList() const { return deck; diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/server_deck_validation_strategy.h b/libcockatrice_network/libcockatrice/network/server/remote/game/server_deck_validation_strategy.h deleted file mode 100644 index 8298214b4..000000000 --- a/libcockatrice_network/libcockatrice/network/server/remote/game/server_deck_validation_strategy.h +++ /dev/null @@ -1,45 +0,0 @@ -#ifndef SERVER_DECK_VALIDATION_STRATEGY_H -#define SERVER_DECK_VALIDATION_STRATEGY_H - -#include - -class DeckList; -class Server_Game; -class Server_Player; -class ResponseContainer; - -/** - * @brief Strategy for validating a player's deck before it is loaded into a game. - * - * Subclasses decide whether a deck may be accepted; the default implementation - * accepts every deck. - */ -class Server_DeckValidationStrategy -{ -public: - virtual ~Server_DeckValidationStrategy() = default; - - /** - * @brief Validate @p deck for @p player in @p game. - * - * @p rc is an out parameter used to attach the response details for a rejected - * deck (e.g. an error response extension via ResponseContainer::setResponseExtension). - * @return Response::RespOk when the deck is accepted, an error code otherwise. - */ - virtual Response::ResponseCode - validate(Server_Game *game, Server_Player *player, DeckList *deck, ResponseContainer &rc) = 0; -}; - -/** - * @brief Default deck validation strategy that accepts every deck. - */ -class Server_DefaultDeckValidationStrategy : public Server_DeckValidationStrategy -{ -public: - Response::ResponseCode validate(Server_Game *, Server_Player *, DeckList *, ResponseContainer &) override - { - return Response::RespOk; - } -}; - -#endif diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/server_game.cpp b/libcockatrice_network/libcockatrice/network/server/remote/game/server_game.cpp index 131ff1077..4761199e5 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/game/server_game.cpp +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_game.cpp @@ -32,7 +32,6 @@ #include "server_spectator.h" #include -#include #include #include #include @@ -54,22 +53,34 @@ #include #include -Server_Game::Server_Game(const GameConfig &config, Server_Room *_room) - : QObject(), room(_room), nextPlayerId(0), hostId(0), creatorInfo(new ServerInfo_User(config.creatorInfo)), - gameStarted(false), gameClosed(false), gameId(config.gameId), description(config.description.simplified()), - password(config.password), maxPlayers(config.maxPlayers), gameTypes(config.gameTypes), activePlayer(-1), - activePhase(-1), onlyBuddies(config.onlyBuddies), onlyRegistered(config.onlyRegistered), - spectatorsAllowed(config.spectatorsAllowed), spectatorsNeedPassword(config.spectatorsNeedPassword), - spectatorsCanTalk(config.spectatorsCanTalk), spectatorsSeeEverything(config.spectatorsSeeEverything), - startingLifeTotal(config.startingLifeTotal), shareDecklistsOnLoad(config.shareDecklistsOnLoad), - inactivityCounter(0), startTimeOfThisGame(0), secondsElapsed(0), firstGameStarted(false), - turnOrderReversed(false), startTime(QDateTime::currentDateTime()), pingClock(nullptr), - deckValidationStrategy(new Server_DefaultDeckValidationStrategy), - lifecycleStrategy(new Server_DefaultLifecycleStrategy), matchResultStrategy(new Server_NullMatchResultStrategy), +Server_Game::Server_Game(const ServerInfo_User &_creatorInfo, + int _gameId, + const QString &_description, + const QString &_password, + int _maxPlayers, + const QList &_gameTypes, + bool _onlyBuddies, + bool _onlyRegistered, + bool _spectatorsAllowed, + bool _spectatorsNeedPassword, + bool _spectatorsCanTalk, + bool _spectatorsSeeEverything, + int _startingLifeTotal, + bool _shareDecklistsOnLoad, + Server_Room *_room) + : QObject(), room(_room), nextPlayerId(0), hostId(0), creatorInfo(new ServerInfo_User(_creatorInfo)), + gameStarted(false), gameClosed(false), gameId(_gameId), password(_password), maxPlayers(_maxPlayers), + gameTypes(_gameTypes), activePlayer(-1), activePhase(-1), onlyBuddies(_onlyBuddies), + onlyRegistered(_onlyRegistered), spectatorsAllowed(_spectatorsAllowed), + spectatorsNeedPassword(_spectatorsNeedPassword), spectatorsCanTalk(_spectatorsCanTalk), + spectatorsSeeEverything(_spectatorsSeeEverything), startingLifeTotal(_startingLifeTotal), + shareDecklistsOnLoad(_shareDecklistsOnLoad), inactivityCounter(0), startTimeOfThisGame(0), secondsElapsed(0), + firstGameStarted(false), turnOrderReversed(false), startTime(QDateTime::currentDateTime()), pingClock(nullptr), gameMutex() { currentReplay = new GameReplay; currentReplay->set_replay_id(room->getServer()->getDatabaseInterface()->getNextReplayId()); + description = _description.simplified(); connect(this, &Server_Game::sigStartGameIfReady, this, &Server_Game::doStartGameIfReady, Qt::QueuedConnection); @@ -239,17 +250,6 @@ int Server_Game::getPlayerCount() const return participants.size() - getSpectatorCount(); } -int Server_Game::getCardsInGame() const -{ - QMutexLocker locker(&gameMutex); - - int result = 0; - for (auto *player : getPlayers()) { - result += player->getCardCount(); - } - return result; -} - int Server_Game::getSpectatorCount() const { QMutexLocker locker(&gameMutex); @@ -342,15 +342,7 @@ void Server_Game::doStartGameIfReady(bool forceStartGame) } } - // Only actual starts are timed. The early returns above are no-ops. - QElapsedTimer startupTimer; - startupTimer.start(); players = getPlayers(); // players could have been kicked, get new list of players - if (lifecycleStrategy->onGameStarting(this) == Server_GameLifecycleStrategy::StartAction::Handled) { - locker.unlock(); - return; - } - for (Server_AbstractPlayer *player : players.values()) { player->setupZones(); } @@ -388,7 +380,6 @@ void Server_Game::doStartGameIfReady(bool forceStartGame) activePlayer = -1; nextTurn(); - room->getServer()->observeGameStartDurationMs(startupTimer.nsecsElapsed() / 1000000); locker.unlock(); @@ -409,12 +400,10 @@ void Server_Game::stopGameIfFinished() QMutexLocker locker(&gameMutex); int playing = 0; - Server_AbstractPlayer *lastPlayer = nullptr; auto players = getPlayers(); for (auto *player : players.values()) { if (!player->getConceded()) { ++playing; - lastPlayer = player; } } if (playing > 1) { @@ -430,16 +419,6 @@ void Server_Game::stopGameIfFinished() sendGameStateToPlayers(); - bool matchDecided = matchResultStrategy->onGameFinished(this, playing, lastPlayer); - if (matchDecided) { - locker.unlock(); - - sendGameEventContainer(prepareGameEvent(Event_GameClosed(), -1)); - gameClosed = true; - deleteLater(); - return; - } - locker.unlock(); ServerInfo_Game gameInfo; @@ -465,7 +444,7 @@ Response::ResponseCode Server_Game::checkJoin(ServerInfo_User *user, if (asJudge && !(user->user_level() & ServerInfo_User::IsJudge)) { return Response::RespUserLevelTooLow; } - if (!(overrideRestrictions && (user->user_level() & (ServerInfo_User::IsModerator | ServerInfo_User::IsJudge)))) { + if (!(overrideRestrictions && (user->user_level() & ServerInfo_User::IsModerator))) { if ((_password != password) && !(spectator && !spectatorsNeedPassword)) { return Response::RespWrongPassword; } @@ -547,7 +526,10 @@ void Server_Game::addPlayer(Server_AbstractUserInterface *userInterface, if (broadcastUpdate) { ServerInfo_Game gameInfo; - getInfo(gameInfo); + gameInfo.set_room_id(room->getId()); + gameInfo.set_game_id(gameId); + gameInfo.set_player_count(getPlayerCount()); + gameInfo.set_spectators_count(getSpectatorCount()); emit gameInfoChanged(gameInfo); } @@ -606,7 +588,10 @@ void Server_Game::removeParticipant(Server_AbstractParticipant *participant, Eve } ServerInfo_Game gameInfo; - getInfo(gameInfo); + gameInfo.set_room_id(room->getId()); + gameInfo.set_game_id(gameId); + gameInfo.set_player_count(getPlayerCount()); + gameInfo.set_spectators_count(getSpectatorCount()); emit gameInfoChanged(gameInfo); } @@ -862,12 +847,6 @@ void Server_Game::getInfo(ServerInfo_Game &result) const result.set_player_count(getPlayerCount()); result.set_started(gameStarted); result.mutable_creator_info()->CopyFrom(*getCreatorInfo()); - const Server_AbstractParticipant *host = participants.value(hostId, nullptr); - if (host != nullptr) { - host->copyUserInfo(*result.mutable_host_info(), false); - } else { - result.mutable_host_info()->CopyFrom(*getCreatorInfo()); - } result.set_only_buddies(onlyBuddies); result.set_only_registered(onlyRegistered); result.set_spectators_allowed(getSpectatorsAllowed()); @@ -922,8 +901,3 @@ void Server_Game::returnCardsFromPlayer(GameEventStorage &ges, Server_AbstractPl } } } - -void Server_Game::setDeckValidationStrategy(Server_DeckValidationStrategy *strategy) -{ - deckValidationStrategy.reset(strategy); -} diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/server_game.h b/libcockatrice_network/libcockatrice/network/server/remote/game/server_game.h index 1ed4fe4ca..e0e7896b7 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/game/server_game.h +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_game.h @@ -21,16 +21,11 @@ #define SERVERGAME_H #include "../server_response_containers.h" -#include "game_config.h" -#include "server_deck_validation_strategy.h" -#include "server_game_lifecycle_strategy.h" -#include "server_match_result_strategy.h" #include #include #include #include -#include #include #include #include @@ -83,12 +78,6 @@ private: QList replayList; GameReplay *currentReplay; - QScopedPointer deckValidationStrategy; - - QScopedPointer lifecycleStrategy; - - QScopedPointer matchResultStrategy; - void createGameStateChangedEvent(Event_GameStateChanged *event, Server_AbstractParticipant *recipient, bool omniscient, @@ -103,7 +92,21 @@ private slots: public: mutable QRecursiveMutex gameMutex; - Server_Game(const GameConfig &config, Server_Room *parent); + Server_Game(const ServerInfo_User &_creatorInfo, + int _gameId, + const QString &_description, + const QString &_password, + int _maxPlayers, + const QList &_gameTypes, + bool _onlyBuddies, + bool _onlyRegistered, + bool _spectatorsAllowed, + bool _spectatorsNeedPassword, + bool _spectatorsCanTalk, + bool _spectatorsSeeEverything, + int _startingLifeTotal, + bool _shareDecklistsOnLoad, + Server_Room *parent); ~Server_Game() override; Server_Room *getRoom() const { @@ -123,8 +126,6 @@ public: return gameStarted; } int getPlayerCount() const; - /// Total cards across all players' zones. Takes gameMutex itself. - int getCardsInGame() const; int getSpectatorCount() const; QMap getPlayers() const; Server_AbstractPlayer *getPlayer(int id) const; @@ -220,20 +221,6 @@ public: GameEventStorageItem::SendToOthers, int privatePlayerId = -1); void returnCardsFromPlayer(GameEventStorage &ges, Server_AbstractPlayer *player); - - /** @brief Get the current deck validation strategy (non-owning). */ - Server_DeckValidationStrategy *getDeckValidationStrategy() const - { - return deckValidationStrategy.data(); - } - /** @brief Replace the deck validation strategy; takes ownership of @p strategy. */ - void setDeckValidationStrategy(Server_DeckValidationStrategy *strategy); - - /** @brief Get the current game lifecycle strategy (non-owning). */ - Server_GameLifecycleStrategy *getLifecycleStrategy() const - { - return lifecycleStrategy.data(); - } }; #endif diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/server_game_lifecycle_strategy.h b/libcockatrice_network/libcockatrice/network/server/remote/game/server_game_lifecycle_strategy.h deleted file mode 100644 index b71ce486d..000000000 --- a/libcockatrice_network/libcockatrice/network/server/remote/game/server_game_lifecycle_strategy.h +++ /dev/null @@ -1,43 +0,0 @@ -#ifndef SERVER_GAME_LIFECYCLE_STRATEGY_H -#define SERVER_GAME_LIFECYCLE_STRATEGY_H - -class Server_Game; - -/** - * @brief Strategy hook invoked around a game's lifecycle transitions. - * - * Subclasses can intercept the game start to perform custom setup (e.g. draft or - * tournament initialization); the default implementation never intercepts. - */ -class Server_GameLifecycleStrategy -{ -public: - virtual ~Server_GameLifecycleStrategy() = default; - - /** @brief How the game start should proceed after this hook returns. */ - enum class StartAction - { - ProceedNormal, ///< Continue with the normal game start flow. - Handled, ///< The strategy handled the start; abort the normal flow. - }; - - /** - * @brief Called when a game is about to start. - * @return How the start flow should proceed. - */ - virtual StartAction onGameStarting(Server_Game *game) = 0; -}; - -/** - * @brief Default lifecycle strategy that never intercepts the game start. - */ -class Server_DefaultLifecycleStrategy : public Server_GameLifecycleStrategy -{ -public: - StartAction onGameStarting(Server_Game *) override - { - return StartAction::ProceedNormal; - } -}; - -#endif diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/server_match_game_factory.h b/libcockatrice_network/libcockatrice/network/server/remote/game/server_match_game_factory.h deleted file mode 100644 index d5cf81cb6..000000000 --- a/libcockatrice_network/libcockatrice/network/server/remote/game/server_match_game_factory.h +++ /dev/null @@ -1,29 +0,0 @@ -#ifndef SERVER_MATCH_GAME_FACTORY_H -#define SERVER_MATCH_GAME_FACTORY_H - -#include - -class GameConfig; -class Server_Game; -class Server_AbstractUserInterface; - -/** - * @brief Abstract factory for creating match games, forward-looking for tournament modes. - * - * Implementations own the rules of how a match game is constructed, how player - * interfaces are resolved, and how games are registered with a room. - */ -class Server_MatchGameFactory -{ -public: - virtual ~Server_MatchGameFactory() = default; - - /** @brief Create a match game from @p config, storing the assigned game id in @p outGameId. */ - virtual Server_Game *createMatchGame(const GameConfig &config, int &outGameId) = 0; - /** @brief Resolve the user interface for @p playerName. */ - virtual Server_AbstractUserInterface *getUserInterface(const QString &playerName) = 0; - /** @brief Register @p game with the room that owns it. */ - virtual void addGameToRoom(Server_Game *game) = 0; -}; - -#endif diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/server_match_result_strategy.h b/libcockatrice_network/libcockatrice/network/server/remote/game/server_match_result_strategy.h deleted file mode 100644 index 51c696db1..000000000 --- a/libcockatrice_network/libcockatrice/network/server/remote/game/server_match_result_strategy.h +++ /dev/null @@ -1,38 +0,0 @@ -#ifndef SERVER_MATCH_RESULT_STRATEGY_H -#define SERVER_MATCH_RESULT_STRATEGY_H - -class Server_AbstractPlayer; -class Server_Game; - -/** - * @brief Strategy hook invoked when a game has finished to decide the match result. - * - * Subclasses can report the match outcome (e.g. to a tournament backend) and decide - * whether the game should be closed permanently; the default implementation never - * closes the game, preserving the normal return-to-lobby behavior. - */ -class Server_MatchResultStrategy -{ -public: - virtual ~Server_MatchResultStrategy() = default; - - /** - * @brief Called when a game has finished. - * @return Whether the game has been decided and should be closed. - */ - virtual bool onGameFinished(Server_Game *game, int playing, Server_AbstractPlayer *lastPlayer) = 0; -}; - -/** - * @brief Default match result strategy that never closes the game. - */ -class Server_NullMatchResultStrategy : public Server_MatchResultStrategy -{ -public: - bool onGameFinished(Server_Game *, int, Server_AbstractPlayer *) override - { - return false; - } -}; - -#endif diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/server_player.cpp b/libcockatrice_network/libcockatrice/network/server/remote/game/server_player.cpp index cafa33c07..d502fc7d6 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/game/server_player.cpp +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_player.cpp @@ -27,7 +27,6 @@ #include #include #include -#include #include #include #include @@ -251,14 +250,6 @@ Server_Player::cmdDeckSelect(const Command_DeckSelect &cmd, ResponseContainer &r Event_PlayerPropertiesChanged event; event.mutable_player_properties()->set_sideboard_locked(true); event.mutable_player_properties()->set_deck_hash(deck->getDeckHash().toStdString()); - const auto &playmat = deck->getPlaymat(); - auto *playmatParams = event.mutable_player_properties()->mutable_playmat_params(); - playmatParams->set_card_name(playmat.card.name.left(MAX_NAME_LENGTH).toStdString()); - playmatParams->set_card_provider_id(playmat.card.providerId.left(MAX_NAME_LENGTH).toStdString()); - playmatParams->set_margin_pct_l(playmat.params.marginPctL); - playmatParams->set_margin_pct_r(playmat.params.marginPctR); - playmatParams->set_vertical_offset(playmat.params.verticalOffset); - playmatParams->set_zoom(playmat.params.zoom); ges.enqueueGameEvent(event, playerId); Context_DeckSelect context; @@ -603,46 +594,6 @@ Server_Player::cmdReverseTurn(const Command_ReverseTurn &cmd, ResponseContainer return Server_AbstractParticipant::cmdReverseTurn(cmd, rc, ges); } -Response::ResponseCode -Server_Player::cmdSetPlaymat(const Command_SetPlaymat &cmd, ResponseContainer &rc, GameEventStorage &ges) -{ - Q_UNUSED(rc); - - if (!deck) { - return Response::RespContextError; - } - - const auto &pp = cmd.playmat_params(); - const auto rawName = QString::fromStdString(pp.card_name()); - const auto rawProviderId = QString::fromStdString(pp.card_provider_id()); - if (rawName.length() > MAX_NAME_LENGTH || rawProviderId.length() > MAX_NAME_LENGTH) { - return Response::RespInvalidData; - } - PlaymatInfo playmat; - playmat.card.name = rawName; - playmat.card.providerId = rawProviderId; - playmat.params.marginPctL = qBound(0.0, pp.margin_pct_l(), 0.95); - playmat.params.marginPctR = qBound(0.0, pp.margin_pct_r(), 0.95); - playmat.params.verticalOffset = qBound(0.0, pp.vertical_offset(), 1.0); - playmat.params.zoom = qBound(0.1, pp.zoom(), 4.0); - deck->setPlaymat(playmat); - - Event_PlayerPropertiesChanged event; - auto *props = event.mutable_player_properties(); - props->set_sideboard_locked(sideboardLocked); - props->set_deck_hash(deck->getDeckHash().toStdString()); - auto *playmatParams = props->mutable_playmat_params(); - playmatParams->set_card_name(playmat.card.name.toStdString()); - playmatParams->set_card_provider_id(playmat.card.providerId.toStdString()); - playmatParams->set_margin_pct_l(playmat.params.marginPctL); - playmatParams->set_margin_pct_r(playmat.params.marginPctR); - playmatParams->set_vertical_offset(playmat.params.verticalOffset); - playmatParams->set_zoom(playmat.params.zoom); - ges.enqueueGameEvent(event, playerId); - - return Response::RespOk; -} - void Server_Player::getInfo(ServerInfo_Player *info, Server_AbstractParticipant *recipient, bool omniscient, diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/server_player.h b/libcockatrice_network/libcockatrice/network/server/remote/game/server_player.h index c35428480..5925ed3c2 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/game/server_player.h +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_player.h @@ -62,8 +62,6 @@ public: cmdSetActivePhase(const Command_SetActivePhase &cmd, ResponseContainer &rc, GameEventStorage &ges) override; Response::ResponseCode cmdReverseTurn(const Command_ReverseTurn & /*cmd*/, ResponseContainer & /*rc*/, GameEventStorage &ges) override; - Response::ResponseCode - cmdSetPlaymat(const Command_SetPlaymat &cmd, ResponseContainer &rc, GameEventStorage &ges) override; Response::ResponseCode cmdChangeZoneProperties(const Command_ChangeZoneProperties &cmd, ResponseContainer &rc, GameEventStorage &ges) override; diff --git a/libcockatrice_network/libcockatrice/network/server/remote/server.h b/libcockatrice_network/libcockatrice/network/server/remote/server.h index 3d27f4210..2fca46593 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/server.h +++ b/libcockatrice_network/libcockatrice/network/server/remote/server.h @@ -38,8 +38,7 @@ enum AuthenticationResult UsernameInvalid, RegistrationRequired, UserIsInactive, - ClientIdRequired, - PasswordChangeRequired + ClientIdRequired }; class Server : public QObject @@ -92,10 +91,6 @@ public: { return QString(); } - virtual SessionEvent *getLoginSessionEvent() const - { - return nullptr; - } virtual QString getRequiredFeatures() const { return QString(); @@ -180,11 +175,6 @@ public: { return false; } - /// Called once per actual game start with how long bringing every player's - /// zones online took, so servers can spot deck sizes that wedge threads. - virtual void observeGameStartDurationMs(qint64 /* elapsedMs */) - { - } Server_DatabaseInterface *getDatabaseInterface() const; int getNextLocalGameId() diff --git a/libcockatrice_network/libcockatrice/network/server/remote/server_abstractuserinterface.cpp b/libcockatrice_network/libcockatrice/network/server/remote/server_abstractuserinterface.cpp index 31fa13d81..641be1eed 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/server_abstractuserinterface.cpp +++ b/libcockatrice_network/libcockatrice/network/server/remote/server_abstractuserinterface.cpp @@ -69,10 +69,6 @@ void Server_AbstractUserInterface::sendResponseContainer(const ResponseContainer } } -void Server_AbstractUserInterface::onLogin(ResponseContainer &) -{ -} - void Server_AbstractUserInterface::playerRemovedFromGame(Server_Game *game) { qDebug() << "Server_AbstractUserInterface::playerRemovedFromGame(): gameId =" << game->getGameId(); diff --git a/libcockatrice_network/libcockatrice/network/server/remote/server_abstractuserinterface.h b/libcockatrice_network/libcockatrice/network/server/remote/server_abstractuserinterface.h index 2da72c01d..b11260003 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/server_abstractuserinterface.h +++ b/libcockatrice_network/libcockatrice/network/server/remote/server_abstractuserinterface.h @@ -42,7 +42,6 @@ public: void playerRemovedFromGame(Server_Game *game); void playerAddedToGame(int gameId, int roomId, int playerId); void joinPersistentGames(ResponseContainer &rc); - virtual void onLogin(ResponseContainer &rc); QMap> getGames() const { diff --git a/libcockatrice_network/libcockatrice/network/server/remote/server_database_interface.h b/libcockatrice_network/libcockatrice/network/server/remote/server_database_interface.h index 1e4fc990b..b43dbde42 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/server_database_interface.h +++ b/libcockatrice_network/libcockatrice/network/server/remote/server_database_interface.h @@ -172,9 +172,6 @@ public: { return false; } - virtual void setForcePasswordChange(const QString & /* user */, bool /* force */) - { - } }; #endif diff --git a/libcockatrice_network/libcockatrice/network/server/remote/server_protocolhandler.cpp b/libcockatrice_network/libcockatrice/network/server/remote/server_protocolhandler.cpp index 8422d703d..c3686ddfa 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/server_protocolhandler.cpp +++ b/libcockatrice_network/libcockatrice/network/server/remote/server_protocolhandler.cpp @@ -1,6 +1,5 @@ #include "server_protocolhandler.h" -#include "game/game_config.h" #include "game/server_game.h" #include "game/server_player.h" #include "server_database_interface.h" @@ -134,22 +133,6 @@ void Server_ProtocolHandler::sendProtocolItem(const RoomEvent &item) Response::ResponseCode Server_ProtocolHandler::processSessionCommandContainer(const CommandContainer &cont, ResponseContainer &rc) { - const auto isPreAuthSessionCommand = [](SessionCommand::SessionCommandType type) { - switch (type) { - case SessionCommand::PING: - case SessionCommand::LOGIN: - case SessionCommand::REGISTER: - case SessionCommand::ACTIVATE: - case SessionCommand::FORGOT_PASSWORD_REQUEST: - case SessionCommand::FORGOT_PASSWORD_RESET: - case SessionCommand::FORGOT_PASSWORD_CHALLENGE: - case SessionCommand::REQUEST_PASSWORD_SALT: - return true; - default: - return false; - } - }; - Response::ResponseCode finalResponseCode = Response::RespOk; for (int i = cont.session_command_size() - 1; i >= 0; --i) { Response::ResponseCode resp = Response::RespInvalidCommand; @@ -158,38 +141,33 @@ Response::ResponseCode Server_ProtocolHandler::processSessionCommandContainer(co if (num != SessionCommand::PING) { // don't log ping commands logDebugMessage(getSafeDebugString(sc)); } - const auto commandType = static_cast(num); - if (authState == NotLoggedIn && !isPreAuthSessionCommand(commandType)) { - resp = Response::RespLoginNeeded; - } else { - switch (commandType) { - case SessionCommand::PING: - resp = cmdPing(sc.GetExtension(Command_Ping::ext), rc); - break; - case SessionCommand::LOGIN: - resp = cmdLogin(sc.GetExtension(Command_Login::ext), rc); - break; - case SessionCommand::MESSAGE: - resp = cmdMessage(sc.GetExtension(Command_Message::ext), rc); - break; - case SessionCommand::GET_GAMES_OF_USER: - resp = cmdGetGamesOfUser(sc.GetExtension(Command_GetGamesOfUser::ext), rc); - break; - case SessionCommand::GET_USER_INFO: - resp = cmdGetUserInfo(sc.GetExtension(Command_GetUserInfo::ext), rc); - break; - case SessionCommand::LIST_ROOMS: - resp = cmdListRooms(sc.GetExtension(Command_ListRooms::ext), rc); - break; - case SessionCommand::JOIN_ROOM: - resp = cmdJoinRoom(sc.GetExtension(Command_JoinRoom::ext), rc); - break; - case SessionCommand::LIST_USERS: - resp = cmdListUsers(sc.GetExtension(Command_ListUsers::ext), rc); - break; - default: - resp = processExtendedSessionCommand(num, sc, rc); - } + switch ((SessionCommand::SessionCommandType)num) { + case SessionCommand::PING: + resp = cmdPing(sc.GetExtension(Command_Ping::ext), rc); + break; + case SessionCommand::LOGIN: + resp = cmdLogin(sc.GetExtension(Command_Login::ext), rc); + break; + case SessionCommand::MESSAGE: + resp = cmdMessage(sc.GetExtension(Command_Message::ext), rc); + break; + case SessionCommand::GET_GAMES_OF_USER: + resp = cmdGetGamesOfUser(sc.GetExtension(Command_GetGamesOfUser::ext), rc); + break; + case SessionCommand::GET_USER_INFO: + resp = cmdGetUserInfo(sc.GetExtension(Command_GetUserInfo::ext), rc); + break; + case SessionCommand::LIST_ROOMS: + resp = cmdListRooms(sc.GetExtension(Command_ListRooms::ext), rc); + break; + case SessionCommand::JOIN_ROOM: + resp = cmdJoinRoom(sc.GetExtension(Command_JoinRoom::ext), rc); + break; + case SessionCommand::LIST_USERS: + resp = cmdListUsers(sc.GetExtension(Command_ListUsers::ext), rc); + break; + default: + resp = processExtendedSessionCommand(num, sc, rc); } if (resp != Response::RespOk) { finalResponseCode = resp; @@ -388,33 +366,6 @@ Response::ResponseCode Server_ProtocolHandler::processAdminCommandContainer(cons return finalResponseCode; } -Response::ResponseCode Server_ProtocolHandler::processDeveloperCommandContainer(const CommandContainer &cont, - ResponseContainer &rc) -{ - if (!userInfo) { - return Response::RespLoginNeeded; - } - if (!(userInfo->user_level() & ServerInfo_User::IsDeveloper)) { - return Response::RespLoginNeeded; - } - - resetIdleTimer(); - - Response::ResponseCode finalResponseCode = Response::RespOk; - for (int i = cont.developer_command_size() - 1; i >= 0; --i) { - Response::ResponseCode resp = Response::RespInvalidCommand; - const DeveloperCommand &sc = cont.developer_command(i); - const int num = getPbExtension(sc); - logDebugMessage(getSafeDebugString(sc)); - - resp = processExtendedDeveloperCommand(num, sc, rc); - if (resp != Response::RespOk) { - finalResponseCode = resp; - } - } - return finalResponseCode; -} - void Server_ProtocolHandler::processCommandContainer(const CommandContainer &cont) { // Command processing must be disabled after prepareDestroy() has been called. @@ -437,8 +388,6 @@ void Server_ProtocolHandler::processCommandContainer(const CommandContainer &con finalResponseCode = processModeratorCommandContainer(cont, responseContainer); } else if (cont.admin_command_size()) { finalResponseCode = processAdminCommandContainer(cont, responseContainer); - } else if (cont.developer_command_size()) { - finalResponseCode = processDeveloperCommandContainer(cont, responseContainer); } else { finalResponseCode = Response::RespInvalidCommand; } @@ -483,12 +432,11 @@ void Server_ProtocolHandler::pingClockTimeout() prepareDestroy(); } - // PrivLevel users, Moderators, Admins, and Developers are not subject to the server idle timeout policy + // PrivLevel users, Moderators, and Admins are not subject to the server idle timeout policy const bool hasPrivLevel = userInfo && QString::fromStdString(userInfo->privlevel()).toLower() != "none"; - const bool isStaff = - userInfo && (userInfo->user_level() & - (ServerInfo_User::IsModerator | ServerInfo_User::IsAdmin | ServerInfo_User::IsDeveloper)); - if (!hasPrivLevel && !isStaff) { + const bool isModOrAdmin = + userInfo && (userInfo->user_level() & (ServerInfo_User::IsModerator | ServerInfo_User::IsAdmin)); + if (!hasPrivLevel && !isModOrAdmin) { if ((server->getIdleClientTimeout() > 0) && (idleClientWarningSent)) { if (timeRunning - lastActionReceived > server->getIdleClientTimeout()) { prepareDestroy(); @@ -594,8 +542,6 @@ Response::ResponseCode Server_ProtocolHandler::cmdLogin(const Command_Login &cmd return Response::RespClientIdRequired; case UserIsInactive: return Response::RespAccountNotActivated; - case PasswordChangeRequired: - return Response::RespPasswordChangeRequired; default: authState = res; usingRealPassword = needsHash; @@ -616,11 +562,6 @@ Response::ResponseCode Server_ProtocolHandler::cmdLogin(const Command_Login &cmd event.set_message(server->getLoginMessage().toStdString()); rc.enqueuePostResponseItem(ServerMessage::SESSION_EVENT, prepareSessionEvent(event)); - SessionEvent *loginEvent = server->getLoginSessionEvent(); - if (loginEvent) { - rc.enqueuePostResponseItem(ServerMessage::SESSION_EVENT, loginEvent); - } - auto *re = new Response_Login; re->mutable_user_info()->CopyFrom(copyUserInfo(true)); @@ -646,7 +587,6 @@ Response::ResponseCode Server_ProtocolHandler::cmdLogin(const Command_Login &cmd joinPersistentGames(rc); databaseInterface->removeForgotPassword(userName); - onLogin(rc); rc.setResponseExtension(re); return Response::RespOk; } @@ -954,22 +894,10 @@ Server_ProtocolHandler::cmdCreateGame(const Command_CreateGame &cmd, Server_Room // When server doesn't permit registered users to exist, do not honor only-reg setting bool onlyRegisteredUsers = cmd.only_registered() && (server->permitUnregisteredUsers()); - GameConfig config{.creatorInfo = copyUserInfo(false), - .gameId = gameId, - .description = description, - .password = QString::fromStdString(cmd.password()), - .maxPlayers = static_cast(cmd.max_players()), - .gameTypes = gameTypes, - .onlyBuddies = cmd.only_buddies(), - .onlyRegistered = onlyRegisteredUsers, - .spectatorsAllowed = cmd.spectators_allowed(), - .spectatorsNeedPassword = cmd.spectators_need_password(), - .spectatorsCanTalk = cmd.spectators_can_talk(), - .spectatorsSeeEverything = cmd.spectators_see_everything(), - .startingLifeTotal = startingLifeTotal, - .shareDecklistsOnLoad = shareDecklistsOnLoad}; - - auto *game = new Server_Game(config, room); + auto *game = new Server_Game(copyUserInfo(false), gameId, description, QString::fromStdString(cmd.password()), + cmd.max_players(), gameTypes, cmd.only_buddies(), onlyRegisteredUsers, + cmd.spectators_allowed(), cmd.spectators_need_password(), cmd.spectators_can_talk(), + cmd.spectators_see_everything(), startingLifeTotal, shareDecklistsOnLoad, room); game->addPlayer(this, rc, asSpectator, asJudge, false); room->addGame(game); diff --git a/libcockatrice_network/libcockatrice/network/server/remote/server_protocolhandler.h b/libcockatrice_network/libcockatrice/network/server/remote/server_protocolhandler.h index 2c8efe50e..0d05b91c8 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/server_protocolhandler.h +++ b/libcockatrice_network/libcockatrice/network/server/remote/server_protocolhandler.h @@ -27,7 +27,6 @@ class CommandContainer; class SessionCommand; class ModeratorCommand; class AdminCommand; -class DeveloperCommand; class Command_Ping; class Command_Login; @@ -99,12 +98,6 @@ private: { return Response::RespFunctionNotAllowed; } - Response::ResponseCode processDeveloperCommandContainer(const CommandContainer &cont, ResponseContainer &rc); - virtual Response::ResponseCode - processExtendedDeveloperCommand(int /* cmdType */, const DeveloperCommand & /* cmd */, ResponseContainer & /* rc */) - { - return Response::RespFunctionNotAllowed; - } void resetIdleTimer(); private slots: @@ -136,7 +129,7 @@ public: return timeRunning - lastDataReceived; } bool addSaidMessageSize(int size); - virtual void processCommandContainer(const CommandContainer &cont); + void processCommandContainer(const CommandContainer &cont); void sendProtocolItem(const Response &item); void sendProtocolItem(const SessionEvent &item); diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/CMakeLists.txt b/libcockatrice_protocol/libcockatrice/protocol/pb/CMakeLists.txt index bc4814d5e..20a4cb08d 100644 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/CMakeLists.txt +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/CMakeLists.txt @@ -15,17 +15,9 @@ set(PROTO_FILES command_deck_del.proto command_deck_del_dir.proto command_deck_download.proto - command_deck_download_public.proto command_deck_list.proto - command_deck_list_other_user.proto command_deck_new_dir.proto command_deck_select.proto - command_deck_set_visibility.proto - command_deck_share_create.proto - command_deck_share_download.proto - command_deck_share_list.proto - command_deck_share_list_mine.proto - command_deck_share_remove.proto command_deck_upload.proto command_del_counter.proto command_delete_arrow.proto @@ -33,7 +25,6 @@ set(PROTO_FILES command_dump_zone.proto command_flip_card.proto command_game_say.proto - command_get_server_stats.proto command_inc_card_counter.proto command_inc_counter.proto command_kick_from_game.proto @@ -44,20 +35,10 @@ set(PROTO_FILES command_ready_start.proto command_replay_delete_match.proto command_replay_download.proto - command_replay_download_by_game_id.proto command_replay_get_code.proto command_replay_list.proto command_replay_modify_match.proto command_replay_submit_code.proto - command_report.proto - command_report_add_comment.proto - command_report_assign.proto - command_report_details.proto - command_report_list.proto - command_report_my_list.proto - command_report_resolve.proto - command_report_stats.proto - command_report_user_info.proto command_reveal_cards.proto command_reverse_turn.proto command_roll_die.proto @@ -65,7 +46,6 @@ set(PROTO_FILES command_set_card_attr.proto command_set_card_counter.proto command_set_counter.proto - command_set_playmat.proto command_set_sideboard_lock.proto command_set_sideboard_plan.proto command_shuffle.proto @@ -80,7 +60,6 @@ set(PROTO_FILES context_ready_start.proto context_set_sideboard_lock.proto context_undo_draw.proto - developer_commands.proto event_add_to_list.proto event_attach_card.proto event_change_zone_properties.proto @@ -100,6 +79,7 @@ set(PROTO_FILES event_game_log_notice.proto event_game_say.proto event_game_state_changed.proto + event_game_state_changed.proto event_join.proto event_join_room.proto event_kicked.proto @@ -145,16 +125,11 @@ set(PROTO_FILES response_card_art_rule_entry.proto response_deck_download.proto response_deck_list.proto - response_deck_share_create.proto - response_deck_share_download.proto - response_deck_share_list.proto - response_deck_share_list_mine.proto response_deck_upload.proto response_dump_zone.proto response_forgotpasswordrequest.proto response_get_admin_notes.proto response_get_games_of_user.proto - response_get_server_stats.proto response_get_user_info.proto response_join_room.proto response_list_users.proto @@ -162,19 +137,8 @@ set(PROTO_FILES response_password_salt.proto response_register.proto response_replay_download.proto - response_replay_download_by_game_id.proto response_replay_get_code.proto response_replay_list.proto - response_report_details.proto - response_report_list.proto - response_report_my_list.proto - response_report_stats.proto - response_report_user_info.proto - response_moderator_last_logins.proto - response_remove_user_avatar.proto - response_reset_user_password.proto - response_user_alts.proto - response_user_sessions.proto response_viewlog_history.proto response_warn_history.proto response_warn_list.proto @@ -187,22 +151,16 @@ set(PROTO_FILES serverinfo_cardcounter.proto serverinfo_chat_message.proto serverinfo_counter.proto - serverinfo_deck_share_item.proto - serverinfo_deck_share_summary.proto serverinfo_deckstorage.proto serverinfo_game.proto serverinfo_gametype.proto - serverinfo_moderator_login.proto serverinfo_player.proto serverinfo_playerping.proto serverinfo_playerproperties.proto serverinfo_replay.proto serverinfo_replay_match.proto - serverinfo_report.proto serverinfo_room.proto serverinfo_user.proto - serverinfo_user_alt.proto - serverinfo_user_session.proto serverinfo_warning.proto serverinfo_zone.proto session_commands.proto diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/admin_commands.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/admin_commands.proto index f1f85e376..8faaec2d2 100644 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/admin_commands.proto +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/admin_commands.proto @@ -5,7 +5,6 @@ message AdminCommand { SHUTDOWN_SERVER = 1001; RELOAD_CONFIG = 1002; ADJUST_MOD = 1003; - RESET_USER_PASSWORD = 1016; } extensions 100 to max; } @@ -37,12 +36,4 @@ message Command_AdjustMod { required string user_name = 1; optional bool should_be_mod = 2; optional bool should_be_judge = 3; - optional bool should_be_developer = 4; -} - -message Command_ResetUserPassword { - extend AdminCommand { - optional Command_ResetUserPassword ext = 1016; - } - optional string user_name = 1; } diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/command_deck_download_public.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/command_deck_download_public.proto deleted file mode 100644 index a5592ef51..000000000 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/command_deck_download_public.proto +++ /dev/null @@ -1,9 +0,0 @@ -syntax = "proto2"; -import "session_commands.proto"; - -message Command_DeckDownloadPublic { - extend SessionCommand { - optional Command_DeckDownloadPublic ext = 1031; - } - optional uint32 deck_id = 1; -} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/command_deck_list_other_user.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/command_deck_list_other_user.proto deleted file mode 100644 index 2459608e2..000000000 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/command_deck_list_other_user.proto +++ /dev/null @@ -1,9 +0,0 @@ -syntax = "proto2"; -import "session_commands.proto"; - -message Command_DeckListOtherUser { - extend SessionCommand { - optional Command_DeckListOtherUser ext = 1029; - } - optional string user_name = 1; -} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/command_deck_set_visibility.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/command_deck_set_visibility.proto deleted file mode 100644 index 3ada87973..000000000 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/command_deck_set_visibility.proto +++ /dev/null @@ -1,13 +0,0 @@ -syntax = "proto2"; -import "session_commands.proto"; - -message Command_DeckSetVisibility { - extend SessionCommand { - optional Command_DeckSetVisibility ext = 1030; - } - // Set the public visibility of a single deck (mutually exclusive with folder_path). - optional uint32 deck_id = 1; - // Set the public visibility of a folder (all decks under it inherit). - optional string folder_path = 2; - optional bool is_public = 3; -} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/command_deck_share_create.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/command_deck_share_create.proto deleted file mode 100644 index 9cebf8eae..000000000 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/command_deck_share_create.proto +++ /dev/null @@ -1,24 +0,0 @@ -syntax = "proto2"; -import "session_commands.proto"; - -message DeckShareItem { - // Reference an existing deck in the sharer's personal deck storage. - // Mutually exclusive with deck_list. - optional uint32 deck_id = 1; - // Inline deck content in the native format. - // Mutually exclusive with deck_id. - optional string deck_list = 2; - // Color identity of the deck (e.g. "WUBRG"), computed by the sharing client. - optional string color_identity = 3; -} - -message Command_DeckShareCreate { - extend SessionCommand { - optional Command_DeckShareCreate ext = 1026; - } - optional string name = 1; - repeated DeckShareItem items = 2; - // Path of a folder in the sharer's personal deck storage. When set, all - // decks in that folder are shared (resolved by the server). - optional string folder_path = 3; -} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/command_deck_share_download.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/command_deck_share_download.proto deleted file mode 100644 index 251a662b3..000000000 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/command_deck_share_download.proto +++ /dev/null @@ -1,10 +0,0 @@ -syntax = "proto2"; -import "session_commands.proto"; - -message Command_DeckShareDownload { - extend SessionCommand { - optional Command_DeckShareDownload ext = 1028; - } - optional string token = 1; - optional uint32 item_id = 2; -} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/command_deck_share_list.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/command_deck_share_list.proto deleted file mode 100644 index b75fd65f9..000000000 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/command_deck_share_list.proto +++ /dev/null @@ -1,9 +0,0 @@ -syntax = "proto2"; -import "session_commands.proto"; - -message Command_DeckShareList { - extend SessionCommand { - optional Command_DeckShareList ext = 1027; - } - optional string token = 1; -} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/command_deck_share_list_mine.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/command_deck_share_list_mine.proto deleted file mode 100644 index 75bc30714..000000000 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/command_deck_share_list_mine.proto +++ /dev/null @@ -1,10 +0,0 @@ -syntax = "proto2"; -import "session_commands.proto"; - -// Requests the list of share bundles created by the calling user, so they can -// be reviewed and revoked before they expire. -message Command_DeckShareListMine { - extend SessionCommand { - optional Command_DeckShareListMine ext = 1032; - } -} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/command_deck_share_remove.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/command_deck_share_remove.proto deleted file mode 100644 index 348996da9..000000000 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/command_deck_share_remove.proto +++ /dev/null @@ -1,11 +0,0 @@ -syntax = "proto2"; -import "session_commands.proto"; - -// Revokes one of the calling user's own share bundles. The referenced items -// are removed by cascade. -message Command_DeckShareRemove { - extend SessionCommand { - optional Command_DeckShareRemove ext = 1033; - } - optional uint32 share_id = 1; -} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/command_deck_upload.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/command_deck_upload.proto index 1a5d44e9e..63d9c80ef 100644 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/command_deck_upload.proto +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/command_deck_upload.proto @@ -8,10 +8,4 @@ message Command_DeckUpload { optional string path = 1; // to upload a new deck optional uint32 deck_id = 2; // to replace an existing deck optional string deck_list = 3; - optional bool is_public = 4; // mark the deck public on upload (publish) - // The server derives the banner card and tags from deck_list, so clients only - // need to send the color identity, which cannot be computed server-side. - reserved 5, 6, 8; - reserved "banner_card_name", "banner_card_provider", "tags"; - optional string color_identity = 7; } diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/command_get_server_stats.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/command_get_server_stats.proto deleted file mode 100644 index 33c56293b..000000000 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/command_get_server_stats.proto +++ /dev/null @@ -1,8 +0,0 @@ -syntax = "proto2"; -import "developer_commands.proto"; - -message Command_GetServerStats { - extend DeveloperCommand { - optional Command_GetServerStats ext = 1000; - } -} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/command_replay_download_by_game_id.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/command_replay_download_by_game_id.proto deleted file mode 100644 index 8fa61a517..000000000 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/command_replay_download_by_game_id.proto +++ /dev/null @@ -1,9 +0,0 @@ -syntax = "proto2"; -import "moderator_commands.proto"; - -message Command_ReplayDownloadByGameId { - extend ModeratorCommand { - optional Command_ReplayDownloadByGameId ext = 1203; - } - required sint32 game_id = 1; -} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/command_report.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/command_report.proto deleted file mode 100644 index 3a8c1548e..000000000 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/command_report.proto +++ /dev/null @@ -1,13 +0,0 @@ -syntax = "proto2"; -import "session_commands.proto"; - -message Command_Report { - extend SessionCommand { - optional Command_Report ext = 1200; - } - optional string reported_user = 1; - optional int32 game_id = 2; - optional string category = 3; - optional string description = 4; - optional string chat_log = 5; -} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/command_report_add_comment.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/command_report_add_comment.proto deleted file mode 100644 index 1fc5696e5..000000000 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/command_report_add_comment.proto +++ /dev/null @@ -1,10 +0,0 @@ -syntax = "proto2"; -import "session_commands.proto"; - -message Command_ReportAddComment { - extend SessionCommand { - optional Command_ReportAddComment ext = 1205; - } - required int32 report_id = 1; - required string comment = 2; -} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/command_report_assign.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/command_report_assign.proto deleted file mode 100644 index 2b72205f8..000000000 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/command_report_assign.proto +++ /dev/null @@ -1,9 +0,0 @@ -syntax = "proto2"; -import "moderator_commands.proto"; - -message Command_ReportAssign { - extend ModeratorCommand { - optional Command_ReportAssign ext = 1201; - } - required int32 report_id = 1; -} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/command_report_details.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/command_report_details.proto deleted file mode 100644 index e3ae8cfb4..000000000 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/command_report_details.proto +++ /dev/null @@ -1,9 +0,0 @@ -syntax = "proto2"; -import "session_commands.proto"; - -message Command_ReportDetails { - extend SessionCommand { - optional Command_ReportDetails ext = 1206; - } - required int32 report_id = 1; -} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/command_report_list.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/command_report_list.proto deleted file mode 100644 index 6993de1a7..000000000 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/command_report_list.proto +++ /dev/null @@ -1,11 +0,0 @@ -syntax = "proto2"; -import "moderator_commands.proto"; - -message Command_ReportList { - extend ModeratorCommand { - optional Command_ReportList ext = 1200; - } - optional bool unresolved_only = 1; - optional uint32 offset = 2 [default = 0]; - optional uint32 limit = 3 [default = 100]; -} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/command_report_my_list.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/command_report_my_list.proto deleted file mode 100644 index 7ee18a65d..000000000 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/command_report_my_list.proto +++ /dev/null @@ -1,8 +0,0 @@ -syntax = "proto2"; -import "session_commands.proto"; - -message Command_ReportMyList { - extend SessionCommand { - optional Command_ReportMyList ext = 1204; - } -} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/command_report_resolve.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/command_report_resolve.proto deleted file mode 100644 index 312ca0f9d..000000000 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/command_report_resolve.proto +++ /dev/null @@ -1,11 +0,0 @@ -syntax = "proto2"; -import "moderator_commands.proto"; - -message Command_ReportResolve { - extend ModeratorCommand { - optional Command_ReportResolve ext = 1202; - } - required int32 report_id = 1; - optional string resolution_note = 2; - optional bool dismissed = 3; -} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/command_report_stats.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/command_report_stats.proto deleted file mode 100644 index b3c6eb1a9..000000000 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/command_report_stats.proto +++ /dev/null @@ -1,8 +0,0 @@ -syntax = "proto2"; -import "moderator_commands.proto"; - -message Command_ReportStats { - extend ModeratorCommand { - optional Command_ReportStats ext = 1205; - } -} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/command_report_user_info.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/command_report_user_info.proto deleted file mode 100644 index 8f83cb5a3..000000000 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/command_report_user_info.proto +++ /dev/null @@ -1,9 +0,0 @@ -syntax = "proto2"; -import "moderator_commands.proto"; - -message Command_ReportUserInfo { - extend ModeratorCommand { - optional Command_ReportUserInfo ext = 1204; - } - optional string user_name = 1; -} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/command_set_playmat.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/command_set_playmat.proto deleted file mode 100644 index ad9a29a44..000000000 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/command_set_playmat.proto +++ /dev/null @@ -1,9 +0,0 @@ -syntax = "proto2"; -import "game_commands.proto"; -import "serverinfo_playerproperties.proto"; -message Command_SetPlaymat { - extend GameCommand { - optional Command_SetPlaymat ext = 1035; - } - optional ServerInfo_PlayerProperties.PlaymatParams playmat_params = 1; -} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/commands.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/commands.proto index 964407819..b6eaf6733 100644 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/commands.proto +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/commands.proto @@ -4,7 +4,6 @@ import "game_commands.proto"; import "room_commands.proto"; import "moderator_commands.proto"; import "admin_commands.proto"; -import "developer_commands.proto"; message CommandContainer { optional uint64 cmd_id = 1; @@ -17,5 +16,4 @@ message CommandContainer { repeated RoomCommand room_command = 102; repeated ModeratorCommand moderator_command = 103; repeated AdminCommand admin_command = 104; - repeated DeveloperCommand developer_command = 105; } diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/developer_commands.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/developer_commands.proto deleted file mode 100644 index bed47d44c..000000000 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/developer_commands.proto +++ /dev/null @@ -1,8 +0,0 @@ -syntax = "proto2"; -message DeveloperCommand { - enum DeveloperCommandType { - GET_SERVER_STATS = 1000; - VIEWLOG_HISTORY = 1001; - } - extensions 100 to max; -} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/event_notify_user.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/event_notify_user.proto index b722dfc71..3a90d278b 100644 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/event_notify_user.proto +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/event_notify_user.proto @@ -9,8 +9,6 @@ message Event_NotifyUser { WARNING = 2; IDLEWARNING = 3; CUSTOM = 4; - REPORT_RESOLVED = 5; - REPORT_COMMENT = 6; } extend SessionEvent { diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/game_commands.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/game_commands.proto index 2e5b88978..8ecf7ec9c 100644 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/game_commands.proto +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/game_commands.proto @@ -175,11 +175,6 @@ message GameCommand { /// Server: Server_Player::cmdReverseTurn /// Client: reflected via subsequent turn events REVERSE_TURN = 1034; - - /// Set the player's playmat independently of the deck. - /// Server: Server_Player::cmdSetPlaymat - /// Client: reflected via player properties changed event - SET_PLAYMAT = 1035; } extensions 100 to max; diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/moderator_commands.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/moderator_commands.proto index 4f1e80c27..ca46e4dd7 100644 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/moderator_commands.proto +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/moderator_commands.proto @@ -1,5 +1,4 @@ syntax = "proto2"; -import "developer_commands.proto"; message ModeratorCommand { enum ModeratorCommandType { BAN_FROM_SERVER = 1000; @@ -15,17 +14,6 @@ message ModeratorCommand { ADD_CARD_ART_RULE = 1010; REMOVE_CARD_ART_RULE = 1011; LIST_CARD_ART_RULES = 1012; - GET_USER_SESSIONS = 1013; - GET_USER_ALTS = 1014; - GET_MODERATOR_LAST_LOGINS = 1015; - RESET_USER_PASSWORD = 1016; - REMOVE_USER_AVATAR = 1017; - REPORT_LIST = 1200; - REPORT_ASSIGN = 1201; - REPORT_RESOLVE = 1202; - REPLAY_DOWNLOAD_BY_GAME_ID = 1203; - REPORT_USER_INFO = 1204; - REPORT_STATS = 1205; } extensions 100 to max; } @@ -81,9 +69,6 @@ message Command_ViewLogHistory { extend ModeratorCommand { optional Command_ViewLogHistory ext = 1005; } - extend DeveloperCommand { - optional Command_ViewLogHistory dev_ext = 1001; - } optional string user_name = 1; // user that created message optional string ip_address = 2; // ip address of user that created message optional string game_name = 3; // client id of user that created the message @@ -150,31 +135,3 @@ message Command_ListCardArtRules { optional Command_ListCardArtRules ext = 1012; } } - -message Command_GetUserSessions { - extend ModeratorCommand { - optional Command_GetUserSessions ext = 1013; - } - optional string user_name = 1; - optional uint32 limit = 2 [default = 110]; -} - -message Command_GetUserAlts { - extend ModeratorCommand { - optional Command_GetUserAlts ext = 1014; - } - optional string user_name = 1; -} - -message Command_GetModeratorLastLogins { - extend ModeratorCommand { - optional Command_GetModeratorLastLogins ext = 1015; - } -} - -message Command_RemoveUserAvatar { - extend ModeratorCommand { - optional Command_RemoveUserAvatar ext = 1017; - } - optional string user_name = 1; -} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/response.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/response.proto index caf9febde..e1f415ce6 100644 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/response.proto +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/response.proto @@ -53,7 +53,6 @@ message Response { RespClientUpdateRequired = 35; // Client is missing features that the server is requiring RespServerFull = 36; // Server user limit reached RespEmailBlackListed = 37; // Server has blocked the email address provided for registration for some reason - RespPasswordChangeRequired = 38; // Server requires the user to change their password before proceeding } // Type of response, used to route handling on the client @@ -77,14 +76,9 @@ message Response { FORGOT_PASSWORD_REQUEST = 1016; // Response to password reset request PASSWORD_SALT = 1017; // Response containing password salt GET_ADMIN_NOTES = 1018; // Response with admin notes - GET_SERVER_STATS = 1019; // Response with server status statistics REPLAY_LIST = 1100; // Response listing replays REPLAY_DOWNLOAD = 1101; // Response for replay download REPLAY_GET_CODE = 1102; // Response containing replay code - DECK_SHARE_CREATE = 1103; // Response to deck share creation - DECK_SHARE_LIST = 1104; // Response listing shared decks - DECK_SHARE_DOWNLOAD = 1105; // Response for shared deck download - DECK_SHARE_LIST_MINE = 1106; // Response listing the caller's own shares CARD_ART_RULE_LIST = 1200; // Response containing a list of card art rules } diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/response_deck_share_create.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/response_deck_share_create.proto deleted file mode 100644 index 574c02eb6..000000000 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/response_deck_share_create.proto +++ /dev/null @@ -1,11 +0,0 @@ -syntax = "proto2"; -import "response.proto"; - -message Response_DeckShareCreate { - extend Response { - optional Response_DeckShareCreate ext = 1103; - } - optional string token = 1; - optional uint64 expires_at = 2; - optional uint32 item_count = 3; -} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/response_deck_share_download.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/response_deck_share_download.proto deleted file mode 100644 index def0ccbe6..000000000 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/response_deck_share_download.proto +++ /dev/null @@ -1,9 +0,0 @@ -syntax = "proto2"; -import "response.proto"; - -message Response_DeckShareDownload { - extend Response { - optional Response_DeckShareDownload ext = 1105; - } - optional string deck = 1; -} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/response_deck_share_list.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/response_deck_share_list.proto deleted file mode 100644 index 3edffa8ac..000000000 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/response_deck_share_list.proto +++ /dev/null @@ -1,12 +0,0 @@ -syntax = "proto2"; -import "response.proto"; -import "serverinfo_deck_share_item.proto"; - -message Response_DeckShareList { - extend Response { - optional Response_DeckShareList ext = 1104; - } - optional string name = 1; - optional uint64 expires_at = 2; - repeated ServerInfo_DeckShareItem items = 3; -} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/response_deck_share_list_mine.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/response_deck_share_list_mine.proto deleted file mode 100644 index e3cbf83b9..000000000 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/response_deck_share_list_mine.proto +++ /dev/null @@ -1,10 +0,0 @@ -syntax = "proto2"; -import "response.proto"; -import "serverinfo_deck_share_summary.proto"; - -message Response_DeckShareListMine { - extend Response { - optional Response_DeckShareListMine ext = 1106; - } - repeated ServerInfo_DeckShareSummary shares = 1; -} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/response_get_server_stats.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/response_get_server_stats.proto deleted file mode 100644 index bb8ff3c43..000000000 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/response_get_server_stats.proto +++ /dev/null @@ -1,41 +0,0 @@ -syntax = "proto2"; -import "response.proto"; - -message CommandStats { - optional uint32 kind_index = 1; // 0=session, 1=room, 2=game, 3=moderator, 4=admin, 5=developer - optional uint32 extension_number = 2; // protobuf extension number within the kind - optional string command_name = 3; // e.g. "session/Command_Ping" - optional uint64 count = 4; // number of times observed - optional uint64 total_ms = 5; // cumulative processing milliseconds -} - -message Response_GetServerStats { - extend Response { - optional Response_GetServerStats ext = 1220; - } - - optional uint64 users_count = 1; - optional uint64 mods_count = 2; - optional uint64 games_count = 3; - - // Traffic recorded during the last status update tick - optional uint64 tx_bytes = 4; - optional uint64 rx_bytes = 5; - - optional uint64 uptime_secs = 6; - optional uint64 timest = 7; // unix timestamp of the snapshot - - // Live metrics from MetricsRegistry (reset on server restart) - optional uint64 cards_in_games = 8; - optional uint64 eventloop_stalls_total = 9; - optional uint64 eventloop_last_stall_ms = 10; - optional uint64 eventloop_max_stall_ms = 11; - optional uint64 total_commands = 12; - optional uint64 total_command_time_ms = 13; - optional int32 active_command_types = 14; - optional uint64 game_start_count = 15; - optional uint64 game_start_total_ms = 16; - - // Per-command breakdown (only types with count > 0) - repeated CommandStats command_stats = 20; -} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/response_moderator_last_logins.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/response_moderator_last_logins.proto deleted file mode 100644 index 6288880c4..000000000 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/response_moderator_last_logins.proto +++ /dev/null @@ -1,10 +0,0 @@ -syntax = "proto2"; -import "response.proto"; -import "serverinfo_moderator_login.proto"; - -message Response_ModeratorLastLogins { - extend Response { - optional Response_ModeratorLastLogins ext = 1217; - } - repeated ServerInfo_ModeratorLogin logins = 1; -} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/response_remove_user_avatar.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/response_remove_user_avatar.proto deleted file mode 100644 index e5697d4e0..000000000 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/response_remove_user_avatar.proto +++ /dev/null @@ -1,9 +0,0 @@ -syntax = "proto2"; -import "response.proto"; - -message Response_RemoveUserAvatar { - extend Response { - optional Response_RemoveUserAvatar ext = 1219; - } - optional string user_name = 1; -} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/response_replay_download_by_game_id.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/response_replay_download_by_game_id.proto deleted file mode 100644 index 77a5feb81..000000000 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/response_replay_download_by_game_id.proto +++ /dev/null @@ -1,10 +0,0 @@ -syntax = "proto2"; -import "response.proto"; - -message Response_ReplayDownloadByGameId { - extend Response { - optional Response_ReplayDownloadByGameId ext = 1203; - } - optional bytes replay_data = 1; - optional sint32 replay_id = 2; -} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/response_report_details.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/response_report_details.proto deleted file mode 100644 index 463beaefc..000000000 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/response_report_details.proto +++ /dev/null @@ -1,10 +0,0 @@ -syntax = "proto2"; -import "response.proto"; -import "serverinfo_report.proto"; - -message Response_ReportDetails { - extend Response { - optional Response_ReportDetails ext = 1214; - } - optional ServerInfo_Report report = 1; -} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/response_report_list.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/response_report_list.proto deleted file mode 100644 index 73d9fbef8..000000000 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/response_report_list.proto +++ /dev/null @@ -1,11 +0,0 @@ -syntax = "proto2"; -import "response.proto"; -import "serverinfo_report.proto"; - -message Response_ReportList { - extend Response { - optional Response_ReportList ext = 1210; - } - repeated ServerInfo_Report reports = 1; - optional uint32 total_count = 2; -} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/response_report_my_list.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/response_report_my_list.proto deleted file mode 100644 index fdd02a3f1..000000000 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/response_report_my_list.proto +++ /dev/null @@ -1,10 +0,0 @@ -syntax = "proto2"; -import "response.proto"; -import "serverinfo_report.proto"; - -message Response_ReportMyList { - extend Response { - optional Response_ReportMyList ext = 1213; - } - repeated ServerInfo_Report reports = 1; -} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/response_report_stats.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/response_report_stats.proto deleted file mode 100644 index 8bd2cf766..000000000 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/response_report_stats.proto +++ /dev/null @@ -1,36 +0,0 @@ -syntax = "proto2"; -import "response.proto"; - -message Response_ReportStats { - extend Response { - optional Response_ReportStats ext = 1212; - } - - optional int32 total_reports = 1; - optional int32 total_pending = 2; - optional int32 total_assigned = 3; - optional int32 total_resolved = 4; - - optional int32 reports_last_24h = 5; - optional int32 reports_last_7d = 6; - optional int32 reports_last_30d = 7; - - optional double avg_resolution_hours = 8; - - optional int32 reports_this_week = 9; - optional int32 reports_last_week = 10; - - repeated ReportCategoryCount category_counts = 11; - repeated ReportTopUser top_reported_users = 12; - repeated ReportTopUser top_reporters = 13; -} - -message ReportCategoryCount { - optional string category = 1; - optional int32 count = 2; -} - -message ReportTopUser { - optional string user_name = 1; - optional int32 count = 2; -} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/response_report_user_info.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/response_report_user_info.proto deleted file mode 100644 index 5f9e8ec38..000000000 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/response_report_user_info.proto +++ /dev/null @@ -1,21 +0,0 @@ -syntax = "proto2"; -import "response.proto"; -import "serverinfo_report.proto"; - -message Response_ReportUserInfo { - extend Response { - optional Response_ReportUserInfo ext = 1211; - } - - optional string user_name = 1; - optional int32 total_reports = 2; - optional int32 total_bans = 3; - optional int32 total_warns = 4; - optional int64 registration_time = 5; - optional bool is_admin = 6; - optional bool is_active = 7; - optional string admin_notes = 8; - repeated ServerInfo_Report recent_reports = 9; - // Last known login of the user, epoch seconds; 0 = unknown. - optional int64 last_login = 10; -} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/response_reset_user_password.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/response_reset_user_password.proto deleted file mode 100644 index aa379e573..000000000 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/response_reset_user_password.proto +++ /dev/null @@ -1,12 +0,0 @@ -syntax = "proto2"; -import "response.proto"; - -message Response_ResetUserPassword { - extend Response { - optional Response_ResetUserPassword ext = 1218; - } - optional string user_name = 1; - // The generated temporary password, shown to the moderator who - // requested the reset. The affected user must change it on first login. - optional string temporary_password = 2; -} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/response_user_alts.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/response_user_alts.proto deleted file mode 100644 index 6bc48d6cb..000000000 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/response_user_alts.proto +++ /dev/null @@ -1,10 +0,0 @@ -syntax = "proto2"; -import "response.proto"; -import "serverinfo_user_alt.proto"; - -message Response_UserAlts { - extend Response { - optional Response_UserAlts ext = 1216; - } - repeated ServerInfo_UserAlt alts = 1; -} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/response_user_sessions.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/response_user_sessions.proto deleted file mode 100644 index 7174fd3f7..000000000 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/response_user_sessions.proto +++ /dev/null @@ -1,10 +0,0 @@ -syntax = "proto2"; -import "response.proto"; -import "serverinfo_user_session.proto"; - -message Response_UserSessions { - extend Response { - optional Response_UserSessions ext = 1215; - } - repeated ServerInfo_UserSession sessions = 1; -} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/response_warn_list.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/response_warn_list.proto index cddd08a51..d48352529 100644 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/response_warn_list.proto +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/response_warn_list.proto @@ -8,7 +8,4 @@ message Response_WarnList { repeated string warning = 1; optional string user_name = 2; optional string user_clientid = 3; - // Recommended starting intervention level per warning category, - // aligned by index with `warning`. Absent or shorter lists default to 1. - repeated uint32 warning_il = 4; } diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_deck_share_item.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_deck_share_item.proto deleted file mode 100644 index bac025419..000000000 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_deck_share_item.proto +++ /dev/null @@ -1,10 +0,0 @@ -syntax = "proto2"; - -message ServerInfo_DeckShareItem { - optional uint32 id = 1; - optional string name = 2; - repeated string tags = 3; - optional string banner_card = 4; - optional string game_format = 5; - optional string color_identity = 6; -} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_deck_share_summary.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_deck_share_summary.proto deleted file mode 100644 index 43273a54f..000000000 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_deck_share_summary.proto +++ /dev/null @@ -1,10 +0,0 @@ -syntax = "proto2"; - -// A share bundle created by a user, as reported by a "list my shares" query. -message ServerInfo_DeckShareSummary { - optional uint32 id = 1; - optional string name = 2; - optional uint64 creation_time = 3; - optional uint64 expires_at = 4; - optional uint32 item_count = 5; -} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_deckstorage.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_deckstorage.proto index b04d676d8..16e3f28e3 100644 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_deckstorage.proto +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_deckstorage.proto @@ -1,21 +1,10 @@ syntax = "proto2"; message ServerInfo_DeckStorage_File { optional uint32 creation_time = 1; - optional bool is_public = 2; - // Preview metadata computed by the uploading client, so other clients can - // render this deck (e.g. in a visual storage grid) without downloading the - // full deck list. Empty for decks uploaded before the metadata columns. - optional string banner_card_name = 3; - optional string banner_card_provider = 4; - optional string color_identity = 5; - // Tag names associated with the deck. Empty for decks uploaded before the - // tags column existed. - repeated string tags = 6; } message ServerInfo_DeckStorage_Folder { repeated ServerInfo_DeckStorage_TreeItem items = 1; - optional bool is_public = 2; } message ServerInfo_DeckStorage_TreeItem { diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_game.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_game.proto index 9989ae18a..9a56e034c 100644 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_game.proto +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_game.proto @@ -62,7 +62,4 @@ message ServerInfo_Game { // whether the game is closed. Closed games are finished and can't be interacted with optional bool closed = 52; - - // the current host of the game, which may differ from the creator after a host transfer - optional ServerInfo_User host_info = 53; } diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_moderator_login.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_moderator_login.proto deleted file mode 100644 index 21db2d3fa..000000000 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_moderator_login.proto +++ /dev/null @@ -1,11 +0,0 @@ -syntax = "proto2"; - -/** - * The last login date of a staff member (moderator/judge/admin). - * Used by the moderation "Moderator Last Logins" tool. - */ -message ServerInfo_ModeratorLogin { - optional string user_name = 1; // staff account name - optional uint64 last_login = 2; // last known login, epoch seconds; 0 = unknown - optional uint32 user_level = 3; // ServerInfo_User::UserLevelFlag mask (moderator/judge/admin) -} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_playerproperties.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_playerproperties.proto index ae19e4018..cdd0ee42c 100644 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_playerproperties.proto +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_playerproperties.proto @@ -2,15 +2,6 @@ syntax = "proto2"; import "serverinfo_user.proto"; message ServerInfo_PlayerProperties { - message PlaymatParams { - optional string card_name = 1; - optional string card_provider_id = 2; - optional double margin_pct_l = 3 [default = 0.07]; - optional double margin_pct_r = 4 [default = 0.07]; - optional double vertical_offset = 5 [default = 0.33]; - optional double zoom = 6 [default = 1.0]; - } - optional sint32 player_id = 1; optional ServerInfo_User user_info = 2; optional bool spectator = 3; @@ -20,5 +11,4 @@ message ServerInfo_PlayerProperties { optional sint32 ping_seconds = 7; optional bool sideboard_locked = 8; optional bool judge = 9; - optional PlaymatParams playmat_params = 10; } diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_report.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_report.proto deleted file mode 100644 index a145fd855..000000000 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_report.proto +++ /dev/null @@ -1,36 +0,0 @@ -syntax = "proto2"; - -message ServerInfo_ReportComment { - optional string author_name = 1; - optional string comment_text = 2; - optional int64 comment_time = 3; - optional bool is_moderator = 4; -} - -message ServerInfo_Report { - optional int32 report_id = 1; - - optional string reporter_name = 2; - optional string reported_user_name = 3; - - optional int32 game_id = 4; - optional int32 replay_id = 5; - optional int32 room_id = 6; - - optional string category = 7; - optional string status = 8; - - optional string description = 9; - - optional int64 report_time = 10; - - optional string assigned_mod_name = 11; - - optional int64 resolution_time = 12; - - repeated ServerInfo_ReportComment comments = 13; - - optional string chat_log = 14; - - optional string resolution_note = 15; -} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_user.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_user.proto index ea3f56705..98cc3ce6a 100644 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_user.proto +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_user.proto @@ -8,7 +8,6 @@ message ServerInfo_User { IsModerator = 4; IsAdmin = 8; IsJudge = 16; - IsDeveloper = 32; }; message PawnColorsOverride { optional string left_side = 1; diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_user_alt.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_user_alt.proto deleted file mode 100644 index 191dd5063..000000000 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_user_alt.proto +++ /dev/null @@ -1,16 +0,0 @@ -syntax = "proto2"; - -/** - * An account that shares an IP address, client id or eMail address with - * the account being investigated. Used by the moderation "Get User Alts" tool. - */ -message ServerInfo_UserAlt { - optional string user_name = 1; // account name - optional string email = 2; // registration eMail - optional string clientid = 3; // client id - optional uint64 registration_time = 4; // account registration, epoch seconds - optional uint64 last_login = 5; // last known login, epoch seconds; 0 = unknown - 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 -} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_user_session.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_user_session.proto deleted file mode 100644 index 7ce240050..000000000 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_user_session.proto +++ /dev/null @@ -1,14 +0,0 @@ -syntax = "proto2"; - -/** - * A single login session of a user on the server, as stored in the - * sessions table. Used by the moderation "Get User Sessions" tool. - */ -message ServerInfo_UserSession { - optional string user_name = 1; // account that was logged in - optional string ip_address = 2; // IP address used for the session - optional string clientid = 3; // client id used for the session - optional uint64 start_time = 4; // session start, epoch seconds - optional uint64 end_time = 5; // session end, epoch seconds; 0 = still active - optional string connection_type = 6; // "tcp" or "websocket" -} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/session_commands.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/session_commands.proto index 4b7fe9c85..9d207c711 100644 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/session_commands.proto +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/session_commands.proto @@ -28,25 +28,12 @@ message SessionCommand { FORGOT_PASSWORD_CHALLENGE = 1023; REQUEST_PASSWORD_SALT = 1024; SET_CARD_ART_PARAMS = 1025; - DECK_SHARE_CREATE = 1026; - DECK_SHARE_LIST = 1027; - DECK_SHARE_DOWNLOAD = 1028; - DECK_LIST_OTHER_USER = 1029; - DECK_SET_VISIBILITY = 1030; - DECK_DOWNLOAD_PUBLIC = 1031; - DECK_SHARE_LIST_MINE = 1032; - DECK_SHARE_REMOVE = 1033; REPLAY_LIST = 1100; REPLAY_DOWNLOAD = 1101; REPLAY_MODIFY_MATCH = 1102; REPLAY_DELETE_MATCH = 1103; REPLAY_GET_CODE = 1104; REPLAY_SUBMIT_CODE = 1105; - REPORT = 1200; - // 1201-1203 reserved: removed during squash - REPORT_MY_LIST = 1204; - REPORT_ADD_COMMENT = 1205; - REPORT_DETAILS = 1206; } extensions 100 to max; } diff --git a/libcockatrice_protocol/libcockatrice/protocol/pending_command.cpp b/libcockatrice_protocol/libcockatrice/protocol/pending_command.cpp index 62d35313e..4a9943d33 100644 --- a/libcockatrice_protocol/libcockatrice/protocol/pending_command.cpp +++ b/libcockatrice_protocol/libcockatrice/protocol/pending_command.cpp @@ -29,13 +29,3 @@ int PendingCommand::tick() { return ++ticks; } - -void PendingCommand::startTiming() -{ - startTime.start(); -} - -qint64 PendingCommand::elapsedMs() const -{ - return startTime.isValid() ? startTime.nsecsElapsed() / 1000000 : -1; -} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pending_command.h b/libcockatrice_protocol/libcockatrice/protocol/pending_command.h index b8f861d08..dbe57e7fc 100644 --- a/libcockatrice_protocol/libcockatrice/protocol/pending_command.h +++ b/libcockatrice_protocol/libcockatrice/protocol/pending_command.h @@ -7,7 +7,6 @@ #ifndef PENDING_COMMAND_H #define PENDING_COMMAND_H -#include #include #include #include @@ -22,7 +21,6 @@ private: CommandContainer commandContainer; QVariant extraData; int ticks; - QElapsedTimer startTime; public: explicit PendingCommand(const CommandContainer &_commandContainer, QVariant _extraData = QVariant()); @@ -31,18 +29,6 @@ public: QVariant getExtraData() const; void processResponse(const Response &response); int tick(); - - /** - * @brief Starts the round-trip timer. Called by the client thread right - * before the command container is handed to the transport layer. - */ - void startTiming(); - - /** - * @return Milliseconds elapsed since startTiming(), or -1 if the timer was - * never started. - */ - qint64 elapsedMs() const; }; #endif diff --git a/libcockatrice_rng/libcockatrice/rng/rng_sfmt.cpp b/libcockatrice_rng/libcockatrice/rng/rng_sfmt.cpp index 4c578b4e4..5b38deb3f 100644 --- a/libcockatrice_rng/libcockatrice/rng/rng_sfmt.cpp +++ b/libcockatrice_rng/libcockatrice/rng/rng_sfmt.cpp @@ -1,5 +1,6 @@ #include "rng_sfmt.h" +#include #include #include #include @@ -10,11 +11,10 @@ #define UINT64_MAX (~(uint64_t)0) #endif -RNG_SFMT::RNG_SFMT(uint64_t seed, QObject *parent) : RNG_Abstract(parent) +RNG_SFMT::RNG_SFMT(QObject *parent) : RNG_Abstract(parent) { - // initialize the random number generator with a 64bit seed, e.g. from a CSPRNG - uint32_t seedArray[2] = {static_cast(seed), static_cast(seed >> 32)}; - sfmt_init_by_array(&sfmt, seedArray, 2); + // initialize the random number generator with a 32bit integer seed (timestamp) + sfmt_init_gen_rand(&sfmt, QDateTime::currentDateTime().toSecsSinceEpoch()); } /** diff --git a/libcockatrice_rng/libcockatrice/rng/rng_sfmt.h b/libcockatrice_rng/libcockatrice/rng/rng_sfmt.h index a180dad99..7e9f53df3 100644 --- a/libcockatrice_rng/libcockatrice/rng/rng_sfmt.h +++ b/libcockatrice_rng/libcockatrice/rng/rng_sfmt.h @@ -36,7 +36,7 @@ private: unsigned int cdf(unsigned int min, unsigned int max); public: - explicit RNG_SFMT(uint64_t seed, QObject *parent = nullptr); + explicit RNG_SFMT(QObject *parent = nullptr); unsigned int rand(int min, int max) override; }; diff --git a/libcockatrice_settings/libcockatrice/settings/appearance_settings.cpp b/libcockatrice_settings/libcockatrice/settings/appearance_settings.cpp index 839e33be4..45a02299e 100644 --- a/libcockatrice_settings/libcockatrice/settings/appearance_settings.cpp +++ b/libcockatrice_settings/libcockatrice/settings/appearance_settings.cpp @@ -69,25 +69,3 @@ void AppearanceSettings::setHomeTabDisplayCardName(bool _displayCardName) setValue(_displayCardName, "homeTabDisplayCardName"); emit homeTabDisplayCardNameChanged(); } - -bool AppearanceSettings::getHomeTabBackgroundDim() const -{ - return getValue("homeTabBackgroundDim", QString(), QString(), true).toBool(); -} - -void AppearanceSettings::setHomeTabBackgroundDim(bool _dimBackground) -{ - setValue(_dimBackground, "homeTabBackgroundDim"); - emit homeTabBackgroundDimChanged(); -} - -int AppearanceSettings::getHomeTabButtonColorSourceIndex() const -{ - return getValue("homeTabButtonColorSource", "", "", 0).toInt(); -} - -void AppearanceSettings::setHomeTabButtonColorSourceIndex(int index) -{ - setValue(index, "homeTabButtonColorSource"); - emit homeTabButtonColorChanged(); -} diff --git a/libcockatrice_settings/libcockatrice/settings/appearance_settings.h b/libcockatrice_settings/libcockatrice/settings/appearance_settings.h index a4504798e..d9b326bee 100644 --- a/libcockatrice_settings/libcockatrice/settings/appearance_settings.h +++ b/libcockatrice_settings/libcockatrice/settings/appearance_settings.h @@ -27,10 +27,6 @@ public: void setHomeTabBackgroundShuffleFrequency(int _frequency); [[nodiscard]] bool getHomeTabDisplayCardName() const; void setHomeTabDisplayCardName(bool _displayCardName); - [[nodiscard]] bool getHomeTabBackgroundDim() const; - void setHomeTabBackgroundDim(bool _dimBackground); - [[nodiscard]] int getHomeTabButtonColorSourceIndex() const; - void setHomeTabButtonColorSourceIndex(int index); signals: void themeNameChanged(); @@ -38,8 +34,6 @@ signals: void homeTabBackgroundSourceChanged(); void homeTabBackgroundShuffleFrequencyChanged(); void homeTabDisplayCardNameChanged(); - void homeTabBackgroundDimChanged(); - void homeTabButtonColorChanged(); public: explicit AppearanceSettings(const QString &settingPath, QObject *parent = nullptr); diff --git a/libcockatrice_settings/libcockatrice/settings/cards_display_settings.cpp b/libcockatrice_settings/libcockatrice/settings/cards_display_settings.cpp index 94e888504..6ec1af962 100644 --- a/libcockatrice_settings/libcockatrice/settings/cards_display_settings.cpp +++ b/libcockatrice_settings/libcockatrice/settings/cards_display_settings.cpp @@ -50,11 +50,6 @@ bool CardsDisplaySettings::getTapAnimation() const return getValue("tapAnimation", QString(), QString(), true).toBool(); } -bool CardsDisplaySettings::getArrowDrawAnimation() const -{ - return getValue("arrowDrawAnimation", QString(), QString(), true).toBool(); -} - bool CardsDisplaySettings::getAutoRotateSidewaysLayoutCards() const { return getValue("autoRotateSidewaysLayoutCards", QString(), QString(), true).toBool(); @@ -105,16 +100,6 @@ int CardsDisplaySettings::getSampleHandSize() const return getValue("sampleHandSize", "cards", "cardSize", 7).toInt(); } -QString CardsDisplaySettings::getCardLang() const -{ - return getValue("cardLang", QString(), QString(), "en").toString(); -} - -int CardsDisplaySettings::getCardSearchLanguage() const -{ - return getValue("cardSearchLanguage", QString(), QString(), static_cast(SearchLanguageMode::English)).toInt(); -} - void CardsDisplaySettings::setDisplayCardNames(bool _displayCardNames) { setValue(_displayCardNames, "displayCardNames"); @@ -174,11 +159,6 @@ void CardsDisplaySettings::setTapAnimation(bool _tapAnimation) setValue(_tapAnimation, "tapAnimation"); } -void CardsDisplaySettings::setArrowDrawAnimation(bool _arrowDrawAnimation) -{ - setValue(_arrowDrawAnimation, "arrowDrawAnimation"); -} - void CardsDisplaySettings::setAutoRotateSidewaysLayoutCards(bool _autoRotateSidewaysLayoutCards) { setValue(_autoRotateSidewaysLayoutCards, "autoRotateSidewaysLayoutCards"); @@ -234,25 +214,3 @@ void CardsDisplaySettings::setSampleHandSize(int _sampleHandSize) setValue(_sampleHandSize, "sampleHandSize", "cards", "cardSize"); emit sampleHandSizeChanged(_sampleHandSize); } - -void CardsDisplaySettings::setCardLang(const QString &_cardLang) -{ - if (_cardLang == getCardLang()) { - return; - } - setValue(_cardLang, "cardLang"); - // Flush to disk immediately: the Oracle tool is a separate process that - // reads this value to decide which foreignData to import, so it must not - // observe a stale (pre-change) value. - sync(); - emit cardLangChanged(_cardLang); -} - -void CardsDisplaySettings::setCardSearchLanguage(int _cardSearchLanguage) -{ - if (_cardSearchLanguage == getCardSearchLanguage()) { - return; - } - setValue(_cardSearchLanguage, "cardSearchLanguage"); - emit cardSearchLanguageChanged(_cardSearchLanguage); -} diff --git a/libcockatrice_settings/libcockatrice/settings/cards_display_settings.h b/libcockatrice_settings/libcockatrice/settings/cards_display_settings.h index 85eb5adbd..15a3e3ff4 100644 --- a/libcockatrice_settings/libcockatrice/settings/cards_display_settings.h +++ b/libcockatrice_settings/libcockatrice/settings/cards_display_settings.h @@ -3,7 +3,6 @@ #include "settings_manager.h" -#include #include class CardsDisplaySettings : public SettingsManager, public ICardsDisplaySettingsProvider @@ -21,7 +20,6 @@ public: [[nodiscard]] bool getIncludeRebalancedCards() const override; [[nodiscard]] bool getPrintingSelectorNavigationButtonsVisible() const override; [[nodiscard]] bool getTapAnimation() const override; - [[nodiscard]] bool getArrowDrawAnimation() const override; [[nodiscard]] bool getAutoRotateSidewaysLayoutCards() const override; [[nodiscard]] bool getScaleCards() const override; [[nodiscard]] int getStackCardOverlapPercent() const override; @@ -32,8 +30,6 @@ public: [[nodiscard]] int getEDHRecCardSize() const override; [[nodiscard]] int getArchidektPreviewSize() const override; [[nodiscard]] int getSampleHandSize() const override; - [[nodiscard]] QString getCardLang() const override; - [[nodiscard]] int getCardSearchLanguage() const override; void setDisplayCardNames(bool _displayCardNames); void setRoundCardCorners(bool _roundCardCorners); @@ -44,7 +40,6 @@ public: void setIncludeRebalancedCards(bool _includeRebalancedCards); void setPrintingSelectorNavigationButtonsVisible(bool _navigationButtonsVisible); void setTapAnimation(bool _tapAnimation); - void setArrowDrawAnimation(bool _arrowDrawAnimation); void setAutoRotateSidewaysLayoutCards(bool _autoRotateSidewaysLayoutCards); void setCardScaling(bool _scaleCards); void setStackCardOverlapPercent(int _verticalCardOverlapPercent); @@ -55,8 +50,6 @@ public: void setEDHRecCardSize(int _edhrecCardSize); void setArchidektPreviewCardSize(int _archidektPreviewCardSize); void setSampleHandSize(int _sampleHandSize); - void setCardLang(const QString &_cardLang); - void setCardSearchLanguage(int _cardSearchLanguage); signals: void displayCardNamesChanged(); @@ -73,8 +66,6 @@ signals: void edhRecCardSizeChanged(); void archidektPreviewSizeChanged(); void sampleHandSizeChanged(int amount); - void cardLangChanged(const QString &lang); - void cardSearchLanguageChanged(int cardSearchLanguage); public: explicit CardsDisplaySettings(const QString &settingPath, QObject *parent = nullptr); diff --git a/libcockatrice_settings/libcockatrice/settings/chat_settings.cpp b/libcockatrice_settings/libcockatrice/settings/chat_settings.cpp index 974173371..5e1102473 100644 --- a/libcockatrice_settings/libcockatrice/settings/chat_settings.cpp +++ b/libcockatrice_settings/libcockatrice/settings/chat_settings.cpp @@ -65,11 +65,6 @@ bool ChatSettings::getRoomHistory() const return getValue("roomHistory", QString(), QString(), true).toBool(); } -bool ChatSettings::getIgnoreAllPrivateMessages() const -{ - return getValue("ignoreAllPrivateMessages", QString(), QString(), false).toBool(); -} - QString ChatSettings::getHighlightWords() const { return getValue("highlightWords").toString(); @@ -136,11 +131,6 @@ void ChatSettings::setRoomHistory(bool _roomHistory) setValue(_roomHistory, "roomHistory"); } -void ChatSettings::setIgnoreAllPrivateMessages(bool _ignoreAllPrivateMessages) -{ - setValue(_ignoreAllPrivateMessages, "ignoreAllPrivateMessages"); -} - void ChatSettings::setHighlightWords(const QString &_highlightWords) { setValue(_highlightWords, "highlightWords"); diff --git a/libcockatrice_settings/libcockatrice/settings/chat_settings.h b/libcockatrice_settings/libcockatrice/settings/chat_settings.h index 9566671e2..7cf4be3f6 100644 --- a/libcockatrice_settings/libcockatrice/settings/chat_settings.h +++ b/libcockatrice_settings/libcockatrice/settings/chat_settings.h @@ -23,7 +23,6 @@ public: [[nodiscard]] bool getShowMessagePopup() const override; [[nodiscard]] bool getShowMentionPopup() const override; [[nodiscard]] bool getRoomHistory() const override; - [[nodiscard]] bool getIgnoreAllPrivateMessages() const override; [[nodiscard]] QString getHighlightWords() const override; void setChatMention(bool _chatMention); @@ -38,7 +37,6 @@ public: void setShowMessagePopups(bool _showMessagePopups); void setShowMentionPopups(bool _showMentionPopups); void setRoomHistory(bool _roomHistory); - void setIgnoreAllPrivateMessages(bool _ignoreAllPrivateMessages); void setHighlightWords(const QString &_highlightWords); signals: diff --git a/libcockatrice_settings/libcockatrice/settings/deck_editor_settings.cpp b/libcockatrice_settings/libcockatrice/settings/deck_editor_settings.cpp index 44cdcd86f..65296a450 100644 --- a/libcockatrice_settings/libcockatrice/settings/deck_editor_settings.cpp +++ b/libcockatrice_settings/libcockatrice/settings/deck_editor_settings.cpp @@ -25,11 +25,6 @@ int DeckEditorSettings::getDefaultDeckEditorType() const return getValue("defaultDeckEditorType", QString(), QString(), 1).toInt(); } -int DeckEditorSettings::getVdeStartupTab() const -{ - return getValue("vdeStartupTab", QString(), QString(), VdeStartupTabContext).toInt(); -} - void DeckEditorSettings::setOpenDeckInNewTab(bool _openDeckInNewTab) { setValue(_openDeckInNewTab, "openDeckInNewTab"); @@ -52,12 +47,6 @@ void DeckEditorSettings::setDefaultDeckEditorType(int _defaultDeckEditorType) setValue(_defaultDeckEditorType, "defaultDeckEditorType"); } -void DeckEditorSettings::setVdeStartupTab(int _vdeStartupTab) -{ - setValue(_vdeStartupTab, "vdeStartupTab"); - emit vdeStartupTabChanged(_vdeStartupTab); -} - int DeckEditorSettings::getCommanderSpellbookIntegrationEnabled() const { return getValue("commanderspellbookintegrationenabled", QString(), QString(), diff --git a/libcockatrice_settings/libcockatrice/settings/deck_editor_settings.h b/libcockatrice_settings/libcockatrice/settings/deck_editor_settings.h index 0c87a270a..70f91be9b 100644 --- a/libcockatrice_settings/libcockatrice/settings/deck_editor_settings.h +++ b/libcockatrice_settings/libcockatrice/settings/deck_editor_settings.h @@ -13,13 +13,6 @@ enum commanderSpellbookIntegrationEnabledIndex commanderSpellbookIntegrationEnabledIndexUnprompted, }; -enum VdeStartupTab -{ - VdeStartupTabContext, ///< Match the opened deck: new decks show the database display, existing decks the deck view - VdeStartupTabDeckDisplay, ///< Always open the Visual Deck View - VdeStartupTabDatabaseDisplay, ///< Always open the Visual Database Display -}; - class DeckEditorSettings : public SettingsManager, public IDeckEditorSettingsProvider { Q_OBJECT @@ -30,7 +23,6 @@ public: [[nodiscard]] bool getBannerCardComboBoxVisible() const override; [[nodiscard]] bool getTagsWidgetVisible() const override; [[nodiscard]] int getDefaultDeckEditorType() const override; - [[nodiscard]] int getVdeStartupTab() const; [[nodiscard]] int getCommanderSpellbookIntegrationEnabled() const; [[nodiscard]] bool getCommanderSpellbookIntegrationUseOfficialBracketNames() const; @@ -38,7 +30,6 @@ public: void setBannerCardComboBoxVisible(bool _bannerCardComboBoxVisible); void setTagsWidgetVisible(bool _tagsWidgetVisible); void setDefaultDeckEditorType(int _defaultDeckEditorType); - void setVdeStartupTab(int _vdeStartupTab); void setCommanderSpellbookIntegrationEnabled(int _commanderSpellbookIntegrationEnabled); void setCommanderSpellbookIntegrationUseOfficialBracketNames(bool _useOfficialBracketNames); @@ -47,7 +38,6 @@ signals: void tagsWidgetVisibleChanged(bool visible); void commanderSpellbookIntegrationEnabledChanged(int enabled); void commanderSpellbookIntegrationUseOfficialBracketNamesChanged(bool useOfficialBracketNames); - void vdeStartupTabChanged(int vdeStartupTab); public: explicit DeckEditorSettings(const QString &settingPath, QObject *parent = nullptr); diff --git a/libcockatrice_settings/libcockatrice/settings/download_settings.cpp b/libcockatrice_settings/libcockatrice/settings/download_settings.cpp index eb73e58ee..cfa1c054e 100644 --- a/libcockatrice_settings/libcockatrice/settings/download_settings.cpp +++ b/libcockatrice_settings/libcockatrice/settings/download_settings.cpp @@ -4,14 +4,11 @@ const QStringList DownloadSettings::DEFAULT_DOWNLOAD_URLS = { "https://cards.scryfall.io/large/!prop:side!/!set:uuid_substr_0_1!/!set:uuid_substr_1_1!/!set:uuid!.jpg", - "https://api.scryfall.com/cards/!set:uuid!?format=image&face=!prop:side!&lang=!sflang!", - "https://api.scryfall.com/cards/multiverse/!set:muid!?format=image&lang=!sflang!", + "https://api.scryfall.com/cards/!set:uuid!?format=image&face=!prop:side!", + "https://api.scryfall.com/cards/multiverse/!set:muid!?format=image", "https://gatherer.wizards.com/Handlers/Image.ashx?multiverseid=!set:muid!&type=card", "https://gatherer.wizards.com/Handlers/Image.ashx?name=!name!&type=card"}; -const QString DownloadSettings::SCRYFALL_NAMED_LOCALIZED_URL = - "https://api.scryfall.com/cards/named?fuzzy=!localizedName!&lang=!sflang!&format=image&face=!prop:side!"; - DownloadSettings::DownloadSettings(const QString &settingPath, QObject *parent = nullptr) : SettingsManager(settingPath + "downloads.ini", "downloads", QString(), parent) { @@ -32,18 +29,6 @@ void DownloadSettings::resetToDefaultURLs() setValue(QVariant::fromValue(DEFAULT_DOWNLOAD_URLS), "urls"); } -bool DownloadSettings::addLocalizedScryfallUrl() -{ - const QStringList urls = getAllURLs(); - if (urls.contains(SCRYFALL_NAMED_LOCALIZED_URL)) { - return false; - } - QStringList updated = urls; - updated.prepend(SCRYFALL_NAMED_LOCALIZED_URL); - setDownloadUrls(updated); - return true; -} - bool DownloadSettings::getPicDownload() const { return getValue("pictureDownload", QString(), QString(), true).toBool(); diff --git a/libcockatrice_settings/libcockatrice/settings/download_settings.h b/libcockatrice_settings/libcockatrice/settings/download_settings.h index ae49884f0..a3a6f4ca9 100644 --- a/libcockatrice_settings/libcockatrice/settings/download_settings.h +++ b/libcockatrice_settings/libcockatrice/settings/download_settings.h @@ -15,7 +15,6 @@ class DownloadSettings : public SettingsManager friend class SettingsCache; static const QStringList DEFAULT_DOWNLOAD_URLS; - static const QString SCRYFALL_NAMED_LOCALIZED_URL; public: explicit DownloadSettings(const QString &, QObject *); @@ -23,7 +22,6 @@ public: QStringList getAllURLs() const; void setDownloadUrls(const QStringList &downloadURLs); void resetToDefaultURLs(); - [[nodiscard]] bool addLocalizedScryfallUrl(); [[nodiscard]] bool getPicDownload() const; void setPicDownload(bool _picDownload); [[nodiscard]] bool getDownloadSpoilersStatus() const; diff --git a/libcockatrice_settings/libcockatrice/settings/game_filters_settings.cpp b/libcockatrice_settings/libcockatrice/settings/game_filters_settings.cpp index 0edba44dd..ad972b433 100644 --- a/libcockatrice_settings/libcockatrice/settings/game_filters_settings.cpp +++ b/libcockatrice_settings/libcockatrice/settings/game_filters_settings.cpp @@ -104,14 +104,14 @@ QString GameFiltersSettings::getGameNameFilter() const return getValue("gameNameFilter").toString(); } -void GameFiltersSettings::setHostNameFilters(QStringList hostName) +void GameFiltersSettings::setCreatorNameFilters(QStringList creatorName) { - setValue(hostName, "hostNameFilter"); + setValue(creatorName, "creatorNameFilter"); } -QStringList GameFiltersSettings::getHostNameFilters() const +QStringList GameFiltersSettings::getCreatorNameFilters() const { - return getValue("hostNameFilter").toStringList(); + return getValue("creatorNameFilter").toStringList(); } void GameFiltersSettings::setMinPlayers(int min) diff --git a/libcockatrice_settings/libcockatrice/settings/game_filters_settings.h b/libcockatrice_settings/libcockatrice/settings/game_filters_settings.h index 11480d483..24f582007 100644 --- a/libcockatrice_settings/libcockatrice/settings/game_filters_settings.h +++ b/libcockatrice_settings/libcockatrice/settings/game_filters_settings.h @@ -24,7 +24,7 @@ public: bool isHideNotBuddyCreatedGames() const; bool isHideOpenDecklistGames() const; QString getGameNameFilter() const; - QStringList getHostNameFilters() const; + QStringList getCreatorNameFilters() const; int getMinPlayers() const; int getMaxPlayers() const; QTime getMaxGameAge() const; @@ -42,7 +42,7 @@ public: void setHidePasswordProtectedGames(bool hide); void setHideNotBuddyCreatedGames(bool hide); void setGameNameFilter(QString gameName); - void setHostNameFilters(QStringList hostName); + void setCreatorNameFilters(QStringList creatorName); void setMinPlayers(int min); void setMaxPlayers(int max); void setMaxGameAge(const QTime &maxGameAge); diff --git a/libcockatrice_settings/libcockatrice/settings/interface_settings.cpp b/libcockatrice_settings/libcockatrice/settings/interface_settings.cpp index b92a5fdec..0fa56ee33 100644 --- a/libcockatrice_settings/libcockatrice/settings/interface_settings.cpp +++ b/libcockatrice_settings/libcockatrice/settings/interface_settings.cpp @@ -1,35 +1,5 @@ #include "interface_settings.h" -namespace -{ -const QChar PLAYMAT_FIELD_SEP = QChar(0x1F); ///< Separator between PlaymatInfo fields. - -QString encodePlaymatInfo(const PlaymatInfo &res) -{ - return res.card.name + PLAYMAT_FIELD_SEP + res.card.providerId + PLAYMAT_FIELD_SEP + - QString::number(res.params.marginPctL, 'f', 4) + PLAYMAT_FIELD_SEP + - QString::number(res.params.marginPctR, 'f', 4) + PLAYMAT_FIELD_SEP + - QString::number(res.params.verticalOffset, 'f', 4) + PLAYMAT_FIELD_SEP + - QString::number(res.params.zoom, 'f', 4); -} - -PlaymatInfo decodePlaymatInfo(const QString &encoded) -{ - const QStringList fields = encoded.split(PLAYMAT_FIELD_SEP); - if (fields.size() != 6) { - return {}; - } - PlaymatInfo res; - res.card.name = fields.at(0); - res.card.providerId = fields.at(1); - res.params.marginPctL = fields.at(2).toDouble(); - res.params.marginPctR = fields.at(3).toDouble(); - res.params.verticalOffset = fields.at(4).toDouble(); - res.params.zoom = fields.at(5).toDouble(); - return res; -} -} // namespace - InterfaceSettings::InterfaceSettings(const QString &settingPath, QObject *parent) : SettingsManager(settingPath + "interface.ini", "interface", QString(), parent) { @@ -150,11 +120,6 @@ qreal InterfaceSettings::getFastForwardSpeed() const return getValue("fastForwardSpeed", "replay", QString(), 10).toReal(); } -bool InterfaceSettings::getSkipEmptySections() const -{ - return getValue("skipEmptySections", "replay", QString(), false).toBool(); -} - bool InterfaceSettings::getLeftJustified() const { return getValue("leftJustified", QString(), QString(), false).toBool(); @@ -190,51 +155,6 @@ bool InterfaceSettings::getShowGameSelectorFilterToolbar() const return getValue("showGameSelectorFilterToolbar", QString(), QString(), true).toBool(); } -int InterfaceSettings::getPlaymatVisibility() const -{ - return qBound(0, getValue("playmatvisibility", QString(), QString(), 2).toInt(), 2); -} - -QList InterfaceSettings::getPlaymatFallbackList() const -{ - const QStringList entries = getValue("playmatFallbackList", QString(), QString(), QStringList()).toStringList(); - QList result; - result.reserve(entries.size()); - for (const QString &entry : entries) { - const PlaymatInfo res = decodePlaymatInfo(entry); - if (!res.card.isEmpty()) { - result.append(res); - } - } - return result; -} - -int InterfaceSettings::getPlaymatMode() const -{ - return qBound(0, getValue("playmatMode", QString(), QString(), 1).toInt(), 2); -} - -int InterfaceSettings::getPlaymatFallbackBehavior() const -{ - return qBound(0, getValue("playmatFallbackBehavior", QString(), QString(), 0).toInt(), 2); -} - -bool InterfaceSettings::getLifeCounterAnimationsEnabled() const -{ - return getValue("lifeCounterAnimationsEnabled", QString(), QString(), true).toBool(); -} - -bool InterfaceSettings::getBattlefieldFlashEnabled() const -{ - return getValue("battlefieldFlashEnabled", QString(), QString(), true).toBool(); -} - -QStringList InterfaceSettings::getUserListExpandedSections() const -{ - return getValue("userListExpandedSections", QString(), QString(), QStringList({"buddy", "online", "ignore"})) - .toStringList(); -} - void InterfaceSettings::setUseTearOffMenus(bool _useTearOffMenus) { setValue(_useTearOffMenus, "useTearOffMenus"); @@ -359,11 +279,6 @@ void InterfaceSettings::setFastForwardSpeed(qreal _value) setValue(_value, "fastForwardSpeed", "replay"); } -void InterfaceSettings::setSkipEmptySections(bool _value) -{ - setValue(_value, "skipEmptySections", "replay"); -} - void InterfaceSettings::setLeftJustified(bool _leftJustified) { setValue(_leftJustified, "leftJustified"); @@ -401,60 +316,3 @@ void InterfaceSettings::setShowGameSelectorFilterToolbar(bool _showGameSelectorF setValue(_showGameSelectorFilterToolbar, "showGameSelectorFilterToolbar"); emit showGameSelectorFilterToolbarChanged(_showGameSelectorFilterToolbar); } - -void InterfaceSettings::setPlaymatVisibility(int _visibility) -{ - if (getPlaymatVisibility() == _visibility) { - return; - } - setValue(_visibility, "playmatvisibility"); - emit playmatVisibilityChanged(_visibility); -} - -void InterfaceSettings::setPlaymatFallbackList(const QList &_fallbackList) -{ - QStringList entries; - entries.reserve(_fallbackList.size()); - for (const PlaymatInfo &res : _fallbackList) { - entries.append(encodePlaymatInfo(res)); - } - setValue(entries, "playmatFallbackList"); - emit playmatSettingsChanged(); -} - -void InterfaceSettings::setPlaymatMode(int _mode) -{ - const int mode = qBound(0, _mode, 2); - if (getPlaymatMode() == mode) { - return; - } - setValue(mode, "playmatMode"); - emit playmatSettingsChanged(); -} - -void InterfaceSettings::setPlaymatFallbackBehavior(int _behavior) -{ - const int behavior = qBound(0, _behavior, 2); - if (getPlaymatFallbackBehavior() == behavior) { - return; - } - setValue(behavior, "playmatFallbackBehavior"); - emit playmatSettingsChanged(); -} - -void InterfaceSettings::setLifeCounterAnimationsEnabled(bool _lifeCounterAnimationsEnabled) -{ - setValue(_lifeCounterAnimationsEnabled, "lifeCounterAnimationsEnabled"); - emit lifeCounterAnimationsEnabledChanged(_lifeCounterAnimationsEnabled); -} - -void InterfaceSettings::setBattlefieldFlashEnabled(bool _battlefieldFlashEnabled) -{ - setValue(_battlefieldFlashEnabled, "battlefieldFlashEnabled"); - emit battlefieldFlashEnabledChanged(_battlefieldFlashEnabled); -} - -void InterfaceSettings::setUserListExpandedSections(const QStringList §ions) -{ - setValue(sections, "userListExpandedSections"); -} diff --git a/libcockatrice_settings/libcockatrice/settings/interface_settings.h b/libcockatrice_settings/libcockatrice/settings/interface_settings.h index 6b226f307..7ef367cb9 100644 --- a/libcockatrice_settings/libcockatrice/settings/interface_settings.h +++ b/libcockatrice_settings/libcockatrice/settings/interface_settings.h @@ -34,7 +34,6 @@ public: [[nodiscard]] int getMinPlayersForMultiColumnLayout() const override; [[nodiscard]] int getRewindBufferingMs() const override; [[nodiscard]] qreal getFastForwardSpeed() const override; - [[nodiscard]] bool getSkipEmptySections() const override; [[nodiscard]] bool getLeftJustified() const override; [[nodiscard]] int getZoneViewGroupByIndex() const override; [[nodiscard]] int getZoneViewSortByIndex() const override; @@ -42,13 +41,6 @@ public: [[nodiscard]] bool getShowStatusBar() const override; [[nodiscard]] bool getShowShortcuts() const override; [[nodiscard]] bool getShowGameSelectorFilterToolbar() const override; - [[nodiscard]] int getPlaymatVisibility() const override; - [[nodiscard]] QList getPlaymatFallbackList() const override; - [[nodiscard]] int getPlaymatMode() const override; - [[nodiscard]] int getPlaymatFallbackBehavior() const override; - [[nodiscard]] bool getLifeCounterAnimationsEnabled() const override; - [[nodiscard]] bool getBattlefieldFlashEnabled() const override; - [[nodiscard]] QStringList getUserListExpandedSections() const override; void setUseTearOffMenus(bool _useTearOffMenus); void setCardViewInitialRowsMax(int _cardViewInitialRowsMax); @@ -73,7 +65,6 @@ public: void setMinPlayersForMultiColumnLayout(int _minPlayersForMultiColumnLayout); void setRewindBufferingMs(int _rewindBufferingMs); void setFastForwardSpeed(qreal _value); - void setSkipEmptySections(bool _value); void setLeftJustified(bool _leftJustified); void setZoneViewGroupByIndex(int _zoneViewGroupByIndex); void setZoneViewSortByIndex(int _zoneViewSortByIndex); @@ -81,13 +72,6 @@ public: void setShowStatusBar(bool _showStatusBar); void setShowShortcuts(bool _showShortcuts); void setShowGameSelectorFilterToolbar(bool _showGameSelectorFilterToolbar); - void setPlaymatVisibility(int _visibility); - void setPlaymatFallbackList(const QList &_fallbackList); - void setPlaymatMode(int _mode); - void setPlaymatFallbackBehavior(int _behavior); - void setLifeCounterAnimationsEnabled(bool _lifeCounterAnimationsEnabled); - void setBattlefieldFlashEnabled(bool _battlefieldFlashEnabled); - void setUserListExpandedSections(const QStringList §ions); signals: void useTearOffMenusChanged(bool state); @@ -99,10 +83,6 @@ signals: void tallyTypeChanged(int type); void showStatusBarChanged(bool state); void showGameSelectorFilterToolbarChanged(bool state); - void playmatVisibilityChanged(int visibility); - void playmatSettingsChanged(); - void lifeCounterAnimationsEnabledChanged(bool state); - void battlefieldFlashEnabledChanged(bool state); public: explicit InterfaceSettings(const QString &settingPath, QObject *parent = nullptr); diff --git a/libcockatrice_settings/libcockatrice/settings/servers_settings.cpp b/libcockatrice_settings/libcockatrice/settings/servers_settings.cpp index 436e260c5..811b0c842 100644 --- a/libcockatrice_settings/libcockatrice/settings/servers_settings.cpp +++ b/libcockatrice_settings/libcockatrice/settings/servers_settings.cpp @@ -171,12 +171,7 @@ void ServersSettings::addNewServer(const QString &saveName, bool savePassword, const QString &site) { - // Match the exact host-plus-port server the caller is adding, so a link or - // public-server list entry cannot clobber the port (and credentials) of an - // unrelated entry that happens to share the same hostname. - const int existingIndex = findServerIndex(serv, port); - if (existingIndex >= 0) { - updateServerFields(existingIndex, saveName, username, password, savePassword, site); + if (updateExistingServer(saveName, serv, port, username, password, savePassword, site)) { return; } @@ -276,7 +271,22 @@ bool ServersSettings::updateExistingServer(QString saveName, for (int i = 0; i <= size; ++i) { if (serv == getValue(QString("server%1").arg(i), "server", "server_details").toString()) { setValue(port, QString("port%1").arg(i), "server", "server_details"); - updateServerFields(i, saveName, username, password, savePassword, site); + if (!username.isEmpty()) { + setValue(username, QString("username%1").arg(i), "server", "server_details"); + } + + if (savePassword && !password.isEmpty()) { + setValue(password, QString("password%1").arg(i), "server", "server_details"); + } else { + setValue(QString(), QString("password%1").arg(i), "server", "server_details"); + } + + if (!site.isEmpty()) { + setValue(site, QString("site%1").arg(i), "server", "server_details"); + } + + setValue(savePassword, QString("savePassword%1").arg(i), "server", "server_details"); + setValue(saveName, QString("saveName%1").arg(i), "server", "server_details"); return true; } @@ -284,31 +294,6 @@ bool ServersSettings::updateExistingServer(QString saveName, return false; } -void ServersSettings::updateServerFields(int index, - const QString &saveName, - const QString &username, - const QString &password, - bool savePassword, - const QString &site) -{ - if (!username.isEmpty()) { - setValue(username, QString("username%1").arg(index), "server", "server_details"); - } - - if (savePassword && !password.isEmpty()) { - setValue(password, QString("password%1").arg(index), "server", "server_details"); - } else { - setValue(QString(), QString("password%1").arg(index), "server", "server_details"); - } - - if (!site.isEmpty()) { - setValue(site, QString("site%1").arg(index), "server", "server_details"); - } - - setValue(savePassword, QString("savePassword%1").arg(index), "server", "server_details"); - setValue(saveName, QString("saveName%1").arg(index), "server", "server_details"); -} - int ServersSettings::findServerIndex(const QString &host, const QString &port) const { int size = getValue("totalServers", "server", "server_details").toInt(); @@ -325,21 +310,6 @@ int ServersSettings::findServerIndex(const QString &host, const QString &port) c return -1; } -int ServersSettings::findHostIndex(const QString &host) const -{ - int size = getValue("totalServers", "server", "server_details").toInt(); - - for (int i = 0; i <= size; ++i) { - QString storedHost = getValue(QString("server%1").arg(i), "server", "server_details").toString(); - - if (storedHost.compare(host, Qt::CaseInsensitive) == 0) { - return i; - } - } - - return -1; -} - bool ServersSettings::hasUsername(const QString &host, const QString &port) const { int index = findServerIndex(host, port); diff --git a/libcockatrice_settings/libcockatrice/settings/servers_settings.h b/libcockatrice_settings/libcockatrice/settings/servers_settings.h index 93651c813..f9803a158 100644 --- a/libcockatrice_settings/libcockatrice/settings/servers_settings.h +++ b/libcockatrice_settings/libcockatrice/settings/servers_settings.h @@ -61,14 +61,7 @@ public: QString password, bool savePassword, QString site = QString()); - void updateServerFields(int index, - const QString &saveName, - const QString &username, - const QString &password, - bool savePassword, - const QString &site); int findServerIndex(const QString &host, const QString &port) const; - int findHostIndex(const QString &host) const; bool hasUsername(const QString &host, const QString &port) const; bool hasCredentials(const QString &host, const QString &port) const; bool hasLoginData(const QString &host, const QString &port) const; diff --git a/libcockatrice_settings/libcockatrice/settings/settings_migration.cpp b/libcockatrice_settings/libcockatrice/settings/settings_migration.cpp index f6e107dad..37dc9a0a0 100644 --- a/libcockatrice_settings/libcockatrice/settings/settings_migration.cpp +++ b/libcockatrice_settings/libcockatrice/settings/settings_migration.cpp @@ -374,11 +374,7 @@ static void migrateAppearanceSettings(const QString &settingsPath, QSettings &gl QSettings appearanceIni(settingsPath + "appearance.ini", QSettings::IniFormat); for (auto it = appearanceKeyMap.constBegin(); it != appearanceKeyMap.constEnd(); ++it) { if (globalIni.contains(it.key())) { - QVariant value = globalIni.value(it.key()); - if (it.key() == "theme/name" && value.toString() == "Default") { - value = "System"; - } - appearanceIni.setValue(it.value(), value); + appearanceIni.setValue(it.value(), globalIni.value(it.key())); } } } diff --git a/libcockatrice_settings/libcockatrice/settings/tabs_settings.cpp b/libcockatrice_settings/libcockatrice/settings/tabs_settings.cpp index cf5bfd81a..1838f667e 100644 --- a/libcockatrice_settings/libcockatrice/settings/tabs_settings.cpp +++ b/libcockatrice_settings/libcockatrice/settings/tabs_settings.cpp @@ -5,26 +5,6 @@ TabsSettings::TabsSettings(const QString &settingPath, QObject *parent) { } -int TabsSettings::getStartupTabIndex() const -{ - return getValue("startupTab", QString(), QString(), StartupTab::StartupTabHome).toInt(); -} - -QString TabsSettings::getStartupServerHost() const -{ - return getValue("startupServerHost", QString(), QString(), QString()).toString(); -} - -QString TabsSettings::getStartupServerPort() const -{ - return getValue("startupServerPort", QString(), QString(), QString()).toString(); -} - -QString TabsSettings::getStartupRoomName() const -{ - return getValue("startupRoomName", QString(), QString(), QString()).toString(); -} - bool TabsSettings::getTabVisualDeckStorageOpen() const { return getValue("visualDeckStorage", QString(), QString(), true).toBool(); @@ -60,57 +40,6 @@ bool TabsSettings::getTabLogOpen() const return getValue("log", QString(), QString(), true).toBool(); } -void TabsSettings::setStartupTabIndex(int value) -{ - if (getStartupTabIndex() == value) { - return; - } - setValue(value, "startupTab"); - emit startupTabIndexChanged(value); -} - -void TabsSettings::setStartupServerHost(const QString &host) -{ - if (getStartupServerHost() == host) { - return; - } - setValue(host, "startupServerHost"); - emit startupServerHostChanged(host); -} - -void TabsSettings::setStartupServerPort(const QString &port) -{ - if (getStartupServerPort() == port) { - return; - } - setValue(port, "startupServerPort"); - emit startupServerPortChanged(port); -} - -void TabsSettings::setStartupRoomName(const QString &roomName) -{ - if (getStartupRoomName() == roomName) { - return; - } - setValue(roomName, "startupRoomName"); - emit startupRoomNameChanged(roomName); -} - -bool TabsSettings::getTabReportOpen() const -{ - return getValue("report", QString(), QString(), false).toBool(); -} - -bool TabsSettings::getTabModerationOpen() const -{ - return getValue("moderation", QString(), QString(), false).toBool(); -} - -bool TabsSettings::getTabCardArtRulesOpen() const -{ - return getValue("cardArtRules", QString(), QString(), false).toBool(); -} - void TabsSettings::setTabVisualDeckStorageOpen(bool value) { setValue(value, "visualDeckStorage"); @@ -145,18 +74,3 @@ void TabsSettings::setTabLogOpen(bool value) { setValue(value, "log"); } - -void TabsSettings::setTabReportOpen(bool value) -{ - setValue(value, "report"); -} - -void TabsSettings::setTabModerationOpen(bool value) -{ - setValue(value, "moderation"); -} - -void TabsSettings::setTabCardArtRulesOpen(bool value) -{ - setValue(value, "cardArtRules"); -} diff --git a/libcockatrice_settings/libcockatrice/settings/tabs_settings.h b/libcockatrice_settings/libcockatrice/settings/tabs_settings.h index eb78d311b..c8e952b87 100644 --- a/libcockatrice_settings/libcockatrice/settings/tabs_settings.h +++ b/libcockatrice_settings/libcockatrice/settings/tabs_settings.h @@ -5,35 +5,12 @@ #include -/** - * @brief The tab the application selects after launch. - * - * The destination is a request: tabs that were not open before (Deck Editor, Server Room, …) - * are opened as part of the startup flow. Destinations that require a server connection use the - * intent system to satisfy their pre-conditions. - */ -enum StartupTab -{ - StartupTabHome, ///< The Home tab - StartupTabVisualDeckStorage, ///< The visual deck storage tab - StartupTabDeckStorage, ///< The deck storage tab - StartupTabReplays, ///< The game replays tab - StartupTabDeckEditor, ///< A fresh classic deck editor tab - StartupTabVisualDeckEditor, ///< A fresh visual deck editor tab - StartupTabServer, ///< The server lobby: connect and select the server tab - StartupTabServerRoom ///< A server room: connect and join the room by name -}; - class TabsSettings : public SettingsManager, public ITabsSettingsProvider { Q_OBJECT friend class SettingsCache; public: - [[nodiscard]] int getStartupTabIndex() const override; - [[nodiscard]] QString getStartupServerHost() const override; - [[nodiscard]] QString getStartupServerPort() const override; - [[nodiscard]] QString getStartupRoomName() const override; [[nodiscard]] bool getTabVisualDeckStorageOpen() const override; [[nodiscard]] bool getTabServerOpen() const override; [[nodiscard]] bool getTabAccountOpen() const override; @@ -41,14 +18,7 @@ public: [[nodiscard]] bool getTabReplaysOpen() const override; [[nodiscard]] bool getTabAdminOpen() const override; [[nodiscard]] bool getTabLogOpen() const override; - [[nodiscard]] bool getTabReportOpen() const override; - [[nodiscard]] bool getTabModerationOpen() const override; - [[nodiscard]] bool getTabCardArtRulesOpen() const override; - void setStartupTabIndex(int value); - void setStartupServerHost(const QString &host); - void setStartupServerPort(const QString &port); - void setStartupRoomName(const QString &roomName); void setTabVisualDeckStorageOpen(bool value); void setTabServerOpen(bool value); void setTabAccountOpen(bool value); @@ -56,15 +26,6 @@ public: void setTabReplaysOpen(bool value); void setTabAdminOpen(bool value); void setTabLogOpen(bool value); - void setTabReportOpen(bool value); - void setTabModerationOpen(bool value); - void setTabCardArtRulesOpen(bool value); - -signals: - void startupTabIndexChanged(int index); - void startupServerHostChanged(const QString &host); - void startupServerPortChanged(const QString &port); - void startupRoomNameChanged(const QString &roomName); public: explicit TabsSettings(const QString &settingPath, QObject *parent = nullptr); diff --git a/libcockatrice_settings/libcockatrice/settings/visual_deck_storage_settings.cpp b/libcockatrice_settings/libcockatrice/settings/visual_deck_storage_settings.cpp index 3320598d8..1b21af58e 100644 --- a/libcockatrice_settings/libcockatrice/settings/visual_deck_storage_settings.cpp +++ b/libcockatrice_settings/libcockatrice/settings/visual_deck_storage_settings.cpp @@ -130,11 +130,6 @@ bool VisualDeckStorageSettings::getVisualDeckStorageShowTagsOnDeckPreviews() con return getValue("showTagsOnDeckPreviews", "interface", "visualDeckStorage", true).toBool(); } -bool VisualDeckStorageSettings::getVisualDeckStorageShowUploadTime() const -{ - return getValue("showUploadTime", "interface", "visualDeckStorage", true).toBool(); -} - bool VisualDeckStorageSettings::getVisualDeckStorageDrawUnusedColorIdentities() const { return getValue("drawUnusedColorIdentities", "interface", "visualDeckStorage", true).toBool(); @@ -225,12 +220,6 @@ void VisualDeckStorageSettings::setVisualDeckStorageShowTagsOnDeckPreviews(bool emit visualDeckStorageShowTagsOnDeckPreviewsChanged(_showTags); } -void VisualDeckStorageSettings::setVisualDeckStorageShowUploadTime(bool value) -{ - setValue(value, "showUploadTime", "interface", "visualDeckStorage"); - emit visualDeckStorageShowUploadTimeChanged(value); -} - void VisualDeckStorageSettings::setVisualDeckStorageDrawUnusedColorIdentities(bool _draw) { setValue(_draw, "drawUnusedColorIdentities", "interface", "visualDeckStorage"); diff --git a/libcockatrice_settings/libcockatrice/settings/visual_deck_storage_settings.h b/libcockatrice_settings/libcockatrice/settings/visual_deck_storage_settings.h index 9bc9d4172..fd2a76663 100644 --- a/libcockatrice_settings/libcockatrice/settings/visual_deck_storage_settings.h +++ b/libcockatrice_settings/libcockatrice/settings/visual_deck_storage_settings.h @@ -20,7 +20,6 @@ public: [[nodiscard]] bool getVisualDeckStorageShowColorIdentity() const override; [[nodiscard]] bool getVisualDeckStorageShowBannerCardComboBox() const override; [[nodiscard]] bool getVisualDeckStorageShowTagsOnDeckPreviews() const override; - [[nodiscard]] bool getVisualDeckStorageShowUploadTime() const; [[nodiscard]] bool getVisualDeckStorageDrawUnusedColorIdentities() const override; [[nodiscard]] int getVisualDeckStorageUnusedColorIdentitiesOpacity() const override; [[nodiscard]] int getVisualDeckStorageTooltipType() const override; @@ -39,7 +38,6 @@ public: void setVisualDeckStorageShowColorIdentity(bool value); void setVisualDeckStorageShowBannerCardComboBox(bool _showBannerCardComboBox); void setVisualDeckStorageShowTagsOnDeckPreviews(bool _showTags); - void setVisualDeckStorageShowUploadTime(bool value); void setVisualDeckStorageDrawUnusedColorIdentities(bool _draw); void setVisualDeckStorageUnusedColorIdentitiesOpacity(int _opacity); void setVisualDeckStorageTooltipType(int value); @@ -56,7 +54,6 @@ signals: void visualDeckStorageShowColorIdentityChanged(bool _visible); void visualDeckStorageShowBannerCardComboBoxChanged(bool _visible); void visualDeckStorageShowTagsOnDeckPreviewsChanged(bool _visible); - void visualDeckStorageShowUploadTimeChanged(bool _visible); void visualDeckStorageDrawUnusedColorIdentitiesChanged(bool _visible); void visualDeckStorageUnusedColorIdentitiesOpacityChanged(bool value); void visualDeckStorageInGameChanged(bool enabled); diff --git a/libcockatrice_utility/CMakeLists.txt b/libcockatrice_utility/CMakeLists.txt index db23f7951..79f5a11e4 100644 --- a/libcockatrice_utility/CMakeLists.txt +++ b/libcockatrice_utility/CMakeLists.txt @@ -5,21 +5,16 @@ set(CMAKE_AUTOMOC ON) set(CMAKE_AUTOUIC ON) set(CMAKE_AUTORCC ON) -set(UTILITY_SOURCES - libcockatrice/utility/cryptoutil.cpp libcockatrice/utility/expression.cpp libcockatrice/utility/levenshtein.cpp - libcockatrice/utility/passwordhasher.cpp libcockatrice/utility/server_rate_limiter.cpp - libcockatrice/utility/warning_categories.cpp +set(UTILITY_SOURCES libcockatrice/utility/expression.cpp libcockatrice/utility/levenshtein.cpp + libcockatrice/utility/passwordhasher.cpp libcockatrice/utility/server_rate_limiter.cpp ) set(UTILITY_HEADERS - libcockatrice/utility/card_ref.h libcockatrice/utility/color.h - libcockatrice/utility/cryptoutil.h libcockatrice/utility/expression.h libcockatrice/utility/levenshtein.h libcockatrice/utility/macros.h libcockatrice/utility/passwordhasher.h - libcockatrice/utility/playmat_params.h libcockatrice/utility/string_limits.h libcockatrice/utility/dice_limits.h libcockatrice/utility/counter_limits.h @@ -27,16 +22,13 @@ set(UTILITY_HEADERS libcockatrice/utility/zone_names.h libcockatrice/utility/days_years_between.h libcockatrice/utility/server_rate_limiter.h - libcockatrice/utility/warning_categories.h ) add_library(libcockatrice_utility STATIC ${UTILITY_SOURCES} ${UTILITY_HEADERS}) target_include_directories(libcockatrice_utility PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) -find_package(OpenSSL REQUIRED) - -target_link_libraries(libcockatrice_utility PUBLIC libcockatrice_rng OpenSSL::Crypto ${QT_CORE_MODULE}) +target_link_libraries(libcockatrice_utility PUBLIC libcockatrice_rng ${QT_CORE_MODULE}) set(ORACLE_LIBS) diff --git a/libcockatrice_utility/libcockatrice/utility/cryptoutil.cpp b/libcockatrice_utility/libcockatrice/utility/cryptoutil.cpp deleted file mode 100644 index 416ef261b..000000000 --- a/libcockatrice_utility/libcockatrice/utility/cryptoutil.cpp +++ /dev/null @@ -1,25 +0,0 @@ -#include "cryptoutil.h" - -#include - -namespace CryptoUtil -{ -QByteArray randomBytes(int count) -{ - QByteArray bytes(count, '\0'); - if (RAND_bytes(reinterpret_cast(bytes.data()), count) != 1) { - // Randomness failure is fatal: never fall back to a predictable source. - qFatal("CryptoUtil::randomBytes: RAND_bytes failed"); - } - return bytes; -} - -quint64 randomUInt64() -{ - quint64 value; - if (RAND_bytes(reinterpret_cast(&value), sizeof(value)) != 1) { - qFatal("CryptoUtil::randomUInt64: RAND_bytes failed"); - } - return value; -} -} // namespace CryptoUtil diff --git a/libcockatrice_utility/libcockatrice/utility/cryptoutil.h b/libcockatrice_utility/libcockatrice/utility/cryptoutil.h deleted file mode 100644 index dba9dc37d..000000000 --- a/libcockatrice_utility/libcockatrice/utility/cryptoutil.h +++ /dev/null @@ -1,13 +0,0 @@ -#ifndef CRYPTOUTIL_H -#define CRYPTOUTIL_H - -#include -#include - -namespace CryptoUtil -{ -QByteArray randomBytes(int count); -quint64 randomUInt64(); -} // namespace CryptoUtil - -#endif diff --git a/libcockatrice_utility/libcockatrice/utility/passwordhasher.cpp b/libcockatrice_utility/libcockatrice/utility/passwordhasher.cpp index 1c22fdcfa..c40c5f94f 100644 --- a/libcockatrice_utility/libcockatrice/utility/passwordhasher.cpp +++ b/libcockatrice_utility/libcockatrice/utility/passwordhasher.cpp @@ -1,7 +1,7 @@ #include "passwordhasher.h" #include -#include +#include QString PasswordHasher::computeHash(const QString &password, const QString &salt) { @@ -21,28 +21,12 @@ QString PasswordHasher::generateRandomSalt(const int len) static const char alphanum[] = "0123456789" "ABCDEFGHIJKLMNOPQRSTUVWXYZ" "abcdefghijklmnopqrstuvwxyz"; - const int size = sizeof(alphanum) - 1; - - // Two bytes per character, corrected for modulo bias via rejection sampling. - const int bucketSize = 65536 / size; - const int limit = bucketSize * size; QString ret; - ret.reserve(len); - QByteArray random = CryptoUtil::randomBytes(len * 2); - int bytesUsed = 0; + int size = sizeof(alphanum) - 1; + for (int i = 0; i < len; ++i) { - unsigned int value; - do { - if (bytesUsed >= random.size()) { - random = CryptoUtil::randomBytes(len * 2); - bytesUsed = 0; - } - value = static_cast(static_cast(random.at(bytesUsed))) << 8 | - static_cast(static_cast(random.at(bytesUsed + 1))); - bytesUsed += 2; - } while (value >= limit); - ret.append(alphanum[value / bucketSize]); + ret.append(alphanum[rng->rand(0, size)]); } return ret; @@ -50,5 +34,5 @@ QString PasswordHasher::generateRandomSalt(const int len) QString PasswordHasher::generateActivationToken() { - return QString(CryptoUtil::randomBytes(16).toBase64().left(16)); + return QCryptographicHash::hash(generateRandomSalt().toUtf8(), QCryptographicHash::Md5).toBase64().left(16); } diff --git a/libcockatrice_utility/libcockatrice/utility/peglib.h b/libcockatrice_utility/libcockatrice/utility/peglib.h index a67a86549..e7e558dff 100644 --- a/libcockatrice_utility/libcockatrice/utility/peglib.h +++ b/libcockatrice_utility/libcockatrice/utility/peglib.h @@ -1,4 +1,4 @@ -// +// // peglib.h // // Copyright (c) 2022 Yuji Hirose. All rights reserved. @@ -7,9 +7,6 @@ #pragma once -#define CPPPEGLIB_VERSION "1.16.0" -#define CPPPEGLIB_VERSION_NUM "0x011000" - /* * Configuration */ @@ -49,8 +46,6 @@ namespace peg { -struct GrammarBlob; - /*----------------------------------------------------------------------------- * scope_exit *---------------------------------------------------------------------------*/ @@ -340,14 +335,6 @@ inline std::string resolve_escape_sequence(const char *s, size_t n) { r += ']'; i++; break; - case '^': - r += '^'; - i++; - break; - case '-': - r += '-'; - i++; - break; case '\\': r += '\\'; i++; @@ -374,46 +361,6 @@ inline std::string resolve_escape_sequence(const char *s, size_t n) { return r; } -/* - * Predefined character classes (ASCII semantics) - */ -inline const std::vector> * -predefined_character_class(std::string_view name) { - static const std::map>> - table = { - {"alnum", {{'0', '9'}, {'A', 'Z'}, {'a', 'z'}}}, - {"alpha", {{'A', 'Z'}, {'a', 'z'}}}, - {"ascii", {{0x00, 0x7F}}}, - {"blank", {{'\t', '\t'}, {' ', ' '}}}, - {"cntrl", {{0x00, 0x1F}, {0x7F, 0x7F}}}, - {"digit", {{'0', '9'}}}, - {"graph", {{0x21, 0x7E}}}, - {"lower", {{'a', 'z'}}}, - {"print", {{0x20, 0x7E}}}, - {"punct", {{0x21, 0x2F}, {0x3A, 0x40}, {0x5B, 0x60}, {0x7B, 0x7E}}}, - {"space", {{'\t', '\r'}, {' ', ' '}}}, - {"upper", {{'A', 'Z'}}}, - {"word", {{'0', '9'}, {'A', 'Z'}, {'_', '_'}, {'a', 'z'}}}, - {"xdigit", {{'0', '9'}, {'A', 'F'}, {'a', 'f'}}}, - }; - auto it = table.find(name); - return it != table.end() ? &it->second : nullptr; -} - -// Ranges must be sorted and non-overlapping. -inline std::vector> complement_character_ranges( - const std::vector> &ranges) { - std::vector> r; - char32_t next = 0; - for (const auto &[lo, hi] : ranges) { - if (lo > next) { r.emplace_back(next, lo - 1); } - next = hi + 1; - } - if (next <= 0x10FFFF) { r.emplace_back(next, 0x10FFFF); } - return r; -} - /*----------------------------------------------------------------------------- * token_to_number_ - This function should be removed eventually *---------------------------------------------------------------------------*/ @@ -501,7 +448,6 @@ public: size_t items_count() const { return items_count_; } friend struct ComputeFirstSet; - friend struct GrammarBlob; private: struct Info { @@ -560,7 +506,7 @@ inline constexpr unsigned int str2tag(std::string_view sv) { namespace udl { -inline constexpr unsigned int operator""_(const char *s, size_t l) { +inline constexpr unsigned int operator"" _(const char *s, size_t l) { return str2tag_core(s, l, 0); } @@ -778,25 +724,6 @@ inline bool fail(size_t len) { return len == static_cast(-1); } using Log = std::function; -/* - * ErrorReport - structured error information passed to an ErrorReporter. - * Unlike Log, nothing is flattened into a display string, so applications - * can map errors to their own error types, localize messages, or feed - * diagnostics to IDEs. - */ -struct ErrorReport { - size_t line = 0; // 1-based - size_t col = 1; // 1-based - size_t position = 0; // byte offset in the input - std::string unexpected_token; // heuristic token at the error position - std::vector expected_literals; - std::vector expected_rules; // rules starting with '_' excluded - std::string message; // custom error_message if any (placeholders resolved) - std::string label; // rule name or recovery label the error belongs to -}; - -using ErrorReporter = std::function; - /* * ErrorInfo */ @@ -825,11 +752,7 @@ struct ErrorInfo { expected_tokens.emplace_back(error_literal, error_rule); } - void output_log(const Log &log, const char *s, size_t n) { - output_log(log, nullptr, s, n); - } - void output_log(const Log &log, const ErrorReporter &reporter, const char *s, - size_t n); + void output_log(const Log &log, const char *s, size_t n); private: int cast_char(char c) const { return static_cast(c); } @@ -885,131 +808,6 @@ using TracerLeave = std::function; -// Packrat memoization table: open-addressing hash map keyed by the fused -// (position * rule count + rule id) index. The insert-heavy access pattern -// makes node-based containers a bottleneck, so keys and lengths live in one -// flat array of 16-byte POD slots probed linearly; semantic values go into a -// parallel array that is never allocated when no cached result carries a -// value. Erased slots become tombstones (erase only happens during -// left-recursion cache invalidation). -class PackratCache { -public: - explicit PackratCache(size_t expected_entries) { - while (initial_capacity_ < expected_entries) { - initial_capacity_ *= 2; - } - } - - bool find(size_t key, size_t &len, std::any &val) const { - if (slots_.empty()) { return false; } - auto mask = slots_.size() - 1; - auto i = mix(key) & mask; - while (true) { - auto &slot = slots_[i]; - if (slot.key == key) { - len = slot.len; - if (!vals_.empty()) { - val = vals_[i]; - } else { - val.reset(); - } - return true; - } - if (slot.key == kEmpty) { return false; } - i = (i + 1) & mask; - } - } - - void insert_or_assign(size_t key, size_t len, const std::any &val) { - if (slots_.empty() || (used_ + 1) * 4 > slots_.size() * 3) { grow(); } - auto mask = slots_.size() - 1; - auto i = mix(key) & mask; - auto insert_pos = kEmpty; - while (true) { - auto &slot = slots_[i]; - if (slot.key == key) { - insert_pos = i; - break; - } - if (slot.key == kTombstone) { - if (insert_pos == kEmpty) { insert_pos = i; } - } else if (slot.key == kEmpty) { - if (insert_pos == kEmpty) { insert_pos = i; } - if (slots_[insert_pos].key == kEmpty) { used_++; } - break; - } - i = (i + 1) & mask; - } - auto &dest = slots_[insert_pos]; - dest.key = key; - dest.len = len; - if (val.has_value()) { - if (vals_.empty()) { vals_.resize(slots_.size()); } - vals_[insert_pos] = val; - } else if (!vals_.empty()) { - vals_[insert_pos].reset(); - } - } - - void erase(size_t key) { - if (slots_.empty()) { return; } - auto mask = slots_.size() - 1; - auto i = mix(key) & mask; - while (true) { - auto &slot = slots_[i]; - if (slot.key == key) { - slot.key = kTombstone; - if (!vals_.empty()) { vals_[i].reset(); } - return; - } - if (slot.key == kEmpty) { return; } - i = (i + 1) & mask; - } - } - -private: - static constexpr size_t kEmpty = static_cast(-1); - static constexpr size_t kTombstone = static_cast(-2); - - struct Slot { - size_t key = kEmpty; - size_t len = 0; - }; - - static size_t mix(size_t key) { - // Mix in 64 bits so `h >> 32` stays well-defined where size_t is 32-bit - // (wasm32); on 64-bit targets this is bit-identical to the size_t mix. - auto h = static_cast(key) * 0x9E3779B97F4A7C15ull; - return static_cast(h ^ (h >> 32)); - } - - void grow() { - auto new_cap = slots_.empty() ? initial_capacity_ : slots_.size() * 2; - std::vector old_slots = std::move(slots_); - std::vector old_vals = std::move(vals_); - slots_.assign(new_cap, Slot{}); - if (!old_vals.empty()) { vals_.assign(new_cap, std::any()); } - used_ = 0; - auto mask = new_cap - 1; - for (size_t j = 0; j < old_slots.size(); j++) { - auto &slot = old_slots[j]; - if (slot.key == kEmpty || slot.key == kTombstone) { continue; } - auto i = mix(slot.key) & mask; - while (slots_[i].key != kEmpty) { - i = (i + 1) & mask; - } - slots_[i] = slot; - if (!old_vals.empty()) { vals_[i] = std::move(old_vals[j]); } - used_++; - } - } - - size_t initial_capacity_ = 1024; - std::vector slots_; - std::vector vals_; - size_t used_ = 0; // occupied + tombstone slots -}; - class Context { public: const char *path; @@ -1019,18 +817,11 @@ public: ErrorInfo error_info; bool recovered = false; - std::vector> value_stack; + std::vector> value_stack; size_t value_stack_size = 0; std::vector rule_stack; - - // One frame per rule reference: the macro arguments in scope, and the - // instantiation they identify (0 for anything but a left-recursive macro). - struct ArgsFrame { - std::vector> args; - size_t macro_inst = 0; - }; - std::vector args_stack; + std::vector>> args_stack; size_t in_token_boundary_count = 0; @@ -1045,115 +836,68 @@ public: const size_t def_count; const bool enablePackratParsing; - const std::vector *packrat_index; // def_id -> cache slot or -1 - size_t packrat_cached_count; // number of memoized rules std::vector cache_registered; std::vector cache_success; - // Innermost active start position per rule; re-entry guard for rules that - // are not memoized (replaces the per-position bitvector for them). - std::vector active_pos; - PackratCache cache_values; + std::map, std::tuple> + cache_values; // Left recursion support struct LRMemo { size_t len = static_cast(-1); std::any val; }; - - // A left-recursive rule instance: the definition plus, for a macro, the - // instantiation it was invoked with (0 for a plain rule). Two - // instantiations of the same macro grow independent seeds. - using LRRule = std::pair; - using LRKey = std::pair; - - std::map lr_memo; + std::map, LRMemo> lr_memo; // Rules whose lr_memo was hit during the current parse scope. // Used to track LR cycle membership. - std::set lr_refs_hit; + std::set lr_refs_hit; // Rules currently in their seeding/growing phase at a given position. // Protected from having their lr_memo erased by inner growers. - std::set lr_active_seeds; - - // Interned macro instantiations: (definition, resolved arguments) -> id. - std::map, size_t> macro_inst_ids; - size_t next_macro_inst_ = 1; - - // Map a def_id to its slot in the cache tables, or -1 for guard-only - // rules (not memoized). - int32_t cache_slot(size_t def_id) const { - if (!packrat_index) { return static_cast(def_id); } - return def_id < packrat_index->size() ? (*packrat_index)[def_id] : -1; - } + std::set> lr_active_seeds; void clear_packrat_cache(const char *pos, size_t def_id) { if (!enablePackratParsing) { return; } - auto slot = cache_slot(def_id); - if (slot < 0) { return; } auto col = static_cast(pos - s); - auto idx = packrat_cached_count * col + static_cast(slot); + auto idx = def_count * col + def_id; if (idx < cache_registered.size()) { cache_registered[idx] = false; cache_success[idx] = false; } - cache_values.erase(idx); + cache_values.erase(std::make_pair(col, def_id)); } void write_packrat_cache(const char *pos, size_t def_id, size_t len, const std::any &val) { if (!enablePackratParsing) { return; } - auto slot = cache_slot(def_id); - if (slot < 0) { return; } auto col = pos - s; - auto idx = packrat_cached_count * static_cast(col) + - static_cast(slot); + auto idx = def_count * static_cast(col) + def_id; if (idx >= cache_registered.size()) { return; } cache_registered[idx] = true; cache_success[idx] = true; - cache_values.insert_or_assign(idx, len, val); + auto key = std::pair(col, def_id); + cache_values[key] = std::pair(len, val); } TracerEnter tracer_enter; TracerLeave tracer_leave; - const bool has_tracer; std::any trace_data; const bool verbose_trace; - // Byte-wise tolower frozen at parse start, so case-insensitive matching - // avoids a locale-sensitive libc call per input byte. - unsigned char tolower_table[256]; - Log log; - ErrorReporter error_reporter; Context(const char *path, const char *s, size_t l, size_t def_count, std::shared_ptr whitespaceOpe, std::shared_ptr wordOpe, bool enablePackratParsing, TracerEnter tracer_enter, TracerLeave tracer_leave, std::any trace_data, bool verbose_trace, - Log log, ErrorReporter error_reporter = nullptr, - const std::vector *packrat_index = nullptr, - size_t packrat_cached_count = 0) + Log log) : path(path), s(s), l(l), whitespaceOpe(whitespaceOpe), wordOpe(wordOpe), def_count(def_count), enablePackratParsing(enablePackratParsing), - packrat_index(packrat_index), - packrat_cached_count(packrat_index ? packrat_cached_count : def_count), - cache_registered( - enablePackratParsing ? this->packrat_cached_count * (l + 1) : 0), - cache_success( - enablePackratParsing ? this->packrat_cached_count * (l + 1) : 0), - active_pos(enablePackratParsing ? def_count : 0, nullptr), - cache_values(enablePackratParsing ? (packrat_index ? l / 8 + 16 : l / 2) - : 0), + cache_registered(enablePackratParsing ? def_count * (l + 1) : 0), + cache_success(enablePackratParsing ? def_count * (l + 1) : 0), tracer_enter(tracer_enter), tracer_leave(tracer_leave), - has_tracer(tracer_enter && tracer_leave), trace_data(trace_data), - verbose_trace(verbose_trace), log(log), error_reporter(error_reporter) { - - for (size_t i = 0; i < 256; i++) { - tolower_table[i] = - static_cast(std::tolower(static_cast(i))); - } + trace_data(trace_data), verbose_trace(verbose_trace), log(log) { push_args({}); } @@ -1174,6 +918,11 @@ public: }; std::vector *packrat_stats = nullptr; + // Per-rule packrat filter: if set, only rules with filter[def_id]=true + // use full memoization (cache_values map). Others use bitvector-only + // re-entry guard. + const std::vector *packrat_rule_filter = nullptr; + template void packrat(const char *a_s, size_t def_id, size_t &len, std::any &val, T fn) { @@ -1182,47 +931,23 @@ public: return; } - auto slot = cache_slot(def_id); - if (slot < 0) { - // Guard-only rule: no memoization. Recursion at the same position is - // caught by the per-rule active-position guard. - if (active_pos[def_id] == a_s) { - if (packrat_stats && def_id < packrat_stats->size()) { - (*packrat_stats)[def_id].hits++; - } - len = static_cast(-1); - return; - } - if (packrat_stats && def_id < packrat_stats->size()) { - (*packrat_stats)[def_id].misses++; - } - auto save = active_pos[def_id]; - active_pos[def_id] = a_s; - fn(val); - active_pos[def_id] = save; - return; - } - auto col = a_s - s; - auto idx = packrat_cached_count * static_cast(col) + - static_cast(slot); + auto idx = def_count * static_cast(col) + def_id; if (cache_registered[idx]) { if (packrat_stats && def_id < packrat_stats->size()) { (*packrat_stats)[def_id].hits++; } if (cache_success[idx]) { - if (!cache_values.find(idx, len, val)) { - len = 0; - val.reset(); - } + auto key = std::pair(col, def_id); + std::tie(len, val) = cache_values[key]; return; } else { len = static_cast(-1); return; } } else { - // Pre-register as failure (re-entry guard + failure memoization) + // Pre-register as failure (re-entry guard for all rules) cache_registered[idx] = true; cache_success[idx] = false; @@ -1232,7 +957,15 @@ public: fn(val); - if (success(len)) { write_packrat_cache(a_s, def_id, len, val); } + bool full_memo = + !packrat_rule_filter || (def_id < packrat_rule_filter->size() && + (*packrat_rule_filter)[def_id]); + if (full_memo) { + if (success(len)) { write_packrat_cache(a_s, def_id, len, val); } + } else { + // Guard-only: undo registration so future calls re-parse + cache_registered[idx] = false; + } return; } } @@ -1241,7 +974,7 @@ public: SemanticValues &push_semantic_values_scope() { assert(value_stack_size <= value_stack.size()); if (value_stack_size == value_stack.size()) { - value_stack.emplace_back(std::make_unique(this)); + value_stack.emplace_back(std::make_shared(this)); } else { auto &vs = *value_stack[value_stack_size]; if (!vs.empty()) { @@ -1263,31 +996,14 @@ public: void pop_semantic_values_scope() { value_stack_size--; } // Arguments - void push_args(std::vector> &&args, - size_t macro_inst = 0) { - args_stack.push_back({std::move(args), macro_inst}); + void push_args(std::vector> &&args) { + args_stack.emplace_back(std::move(args)); } void pop_args() { args_stack.pop_back(); } const std::vector> &top_args() const { - return args_stack[args_stack.size() - 1].args; - } - - size_t top_macro_inst() const { - return args_stack[args_stack.size() - 1].macro_inst; - } - - // Identify a macro invocation by what its resolved arguments denote (see - // macro_inst_key). `Sum(A)` inside `Sum(N)`'s own body resolves A back to - // the argument the outer call was given, so both invocations intern to the - // same id and the inner one finds the outer's seed — which is what makes - // growing terminate. - size_t intern_macro_inst(std::vector &&key) { - auto [it, inserted] = - macro_inst_ids.emplace(std::move(key), next_macro_inst_); - if (inserted) { next_macro_inst_++; } - return it->second; + return args_stack[args_stack.size() - 1]; } // Snapshot/Rollback @@ -1453,8 +1169,8 @@ private: lower_heap.reset(new char[id_len]); lower = lower_heap.get(); } - std::transform(s, s + id_len, lower, [&c](unsigned char ch) { - return static_cast(c.tolower_table[ch]); + std::transform(s, s + id_len, lower, [](unsigned char ch) { + return static_cast(std::tolower(ch)); }); std::string_view lower_sv(lower, id_len); @@ -1525,8 +1241,7 @@ public: const auto &fs = first_sets_[id]; if (!fs.any_char && !fs.can_be_empty && !fs.chars.test(static_cast(*s))) { - if ((c.log || c.error_reporter) && - (fs.first_literal || fs.first_rule)) { + if (c.log && (fs.first_literal || fs.first_rule)) { if (c.error_info.error_pos <= s) { if (c.error_info.error_pos < s || !(id > 0)) { c.error_info.error_pos = s; @@ -1802,8 +1517,6 @@ public: void accept(Visitor &v) override; friend struct ComputeFirstSet; - friend struct GrammarBlob; - friend struct OpeSignature; bool is_ascii_only() const { return is_ascii_only_; } const std::bitset<256> &ascii_bitset() const { return ascii_bitset_; } @@ -2086,10 +1799,6 @@ public: std::shared_ptr atom_; std::shared_ptr binop_; BinOpeInfo info_; - // Owned backing storage for info_ keys when this node is built by - // GrammarBlob::deserialize. Grammars parsed from source leave this empty and - // point info_ keys into the retained grammar text instead. - std::vector info_keys_; const Definition &rule_; private: @@ -2470,35 +2179,11 @@ struct DetectLeftRecursion : public TraversalVisitor { const char *error_s = nullptr; - // What a bare parameter reference denotes, plus the frame it was found at - // -- see visit_in_defining_scope. - struct ResolvedArg { - std::shared_ptr ope; - size_t depth = 0; - }; - ResolvedArg resolve_macro_arg(size_t iarg) const; - void visit_in_defining_scope(const ResolvedArg &arg); - - // A macro's body depends on its arguments, so "already visited" has to be - // per instantiation, not per name: in `A <- W('z') / W(A)`, visiting W with - // 'z' says nothing about W with A. Instantiations are identified by their - // resolved arguments, the same way as at parse time. - size_t intern_macro_inst(const Reference &ope); - - // A macro that instantiates itself with a growing argument - // (`M(s) <- M(s / 'x')`) has no finite set of instantiations. Stop - // descending instead of looping forever; the rule is then reported as - // non-left-recursive, which is what this analysis did for every macro - // before it became instantiation-aware. The bound is on nesting depth in - // general, not self-recursion specifically, so it also caps any other - // chain of nested macro calls -- generously, for real grammars. - static const size_t max_macro_inst_depth = 32; + std::shared_ptr resolve_macro_arg(size_t iarg) const; private: std::string name_; - std::set> refs_; - std::map, size_t> macro_inst_ids_; - size_t next_macro_inst_ = 1; + std::unordered_set refs_; bool done_ = false; std::vector> *> macro_args_stack_; }; @@ -2536,104 +2221,6 @@ struct ComputeCanBeEmpty : public TraversalVisitor { void visit(Cut &) override { result = false; } }; -// Structural signature of an Ope. Two alternatives whose first k elements -// have equal signatures consume the same text, so their (k+1)-th elements -// start at the same position. Opes whose state cannot be serialized get -// their address instead: that only ever reads as "these differ", which -// costs an optimization rather than adding one. -struct OpeSignature : public Ope::Visitor { - using Ope::Visitor::visit; - std::string s; - - void visit(Sequence &ope) override { group("seq", ope.opes_); } - void visit(PrioritizedChoice &ope) override { group("cho", ope.opes_); } - void visit(Repetition &ope) override { - s += "(rep " + std::to_string(ope.min_) + " " + - (ope.max_ == std::numeric_limits::max() - ? std::string("inf") - : std::to_string(ope.max_)); - wrap(*ope.ope_); - } - void visit(AndPredicate &ope) override { unary("and", *ope.ope_); } - void visit(NotPredicate &ope) override { unary("not", *ope.ope_); } - void visit(CaptureScope &ope) override { unary("cps", *ope.ope_); } - void visit(Capture &ope) override { unary("cap", *ope.ope_); } - void visit(TokenBoundary &ope) override { unary("tok", *ope.ope_); } - void visit(Ignore &ope) override { unary("ign", *ope.ope_); } - void visit(Whitespace &ope) override { unary("wsp", *ope.ope_); } - void visit(Recovery &ope) override { unary("rec", *ope.ope_); } - // A rule is named, never expanded — that is what keeps a recursive - // grammar's signature finite. WeakHolder only ever wraps a Holder, so - // descending through it lands on a name too. - void visit(Holder &ope) override { s += "(hld " + ope.name() + ")"; } - void visit(WeakHolder &ope) override { - if (auto p = ope.weak_.lock()) { - unary("wek", *p); - } else { - opaque(&ope); - } - } - void visit(Reference &ope) override { - s += "(ref " + ope.name_; - for (auto &arg : ope.args_) { - s += ' '; - arg->accept(*this); - } - s += ')'; - } - void visit(LiteralString &ope) override { - s += "(lit " + std::to_string(ope.ignore_case_) + " " + ope.lit_ + ")"; - } - void visit(CharacterClass &ope) override { - s += "(cls " + std::to_string(ope.negated_) + " " + - std::to_string(ope.ignore_case_); - for (const auto &[lo, hi] : ope.ranges_) { - s += " " + std::to_string(static_cast(lo)) + "-" + - std::to_string(static_cast(hi)); - } - s += ')'; - } - void visit(Character &ope) override { - s += "(chr " + std::to_string(static_cast(ope.ch_)) + ")"; - } - void visit(AnyCharacter &) override { s += "(any)"; } - void visit(Dictionary &ope) override { opaque(&ope); } - void visit(User &ope) override { opaque(&ope); } - void visit(BackReference &ope) override { opaque(&ope); } - void visit(PrecedenceClimbing &ope) override { opaque(&ope); } - void visit(Cut &ope) override { opaque(&ope); } - - static std::string get(Ope &ope) { - OpeSignature vis; - ope.accept(vis); - return std::move(vis.s); - } - -private: - void group(const char *tag, const std::vector> &v) { - s += '('; - s += tag; - for (const auto &op : v) { - s += ' '; - op->accept(*this); - } - s += ')'; - } - void unary(const char *tag, Ope &inner) { - s += '('; - s += tag; - wrap(inner); - } - void wrap(Ope &inner) { - s += ' '; - inner.accept(*this); - s += ')'; - } - void opaque(const void *p) { - s += "(opq " + std::to_string(reinterpret_cast(p)) + ")"; - } -}; - struct HasEmptyElement : public TraversalVisitor { using TraversalVisitor::visit; @@ -2984,7 +2571,6 @@ struct SetupFirstSets : public TraversalVisitor { if (cc && cc->is_ascii_only()) { ope.span_bitset_ = &cc->ascii_bitset(); } } void visit(Reference &ope) override; - void visit(Holder &ope) override; private: ComputeFirstSet::FirstSetCache first_set_cache_; @@ -3031,40 +2617,37 @@ public: } Result parse(const char *s, size_t n, const char *path = nullptr, - Log log = nullptr, - ErrorReporter error_reporter = nullptr) const { + Log log = nullptr) const { SemanticValues vs; std::any dt; - return parse_core(s, n, vs, dt, path, log, error_reporter); + return parse_core(s, n, vs, dt, path, log); } - Result parse(const char *s, const char *path = nullptr, Log log = nullptr, - ErrorReporter error_reporter = nullptr) const { + Result parse(const char *s, const char *path = nullptr, + Log log = nullptr) const { auto n = strlen(s); - return parse(s, n, path, log, error_reporter); + return parse(s, n, path, log); } Result parse(const char *s, size_t n, std::any &dt, - const char *path = nullptr, Log log = nullptr, - ErrorReporter error_reporter = nullptr) const { + const char *path = nullptr, Log log = nullptr) const { SemanticValues vs; - return parse_core(s, n, vs, dt, path, log, error_reporter); + return parse_core(s, n, vs, dt, path, log); } Result parse(const char *s, std::any &dt, const char *path = nullptr, - Log log = nullptr, - ErrorReporter error_reporter = nullptr) const { + Log log = nullptr) const { auto n = strlen(s); - return parse(s, n, dt, path, log, error_reporter); + return parse(s, n, dt, path, log); } template Result parse_and_get_value(const char *s, size_t n, T &val, - const char *path = nullptr, Log log = nullptr, - ErrorReporter error_reporter = nullptr) const { + const char *path = nullptr, + Log log = nullptr) const { SemanticValues vs; std::any dt; - auto r = parse_core(s, n, vs, dt, path, log, error_reporter); + auto r = parse_core(s, n, vs, dt, path, log); if (r.ret && !vs.empty() && vs.front().has_value()) { val = std::any_cast(vs[0]); } @@ -3073,18 +2656,17 @@ public: template Result parse_and_get_value(const char *s, T &val, const char *path = nullptr, - Log log = nullptr, - ErrorReporter error_reporter = nullptr) const { + Log log = nullptr) const { auto n = strlen(s); - return parse_and_get_value(s, n, val, path, log, error_reporter); + return parse_and_get_value(s, n, val, path, log); } template Result parse_and_get_value(const char *s, size_t n, std::any &dt, T &val, - const char *path = nullptr, Log log = nullptr, - ErrorReporter error_reporter = nullptr) const { + const char *path = nullptr, + Log log = nullptr) const { SemanticValues vs; - auto r = parse_core(s, n, vs, dt, path, log, error_reporter); + auto r = parse_core(s, n, vs, dt, path, log); if (r.ret && !vs.empty() && vs.front().has_value()) { val = std::any_cast(vs[0]); } @@ -3093,10 +2675,10 @@ public: template Result parse_and_get_value(const char *s, std::any &dt, T &val, - const char *path = nullptr, Log log = nullptr, - ErrorReporter error_reporter = nullptr) const { + const char *path = nullptr, + Log log = nullptr) const { auto n = strlen(s); - return parse_and_get_value(s, n, dt, val, path, log, error_reporter); + return parse_and_get_value(s, n, dt, val, path, log); } #if defined(__cpp_lib_char8_t) @@ -3207,10 +2789,6 @@ public: std::string error_message; bool no_ast_opt = false; - bool no_whitespace = false; // Disable %whitespace skipping inside this rule - // (like a token boundary, without capturing) - std::string ast_name; // When non-empty, AST nodes produced by this rule carry - // this name/tag instead of the rule's own name bool eoi_check = true; @@ -3238,8 +2816,7 @@ private: void initialize_packrat_filter() const; Result parse_core(const char *s, size_t n, SemanticValues &vs, std::any &dt, - const char *path, Log log, - ErrorReporter error_reporter = nullptr) const { + const char *path, Log log) const { initialize_definition_ids(); std::shared_ptr ope = holder_; @@ -3250,28 +2827,22 @@ private: if (tracer_end) { tracer_end(trace_data); } }); - const std::vector *packrat_index = nullptr; - size_t packrat_cached_count = 0; - if (enablePackratParsing) { - initialize_packrat_filter(); - if (!packrat_index_.empty()) { - packrat_index = &packrat_index_; - packrat_cached_count = packrat_cached_count_; - } else { - packrat_cached_count = definition_ids_.size(); - } - } - Context c(path, s, n, definition_ids_.size(), whitespaceOpe, wordOpe, enablePackratParsing, tracer_enter, tracer_leave, trace_data, - verbose_trace, log, error_reporter, packrat_index, - packrat_cached_count); + verbose_trace, log); if (collect_packrat_stats) { packrat_stats_.resize(definition_ids_.size()); c.packrat_stats = &packrat_stats_; } + if (enablePackratParsing) { + initialize_packrat_filter(); + if (!packrat_filter_.empty()) { + c.packrat_rule_filter = &packrat_filter_; + } + } + size_t i = 0; if (whitespaceOpe) { @@ -3310,8 +2881,7 @@ private: mutable std::once_flag definition_ids_init_; mutable std::unordered_map definition_ids_; mutable std::once_flag packrat_filter_init_; - mutable std::vector packrat_index_; // def_id -> cache slot or -1 - mutable size_t packrat_cached_count_ = 0; + mutable std::vector packrat_filter_; }; /* @@ -3325,11 +2895,9 @@ inline size_t parse_literal(const char *s, size_t n, SemanticValues &vs, size_t i = 0; for (; i < lit.size(); i++) { if (i >= n || - (ignore_case - ? (static_cast( - c.tolower_table[static_cast(s[i])]) != - lower_lit[i]) - : (s[i] != lit[i]))) { + (ignore_case ? (static_cast(std::tolower( + static_cast(s[i]))) != lower_lit[i]) + : (s[i] != lit[i]))) { c.set_error_pos(s, lit.data()); return static_cast(-1); } @@ -3382,15 +2950,14 @@ inline std::pair SemanticValues::line_info() const { return c_->line_info(sv_.data()); } -inline void ErrorInfo::output_log(const Log &log, const ErrorReporter &reporter, - const char *s, size_t n) { +inline void ErrorInfo::output_log(const Log &log, const char *s, size_t n) { if (message_pos) { if (message_pos > last_output_pos) { last_output_pos = message_pos; auto line = line_info(s, message_pos); std::string msg; - auto unexpected_token = heuristic_error_token(s, n, message_pos); - if (!unexpected_token.empty()) { + if (auto unexpected_token = heuristic_error_token(s, n, message_pos); + !unexpected_token.empty()) { msg = replace_all(message, "%t", unexpected_token); auto unexpected_char = unexpected_token.substr( @@ -3401,28 +2968,13 @@ inline void ErrorInfo::output_log(const Log &log, const ErrorReporter &reporter, } else { msg = message; } - if (reporter) { - ErrorReport report; - report.line = line.first; - report.col = line.second; - report.position = static_cast(message_pos - s); - report.unexpected_token = unexpected_token; - report.message = msg; - report.label = label; - reporter(report); - } - if (log) { log(line.first, line.second, msg, label); } + log(line.first, line.second, msg, label); } } else if (error_pos) { if (error_pos > last_output_pos) { last_output_pos = error_pos; auto line = line_info(s, error_pos); - ErrorReport report; - report.line = line.first; - report.col = line.second; - report.position = static_cast(error_pos - s); - std::string msg; if (expected_tokens.empty()) { msg = "syntax error."; @@ -3435,7 +2987,6 @@ inline void ErrorInfo::output_log(const Log &log, const ErrorReporter &reporter, msg += ", unexpected '"; msg += unexpected_token; msg += "'"; - report.unexpected_token = unexpected_token; } auto first_item = true; @@ -3450,11 +3001,9 @@ inline void ErrorInfo::output_log(const Log &log, const ErrorReporter &reporter, msg += "'"; msg += error_literal; msg += "'"; - report.expected_literals.emplace_back(error_literal); } else { msg += "<" + error_rule->name + ">"; if (label.empty()) { label = error_rule->name; } - report.expected_rules.emplace_back(error_rule->name); } first_item = false; } @@ -3463,11 +3012,7 @@ inline void ErrorInfo::output_log(const Log &log, const ErrorReporter &reporter, } msg += "."; } - if (reporter) { - report.label = label; - reporter(report); - } - if (log) { log(line.first, line.second, msg, label); } + log(line.first, line.second, msg, label); } } } @@ -3482,7 +3027,7 @@ inline size_t Context::skip_whitespace(const char *a_s, size_t n, } inline void Context::set_error_pos(const char *a_s, const char *literal) { - if (log || error_reporter) { + if (log) { if (error_info.error_pos <= a_s) { if (error_info.error_pos < a_s || !error_info.keep_previous_token) { error_info.error_pos = a_s; @@ -3529,7 +3074,7 @@ inline void Context::trace_leave(const Ope &ope, const char *a_s, size_t n, } inline bool Context::is_traceable(const Ope &ope) const { - if (has_tracer) { + if (tracer_enter && tracer_leave) { if (ignore_trace_state) { return false; } return !dynamic_cast(&ope); } @@ -3624,56 +3169,14 @@ inline size_t TokenBoundary::parse_core(const char *s, size_t n, return len; } -// Resolve `%{name}` placeholders in a custom error message against the -// named captures recorded so far ($name<...>). Unknown names resolve to an -// empty string. `%t` / `%c` are resolved later, at log-output time. -inline std::string resolve_capture_placeholders(const std::string &msg, - const Context &c) { - auto pos = msg.find("%{"); - if (pos == std::string::npos) { return msg; } - - std::string r; - size_t i = 0; - while (pos != std::string::npos) { - auto end = msg.find('}', pos + 2); - if (end == std::string::npos) { break; } - r.append(msg, i, pos - i); - auto name = std::string_view(msg).substr(pos + 2, end - (pos + 2)); - for (auto it = c.capture_entries.rbegin(); it != c.capture_entries.rend(); - ++it) { - if (it->first == name) { - // The captured span can include whitespace skipped after a token - // boundary; trim it for display. - auto v = std::string_view(it->second); - while (!v.empty() && - std::isspace(static_cast(v.back()))) { - v.remove_suffix(1); - } - while (!v.empty() && - std::isspace(static_cast(v.front()))) { - v.remove_prefix(1); - } - r += v; - break; - } - } - i = end + 1; - pos = msg.find("%{", i); - } - r.append(msg, i, msg.size() - i); - return r; -} - inline size_t Holder::parse_core(const char *s, size_t n, SemanticValues &vs, Context &c, std::any &dt) const { if (!ope_) { throw std::logic_error("Uninitialized definition ope was used..."); } - // Macro reference. A left-recursive macro cannot take this path: it needs - // the seed-growing below, which in turn needs its own semantic value scope - // to memoise. Such a macro forms a scope like a plain rule does. - if (outer_->is_macro && !outer_->is_left_recursive) { + // Macro reference + if (outer_->is_macro) { c.rule_stack.push_back(outer_); auto len = ope_->parse(s, n, vs, c, dt); c.rule_stack.pop_back(); @@ -3698,23 +3201,7 @@ inline size_t Holder::parse_core(const char *s, size_t n, SemanticValues &vs, }); c.rule_stack.push_back(outer_); - if (outer_->no_whitespace) { - { - c.in_token_boundary_count++; - auto se2 = scope_exit([&]() { c.in_token_boundary_count--; }); - parse_len = ope_->parse(s, n, chvs, c, dt); - } - if (success(parse_len)) { - auto wl = c.skip_whitespace(s + parse_len, n - parse_len, chvs, dt); - if (fail(wl)) { - parse_len = wl; - } else { - parse_len += wl; - } - } - } else { - parse_len = ope_->parse(s, n, chvs, c, dt); - } + parse_len = ope_->parse(s, n, chvs, c, dt); c.rule_stack.pop_back(); if (success(parse_len)) { @@ -3734,8 +3221,7 @@ inline size_t Holder::parse_core(const char *s, size_t n, SemanticValues &vs, std::any predicate_data; if (outer_->predicate) { if (!outer_->predicate(chvs, dt, msg, predicate_data)) { - if ((c.log || c.error_reporter) && !msg.empty() && - c.error_info.message_pos < s) { + if (c.log && !msg.empty() && c.error_info.message_pos < s) { c.error_info.message_pos = s; c.error_info.message = msg; c.error_info.label = outer_->name; @@ -3747,19 +3233,17 @@ inline size_t Holder::parse_core(const char *s, size_t n, SemanticValues &vs, if (success(parse_len)) { if (!c.recovered) { parse_val = reduce(chvs, dt, predicate_data); } } else { - if ((c.log || c.error_reporter) && !msg.empty() && - c.error_info.message_pos < s) { + if (c.log && !msg.empty() && c.error_info.message_pos < s) { c.error_info.message_pos = s; c.error_info.message = msg; c.error_info.label = outer_->name; } } } else { - if ((c.log || c.error_reporter) && !outer_->error_message.empty() && + if (c.log && !outer_->error_message.empty() && c.error_info.message_pos < s) { c.error_info.message_pos = s; - c.error_info.message = - resolve_capture_placeholders(outer_->error_message, c); + c.error_info.message = outer_->error_message; c.error_info.label = outer_->name; } } @@ -3768,10 +3252,7 @@ inline size_t Holder::parse_core(const char *s, size_t n, SemanticValues &vs, }; if (outer_->is_left_recursive) { - // A macro grows one seed per instantiation: Sum(D) and Sum(L) are - // different rules as far as the memo is concerned. - auto lr_rule = Context::LRRule(outer_, c.top_macro_inst()); - auto lr_key = Context::LRKey(lr_rule, s); + auto lr_key = std::make_pair(outer_, s); // Check LR memo first auto it = c.lr_memo.find(lr_key); @@ -3784,7 +3265,7 @@ inline size_t Holder::parse_core(const char *s, size_t n, SemanticValues &vs, } // Record that this rule's lr_memo was accessed. // Any LR rule currently seeding will know we're in its cycle. - c.lr_refs_hit.insert(lr_rule); + c.lr_refs_hit.insert(outer_); } else { // Seed with FAIL c.lr_memo[lr_key] = {static_cast(-1), {}}; @@ -3806,7 +3287,7 @@ inline size_t Holder::parse_core(const char *s, size_t n, SemanticValues &vs, // the cycle, so add self — this lets parent seeders see us as // a transitive cycle member. auto cycle_rules = c.lr_refs_hit; - if (!cycle_rules.empty()) { cycle_rules.insert(lr_rule); } + if (!cycle_rules.empty()) { cycle_rules.insert(outer_); } // Restore parent's refs and propagate cycle info upward c.lr_refs_hit = std::move(saved_refs); @@ -3822,17 +3303,15 @@ inline size_t Holder::parse_core(const char *s, size_t n, SemanticValues &vs, c.lr_memo[lr_key] = {len, val}; while (true) { - // Clear this rule's packrat cache. A macro is never written there - // (that cache is keyed by rule id alone, which cannot tell two - // instantiations apart), so there is nothing to clear for one. - if (!outer_->is_macro) { c.clear_packrat_cache(s, outer_->id); } + // Clear this rule's packrat cache + c.clear_packrat_cache(s, outer_->id); // Clear lr_memo for cycle-dependent rules at this position, // but NOT for rules currently in their own seeding phase // (lr_active_seeds) — those are outer growers we must not // interfere with. for (auto memo_it = c.lr_memo.begin(); memo_it != c.lr_memo.end();) { - if (memo_it->first.second == s && memo_it->first.first != lr_rule && + if (memo_it->first.second == s && memo_it->first.first != outer_ && cycle_rules.count(memo_it->first.first) && !c.lr_active_seeds.count(memo_it->first)) { memo_it = c.lr_memo.erase(memo_it); @@ -3855,9 +3334,7 @@ inline size_t Holder::parse_core(const char *s, size_t n, SemanticValues &vs, // Write final result to packrat cache (lr_memo entry is kept as // the primary lookup for LR rules at this position) - if (success(len) && !outer_->is_macro) { - c.write_packrat_cache(s, outer_->id, len, val); - } + if (success(len)) { c.write_packrat_cache(s, outer_->id, len, val); } } } else { if (c.enablePackratParsing) { @@ -3871,7 +3348,7 @@ inline size_t Holder::parse_core(const char *s, size_t n, SemanticValues &vs, } else { // Without packrat, use lr_memo as re-entry guard to prevent // stack overflow from undetected left recursion. - auto guard_key = Context::LRKey({outer_, c.top_macro_inst()}, s); + auto guard_key = std::make_pair(outer_, s); if (c.lr_memo.count(guard_key)) { len = static_cast(-1); } else { @@ -3913,23 +3390,6 @@ inline const std::string &Holder::trace_name() const { return trace_name_; } -// Key a macro instantiation by what each argument denotes rather than by the -// node that spells it: `M(N)` written at two call sites builds two Reference -// nodes for the same rule N, and those are the same instantiation. -inline std::vector -macro_inst_key(const Definition *def, - const std::vector> &args) { - std::vector key; - key.reserve(args.size() + 1); - key.push_back(def); - for (const auto &arg : args) { - auto ref = dynamic_cast(arg.get()); - key.push_back(ref && ref->rule_ ? static_cast(ref->rule_) - : static_cast(arg.get())); - } - return key; -} - inline size_t Reference::parse_core(const char *s, size_t n, SemanticValues &vs, Context &c, std::any &dt) const { auto save_ignore_trace_state = c.ignore_trace_state; @@ -3952,10 +3412,7 @@ inline size_t Reference::parse_core(const char *s, size_t n, SemanticValues &vs, args.emplace_back(std::move(vis.found_ope)); } - auto inst = rule_->is_left_recursive - ? c.intern_macro_inst(macro_inst_key(rule_, args)) - : 0; - c.push_args(std::move(args), inst); + c.push_args(std::move(args)); auto se = scope_exit([&]() { c.pop_args(); }); return rule_->holder_->parse(s, n, vs, c, dt); } else { @@ -4091,12 +3548,11 @@ inline size_t Recovery::parse_core(const char *s, size_t n, const auto &rule = dynamic_cast(*ope_); // Custom error message - if (c.log || c.error_reporter) { + if (c.log) { auto label = dynamic_cast(rule.args_[0].get()); if (label && !label->rule_->error_message.empty()) { c.error_info.message_pos = s; - c.error_info.message = - resolve_capture_placeholders(label->rule_->error_message, c); + c.error_info.message = label->rule_->error_message; c.error_info.label = label->rule_->name; } } @@ -4105,13 +3561,8 @@ inline size_t Recovery::parse_core(const char *s, size_t n, auto len = static_cast(-1); { auto save_log = c.log; - auto save_reporter = c.error_reporter; c.log = nullptr; - c.error_reporter = nullptr; - auto se = scope_exit([&]() { - c.log = save_log; - c.error_reporter = save_reporter; - }); + auto se = scope_exit([&]() { c.log = save_log; }); SemanticValues dummy_vs; std::any dummy_dt; @@ -4122,8 +3573,8 @@ inline size_t Recovery::parse_core(const char *s, size_t n, if (success(len)) { c.recovered = true; - if (c.log || c.error_reporter) { - c.error_info.output_log(c.log, c.error_reporter, c.s, c.l); + if (c.log) { + c.error_info.output_log(c.log, c.s, c.l); c.error_info.clear(); } } @@ -4211,37 +3662,32 @@ inline void ComputeCanBeEmpty::visit(Reference &ope) { } inline void DetectLeftRecursion::visit(Reference &ope) { - // Macro parameter reference: what it denotes lives in an enclosing - // instantiation (e.g. B(X) <- C(X) where X is itself a param ref). - auto param = !ope.rule_ && !macro_args_stack_.empty() - ? resolve_macro_arg(ope.iarg_) - : ResolvedArg{}; - if (ope.name_ == name_) { error_s = ope.s_; - } else if (param.ope) { - visit_in_defining_scope(param); - if (done_ == false) { return; } - } else if (ope.is_macro_ && - macro_args_stack_.size() >= max_macro_inst_depth) { - // Unbounded instantiation chain; stop descending. - } else if (ope.rule_ && - refs_ - .emplace(ope.rule_, ope.is_macro_ ? intern_macro_inst(ope) : 0) - .second) { - if (ope.is_macro_) { macro_args_stack_.push_back(&ope.args_); } - ope.rule_->accept(*this); - if (ope.is_macro_) { macro_args_stack_.pop_back(); } - if (done_ == false) { return; } + } else if (!ope.rule_ && !macro_args_stack_.empty()) { + // Macro parameter reference: resolve through nested macro arg + // stacks (e.g. B(X) <- C(X) where X is itself a param ref). + auto resolved = resolve_macro_arg(ope.iarg_); + if (resolved) { + resolved->accept(*this); + if (done_ == false) { return; } + } + } else if (!refs_.count(ope.name_)) { + refs_.insert(ope.name_); + if (ope.rule_) { + if (ope.is_macro_) { macro_args_stack_.push_back(&ope.args_); } + ope.rule_->accept(*this); + if (ope.is_macro_) { macro_args_stack_.pop_back(); } + if (done_ == false) { return; } + } } // If the referenced rule can match empty, don't mark as done — // the sequence may continue past this element to find LR. if (!ope.rule_ && !macro_args_stack_.empty()) { - if (param.ope) { - // ComputeCanBeEmpty never consults the frame stack, so the scope it - // runs in cannot matter. + auto resolved = resolve_macro_arg(ope.iarg_); + if (resolved) { ComputeCanBeEmpty cbe; - param.ope->accept(cbe); + resolved->accept(cbe); done_ = !cbe.result; } else { done_ = true; @@ -4251,51 +3697,20 @@ inline void DetectLeftRecursion::visit(Reference &ope) { } } -inline size_t DetectLeftRecursion::intern_macro_inst(const Reference &ope) { - // Resolve bare parameter references to what the enclosing instantiation was - // given, so a macro passing its own parameter through interns to the same - // instantiation instead of a fresh one at every nesting level. - std::vector> args; - args.reserve(ope.args_.size()); - for (const auto &arg : ope.args_) { - auto ref = dynamic_cast(arg.get()); - auto resolved = ref && !ref->rule_ && !macro_args_stack_.empty() - ? resolve_macro_arg(ref->iarg_).ope - : nullptr; - args.push_back(resolved ? resolved : arg); - } - auto [it, inserted] = macro_inst_ids_.emplace(macro_inst_key(ope.rule_, args), - next_macro_inst_); - if (inserted) { next_macro_inst_++; } - return it->second; -} - -inline void -DetectLeftRecursion::visit_in_defining_scope(const ResolvedArg &arg) { - // The frames below the one holding it are the scope it was written in. - // `W(X) <- Y(X / 'x')` passes Y an argument whose own `X` means W's - // parameter, not Y's -- leaving Y's frame visible would resolve that `X` - // right back to `X / 'x'`, forever. - auto saved = macro_args_stack_; - auto se = scope_exit([&]() { macro_args_stack_ = std::move(saved); }); - macro_args_stack_.resize(arg.depth); - arg.ope->accept(*this); -} - -inline DetectLeftRecursion::ResolvedArg +inline std::shared_ptr DetectLeftRecursion::resolve_macro_arg(size_t iarg) const { for (int i = static_cast(macro_args_stack_.size()) - 1; i >= 0; i--) { auto &args = *macro_args_stack_[i]; - if (iarg >= args.size()) { return {}; } + if (iarg >= args.size()) { return nullptr; } auto ref = dynamic_cast(args[iarg].get()); if (ref && !ref->rule_) { // Another param ref — resolve using parent level's args iarg = ref->iarg_; continue; } - return {args[iarg], static_cast(i)}; + return args[iarg]; } - return {}; + return nullptr; } inline void HasEmptyElement::visit(Sequence &ope) { @@ -4448,16 +3863,8 @@ inline void ComputeFirstSet::visit(Reference &ope) { inline void SetupFirstSets::visit(Reference &ope) { if (!ope.rule_) { return; } - ope.rule_->accept(*this); // re-entry is guarded at the rule's Holder -} - -// Guard rule setup by Definition so a SetupFirstSets shared across all rules -// visits each rule's body at most once for the whole grammar. Without this the -// per-rule setup re-walks every reachable rule once per referencing rule, which -// is O(N^2) for grammars with dense cross-references. -inline void SetupFirstSets::visit(Holder &ope) { - if (!visited_rules_.insert(ope.outer_).second) { return; } - ope.ope_->accept(*this); + if (!visited_rules_.insert(ope.rule_).second) { return; } + ope.rule_->accept(*this); } inline void SetupFirstSets::visit(Sequence &ope) { @@ -4575,47 +3982,17 @@ inline void Definition::initialize_packrat_filter() const { auto def_count = definition_ids_.size(); if (def_count == 0) { return; } - // Collect rule IDs that can be invoked at the *same start position* as - // the given Ope subtree (leftmost reachability). A packrat cache hit - // requires the same rule to be queried twice at the same position, and - // in a PEG that only happens when alternatives of a choice share a - // leftmost prefix — rules reachable only past a consuming element can - // never be re-queried by a sibling alternative. - struct CollectLeftmostRules : public TraversalVisitor { + // Collect rule IDs reachable from an Ope subtree (bitvector indexed by + // def_id) + struct CollectReachableRules : public TraversalVisitor { using TraversalVisitor::visit; std::vector reachable; // indexed by def_id - std::vector - visited_rules; // indexed by def_id; guards Holder cycles - CollectLeftmostRules(size_t n) - : reachable(n, false), visited_rules(n, false) {} + CollectReachableRules(size_t n) : reachable(n, false) {} - // Collect from the position element `from` starts at: element `from` - // itself, plus what follows for as long as elements can match empty — - // only up to (and including) the first one that must consume input. - void collect(const std::vector> &opes, size_t from) { - for (auto i = from; i < opes.size(); i++) { - opes[i]->accept(*this); - ComputeCanBeEmpty empty_vis; - opes[i]->accept(empty_vis); - if (!empty_vis.result) { break; } - } - } - - void visit(Sequence &ope) override { collect(ope.opes_, 0); } void visit(Holder &ope) override { auto id = ope.outer_->id; - if (id < reachable.size()) { - reachable[id] = true; - - // Grammars built directly via the combinator API embed rules through - // WeakHolder rather than Reference, so a recursive rule forms a - // Holder cycle with no Reference to break it. Guard re-entry to avoid - // infinite recursion (reachability is monotone, so revisiting a rule - // we have already traversed adds nothing). - if (visited_rules[id]) { return; } - visited_rules[id] = true; - } + if (id < reachable.size()) { reachable[id] = true; } ope.ope_->accept(*this); } void visit(Reference &ope) override { @@ -4627,8 +4004,7 @@ inline void Definition::initialize_packrat_filter() const { } }; - // Find rules that benefit: queried by 2+ alternatives of the same choice - // at the same position + // Find rules that benefit: reachable from 2+ alternatives of same choice std::vector benefits(def_count, false); struct FindBacktrackRules : public TraversalVisitor { @@ -4640,60 +4016,24 @@ inline void Definition::initialize_packrat_filter() const { FindBacktrackRules(std::vector &b, size_t n) : benefits(b), def_count(n), visited_rules(n, false) {} - using Elements = std::vector>; - - // An alternative's top-level elements, so a shared prefix can be walked - // element by element. By value: this runs once per grammar. - static Elements elements_of(const std::shared_ptr &alt) { - if (auto *seq = dynamic_cast(alt.get())) { - return seq->opes_; + void visit(PrioritizedChoice &ope) override { + // For each alternative, collect reachable rules as bitvectors + std::vector> alt_reachable; + for (auto &op : ope.opes_) { + CollectReachableRules crr(def_count); + op->accept(crr); + alt_reachable.push_back(std::move(crr.reachable)); } - return {alt}; - } - // `group` holds alternatives that agree on their first `k` elements, so - // every one of them reaches element k at the same input position — that - // is exactly when a packrat cache entry can hit. k == 0 is the plain - // "alternatives of one choice" case; deeper k is what a shared prefix - // like `'(' _ PATTERN _ ',' _` hides. - void mark_aligned(const std::vector &group, size_t k) { - if (group.size() < 2) { return; } - - std::vector> reachable; - reachable.reserve(group.size()); - for (const auto &seq : group) { - CollectLeftmostRules clr(def_count); - clr.collect(seq, k); - reachable.push_back(std::move(clr.reachable)); - } + // Mark rules reachable from 2+ alternatives for (size_t id = 0; id < def_count; id++) { size_t count = 0; - for (const auto &alt : reachable) { + for (auto &alt : alt_reachable) { if (alt[id]) { count++; } } if (count >= 2) { benefits[id] = true; } } - // Only alternatives that also agree on element k stay aligned past it. - std::map> aligned; - for (const auto &seq : group) { - if (k < seq.size()) { - aligned[OpeSignature::get(*seq[k])].push_back(seq); - } - } - for (const auto &[sig, sub] : aligned) { - mark_aligned(sub, k + 1); - } - } - - void visit(PrioritizedChoice &ope) override { - std::vector group; - group.reserve(ope.opes_.size()); - for (const auto &op : ope.opes_) { - group.push_back(elements_of(op)); - } - mark_aligned(group, 0); - // Recurse into alternatives for (auto &op : ope.opes_) { op->accept(*this); @@ -4716,23 +4056,7 @@ inline void Definition::initialize_packrat_filter() const { if (whitespaceOpe) { whitespaceOpe->accept(finder); } if (wordOpe) { wordOpe->accept(finder); } - // Left-recursive rules read and write the packrat cache directly during - // seed-growing, so they must stay in the cached set. Macros are the - // exception: they use lr_memo only, keyed by instantiation. - for (const auto &[ptr, id] : definition_ids_) { - auto *def = static_cast(ptr); - if (def->is_left_recursive && !def->is_macro && id < def_count) { - benefits[id] = true; - } - } - - // Compact index: def_id -> slot in the cache tables (-1 = guard only) - packrat_index_.assign(def_count, -1); - int32_t k = 0; - for (size_t id = 0; id < def_count; id++) { - if (benefits[id]) { packrat_index_[id] = k++; } - } - packrat_cached_count_ = static_cast(k); + packrat_filter_ = std::move(benefits); }); } @@ -4770,419 +4094,6 @@ inline void FindReference::visit(Reference &ope) { found_ope = ope.shared_from_this(); } -/*----------------------------------------------------------------------------- - * Grammar serialization - * - * Serialize a compiled Grammar (the operator tree) to a byte blob and back, - * letting an application skip the meta-parse on startup by embedding a - * prebuilt blob. Structure only: semantic callbacks (actions / enter / leave / - * predicate, attached by enable_ast() etc.) are NOT serialized and must be - * re-applied after deserialize. References resolve by name (no pointer fixup); - * first-sets and keyword guards are recomputed on load (O(N)). The - * `precedence` instruction is supported (its operator table is structural). - * Grammars using the `User` operator or a Capture with a match action are - * rejected. The blob is specific to this peglib version's layout. - *---------------------------------------------------------------------------*/ - -struct GrammarBlob { - enum Tag : uint8_t { - T_Sequence, - T_Choice, - T_Repetition, - T_And, - T_Not, - T_Dictionary, - T_Literal, - T_CharClass, - T_Char, - T_AnyChar, - T_CaptureScope, - T_Capture, - T_TokenBoundary, - T_Ignore, - T_BackRef, - T_Reference, - T_Whitespace, - T_Recovery, - T_Cut, - T_PrecedenceClimbing, - T_Null - }; - - struct Writer { - std::vector b; - void u8(uint8_t v) { b.push_back(v); } - void u32(uint32_t v) { - for (int i = 0; i < 4; i++) - b.push_back((v >> (8 * i)) & 0xff); - } - void u64(uint64_t v) { - for (int i = 0; i < 8; i++) - b.push_back((v >> (8 * i)) & 0xff); - } - void str(const std::string &s) { - u32((uint32_t)s.size()); - b.insert(b.end(), s.begin(), s.end()); - } - }; - - static void write_ope(Writer &w, const std::shared_ptr &o) { - if (!o) { - w.u8(T_Null); - return; - } - Ope *p = o.get(); - if (auto x = dynamic_cast(p)) { - w.u8(T_Sequence); - w.u32((uint32_t)x->opes_.size()); - for (auto &c : x->opes_) - write_ope(w, c); - } else if (auto x = dynamic_cast(p)) { - w.u8(T_Choice); - w.u8(x->for_label_ ? 1 : 0); - w.u32((uint32_t)x->opes_.size()); - for (auto &c : x->opes_) - write_ope(w, c); - } else if (auto x = dynamic_cast(p)) { - w.u8(T_Repetition); - w.u64(x->min_); - w.u64(x->max_); - write_ope(w, x->ope_); - } else if (auto x = dynamic_cast(p)) { - w.u8(T_And); - write_ope(w, x->ope_); - } else if (auto x = dynamic_cast(p)) { - w.u8(T_Not); - write_ope(w, x->ope_); - } else if (auto x = dynamic_cast(p)) { - w.u8(T_Dictionary); - w.u8(x->trie_.ignore_case_ ? 1 : 0); - // Recover words in their original choice-index order. The Trie stores - // each full word's id (its index in the constructor vector), which - // parse_core reports as vs.choice(). Iterating dic_ directly yields - // sorted key order and would renumber the choices, so place each word at - // its id. - std::vector words(x->trie_.items_count()); - for (auto &kv : x->trie_.dic_) - if (kv.second.match && kv.second.id < words.size()) - words[kv.second.id] = kv.first; - w.u32((uint32_t)words.size()); - for (auto &s : words) - w.str(s); - } else if (auto x = dynamic_cast(p)) { - w.u8(T_Literal); - w.u8(x->ignore_case_ ? 1 : 0); - w.str(x->lit_); - } else if (auto x = dynamic_cast(p)) { - w.u8(T_CharClass); - w.u8(x->negated_ ? 1 : 0); - w.u8(x->ignore_case_ ? 1 : 0); - w.u32((uint32_t)x->ranges_.size()); - for (auto &r : x->ranges_) { - w.u32((uint32_t)r.first); - w.u32((uint32_t)r.second); - } - } else if (auto x = dynamic_cast(p)) { - w.u8(T_Char); - w.u32((uint32_t)x->ch_); - } else if (dynamic_cast(p)) { - w.u8(T_AnyChar); - } else if (auto x = dynamic_cast(p)) { - w.u8(T_CaptureScope); - write_ope(w, x->ope_); - } else if (auto x = dynamic_cast(p)) { - if (x->match_action_) { - throw std::runtime_error( - "GrammarBlob: Capture with a match action is not serializable"); - } - w.u8(T_Capture); - write_ope(w, x->ope_); - } else if (auto x = dynamic_cast(p)) { - w.u8(T_TokenBoundary); - write_ope(w, x->ope_); - } else if (auto x = dynamic_cast(p)) { - w.u8(T_Ignore); - write_ope(w, x->ope_); - } else if (auto x = dynamic_cast(p)) { - w.u8(T_BackRef); - w.str(x->name_); - } else if (auto x = dynamic_cast(p)) { - w.u8(T_Reference); - w.u8(x->is_macro_ ? 1 : 0); - w.str(x->name_); - w.u32((uint32_t)x->args_.size()); - for (auto &a : x->args_) - write_ope(w, a); - } else if (auto x = dynamic_cast(p)) { - w.u8(T_Whitespace); - write_ope(w, x->ope_); - } else if (auto x = dynamic_cast(p)) { - w.u8(T_Recovery); - write_ope(w, x->ope_); - } else if (dynamic_cast(p)) { - w.u8(T_Cut); - } else if (auto x = dynamic_cast(p)) { - w.u8(T_PrecedenceClimbing); - write_ope(w, x->atom_); - write_ope(w, x->binop_); - w.u32((uint32_t)x->info_.size()); - for (auto &[key, pri] : x->info_) { - w.str(std::string(key)); - w.u64((uint64_t)pri.first); - w.u8((uint8_t)pri.second); - } - } else { - throw std::runtime_error( - "GrammarBlob: operator not serializable (a custom User operator or " - "a Capture with a match action)"); - } - } - - struct Reader { - const uint8_t *p, *end; - uint8_t u8() { - if (p >= end) - throw std::runtime_error("GrammarBlob: unexpected end of blob"); - return *p++; - } - uint32_t u32() { - uint32_t v = 0; - for (int i = 0; i < 4; i++) - v |= (uint32_t)u8() << (8 * i); - return v; - } - uint64_t u64() { - uint64_t v = 0; - for (int i = 0; i < 8; i++) - v |= (uint64_t)u8() << (8 * i); - return v; - } - std::string str() { - uint32_t n = u32(); - std::string s((const char *)p, (const char *)p + n); - p += n; - return s; - } - }; - - static std::shared_ptr read_ope(Reader &r, Grammar &g, - Definition *owner) { - switch (r.u8()) { - case T_Null: return nullptr; - case T_Sequence: { - uint32_t n = r.u32(); - std::vector> v; - for (uint32_t i = 0; i < n; i++) - v.push_back(read_ope(r, g, owner)); - return std::make_shared(std::move(v)); - } - case T_Choice: { - bool fl = r.u8(); - uint32_t n = r.u32(); - std::vector> v; - for (uint32_t i = 0; i < n; i++) - v.push_back(read_ope(r, g, owner)); - auto c = std::make_shared(std::move(v)); - c->for_label_ = fl; - return c; - } - case T_Repetition: { - uint64_t mn = r.u64(), mx = r.u64(); - auto o = read_ope(r, g, owner); - return std::make_shared(o, mn, mx); - } - case T_And: return std::make_shared(read_ope(r, g, owner)); - case T_Not: return std::make_shared(read_ope(r, g, owner)); - case T_Dictionary: { - bool ic = r.u8(); - uint32_t n = r.u32(); - std::vector words; - for (uint32_t i = 0; i < n; i++) - words.push_back(r.str()); - return std::make_shared(words, ic); - } - case T_Literal: { - bool ic = r.u8(); - std::string s = r.str(); - return std::make_shared(std::move(s), ic); - } - case T_CharClass: { - bool neg = r.u8(), ic = r.u8(); - uint32_t n = r.u32(); - std::vector> ranges; - for (uint32_t i = 0; i < n; i++) { - auto lo = r.u32(), hi = r.u32(); - ranges.emplace_back((char32_t)lo, (char32_t)hi); - } - return std::make_shared(ranges, neg, ic); - } - case T_Char: return std::make_shared((char32_t)r.u32()); - case T_AnyChar: return std::make_shared(); - case T_CaptureScope: - return std::make_shared(read_ope(r, g, owner)); - case T_Capture: { - auto o = read_ope(r, g, owner); - return std::make_shared(o, nullptr); - } - case T_TokenBoundary: - return std::make_shared(read_ope(r, g, owner)); - case T_Ignore: return std::make_shared(read_ope(r, g, owner)); - case T_BackRef: return std::make_shared(r.str()); - case T_Reference: { - bool im = r.u8(); - std::string nm = r.str(); - uint32_t n = r.u32(); - std::vector> args; - for (uint32_t i = 0; i < n; i++) - args.push_back(read_ope(r, g, owner)); - return std::make_shared(g, nm, nullptr, im, args); - } - case T_Whitespace: - return std::make_shared(read_ope(r, g, owner)); - case T_Recovery: return std::make_shared(read_ope(r, g, owner)); - case T_Cut: return std::make_shared(); - case T_PrecedenceClimbing: { - if (!owner) { - throw std::runtime_error( - "GrammarBlob: 'precedence' operator outside a rule body"); - } - auto atom = read_ope(r, g, owner); - auto binop = read_ope(r, g, owner); - uint32_t n = r.u32(); - auto pc = std::make_shared( - atom, binop, PrecedenceClimbing::BinOpeInfo{}, *owner); - // info_ keys are string_views; back them with owned strings whose - // addresses stay stable (reserve avoids reallocation, and the node is - // never moved once held by shared_ptr). - pc->info_keys_.reserve(n); - for (uint32_t i = 0; i < n; i++) { - std::string key = r.str(); - auto level = (size_t)r.u64(); - auto assoc = (char)r.u8(); - pc->info_keys_.push_back(std::move(key)); - pc->info_[pc->info_keys_.back()] = std::pair(level, assoc); - } - return pc; - } - default: throw std::runtime_error("GrammarBlob: bad operator tag"); - } - } - - static const uint32_t MAGIC = 0x50454732; // "PEG2" - - static std::vector serialize(const Grammar &g, - const std::string &start) { - Writer w; - w.u32(MAGIC); - w.str(start); - w.u32((uint32_t)g.size()); - // Grammar is an unordered_map, whose iteration order is implementation - // defined: walking it directly yields different bytes for the same grammar - // on different standard libraries, so a blob generated on one platform - // cannot be byte-compared on another. Emit the definitions by name. - // deserialize() rebuilds the map from the names, so the order carries no - // meaning of its own. - std::vector defs; - defs.reserve(g.size()); - for (auto &kv : g) - defs.push_back(&kv); - std::sort(defs.begin(), defs.end(), - [](const auto *a, const auto *b) { return a->first < b->first; }); - for (auto *kv : defs) { - const auto &name = kv->first; - const auto &def = kv->second; - w.str(name); - uint8_t flags = - (def.ignoreSemanticValue ? 1 : 0) | (def.is_macro ? 2 : 0) | - (def.no_ast_opt ? 4 : 0) | (def.eoi_check ? 8 : 0) | - (def.enablePackratParsing ? 16 : 0) | - (def.is_left_recursive ? 32 : 0) | (def.can_be_empty ? 64 : 0) | - (def.disable_action ? 128 : 0); - w.u8(flags); - uint8_t flags2 = (def.no_whitespace ? 1 : 0); - w.u8(flags2); - w.u32((uint32_t)def.params.size()); - for (auto &s : def.params) - w.str(s); - w.str(def.ast_name); - w.str(def.error_message); - write_ope(w, const_cast(def).get_core_operator()); - } - return std::move(w.b); - } - - static std::shared_ptr deserialize(const std::vector &blob, - std::string &start_out) { - Reader r{blob.data(), blob.data() + blob.size()}; - if (r.u32() != MAGIC) - throw std::runtime_error("GrammarBlob: bad magic / not a grammar blob"); - start_out = r.str(); - uint32_t ndef = r.u32(); - auto g = std::make_shared(); - // Create each Definition before reading its body: a PrecedenceClimbing node - // needs a stable reference to its owning rule at construction. Grammar is a - // node-based map, so references stay valid as later rules are inserted. - for (uint32_t i = 0; i < ndef; i++) { - std::string name = r.str(); - uint8_t flags = r.u8(); - uint8_t flags2 = r.u8(); - uint32_t np = r.u32(); - std::vector params; - for (uint32_t k = 0; k < np; k++) - params.push_back(r.str()); - std::string ast_name = r.str(); - std::string err = r.str(); - - auto &def = (*g)[name]; - def.name = name; - def.ignoreSemanticValue = flags & 1; - def.is_macro = flags & 2; - def.no_ast_opt = flags & 4; - def.eoi_check = flags & 8; - def.enablePackratParsing = flags & 16; - def.is_left_recursive = flags & 32; - def.can_be_empty = flags & 64; - def.disable_action = flags & 128; - def.no_whitespace = flags2 & 1; - def.params = std::move(params); - def.ast_name = std::move(ast_name); - def.error_message = std::move(err); - - auto body = read_ope(r, *g, &def); - def <= body; - } - for (auto &x : *g) { - LinkReferences vis(*g, x.second.params); - x.second.accept(vis); - // TraversalVisitor descends only into a PrecedenceClimbing's atom_. In - // the from-source path binop_ is linked while the body is still a - // Sequence, before precedence lowering; a deserialized node is built - // already lowered so its binop_ reference must be linked explicitly here. - auto core = x.second.get_core_operator(); - if (auto pc = std::dynamic_pointer_cast(core)) { - pc->binop_->accept(vis); - } - } - { - SetupFirstSets vis; // shared across rules -> O(N) - for (auto &x : *g) - x.second.accept(vis); - } - // Re-derive automatic whitespace/word skipping on the start rule from the - // %whitespace / %word definitions, exactly as ParserGenerator does. Sharing - // the (already linked and first-set) definition operators avoids leaving - // references inside the skipping ope unlinked, and keeps the blob smaller. - if (g->count(WHITESPACE_DEFINITION_NAME)) { - (*g)[start_out].whitespaceOpe = - wsp((*g)[WHITESPACE_DEFINITION_NAME].get_core_operator()); - } - if (g->count(WORD_DEFINITION_NAME)) { - (*g)[start_out].wordOpe = (*g)[WORD_DEFINITION_NAME].get_core_operator(); - } - return g; - } -}; - /*----------------------------------------------------------------------------- * PEG parser generator *---------------------------------------------------------------------------*/ @@ -5229,17 +4140,6 @@ private: ParserGenerator() { make_grammar(); setup_actions(); - // Apply First-Set filtering to the bootstrap meta-grammar itself so that - // parsing a grammar (the bulk of load_grammar) skips alternatives whose - // next byte cannot match. This is safe -- First-Set filtering only skips - // alternatives that would have failed anyway, so no semantic action that - // would have committed is skipped (unlike packrat, which is unsound here). - { - SetupFirstSets vis; - for (auto &x : g) { - x.second.accept(vis); - } - } } struct Instruction { @@ -5281,13 +4181,11 @@ private: void make_grammar() { // Setup PEG syntax parser g["Grammar"] <= seq(g["Spacing"], oom(g["Definition"]), g["EndOfFile"]); - // Left-factored: parse the rule name (IdentCont) once, then optionally the - // macro parameter list. `opt(Parameters)` pushes a value only for a macro - // (so the value layout matches the old two-alternative form), and Spacing - // (~, no value) consumes the gap before LEFTARROW that Identifier used to. - g["Definition"] <= seq(g["Ignore"], g["IdentCont"], opt(g["Parameters"]), - g["Spacing"], g["LEFTARROW"], g["Expression"], - opt(g["Instruction"])); + g["Definition"] <= + cho(seq(g["Ignore"], g["IdentCont"], g["Parameters"], g["LEFTARROW"], + g["Expression"], opt(g["Instruction"])), + seq(g["Ignore"], g["Identifier"], g["LEFTARROW"], g["Expression"], + opt(g["Instruction"]))); g["Expression"] <= seq(g["Sequence"], zom(seq(g["SLASH"], g["Sequence"]))); g["Sequence"] <= zom(cho(g["CUT"], g["Prefix"])); g["Prefix"] <= seq(opt(cho(g["AND"], g["NOT"])), g["SuffixWithLabel"]); @@ -5295,19 +4193,17 @@ private: seq(g["Suffix"], opt(seq(g["LABEL"], g["Identifier"]))); g["Suffix"] <= seq(g["Primary"], opt(g["Loop"])); g["Loop"] <= cho(g["QUESTION"], g["STAR"], g["PLUS"], g["Repetition"]); - // Left-factored: a macro reference (`Name(args)`) and a plain reference - // (`Name`) share the leading `Ignore IdentCont`, so parse it once and let - // `opt(Arguments)` decide. opt() pushes the argument list only for a macro - // reference, so vs.size() distinguishes the two in the action. - g["Primary"] <= - cho(seq(g["Ignore"], g["IdentCont"], opt(g["Arguments"]), g["Spacing"], - npd(seq(opt(g["Parameters"]), g["LEFTARROW"]))), - seq(g["OPEN"], g["Expression"], g["CLOSE"]), - seq(g["BeginTok"], g["Expression"], g["EndTok"]), g["CapScope"], - seq(g["BeginCap"], g["Expression"], g["EndCap"]), g["BackRef"], - g["DictionaryI"], g["LiteralI"], g["Dictionary"], g["Literal"], - g["NegatedClassI"], g["NegatedClass"], g["ClassI"], g["Class"], - g["DOT"]); + g["Primary"] <= cho(seq(g["Ignore"], g["IdentCont"], g["Arguments"], + npd(g["LEFTARROW"])), + seq(g["Ignore"], g["Identifier"], + npd(seq(opt(g["Parameters"]), g["LEFTARROW"]))), + seq(g["OPEN"], g["Expression"], g["CLOSE"]), + seq(g["BeginTok"], g["Expression"], g["EndTok"]), + g["CapScope"], + seq(g["BeginCap"], g["Expression"], g["EndCap"]), + g["BackRef"], g["DictionaryI"], g["LiteralI"], + g["Dictionary"], g["Literal"], g["NegatedClassI"], + g["NegatedClass"], g["ClassI"], g["Class"], g["DOT"]); g["Identifier"] <= seq(g["IdentCont"], g["Spacing"]); g["IdentCont"] <= tok(seq(g["IdentStart"], zom(g["IdentRest"]))); @@ -5356,12 +4252,8 @@ private: // NOTE: This is different from The original Brian Ford's paper, and this // modification allows us to specify `[+-]` as a valid char class. - g["Range"] <= cho(seq(g["Char"], chr('-'), npd(chr(']')), g["Char"]), - g["ClassEscape"], g["PosixClass"], g["Char"]); - - g["ClassEscape"] <= seq(chr('\\'), cls("dDwWsS")); - g["PosixClass"] <= - seq(lit("[:"), opt(chr('^')), oom(cls("a-z")), lit(":]")); + g["Range"] <= + cho(seq(g["Char"], chr('-'), npd(chr(']')), g["Char"]), g["Char"]); g["Char"] <= cho(seq(chr('\\'), cls("fnrtv'\"[]\\^-")), @@ -5431,8 +4323,8 @@ private: opt(seq(g["InstructionItem"], zom(seq(g["InstructionItemSeparator"], g["InstructionItem"])))), g["EndBracket"]); - g["InstructionItem"] <= cho(g["PrecedenceClimbing"], g["ErrorMessage"], - g["NoAstOpt"], g["NoWhitespace"], g["AstName"]); + g["InstructionItem"] <= + cho(g["PrecedenceClimbing"], g["ErrorMessage"], g["NoAstOpt"]); ~g["InstructionItemSeparator"] <= seq(chr(';'), g["Spacing"]); ~g["SpacesZom"] <= zom(g["Space"]); @@ -5465,13 +4357,6 @@ private: // No Ast node optimization instruction g["NoAstOpt"] <= seq(lit("no_ast_opt"), g["SpacesZom"]); - // No whitespace skipping instruction - g["NoWhitespace"] <= seq(lit("no_whitespace"), g["SpacesZom"]); - - // AST node name override instruction: `{ ast_name: NodeTag }` - g["AstName"] <= seq(lit("ast_name"), g["SpacesZom"], lit(":"), - g["SpacesZom"], g["Identifier"], g["SpacesZom"]); - // Set definition names for (auto &x : g) { x.second.name = x.first; @@ -5482,10 +4367,7 @@ private: g["Definition"] = [&](const SemanticValues &vs, std::any &dt) { auto &data = *std::any_cast(dt); - // Macro iff the optional Parameters matched: its value (the parameter - // name list) then sits at vs[2]. A plain definition has LEFTARROW's value - // there instead. - auto is_macro = vs[2].type() == typeid(std::vector); + auto is_macro = vs.choice() == 0; auto ignore = std::any_cast(vs[0]); auto name = std::any_cast(vs[1]); @@ -5511,6 +4393,9 @@ private: if (types.find(type) == types.end()) { data.instructions[name].push_back(instruction); types.insert(instruction.type); + if (type == "declare_symbol" || type == "check_symbol") { + if (!TokenChecker::is_token(*ope)) { ope = tok(ope); } + } } else { data.duplicates_of_instruction.emplace_back(type, instruction.sv.data()); @@ -5529,9 +4414,7 @@ private: rule.is_macro = is_macro; rule.params = params; - // Reserved `%`-prefixed rules (%whitespace, %word, ...) are directives, - // not parseable entry points, so they must not become the start rule. - if (data.start.empty() && name[0] != '%') { + if (data.start.empty()) { data.start = rule.name; data.start_pos = rule.s_; } @@ -5648,9 +4531,9 @@ private: auto &data = *std::any_cast(dt); switch (vs.choice()) { - case 0: { // Reference / Macro reference (left-factored) - // Macro reference iff opt(Arguments) matched and pushed the arg list. - auto is_macro = vs.size() > 2; + case 0: // Macro Reference + case 1: { // Reference + auto is_macro = vs.choice() == 0; auto ignore = std::any_cast(vs[0]); const auto &ident = std::any_cast(vs[1]); @@ -5668,16 +4551,16 @@ private: return ope; } } - case 1: { // (Expression) + case 2: { // (Expression) return std::any_cast>(vs[0]); } - case 2: { // TokenBoundary + case 3: { // TokenBoundary return tok(std::any_cast>(vs[0])); } - case 3: { // CaptureScope + case 4: { // CaptureScope return csc(std::any_cast>(vs[0])); } - case 4: { // Capture + case 5: { // Capture const auto &name = std::any_cast(vs[0]); auto ope = std::any_cast>(vs[1]); @@ -5724,36 +4607,23 @@ private: return resolve_escape_sequence(tok.data(), tok.size()); }; - // A Range produces either a single range (std::pair) or a range list - // (std::vector) for `\d`-style escapes and POSIX classes. - auto collect_ranges = [](const SemanticValues &vs) { - std::vector> ranges; - for (const auto &v : vs) { - if (v.type() == typeid(std::pair)) { - ranges.push_back(std::any_cast>(v)); - } else { - const auto &vec = - std::any_cast> &>( - v); - ranges.insert(ranges.end(), vec.begin(), vec.end()); - } - } - return ranges; + g["Class"] = [](const SemanticValues &vs) { + auto ranges = vs.transform>(); + return cls(ranges); }; - - g["Class"] = [collect_ranges](const SemanticValues &vs) { - return cls(collect_ranges(vs)); + g["ClassI"] = [](const SemanticValues &vs) { + auto ranges = vs.transform>(); + return cls(ranges, true); }; - g["ClassI"] = [collect_ranges](const SemanticValues &vs) { - return cls(collect_ranges(vs), true); + g["NegatedClass"] = [](const SemanticValues &vs) { + auto ranges = vs.transform>(); + return ncls(ranges); }; - g["NegatedClass"] = [collect_ranges](const SemanticValues &vs) { - return ncls(collect_ranges(vs)); + g["NegatedClassI"] = [](const SemanticValues &vs) { + auto ranges = vs.transform>(); + return ncls(ranges, true); }; - g["NegatedClassI"] = [collect_ranges](const SemanticValues &vs) { - return ncls(collect_ranges(vs), true); - }; - g["Range"] = [](const SemanticValues &vs) -> std::any { + g["Range"] = [](const SemanticValues &vs) { switch (vs.choice()) { case 0: { auto s1 = std::any_cast(vs[0]); @@ -5766,10 +4636,7 @@ private: } return std::pair(cp1, cp2); } - case 1: // ClassEscape - case 2: // PosixClass - return vs[0]; - case 3: { + case 1: { auto s = std::any_cast(vs[0]); auto cp = decode_codepoint(s.data(), s.length()); return std::pair(cp, cp); @@ -5777,33 +4644,6 @@ private: } return std::pair(0, 0); }; - g["ClassEscape"] = [](const SemanticValues &vs) { - auto ch = vs.sv()[1]; - const char *name = nullptr; - switch (ch) { - case 'd': - case 'D': name = "digit"; break; - case 's': - case 'S': name = "space"; break; - default: name = "word"; break; - } - auto ranges = *predefined_character_class(name); - if (ch == 'D' || ch == 'S' || ch == 'W') { - ranges = complement_character_ranges(ranges); - } - return ranges; - }; - g["PosixClass"] = [](const SemanticValues &vs) { - auto sv = vs.sv(); // `[:name:]` or `[:^name:]` - auto negated = sv[2] == '^'; - auto name = sv.substr(negated ? 3 : 2, sv.size() - (negated ? 5 : 4)); - auto ranges = predefined_character_class(name); - if (!ranges) { - auto msg = "invalid POSIX character class '" + std::string(name) + "'"; - throw SyntaxErrorException(msg.c_str(), vs.line_info()); - } - return negated ? complement_character_ranges(*ranges) : *ranges; - }; g["Char"] = [](const SemanticValues &vs) { return resolve_escape_sequence(vs.sv().data(), vs.sv().length()); }; @@ -5933,21 +4773,6 @@ private: return instruction; }; - g["NoWhitespace"] = [](const SemanticValues &vs) { - Instruction instruction; - instruction.type = "no_whitespace"; - instruction.sv = vs.sv(); - return instruction; - }; - - g["AstName"] = [](const SemanticValues &vs) { - Instruction instruction; - instruction.type = "ast_name"; - instruction.data = std::any_cast(vs[0]); - instruction.sv = vs.sv(); - return instruction; - }; - g["Instruction"] = [](const SemanticValues &vs) { return vs.transform(); }; @@ -6251,23 +5076,14 @@ private: rule.error_message = std::any_cast(instruction.data); } else if (instruction.type == "no_ast_opt") { rule.no_ast_opt = true; - } else if (instruction.type == "no_whitespace") { - rule.no_whitespace = true; - } else if (instruction.type == "ast_name") { - rule.ast_name = std::any_cast(instruction.data); } } } - // Setup First-Set and ISpan optimizations. A single visitor is shared - // across all rules so its first-set cache and visited-rule set persist: - // each rule's first-sets are computed once (O(N)) instead of re-walking - // every reachable rule once per referencing rule (O(N^2)). - { + // Setup First-Set and ISpan optimizations + for (auto &x : grammar) { SetupFirstSets vis; - for (auto &x : grammar) { - x.second.accept(vis); - } + x.second.accept(vis); } return {data.grammar, start, data.enablePackratParsing}; @@ -6301,24 +5117,21 @@ template struct AstBase : public Annotation { AstBase(const char *path, size_t line, size_t column, const char *name, const std::vector> &nodes, size_t position = 0, size_t length = 0, size_t choice_count = 0, - size_t choice = 0, bool preserve_position = false) + size_t choice = 0) : path(path ? path : ""), line(line), column(column), name(name), position(position), length(length), choice_count(choice_count), choice(choice), original_name(name), original_choice_count(choice_count), original_choice(choice), - tag(str2tag(name)), original_tag(tag), is_token(false), - preserve_position(preserve_position), nodes(nodes) {} + tag(str2tag(name)), original_tag(tag), is_token(false), nodes(nodes) {} AstBase(const char *path, size_t line, size_t column, const char *name, const std::string_view &token, size_t position = 0, size_t length = 0, - size_t choice_count = 0, size_t choice = 0, - bool preserve_position = false) + size_t choice_count = 0, size_t choice = 0) : path(path ? path : ""), line(line), column(column), name(name), position(position), length(length), choice_count(choice_count), choice(choice), original_name(name), original_choice_count(choice_count), original_choice(choice), - tag(str2tag(name)), original_tag(tag), is_token(true), - preserve_position(preserve_position), token(token) {} + tag(str2tag(name)), original_tag(tag), is_token(true), token(token) {} AstBase(const AstBase &ast, const char *original_name, size_t position = 0, size_t length = 0, size_t original_choice_count = 0, @@ -6329,8 +5142,7 @@ template struct AstBase : public Annotation { original_choice_count(original_choice_count), original_choice(original_choice), tag(ast.tag), original_tag(str2tag(original_name)), is_token(ast.is_token), - preserve_position(ast.preserve_position), token(ast.token), - nodes(ast.nodes), parent(ast.parent) {} + token(ast.token), nodes(ast.nodes), parent(ast.parent) {} const std::string path; const size_t line = 1; @@ -6348,7 +5160,6 @@ template struct AstBase : public Annotation { const unsigned int original_tag; const bool is_token; - const bool preserve_position; const std::string_view token; std::vector>> nodes; @@ -6411,10 +5222,8 @@ struct AstOptimizer { if (opt && original->nodes.size() == 1) { auto child = optimize(original->nodes[0], parent); - auto pos = - child->preserve_position ? child->position : original->position; - auto len = child->preserve_position ? child->length : original->length; - auto ast = std::make_shared(*child, original->name.data(), pos, len, + auto ast = std::make_shared(*child, original->name.data(), + original->position, original->length, original->choice_count, original->choice); for (auto &node : ast->nodes) { node->parent = ast; @@ -6444,23 +5253,18 @@ template void add_ast_action(Definition &rule) { rule.action = [&](const SemanticValues &vs) { auto line = vs.line_info(); - // `{ ast_name: X }` overrides the node's name/tag (falls back to the - // rule's own name when unset). - const char *node_name = - rule.ast_name.empty() ? rule.name.data() : rule.ast_name.data(); - if (rule.is_token()) { return std::make_shared( - vs.path, line.first, line.second, node_name, vs.token(), + vs.path, line.first, line.second, rule.name.data(), vs.token(), std::distance(vs.ss, vs.sv().data()), vs.sv().length(), - vs.choice_count(), vs.choice(), rule.no_ast_opt); + vs.choice_count(), vs.choice()); } - auto ast = std::make_shared(vs.path, line.first, line.second, node_name, - vs.transform>(), - std::distance(vs.ss, vs.sv().data()), - vs.sv().length(), vs.choice_count(), - vs.choice(), rule.no_ast_opt); + auto ast = + std::make_shared(vs.path, line.first, line.second, rule.name.data(), + vs.transform>(), + std::distance(vs.ss, vs.sv().data()), + vs.sv().length(), vs.choice_count(), vs.choice()); for (auto &node : ast->nodes) { node->parent = ast; @@ -6657,32 +5461,10 @@ public: return load_grammar(sv.data(), sv.size(), Rules(), start); } - // Serialize the loaded grammar to a portable byte blob (see GrammarBlob). - // Semantic callbacks are not included; throws if the grammar is not - // serializable (uses the `User` operator or a Capture with a match action). - std::vector serialize_grammar() const { - return GrammarBlob::serialize(*grammar_, start_); - } - - // Load a grammar from a blob produced by serialize_grammar() / GrammarBlob, - // skipping the meta-parse. Re-apply enable_ast() etc. afterwards as needed. - bool load_blob(const std::vector &blob) { - try { - grammar_ = GrammarBlob::deserialize(blob, start_); - } catch (const std::exception &) { return false; } - if (grammar_ != nullptr) { - // Symmetry with load_grammar(): restore the parser-level packrat flag - // from the blob so a later enable_packrat_parsing() re-applies it - // instead of resetting the start rule to the false member default. - enablePackratParsing_ = (*grammar_)[start_].enablePackratParsing; - } - return grammar_ != nullptr; - } - bool parse_n(const char *s, size_t n, const char *path = nullptr) const { if (grammar_ != nullptr) { const auto &rule = (*grammar_)[start_]; - auto result = rule.parse(s, n, path, log_, error_reporter_); + auto result = rule.parse(s, n, path, log_); return post_process(s, n, result); } return false; @@ -6692,7 +5474,7 @@ public: const char *path = nullptr) const { if (grammar_ != nullptr) { const auto &rule = (*grammar_)[start_]; - auto result = rule.parse(s, n, dt, path, log_, error_reporter_); + auto result = rule.parse(s, n, dt, path, log_); return post_process(s, n, result); } return false; @@ -6703,8 +5485,7 @@ public: const char *path = nullptr) const { if (grammar_ != nullptr) { const auto &rule = (*grammar_)[start_]; - auto result = - rule.parse_and_get_value(s, n, val, path, log_, error_reporter_); + auto result = rule.parse_and_get_value(s, n, val, path, log_); return post_process(s, n, result); } return false; @@ -6715,8 +5496,7 @@ public: const char *path = nullptr) const { if (grammar_ != nullptr) { const auto &rule = (*grammar_)[start_]; - auto result = - rule.parse_and_get_value(s, n, dt, val, path, log_, error_reporter_); + auto result = rule.parse_and_get_value(s, n, dt, val, path, log_); return post_process(s, n, result); } return false; @@ -6833,12 +5613,6 @@ public: void set_logger(Log log) { log_ = log; } - // Receive structured error information instead of (or in addition to) the - // formatted string passed to the logger. - void set_error_reporter(ErrorReporter reporter) { - error_reporter_ = reporter; - } - void set_logger( std::function log) { @@ -6848,20 +5622,14 @@ public: private: bool post_process(const char *s, size_t n, Definition::Result &r) const { - if ((log_ || error_reporter_) && !r.ret) { - r.error_info.output_log(log_, error_reporter_, s, n); - } + if (log_ && !r.ret) { r.error_info.output_log(log_, s, n); } return r.ret && !r.recovered; } std::vector get_no_ast_opt_rules() const { std::vector rules; for (auto &[name, rule] : *grammar_) { - // The optimizer keeps nodes by their emitted name, so honor the - // `ast_name` override when present (else the rule's own name). - if (rule.no_ast_opt) { - rules.push_back(rule.ast_name.empty() ? name : rule.ast_name); - } + if (rule.no_ast_opt) { rules.push_back(name); } } return rules; } @@ -6871,7 +5639,6 @@ private: bool enableLeftRecursion_ = true; bool enablePackratParsing_ = false; Log log_; - ErrorReporter error_reporter_; }; /*----------------------------------------------------------------------------- diff --git a/libcockatrice_utility/libcockatrice/utility/playmat_params.h b/libcockatrice_utility/libcockatrice/utility/playmat_params.h deleted file mode 100644 index c64a6f2ae..000000000 --- a/libcockatrice_utility/libcockatrice/utility/playmat_params.h +++ /dev/null @@ -1,52 +0,0 @@ -#ifndef COCKATRICE_PLAYMAT_PARAMS_H -#define COCKATRICE_PLAYMAT_PARAMS_H - -#include "card_ref.h" - -#include - -/** - * @struct PlaymatParams - * @ingroup Decks - * @brief Positioning parameters for a playmat card image. - * - * Controls how the cropped card art is positioned within the - * combined table+stack play area. The coordinate system is - * relative to the cropped art source image. - */ -struct PlaymatParams -{ - double marginPctL = 0.07; ///< Left margin as fraction of card width (0.0–0.95). - double marginPctR = 0.07; ///< Right margin as fraction of card width (0.0–0.95). - double verticalOffset = 0.33; ///< Vertical position within card (0.0=top, 1.0=bottom). - double zoom = 1.0; ///< Scale factor (0.1–4.0). - - bool operator==(const PlaymatParams &other) const - { - return qFuzzyCompare(marginPctL, other.marginPctL) && qFuzzyCompare(marginPctR, other.marginPctR) && - qFuzzyCompare(verticalOffset, other.verticalOffset) && qFuzzyCompare(zoom, other.zoom); - } - - bool operator!=(const PlaymatParams &other) const - { - return !(*this == other); - } -}; - -/** - * @struct PlaymatInfo - * @ingroup Decks - * @brief A resolved playmat (card + positioning parameters). - */ -struct PlaymatInfo -{ - CardRef card; ///< The card whose art is used as playmat. - PlaymatParams params; ///< Positioning parameters for the playmat card image. - - bool operator==(const PlaymatInfo &other) const - { - return card == other.card && params == other.params; - } -}; - -#endif // COCKATRICE_PLAYMAT_PARAMS_H diff --git a/libcockatrice_utility/libcockatrice/utility/string_limits.h b/libcockatrice_utility/libcockatrice/utility/string_limits.h index 5079ec46d..cca804bf0 100644 --- a/libcockatrice_utility/libcockatrice/utility/string_limits.h +++ b/libcockatrice_utility/libcockatrice/utility/string_limits.h @@ -22,19 +22,6 @@ inline QString textFromStdString(const std::string &_string) { return QString::fromUtf8(_string.data(), std::min(int(_string.size()), MAX_TEXT_LENGTH)); } -/** @brief Returns a QString from a std::string, truncated to at most MAX_TEXT_LENGTH bytes, keeping the tail. */ -inline QString textTailFromStdString(const std::string &_string) -{ - if (int(_string.size()) <= MAX_TEXT_LENGTH) { - return QString::fromUtf8(_string.data(), int(_string.size())); - } - - int start = int(_string.size()) - MAX_TEXT_LENGTH; - while (start < int(_string.size()) && (static_cast(_string[start]) & 0xC0) == 0x80) { - ++start; - } - return QString::fromUtf8(_string.data() + start, int(_string.size()) - start); -} /** @brief Returns a QString from a std::string, truncated to at most MAX_FILE_LENGTH bytes. */ inline QString fileFromStdString(const std::string &_string) { diff --git a/libcockatrice_utility/libcockatrice/utility/warning_categories.cpp b/libcockatrice_utility/libcockatrice/utility/warning_categories.cpp deleted file mode 100644 index b5ca563ff..000000000 --- a/libcockatrice_utility/libcockatrice/utility/warning_categories.cpp +++ /dev/null @@ -1,24 +0,0 @@ -#include "warning_categories.h" - -QList parseWarningCategories(const QString &value) -{ - QList categories; - const QStringList entries = value.split(',', Qt::SkipEmptyParts); - for (const QString &entry : entries) { - const QStringList parts = entry.split('|'); - WarningCategory category; - category.name = parts.first().trimmed(); - if (category.name.isEmpty()) { - continue; - } - if (parts.size() > 1) { - bool ok = false; - const int il = parts.at(1).trimmed().toInt(&ok); - if (ok && il > 0) { - category.startingIl = il; - } - } - categories.append(category); - } - return categories; -} diff --git a/libcockatrice_utility/libcockatrice/utility/warning_categories.h b/libcockatrice_utility/libcockatrice/utility/warning_categories.h deleted file mode 100644 index ce5308cf4..000000000 --- a/libcockatrice_utility/libcockatrice/utility/warning_categories.h +++ /dev/null @@ -1,28 +0,0 @@ -#ifndef WARNING_CATEGORIES_H -#define WARNING_CATEGORIES_H - -#include -#include - -/** - * A warning category the server offers to moderators, optionally carrying a - * recommended starting intervention level (see the moderator guide). - */ -struct WarningCategory -{ - QString name; - int startingIl = 1; -}; - -/** - * Parses the `server/officialwarnings` setting value into warning categories. - * - * Entries are separated by commas. Each entry is a category name, optionally - * followed by "|" and the recommended starting intervention level: - * "Abusive Language|1,Cheating|2,Spamming" - * Entries without an explicit level default to intervention level 1. - * Empty entries are skipped. - */ -QList parseWarningCategories(const QString &value); - -#endif // WARNING_CATEGORIES_H diff --git a/oracle/CMakeLists.txt b/oracle/CMakeLists.txt index a942870b7..0736db7f5 100644 --- a/oracle/CMakeLists.txt +++ b/oracle/CMakeLists.txt @@ -23,12 +23,11 @@ set(oracle_SOURCES src/pages.cpp src/pagetemplates.cpp src/parsehelpers.cpp - src/raw_json_scanner.cpp + src/qt-json/json.cpp ../cockatrice/src/client/settings/cache_settings.cpp ../cockatrice/src/client/settings/card_counter_settings.cpp ../cockatrice/src/client/settings/shortcuts_settings.cpp ../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_manager.cpp ../cockatrice/src/interface/widgets/quick_settings/settings_button_widget.cpp @@ -114,8 +113,6 @@ qt6_add_executable( MANUAL_FINALIZATION ) -target_precompile_headers(oracle PRIVATE "${CMAKE_SOURCE_DIR}/cmake/pch/qtwidgets_pch.h") - # ------------------------ # Link libraries # ------------------------ @@ -213,19 +210,10 @@ if(WIN32) list(APPEND libSearchDirs ${QT_LIBRARY_DIR}) install( - DIRECTORY "$/" + DIRECTORY "${CMAKE_BINARY_DIR}/${PROJECT_NAME}/${CMAKE_BUILD_TYPE}/" DESTINATION ./ FILES_MATCHING PATTERN "*.dll" - PATTERN "*.pdb" EXCLUDE - PATTERN "*.dir*" EXCLUDE - PATTERN "*_autogen*" EXCLUDE - PATTERN "*.tlog*" EXCLUDE - PATTERN "CMakeFiles*" EXCLUDE - PATTERN "x64*" EXCLUDE - PATTERN ".qt*" EXCLUDE - PATTERN ".qsb*" EXCLUDE - PATTERN ".lupdate*" EXCLUDE ) # Qt plugins: iconengines, platforms, styles, tls (Qt6) diff --git a/oracle/src/main.cpp b/oracle/src/main.cpp index 0f962f9be..bb88153b1 100644 --- a/oracle/src/main.cpp +++ b/oracle/src/main.cpp @@ -50,8 +50,8 @@ int main(int argc, char *argv[]) QApplication app(argc, argv); QCoreApplication::setOrganizationName("Cockatrice"); - QCoreApplication::setOrganizationDomain("Cockatrice"); - // This can't be changed, as it influences the default save path for cards.xml + QCoreApplication::setOrganizationDomain("cockatrice"); + // this can't be changed, as it influences the default save path for cards.xml QCoreApplication::setApplicationName("Cockatrice"); // If the program is opened with the -s flag, it will only do spoilers. Otherwise it will do MTGJSON/Tokens @@ -83,7 +83,7 @@ int main(int argc, char *argv[]) QIcon icon("theme:appicon.svg"); wizard.setWindowIcon(icon); // set name of the app desktop file; used by wayland to load the window icon - QGuiApplication::setDesktopFileName("Oracle"); + QGuiApplication::setDesktopFileName("oracle"); wizard.show(); diff --git a/oracle/src/oracleimporter.cpp b/oracle/src/oracleimporter.cpp index 88b522197..a9008c4da 100644 --- a/oracle/src/oracleimporter.cpp +++ b/oracle/src/oracleimporter.cpp @@ -3,16 +3,12 @@ #include "libcockatrice/interfaces/noop_card_preference_provider.h" #include "libcockatrice/interfaces/noop_card_set_priority_controller.h" #include "parsehelpers.h" +#include "qt-json/json.h" #include -#include -#include -#include #include -#include #include #include -#include #include #include @@ -23,9 +19,8 @@ static const QList kSingletonCounts = {{1, "legal"}, {0, "banned"} SplitCardPart::SplitCardPart(const QString &_name, const QString &_text, const QHash &_properties, - const PrintingInfo &_printingInfo, - const QString &_localizedText) - : name(_name), text(_text), localizedText(_localizedText), properties(_properties), printingInfo(_printingInfo) + const PrintingInfo &_printingInfo) + : name(_name), text(_text), properties(_properties), printingInfo(_printingInfo) { } @@ -35,42 +30,6 @@ OracleImporter::OracleImporter(QObject *parent) : QObject(parent) { } -void OracleImporter::setCardLang(const QString &lang) -{ - cardLang = lang.trimmed().toLower(); - localizationEnabled = cardLang != "en" && CardLocalization::supportedLanguages().contains(cardLang); -} - -/** - * @brief Maps the MTGJSON foreignData language names to the short codes used by - * Scryfall and stored in cards.xml (e.g. "German" -> "de"). - * @param language The language name found in the MTGJSON foreignData entries. - * @return The short language code, or an empty string if unknown. - */ -static QString mtgjsonLanguageToCode(const QString &language) -{ - static const QHash map = { - {"Chinese Simplified", "zhs"}, - {"Chinese Traditional", "zht"}, - {"English", "en"}, - {"French", "fr"}, - {"German", "de"}, - {"Greek", "grc"}, - {"Ancient Greek", "grc"}, - {"Hebrew", "he"}, - {"Italian", "it"}, - {"Japanese", "ja"}, - {"Korean", "ko"}, - {"Latin", "la"}, - {"Phyrexian", "ph"}, - {"Portuguese (Brazil)", "pt"}, - {"Russian", "ru"}, - {"Sanskrit", "sa"}, - {"Spanish", "es"}, - }; - return map.value(language); -} - static CardSet::Priority getSetPriority(const QString &setType, const QString &shortName) { if (!setTypePriorities.contains(setType.toLower())) { @@ -83,37 +42,31 @@ static CardSet::Priority getSetPriority(const QString &setType, const QString &s return priority; } -bool OracleImporter::readSetsFromByteArray(QByteArray data) +bool OracleImporter::readSetsFromByteArray(const QByteArray &data) { - const RawJson::ScanProgressCallback progress = - progressReporting - ? [this]( - qsizetype bytesRead, - qsizetype - totalBytes) { emit dataReadProgress(static_cast(bytesRead), static_cast(totalBytes)); } - : RawJson::ScanProgressCallback{}; - - RawJson::ScanError error; - const QList ranges = RawJson::scanSetRanges(data, &error, progress); - if (error.isError()) { - qDebug() << "error: RawJson::scanSetRanges():" << error.message; + bool ok; + auto setsMap = QtJson::Json::parse(QString(data), ok).toMap().value("data").toMap(); + if (!ok) { + qDebug() << "error: QtJson::Json::parse()"; return false; } QList newSetList; - newSetList.reserve(ranges.size()); - for (const RawJson::SetRange &range : ranges) { - QString shortName = range.code.toUpper(); - QString longName = range.name; - QString setType = range.type; - QDate releaseDate = QDate::fromString(range.releaseDate, Qt::ISODate); + QListIterator it(setsMap.values()); + + while (it.hasNext()) { + QVariantMap map = it.next().toMap(); + QString shortName = map.value("code").toString().toUpper(); + QString longName = map.value("name").toString(); + QList setCards = map.value("cards").toList(); + QString setType = map.value("type").toString(); + QDate releaseDate = map.value("releaseDate").toDate(); CardSet::Priority priority = getSetPriority(setType, shortName); // capitalize set type if (setType.length() > 0) { // basic grammar for words that aren't capitalized, like in "From the Vault" - static const QStringList noCapitalize = {"the", "a", "an", "on", "to", "for", - "of", "in", "and", "with", "or"}; + const QStringList noCapitalize = {"the", "a", "an", "on", "to", "for", "of", "in", "and", "with", "or"}; QStringList words = setType.split("_"); setType.clear(); bool first = false; @@ -121,15 +74,13 @@ bool OracleImporter::readSetsFromByteArray(QByteArray data) if (first && noCapitalize.contains(item)) { setType += item + QString(" "); } else { - setType += item[0].toUpper() + item.mid(1) + QString(" "); + setType += item[0].toUpper() + item.mid(1, -1) + QString(" "); first = true; } } setType = setType.trimmed(); } - SetToDownload set(shortName, longName, priority, setType, releaseDate); - set.setRawRange(range.dataRange); - newSetList.append(set); + newSetList.append(SetToDownload(shortName, longName, setCards, priority, setType, releaseDate)); } std::sort(newSetList.begin(), newSetList.end()); @@ -138,34 +89,19 @@ bool OracleImporter::readSetsFromByteArray(QByteArray data) return false; } allSets = newSetList; - rawSetsData = std::move(data); return true; } -/** - * The priority order used to pick a card's main type when a card has multiple - * types (e.g. "Artifact Creature") or multiple faces (e.g. split/adventure cards). - * A lower index means a higher priority. - */ -static const QStringList MAIN_CARD_TYPE_PRIORITY = {"Planeswalker", "Creature", "Land", "Sorcery", - "Instant", "Artifact", "Enchantment"}; - -/** - * Returns the priority (index) of the given main card type. Known types map to their - * position in {@link mainCardTypePriority()}, unknown types map to -1 (lowest priority). - */ -static int mainCardTypePriority(const QString &mainCardType) -{ - return MAIN_CARD_TYPE_PRIORITY.indexOf(mainCardType); -} - static QString getMainCardType(const QStringList &typeList) { if (typeList.isEmpty()) { return {}; } - for (const auto &type : MAIN_CARD_TYPE_PRIORITY) { + static const QStringList typePriority = {"Planeswalker", "Creature", "Land", "Sorcery", + "Instant", "Artifact", "Enchantment"}; + + for (const auto &type : typePriority) { if (typeList.contains(type)) { return type; } @@ -186,8 +122,14 @@ static void sortAndReduceColors(QString &colors) std::sort(colors.begin(), colors.end(), [](const QChar a, const QChar b) { return colorOrder.value(a, INT_MAX) < colorOrder.value(b, INT_MAX); }); // reduce - auto last = std::unique(colors.begin(), colors.end()); - colors.erase(last, colors.end()); + QChar lastChar = '\0'; + for (int i = 0; i < colors.size(); ++i) { + if (colors.at(i) == lastChar) { + colors.remove(i, 1); + } else { + lastChar = colors.at(i); + } + } } CardInfoPtr OracleImporter::addCard(QString name, @@ -200,12 +142,9 @@ CardInfoPtr OracleImporter::addCard(QString name, // Workaround for card name weirdness name = name.replace("Æ", "AE"); name = name.replace("’", "'"); - auto existingIt = cards.constFind(name); - if (existingIt != cards.constEnd()) { - CardInfoPtr card = existingIt.value(); + if (cards.contains(name)) { + CardInfoPtr card = cards.value(name); card->addToSet(printingInfo.getSet(), printingInfo); - // Only merge legalities when the card has none yet, so multi-format - // printings don't overwrite each other's legality lists. if (card->getProperties().filter(formatRegex).empty()) { card->combineLegalities(properties); } @@ -243,9 +182,8 @@ CardInfoPtr OracleImporter::addCard(QString name, // DETECT CARD POSITIONING INFO - QString layoutVal = properties.value("layout"); - bool landscapeOrientation = - properties.value("maintype") == "Battle" || layoutVal == "split" || layoutVal == "planar"; + bool landscapeOrientation = properties.value("maintype") == "Battle" || properties.value("layout") == "split" || + properties.value("layout") == "planar"; // cards that enter the field tapped bool cipt = parseCipt(name, text) || landscapeOrientation; @@ -284,110 +222,12 @@ CardInfoPtr OracleImporter::addCard(QString name, return newCard; } -static QString getJsonString(const QJsonObject &obj, const QString &key) +static QString getStringPropertyFromMap(const QVariantMap &card, const QString &propertyName) { - // QVariant coerces numbers and booleans to text, while QJsonValue::toString() - // returns a null string for them — some MTGJSON fields (manaValue, - // convertedManaCost, isOnlineOnly, isRebalanced) carry those types. - return obj.value(key).toVariant().toString(); + return card.contains(propertyName) ? card.value(propertyName).toString() : QString(""); } -static QString normalizeCardName(QString name) -{ - // Mirror of the name cleanup applied in addCard(), so collected localization - // keys line up with the card map keys (Æ → AE, curly apostrophe → straight). - name = name.replace("Æ", "AE"); - name = name.replace("’", "'"); - return name; -} - -static QString matchingForeignEntryText(const QJsonObject &card, const QString &cardLang) -{ - // Multi-face cards (split/aftermath/adventure/prepare) expose each face as a - // separate card object, each with its own foreignData entry carrying that - // face's rules text; single-face cards carry the full text in one entry. - const QJsonArray foreignData = card.value("foreignData").toArray(); - for (const QJsonValue &entryValue : foreignData) { - const QJsonObject entry = entryValue.toObject(); - // MTGJSON reports languages by long-form name ("German"); match the - // short code ("de") that Scryfall and cards.xml use. - if (mtgjsonLanguageToCode(getJsonString(entry, "language")) == cardLang) { - return getJsonString(entry, "text"); - } - } - return QString(); -} - -void OracleImporter::collectForeignData(const QString &cardKey, - const CardSetPtr ¤tSet, - const QJsonObject &card, - bool collectText) -{ - if (!localizationEnabled) { - return; - } - - LocalizedCardEntry incoming; - bool found = false; - const QJsonArray foreignData = card.value("foreignData").toArray(); - for (const QJsonValue &entryValue : foreignData) { - const QJsonObject entry = entryValue.toObject(); - // MTGJSON reports languages by long-form name ("German"); match the - // short code ("de") that Scryfall and cards.xml use. - if (mtgjsonLanguageToCode(getJsonString(entry, "language")) != cardLang) { - continue; - } - incoming.name = getJsonString(entry, "name"); - incoming.text = collectText ? getJsonString(entry, "text") : QString(); - found = true; - break; - } - if (!found) { - return; - } - - // Prefer the entry from the highest-priority set (lower enum value = more - // authoritative); printings of equal priority keep the first one seen. - incoming.priority = currentSet->getPriority(); - const auto existing = localizedEntries.constFind(cardKey); - if (existing == localizedEntries.constEnd() || incoming.priority < existing->priority) { - localizedEntries.insert(cardKey, incoming); - } -} - -void OracleImporter::applyLocalizedData() -{ - if (!localizationEnabled) { - return; - } - for (auto it = localizedEntries.constBegin(); it != localizedEntries.constEnd(); ++it) { - CardInfoPtr card = cards.value(it.key()); - if (card.isNull()) { - continue; - } - const LocalizedCardEntry &entry = it.value(); - if (!entry.name.isEmpty()) { - card->setLocalizedName(cardLang, entry.name); - } - if (!entry.text.isEmpty()) { - card->setLocalizedText(cardLang, entry.text); - } - } - localizedEntries.clear(); - - for (auto it = splitLocalizedTexts.constBegin(); it != splitLocalizedTexts.constEnd(); ++it) { - CardInfoPtr card = cards.value(it.key()); - if (card.isNull()) { - continue; - } - if (!it.value().text.isEmpty()) { - card->setLocalizedText(cardLang, it.value().text); - } - } - splitLocalizedTexts.clear(); -} - -int OracleImporter::importCardsFromSet(const CardSetPtr ¤tSet, const QJsonArray &cardsList) +int OracleImporter::importCardsFromSet(const CardSetPtr ¤tSet, const QList &cardsList) { // mtgjson name => xml name static const QMap cardProperties{ @@ -397,18 +237,15 @@ int OracleImporter::importCardsFromSet(const CardSetPtr ¤tSet, const QJson }; // mtgjson name => xml name - static const QMap setInfoProperties{{"number", "num"}, - {"rarity", "rarity"}, - {"isOnlineOnly", "isOnlineOnly"}, - {"isRebalanced", "isRebalanced"}, - {"artist", "artist"}}; + static const QMap setInfoProperties{ + {"number", "num"}, {"rarity", "rarity"}, {"isOnlineOnly", "isOnlineOnly"}, {"isRebalanced", "isRebalanced"}}; // mtgjson name => xml name static const QMap identifierProperties{{"multiverseId", "muid"}, {"scryfallId", "uuid"}}; static const QString ptSeparator = "/"; static constexpr bool isToken = false; - static const QSet setsWithCardsWithSameNameButDifferentText = {"UST"}; + static const QList setsWithCardsWithSameNameButDifferentText = {"UST"}; int numCards = 0; @@ -416,16 +253,16 @@ int OracleImporter::importCardsFromSet(const CardSetPtr ¤tSet, const QJson QMap, QString>> splitCards; // Keeps track of all names encountered so far - QSet allNameProps; + QList allNameProps; - for (const QJsonValue &cardVal : cardsList) { - QJsonObject card = cardVal.toObject(); + for (const QVariant &cardVar : cardsList) { + QVariantMap card = cardVar.toMap(); /* Currently used layouts are: * augment, double_faced_token, flip, host, leveler, meld, normal, planar, * saga, scheme, split, token, transform, vanguard */ - QString layout = getJsonString(card, "layout"); + QString layout = getStringPropertyFromMap(card, "layout"); // don't import tokens from the json file if (layout == "token") { @@ -433,9 +270,9 @@ int OracleImporter::importCardsFromSet(const CardSetPtr ¤tSet, const QJson } // normal cards handling - QString name = getJsonString(card, "name"); - QString text = getJsonString(card, "text"); - QString faceName = getJsonString(card, "faceName"); + QString name = getStringPropertyFromMap(card, "name"); + QString text = getStringPropertyFromMap(card, "text"); + QString faceName = getStringPropertyFromMap(card, "faceName"); if (faceName.isEmpty()) { faceName = name; } @@ -443,38 +280,43 @@ int OracleImporter::importCardsFromSet(const CardSetPtr ¤tSet, const QJson // card properties QHash properties; for (auto i = cardProperties.cbegin(), end = cardProperties.cend(); i != end; ++i) { - QString propertyValue = getJsonString(card, i.key()); + QString mtgjsonProperty = i.key(); + QString xmlPropertyName = i.value(); + QString propertyValue = getStringPropertyFromMap(card, mtgjsonProperty); if (!propertyValue.isEmpty()) { - properties.insert(i.value(), propertyValue); + properties.insert(xmlPropertyName, propertyValue); } } // per-set properties QHash printingProps; for (auto i = setInfoProperties.cbegin(), end = setInfoProperties.cend(); i != end; ++i) { - QString propertyValue = getJsonString(card, i.key()); + QString mtgjsonProperty = i.key(); + QString xmlPropertyName = i.value(); + QString propertyValue = getStringPropertyFromMap(card, mtgjsonProperty); if (!propertyValue.isEmpty()) { - printingProps.insert(i.value(), propertyValue); + printingProps.insert(xmlPropertyName, propertyValue); } } // handle flavorNames specially due to double-faced cards - QString faceFlavorName = getJsonString(card, "faceFlavorName"); - QString flavorName = !faceFlavorName.isEmpty() ? faceFlavorName : getJsonString(card, "flavorName"); + QString faceFlavorName = getStringPropertyFromMap(card, "faceFlavorName"); + QString flavorName = !faceFlavorName.isEmpty() ? faceFlavorName : getStringPropertyFromMap(card, "flavorName"); if (!flavorName.isEmpty()) { printingProps.insert("flavorName", flavorName); } // Identifiers - QJsonObject identifiers = card.value("identifiers").toObject(); for (auto i = identifierProperties.cbegin(), end = identifierProperties.cend(); i != end; ++i) { - QString propertyValue = getJsonString(identifiers, i.key()); + QString mtgjsonProperty = i.key(); + QString xmlPropertyName = i.value(); + QString propertyValue = getStringPropertyFromMap(card.value("identifiers").toMap(), mtgjsonProperty); if (!propertyValue.isEmpty()) { - printingProps.insert(i.value(), propertyValue); + printingProps.insert(xmlPropertyName, propertyValue); } } - PrintingInfo printingInfo(currentSet, LazyPropertiesHash(printingProps)); + PrintingInfo printingInfo(currentSet, printingProps); QString numComponent; const QString numProperty = printingInfo.getProperty("num"); @@ -486,26 +328,21 @@ int OracleImporter::importCardsFromSet(const CardSetPtr ¤tSet, const QJson allNameProps.contains(faceName) && layout == "normal" && lastChar.isLetter()) { numComponent = " (" + QString(lastChar).toLower() + ")"; } - allNameProps.insert(faceName); + allNameProps.append(faceName); // special handling properties - QString colors; - for (const QJsonValue &color : card.value("colors").toArray()) { - colors += color.toString(); - } + QString colors = card.value("colors").toStringList().join(""); if (!colors.isEmpty()) { properties.insert("colors", colors); } - QString colorIdentity; - for (const QJsonValue &color : card.value("colorIdentity").toArray()) { - colorIdentity += color.toString(); - } + // special handling properties + QString colorIdentity = card.value("colorIdentity").toStringList().join(""); if (!colorIdentity.isEmpty()) { properties.insert("coloridentity", colorIdentity); } - const auto &mainCardType = getMainCardType(card.value("types").toVariant().toStringList()); + const auto &mainCardType = getMainCardType(card.value("types").toStringList()); if (mainCardType.isEmpty()) { qDebug() << "warning: no mainCardType for card:" << name; } else { @@ -514,47 +351,39 @@ int OracleImporter::importCardsFromSet(const CardSetPtr ¤tSet, const QJson // Depending on whether power and/or toughness are present, the format // is either P/T (most common), P (no toughness), or /T (no power). - QString power = getJsonString(card, "power"); - QString toughness = getJsonString(card, "toughness"); + QString power = getStringPropertyFromMap(card, "power"); + QString toughness = getStringPropertyFromMap(card, "toughness"); if (toughness.isEmpty() && !power.isEmpty()) { properties.insert("pt", power); } else if (!toughness.isEmpty()) { properties.insert("pt", power + ptSeparator + toughness); } - auto legalities = card.value("legalities").toObject(); - for (auto i = legalities.constBegin(), end = legalities.constEnd(); i != end; ++i) { + auto legalities = card.value("legalities").toMap(); + for (auto i = legalities.cbegin(), end = legalities.cend(); i != end; ++i) { properties.insert(QString("format-%1").arg(i.key()), i.value().toString().toLower()); } // split cards are considered a single card, enqueue for later merging if (layout == "split" || layout == "aftermath" || layout == "adventure" || layout == "prepare") { - auto _faceName = getJsonString(card, "faceName"); - // MTGJSON exposes each face as a separate card object, each with its - // own foreignData entry holding that face's rules text; collect it so - // the per-face texts can be joined the same way as the English text. - const QString faceLocalizedText = - localizationEnabled ? matchingForeignEntryText(card, cardLang) : QString(); - SplitCardPart split(_faceName, text, properties, printingInfo, faceLocalizedText); + auto _faceName = getStringPropertyFromMap(card, "faceName"); + SplitCardPart split(_faceName, text, properties, printingInfo); auto found_iter = splitCards.find(name + numProperty); if (found_iter == splitCards.end()) { splitCards.insert(name + numProperty, {{split}, name}); } else { found_iter->first.append(split); } - // MTGJSON's foreignData name is the joined name present on every - // face, so collect the name once. - collectForeignData(normalizeCardName(name), currentSet, card, false); } else { // relations QList relatedCards; // add other face for split cards as card relation - if (!getJsonString(card, "side").isEmpty()) { - auto faceManaValue = getJsonString(card, "faceManaValue"); + if (!getStringPropertyFromMap(card, "side").isEmpty()) { + auto faceManaValue = getStringPropertyFromMap(card, "faceManaValue"); if (faceManaValue.isEmpty()) { // check the old name for the property, for backwards compatibility purposes - faceManaValue = getJsonString(card, "faceConvertedManaCost"); + faceManaValue = getStringPropertyFromMap(card, "faceConvertedManaCost"); } properties["cmc"] = faceManaValue; @@ -574,23 +403,20 @@ int OracleImporter::importCardsFromSet(const CardSetPtr ¤tSet, const QJson name = faceName; } - // mtgjson related cards - QJsonObject givenRelated = card.value("relatedCards").toObject(); - if (!givenRelated.isEmpty()) { + // mtgjon related cards + if (card.contains("relatedCards")) { + QVariantMap givenRelated = card.value("relatedCards").toMap(); // conjured cards from a spellbook - QJsonArray spellbook = givenRelated.value("spellbook").toArray(); - if (!spellbook.isEmpty()) { - for (const QJsonValue &spbkVal : spellbook) { - relatedCards.append(new CardRelation(spbkVal.toString(), CardRelationType::DoesNotAttach, false, - false, 1, true)); + if (givenRelated.contains("spellbook")) { + auto spbk = givenRelated.value("spellbook").toStringList(); + for (const QString &spbkName : spbk) { + relatedCards.append( + new CardRelation(spbkName, CardRelationType::DoesNotAttach, false, false, 1, true)); } } } - collectForeignData(normalizeCardName(name + numComponent), currentSet, card); - - CardInfoPtr newCard = - addCard(name + numComponent, text, isToken, std::move(properties), relatedCards, printingInfo); + CardInfoPtr newCard = addCard(name + numComponent, text, isToken, properties, relatedCards, printingInfo); numCards++; } } @@ -598,12 +424,11 @@ int OracleImporter::importCardsFromSet(const CardSetPtr ¤tSet, const QJson // split cards handling static const QString splitCardPropSeparator = QString(" // "); static const QString splitCardTextSeparator = QString("\n\n---\n\n"); + static const QList noRelatedCards = {}; QList, QString>> partsAndNames = splitCards.values(); for (auto [splitCardParts, name] : partsAndNames) { QString text; - QString localizedText; - bool localizedTextComplete = true; QHash properties; PrintingInfo printingInfo; @@ -613,21 +438,6 @@ int OracleImporter::importCardsFromSet(const CardSetPtr ¤tSet, const QJson } text.append(tmp.getText()); - // Build the cardLang text by joining each face's translated text with - // the same separator as the English text. Any face missing a complete - // translation abandons the whole join, falling back to the English text. - if (localizedTextComplete) { - const QString partLocalizedText = tmp.getLocalizedText(); - if (partLocalizedText.isEmpty()) { - localizedTextComplete = false; - } else { - if (!localizedText.isEmpty()) { - localizedText.append(splitCardTextSeparator); - } - localizedText.append(partLocalizedText); - } - } - if (properties.isEmpty()) { properties = tmp.getProperties(); printingInfo = tmp.getPrintingInfo(); @@ -640,17 +450,10 @@ int OracleImporter::importCardsFromSet(const CardSetPtr ¤tSet, const QJson if (!thisCardPropertyValue.isEmpty() && originalPropertyValue != thisCardPropertyValue) { if (originalPropertyValue.isEmpty()) { // don't create //es if one field is empty properties.insert(prop, thisCardPropertyValue); - } else if (prop == "colors" || prop == "coloridentity") { // the card is both colors + } else if (prop == "colors") { // the card is both colors properties.insert(prop, originalPropertyValue + thisCardPropertyValue); - } else if (prop == "maintype") { - // Use the same priority as getMainCardType() to pick the - // "best" type across faces — e.g. Creature over Instant - // for adventure cards like Bonecrusher Giant. - int currentPriority = mainCardTypePriority(originalPropertyValue); - int newPriority = mainCardTypePriority(thisCardPropertyValue); - if (newPriority >= 0 && (currentPriority < 0 || newPriority < currentPriority)) { - properties.insert(prop, thisCardPropertyValue); - } + } else if (prop == "maintype") { // don't create maintypes with //es in them + continue; } else { properties.insert(prop, originalPropertyValue + splitCardPropSeparator + thisCardPropertyValue); @@ -659,32 +462,20 @@ int OracleImporter::importCardsFromSet(const CardSetPtr ¤tSet, const QJson } } } - CardInfoPtr newCard = addCard(name, text, isToken, std::move(properties), {}, printingInfo); - if (localizationEnabled && localizedTextComplete && !localizedText.isEmpty()) { - // Same priority policy as collectForeignData(): the joined text from the - // highest-priority set seen so far wins, applied once all printings are in. - LocalizedCardEntry entry; - entry.text = localizedText; - entry.priority = currentSet->getPriority(); - const QString entryKey = normalizeCardName(name); - const auto existing = splitLocalizedTexts.constFind(entryKey); - if (existing == splitLocalizedTexts.constEnd() || entry.priority < existing->priority) { - splitLocalizedTexts.insert(entryKey, entry); - } - } + CardInfoPtr newCard = addCard(name, text, isToken, properties, noRelatedCards, printingInfo); numCards++; } return numCards; } -static FormatRulesNameMap buildDefaultMagicFormats() +FormatRulesNameMap OracleImporter::createDefaultMagicFormats() { // Predefined common exceptions CardCondition superTypeIsBasic; superTypeIsBasic.field = "type"; superTypeIsBasic.matchType = "regex"; - superTypeIsBasic.value = R"(\bBasic\b[^—]+\bLand\b)"; + superTypeIsBasic.value = "\bBasic\b[^—]+\bLand\b"; ExceptionRule basicLands; basicLands.conditions.append(superTypeIsBasic); @@ -697,6 +488,7 @@ static FormatRulesNameMap buildDefaultMagicFormats() ExceptionRule mayContainAnyNumber; mayContainAnyNumber.conditions.append(anyNumberAllowed); + // Map to store default rules FormatRulesNameMap defaultFormatRulesNameMap; // ----------------- Helper lambda to create format ----------------- @@ -742,29 +534,10 @@ static FormatRulesNameMap buildDefaultMagicFormats() return defaultFormatRulesNameMap; } -const FormatRulesNameMap &OracleImporter::createDefaultMagicFormats() -{ - static const FormatRulesNameMap cached = buildDefaultMagicFormats(); - return cached; -} - int OracleImporter::startImport() { static ICardSetPriorityController *noOpController = new NoopCardSetPriorityController(); - importCancelled.storeRelease(0); - - // Pre-allocate the cards hash to avoid rehashing during import. Keys are - // distinct card names while raw ranges only count printings (AllPrintings - // ~100k printings vs ~35k names), so this over-reserves somewhat; an exact - // distinct-name count would require eagerly parsing, which the lazy reader - // deliberately avoids. It's a capacity hint, so the overshoot is harmless. - int estimatedCards = 0; - for (const SetToDownload &curSetToParse : allSets) { - estimatedCards += curSetToParse.getRawRange().cardCount; - } - cards.reserve(estimatedCards); - // add an empty set for tokens CardSetPtr tokenSet = CardSet::newInstance(noOpController, CardSet::TOKENS_SETNAME, tr("Dummy set containing tokens"), "Tokens"); @@ -773,61 +546,20 @@ int OracleImporter::startImport() int setIndex = 0; for (const SetToDownload &curSetToParse : allSets) { - if (importCancelled.loadAcquire()) { - // The wizard was closed mid-import: stop at the next set boundary so - // the caller can wait for this future without processing every set. - break; - } - CardSetPtr newSet = CardSet::newInstance(noOpController, curSetToParse.getShortName(), curSetToParse.getLongName(), curSetToParse.getSetType(), curSetToParse.getReleaseDate(), curSetToParse.getPriority()); - - // parse only this set's slice of the raw document so the whole JSON tree is - // never kept in memory at once - const RawJson::SetDataRange &rawRange = curSetToParse.getRawRange(); - const qsizetype rangeEnd = rawRange.start + rawRange.length; - if (rawRange.start < 0 || rawRange.length <= 0 || rangeEnd > rawSetsData.size()) { - // rawSetsData is cleared by releaseSetData() while SetToDownload copies - // taken from getSets() keep their ranges, and nothing else enforces the - // pairing — so never index past the buffer on stale/mismatched ranges. - qWarning() << "error: out-of-bounds raw range for set" << curSetToParse.getShortName() << "skipping"; - ++setIndex; - emit setIndexChanged(0, setIndex, curSetToParse.getLongName()); - continue; - } - // sliced() shares the buffer instead of deep-copying the slice; the largest - // sets in AllPrintings are tens of MB, so the copy is worth avoiding here. - const QByteArray setBytes = rawSetsData.sliced(rawRange.start, rawRange.length); - QJsonParseError parseError; - const QJsonDocument setDoc = QJsonDocument::fromJson(setBytes, &parseError); - if (parseError.error != QJsonParseError::NoError) { - qWarning() << "error: parsing card data for set" << curSetToParse.getShortName() << ":" - << parseError.errorString(); - ++setIndex; - // Keep the progress accounting honest: a set that failed to parse - // still advanced the index, so report it (with zero imported cards) - // rather than letting SaveSetsPage's bar stall per failed set. - emit setIndexChanged(0, setIndex, curSetToParse.getLongName()); - continue; - } - - // Only add the set to the database once its slice parsed cleanly; - // a set that fails here must not persist as an empty set in cards.xml. if (!sets.contains(newSet->getShortName())) { sets.insert(newSet->getShortName(), newSet); } - const QJsonArray setCards = setDoc.object().value("cards").toArray(); - int numCardsInSet = importCardsFromSet(newSet, setCards); + int numCardsInSet = importCardsFromSet(newSet, curSetToParse.getCards()); ++setIndex; emit setIndexChanged(numCardsInSet, setIndex, curSetToParse.getLongName()); } - applyLocalizedData(); - emit setIndexChanged(0, setIndex, QString()); // total number of sets @@ -841,18 +573,9 @@ bool OracleImporter::saveToFile(const QString &fileName, const QString &sourceUr return parser.saveToFile(createDefaultMagicFormats(), sets, cards, fileName, sourceUrl, sourceVersion); } -void OracleImporter::releaseSetData() -{ - allSets.clear(); - rawSetsData.clear(); -} - void OracleImporter::clear() { sets.clear(); cards.clear(); allSets.clear(); - rawSetsData.clear(); - localizedEntries.clear(); - splitLocalizedTexts.clear(); } diff --git a/oracle/src/oracleimporter.h b/oracle/src/oracleimporter.h index c8d3a19bf..99644f9ce 100644 --- a/oracle/src/oracleimporter.h +++ b/oracle/src/oracleimporter.h @@ -1,12 +1,6 @@ #ifndef ORACLEIMPORTER_H #define ORACLEIMPORTER_H -#include "raw_json_scanner.h" - -#include -#include -#include -#include #include #include #include @@ -50,12 +44,10 @@ class SetToDownload { private: QString shortName, longName; + QList cards; QDate releaseDate; QString setType; CardSet::Priority priority; - // Byte range of this set's object within the importer's raw JSON text. Parsing - // one set at a time keeps peak memory low instead of holding the whole document. - RawJson::SetDataRange rawRange; public: const QString &getShortName() const @@ -66,6 +58,10 @@ public: { return longName; } + const QList &getCards() const + { + return cards; + } const QString &getSetType() const { return setType; @@ -78,23 +74,16 @@ public: { return priority; } - const RawJson::SetDataRange &getRawRange() const - { - return rawRange; - } SetToDownload(QString _shortName, QString _longName, + QList _cards, CardSet::Priority _priority, QString _setType = QString(), const QDate &_releaseDate = QDate()) - : shortName(std::move(_shortName)), longName(std::move(_longName)), releaseDate(_releaseDate), - setType(std::move(_setType)), priority(_priority) + : shortName(std::move(_shortName)), longName(std::move(_longName)), cards(std::move(_cards)), + releaseDate(_releaseDate), setType(std::move(_setType)), priority(_priority) { } - void setRawRange(const RawJson::SetDataRange &_rawRange) - { - rawRange = _rawRange; - } bool operator<(const SetToDownload &set) const { return longName.compare(set.longName, Qt::CaseInsensitive) < 0; @@ -107,8 +96,7 @@ public: SplitCardPart(const QString &_name, const QString &_text, const QHash &_properties, - const PrintingInfo &_printingInfo, - const QString &_localizedText = QString()); + const PrintingInfo &_printingInfo); inline const QString &getName() const { return name; @@ -117,13 +105,6 @@ public: { return text; } - /** - * @brief The cardLang rules text of this face's foreignData entry, if any. - */ - inline const QString &getLocalizedText() const - { - return localizedText; - } inline const QHash &getProperties() const { return properties; @@ -136,18 +117,10 @@ public: private: QString name; QString text; - QString localizedText; QHash properties; PrintingInfo printingInfo; }; -struct LocalizedCardEntry -{ - QString name; - QString text; - CardSet::Priority priority = CardSet::PriorityLowest; -}; - class OracleImporter : public QObject { Q_OBJECT @@ -166,130 +139,23 @@ private: QList allSets; - /** - * The raw JSON text of the source document, retained for lazy per-set - * parsing during startImport(). Frees the card data as each set is imported. - */ - QByteArray rawSetsData; - - /** - * Whether readSetsFromByteArray() should report scan progress via - * dataReadProgress. A background run routes that signal to stdout (for the - * hosting Cockatrice client to parse); the flag exists to skip the scanner - * instrumentation entirely when no consumer needs it. - */ - bool progressReporting = true; - - /** - * Atomic "please stop importing" flag. startImport() checks it between sets - * so a wizard being closed mid-import can be torn down without waiting for - * the whole import (or racing it). - */ - QAtomicInt importCancelled; - - /** - * The ISO-639 language code whose foreignData is imported; "en" by default. - */ - QString cardLang = "en"; - - /** - * Whether cardLang is a supported language other than English, so per-card - * foreignData scanning can be skipped entirely when disabled. - */ - bool localizationEnabled = false; - - /** - * Localized name/text collected per imported card key while parsing sets, - * applied to the CardInfo objects by applyLocalizedData() once all - * printings have been seen so the best-priority one wins. - */ - QMap localizedEntries; - - /** - * cardLang rules text collected for split-card names while parsing sets, - * applied by applyLocalizedData(). Kept apart from localizedEntries because - * MTGJSON emits each split face as its own card object with the joined name - * on every foreignData entry: names and the per-face text join have different - * completeness and must not overwrite each other under the same key. - */ - QMap splitLocalizedTexts; - CardInfoPtr addCard(QString name, const QString &text, bool isToken, QHash properties, const QList &relatedCards, const PrintingInfo &printingInfo); - - /** - * Records the first foreignData entry matching cardLang for the given card - * key, keeping the entry from the highest-priority set seen so far. - * - * Multi-face cards (split, adventure, aftermath, prepare) pass collectText = - * false: MTGJSON emits one foreignData entry per face with the same joined - * name but only that face's text, so the name is collected here while the - * per-face texts are joined during the split-card merge. - */ - void collectForeignData(const QString &cardKey, - const CardSetPtr ¤tSet, - const QJsonObject &card, - bool collectText = true); signals: void setIndexChanged(int cardsImported, int setIndex, const QString &setName); void dataReadProgress(int bytesRead, int totalBytes); public: explicit OracleImporter(QObject *parent = nullptr); - /** - * @brief Controls whether readSetsFromByteArray() instruments the raw scan. - * - * When enabled (the default) the raw scanner reports progress via - * dataReadProgress(), which an interactive wizard shows on its progress bar - * and a background run routes to stdout for the hosting client. Switch it - * off only when nothing will consume scan progress. - */ - void setProgressReporting(bool enabled) - { - progressReporting = enabled; - } - /** - * Selects the ISO-639 language code whose foreignData is imported. - * English (the default) and unsupported codes disable localization. - */ - void setCardLang(const QString &lang); - const QString &getCardLang() const - { - return cardLang; - } - /** - * Scans the given JSON document for set metadata. Takes the data by value so - * the wizard can hand over its decompressed buffer without copying it. - */ - bool readSetsFromByteArray(QByteArray data); + bool readSetsFromByteArray(const QByteArray &data); int startImport(); - /** - * @brief Requests an in-flight startImport() to stop at the next set boundary. - * - * Works by setting an atomic flag that startImport() polls between sets, so - * cancelImport() followed by a short waitForFinished() on the running future is - * safe the moment the wizard is about to be destroyed. - */ - void cancelImport() - { - importCancelled.storeRelease(1); - } - /** - * Applies the collected localized names/texts to the imported cards. - * Called automatically at the end of startImport(); exposed separately so - * tests can drive it after importing sets directly. - */ - void applyLocalizedData(); bool saveToFile(const QString &fileName, const QString &sourceUrl, const QString &sourceVersion); - int importCardsFromSet(const CardSetPtr ¤tSet, const QJsonArray &cardsList); - /** - * @brief Returns the default format rules. The result is memoized on first use and must be treated as immutable. - */ - const FormatRulesNameMap &createDefaultMagicFormats(); + int importCardsFromSet(const CardSetPtr ¤tSet, const QList &cardsList); + FormatRulesNameMap createDefaultMagicFormats(); const CardNameMap &getCardList() const { return cards; @@ -298,11 +164,6 @@ public: { return allSets; } - const QByteArray &getRawSetsData() const - { - return rawSetsData; - } - void releaseSetData(); void clear(); }; diff --git a/oracle/src/oraclewizard.cpp b/oracle/src/oraclewizard.cpp index 9ce0d7b51..11200dfc5 100644 --- a/oracle/src/oraclewizard.cpp +++ b/oracle/src/oraclewizard.cpp @@ -15,7 +15,6 @@ #include #include #include -#include #include OracleWizard::OracleWizard(QWidget *parent) : QWizard(parent) @@ -38,9 +37,6 @@ OracleWizard::OracleWizard(QWidget *parent) : QWizard(parent) connect(&SettingsCache::instance().personal(), &PersonalSettings::langChanged, this, &OracleWizard::updateLanguage); importer = new OracleImporter(this); - // Import card text in the language the client displays, if supported: - // foreignData for any other language is never imported. - importer->setCardLang(SettingsCache::instance().cardsDisplay().getCardLang()); nam = new QNetworkAccessManager(this); @@ -114,24 +110,6 @@ void OracleWizard::accept() QDialog::accept(); } -void OracleWizard::reject() -{ - // The wizard is being closed while a page may still run a worker on the - // importer. Ask it to stop before the wizard (and the importer child) is - // destroyed, so the worker thread never touches freed memory. - if (auto *active = dynamic_cast(currentPage())) { - active->cancelWork(); - } - QWizard::reject(); -} - -void OracleWizard::runInBackground() -{ - backgroundMode = true; - hide(); - currentPage()->initializePage(); -} - void OracleWizard::enableButtons() { button(QWizard::NextButton)->setDisabled(false); diff --git a/oracle/src/oraclewizard.h b/oracle/src/oraclewizard.h index 9a509ce5e..78427175c 100644 --- a/oracle/src/oraclewizard.h +++ b/oracle/src/oraclewizard.h @@ -23,7 +23,6 @@ class OracleWizard : public QWizard public: explicit OracleWizard(QWidget *parent = nullptr); void accept() override; - void reject() override; void enableButtons(); void disableButtons(); void retranslateUi(); @@ -53,7 +52,12 @@ public: } bool saveTokensToFile(const QString &fileName); - void runInBackground(); + void runInBackground() + { + backgroundMode = true; + hide(); + currentPage()->initializePage(); + } public: OracleImporter *importer; diff --git a/oracle/src/pages.cpp b/oracle/src/pages.cpp index df0c3f51d..df4d1a98c 100644 --- a/oracle/src/pages.cpp +++ b/oracle/src/pages.cpp @@ -12,7 +12,6 @@ #include #include #include -#include #include #include #include @@ -21,17 +20,14 @@ #include #include #include -#include #include #include #include #include #include #include -#include #include #include -#include #include #ifdef HAS_LZMA @@ -57,115 +53,6 @@ #define ALLSETS_URL "https://www.mtgjson.com/api/v5/AllPrintings.json" #endif -/** - * @brief Emits one machine-readable background-run progress line to stdout. - * - * Used only in background mode, so the hosting Cockatrice client can parse these - * lines to drive a determinate progress bar. stderr stays reserved for - * human-readable log output. - */ -static void emitBackgroundProgress(const char *stage, qint64 done, qint64 total) -{ - QTextStream out(stdout); - out << "PROGRESS " << stage << ' ' << done << ' ' << total << '\n'; - out.flush(); -} - -namespace -{ - -/** - * @brief Decompresses and dispatches a sets-file payload on a worker thread. - * - * Iteratively unwraps xz/zip compression, then either hands the JSON to the - * importer (which reports scan progress via dataReadProgress) or returns the raw - * XML for the plain-XML path. Must never touch the wizard or the page: the caller - * consumes the returned LoadSetsResult on the UI thread in importFinished(). - */ -LoadSetsResult loadSetsData(const QPointer &importer, QByteArray data) -{ - LoadSetsResult result; - - while (true) { - if (data.startsWith(XZ_SIGNATURE)) { -#ifdef HAS_LZMA - QBuffer inBuffer(&data); - QByteArray decompressed; - QBuffer outBuffer(&decompressed); - inBuffer.open(QBuffer::ReadOnly); - outBuffer.open(QBuffer::WriteOnly); - XzDecompressor xz; - if (!xz.decompress(&inBuffer, &outBuffer)) { - result.errorMessage = LoadSetsPage::tr("Xz extraction failed."); - result.offerUncompressedFallback = true; - return result; - } - data = decompressed; - continue; -#else - result.errorMessage = - LoadSetsPage::tr("Sorry, this version of Oracle does not support xz compressed files."); - result.offerUncompressedFallback = true; - return result; -#endif - } - - if (data.startsWith(ZIP_SIGNATURE)) { -#ifdef HAS_ZLIB - QBuffer inBuffer(&data); - UnZip uz; - const UnZip::ErrorCode openEc = uz.openArchive(&inBuffer); - if (openEc != UnZip::Ok) { - result.errorMessage = LoadSetsPage::tr("Failed to open Zip archive: %1.").arg(uz.formatError(openEc)); - result.offerUncompressedFallback = true; - return result; - } - if (uz.fileList().size() != 1) { - result.errorMessage = - LoadSetsPage::tr("Zip extraction failed: the Zip archive doesn't contain exactly one file."); - result.offerUncompressedFallback = true; - return result; - } - const QString fileName = uz.fileList().at(0); - QByteArray decompressed; - QBuffer outBuffer(&decompressed); - outBuffer.open(QBuffer::ReadWrite); - const UnZip::ErrorCode ec = uz.extractFile(fileName, &outBuffer); - uz.closeArchive(); - if (ec != UnZip::Ok) { - result.errorMessage = LoadSetsPage::tr("Zip extraction failed: %1.").arg(uz.formatError(ec)); - result.offerUncompressedFallback = true; - return result; - } - data = decompressed; - continue; -#else - result.errorMessage = LoadSetsPage::tr("Sorry, this version of Oracle does not support zipped files."); - result.offerUncompressedFallback = true; - return result; -#endif - } - break; - } - - if (data.startsWith("<")) { - result.ok = true; - result.plainXml = true; - result.xmlData = std::move(data); - return result; - } - - if (data.startsWith("{")) { - result.ok = importer && importer->readSetsFromByteArray(std::move(data)); - return result; - } - - result.errorMessage = LoadSetsPage::tr("Failed to interpret downloaded data."); - return result; -} - -} // namespace - #define TOKENS_URL "https://raw.githubusercontent.com/Cockatrice/Magic-Token/master/tokens.xml" #define SPOILERS_URL "https://raw.githubusercontent.com/Cockatrice/Magic-Spoiler/files/spoiler.xml" @@ -295,11 +182,6 @@ LoadSetsPage::LoadSetsPage(QWidget *parent) : OracleWizardPage(parent) setLayout(layout); } -bool LoadSetsPage::isComplete() const -{ - return !loadActive; -} - void LoadSetsPage::initializePage() { urlLineEdit->setText(wizard()->settings->value("allsetsurl", ALLSETS_URL).toString()); @@ -378,7 +260,7 @@ bool LoadSetsPage::validatePage() return false; } - progressLabel->setText(tr("Downloading (0 MB)")); + progressLabel->setText(tr("Downloading (0MB)")); // show an infinite progressbar progressBar->setMaximum(0); progressBar->setMinimum(0); @@ -397,13 +279,18 @@ bool LoadSetsPage::validatePage() return false; } + if (!setsFile.open(QIODevice::ReadOnly)) { + QMessageBox::critical(nullptr, tr("Error"), tr("Cannot open file '%1'.").arg(fileLineEdit->text())); + return false; + } + wizard()->disableButtons(); setEnabled(false); wizard()->setCardSourceUrl(setsFile.fileName()); wizard()->setCardSourceVersion("unknown"); - readSetsFromFile(setsFile.fileName()); + readSetsFromByteArray(setsFile.readAll()); } return false; @@ -452,14 +339,11 @@ void LoadSetsPage::downloadSetsFile(const QUrl &url) void LoadSetsPage::actDownloadProgressSetsFile(qint64 received, qint64 total) { - if (wizard()->backgroundMode) { - emitBackgroundProgress("download", received, total); - } if (total > 0) { progressBar->setMaximum(static_cast(total)); progressBar->setValue(static_cast(received)); } - progressLabel->setText(tr("Downloading (%1 MB)").arg((int)received / (1024 * 1024))); + progressLabel->setText(tr("Downloading (%1MB)").arg((int)received / (1024 * 1024))); } void LoadSetsPage::actDownloadFinishedSetsFile() @@ -500,97 +384,106 @@ void LoadSetsPage::actDownloadFinishedSetsFile() reply->deleteLater(); } -void LoadSetsPage::updateParsingProgress(int bytesRead, int totalBytes) +void LoadSetsPage::readSetsFromByteArray(QByteArray _data) { - if (totalBytes <= 0) { - return; - } - progressBar->setRange(0, totalBytes); - progressBar->setValue(bytesRead); - const int percent = static_cast((100.0 * bytesRead) / totalBytes); - progressLabel->setText(tr("Parsing file (%1%)").arg(percent)); -} - -void LoadSetsPage::scanProgressToStdout(int bytesRead, int totalBytes) -{ - emitBackgroundProgress("scan", bytesRead, totalBytes); -} - -void LoadSetsPage::beginLoadSets(bool compressedFile) -{ - // Show an infinite progressbar while the worker decompresses; the scan - // steals the label via dataReadProgress as soon as it starts. + // show an infinite progressbar progressBar->setMaximum(0); progressBar->setMinimum(0); progressBar->setValue(0); - progressLabel->setText(compressedFile ? tr("Extracting file...") : tr("Parsing file")); + progressLabel->setText(tr("Parsing file")); progressLabel->show(); progressBar->show(); - // Keep Next disabled (via completeChanged) until the worker reports in; - // updateButtonStates() re-evaluates button state whenever we re-enable. - loadActive = true; - emit completeChanged(); - wizard()->downloadedPlainXml = false; wizard()->xmlData.clear(); - - if (wizard()->backgroundMode) { - connect(wizard()->importer, &OracleImporter::dataReadProgress, this, &LoadSetsPage::scanProgressToStdout, - Qt::UniqueConnection); - } else { - connect(wizard()->importer, &OracleImporter::dataReadProgress, this, &LoadSetsPage::updateParsingProgress, - Qt::UniqueConnection); - } + readSetsFromByteArrayRef(_data); } -void LoadSetsPage::readSetsFromByteArray(QByteArray _data) +void LoadSetsPage::readSetsFromByteArrayRef(QByteArray &_data) { - const bool compressed = _data.startsWith(XZ_SIGNATURE) || _data.startsWith(ZIP_SIGNATURE); - beginLoadSets(compressed); - - // Decompress and scan off the UI thread so a large download can't freeze the window. - const QPointer importer = wizard()->importer; - future = QtConcurrent::run( - [importer, data = std::move(_data)]() mutable { return loadSetsData(importer, std::move(data)); }); - watcher.setFuture(future); -} - -void LoadSetsPage::readSetsFromFile(const QString &fileName) -{ - // Peek at the header on the UI thread so the status text can distinguish - // "Extracting file..." from a plain JSON parse; the full read happens in the worker. - QFile headerFile(fileName); - bool compressed = false; - if (headerFile.open(QIODevice::ReadOnly)) { - const QByteArray header = headerFile.read(6); - compressed = header.startsWith(XZ_SIGNATURE) || header.startsWith(ZIP_SIGNATURE); - } - beginLoadSets(compressed); - - // Read, decompress and scan off the UI thread (a plain JSON can be hundreds - // of MB, so even the read itself must not block the window). - const QPointer importer = wizard()->importer; - future = QtConcurrent::run([importer, fileName]() mutable -> LoadSetsResult { - QFile file(fileName); - if (!file.open(QIODevice::ReadOnly)) { - LoadSetsResult readError; - readError.errorMessage = LoadSetsPage::tr("Cannot open file '%1'.").arg(fileName); - return readError; + // unzip the file if needed + if (_data.startsWith(XZ_SIGNATURE)) { +#ifdef HAS_LZMA + // zipped file + auto *inBuffer = new QBuffer(&_data); + auto newData = QByteArray(); + auto *outBuffer = new QBuffer(&newData); + inBuffer->open(QBuffer::ReadOnly); + outBuffer->open(QBuffer::WriteOnly); + XzDecompressor xz; + if (!xz.decompress(inBuffer, outBuffer)) { + zipDownloadFailed(tr("Xz extraction failed.")); + return; } - return loadSetsData(importer, file.readAll()); - }); - watcher.setFuture(future); -} + _data.clear(); + readSetsFromByteArrayRef(newData); + return; +#else + zipDownloadFailed(tr("Sorry, this version of Oracle does not support xz compressed files.")); -void LoadSetsPage::cancelWork() -{ - // The scan is short-lived; just wait it out before the wizard (and its - // importer) can be torn down underneath the worker thread. - if (future.isRunning()) { - future.cancel(); - watcher.cancel(); - future.waitForFinished(); + wizard()->enableButtons(); + setEnabled(true); + progressLabel->hide(); + progressBar->hide(); + return; +#endif + } else if (_data.startsWith(ZIP_SIGNATURE)) { +#ifdef HAS_ZLIB + // zipped file + auto *inBuffer = new QBuffer(&_data); + auto newData = QByteArray(); + auto *outBuffer = new QBuffer(&newData); + QString fileName; + UnZip::ErrorCode ec; + UnZip uz; + + ec = uz.openArchive(inBuffer); + if (ec != UnZip::Ok) { + zipDownloadFailed(tr("Failed to open Zip archive: %1.").arg(uz.formatError(ec))); + return; + } + + if (uz.fileList().size() != 1) { + zipDownloadFailed(tr("Zip extraction failed: the Zip archive doesn't contain exactly one file.")); + return; + } + fileName = uz.fileList().at(0); + + outBuffer->open(QBuffer::ReadWrite); + ec = uz.extractFile(fileName, outBuffer); + if (ec != UnZip::Ok) { + zipDownloadFailed(tr("Zip extraction failed: %1.").arg(uz.formatError(ec))); + uz.closeArchive(); + return; + } + _data.clear(); + readSetsFromByteArrayRef(newData); + return; +#else + zipDownloadFailed(tr("Sorry, this version of Oracle does not support zipped files.")); + + wizard()->enableButtons(); + setEnabled(true); + progressLabel->hide(); + progressBar->hide(); + return; +#endif + } else if (_data.startsWith("{")) { + // Start the computation. + jsonData = std::move(_data); + future = QtConcurrent::run([this] { return wizard()->importer->readSetsFromByteArray(std::move(jsonData)); }); + watcher.setFuture(future); + } else if (_data.startsWith("<")) { + // save xml file and don't do any processing + wizard()->downloadedPlainXml = true; + wizard()->xmlData = std::move(_data); + importFinished(); + } else { + wizard()->enableButtons(); + setEnabled(true); + progressLabel->hide(); + progressBar->hide(); + QMessageBox::critical(this, tr("Error"), tr("Failed to interpret downloaded data.")); } } @@ -616,65 +509,17 @@ void LoadSetsPage::zipDownloadFailed(const QString &message) void LoadSetsPage::importFinished() { - loadActive = false; - emit completeChanged(); wizard()->enableButtons(); setEnabled(true); + progressLabel->hide(); + progressBar->hide(); - const LoadSetsResult result = watcher.result(); - - if (result.plainXml) { - wizard()->downloadedPlainXml = true; - wizard()->xmlData = result.xmlData; + if (wizard()->downloadedPlainXml || watcher.future().result()) { + wizard()->next(); + } else { + QMessageBox::critical(this, tr("Error"), + tr("The file was retrieved successfully, but it does not contain any sets data.")); } - - if (wizard()->backgroundMode) { - progressLabel->hide(); - progressBar->hide(); - if (!result.errorMessage.isEmpty()) { - qWarning() << result.errorMessage; - } else if (!result.ok && !result.plainXml) { - qWarning() << tr("The file was retrieved successfully, but it does not contain any sets data."); - } - emit readyToContinue(); - return; - } - - const auto fail = [this](const QString &message) { - progressLabel->hide(); - progressBar->hide(); - QMessageBox::critical(this, tr("Error"), message); - }; - - if (!result.errorMessage.isEmpty()) { - if (result.offerUncompressedFallback) { - zipDownloadFailed(result.errorMessage); - return; - } - fail(result.errorMessage); - return; - } - - if (!result.ok && !result.plainXml) { - fail(tr("The file was retrieved successfully, but it does not contain any sets data.")); - return; - } - - // Snap the bar to 100% and hold it there for a moment so the completed state - // is actually visible before the next page's own import progress takes over - // (a zero-length deferral can still fire before the repaint is delivered). - progressBar->setMaximum(1); - progressBar->setValue(1); - progressLabel->setText(tr("Parsing file (100%)")); - QTimer::singleShot(500, this, [this] { - if (wizard()->currentPage() == this) { - // Leave the page pristine: hide the completed load bar so a later - // Back from the save page doesn't show stale progress. - progressLabel->hide(); - progressBar->hide(); - wizard()->next(); - } - }); } SaveSetsPage::SaveSetsPage(QWidget *parent) : OracleWizardPage(parent) @@ -682,113 +527,46 @@ SaveSetsPage::SaveSetsPage(QWidget *parent) : OracleWizardPage(parent) pathLabel = new QLabel(this); saveLabel = new QLabel(this); - progressBar = new QProgressBar(this); - progressBar->hide(); - defaultPathCheckBox = new QCheckBox(this); messageLog = new QTextEdit(this); messageLog->setReadOnly(true); auto *layout = new QGridLayout(this); - layout->addWidget(progressBar, 0, 0); - layout->addWidget(messageLog, 1, 0); - layout->addWidget(saveLabel, 2, 0); - layout->addWidget(pathLabel, 3, 0); - layout->addWidget(defaultPathCheckBox, 4, 0); + layout->addWidget(messageLog, 0, 0); + layout->addWidget(saveLabel, 1, 0); + layout->addWidget(pathLabel, 2, 0); + layout->addWidget(defaultPathCheckBox, 3, 0); setLayout(layout); } -bool SaveSetsPage::isComplete() const -{ - return !importActive; -} - void SaveSetsPage::cleanupPage() { - cancelWork(); - disconnect(wizard()->importer, &OracleImporter::setIndexChanged, this, &SaveSetsPage::updateTotalProgress); - disconnect(&importWatcher, &QFutureWatcher::finished, this, &SaveSetsPage::importFinished); wizard()->importer->clear(); + disconnect(wizard()->importer, &OracleImporter::setIndexChanged, nullptr, nullptr); } void SaveSetsPage::initializePage() { + messageLog->clear(); + retranslateUi(); if (wizard()->downloadedPlainXml) { messageLog->hide(); - progressBar->hide(); - if (wizard()->backgroundMode) { - emit readyToContinue(); + } else { + messageLog->show(); + connect(wizard()->importer, &OracleImporter::setIndexChanged, this, &SaveSetsPage::updateTotalProgress); + + int setsImported = wizard()->importer->startImport(); + + if (setsImported == 0) { + QMessageBox::critical(this, tr("Error"), tr("No set has been imported.")); } - return; - } - - messageLog->clear(); - messageLog->show(); - progressBar->show(); - - totalSets = wizard()->importer->getSets().size(); - progressBar->setRange(0, totalSets); - progressBar->setValue(0); - - connect(wizard()->importer, &OracleImporter::setIndexChanged, this, &SaveSetsPage::updateTotalProgress, - Qt::UniqueConnection); - connect(&importWatcher, &QFutureWatcher::finished, this, &SaveSetsPage::importFinished, Qt::UniqueConnection); - - wizard()->disableButtons(); - importActive = true; - emit completeChanged(); - - const QPointer importer = wizard()->importer; - importFuture = QtConcurrent::run([importer] { return importer ? importer->startImport() : 0; }); - importWatcher.setFuture(importFuture); -} - -void SaveSetsPage::cancelWork() -{ - if (!importActive) { - return; - } - // Ask the worker to stop at the next set boundary, then wait it out so the - // wizard (and the importer it owns) is never torn down under a running thread. - importActive = false; - emit completeChanged(); - wizard()->importer->cancelImport(); - importFuture.cancel(); - importWatcher.cancel(); - importFuture.waitForFinished(); -} - -void SaveSetsPage::importFinished() -{ - if (!importActive) { - return; - } - importActive = false; - emit completeChanged(); - - wizard()->enableButtons(); - - const int setsImported = importWatcher.result(); - const QPointer importer = wizard()->importer; - if (importer) { - importer->releaseSetData(); } if (wizard()->backgroundMode) { - if (setsImported == 0) { - qWarning() << tr("No set has been imported."); - } emit readyToContinue(); - return; - } - - progressBar->setValue(progressBar->maximum()); - - if (setsImported == 0) { - QMessageBox::critical(this, tr("Error"), tr("No set has been imported.")); } } @@ -796,7 +574,7 @@ void SaveSetsPage::retranslateUi() { setTitle(tr("Sets imported")); if (wizard()->downloadedPlainXml) { - setSubTitle(tr("A Cockatrice card database file of %1 MB has been downloaded.") + setSubTitle(tr("A cockatrice database file of %1 MB has been downloaded.") .arg(qRound(wizard()->xmlData.size() / 1000000.0))); } else { setSubTitle(tr("The following sets have been found:")); @@ -810,28 +588,13 @@ void SaveSetsPage::retranslateUi() setButtonText(QWizard::NextButton, tr("&Save")); } -void SaveSetsPage::updateTotalProgress(int cardsImported, int setIndex, const QString &setName) +void SaveSetsPage::updateTotalProgress(int cardsImported, int /* setIndex */, const QString &setName) { - if (!importActive) { - return; - } if (setName.isEmpty()) { - progressBar->setValue(progressBar->maximum()); - const int cardCount = wizard()->importer->getCardList().size(); - if (wizard()->backgroundMode) { - qInfo() << tr("Import finished: %1 cards.").arg(cardCount); - emitBackgroundProgress("import", totalSets, totalSets); - } else { - messageLog->append("" + tr("Import finished: %1 cards.").arg(cardCount) + ""); - } + messageLog->append("" + tr("Import finished: %1 cards.").arg(wizard()->importer->getCardList().size()) + + ""); } else { - progressBar->setValue(setIndex); - if (wizard()->backgroundMode) { - qInfo() << tr("%1: %2 cards imported").arg(setName).arg(cardsImported); - emitBackgroundProgress("import", setIndex, totalSets); - } else { - messageLog->append(tr("%1: %2 cards imported").arg(setName).arg(cardsImported)); - } + messageLog->append(tr("%1: %2 cards imported").arg(setName).arg(cardsImported)); } messageLog->verticalScrollBar()->setValue(messageLog->verticalScrollBar()->maximum()); @@ -977,4 +740,4 @@ void LoadSpoilersPage::retranslateUi() pathLabel->setText(tr("The spoiler database will be saved at the following location:") + "
" + SettingsCache::instance().getSpoilerCardDatabasePath()); defaultPathCheckBox->setText(tr("Save to a custom path (not recommended)")); -} +} \ No newline at end of file diff --git a/oracle/src/pages.h b/oracle/src/pages.h index d0a9fcd43..066cc2e1b 100644 --- a/oracle/src/pages.h +++ b/oracle/src/pages.h @@ -3,10 +3,8 @@ #include "pagetemplates.h" -#include #include #include -#include #include #include #include @@ -59,31 +57,19 @@ protected: void initializePage() override; }; -/** @brief Result of a worker-thread sets-file load (read + decompress + dispatch). */ -struct LoadSetsResult -{ - bool ok = false; ///< JSON scan produced set data (or plain XML was handled) - bool plainXml = false; ///< input was a plain Cockatrice XML database - QByteArray xmlData; ///< raw XML for the plain-XML path - QString errorMessage; ///< set when the input could not be processed - bool offerUncompressedFallback = false; ///< decompression-only failure: offer the uncompressed URL -}; - class LoadSetsPage : public OracleWizardPage { Q_OBJECT public: explicit LoadSetsPage(QWidget *parent = nullptr); void retranslateUi() override; - bool isComplete() const override; protected: void initializePage() override; bool validatePage() override; void readSetsFromByteArray(QByteArray _data); - void readSetsFromFile(const QString &fileName); + void readSetsFromByteArrayRef(QByteArray &_data); void downloadSetsFile(const QUrl &url); - void cancelWork() override; private: QRadioButton *urlRadioButton; @@ -95,19 +81,15 @@ private: QLabel *progressLabel; QProgressBar *progressBar; - QFutureWatcher watcher; - QFuture future; - bool loadActive = false; - - void beginLoadSets(bool compressedFile = false); + QFutureWatcher watcher; + QFuture future; + QByteArray jsonData; private slots: void actLoadSetsFile(); void actRestoreDefaultUrl(); void actDownloadProgressSetsFile(qint64 received, qint64 total); void actDownloadFinishedSetsFile(); - void updateParsingProgress(int bytesRead, int totalBytes); - void scanProgressToStdout(int bytesRead, int totalBytes); void importFinished(); void zipDownloadFailed(const QString &message); }; @@ -118,28 +100,19 @@ class SaveSetsPage : public OracleWizardPage public: explicit SaveSetsPage(QWidget *parent = nullptr); void retranslateUi() override; - bool isComplete() const override; private: QTextEdit *messageLog; - QProgressBar *progressBar; QCheckBox *defaultPathCheckBox; QLabel *pathLabel; QLabel *saveLabel; - QFutureWatcher importWatcher; - QFuture importFuture; - int totalSets = 0; - bool importActive = false; - protected: void initializePage() override; void cleanupPage() override; bool validatePage() override; - void cancelWork() override; private slots: - void importFinished(); void updateTotalProgress(int cardsImported, int setIndex, const QString &setName); }; diff --git a/oracle/src/pagetemplates.cpp b/oracle/src/pagetemplates.cpp index 0ffeabbb3..4a27fef30 100644 --- a/oracle/src/pagetemplates.cpp +++ b/oracle/src/pagetemplates.cpp @@ -112,7 +112,7 @@ bool SimpleDownloadFilePage::validatePage() return false; } - progressLabel->setText(tr("Downloading (0 MB)")); + progressLabel->setText(tr("Downloading (0MB)")); // show an infinite progressbar progressBar->setMaximum(0); progressBar->setMinimum(0); diff --git a/oracle/src/pagetemplates.h b/oracle/src/pagetemplates.h index ccad7ec62..6e79c867e 100644 --- a/oracle/src/pagetemplates.h +++ b/oracle/src/pagetemplates.h @@ -20,14 +20,6 @@ public: } virtual void retranslateUi() = 0; - /** - * @brief Asks an active page to stop any background worker before the wizard - * (and its importer) can be torn down underneath it. Default is a no-op. - */ - virtual void cancelWork() - { - } - signals: void readyToContinue(); diff --git a/oracle/src/parsehelpers.cpp b/oracle/src/parsehelpers.cpp index ee7c4eb2e..a97bf67c9 100644 --- a/oracle/src/parsehelpers.cpp +++ b/oracle/src/parsehelpers.cpp @@ -20,7 +20,7 @@ * Note that "...enters tapped unless..." returns false. * * @param name The name of the card - * @param text The Oracle text of the card + * @param text The oracle text of the card */ bool parseCipt(const QString &name, const QString &text) { diff --git a/oracle/src/qt-json/AUTHORS b/oracle/src/qt-json/AUTHORS new file mode 100644 index 000000000..29a85929f --- /dev/null +++ b/oracle/src/qt-json/AUTHORS @@ -0,0 +1,3 @@ +Eeli Reilin +Luis Gustavo S. Barreto +Stephen Kockentiedt diff --git a/oracle/src/qt-json/LICENSE b/oracle/src/qt-json/LICENSE new file mode 100644 index 000000000..3c42b515a --- /dev/null +++ b/oracle/src/qt-json/LICENSE @@ -0,0 +1,27 @@ +Copyright 2011 Eeli Reilin. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY ''AS IS'' AND ANY EXPRESS OR +IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO +EVENT SHALL EELI REILIN OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE +OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +The views and conclusions contained in the software and documentation +are those of the authors and should not be interpreted as representing +official policies, either expressed or implied, of Eeli Reilin. + diff --git a/oracle/src/qt-json/README b/oracle/src/qt-json/README new file mode 100644 index 000000000..b60c1599b --- /dev/null +++ b/oracle/src/qt-json/README @@ -0,0 +1,96 @@ +######################################################################## +1. INTRODUCTION + +The Json class is a simple class for parsing JSON data into a QVariant +hierarchies. Now, we can also reverse the process and serialize +QVariant hierarchies into valid JSON data. + + +######################################################################## +2. HOW TO USE + +The parser is really easy to use. Let's say we have the following +QString of JSON data: + +------------------------------------------------------------------------ +{ + "encoding" : "UTF-8", + "plug-ins" : [ + "python", + "c++", + "ruby" + ], + "indent" : { + "length" : 3, + "use_space" : true + } +} +------------------------------------------------------------------------ + +We would first call the parse-method: + +------------------------------------------------------------------------ +//Say that we're using the QtJson namespace +using namespace QtJson; +bool ok; +//json is a QString containing the JSON data +QVariantMap result = Json::parse(json, ok).toMap(); + +if(!ok) { + qFatal("An error occurred during parsing"); + exit(1); +} +------------------------------------------------------------------------ + +Assuming the parsing process completed without errors, we would then +go through the hierarchy: + +------------------------------------------------------------------------ +qDebug() << "encoding:" << result["encoding"].toString(); +qDebug() << "plugins:"; + +foreach(QVariant plugin, result["plug-ins"].toList()) { + qDebug() << "\t-" << plugin.toString(); +} + +QVariantMap nestedMap = result["indent"].toMap(); +qDebug() << "length:" << nestedMap["length"].toInt(); +qDebug() << "use_space:" << nestedMap["use_space"].toBool(); +------------------------------------------------------------------------ + +The previous code would print out the following: + +------------------------------------------------------------------------ +encoding: "UTF-8" +plugins: + - "python" + - "c++" + - "ruby" +length: 3 +use_space: true +------------------------------------------------------------------------ + +To write JSON data from Qt object is as simple as parsing: + +------------------------------------------------------------------------ +QVariantMap map; +map["name"] = "Name"; +map["age"] = 22; + +QByteArray data = Json::serialize(map); +------------------------------------------------------------------------ + +The byte array 'data' contains valid JSON data: + +------------------------------------------------------------------------ +{ + name: "Luis Gustavo", + age: 22, +} +------------------------------------------------------------------------ + + +######################################################################## +4. CONTRIBUTING + +The code is available to download at GitHub. Contribute if you dare! diff --git a/oracle/src/qt-json/json.cpp b/oracle/src/qt-json/json.cpp new file mode 100644 index 000000000..ff739b49d --- /dev/null +++ b/oracle/src/qt-json/json.cpp @@ -0,0 +1,545 @@ +/* Copyright 2011 Eeli Reilin. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY ''AS IS'' AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO + * EVENT SHALL EELI REILIN OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, + * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, + * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * The views and conclusions contained in the software and documentation + * are those of the authors and should not be interpreted as representing + * official policies, either expressed or implied, of Eeli Reilin. + */ + +/** + * \file json.cpp + */ + +#include "json.h" + +#include +#include + +namespace QtJson +{ + +static QString sanitizeString(QString str) +{ + str.replace(QLatin1String("\\"), QLatin1String("\\\\")); + str.replace(QLatin1String("\""), QLatin1String("\\\"")); + str.replace(QLatin1String("\b"), QLatin1String("\\b")); + str.replace(QLatin1String("\f"), QLatin1String("\\f")); + str.replace(QLatin1String("\n"), QLatin1String("\\n")); + str.replace(QLatin1String("\r"), QLatin1String("\\r")); + str.replace(QLatin1String("\t"), QLatin1String("\\t")); + return QString(QLatin1String("\"%1\"")).arg(str); +} + +static QByteArray join(const QList &list, const QByteArray &sep) +{ + QByteArray res; + for (const QByteArray &i : list) { + if (!res.isEmpty()) { + res += sep; + } + res += i; + } + return res; +} + +/** + * parse + */ +QVariant Json::parse(const QString &json) +{ + bool success = true; + return Json::parse(json, success); +} + +/** + * parse + */ +QVariant Json::parse(const QString &json, bool &success) +{ + success = true; + + // Return an empty QVariant if the JSON data is either null or empty + if (!json.isNull() || !json.isEmpty()) { + // We'll start from index 0 + int index = 0; + + // Parse the first value + QVariant value = Json::parseValue(json, index, success); + + // Return the parsed value + return value; + } else { + // Return the empty QVariant + return QVariant(); + } +} + +QByteArray Json::serialize(const QVariant &data) +{ + bool success = true; + return Json::serialize(data, success); +} + +QByteArray Json::serialize(const QVariant &data, bool &success) +{ + QByteArray str; + success = true; + + if (!data.isValid()) // invalid or null? + { + str = "null"; + } + else if ((data.typeId() == QMetaType::Type::QVariantList) || + (data.typeId() == QMetaType::Type::QStringList)) // variant is a list? + { + QList values; + const QVariantList list = data.toList(); + for (const QVariant &v : list) { + QByteArray serializedValue = serialize(v); + if (serializedValue.isNull()) { + success = false; + break; + } + values << serializedValue; + } + + str = "[ " + join(values, ", ") + " ]"; + } + else if ((data.typeId() == QMetaType::Type::QVariantHash)) // variant is a hash? + { + const QVariantHash vhash = data.toHash(); + QHashIterator it(vhash); + str = "{ "; + QList pairs; + + while (it.hasNext()) { + it.next(); + QByteArray serializedValue = serialize(it.value()); + + if (serializedValue.isNull()) { + success = false; + break; + } + + pairs << sanitizeString(it.key()).toUtf8() + " : " + serializedValue; + } + + str += join(pairs, ", "); + str += " }"; + } + else if ((data.typeId() == QMetaType::Type::QVariantMap)) // variant is a map? + { + const QVariantMap vmap = data.toMap(); + QMapIterator it(vmap); + str = "{ "; + QList pairs; + while (it.hasNext()) { + it.next(); + QByteArray serializedValue = serialize(it.value()); + if (serializedValue.isNull()) { + success = false; + break; + } + pairs << sanitizeString(it.key()).toUtf8() + " : " + serializedValue; + } + str += join(pairs, ", "); + str += " }"; + } + else if ((data.typeId() == QMetaType::Type::QString) || + (data.typeId() == QMetaType::Type::QByteArray)) // a string or a byte array? + { + str = sanitizeString(data.toString()).toUtf8(); + } + else if (data.typeId() == QMetaType::Type::Double) // double? + { + str = QByteArray::number(data.toDouble(), 'g', 20); + if (!str.contains(".") && !str.contains("e")) { + str += ".0"; + } + } + else if (data.typeId() == QMetaType::Type::Bool) // boolean value? + { + str = data.toBool() ? "true" : "false"; + } + else if (data.typeId() == QMetaType::Type::ULongLong) // large unsigned number? + { + str = QByteArray::number(data.value()); + } else if (data.canConvert()) // any signed number? + { + str = QByteArray::number(data.value()); + } else if (data.canConvert()) { + str = QString::number(data.value()).toUtf8(); + } else if (data.canConvert()) // can value be converted to string? + { + // this will catch QDate, QDateTime, QUrl, ... + str = sanitizeString(data.toString()).toUtf8(); + } else { + success = false; + } + if (success) { + return str; + } else { + return QByteArray(); + } +} + +/** + * parseValue + */ +QVariant Json::parseValue(const QString &json, int &index, bool &success) +{ + // Determine what kind of data we should parse by + // checking out the upcoming token + switch (Json::lookAhead(json, index)) { + case JsonTokenString: + return Json::parseString(json, index, success); + case JsonTokenNumber: + return Json::parseNumber(json, index); + case JsonTokenCurlyOpen: + return Json::parseObject(json, index, success); + case JsonTokenSquaredOpen: + return Json::parseArray(json, index, success); + case JsonTokenTrue: + Json::nextToken(json, index); + return QVariant(true); + case JsonTokenFalse: + Json::nextToken(json, index); + return QVariant(false); + case JsonTokenNull: + Json::nextToken(json, index); + return QVariant(); + case JsonTokenNone: + break; + } + + // If there were no tokens, flag the failure and return an empty QVariant + success = false; + return QVariant(); +} + +/** + * parseObject + */ +QVariant Json::parseObject(const QString &json, int &index, bool &success) +{ + QVariantMap map; + int token; + + // Get rid of the whitespace and increment index + Json::nextToken(json, index); + + // Loop through all of the key/value pairs of the object + bool done = false; + while (!done) { + // Get the upcoming token + token = Json::lookAhead(json, index); + + if (token == JsonTokenNone) { + success = false; + return QVariantMap(); + } else if (token == JsonTokenComma) { + Json::nextToken(json, index); + } else if (token == JsonTokenCurlyClose) { + Json::nextToken(json, index); + return map; + } else { + // Parse the key/value pair's name + QString name = Json::parseString(json, index, success).toString(); + + if (!success) { + return QVariantMap(); + } + + // Get the next token + token = Json::nextToken(json, index); + + // If the next token is not a colon, flag the failure + // return an empty QVariant + if (token != JsonTokenColon) { + success = false; + return QVariant(QVariantMap()); + } + + // Parse the key/value pair's value + QVariant value = Json::parseValue(json, index, success); + + if (!success) { + return QVariantMap(); + } + + // Assign the value to the key in the map + map[name] = value; + } + } + + // Return the map successfully + return QVariant(map); +} + +/** + * parseArray + */ +QVariant Json::parseArray(const QString &json, int &index, bool &success) +{ + QVariantList list; + + Json::nextToken(json, index); + + bool done = false; + while (!done) { + int token = Json::lookAhead(json, index); + + if (token == JsonTokenNone) { + success = false; + return QVariantList(); + } else if (token == JsonTokenComma) { + Json::nextToken(json, index); + } else if (token == JsonTokenSquaredClose) { + Json::nextToken(json, index); + break; + } else { + QVariant value = Json::parseValue(json, index, success); + + if (!success) { + return QVariantList(); + } + + list.push_back(value); + } + } + + return QVariant(list); +} + +/** + * parseString + */ +QVariant Json::parseString(const QString &json, int &index, bool &success) +{ + QString s; + QChar c; + + Json::eatWhitespace(json, index); + + c = json[index++]; + + bool complete = false; + while (!complete) { + if (index == json.size()) { + break; + } + + c = json[index++]; + + if (c == '\"') { + complete = true; + break; + } else if (c == '\\') { + if (index == json.size()) { + break; + } + + c = json[index++]; + + if (c == '\"') { + s.append('\"'); + } else if (c == '\\') { + s.append('\\'); + } else if (c == '/') { + s.append('/'); + } else if (c == 'b') { + s.append('\b'); + } else if (c == 'f') { + s.append('\f'); + } else if (c == 'n') { + s.append('\n'); + } else if (c == 'r') { + s.append('\r'); + } else if (c == 't') { + s.append('\t'); + } else if (c == 'u') { + int remainingLength = json.size() - index; + + if (remainingLength >= 4) { + QString unicodeStr = json.mid(index, 4); + + int symbol = unicodeStr.toInt(0, 16); + + s.append(QChar(symbol)); + + index += 4; + } else { + break; + } + } + } else { + s.append(c); + } + } + + if (!complete) { + success = false; + return QVariant(); + } + + return QVariant(s); +} + +/** + * parseNumber + */ +QVariant Json::parseNumber(const QString &json, int &index) +{ + Json::eatWhitespace(json, index); + + int lastIndex = Json::lastIndexOfNumber(json, index); + int charLength = (lastIndex - index) + 1; + QString numberStr; + + numberStr = json.mid(index, charLength); + + index = lastIndex + 1; + + if (numberStr.contains('.')) { + return QVariant(numberStr.toDouble(NULL)); + } else if (numberStr.startsWith('-')) { + return QVariant(numberStr.toLongLong(NULL)); + } else { + return QVariant(numberStr.toULongLong(NULL)); + } +} + +/** + * lastIndexOfNumber + */ +int Json::lastIndexOfNumber(const QString &json, int index) +{ + static const QString numericCharacters("0123456789+-.eE"); + int lastIndex; + + for (lastIndex = index; lastIndex < json.size(); lastIndex++) { + if (numericCharacters.indexOf(json[lastIndex]) == -1) { + break; + } + } + + return lastIndex - 1; +} + +/** + * eatWhitespace + */ +void Json::eatWhitespace(const QString &json, int &index) +{ + static const QString whitespaceChars(" \t\n\r"); + for (; index < json.size(); index++) { + if (whitespaceChars.indexOf(json[index]) == -1) { + break; + } + } +} + +/** + * lookAhead + */ +int Json::lookAhead(const QString &json, int index) +{ + int saveIndex = index; + return Json::nextToken(json, saveIndex); +} + +/** + * nextToken + */ +int Json::nextToken(const QString &json, int &index) +{ + Json::eatWhitespace(json, index); + + if (index == json.size()) { + return JsonTokenNone; + } + + QChar c = json[index]; + index++; + switch (c.toLatin1()) { + case '{': + return JsonTokenCurlyOpen; + case '}': + return JsonTokenCurlyClose; + case '[': + return JsonTokenSquaredOpen; + case ']': + return JsonTokenSquaredClose; + case ',': + return JsonTokenComma; + case '"': + return JsonTokenString; + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + case '-': + return JsonTokenNumber; + case ':': + return JsonTokenColon; + } + + index--; + + int remainingLength = json.size() - index; + + // True + if (remainingLength >= 4) { + if (json[index] == 't' && json[index + 1] == 'r' && json[index + 2] == 'u' && json[index + 3] == 'e') { + index += 4; + return JsonTokenTrue; + } + } + + // False + if (remainingLength >= 5) { + if (json[index] == 'f' && json[index + 1] == 'a' && json[index + 2] == 'l' && json[index + 3] == 's' && + json[index + 4] == 'e') { + index += 5; + return JsonTokenFalse; + } + } + + // Null + if (remainingLength >= 4) { + if (json[index] == 'n' && json[index + 1] == 'u' && json[index + 2] == 'l' && json[index + 3] == 'l') { + index += 4; + return JsonTokenNull; + } + } + + return JsonTokenNone; +} + +} // namespace QtJson diff --git a/oracle/src/qt-json/json.h b/oracle/src/qt-json/json.h new file mode 100644 index 000000000..cf0499d4e --- /dev/null +++ b/oracle/src/qt-json/json.h @@ -0,0 +1,204 @@ +/* Copyright 2011 Eeli Reilin. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY ''AS IS'' AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO + * EVENT SHALL EELI REILIN OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, + * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, + * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * The views and conclusions contained in the software and documentation + * are those of the authors and should not be interpreted as representing + * official policies, either expressed or implied, of Eeli Reilin. + */ + +/** + * \file json.h + */ + +#ifndef JSON_H +#define JSON_H + +#include +#include + +namespace QtJson +{ + +/** + * \enum JsonToken + */ +enum JsonToken +{ + JsonTokenNone = 0, + JsonTokenCurlyOpen = 1, + JsonTokenCurlyClose = 2, + JsonTokenSquaredOpen = 3, + JsonTokenSquaredClose = 4, + JsonTokenColon = 5, + JsonTokenComma = 6, + JsonTokenString = 7, + JsonTokenNumber = 8, + JsonTokenTrue = 9, + JsonTokenFalse = 10, + JsonTokenNull = 11 +}; + +/** + * \class Json + * \brief A JSON data parser + * + * Json parses a JSON data into a QVariant hierarchy. + */ +class Json +{ + public: + /** + * Parse a JSON string + * + * \param json The JSON data + */ + static QVariant parse(const QString &json); + + /** + * Parse a JSON string + * + * \param json The JSON data + * \param success The success of the parsing + */ + static QVariant parse(const QString &json, bool &success); + + /** + * This method generates a textual JSON representation + * + * \param data The JSON data generated by the parser. + * \param success The success of the serialization + */ + static QByteArray serialize(const QVariant &data); + + /** + * This method generates a textual JSON representation + * + * \param data The JSON data generated by the parser. + * \param success The success of the serialization + * + * \return QByteArray Textual JSON representation + */ + static QByteArray serialize(const QVariant &data, bool &success); + + private: + /** + * Parses a value starting from index + * + * \param json The JSON data + * \param index The start index + * \param success The success of the parse process + * + * \return QVariant The parsed value + */ + static QVariant parseValue(const QString &json, int &index, + bool &success); + + /** + * Parses an object starting from index + * + * \param json The JSON data + * \param index The start index + * \param success The success of the object parse + * + * \return QVariant The parsed object map + */ + static QVariant parseObject(const QString &json, int &index, + bool &success); + + /** + * Parses an array starting from index + * + * \param json The JSON data + * \param index The starting index + * \param success The success of the array parse + * + * \return QVariant The parsed variant array + */ + static QVariant parseArray(const QString &json, int &index, + bool &success); + + /** + * Parses a string starting from index + * + * \param json The JSON data + * \param index The starting index + * \param success The success of the string parse + * + * \return QVariant The parsed string + */ + static QVariant parseString(const QString &json, int &index, + bool &success); + + /** + * Parses a number starting from index + * + * \param json The JSON data + * \param index The starting index + * + * \return QVariant The parsed number + */ + static QVariant parseNumber(const QString &json, int &index); + + /** + * Get the last index of a number starting from index + * + * \param json The JSON data + * \param index The starting index + * + * \return The last index of the number + */ + static int lastIndexOfNumber(const QString &json, int index); + + /** + * Skip unwanted whitespace symbols starting from index + * + * \param json The JSON data + * \param index The start index + */ + static void eatWhitespace(const QString &json, int &index); + + /** + * Check what token lies ahead + * + * \param json The JSON data + * \param index The starting index + * + * \return int The upcoming token + */ + static int lookAhead(const QString &json, int index); + + /** + * Get the next JSON token + * + * \param json The JSON data + * \param index The starting index + * + * \return int The next JSON token + */ + static int nextToken(const QString &json, int &index); +}; + + +} //end namespace + +#endif //JSON_H diff --git a/oracle/src/raw_json_scanner.cpp b/oracle/src/raw_json_scanner.cpp deleted file mode 100644 index 8c4633598..000000000 --- a/oracle/src/raw_json_scanner.cpp +++ /dev/null @@ -1,677 +0,0 @@ -#include "raw_json_scanner.h" - -#include -#include - -namespace -{ - -// Nesting cap matching QJsonDocument's limit, so a pathologically deep document -// fails shallowly instead of overflowing the stack through the recursive -// skipValue/skipArray/skipObject walk (Qt's parser caps at 1024 for the same -// reason and reports DeepNesting). -constexpr int kMaxNestingDepth = 1024; - -/** - * @brief Throttled byte-position reporting for scanSetRanges(). - * - * Threaded through the skip walk so progress can be reported without materializing - * the whole document. Reports are rate-limited so a GUI showing progress isn't - * flooded with interrupts: a callback is invoked at most ~100 times per scan - * regardless of element count. The closing stretch (the last ~1%) is reported - * more finely so a large document doesn't stall the progress bar on the final - * percent before the scan wraps up. - */ -struct ScanProgress -{ - const char *begin = nullptr; - qsizetype size = 0; - RawJson::ScanProgressCallback callback; - qsizetype step = 1; - qsizetype lastReported = 0; - - /** - * @brief Reports the scanner's absolute offset, unless within @p step bytes - * of the previous report and not yet at the end of the document. - */ - void report(const char *p) - { - if (!callback) { - return; - } - const qsizetype offset = p - begin; - if (offset == lastReported) { - return; // the final element often already sits exactly at the end - } - const qsizetype reportingStep = offset >= size - step ? std::max(1, step / 16) : step; - if (offset - lastReported < reportingStep && offset < size) { - return; - } - lastReported = offset; - callback(offset, size); - } -}; - -inline bool isWhitespace(char c) -{ - return c == ' ' || c == '\t' || c == '\r' || c == '\n'; -} - -const char *skipWhitespace(const char *p, const char *end) -{ - while (p < end && isWhitespace(*p)) { - ++p; - } - return p; -} - -inline bool isHexDigit(char c) -{ - return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'); -} - -inline quint8 hexValue(char c) -{ - if (c >= '0' && c <= '9') { - return c - '0'; - } - if (c >= 'a' && c <= 'f') { - return c - 'a' + 10; - } - return c - 'A' + 10; -} - -/** - * @brief Skips past a JSON string without decoding it, validating escapes. - * @param p In: pointing at the opening quote. Out: pointing past the closing quote. - */ -bool skipString(const char *&p, const char *end) -{ - ++p; // opening quote - for (;;) { - const void *quote = memchr(p, '"', static_cast(end - p)); - if (!quote) { - return false; // unterminated string - } - // Backslash escapes can only appear before the closing quote, so bound - // the scan to the string extent instead of the rest of the document. - const void *backslash = memchr(p, '\\', static_cast(static_cast(quote) - p)); - if (!backslash) { - p = static_cast(quote) + 1; - return true; - } - const char *b = static_cast(backslash); - if (end - b < 2) { - return false; - } - const char escaped = b[1]; - if (escaped == 'u') { - if (end - b < 6) { - return false; - } - quint32 codepoint = 0; - for (int i = 0; i < 4; ++i) { - if (!isHexDigit(b[2 + i])) { - return false; - } - codepoint = codepoint * 16 + hexValue(b[2 + i]); - } - p = b + 6; - if (codepoint >= 0xD800 && codepoint <= 0xDBFF) { - // expect the low-surrogate escape for the second half - if (end - p < 6 || p[0] != '\\' || p[1] != 'u') { - return false; // unpaired high surrogate - } - quint32 low = 0; - for (int i = 0; i < 4; ++i) { - if (!isHexDigit(p[2 + i])) { - return false; - } - low = low * 16 + hexValue(p[2 + i]); - } - if (low < 0xDC00 || low > 0xDFFF) { - return false; - } - p += 6; - } else if (codepoint >= 0xDC00 && codepoint <= 0xDFFF) { - return false; // unpaired low surrogate - } - continue; - } - switch (escaped) { - case '"': - case '\\': - case '/': - case 'b': - case 'f': - case 'n': - case 'r': - case 't': - p = b + 2; - continue; - default: - return false; // invalid escape - } - } -} - -/** - * @brief Decodes a JSON string into @p out, validating it as it goes. - * @param p In: pointing at the opening quote. Out: pointing past the closing quote. - */ -bool decodeString(const char *&p, const char *end, QString &out) -{ - out.clear(); - QByteArray utf8; - auto flush = [&out, &utf8]() { - if (!utf8.isEmpty()) { - out += QString::fromUtf8(utf8); - utf8.clear(); - } - }; - - ++p; // opening quote - while (p < end) { - const char c = *p; - if (c == '\\') { - flush(); - ++p; // escaped character - if (p >= end) { - return false; - } - const char escaped = *p; - if (escaped == 'u') { - ++p; // first hex digit - if (p + 4 > end) { - return false; - } - quint32 codepoint = 0; - for (int i = 0; i < 4; ++i) { - if (!isHexDigit(p[i])) { - return false; - } - codepoint = codepoint * 16 + hexValue(p[i]); - } - p += 4; - if (codepoint >= 0xD800 && codepoint <= 0xDBFF) { - // expect a low-surrogate escape for the second half - if (p + 6 > end || p[0] != '\\' || p[1] != 'u') { - return false; // unpaired high surrogate - } - quint32 low = 0; - for (int i = 0; i < 4; ++i) { - if (!isHexDigit(p[2 + i])) { - return false; - } - low = low * 16 + hexValue(p[2 + i]); - } - if (low < 0xDC00 || low > 0xDFFF) { - return false; - } - out += QChar(codepoint); - out += QChar(low); - p += 6; - } else if (codepoint >= 0xDC00 && codepoint <= 0xDFFF) { - return false; // unpaired low surrogate - } else { - out += QChar(codepoint); - } - continue; - } - switch (escaped) { - case '"': - out += '"'; - break; - case '\\': - out += '\\'; - break; - case '/': - out += '/'; - break; - case 'b': - out += '\b'; - break; - case 'f': - out += '\f'; - break; - case 'n': - out += '\n'; - break; - case 'r': - out += '\r'; - break; - case 't': - out += '\t'; - break; - default: - return false; - } - ++p; - continue; - } - if (c == '"') { - ++p; - flush(); - return true; - } - // Deliberately accept unescaped control characters (e.g. a tab inside - // a set name): QJsonDocument and skipString accept them too, so - // rejecting them here would fail the whole document on a byte that - // Qt is fine with — the very total-failure mode this scanner avoids. - utf8 += c; - ++p; - } - return false; -} - -/** - * @brief Reads a set-metadata field, tolerating null and non-string values. - * - * A set's metadata may carry null or non-string values in otherwise-valid - * payloads ("releaseDate": null, "type": 7). The token itself was already - * structurally validated by skipValue, so a non-string value is accepted and - * leaves @p out at its default (empty) — one bad set must not abort the - * import of every other set in the document. - */ -bool decodeStringMember(const char *&fs, const char *&fe, QString &out) -{ - if (fs >= fe) { - return false; - } - if (*fs != '"') { - return true; - } - return decodeString(fs, fe, out); -} - -bool matchLiteral(const char *&p, const char *end, const char *literal, int length) -{ - if (end - p < length || memcmp(p, literal, static_cast(length)) != 0) { - return false; - } - const char *after = p + length; - if (after < end && (QChar::isLetter(*after) || QChar::isDigit(*after) || *after == '_')) { - return false; - } - p = after; - return true; -} - -bool skipNumber(const char *&p, const char *end) -{ - // JSON number: -?(0|[1-9][0-9]*)(\.[0-9]+)?([eE][+-]?[0-9]+)? - if (p < end && *p == '-') { - ++p; - } - if (p < end && *p == '0') { - ++p; - } else if (p < end && *p >= '1' && *p <= '9') { - ++p; - while (p < end && QChar::isDigit(*p)) { - ++p; - } - } else { - return false; - } - if (p < end && *p == '.') { - ++p; - if (p >= end || !QChar::isDigit(*p)) { - return false; - } - while (p < end && QChar::isDigit(*p)) { - ++p; - } - } - if (p < end && (*p == 'e' || *p == 'E')) { - ++p; - if (p < end && (*p == '+' || *p == '-')) { - ++p; - } - if (p >= end || !QChar::isDigit(*p)) { - return false; - } - while (p < end && QChar::isDigit(*p)) { - ++p; - } - } - return true; -} - -bool skipValue(const char *&p, const char *end, int depth, ScanProgress &scan); -bool skipObject(const char *&p, const char *end, int depth, ScanProgress &scan); -bool skipArray(const char *&p, const char *end, int depth, ScanProgress &scan); - -bool skipPrimitive(const char *&p, const char *end) -{ - if (p >= end) { - return false; - } - const char c = *p; - if (c == '"') { - return skipString(p, end); - } - if (c == 't') { - return matchLiteral(p, end, "true", 4); - } - if (c == 'f') { - return matchLiteral(p, end, "false", 5); - } - if (c == 'n') { - return matchLiteral(p, end, "null", 4); - } - if (c == '-' || (c >= '0' && c <= '9')) { - return skipNumber(p, end); - } - return false; -} - -bool skipObject(const char *&p, const char *end, int depth, ScanProgress &scan) -{ - if (depth <= 0) { - return false; // nest deeper than the cap - } - ++p; // '{' - p = skipWhitespace(p, end); - if (p < end && *p == '}') { - ++p; - return true; - } - for (;;) { - p = skipWhitespace(p, end); - if (p >= end || *p != '"') { - return false; - } - if (!skipString(p, end)) { - return false; - } - p = skipWhitespace(p, end); - if (p >= end || *p != ':') { - return false; - } - ++p; - if (!skipValue(p, end, depth - 1, scan)) { - return false; - } - scan.report(p); - p = skipWhitespace(p, end); - if (p >= end) { - return false; - } - if (*p == ',') { - ++p; - continue; - } - if (*p == '}') { - ++p; - return true; - } - return false; - } -} - -bool skipArray(const char *&p, const char *end, int depth, ScanProgress &scan) -{ - if (depth <= 0) { - return false; // nest deeper than the cap - } - ++p; // '[' - p = skipWhitespace(p, end); - if (p < end && *p == ']') { - ++p; - return true; - } - for (;;) { - if (!skipValue(p, end, depth - 1, scan)) { - return false; - } - scan.report(p); - p = skipWhitespace(p, end); - if (p >= end) { - return false; - } - if (*p == ',') { - ++p; - continue; - } - if (*p == ']') { - ++p; - return true; - } - return false; - } -} - -bool skipValue(const char *&p, const char *end, int depth, ScanProgress &scan) -{ - p = skipWhitespace(p, end); - if (p >= end) { - return false; - } - const char c = *p; - if (c == '{') { - // pass depth through: skipObject consumes the single decrement for this level - return skipObject(p, end, depth, scan); - } - if (c == '[') { - return skipArray(p, end, depth, scan); - } - // a primitive is a leaf, so it never wastes a nesting level - return skipPrimitive(p, end); -} - -/** - * @brief Iterates the members of the object starting at @p p. - * - * For each member invokes @p memberCallback with the key and the byte range of - * its value. Advancing @p p is unaffected by the callback. - */ -template -bool forEachObjectMember(const char *&p, const char *end, int depth, F &&memberCallback, ScanProgress &scan) -{ - if (depth <= 0) { - return false; // nest deeper than the cap - } - ++p; // '{' - p = skipWhitespace(p, end); - if (p < end && *p == '}') { - ++p; - return true; - } - for (;;) { - p = skipWhitespace(p, end); - if (p >= end || *p != '"') { - return false; - } - QString key; - if (!decodeString(p, end, key)) { - return false; - } - p = skipWhitespace(p, end); - if (p >= end || *p != ':') { - return false; - } - ++p; - const char *valueStart = skipWhitespace(p, end); - const char *valueEnd = valueStart; - if (!skipValue(valueEnd, end, depth - 1, scan)) { - return false; - } - if (!memberCallback(key, valueStart, valueEnd)) { - return false; - } - p = valueEnd; - p = skipWhitespace(p, end); - if (p >= end) { - return false; - } - if (*p == ',') { - ++p; - continue; - } - if (*p == '}') { - ++p; - return true; - } - return false; - } -} - -// Counts the direct elements of an array value; returns -1 if the array is malformed. -int countArrayElements(const char *p, const char *end, int depth, ScanProgress &scan) -{ - if (depth <= 0) { - return -1; // nest deeper than the cap - } - ++p; // '[' - p = skipWhitespace(p, end); - int count = 0; - if (p < end && *p == ']') { - return 0; - } - for (;;) { - if (!skipValue(p, end, depth - 1, scan)) { - return -1; - } - scan.report(p); - ++count; - p = skipWhitespace(p, end); - if (p >= end) { - return -1; - } - if (*p == ',') { - ++p; - continue; - } - if (*p == ']') { - return count; - } - return -1; - } -} - -} // namespace - -namespace RawJson -{ - -QList scanSetRanges(const QByteArray &json, ScanError *error, const ScanProgressCallback &progress) -{ - QList ranges; - if (error) { - *error = ScanError{}; - } - - const auto fail = [&](const QString &message) -> QList { - if (error) { - error->message = message; - } - return {}; - }; - - const char *begin = json.constData(); - const char *end = begin + json.size(); - if (begin >= end) { - return fail(QStringLiteral("empty JSON document")); - } - - // Throttle reports to ~100 per scan so a GUI thread unthrottling them never - // drowns under per-card interrupts, whatever the document size. - ScanProgress scan; - scan.begin = begin; - scan.size = end - begin; - scan.step = std::max(1, scan.size / 100); - scan.callback = progress; - - const char *p = skipWhitespace(begin, end); - if (p >= end || *p != '{') { - return fail(QStringLiteral("top-level JSON must be an object")); - } - - bool foundData = false; - bool malformedSetData = false; - - const auto topLevelCallback = [&](const QString &key, const char *valueStart, const char *valueEnd) { - if (key == QStringLiteral("data")) { - foundData = true; - if (valueStart >= valueEnd || *valueStart != '{') { - malformedSetData = true; - return false; - } - const char *setP = valueStart; - const bool ok = forEachObjectMember( - setP, valueEnd, kMaxNestingDepth - 1, - [&](const QString &setCode, const char *setStart, const char *setEnd) { - if (setStart >= setEnd || *setStart != '{') { - malformedSetData = true; - return false; - } - SetRange range; - range.dataRange.start = setStart - begin; - range.dataRange.length = setEnd - setStart; - range.code = setCode; - - const char *memberP = setStart; - const bool metaOk = forEachObjectMember( - memberP, setEnd, kMaxNestingDepth - 2, - [&](const QString &field, const char *fs, const char *fe) { - if (field == QStringLiteral("code")) { - return decodeStringMember(fs, fe, range.code); - } - if (field == QStringLiteral("name")) { - return decodeStringMember(fs, fe, range.name); - } - if (field == QStringLiteral("type")) { - return decodeStringMember(fs, fe, range.type); - } - if (field == QStringLiteral("releaseDate")) { - return decodeStringMember(fs, fe, range.releaseDate); - } - if (field == QStringLiteral("cards")) { - if (fs >= fe) { - return false; - } - if (*fs != '[') { - // e.g. "cards": null — treat as an empty array, - // matching Qt's tolerance. - return true; - } - range.dataRange.cardCount = countArrayElements(fs, fe, kMaxNestingDepth - 2, scan); - return range.dataRange.cardCount >= 0; - } - return true; - }, - scan); - if (!metaOk) { - malformedSetData = true; - return false; - } - ranges.append(range); - return true; - }, - scan); - if (!ok) { - malformedSetData = true; - return false; - } - } - return true; - }; - - if (!forEachObjectMember(p, end, kMaxNestingDepth, topLevelCallback, scan)) { - return fail(malformedSetData ? QStringLiteral("malformed set data") : QStringLiteral("malformed JSON")); - } - p = skipWhitespace(p, end); - if (p != end) { - return fail(QStringLiteral("trailing content after top-level JSON object")); - } - if (!foundData) { - return fail(QStringLiteral("missing \"data\" object")); - } - if (ranges.isEmpty()) { - return fail(QStringLiteral("no sets found in \"data\"")); - } - scan.report(end); - return ranges; -} - -} // namespace RawJson \ No newline at end of file diff --git a/oracle/src/raw_json_scanner.h b/oracle/src/raw_json_scanner.h deleted file mode 100644 index 04fcc52e4..000000000 --- a/oracle/src/raw_json_scanner.h +++ /dev/null @@ -1,89 +0,0 @@ -#ifndef RAW_JSON_SCANNER_H -#define RAW_JSON_SCANNER_H - -#include -#include -#include -#include - -namespace RawJson -{ - -/** - * @brief The byte extent of a set's object inside the scanned document, plus - * the size of its cards array. This is the slice SetToDownload needs for lazy - * per-set parsing; the metadata strings live in SetRange alongside it. - */ -struct SetDataRange -{ - /** @brief Byte offset of the set's object within the scanned buffer. */ - qsizetype start = -1; - /** @brief Byte length of the set's object, including the surrounding braces. */ - qsizetype length = 0; - /** @brief Number of entries in the set's "cards" array. */ - int cardCount = 0; -}; - -struct SetRange -{ - /** @brief The byte slice of this set within the document. */ - SetDataRange dataRange; - QString code; - QString name; - QString type; - QString releaseDate; -}; - -struct ScanError -{ - bool isError() const - { - return !message.isEmpty(); - } - QString message; -}; - -/** - * @brief Optional progress callback receiving @c (bytesRead, totalBytes) while - * the document is walked. Invoked from the scanning thread; the caller decides - * how the throttled offsets are relayed to a GUI event loop. - */ -using ScanProgressCallback = std::function; - -/** - * @brief Scans a full MTGJSON document without materializing the JSON tree. - * - * Splits the top-level "data" object into per-set byte ranges and reads each - * set's metadata directly from the raw bytes. The Oracle importer can then - * parse one set at a time during import, keeping peak memory far below a single - * QJsonDocument::fromJson() over the whole file. - * - * The whole document is structurally validated while scanning (strings, - * escapes, braces, and a trailing-content check) and nesting depth is capped at - * 1024 to match QJsonDocument, so pathologically deep documents fail shallowly - * instead of exhausting the stack. Verdicts agree with QJsonDocument::fromJson - * on structurally malformed input; unlike Qt, string metadata fields - * ("name", "type", "releaseDate", "code") tolerate null / non-string values by - * defaulting to empty rather than rejecting the whole document, so one broken - * set cannot abort the import of the rest. - * - * Following QJsonDocument::fromJson's convention, the parsed ranges are - * returned by value and any failure is reported through the @p error out - * parameter. - * - * @param json The raw MTGJSON document bytes. - * @param error Out parameter. Set to an error ScanError when the document - * cannot be parsed, otherwise left empty. Passing a null - * pointer disables error reporting. - * @param progress Optional progress callback. When non-empty it is invoked as - * the scanner advances through the document, throttled to a - * tiny fraction of the total size. - * @return The detected per-set ranges, or an empty list on failure. - */ -QList scanSetRanges(const QByteArray &json, - ScanError *error = nullptr, - const ScanProgressCallback &progress = ScanProgressCallback()); - -} // namespace RawJson - -#endif // RAW_JSON_SCANNER_H \ No newline at end of file diff --git a/servatrice/CMakeLists.txt b/servatrice/CMakeLists.txt index 21f71a908..aba63800c 100644 --- a/servatrice/CMakeLists.txt +++ b/servatrice/CMakeLists.txt @@ -6,9 +6,7 @@ project(Servatrice VERSION "${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}.${ set(servatrice_SOURCES src/email_parser.cpp - src/event_loop_watchdog.cpp src/main.cpp - src/metrics_registry.cpp src/servatrice.cpp src/servatrice_connection_pool.cpp src/servatrice_database_interface.cpp @@ -97,8 +95,6 @@ set(DESKTOPDIR # Build servatrice binary and link it add_executable(servatrice MACOSX_BUNDLE ${servatrice_MOC_SRCS} ${servatrice_RESOURCES_RCC} ${servatrice_SOURCES}) -target_precompile_headers(servatrice PRIVATE "${CMAKE_SOURCE_DIR}/cmake/pch/qtcore_pch.h") - if(CMAKE_HOST_SYSTEM MATCHES "FreeBSD") target_link_libraries( servatrice libcockatrice_deck_list libcockatrice_network_server_remote Threads::Threads ${SERVATRICE_QT_MODULES} @@ -184,19 +180,10 @@ if(WIN32) set(qtconf_dest_dir .) install( - DIRECTORY "$/" + DIRECTORY "${CMAKE_BINARY_DIR}/${PROJECT_NAME}/${CMAKE_BUILD_TYPE}/" DESTINATION ./ FILES_MATCHING PATTERN "*.dll" - PATTERN "*.pdb" EXCLUDE - PATTERN "*.dir*" EXCLUDE - PATTERN "*_autogen*" EXCLUDE - PATTERN "*.tlog*" EXCLUDE - PATTERN "CMakeFiles*" EXCLUDE - PATTERN "x64*" EXCLUDE - PATTERN ".qt*" EXCLUDE - PATTERN ".qsb*" EXCLUDE - PATTERN ".lupdate*" EXCLUDE ) # Qt plugins: platforms, sqldrivers, tls (Qt6) diff --git a/servatrice/migrations/servatrice_0035_to_0036.sql b/servatrice/migrations/servatrice_0035_to_0036.sql deleted file mode 100644 index 9727eeba5..000000000 --- a/servatrice/migrations/servatrice_0035_to_0036.sql +++ /dev/null @@ -1,53 +0,0 @@ --- Servatrice db migration from version 35 to version 36 - -CREATE TABLE IF NOT EXISTS `cockatrice_reports` ( - `id` int(11) unsigned NOT NULL AUTO_INCREMENT, - `reporter_id` int(7) unsigned NULL, - `reporter_name` varchar(35) NOT NULL, - `reported_user_id` int(7) unsigned NULL, - `reported_user_name` varchar(35) NOT NULL, - `game_id` int(7) unsigned NULL, - `room_id` int(7) unsigned NULL, - `category` varchar(255) NOT NULL, - `description` text NOT NULL, - `chat_log` mediumtext NULL, - `created_at` datetime NOT NULL, - `resolution_time` datetime NULL, - `status` enum('open','assigned','resolved','dismissed') NOT NULL DEFAULT 'open', - `assigned_to` int(7) unsigned NULL, - `resolved_by` int(7) unsigned NULL, - `resolution_note` text, - `notified` tinyint(1) NOT NULL DEFAULT 0, - PRIMARY KEY (`id`), - INDEX `idx_status` (`status`), - INDEX `idx_created` (`created_at`), - INDEX `idx_reporter_id_created` (`reporter_id`, `created_at`), - INDEX `idx_reported_user_name` (`reported_user_name`), - INDEX `idx_status_created` (`status`, `created_at`), - FOREIGN KEY (`reporter_id`) REFERENCES `cockatrice_users`(`id`) ON DELETE SET NULL ON UPDATE CASCADE, - FOREIGN KEY (`reported_user_id`) REFERENCES `cockatrice_users`(`id`) ON DELETE SET NULL ON UPDATE CASCADE, - FOREIGN KEY (`assigned_to`) REFERENCES `cockatrice_users`(`id`) ON DELETE SET NULL ON UPDATE CASCADE, - FOREIGN KEY (`resolved_by`) REFERENCES `cockatrice_users`(`id`) ON DELETE SET NULL ON UPDATE CASCADE -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci; - -CREATE TABLE IF NOT EXISTS `cockatrice_report_comments` ( - `id` int(11) unsigned NOT NULL AUTO_INCREMENT, - `report_id` int(11) unsigned NOT NULL, - `author_name` varchar(35) NOT NULL, - `author_id` int(7) unsigned NULL, - `comment_text` text NOT NULL, - `created_at` datetime NOT NULL, - `is_moderator` tinyint(1) NOT NULL DEFAULT 0, - `notified` tinyint(1) NOT NULL DEFAULT 0, - PRIMARY KEY (`id`), - INDEX `idx_report_id` (`report_id`), - INDEX `idx_notified` (`notified`), - FOREIGN KEY (`report_id`) REFERENCES `cockatrice_reports`(`id`) ON DELETE CASCADE ON UPDATE CASCADE, - FOREIGN KEY (`author_id`) REFERENCES `cockatrice_users`(`id`) ON DELETE SET NULL ON UPDATE CASCADE -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci; - -ALTER TABLE `cockatrice_users` - ADD COLUMN `force_password_change` tinyint(1) NOT NULL DEFAULT 0 - AFTER `passwordLastChangedDate`; - -UPDATE cockatrice_schema_version SET version=36 WHERE version=35; diff --git a/servatrice/migrations/servatrice_0036_to_0037.sql b/servatrice/migrations/servatrice_0036_to_0037.sql deleted file mode 100644 index 576ab53b4..000000000 --- a/servatrice/migrations/servatrice_0036_to_0037.sql +++ /dev/null @@ -1,71 +0,0 @@ --- Servatrice db migration from version 36 to version 37 - --- Deck sharing (temporary share links + permanent public decks). --- --- This feature was developed behind several intermediate migrations that have --- never shipped, so they are folded into this single 36 -> 37 migration: --- temporary share links, permanent public-deck visibility, preview metadata, --- and per-deck tags. - --- 1. Temporary deck shares: a named bundle of decks that can be fetched by --- anyone who knows the (unguessable) token, until the share expires. -CREATE TABLE IF NOT EXISTS `cockatrice_deck_share` ( - `id` int(7) unsigned zerofill NOT NULL auto_increment, - `token` varchar(64) COLLATE utf8mb4_bin NOT NULL, - `name` varchar(64) NOT NULL, - `created_by` int(7) unsigned NULL, - `created_at` datetime NOT NULL, - `expires_at` datetime NOT NULL, - PRIMARY KEY (`id`), - UNIQUE KEY `token` (`token`), - KEY `expires_at` (`expires_at`), - FOREIGN KEY(`created_by`) REFERENCES `cockatrice_users`(`id`) ON DELETE SET NULL ON UPDATE CASCADE -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci; - --- Individual decks inside a share bundle. Content is materialized at share --- time so expiring/deleting a share can cascade cleanly. The metadata columns --- are nullable because Qt's MySQL driver binds empty QStrings as NULL when --- using prepared statements. -CREATE TABLE IF NOT EXISTS `cockatrice_deck_share_item` ( - `id` int(7) unsigned zerofill NOT NULL auto_increment, - `share_id` int(7) unsigned zerofill NOT NULL, - `name` varchar(50) NOT NULL, - `tags` text NULL, - `banner_card` varchar(255) NULL, - `game_format` varchar(50) NULL, - `color_identity` varchar(5) NULL, - `content` text NOT NULL, - `position` int(7) NOT NULL, - PRIMARY KEY (`id`), - KEY `share_id` (`share_id`), - FOREIGN KEY(`share_id`) REFERENCES `cockatrice_deck_share`(`id`) ON DELETE CASCADE ON UPDATE CASCADE -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci; - --- 2. Permanent deck sharing: add public visibility flags to the deck storage --- tables. A deck is visible to other users if it is marked public, or if any --- ancestor folder is marked public (inherited). Existing decks default to --- private, so the upgrade does not expose any data. -ALTER TABLE `cockatrice_decklist_files` - ADD COLUMN `is_public` tinyint(1) NOT NULL DEFAULT 0 AFTER `content`; - -ALTER TABLE `cockatrice_decklist_folders` - ADD COLUMN `is_public` tinyint(1) NOT NULL DEFAULT 0 AFTER `name`; - --- 3. Per-deck preview metadata so clients can render another user's public --- decks (e.g. in a visual deck storage grid) without downloading each deck --- list. The metadata is derived by the server from the deck content (the color --- identity is supplied by the uploading client); decks uploaded before this --- migration have empty values until they are re-uploaded. -ALTER TABLE `cockatrice_decklist_files` - ADD COLUMN `banner_card_name` varchar(255) NULL AFTER `is_public`, - ADD COLUMN `banner_card_provider` varchar(32) NULL AFTER `banner_card_name`, - ADD COLUMN `color_identity` varchar(5) NULL AFTER `banner_card_provider`; - --- 4. Per-deck tags for public decks. The server renders the deck's own tags --- into a JSON array, so another user's public decks can filter by tag without --- downloading each deck list. Decks uploaded before this migration have NULL --- tags until they are re-uploaded. -ALTER TABLE `cockatrice_decklist_files` - ADD COLUMN `tags` text NULL AFTER `color_identity`; - -UPDATE cockatrice_schema_version SET version=37 WHERE version=36; diff --git a/servatrice/servatrice.ini.example b/servatrice/servatrice.ini.example index ccd4f3c3f..fac743c39 100644 --- a/servatrice/servatrice.ini.example +++ b/servatrice/servatrice.ini.example @@ -79,10 +79,8 @@ requiredfeatures="" ; You can define custom warnings that users are sent when the moderation staff uses the right client warn user ; menu option. This list is comma seperated that each item will appear in the drop down list for staff members -; to choose from. Each entry may optionally carry a recommended starting intervention level (see the moderator -; guide) by appending "|" and the level number. Entries without an explicit level default to intervention -; level 1. Example: "Flaming,Foul Language" -officialwarnings="Abusive Language|1,Calling Out User|1,Causing Drama|1,Cheating|2,Disrespecting Staff|1,Disrupting a Draft|1,Inappropriate Avatar|3,Inappropriate Game Name|1,Kicking Without Valid Reason|1,Spamming|1,Targeted Harassment|2" +; to choose from. Example: "Flaming,Foul Language" +officialwarnings="Flaming,Spamming,Causing Drama,Abusive Language" ; Maximum time in seconds a player can stay connected but idle. Default is 3600 (0 = disabled) ; Clients will be notified at the 90% time period of pending disconnection if they do not take action. @@ -375,26 +373,6 @@ command_counting_interval=10 ; Maximum number of game commands in an interval before new commands gets dropped; default is 20 max_command_count_per_interval=20 -[reporting] -; Maximum number of user reports a single user can file per day; default is 10; set to 0 to disable the limit -max_reports_per_day=10 - -; Maximum number of report comments a single user can post per hour; default is 30; set to 0 to disable the limit -max_comments_per_hour=30 - -[metrics] -; Command containers that take longer than this many milliseconds are logged -; as slow commands. Set to 0 to disable the log line. A latency spike produces -; one warning per slow container with no rate limiting of its own -- a bad -; patch can briefly flood the log, which is how you notice it. -slow_command_ms=500 - -; Each socket pool thread runs a watchdog heartbeat. If a heartbeat arrives -; this many milliseconds late, the stall is logged and exposed as -; servatrice_eventloop_* metrics in the Developer tab. Set to 0 to disable -; the watchdogs. -stall_warn_ms=2000 - [logging] ; Admin/Moderators can query the stored logs for information when looking up reports by various players. This ; option can allow or disallow them from doing so. @@ -452,24 +430,3 @@ ssl_cert=ssl_cert.pem ; Filename of the private key for the server-to-server certificate ssl_key=ssl_key.pem - - -[deck_share] - -; How many days a created deck share link remains valid before it expires. -; Default: 7 -expiry_days=7 - -; How often (in minutes) the server checks for and removes expired deck shares. -; A value of 0 disables the automatic cleanup. -; Default: 60 -cleanup_interval=60 - -; Maximum number of decks a single share link can contain. -; Default: 50 -max_decks_per_share=50 - -; Maximum number of share links a single user may create per day. -; A value of 0 disables the limit. -; Default: 50 -max_shares_per_day=50 diff --git a/servatrice/servatrice.sql b/servatrice/servatrice.sql index 4fbc1d8bd..7f530063c 100644 --- a/servatrice/servatrice.sql +++ b/servatrice/servatrice.sql @@ -20,14 +20,11 @@ CREATE TABLE IF NOT EXISTS `cockatrice_schema_version` ( PRIMARY KEY (`version`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci; -INSERT INTO cockatrice_schema_version VALUES(37); +INSERT INTO cockatrice_schema_version VALUES(35); -- users and user data tables CREATE TABLE IF NOT EXISTS `cockatrice_users` ( `id` int(7) unsigned zerofill NOT NULL auto_increment, - -- Bitfield of staff levels: 1 = admin (implies moderator), 2 = moderator, - -- 4 = judge, 8 = developer. Operators set these by hand with - -- "UPDATE cockatrice_users SET admin = ...". `admin` tinyint(1) NOT NULL, `name` varchar(35) NOT NULL, `realname` varchar(255) NOT NULL, @@ -44,7 +41,6 @@ CREATE TABLE IF NOT EXISTS `cockatrice_users` ( `privlevelStartDate` datetime NOT NULL, `privlevelEndDate` datetime NOT NULL, `passwordLastChangedDate` datetime NOT NULL DEFAULT '0000-00-00 00:00:00', - `force_password_change` tinyint(1) NOT NULL DEFAULT 0, `leftPawnColorOverride` varchar(255), `rightPawnColorOverride` varchar(255), `card_art_params` TEXT DEFAULT NULL, @@ -66,56 +62,16 @@ CREATE TABLE IF NOT EXISTS `cockatrice_decklist_files` ( `name` varchar(50) NOT NULL, `upload_time` datetime NOT NULL, `content` text NOT NULL, - `is_public` tinyint(1) NOT NULL DEFAULT 0, - `banner_card_name` varchar(255) NULL, - `banner_card_provider` varchar(32) NULL, - `color_identity` varchar(5) NULL, - `tags` text NULL, PRIMARY KEY (`id`), KEY `FolderPlusUser` (`id_folder`,`id_user`), FOREIGN KEY(`id_user`) REFERENCES `cockatrice_users`(`id`) ON DELETE CASCADE ON UPDATE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci; --- Temporary deck shares: a named bundle of decks that can be fetched by --- anyone who knows the (unguessable) token, until the share expires. -CREATE TABLE IF NOT EXISTS `cockatrice_deck_share` ( - `id` int(7) unsigned zerofill NOT NULL auto_increment, - `token` varchar(64) COLLATE utf8mb4_bin NOT NULL, - `name` varchar(64) NOT NULL, - `created_by` int(7) unsigned NULL, - `created_at` datetime NOT NULL, - `expires_at` datetime NOT NULL, - PRIMARY KEY (`id`), - UNIQUE KEY `token` (`token`), - KEY `expires_at` (`expires_at`), - FOREIGN KEY(`created_by`) REFERENCES `cockatrice_users`(`id`) ON DELETE SET NULL ON UPDATE CASCADE -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci; - --- Individual decks inside a share bundle. Content is materialized at share --- time so expiring/deleting a share can cascade cleanly. The metadata columns --- are nullable because Qt's MySQL driver binds empty QStrings as NULL when --- using prepared statements. -CREATE TABLE IF NOT EXISTS `cockatrice_deck_share_item` ( - `id` int(7) unsigned zerofill NOT NULL auto_increment, - `share_id` int(7) unsigned zerofill NOT NULL, - `name` varchar(50) NOT NULL, - `tags` text NULL, - `banner_card` varchar(255) NULL, - `game_format` varchar(50) NULL, - `color_identity` varchar(5) NULL, - `content` text NOT NULL, - `position` int(7) NOT NULL, - PRIMARY KEY (`id`), - KEY `share_id` (`share_id`), - FOREIGN KEY(`share_id`) REFERENCES `cockatrice_deck_share`(`id`) ON DELETE CASCADE ON UPDATE CASCADE -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci; - CREATE TABLE IF NOT EXISTS `cockatrice_decklist_folders` ( `id` int(7) unsigned zerofill NOT NULL auto_increment, `id_parent` int(7) unsigned zerofill NOT NULL, `id_user` int(7) unsigned NULL, `name` varchar(30) NOT NULL, - `is_public` tinyint(1) NOT NULL DEFAULT 0, PRIMARY KEY (`id`), KEY `ParentPlusUser` (`id_parent`,`id_user`), FOREIGN KEY(`id_user`) REFERENCES `cockatrice_users`(`id`) ON DELETE CASCADE ON UPDATE CASCADE @@ -277,52 +233,6 @@ CREATE TABLE IF NOT EXISTS `cockatrice_warnings` ( INDEX `idx_user_name` (`user_name`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci; -CREATE TABLE IF NOT EXISTS `cockatrice_reports` ( - `id` int(11) unsigned NOT NULL AUTO_INCREMENT, - `reporter_id` int(7) unsigned NULL, - `reporter_name` varchar(35) NOT NULL, - `reported_user_id` int(7) unsigned NULL, - `reported_user_name` varchar(35) NOT NULL, - `game_id` int(7) unsigned NULL, - `room_id` int(7) unsigned NULL, - `category` varchar(255) NOT NULL, - `description` text NOT NULL, - `chat_log` mediumtext NULL, - `created_at` datetime NOT NULL, - `resolution_time` datetime NULL, - `status` enum('open','assigned','resolved','dismissed') NOT NULL DEFAULT 'open', - `assigned_to` int(7) unsigned NULL, - `resolved_by` int(7) unsigned NULL, - `resolution_note` text, - `notified` tinyint(1) NOT NULL DEFAULT 0, - PRIMARY KEY (`id`), - INDEX `idx_status` (`status`), - INDEX `idx_created` (`created_at`), - INDEX `idx_reporter_id_created` (`reporter_id`, `created_at`), - INDEX `idx_reported_user_name` (`reported_user_name`), - INDEX `idx_status_created` (`status`, `created_at`), - FOREIGN KEY (`reporter_id`) REFERENCES `cockatrice_users`(`id`) ON DELETE SET NULL ON UPDATE CASCADE, - FOREIGN KEY (`reported_user_id`) REFERENCES `cockatrice_users`(`id`) ON DELETE SET NULL ON UPDATE CASCADE, - FOREIGN KEY (`assigned_to`) REFERENCES `cockatrice_users`(`id`) ON DELETE SET NULL ON UPDATE CASCADE, - FOREIGN KEY (`resolved_by`) REFERENCES `cockatrice_users`(`id`) ON DELETE SET NULL ON UPDATE CASCADE -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci; - -CREATE TABLE IF NOT EXISTS `cockatrice_report_comments` ( - `id` int(11) unsigned NOT NULL AUTO_INCREMENT, - `report_id` int(11) unsigned NOT NULL, - `author_name` varchar(35) NOT NULL, - `author_id` int(7) unsigned NULL, - `comment_text` text NOT NULL, - `created_at` datetime NOT NULL, - `is_moderator` tinyint(1) NOT NULL DEFAULT 0, - `notified` tinyint(1) NOT NULL DEFAULT 0, - PRIMARY KEY (`id`), - INDEX `idx_report_id` (`report_id`), - INDEX `idx_notified` (`notified`), - FOREIGN KEY (`report_id`) REFERENCES `cockatrice_reports`(`id`) ON DELETE CASCADE ON UPDATE CASCADE, - FOREIGN KEY (`author_id`) REFERENCES `cockatrice_users`(`id`) ON DELETE SET NULL ON UPDATE CASCADE -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci; - CREATE TABLE IF NOT EXISTS `cockatrice_log` ( `log_time` datetime NOT NULL, `sender_id` int(7) unsigned NULL, diff --git a/servatrice/src/deck_tag_serialization.h b/servatrice/src/deck_tag_serialization.h deleted file mode 100644 index 162b0604d..000000000 --- a/servatrice/src/deck_tag_serialization.h +++ /dev/null @@ -1,39 +0,0 @@ -#ifndef DECK_TAG_SERIALIZATION_H -#define DECK_TAG_SERIALIZATION_H - -#include -#include -#include -#include -#include - -/** - * @brief Encodes deck tags as a compact JSON array for storage in a text column. - * - * Deck tags are stored as JSON (rather than a delimited string) so tag names may - * contain any character, and decoded uniformly everywhere they are read. - */ -inline QString serializeDeckTags(const QStringList &tags) -{ - QJsonArray array; - for (const QString &tag : tags) { - array.append(tag); - } - return QString::fromUtf8(QJsonDocument(array).toJson(QJsonDocument::Compact)); -} - -/** @brief Decodes deck tags previously written by serializeDeckTags. */ -inline QStringList deserializeDeckTags(const QString &serialized) -{ - QStringList tags; - if (serialized.isEmpty()) { - return tags; - } - const QJsonArray array = QJsonDocument::fromJson(serialized.toUtf8()).array(); - for (const QJsonValue &tag : array) { - tags.append(tag.toString()); - } - return tags; -} - -#endif // DECK_TAG_SERIALIZATION_H diff --git a/servatrice/src/event_loop_watchdog.cpp b/servatrice/src/event_loop_watchdog.cpp deleted file mode 100644 index e50bd4201..000000000 --- a/servatrice/src/event_loop_watchdog.cpp +++ /dev/null @@ -1,34 +0,0 @@ -/** - * @file event_loop_watchdog.cpp - * @ingroup Servatrice - */ - -#include "event_loop_watchdog.h" - -#include "servatrice.h" - -#include - -EventLoopWatchdog::EventLoopWatchdog(Servatrice *_servatrice, QString _threadName) - : QObject(nullptr), servatrice(_servatrice), threadName(std::move(_threadName)) -{ -} - -void EventLoopWatchdog::start() -{ - heartbeatTimer = new QTimer(this); - sinceLastTick.start(); - connect(heartbeatTimer, &QTimer::timeout, this, &EventLoopWatchdog::checkHeartbeat); - heartbeatTimer->start(HeartbeatIntervalMs); -} - -void EventLoopWatchdog::checkHeartbeat() -{ - const qint64 elapsedMs = sinceLastTick.restart(); - const qint64 overshootMs = qMax(0, elapsedMs - HeartbeatIntervalMs); - if (overshootMs < servatrice->getMetricsStallWarnMs()) { - return; - } - - servatrice->observeEventLoopStall(threadName, overshootMs); -} diff --git a/servatrice/src/event_loop_watchdog.h b/servatrice/src/event_loop_watchdog.h deleted file mode 100644 index b9061ff97..000000000 --- a/servatrice/src/event_loop_watchdog.h +++ /dev/null @@ -1,50 +0,0 @@ -/** - * @file event_loop_watchdog.h - * @ingroup Servatrice - */ - -#ifndef EVENT_LOOP_WATCHDOG_H -#define EVENT_LOOP_WATCHDOG_H - -#include -#include -#include - -class Servatrice; -class QTimer; - -/** - * @brief Detects blocked or overloaded worker event loops. - * - * One instance lives in each socket pool thread. A heartbeat timer tick that - * arrives late means the loop spent that time elsewhere: busy work, a queued - * slot, or a hard wedge. Overshoots past the configured threshold bump - * lock-free counters on the metrics registry and log one warning per stall, - * so a stuck pool thread becomes visible instead of silent lag. - */ -class EventLoopWatchdog : public QObject -{ - Q_OBJECT -public: - /// How often the heartbeat expects to fire. Small enough to catch short stalls. - static constexpr int HeartbeatIntervalMs = 500; - - EventLoopWatchdog(Servatrice *_servatrice, QString _threadName); - - /** - * Starts the heartbeat timer. Must be invoked queued after the instance - * was moved to its target thread so the timer lives there too. - */ - void start(); - -private slots: - void checkHeartbeat(); - -private: - Servatrice *servatrice; - QString threadName; - QElapsedTimer sinceLastTick; - QTimer *heartbeatTimer = nullptr; -}; - -#endif diff --git a/servatrice/src/main.cpp b/servatrice/src/main.cpp index 13bf95a82..9e7fe38d9 100644 --- a/servatrice/src/main.cpp +++ b/servatrice/src/main.cpp @@ -33,7 +33,6 @@ #include #include #include -#include #include RNG_Abstract *rng; @@ -170,7 +169,7 @@ int main(int argc, char *argv[]) signalhandler = new SignalHandler(); - rng = new RNG_SFMT(CryptoUtil::randomUInt64()); + rng = new RNG_SFMT; std::cerr << "Servatrice " << VERSION_STRING << " starting." << std::endl; std::cerr << "-------------------------" << std::endl; diff --git a/servatrice/src/metrics_registry.cpp b/servatrice/src/metrics_registry.cpp deleted file mode 100644 index 99ce390f1..000000000 --- a/servatrice/src/metrics_registry.cpp +++ /dev/null @@ -1,70 +0,0 @@ -#include "metrics_registry.h" - -#include - -void MetricsRegistry::observeCommand(int typeId, qint64 elapsedMs) -{ - if (typeId < 0 || typeId >= MaxTypes) { - typeId = MaxTypes - 1; // overflow slot keeps misrouted ids visible - } - if (elapsedMs < 0) { - elapsedMs = 0; - } - - TypeStats &stats = slotFor(typeId); - stats.count.fetch_add(1, std::memory_order_relaxed); - stats.totalMs.fetch_add(elapsedMs, std::memory_order_relaxed); - totalCommandsCounter.fetch_add(1, std::memory_order_relaxed); - totalTimeCounter.fetch_add(elapsedMs, std::memory_order_relaxed); -} - -void MetricsRegistry::observeGameStartDurationMs(qint64 elapsedMs) -{ - if (elapsedMs < 0) { - elapsedMs = 0; - } - - gameStartStats.count.fetch_add(1, std::memory_order_relaxed); - gameStartStats.totalMs.fetch_add(elapsedMs, std::memory_order_relaxed); -} - -MetricsRegistry::TypeStats &MetricsRegistry::slotFor(int typeId) -{ - return typeSlots[static_cast(typeId)]; -} - -const MetricsRegistry::TypeStats &MetricsRegistry::slotFor(int typeId) const -{ - return typeSlots[static_cast(typeId)]; -} - -int MetricsRegistry::activeTypeCount() const -{ - int active = 0; - for (int type = 0; type < MaxTypes; ++type) { - if (slotFor(type).count.load(std::memory_order_relaxed) > 0) { - ++active; - } - } - return active; -} - -QList MetricsRegistry::collectActiveStats() const -{ - QList result; - for (int type = 0; type < MaxTypes; ++type) { - const TypeStats &stats = slotFor(type); - const qint64 count = stats.count.load(std::memory_order_relaxed); - if (count == 0) { - continue; - } - result.append({type, count, stats.totalMs.load(std::memory_order_relaxed)}); - } - return result; -} - -MetricsRegistry::GameStartSnapshot MetricsRegistry::getGameStartSnapshot() const -{ - return {gameStartStats.count.load(std::memory_order_relaxed), - gameStartStats.totalMs.load(std::memory_order_relaxed)}; -} \ No newline at end of file diff --git a/servatrice/src/metrics_registry.h b/servatrice/src/metrics_registry.h deleted file mode 100644 index 4df1057c2..000000000 --- a/servatrice/src/metrics_registry.h +++ /dev/null @@ -1,120 +0,0 @@ -/** - * @file metrics_registry.h - * @ingroup Servatrice - */ - -#ifndef METRICS_REGISTRY_H -#define METRICS_REGISTRY_H - -#include -#include -#include -#include - -/** - * @brief Lock-free accumulation of command processing statistics. - * - * observeCommand() is called once per processed command from whichever socket - * thread handled it. It uses relaxed atomic adds on preallocated storage only, - * so it introduces no locks, allocations, or shared cache-line ping-pong - * beyond the unavoidable counter updates. - * - * Reading happens rarely (metrics scraping), accepts momentary tears between - * related counters, and therefore also needs no synchronization. - * - * Only counts and totals are retained. An earlier Prometheus-style cumulative - * histogram (per-type, time-bucketed) was cut because nothing in the server - * ever wrote it out; it belongs to the future /metrics exporter that needs it. - */ -class MetricsRegistry -{ -public: - /** - * Extension numbers are only unique per command kind, so recorded ids - * combine the kind index with the protobuf extension number. - * - * The stride is only as wide as it needs to be: 1280 is the first round - * number above the largest extension actually in use (ModeratorCommand = - * 1206) and keeps the preallocated TypeStats array small. Bump it if a new - * command exceeds it. - */ - static constexpr int KindStride = 1280; - - static constexpr int NumKinds = 6; - - static constexpr const char *KindNames[NumKinds] = {"session", "room", "game", "moderator", "admin", "developer"}; - - /// Upper bound on distinct command type ids (see typeIdFor). - static constexpr int MaxTypes = NumKinds * KindStride; - - /// Guard against typeIdFor() overflowing into the neighbouring kind's slots. - static_assert(KindStride > 1206, "KindStride must exceed the highest command extension number in use"); - - static int typeIdFor(int kindIndex, int extensionNumber) - { - return kindIndex * KindStride + extensionNumber; - } - - void observeCommand(int typeId, qint64 elapsedMs); - - /** - * Records how long one game start took to bring every player's zones - * online. Kept separate from command timings because it is triggered by - * the server itself and can dwarf any single command when decks are huge. - */ - void observeGameStartDurationMs(qint64 elapsedMs); - - /// Total number of observed commands across all types. - qint64 totalCommands() const - { - return totalCommandsCounter.load(std::memory_order_relaxed); - } - - /// Cumulative processing milliseconds across all types. - qint64 totalTimeMs() const - { - return totalTimeCounter.load(std::memory_order_relaxed); - } - - /// Number of distinct type slots that have seen at least one sample. - int activeTypeCount() const; - - struct ActiveTypeStats - { - int typeId; - qint64 count; - qint64 totalMs; - }; - - /** - * Returns stats for every type slot that has seen at least one sample. - * Callers resolve the numeric type id to a human-readable label via - * typeIdFor()/KindNames as needed. - */ - QList collectActiveStats() const; - - struct GameStartSnapshot - { - qint64 count; - qint64 totalMs; - }; - - GameStartSnapshot getGameStartSnapshot() const; - -private: - struct TypeStats - { - std::atomic count{0}; - std::atomic totalMs{0}; - }; - - TypeStats &slotFor(int typeId); - const TypeStats &slotFor(int typeId) const; - - std::array typeSlots{}; - TypeStats gameStartStats{}; - std::atomic totalCommandsCounter{0}; - std::atomic totalTimeCounter{0}; -}; - -#endif \ No newline at end of file diff --git a/servatrice/src/servatrice.cpp b/servatrice/src/servatrice.cpp index 3dd7ab510..aa50e068a 100644 --- a/servatrice/src/servatrice.cpp +++ b/servatrice/src/servatrice.cpp @@ -20,7 +20,6 @@ #include "servatrice.h" #include "email_parser.h" -#include "event_loop_watchdog.h" #include "isl_interface.h" #include "main.h" #include "servatrice_connection_pool.h" @@ -39,7 +38,6 @@ #include #include #include -#include #include #include #include @@ -65,7 +63,6 @@ Servatrice_GameServer::Servatrice_GameServer(Servatrice *_server, server->addDatabaseInterface(newThread, newDatabaseInterface); newThread->start(); - server->watchWorkerThread(newThread); QMetaObject::invokeMethod(newDatabaseInterface, "initDatabase", Qt::BlockingQueuedConnection, Q_ARG(QSqlDatabase, _sqlDatabase)); @@ -89,6 +86,7 @@ void Servatrice_GameServer::incomingConnection(qintptr socketDescriptor) Servatrice_ConnectionPool *pool = findLeastUsedConnectionPool(); auto ssi = new TcpServerSocketInterface(server, pool->getDatabaseInterface()); + connect(ssi, SIGNAL(incTxBytes(qint64)), this, SLOT(incTxBytes(qint64))); ssi->moveToThread(pool->thread()); pool->addClient(); connect(ssi, SIGNAL(destroyed()), pool, SLOT(removeClient())); @@ -133,7 +131,6 @@ Servatrice_WebsocketGameServer::Servatrice_WebsocketGameServer(Servatrice *_serv server->addDatabaseInterface(newThread, newDatabaseInterface); newThread->start(); - server->watchWorkerThread(newThread); QMetaObject::invokeMethod(newDatabaseInterface, "initDatabase", Qt::BlockingQueuedConnection, Q_ARG(QSqlDatabase, _sqlDatabase)); @@ -159,6 +156,7 @@ void Servatrice_WebsocketGameServer::onNewConnection() Servatrice_ConnectionPool *pool = findLeastUsedConnectionPool(); auto ssi = new WebsocketServerSocketInterface(server, pool->getDatabaseInterface()); + connect(ssi, SIGNAL(incTxBytes(quint64)), this, SLOT(incTxBytes(quint64))); /* * Due to a Qt limitation, websockets can't be moved to another thread. * This will hopefully change in Qt6 if QtWebSocket will be integrated in QtNetwork @@ -228,13 +226,6 @@ bool Servatrice::initServer() { serverId = getServerID(); - - // METRICS (always active. Slow-command logging and stall watchdogs are - // controlled by their respective thresholds below). Read up front so the - // values are available before any pool thread is started and watchdogged. - metricsSlowCommandMs = settingsCache->value("metrics/slow_command_ms", 500).toInt(); - metricsStallWarnMs = qMax(0, settingsCache->value("metrics/stall_warn_ms", 2000).toInt()); - if (getAuthenticationMethodString() == "sql") { qDebug() << "Authenticating method: sql"; authenticationMethod = AuthenticationSql; @@ -437,14 +428,6 @@ bool Servatrice::initServer() statusUpdateClock->start(getServerStatusUpdateTime()); } - deckShareCleanupClock = new QTimer(this); - connect(deckShareCleanupClock, SIGNAL(timeout()), this, SLOT(cleanupExpiredDeckShares())); - const int deckShareCleanupInterval = getDeckShareCleanupInterval(); - if (deckShareCleanupInterval > 0) { - qDebug() << "Starting deck share cleanup clock, interval" << deckShareCleanupInterval << "ms"; - deckShareCleanupClock->start(deckShareCleanupInterval); - } - // SOCKET SERVER if (getNumberOfTCPPools() > 0) { gameServer = @@ -487,55 +470,9 @@ bool Servatrice::initServer() } setRequiredFeatures(getRequiredFeatures()); - return true; } -void Servatrice::observeGameStartDurationMs(qint64 elapsedMs) -{ - metricsRegistry.observeGameStartDurationMs(elapsedMs); -} - -void Servatrice::observeEventLoopStall(const QString &threadName, qint64 overshootMs) -{ - eventLoopStallsTotal.fetch_add(1, std::memory_order_relaxed); - eventLoopLastStallMs.store(overshootMs, std::memory_order_relaxed); - qint64 prevMax = eventLoopMaxStallMs.load(std::memory_order_relaxed); - while (overshootMs > prevMax && - !eventLoopMaxStallMs.compare_exchange_weak(prevMax, overshootMs, std::memory_order_relaxed)) { - // retry until the max is at least as high as the new sample - } - - qWarning() << "Event loop stall in" << threadName << "- heartbeat overshot by" << overshootMs << "ms"; -} - -void Servatrice::watchWorkerThread(QThread *thread) -{ - if (metricsStallWarnMs <= 0) { - return; // watchdogs disabled via metrics/stall_warn_ms = 0 - } - - auto *watchdog = new EventLoopWatchdog(this, thread->objectName()); - connect(thread, &QThread::finished, watchdog, &QObject::deleteLater); - watchdog->moveToThread(thread); - QMetaObject::invokeMethod(watchdog, &EventLoopWatchdog::start, Qt::QueuedConnection); -} - -qint64 Servatrice::getCardsInGamesTotal() const -{ - qint64 total = 0; - QReadLocker roomsLocker(&roomsLock); // locking order: roomsLock before gamesLock/gameMutex - QMapIterator roomIterator(rooms); - while (roomIterator.hasNext()) { - Server_Room *room = roomIterator.next().value(); - QReadLocker gamesLocker(&room->gamesLock); - for (auto *game : room->getGames()) { - total += game->getCardsInGame(); - } - } - return total; -} - void Servatrice::addDatabaseInterface(QThread *thread, Servatrice_DatabaseInterface *databaseInterface) { databaseInterfaces.insert(thread, databaseInterface); @@ -577,23 +514,6 @@ QList Servatrice::getServerList() const return result; } -std::shared_ptr Servatrice::getCachedReportStats() const -{ - QMutexLocker locker(&reportStatsMutex); - if (!reportStatsTimestamp.isValid() || - reportStatsTimestamp.secsTo(QDateTime::currentDateTime()) >= reportStatsCacheTtlSeconds) { - return nullptr; - } - return reportStatsCache; -} - -void Servatrice::cacheReportStats(const Response_ReportStats &stats) -{ - QMutexLocker locker(&reportStatsMutex); - reportStatsCache = std::make_shared(stats); - reportStatsTimestamp = QDateTime::currentDateTime(); -} - int Servatrice::getUsersWithAddress(const QHostAddress &address) const { int result = 0; @@ -663,11 +583,6 @@ void Servatrice::setRequiredFeatures(const QString &featureList) qDebug() << "Set required client features to:" << serverRequiredFeatureList; } -void Servatrice::cleanupExpiredDeckShares() -{ - servatriceDatabaseInterface->cleanupExpiredDeckShares(); -} - void Servatrice::statusUpdate() { if (!servatriceDatabaseInterface->checkSql()) { @@ -758,27 +673,8 @@ void Servatrice::statusUpdate() } } -SessionEvent *Servatrice::makeShutdownEvent() const -{ - Event_ServerShutdown event; - event.set_reason(shutdownReason.toStdString()); - event.set_minutes(static_cast(shutdownMinutes)); - return Server_ProtocolHandler::prepareSessionEvent(event); -} - -SessionEvent *Servatrice::getLoginSessionEvent() const -{ - // Notify newly logged-in users of a pending server shutdown - QMutexLocker locker(&shutdownStateMutex); - if (shutdownTimer && shutdownMinutes > 0) { - return makeShutdownEvent(); - } - return nullptr; -} - void Servatrice::scheduleShutdown(const QString &reason, int minutes) { - shutdownStateMutex.lock(); shutdownReason = reason; shutdownMinutes = minutes; nextShutdownMessageMinutes = shutdownMinutes; @@ -787,7 +683,6 @@ void Servatrice::scheduleShutdown(const QString &reason, int minutes) connect(shutdownTimer, SIGNAL(timeout()), this, SLOT(shutdownTimeout())); shutdownTimer->start(60000); } - shutdownStateMutex.unlock(); shutdownTimeout(); } @@ -807,7 +702,6 @@ void Servatrice::incRxBytes(quint64 num) void Servatrice::shutdownTimeout() { - QMutexLocker locker(&shutdownStateMutex); // Show every time counter cut in half & every minute for last 5 minutes if (shutdownMinutes <= 5 || shutdownMinutes == nextShutdownMessageMinutes) { if (shutdownMinutes == nextShutdownMessageMinutes) { @@ -816,7 +710,10 @@ void Servatrice::shutdownTimeout() SessionEvent *se; if (shutdownMinutes) { - se = makeShutdownEvent(); + Event_ServerShutdown event; + event.set_reason(shutdownReason.toStdString()); + event.set_minutes(static_cast(shutdownMinutes)); + se = Server_ProtocolHandler::prepareSessionEvent(event); } else { Event_ConnectionClosed event; event.set_reason(Event_ConnectionClosed::SERVER_SHUTDOWN); @@ -1080,27 +977,6 @@ int Servatrice::getServerStatusUpdateTime() const return settingsCache->value("server/statusupdate", 15000).toInt(); } -int Servatrice::getDeckShareExpiryDays() const -{ - return qMax(1, settingsCache->value("deck_share/expiry_days", 7).toInt()); -} - -int Servatrice::getDeckShareCleanupInterval() const -{ - // default: every 60 minutes - return settingsCache->value("deck_share/cleanup_interval", 60).toInt() * 60000; -} - -int Servatrice::getDeckShareMaxDecksPerShare() const -{ - return settingsCache->value("deck_share/max_decks_per_share", 50).toInt(); -} - -int Servatrice::getDeckShareMaxSharesPerDay() const -{ - return settingsCache->value("deck_share/max_shares_per_day", 50).toInt(); -} - int Servatrice::getNumberOfTCPPools() const { return settingsCache->value("server/number_pools", 1).toInt(); diff --git a/servatrice/src/servatrice.h b/servatrice/src/servatrice.h index f39a44d51..62fb382cb 100644 --- a/servatrice/src/servatrice.h +++ b/servatrice/src/servatrice.h @@ -20,9 +20,6 @@ #ifndef SERVATRICE_H #define SERVATRICE_H -#include "metrics_registry.h" - -#include #include #include #include @@ -32,9 +29,6 @@ #include #include #include -#include -#include -#include #include #include @@ -146,7 +140,6 @@ public: private slots: void statusUpdate(); void shutdownTimeout(); - void cleanupExpiredDeckShares(); protected: void doSendIslMessage(const IslMessage &msg, int _serverId) override; @@ -160,13 +153,11 @@ private: AuthenticationMethod authenticationMethod; DatabaseType databaseType; QTimer *pingClock, *statusUpdateClock; - QTimer *deckShareCleanupClock; Servatrice_GameServer *gameServer; Servatrice_WebsocketGameServer *websocketGameServer; Servatrice_IslServer *islServer; mutable QMutex loginMessageMutex; QString loginMessage; - mutable QMutex shutdownStateMutex; QString dbPrefix; QMap serverRequiredFeatureList; QString officialWarnings; @@ -175,23 +166,12 @@ private: int uptime; QMutex txBytesMutex, rxBytesMutex; quint64 txBytes, rxBytes; - MetricsRegistry metricsRegistry; - int metricsSlowCommandMs = 500; - int metricsStallWarnMs = 2000; - std::atomic eventLoopStallsTotal{0}; ///< heartbeat overshoots past the warn threshold - std::atomic eventLoopLastStallMs{0}; ///< overshoot of the most recent stall - std::atomic eventLoopMaxStallMs{0}; ///< worst overshoot seen since process start QString shutdownReason; int shutdownMinutes; int nextShutdownMessageMinutes; QTimer *shutdownTimer; - mutable QMutex reportStatsMutex; - QDateTime reportStatsTimestamp; - std::shared_ptr reportStatsCache; - static constexpr int reportStatsCacheTtlSeconds = 60; - mutable QMutex serverListMutex; QList serverList; void updateServerList(); @@ -236,8 +216,6 @@ public: QMutexLocker locker(&loginMessageMutex); return loginMessage; } - SessionEvent *getLoginSessionEvent() const override; - SessionEvent *makeShutdownEvent() const; QString getRequiredFeatures() const override; QString getAuthenticationMethodString() const; QString getDBTypeString() const; @@ -278,10 +256,6 @@ public: int getMaxGameInactivityTime() const override; int getMaxPlayerInactivityTime() const override; int getClientKeepAlive() const override; - int getDeckShareExpiryDays() const; - int getDeckShareCleanupInterval() const; - int getDeckShareMaxDecksPerShare() const; - int getDeckShareMaxSharesPerDay() const; int getMaxUsersPerAddress() const; int getMessageCountingInterval() const override; int getMaxMessageCountPerInterval() const override; @@ -301,59 +275,11 @@ public: void incRxBytes(quint64 num); void addDatabaseInterface(QThread *thread, Servatrice_DatabaseInterface *databaseInterface); - // Metrics (see [metrics] section in servatrice.ini.example) - MetricsRegistry &getMetricsRegistry() - { - return metricsRegistry; - } - /** - * Sums cards across all zones of all running games. Each game takes its - * own gameMutex -- the hot per-game lock every game action contends on -- - * and then iterates every player's zones, so the scrape cost is really - * O(total cards in play) plus one mutex acquisition per live game. Keep - * scrapes infrequent in big multiplayer rooms. - */ - qint64 getCardsInGamesTotal() const; - int getMetricsSlowCommandMs() const - { - return metricsSlowCommandMs; - } - /// Heartbeat overshoot that counts as a stall. A value of 0 disables the watchdogs. - int getMetricsStallWarnMs() const - { - return metricsStallWarnMs; - } - void observeGameStartDurationMs(qint64 elapsedMs) override; - qint64 getEventLoopStallsTotal() const - { - return eventLoopStallsTotal.load(std::memory_order_relaxed); - } - qint64 getEventLoopLastStallMs() const - { - return eventLoopLastStallMs.load(std::memory_order_relaxed); - } - qint64 getEventLoopMaxStallMs() const - { - return eventLoopMaxStallMs.load(std::memory_order_relaxed); - } - /// Records one heartbeat overshoot and logs a single warning for it. - void observeEventLoopStall(const QString &threadName, qint64 overshootMs); - /** - * Installs an EventLoopWatchdog in @p thread. Called once per socket pool - * thread right after it starts. - */ - void watchWorkerThread(QThread *thread); - bool islConnectionExists(int _serverId) const; void addIslInterface(int _serverId, IslInterface *interface); void removeIslInterface(int _serverId); QReadWriteLock islLock; - // The moderation queue statistics are shared between all connected moderators and - // cached briefly to avoid re-running several full-table queries on every refresh. - std::shared_ptr getCachedReportStats() const; - void cacheReportStats(const Response_ReportStats &stats); - QList getServerList() const; }; diff --git a/servatrice/src/servatrice_database_interface.cpp b/servatrice/src/servatrice_database_interface.cpp index bb67d78eb..d5e1f13ef 100644 --- a/servatrice/src/servatrice_database_interface.cpp +++ b/servatrice/src/servatrice_database_interface.cpp @@ -1,6 +1,5 @@ #include "servatrice_database_interface.h" -#include "deck_tag_serialization.h" #include "servatrice.h" #include "serversocketinterface.h" #include "settingscache.h" @@ -8,16 +7,13 @@ #include #include #include -#include #include #include #include #include #include -#include #include #include -#include #include inline Q_LOGGING_CATEGORY(DatabaseInterfaceLog, "database_interface"); @@ -89,10 +85,9 @@ bool Servatrice_DatabaseInterface::openDatabase() << dbversion << "to version" << expectedversion; return false; } else if (dbversion > expectedversion) { - qCCritical(DatabaseInterfaceLog) - << poolStr << "Error opening database: the database schema version" << dbversion - << "is too new, you need to update Servatrice" << "(Currently running Servatrice actually uses version" - << expectedversion << ")"; + qCCritical(DatabaseInterfaceLog) << poolStr << "Error opening database: the database schema version" + << dbversion << "is too new, you need to update servatrice" + << "(this servatrice actually uses version" << expectedversion << ")"; return false; } } else { @@ -102,59 +97,12 @@ bool Servatrice_DatabaseInterface::openDatabase() return false; } - if (sqlDatabase.driverName() != "QMYSQL") { - qCCritical(DatabaseInterfaceLog) - << poolStr - << "Error opening database: connection is not a MySQL/MariaDB database, Servatrice only " - "supports the QMYSQL driver (actual driver:" - << sqlDatabase.driverName() << ")."; - return false; - } - - bool strictModeCheckOk = false; - const bool strictModeEnabled = isStrictModeEnabled(strictModeCheckOk); - if (!strictModeCheckOk) { - qCCritical(DatabaseInterfaceLog) << poolStr - << "Error opening database: unable to determine whether MySQL/MariaDB strict " - "mode is enabled"; - return false; - } - if (strictModeEnabled) { - qCCritical(DatabaseInterfaceLog) << poolStr - << "Error opening database: MySQL/MariaDB strict mode is enabled, which " - "breaks most Servatrice database operations. Please disable strict mode " - "by removing STRICT_TRANS_TABLES and STRICT_ALL_TABLES from sql_mode, " - "for example by adding 'sql_mode=NO_ENGINE_SUBSTITUTION' under [mysqld] " - "in your my.cnf (or my.ini on Windows) and restarting the database " - "server."; - return false; - } - // reset all prepared statements qDeleteAll(preparedStatements); preparedStatements.clear(); return true; } -bool Servatrice_DatabaseInterface::isStrictModeEnabled(bool &ok) const -{ - ok = true; - - QSqlQuery query(sqlDatabase); - if (!query.exec("SELECT @@GLOBAL.sql_mode")) { - ok = false; - return false; - } - - const QStringList modes = query.next() ? query.value(0).toString().split(',') : QStringList(); - for (const QString &mode : modes) { - if (mode.trimmed() == "STRICT_TRANS_TABLES" || mode.trimmed() == "STRICT_ALL_TABLES") { - return true; - } - } - return false; -} - bool Servatrice_DatabaseInterface::checkSql() { if (!sqlDatabase.isValid()) { @@ -391,8 +339,8 @@ AuthenticationResult Servatrice_DatabaseInterface::checkUserPassword(Server_Prot return UserIsBanned; } - QSqlQuery *passwordQuery = prepareQuery( - "select password_sha512, active, force_password_change from {prefix}_users where name = :name"); + QSqlQuery *passwordQuery = + prepareQuery("select password_sha512, active from {prefix}_users where name = :name"); passwordQuery->bindValue(":name", user); if (!execSqlQuery(passwordQuery)) { qCWarning(DatabaseInterfaceLog) << "Login denied: SQL error"; @@ -402,7 +350,6 @@ AuthenticationResult Servatrice_DatabaseInterface::checkUserPassword(Server_Prot if (passwordQuery->next()) { const QString correctPasswordSha512 = passwordQuery->value(0).toString(); const bool userIsActive = passwordQuery->value(1).toBool(); - const bool forceChange = passwordQuery->value(2).toBool(); if (!userIsActive) { qCWarning(DatabaseInterfaceLog) << "Login denied: user not active"; return UserIsInactive; @@ -414,10 +361,6 @@ AuthenticationResult Servatrice_DatabaseInterface::checkUserPassword(Server_Prot hashedPassword = password; } if (correctPasswordSha512 == hashedPassword) { - if (forceChange) { - qCDebug(DatabaseInterfaceLog) << "Login accepted but password change required"; - return PasswordChangeRequired; - } qCDebug(DatabaseInterfaceLog) << "Login accepted: password right"; return PasswordRight; } else { @@ -692,10 +635,6 @@ ServerInfo_User Servatrice_DatabaseInterface::evalUserQueryResult(const QSqlQuer userLevel |= ServerInfo_User::IsJudge; } - if (is_admin & 8) { - userLevel |= ServerInfo_User::IsDeveloper; - } - result.set_user_level(userLevel); const QString country = query->value(3).toString(); @@ -1081,183 +1020,6 @@ DeckList *Servatrice_DatabaseInterface::getDeckFromDatabase(int deckId, int user return deck; } -bool Servatrice_DatabaseInterface::createDeckShare(const QString &token, - const QString &name, - int userId, - const QList &items, - int expiryDays, - qint64 &expiresAt) -{ - checkSql(); - - if (items.isEmpty()) { - return false; - } - - if (!sqlDatabase.transaction()) { - return false; - } - - QSqlQuery *query = prepareQuery("insert into {prefix}_deck_share (token, name, created_by, created_at, expires_at) " - "values (:token, :name, :created_by, NOW(), DATE_ADD(NOW(), INTERVAL :days DAY))"); - query->bindValue(":token", token); - query->bindValue(":name", name); - query->bindValue(":created_by", userId < 1 ? QVariant() : userId); - query->bindValue(":days", expiryDays); - if (!execSqlQuery(query)) { - // A failed execSqlQuery has already closed and reopened the connection, - // which implicitly discards the transaction; rollback below is a no-op. - sqlDatabase.rollback(); - return false; - } - - const int shareId = query->lastInsertId().toInt(); - - // Read the expiry back from the database so the value returned to the client - // matches the server clock rather than being approximated client-side. - QSqlQuery *expiryQuery = prepareQuery("select UNIX_TIMESTAMP(expires_at) from {prefix}_deck_share where id = :id"); - expiryQuery->bindValue(":id", shareId); - if (!execSqlQuery(expiryQuery) || !expiryQuery->next()) { - // See the note above: after a failed execSqlQuery the transaction is - // already gone because the connection was torn down. - sqlDatabase.rollback(); - return false; - } - expiresAt = expiryQuery->value(0).toLongLong(); - - for (int i = 0; i < items.size(); ++i) { - const DeckShareItemRecord &item = items.at(i); - QSqlQuery *itemQuery = prepareQuery("insert into {prefix}_deck_share_item (share_id, name, tags, banner_card, " - "game_format, color_identity, content, position) values (:share_id, :name, " - ":tags, :banner_card, :game_format, :color_identity, :content, :position)"); - itemQuery->bindValue(":share_id", shareId); - itemQuery->bindValue(":name", item.name); - itemQuery->bindValue(":tags", serializeDeckTags(item.tags)); - itemQuery->bindValue(":banner_card", item.bannerCard); - itemQuery->bindValue(":game_format", item.gameFormat); - itemQuery->bindValue(":color_identity", item.colorIdentity); - itemQuery->bindValue(":content", item.content); - itemQuery->bindValue(":position", i); - if (!execSqlQuery(itemQuery)) { - // See the note above: the transaction is already gone after the - // reconnect performed by a failed execSqlQuery. - sqlDatabase.rollback(); - return false; - } - } - - if (!sqlDatabase.commit()) { - sqlDatabase.rollback(); - return false; - } - return true; -} - -bool Servatrice_DatabaseInterface::getDeckShareList(const QString &token, - QString &name, - qint64 &expiresAt, - QList &items) -{ - checkSql(); - - QSqlQuery *query = - prepareQuery("select id, name, UNIX_TIMESTAMP(expires_at) from {prefix}_deck_share where token = " - ":token and expires_at > now()"); - query->bindValue(":token", token); - execSqlQuery(query); - if (!query->next()) { - return false; - } - - const int shareId = query->value(0).toInt(); - name = query->value(1).toString(); - expiresAt = query->value(2).toLongLong(); - items.clear(); - - QSqlQuery *itemQuery = - prepareQuery("select id, name, tags, banner_card, game_format, color_identity from {prefix}_deck_share_item " - "where share_id = :share_id order by position"); - itemQuery->bindValue(":share_id", shareId); - execSqlQuery(itemQuery); - while (itemQuery->next()) { - DeckShareItemRecord item; - item.id = itemQuery->value(0).toInt(); - item.name = itemQuery->value(1).toString(); - item.tags = deserializeDeckTags(itemQuery->value(2).toString()); - item.bannerCard = itemQuery->value(3).toString(); - item.gameFormat = itemQuery->value(4).toString(); - item.colorIdentity = itemQuery->value(5).toString(); - items.append(item); - } - - return true; -} - -bool Servatrice_DatabaseInterface::getDeckShareItem(const QString &token, int itemId, QString &content) -{ - checkSql(); - - QSqlQuery *query = prepareQuery("select i.content from {prefix}_deck_share_item i join {prefix}_deck_share s on " - "s.id = i.share_id where s.token = :token and s.expires_at > now() and i.id = :id"); - query->bindValue(":token", token); - query->bindValue(":id", itemId); - execSqlQuery(query); - if (!query->next()) { - return false; - } - - content = query->value(0).toString(); - return true; -} - -void Servatrice_DatabaseInterface::cleanupExpiredDeckShares() -{ - checkSql(); - - QSqlQuery *query = prepareQuery("delete from {prefix}_deck_share where expires_at < now()"); - execSqlQuery(query); -} - -bool Servatrice_DatabaseInterface::getDeckSharesForUser(int userId, QList &shares) -{ - checkSql(); - - QSqlQuery *query = prepareQuery("select s.id, s.name, UNIX_TIMESTAMP(s.created_at), " - "UNIX_TIMESTAMP(s.expires_at), count(i.id) from {prefix}_deck_share s left join " - "{prefix}_deck_share_item i on i.share_id = s.id " - "where s.created_by = :created_by and s.expires_at > now() " - "group by s.id, s.name, s.created_at, s.expires_at order by s.created_at desc"); - query->bindValue(":created_by", userId); - if (!execSqlQuery(query)) { - return false; - } - - shares.clear(); - while (query->next()) { - DeckShareSummaryRecord summary; - summary.id = query->value(0).toInt(); - summary.name = query->value(1).toString(); - summary.creationTime = query->value(2).toLongLong(); - summary.expiresAt = query->value(3).toLongLong(); - summary.itemCount = query->value(4).toInt(); - shares.append(summary); - } - return true; -} - -bool Servatrice_DatabaseInterface::deleteDeckShare(int shareId, int userId) -{ - checkSql(); - - QSqlQuery *query = prepareQuery("delete from {prefix}_deck_share where id = :id and created_by = :created_by"); - query->bindValue(":id", shareId); - query->bindValue(":created_by", userId); - if (!execSqlQuery(query)) { - return false; - } - return query->numRowsAffected() > 0; -} - void Servatrice_DatabaseInterface::logMessage(const int senderId, const QString &senderName, const QString &senderIp, @@ -1322,22 +1084,11 @@ bool Servatrice_DatabaseInterface::changeUserPassword(const QString &user, "passwordLastChangedDate = NOW() where name = :name"); passwordQuery->bindValue(":password", passwordSha512); passwordQuery->bindValue(":name", user); - if (!execSqlQuery(passwordQuery)) { - return false; - } - return passwordQuery->numRowsAffected() > 0; -} - -void Servatrice_DatabaseInterface::setForcePasswordChange(const QString &user, bool force) -{ - if (!checkSql()) { - return; + if (execSqlQuery(passwordQuery)) { + return true; } - QSqlQuery *query = prepareQuery("UPDATE {prefix}_users SET force_password_change = :force WHERE name = :name"); - query->bindValue(":force", force ? 1 : 0); - query->bindValue(":name", user); - execSqlQuery(query); + return false; } bool Servatrice_DatabaseInterface::changeUserPassword(const QString &user, @@ -1563,204 +1314,6 @@ QList Servatrice_DatabaseInterface::getUserWarnHistory(const return results; } -QList Servatrice_DatabaseInterface::getUserSessions(const QString &userName, int limit) -{ - QList results; - - if (!checkSql()) { - return results; - } - - QSqlQuery *query = prepareQuery("SELECT user_name, ip_address, clientid, " - "UNIX_TIMESTAMP(start_time), UNIX_TIMESTAMP(end_time), connection_type " - "FROM {prefix}_sessions WHERE user_name = :user_name " - "ORDER BY start_time DESC LIMIT :limit"); - query->bindValue(":user_name", userName); - query->bindValue(":limit", limit); - - if (!execSqlQuery(query)) { - qCWarning(DatabaseInterfaceLog) << "Failed to collect session history information: SQL Error"; - return results; - } - - while (query->next()) { - ServerInfo_UserSession sessionDetails; - sessionDetails.set_user_name(query->value(0).toString().toStdString()); - sessionDetails.set_ip_address(query->value(1).toString().toStdString()); - sessionDetails.set_clientid(query->value(2).toString().toStdString()); - sessionDetails.set_start_time(query->value(3).toLongLong()); - if (!query->value(4).isNull()) { - sessionDetails.set_end_time(query->value(4).toLongLong()); - } - sessionDetails.set_connection_type(query->value(5).toString().toStdString()); - results << sessionDetails; - } - - return results; -} - -QList Servatrice_DatabaseInterface::getUserAlts(const QString &userName) -{ - QList results; - - if (!checkSql()) { - return results; - } - - // 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(); - - 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"); - - 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); - } - - 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; -} - -QList Servatrice_DatabaseInterface::getModeratorLastLogins() -{ - QList results; - - if (!checkSql()) { - return results; - } - - QSqlQuery *query = prepareQuery("SELECT u.name, u.admin, UNIX_TIMESTAMP(a.last_login) " - "FROM {prefix}_users u " - "LEFT JOIN {prefix}_user_analytics a ON a.id = u.id " - "WHERE (u.admin & 15) <> 0 ORDER BY u.name"); - - if (!execSqlQuery(query)) { - qCWarning(DatabaseInterfaceLog) << "Failed to collect moderator login information: SQL Error"; - return results; - } - - while (query->next()) { - ServerInfo_ModeratorLogin loginDetails; - loginDetails.set_user_name(query->value(0).toString().toStdString()); - - const int isAdmin = query->value(1).toInt(); - int userLevel = ServerInfo_User::IsUser | ServerInfo_User::IsRegistered; - if (isAdmin & 1) { - userLevel |= ServerInfo_User::IsAdmin | ServerInfo_User::IsModerator; - } else if (isAdmin & 2) { - userLevel |= ServerInfo_User::IsModerator; - } - if (isAdmin & 4) { - userLevel |= ServerInfo_User::IsJudge; - } - if (isAdmin & 8) { - userLevel |= ServerInfo_User::IsDeveloper; - } - loginDetails.set_user_level(userLevel); - - if (!query->value(2).isNull()) { - loginDetails.set_last_login(query->value(2).toLongLong()); - } - results << loginDetails; - } - - return results; -} - -Servatrice_DatabaseInterface::UptimeSnapshot Servatrice_DatabaseInterface::getLatestUptimeSnapshot(int serverId) -{ - UptimeSnapshot snapshot; - - if (!checkSql()) { - return snapshot; - } - - QSqlQuery *query = prepareQuery("SELECT users_count, mods_count, games_count, tx_bytes, rx_bytes, uptime, " - "UNIX_TIMESTAMP(timest) FROM {prefix}_uptime " - "WHERE id_server = :id_server ORDER BY timest DESC LIMIT 1"); - query->bindValue(":id_server", serverId); - - if (!execSqlQuery(query)) { - qCWarning(DatabaseInterfaceLog) << "Failed to collect server stats snapshot: SQL Error"; - return snapshot; - } - - if (query->next()) { - snapshot.valid = true; - snapshot.usersCount = query->value(0).toULongLong(); - snapshot.modsCount = query->value(1).toULongLong(); - snapshot.gamesCount = query->value(2).toULongLong(); - snapshot.txBytes = query->value(3).toULongLong(); - snapshot.rxBytes = query->value(4).toULongLong(); - snapshot.uptimeSecs = query->value(5).toULongLong(); - snapshot.timest = query->value(6).toULongLong(); - } - - return snapshot; -} - -bool Servatrice_DatabaseInterface::removeUserAvatar(const QString &userName) -{ - if (!checkSql()) { - return false; - } - - QSqlQuery *query = prepareQuery("UPDATE {prefix}_users SET avatar_bmp = '' WHERE name = :user_name"); - query->bindValue(":user_name", userName); - if (!execSqlQuery(query)) { - return false; - } - return query->numRowsAffected() > 0; -} - QList Servatrice_DatabaseInterface::getMessageLogHistory(const QString &user, const QString &ipaddress, const QString &gamename, diff --git a/servatrice/src/servatrice_database_interface.h b/servatrice/src/servatrice_database_interface.h index ce51a2474..1e3501ec7 100644 --- a/servatrice/src/servatrice_database_interface.h +++ b/servatrice/src/servatrice_database_interface.h @@ -6,39 +6,14 @@ #include #include #include -#include -#include -#include #include #include #include -#define DATABASE_SCHEMA_VERSION 37 +#define DATABASE_SCHEMA_VERSION 35 class Servatrice; -/** @brief Metadata of a single deck inside a temporary deck share bundle. */ -struct DeckShareItemRecord -{ - int id = -1; ///< Database id, used for downloads. - QString name; ///< Deck name. - QStringList tags; ///< Deck tags. - QString bannerCard; ///< Banner card name (deck image). - QString gameFormat; ///< Game format the deck was built for. - QString colorIdentity; ///< Color identity, e.g. "WUBRG". - QString content; ///< Deck content (native format); empty in list queries. -}; - -/** @brief Summary of a share bundle owned by a user. */ -struct DeckShareSummaryRecord -{ - int id = -1; ///< Database id, used for revocation. - QString name; ///< Share name. - qint64 creationTime = 0; ///< Unix timestamp at which the share was created. - qint64 expiresAt = 0; ///< Unix timestamp at which the share expires. - int itemCount = 0; ///< Number of decks in the bundle. -}; - class Servatrice_DatabaseInterface : public Server_DatabaseInterface { Q_OBJECT @@ -54,7 +29,6 @@ private: bool checkUserIsIpBanned(const QString &ipAddress, QString &banReason, int &banSecondsRemaining); /** Must be called after checkSql and server is known to be in auth mode. */ bool checkUserIsNameBanned(QString const &userName, QString &banReason, int &banSecondsRemaining); - bool isStrictModeEnabled(bool &ok) const; protected: AuthenticationResult checkUserPassword(Server_ProtocolHandler *handler, @@ -102,37 +76,6 @@ public: const QList &replayList) override; DeckList *getDeckFromDatabase(int deckId, int userId) override; - /** - * @brief Creates a new temporary deck share bundle. - * @param expiresAt Receives the actual expiry read back from the database. - * @return false on failure. - */ - bool createDeckShare(const QString &token, - const QString &name, - int userId, - const QList &items, - int expiryDays, - qint64 &expiresAt); - /** @brief Lists the share bundles created by a user, newest first. */ - bool getDeckSharesForUser(int userId, QList &shares); - /** - * @brief Deletes one of a user's own share bundles (cascades to its items). - * @return false if no such bundle belongs to the user. - */ - bool deleteDeckShare(int shareId, int userId); - /** - * @brief Looks up a valid (non-expired) share bundle by token. - * @return false if the token is unknown or expired. - */ - bool getDeckShareList(const QString &token, QString &name, qint64 &expiresAt, QList &items); - /** - * @brief Fetches the content of one item of a valid share bundle. - * @return false if the token is unknown/expired or the item does not belong to the bundle. - */ - bool getDeckShareItem(const QString &token, int itemId, QString &content); - /** @brief Deletes all expired share bundles (cascades to their items). */ - void cleanupExpiredDeckShares(); - int getNextGameId() override; int getNextReplayId() override; int getActiveUserCount(QString connectionType = QString()) override; @@ -176,7 +119,6 @@ public: bool oldPasswordNeedsHash, const QString &newPassword, bool newPasswordNeedsHash) override; - void setForcePasswordChange(const QString &user, bool force) override; QList getUserBanHistory(const QString userName); bool addWarning(const QString userName, const QString adminName, const QString warningReason, const QString clientID); @@ -191,25 +133,6 @@ public: bool &room, int &range, int &maxresults); - QList getUserSessions(const QString &userName, int limit); - QList getUserAlts(const QString &userName); - QList getModeratorLastLogins(); - - // Uptime snapshot as recorded by Servatrice::statusUpdate() into the - // {prefix}_uptime table. valid is false when no snapshot exists yet. - struct UptimeSnapshot - { - bool valid = false; - quint64 usersCount = 0; - quint64 modsCount = 0; - quint64 gamesCount = 0; - quint64 txBytes = 0; - quint64 rxBytes = 0; - quint64 uptimeSecs = 0; - quint64 timest = 0; - }; - UptimeSnapshot getLatestUptimeSnapshot(int serverId); - bool removeUserAvatar(const QString &userName); bool addForgotPassword(const QString &user); bool removeForgotPassword(const QString &user) override; bool doesForgotPasswordExist(const QString &user); diff --git a/servatrice/src/serversocketinterface.cpp b/servatrice/src/serversocketinterface.cpp index c82a8dd73..842ddb4c8 100644 --- a/servatrice/src/serversocketinterface.cpp +++ b/servatrice/src/serversocketinterface.cpp @@ -20,7 +20,6 @@ #include "serversocketinterface.h" -#include "deck_tag_serialization.h" #include "email_parser.h" #include "main.h" #include "servatrice.h" @@ -31,53 +30,29 @@ #include #include -#include #include #include #include #include -#include #include #include #include #include -#include #include -#include #include #include -#include #include #include #include -#include #include -#include #include -#include -#include -#include -#include -#include -#include #include -#include #include #include -#include #include #include #include #include -#include -#include -#include -#include -#include -#include -#include -#include -#include #include #include #include @@ -92,44 +67,23 @@ #include #include #include -#include -#include -#include -#include #include #include #include -#include -#include #include #include -#include #include -#include #include #include -#include -#include -#include -#include -#include -#include -#include -#include #include #include #include #include #include #include -#include #include #include -#include -#include -#include #include -#include #include #include #include @@ -210,77 +164,6 @@ void AbstractServerSocketInterface::logDebugMessage(const QString &message) logger->logMessage(message, this); } -void AbstractServerSocketInterface::processCommandContainer(const CommandContainer &cont) -{ - QElapsedTimer timer; - timer.start(); - Server_ProtocolHandler::processCommandContainer(cont); - const qint64 elapsedMs = timer.nsecsElapsed() / 1000000; - - // The base dispatch is an if/else-if chain — at most one family is - // actually processed. Recording every family in the container would - // let an unauthenticated client stampforge developer/moderator/admin - // samples by batching them alongside a session command the server - // actually runs. Mirror the base's selection and skip entirely when - // deleted or when no family matched. - if (deleted) { - return; - } - - // When getPbExtension returns -1 (no extension set) and the kind is - // non-zero, typeIdFor wraps into the previous kind's range instead of - // hitting the typeId < 0 guard in observeCommand. Skip such entries. - int kind = -1; - if (cont.game_command_size()) { - kind = 2; - } else if (cont.room_command_size()) { - kind = 1; - } else if (cont.session_command_size()) { - kind = 0; - } else if (cont.moderator_command_size()) { - kind = 3; - } else if (cont.admin_command_size()) { - kind = 4; - } else if (cont.developer_command_size()) { - kind = 5; - } - - if (kind >= 0) { - auto recordDispatched = [&](int familyKind, const auto &cmds) { - for (const auto &cmd : cmds) { - const int ext = getPbExtension(cmd); - if (ext >= 0) { - servatrice->getMetricsRegistry().observeCommand(MetricsRegistry::typeIdFor(familyKind, ext), - elapsedMs); - } - } - }; - - if (kind == 0) { - recordDispatched(kind, cont.session_command()); - } else if (kind == 1) { - recordDispatched(kind, cont.room_command()); - } else if (kind == 2) { - recordDispatched(kind, cont.game_command()); - } else if (kind == 3) { - recordDispatched(kind, cont.moderator_command()); - } else if (kind == 4) { - recordDispatched(kind, cont.admin_command()); - } else { - recordDispatched(kind, cont.developer_command()); - } - } - - const int slowCommandMs = servatrice->getMetricsSlowCommandMs(); - if (slowCommandMs > 0 && elapsedMs >= slowCommandMs) { - const ServerInfo_User *info = getUserInfo(); - const QString user = authState == PasswordRight && info ? QString::fromStdString(info->name()) - : QStringLiteral("unauthenticated"); - qCWarning(AbstractServerSocketInterfaceLog) << "slow command container from" << user << "processed in" - << elapsedMs << "ms (" << cont.ByteSizeLong() << "bytes)"; - } -} - Response::ResponseCode AbstractServerSocketInterface::processExtendedSessionCommand(int cmdType, const SessionCommand &cmd, ResponseContainer &rc) @@ -292,12 +175,6 @@ Response::ResponseCode AbstractServerSocketInterface::processExtendedSessionComm return cmdRemoveFromList(cmd.GetExtension(Command_RemoveFromList::ext), rc); case SessionCommand::DECK_LIST: return cmdDeckList(cmd.GetExtension(Command_DeckList::ext), rc); - case SessionCommand::DECK_LIST_OTHER_USER: - return cmdDeckListOtherUser(cmd.GetExtension(Command_DeckListOtherUser::ext), rc); - case SessionCommand::DECK_SET_VISIBILITY: - return cmdDeckSetVisibility(cmd.GetExtension(Command_DeckSetVisibility::ext), rc); - case SessionCommand::DECK_DOWNLOAD_PUBLIC: - return cmdDeckDownloadPublic(cmd.GetExtension(Command_DeckDownloadPublic::ext), rc); case SessionCommand::DECK_NEW_DIR: return cmdDeckNewDir(cmd.GetExtension(Command_DeckNewDir::ext), rc); case SessionCommand::DECK_DEL_DIR: @@ -341,29 +218,11 @@ Response::ResponseCode AbstractServerSocketInterface::processExtendedSessionComm return cmdAccountImage(cmd.GetExtension(Command_AccountImage::ext), rc); case SessionCommand::SET_CARD_ART_PARAMS: return cmdSetCardArtParams(cmd.GetExtension(Command_SetCardArtParams::ext), rc); - case SessionCommand::DECK_SHARE_CREATE: - return cmdDeckShareCreate(cmd.GetExtension(Command_DeckShareCreate::ext), rc); - case SessionCommand::DECK_SHARE_LIST: - return cmdDeckShareList(cmd.GetExtension(Command_DeckShareList::ext), rc); - case SessionCommand::DECK_SHARE_LIST_MINE: - return cmdDeckShareListMine(cmd.GetExtension(Command_DeckShareListMine::ext), rc); - case SessionCommand::DECK_SHARE_REMOVE: - return cmdDeckShareRemove(cmd.GetExtension(Command_DeckShareRemove::ext), rc); - case SessionCommand::DECK_SHARE_DOWNLOAD: - return cmdDeckShareDownload(cmd.GetExtension(Command_DeckShareDownload::ext), rc); case SessionCommand::ACCOUNT_PASSWORD: return cmdAccountPassword(cmd.GetExtension(Command_AccountPassword::ext), rc); case SessionCommand::REQUEST_PASSWORD_SALT: return cmdRequestPasswordSalt(cmd.GetExtension(Command_RequestPasswordSalt::ext), rc); break; - case SessionCommand::REPORT: - return cmdReport(cmd.GetExtension(Command_Report::ext), rc); - case SessionCommand::REPORT_MY_LIST: - return cmdReportMyList(cmd.GetExtension(Command_ReportMyList::ext), rc); - case SessionCommand::REPORT_ADD_COMMENT: - return cmdReportAddComment(cmd.GetExtension(Command_ReportAddComment::ext), rc); - case SessionCommand::REPORT_DETAILS: - return cmdReportDetails(cmd.GetExtension(Command_ReportDetails::ext), rc); default: return Response::RespFunctionNotAllowed; } @@ -384,18 +243,10 @@ Response::ResponseCode AbstractServerSocketInterface::processExtendedModeratorCo return cmdGetWarnHistory(cmd.GetExtension(Command_GetWarnHistory::ext), rc); case ModeratorCommand::WARN_LIST: return cmdGetWarnList(cmd.GetExtension(Command_GetWarnList::ext), rc); - case ModeratorCommand::REPORT_LIST: - return cmdReportList(cmd.GetExtension(Command_ReportList::ext), rc); - case ModeratorCommand::REPORT_ASSIGN: - return cmdReportAssign(cmd.GetExtension(Command_ReportAssign::ext), rc); - case ModeratorCommand::REPORT_RESOLVE: - return cmdReportResolve(cmd.GetExtension(Command_ReportResolve::ext), rc); case ModeratorCommand::VIEWLOG_HISTORY: - return cmdGetLogHistory(cmd.GetExtension(Command_ViewLogHistory::ext), rc, true); + return cmdGetLogHistory(cmd.GetExtension(Command_ViewLogHistory::ext), rc); case ModeratorCommand::GRANT_REPLAY_ACCESS: return cmdGrantReplayAccess(cmd.GetExtension(Command_GrantReplayAccess::ext), rc); - case ModeratorCommand::REPLAY_DOWNLOAD_BY_GAME_ID: - return cmdReplayDownloadByGameId(cmd.GetExtension(Command_ReplayDownloadByGameId::ext), rc); case ModeratorCommand::FORCE_ACTIVATE_USER: return cmdForceActivateUser(cmd.GetExtension(Command_ForceActivateUser::ext), rc); case ModeratorCommand::GET_ADMIN_NOTES: @@ -408,18 +259,6 @@ Response::ResponseCode AbstractServerSocketInterface::processExtendedModeratorCo return cmdRemoveCardArtRule(cmd.GetExtension((Command_RemoveCardArtRule::ext)), rc); case ModeratorCommand::LIST_CARD_ART_RULES: return cmdListCardArtRules(cmd.GetExtension((Command_ListCardArtRules::ext)), rc); - case ModeratorCommand::REPORT_USER_INFO: - return cmdReportUserInfo(cmd.GetExtension(Command_ReportUserInfo::ext), rc); - case ModeratorCommand::REPORT_STATS: - return cmdReportStats(cmd.GetExtension(Command_ReportStats::ext), rc); - case ModeratorCommand::GET_USER_SESSIONS: - return cmdGetUserSessions(cmd.GetExtension(Command_GetUserSessions::ext), rc); - case ModeratorCommand::GET_USER_ALTS: - return cmdGetUserAlts(cmd.GetExtension(Command_GetUserAlts::ext), rc); - case ModeratorCommand::GET_MODERATOR_LAST_LOGINS: - return cmdGetModeratorLastLogins(cmd.GetExtension(Command_GetModeratorLastLogins::ext), rc); - case ModeratorCommand::REMOVE_USER_AVATAR: - return cmdRemoveUserAvatar(cmd.GetExtension(Command_RemoveUserAvatar::ext), rc); default: return Response::RespFunctionNotAllowed; } @@ -437,28 +276,6 @@ AbstractServerSocketInterface::processExtendedAdminCommand(int cmdType, const Ad return cmdReloadConfig(cmd.GetExtension(Command_ReloadConfig::ext), rc); case AdminCommand::ADJUST_MOD: return cmdAdjustMod(cmd.GetExtension(Command_AdjustMod::ext), rc); - case AdminCommand::RESET_USER_PASSWORD: - return cmdResetUserPassword(cmd.GetExtension(Command_ResetUserPassword::ext), rc); - default: - return Response::RespFunctionNotAllowed; - } -} - -// DEVELOPER FUNCTIONS. -// Permission is checked by processDeveloperCommandContainer. Only stats-style -// queries live here, never community moderation or server administration. -Response::ResponseCode AbstractServerSocketInterface::processExtendedDeveloperCommand(int cmdType, - const DeveloperCommand &cmd, - ResponseContainer &rc) -{ - switch ((DeveloperCommand::DeveloperCommandType)cmdType) { - case DeveloperCommand::GET_SERVER_STATS: - return cmdGetServerStats(cmd.GetExtension(Command_GetServerStats::ext), rc); - case DeveloperCommand::VIEWLOG_HISTORY: { - // Same query as the moderator log view, carried by the developer - // command family, but narrows out private chats and sender IPs. - return cmdGetLogHistory(cmd.GetExtension(Command_ViewLogHistory::dev_ext), rc, false); - } default: return Response::RespFunctionNotAllowed; } @@ -597,73 +414,46 @@ int AbstractServerSocketInterface::getDeckPathId(const QString &path) return getDeckPathId(0, path.split("/")); } -bool AbstractServerSocketInterface::deckListHelper(int folderId, - ServerInfo_DeckStorage_Folder *folder, - int userId, - bool inheritedPublic, - bool publicOnly) +bool AbstractServerSocketInterface::deckListHelper(int folderId, ServerInfo_DeckStorage_Folder *folder) { - QSqlQuery *query = sqlInterface->prepareQuery("select id, name, is_public from {prefix}_decklist_folders where " - "id_parent = :id_parent and id_user = :id_user"); + QSqlQuery *query = sqlInterface->prepareQuery( + "select id, name from {prefix}_decklist_folders where id_parent = :id_parent and id_user = :id_user"); query->bindValue(":id_parent", folderId); - query->bindValue(":id_user", userId); + query->bindValue(":id_user", userInfo->id()); if (!sqlInterface->execSqlQuery(query)) { return false; } - QList>> folderRows; + QMap results; while (query->next()) { - folderRows.append({query->value(0).toInt(), {query->value(1).toString(), query->value(2).toBool()}}); + results[query->value(0).toInt()] = query->value(1).toString(); } - std::sort(folderRows.begin(), folderRows.end(), [](const auto &a, const auto &b) { return a.first < b.first; }); - - for (const auto &[folderIdValue, folderInfo] : folderRows) { - const QString name = folderInfo.first; - const bool ownPublic = folderInfo.second; - const bool effectivePublic = inheritedPublic || ownPublic; + for (int key : results.keys()) { ServerInfo_DeckStorage_TreeItem *newItem = folder->add_items(); - newItem->set_id(folderIdValue); - newItem->set_name(name.toStdString()); - newItem->mutable_folder()->set_is_public(ownPublic); + newItem->set_id(key); + newItem->set_name(results.value(key).toStdString()); - if (!deckListHelper(newItem->id(), newItem->mutable_folder(), userId, effectivePublic, publicOnly)) { + if (!deckListHelper(newItem->id(), newItem->mutable_folder())) { return false; } - - if (publicOnly && !effectivePublic && newItem->mutable_folder()->items_size() == 0) { - folder->mutable_items()->RemoveLast(); - } } - query = sqlInterface->prepareQuery("select id, name, upload_time, is_public, banner_card_name, " - "banner_card_provider, color_identity, tags from {prefix}_decklist_files where " - "id_folder = :id_folder and id_user = :id_user"); + query = sqlInterface->prepareQuery("select id, name, upload_time from {prefix}_decklist_files where id_folder = " + ":id_folder and id_user = :id_user"); query->bindValue(":id_folder", folderId); - query->bindValue(":id_user", userId); + query->bindValue(":id_user", userInfo->id()); if (!sqlInterface->execSqlQuery(query)) { return false; } while (query->next()) { - const bool ownPublic = query->value(3).toBool(); - if (publicOnly && !(inheritedPublic || ownPublic)) { - continue; - } - ServerInfo_DeckStorage_TreeItem *newItem = folder->add_items(); newItem->set_id(query->value(0).toInt()); newItem->set_name(query->value(1).toString().toStdString()); ServerInfo_DeckStorage_File *newFile = newItem->mutable_file(); newFile->set_creation_time(query->value(2).toDateTime().toSecsSinceEpoch()); - newFile->set_is_public(ownPublic); - newFile->set_banner_card_name(query->value(4).toString().toStdString()); - newFile->set_banner_card_provider(query->value(5).toString().toStdString()); - newFile->set_color_identity(query->value(6).toString().toStdString()); - for (const QString &tag : deserializeDeckTags(query->value(7).toString())) { - newFile->add_tags(tag.toStdString()); - } } return true; @@ -684,7 +474,7 @@ Response::ResponseCode AbstractServerSocketInterface::cmdDeckList(const Command_ Response_DeckList *re = new Response_DeckList; ServerInfo_DeckStorage_Folder *root = re->mutable_root(); - if (!deckListHelper(0, root, userInfo->id(), false, false)) { + if (!deckListHelper(0, root)) { return Response::RespContextError; } @@ -692,160 +482,6 @@ Response::ResponseCode AbstractServerSocketInterface::cmdDeckList(const Command_ return Response::RespOk; } -Response::ResponseCode AbstractServerSocketInterface::cmdDeckListOtherUser(const Command_DeckListOtherUser &cmd, - ResponseContainer &rc) -{ - if (authState != PasswordRight) { - return Response::RespFunctionNotAllowed; - } - - sqlInterface->checkSql(); - - const QString userName = nameFromStdString(cmd.user_name()); - const int userId = sqlInterface->getUserIdInDB(userName); - if (userId == -1) { - return Response::RespNameNotFound; - } - - Response_DeckList *re = new Response_DeckList; - ServerInfo_DeckStorage_Folder *root = re->mutable_root(); - - if (!deckListHelper(0, root, userId, false, true)) { - return Response::RespContextError; - } - - rc.setResponseExtension(re); - return Response::RespOk; -} - -int AbstractServerSocketInterface::getDeckOwnerId(int deckId) -{ - QSqlQuery *query = sqlInterface->prepareQuery("select id_user from {prefix}_decklist_files where id = :id"); - query->bindValue(":id", deckId); - if (!sqlInterface->execSqlQuery(query)) { - return -1; - } - if (!query->next()) { - return -1; - } - return query->value(0).toInt(); -} - -bool AbstractServerSocketInterface::isDeckEffectivelyPublic(int deckId) -{ - QSqlQuery *query = - sqlInterface->prepareQuery("select is_public, id_folder from {prefix}_decklist_files where id = :id"); - query->bindValue(":id", deckId); - if (!sqlInterface->execSqlQuery(query)) { - return false; - } - if (!query->next()) { - return false; - } - if (query->value(0).toBool()) { - return true; - } - - int folderId = query->value(1).toInt(); - int guard = 0; - while (folderId != 0 && guard < 100) { - QSqlQuery *folderQuery = - sqlInterface->prepareQuery("select is_public, id_parent from {prefix}_decklist_folders where id = :id"); - folderQuery->bindValue(":id", folderId); - if (!sqlInterface->execSqlQuery(folderQuery)) { - return false; - } - if (!folderQuery->next()) { - return false; - } - if (folderQuery->value(0).toBool()) { - return true; - } - folderId = folderQuery->value(1).toInt(); - ++guard; - } - return false; -} - -Response::ResponseCode AbstractServerSocketInterface::cmdDeckSetVisibility(const Command_DeckSetVisibility &cmd, - ResponseContainer & /*rc*/) -{ - if (authState != PasswordRight) { - return Response::RespFunctionNotAllowed; - } - - sqlInterface->checkSql(); - - if (cmd.has_deck_id()) { - QSqlQuery *query = - sqlInterface->prepareQuery("select 1 from {prefix}_decklist_files where id = :id and id_user = :id_user"); - query->bindValue(":id", cmd.deck_id()); - query->bindValue(":id_user", userInfo->id()); - sqlInterface->execSqlQuery(query); - if (!query->next()) { - return Response::RespNameNotFound; - } - - query = sqlInterface->prepareQuery("update {prefix}_decklist_files set is_public = :is_public where id = :id " - "and id_user = :id_user"); - query->bindValue(":is_public", cmd.is_public() ? 1 : 0); - query->bindValue(":id", cmd.deck_id()); - query->bindValue(":id_user", userInfo->id()); - if (!sqlInterface->execSqlQuery(query)) { - return Response::RespContextError; - } - } else if (cmd.has_folder_path()) { - const int folderId = getDeckPathId(nameFromStdString(cmd.folder_path())); - if (folderId == -1 || folderId == 0) { - return Response::RespNameNotFound; - } - - QSqlQuery *query = - sqlInterface->prepareQuery("update {prefix}_decklist_folders set is_public = :is_public where id = :id " - "and id_user = :id_user"); - query->bindValue(":is_public", cmd.is_public() ? 1 : 0); - query->bindValue(":id", folderId); - query->bindValue(":id_user", userInfo->id()); - if (!sqlInterface->execSqlQuery(query)) { - return Response::RespContextError; - } - } else { - return Response::RespInvalidData; - } - - return Response::RespOk; -} - -Response::ResponseCode AbstractServerSocketInterface::cmdDeckDownloadPublic(const Command_DeckDownloadPublic &cmd, - ResponseContainer &rc) -{ - if (authState != PasswordRight) { - return Response::RespFunctionNotAllowed; - } - - sqlInterface->checkSql(); - - const int deckId = cmd.deck_id(); - const int ownerId = getDeckOwnerId(deckId); - if (ownerId == -1 || !isDeckEffectivelyPublic(deckId)) { - return Response::RespNameNotFound; - } - - DeckList *deck; - try { - deck = sqlInterface->getDeckFromDatabase(deckId, ownerId); - } catch (Response::ResponseCode &r) { - return r; - } - - Response_DeckDownload *re = new Response_DeckDownload; - re->set_deck(deck->writeToString_Native().toStdString()); - rc.setResponseExtension(re); - delete deck; - - return Response::RespOk; -} - Response::ResponseCode AbstractServerSocketInterface::cmdDeckNewDir(const Command_DeckNewDir &cmd, ResponseContainer & /*rc*/) { @@ -954,22 +590,6 @@ Response::ResponseCode AbstractServerSocketInterface::cmdDeckDel(const Command_D return Response::RespOk; } -namespace -{ -/** @brief Keeps only the WUBRG colors from a color identity string, deduplicated. */ -QString sanitizeColorIdentity(const QString &colorIdentity) -{ - QString sanitized; - for (const QChar &color : colorIdentity) { - const QChar upper = color.toUpper(); - if (QStringLiteral("WUBRG").contains(upper) && !sanitized.contains(upper)) { - sanitized.append(upper); - } - } - return sanitized; -} -} // namespace - Response::ResponseCode AbstractServerSocketInterface::cmdDeckUpload(const Command_DeckUpload &cmd, ResponseContainer &rc) { @@ -994,14 +614,6 @@ Response::ResponseCode AbstractServerSocketInterface::cmdDeckUpload(const Comman deckName = "Unnamed deck"; } - // The server derives the banner card and tags from the deck itself. Only the - // color identity must come from the client, since the server has no card - // database to compute it. All values are bounded to the column sizes. - const QString bannerCardName = deck.getBannerCard().name.left(255); - const QString bannerCardProvider = deck.getBannerCard().providerId.left(32); - const QString tagsJson = serializeDeckTags(deck.getTags()); - const QString colorIdentity = sanitizeColorIdentity(nameFromStdString(cmd.color_identity())); - if (cmd.has_path()) { int folderId = getDeckPathId(nameFromStdString(cmd.path())); if (folderId == -1) { @@ -1010,74 +622,38 @@ Response::ResponseCode AbstractServerSocketInterface::cmdDeckUpload(const Comman QSqlQuery *query = sqlInterface->prepareQuery("insert into {prefix}_decklist_files (id_folder, id_user, name, upload_time, " - "content, is_public, banner_card_name, banner_card_provider, color_identity, " - "tags) values(:id_folder, :id_user, :name, NOW(), :content, :is_public, " - ":banner_card_name, :banner_card_provider, :color_identity, :tags)"); + "content) values(:id_folder, :id_user, :name, NOW(), :content)"); query->bindValue(":id_folder", folderId); query->bindValue(":id_user", userInfo->id()); query->bindValue(":name", deckName); query->bindValue(":content", deckStr); - query->bindValue(":is_public", cmd.has_is_public() && cmd.is_public() ? 1 : 0); - query->bindValue(":banner_card_name", bannerCardName); - query->bindValue(":banner_card_provider", bannerCardProvider); - query->bindValue(":color_identity", colorIdentity); - query->bindValue(":tags", tagsJson); - if (!sqlInterface->execSqlQuery(query)) { - return Response::RespContextError; - } + sqlInterface->execSqlQuery(query); Response_DeckUpload *re = new Response_DeckUpload; ServerInfo_DeckStorage_TreeItem *fileInfo = re->mutable_new_file(); fileInfo->set_id(query->lastInsertId().toInt()); fileInfo->set_name(deckName.toStdString()); fileInfo->mutable_file()->set_creation_time(QDateTime::currentDateTime().toSecsSinceEpoch()); - fileInfo->mutable_file()->set_is_public(cmd.has_is_public() && cmd.is_public()); rc.setResponseExtension(re); } else if (cmd.has_deck_id()) { - QString updateQuery = "update {prefix}_decklist_files set name=:name, upload_time=NOW(), content=:content, " - "banner_card_name=:banner_card_name, banner_card_provider=:banner_card_provider, " - "color_identity=:color_identity, tags=:tags"; - if (cmd.has_is_public()) { - updateQuery += ", is_public=:is_public"; - } - updateQuery += " where id = :id_deck and id_user = :id_user"; - - QSqlQuery *query = sqlInterface->prepareQuery(updateQuery); + QSqlQuery *query = + sqlInterface->prepareQuery("update {prefix}_decklist_files set name=:name, upload_time=NOW(), " + "content=:content where id = :id_deck and id_user = :id_user"); query->bindValue(":id_deck", cmd.deck_id()); query->bindValue(":id_user", userInfo->id()); query->bindValue(":name", deckName); query->bindValue(":content", deckStr); - query->bindValue(":banner_card_name", bannerCardName); - query->bindValue(":banner_card_provider", bannerCardProvider); - query->bindValue(":color_identity", colorIdentity); - query->bindValue(":tags", tagsJson); - if (cmd.has_is_public()) { - query->bindValue(":is_public", cmd.is_public() ? 1 : 0); - } - if (!sqlInterface->execSqlQuery(query)) { - return Response::RespContextError; - } + sqlInterface->execSqlQuery(query); if (query->numRowsAffected() == 0) { return Response::RespNameNotFound; } - QSqlQuery *visibilityQuery = - sqlInterface->prepareQuery("select is_public from {prefix}_decklist_files where id = :id and " - "id_user = :id_user"); - visibilityQuery->bindValue(":id", cmd.deck_id()); - visibilityQuery->bindValue(":id_user", userInfo->id()); - if (!sqlInterface->execSqlQuery(visibilityQuery)) { - return Response::RespContextError; - } - const bool isPublic = visibilityQuery->next() && visibilityQuery->value(0).toBool(); - Response_DeckUpload *re = new Response_DeckUpload; ServerInfo_DeckStorage_TreeItem *fileInfo = re->mutable_new_file(); fileInfo->set_id(cmd.deck_id()); fileInfo->set_name(deckName.toStdString()); fileInfo->mutable_file()->set_creation_time(QDateTime::currentDateTime().toSecsSinceEpoch()); - fileInfo->mutable_file()->set_is_public(isPublic); rc.setResponseExtension(re); } else { return Response::RespInvalidData; @@ -1108,248 +684,6 @@ Response::ResponseCode AbstractServerSocketInterface::cmdDeckDownload(const Comm return Response::RespOk; } -namespace -{ -/** @brief Builds a cryptographically random, URL-safe share token. */ -QString generateShareToken() -{ - QByteArray bytes(32, Qt::Uninitialized); - QRandomGenerator::system()->fillRange(reinterpret_cast(bytes.data()), bytes.size() / sizeof(quint32)); - return QString::fromLatin1(bytes.toBase64(QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals)); -} - -/** @brief Extracts the share metadata for a deck, materializing its content. */ -DeckShareItemRecord makeShareItemFromDeck(const DeckList &deck, const QString &colorIdentity) -{ - DeckShareItemRecord item; - item.name = deck.getName(); - if (item.name.isEmpty()) { - item.name = "Unnamed deck"; - } - item.tags = deck.getTags(); - item.bannerCard = deck.getBannerCard().name; - item.gameFormat = deck.getGameFormat(); - item.colorIdentity = sanitizeColorIdentity(colorIdentity); - item.content = deck.writeToString_Native(); - return item; -} -} // namespace - -Response::ResponseCode AbstractServerSocketInterface::cmdDeckShareCreate(const Command_DeckShareCreate &cmd, - ResponseContainer &rc) -{ - if (authState != PasswordRight) { - return Response::RespFunctionNotAllowed; - } - - sqlInterface->checkSql(); - - const int maxItems = servatrice->getDeckShareMaxDecksPerShare(); - if (maxItems > 0 && cmd.items_size() > maxItems) { - return Response::RespInvalidData; - } - - // Per-user rate limit so share links cannot be used to build an unbounded - // word-of-mouth leak of public decks. - const int maxSharesPerDay = servatrice->getDeckShareMaxSharesPerDay(); - if (maxSharesPerDay > 0) { - QSqlQuery *countQuery = sqlInterface->prepareQuery("select count(*) from {prefix}_deck_share where " - "created_by = :created_by and created_at >= " - "DATE_SUB(NOW(), INTERVAL 1 DAY)"); - countQuery->bindValue(":created_by", userInfo->id()); - if (!sqlInterface->execSqlQuery(countQuery) || !countQuery->next()) { - return Response::RespContextError; - } - if (countQuery->value(0).toInt() >= maxSharesPerDay) { - return Response::RespTooManyRequests; - } - } - - QList items; - if (cmd.items_size() > 0) { - for (const DeckShareItem &shareItem : cmd.items()) { - if (shareItem.has_deck_list()) { - DeckList deck; - if (!deck.loadFromString_Native(fileFromStdString(shareItem.deck_list()))) { - return Response::RespContextError; - } - items.append(makeShareItemFromDeck(deck, nameFromStdString(shareItem.color_identity()))); - } else if (shareItem.has_deck_id()) { - DeckList *deck; - try { - deck = sqlInterface->getDeckFromDatabase(shareItem.deck_id(), userInfo->id()); - } catch (Response::ResponseCode &r) { - return r; - } - items.append(makeShareItemFromDeck(*deck, nameFromStdString(shareItem.color_identity()))); - delete deck; - } else { - return Response::RespInvalidData; - } - } - } else if (cmd.has_folder_path()) { - const int folderId = getDeckPathId(nameFromStdString(cmd.folder_path())); - if (folderId == -1) { - return Response::RespNameNotFound; - } - - // Drain the deck list before resolving each deck: getDeckFromDatabase - // issues its own query on the same cached statement set. - QSqlQuery *query = - sqlInterface->prepareQuery("select id, color_identity from {prefix}_decklist_files where id_folder = " - ":id_folder and id_user = :id_user"); - query->bindValue(":id_folder", folderId); - query->bindValue(":id_user", userInfo->id()); - if (!sqlInterface->execSqlQuery(query)) { - return Response::RespContextError; - } - QList> deckRows; - while (query->next()) { - deckRows.append({query->value(0).toInt(), query->value(1).toString()}); - } - for (const auto &[deckId, colorIdentity] : deckRows) { - DeckList *deck; - try { - deck = sqlInterface->getDeckFromDatabase(deckId, userInfo->id()); - } catch (Response::ResponseCode &r) { - return r; - } - items.append(makeShareItemFromDeck(*deck, colorIdentity)); - delete deck; - } - } else { - return Response::RespInvalidData; - } - - if (items.isEmpty() || (maxItems > 0 && items.size() > maxItems)) { - return Response::RespInvalidData; - } - - QString shareName = nameFromStdString(cmd.name()); - if (shareName.isEmpty()) { - shareName = "Shared decks"; - } - - const QString token = generateShareToken(); - qint64 expiresAt = 0; - if (!sqlInterface->createDeckShare(token, shareName, userInfo->id(), items, servatrice->getDeckShareExpiryDays(), - expiresAt)) { - return Response::RespInvalidData; - } - - Response_DeckShareCreate *re = new Response_DeckShareCreate; - re->set_token(token.toStdString()); - re->set_expires_at(expiresAt); - re->set_item_count(items.size()); - rc.setResponseExtension(re); - - return Response::RespOk; -} - -Response::ResponseCode AbstractServerSocketInterface::cmdDeckShareListMine(const Command_DeckShareListMine & /*cmd*/, - ResponseContainer &rc) -{ - if (authState != PasswordRight) { - return Response::RespFunctionNotAllowed; - } - - sqlInterface->checkSql(); - - QList shares; - if (!sqlInterface->getDeckSharesForUser(userInfo->id(), shares)) { - return Response::RespContextError; - } - - Response_DeckShareListMine *re = new Response_DeckShareListMine; - for (const DeckShareSummaryRecord &share : shares) { - ServerInfo_DeckShareSummary *summary = re->add_shares(); - summary->set_id(share.id); - summary->set_name(share.name.toStdString()); - summary->set_creation_time(share.creationTime); - summary->set_expires_at(share.expiresAt); - summary->set_item_count(share.itemCount); - } - rc.setResponseExtension(re); - - return Response::RespOk; -} - -Response::ResponseCode AbstractServerSocketInterface::cmdDeckShareRemove(const Command_DeckShareRemove &cmd, - ResponseContainer & /*rc*/) -{ - if (authState != PasswordRight) { - return Response::RespFunctionNotAllowed; - } - - if (!cmd.has_share_id()) { - return Response::RespInvalidData; - } - - sqlInterface->checkSql(); - - if (!sqlInterface->deleteDeckShare(cmd.share_id(), userInfo->id())) { - return Response::RespNameNotFound; - } - - return Response::RespOk; -} - -Response::ResponseCode AbstractServerSocketInterface::cmdDeckShareList(const Command_DeckShareList &cmd, - ResponseContainer &rc) -{ - if (authState != PasswordRight) { - return Response::RespFunctionNotAllowed; - } - - sqlInterface->checkSql(); - - QString name; - qint64 expiresAt = 0; - QList items; - if (!sqlInterface->getDeckShareList(nameFromStdString(cmd.token()), name, expiresAt, items)) { - return Response::RespNameNotFound; - } - - Response_DeckShareList *re = new Response_DeckShareList; - re->set_name(name.toStdString()); - re->set_expires_at(expiresAt); - for (const DeckShareItemRecord &item : items) { - ServerInfo_DeckShareItem *itemInfo = re->add_items(); - itemInfo->set_id(item.id); - itemInfo->set_name(item.name.toStdString()); - for (const QString &tag : item.tags) { - itemInfo->add_tags(tag.toStdString()); - } - itemInfo->set_banner_card(item.bannerCard.toStdString()); - itemInfo->set_game_format(item.gameFormat.toStdString()); - itemInfo->set_color_identity(item.colorIdentity.toStdString()); - } - rc.setResponseExtension(re); - - return Response::RespOk; -} - -Response::ResponseCode AbstractServerSocketInterface::cmdDeckShareDownload(const Command_DeckShareDownload &cmd, - ResponseContainer &rc) -{ - if (authState != PasswordRight) { - return Response::RespFunctionNotAllowed; - } - - sqlInterface->checkSql(); - - QString content; - if (!sqlInterface->getDeckShareItem(nameFromStdString(cmd.token()), cmd.item_id(), content)) { - return Response::RespNameNotFound; - } - - Response_DeckShareDownload *re = new Response_DeckShareDownload; - re->set_deck(content.toStdString()); - rc.setResponseExtension(re); - - return Response::RespOk; -} - Response::ResponseCode AbstractServerSocketInterface::cmdReplayList(const Command_ReplayList & /*cmd*/, ResponseContainer &rc) { @@ -1562,10 +896,6 @@ Response::ResponseCode AbstractServerSocketInterface::cmdReplayGetCode(const Com Response::ResponseCode AbstractServerSocketInterface::cmdReplaySubmitCode(const Command_ReplaySubmitCode &cmd, ResponseContainer & /*rc*/) { - if (authState != PasswordRight) { - return Response::RespFunctionNotAllowed; - } - // code is of the form - QString code = QString::fromStdString(cmd.replay_code()); QStringList split = code.split("-"); @@ -1630,13 +960,12 @@ Response::ResponseCode AbstractServerSocketInterface::cmdReplaySubmitCode(const // MODERATOR FUNCTIONS. // May be called by admins and moderators. Permission is checked by the calling function. Response::ResponseCode AbstractServerSocketInterface::cmdGetLogHistory(const Command_ViewLogHistory &cmd, - ResponseContainer &rc, - bool allowPrivateChat) + ResponseContainer &rc) { QList messageList; QString userName = nameFromStdString(cmd.user_name()); - QString ipAddress = allowPrivateChat ? nameFromStdString(cmd.ip_address()) : QString(); + QString ipAddress = nameFromStdString(cmd.ip_address()); QString gameName = nameFromStdString(cmd.game_name()); QString gameID = nameFromStdString(cmd.game_id()); QString message = textFromStdString(cmd.message()); @@ -1651,21 +980,11 @@ Response::ResponseCode AbstractServerSocketInterface::cmdGetLogHistory(const Com if (nameFromStdString(cmd.log_location(i)).simplified() == "game") { gameType = true; } - if (nameFromStdString(cmd.log_location(i)).simplified() == "chat" && allowPrivateChat) { + if (nameFromStdString(cmd.log_location(i)).simplified() == "chat") { chatType = true; } } - // For callers that must not see private conversations, never leave the - // target-type filter empty: if the request only asked for "chat" (or for - // nothing at all) the query below would carry no target_type restriction - // and would return every row, private messages included. Fall back to the - // game/room diagnostics the caller is allowed to see. - if (!allowPrivateChat && !gameType && !roomType) { - gameType = true; - roomType = true; - } - int dateRange = cmd.date_range(); int maximumResults = cmd.maximum_results(); @@ -1675,11 +994,7 @@ Response::ResponseCode AbstractServerSocketInterface::cmdGetLogHistory(const Com QListIterator messageIterator(sqlInterface->getMessageLogHistory( userName, ipAddress, gameName, gameID, message, chatType, gameType, roomType, dateRange, maximumResults)); while (messageIterator.hasNext()) { - ServerInfo_ChatMessage chatMessage = messageIterator.next(); - if (!allowPrivateChat) { - chatMessage.clear_sender_ip(); - } - re->add_log_message()->CopyFrom(chatMessage); + re->add_log_message()->CopyFrom(messageIterator.next()); } } else { ServerInfo_ChatMessage chatMessage; @@ -1748,10 +1063,9 @@ Response::ResponseCode AbstractServerSocketInterface::cmdGetWarnList(const Comma Response_WarnList *re = new Response_WarnList; QString officialWarnings = settingsCache->value("server/officialwarnings").toString(); - const QList categories = parseWarningCategories(officialWarnings); - for (const WarningCategory &category : categories) { - re->add_warning(category.name.toStdString()); - re->add_warning_il(category.startingIl); + QStringList warningsList = officialWarnings.split(",", Qt::SkipEmptyParts); + for (const QString &warning : warningsList) { + re->add_warning(warning.toStdString()); } re->set_user_name(nameFromStdString(cmd.user_name()).toStdString()); re->set_user_clientid(nameFromStdString(cmd.user_clientid()).toStdString()); @@ -1935,1083 +1249,6 @@ Response::ResponseCode AbstractServerSocketInterface::cmdBanFromServer(const Com return Response::RespOk; } -Response::ResponseCode AbstractServerSocketInterface::cmdReportList(const Command_ReportList &cmd, - ResponseContainer &rc) -{ - if (!sqlInterface->checkSql()) { - return Response::RespInternalError; - } - - const bool unresolvedOnly = cmd.unresolved_only(); - const int offset = static_cast(cmd.offset()); - const int limit = qMin(static_cast(cmd.limit()), 1000); - - // Columns: 0=id, 1=reporter_name, 2=reported_user_name, 3=game_id, - // 4=category, 5=description, 6=created_at, 7=status, - // 8=resolution_note, 9=assigned_mod_name, - // 10=room_id, 11=replay_id - QString whereClause; - if (unresolvedOnly) { - whereClause = "WHERE r.status = 'open' OR r.status = 'assigned' "; - } - - // Total count query - QSqlQuery *countQuery = sqlInterface->prepareQuery("SELECT COUNT(*) FROM {prefix}_reports r " + whereClause); - int totalCount = 0; - if (sqlInterface->execSqlQuery(countQuery) && countQuery->next()) { - totalCount = countQuery->value(0).toInt(); - } - - QString queryStr = "SELECT r.id, r.reporter_name, r.reported_user_name, r.game_id, " - "r.category, r.description, r.created_at, r.status, " - "r.resolution_note, u.name AS assigned_mod_name, r.room_id, " - "(SELECT id FROM {prefix}_replays WHERE id_game = r.game_id ORDER BY id DESC LIMIT 1) " - "AS replay_id " - "FROM {prefix}_reports r " - "LEFT JOIN {prefix}_users u ON r.assigned_to = u.id "; - - if (unresolvedOnly) { - queryStr += "WHERE r.status = 'open' OR r.status = 'assigned' "; - } - - queryStr += "ORDER BY r.created_at DESC LIMIT :limit OFFSET :offset"; - - QSqlQuery *query = sqlInterface->prepareQuery(queryStr); - query->bindValue(":limit", limit); - query->bindValue(":offset", offset); - if (!sqlInterface->execSqlQuery(query)) { - return Response::RespInternalError; - } - - Response_ReportList *re = new Response_ReportList; - re->set_total_count(totalCount); - - while (query->next()) { - ServerInfo_Report *info = re->add_reports(); - - info->set_report_id(query->value(0).toInt()); - info->set_reporter_name(query->value(1).toString().toStdString()); - info->set_reported_user_name(query->value(2).toString().toStdString()); - - if (!query->value(3).isNull()) { - info->set_game_id(query->value(3).toInt()); - } - - info->set_category(query->value(4).toString().toStdString()); - info->set_description(query->value(5).toString().toStdString()); - info->set_report_time(query->value(6).toDateTime().toSecsSinceEpoch()); - info->set_status(query->value(7).toString().toStdString()); - - if (!query->value(8).isNull()) { - info->set_resolution_note(query->value(8).toString().toStdString()); - } - - if (!query->value(9).isNull()) { - info->set_assigned_mod_name(query->value(9).toString().toStdString()); - } - - if (!query->value(10).isNull()) { - info->set_room_id(query->value(10).toInt()); - } - - if (!query->value(11).isNull()) { - info->set_replay_id(query->value(11).toInt()); - } - } - - rc.setResponseExtension(re); - return Response::RespOk; -} - -Response::ResponseCode AbstractServerSocketInterface::cmdReportAssign(const Command_ReportAssign &cmd, - ResponseContainer & /*rc*/) -{ - if (!sqlInterface->checkSql()) { - return Response::RespInternalError; - } - - int reportId = cmd.report_id(); - int modId = userInfo->id(); - - QSqlQuery *lookupQuery = sqlInterface->prepareQuery("SELECT status FROM {prefix}_reports WHERE id = :id"); - lookupQuery->bindValue(":id", reportId); - - if (!sqlInterface->execSqlQuery(lookupQuery)) { - return Response::RespInternalError; - } - - if (!lookupQuery->next()) { - return Response::RespNameNotFound; - } - - if (lookupQuery->value(0).toString() != "open") { - return Response::RespInvalidData; - } - - QSqlQuery *query = sqlInterface->prepareQuery("UPDATE {prefix}_reports " - "SET status = 'assigned', assigned_to = :mod_id " - "WHERE id = :id AND status = 'open'"); - - query->bindValue(":mod_id", modId); - query->bindValue(":id", reportId); - - if (!sqlInterface->execSqlQuery(query)) { - return Response::RespInternalError; - } - - if (query->numRowsAffected() == 0) { - // The report was taken by another moderator between the lookup and this update. - return Response::RespInvalidData; - } - - return Response::RespOk; -} - -Response::ResponseCode AbstractServerSocketInterface::cmdReportResolve(const Command_ReportResolve &cmd, - ResponseContainer & /*rc*/) -{ - if (!sqlInterface->checkSql()) { - return Response::RespInternalError; - } - - int reportId = cmd.report_id(); - QString note = textFromStdString(cmd.resolution_note()); - bool dismissed = cmd.dismissed(); - - QString newStatus = dismissed ? "dismissed" : "resolved"; - - QSqlQuery *query = sqlInterface->prepareQuery("UPDATE {prefix}_reports " - "SET status = :status, resolution_note = :note, " - "resolution_time = NOW(), resolved_by = :mod_id " - "WHERE id = :id AND status IN ('open', 'assigned')"); - - query->bindValue(":status", newStatus); - query->bindValue(":note", note); - query->bindValue(":mod_id", userInfo->id()); - query->bindValue(":id", reportId); - - if (!sqlInterface->execSqlQuery(query)) { - return Response::RespInternalError; - } - - if (query->numRowsAffected() == 0) { - return Response::RespInvalidData; - } - - sqlInterface->addAuditRecord(QString::number(reportId), this->getAddress(), - QString::fromStdString(userInfo->clientid()), - dismissed ? "REPORT_DISMISSED" : "REPORT_RESOLVED", - QString("Report #%1 %2").arg(reportId).arg(newStatus), true); - - // Notifying the reporter is best-effort: the resolve already succeeded. If any of the lookup - // queries below fail, the report is simply left with notified = 0 and the notification is - // delivered later via sendPendingReportNotifications. - QSqlQuery *lookupQuery = - sqlInterface->prepareQuery("SELECT reporter_id, reported_user_name FROM {prefix}_reports WHERE id = :id"); - lookupQuery->bindValue(":id", reportId); - - QString reporterName; - QString reportedUser; - if (sqlInterface->execSqlQuery(lookupQuery) && lookupQuery->next()) { - int reporterId = lookupQuery->value(0).toInt(); - reportedUser = lookupQuery->value(1).toString(); - - QSqlQuery *nameQuery = sqlInterface->prepareQuery("SELECT name FROM {prefix}_users WHERE id = :id"); - nameQuery->bindValue(":id", reporterId); - if (sqlInterface->execSqlQuery(nameQuery) && nameQuery->next()) { - reporterName = nameQuery->value(0).toString(); - } - } - - const QString ownName = QString::fromStdString(userInfo->name()); - if (!reporterName.isEmpty()) { - if (reporterName == ownName) { - // Resolving your own report: no self-notification needed, but mark it as notified - // so it is not delivered on the next login via sendPendingReportNotifications. - QSqlQuery *notifiedQuery = - sqlInterface->prepareQuery("UPDATE {prefix}_reports SET notified = 1 WHERE id = :id"); - notifiedQuery->bindValue(":id", reportId); - sqlInterface->execSqlQuery(notifiedQuery); - } else { - QReadLocker clientsLocker(&servatrice->clientsLock); - AbstractServerSocketInterface *reporter = - static_cast(server->getUsers().value(reporterName)); - if (reporter) { - Event_NotifyUser event; - event.set_type(Event_NotifyUser::REPORT_RESOLVED); - event.set_custom_title(dismissed ? tr("Report Dismissed").toStdString() - : tr("Report Resolved").toStdString()); - - QString content = dismissed ? tr("Your report about %1 has been dismissed.").arg(reportedUser) - : tr("Your report about %1 has been resolved.").arg(reportedUser); - if (!note.isEmpty()) { - content += "\n" + tr("Note: %1").arg(note); - } - event.set_custom_content(content.toStdString()); - - SessionEvent *se = reporter->prepareSessionEvent(event); - reporter->sendProtocolItem(*se); - delete se; - - // Only mark the report as notified if the reporter is still connected; otherwise the - // notification is delivered on their next login via sendPendingReportNotifications. - QSqlQuery *notifiedQuery = - sqlInterface->prepareQuery("UPDATE {prefix}_reports SET notified = 1 WHERE id = :id"); - notifiedQuery->bindValue(":id", reportId); - sqlInterface->execSqlQuery(notifiedQuery); - } - } - } - - return Response::RespOk; -} - -Response::ResponseCode AbstractServerSocketInterface::cmdReportUserInfo(const Command_ReportUserInfo &cmd, - ResponseContainer &rc) -{ - if (!sqlInterface->checkSql()) { - return Response::RespInternalError; - } - - QString userName = nameFromStdString(cmd.user_name()); - - QSqlQuery *userQuery = sqlInterface->prepareQuery("SELECT u.admin, u.active, u.registrationDate, u.adminnotes " - "FROM {prefix}_users u " - "WHERE u.name = :name"); - userQuery->bindValue(":name", userName); - - if (!sqlInterface->execSqlQuery(userQuery)) { - return Response::RespInternalError; - } - - if (!userQuery->next()) { - return Response::RespNameNotFound; - } - - Response_ReportUserInfo *re = new Response_ReportUserInfo; - re->set_user_name(cmd.user_name()); - - re->set_is_admin(userQuery->value(0).toBool()); - re->set_is_active(userQuery->value(1).toBool()); - re->set_registration_time(userQuery->value(2).toDateTime().toSecsSinceEpoch()); - - if (!userQuery->value(3).isNull()) { - re->set_admin_notes(userQuery->value(3).toString().toStdString()); - } - - QSqlQuery *reportCountQuery = - sqlInterface->prepareQuery("SELECT COUNT(*) FROM {prefix}_reports WHERE reported_user_name = :name"); - reportCountQuery->bindValue(":name", userName); - if (sqlInterface->execSqlQuery(reportCountQuery) && reportCountQuery->next()) { - re->set_total_reports(reportCountQuery->value(0).toInt()); - } - - QSqlQuery *banCountQuery = sqlInterface->prepareQuery("SELECT COUNT(*) FROM {prefix}_bans WHERE user_name = :name"); - banCountQuery->bindValue(":name", userName); - if (sqlInterface->execSqlQuery(banCountQuery) && banCountQuery->next()) { - re->set_total_bans(banCountQuery->value(0).toInt()); - } - - QSqlQuery *warnCountQuery = - sqlInterface->prepareQuery("SELECT COUNT(*) FROM {prefix}_warnings WHERE user_name = :name"); - warnCountQuery->bindValue(":name", userName); - if (sqlInterface->execSqlQuery(warnCountQuery) && warnCountQuery->next()) { - re->set_total_warns(warnCountQuery->value(0).toInt()); - } - - QSqlQuery *lastLoginQuery = sqlInterface->prepareQuery("SELECT UNIX_TIMESTAMP(a.last_login) " - "FROM {prefix}_user_analytics a " - "JOIN {prefix}_users u ON u.id = a.id " - "WHERE u.name = :name"); - lastLoginQuery->bindValue(":name", userName); - if (sqlInterface->execSqlQuery(lastLoginQuery) && lastLoginQuery->next() && !lastLoginQuery->value(0).isNull()) { - re->set_last_login(lastLoginQuery->value(0).toLongLong()); - } - - QSqlQuery *recentQuery = - sqlInterface->prepareQuery("SELECT r.id, r.reporter_name, r.reported_user_name, r.game_id, " - "r.category, r.description, r.created_at, r.status, " - "r.resolution_note, ru.name AS assigned_mod_name " - "FROM {prefix}_reports r " - "LEFT JOIN {prefix}_users ru ON r.assigned_to = ru.id " - "WHERE r.reported_user_name = :name " - "ORDER BY r.created_at DESC LIMIT 10"); - recentQuery->bindValue(":name", userName); - - if (sqlInterface->execSqlQuery(recentQuery)) { - while (recentQuery->next()) { - ServerInfo_Report *info = re->add_recent_reports(); - info->set_report_id(recentQuery->value(0).toInt()); - info->set_reporter_name(recentQuery->value(1).toString().toStdString()); - info->set_reported_user_name(recentQuery->value(2).toString().toStdString()); - - if (!recentQuery->value(3).isNull()) { - info->set_game_id(recentQuery->value(3).toInt()); - } - - info->set_category(recentQuery->value(4).toString().toStdString()); - info->set_description(recentQuery->value(5).toString().toStdString()); - info->set_report_time(recentQuery->value(6).toDateTime().toSecsSinceEpoch()); - info->set_status(recentQuery->value(7).toString().toStdString()); - - if (!recentQuery->value(8).isNull()) { - info->set_resolution_note(recentQuery->value(8).toString().toStdString()); - } - - if (!recentQuery->value(9).isNull()) { - info->set_assigned_mod_name(recentQuery->value(9).toString().toStdString()); - } - } - } - - rc.setResponseExtension(re); - return Response::RespOk; -} - -Response::ResponseCode AbstractServerSocketInterface::cmdGetServerStats(const Command_GetServerStats & /*cmd */, - ResponseContainer &rc) -{ - if (!sqlInterface->checkSql()) { - return Response::RespInternalError; - } - - // Servatrice::statusUpdate() periodically snapshots server health into the - // uptime table. Serve the freshest snapshot for this server. - const auto snapshot = sqlInterface->getLatestUptimeSnapshot(servatrice->getServerID()); - if (!snapshot.valid) { - // No snapshot yet (fresh server, or statusUpdate() has not ticked). - return Response::RespInternalError; - } - - auto *re = new Response_GetServerStats; - re->set_users_count(snapshot.usersCount); - re->set_mods_count(snapshot.modsCount); - re->set_games_count(snapshot.gamesCount); - re->set_tx_bytes(snapshot.txBytes); - re->set_rx_bytes(snapshot.rxBytes); - re->set_uptime_secs(snapshot.uptimeSecs); - re->set_timest(snapshot.timest); - - // Live metrics from the in-process MetricsRegistry (resets on server restart) - re->set_cards_in_games(static_cast(servatrice->getCardsInGamesTotal())); - re->set_eventloop_stalls_total(static_cast(servatrice->getEventLoopStallsTotal())); - re->set_eventloop_last_stall_ms(static_cast(servatrice->getEventLoopLastStallMs())); - re->set_eventloop_max_stall_ms(static_cast(servatrice->getEventLoopMaxStallMs())); - re->set_total_commands(static_cast(servatrice->getMetricsRegistry().totalCommands())); - re->set_total_command_time_ms( - static_cast(servatrice->getMetricsRegistry().totalTimeMs())); - re->set_active_command_types(servatrice->getMetricsRegistry().activeTypeCount()); - - const auto gameStart = servatrice->getMetricsRegistry().getGameStartSnapshot(); - re->set_game_start_count(static_cast(gameStart.count)); - re->set_game_start_total_ms(static_cast(gameStart.totalMs)); - - // Per-command breakdown: resolve protobuf extension names via the descriptor pool - static const char *messageNames[] = {"SessionCommand", "RoomCommand", "GameCommand", - "ModeratorCommand", "AdminCommand", "DeveloperCommand"}; - const auto activeStats = servatrice->getMetricsRegistry().collectActiveStats(); - for (const auto &stat : activeStats) { - const int kind = stat.typeId / MetricsRegistry::KindStride; - const int number = stat.typeId % MetricsRegistry::KindStride; - - QString label; - if (kind >= 0 && kind < MetricsRegistry::NumKinds) { - const google::protobuf::DescriptorPool *pool = google::protobuf::DescriptorPool::generated_pool(); - const google::protobuf::Descriptor *message = pool->FindMessageTypeByName(messageNames[kind]); - const google::protobuf::FieldDescriptor *extension = - message ? pool->FindExtensionByNumber(message, number) : nullptr; - if (extension) { - label = QString::fromLatin1(MetricsRegistry::KindNames[kind]) + QStringLiteral("/") + - QString::fromStdString(std::string(extension->message_type()->name())); - } - } - if (label.isEmpty()) { - label = QString::number(stat.typeId); - } - - CommandStats *cs = re->add_command_stats(); - cs->set_kind_index(static_cast(kind)); - cs->set_extension_number(static_cast(number)); - cs->set_command_name(label.toStdString()); - cs->set_count(static_cast(stat.count)); - cs->set_total_ms(static_cast(stat.totalMs)); - } - - rc.setResponseExtension(re); - return Response::RespOk; -} - -Response::ResponseCode AbstractServerSocketInterface::cmdReportStats(const Command_ReportStats & /*cmd */, - ResponseContainer &rc) -{ - if (!sqlInterface->checkSql()) { - return Response::RespInternalError; - } - - if (const auto cached = servatrice->getCachedReportStats()) { - rc.setResponseExtension(new Response_ReportStats(*cached)); - return Response::RespOk; - } - - Response_ReportStats *re = new Response_ReportStats; - - QSqlQuery *overviewQuery = sqlInterface->prepareQuery( - "SELECT COUNT(*) AS total, " - "SUM(status IN ('open', 'assigned')) AS pending, " - "SUM(status = 'assigned') AS assigned, " - "SUM(status IN ('resolved', 'dismissed')) AS resolved, " - "SUM(created_at >= DATE_SUB(NOW(), INTERVAL 1 DAY)) AS last24h, " - "SUM(created_at >= DATE_SUB(NOW(), INTERVAL 7 DAY)) AS last7d, " - "SUM(created_at >= DATE_SUB(NOW(), INTERVAL 30 DAY)) AS last30d, " - "SUM(created_at >= DATE_SUB(CURDATE(), INTERVAL WEEKDAY(CURDATE()) DAY)) AS this_week " - "FROM {prefix}_reports"); - if (!sqlInterface->execSqlQuery(overviewQuery) || !overviewQuery->next()) { - delete re; - return Response::RespInternalError; - } - - re->set_total_reports(overviewQuery->value(0).toInt()); - re->set_total_pending(overviewQuery->value(1).toInt()); - re->set_total_assigned(overviewQuery->value(2).toInt()); - re->set_total_resolved(overviewQuery->value(3).toInt()); - re->set_reports_last_24h(overviewQuery->value(4).toInt()); - re->set_reports_last_7d(overviewQuery->value(5).toInt()); - re->set_reports_last_30d(overviewQuery->value(6).toInt()); - re->set_reports_this_week(overviewQuery->value(7).toInt()); - - bool allQueriesOk = true; - - QSqlQuery *lastWeekQuery = - sqlInterface->prepareQuery("SELECT COUNT(*) FROM {prefix}_reports WHERE created_at >= DATE_SUB(CURDATE(), " - "INTERVAL WEEKDAY(CURDATE()) + 7 DAY) " - "AND created_at < DATE_SUB(CURDATE(), INTERVAL WEEKDAY(CURDATE()) DAY)"); - if (sqlInterface->execSqlQuery(lastWeekQuery) && lastWeekQuery->next()) { - re->set_reports_last_week(lastWeekQuery->value(0).toInt()); - } else { - allQueriesOk = false; - } - - QSqlQuery *avgResQuery = sqlInterface->prepareQuery( - "SELECT AVG(TIMESTAMPDIFF(HOUR, created_at, resolution_time)) " - "FROM {prefix}_reports WHERE status IN ('resolved','dismissed') AND resolution_time IS NOT NULL"); - if (sqlInterface->execSqlQuery(avgResQuery) && avgResQuery->next()) { - if (!avgResQuery->value(0).isNull()) { - re->set_avg_resolution_hours(avgResQuery->value(0).toDouble()); - } - } else { - allQueriesOk = false; - } - - QSqlQuery *catQuery = sqlInterface->prepareQuery( - "SELECT category, COUNT(*) AS cnt FROM {prefix}_reports GROUP BY category ORDER BY cnt DESC LIMIT 10"); - if (sqlInterface->execSqlQuery(catQuery)) { - while (catQuery->next()) { - ReportCategoryCount *cc = re->add_category_counts(); - cc->set_category(catQuery->value(0).toString().toStdString()); - cc->set_count(catQuery->value(1).toInt()); - } - } else { - allQueriesOk = false; - } - - QSqlQuery *topReportedQuery = - sqlInterface->prepareQuery("SELECT reported_user_name, COUNT(*) AS cnt FROM {prefix}_reports " - "GROUP BY reported_user_name ORDER BY cnt DESC LIMIT 10"); - if (sqlInterface->execSqlQuery(topReportedQuery)) { - while (topReportedQuery->next()) { - ReportTopUser *tu = re->add_top_reported_users(); - tu->set_user_name(topReportedQuery->value(0).toString().toStdString()); - tu->set_count(topReportedQuery->value(1).toInt()); - } - } else { - allQueriesOk = false; - } - - QSqlQuery *topReporterQuery = - sqlInterface->prepareQuery("SELECT reporter_name, COUNT(*) AS cnt FROM {prefix}_reports " - "GROUP BY reporter_name ORDER BY cnt DESC LIMIT 10"); - if (sqlInterface->execSqlQuery(topReporterQuery)) { - while (topReporterQuery->next()) { - ReportTopUser *tu = re->add_top_reporters(); - tu->set_user_name(topReporterQuery->value(0).toString().toStdString()); - tu->set_count(topReporterQuery->value(1).toInt()); - } - } else { - allQueriesOk = false; - } - - if (allQueriesOk) { - servatrice->cacheReportStats(*re); - } - - rc.setResponseExtension(re); - return Response::RespOk; -} - -Response::ResponseCode AbstractServerSocketInterface::cmdGetUserSessions(const Command_GetUserSessions &cmd, - ResponseContainer &rc) -{ - if (!sqlInterface->checkSql()) { - return Response::RespInternalError; - } - - const QString userName = nameFromStdString(cmd.user_name()); - if (userName.isEmpty()) { - return Response::RespContextError; - } - - Response_UserSessions *re = new Response_UserSessions; - const int limit = qMin(static_cast(cmd.limit()), 500); - const QList sessions = sqlInterface->getUserSessions(userName, limit); - for (const ServerInfo_UserSession &session : sessions) { - re->add_sessions()->CopyFrom(session); - } - rc.setResponseExtension(re); - return Response::RespOk; -} - -Response::ResponseCode AbstractServerSocketInterface::cmdGetUserAlts(const Command_GetUserAlts &cmd, - ResponseContainer &rc) -{ - if (!sqlInterface->checkSql()) { - return Response::RespInternalError; - } - - const QString userName = nameFromStdString(cmd.user_name()); - if (userName.isEmpty()) { - return Response::RespContextError; - } - - Response_UserAlts *re = new Response_UserAlts; - const QList alts = sqlInterface->getUserAlts(userName); - for (const ServerInfo_UserAlt &alt : alts) { - re->add_alts()->CopyFrom(alt); - } - rc.setResponseExtension(re); - return Response::RespOk; -} - -Response::ResponseCode AbstractServerSocketInterface::cmdGetModeratorLastLogins(const Command_GetModeratorLastLogins &, - ResponseContainer &rc) -{ - if (!sqlInterface->checkSql()) { - return Response::RespInternalError; - } - - Response_ModeratorLastLogins *re = new Response_ModeratorLastLogins; - const QList logins = sqlInterface->getModeratorLastLogins(); - for (const ServerInfo_ModeratorLogin &login : logins) { - re->add_logins()->CopyFrom(login); - } - rc.setResponseExtension(re); - return Response::RespOk; -} - -Response::ResponseCode AbstractServerSocketInterface::cmdResetUserPassword(const Command_ResetUserPassword &cmd, - ResponseContainer &rc) -{ - if (!sqlInterface->checkSql()) { - return Response::RespInternalError; - } - - const QString userName = nameFromStdString(cmd.user_name()).simplified(); - if (userName.isEmpty()) { - return Response::RespContextError; - } - - // Look up the target user's privilege level to prevent escalation. - QSqlQuery *privQuery = sqlInterface->prepareQuery("SELECT admin FROM {prefix}_users WHERE name = :name"); - privQuery->bindValue(":name", userName); - if (!sqlInterface->execSqlQuery(privQuery) || !privQuery->next()) { - return Response::RespNameNotFound; - } - const int targetAdmin = privQuery->value(0).toInt(); - const bool targetIsAdmin = targetAdmin & 1; - const bool targetIsMod = targetAdmin & 2; - - const bool callerIsAdmin = userInfo->user_level() & ServerInfo_User::IsAdmin; - - // A moderator must not reset the password of an admin or another moderator. - if (!callerIsAdmin && (targetIsAdmin || targetIsMod)) { - return Response::RespAccessDenied; - } - - const QString tempPassword = PasswordHasher::generateRandomSalt(); - if (!sqlInterface->changeUserPassword(userName, tempPassword, true)) { - return Response::RespInternalError; - } - sqlInterface->setForcePasswordChange(userName, true); - - sqlInterface->addAuditRecord(userName, this->getAddress(), QString::fromStdString(userInfo->clientid()), - "PASSWORD_RESET", "Admin password reset", true); - - // Notify the affected user if they are currently online. - QReadLocker clientsLocker(&servatrice->clientsLock); - AbstractServerSocketInterface *targetSession = - static_cast(server->getUsers().value(userName)); - if (targetSession) { - Event_NotifyUser event; - event.set_type(Event_NotifyUser::CUSTOM); - event.set_custom_title(tr("Password Reset").toStdString()); - event.set_custom_content(tr("An administrator has reset your password. Please log in with the new " - "password provided to you and change it immediately.") - .toStdString()); - SessionEvent *se = targetSession->prepareSessionEvent(event); - targetSession->sendProtocolItem(*se); - delete se; - } - - Response_ResetUserPassword *re = new Response_ResetUserPassword; - re->set_user_name(userName.toStdString()); - re->set_temporary_password(tempPassword.toStdString()); - rc.setResponseExtension(re); - return Response::RespOk; -} - -Response::ResponseCode AbstractServerSocketInterface::cmdRemoveUserAvatar(const Command_RemoveUserAvatar &cmd, - ResponseContainer &rc) -{ - if (!sqlInterface->checkSql()) { - return Response::RespInternalError; - } - - const QString userName = nameFromStdString(cmd.user_name()).simplified(); - if (userName.isEmpty()) { - return Response::RespContextError; - } - - if (!sqlInterface->removeUserAvatar(userName)) { - return Response::RespInternalError; - } - - sqlInterface->addAuditRecord(userName, this->getAddress(), QString::fromStdString(userInfo->clientid()), - "REMOVE_USER_AVATAR", "Moderator removed user avatar", true); - - Response_RemoveUserAvatar *re = new Response_RemoveUserAvatar; - re->set_user_name(userName.toStdString()); - rc.setResponseExtension(re); - return Response::RespOk; -} - -void AbstractServerSocketInterface::sendPendingReportNotifications(ResponseContainer &rc) -{ - if (!sqlInterface->checkSql()) { - return; - } - - QSqlQuery *query = sqlInterface->prepareQuery("SELECT id, reported_user_name, status, resolution_note " - "FROM {prefix}_reports " - "WHERE reporter_id = :reporter_id AND notified = 0 " - "AND status IN ('resolved', 'dismissed')"); - query->bindValue(":reporter_id", userInfo->id()); - - if (!sqlInterface->execSqlQuery(query)) { - return; - } - - while (query->next()) { - const int reportId = query->value(0).toInt(); - const QString reportedUser = query->value(1).toString(); - const bool dismissed = query->value(2).toString() == "dismissed"; - const QString note = query->value(3).toString(); - - Event_NotifyUser event; - event.set_type(Event_NotifyUser::REPORT_RESOLVED); - event.set_custom_title(dismissed ? tr("Report Dismissed").toStdString() : tr("Report Resolved").toStdString()); - - QString content = dismissed ? tr("Your report about %1 has been dismissed.").arg(reportedUser) - : tr("Your report about %1 has been resolved.").arg(reportedUser); - if (!note.isEmpty()) { - content += "\n" + tr("Note: %1").arg(note); - } - event.set_custom_content(content.toStdString()); - - rc.enqueuePreResponseItem(ServerMessage::SESSION_EVENT, prepareSessionEvent(event)); - - QSqlQuery *notifiedQuery = - sqlInterface->prepareQuery("UPDATE {prefix}_reports SET notified = 1 WHERE id = :id"); - notifiedQuery->bindValue(":id", reportId); - sqlInterface->execSqlQuery(notifiedQuery); - } - - QSqlQuery *commentQuery = sqlInterface->prepareQuery("SELECT c.id, c.report_id, c.author_name, c.comment_text " - "FROM {prefix}_report_comments c " - "JOIN {prefix}_reports r ON r.id = c.report_id " - "WHERE c.notified = 0 " - "AND c.author_id != :user_id " - "AND ((c.is_moderator = 1 AND r.reporter_id = :user_id) " - "OR (c.is_moderator = 0 AND r.assigned_to = :user_id) " - "OR (c.is_moderator = 1 AND r.assigned_to = :user_id)) " - "ORDER BY c.created_at ASC"); - commentQuery->bindValue(":user_id", userInfo->id()); - - if (!sqlInterface->execSqlQuery(commentQuery)) { - return; - } - - while (commentQuery->next()) { - const qlonglong commentId = commentQuery->value(0).toLongLong(); - const int reportId = commentQuery->value(1).toInt(); - const QString authorName = commentQuery->value(2).toString(); - const QString commentText = commentQuery->value(3).toString(); - - Event_NotifyUser event; - event.set_type(Event_NotifyUser::REPORT_COMMENT); - event.set_custom_title(tr("New Comment on Report #%1").arg(reportId).toStdString()); - event.set_custom_content(tr("%1 commented:\n%2").arg(authorName, commentText).toStdString()); - - rc.enqueuePreResponseItem(ServerMessage::SESSION_EVENT, prepareSessionEvent(event)); - - QSqlQuery *notifiedQuery = - sqlInterface->prepareQuery("UPDATE {prefix}_report_comments SET notified = 1 WHERE id = :id"); - notifiedQuery->bindValue(":id", commentId); - sqlInterface->execSqlQuery(notifiedQuery); - } -} - -void AbstractServerSocketInterface::onLogin(ResponseContainer &rc) -{ - sendPendingReportNotifications(rc); -} - -Response::ResponseCode AbstractServerSocketInterface::cmdReportMyList(const Command_ReportMyList & /*cmd */, - ResponseContainer &rc) -{ - if (authState != PasswordRight) { - return Response::RespFunctionNotAllowed; - } - - if (!sqlInterface->checkSql()) { - return Response::RespInternalError; - } - - sendPendingReportNotifications(rc); - - QString queryStr = "SELECT r.id, r.reporter_name, r.reported_user_name, r.game_id, " - "r.category, r.description, r.created_at, r.status, " - "r.resolution_note, u.name AS assigned_mod_name, r.room_id, " - "(SELECT id FROM {prefix}_replays WHERE id_game = r.game_id ORDER BY id DESC LIMIT 1) " - "AS replay_id " - "FROM {prefix}_reports r " - "LEFT JOIN {prefix}_users u ON r.assigned_to = u.id " - "WHERE r.reporter_id = :reporter_id " - "ORDER BY r.created_at DESC LIMIT 200"; - - QSqlQuery *query = sqlInterface->prepareQuery(queryStr); - query->bindValue(":reporter_id", userInfo->id()); - - if (!sqlInterface->execSqlQuery(query)) { - return Response::RespInternalError; - } - - Response_ReportMyList *re = new Response_ReportMyList; - - while (query->next()) { - ServerInfo_Report *info = re->add_reports(); - - info->set_report_id(query->value(0).toInt()); - info->set_reporter_name(query->value(1).toString().toStdString()); - info->set_reported_user_name(query->value(2).toString().toStdString()); - - if (!query->value(3).isNull()) { - info->set_game_id(query->value(3).toInt()); - } - - info->set_category(query->value(4).toString().toStdString()); - info->set_description(query->value(5).toString().toStdString()); - info->set_report_time(query->value(6).toDateTime().toSecsSinceEpoch()); - info->set_status(query->value(7).toString().toStdString()); - - if (!query->value(8).isNull()) { - info->set_resolution_note(query->value(8).toString().toStdString()); - } - - if (!query->value(9).isNull()) { - info->set_assigned_mod_name(query->value(9).toString().toStdString()); - } - - if (!query->value(10).isNull()) { - info->set_room_id(query->value(10).toInt()); - } - - if (!query->value(11).isNull()) { - info->set_replay_id(query->value(11).toInt()); - } - } - - rc.setResponseExtension(re); - return Response::RespOk; -} - -Response::ResponseCode AbstractServerSocketInterface::cmdReportDetails(const Command_ReportDetails &cmd, - ResponseContainer &rc) -{ - if (authState != PasswordRight) { - return Response::RespFunctionNotAllowed; - } - - if (!sqlInterface->checkSql()) { - return Response::RespInternalError; - } - - const int reportId = cmd.report_id(); - - // Columns: 0=id, 1=reporter_id, 2=reporter_name, 3=reported_user_name, 4=game_id, - // 5=category, 6=description, 7=created_at, 8=status, - // 9=resolution_note, 10=assigned_mod_name, 11=chat_log, - // 12=room_id, 13=resolution_time, 14=replay_id - QString queryStr = "SELECT r.id, r.reporter_id, r.reporter_name, r.reported_user_name, r.game_id, " - "r.category, r.description, r.created_at, r.status, " - "r.resolution_note, u.name AS assigned_mod_name, r.chat_log, r.room_id, " - "r.resolution_time, " - "(SELECT id FROM {prefix}_replays WHERE id_game = r.game_id ORDER BY id DESC LIMIT 1) " - "AS replay_id " - "FROM {prefix}_reports r " - "LEFT JOIN {prefix}_users u ON r.assigned_to = u.id " - "WHERE r.id = :id"; - QSqlQuery *query = sqlInterface->prepareQuery(queryStr); - query->bindValue(":id", reportId); - - if (!sqlInterface->execSqlQuery(query)) { - return Response::RespInternalError; - } - - if (!query->next()) { - return Response::RespNameNotFound; - } - - const bool isMod = userInfo->user_level() & ServerInfo_User::IsModerator; - const int reporterId = query->value(1).toInt(); - if (reporterId != userInfo->id() && !isMod) { - return Response::RespAccessDenied; - } - - ServerInfo_Report *info = new ServerInfo_Report; - info->set_report_id(query->value(0).toInt()); - info->set_reporter_name(query->value(2).toString().toStdString()); - info->set_reported_user_name(query->value(3).toString().toStdString()); - - if (!query->value(4).isNull()) { - info->set_game_id(query->value(4).toInt()); - } - - info->set_category(query->value(5).toString().toStdString()); - info->set_description(query->value(6).toString().toStdString()); - info->set_report_time(query->value(7).toDateTime().toSecsSinceEpoch()); - info->set_status(query->value(8).toString().toStdString()); - - if (!query->value(9).isNull()) { - info->set_resolution_note(query->value(9).toString().toStdString()); - } - - if (!query->value(10).isNull()) { - info->set_assigned_mod_name(query->value(10).toString().toStdString()); - } - - if (!query->value(11).isNull()) { - info->set_chat_log(query->value(11).toString().toStdString()); - } - - if (!query->value(12).isNull()) { - info->set_room_id(query->value(12).toInt()); - } - - if (!query->value(13).isNull()) { - info->set_resolution_time(query->value(13).toDateTime().toSecsSinceEpoch()); - } - - if (!query->value(14).isNull()) { - info->set_replay_id(query->value(14).toInt()); - } - - QSqlQuery *commentQuery = - sqlInterface->prepareQuery("SELECT c.author_name, c.comment_text, c.created_at, c.is_moderator " - "FROM {prefix}_report_comments c " - "WHERE c.report_id = :report_id ORDER BY c.created_at ASC"); - commentQuery->bindValue(":report_id", reportId); - - if (sqlInterface->execSqlQuery(commentQuery)) { - while (commentQuery->next()) { - ServerInfo_ReportComment *comment = info->add_comments(); - comment->set_author_name(commentQuery->value(0).toString().toStdString()); - comment->set_comment_text(commentQuery->value(1).toString().toStdString()); - comment->set_comment_time(commentQuery->value(2).toDateTime().toSecsSinceEpoch()); - comment->set_is_moderator(commentQuery->value(3).toBool()); - } - } - - Response_ReportDetails *re = new Response_ReportDetails; - re->set_allocated_report(info); - rc.setResponseExtension(re); - return Response::RespOk; -} - -Response::ResponseCode AbstractServerSocketInterface::cmdReportAddComment(const Command_ReportAddComment &cmd, - ResponseContainer & /*rc*/) -{ - if (authState != PasswordRight) { - return Response::RespFunctionNotAllowed; - } - - if (!sqlInterface->checkSql()) { - return Response::RespInternalError; - } - - int reportId = cmd.report_id(); - QString commentText = textFromStdString(cmd.comment()).trimmed(); - - if (commentText.isEmpty()) { - return Response::RespInvalidData; - } - - const int maxCommentsPerHour = settingsCache->value("reporting/max_comments_per_hour", 30).toInt(); - if (maxCommentsPerHour > 0) { - QSqlQuery *countQuery = - sqlInterface->prepareQuery("SELECT COUNT(*) FROM {prefix}_report_comments WHERE author_id = :id " - "AND created_at >= DATE_SUB(NOW(), INTERVAL 1 HOUR)"); - countQuery->bindValue(":id", userInfo->id()); - if (sqlInterface->execSqlQuery(countQuery) && countQuery->next() && - countQuery->value(0).toInt() >= maxCommentsPerHour) { - return Response::RespTooManyRequests; - } - } - - QSqlQuery *checkQuery = sqlInterface->prepareQuery( - "SELECT reporter_id, reporter_name, assigned_to, status FROM {prefix}_reports WHERE id = :id"); - checkQuery->bindValue(":id", reportId); - - if (!sqlInterface->execSqlQuery(checkQuery)) { - return Response::RespInternalError; - } - - if (!checkQuery->next()) { - return Response::RespNameNotFound; - } - - int reporterId = checkQuery->value(0).toInt(); - QString reporterName = checkQuery->value(1).toString(); - int assignedToId = checkQuery->value(2).toInt(); - - bool isMod = userInfo->user_level() & ServerInfo_User::IsModerator; - - // Only the reporter (by id) or a moderator may comment on a report. - if (reporterId != userInfo->id() && !isMod) { - return Response::RespAccessDenied; - } - - QString reportStatus = checkQuery->value(3).toString(); - if (reportStatus == "resolved" || reportStatus == "dismissed") { - return Response::RespInvalidData; - } - - QSqlQuery *insertQuery = sqlInterface->prepareQuery( - "INSERT INTO {prefix}_report_comments (report_id, author_name, author_id, comment_text, created_at, " - "is_moderator) " - "VALUES (:report_id, :author_name, :author_id, :comment_text, NOW(), :is_moderator)"); - insertQuery->bindValue(":report_id", reportId); - insertQuery->bindValue(":author_name", QString::fromStdString(userInfo->name())); - insertQuery->bindValue(":author_id", userInfo->id()); - insertQuery->bindValue(":comment_text", commentText); - insertQuery->bindValue(":is_moderator", isMod); - - if (!sqlInterface->execSqlQuery(insertQuery)) { - return Response::RespInternalError; - } - - const qlonglong commentId = insertQuery->lastInsertId().toLongLong(); - - QStringList recipients; - if (isMod) { - // A moderator comment reaches both the reporter and (if assigned) the assigned moderator. - recipients.append(reporterName); - if (assignedToId > 0) { - QSqlQuery *modNameQuery = sqlInterface->prepareQuery("SELECT name FROM {prefix}_users WHERE id = :id"); - modNameQuery->bindValue(":id", assignedToId); - if (sqlInterface->execSqlQuery(modNameQuery) && modNameQuery->next()) { - recipients.append(modNameQuery->value(0).toString()); - } - } - } else if (assignedToId > 0) { - QSqlQuery *modNameQuery = sqlInterface->prepareQuery("SELECT name FROM {prefix}_users WHERE id = :id"); - modNameQuery->bindValue(":id", assignedToId); - if (sqlInterface->execSqlQuery(modNameQuery) && modNameQuery->next()) { - recipients.append(modNameQuery->value(0).toString()); - } - } - - const QString ownName = QString::fromStdString(userInfo->name()); - bool allNotified = !recipients.isEmpty(); - QReadLocker clientsLocker(&servatrice->clientsLock); - for (const QString ¬ifyName : recipients) { - if (notifyName == ownName) { - continue; - } - - AbstractServerSocketInterface *notify = - static_cast(server->getUsers().value(notifyName)); - if (!notify) { - // The recipient is offline; leave `notified` = 0 so the notification is - // delivered on their next login via sendPendingReportNotifications. - allNotified = false; - continue; - } - - Event_NotifyUser event; - event.set_type(Event_NotifyUser::REPORT_COMMENT); - event.set_custom_title(tr("New Comment on Report #%1").arg(reportId).toStdString()); - event.set_custom_content( - tr("%1 commented:\n%2").arg(QString::fromStdString(userInfo->name()), commentText).toStdString()); - - SessionEvent *se = notify->prepareSessionEvent(event); - notify->sendProtocolItem(*se); - delete se; - } - - if (allNotified) { - QSqlQuery *notifiedQuery = - sqlInterface->prepareQuery("UPDATE {prefix}_report_comments SET notified = 1 WHERE id = :id"); - notifiedQuery->bindValue(":id", commentId); - sqlInterface->execSqlQuery(notifiedQuery); - } - - return Response::RespOk; -} - -Response::ResponseCode -AbstractServerSocketInterface::cmdReplayDownloadByGameId(const Command_ReplayDownloadByGameId &cmd, - ResponseContainer &rc) -{ - if (authState != PasswordRight) { - return Response::RespFunctionNotAllowed; - } - - if (!sqlInterface->checkSql()) { - return Response::RespInternalError; - } - - QSqlQuery *query = sqlInterface->prepareQuery("SELECT r.id, r.replay FROM {prefix}_replays r " - "WHERE r.id_game = :game_id ORDER BY r.id DESC LIMIT 1"); - query->bindValue(":game_id", cmd.game_id()); - - if (!sqlInterface->execSqlQuery(query)) { - return Response::RespInternalError; - } - - if (!query->next()) { - return Response::RespNameNotFound; - } - - int replayId = query->value(0).toInt(); - QByteArray data = query->value(1).toByteArray(); - - Response_ReplayDownloadByGameId *re = new Response_ReplayDownloadByGameId; - re->set_replay_data(data.data(), data.size()); - re->set_replay_id(replayId); - rc.setResponseExtension(re); - - return Response::RespOk; -} - Response::ResponseCode AbstractServerSocketInterface::cmdRegisterAccount(const Command_Register &cmd, ResponseContainer &rc) { @@ -3544,8 +1781,6 @@ Response::ResponseCode AbstractServerSocketInterface::cmdAccountPassword(const C return Response::RespWrongPassword; } - databaseInterface->setForcePasswordChange(userName, false); - return Response::RespOk; } @@ -3682,7 +1917,6 @@ Response::ResponseCode AbstractServerSocketInterface::cmdForgotPasswordReset(con "PASSWORD_RESET", "", true); } - sqlInterface->setForcePasswordChange(nameFromStdString(cmd.user_name()), false); sqlInterface->removeForgotPassword(nameFromStdString(cmd.user_name())); return Response::RespOk; } @@ -3751,100 +1985,6 @@ Response::ResponseCode AbstractServerSocketInterface::cmdRequestPasswordSalt(con return Response::RespOk; } -Response::ResponseCode AbstractServerSocketInterface::cmdReport(const Command_Report &cmd, ResponseContainer & /*rc*/) -{ - if (authState != PasswordRight) { - return Response::RespFunctionNotAllowed; - } - - if (!sqlInterface->checkSql()) { - return Response::RespInternalError; - } - - if (!cmd.has_reported_user() || !cmd.has_category() || !cmd.has_description()) { - return Response::RespInvalidData; - } - - QString reporterName = QString::fromStdString(userInfo->name()); - QString reportedUser = nameFromStdString(cmd.reported_user()); - QString category = nameFromStdString(cmd.category()); - QString description = textFromStdString(cmd.description()); - QString chatLog = textTailFromStdString(cmd.chat_log()); - - if (reportedUser.isEmpty() || category.isEmpty() || description.isEmpty()) { - return Response::RespInvalidData; - } - - static const QStringList validCategories = {"cheating", "bug_abuse", "verbal_abuse", "other"}; - if (!validCategories.contains(category.toLower())) { - return Response::RespInvalidData; - } - - const int maxReportsPerDay = settingsCache->value("reporting/max_reports_per_day", 10).toInt(); - if (maxReportsPerDay > 0) { - QSqlQuery *countQuery = - sqlInterface->prepareQuery("SELECT COUNT(*) FROM {prefix}_reports WHERE reporter_id = :id " - "AND created_at >= DATE_SUB(NOW(), INTERVAL 1 DAY)"); - countQuery->bindValue(":id", userInfo->id()); - if (sqlInterface->execSqlQuery(countQuery) && countQuery->next() && - countQuery->value(0).toInt() >= maxReportsPerDay) { - return Response::RespTooManyRequests; - } - } - - int reportedUserId = -1; - QSqlQuery *lookupQuery = sqlInterface->prepareQuery("SELECT id FROM {prefix}_users WHERE name = :name"); - lookupQuery->bindValue(":name", reportedUser); - if (sqlInterface->execSqlQuery(lookupQuery) && lookupQuery->next()) { - reportedUserId = lookupQuery->value(0).toInt(); - } - - if (reportedUserId == -1) { - return Response::RespNameNotFound; - } - - int roomId = 0; - const int gameId = cmd.game_id(); - if (gameId > 0) { - QReadLocker roomsLocker(&servatrice->roomsLock); - const QMap &rooms = servatrice->getRooms(); - for (auto it = rooms.constBegin(); it != rooms.constEnd(); ++it) { - QReadLocker gamesLocker(&it.value()->gamesLock); - if (it.value()->getGames().contains(gameId)) { - roomId = it.key(); - break; - } - } - } - - QSqlQuery *query = - sqlInterface->prepareQuery("insert into {prefix}_reports " - "(reporter_id, reporter_name, reported_user_id, reported_user_name, " - "game_id, room_id, category, description, chat_log, created_at, status) " - "values " - "(:reporter_id, :reporter_name, :reported_user_id, :reported_user_name, " - ":game_id, :room_id, :category, :description, :chat_log, NOW(), 'open')"); - - query->bindValue(":reporter_id", userInfo->id()); - query->bindValue(":reporter_name", reporterName); - query->bindValue(":reported_user_id", reportedUserId); - query->bindValue(":reported_user_name", reportedUser); - - query->bindValue(":game_id", gameId > 0 ? gameId : QVariant()); - - query->bindValue(":room_id", roomId > 0 ? roomId : QVariant()); - - query->bindValue(":category", category); - query->bindValue(":description", description); - query->bindValue(":chat_log", chatLog.isEmpty() ? QVariant() : chatLog); - - if (!sqlInterface->execSqlQuery(query)) { - return Response::RespInternalError; - } - - return Response::RespOk; -} - // ADMIN FUNCTIONS. // Permission is checked by the calling function. @@ -3913,7 +2053,7 @@ bool AbstractServerSocketInterface::removeAdminFlagFromUser(const QString &userN if (user) { Event_ConnectionClosed event; event.set_reason(Event_ConnectionClosed::DEMOTED); - event.set_reason_str("Your moderator, judge, and/or developer status has been revoked."); + event.set_reason_str("Your moderator and/or judge status has been revoked."); event.set_end_time(QDateTime::currentDateTime().toSecsSinceEpoch()); SessionEvent *se = user->prepareSessionEvent(event); @@ -3955,18 +2095,6 @@ Response::ResponseCode AbstractServerSocketInterface::cmdAdjustMod(const Command } } - if (cmd.has_should_be_developer()) { - if (cmd.should_be_developer()) { - if (!addAdminFlagToUser(userName, 8)) { - return Response::RespInternalError; - } - } else { - if (!removeAdminFlagFromUser(userName, 8)) { - return Response::RespInternalError; - } - } - } - return Response::RespOk; } diff --git a/servatrice/src/serversocketinterface.h b/servatrice/src/serversocketinterface.h index 36b900d28..0d66ae78f 100644 --- a/servatrice/src/serversocketinterface.h +++ b/servatrice/src/serversocketinterface.h @@ -24,17 +24,6 @@ #include #include #include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include #include class Servatrice; @@ -45,19 +34,11 @@ class ServerInfo_DeckStorage_Folder; class Command_AddToList; class Command_RemoveFromList; class Command_DeckList; -class Command_DeckListOtherUser; class Command_DeckNewDir; class Command_DeckDelDir; class Command_DeckDel; class Command_DeckDownload; -class Command_DeckDownloadPublic; class Command_DeckUpload; -class Command_DeckSetVisibility; -class Command_DeckShareCreate; -class Command_DeckShareList; -class Command_DeckShareListMine; -class Command_DeckShareRemove; -class Command_DeckShareDownload; class Command_ReplayList; class Command_ReplayDownload; class Command_ReplayModifyMatch; @@ -69,7 +50,6 @@ class Command_BanFromServer; class Command_UpdateServerMessage; class Command_ShutdownServer; class Command_ReloadConfig; -class Command_ReplayDownloadByGameId; class Command_AccountEdit; class Command_AccountImage; @@ -87,9 +67,8 @@ signals: void incTxBytes(qint64 amount); protected: - void logDebugMessage(const QString &message) override; + void logDebugMessage(const QString &message); bool tooManyRegistrationAttempts(const QString &ipAddress); - void processCommandContainer(const CommandContainer &cont) override; virtual void writeToSocket(QByteArray &data) = 0; virtual void flushSocket() = 0; @@ -105,16 +84,8 @@ private: Response::ResponseCode cmdRemoveFromList(const Command_RemoveFromList &cmd, ResponseContainer &rc); int getDeckPathId(int basePathId, QStringList path); int getDeckPathId(const QString &path); - bool deckListHelper(int folderId, - ServerInfo_DeckStorage_Folder *folder, - int userId, - bool inheritedPublic, - bool publicOnly); - int getDeckOwnerId(int deckId); - bool isDeckEffectivelyPublic(int deckId); + bool deckListHelper(int folderId, ServerInfo_DeckStorage_Folder *folder); Response::ResponseCode cmdDeckList(const Command_DeckList &cmd, ResponseContainer &rc); - Response::ResponseCode cmdDeckListOtherUser(const Command_DeckListOtherUser &cmd, ResponseContainer &rc); - Response::ResponseCode cmdDeckSetVisibility(const Command_DeckSetVisibility &cmd, ResponseContainer &rc); Response::ResponseCode cmdDeckNewDir(const Command_DeckNewDir &cmd, ResponseContainer &rc); void deckDelDirHelper(int basePathId); void sendServerMessage(const QString userName, const QString message); @@ -123,12 +94,6 @@ private: Response::ResponseCode cmdDeckUpload(const Command_DeckUpload &cmd, ResponseContainer &rc); DeckList *getDeckFromDatabase(int deckId); Response::ResponseCode cmdDeckDownload(const Command_DeckDownload &cmd, ResponseContainer &rc); - Response::ResponseCode cmdDeckDownloadPublic(const Command_DeckDownloadPublic &cmd, ResponseContainer &rc); - Response::ResponseCode cmdDeckShareCreate(const Command_DeckShareCreate &cmd, ResponseContainer &rc); - Response::ResponseCode cmdDeckShareList(const Command_DeckShareList &cmd, ResponseContainer &rc); - Response::ResponseCode cmdDeckShareListMine(const Command_DeckShareListMine &cmd, ResponseContainer &rc); - Response::ResponseCode cmdDeckShareRemove(const Command_DeckShareRemove &cmd, ResponseContainer &rc); - Response::ResponseCode cmdDeckShareDownload(const Command_DeckShareDownload &cmd, ResponseContainer &rc); Response::ResponseCode cmdReplayList(const Command_ReplayList &cmd, ResponseContainer &rc); Response::ResponseCode cmdReplayDownload(const Command_ReplayDownload &cmd, ResponseContainer &rc); Response::ResponseCode cmdReplayModifyMatch(const Command_ReplayModifyMatch &cmd, ResponseContainer &rc); @@ -137,19 +102,13 @@ private: Response::ResponseCode cmdReplayGetCode(const Command_ReplayGetCode &cmd, ResponseContainer &rc); Response::ResponseCode cmdReplaySubmitCode(const Command_ReplaySubmitCode &cmd, ResponseContainer &rc); Response::ResponseCode cmdBanFromServer(const Command_BanFromServer &cmd, ResponseContainer &rc); - Response::ResponseCode cmdReportList(const Command_ReportList &cmd, ResponseContainer &rc); Response::ResponseCode cmdWarnUser(const Command_WarnUser &cmd, ResponseContainer &rc); - Response::ResponseCode - cmdGetLogHistory(const Command_ViewLogHistory &cmd, ResponseContainer &rc, bool allowPrivateChat); + Response::ResponseCode cmdGetLogHistory(const Command_ViewLogHistory &cmd, ResponseContainer &rc); Response::ResponseCode cmdGetBanHistory(const Command_GetBanHistory &cmd, ResponseContainer &rc); Response::ResponseCode cmdGetWarnList(const Command_GetWarnList &cmd, ResponseContainer &rc); Response::ResponseCode cmdGetWarnHistory(const Command_GetWarnHistory &cmd, ResponseContainer &rc); Response::ResponseCode cmdShutdownServer(const Command_ShutdownServer &cmd, ResponseContainer &rc); Response::ResponseCode cmdUpdateServerMessage(const Command_UpdateServerMessage &cmd, ResponseContainer &rc); - Response::ResponseCode cmdReportAssign(const Command_ReportAssign &cmd, ResponseContainer &); - Response::ResponseCode cmdReportResolve(const Command_ReportResolve &cmd, ResponseContainer &); - Response::ResponseCode cmdReportUserInfo(const Command_ReportUserInfo &cmd, ResponseContainer &rc); - Response::ResponseCode cmdReportStats(const Command_ReportStats &cmd, ResponseContainer &rc); Response::ResponseCode cmdRegisterAccount(const Command_Register &cmd, ResponseContainer &rc); Response::ResponseCode cmdActivateAccount(const Command_Activate &cmd, ResponseContainer & /* rc */); Response::ResponseCode cmdReloadConfig(const Command_ReloadConfig & /* cmd */, ResponseContainer & /*rc*/); @@ -163,21 +122,10 @@ private: Response::ResponseCode cmdForgotPasswordChallenge(const Command_ForgotPasswordChallenge &cmd, ResponseContainer &rc); Response::ResponseCode cmdRequestPasswordSalt(const Command_RequestPasswordSalt &cmd, ResponseContainer &rc); - Response::ResponseCode cmdReport(const Command_Report &cmd, ResponseContainer &); - Response::ResponseCode cmdReportMyList(const Command_ReportMyList &cmd, ResponseContainer &rc); - void sendPendingReportNotifications(ResponseContainer &rc); - void onLogin(ResponseContainer &rc) override; - Response::ResponseCode cmdReportDetails(const Command_ReportDetails &cmd, ResponseContainer &rc); - Response::ResponseCode cmdReportAddComment(const Command_ReportAddComment &cmd, ResponseContainer &rc); - Response::ResponseCode cmdReplayDownloadByGameId(const Command_ReplayDownloadByGameId &cmd, ResponseContainer &rc); + Response::ResponseCode processExtendedSessionCommand(int cmdType, const SessionCommand &cmd, ResponseContainer &rc); Response::ResponseCode - processExtendedSessionCommand(int cmdType, const SessionCommand &cmd, ResponseContainer &rc) override; - Response::ResponseCode - processExtendedModeratorCommand(int cmdType, const ModeratorCommand &cmd, ResponseContainer &rc) override; - Response::ResponseCode - processExtendedAdminCommand(int cmdType, const AdminCommand &cmd, ResponseContainer &rc) override; - Response::ResponseCode - processExtendedDeveloperCommand(int cmdType, const DeveloperCommand &cmd, ResponseContainer &rc) override; + processExtendedModeratorCommand(int cmdType, const ModeratorCommand &cmd, ResponseContainer &rc); + Response::ResponseCode processExtendedAdminCommand(int cmdType, const AdminCommand &cmd, ResponseContainer &rc); Response::ResponseCode cmdAccountEdit(const Command_AccountEdit &cmd, ResponseContainer &rc); Response::ResponseCode cmdAccountImage(const Command_AccountImage &cmd, ResponseContainer &rc); @@ -193,14 +141,6 @@ private: Response::ResponseCode cmdGetAdminNotes(const Command_GetAdminNotes &cmd, ResponseContainer &rc); Response::ResponseCode cmdUpdateAdminNotes(const Command_UpdateAdminNotes &cmd, ResponseContainer &rc); - Response::ResponseCode cmdGetUserSessions(const Command_GetUserSessions &cmd, ResponseContainer &rc); - Response::ResponseCode cmdGetUserAlts(const Command_GetUserAlts &cmd, ResponseContainer &rc); - Response::ResponseCode cmdGetModeratorLastLogins(const Command_GetModeratorLastLogins &cmd, ResponseContainer &rc); - Response::ResponseCode cmdResetUserPassword(const Command_ResetUserPassword &cmd, ResponseContainer &rc); - Response::ResponseCode cmdRemoveUserAvatar(const Command_RemoveUserAvatar &cmd, ResponseContainer &rc); - - Response::ResponseCode cmdGetServerStats(const Command_GetServerStats &cmd, ResponseContainer &rc); - bool addAdminFlagToUser(const QString &user, int flag); bool removeAdminFlagFromUser(const QString &user, int flag); @@ -217,9 +157,9 @@ public: bool initSession(); virtual QHostAddress getPeerAddress() const = 0; - QString getAddress() const override = 0; + virtual QString getAddress() const = 0; - void transmitProtocolItem(const ServerMessage &item) override; + void transmitProtocolItem(const ServerMessage &item); }; class TcpServerSocketInterface : public AbstractServerSocketInterface diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 7bb834d7e..1c3f4c2c6 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -7,20 +7,12 @@ add_test(NAME expression_test COMMAND expression_test) add_test(NAME clamped_arithmetic_test COMMAND clamped_arithmetic_test) add_test(NAME test_age_formatting COMMAND test_age_formatting) add_test(NAME password_hash_test COMMAND password_hash_test) -add_test(NAME playmat_resolver_test COMMAND playmat_resolver_test) add_test(NAME server_card_counter_test COMMAND server_card_counter_test) add_test(NAME server_counter_test COMMAND server_counter_test) add_test(NAME server_rate_limiter_test COMMAND server_rate_limiter_test) -add_test(NAME server_developer_role_test COMMAND server_developer_role_test) -add_test(NAME server_game_join_test COMMAND server_game_join_test) -add_test(NAME warning_categories_test COMMAND warning_categories_test) -add_test(NAME lag_monitor_test COMMAND lag_monitor_test) -add_test(NAME latency_tracker_test COMMAND latency_tracker_test) -add_test(NAME metrics_registry_test COMMAND metrics_registry_test) -add_test(NAME loader_local_matching_test COMMAND loader_local_matching_test) add_test(NAME deck_hash_performance_test COMMAND deck_hash_performance_test) -set_tests_properties(deck_hash_performance_test PROPERTIES TIMEOUT 15) +set_tests_properties(deck_hash_performance_test PROPERTIES TIMEOUT 5) # Find GTest @@ -29,29 +21,10 @@ add_executable(expression_test expression_test.cpp) add_executable(clamped_arithmetic_test clamped_arithmetic_test.cpp) add_executable(test_age_formatting test_age_formatting.cpp) add_executable(password_hash_test password_hash_test.cpp) -add_executable(playmat_resolver_test playmat_resolver_test.cpp) add_executable(deck_hash_performance_test deck_hash_performance_test.cpp) add_executable(server_card_counter_test server_card_counter_test.cpp) add_executable(server_counter_test server_counter_test.cpp) add_executable(server_rate_limiter_test server_rate_limiter_test.cpp) -add_executable(server_developer_role_test server_developer_role_test.cpp) -add_executable(server_game_join_test server_game_join_test.cpp) -add_executable(warning_categories_test warning_categories_test.cpp) -add_executable(lag_monitor_test ${CMAKE_SOURCE_DIR}/cockatrice/src/client/lag_monitor.cpp lag_monitor_test.cpp) -target_include_directories(lag_monitor_test PRIVATE ${CMAKE_SOURCE_DIR}/cockatrice/src) -add_executable(latency_tracker_test latency_tracker_test.cpp) -add_executable(metrics_registry_test ../servatrice/src/metrics_registry.cpp metrics_registry_test.cpp) -add_executable( - loader_local_matching_test - ${CMAKE_SOURCE_DIR}/cockatrice/src/interface/card_picture_loader/card_picture_loader_local.cpp - ${CMAKE_SOURCE_DIR}/cockatrice/src/client/settings/cache_settings.cpp - ${CMAKE_SOURCE_DIR}/cockatrice/src/client/settings/card_counter_settings.cpp - ${CMAKE_SOURCE_DIR}/cockatrice/src/client/settings/shortcuts_settings.cpp - ${CMAKE_SOURCE_DIR}/cockatrice/src/client/network/update/client/release_channel.cpp - ${VERSION_STRING_CPP} - loader_local_matching_test.cpp -) -target_include_directories(loader_local_matching_test PRIVATE ${CMAKE_SOURCE_DIR}/cockatrice/src) find_package(GTest) @@ -83,18 +56,10 @@ if(NOT GTEST_FOUND) add_dependencies(clamped_arithmetic_test gtest) add_dependencies(test_age_formatting gtest) add_dependencies(password_hash_test gtest) - add_dependencies(playmat_resolver_test gtest) add_dependencies(deck_hash_performance_test gtest) add_dependencies(server_card_counter_test gtest) add_dependencies(server_counter_test gtest) add_dependencies(server_rate_limiter_test gtest) - add_dependencies(server_developer_role_test gtest) - add_dependencies(server_game_join_test gtest) - add_dependencies(warning_categories_test gtest) - add_dependencies(lag_monitor_test gtest) - add_dependencies(latency_tracker_test gtest) - add_dependencies(metrics_registry_test gtest) - add_dependencies(loader_local_matching_test gtest) endif() include_directories(${GTEST_INCLUDE_DIRS}) @@ -109,10 +74,6 @@ target_link_libraries( target_link_libraries( password_hash_test libcockatrice_utility Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES} ) -target_link_libraries( - playmat_resolver_test libcockatrice_deck_list libcockatrice_utility Threads::Threads ${GTEST_BOTH_LIBRARIES} - ${TEST_QT_MODULES} -) target_link_libraries( deck_hash_performance_test libcockatrice_deck_list libcockatrice_utility Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES} @@ -126,31 +87,9 @@ target_link_libraries( target_link_libraries( server_rate_limiter_test libcockatrice_utility Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES} ) -target_link_libraries( - server_developer_role_test libcockatrice_network libcockatrice_rng Threads::Threads ${GTEST_BOTH_LIBRARIES} - ${TEST_QT_MODULES} -) -target_link_libraries( - server_game_join_test libcockatrice_network_server_remote libcockatrice_rng Threads::Threads ${GTEST_BOTH_LIBRARIES} - ${TEST_QT_MODULES} -) -target_link_libraries( - warning_categories_test libcockatrice_utility Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES} -) -target_link_libraries(lag_monitor_test Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES}) -target_link_libraries( - latency_tracker_test libcockatrice_network Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES} -) -target_include_directories(metrics_registry_test PRIVATE ${CMAKE_SOURCE_DIR}/servatrice/src) -target_link_libraries(metrics_registry_test ${TEST_QT_MODULES} Threads::Threads ${GTEST_BOTH_LIBRARIES}) -target_link_libraries( - loader_local_matching_test libcockatrice_settings Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES} -) add_subdirectory(card_zone_algorithms) add_subdirectory(carddatabase) -add_subdirectory(deck_list_model) -add_subdirectory(deck_list_zones) add_subdirectory(loading_from_clipboard) add_subdirectory(movecard_tests) add_subdirectory(oracle) diff --git a/tests/card_zone_algorithms/card_zone_algorithms_test.cpp b/tests/card_zone_algorithms/card_zone_algorithms_test.cpp index 8fab42566..cc098cae9 100644 --- a/tests/card_zone_algorithms/card_zone_algorithms_test.cpp +++ b/tests/card_zone_algorithms/card_zone_algorithms_test.cpp @@ -134,35 +134,6 @@ TEST_F(AddCardAlgorithmTest, MidListInsertionPreservesOrder) EXPECT_EQ(knownList.at(2), &b); } -// Reconnecting to a game rebuilds zones from a ServerInfo_Zone. Non-coordinate zones -// (hand, piles, stack) report x == 0 on every card, so inserting each rebuilt card at -// that index would reverse the received server order. Appending (-1) keeps it. -TEST_F(AddCardAlgorithmTest, RebuildInsertAtZeroReversesServerOrder) -{ - MockCard a, b, c; - CardZoneAlgorithms::addCardToList(knownList, &a, 0, false); - CardZoneAlgorithms::addCardToList(knownList, &b, 0, false); - CardZoneAlgorithms::addCardToList(knownList, &c, 0, false); - - EXPECT_EQ(knownList.size(), 3); - EXPECT_EQ(knownList.at(0), &c); - EXPECT_EQ(knownList.at(1), &b); - EXPECT_EQ(knownList.at(2), &a); -} - -TEST_F(AddCardAlgorithmTest, RebuildAppendPreservesServerOrder) -{ - MockCard a, b, c; - CardZoneAlgorithms::addCardToList(knownList, &a, -1, false); - CardZoneAlgorithms::addCardToList(knownList, &b, -1, false); - CardZoneAlgorithms::addCardToList(knownList, &c, -1, false); - - EXPECT_EQ(knownList.size(), 3); - EXPECT_EQ(knownList.at(0), &a); - EXPECT_EQ(knownList.at(1), &b); - EXPECT_EQ(knownList.at(2), &c); -} - TEST_F(AddCardAlgorithmTest, KeepAnnotationsFalsePassedThrough) { MockCard card; diff --git a/tests/carddatabase/carddatabase_test.cpp b/tests/carddatabase/carddatabase_test.cpp index 392f6395a..3fa0e3834 100644 --- a/tests/carddatabase/carddatabase_test.cpp +++ b/tests/carddatabase/carddatabase_test.cpp @@ -2,14 +2,9 @@ #include "test_card_database_path_provider.h" #include "gtest/gtest.h" -#include -#include -#include -#include -#include -#include #include #include + namespace { @@ -38,49 +33,6 @@ TEST(CardDatabaseTest, LoadXml) ASSERT_EQ(0, db->query()->getAllMainCardTypes().size()) << "Types not empty after clear"; ASSERT_EQ(NotLoaded, db->getLoadStatus()) << "Incorrect status after clear"; } - -TEST(CardDatabaseTest, Xml4LocalizedDataRoundTrip) -{ - NoopCardSetPriorityController controller; - CardSetPtr set = - CardSet::newInstance(&controller, "TST", "Test Set", "expansion", QDate(), CardSet::PriorityPrimary); - - QHash props; - props["manacost"] = "1R"; - PrintingInfo printing(set, LazyPropertiesHash(props)); - SetToPrintingsMap setsInfo; - setsInfo["TST"].append(printing); - - CardInfo::UiAttributes attributes = {.tableRow = 1}; - CardInfoPtr card = - CardInfo::newInstance("Lightning Bolt", "Deal 3 damage.", false, {}, {}, {}, setsInfo, attributes); - card->setLocalizedName("de", "Blitzschlag"); - card->setLocalizedText("de", "Blitzschlag fügt 3 Schadenspunkte zu."); - - SetNameMap sets; - sets.insert("TST", set); - CardNameMap cards; - cards.insert("Lightning Bolt", card); - - QTemporaryDir tempDir; - const QString fileName = tempDir.filePath("cards.xml"); - NoopCardPreferenceProvider prefProvider; - CockatriceXml4Parser writer(&prefProvider, &controller); - ASSERT_TRUE(writer.saveToFile({}, sets, cards, fileName)); - - CardDatabaseData data; - CockatriceXml4Parser parser(&prefProvider, &controller); - QFile file(fileName); - ASSERT_TRUE(file.open(QIODevice::ReadOnly)); - parser.parseFileInto(file, data); - - CardInfoPtr loaded = data.cards.value("Lightning Bolt"); - ASSERT_FALSE(loaded.isNull()); - ASSERT_EQ(loaded->getName(), "Lightning Bolt"); - ASSERT_EQ(loaded->getLocalizedName("de"), "Blitzschlag"); - ASSERT_EQ(loaded->getLocalizedText("de"), "Blitzschlag fügt 3 Schadenspunkte zu."); - ASSERT_EQ(loaded->getLocalizedText("fr"), "Deal 3 damage."); -} } // namespace int main(int argc, char **argv) diff --git a/tests/carddatabase/filter_string_test.cpp b/tests/carddatabase/filter_string_test.cpp index dcf1cf0a2..c6d68be1f 100644 --- a/tests/carddatabase/filter_string_test.cpp +++ b/tests/carddatabase/filter_string_test.cpp @@ -2,9 +2,6 @@ #include "test_card_database_path_provider.h" #include "gtest/gtest.h" -#include -#include -#include #include #include #include @@ -74,142 +71,10 @@ QUERY(Color2, cat, "c:gw", true) QUERY(Color3, cat, "c!g", true) QUERY(Color4, cat, "c!gw", false) -QUERY(SetCodeCaseInsensitive1, cat, "set:cat", true) -QUERY(SetCodeCaseInsensitive2, cat, "set:CAT", true) -QUERY(SetCodeCaseInsensitive3, cat, "set:CAt", true) -QUERY(SetCodeShortForm, cat, "e:cat", true) -QUERY(SetCodeWrongSet, cat, "set:who", false) -QUERY(SetCodeWrongSet2, doctor, "set:cat", false) - QUERY(BracketNextToUnquotedString, cat, "(o:woof OR o:meow)", true) -CardInfoPtr localizedCat() -{ - CardInfoPtr localized = CardInfo::newInstance("Cat", "Meow!", false, {}, {}, {}, {}, {}); - localized->setLocalizedName("de", "Kater"); - localized->setLocalizedText("de", "miaut"); - return localized; -} - -TEST_F(CardQuery, SearchLanguageEnglishMatchesOnlyEnglish) -{ - const CardData localized = localizedCat(); - ASSERT_TRUE(FilterString("Cat", CardSearchLanguage{"de", SearchLanguageMode::English}).check(localized)); - ASSERT_FALSE(FilterString("Kater", CardSearchLanguage{"de", SearchLanguageMode::English}).check(localized)); -} - -TEST_F(CardQuery, SearchLanguageSelectedMatchesLocalizedNameAndText) -{ - const CardData localized = localizedCat(); - ASSERT_TRUE(FilterString("Kater", CardSearchLanguage{"de", SearchLanguageMode::Selected}).check(localized)); - ASSERT_TRUE(FilterString("o:miaut", CardSearchLanguage{"de", SearchLanguageMode::Selected}).check(localized)); - ASSERT_FALSE(FilterString("Cat", CardSearchLanguage{"de", SearchLanguageMode::Selected}).check(localized)); -} - -TEST_F(CardQuery, SearchLanguageSelectedFallsBackToEnglishForUntranslatedCards) -{ - const CardData localized = localizedCat(); - ASSERT_TRUE(FilterString("Cat", CardSearchLanguage{"fr", SearchLanguageMode::Selected}).check(localized)); - ASSERT_FALSE(FilterString("Kater", CardSearchLanguage{"fr", SearchLanguageMode::Selected}).check(localized)); -} - -TEST_F(CardQuery, SearchLanguageBothMatchesEitherLanguage) -{ - const CardData localized = localizedCat(); - ASSERT_TRUE(FilterString("Cat", CardSearchLanguage{"de", SearchLanguageMode::Both}).check(localized)); - ASSERT_TRUE(FilterString("Kater", CardSearchLanguage{"de", SearchLanguageMode::Both}).check(localized)); -} - -TEST_F(CardQuery, SearchLanguageIsBoundPerInstance) -{ - const CardData localized = localizedCat(); - - FilterString germanQuery("Kater", CardSearchLanguage{"de", SearchLanguageMode::Selected}); - ASSERT_TRUE(germanQuery.check(localized)); - - // Constructing an English-bound instance afterwards must not change the - // language the earlier instance searches in. - FilterString englishQuery("Kater", CardSearchLanguage{"", SearchLanguageMode::English}); - ASSERT_FALSE(englishQuery.check(localized)); - ASSERT_TRUE(germanQuery.check(localized)); -} - } // namespace -class SetQuery : public ::testing::Test -{ -protected: - void SetUp() override - { - // EOE and EOC share a release date, like a set and its Commander counterpart. - const QDate sharedReleaseDate(2026, 3, 13); - mainSet = CardSet::newInstance(&controller, "EOE", "Edge of Eternities", "expansion", sharedReleaseDate, - CardSet::PriorityPrimary); - commanderSet = CardSet::newInstance(&controller, "EOC", "Edge of Eternities Commander", "commander", - sharedReleaseDate, CardSet::PrioritySecondary); - oldSet = CardSet::newInstance(&controller, "OLD", "Older Set", "expansion", QDate(2020, 1, 1), - CardSet::PriorityPrimary); - - inBothSets = newCardWithPrintings({{"EOE", mainSet}, {"EOC", commanderSet}}); - onlyInCommanderSet = newCardWithPrintings({{"EOC", commanderSet}}); - onlyInOldSet = newCardWithPrintings({{"OLD", oldSet}}); - - // SetExpression resolves set codes to release dates through the global set list. - auto *database = CardDatabaseManager::getInstance(); - database->addSet(mainSet); - database->addSet(commanderSet); - database->addSet(oldSet); - } - - CardInfoPtr newCardWithPrintings(const QList> &printings) - { - SetToPrintingsMap setsInfo; - for (const auto &printing : printings) { - setsInfo[printing.first].append(PrintingInfo(printing.second)); - } - return CardInfo::newInstance("Test Card", "", false, {}, {}, {}, setsInfo, CardInfo::UiAttributes()); - } - - NoopCardSetPriorityController controller; - CardSetPtr mainSet; - CardSetPtr commanderSet; - CardSetPtr oldSet; - CardInfoPtr inBothSets; - CardInfoPtr onlyInCommanderSet; - CardInfoPtr onlyInOldSet; -}; - -TEST_F(SetQuery, ExactMatchSeparatesSameDaySets) -{ - EXPECT_TRUE(FilterString("set:EOE").check(inBothSets)); - EXPECT_TRUE(FilterString("e:EOE").check(inBothSets)); - EXPECT_TRUE(FilterString("set:EOC").check(inBothSets)); - EXPECT_FALSE(FilterString("set:EOE").check(onlyInCommanderSet)); - EXPECT_TRUE(FilterString("set:EOC").check(onlyInCommanderSet)); -} - -TEST_F(SetQuery, ExactMatchIsCaseInsensitive) -{ - EXPECT_TRUE(FilterString("set:eoe").check(inBothSets)); - EXPECT_TRUE(FilterString("set:EoE").check(inBothSets)); - EXPECT_FALSE(FilterString("set:eoe").check(onlyInCommanderSet)); -} - -TEST_F(SetQuery, NotEqualsMatchesPrintingsOutsideTheSet) -{ - EXPECT_FALSE(FilterString("set!EOE").check(inBothSets)); - EXPECT_TRUE(FilterString("set!EOE").check(onlyInCommanderSet)); -} - -TEST_F(SetQuery, ComparisonMatchesByReleaseDate) -{ - // OLD (2020) predates EOE/EOC (2026-03-13), so comparison operators still use release dates. - EXPECT_TRUE(FilterString("setEOE").check(onlyInOldSet)); - EXPECT_TRUE(FilterString("set>=EOE").check(inBothSets)); - EXPECT_FALSE(FilterString("set -#include -#include -#include - -namespace -{ - -DecklistModelCardNode *cardNode(InnerDecklistNode *parent, const QString &name, int number) -{ - // The underlying data node is detached; only the model wrapper is attached to the shadow tree. - auto *data = new DecklistCardNode(name, number, nullptr); - return new DecklistModelCardNode(data, parent); -} - -QStringList childNames(const InnerDecklistNode *node) -{ - QStringList names; - for (int i = 0; i < node->size(); ++i) { - names.append(node->at(i)->getName()); - } - return names; -} - -} // namespace - -// ===================================================================================================================== -// isCustomZone -// ===================================================================================================================== - -TEST(DeckListModelCustomZones, IsCustomZoneDistinguishesZoneFromGroup) -{ - InnerDecklistNode root; - auto *board = new InnerDecklistNode(DECK_ZONE_MAIN, &root); - auto *group = new InnerDecklistNode("Creature", board); - auto *zone = new DecklistModelSubZoneNode("Removal", board); - - auto *card = cardNode(group, "A", 1); - - EXPECT_FALSE(DeckListModelCustomZones::isCustomZone(board)); - EXPECT_FALSE(DeckListModelCustomZones::isCustomZone(group)); - EXPECT_FALSE(DeckListModelCustomZones::isCustomZone(card)); - EXPECT_TRUE(DeckListModelCustomZones::isCustomZone(zone)); -} - -// ===================================================================================================================== -// findSubZoneByName -// ===================================================================================================================== - -TEST(DeckListModelCustomZones, FindSubZoneByNameFindsAcrossBoards) -{ - InnerDecklistNode root; - auto *main = new InnerDecklistNode(DECK_ZONE_MAIN, &root); - auto *side = new InnerDecklistNode(DECK_ZONE_SIDE, &root); - new DecklistModelSubZoneNode("Removal", main); - new DecklistModelSubZoneNode("Utility", side); - new InnerDecklistNode("Plain", main); // not a custom zone - - auto *removal = DeckListModelCustomZones::findSubZoneByName(&root, "Removal"); - ASSERT_NE(removal, nullptr); - EXPECT_EQ(removal->getName(), QString("Removal")); - - auto *utility = DeckListModelCustomZones::findSubZoneByName(&root, "Utility"); - ASSERT_NE(utility, nullptr); - EXPECT_EQ(utility->getName(), QString("Utility")); - - // Names are deck-unique; a plain group or built-in board is not matched. - EXPECT_EQ(DeckListModelCustomZones::findSubZoneByName(&root, "Plain"), nullptr); - EXPECT_EQ(DeckListModelCustomZones::findSubZoneByName(&root, DECK_ZONE_MAIN), nullptr); - EXPECT_EQ(DeckListModelCustomZones::findSubZoneByName(&root, "Missing"), nullptr); -} - -// ===================================================================================================================== -// mirrorCustomZones -// ===================================================================================================================== - -TEST(DeckListModelCustomZones, MirrorCustomZonesCopiesCardsFlat) -{ - // Deck-tree board zone: one direct card plus one nested custom zone. - auto *deckBoard = new InnerDecklistNode(DECK_ZONE_MAIN); - new DecklistCardNode("Direct", 2, deckBoard); - - auto *deckZone = new InnerDecklistNode("Removal", deckBoard); - auto *deckCard1 = new DecklistCardNode("Bolt", 3, deckZone); - auto *deckCard2 = new DecklistCardNode("Swords", 1, deckZone); - - InnerDecklistNode shadowRoot; - auto *shadowBoard = new InnerDecklistNode(DECK_ZONE_MAIN, &shadowRoot); - - DeckListModelCustomZones::mirrorCustomZones(deckBoard, shadowBoard); - - // Only the custom zone is mirrored as a sub-zone; the direct card is not. - ASSERT_EQ(shadowBoard->size(), 1); - auto *shadowZone = dynamic_cast(shadowBoard->at(0)); - ASSERT_NE(shadowZone, nullptr); - EXPECT_EQ(shadowZone->getName(), QString("Removal")); - - // Cards live flat (un-grouped) inside the mirrored zone, wrapping the same data nodes. - ASSERT_EQ(shadowZone->size(), 2); - auto *shadowCard1 = dynamic_cast(shadowZone->at(0)); - auto *shadowCard2 = dynamic_cast(shadowZone->at(1)); - ASSERT_NE(shadowCard1, nullptr); - ASSERT_NE(shadowCard2, nullptr); - EXPECT_EQ(shadowCard1->getDataNode(), deckCard1); - EXPECT_EQ(shadowCard2->getDataNode(), deckCard2); -} - -TEST(DeckListModelCustomZones, MirrorCustomZonesWithNoCustomZonesIsNoop) -{ - // A board zone with only direct cards has nothing to mirror. - auto *deckBoard = new InnerDecklistNode(DECK_ZONE_MAIN); - new DecklistCardNode("Direct", 2, deckBoard); - - InnerDecklistNode shadowRoot; - auto *shadowBoard = new InnerDecklistNode(DECK_ZONE_MAIN, &shadowRoot); - - DeckListModelCustomZones::mirrorCustomZones(deckBoard, shadowBoard); - EXPECT_EQ(shadowBoard->size(), 0); -} - -TEST(DeckListModelCustomZones, MirrorCustomZonesFlattensNestedSubzones) -{ - // Cards deeper than one level under a custom zone still get a model row. - auto *deckBoard = new InnerDecklistNode(DECK_ZONE_MAIN); - auto *deckZone = new InnerDecklistNode("Removal", deckBoard); - auto *deckCard1 = new DecklistCardNode("Bolt", 1, deckZone); - auto *deeper = new InnerDecklistNode("Deeper", deckZone); - auto *deckCard2 = new DecklistCardNode("Swords", 1, deeper); - - InnerDecklistNode shadowRoot; - auto *shadowBoard = new InnerDecklistNode(DECK_ZONE_MAIN, &shadowRoot); - - DeckListModelCustomZones::mirrorCustomZones(deckBoard, shadowBoard); - - ASSERT_EQ(shadowBoard->size(), 1); - auto *shadowZone = dynamic_cast(shadowBoard->at(0)); - ASSERT_NE(shadowZone, nullptr); - EXPECT_EQ(shadowZone->getName(), QString("Removal")); - - // Both cards are flattened into the mirrored zone, preserving order. - ASSERT_EQ(shadowZone->size(), 2); - auto *shadowCard1 = dynamic_cast(shadowZone->at(0)); - auto *shadowCard2 = dynamic_cast(shadowZone->at(1)); - ASSERT_NE(shadowCard1, nullptr); - ASSERT_NE(shadowCard2, nullptr); - EXPECT_EQ(shadowCard1->getDataNode(), deckCard1); - EXPECT_EQ(shadowCard2->getDataNode(), deckCard2); -} - -// ===================================================================================================================== -// findGroupChild -// ===================================================================================================================== - -TEST(DeckListModelCustomZones, FindGroupChildSkipsCustomZones) -{ - InnerDecklistNode root; - auto *board = new InnerDecklistNode(DECK_ZONE_MAIN, &root); - auto *group = new InnerDecklistNode("Creature", board); - new DecklistModelSubZoneNode("Creature", board); - - EXPECT_EQ(DeckListModelCustomZones::findGroupChild(board, "Creature"), group); - EXPECT_EQ(DeckListModelCustomZones::findGroupChild(board, "Missing"), nullptr); - EXPECT_EQ(DeckListModelCustomZones::findGroupChild(&root, DECK_ZONE_MAIN), board); -} - -// ===================================================================================================================== -// sortWithCustomZonesLast -// ===================================================================================================================== - -TEST(DeckListModelCustomZones, SortBoardKeepsCustomZonesAfterGroupsAscending) -{ - InnerDecklistNode root; - auto *board = new InnerDecklistNode(DECK_ZONE_MAIN, &root); - new DecklistModelSubZoneNode("Zebra", board); - new InnerDecklistNode("Creature", board); - new InnerDecklistNode("Instant", board); - new DecklistModelSubZoneNode("Alpha", board); - - root.setSortMethod(DeckSortMethod::ByName); - - auto mapping = DeckListModelCustomZones::sortWithCustomZonesLast(&root, board, Qt::AscendingOrder); - - // Groups sort first (by name), then custom zones (by name), always after groups. - EXPECT_EQ(childNames(board), (QStringList{"Creature", "Instant", "Alpha", "Zebra"})); - - // Some non-identity movement occurred. - EXPECT_FALSE(mapping.isEmpty()); -} - -TEST(DeckListModelCustomZones, SortBoardKeepsCustomZonesAfterGroupsDescending) -{ - InnerDecklistNode root; - auto *board = new InnerDecklistNode(DECK_ZONE_MAIN, &root); - new DecklistModelSubZoneNode("Zebra", board); - new InnerDecklistNode("Creature", board); - new InnerDecklistNode("Instant", board); - new DecklistModelSubZoneNode("Alpha", board); - - root.setSortMethod(DeckSortMethod::ByName); - - (void)DeckListModelCustomZones::sortWithCustomZonesLast(&root, board, Qt::DescendingOrder); - - // Groups still lead (descending), custom zones still last. - EXPECT_EQ(childNames(board), (QStringList{"Instant", "Creature", "Zebra", "Alpha"})); -} - -TEST(DeckListModelCustomZones, SortBoardMappingIsConsistent) -{ - InnerDecklistNode root; - auto *board = new InnerDecklistNode(DECK_ZONE_MAIN, &root); - - QList originalOrder; - auto *g0 = new InnerDecklistNode("Creature", board); - originalOrder.append(g0); - auto *z0 = new DecklistModelSubZoneNode("Zebra", board); - originalOrder.append(z0); - auto *g1 = new InnerDecklistNode("Instant", board); - originalOrder.append(g1); - auto *z1 = new DecklistModelSubZoneNode("Alpha", board); - originalOrder.append(z1); - - root.setSortMethod(DeckSortMethod::ByName); - - auto mapping = DeckListModelCustomZones::sortWithCustomZonesLast(&root, board, Qt::AscendingOrder); - - // The mapping reports, for each final row, the original row of the node now sitting there. - ASSERT_EQ(mapping.size(), board->size()); - for (const auto &move : mapping) { - const int preSortRow = move.first; - const int finalRow = move.second; - ASSERT_GE(preSortRow, 0); - ASSERT_LT(preSortRow, originalOrder.size()); - EXPECT_EQ(board->at(finalRow), originalOrder[preSortRow]) << "row " << finalRow; - } - - // Final order sanity: groups first in name order, then custom zones. - EXPECT_EQ(childNames(board), (QStringList{"Creature", "Instant", "Alpha", "Zebra"})); -} - -TEST(DeckListModelCustomZones, SortPlainNodeDoesNotReorderCustomZones) -{ - // A non-board node (e.g. a group whose children are cards) is sorted plainly; - // custom zones are not a special case there. Cards sort by name. - InnerDecklistNode root; - auto *board = new InnerDecklistNode(DECK_ZONE_MAIN, &root); - auto *group = new InnerDecklistNode("Creature", board); - cardNode(group, "Swords", 1); - cardNode(group, "Bolt", 3); - - root.setSortMethod(DeckSortMethod::ByName); - - auto mapping = DeckListModelCustomZones::sortWithCustomZonesLast(&root, group, Qt::AscendingOrder); - EXPECT_EQ(childNames(group), (QStringList{"Bolt", "Swords"})); - ASSERT_EQ(mapping.size(), 2); - EXPECT_EQ(mapping[0].first, 1); // "Bolt" was originally at row 1 - EXPECT_EQ(mapping[0].second, 0); - EXPECT_EQ(mapping[1].first, 0); - EXPECT_EQ(mapping[1].second, 1); -} - -int main(int argc, char **argv) -{ - ::testing::InitGoogleTest(&argc, argv); - return RUN_ALL_TESTS(); -} diff --git a/tests/deck_list_model/deck_list_model_zone_integration_test.cpp b/tests/deck_list_model/deck_list_model_zone_integration_test.cpp deleted file mode 100644 index a4562c95d..000000000 --- a/tests/deck_list_model/deck_list_model_zone_integration_test.cpp +++ /dev/null @@ -1,283 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace -{ - -int totalCustomZoneRows(const DeckListModel &model) -{ - int count = 0; - const int rootRows = model.rowCount(QModelIndex()); - for (int r = 0; r < rootRows; ++r) { - const QModelIndex board = model.index(r, 0, QModelIndex()); - const int childRows = model.rowCount(board); - for (int c = 0; c < childRows; ++c) { - const QModelIndex child = model.index(c, 0, board); - if (child.data(DeckRoles::IsCustomZoneRole).toBool()) { - ++count; - } - } - } - return count; -} - -QModelIndex findBoardIndex(const DeckListModel &model, const QString &boardName) -{ - for (int r = 0; r < model.rowCount(QModelIndex()); ++r) { - const QModelIndex idx = model.index(r, 0, QModelIndex()); - if (idx.data(DeckRoles::IsCardRole).toBool()) { - continue; - } - const QString name = idx.sibling(idx.row(), DeckListModelColumns::CARD_NAME).data(Qt::EditRole).toString(); - if (name == boardName) { - return idx; - } - } - return {}; -} - -QModelIndex findZoneRow(const DeckListModel &model, const QModelIndex &board) -{ - for (int r = 0; r < model.rowCount(board); ++r) { - const QModelIndex child = model.index(r, 0, board); - if (child.data(DeckRoles::IsCustomZoneRole).toBool()) { - return child; - } - } - return {}; -} - -} // namespace - -// The "Add to Zone" combobox/submenu lists getCustomZoneNames(), which reads the -// deck tree. These verify the source data a freshly-created zone populates. - -TEST(DeckListModelZoneIntegration, CreateZoneThenReadCustomZoneNames) -{ - QSharedPointer deck(new DeckList()); - DeckListModel model(nullptr, deck); - auto *tree = deck->getTree(); - - ASSERT_NE(tree->addCustomZone(DECK_ZONE_MAIN, "Removal"), nullptr); - EXPECT_EQ(model.getCustomZoneNames(DECK_ZONE_MAIN), (QStringList{"Removal"})); -} - -TEST(DeckListModelZoneIntegration, CreateTwoZonesThenReadBoth) -{ - QSharedPointer deck(new DeckList()); - DeckListModel model(nullptr, deck); - auto *tree = deck->getTree(); - - ASSERT_NE(tree->addCustomZone(DECK_ZONE_MAIN, "Removal"), nullptr); - ASSERT_NE(tree->addCustomZone(DECK_ZONE_MAIN, "Utility"), nullptr); - EXPECT_EQ(model.getCustomZoneNames(DECK_ZONE_MAIN), (QStringList{"Removal", "Utility"})); -} - -// Mirroring regression: rebuildTree must mirror each custom zone exactly once. -TEST(DeckListModelZoneIntegration, RebuildTreeMirrorsEachZoneOnce) -{ - QSharedPointer deck(new DeckList()); - DeckListModel model(nullptr, deck); - auto *tree = deck->getTree(); - - // One direct mainboard card plus two nested custom zones. - tree->addCard("Lightning Bolt", 2, DECK_ZONE_MAIN, -1); - ASSERT_NE(tree->addCustomZone(DECK_ZONE_MAIN, "Removal"), nullptr); - tree->addCard("Swords to Plowshares", 1, "Removal", -1); - ASSERT_NE(tree->addCustomZone(DECK_ZONE_MAIN, "Utility"), nullptr); - - model.rebuildTree(); - - EXPECT_EQ(model.getCustomZoneNames(DECK_ZONE_MAIN), (QStringList{"Removal", "Utility"})); - EXPECT_EQ(totalCustomZoneRows(model), 2); -} - -// ===================================================================================================================== -// Model behaviour: addCard routing, findCard lookup, removeRows guard, empty-zone survival. -// ===================================================================================================================== - -TEST(DeckListModelZoneIntegration, AddCardRoutesIntoMirroredCustomZone) -{ - QSharedPointer deck(new DeckList()); - DeckListModel model(nullptr, deck); - auto *tree = deck->getTree(); - - ASSERT_NE(tree->addCustomZone(DECK_ZONE_MAIN, "Removal"), nullptr); - model.rebuildTree(); - - QModelIndex added = model.addCard(ExactCard(CardInfo::newInstance("Lightning Bolt")), "Removal"); - ASSERT_TRUE(added.isValid()); - - // The card is a direct child of the mirrored custom zone, not a new top-level zone. - const QModelIndex zoneParent = added.parent(); - ASSERT_TRUE(zoneParent.isValid()); - EXPECT_TRUE(zoneParent.data(DeckRoles::IsCustomZoneRole).toBool()); - EXPECT_EQ(zoneParent.sibling(zoneParent.row(), DeckListModelColumns::CARD_NAME).data(Qt::DisplayRole).toString(), - QString("Removal")); - - // No "Removal" top-level zone appeared in the deck tree. - auto *listRoot = tree->getRoot(); - bool topLevelRemoval = false; - for (int i = 0; i < listRoot->size(); ++i) { - if (auto *zone = dynamic_cast(listRoot->at(i))) { - topLevelRemoval |= zone->getName() == "Removal"; - } - } - EXPECT_FALSE(topLevelRemoval); -} - -TEST(DeckListModelZoneIntegration, AddCardToUnmirroredCustomZoneRebuildsNotCreatesTopLevel) -{ - QSharedPointer deck(new DeckList()); - DeckListModel model(nullptr, deck); - auto *tree = deck->getTree(); - - // The zone exists on the deck tree but the shadow tree has never mirrored it. - ASSERT_NE(tree->addCustomZone(DECK_ZONE_MAIN, "Removal"), nullptr); - - QModelIndex added = model.addCard(ExactCard(CardInfo::newInstance("Lightning Bolt")), "Removal"); - ASSERT_TRUE(added.isValid()); - - const QModelIndex zoneParent = added.parent(); - ASSERT_TRUE(zoneParent.isValid()); - EXPECT_TRUE(zoneParent.data(DeckRoles::IsCustomZoneRole).toBool()); - EXPECT_EQ(zoneParent.sibling(zoneParent.row(), DeckListModelColumns::CARD_NAME).data(Qt::DisplayRole).toString(), - QString("Removal")); -} - -TEST(DeckListModelZoneIntegration, AddCardCreatesGroupSeparatelyFromSameNamedZone) -{ - QSharedPointer deck(new DeckList()); - DeckListModel model(nullptr, deck); - auto *tree = deck->getTree(); - - // A custom zone named exactly like a grouping criterion. - ASSERT_NE(tree->addCustomZone(DECK_ZONE_MAIN, "Creature"), nullptr); - model.rebuildTree(); - - CardInfoPtr bear = CardInfo::newInstance("Grizzly Bears"); - bear->setProperty(Mtg::MainCardType, "Creature"); - - QModelIndex added = model.addCard(ExactCard(bear), DECK_ZONE_MAIN); - ASSERT_TRUE(added.isValid()); - - // The card lands in a *group* node called "Creature", not swallowed by the custom zone. - const QModelIndex groupParent = added.parent(); - ASSERT_TRUE(groupParent.isValid()); - EXPECT_FALSE(groupParent.data(DeckRoles::IsCustomZoneRole).toBool()); - EXPECT_EQ(groupParent.sibling(groupParent.row(), DeckListModelColumns::CARD_NAME).data(Qt::DisplayRole).toString(), - QString("Creature")); - - // The board keeps both rows: the "Creature" group and the "Creature" custom zone. - const QModelIndex boardIndex = groupParent.parent(); - ASSERT_TRUE(boardIndex.isValid()); - EXPECT_EQ(model.rowCount(boardIndex), 2); -} - -TEST(DeckListModelZoneIntegration, FindCardResolvesCardInsideCustomZone) -{ - QSharedPointer deck(new DeckList()); - DeckListModel model(nullptr, deck); - auto *tree = deck->getTree(); - - ASSERT_NE(tree->addCustomZone(DECK_ZONE_MAIN, "Removal"), nullptr); - model.rebuildTree(); - - // findCard resolves through the card database; register the card we add. - const QString cardName = "Swords to Plowshares"; - CardInfoPtr info = CardInfo::newInstance(cardName); - CardDatabaseManager::getInstance()->addCard(info); - - QModelIndex added = model.addCard(ExactCard(info), "Removal"); - ASSERT_TRUE(added.isValid()); - - QModelIndex found = model.findCard(cardName, "Removal"); - EXPECT_TRUE(found.isValid()); - EXPECT_EQ(found, added); -} - -TEST(DeckListModelZoneIntegration, RemoveRowsRefusesCustomZoneRow) -{ - QSharedPointer deck(new DeckList()); - DeckListModel model(nullptr, deck); - auto *tree = deck->getTree(); - - ASSERT_NE(tree->addCustomZone(DECK_ZONE_MAIN, "Removal"), nullptr); - tree->addCard("Lightning Bolt", 2, DECK_ZONE_MAIN, -1); - model.rebuildTree(); - - const QModelIndex mainIndex = findBoardIndex(model, DECK_ZONE_MAIN); - ASSERT_TRUE(mainIndex.isValid()); - const QModelIndex zoneRow = findZoneRow(model, mainIndex); - ASSERT_TRUE(zoneRow.isValid()); - - EXPECT_FALSE(model.removeRow(zoneRow.row(), zoneRow.parent())); - EXPECT_EQ(model.rowCount(mainIndex), 2); // the zone survives, alongside the card group -} - -TEST(DeckListModelZoneIntegration, EmptyCustomZoneSurvivesMirrorAndPruning) -{ - QSharedPointer deck(new DeckList()); - DeckListModel model(nullptr, deck); - auto *tree = deck->getTree(); - - // An empty custom zone must be mirrored (the stack deliberately keeps it alive). - ASSERT_NE(tree->addCustomZone(DECK_ZONE_MAIN, "Removal"), nullptr); - model.rebuildTree(); - - const QModelIndex mainIndex = findBoardIndex(model, DECK_ZONE_MAIN); - ASSERT_TRUE(mainIndex.isValid()); - EXPECT_EQ(model.rowCount(mainIndex), 1); - EXPECT_TRUE(findZoneRow(model, mainIndex).isValid()); -} - -// Regression: a board card named like the requested zone must not be mistaken for -// a zone. Previously `findChild` matched any child by name, so a mainboard card -// called "Lightning Bolt" made addCard believe a "Lightning Bolt" zone existed and -// recurse through rebuildTree forever. -TEST(DeckListModelZoneIntegration, AddCardToCardNamedZoneDoesNotRecurse) -{ - QSharedPointer deck(new DeckList()); - DeckListModel model(nullptr, deck); - auto *tree = deck->getTree(); - - tree->addCard("Lightning Bolt", 2, DECK_ZONE_MAIN, -1); - - QModelIndex added = model.addCard(ExactCard(CardInfo::newInstance("Swords to Plowshares")), "Lightning Bolt"); - ASSERT_TRUE(added.isValid()); -} - -// Regression: adding to a custom zone that holds a nested sub-zone mirrored the -// nested cards as flattened shadow rows, so the sorted shadow row index pointed -// past the deck zone's direct children. The card must be appended to the deck -// zone instead of being written out of range. -TEST(DeckListModelZoneIntegration, AddCardToCustomZoneWithNestedSubZoneAppends) -{ - QSharedPointer deck(new DeckList()); - DeckListModel model(nullptr, deck); - auto *tree = deck->getTree(); - - auto *removal = tree->addCustomZone(DECK_ZONE_MAIN, "Removal"); - ASSERT_NE(removal, nullptr); - auto *deeper = new InnerDecklistNode("Deeper", removal); - new DecklistCardNode("Lightning Bolt", 2, deeper, -1); - model.rebuildTree(); - - QModelIndex added = model.addCard(ExactCard(CardInfo::newInstance("Swords to Plowshares")), "Removal"); - ASSERT_TRUE(added.isValid()); - ASSERT_TRUE(added.parent().data(DeckRoles::IsCustomZoneRole).toBool()); -} - -int main(int argc, char **argv) -{ - ::testing::InitGoogleTest(&argc, argv); - return RUN_ALL_TESTS(); -} diff --git a/tests/deck_list_zones/CMakeLists.txt b/tests/deck_list_zones/CMakeLists.txt deleted file mode 100644 index 0710be94d..000000000 --- a/tests/deck_list_zones/CMakeLists.txt +++ /dev/null @@ -1,10 +0,0 @@ -add_executable(deck_list_zones_test deck_list_zones_test.cpp) - -if(NOT GTEST_FOUND) - add_dependencies(deck_list_zones_test gtest) -endif() - -target_link_libraries( - deck_list_zones_test libcockatrice_deck_list Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES} -) -add_test(NAME deck_list_zones_test COMMAND deck_list_zones_test) diff --git a/tests/deck_list_zones/deck_list_zones_test.cpp b/tests/deck_list_zones/deck_list_zones_test.cpp deleted file mode 100644 index 801f226a9..000000000 --- a/tests/deck_list_zones/deck_list_zones_test.cpp +++ /dev/null @@ -1,392 +0,0 @@ -/** - * @file deck_list_zones_test.cpp - * @brief Tests for custom deck zones (deck-unique zones nested under a board zone). - * - * Custom zones allow players to organize cards within a board (e.g. "Removal" under - * the mainboard) without changing the board semantics: cards in a custom zone under - * "main" are still mainboard cards for hashing, sideboard size, legality and export. - */ - -#include -#include -#include -#include -#include -#include - -namespace -{ - -/** - * @brief Collects (board zone name, card node) pairs via forEachCard. - */ -struct BoardCardPair -{ - QString boardZone; - QString cardName; - int amount; -}; - -QList collectBoardCardPairs(const DeckList &deck) -{ - QList result; - deck.forEachCard([&result](InnerDecklistNode *boardZone, DecklistCardNode *card) { - result.append({boardZone->getName(), card->getName(), card->getNumber()}); - }); - return result; -} - -bool hasPair(const QList &pairs, const QString &boardZone, const QString &cardName) -{ - for (const auto &pair : pairs) { - if (pair.boardZone == boardZone && pair.cardName == cardName) { - return true; - } - } - return false; -} - -int totalCards(const QList &pairs) -{ - int total = 0; - for (const auto &pair : pairs) { - total += pair.amount; - } - return total; -} - -} // namespace - -// ===================================================================================================================== -// Zone creation -// ===================================================================================================================== - -TEST(DeckListZones, AddCustomZoneNestsUnderBoard) -{ - DeckList deck; - auto *tree = deck.getTree(); - - auto *zone = tree->addCustomZone(DECK_ZONE_MAIN, "Removal"); - ASSERT_NE(zone, nullptr); - EXPECT_EQ(zone->getName(), QString("Removal")); - ASSERT_NE(zone->getParent(), nullptr); - EXPECT_EQ(zone->getParent()->getName(), QString(DECK_ZONE_MAIN)); - - // The custom zone is nested, not a new top-level zone. - auto topLevelZones = tree->getZoneNodes(); - QStringList topLevelNames; - for (auto *node : topLevelZones) { - topLevelNames.append(node->getName()); - } - EXPECT_FALSE(topLevelNames.contains("Removal")); - - // It is discoverable through the board zone. - auto customZones = tree->getCustomZones(DECK_ZONE_MAIN); - ASSERT_EQ(customZones.size(), 1); - EXPECT_EQ(customZones.first()->getName(), QString("Removal")); -} - -TEST(DeckListZones, CustomZoneNamesAreDeckUnique) -{ - DeckList deck; - auto *tree = deck.getTree(); - - ASSERT_NE(tree->addCustomZone(DECK_ZONE_MAIN, "Removal"), nullptr); - // Same name on a different board is rejected. - EXPECT_EQ(tree->addCustomZone(DECK_ZONE_SIDE, "Removal"), nullptr); - // A name that collides with a built-in board zone is rejected. - EXPECT_EQ(tree->addCustomZone(DECK_ZONE_MAIN, DECK_ZONE_MAIN), nullptr); - EXPECT_EQ(tree->addCustomZone(DECK_ZONE_MAIN, DECK_ZONE_SIDE), nullptr); - EXPECT_EQ(tree->addCustomZone(DECK_ZONE_MAIN, DECK_ZONE_MAYBEBOARD), nullptr); - EXPECT_EQ(tree->addCustomZone(DECK_ZONE_MAIN, DECK_ZONE_TOKENS), nullptr); -} - -TEST(DeckListZones, AddCustomZoneUnknownBoardFails) -{ - DeckList deck; - auto *tree = deck.getTree(); - - EXPECT_EQ(tree->addCustomZone("not_a_board", "Removal"), nullptr); -} - -TEST(DeckListZones, MaybeboardIsLazilyCreated) -{ - DeckList deck; - auto *tree = deck.getTree(); - - ASSERT_NE(tree->addCustomZone(DECK_ZONE_MAYBEBOARD, "Candidates"), nullptr); - - // The maybeboard board zone now exists, with the custom zone nested inside. - auto customZones = tree->getCustomZones(DECK_ZONE_MAYBEBOARD); - ASSERT_EQ(customZones.size(), 1); - EXPECT_EQ(customZones.first()->getName(), QString("Candidates")); -} - -// ===================================================================================================================== -// Card placement -// ===================================================================================================================== - -TEST(DeckListZones, AddCardToCustomZoneKeepsBoardSemantics) -{ - DeckList deck; - auto *tree = deck.getTree(); - - ASSERT_NE(tree->addCustomZone(DECK_ZONE_MAIN, "Removal"), nullptr); - tree->addCard("Lightning Bolt", 4, "Removal", -1); - - // The card is reported as a mainboard card. - auto pairs = collectBoardCardPairs(deck); - EXPECT_TRUE(hasPair(pairs, DECK_ZONE_MAIN, "Lightning Bolt")); - EXPECT_FALSE(hasPair(pairs, DECK_ZONE_SIDE, "Lightning Bolt")); - - // It is physically nested inside the custom zone. - auto customZones = tree->getCustomZones(DECK_ZONE_MAIN); - ASSERT_EQ(customZones.size(), 1); - ASSERT_EQ(customZones.first()->size(), 1); - auto *card = dynamic_cast(customZones.first()->at(0)); - ASSERT_NE(card, nullptr); - EXPECT_EQ(card->getName(), QString("Lightning Bolt")); - EXPECT_EQ(card->getNumber(), 4); - - // Zone-scoped queries include it. - EXPECT_TRUE(deck.getCardList({DECK_ZONE_MAIN}).contains("Lightning Bolt")); - EXPECT_FALSE(deck.getCardList({DECK_ZONE_SIDE}).contains("Lightning Bolt")); - EXPECT_EQ(deck.getCardNodes({DECK_ZONE_MAIN}).size(), 1); -} - -TEST(DeckListZones, LegacyTopLevelZoneStillWorks) -{ - DeckList deck; - auto *tree = deck.getTree(); - - // Unknown zone names create a legacy top-level zone (backwards compatibility). - tree->addCard("Legacy Card", 2, "custom_legacy_zone", -1); - - auto pairs = collectBoardCardPairs(deck); - EXPECT_TRUE(hasPair(pairs, "custom_legacy_zone", "Legacy Card")); - EXPECT_EQ(deck.getCardList({}).count("Legacy Card"), 1); -} - -// ===================================================================================================================== -// Zone management -// ===================================================================================================================== - -TEST(DeckListZones, RenameCustomZone) -{ - DeckList deck; - auto *tree = deck.getTree(); - - ASSERT_NE(tree->addCustomZone(DECK_ZONE_MAIN, "Removal"), nullptr); - tree->addCard("Lightning Bolt", 2, "Removal", -1); - - EXPECT_TRUE(tree->renameCustomZone("Removal", "Bolt Zone")); - EXPECT_TRUE(hasPair(collectBoardCardPairs(deck), DECK_ZONE_MAIN, "Lightning Bolt")); - EXPECT_EQ(tree->getCustomZones(DECK_ZONE_MAIN).size(), 1); - EXPECT_EQ(tree->getCustomZones(DECK_ZONE_MAIN).first()->getName(), QString("Bolt Zone")); - - // Renaming to a taken name fails. - ASSERT_NE(tree->addCustomZone(DECK_ZONE_MAIN, "Other"), nullptr); - EXPECT_FALSE(tree->renameCustomZone("Bolt Zone", "Other")); - // Renaming a nonexistent zone fails. - EXPECT_FALSE(tree->renameCustomZone("Ghost Zone", "Whatever")); -} - -TEST(DeckListZones, MoveCustomZoneMovesCards) -{ - DeckList deck; - auto *tree = deck.getTree(); - - ASSERT_NE(tree->addCustomZone(DECK_ZONE_MAIN, "Removal"), nullptr); - tree->addCard("Lightning Bolt", 2, "Removal", -1); - - EXPECT_TRUE(tree->moveCustomZone("Removal", DECK_ZONE_SIDE)); - - auto pairs = collectBoardCardPairs(deck); - EXPECT_TRUE(hasPair(pairs, DECK_ZONE_SIDE, "Lightning Bolt")); - EXPECT_FALSE(hasPair(pairs, DECK_ZONE_MAIN, "Lightning Bolt")); - - // The custom zone is now nested under side. - EXPECT_EQ(tree->getCustomZones(DECK_ZONE_MAIN).size(), 0); - EXPECT_EQ(tree->getCustomZones(DECK_ZONE_SIDE).size(), 1); - - // Moving to an unknown board fails. - EXPECT_FALSE(tree->moveCustomZone("Removal", "not_a_board")); -} - -TEST(DeckListZones, MoveCustomZoneFailsForUnknownBoard) -{ - DeckList deck; - auto *tree = deck.getTree(); - - ASSERT_NE(tree->addCustomZone(DECK_ZONE_MAIN, "Removal"), nullptr); - tree->addCard("Lightning Bolt", 2, "Removal", -1); - - EXPECT_FALSE(tree->moveCustomZone("Removal", "not_a_board")); - - // The zone is still under main. - EXPECT_EQ(tree->getCustomZones(DECK_ZONE_MAIN).size(), 1); -} - -// Regression: findCustomZoneByName walks every top-level zone, so a custom zone -// an imported deck carries under a non-standard board (tokens) is still found and -// movable. The pre-fix manager-level moveCustomZone only scanned the standard -// boards and returned false for these with no feedback. -TEST(DeckListZones, MoveCustomZoneNestedUnderTokensBoard) -{ - DeckList deck; - auto *tree = deck.getTree(); - auto *root = tree->getRoot(); - - auto *tokens = new InnerDecklistNode(DECK_ZONE_TOKENS, root); - auto *removal = new InnerDecklistNode("Removal", tokens); - new DecklistCardNode("Lightning Bolt", 2, removal, -1); - - EXPECT_TRUE(tree->findCustomZoneByName("Removal")); - EXPECT_TRUE(tree->moveCustomZone("Removal", DECK_ZONE_SIDE)); - - auto pairs = collectBoardCardPairs(deck); - EXPECT_FALSE(hasPair(pairs, DECK_ZONE_TOKENS, "Lightning Bolt")); - EXPECT_TRUE(hasPair(pairs, DECK_ZONE_SIDE, "Lightning Bolt")); -} - -TEST(DeckListZones, RemoveCustomZoneRemovesCards) -{ - DeckList deck; - auto *tree = deck.getTree(); - - ASSERT_NE(tree->addCustomZone(DECK_ZONE_MAIN, "Removal"), nullptr); - tree->addCard("Lightning Bolt", 2, "Removal", -1); - - EXPECT_TRUE(tree->removeCustomZone("Removal")); - EXPECT_EQ(tree->getCustomZones(DECK_ZONE_MAIN).size(), 0); - EXPECT_TRUE(deck.getCardList({DECK_ZONE_MAIN}).isEmpty()); - EXPECT_FALSE(tree->removeCustomZone("Removal")); -} - -TEST(DeckListZones, EmptyCustomZoneIsKeptOnCardDeletion) -{ - DeckList deck; - auto *tree = deck.getTree(); - - ASSERT_NE(tree->addCustomZone(DECK_ZONE_MAIN, "Removal"), nullptr); - auto *card = tree->addCard("Lightning Bolt", 1, "Removal", -1); - - // Deleting the last card must not delete the empty custom zone. - EXPECT_TRUE(tree->deleteNode(card)); - EXPECT_EQ(tree->getCustomZones(DECK_ZONE_MAIN).size(), 1); -} - -// ===================================================================================================================== -// Deck-wide behavior -// ===================================================================================================================== - -TEST(DeckListZones, HashCountsCustomZoneCardsByBoard) -{ - // Deck A: cards directly in main and side. - DeckList direct; - direct.addCard("Mountain", DECK_ZONE_MAIN); - direct.addCard("Lightning Bolt", DECK_ZONE_MAIN); - direct.addCard("Island", DECK_ZONE_SIDE); - - // Deck B: identical, but organized in custom zones. - DeckList organized; - auto *tree = organized.getTree(); - ASSERT_NE(tree->addCustomZone(DECK_ZONE_MAIN, "Lands"), nullptr); - ASSERT_NE(tree->addCustomZone(DECK_ZONE_MAIN, "Removal"), nullptr); - ASSERT_NE(tree->addCustomZone(DECK_ZONE_SIDE, "Side Tech"), nullptr); - tree->addCard("Mountain", 1, "Lands", -1); - tree->addCard("Lightning Bolt", 1, "Removal", -1); - tree->addCard("Island", 1, "Side Tech", -1); - - EXPECT_EQ(direct.getDeckHash(), organized.getDeckHash()); -} - -TEST(DeckListZones, SideboardSizeCountsCustomZoneCards) -{ - DeckList deck; - auto *tree = deck.getTree(); - - ASSERT_NE(tree->addCustomZone(DECK_ZONE_SIDE, "Side Tech"), nullptr); - tree->addCard("Island", 3, "Side Tech", -1); - tree->addCard("Forest", 2, DECK_ZONE_SIDE, -1); - - EXPECT_EQ(deck.getSideboardSize(), 5); -} - -TEST(DeckListZones, PlainExportIncludesMainAndSideCustomZones) -{ - DeckList deck; - auto *tree = deck.getTree(); - - ASSERT_NE(tree->addCustomZone(DECK_ZONE_MAIN, "Removal"), nullptr); - ASSERT_NE(tree->addCustomZone(DECK_ZONE_SIDE, "Side Tech"), nullptr); - tree->addCard("Lightning Bolt", 2, "Removal", -1); - tree->addCard("Island", 1, "Side Tech", -1); - - const QString plain = deck.writeToString_Plain(false, false); - EXPECT_TRUE(plain.contains("2 Lightning Bolt")); - EXPECT_TRUE(plain.contains("1 Island")); -} - -TEST(DeckListZones, PlainExportSkipsMaybeboardCustomZones) -{ - DeckList deck; - auto *tree = deck.getTree(); - - ASSERT_NE(tree->addCustomZone(DECK_ZONE_MAYBEBOARD, "Candidates"), nullptr); - tree->addCard("Wish Card", 4, "Candidates", -1); - tree->addCard("Mountain", 1, DECK_ZONE_MAIN, -1); - - const QString plain = deck.writeToString_Plain(false, false); - EXPECT_FALSE(plain.contains("Wish Card")); - EXPECT_TRUE(plain.contains("1 Mountain")); -} - -TEST(DeckListZones, NativeRoundTripPreservesCustomZones) -{ - DeckList deck; - auto *tree = deck.getTree(); - - ASSERT_NE(tree->addCustomZone(DECK_ZONE_MAIN, "Removal"), nullptr); - ASSERT_NE(tree->addCustomZone(DECK_ZONE_SIDE, "Side Tech"), nullptr); - tree->addCard("Lightning Bolt", 2, "Removal", -1); - tree->addCard("Island", 3, "Side Tech", -1); - tree->addCard("Mountain", 1, DECK_ZONE_MAIN, -1); - - // Round-trip through the native format. - DeckList restored(deck.writeToString_Native()); - auto *restoredTree = restored.getTree(); - - EXPECT_EQ(restored.getDeckHash(), deck.getDeckHash()); - EXPECT_EQ(restoredTree->getCustomZones(DECK_ZONE_MAIN).size(), 1); - EXPECT_EQ(restoredTree->getCustomZones(DECK_ZONE_MAIN).first()->getName(), QString("Removal")); - EXPECT_EQ(restoredTree->getCustomZones(DECK_ZONE_SIDE).size(), 1); - - auto pairs = collectBoardCardPairs(restored); - EXPECT_TRUE(hasPair(pairs, DECK_ZONE_MAIN, "Lightning Bolt")); - EXPECT_TRUE(hasPair(pairs, DECK_ZONE_SIDE, "Island")); - EXPECT_TRUE(hasPair(pairs, DECK_ZONE_MAIN, "Mountain")); - EXPECT_EQ(totalCards(pairs), 6); -} - -TEST(DeckListZones, MaybeboardCustomZoneCardsAreExcludedFromHash) -{ - // Maybeboard cards are editor-only and must never affect the deck hash. - DeckList deck; - auto *tree = deck.getTree(); - - ASSERT_NE(tree->addCustomZone(DECK_ZONE_MAYBEBOARD, "Candidates"), nullptr); - tree->addCard("Wish Card", 4, "Candidates", -1); - tree->addCard("Mountain", 1, DECK_ZONE_MAIN, -1); - - DeckList expected; - expected.addCard("Mountain", DECK_ZONE_MAIN); - - EXPECT_EQ(deck.getDeckHash(), expected.getDeckHash()); -} - -int main(int argc, char **argv) -{ - ::testing::InitGoogleTest(&argc, argv); - return RUN_ALL_TESTS(); -} diff --git a/tests/lag_monitor_test.cpp b/tests/lag_monitor_test.cpp deleted file mode 100644 index 9948eeabb..000000000 --- a/tests/lag_monitor_test.cpp +++ /dev/null @@ -1,103 +0,0 @@ -#include "client/lag_monitor.h" - -#include -#include -#include -#include -#include - -namespace -{ - -/// Timestamps are taken at recording time; allow generous scheduler slack. -constexpr qint64 TIMESTAMP_SLACK_MS = 10000; - -} // namespace - -class LagMonitorTest : public ::testing::Test -{ -protected: - LagMonitor monitor; -}; - -TEST_F(LagMonitorTest, GapAtOrBelowThresholdIsIgnored) -{ - monitor.recordGap(0); - monitor.recordGap(LagMonitor::TICK_INTERVAL_MS); - monitor.recordGap(LagMonitor::STALL_THRESHOLD_MS); - - EXPECT_TRUE(monitor.recentStalls().isEmpty()); -} - -TEST_F(LagMonitorTest, GapAboveThresholdIsRecorded) -{ - monitor.recordGap(LagMonitor::STALL_THRESHOLD_MS + 1); - - const QList stalls = monitor.recentStalls(); - ASSERT_EQ(1, stalls.size()); - EXPECT_EQ(LagMonitor::STALL_THRESHOLD_MS + 1, stalls.first().durationMs); -} - -TEST_F(LagMonitorTest, RecordedTimestampIsFresh) -{ - monitor.recordGap(LagMonitor::STALL_THRESHOLD_MS + 1); - - const qint64 now = QDateTime::currentMSecsSinceEpoch(); - ASSERT_EQ(1, monitor.recentStalls().size()); - EXPECT_LE(qAbs(monitor.recentStalls().first().timestampMsSinceEpoch - now), TIMESTAMP_SLACK_MS); -} - -TEST_F(LagMonitorTest, RecordsAreTrimmedToMaxOldestFirst) -{ - for (int i = 0; i < LagMonitor::MAX_RECORDED_STALLS + 5; ++i) { - monitor.recordGap(LagMonitor::STALL_THRESHOLD_MS + 1 + i); - } - - const QList stalls = monitor.recentStalls(); - ASSERT_EQ(LagMonitor::MAX_RECORDED_STALLS, stalls.size()); - EXPECT_EQ(LagMonitor::STALL_THRESHOLD_MS + 6, stalls.first().durationMs); - EXPECT_EQ(LagMonitor::STALL_THRESHOLD_MS + 5 + LagMonitor::MAX_RECORDED_STALLS, stalls.last().durationMs); -} - -TEST_F(LagMonitorTest, GapAtPlausibilityCapIsKept) -{ - monitor.recordGap(LagMonitor::MAX_PLAUSIBLE_STALL_MS); - - ASSERT_EQ(1, monitor.recentStalls().size()); - EXPECT_EQ(LagMonitor::MAX_PLAUSIBLE_STALL_MS, monitor.recentStalls().first().durationMs); -} - -TEST_F(LagMonitorTest, GapBeyondPlausibilityCapIsDropped) -{ - monitor.recordGap(LagMonitor::MAX_PLAUSIBLE_STALL_MS + 1); - - EXPECT_TRUE(monitor.recentStalls().isEmpty()); -} - -TEST_F(LagMonitorTest, ClearStallsEmptiesList) -{ - monitor.recordGap(LagMonitor::STALL_THRESHOLD_MS + 1); - ASSERT_EQ(1, monitor.recentStalls().size()); - - monitor.clearStalls(); - - EXPECT_TRUE(monitor.recentStalls().isEmpty()); -} - -TEST_F(LagMonitorTest, ApplicationStateChangeDoesNotRecordAStall) -{ - QObject probe; - QEvent event(QEvent::ApplicationStateChange); - - QCoreApplication::sendEvent(&probe, &event); - - EXPECT_TRUE(monitor.recentStalls().isEmpty()); -} - -int main(int argc, char **argv) -{ - QLoggingCategory::setFilterRules("lag_monitor.*=false"); - QCoreApplication app(argc, argv); - ::testing::InitGoogleTest(&argc, argv); - return RUN_ALL_TESTS(); -} diff --git a/tests/latency_tracker_test.cpp b/tests/latency_tracker_test.cpp deleted file mode 100644 index 68631b7ca..000000000 --- a/tests/latency_tracker_test.cpp +++ /dev/null @@ -1,149 +0,0 @@ -#include -#include - -TEST(LatencyTrackerTest, EmptyTrackerYieldsZeroedStats) -{ - LatencyTracker tracker; - - const auto stats = tracker.stats(); - - EXPECT_EQ(0, stats.sampleCount); - EXPECT_EQ(0, stats.lastMs); - EXPECT_EQ(0, stats.medianMs); - EXPECT_EQ(0, stats.p95Ms); - EXPECT_EQ(0, stats.maxMs); -} - -TEST(LatencyTrackerTest, SingleSampleIsEveryStatistic) -{ - LatencyTracker tracker; - tracker.addSample(42); - - const auto stats = tracker.stats(); - - EXPECT_EQ(1, stats.sampleCount); - EXPECT_EQ(42, stats.lastMs); - EXPECT_EQ(42, stats.medianMs); - EXPECT_EQ(42, stats.p95Ms); - EXPECT_EQ(42, stats.maxMs); -} - -TEST(LatencyTrackerTest, OddSizedWindowMedianAndP95) -{ - LatencyTracker tracker; - for (qint64 ms : {qint64(50), qint64(10), qint64(30), qint64(20), qint64(40)}) { - tracker.addSample(ms); - } - - const auto stats = tracker.stats(); - - EXPECT_EQ(5, stats.sampleCount); - EXPECT_EQ(40, stats.lastMs); - EXPECT_EQ(30, stats.medianMs); - // nearest-rank p95 of 5 samples: ceil(4.75) = 5th smallest - EXPECT_EQ(50, stats.p95Ms); - EXPECT_EQ(50, stats.maxMs); -} - -TEST(LatencyTrackerTest, EvenSizedWindowMedianIsAverageOfMiddleTwo) -{ - LatencyTracker tracker; - for (qint64 ms : {qint64(10), qint64(20), qint64(30), qint64(40)}) { - tracker.addSample(ms); - } - - const auto stats = tracker.stats(); - - EXPECT_EQ(4, stats.sampleCount); - EXPECT_EQ(25, stats.medianMs); - // nearest-rank p95 of 4 samples: ceil(3.8) = 4th smallest - EXPECT_EQ(40, stats.p95Ms); -} - -TEST(LatencyTrackerTest, WindowEvictsOldestSamples) -{ - LatencyTracker tracker; - for (int i = 0; i <= 99; ++i) { - tracker.addSample(i); - } - - const auto stats = tracker.stats(); - - EXPECT_EQ(LatencyTracker::WindowSize, stats.sampleCount); - EXPECT_EQ(99, stats.lastMs); - EXPECT_EQ(99, stats.maxMs); - // window now contains 36..99 (64 samples) - EXPECT_EQ(67, stats.medianMs); // (67 + 68) / 2 with integer division - EXPECT_EQ(96, stats.p95Ms); // ceil(0.95 * 64) - 1 = index 60 -> 36 + 60 -} - -TEST(LatencyTrackerTest, ClearResetsAllState) -{ - LatencyTracker tracker; - for (int i = 0; i <= 99; ++i) { - tracker.addSample(i); - } - - tracker.clear(); - const auto cleared = tracker.stats(); - EXPECT_EQ(0, cleared.sampleCount); - - tracker.addSample(7); - const auto stats = tracker.stats(); - EXPECT_EQ(1, stats.sampleCount); - EXPECT_EQ(7, stats.lastMs); - EXPECT_EQ(7, stats.medianMs); -} - -TEST(LatencyTrackerTest, LastSampleSurvivesWraparound) -{ - LatencyTracker tracker; - for (int i = 0; i < LatencyTracker::WindowSize; ++i) { - tracker.addSample(i); - } - tracker.addSample(1000); - - EXPECT_EQ(1000, tracker.stats().lastMs); -} - -TEST(LatencyTrackerTest, RecentSamplesAreChronologicalOldestFirst) -{ - LatencyTracker tracker; - for (qint64 ms : {qint64(50), qint64(10), qint64(30)}) { - tracker.addSample(ms); - } - - const QList samples = tracker.recentSamples(); - - EXPECT_EQ((QList{50, 10, 30}), samples); -} - -TEST(LatencyTrackerTest, RecentSamplesFollowRingBufferWraparound) -{ - LatencyTracker tracker; - for (int i = 0; i <= 99; ++i) { - tracker.addSample(i); - } - - const QList samples = tracker.recentSamples(); - - ASSERT_EQ(LatencyTracker::WindowSize, samples.size()); - EXPECT_EQ(36, samples.first()); - EXPECT_EQ(99, samples.last()); -} - -TEST(LatencyTrackerTest, RecentSamplesEmptyOnFreshAndClearedTracker) -{ - LatencyTracker tracker; - EXPECT_TRUE(tracker.recentSamples().isEmpty()); - - tracker.addSample(5); - tracker.clear(); - EXPECT_TRUE(tracker.recentSamples().isEmpty()); -} - -int main(int argc, char **argv) -{ - ::testing::InitGoogleTest(&argc, argv); - return RUN_ALL_TESTS(); -} diff --git a/tests/loader_local_matching_test.cpp b/tests/loader_local_matching_test.cpp deleted file mode 100644 index 7ffa7157d..000000000 --- a/tests/loader_local_matching_test.cpp +++ /dev/null @@ -1,187 +0,0 @@ -#include "client/settings/cache_settings.h" -#include "interface/card_picture_loader/card_picture_loader_local.h" - -#include "gtest/gtest.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace -{ - -/** - * @brief Builds an ExactCard with the requested identity fields. - * - * Mirrors how the client constructs cards: the set short name feeds tryLoad()'s - * setName, and the "num" printing property feeds the collector number. - */ -ExactCard cardFor(const QString &name, const QString &setShortName, const QString &collectorNumber) -{ - CardSetPtr set; - if (!setShortName.isEmpty()) { - set = CardSet::newInstance(new NoopCardSetPriorityController(), setShortName, setShortName); - } - - LazyPropertiesHash properties; - if (!collectorNumber.isEmpty()) { - properties.insert("num", collectorNumber); - } - - return ExactCard(CardInfo::newInstance(name), PrintingInfo(set, properties)); -} - -class LocalMatcherTest : public ::testing::Test -{ -protected: - QTemporaryDir tempDir; ///< Sandboxed "pics" root for every test. - CardPictureLoaderLocal *loader = nullptr; ///< Constructed per test against the sandboxed paths. - - QString picsPath() const - { - return tempDir.path() + "/pics"; - } - - void SetUp() override - { - // The loader ctor snapshots the global picture paths once, so point them at the - // sandbox before constructing it. - SettingsCache::instance().paths().setPicsPath(picsPath()); - SettingsCache::instance().paths().setCustomPicsPath(picsPath() + "/CUSTOM/"); - - loader = new CardPictureLoaderLocal(nullptr); - } - - void TearDown() override - { - delete loader; - loader = nullptr; - } - - /** - * @brief Writes a valid 1x1 PNG under the sandboxed pics path. - */ - void writePngUnderPics(const QString &relativePath, const QColor &color = Qt::red) - { - const QString fullPath = picsPath() + "/" + relativePath; - ASSERT_TRUE(QDir().mkpath(QFileInfo(fullPath).absolutePath())); - - QImage image(1, 1, QImage::Format_RGB32); - image.fill(color); - - QImageWriter writer(fullPath, "PNG"); - ASSERT_TRUE(writer.write(image)); - } -}; - -TEST_F(LocalMatcherTest, ExactMatchBareFileWinsOverSuffixedCompanion) -{ - writePngUnderPics("downloadedPics/TestCard.png"); - writePngUnderPics("downloadedPics/TestCard (1).png"); - - const QImage image = loader->tryLoad(cardFor("TestCard", "", "")); - - EXPECT_FALSE(image.isNull()) << "The bare TestCard.png must be picked over its suffixed companion"; -} - -TEST_F(LocalMatcherTest, SuffixedFileWithoutExactMatchIsNotLoaded) -{ - // The pre-refactor prefix match would have accepted "TestCard (1).png" for "TestCard". - writePngUnderPics("downloadedPics/TestCard (1).png"); - - const QImage image = loader->tryLoad(cardFor("TestCard", "", "")); - - EXPECT_TRUE(image.isNull()) << "A suffixed file must not satisfy an exact card-name lookup"; -} - -TEST_F(LocalMatcherTest, SetFolderLookupIgnoresSuffixedFiles) -{ - writePngUnderPics("M10/TestCard (1).png"); - - const QImage image = loader->tryLoad(cardFor("TestCard", "M10", "")); - - EXPECT_TRUE(image.isNull()) << "Set-folder lookups must also require an exact name match"; -} - -TEST_F(LocalMatcherTest, SetFolderLookupStillResolvesExactFile) -{ - writePngUnderPics("M10/TestCard.png"); - writePngUnderPics("M10/TestCard (1).png"); - - const QImage image = loader->tryLoad(cardFor("TestCard", "M10", "")); - - EXPECT_FALSE(image.isNull()) << "The exact file in the set folder must still resolve"; -} - -TEST_F(LocalMatcherTest, RootDownloadedPicsFallbackResolvesSchemeFilename) -{ - // Non-set-folder export schemes (Name_Set_Collector) write straight into downloadedPics/. - writePngUnderPics("downloadedPics/TestCard_M10_1.png"); - - const QImage image = loader->tryLoad(cardFor("TestCard", "M10", "1")); - - EXPECT_FALSE(image.isNull()) << "downloadedPics/TestCard_M10_1.png must resolve via the root fallback"; -} - -TEST_F(LocalMatcherTest, RootFallbackResolvesDashSeparatedVariant) -{ - writePngUnderPics("downloadedPics/TestCard-M10-1.png"); - - const QImage image = loader->tryLoad(cardFor("TestCard", "M10", "1")); - - EXPECT_FALSE(image.isNull()) << "The dash-separated import variant must resolve via the root fallback"; -} - -TEST_F(LocalMatcherTest, DownloadedPicsSetSubfolderStillResolves) -{ - writePngUnderPics("downloadedPics/M10/TestCard_M10_1.png"); - - const QImage image = loader->tryLoad(cardFor("TestCard", "M10", "1")); - - EXPECT_FALSE(image.isNull()) << "The set-subfolder export scheme must keep resolving"; -} - -TEST_F(LocalMatcherTest, SetFolderCandidateTakesPrecedenceOverRootFallback) -{ - writePngUnderPics("M10/TestCard_M10_1.png", Qt::red); - writePngUnderPics("downloadedPics/TestCard_M10_1.png", Qt::blue); - - const QImage image = loader->tryLoad(cardFor("TestCard", "M10", "1")); - - ASSERT_FALSE(image.isNull()); - EXPECT_EQ(image.pixelColor(0, 0), QColor(Qt::red)) << "The set-folder candidate must be preferred"; -} - -} // namespace - -int main(int argc, char **argv) -{ - // Redirect SettingsCache reads/writes (app-data location) away from the real user profile. - QStandardPaths::setTestModeEnabled(true); - - // Some CI containers run as a uid without a passwd entry (e.g. GitHub's docker - // runner), so HOME resolves to "/" and the test-mode qttest data dir cannot be - // created. SettingsCache's QSettings then silently drops every write, reads come - // back empty, and the paths the loader searches are "". Give the test a writable - // HOME for the duration of the run so settings behave like on a normal machine. - QTemporaryDir home; - if (home.isValid()) { - qputenv("HOME", home.path().toLocal8Bit()); - } - - QCoreApplication app(argc, argv); - QLoggingCategory::setFilterRules("card_picture_loader.*=false\nsettings_cache.*=false"); - - ::testing::InitGoogleTest(&argc, argv); - return RUN_ALL_TESTS(); -} \ No newline at end of file diff --git a/tests/metrics_registry_test.cpp b/tests/metrics_registry_test.cpp deleted file mode 100644 index 1abef58e1..000000000 --- a/tests/metrics_registry_test.cpp +++ /dev/null @@ -1,83 +0,0 @@ -#include -#include -#include -#include - -TEST(MetricsRegistryTest, EmptyRegistryHasZeroedCounters) -{ - MetricsRegistry registry; - - EXPECT_EQ(0, registry.totalCommands()); - EXPECT_EQ(0, registry.totalTimeMs()); - EXPECT_EQ(0, registry.activeTypeCount()); - EXPECT_EQ(0, registry.getGameStartSnapshot().count); -} - -TEST(MetricsRegistryTest, SampleIsRecordedInTotalsAndSlot) -{ - MetricsRegistry registry; - registry.observeCommand(MetricsRegistry::typeIdFor(0, 1000), 7); - - EXPECT_EQ(1, registry.totalCommands()); - EXPECT_EQ(7, registry.totalTimeMs()); - EXPECT_EQ(1, registry.activeTypeCount()); - - const auto stats = registry.collectActiveStats(); - ASSERT_EQ(1, stats.size()); - EXPECT_EQ(MetricsRegistry::typeIdFor(0, 1000), stats[0].typeId); - EXPECT_EQ(1, stats[0].count); - EXPECT_EQ(7, stats[0].totalMs); -} - -TEST(MetricsRegistryTest, KindEncodingSeparatesSameExtensionNumber) -{ - MetricsRegistry registry; - const int sessionPing = MetricsRegistry::typeIdFor(0, 1000); - const int roomLeaveRoom = MetricsRegistry::typeIdFor(1, 1000); - ASSERT_NE(sessionPing, roomLeaveRoom); - - registry.observeCommand(sessionPing, 1); - registry.observeCommand(roomLeaveRoom, 5000); - - EXPECT_EQ(2, registry.activeTypeCount()); -} - -TEST(MetricsRegistryTest, OutOfRangeIdsLandInOverflowSlot) -{ - MetricsRegistry registry; - registry.observeCommand(-1, 4); - registry.observeCommand(MetricsRegistry::MaxTypes + 12345, 4); - - EXPECT_EQ(2, registry.totalCommands()); - EXPECT_EQ(1, registry.activeTypeCount()); // both collapsed into one slot - EXPECT_EQ(8, registry.totalTimeMs()); -} - -TEST(MetricsRegistryTest, NegativeDurationsAreClamped) -{ - MetricsRegistry registry; - registry.observeCommand(MetricsRegistry::typeIdFor(0, 1000), -50); - - EXPECT_EQ(0, registry.totalTimeMs()); -} - -TEST(MetricsRegistryTest, GameStartTrackedSeparatelyFromCommands) -{ - MetricsRegistry registry; - registry.observeGameStartDurationMs(120); - - EXPECT_EQ(0, registry.totalCommands()); - EXPECT_EQ(0, registry.totalTimeMs()); - EXPECT_EQ(0, registry.activeTypeCount()); - - const auto snapshot = registry.getGameStartSnapshot(); - EXPECT_EQ(1, snapshot.count); - EXPECT_EQ(120, snapshot.totalMs); -} - -int main(int argc, char **argv) -{ - QCoreApplication app(argc, argv); - ::testing::InitGoogleTest(&argc, argv); - return RUN_ALL_TESTS(); -} \ No newline at end of file diff --git a/tests/movecard_tests/reverse_card_move_test.cpp b/tests/movecard_tests/reverse_card_move_test.cpp index 64c93078c..2231a7e3b 100644 --- a/tests/movecard_tests/reverse_card_move_test.cpp +++ b/tests/movecard_tests/reverse_card_move_test.cpp @@ -1,4 +1,3 @@ -#include "game/game_config.h" #include "game/server_abstract_player.h" #include "game/server_card.h" #include "game/server_cardzone.h" @@ -23,21 +22,7 @@ TEST(ReverseCardMoveTest, MoveCardFromBottomTest) // instantiate a fake server instance FakeServer server; Server_Room room(0, 0, "", "", "", "", false, "", {}, &server); - GameConfig config{.creatorInfo = user, - .gameId = 1, - .description = QString(), - .password = QString(), - .maxPlayers = 2, - .gameTypes = QList(), - .onlyBuddies = false, - .onlyRegistered = false, - .spectatorsAllowed = false, - .spectatorsNeedPassword = false, - .spectatorsCanTalk = false, - .spectatorsSeeEverything = false, - .startingLifeTotal = 20, - .shareDecklistsOnLoad = false}; - Server_Game game(config, &room); + Server_Game game(user, 1, "", "", 2, QList(), false, false, false, false, false, false, 20, false, &room); Server_AbstractPlayer player(&game, 1, user, false, nullptr); Server_CardZone deckZone(&player, ZoneNames::DECK, true, ServerInfo_Zone::PublicZone); Server_CardZone exileZone(&player, ZoneNames::EXILE, true, ServerInfo_Zone::PublicZone); diff --git a/tests/oracle/CMakeLists.txt b/tests/oracle/CMakeLists.txt index 9bc5ee5be..c5c1e9097 100644 --- a/tests/oracle/CMakeLists.txt +++ b/tests/oracle/CMakeLists.txt @@ -7,63 +7,3 @@ endif() target_link_libraries(parse_cipt_test Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES}) add_test(NAME parse_cipt_test COMMAND parse_cipt_test) - -# Oracle importer unit tests -add_executable( - oracle_importer_test ${VERSION_STRING_CPP} ../../oracle/src/oracleimporter.cpp ../../oracle/src/parsehelpers.cpp - ../../oracle/src/raw_json_scanner.cpp oracle_importer_test.cpp -) - -if(NOT GTEST_FOUND) - add_dependencies(oracle_importer_test gtest) -endif() - -target_link_libraries( - oracle_importer_test libcockatrice_card libcockatrice_interfaces Threads::Threads ${GTEST_BOTH_LIBRARIES} - ${TEST_QT_MODULES} -) - -add_test(NAME oracle_importer_test COMMAND oracle_importer_test) - -# Oracle importer benchmark tests (manual, not run in CI, incl. RAM benchmark) -# Optional compression libs, mirrored from oracle/CMakeLists.txt, so the benchmark -# can download and decompress whatever AllPrintings format the default URL selects. -find_package(ZLIB) -if(ZLIB_FOUND) - add_definitions("-DHAS_ZLIB") - set(_ORACLE_BENCH_EXTRA_SOURCES ../../oracle/src/zip/unzip.cpp ../../oracle/src/zip/zipglobal.cpp) - set(_ORACLE_BENCH_EXTRA_LIBRARIES ${ZLIB_LIBRARIES}) - include_directories(${ZLIB_INCLUDE_DIRS}) -else() - message(STATUS "Oracle tests: zlib not found; zip download benchmark disabled") -endif() - -find_package(LibLZMA) -if(LIBLZMA_FOUND) - add_definitions("-DHAS_LZMA") - list(APPEND _ORACLE_BENCH_EXTRA_SOURCES ../../oracle/src/lzma/decompress.cpp) - list(APPEND _ORACLE_BENCH_EXTRA_LIBRARIES ${LIBLZMA_LIBRARIES}) - include_directories(${LIBLZMA_INCLUDE_DIRS}) -else() - message(STATUS "Oracle tests: LibLZMA not found; xz download benchmark disabled") -endif() - -add_executable( - oracle_importer_benchmark_test - ${VERSION_STRING_CPP} ../../oracle/src/oracleimporter.cpp ../../oracle/src/parsehelpers.cpp - ../../oracle/src/raw_json_scanner.cpp oracle_importer_benchmark_test.cpp ${_ORACLE_BENCH_EXTRA_SOURCES} -) - -if(NOT GTEST_FOUND) - add_dependencies(oracle_importer_benchmark_test gtest) -endif() - -target_link_libraries( - oracle_importer_benchmark_test - libcockatrice_card - libcockatrice_interfaces - Threads::Threads - ${GTEST_BOTH_LIBRARIES} - ${TEST_QT_MODULES} - ${_ORACLE_BENCH_EXTRA_LIBRARIES} -) diff --git a/tests/oracle/oracle_importer_benchmark_test.cpp b/tests/oracle/oracle_importer_benchmark_test.cpp deleted file mode 100644 index 633c9e40a..000000000 --- a/tests/oracle/oracle_importer_benchmark_test.cpp +++ /dev/null @@ -1,578 +0,0 @@ -#include "../../oracle/src/oracleimporter.h" - -#include "gtest/gtest.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#if defined(HAS_LZMA) -#include "../../oracle/src/lzma/decompress.h" -#endif -#if defined(HAS_ZLIB) -#include "../../oracle/src/zip/unzip.h" -#endif -#if defined(Q_OS_MACOS) -#include -#include -#endif - -// Helper: build a synthetic MTGJSON-style JSON with the given number of sets and cards per set -static QByteArray buildSyntheticData(int numSets, int cardsPerSet) -{ - QJsonObject dataObj; - for (int s = 0; s < numSets; ++s) { - QJsonArray cardsArray; - for (int c = 0; c < cardsPerSet; ++c) { - QJsonObject card; - card["name"] = QString("Card %1").arg(s * cardsPerSet + c); - card["text"] = "This is a test card with some rules text."; - card["layout"] = "normal"; - card["manaCost"] = "{W}"; - card["type"] = "Creature — Human"; - card["power"] = "2"; - card["toughness"] = "2"; - card["colors"] = QJsonArray{"W"}; - card["colorIdentity"] = QJsonArray{"W"}; - card["types"] = QJsonArray{"Creature"}; - // Real MTGJSON types: floats and booleans, not strings. This - // exercises the QVariant coercion in the property reader. - card["convertedManaCost"] = 1.0; - card["manaValue"] = 1.0; - card["isOnlineOnly"] = false; - card["isRebalanced"] = false; - - QJsonObject legalities; - legalities["standard"] = "legal"; - legalities["modern"] = "legal"; - legalities["legacy"] = "legal"; - legalities["vintage"] = "legal"; - legalities["commander"] = "legal"; - card["legalities"] = legalities; - - QJsonObject identifiers; - identifiers["scryfallId"] = QString("id-%1-%2").arg(s).arg(c); - card["identifiers"] = identifiers; - - // In AllPrintings, number and rarity are flat fields on the card - // object, exactly as set below. - card["number"] = QString::number(c + 1); - card["rarity"] = "common"; - - cardsArray.append(card); - } - - QJsonObject setObj; - setObj["code"] = QString("T%1").arg(s, 2, 10, QChar('0')); - setObj["name"] = QString("Test Set %1").arg(s); - setObj["type"] = "expansion"; - setObj["releaseDate"] = "2024-01-01"; - setObj["cards"] = cardsArray; - - dataObj[QString("T%1").arg(s, 2, 10, QChar('0'))] = setObj; - } - - QJsonObject root; - root["data"] = dataObj; - return QJsonDocument(root).toJson(QJsonDocument::Compact); -} - -// ============================================================================ -// Import throughput benchmark -// ============================================================================ - -TEST(OracleBenchmark, ImportThroughput) -{ - static constexpr int numSets = 10; - static constexpr int cardsPerSet = 500; - - QByteArray data = buildSyntheticData(numSets, cardsPerSet); - - OracleImporter importer; - - // Phase 1: Parse JSON - QElapsedTimer timer; - timer.start(); - bool ok = importer.readSetsFromByteArray(data); - ASSERT_TRUE(ok); - qint64 parseMs = timer.elapsed(); - - // Phase 2: Import cards - timer.restart(); - int importedSets = importer.startImport(); - qint64 importMs = timer.elapsed(); - - int totalImported = 0; - for (const auto &card : importer.getCardList()) { - Q_UNUSED(card); - totalImported++; - } - - // The fixture generates globally unique card names, so the expected - // counts are exact: a regression here means cards were dropped. - ASSERT_EQ(importedSets, numSets); - ASSERT_EQ(totalImported, numSets * cardsPerSet); - // Real-data probe: numeric convertedManaCost must be coerced to text - // (regression for the QJsonValue::toString() reader in #7214). - auto probeCard = importer.getCardList().value("Card 0"); - ASSERT_FALSE(probeCard.isNull()); - ASSERT_EQ(probeCard->getProperty("cmc"), "1"); - - qDebug().noquote() - << QString("Oracle Import Benchmark: %1 sets, %2 unique cards").arg(importedSets).arg(totalImported); - qDebug().noquote() << QString(" JSON parse: %1 ms").arg(parseMs); - qDebug().noquote() << QString(" Card import: %1 ms").arg(importMs); - qDebug().noquote() << QString(" Total: %1 ms").arg(parseMs + importMs); - if (importMs > 0) { - qDebug().noquote() << QString(" Throughput: %1 cards/sec") - .arg(static_cast(totalImported) / importMs * 1000.0, 0, 'f', 0); - } -} - -// ============================================================================ -// readSetsFromByteArray benchmark -// ============================================================================ - -TEST(OracleBenchmark, ParseJsonThroughput) -{ - static constexpr int numSets = 20; - static constexpr int cardsPerSet = 1000; - - QByteArray data = buildSyntheticData(numSets, cardsPerSet); - - // Run 5 iterations and report average - static constexpr int iterations = 5; - qint64 totalMs = 0; - - for (int i = 0; i < iterations; ++i) { - OracleImporter importer; - QByteArray source = data; - QElapsedTimer timer; - timer.start(); - bool ok = importer.readSetsFromByteArray(std::move(source)); - ASSERT_TRUE(ok); - totalMs += timer.elapsed(); - } - - qint64 avgMs = totalMs / iterations; - qDebug().noquote() << QString("Parse Benchmark (%1 iterations): avg %2 ms for %3 sets x %4 cards") - .arg(iterations) - .arg(avgMs) - .arg(numSets) - .arg(cardsPerSet); -} - -// ============================================================================ -// Split card merging benchmark -// ============================================================================ - -TEST(OracleBenchmark, SplitCardMerging) -{ - static constexpr int numSplitCards = 1000; - - QJsonArray cardsList; - for (int i = 0; i < numSplitCards; ++i) { - QJsonObject face1; - face1["name"] = QString("Fire %1 // Ice %1").arg(i); - face1["text"] = "Fire side text."; - face1["layout"] = "split"; - face1["side"] = "a"; - face1["faceName"] = QString("Fire %1").arg(i); - face1["colors"] = QJsonArray{"R"}; - face1["colorIdentity"] = QJsonArray{"R"}; - face1["types"] = QJsonArray{"Instant"}; - face1["manaCost"] = "{R}"; - face1["legalities"] = QJsonObject{{"standard", "not_legal"}}; - face1["identifiers"] = QJsonObject{{"scryfallId", QString("f-%1").arg(i)}}; - face1["number"] = QString::number(i + 1); - face1["rarity"] = "uncommon"; - - QJsonObject face2; - face2["name"] = QString("Fire %1 // Ice %1").arg(i); - face2["text"] = "Ice side text."; - face2["layout"] = "split"; - face2["side"] = "b"; - face2["faceName"] = QString("Ice %1").arg(i); - face2["colors"] = QJsonArray{"U"}; - face2["colorIdentity"] = QJsonArray{"U"}; - face2["types"] = QJsonArray{"Instant"}; - face2["manaCost"] = "{U}"; - face2["legalities"] = QJsonObject{{"standard", "not_legal"}}; - face2["identifiers"] = QJsonObject{{"scryfallId", QString("i-%1").arg(i)}}; - face2["number"] = QString::number(i + 1); - face2["rarity"] = "uncommon"; - - cardsList.append(face1); - cardsList.append(face2); - } - - NoopCardSetPriorityController controller; - OracleImporter importer; - CardSetPtr set = CardSet::newInstance(&controller, "TST", "Split Test"); - - QElapsedTimer timer; - timer.start(); - int count = importer.importCardsFromSet(set, cardsList); - qint64 ms = timer.elapsed(); - - ASSERT_EQ(count, numSplitCards); - qDebug().noquote() << QString("Split Card Merge Benchmark: %1 cards in %2 ms (%3 cards/sec)") - .arg(count) - .arg(ms) - .arg(ms > 0 ? static_cast(count) / ms * 1000.0 : 0.0, 0, 'f', 0); -} - -// ============================================================================ -// sortAndReduceColors microbenchmark -// ============================================================================ - -// We can't call sortAndReduceColors directly (it's static), so we benchmark -// through importCardsFromSet with color properties. - -TEST(OracleBenchmark, ImportCardsWithColors) -{ - static constexpr int numCards = 10000; - - NoopCardSetPriorityController controller; - OracleImporter importer; - CardSetPtr set = CardSet::newInstance(&controller, "TST", "Color Test"); - - QJsonArray cardsList; - for (int i = 0; i < numCards; ++i) { - QJsonObject card; - card["name"] = QString("Color Card %1").arg(i); - card["text"] = "Rules text."; - card["layout"] = "normal"; - card["manaCost"] = "{W}"; - card["type"] = "Creature — Human"; - card["types"] = QJsonArray{"Creature"}; - card["colors"] = QJsonArray{"B", "R", "G", "W", "U"}; - card["colorIdentity"] = QJsonArray{"B", "R", "G", "W", "U"}; - card["number"] = QString::number(i + 1); - card["rarity"] = "common"; - card["legalities"] = QJsonObject{{"standard", "legal"}}; - card["identifiers"] = QJsonObject{{"scryfallId", QString("c-%1").arg(i)}}; - cardsList.append(card); - } - - QElapsedTimer timer; - timer.start(); - int count = importer.importCardsFromSet(set, cardsList); - qint64 ms = timer.elapsed(); - - ASSERT_EQ(count, numCards); - qDebug().noquote() << QString("Import with Colors Benchmark: %1 cards in %2 ms (%3 cards/sec)") - .arg(count) - .arg(ms) - .arg(ms > 0 ? static_cast(count) / ms * 1000.0 : 0.0, 0, 'f', 0); -} - -// ============================================================================ -// RAM usage measurement -// ============================================================================ - -// Mirrors the default AllPrintings URL selection in oracle/src/pages.cpp. -#if defined(HAS_LZMA) -static const QUrl kDefaultAllPrintingsUrl("https://www.mtgjson.com/api/v5/AllPrintings.json.xz"); -#elif defined(HAS_ZLIB) -static const QUrl kDefaultAllPrintingsUrl("https://www.mtgjson.com/api/v5/AllPrintings.json.zip"); -#else -static const QUrl kDefaultAllPrintingsUrl("https://www.mtgjson.com/api/v5/AllPrintings.json"); -#endif - -// Magic bytes also from oracle/src/pages.cpp -static const QByteArray kXzSignature("\xFD\x37\x7A\x58\x5A", 6); -static const QByteArray kZipSignature("PK"); - -struct MemorySnapshot -{ - qint64 peakRssKb = -1; // process high-water mark (VmHWM on Linux, ru_maxrss on macOS) - qint64 rssKb = -1; // current resident set size - bool available = false; - - static MemorySnapshot current() - { - MemorySnapshot snap; -#if defined(Q_OS_LINUX) - QFile statusFile("/proc/self/status"); - if (statusFile.open(QIODevice::ReadOnly | QIODevice::Text)) { - // /proc files report size() == 0, so atEnd() is immediately true: read everything first. - const QList lines = statusFile.readAll().split('\n'); - for (const QByteArray &line : lines) { - if (line.startsWith("VmHWM:")) { - snap.peakRssKb = line.mid(6).trimmed().split(' ').value(0).toLongLong(); - } else if (line.startsWith("VmRSS:")) { - snap.rssKb = line.mid(6).trimmed().split(' ').value(0).toLongLong(); - } - } - snap.available = snap.peakRssKb >= 0; - } -#elif defined(Q_OS_MACOS) - struct rusage usage; - if (getrusage(RUSAGE_SELF, &usage) == 0) { - snap.peakRssKb = usage.ru_maxrss / 1024; // bytes -> kB - snap.available = snap.peakRssKb >= 0; - } - // getrusage has no current-RSS equivalent; task_info's resident_size - // is the closest macOS analog to Linux VmRSS. - mach_task_basic_info info = {}; - mach_msg_type_number_t count = MACH_TASK_BASIC_INFO_COUNT; - if (task_info(mach_task_self(), MACH_TASK_BASIC_INFO, reinterpret_cast(&info), &count) == - KERN_SUCCESS) { - snap.rssKb = info.resident_size / 1024; - } -#endif - return snap; - } -}; - -static QString formatKb(qint64 kb) -{ - if (kb < 0) { - return "N/A"; - } - return QString("%1 MB").arg(kb / 1024.0, 0, 'f', 1); -} - -static void logRamPhase(const QString &phase, const MemorySnapshot &baseline, const MemorySnapshot ¤t) -{ - if (!baseline.available || !current.available) { - qDebug().noquote() << QString(" %1: memory stats unavailable on this platform").arg(phase); - return; - } - // VmHWM / ru_maxrss are monotonically non-decreasing high-water marks, so a - // peak-based delta between phases is ~0.0 MB by construction once the - // fixture build has set the process peak. The live signals are current RSS - // and the process peak; the delta is meaningful only where the baseline was - // taken immediately before the phase it measures (e.g. the import phase, - // which compares afterParse against afterImport). - QString rssDelta = "N/A"; - if (current.rssKb >= 0 && baseline.rssKb >= 0) { - rssDelta = formatKb(current.rssKb - baseline.rssKb); - } - qDebug().noquote() << QString(" %1: current RSS %2 | delta vs baseline %3 | process peak %4") - .arg(phase) - .arg(formatKb(current.rssKb)) - .arg(rssDelta) - .arg(formatKb(current.peakRssKb)); -} - -// Decompresses the download payload when the default URL is a compressed build, -// mirroring the wizard's magic-byte handling in oracle/src/pages.cpp. -static QByteArray decompressSetsData(const QByteArray &payload) -{ - if (payload.startsWith(kXzSignature)) { -#if defined(HAS_LZMA) - QBuffer inBuffer(const_cast(&payload)); - QByteArray out; - QBuffer outBuffer(&out); - inBuffer.open(QIODevice::ReadOnly); - outBuffer.open(QIODevice::WriteOnly); - XzDecompressor xz; - if (!xz.decompress(&inBuffer, &outBuffer)) { - qDebug() << "RAM benchmark: xz decompression failed"; - return {}; - } - return out; -#else - qDebug() << "RAM benchmark: download is xz-compressed but this build has no LZMA support"; - return {}; -#endif - } - if (payload.startsWith(kZipSignature)) { -#if defined(HAS_ZLIB) - QBuffer inBuffer(const_cast(&payload)); - inBuffer.open(QIODevice::ReadOnly); - UnZip unzip; - if (unzip.openArchive(&inBuffer) != UnZip::Ok) { - qDebug() << "RAM benchmark: zip archive open failed"; - return {}; - } - if (unzip.fileList().size() != 1) { - qDebug() << "RAM benchmark: zip archive doesn't contain exactly one file"; - return {}; - } - QByteArray out; - QBuffer outBuffer(&out); - outBuffer.open(QIODevice::WriteOnly); - const auto errorCode = unzip.extractFile(unzip.fileList().value(0), &outBuffer); - unzip.closeArchive(); - if (errorCode != UnZip::Ok) { - qDebug() << "RAM benchmark: zip extraction failed"; - return {}; - } - return out; -#else - qDebug() << "RAM benchmark: download is zip-compressed but this build has no zlib support"; - return {}; -#endif - } - return payload; -} - -TEST(OracleBenchmark, ImportRamUsage) -{ - static constexpr int numSets = 30; - static constexpr int cardsPerSet = 2000; // ~60k cards, roughly AllPrintings scale - - // Baseline must precede the fixture build: a high-water mark set while - // generating the synthetic JSON would otherwise mask the importer phases. - // Where memory stats are unavailable (Windows), skip before doing the - // 60k-card fixture build, which would otherwise be pure wasted work. - const MemorySnapshot baseline = MemorySnapshot::current(); - if (!baseline.available) { - GTEST_SKIP() << "Memory stats unavailable on this platform"; - } - - const QByteArray data = buildSyntheticData(numSets, cardsPerSet); - - // The fixture build leaves freed-but-unreturned arenas behind (current RSS - // rarely falls once glibc allocates). Baseline immediately after it so the - // parse phase measures only the importer's own growth (~40 MB) rather than - // swallowing the fixture builder's spike. - const MemorySnapshot afterFixture = MemorySnapshot::current(); - logRamPhase("fixture build", baseline, afterFixture); - - NoopCardSetPriorityController controller; - OracleImporter importer; - - QElapsedTimer timer; - timer.start(); - ASSERT_TRUE(importer.readSetsFromByteArray(std::move(data))); - const qint64 parseMs = timer.elapsed(); - const MemorySnapshot afterParse = MemorySnapshot::current(); - - timer.restart(); - const int importedSets = importer.startImport(); - const qint64 importMs = timer.elapsed(); - const MemorySnapshot afterImport = MemorySnapshot::current(); - - importer.releaseSetData(); - const MemorySnapshot afterRelease = MemorySnapshot::current(); - - const int totalCards = importer.getCardList().size(); - qDebug().noquote() << QString("Oracle RAM Benchmark (synthetic): %1 sets, %2 cards, %3 MB JSON") - .arg(importedSets) - .arg(totalCards) - .arg(data.size() / (1024.0 * 1024.0), 0, 'f', 1); - qDebug().noquote() << QString(" JSON parse: %1 ms").arg(parseMs); - qDebug().noquote() << QString(" Card import: %1 ms").arg(importMs); - logRamPhase("parse", afterFixture, afterParse); - logRamPhase("import", afterParse, afterImport); - logRamPhase("after releaseSetData()", afterImport, afterRelease); - - // Freeing the parsed tree rarely moves current RSS (allocator reuse), so the - // meaningful signal that release actually dropped the buffers is emptiness, - // not an RSS delta. - ASSERT_TRUE(importer.getSets().isEmpty()); -} - -TEST(OracleBenchmark, ImportRamUsageAllPrintings) -{ - // Only "1" enables the download: unset (the default and the CI setup) and - // an explicit "0" both disable it. - bool envOk = false; - const int enabled = qEnvironmentVariableIntValue("COCKATRICE_ORACLE_RAM_BENCHMARK", &envOk); - if (!envOk || enabled == 0) { - GTEST_SKIP() << "Set COCKATRICE_ORACLE_RAM_BENCHMARK=1 to download the real AllPrintings dataset for this " - "RAM benchmark. Default URL: " - << kDefaultAllPrintingsUrl.toDisplayString().toStdString(); - } - - // Baseline must precede the request so the phase covers the download + - // decompress step, including the payload materialized by readAll(). - const MemorySnapshot baseline = MemorySnapshot::current(); - if (!baseline.available) { - GTEST_SKIP() << "Memory stats unavailable on this platform"; - } - - QNetworkAccessManager nam; - QNetworkRequest request(kDefaultAllPrintingsUrl); - request.setHeader(QNetworkRequest::UserAgentHeader, "Cockatrice Oracle RAM benchmark"); - QNetworkReply *reply = nam.get(request); - - QEventLoop loop; - QTimer timeoutTimer; - timeoutTimer.setSingleShot(true); - bool timedOut = false; - QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit); - QObject::connect(&timeoutTimer, &QTimer::timeout, &loop, [&] { - timedOut = true; - reply->abort(); - }); - timeoutTimer.start(10 * 60 * 1000); - loop.exec(); - timeoutTimer.stop(); - - // abort() leaves reply->error() as OperationCanceledError, so a timed-out - // download takes the same GTEST_SKIP path as any other network error - // instead of reading a truncated body and failing the parse below. - if (timedOut || reply->error() != QNetworkReply::NoError) { - GTEST_SKIP() << "Download failed: " << reply->errorString().toStdString(); - } - const QByteArray payload = reply->readAll(); - reply->deleteLater(); - - // mtgjson can answer 200 with an HTML page (mirrors the wizard's '<' check - // in pages.cpp); reject it before trying to decompress/parse. - if (payload.startsWith("<")) { - GTEST_SKIP() << "Download returned a non-JSON body (HTML page instead of data), skipping"; - } - - const QByteArray setsData = decompressSetsData(payload); - const MemorySnapshot afterDownload = MemorySnapshot::current(); - if (setsData.isEmpty()) { - GTEST_SKIP() << "No data to import (download or decompression failed)"; - } - - NoopCardSetPriorityController controller; - OracleImporter importer; - - QElapsedTimer timer; - timer.start(); - ASSERT_TRUE(importer.readSetsFromByteArray(std::move(setsData))); - const qint64 parseMs = timer.elapsed(); - const MemorySnapshot afterParse = MemorySnapshot::current(); - - timer.restart(); - const int importedSets = importer.startImport(); - const qint64 importMs = timer.elapsed(); - const MemorySnapshot afterImport = MemorySnapshot::current(); - - importer.releaseSetData(); - const MemorySnapshot afterRelease = MemorySnapshot::current(); - - const int totalCards = importer.getCardList().size(); - qDebug().noquote() << QString("Oracle RAM Benchmark (real AllPrintings): %1 sets, %2 unique cards") - .arg(importedSets) - .arg(totalCards); - qDebug().noquote() << QString(" URL: %1").arg(kDefaultAllPrintingsUrl.toDisplayString()); - qDebug().noquote() << QString(" Downloaded: %1 MB, decompressed: %2 MB") - .arg(payload.size() / (1024.0 * 1024.0), 0, 'f', 1) - .arg(setsData.size() / (1024.0 * 1024.0), 0, 'f', 1); - qDebug().noquote() << QString(" JSON parse: %1 ms").arg(parseMs); - qDebug().noquote() << QString(" Card import: %1 ms").arg(importMs); - logRamPhase("download+decompress", baseline, afterDownload); - logRamPhase("parse", afterDownload, afterParse); - logRamPhase("import", afterParse, afterImport); - logRamPhase("after releaseSetData()", afterImport, afterRelease); -} - -int main(int argc, char **argv) -{ - // Required for the event loop used by the real-AllPrintings download benchmark - QCoreApplication app(argc, argv); - ::testing::InitGoogleTest(&argc, argv); - return RUN_ALL_TESTS(); -} diff --git a/tests/oracle/oracle_importer_test.cpp b/tests/oracle/oracle_importer_test.cpp deleted file mode 100644 index f66616e37..000000000 --- a/tests/oracle/oracle_importer_test.cpp +++ /dev/null @@ -1,1128 +0,0 @@ -#include "../../oracle/src/oracleimporter.h" - -#include "gtest/gtest.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include - -class OracleImporterTest : public ::testing::Test -{ -protected: - void SetUp() override - { - controller = new NoopCardSetPriorityController(); - importer = new OracleImporter(); - set = CardSet::newInstance(controller, "TST", "Test Set"); - } - - void TearDown() override - { - delete importer; - delete controller; - } - - // Helper: build a minimal card JSON object - QJsonObject makeCard(const QString &name, - const QString &colors = "", - const QString &colorIdentity = "", - const QVariantMap &legalities = {}) - { - QJsonObject card; - card["name"] = name; - card["text"] = "Rules text."; - card["layout"] = "normal"; - card["manaCost"] = "{W}"; - card["type"] = "Creature — Human"; - card["types"] = QJsonArray{"Creature"}; - card["number"] = "1"; - card["rarity"] = "common"; - - if (!colors.isEmpty()) { - QJsonArray arr; - for (const QChar &c : colors) { - arr.append(QString(c)); - } - card["colors"] = arr; - } - if (!colorIdentity.isEmpty()) { - QJsonArray arr; - for (const QChar &c : colorIdentity) { - arr.append(QString(c)); - } - card["colorIdentity"] = arr; - } - if (!legalities.isEmpty()) { - QJsonObject legalObj; - for (auto it = legalities.constBegin(); it != legalities.constEnd(); ++it) { - legalObj[it.key()] = it.value().toString(); - } - card["legalities"] = legalObj; - } - - QJsonObject identifiers; - identifiers["scryfallId"] = QUuid::createUuid().toString(QUuid::WithoutBraces); - card["identifiers"] = identifiers; - - return card; - } - - // Helper: build a single MTGJSON foreignData entry - QJsonObject makeForeignEntry(const QString &language, const QString &name, const QString &text) - { - QJsonObject entry; - entry["language"] = language; - entry["name"] = name; - if (!text.isEmpty()) { - entry["text"] = text; - } - return entry; - } - - NoopCardSetPriorityController *controller; - OracleImporter *importer; - CardSetPtr set; -}; - -// ============================================================================ -// sortAndReduceColors tests (tested via importCardsFromSet) -// ============================================================================ - -TEST_F(OracleImporterTest, SortAndReduceColorsSingleColor) -{ - QJsonArray cards{makeCard("Red Card", "R", "R")}; - importer->importCardsFromSet(set, cards); - - auto card = importer->getCardList().value("Red Card"); - ASSERT_FALSE(card.isNull()); - ASSERT_EQ(card->getProperty("colors"), "R"); -} - -TEST_F(OracleImporterTest, SortAndReduceColorsDeduplicates) -{ - QJsonArray cards{makeCard("Dedup Card", "WWUUB", "WU")}; - importer->importCardsFromSet(set, cards); - - auto card = importer->getCardList().value("Dedup Card"); - ASSERT_FALSE(card.isNull()); - ASSERT_EQ(card->getProperty("colors"), "WUB"); -} - -TEST_F(OracleImporterTest, SortAndReduceColorsSortsWUBRG) -{ - QJsonArray cards{makeCard("Sort Card", "RGW", "RGW")}; - importer->importCardsFromSet(set, cards); - - auto card = importer->getCardList().value("Sort Card"); - ASSERT_FALSE(card.isNull()); - ASSERT_EQ(card->getProperty("colors"), "WRG"); -} - -TEST_F(OracleImporterTest, SortAndReduceColorsAllFive) -{ - QJsonArray cards{makeCard("Five Color", "BRGWU", "BRGWU")}; - importer->importCardsFromSet(set, cards); - - auto card = importer->getCardList().value("Five Color"); - ASSERT_FALSE(card.isNull()); - ASSERT_EQ(card->getProperty("colors"), "WUBRG"); -} - -TEST_F(OracleImporterTest, SortAndReduceColorIdentity) -{ - QJsonArray cards{makeCard("Color Id Card", "W", "GWR")}; - importer->importCardsFromSet(set, cards); - - auto card = importer->getCardList().value("Color Id Card"); - ASSERT_FALSE(card.isNull()); - ASSERT_EQ(card->getProperty("coloridentity"), "WRG"); -} - -TEST_F(OracleImporterTest, SingleColorNotSorted) -{ - QJsonArray cards{makeCard("Single Card", "B", "B")}; - importer->importCardsFromSet(set, cards); - - auto card = importer->getCardList().value("Single Card"); - ASSERT_FALSE(card.isNull()); - ASSERT_EQ(card->getProperty("colors"), "B"); -} - -// ============================================================================ -// Legality guard tests -// ============================================================================ - -TEST_F(OracleImporterTest, NewCardKeepsLegalityProperties) -{ - // Verifies that format-* properties survive addCard on a fresh card - // (not the combineLegalities guard, which only runs on existing printings). - QVariantMap leg; - leg["standard"] = "legal"; - leg["modern"] = "legal"; - QJsonArray cards{makeCard("Legal Card", "", "", leg)}; - - importer->importCardsFromSet(set, cards); - auto card = importer->getCardList().value("Legal Card"); - ASSERT_FALSE(card.isNull()); - ASSERT_EQ(card->getProperty("format-standard"), "legal"); - ASSERT_EQ(card->getProperty("format-modern"), "legal"); -} - -TEST_F(OracleImporterTest, LegalityMergeAllowedWhenCardHasNoLegalities) -{ - // First printing carries no legalities at all, so the guard's - // `properties.filter(formatRegex).empty()` predicate is true and the - // second printing's legalities must be merged in. - QJsonArray cards1{makeCard("Unmerged Card")}; - importer->importCardsFromSet(set, cards1); - - CardSetPtr set2 = CardSet::newInstance(controller, "TS2", "Second Set"); - QVariantMap leg; - leg["standard"] = "legal"; - QJsonArray cards2{makeCard("Unmerged Card", "", "", leg)}; - importer->importCardsFromSet(set2, cards2); - - auto card = importer->getCardList().value("Unmerged Card"); - ASSERT_FALSE(card.isNull()); - ASSERT_EQ(card->getProperty("format-standard"), "legal"); -} - -TEST_F(OracleImporterTest, LegalityGuardPreservesFirstPrinting) -{ - // First printing: standard=legal, modern=legal - QVariantMap leg1; - leg1["standard"] = "legal"; - leg1["modern"] = "legal"; - QJsonArray cards1{makeCard("Guarded Card", "", "", leg1)}; - importer->importCardsFromSet(set, cards1); - - // Second printing: standard=banned, modern=not_legal - CardSetPtr set2 = CardSet::newInstance(controller, "TS2", "Second Set"); - QVariantMap leg2; - leg2["standard"] = "banned"; - leg2["modern"] = "not_legal"; - QJsonArray cards2{makeCard("Guarded Card", "", "", leg2)}; - importer->importCardsFromSet(set2, cards2); - - auto card = importer->getCardList().value("Guarded Card"); - ASSERT_FALSE(card.isNull()); - // Guard should preserve first printing's legalities - ASSERT_EQ(card->getProperty("format-standard"), "legal"); - ASSERT_EQ(card->getProperty("format-modern"), "legal"); -} - -// ============================================================================ -// createDefaultMagicFormats tests -// ============================================================================ - -TEST_F(OracleImporterTest, CreateDefaultMagicFormatsContainsExpectedFormats) -{ - auto formats = importer->createDefaultMagicFormats(); - ASSERT_TRUE(formats.contains("standard")); - ASSERT_TRUE(formats.contains("modern")); - ASSERT_TRUE(formats.contains("legacy")); - ASSERT_TRUE(formats.contains("vintage")); - ASSERT_TRUE(formats.contains("commander")); - ASSERT_TRUE(formats.contains("pauper")); - ASSERT_TRUE(formats.contains("pioneer")); - ASSERT_TRUE(formats.contains("brawl")); - ASSERT_TRUE(formats.contains("historic")); - ASSERT_TRUE(formats.contains("timeless")); - ASSERT_TRUE(formats.contains("duel")); - ASSERT_TRUE(formats.contains("oathbreaker")); -} - -TEST_F(OracleImporterTest, CreateDefaultMagicFormatsSingletonDeckSizes) -{ - auto formats = importer->createDefaultMagicFormats(); - auto commander = formats.value("commander"); - ASSERT_FALSE(commander.isNull()); - ASSERT_EQ(commander->minDeckSize, 100); - ASSERT_EQ(commander->maxDeckSize, 100); - ASSERT_EQ(commander->maxSideboardSize, 15); - - auto brawl = formats.value("brawl"); - ASSERT_FALSE(brawl.isNull()); - ASSERT_EQ(brawl->minDeckSize, 60); - ASSERT_EQ(brawl->maxDeckSize, 60); -} - -TEST_F(OracleImporterTest, CreateDefaultMagicFormatsVintageHasRestricted) -{ - auto formats = importer->createDefaultMagicFormats(); - auto vintage = formats.value("vintage"); - ASSERT_FALSE(vintage.isNull()); - bool hasRestricted = false; - for (const auto &ac : vintage->allowedCounts) { - if (ac.label == "restricted") { - hasRestricted = true; - ASSERT_EQ(ac.max, 1); - } - } - ASSERT_TRUE(hasRestricted); -} - -TEST_F(OracleImporterTest, CreateDefaultMagicFormatsRegexMatchesBasicLands) -{ - auto formats = importer->createDefaultMagicFormats(); - auto standard = formats.value("standard"); - ASSERT_FALSE(standard.isNull()); - ASSERT_FALSE(standard->exceptions.isEmpty()); - - auto &basicLandsException = standard->exceptions.first(); - ASSERT_FALSE(basicLandsException.conditions.isEmpty()); - - auto &condition = basicLandsException.conditions.first(); - ASSERT_EQ(condition.field, "type"); - ASSERT_EQ(condition.matchType, "regex"); - - // Verify the regex actually works (was broken before: \b = backspace, not word boundary) - QRegularExpression regex(condition.value); - ASSERT_TRUE(regex.isValid()); - ASSERT_TRUE(regex.match("Basic Land — Forest").hasMatch()); - ASSERT_TRUE(regex.match("Basic Snow Land — Mountain").hasMatch()); - ASSERT_FALSE(regex.match("Creature — Elf Warrior").hasMatch()); -} - -TEST_F(OracleImporterTest, CreateDefaultMagicFormatsCaching) -{ - // The memoized map returns the same FormatRulesPtr instances, so the - // shared pointers must be identical across calls. This is the only - // observable effect of the cache: contents would match either way. - auto first = importer->createDefaultMagicFormats(); - auto second = importer->createDefaultMagicFormats(); - ASSERT_EQ(first.value("standard").data(), second.value("standard").data()); -} - -// ============================================================================ -// readSetsFromByteArray tests -// ============================================================================ - -TEST_F(OracleImporterTest, ReadSetsFromByteArrayValidJson) -{ - QJsonObject setObj; - setObj["code"] = "tst"; - setObj["name"] = "Test Set"; - setObj["type"] = "expansion"; - setObj["releaseDate"] = "2024-01-01"; - setObj["cards"] = QJsonArray(); - - QJsonObject root; - root["data"] = QJsonObject{{"TST", setObj}}; - - QByteArray data = QJsonDocument(root).toJson(); - ASSERT_TRUE(importer->readSetsFromByteArray(data)); - ASSERT_EQ(importer->getSets().size(), 1); - ASSERT_EQ(importer->getSets().first().getShortName(), "TST"); -} - -TEST_F(OracleImporterTest, ReadSetsFromByteArrayInvalidJson) -{ - QByteArray data = "not valid json"; - ASSERT_FALSE(importer->readSetsFromByteArray(data)); -} - -TEST_F(OracleImporterTest, ReadSetsFromByteArrayEmptyData) -{ - QJsonObject root; - root["data"] = QJsonObject(); - - QByteArray data = QJsonDocument(root).toJson(); - ASSERT_FALSE(importer->readSetsFromByteArray(data)); -} - -TEST_F(OracleImporterTest, ReadSetsFromByteArrayCapitalizesSetType) -{ - QJsonObject setObj; - setObj["code"] = "ftv"; - setObj["name"] = "From The Vault"; - setObj["type"] = "from_the_vault"; - setObj["releaseDate"] = "2024-01-01"; - setObj["cards"] = QJsonArray(); - - QJsonObject root; - root["data"] = QJsonObject{{"FTV", setObj}}; - - QByteArray data = QJsonDocument(root).toJson(); - ASSERT_TRUE(importer->readSetsFromByteArray(data)); - ASSERT_EQ(importer->getSets().first().getSetType(), "From the Vault"); -} - -TEST_F(OracleImporterTest, ReadSetsFromByteArraySortsSetsByName) -{ - // QJsonObject iterates keys in lexicographic order ("AAA" before "ZZZ"), - // so leaving the natural order matching the alphabetical sort makes the - // assertion pass trivially. Inverting it keeps the sort meaningful: - // iteration yields "AAA" (Zeta Set) first, then the sort by name must - // promote "ZZZ" (Alpha Set) to the front. - QJsonObject setA; - setA["code"] = "aaa"; - setA["name"] = "Zeta Set"; - setA["type"] = "expansion"; - setA["releaseDate"] = "2024-01-01"; - setA["cards"] = QJsonArray(); - - QJsonObject setB; - setB["code"] = "zzz"; - setB["name"] = "Alpha Set"; - setB["type"] = "expansion"; - setB["releaseDate"] = "2024-01-01"; - setB["cards"] = QJsonArray(); - - QJsonObject root; - root["data"] = QJsonObject{{"AAA", setA}, {"ZZZ", setB}}; - - QByteArray data = QJsonDocument(root).toJson(); - ASSERT_TRUE(importer->readSetsFromByteArray(data)); - auto sets = importer->getSets(); - ASSERT_GE(sets.size(), 2); - ASSERT_EQ(sets.first().getShortName(), "ZZZ"); -} - -// ============================================================================ -// Split card coloridentity tests -// ============================================================================ - -TEST_F(OracleImporterTest, SplitCardColorIdentityConcatenated) -{ - QJsonObject leg{{"standard", "not_legal"}}; - - QJsonObject face1; - face1["name"] = "Fire // Ice"; - face1["text"] = "Fire deals 2 damage."; - face1["layout"] = "split"; - face1["side"] = "a"; - face1["faceName"] = "Fire"; - face1["colors"] = QJsonArray{"R"}; - face1["colorIdentity"] = QJsonArray{"R"}; - face1["types"] = QJsonArray{"Instant"}; - face1["manaCost"] = "{R}"; - face1["legalities"] = leg; - face1["identifiers"] = QJsonObject{{"scryfallId", "aaa"}}; - face1["number"] = "1"; - face1["rarity"] = "uncommon"; - - QJsonObject face2; - face2["name"] = "Fire // Ice"; - face2["text"] = "Ice taps target artifact."; - face2["layout"] = "split"; - face2["side"] = "b"; - face2["faceName"] = "Ice"; - face2["colors"] = QJsonArray{"U"}; - face2["colorIdentity"] = QJsonArray{"U"}; - face2["types"] = QJsonArray{"Instant"}; - face2["manaCost"] = "{U}"; - face2["legalities"] = leg; - face2["identifiers"] = QJsonObject{{"scryfallId", "bbb"}}; - face2["number"] = "1"; - face2["rarity"] = "uncommon"; - - QJsonArray cardsList{face1, face2}; - int count = importer->importCardsFromSet(set, cardsList); - ASSERT_EQ(count, 1); - - auto card = importer->getCardList().value("Fire // Ice"); - ASSERT_FALSE(card.isNull()); - - // coloridentity should be "RU" (concatenated), then sorted to "UR" - // by sortAndReduceColors when it reaches addCard - ASSERT_EQ(card->getProperty("coloridentity"), "UR"); -} - -TEST_F(OracleImporterTest, SplitCardColorsConcatenated) -{ - QJsonObject leg{{"standard", "not_legal"}}; - - QJsonObject face1; - face1["name"] = "Fire // Ice"; - face1["text"] = "Fire deals 2 damage."; - face1["layout"] = "split"; - face1["side"] = "a"; - face1["faceName"] = "Fire"; - face1["colors"] = QJsonArray{"R"}; - face1["colorIdentity"] = QJsonArray{"R"}; - face1["types"] = QJsonArray{"Instant"}; - face1["manaCost"] = "{R}"; - face1["legalities"] = leg; - face1["identifiers"] = QJsonObject{{"scryfallId", "aaa"}}; - face1["number"] = "1"; - face1["rarity"] = "uncommon"; - - QJsonObject face2; - face2["name"] = "Fire // Ice"; - face2["text"] = "Ice taps target artifact."; - face2["layout"] = "split"; - face2["side"] = "b"; - face2["faceName"] = "Ice"; - face2["colors"] = QJsonArray{"U"}; - face2["colorIdentity"] = QJsonArray{"U"}; - face2["types"] = QJsonArray{"Instant"}; - face2["manaCost"] = "{U}"; - face2["legalities"] = leg; - face2["identifiers"] = QJsonObject{{"scryfallId", "bbb"}}; - face2["number"] = "1"; - face2["rarity"] = "uncommon"; - - QJsonArray cardsList{face1, face2}; - importer->importCardsFromSet(set, cardsList); - - auto card = importer->getCardList().value("Fire // Ice"); - ASSERT_FALSE(card.isNull()); - - QString colors = card->getProperty("colors"); - ASSERT_FALSE(colors.contains("//")) << "colors should not contain '//', got: " << colors.toStdString(); - ASSERT_TRUE(colors.contains("R")); - ASSERT_TRUE(colors.contains("U")); -} - -// ============================================================================ -// Mana cost formatting tests -// ============================================================================ - -TEST_F(OracleImporterTest, ManaCostStripsBraces) -{ - QJsonObject card = makeCard("Mana Card"); - card["manaCost"] = "{2}{W}{B}"; - QJsonArray cards{card}; - - importer->importCardsFromSet(set, cards); - auto result = importer->getCardList().value("Mana Card"); - ASSERT_FALSE(result.isNull()); - ASSERT_EQ(result->getProperty("manacost"), "2WB"); -} - -// cmc comes through as a JSON number ("convertedManaCost"/"manaValue" are -// floats in AllPrintings), so this pins the number-to-text coercion that -// QJsonValue::toString() dropped in #7214. -TEST_F(OracleImporterTest, NumericManaValueCoercedToCmc) -{ - QJsonObject card = makeCard("Cmc Card"); - card["manaValue"] = 3; - QJsonArray cards{card}; - - importer->importCardsFromSet(set, cards); - auto result = importer->getCardList().value("Cmc Card"); - ASSERT_FALSE(result.isNull()); - ASSERT_EQ(result->getProperty("cmc"), "3"); -} - -TEST_F(OracleImporterTest, LegacyConvertedManaCostCoercedToCmc) -{ - QJsonObject card = makeCard("Legacy Cmc Card"); - card["convertedManaCost"] = 3.0; - QJsonArray cards{card}; - - importer->importCardsFromSet(set, cards); - auto result = importer->getCardList().value("Legacy Cmc Card"); - ASSERT_FALSE(result.isNull()); - ASSERT_EQ(result->getProperty("cmc"), "3"); -} - -// ============================================================================ -// Card deduplication tests -// ============================================================================ - -TEST_F(OracleImporterTest, DuplicateCardNameReturnsExisting) -{ - QJsonArray cards{makeCard("Dupe Card")}; - importer->importCardsFromSet(set, cards); - - CardSetPtr set2 = CardSet::newInstance(controller, "TS2", "Second Set"); - QJsonArray cards2{makeCard("Dupe Card")}; - importer->importCardsFromSet(set2, cards2); - - ASSERT_EQ(importer->getCardList().size(), 1); -} - -TEST_F(OracleImporterTest, AELigatureReplaced) -{ - QJsonObject card = makeCard(QString::fromUtf8("\xC3\x86ther Vial")); // Æther Vial - QJsonArray cards{card}; - - importer->importCardsFromSet(set, cards); - // Æ is replaced with AE, resulting in "AEther Vial" - ASSERT_FALSE(importer->getCardList().contains(QString::fromUtf8("\xC3\x86ther Vial"))); - ASSERT_TRUE(importer->getCardList().contains("AEther Vial")); -} - -TEST_F(OracleImporterTest, ApostropheNormalized) -{ - QJsonObject card = makeCard(QString::fromUtf8("Jace\u2019s Ingenuity")); - QJsonArray cards{card}; - - importer->importCardsFromSet(set, cards); - ASSERT_TRUE(importer->getCardList().contains("Jace's Ingenuity")); -} - -// ============================================================================ -// RawJson scanner tests -// ============================================================================ - -TEST_F(OracleImporterTest, ScanSetRangesMatchFullJsonParse) -{ - QJsonObject root; - QJsonObject data; - data["AAA"] = makeCard("Alpha Card"); - data["BBB"] = makeCard("Beta Card"); - root["data"] = data; - - const QByteArray bytes = QJsonDocument(root).toJson(QJsonDocument::Compact); - - RawJson::ScanError error; - const QList ranges = RawJson::scanSetRanges(bytes, &error); - ASSERT_FALSE(error.isError()) << error.message.toStdString(); - ASSERT_EQ(ranges.size(), 2); - - const QJsonObject wholeData = QJsonDocument::fromJson(bytes).object().value("data").toObject(); - for (const RawJson::SetRange &range : ranges) { - QJsonParseError parseError; - const QJsonDocument sliceDoc = QJsonDocument::fromJson( - QByteArray(bytes.constData() + range.dataRange.start, range.dataRange.length), &parseError); - ASSERT_EQ(parseError.error, QJsonParseError::NoError) - << range.code.toStdString() << ": " << parseError.errorString().toStdString(); - ASSERT_EQ(sliceDoc.object(), wholeData.value(range.code).toObject()) << "set " << range.code.toStdString(); - } -} - -TEST_F(OracleImporterTest, ScanSetRangesDecodesEscapesAndCountsCards) -{ - const QByteArray json = "{\"data\":{\"KEY\":{\"code\":\"zzz\",\"name\":\"\\u00c9tude \\ud83d\\ude00\"," - "\"type\":\"expansion\",\"releaseDate\":\"2024-01-05\"," - "\"cards\":[{\"name\":\"a\"},{\"name\":\"b\"},{\"name\":\"c\"}]}}}"; - - RawJson::ScanError error; - const QList ranges = RawJson::scanSetRanges(json, &error); - ASSERT_FALSE(error.isError()); - ASSERT_EQ(ranges.size(), 1); - - const RawJson::SetRange &range = ranges.first(); - ASSERT_EQ(range.code, "zzz"); // inner "code" wins over the object key - const QString expectedName = QString::fromUtf8("\xC3\x89tude ") + QChar(0xD83D) + QChar(0xDE00); - ASSERT_EQ(range.name, expectedName); - ASSERT_EQ(range.type, "expansion"); - ASSERT_EQ(range.releaseDate, "2024-01-05"); - ASSERT_EQ(range.dataRange.cardCount, 3); - - QJsonParseError parseError; - const QJsonDocument sliceDoc = QJsonDocument::fromJson( - QByteArray(json.constData() + range.dataRange.start, range.dataRange.length), &parseError); - ASSERT_EQ(parseError.error, QJsonParseError::NoError); - ASSERT_EQ(sliceDoc.object().value("name").toString(), expectedName); - ASSERT_EQ(sliceDoc.object().value("cards").toArray().size(), 3); -} - -TEST_F(OracleImporterTest, ScanSetRangesRejectsInvalidJson) -{ - const QList invalid = {"not json", - "[]", - "{\"data\":[]}", - "{\"data\":{}}", - "{\"other\":{}}", - "{\"data\":{\"A\":{\"code\":\"a\",\"name\":\"ok\",\"type\":\"x\"," - "\"releaseDate\":\"2024-01-01\",\"cards\":[]}}} trailing", - "{\"data\":{\"A\":{\"cards\":[{\"name\":\"\\uZZZZ\"}]}}}", - "{\"data\":{\"A\":{\"cards\":[{\"name\":\"bad \\q escape\"}]}}}", - "{\"data\":{\"A\":{\"cards\":[{\"name\":\"\\ud800\"}]}}}"}; - - for (const QByteArray &json : invalid) { - RawJson::ScanError error; - RawJson::scanSetRanges(json, &error); - EXPECT_TRUE(error.isError()) << "expected failure for: " << json.constData(); - } -} - -TEST_F(OracleImporterTest, ScanSetRangesMatchesFullJsonParseVerdicts) -{ - // Verdicts must agree with QJsonDocument::fromJson for the inputs below — - // including the metadata quirks ("name": null, "type": 7, "releaseDate": null, - // "cards": null) that used to make the scanner reject sets Qt accepts. - const QList inputs = { - "{\"data\":{\"A\":{\"code\":\"a\",\"name\":\"ok\",\"type\":\"x\",\"releaseDate\":\"2024-01-01\",\"cards\":[{" - "\"n\":1}]}}}", - "{\"data\":{\"A\":{\"code\":\"a\",\"name\":null,\"type\":\"x\",\"releaseDate\":\"2024-01-01\",\"cards\":[]}}}", - "{\"data\":{\"A\":{\"code\":\"a\",\"name\":\"ok\",\"type\":null,\"releaseDate\":\"2024-01-01\",\"cards\":[]}}}", - "{\"data\":{\"A\":{\"code\":\"a\",\"name\":\"ok\",\"type\":7,\"releaseDate\":\"2024-01-01\",\"cards\":null}}}", - "{\"data\":{\"A\":{\"code\":\"a\",\"name\":\"ok\",\"releaseDate\":\"2024-01-01\",\"cards\":[1,2,3]}}}", - // unescaped control character inside a string: QJsonDocument and - // skipString both accept it, so the scanner must not reject the whole doc - "{\"data\":{\"A\":{\"code\":\"a\",\"name\":\"N\tX\",\"releaseDate\":\"2024-01-01\",\"cards\":[{\"n\":1}]}}}", - // structurally invalid JSON (both parsers must reject) - "not json", - "{\"data\":{\"A\":{\"name\":\"unterminated}}", - }; - - for (const QByteArray &input : inputs) { - QJsonParseError qtError; - QJsonDocument::fromJson(input, &qtError); - const bool qtOk = qtError.error == QJsonParseError::NoError; - - RawJson::ScanError scanError; - const QList ranges = RawJson::scanSetRanges(input, &scanError); - EXPECT_EQ(qtOk, !scanError.isError()) << "verdict mismatch for: " << input.constData(); - if (scanError.isError()) { - continue; - } - for (const RawJson::SetRange &range : ranges) { - QJsonParseError sliceError; - QJsonDocument::fromJson(QByteArray(input.constData() + range.dataRange.start, range.dataRange.length), - &sliceError); - EXPECT_EQ(sliceError.error, QJsonParseError::NoError) << "bad range slice for: " << input.constData(); - } - } -} - -TEST_F(OracleImporterTest, ScanSetRangesRejectsDeepNesting) -{ - // Far beyond the shared 1024 container cap: Qt reports DeepNesting and the - // scanner must reject too, without overflowing the stack through its - // recursive skipValue walk. - QString nesting; - nesting.reserve(10000); - for (int i = 0; i < 5000; ++i) { - nesting += '['; - } - for (int i = 0; i < 5000; ++i) { - nesting += ']'; - } - const QByteArray json = ("{\"data\":{\"A\":{\"code\":\"a\",\"cards\":" + nesting + "}}}").toUtf8(); - - QJsonParseError qtError; - QJsonDocument::fromJson(json, &qtError); - ASSERT_NE(qtError.error, QJsonParseError::NoError) << "expected Qt to reject deep nesting"; - - RawJson::ScanError scanError; - RawJson::scanSetRanges(json, &scanError); - ASSERT_TRUE(scanError.isError()) << "scanner accepted a document Qt rejects as too deeply nested"; -} - -TEST_F(OracleImporterTest, ScanSetRangesAcceptsQtMaxNesting) -{ - // Pins the boundary rather than only the far-past case: a depth Qt still - // accepts must be accepted by the scanner too. Before the fix the scanner's - // cap was roughly half of Qt's (each level cost two decrements), so a - // depth of 1000 here was rejected even though QJsonDocument parses it. - constexpr int depth = 1000; - QString nesting; - nesting.reserve(2 * depth); - for (int i = 0; i < depth; ++i) { - nesting += '['; - } - for (int i = 0; i < depth; ++i) { - nesting += ']'; - } - const QByteArray json = ("{\"data\":{\"A\":{\"code\":\"a\",\"cards\":" + nesting + "}}}").toUtf8(); - - QJsonParseError qtError; - QJsonDocument::fromJson(json, &qtError); - ASSERT_EQ(qtError.error, QJsonParseError::NoError) << "expected Qt to accept depth " << depth; - - RawJson::ScanError scanError; - RawJson::scanSetRanges(json, &scanError); - ASSERT_FALSE(scanError.isError()) << "scanner rejected a document Qt accepts at depth " << depth; -} - -// ============================================================================ -// Lazy per-set parsing tests -// ============================================================================ - -TEST_F(OracleImporterTest, StartImportParsesSetsLazily) -{ - QJsonObject setObj = makeCard("Lazy Import Card"); - QJsonArray cards; - cards.append(setObj); - QJsonObject dataSet; - dataSet["code"] = "tst"; - dataSet["name"] = "Test Set"; - dataSet["type"] = "expansion"; - dataSet["releaseDate"] = "2024-01-01"; - dataSet["cards"] = cards; - - QJsonObject root; - root["data"] = QJsonObject{{"TST", dataSet}}; - - const QByteArray data = QJsonDocument(root).toJson(QJsonDocument::Compact); - ASSERT_TRUE(importer->readSetsFromByteArray(data)); - ASSERT_FALSE(importer->getRawSetsData().isEmpty()); - - const int importedSets = importer->startImport(); - ASSERT_EQ(importedSets, 1); - ASSERT_EQ(importer->getCardList().size(), 1); - ASSERT_FALSE(importer->getCardList().value("Lazy Import Card").isNull()); -} - -// ============================================================================ -// Scan progress reporting tests -// ============================================================================ - -TEST(OracleScanProgress, ScanProgressReportsMonotonicBytesToTotal) -{ - QJsonObject setObj; - setObj["code"] = "tst"; - setObj["name"] = "Test Set"; - setObj["type"] = "expansion"; - setObj["releaseDate"] = "2024-01-01"; - QJsonArray cards; - for (int i = 0; i < 40; ++i) { - QJsonObject card; - card["name"] = QString("Card %1").arg(i); - card["text"] = "Some rules text used to bulk up the card payload."; - card["layout"] = "normal"; - cards.append(card); - } - setObj["cards"] = cards; - - QJsonObject root; - root["data"] = QJsonObject{{"TST", setObj}}; - - const QByteArray data = QJsonDocument(root).toJson(QJsonDocument::Compact); - - QList> reports; - RawJson::ScanError error; - const QList ranges = - RawJson::scanSetRanges(data, &error, [&reports](qsizetype bytesRead, qsizetype totalBytes) { - reports.append({bytesRead, totalBytes}); - }); - - ASSERT_FALSE(error.isError()) << error.message.toStdString(); - ASSERT_EQ(ranges.size(), 1); - ASSERT_FALSE(reports.isEmpty()); - ASSERT_GT(reports.size(), 1); - - qsizetype last = 0; - for (const auto &[bytesRead, totalBytes] : reports) { - ASSERT_EQ(totalBytes, data.size()); - ASSERT_GE(bytesRead, last) << "scan progress must be monotonic"; - ASSERT_LE(bytesRead, totalBytes) << "scan progress must not overshoot the document size"; - last = bytesRead; - } - ASSERT_EQ(reports.constLast().first, data.size()) << "scan must end at 100%"; - ASSERT_LE(reports.size(), 160) << "scan reports must be throttled"; -} - -TEST(OracleScanProgress, ScanWithoutCallbackStillParses) -{ - QJsonObject setObj; - setObj["code"] = "tst"; - setObj["name"] = "Test Set"; - setObj["type"] = "expansion"; - setObj["releaseDate"] = "2024-01-01"; - setObj["cards"] = QJsonArray(); - - QJsonObject root; - root["data"] = QJsonObject{{"TST", setObj}}; - - const QByteArray data = QJsonDocument(root).toJson(QJsonDocument::Compact); - - RawJson::ScanError error; - const QList ranges = RawJson::scanSetRanges(data, &error); - - ASSERT_FALSE(error.isError()) << error.message.toStdString(); - ASSERT_EQ(ranges.size(), 1); - ASSERT_EQ(ranges.first().code, "tst"); -} - -TEST_F(OracleImporterTest, ReadSetsFromByteArrayEmitsScanProgress) -{ - QJsonObject setObj; - setObj["code"] = "tst"; - setObj["name"] = "Test Set"; - setObj["type"] = "expansion"; - setObj["releaseDate"] = "2024-01-01"; - QJsonArray cards; - for (int i = 0; i < 40; ++i) { - QJsonObject card; - card["name"] = QString("Card %1").arg(i); - cards.append(card); - } - setObj["cards"] = cards; - - QJsonObject root; - root["data"] = QJsonObject{{"TST", setObj}}; - - const QByteArray data = QJsonDocument(root).toJson(QJsonDocument::Compact); - - QList> emissions; - QObject::connect(importer, &OracleImporter::dataReadProgress, - [&emissions](int bytesRead, int totalBytes) { emissions.append({bytesRead, totalBytes}); }); - - ASSERT_TRUE(importer->readSetsFromByteArray(data)); - ASSERT_FALSE(emissions.isEmpty()); - for (const auto &[bytesRead, totalBytes] : emissions) { - ASSERT_EQ(totalBytes, data.size()); - ASSERT_GE(bytesRead, 0); - ASSERT_LE(bytesRead, totalBytes); - } - ASSERT_EQ(emissions.constLast().first, data.size()); -} - -TEST_F(OracleImporterTest, DisablingProgressReportingSuppressesScanEmissions) -{ - QJsonObject setObj; - setObj["code"] = "tst"; - setObj["name"] = "Test Set"; - setObj["type"] = "expansion"; - setObj["releaseDate"] = "2024-01-01"; - setObj["cards"] = QJsonArray(); - QJsonObject root; - root["data"] = QJsonObject{{"TST", setObj}}; - const QByteArray data = QJsonDocument(root).toJson(QJsonDocument::Compact); - - int emissions = 0; - QObject::connect(importer, &OracleImporter::dataReadProgress, [&emissions](int, int) { ++emissions; }); - - importer->setProgressReporting(false); - ASSERT_TRUE(importer->readSetsFromByteArray(data)); - ASSERT_EQ(emissions, 0); - - importer->setProgressReporting(true); - ASSERT_TRUE(importer->readSetsFromByteArray(data)); - ASSERT_GT(emissions, 0); -} - -// Localized card text tests -// ============================================================================ - -TEST_F(OracleImporterTest, ImportsLocalizedTextForRequestedLanguage) -{ - QJsonObject card = makeCard("Lightning Bolt"); - card["foreignData"] = - QJsonArray{makeForeignEntry("German", "Blitzschlag", "Blitzschlag fügt 3 Schadenspunkte zu.")}; - QJsonArray cards{card}; - - importer->setCardLang("de"); - importer->importCardsFromSet(set, cards); - importer->applyLocalizedData(); - - auto result = importer->getCardList().value("Lightning Bolt"); - ASSERT_FALSE(result.isNull()); - ASSERT_EQ(result->getLocalizedName("de"), "Blitzschlag"); - ASSERT_EQ(result->getLocalizedText("de"), "Blitzschlag fügt 3 Schadenspunkte zu."); - // English identity untouched - ASSERT_EQ(result->getName(), "Lightning Bolt"); - ASSERT_EQ(result->getText(), "Rules text."); -} - -TEST_F(OracleImporterTest, ImportsLocalizedNameAndTextForMultiFaceCards) -{ - // MTGJSON reports multi-face cards (adventure/split/aftermath/prepare) as one - // card object per face; every face carries the joined name but only its own - // face's rules text in foreignData. The importer joins the per-face texts with - // the same separator as the English merge. - QJsonObject front = makeCard("Disruptive Stormbrood // Petty Revenge"); - front["layout"] = "adventure"; - front["faceName"] = "Disruptive Stormbrood"; - front["side"] = "a"; - front["foreignData"] = QJsonArray{ - makeForeignEntry("German", "Disruptive Stormbrood // Kleinliche Rache", - "Fliegend\nWenn diese Kreatur ins Spiel kommt, zerstöre bis zu ein Artefakt oder eine " - "Verzauberung deiner Wahl.")}; - QJsonObject back = makeCard("Disruptive Stormbrood // Petty Revenge"); - back["layout"] = "adventure"; - back["faceName"] = "Petty Revenge"; - back["side"] = "b"; - back["text"] = "Destroy target creature."; - back["foreignData"] = QJsonArray{makeForeignEntry("German", "Disruptive Stormbrood // Kleinliche Rache", - "Zerstöre eine Kreatur deiner Wahl mit Stärke 3 oder weniger.")}; - QJsonArray cards{front, back}; - - importer->setCardLang("de"); - importer->importCardsFromSet(set, cards); - importer->applyLocalizedData(); - - auto result = importer->getCardList().value("Disruptive Stormbrood // Petty Revenge"); - ASSERT_FALSE(result.isNull()); - ASSERT_EQ(result->getLocalizedName("de"), "Disruptive Stormbrood // Kleinliche Rache"); - ASSERT_EQ(result->getLocalizedText("de"), - "Fliegend\nWenn diese Kreatur ins Spiel kommt, zerstöre bis zu ein Artefakt oder eine Verzauberung " - "deiner Wahl.\n\n---\n\nZerstöre eine Kreatur deiner Wahl mit Stärke 3 oder weniger."); - // English identity untouched - ASSERT_EQ(result->getName(), "Disruptive Stormbrood // Petty Revenge"); - ASSERT_EQ(result->getText(), "Rules text.\n\n---\n\nDestroy target creature."); -} - -TEST_F(OracleImporterTest, MultiFaceCardsWithoutCompleteForeignTextKeepEnglishText) -{ - // Both faces must carry a foreignData text for the joined text; otherwise the - // rules text stays English while the localized name (from a later complete - // printing) is still applied. - QJsonObject front = makeCard("Wear // Tear"); - front["layout"] = "split"; - front["faceName"] = "Wear"; - front["side"] = "a"; - front["foreignData"] = QJsonArray{makeForeignEntry("German", "Verschleiß // Zerrreißung", "Verschleiß-Text.")}; - QJsonObject back = makeCard("Wear // Tear"); - back["layout"] = "split"; - back["faceName"] = "Tear"; - back["side"] = "b"; - back["text"] = "Tear rules text."; - back["foreignData"] = QJsonArray{makeForeignEntry("German", "Verschleiß // Zerrreißung", "")}; - QJsonArray cards{front, back}; - - importer->setCardLang("de"); - importer->importCardsFromSet(set, cards); - importer->applyLocalizedData(); - - auto result = importer->getCardList().value("Wear // Tear"); - ASSERT_FALSE(result.isNull()); - // The joined name is still applied. - ASSERT_EQ(result->getLocalizedName("de"), "Verschleiß // Zerrreißung"); - // The incomplete text must not become the card's localized text. - ASSERT_TRUE(result->getLocalizedTexts().isEmpty()); - ASSERT_EQ(result->getText(), "Rules text.\n\n---\n\nTear rules text."); -} - -TEST_F(OracleImporterTest, DefaultLanguageSkipsForeignData) -{ - QJsonObject card = makeCard("Lightning Bolt"); - card["foreignData"] = - QJsonArray{makeForeignEntry("German", "Blitzschlag", "Blitzschlag fügt 3 Schadenspunkte zu.")}; - QJsonArray cards{card}; - - // cardLang defaults to "en" — foreignData must never be imported - importer->importCardsFromSet(set, cards); - importer->applyLocalizedData(); - - auto result = importer->getCardList().value("Lightning Bolt"); - ASSERT_FALSE(result.isNull()); - ASSERT_TRUE(result->getLocalizedNames().isEmpty()); - ASSERT_TRUE(result->getLocalizedTexts().isEmpty()); -} - -TEST_F(OracleImporterTest, UnsupportedLanguageSkipsForeignData) -{ - QJsonObject card = makeCard("Lightning Bolt"); - card["foreignData"] = QJsonArray{makeForeignEntry("xx", "Kochanie", "Grzmot uderza.")}; - QJsonArray cards{card}; - - importer->setCardLang("xx"); - importer->importCardsFromSet(set, cards); - importer->applyLocalizedData(); - - auto result = importer->getCardList().value("Lightning Bolt"); - ASSERT_FALSE(result.isNull()); - ASSERT_TRUE(result->getLocalizedNames().isEmpty()); -} - -TEST_F(OracleImporterTest, NonMatchingLanguageNotCollected) -{ - QJsonObject card = makeCard("Lightning Bolt"); - card["foreignData"] = QJsonArray{makeForeignEntry("French", "Éclair", "L'Éclair inflige 3 blessures.")}; - QJsonArray cards{card}; - - importer->setCardLang("de"); - importer->importCardsFromSet(set, cards); - importer->applyLocalizedData(); - - auto result = importer->getCardList().value("Lightning Bolt"); - ASSERT_FALSE(result.isNull()); - ASSERT_TRUE(result->getLocalizedNames().isEmpty()); -} - -TEST_F(OracleImporterTest, HigherPrioritySetWinsForReprint) -{ - // First printing in a reprint set, then another in a (more authoritative) - // core set: the core set's German text must win even though it was seen later. - QJsonObject reprintCard = makeCard("Lightning Bolt"); - reprintCard["foreignData"] = QJsonArray{makeForeignEntry("German", "Blitzschlag", "Älterer deutscher Text.")}; - CardSetPtr reprintSet = - CardSet::newInstance(controller, "TS2", "Second Set", QString(), QDate(), CardSet::PriorityReprint); - importer->setCardLang("de"); - importer->importCardsFromSet(reprintSet, QJsonArray{reprintCard}); - - QJsonObject primaryCard = makeCard("Lightning Bolt"); - primaryCard["foreignData"] = - QJsonArray{makeForeignEntry("German", "Blitzschlag", "Blitzschlag fügt 3 Schadenspunkte zu.")}; - CardSetPtr primarySet = - CardSet::newInstance(controller, "TS3", "Third Set", QString(), QDate(), CardSet::PriorityPrimary); - importer->importCardsFromSet(primarySet, QJsonArray{primaryCard}); - importer->applyLocalizedData(); - - auto result = importer->getCardList().value("Lightning Bolt"); - ASSERT_FALSE(result.isNull()); - ASSERT_EQ(result->getLocalizedName("de"), "Blitzschlag"); - ASSERT_EQ(result->getLocalizedText("de"), "Blitzschlag fügt 3 Schadenspunkte zu."); - ASSERT_EQ(importer->getCardList().size(), 1); -} - -TEST_F(OracleImporterTest, HigherPrioritySplitSetWinsForReprint) -{ - // Split cards print each face as its own card object; the joined text is - // collected per set with the same priority policy as single-face cards, so a - // reprint set's German text must yield to the core set's even when reprints - // are imported first. - QJsonObject reprintFront = makeCard("Wear // Tear"); - reprintFront["layout"] = "split"; - reprintFront["faceName"] = "Wear"; - reprintFront["side"] = "a"; - reprintFront["foreignData"] = - QJsonArray{makeForeignEntry("German", "Verschleiß // Zerrreißung", "Wear alter Text.")}; - QJsonObject reprintBack = makeCard("Wear // Tear"); - reprintBack["layout"] = "split"; - reprintBack["faceName"] = "Tear"; - reprintBack["side"] = "b"; - reprintBack["foreignData"] = - QJsonArray{makeForeignEntry("German", "Verschleiß // Zerrreißung", "Tear alter Text.")}; - CardSetPtr reprintSet = - CardSet::newInstance(controller, "TS2", "Second Set", QString(), QDate(), CardSet::PriorityReprint); - importer->setCardLang("de"); - importer->importCardsFromSet(reprintSet, QJsonArray{reprintFront, reprintBack}); - - QJsonObject primaryFront = makeCard("Wear // Tear"); - primaryFront["layout"] = "split"; - primaryFront["faceName"] = "Wear"; - primaryFront["side"] = "a"; - primaryFront["foreignData"] = - QJsonArray{makeForeignEntry("German", "Verschleiß // Zerrreißung", "Wear neuer Text.")}; - QJsonObject primaryBack = makeCard("Wear // Tear"); - primaryBack["layout"] = "split"; - primaryBack["faceName"] = "Tear"; - primaryBack["side"] = "b"; - primaryBack["foreignData"] = - QJsonArray{makeForeignEntry("German", "Verschleiß // Zerrreißung", "Tear neuer Text.")}; - CardSetPtr primarySet = - CardSet::newInstance(controller, "TS3", "Third Set", QString(), QDate(), CardSet::PriorityPrimary); - importer->importCardsFromSet(primarySet, QJsonArray{primaryFront, primaryBack}); - importer->applyLocalizedData(); - - auto result = importer->getCardList().value("Wear // Tear"); - ASSERT_FALSE(result.isNull()); - ASSERT_EQ(result->getLocalizedName("de"), "Verschleiß // Zerrreißung"); - ASSERT_EQ(result->getLocalizedText("de"), "Wear neuer Text.\n\n---\n\nTear neuer Text."); - ASSERT_EQ(importer->getCardList().size(), 1); -} - -TEST_F(OracleImporterTest, StartImportAppliesLocalizedData) -{ - QJsonObject card = makeCard("Lightning Bolt"); - card["foreignData"] = - QJsonArray{makeForeignEntry("Portuguese (Brazil)", "Raio", "Raio causa 3 de dano a qualquer alvo.")}; - QJsonObject dataSet; - dataSet["code"] = "tst"; - dataSet["name"] = "Test Set"; - dataSet["type"] = "expansion"; - dataSet["releaseDate"] = "2024-01-01"; - dataSet["cards"] = QJsonArray{card}; - - QJsonObject root; - root["data"] = QJsonObject{{"TST", dataSet}}; - - importer->setCardLang("pt"); - ASSERT_TRUE(importer->readSetsFromByteArray(QJsonDocument(root).toJson(QJsonDocument::Compact))); - ASSERT_EQ(importer->startImport(), 1); - - auto result = importer->getCardList().value("Lightning Bolt"); - ASSERT_FALSE(result.isNull()); - ASSERT_EQ(result->getLocalizedName("pt"), "Raio"); - ASSERT_EQ(result->getLocalizedText("pt"), "Raio causa 3 de dano a qualquer alvo."); -} - -int main(int argc, char **argv) -{ - ::testing::InitGoogleTest(&argc, argv); - return RUN_ALL_TESTS(); -} diff --git a/tests/password_hash_test.cpp b/tests/password_hash_test.cpp index 2b8f8bdb7..38d9b6315 100644 --- a/tests/password_hash_test.cpp +++ b/tests/password_hash_test.cpp @@ -1,9 +1,25 @@ #include "gtest/gtest.h" -#include +#include +#include #include +RNG_Abstract *rng; + namespace { +class PasswordHashTest : public ::testing::Test +{ +protected: + void SetUp() override + { + rng = new RNG_SFMT; + } + + void TearDown() override + { + delete rng; + } +}; TEST(PasswordHashTest, RegressionTest) { @@ -13,29 +29,6 @@ TEST(PasswordHashTest, RegressionTest) QString hash = PasswordHasher::computeHash(password, salt); ASSERT_EQ(hash, salt + expected) << "The computed hash value remains the same"; } - -TEST(PasswordHashTest, SaltUsesAlphanumericCharset) -{ - static const char alphanum[] = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; - const QString salt = PasswordHasher::generateRandomSalt(); - ASSERT_EQ(salt.size(), 16); - for (const QChar &c : salt) { - ASSERT_NE(strchr(alphanum, c.toLatin1()), nullptr); - } -} - -TEST(PasswordHashTest, SaltsAreUnique) -{ - const QString salt1 = PasswordHasher::generateRandomSalt(); - const QString salt2 = PasswordHasher::generateRandomSalt(); - ASSERT_NE(salt1, salt2); -} - -TEST(PasswordHashTest, TokenHasExpectedLength) -{ - const QString token = PasswordHasher::generateActivationToken(); - ASSERT_EQ(token.size(), 16); -} } // namespace int main(int argc, char **argv) diff --git a/tests/playmat_resolver_test.cpp b/tests/playmat_resolver_test.cpp deleted file mode 100644 index a931c0647..000000000 --- a/tests/playmat_resolver_test.cpp +++ /dev/null @@ -1,126 +0,0 @@ -#include -#include -#include - -namespace -{ - -PlaymatInfo makePlaymatInfo(const QString &name, const QString &providerId = QString()) -{ - PlaymatInfo info; - info.card = {name, providerId}; - return info; -} - -} // namespace - -TEST(PlaymatResolverTest, EmptyChainReturnsEmpty) -{ - DeckList deck; - const PlaymatInfo resolved = resolveEffectivePlaymat(deck, {}, {}, PlaymatFallbackModeFixed, 0); - EXPECT_TRUE(resolved.card.isEmpty()); -} - -TEST(PlaymatResolverTest, OverrideWinsOverDeckAndFallback) -{ - DeckList deck; - deck.setPlaymat({{QStringLiteral("Deck Mat"), QStringLiteral("deck-provider")}, {}}); - - const PlaymatInfo force = makePlaymatInfo(QStringLiteral("Force Mat"), QStringLiteral("force-provider")); - const QList fallback = {makePlaymatInfo(QStringLiteral("Fallback Mat"))}; - - const PlaymatInfo resolved = resolveEffectivePlaymat(deck, force, fallback, PlaymatFallbackModeFixed, 0); - EXPECT_EQ(resolved.card.name, QStringLiteral("Force Mat")); - EXPECT_EQ(resolved.card.providerId, QStringLiteral("force-provider")); -} - -TEST(PlaymatResolverTest, DeckWinsOverFallback) -{ - DeckList deck; - deck.setPlaymat({{QStringLiteral("Deck Mat"), QStringLiteral("deck-provider")}, {0.1, 0.2, 0.3, 1.5}}); - - const QList fallback = {makePlaymatInfo(QStringLiteral("Fallback Mat"))}; - - const PlaymatInfo resolved = resolveEffectivePlaymat(deck, {}, fallback, PlaymatFallbackModeFixed, 0); - EXPECT_EQ(resolved.card.name, QStringLiteral("Deck Mat")); - EXPECT_EQ(resolved.card.providerId, QStringLiteral("deck-provider")); - EXPECT_DOUBLE_EQ(resolved.params.marginPctL, 0.1); - EXPECT_DOUBLE_EQ(resolved.params.verticalOffset, 0.3); -} - -TEST(PlaymatResolverTest, FallbackUsedWhenDeckHasNone) -{ - DeckList deck; - const QList fallback = {makePlaymatInfo(QStringLiteral("Fallback Mat"))}; - - const PlaymatInfo resolved = resolveEffectivePlaymat(deck, {}, fallback, PlaymatFallbackModeFixed, 0); - EXPECT_EQ(resolved.card.name, QStringLiteral("Fallback Mat")); -} - -TEST(PlaymatResolverTest, FixedAlwaysUsesFirst) -{ - DeckList deck; - const QList fallback = {makePlaymatInfo(QStringLiteral("First")), - makePlaymatInfo(QStringLiteral("Second"))}; - - for (int i = 0; i < 5; ++i) { - const PlaymatInfo resolved = resolveEffectivePlaymat(deck, {}, fallback, PlaymatFallbackModeFixed, i); - EXPECT_EQ(resolved.card.name, QStringLiteral("First")); - } -} - -TEST(PlaymatResolverTest, RoundRobinCyclesAndWraps) -{ - DeckList deck; - const QList fallback = {makePlaymatInfo(QStringLiteral("First")), - makePlaymatInfo(QStringLiteral("Second")), - makePlaymatInfo(QStringLiteral("Third"))}; - - const QStringList expected = {QStringLiteral("First"), QStringLiteral("Second"), QStringLiteral("Third"), - QStringLiteral("First"), QStringLiteral("Second"), QStringLiteral("Third")}; - for (int i = 0; i < expected.size(); ++i) { - const PlaymatInfo resolved = resolveEffectivePlaymat(deck, {}, fallback, PlaymatFallbackModeRoundRobin, i); - EXPECT_EQ(resolved.card.name, expected.at(i)); - } -} - -TEST(PlaymatResolverTest, RoundRobinRespectsCursor) -{ - DeckList deck; - const QList fallback = {makePlaymatInfo(QStringLiteral("First")), - makePlaymatInfo(QStringLiteral("Second"))}; - - const PlaymatInfo resolved = resolveEffectivePlaymat(deck, {}, fallback, PlaymatFallbackModeRoundRobin, 5); - EXPECT_EQ(resolved.card.name, QStringLiteral("Second")); // 5 % 2 == 1 -} - -TEST(PlaymatResolverTest, RandomStaysWithinList) -{ - DeckList deck; - const QList fallback = {makePlaymatInfo(QStringLiteral("First")), - makePlaymatInfo(QStringLiteral("Second")), - makePlaymatInfo(QStringLiteral("Third"))}; - - for (int i = 0; i < 50; ++i) { - const PlaymatInfo resolved = resolveEffectivePlaymat(deck, {}, fallback, PlaymatFallbackModeRandom, i); - ASSERT_FALSE(resolved.card.name.isEmpty()); - EXPECT_TRUE(fallback.contains(resolved)); - } -} - -TEST(PlaymatResolverTest, ForceWithEmptyCardIgnoresFallbackParamsButNotFallback) -{ - DeckList deck; - deck.setPlaymat({{QStringLiteral("Deck Mat")}, {}}); - - // An empty force entry must not mask the deck-configured playmat. - const PlaymatInfo emptyForce; - const PlaymatInfo resolved = resolveEffectivePlaymat(deck, emptyForce, {}, PlaymatFallbackModeFixed, 0); - EXPECT_EQ(resolved.card.name, QStringLiteral("Deck Mat")); -} - -int main(int argc, char **argv) -{ - ::testing::InitGoogleTest(&argc, argv); - return RUN_ALL_TESTS(); -} diff --git a/tests/server_developer_role_test.cpp b/tests/server_developer_role_test.cpp deleted file mode 100644 index 127f606a8..000000000 --- a/tests/server_developer_role_test.cpp +++ /dev/null @@ -1,137 +0,0 @@ -/** @file server_developer_role_test.cpp - * @brief Tests for the developer staff role authorization and dispatch. - * @ingroup Tests - */ - -#include -#include -#include -#include -#include -#include -#include -#include - -// The server_remote library references the global RNG, which is normally -// defined by the servatrice/client executable main(). Provide a stub so the -// unit test can link against it. -RNG_Abstract *rng = nullptr; - -namespace -{ - -class TestDeveloperHandler : public Server_ProtocolHandler -{ -public: - explicit TestDeveloperHandler(Server *_server) : Server_ProtocolHandler(_server, nullptr) - { - } - - QString getAddress() const override - { - return {}; - } - QString getConnectionType() const override - { - return {}; - } - - // Buffer the last response code sent to the client so tests can assert on - // the outcome of processCommandContainer(). - Response::ResponseCode lastResponseCode = Response::RespNothing; - int dispatchCount = 0; - -protected: - void transmitProtocolItem(const ServerMessage &item) override - { - if (item.message_type() == ServerMessage::RESPONSE) { - lastResponseCode = item.response().response_code(); - } - } - - Response::ResponseCode - processExtendedDeveloperCommand(int cmdType, const DeveloperCommand &, ResponseContainer &) override - { - ++dispatchCount; - // Fail closed for anything not explicitly handled. - if (cmdType != DeveloperCommand::GET_SERVER_STATS) { - return Response::RespFunctionNotAllowed; - } - return Response::RespOk; - } -}; - -class DeveloperRoleTest : public ::testing::Test -{ -protected: - Server server; - TestDeveloperHandler handler{&server}; - - void setUserLevel(uint32_t level) - { - ServerInfo_User user; - user.set_user_level(level); - handler.setUserInfo(user); - } -}; - -TEST_F(DeveloperRoleTest, RejectsWhenNotLoggedIn) -{ - CommandContainer cont; - cont.add_developer_command(); - handler.processCommandContainer(cont); - EXPECT_EQ(handler.lastResponseCode, Response::RespLoginNeeded); - EXPECT_EQ(handler.dispatchCount, 0); -} - -TEST_F(DeveloperRoleTest, RejectsPlainUser) -{ - setUserLevel(ServerInfo_User::IsUser | ServerInfo_User::IsRegistered); - - CommandContainer cont; - cont.add_developer_command(); - handler.processCommandContainer(cont); - EXPECT_EQ(handler.lastResponseCode, Response::RespLoginNeeded); - EXPECT_EQ(handler.dispatchCount, 0); -} - -TEST_F(DeveloperRoleTest, RejectsModeratorThatIsNotDeveloper) -{ - setUserLevel(ServerInfo_User::IsModerator); - - CommandContainer cont; - cont.add_developer_command(); - handler.processCommandContainer(cont); - EXPECT_EQ(handler.lastResponseCode, Response::RespLoginNeeded); -} - -TEST_F(DeveloperRoleTest, DispatchesToDeveloperCommandForDeveloper) -{ - setUserLevel(ServerInfo_User::IsDeveloper); - - CommandContainer cont; - DeveloperCommand *cmd = cont.add_developer_command(); - cmd->MutableExtension(Command_GetServerStats::ext); - handler.processCommandContainer(cont); - EXPECT_EQ(handler.lastResponseCode, Response::RespOk); - EXPECT_EQ(handler.dispatchCount, 1); -} - -TEST_F(DeveloperRoleTest, FailClosedForUnknownDeveloperCommand) -{ - setUserLevel(ServerInfo_User::IsDeveloper); - - CommandContainer cont; - cont.add_developer_command(); // no extension set -> getPbExtension() returns -1 - handler.processCommandContainer(cont); - EXPECT_EQ(handler.lastResponseCode, Response::RespFunctionNotAllowed); - EXPECT_EQ(handler.dispatchCount, 1); -} - -} // namespace - -int main(int argc, char **argv) -{ - ::testing::InitGoogleTest(&argc, argv); - return RUN_ALL_TESTS(); -} diff --git a/tests/server_game_join_test.cpp b/tests/server_game_join_test.cpp deleted file mode 100644 index c84e4e066..000000000 --- a/tests/server_game_join_test.cpp +++ /dev/null @@ -1,174 +0,0 @@ -/** @file server_game_join_test.cpp - * @brief Tests for the moderator/judge game-entry restriction override in Server_Game::checkJoin. - * @ingroup Tests - */ - -#include "game/server_game.h" -#include "server.h" -#include "server_database_interface.h" -#include "server_room.h" - -#include -#include -#include - -RNG_Abstract *rng = nullptr; // referenced by the server_remote library - -namespace -{ - -class MockDatabaseInterface : public Server_DatabaseInterface -{ -public: - AuthenticationResult checkUserPassword(Server_ProtocolHandler *, - const QString &, - const QString &, - const QString &, - QString &, - int &, - bool) override - { - return NotLoggedIn; - } - int getNextReplayId() override - { - return 1; - } - int getNextGameId() override - { - return 1; - } - int getActiveUserCount(QString) override - { - return 0; - } - ServerInfo_User getUserData(const QString &, bool) override - { - return ServerInfo_User(); - } -}; - -class FakeServer : public Server -{ -public: - FakeServer() - { - setDatabaseInterface(new MockDatabaseInterface()); - } -}; - -class GameJoinOverrideTest : public ::testing::Test -{ -protected: - FakeServer server; - Server_Room room{0, 0, "", "", "", "", false, "", {}, &server}; - ServerInfo_User creator; - ServerInfo_User plainUser; - ServerInfo_User unregisteredJudge; - ServerInfo_User moderator; - ServerInfo_User judge; - Server_Game *game = nullptr; - - void SetUp() override - { - creator.set_name("creator"); - creator.set_user_level(ServerInfo_User::IsUser | ServerInfo_User::IsRegistered); - plainUser.set_name("plain-user"); - plainUser.set_user_level(ServerInfo_User::IsUser | ServerInfo_User::IsRegistered); - unregisteredJudge.set_name("unregistered-judge"); - unregisteredJudge.set_user_level(ServerInfo_User::IsUser | ServerInfo_User::IsJudge); - moderator.set_name("moderator"); - moderator.set_user_level(ServerInfo_User::IsUser | ServerInfo_User::IsRegistered | - ServerInfo_User::IsModerator); - judge.set_name("judge"); - judge.set_user_level(ServerInfo_User::IsUser | ServerInfo_User::IsRegistered | ServerInfo_User::IsJudge); - } - - void TearDown() override - { - delete game; - } - - Server_Game *makeGame(bool passwordProtected, bool onlyRegistered, bool onlyBuddies, bool spectatorsAllowed) - { - GameConfig config{.creatorInfo = creator, - .gameId = 1, - .description = QString(), - .password = passwordProtected ? "secret" : QString(), - .maxPlayers = 2, - .gameTypes = QList(), - .onlyBuddies = onlyBuddies, - .onlyRegistered = onlyRegistered, - .spectatorsAllowed = spectatorsAllowed, - .spectatorsNeedPassword = true, - .spectatorsCanTalk = false, - .spectatorsSeeEverything = false, - .startingLifeTotal = 20, - .shareDecklistsOnLoad = false}; - return new Server_Game(config, &room); - } -}; - -TEST_F(GameJoinOverrideTest, StaffBypassPasswordRestriction) -{ - game = makeGame(true, false, false, true); - - // A plain user cannot override the password even with the override flag set. - EXPECT_EQ(game->checkJoin(&plainUser, "wrong", false, true, false), Response::RespWrongPassword); - // Moderators and judges may enter any game regardless of the password. - EXPECT_EQ(game->checkJoin(&moderator, "wrong", false, true, false), Response::RespOk); - EXPECT_EQ(game->checkJoin(&judge, "wrong", false, true, false), Response::RespOk); - // Without the override flag judges are still subject to the password. - EXPECT_EQ(game->checkJoin(&judge, "wrong", false, false, true), Response::RespWrongPassword); - EXPECT_EQ(game->checkJoin(&judge, "secret", false, false, true), Response::RespOk); -} - -TEST_F(GameJoinOverrideTest, StaffBypassRegisteredOnlyRestriction) -{ - game = makeGame(false, true, false, true); - - // Without the override flag the only-registered restriction still applies. - EXPECT_EQ(game->checkJoin(&unregisteredJudge, QString(), false, false, false), Response::RespUserLevelTooLow); - // An unregistered judge may enter when overriding restrictions. - EXPECT_EQ(game->checkJoin(&unregisteredJudge, QString(), false, true, false), Response::RespOk); -} - -TEST_F(GameJoinOverrideTest, StaffBypassBuddiesOnlyRestriction) -{ - game = makeGame(false, false, true, true); - - // A plain user who is not on the creator's buddy list gets rejected. - EXPECT_EQ(game->checkJoin(&plainUser, QString(), false, true, false), Response::RespOnlyBuddies); - // Moderators and judges bypass the buddies-only restriction. - EXPECT_EQ(game->checkJoin(&moderator, QString(), false, true, false), Response::RespOk); - EXPECT_EQ(game->checkJoin(&judge, QString(), false, true, false), Response::RespOk); -} - -TEST_F(GameJoinOverrideTest, StaffBypassSpectatorsNotAllowedRestriction) -{ - game = makeGame(false, false, false, false); - - // A plain user cannot spectate when the game disallows spectators. - EXPECT_EQ(game->checkJoin(&plainUser, QString(), true, false, false), Response::RespSpectatorsNotAllowed); - // Moderators and judges may spectate any game regardless of the password - // and the spectator restriction. - EXPECT_EQ(game->checkJoin(&moderator, "wrong", true, true, false), Response::RespOk); - EXPECT_EQ(game->checkJoin(&judge, "wrong", true, true, false), Response::RespOk); -} - -TEST_F(GameJoinOverrideTest, JudgeOverrideDoesNotGrantJudgeJoinToPlainUser) -{ - game = makeGame(false, false, false, true); - - // joining with join_as_judge still requires the judge flag even when overriding. - EXPECT_EQ(game->checkJoin(&plainUser, QString(), false, true, true), Response::RespUserLevelTooLow); - EXPECT_EQ(game->checkJoin(&judge, QString(), false, true, true), Response::RespOk); -} - -} // namespace - -int main(int argc, char **argv) -{ - ::testing::InitGoogleTest(&argc, argv); - return RUN_ALL_TESTS(); -} \ No newline at end of file diff --git a/tests/settings/settings_defaults_test.cpp b/tests/settings/settings_defaults_test.cpp index 6e674cb86..dfdad4780 100644 --- a/tests/settings/settings_defaults_test.cpp +++ b/tests/settings/settings_defaults_test.cpp @@ -188,38 +188,6 @@ TEST_F(SettingsDefaultsTest, Sound_MasterVolume_SetAndGet) // --- TabsSettings --- -TEST_F(SettingsDefaultsTest, Tabs_StartupTab_Default) -{ - TabsSettings s(settingsPath, nullptr); - ASSERT_EQ(s.getStartupTabIndex(), static_cast(StartupTab::StartupTabHome)); -} - -TEST_F(SettingsDefaultsTest, Tabs_StartupTab_SetAndGet) -{ - TabsSettings s(settingsPath, nullptr); - s.setStartupTabIndex(StartupTab::StartupTabServerRoom); - ASSERT_EQ(s.getStartupTabIndex(), static_cast(StartupTab::StartupTabServerRoom)); -} - -TEST_F(SettingsDefaultsTest, Tabs_StartupServer_Default) -{ - TabsSettings s(settingsPath, nullptr); - ASSERT_EQ(s.getStartupServerHost(), QString()); - ASSERT_EQ(s.getStartupServerPort(), QString()); - ASSERT_EQ(s.getStartupRoomName(), QString()); -} - -TEST_F(SettingsDefaultsTest, Tabs_StartupServer_SetAndGet) -{ - TabsSettings s(settingsPath, nullptr); - s.setStartupServerHost("server.cockatrice.us"); - s.setStartupServerPort("4748"); - s.setStartupRoomName("General"); - ASSERT_EQ(s.getStartupServerHost(), QString("server.cockatrice.us")); - ASSERT_EQ(s.getStartupServerPort(), QString("4748")); - ASSERT_EQ(s.getStartupRoomName(), QString("General")); -} - TEST_F(SettingsDefaultsTest, Tabs_AllTabsOpen_Default) { TabsSettings s(settingsPath, nullptr); @@ -232,18 +200,6 @@ TEST_F(SettingsDefaultsTest, Tabs_AllTabsOpen_Default) ASSERT_EQ(s.getTabLogOpen(), true); } -TEST_F(SettingsDefaultsTest, Tabs_ModerationOpen_Default) -{ - TabsSettings s(settingsPath, nullptr); - ASSERT_EQ(s.getTabModerationOpen(), false); -} - -TEST_F(SettingsDefaultsTest, Tabs_CardArtRulesOpen_Default) -{ - TabsSettings s(settingsPath, nullptr); - ASSERT_EQ(s.getTabCardArtRulesOpen(), false); -} - // --- ChatSettings --- TEST_F(SettingsDefaultsTest, Chat_Mention_Default) @@ -300,12 +256,6 @@ TEST_F(SettingsDefaultsTest, Chat_RoomHistory_Default) ASSERT_EQ(s.getRoomHistory(), true); } -TEST_F(SettingsDefaultsTest, Chat_IgnoreAllPrivateMessages_Default) -{ - ChatSettings s(settingsPath, nullptr); - ASSERT_EQ(s.getIgnoreAllPrivateMessages(), false); -} - // --- PersonalSettings --- TEST_F(SettingsDefaultsTest, Personal_Lang_Default) @@ -386,19 +336,6 @@ TEST_F(SettingsDefaultsTest, Appearance_HomeTabDisplayCardName_Default) ASSERT_EQ(s.getHomeTabDisplayCardName(), true); } -TEST_F(SettingsDefaultsTest, Appearance_HomeTabBackgroundDim_Default) -{ - AppearanceSettings s(settingsPath, nullptr); - ASSERT_EQ(s.getHomeTabBackgroundDim(), true); -} - -TEST_F(SettingsDefaultsTest, Appearance_HomeTabBackgroundDim_SetAndGet) -{ - AppearanceSettings s(settingsPath, nullptr); - s.setHomeTabBackgroundDim(false); - ASSERT_EQ(s.getHomeTabBackgroundDim(), false); -} - // --- InterfaceSettings --- TEST_F(SettingsDefaultsTest, Interface_ShowStatusBar_Default) @@ -463,23 +400,6 @@ TEST_F(SettingsDefaultsTest, DeckEditor_DefaultDeckEditorType_Default) ASSERT_EQ(s.getDefaultDeckEditorType(), 1); } -TEST_F(SettingsDefaultsTest, DeckEditor_VdeStartupTab_Default) -{ - DeckEditorSettings s(settingsPath, nullptr); - ASSERT_EQ(s.getVdeStartupTab(), VdeStartupTabContext); -} - -TEST_F(SettingsDefaultsTest, DeckEditor_VdeStartupTab_SetAndGet) -{ - DeckEditorSettings s(settingsPath, nullptr); - s.setVdeStartupTab(VdeStartupTabDeckDisplay); - ASSERT_EQ(s.getVdeStartupTab(), VdeStartupTabDeckDisplay); - s.setVdeStartupTab(VdeStartupTabDatabaseDisplay); - ASSERT_EQ(s.getVdeStartupTab(), VdeStartupTabDatabaseDisplay); - s.setVdeStartupTab(VdeStartupTabContext); - ASSERT_EQ(s.getVdeStartupTab(), VdeStartupTabContext); -} - // --- NetworkSettings --- TEST_F(SettingsDefaultsTest, Network_ClientID_Default) @@ -556,38 +476,6 @@ TEST_F(SettingsDefaultsTest, CardsDisplay_SampleHandSize_Default) ASSERT_EQ(s.getSampleHandSize(), 7); } -TEST_F(SettingsDefaultsTest, CardsDisplay_ArrowDrawAnimation_Default) -{ - CardsDisplaySettings s(settingsPath, nullptr); - ASSERT_EQ(s.getArrowDrawAnimation(), true); -} - -TEST_F(SettingsDefaultsTest, CardsDisplay_CardLang_Default) -{ - CardsDisplaySettings s(settingsPath, nullptr); - ASSERT_EQ(s.getCardLang(), QString("en")); -} - -TEST_F(SettingsDefaultsTest, CardsDisplay_CardLang_SetAndGet) -{ - CardsDisplaySettings s(settingsPath, nullptr); - s.setCardLang("de"); - ASSERT_EQ(s.getCardLang(), QString("de")); -} - -TEST_F(SettingsDefaultsTest, CardsDisplay_CardSearchLanguage_Default) -{ - CardsDisplaySettings s(settingsPath, nullptr); - ASSERT_EQ(s.getCardSearchLanguage(), static_cast(SearchLanguageMode::English)); -} - -TEST_F(SettingsDefaultsTest, CardsDisplay_CardSearchLanguage_SetAndGet) -{ - CardsDisplaySettings s(settingsPath, nullptr); - s.setCardSearchLanguage(static_cast(SearchLanguageMode::Selected)); - ASSERT_EQ(s.getCardSearchLanguage(), static_cast(SearchLanguageMode::Selected)); -} - // --- VisualDeckStorageSettings --- TEST_F(SettingsDefaultsTest, VisualDeckStorage_SortingOrder_Default) @@ -616,19 +504,6 @@ TEST_F(SettingsDefaultsTest, VisualDeckStorage_DefaultTagsList_SetAndGet) ASSERT_EQ(s.getVisualDeckStorageDefaultTagsList(), custom); } -TEST_F(SettingsDefaultsTest, VisualDeckStorage_ShowUploadTime_Default) -{ - VisualDeckStorageSettings s(settingsPath, nullptr); - ASSERT_EQ(s.getVisualDeckStorageShowUploadTime(), true); -} - -TEST_F(SettingsDefaultsTest, VisualDeckStorage_ShowUploadTime_SetAndGet) -{ - VisualDeckStorageSettings s(settingsPath, nullptr); - s.setVisualDeckStorageShowUploadTime(false); - ASSERT_EQ(s.getVisualDeckStorageShowUploadTime(), false); -} - } // namespace int main(int argc, char **argv) diff --git a/tests/warning_categories_test.cpp b/tests/warning_categories_test.cpp deleted file mode 100644 index 432885dd9..000000000 --- a/tests/warning_categories_test.cpp +++ /dev/null @@ -1,89 +0,0 @@ -#include "gtest/gtest.h" -#include -#include - -TEST(WarningCategoriesTest, EmptyValueYieldsNoCategories) -{ - EXPECT_TRUE(parseWarningCategories(QString()).isEmpty()); - EXPECT_TRUE(parseWarningCategories(QString("")).isEmpty()); -} - -TEST(WarningCategoriesTest, PlainNamesDefaultToInterventionLevelOne) -{ - const QList categories = parseWarningCategories("Flaming,Spamming,Causing Drama"); - - ASSERT_EQ(3, categories.size()); - EXPECT_EQ("Flaming", categories.at(0).name); - EXPECT_EQ(1, categories.at(0).startingIl); - EXPECT_EQ("Spamming", categories.at(1).name); - EXPECT_EQ(1, categories.at(1).startingIl); - EXPECT_EQ("Causing Drama", categories.at(2).name); - EXPECT_EQ(1, categories.at(2).startingIl); -} - -TEST(WarningCategoriesTest, ExplicitInterventionLevelsAreParsed) -{ - const QList categories = parseWarningCategories("Cheating|2,Inappropriate Avatar|3"); - - ASSERT_EQ(2, categories.size()); - EXPECT_EQ("Cheating", categories.at(0).name); - EXPECT_EQ(2, categories.at(0).startingIl); - EXPECT_EQ("Inappropriate Avatar", categories.at(1).name); - EXPECT_EQ(3, categories.at(1).startingIl); -} - -TEST(WarningCategoriesTest, MixedEntriesKeepDefaultsForThoseWithoutLevels) -{ - const QList categories = parseWarningCategories("Abusive Language|1,Cheating|2,Spamming"); - - ASSERT_EQ(3, categories.size()); - EXPECT_EQ(1, categories.at(0).startingIl); - EXPECT_EQ(2, categories.at(1).startingIl); - EXPECT_EQ("Spamming", categories.at(2).name); - EXPECT_EQ(1, categories.at(2).startingIl); -} - -TEST(WarningCategoriesTest, EmptyEntriesAreSkipped) -{ - const QList categories = parseWarningCategories("Spamming,,Cheating|2,"); - - ASSERT_EQ(2, categories.size()); - EXPECT_EQ("Spamming", categories.at(0).name); - EXPECT_EQ("Cheating", categories.at(1).name); -} - -TEST(WarningCategoriesTest, WhitespaceIsTrimmed) -{ - const QList categories = parseWarningCategories(" Abusive Language , Cheating | 2 "); - - ASSERT_EQ(2, categories.size()); - EXPECT_EQ("Abusive Language", categories.at(0).name); - EXPECT_EQ(1, categories.at(0).startingIl); - EXPECT_EQ("Cheating", categories.at(1).name); - EXPECT_EQ(2, categories.at(1).startingIl); -} - -TEST(WarningCategoriesTest, InvalidInterventionLevelsFallBackToOne) -{ - const QList categories = parseWarningCategories("Spamming|abc,Cheating|0,Targeted Harassment|-3"); - - ASSERT_EQ(3, categories.size()); - EXPECT_EQ(1, categories.at(0).startingIl); - EXPECT_EQ(1, categories.at(1).startingIl); - EXPECT_EQ(1, categories.at(2).startingIl); -} - -TEST(WarningCategoriesTest, EntryWithOnlyLevelIsSkipped) -{ - const QList categories = parseWarningCategories("|2,Spamming|2"); - - ASSERT_EQ(1, categories.size()); - EXPECT_EQ("Spamming", categories.at(0).name); - EXPECT_EQ(2, categories.at(0).startingIl); -} - -int main(int argc, char **argv) -{ - ::testing::InitGoogleTest(&argc, argv); - return RUN_ALL_TESTS(); -}