diff --git a/.ci/Arch/Dockerfile b/.ci/Arch/Dockerfile index f37315262..36cf5c4ae 100644 --- a/.ci/Arch/Dockerfile +++ b/.ci/Arch/Dockerfile @@ -10,10 +10,8 @@ RUN pacman --sync --refresh --sysupgrade --needed --noconfirm \ ninja \ 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 0fa227d6f..202405b84 100644 --- a/.ci/Debian12/Dockerfile +++ b/.ci/Debian12/Dockerfile @@ -20,9 +20,7 @@ RUN apt-get update && \ qt6-image-formats-plugins \ qt6-l10n-tools \ qt6-multimedia-dev \ - qt6-declarative-dev \ qt6-svg-dev \ - qt6-shadertools-dev \ qt6-tools-dev \ qt6-tools-dev-tools \ qt6-websockets-dev \ diff --git a/.ci/Debian13/Dockerfile b/.ci/Debian13/Dockerfile index 13e8b35c7..d7ab6ac86 100644 --- a/.ci/Debian13/Dockerfile +++ b/.ci/Debian13/Dockerfile @@ -21,9 +21,7 @@ RUN apt-get update && \ qt6-image-formats-plugins \ qt6-l10n-tools \ qt6-multimedia-dev \ - qt6-declarative-dev \ qt6-svg-dev \ - qt6-shadertools-dev \ qt6-tools-dev \ qt6-tools-dev-tools \ qt6-websockets-dev \ diff --git a/.ci/Fedora43/Dockerfile b/.ci/Fedora43/Dockerfile index 68e894543..27570cf99 100644 --- a/.ci/Fedora43/Dockerfile +++ b/.ci/Fedora43/Dockerfile @@ -8,7 +8,7 @@ RUN dnf install -y \ mariadb-devel \ ninja-build \ protobuf-devel \ - qt6-{qtdeclarative,qtshadertools,qttools,qtsvg,qtmultimedia,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 ffd7c1b9b..e6c8da7f3 100644 --- a/.ci/Fedora44/Dockerfile +++ b/.ci/Fedora44/Dockerfile @@ -8,7 +8,7 @@ RUN dnf install -y \ mariadb-devel \ ninja-build \ protobuf-devel \ - qt6-{qtdeclarative,qtshadertools,qttools,qtsvg,qtmultimedia,qtwebsockets}-devel \ + qt6-{qttools,qtsvg,qtmultimedia,qtwebsockets}-devel \ qt6-qtimageformats \ rpm-build \ xz-devel \ diff --git a/.ci/Ubuntu24.04/Dockerfile b/.ci/Ubuntu24.04/Dockerfile index 12320c276..809b2e43a 100644 --- a/.ci/Ubuntu24.04/Dockerfile +++ b/.ci/Ubuntu24.04/Dockerfile @@ -20,9 +20,7 @@ RUN apt-get update && \ qt6-image-formats-plugins \ qt6-l10n-tools \ qt6-multimedia-dev \ - qt6-declarative-dev \ qt6-svg-dev \ - qt6-shadertools-dev \ qt6-tools-dev \ qt6-tools-dev-tools \ qt6-websockets-dev \ diff --git a/.ci/Ubuntu26.04/Dockerfile b/.ci/Ubuntu26.04/Dockerfile index ce3d9cd6c..7b0cd389f 100644 --- a/.ci/Ubuntu26.04/Dockerfile +++ b/.ci/Ubuntu26.04/Dockerfile @@ -21,9 +21,7 @@ RUN apt-get update && \ qt6-image-formats-plugins \ qt6-l10n-tools \ qt6-multimedia-dev \ - qt6-declarative-dev \ qt6-svg-dev \ - qt6-shadertools-dev \ qt6-tools-dev \ qt6-tools-dev-tools \ qt6-websockets-dev \ 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 Linux • Ubuntu 26.04 LTS Resolute Racoon @@ -24,10 +24,10 @@ Available pre-compiled binaries for installation: • Fedora 44 • Fedora 43 - General Linux support is available via a flatpak package hosted at Flathub (x86 & ARM)! - Thanks to courtesy of @FFY00, the app is also available in Arch Linux's official extra repository. +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 e895e2220..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@v6 - - - 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 04037a74e..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,7 +146,7 @@ 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: @@ -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 @@ -200,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 @@ -214,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" @@ -259,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.1 - qt_modules: qtimageformats qtmultimedia qtwebsockets qtshadertools + 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.1 - qt_modules: qtimageformats qtmultimedia qtwebsockets qtshadertools + 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.1 - qt_modules: qtimageformats qtmultimedia qtwebsockets qtshadertools + 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.1 - qt_modules: qtimageformats qtmultimedia qtwebsockets qtshadertools + 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.1 - qt_modules: qtimageformats qtmultimedia qtwebsockets qtshadertools + 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' || '' }} @@ -348,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 @@ -358,11 +359,14 @@ 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: "Install aqtinstall" run: pipx install aqtinstall @@ -380,7 +384,7 @@ jobs: id: restore_qt uses: actions/cache/restore@v6 with: - key: Qt-${{ steps.resolve_qt_version.outputs.version }}-macOS-${{ matrix.soc }}-${{ matrix.qt_modules }}-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 @@ -402,7 +406,7 @@ jobs: 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 }}" @@ -412,7 +416,6 @@ jobs: # 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: true - cache-key-prefix: Qt modules: ${{ matrix.qt_modules }} version: ${{ steps.resolve_qt_version.outputs.version }} @@ -437,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 }} @@ -448,26 +450,25 @@ jobs: USE_CCACHE: ${{ matrix.use_ccache }} 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 df4fe233c..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: type=gha,mode=max,scope=${{ 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@v7 - 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 37fe5d317..e90dc0151 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/Dockerfile b/Dockerfile index 382309d47..7c5c773c9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,45 +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 \ - 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 \ +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/cmake/FindQtRuntime.cmake b/cmake/FindQtRuntime.cmake index 1ee1f215f..78c9aa1b0 100644 --- a/cmake/FindQtRuntime.cmake +++ b/cmake/FindQtRuntime.cmake @@ -18,13 +18,10 @@ if(WITH_CLIENT) Multimedia Network PrintSupport - ShaderTools Svg WebSockets Widgets Xml - Quick - QuickWidgets ) endif() if(WITH_ORACLE) diff --git a/cockatrice/CMakeLists.txt b/cockatrice/CMakeLists.txt index 85a95a9bd..a15fda582 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,16 +36,13 @@ 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_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_settings.cpp src/interface/widgets/dialogs/dlg_startup_card_check.cpp @@ -149,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 @@ -165,7 +155,6 @@ set(cockatrice_SOURCES src/interface/widgets/cards/additional_info/color_identity_widget.cpp src/interface/widgets/cards/additional_info/mana_cost_widget.cpp src/interface/widgets/cards/additional_info/mana_symbol_widget.cpp - src/interface/widgets/cards/art_crop_attribution.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 @@ -230,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 @@ -246,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 @@ -258,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 @@ -273,7 +256,6 @@ set(cockatrice_SOURCES src/interface/widgets/server/user/user_info_connection.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 @@ -288,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 @@ -315,10 +295,8 @@ set(cockatrice_SOURCES 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/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 @@ -383,8 +361,6 @@ set(cockatrice_SOURCES 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_report.cpp src/interface/widgets/tabs/tab_replays.cpp src/interface/widgets/tabs/tab_room.cpp src/interface/widgets/tabs/tab_server.cpp @@ -398,27 +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/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 @@ -437,8 +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/url_parser.cpp src/interface/intents/url_parser.h src/interface/widgets/server/user/user_info_popup.cpp @@ -515,28 +468,6 @@ qt6_add_executable( MANUAL_FINALIZATION ) -qt6_add_shaders( - cockatrice - "onboarding_shaders" - PREFIX - "/onboarding/shaders" - BASE - "src/interface/widgets/onboarding/shaders" - FILES - src/interface/widgets/onboarding/shaders/brand_banner.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 diff --git a/cockatrice/cockatrice.qrc b/cockatrice/cockatrice.qrc index e21bdb0be..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 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/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 890a621c8..4e425fb66 100644 --- a/cockatrice/src/client/network/connection_controller/remote_connection_controller.cpp +++ b/cockatrice/src/client/network/connection_controller/remote_connection_controller.cpp @@ -44,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); @@ -298,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/filters/deck_filter_string.cpp b/cockatrice/src/filters/deck_filter_string.cpp index 4abb8210c..dd873cfa5 100644 --- a/cockatrice/src/filters/deck_filter_string.cpp +++ b/cockatrice/src/filters/deck_filter_string.cpp @@ -52,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); }; }; @@ -67,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 { @@ -119,9 +125,9 @@ static void setupParserRules() 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)) { @@ -140,49 +146,53 @@ 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"; } @@ -195,7 +205,7 @@ DeckFilterString::DeckFilterString(const QString &expr) _error = QString(); if (ba.isEmpty()) { - filter = [](const DeckSearchData &) { return true; }; + filter = [](const DeckPreviewWidget *, const ExtraDeckSearchInfo &) { return true; }; return; } @@ -205,6 +215,6 @@ DeckFilterString::DeckFilterString(const QString &expr) 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 90a6a17eb..916b629ee 100644 --- a/cockatrice/src/filters/deck_filter_string.h +++ b/cockatrice/src/filters/deck_filter_string.h @@ -7,7 +7,7 @@ #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 @@ -16,29 +16,26 @@ 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); - bool check(const DeckSearchData &data) const + 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/game/game_event_handler.cpp b/cockatrice/src/game/game_event_handler.cpp index bc68d4d7c..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: { diff --git a/cockatrice/src/game/player/player_logic.cpp b/cockatrice/src/game/player/player_logic.cpp index 45ba09aac..485e2fc5c 100644 --- a/cockatrice/src/game/player/player_logic.cpp +++ b/cockatrice/src/game/player/player_logic.cpp @@ -250,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_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 1410d0c80..e0029ee2d 100644 --- a/cockatrice/src/game_graphics/board/abstract_card_item.cpp +++ b/cockatrice/src/game_graphics/board/abstract_card_item.cpp @@ -305,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 e63117e13..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) { + connect(state, &CounterState::valueChanged, this, [this](int, int newValue) { value = newValue; - onValueChanged(oldValue, newValue); update(); }); @@ -229,9 +228,3 @@ void AbstractCounterDialog::changeValue(int diff) curValue += diff; setTextValue(QString::number(curValue)); } - -void AbstractCounter::onValueChanged(int /*oldValue*/, int /*newValue*/) -{ - // 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 9ddcc6d58..b319a722d 100644 --- a/cockatrice/src/game_graphics/board/abstract_counter.h +++ b/cockatrice/src/game_graphics/board/abstract_counter.h @@ -35,13 +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. - */ - virtual void onValueChanged(int oldValue, int newValue); - 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.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_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/game_scene.cpp b/cockatrice/src/game_graphics/game_scene.cpp index 87af4c73c..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,14 +45,7 @@ GameScene::GameScene(PhasesToolbar *_phasesToolbar, QObject *parent) GameScene::~GameScene() { - // Sever all incoming connections (animated item destroy-tracking) 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. - QObject::disconnect(nullptr, nullptr, this, nullptr); - delete animationTimer; - animationTimer = nullptr; // Delete all ArrowItems before QGraphicsScene's base destructor runs. // QGraphicsScene::~QGraphicsScene() destroys items in arbitrary order. @@ -252,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 ---------- @@ -469,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()); } @@ -518,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); } @@ -759,45 +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; - } - if (!animatedItems.contains(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); - 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 c12696189..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,11 +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 + QBasicTimer *animationTimer; ///< Timer for card animations + QSet cardsToAnimate; ///< Cards currently animating int playerRotation; ///< Rotation offset for player layout - bool rearranging = false; ///< Guard against re-entrant rearrange - bool needsReArrange = false; ///< Pending rearrange requested during a pass /** * @brief Updates which card is currently hovered based on scene coordinates. @@ -186,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/player/player_graphics_item.cpp b/cockatrice/src/game_graphics/player/player_graphics_item.cpp index 8bf2703e1..d443853ce 100644 --- a/cockatrice/src/game_graphics/player/player_graphics_item.cpp +++ b/cockatrice/src/game_graphics/player/player_graphics_item.cpp @@ -1,9 +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/playmat/playmat_utils.h" #include "../../interface/widgets/tabs/tab_game.h" #include "../board/abstract_card_item.h" #include "../board/counter_general.h" @@ -16,9 +13,6 @@ #include "player_dialogs.h" #include -#include -#include -#include #include PlayerGraphicsItem::PlayerGraphicsItem(PlayerLogic *_player) : player(_player) @@ -34,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); @@ -77,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(); @@ -125,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 = @@ -167,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) @@ -251,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) { - if (newValue < oldValue) { - tableZoneGraphicsItem->triggerDamageShimmer(); - } - }); } else { widget = new GeneralCounter(state, player, true, this); } @@ -366,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 = fullRes; - 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_target.cpp b/cockatrice/src/game_graphics/player/player_target.cpp index 910ee9c17..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,48 +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) -{ - flashDelta = newValue - oldValue; - if (flashDelta == 0) { - return; - } - - if (!SettingsCache::instance().userInterface().getLifeCounterAnimationsEnabled()) { - 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) diff --git a/cockatrice/src/game_graphics/player/player_target.h b/cockatrice/src/game_graphics/player/player_target.h index af0e9c8b7..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) 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/zones/stack_zone.cpp b/cockatrice/src/game_graphics/zones/stack_zone.cpp index e9b14f13d..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, 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/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/intent_join_server_game.cpp b/cockatrice/src/interface/intents/intent_join_server_game.cpp index 205c4dc70..fb9c4d5ce 100644 --- a/cockatrice/src/interface/intents/intent_join_server_game.cpp +++ b/cockatrice/src/interface/intents/intent_join_server_game.cpp @@ -55,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_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/url_parser.cpp b/cockatrice/src/interface/intents/url_parser.cpp index 509390611..8b5309603 100644 --- a/cockatrice/src/interface/intents/url_parser.cpp +++ b/cockatrice/src/interface/intents/url_parser.cpp @@ -1,7 +1,5 @@ #include "url_parser.h" -#include "../widgets/tabs/tab_room.h" -#include "../widgets/tabs/tab_supervisor.h" #include "../window_main.h" #include "contexts/context_join_game.h" #include "intent_join_server_game.h" @@ -11,7 +9,6 @@ #include #include #include -#include #include IntentUrlParser::IntentUrlParser(QObject *parent, MainWindow *_mainWindow) : QObject(parent), mainWindow(_mainWindow) @@ -74,15 +71,6 @@ void IntentUrlParser::handleJoinGame(const QUrlQuery &query) 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; - } - // The join game intent owns the context and the credential lookup; once the // chain finishes (or fails) it deletes the whole tree. ContextConnectToServer *serverContext = &ctx->roomContext.serverContext; @@ -99,38 +87,3 @@ void IntentUrlParser::handleJoinGame(const QUrlQuery &query) getLoginCredentialsIntent->execute(); } - -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); -} diff --git a/cockatrice/src/interface/intents/url_parser.h b/cockatrice/src/interface/intents/url_parser.h index 6d705e013..bac0e3d25 100644 --- a/cockatrice/src/interface/intents/url_parser.h +++ b/cockatrice/src/interface/intents/url_parser.h @@ -4,7 +4,6 @@ #include class MainWindow; -struct ContextJoinGame; class IntentUrlParser : public QObject { Q_OBJECT @@ -15,8 +14,6 @@ public: void handleJoinGame(const QUrlQuery &query); private: - QString generateJoinGameMessage(const ContextJoinGame &context, const QString &gameDescription); - MainWindow *mainWindow; }; diff --git a/cockatrice/src/interface/palette_editor/palette_editor_dialog.cpp b/cockatrice/src/interface/palette_editor/palette_editor_dialog.cpp index 9cde72c01..adae6e152 100644 --- a/cockatrice/src/interface/palette_editor/palette_editor_dialog.cpp +++ b/cockatrice/src/interface/palette_editor/palette_editor_dialog.cpp @@ -290,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().colors == savedConfig.value(it.key()).colors) { - 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].colors != savedConfig.value(loadedScheme).colors) { - 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. @@ -326,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/pixel_map_generator.cpp b/cockatrice/src/interface/pixel_map_generator.cpp index 9b8c4bcdc..d3b0252a6 100644 --- a/cockatrice/src/interface/pixel_map_generator.cpp +++ b/cockatrice/src/interface/pixel_map_generator.cpp @@ -3,7 +3,6 @@ #include #include #include -#include #include #include #include @@ -15,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. * @@ -62,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); @@ -277,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); @@ -419,57 +387,6 @@ 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) { if (qApp->palette().windowText().color().lightness() > 200) { diff --git a/cockatrice/src/interface/pixel_map_generator.h b/cockatrice/src/interface/pixel_map_generator.h index 17720166a..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,34 +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); #endif diff --git a/cockatrice/src/interface/theme_manager.cpp b/cockatrice/src/interface/theme_manager.cpp index e6b4b3c7f..518a97bc6 100644 --- a/cockatrice/src/interface/theme_manager.cpp +++ b/cockatrice/src/interface/theme_manager.cpp @@ -90,17 +90,13 @@ struct PaletteColorInfo } } -static QString usableDefaultStyle(const QString &style) -{ - // The Windows 11 native style is broken: when the OS default - // ("Default" theme selection) would use it, fall back to the Vista style. - // 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(); @@ -123,7 +119,7 @@ void ThemeManager::ensureThemeDirectoryExists() } } -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) { @@ -272,19 +268,6 @@ 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 = getAvailableThemes().value(SettingsCache::instance().getThemeName()); @@ -333,13 +316,13 @@ void ThemeManager::applyStyleAndPalette(const QString &themeName, 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 diff --git a/cockatrice/src/interface/theme_manager.h b/cockatrice/src/interface/theme_manager.h index 79a1b6470..861ab838b 100644 --- a/cockatrice/src/interface/theme_manager.h +++ b/cockatrice/src/interface/theme_manager.h @@ -66,14 +66,7 @@ protected: public: bool isBuiltInTheme(); - // 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 isDarkMode(const QString &themeDirPath); QStringMap &getAvailableThemes(); // Returns the path to the currently active theme directory (empty = default) QString getCurrentThemePath() const @@ -91,10 +84,6 @@ public: // theme directory when it is absent from the resolved (user) directory. static PaletteConfig loadDefaultPaletteConfig(const QString &themeDirPath, const QString &themeName, const QString &colorScheme); - /** @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); 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 1ea1bcb10..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,33 +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 = 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 == 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/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_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/deck_editor/deck_editor_deck_dock_widget.cpp b/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.cpp index 14defc8e9..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,7 +2,6 @@ #include "../../../client/settings/cache_settings.h" #include "../../../client/settings/shortcuts_settings.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" @@ -12,12 +11,10 @@ #include #include #include -#include #include #include #include #include -#include #include #include @@ -231,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"); @@ -451,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( @@ -537,7 +497,6 @@ void DeckEditorDeckDockWidget::syncDisplayWidgetsToModel() syncBannerCardComboBoxSelectionWithDeck(); updateBannerCardComboBox(); bannerCardComboBox->blockSignals(false); - updatePlaymatLabel(); updateHash(); formatComboBox->blockSignals(true); 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 9db01e2e5..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,15 +15,11 @@ #include "deck_list_history_manager_widget.h" #include "deck_list_style_proxy.h" -#include -#include #include #include -#include #include #include #include -#include class CommanderBracketWidget; class DeckListModel; @@ -37,8 +33,6 @@ public: DeckListStyleProxy *proxy; QTreeView *deckView; QComboBox *bannerCardComboBox; - QLabel *playmatLabel; - QPushButton *playmatSettingsButton; void createDeckDock(); ExactCard getCurrentCard(); void retranslateUi(); @@ -108,8 +102,6 @@ private slots: 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_state_manager.cpp b/cockatrice/src/interface/widgets/deck_editor/deck_state_manager.cpp index eda741728..f8fb450ce 100644 --- a/cockatrice/src/interface/widgets/deck_editor/deck_state_manager.cpp +++ b/cockatrice/src/interface/widgets/deck_editor/deck_state_manager.cpp @@ -142,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(); 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 b9c99903e..6fce6be57 100644 --- a/cockatrice/src/interface/widgets/deck_editor/deck_state_manager.h +++ b/cockatrice/src/interface/widgets/deck_editor/deck_state_manager.h @@ -171,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); ///@} 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_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_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 6ae8c9adb..fce99a1a7 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_register.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_register.cpp @@ -1,62 +1,18 @@ #include "dlg_register.h" #include "../../../client/settings/cache_settings.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(QPixmap("theme: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(QPixmap("theme: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.")); @@ -365,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); @@ -398,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; @@ -523,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/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 1550b57e7..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 -{ - Automatic, ///< Extract color from background, or use theme color if no background - FromBackground, ///< Always extract color from background -}; - -struct Entry -{ - Source source; - const char *trKey; ///< key for translation -}; - -inline QList all() -{ - static QList entries = {{Automatic, QT_TR_NOOP("Automatic")}, - {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::Automatic if the value is not within range - */ -inline Source intToSource(int value) -{ - if (value > FromBackground) { - return Automatic; // 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 10fcdcb43..8589e3517 100644 --- a/cockatrice/src/interface/widgets/general/home_widget.cpp +++ b/cockatrice/src/interface/widgets/general/home_widget.cpp @@ -4,10 +4,8 @@ #include "../../../interface/widgets/tabs/tab_supervisor.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 @@ -26,7 +24,7 @@ HomeWidget::HomeWidget(QWidget *parent, TabSupervisor *_tabSupervisor) backgroundSourceCard = new CardInfoPictureArtCropWidget(this); - gradientColors = determineButtonColor(); + gradientColors = extractDominantColors(background); layout->addWidget(createButtons(), 1, 1, Qt::AlignVCenter | Qt::AlignHCenter); @@ -56,8 +54,6 @@ HomeWidget::HomeWidget(QWidget *parent, TabSupervisor *_tabSupervisor) &HomeWidget::initializeBackgroundFromSource); connect(&SettingsCache::instance(), &SettingsCache::themeChanged, this, &HomeWidget::updateButtonsToBackgroundColor); - connect(&SettingsCache::instance().appearance(), &AppearanceSettings::homeTabButtonColorChanged, this, - &HomeWidget::updateButtonsToBackgroundColor); } void HomeWidget::initializeBackgroundFromSource() @@ -100,34 +96,6 @@ void HomeWidget::loadBackgroundSourceDeck() backgroundSourceDeck = deckOpt.has_value() ? deckOpt.value().deckList : DeckList(); } -static bool isDefaultBackgroundAndTheme() -{ - QString sourceId = SettingsCache::instance().appearance().getHomeTabBackgroundSource(); - return themeManager->isBuiltInTheme() && BackgroundSources::fromId(sourceId) == BackgroundSources::Theme; -} - -QPair HomeWidget::determineButtonColor() const -{ - static QPair defaultColor = {QColor::fromRgb(20, 140, 60), QColor::fromRgb(120, 200, 80)}; - - auto colorSource = - HomeTabButtonColor::intToSource(SettingsCache::instance().appearance().getHomeTabButtonColorSourceIndex()); - - switch (colorSource) { - case HomeTabButtonColor::Automatic: { - if (isDefaultBackgroundAndTheme()) { - return defaultColor; - } else { - return extractDominantColors(background); - } - } - case HomeTabButtonColor::FromBackground: - return extractDominantColors(background); - } - - return defaultColor; -} - void HomeWidget::setRandomCard(ExactCard &newCard) { static constexpr int ATTEMPTS = 10; @@ -202,7 +170,7 @@ void HomeWidget::updateBackgroundProperties() void HomeWidget::updateButtonsToBackgroundColor() { - gradientColors = determineButtonColor(); + gradientColors = extractDominantColors(background); for (HomeStyledButton *button : findChildren()) { button->updateStylesheet(gradientColors); button->update(); @@ -297,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) @@ -368,9 +341,8 @@ void HomeWidget::paintEvent(QPaintEvent *event) 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(); @@ -378,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); @@ -407,26 +360,23 @@ 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); diff --git a/cockatrice/src/interface/widgets/general/home_widget.h b/cockatrice/src/interface/widgets/general/home_widget.h index 9df0d7b6a..90d003aa7 100644 --- a/cockatrice/src/interface/widgets/general/home_widget.h +++ b/cockatrice/src/interface/widgets/general/home_widget.h @@ -23,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; @@ -47,7 +47,6 @@ private: void setRandomCard(ExactCard &newCard); void loadBackgroundSourceDeck(); - QPair determineButtonColor() const; }; #endif // HOME_WIDGET_H 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 32f3e89c0..000000000 --- a/cockatrice/src/interface/widgets/onboarding/banner_shader_config.h +++ /dev/null @@ -1,250 +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(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(); - } - } - - 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 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; - - QColor m_colorA{0x1A, 0x1A, 0x20}; - QColor m_colorB{0x0E, 0x0E, 0x12}; - QColor m_accent{0x8B, 0xDD, 0x6B}; - - bool m_logoVisible = false; - qreal m_logoGlow = 1.0; -}; - -#endif // BANNER_SHADER_CONFIG_H 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 618ac6f26..000000000 --- a/cockatrice/src/interface/widgets/onboarding/first_run_wizard.cpp +++ /dev/null @@ -1,218 +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::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 2c186ef95..000000000 --- a/cockatrice/src/interface/widgets/onboarding/first_run_wizard.h +++ /dev/null @@ -1,71 +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); - -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 12116de7a..000000000 --- a/cockatrice/src/interface/widgets/onboarding/pages/card_database_setup_page.cpp +++ /dev/null @@ -1,314 +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 - -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(); - } -} - -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 0461d11d5..000000000 --- a/cockatrice/src/interface/widgets/onboarding/pages/card_database_setup_page.h +++ /dev/null @@ -1,79 +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); - -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 3293b19ac..000000000 --- a/cockatrice/src/interface/widgets/onboarding/pages/theme_setup_page.cpp +++ /dev/null @@ -1,231 +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 - -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); - - 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 newDir = themeManager->getAvailableThemes().value(SettingsCache::instance().getThemeName()); - 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); - - 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(); - - if (PaletteConfig::fromScheme(dirPath, scheme).hasPalette() || - PaletteConfig::fromDefault(dirPath, 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 d1f84c1b9..000000000 --- a/cockatrice/src/interface/widgets/onboarding/pages/theme_setup_page.h +++ /dev/null @@ -1,58 +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; -}; - -#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 f1a385cad..000000000 --- a/cockatrice/src/interface/widgets/onboarding/qml/BrandBanner.qml +++ /dev/null @@ -1,62 +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 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 real uLogoGlow: bannerConfig.logoGlow - fragmentShader: "qrc:/onboarding/shaders/brand_banner.frag.qsb" - } - - // The hero logo itself — breathes cleanly over a 0.5–1.0 opacity range - Image { - id: logo - anchors.centerIn: parent - visible: bannerConfig.logoVisible - source: "qrc:/resources/cockatrice-logo-white.svg" - width: root.height * 0.6 - height: width * (sourceSize.height > 0 ? sourceSize.height / Math.max(sourceSize.width, 1) : 1) - fillMode: Image.PreserveAspectFit - smooth: true - opacity: 0.5 + 0.5 * bannerConfig.logoGlow - sourceSize: Qt.size(256, 256) - - Behavior on opacity { NumberAnimation { duration: 300; easing.type: Easing.InOutSine } } - - transform: Scale { - origin.x: logo.width / 2 - origin.y: logo.height / 2 - xScale: 0.94 + 0.06 * bannerConfig.logoGlow - yScale: 0.94 + 0.06 * bannerConfig.logoGlow - } - } -} 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 fd1fb2a98..000000000 --- a/cockatrice/src/interface/widgets/onboarding/shader_banner_widget.cpp +++ /dev/null @@ -1,195 +0,0 @@ -#include "shader_banner_widget.h" - -#include "banner_shader_config.h" - -#include -#include -#include -#include -#include -#include - -namespace -{ -// Near-black base palette -- the background is dark and quiet so the green -// accent stands out. -constexpr QRgb kColorA = 0x1A1A20; -constexpr QRgb kColorB = 0x0E0E12; -constexpr QRgb kAccent = 0x8BDD6B; -} // namespace - -class GradientFallbackWidget : public QWidget -{ -public: - using QWidget::QWidget; - -protected: - void paintEvent(QPaintEvent *) override - { - QPainter painter(this); - QLinearGradient gradient(0, 0, width(), height()); - gradient.setColorAt(0.0, QColor(kColorA)); - gradient.setColorAt(1.0, QColor(kColorB)); - painter.fillRect(rect(), gradient); - } -}; - -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 - - 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(QColor(kColorA)); - config->setColorB(QColor(kColorB)); - config->setAccent(QColor(kAccent)); - 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::tick() -{ - 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 2e230ad7f..000000000 --- a/cockatrice/src/interface/widgets/onboarding/shader_banner_widget.h +++ /dev/null @@ -1,83 +0,0 @@ -#ifndef SHADER_BANNER_WIDGET_H -#define SHADER_BANNER_WIDGET_H - -#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(); - -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; - - 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 508bd4bc4..000000000 --- a/cockatrice/src/interface/widgets/onboarding/shaders/brand_banner.frag +++ /dev/null @@ -1,461 +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; - 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.10; - - 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 - float centreLight = bloom(cDist, 0.08 * asp, 0.40 * asp); - col += centreLight * 0.20 * uLogoGlow; - - // Flow-noise shimmer gated by Gaussian mask at centre - float shimmer = flowNoise(ac * 0.8 + vec2(55.0, 33.0), t * 0.05) * 0.5 + 0.5; - float shimmerMask = exp(-(cDist * cDist) / (0.18 * asp * 0.18 * asp)); - col += shimmer * shimmerMask * 0.04 * uLogoGlow; - - // 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.006 + hash21(vec2(fi * 2.9, uSeed * 4.7)) * 0.012; - float bright = 0.15 + hash21(vec2(fi * 6.1, uSeed * 0.9)) * 0.30; - - // 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.55, fill * 0.50); - - // 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.20, 0.45, 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.10; - } - } - } - - // Central bloom at banner centre - float cDist = length(ac - center); - col += uAccent.rgb * bloom(cDist, 0.04, 0.25) * 0.12; - - // Periodic expanding ring from centre - float ripplePhase = t * 0.4; - float rippleDist = abs(cDist - fract(ripplePhase) * asp * 0.7); - col += uAccent.rgb * smoothstep(0.02, 0.0, rippleDist) * 0.10; - - 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); - - col *= mix(0.62, 1.0, vignette(uv)); - fragColor = vec4(col, 1.0) * qt_Opacity; -} 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 57706cf93..000000000 --- a/cockatrice/src/interface/widgets/playmat/playmat_settings_dialog.cpp +++ /dev/null @@ -1,324 +0,0 @@ -#include "playmat_settings_dialog.h" - -#include "../../card_picture_loader/card_picture_loader.h" -#include "../cards/art_crop_attribution.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 = fullRes; - 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 0b77ca185..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,7 +1,6 @@ #include "printing_selector_card_overlay_widget.h" #include "../../../client/settings/cache_settings.h" -#include "../cards/card_info_picture_widget.h" #include "printing_selector_card_display_widget.h" #include 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 c23e22651..064064380 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; @@ -392,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]")) { @@ -553,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) { @@ -648,19 +558,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) @@ -788,11 +685,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 f41002247..6580f0262 100644 --- a/cockatrice/src/interface/widgets/server/game_selector.cpp +++ b/cockatrice/src/interface/widgets/server/game_selector.cpp @@ -7,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" @@ -19,6 +18,8 @@ #include #include #include +#include +#include #include #include #include @@ -319,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; @@ -359,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 spectator = asSpectator || game.player_count() == game.max_players(); + bool overrideRestrictions = !tabSupervisor->getAdminLocked(); - - // 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; - 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; } @@ -418,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"; 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/user/user_card_art_provider.cpp b/cockatrice/src/interface/widgets/server/user/user_card_art_provider.cpp index 3a1876fa1..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 @@ -39,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; } @@ -66,10 +63,6 @@ QPixmap UserCardArtProvider::cropCardArt(const QPixmap &fullRes) void UserCardArtProvider::insertIntoCache(const QString &key, const QPixmap &pixmap) { - if (pixmap.isNull()) { - return; - } - if (!cardArtCache.contains(key)) { cacheInsertionOrder.append(key); while (cacheInsertionOrder.size() > MaxCacheEntries) { @@ -136,6 +129,8 @@ void UserCardArtProvider::processQueue() if (!fullRes.isNull()) { 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 2592237c4..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 @@ -26,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 532112964..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) { @@ -563,16 +327,13 @@ void UserCardArtSettingsDialog::reloadPreview() 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 646f2ee33..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,6 +1,5 @@ #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" @@ -42,7 +41,6 @@ UserContextMenu::UserContextMenu(TabSupervisor *_tabSupervisor, QWidget *parent, 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,7 +50,6 @@ UserContextMenu::UserContextMenu(TabSupervisor *_tabSupervisor, QWidget *parent, aPromoteToJudge = new QAction(QString(), this); aDemoteFromJudge = new QAction(QString(), this); aGetAdminNotes = new QAction(QString(), this); - aInvestigateUser = new QAction(QString(), this); retranslateUi(); } @@ -67,7 +64,6 @@ void UserContextMenu::retranslateUi() 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")); @@ -77,7 +73,6 @@ void UserContextMenu::retranslateUi() aPromoteToJudge->setText(tr("Promote user to &judge")); aDemoteFromJudge->setText(tr("Demote user from judge")); aGetAdminNotes->setText(tr("View admin notes")); - aInvestigateUser->setText(tr("Investigate user")); } void UserContextMenu::gamesOfUserReceived(const Response &resp, const CommandContainer &commandContainer) @@ -149,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(); @@ -361,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); @@ -373,17 +366,6 @@ void UserContextMenu::showContextMenu(const QPoint &pos, menu->addAction(aDetails); menu->addAction(aShowGames); 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)) { @@ -401,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); @@ -417,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) && @@ -438,10 +416,10 @@ void UserContextMenu::showContextMenu(const QPoint &pos, menu->addAction(aPromoteToJudge); } } + bool anotherUser = userName != userListProxy->getOwnUsername(); aDetails->setEnabled(true); aChat->setEnabled(anotherUser && online); aShowGames->setEnabled(online); - aReport->setEnabled(anotherUser); aAddToBuddyList->setEnabled(anotherUser); aRemoveFromBuddyList->setEnabled(anotherUser); aAddToIgnoreList->setEnabled(anotherUser); @@ -452,7 +430,6 @@ void UserContextMenu::showContextMenu(const QPoint &pos, aBan->setEnabled(anotherUser); aBanHistory->setEnabled(anotherUser); aGetAdminNotes->setEnabled(anotherUser); - aInvestigateUser->setEnabled(anotherUser); aPromoteToMod->setEnabled(anotherUser); aDemoteFromMod->setEnabled(anotherUser); @@ -474,15 +451,6 @@ 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) { @@ -497,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); @@ -514,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()), @@ -675,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; 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 f1ce931f8..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; @@ -41,14 +38,11 @@ private: QAction *aAddToBuddyList, *aRemoveFromBuddyList; QAction *aAddToIgnoreList, *aRemoveFromIgnoreList; QAction *aKick; - QAction *aReport; QAction *aBan, *aBanHistory; QAction *aPromoteToMod, *aDemoteFromMod; QAction *aPromoteToJudge, *aDemoteFromJudge; QAction *aWarnUser, *aWarnHistory; QAction *aGetAdminNotes; - std::function()> gameInviteLinkProvider; - QAction *aInvestigateUser; signals: void openMessageDialog(const QString &userName, bool focus); private slots: @@ -86,28 +80,9 @@ 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 execAddToBuddy(const QString &userName); @@ -120,12 +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); - -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 875bdfb05..416cd42e3 100644 --- a/cockatrice/src/interface/widgets/server/user/user_info_box.cpp +++ b/cockatrice/src/interface/widgets/server/user/user_info_box.cpp @@ -319,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 a1d58fe5c..74e567dde 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); } @@ -263,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)); @@ -288,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)); @@ -319,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); @@ -511,16 +394,16 @@ 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); @@ -528,20 +411,20 @@ void UserInfoPopup::rebuildActionButtons(const ServerInfo_User &userInfo, bool o // ── 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); } @@ -554,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); }); @@ -570,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) @@ -605,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; } @@ -618,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")); @@ -631,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; } @@ -645,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(); @@ -694,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; } @@ -735,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(); } @@ -773,4 +646,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 02cc2b44e..69517093f 100644 --- a/cockatrice/src/interface/widgets/server/user/user_info_popup.h +++ b/cockatrice/src/interface/widgets/server/user/user_info_popup.h @@ -26,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 ───────────────────────────────────────────────────────────── /** @@ -80,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 ──────────────────────────────────────────────────────────────── @@ -123,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); /** @@ -138,17 +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(); - signals: void mouseEnteredPopup(); void mouseLeftPopup(); @@ -186,30 +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; - - 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_painter.cpp b/cockatrice/src/interface/widgets/server/user/user_list_painter.cpp index 5c65b090d..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,29 +19,6 @@ 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; @@ -83,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); } @@ -153,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); @@ -176,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); @@ -217,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); @@ -238,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); @@ -251,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); @@ -267,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); @@ -276,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); } @@ -319,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; @@ -342,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; @@ -365,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; @@ -385,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::IsModerator) || - userLevel.testFlag(ServerInfo_User::IsJudge); const QRectF cardRect = QRectF(rect).adjusted(3, 2, -3, -2); const int cardRight = getCardRight(option, rect); @@ -395,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 e7570ab26..64cbb7b7d 100644 --- a/cockatrice/src/interface/widgets/server/user/user_list_widget.cpp +++ b/cockatrice/src/interface/widgets/server/user/user_list_widget.cpp @@ -2,9 +2,7 @@ #include "../../../../client/settings/cache_settings.h" #include "../../../card_picture_loader/card_picture_loader.h" -#include "../../cards/art_crop_attribution.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" @@ -13,17 +11,11 @@ #include #include -#include -#include -#include -#include #include #include #include -#include #include #include -#include #include #include #include @@ -31,8 +23,6 @@ #include #include #include -#include -#include #include #include #include @@ -151,7 +141,7 @@ WarningDialog::WarningDialog(const QString userName, const QString clientID, QWi warnClientID = new QLineEdit(clientID); warnClientID->setMaxLength(MAX_NAME_LENGTH); warningOption = new QComboBox(); - warningOption->addItem("", ""); + warningOption->addItem(""); deleteMessages = new QCheckBox(tr("Redact all messages from this user in all rooms")); @@ -184,7 +174,7 @@ void WarningDialog::okClicked() return; } - if (warningOption->currentData().toString().simplified().isEmpty()) { + 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; @@ -205,7 +195,7 @@ QString WarningDialog::getWarnID() const QString WarningDialog::getReason() const { - return warningOption->currentData().toString().simplified(); + return warningOption->currentText().simplified(); } int WarningDialog::getDeleteMessages() const @@ -213,13 +203,9 @@ int WarningDialog::getDeleteMessages() const return deleteMessages->isChecked() ? -1 : 0; } -void WarningDialog::addWarningOption(const QString warning, int startingIl) +void WarningDialog::addWarningOption(const QString warning) { - if (startingIl > 1) { - warningOption->addItem(tr("%1 (IL %2)").arg(warning).arg(startingIl), warning); - } else { - warningOption->addItem(warning, warning); - } + warningOption->addItem(warning); } void BanDialog::okClicked() @@ -337,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) { } @@ -359,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; } } @@ -374,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) @@ -520,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)); } /** @@ -586,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); @@ -604,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); @@ -613,173 +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 - - 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); @@ -788,11 +565,6 @@ UserListWidget::UserListWidget(TabSupervisor *_tabSupervisor, retranslateUi(); } -UserListWidget::~UserListWidget() -{ - qApp->removeEventFilter(this); -} - void UserListWidget::bind(UserListManager *mgr) { manager = mgr; @@ -800,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); } }; @@ -881,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; @@ -907,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); @@ -963,124 +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::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(); } } } @@ -1088,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; } @@ -1168,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; @@ -1207,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(); @@ -1274,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) { @@ -1383,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; @@ -1406,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) { @@ -1432,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; @@ -1525,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); @@ -1684,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 5fed54573..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" @@ -19,11 +18,9 @@ #include #include #include -#include #include #include #include -#include #include #include @@ -39,8 +36,6 @@ class QPlainTextEdit; class Response; class CommandContainer; class UserContextMenu; -class UserListWidget; -class QShowEvent; class BanDialog : public QDialog { @@ -85,7 +80,7 @@ public: [[nodiscard]] QString getWarnID() const; [[nodiscard]] QString getReason() const; [[nodiscard]] int getDeleteMessages() const; - void addWarningOption(const QString warning, int startingIl = 1); + void addWarningOption(const QString warning); }; class AdminNotesDialog : public QDialog @@ -107,15 +102,12 @@ public: 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); @@ -154,12 +146,6 @@ public: BuddyList, IgnoreList }; - enum class Section - { - Buddy, - Online, - Ignore - }; private: UserListManager *manager = nullptr; @@ -167,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); @@ -243,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 881c54167..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 @@ -132,14 +128,6 @@ AppearanceSettingsPage::AppearanceSettingsPage() connect(&homeTabDisplayCardNameCheckBox, &QCheckBox::QT_STATE_CHANGED, &settings.appearance(), &AppearanceSettings::setHomeTabDisplayCardName); - 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; @@ -148,8 +136,6 @@ 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); homeTabGroupBox = new QGroupBox; homeTabGroupBox->setLayout(homeTabGrid); @@ -339,52 +325,10 @@ AppearanceSettingsPage::AppearanceSettingsPage() tableGroupBox = new QGroupBox; tableGroupBox->setLayout(tableGrid); - // 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); - // putting it all together auto *mainLayout = new QVBoxLayout; mainLayout->addWidget(themeGroupBox); mainLayout->addWidget(homeTabGroupBox); - mainLayout->addWidget(playmatGroupBox); mainLayout->addWidget(stylingGroupBox); mainLayout->addWidget(menuGroupBox); mainLayout->addWidget(printingsGroupBox); @@ -431,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); @@ -487,12 +431,6 @@ void AppearanceSettingsPage::cardViewExpandedRowsMaxChanged(int value) } } -void AppearanceSettingsPage::openPlaymatCollectionDialog() -{ - PlaymatCollectionDialog dialog(this); - dialog.exec(); -} - void AppearanceSettingsPage::retranslateUi() { themeGroupBox->setTitle(tr("Theme settings")); @@ -508,9 +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")); - homeTabButtonColorSourceLabel.setText(tr("Home tab button color:")); - homeTabButtonColorSourceBox.setToolTip( - tr("Automatic: extract from background if present, otherwise use theme default")); stylingGroupBox->setTitle(tr("Styling settings")); styleUserListCheckBox.setText(tr("Style user list")); @@ -554,9 +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:")); - 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...")); -} +} \ 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 6b0369694..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,15 +34,11 @@ private: QLabel styleComboLabel; QComboBox styleCombo; QPushButton editPaletteButton; - QLabel homeTabBackgroundSourceLabel; QComboBox homeTabBackgroundSourceBox; QLabel homeTabBackgroundShuffleFrequencyLabel; QSpinBox homeTabBackgroundShuffleFrequencySpinBox; QCheckBox homeTabDisplayCardNameCheckBox; - QLabel homeTabButtonColorSourceLabel; - QComboBox homeTabButtonColorSourceBox; - QCheckBox styleUserListCheckBox; QCheckBox showShortcutsCheckBox; QCheckBox showGameSelectorFilterToolbarCheckBox; @@ -64,12 +59,6 @@ private: QCheckBox horizontalHandCheckBox; QCheckBox leftJustifiedHandCheckBox; QCheckBox invertVerticalCoordinateCheckBox; - QLabel playmatVisibilityLabel; - QComboBox playmatVisibilityCombo; - QLabel playmatModeLabel; - QComboBox playmatModeCombo; - QLabel playmatDefaultLabel; - QPushButton playmatDefaultEditButton; QGroupBox *themeGroupBox; QGroupBox *homeTabGroupBox; QGroupBox *stylingGroupBox; @@ -78,7 +67,6 @@ private: QGroupBox *cardsGroupBox; QGroupBox *cardLayoutGroupBox; QGroupBox *handGroupBox; - QGroupBox *playmatGroupBox; QGroupBox *tableGroupBox; QGroupBox *cardCountersGroupBox; QList cardCounterNames; 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 a293660f9..91c4943e1 100644 --- a/cockatrice/src/interface/widgets/settings_page/general_settings_page.cpp +++ b/cockatrice/src/interface/widgets/settings_page/general_settings_page.cpp @@ -2,7 +2,6 @@ #include "../../../client/settings/cache_settings.h" #include "../main.h" -#include "../server/user/user_info_connection.h" #include "update/client/release_channel.h" #include @@ -12,7 +11,6 @@ #include #include #include -#include #include #include @@ -123,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); @@ -412,17 +357,6 @@ void GeneralSettingsPage::languageBoxChanged(int index) SettingsCache::instance().personal().setLang(languageBox.itemData(index).toString()); } -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() { languageGroupBox->setTitle(tr("Language settings")); @@ -459,20 +393,6 @@ void GeneralSettingsPage::retranslateUi() 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")); - 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")); resetAllPathsButton->setText(tr("Reset all paths")); const auto &settings = SettingsCache::instance(); 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 8dd7e8798..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,9 +20,6 @@ public: GeneralSettingsPage(); void retranslateUi() override; - static QStringList findQmFiles(); - static QString languageName(const QString &lang); - private slots: void deckPathButtonClicked(); void filtersPathButtonClicked(); @@ -33,9 +30,11 @@ private slots: void tokenDatabasePathButtonClicked(); void resetAllPathsClicked(); void languageBoxChanged(int index); - void updateStartupServerControlsVisibility(); private: + QStringList findQmFiles(); + QString languageName(const QString &lang); + QGroupBox *languageGroupBox; QGroupBox *versionGroupBox; QGroupBox *cardDatabaseGroupBox; @@ -72,12 +71,6 @@ private: QLabel updateReleaseChannelLabel; QLabel advertiseTranslationPageLabel; QCheckBox showTipsOnStartup; - QLabel startupTabLabel; - QComboBox startupTabSelector; - QLabel startupServerLabel; - QComboBox startupServerSelector; - QLabel startupRoomLabel; - QLineEdit *startupRoomNameEdit; }; #endif // COCKATRICE_GENERAL_SETTINGS_PAGE_H 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 182e75aac..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 @@ -116,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); @@ -183,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); @@ -249,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); @@ -298,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,12 +310,7 @@ void UserInterfaceSettingsPage::retranslateUi() 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")); @@ -377,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")); 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 0dc4cf4e8..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,8 +17,6 @@ class UserInterfaceSettingsPage : public AbstractSettingsPage Q_OBJECT private slots: void setNotificationEnabled(QT_STATE_CHANGED_T); - void enableAllAnimations(); - void disableAllAnimations(); void updateCommanderSpellbookUiState(); private: @@ -37,12 +34,7 @@ private: QCheckBox showTotalSelectionCountCheckBox; QCheckBox useTearOffMenusCheckBox; QCheckBox keepGameChatFocusCheckBox; - QPushButton enableAllAnimationsButton; - QPushButton disableAllAnimationsButton; QCheckBox tapAnimationCheckBox; - QCheckBox arrowDrawAnimationCheckBox; - QCheckBox lifeCounterAnimationsCheckBox; - QCheckBox battlefieldFlashCheckBox; QCheckBox openDeckInNewTabCheckBox; QLabel visualDeckStoragePromptForConversionLabel; QComboBox visualDeckStoragePromptForConversionSelector; @@ -50,8 +42,6 @@ private: QCheckBox visualDeckStorageSelectionAnimationCheckBox; QLabel defaultDeckEditorTypeLabel; QComboBox defaultDeckEditorTypeSelector; - QLabel vdeStartupTabLabel; - QComboBox vdeStartupTabSelector; QLabel commanderSpellbookIntegrationEnabledLabel; QComboBox commanderSpellbookIntegrationEnabledSelector; QLabel commanderSpellbookIntegrationUseOfficialBracketNamesLabel; 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 a3cda2bfc..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" @@ -264,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/tab_archidekt.cpp b/cockatrice/src/interface/widgets/tabs/api/archidekt/tab_archidekt.cpp index 98b21d0f1..374d35cdf 100644 --- a/cockatrice/src/interface/widgets/tabs/api/archidekt/tab_archidekt.cpp +++ b/cockatrice/src/interface/widgets/tabs/api/archidekt/tab_archidekt.cpp @@ -2,12 +2,12 @@ #include "../../../../../client/settings/cache_settings.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 @@ -19,6 +19,7 @@ #include #include #include +#include #include #include #include @@ -278,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/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 410a48d40..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,7 +126,6 @@ void TabAccount::addToList(const std::string &listName, const QString &userName) void TabAccount::retranslateUi() { - myReportsButton->setText(tr("My Reports")); allUsersList->retranslateUi(); buddyList->retranslateUi(); ignoreList->retranslateUi(); @@ -142,7 +135,6 @@ void TabAccount::retranslateUi() 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()); @@ -150,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(); } @@ -196,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) @@ -247,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_game.cpp b/cockatrice/src/interface/widgets/tabs/tab_game.cpp index 196ea4526..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 @@ -299,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) { @@ -340,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")); @@ -513,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()); @@ -610,8 +538,7 @@ bool TabGame::leaveGame() void TabGame::actSay() { - if (sayEdit->hasVisibleCompleterPopup()) { - sayEdit->hideCompleterPopups(); + if (completer->popup()->isVisible()) { return; } @@ -631,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); } } @@ -701,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) @@ -911,7 +838,6 @@ void TabGame::stopGame() QMapIterator i(deckViewContainers); while (i.hasNext()) { i.next(); - i.value()->playerDeckView->advancePlaymatRotation(); i.value()->show(); } @@ -1052,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()) { @@ -1095,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); @@ -1105,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); } @@ -1122,8 +1038,6 @@ void TabGame::createReplayMenuItems() aRotateViewCCW = nullptr; aResetLayout = nullptr; aGameInfo = nullptr; - aCopyGameLink = nullptr; - aInviteToGame = nullptr; aConcede = nullptr; aFocusChat = nullptr; aLeaveGame = new QAction(this); @@ -1322,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); } @@ -1368,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); @@ -1386,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_message.cpp b/cockatrice/src/interface/widgets/tabs/tab_message.cpp index 9506d96f3..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,58 +94,28 @@ void TabMessage::closeEvent(QCloseEvent *event) event->accept(); } -void TabMessage::sendPrivateMessage(const QString &text) -{ - Command_Message cmd; - cmd.set_user_name(otherUserInfo->name()); - cmd.set_message(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()) { + if (sayEdit->text().isEmpty() || !userOnline) { return; } - if (!userOnline) { - notifyUserOffline(); - return; - } + Command_Message cmd; + cmd.set_user_name(otherUserInfo->name()); + cmd.set_message(sayEdit->text().toStdString()); + + PendingCommand *pend = client->prepareSessionCommand(cmd); + connect(pend, &PendingCommand::finished, this, &TabMessage::messageSent); + client->sendCommand(pend); - 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(); } } @@ -207,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 077b876d2..000000000 --- a/cockatrice/src/interface/widgets/tabs/tab_moderation.cpp +++ /dev/null @@ -1,462 +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::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_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 6245b5301..5705c184e 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_room.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_room.cpp @@ -4,17 +4,10 @@ #include "../../../client/settings/shortcuts_settings.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" @@ -24,15 +17,11 @@ #include #include #include -#include #include -#include #include #include #include #include -#include -#include #include #include #include @@ -63,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); @@ -120,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); @@ -139,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(); @@ -175,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")); @@ -229,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; @@ -264,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) @@ -301,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_supervisor.cpp b/cockatrice/src/interface/widgets/tabs/tab_supervisor.cpp index a59e83637..39fd44438 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" @@ -19,9 +17,7 @@ #include "tab_home.h" #include "tab_logs.h" #include "tab_message.h" -#include "tab_moderation.h" #include "tab_replays.h" -#include "tab_report.h" #include "tab_room.h" #include "tab_server.h" #include "tab_visual_database_display.h" @@ -34,7 +30,6 @@ #include #include #include -#include #include #include #include @@ -43,7 +38,6 @@ #include #include #include -#include #include #include #include @@ -106,7 +100,7 @@ void CloseButton::paintEvent(QPaintEvent * /*event*/) TabSupervisor::TabSupervisor(AbstractClient *_client, QMenu *tabsMenu, QWidget *parent) : QTabWidget(parent), userInfo(nullptr), client(_client), tabsMenu(tabsMenu), tabVisualDeckStorage(nullptr), tabServer(nullptr), tabAccount(nullptr), tabDeckStorage(nullptr), tabReplays(nullptr), tabAdmin(nullptr), - tabLog(nullptr), tabReport(nullptr), tabModeration(nullptr), isLocalGame(false) + tabLog(nullptr), isLocalGame(false) { setElideMode(Qt::ElideRight); setMovable(true); @@ -123,7 +117,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 @@ -178,14 +171,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); - connect(&SettingsCache::instance().shortcuts(), &ShortcutsSettings::shortCutChanged, this, &TabSupervisor::refreshShortcuts); refreshShortcuts(); @@ -225,8 +210,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")); // tabs QList tabs; @@ -236,8 +219,6 @@ void TabSupervisor::retranslateUi() tabs.append(tabAdmin); tabs.append(tabAccount); tabs.append(tabLog); - tabs.append(tabReport); - tabs.append(tabModeration); QMapIterator roomIterator(roomTabs); while (roomIterator.hasNext()) { tabs.append(roomIterator.next().value()); @@ -284,8 +265,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) @@ -338,12 +317,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() { @@ -359,42 +333,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; - } } /** @@ -409,7 +347,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)); @@ -487,8 +424,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(); @@ -496,12 +431,6 @@ 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(); - } openTabCardArtRules(); } @@ -515,8 +444,6 @@ void TabSupervisor::startLocal(const QList &_clients) tabAccount = nullptr; tabAdmin = nullptr; tabLog = nullptr; - tabReport = nullptr; - tabModeration = nullptr; isLocalGame = true; userInfo = new ServerInfo_User; localClients = _clients; @@ -558,12 +485,6 @@ void TabSupervisor::stop() if (tabLog) { tabLog->close(); } - if (tabReport) { - tabReport->close(); - } - if (tabModeration) { - tabModeration->close(); - } } QList tabsToDelete; @@ -801,59 +722,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::updatePingTime(int value, int max) { if (!tabServer) { @@ -866,23 +734,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; @@ -941,7 +792,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) { @@ -988,30 +838,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())) { @@ -1019,10 +845,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()); } @@ -1036,10 +860,9 @@ TabMessage *TabSupervisor::addMessageTab(const QString &receiverName, bool focus return tab; } - 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) { @@ -1063,53 +886,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 (TabGame *tab : gameTabs) { - 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 @@ -1396,24 +1172,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 852ebe645..5d349e18c 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" @@ -23,7 +22,6 @@ #include #include #include -#include class TabCardArtRules; inline Q_LOGGING_CATEGORY(TabSupervisorLog, "tab_supervisor"); @@ -40,8 +38,6 @@ class TabDeckStorage; class TabReplays; class TabAdmin; class TabMessage; -class TabReport; -class TabModeration; class TabAccount; class TabDeckEditor; class TabLog; @@ -98,8 +94,6 @@ private: TabAdmin *tabAdmin; TabCardArtRules *tabCardArtRules; TabLog *tabLog; - TabReport *tabReport; - TabModeration *tabModeration; QMap roomTabs; QMap gameTabs; QList replayTabs; @@ -109,7 +103,7 @@ private: QAction *aTabHome, *aTabDeckEditor, *aTabVisualDeckEditor, *aTabEdhRec, *aTabArchidekt, *aTabVisualDeckStorage, *aTabVisualDatabaseDisplay, *aTabServer, *aTabAccount, *aTabDeckStorage, *aTabReplays, *aTabAdmin, - *aTabCardArtRules, *aTabLog, *aTabReport, *aTabModeration; + *aTabCardArtRules, *aTabLog; int myAddTab(Tab *tab, QAction *manager = nullptr); void addCloseButtonToTab(Tab *tab, int tabIndex, QAction *manager); @@ -158,8 +152,6 @@ public: { return deckEditorTabs; } - [[nodiscard]] QList getGameInviteLinksForRoom(int roomId) const; - void sendInviteToUser(const QString &userName, const QString &inviteText); [[nodiscard]] bool getAdminLocked() const; void closeEvent(QCloseEvent *event) override; bool switchToGameTabIfAlreadyExists(const int gameId); @@ -169,7 +161,6 @@ signals: void localGameEnded(); void adminLockChanged(bool lock); void showWindowIfHidden(); - void cockatriceLinkActivated(const QString &url); public slots: void openDeckInNewTab(const LoadedDeck &deckToOpen); @@ -180,14 +171,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); private slots: void refreshShortcuts(); @@ -197,8 +185,6 @@ private slots: void actTabDeckStorage(bool checked); void actTabAdmin(bool checked); void actTabLog(bool checked); - void actTabReport(bool checked); - void actTabModeration(bool checked); void openTabVisualDeckStorage(); void openTabHome(); @@ -209,14 +195,13 @@ private slots: void actTabCardArtRules(bool checked); void openTabCardArtRules(); void openTabLog(); - void openTabReport(); 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); TabMessage *addMessageTab(const QString &userName, bool focus); void replayLeft(TabGame *tab); 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 209a30642..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 @@ -27,10 +27,10 @@ #include #include #include +#include #include #include #include -#include #include /** @@ -67,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, @@ -99,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() { 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 21335d2d0..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,15 +164,6 @@ public slots: * @return true if successful, false otherwise. */ bool actSaveDeckAs() override; - -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/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 16d5cfd13..000000000 --- a/cockatrice/src/interface/widgets/utility/completer_utils.cpp +++ /dev/null @@ -1,52 +0,0 @@ -#include "completer_utils.h" - -#include "card_completer_styler.h" - -#include -#include -#include -#include -#include -#include -#include -#include - -CardCompleterSetup createCardCompleter(CardDatabaseDisplayModel *displayModel, QObject *parent, int maxVisibleItems) -{ - auto *searchModel = new CardSearchModel(displayModel, parent); - - 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); - - 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/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 4a558a5e0..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 @@ -82,14 +82,17 @@ VisualDatabaseDisplayFilterToolbarWidget::VisualDatabaseDisplayFilterToolbarWidg 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 0cdf60d5d..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,7 +16,6 @@ #include #include -#include #include #include #include @@ -141,6 +140,9 @@ void VisualDatabaseDisplayWidget::initialize() { databaseLoadIndicator->setVisible(false); + filterContainer->initialize(); + filterContainer->setVisible(true); + searchContainer->addWidget(colorFilterWidget); searchContainer->addWidget(clearFilterWidget); searchContainer->addWidget(searchEdit); @@ -156,43 +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(); + retranslateUi(); } void VisualDatabaseDisplayWidget::retranslateUi() @@ -316,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 6e4d87876..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 @@ -115,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_editor_widget.cpp b/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_editor_widget.cpp index e3261b346..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 @@ -10,7 +10,6 @@ #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 @@ -22,6 +21,8 @@ #include #include #include +#include +#include #include #include #include @@ -29,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); @@ -96,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) { 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 fd529ff69..f1dcf113f 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,9 +1,9 @@ #include "deck_preview_color_identity_filter_widget.h" #include "../../cards/additional_info/mana_symbol_widget.h" -#include "../visual_deck_storage_widget.h" +#include "deck_preview_widget.h" -#include +#include DeckPreviewColorIdentityFilterWidget::DeckPreviewColorIdentityFilterWidget(VisualDeckStorageWidget *parent) : QWidget(parent), layout(new QHBoxLayout(this)) @@ -32,6 +32,10 @@ DeckPreviewColorIdentityFilterWidget::DeckPreviewColorIdentityFilterWidget(Visua // Connect the button's clicked signal connect(toggleButton, &QPushButton::clicked, 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(); @@ -41,33 +45,19 @@ void DeckPreviewColorIdentityFilterWidget::retranslateUi() { // Set the toggle button text based on the current mode switch (filterMode) { - case VisualDeckStorageSortFilterProxyModel::FilterMode::ExactMatch: + case ExactMatch: toggleButton->setText(tr("Mode: Exact Match")); break; - case VisualDeckStorageSortFilterProxyModel::FilterMode::Includes: + case Includes: toggleButton->setText(tr("Mode: Includes")); break; - case VisualDeckStorageSortFilterProxyModel::FilterMode::Excludes: + case Excludes: toggleButton->setText(tr("Mode: Excludes")); break; } 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; @@ -78,17 +68,88 @@ void DeckPreviewColorIdentityFilterWidget::updateFilterMode() { // Cycle through the modes switch (filterMode) { - case VisualDeckStorageSortFilterProxyModel::FilterMode::ExactMatch: - filterMode = VisualDeckStorageSortFilterProxyModel::FilterMode::Includes; + case ExactMatch: + filterMode = Includes; break; - case VisualDeckStorageSortFilterProxyModel::FilterMode::Includes: - filterMode = VisualDeckStorageSortFilterProxyModel::FilterMode::Excludes; + case Includes: + filterMode = Excludes; break; - case VisualDeckStorageSortFilterProxyModel::FilterMode::Excludes: - filterMode = VisualDeckStorageSortFilterProxyModel::FilterMode::ExactMatch; + case Excludes: + filterMode = ExactMatch; break; } retranslateUi(); // Update the button text emit filterModeChanged(filterMode); } + +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; + } + } + + // If no colors are active, return the unfiltered list of widgets + if (noColorsActive) { + for (DeckPreviewWidget *previewWidget : widgets) { + previewWidget->filteredByColor = false; + } + return; + } + + for (const auto &widget : widgets) { + QString colorIdentity = widget->getColorIdentity(); + + bool matchesFilter = true; + switch (filterMode) { + case ExactMatch: { + // 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; + } + break; + } + case Includes: + // 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; + } + } + break; + case Excludes: + // Excludes mode: colorIdentity must contain none of the active colors + for (auto it = activeColors.constBegin(); it != activeColors.constEnd(); ++it) { + if (it.value() && colorIdentity.contains(it.key())) { + matchesFilter = false; + break; + } + } + 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 def45de66..8e60b16fb 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,18 +2,18 @@ * @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 @@ -21,34 +21,25 @@ class DeckPreviewColorIdentityFilterWidget : public QWidget Q_OBJECT public: + /** + * How the active colors are matched against a deck's color identity. + */ + enum FilterMode + { + ExactMatch, ///< The color identity consists of exactly the active colors. + Includes, ///< The color identity contains all of the active colors. + Excludes ///< The color identity contains none of the active colors. + }; + Q_ENUM(FilterMode) + 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(FilterMode mode); 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(); @@ -57,7 +48,7 @@ private: QHBoxLayout *layout; QPushButton *toggleButton; QMap activeColors; - VisualDeckStorageSortFilterProxyModel::FilterMode filterMode = VisualDeckStorageSortFilterProxyModel::Includes; + FilterMode filterMode = Includes; // 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_widget.cpp b/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_widget.cpp index 04dcdf7f2..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,69 +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), visualDeckStorageWidget(_visualDeckStorageWidget), model(_model), filePath(_filePath) + : 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::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, @@ -78,29 +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}; } void DeckPreviewWidget::retranslateUi() @@ -114,15 +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); + QList widgets = findChildren(); + for (QWidget *widget : widgets) { + widget->setMaximumWidth(bannerCardDisplayWidget->width()); } } @@ -130,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()); } /** @@ -159,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) @@ -228,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. @@ -264,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())); } } @@ -285,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); @@ -302,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 { @@ -322,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)); } @@ -348,24 +385,17 @@ void DeckPreviewWidget::imageDoubleClickedEvent(QMouseEvent *event, DeckPreviewC 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("Open in deck editor")), &QAction::triggered, this, + [this] { emit openDeckEditor(deckLoader->getDeck()); }); connect(menu->addAction(tr("Edit Tags")), &QAction::triggered, deckTagsDisplayWidget, &DeckPreviewDeckTagsDisplayWidget::openTagEditDlg); @@ -378,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(); @@ -432,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); @@ -492,74 +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. -} - -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 -} - -/** - * 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() -{ - 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(this, filePath)) { - return false; - } - - model->convertToCockatriceFormat(row()); - return true; - } - - // Show the dialog to the user - DialogConvertDeckToCodFormat conversionDialog(this); - 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, filePath)) { - return false; - } - - model->convertToCockatriceFormat(row()); - - if (conversionDialog.dontAskAgain()) { - SettingsCache::instance().visualDeckStorage().setVisualDeckStoragePromptForConversion(false); - SettingsCache::instance().visualDeckStorage().setVisualDeckStorageAlwaysConvert(true); - } - - 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 7bb69f9b9..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,82 +20,72 @@ #include #include -class QEnterEvent; -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; + VisualDeckStorageWidget *visualDeckStorageWidget; + QVBoxLayout *layout; + QString filePath; + 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); 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; 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 imageDoubleClickedEvent(QMouseEvent *event, DeckPreviewCardPictureWidget *instance); + +private slots: + void setTags(const QStringList &tags); void actRenameDeck(); void actRenameFile(); void actDeleteFile(); - - VisualDeckStorageWidget *visualDeckStorageWidget; - VisualDeckStorageModel *model; - QString filePath; - 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. }; class NoScrollFilter : public QObject 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 fbaabf90f..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,285 +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(); - if (matches == deckPreviewWidget->isHidden()) { - deckPreviewWidget->setVisible(matches); - } - 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(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::createWidgetsForFiles() +{ + 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); + } + + 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 257ce1778..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,105 +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); -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 5d0006539..000000000 --- a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_model.cpp +++ /dev/null @@ -1,595 +0,0 @@ -#include "visual_deck_storage_model.h" - -#include "../../deck_loader/deck_loader.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 - -static QString computeColorIdentity(const LoadedDeck &deck); - -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 = computeColorIdentity(*deck); - 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 Computes the color identity of a deck in WUBRG order. - */ -static QString computeColorIdentity(const LoadedDeck &deck) -{ - QStringList cardList = deck.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) { - const QString colors = currentCard->getColors(); // 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; -} - -/** - * @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 = computeColorIdentity(data.deck); - } -} - -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 478431703..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 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 baa5e5792..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); @@ -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 c05da1cb3..000000000 --- a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_sort_filter_proxy_model.cpp +++ /dev/null @@ -1,292 +0,0 @@ -#include "visual_deck_storage_sort_filter_proxy_model.h" - -#include "../../filters/deck_filter_string.h" - -#include -#include - -VisualDeckStorageSortFilterProxyModel::VisualDeckStorageSortFilterProxyModel(QObject *parent) - : QSortFilterProxyModel(parent) -{ - setDynamicSortFilter(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; - } - - DeckFilterString filterString(searchText); - 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; - - bool matches = true; - switch (colorFilterMode) { - case ExactMatch: { - QSet activeColorSet; - for (const QChar &color : activeColors) { - activeColorSet.insert(color.toUpper()); - } - - QSet colorIdentitySet; - for (const QChar &color : colorIdentity) { - colorIdentitySet.insert(color.toUpper()); - } - - matches = activeColorSet == colorIdentitySet; - break; - } - case Includes: - matches = std::all_of(activeColors.begin(), activeColors.end(), - [&colorIdentity](const QChar &color) { return colorIdentity.contains(color); }); - break; - case Excludes: - matches = std::none_of(activeColors.begin(), activeColors.end(), - [&colorIdentity](const QChar &color) { return colorIdentity.contains(color); }); - break; - } - - colorMatches[row] = matches; - } -} - -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 7e771f6a9..000000000 --- a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_sort_filter_proxy_model.h +++ /dev/null @@ -1,105 +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. -}; - -#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 ba52cf8e9..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,8 +2,6 @@ #include "../general/layout_containers/flow_widget.h" #include "deck_preview/deck_preview_tag_display_widget.h" -#include "visual_deck_storage_model.h" -#include "visual_deck_storage_sort_filter_proxy_model.h" #include "visual_deck_storage_widget.h" #include @@ -20,7 +18,7 @@ VisualDeckStorageTagFilterWidget::VisualDeckStorageTagFilterWidget(VisualDeckSto 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); } @@ -31,26 +29,45 @@ void VisualDeckStorageTagFilterWidget::showEvent(QShowEvent *event) refreshTags(); } -/** - * @brief The tags of all decks currently accepted by the proxy model. - */ -QSet VisualDeckStorageTagFilterWidget::gatherAllTags() const +void VisualDeckStorageTagFilterWidget::filterDecksBySelectedTags(const QList &deckPreviews) const { - QSet allTags; - auto *proxy = parent->proxyModel(); + QStringList selectedTags; + QStringList excludedTags; - for (int proxyRow = 0; proxyRow < proxy->rowCount(); ++proxyRow) { - const QModelIndex index = proxy->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); + // 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; } } - return allTags; + // 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() @@ -63,6 +80,8 @@ void VisualDeckStorageTagFilterWidget::refreshTags() void VisualDeckStorageTagFilterWidget::removeTagsNotInList(const QSet &tags) { + auto *flowWidget = findChild(); + for (DeckPreviewTagDisplayWidget *tagWidget : findChildren()) { const QString &tagName = tagWidget->getTagName(); @@ -97,12 +116,20 @@ void VisualDeckStorageTagFilterWidget::addTagIfNotPresent(const QString &tag) 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; + } + // Get all tag widgets QList tagWidgets = findChildren(); @@ -120,26 +147,19 @@ void VisualDeckStorageTagFilterWidget::sortTags() } } -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 337c053c7..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,22 +2,21 @@ * @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 -class FlowWidget; class VisualDeckStorageWidget; class VisualDeckStorageTagFilterWidget : public QWidget { Q_OBJECT VisualDeckStorageWidget *parent; - FlowWidget *flowWidget; [[nodiscard]] QSet gatherAllTags() const; void removeTagsNotInList(const QSet &tags); @@ -28,21 +27,9 @@ class VisualDeckStorageTagFilterWidget : public QWidget public: explicit VisualDeckStorageTagFilterWidget(VisualDeckStorageWidget *_parent); [[nodiscard]] QStringList getAllKnownTags() const; - - /** - * @brief The tags currently in "selected" state. - */ - [[nodiscard]] QStringList selectedTags() const; - - /** - * @brief The tags currently in "excluded" state. - */ - [[nodiscard]] QStringList excludedTags() const; + void filterDecksBySelectedTags(const QList &deckPreviews) const; public slots: - /** - * @brief Rebuilds the tag chips from the tags of the currently visible decks. - */ 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 acb0dcab2..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 @@ -2,29 +2,24 @@ #include "../../../client/settings/cache_settings.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 -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); @@ -80,33 +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); - connect(CardDatabaseManager::getInstance(), &CardDatabase::cardDatabaseLoadingFinished, this, &VisualDeckStorageWidget::createRootFolderWidget); @@ -155,8 +123,6 @@ void VisualDeckStorageWidget::retranslateUi() refreshButton->setToolTip(tr("Refresh loaded files")); quickSettingsWidget->setToolTip(tr("Visual Deck Storage Settings")); - - sortWidget->retranslateUi(); } /** @@ -168,69 +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(); + if (folderWidget) { + tagFilterWidget->filterDecksBySelectedTags(folderWidget->findChildren()); + folderWidget->updateVisibility(); + } } -/** - * 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) @@ -246,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 fe6389414..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 @@ -38,43 +37,29 @@ public: 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); - -protected: void resizeEvent(QResizeEvent *event) override; void showEvent(QShowEvent *event) override; -private: - void reapplySortAndFilters(); +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; @@ -82,10 +67,9 @@ private: QToolButton *refreshButton; 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. + 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 4567991c8..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,17 +31,9 @@ #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/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 "version_string.h" #include "widgets/dialogs/dlg_connect.h" @@ -58,7 +49,6 @@ #include #include #include -#include #include #include #include @@ -87,7 +77,6 @@ #include #include #include -#include #include #define GITHUB_PAGES_URL "https://cockatrice.github.io" @@ -351,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")); @@ -413,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); @@ -493,8 +479,6 @@ void MainWindow::createMenus() helpMenu->addAction(aStatusBar); helpMenu->addAction(aViewLog); helpMenu->addAction(aOpenSettingsFolder); - helpMenu->addSeparator(); - helpMenu->addAction(aFirstRunWizard); } MainWindow::MainWindow(QWidget *parent) @@ -506,7 +490,6 @@ MainWindow::MainWindow(QWidget *parent) pixmapCacheSizeChanged(SettingsCache::instance().cacheStorage().getPixmapCacheSize()); connectionController = new ConnectionController(this, this); - urlParser = new IntentUrlParser(this, this); createActions(); createMenus(); @@ -518,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, @@ -543,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(); @@ -564,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() @@ -591,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); @@ -674,97 +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(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; - } - - 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(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) { @@ -867,8 +750,7 @@ void MainWindow::changeEvent(QEvent *event) connectionController->connectToServerDirect(connectTo.host(), connectTo.port(), connectTo.userName(), connectTo.password()); } else if (SettingsCache::instance().servers().getAutoConnect() && - !SettingsCache::instance().debug().getLocalGameOnStartup() && - !startupDestinationConnectsToServer()) { + !SettingsCache::instance().debug().getLocalGameOnStartup()) { qCInfo(WindowMainStartupAutoconnectLog) << "Attempting auto-connect..."; DlgConnect dlg(this); connectionController->connectToServerDirect(dlg.getHost(), static_cast(dlg.getPort()), @@ -894,11 +776,6 @@ void MainWindow::showWindowIfHidden() show(); } -void MainWindow::handleCockatriceLink(const QString &url) -{ - urlParser->handle(url); -} - void MainWindow::cardDatabaseLoadingFailed() { if (askedForDbUpdater) { @@ -1050,7 +927,6 @@ 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; } @@ -1064,9 +940,6 @@ void MainWindow::createCardUpdateProcess(bool background) void MainWindow::exitCardDatabaseUpdate() { - if (!cardUpdateProcess) { - return; - } cardUpdateProcess->deleteLater(); cardUpdateProcess = nullptr; statusBar()->clearMessage(); @@ -1104,17 +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) { - 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 920145552..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,24 +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); - public slots: void actCheckCardUpdates(); void actCheckCardUpdatesBackground(); @@ -92,7 +83,6 @@ private slots: void actOpenSettingsFolder(); void actShow(); void showWindowIfHidden(); - void handleCockatriceLink(const QString &url); void cardUpdateError(QProcess::ProcessError err); void cardUpdateFinished(int exitCode, QProcess::ExitStatus exitStatus); @@ -115,11 +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; - private: static const QString appName; static const QStringList fileNameFilters; @@ -129,9 +114,6 @@ private: void createTrayIcon(); int getNextCustomSetPrefix(QDir dataDir); - - void runFirstRunWizard(); - inline QString getCardUpdaterBinaryName() { return "oracle"; @@ -147,16 +129,13 @@ 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; QProcess *cardUpdateProcess; DlgViewLog *logviewDialog; diff --git a/cockatrice/src/main.cpp b/cockatrice/src/main.cpp index 84d5d175f..814da9808 100644 --- a/cockatrice/src/main.cpp +++ b/cockatrice/src/main.cpp @@ -390,7 +390,6 @@ int main(int argc, char *argv[]) PingPixmapGenerator::clear(); CountryPixmapGenerator::clear(); UserLevelPixmapGenerator::clear(); - ManaSymbolPixmapGenerator::clear(); return ret; } diff --git a/doc/doxygen/extra-pages/user_documentation/index.md b/doc/doxygen/extra-pages/user_documentation/index.md index 468a28f8d..d7e9d529d 100644 --- a/doc/doxygen/extra-pages/user_documentation/index.md +++ b/doc/doxygen/extra-pages/user_documentation/index.md @@ -11,11 +11,6 @@ - @subpage beta_release -## 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 78ba5586b..000000000 --- a/doc/doxygen/extra-pages/user_documentation/troubleshooting/fixing_card_pictures.md +++ /dev/null @@ -1,109 +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. - -# 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//` - -The following import naming schemes are recognized (using both `_` and `-` as separators): - -| 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. - -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 3fa435be1..83dee9e28 100755 --- a/format.sh +++ b/format.sh @@ -11,7 +11,6 @@ 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" \ @@ -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 081e6fd05..7d3d47eea 100644 --- a/libcockatrice_card/CMakeLists.txt +++ b/libcockatrice_card/CMakeLists.txt @@ -5,7 +5,6 @@ set(CMAKE_AUTORCC ON) set(HEADERS libcockatrice/card/card_info.h libcockatrice/card/card_info_comparator.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 @@ -27,7 +26,6 @@ add_library( ${MOC_SOURCES} libcockatrice/card/card_info.cpp libcockatrice/card/card_info_comparator.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 786e17950..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,33 +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) - : name(_name), text(_text), isToken(_isToken), properties(LazyPropertiesHash(_properties)), - relatedCards(_relatedCards), reverseRelatedCards(_reverseRelatedCards), setsToPrintings(std::move(_sets)), - 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(); @@ -53,7 +87,7 @@ 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, @@ -61,7 +95,7 @@ CardInfo::CardInfo(const QString &_name, QString _simpleName, QSet _altNames) : name(_name), simpleName(std::move(_simpleName)), text(_text), isToken(_isToken), - properties(LazyPropertiesHash(_propertiesBlob)), relatedCards(_relatedCards), + propertiesBlob(std::move(_propertiesBlob)), relatedCards(_relatedCards), reverseRelatedCards(_reverseRelatedCards), setsToPrintings(std::move(_sets)), uiAttributes(_uiAttributes), altNames(std::move(_altNames)) { @@ -79,14 +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) { - CardInfoPtr ptr( - new CardInfo(_name, _text, _isToken, _properties, _relatedCards, _reverseRelatedCards, _sets, _uiAttributes)); + CardInfoPtr ptr(new CardInfo(_name, _text, _isToken, std::move(_properties), _relatedCards, _reverseRelatedCards, + _sets, _uiAttributes)); ptr->setSmartPointer(ptr); for (const auto &printings : _sets) { @@ -169,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 392dc3849..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,8 +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. - 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. @@ -104,7 +114,7 @@ public: explicit CardInfo(const QString &_name, const QString &_text, bool _isToken, - const QHash &_properties, + QHash _properties, const QList &_relatedCards, const QList &_reverseRelatedCards, SetToPrintingsMap _sets, @@ -134,7 +144,7 @@ public: explicit CardInfo(const QString &_name, const QString &_text, bool _isToken, - const QByteArray &_propertiesBlob, + QByteArray _propertiesBlob, const QList &_relatedCards, const QList &_reverseRelatedCards, SetToPrintingsMap _sets, @@ -151,7 +161,7 @@ public: */ CardInfo(const CardInfo &other) : QObject(other.parent()), name(other.name), simpleName(other.simpleName), text(other.text), - isToken(other.isToken), 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) @@ -184,7 +194,7 @@ public: static CardInfoPtr newInstance(const QString &_name, const QString &_text, bool _isToken, - const QHash &_properties, + QHash _properties, const QList &_relatedCards, const QList &_reverseRelatedCards, SetToPrintingsMap _sets, diff --git a/libcockatrice_card/libcockatrice/card/database/card_database_cache.cpp b/libcockatrice_card/libcockatrice/card/database/card_database_cache.cpp index 2b27f50f8..3985889b7 100644 --- a/libcockatrice_card/libcockatrice/card/database/card_database_cache.cpp +++ b/libcockatrice_card/libcockatrice/card/database/card_database_cache.cpp @@ -119,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 --------------------------------------------------------------- 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 ec460d685..8649bbfaf 100644 --- a/libcockatrice_card/libcockatrice/card/database/parser/cockatrice_xml_4.cpp +++ b/libcockatrice_card/libcockatrice/card/database/parser/cockatrice_xml_4.cpp @@ -322,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 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 c7a54a390..33494b9a9 100644 --- a/libcockatrice_deck_list/CMakeLists.txt +++ b/libcockatrice_deck_list/CMakeLists.txt @@ -11,7 +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/playmat_resolver.h libcockatrice/deck_list/sideboard_plan.h ) @@ -27,7 +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/playmat_resolver.cpp libcockatrice/deck_list/sideboard_plan.cpp ) @@ -35,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 af8628d21..d9d2450de 100644 --- a/libcockatrice_deck_list/libcockatrice/deck_list/deck_list.cpp +++ b/libcockatrice_deck_list/libcockatrice/deck_list/deck_list.cpp @@ -17,7 +17,7 @@ static const QString CURRENT_SIDEBOARD_PLAN_KEY = ""; bool DeckList::Metadata::isEmpty() const { - return name.isEmpty() && comments.isEmpty() && bannerCard.isEmpty() && tags.isEmpty() && playmat.card.isEmpty(); + return name.isEmpty() && comments.isEmpty() && bannerCard.isEmpty() && tags.isEmpty(); } DeckList::DeckList() @@ -66,36 +66,6 @@ bool DeckList::readElement(QXmlStreamReader *xml) QString providerId = xml->attributes().value("providerId").toString(); QString cardName = xml->readElementText(); metadata.bannerCard = {cardName, providerId}; - } else if (childName == "playmatCard") { - QString providerId = xml->attributes().value("providerId").toString(); - bool ok; - 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(); - PlaymatInfo playmat; - playmat.card = {cardName, providerId}; - // Clamp 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). - playmat.params.marginPctL = qBound(0.0, marginLStr.toDouble(&ok), 0.95); - if (!ok) { - playmat.params.marginPctL = 0.07; - } - playmat.params.marginPctR = qBound(0.0, marginRStr.toDouble(&ok), 0.95); - if (!ok) { - playmat.params.marginPctR = 0.07; - } - playmat.params.verticalOffset = qBound(0.0, vOffStr.toDouble(&ok), 1.0); - if (!ok) { - playmat.params.verticalOffset = 0.33; - } - playmat.params.zoom = qBound(0.1, zoomStr.toDouble(&ok), 4.0); - if (!ok) { - playmat.params.zoom = 1.0; - } - metadata.playmat = playmat; } else if (childName == "tags") { metadata.tags.clear(); // Clear existing tags while (xml->readNextStartElement()) { @@ -126,16 +96,6 @@ static void writeMetadata(QXmlStreamWriter *xml, const DeckList::Metadata &metad xml->writeAttribute("providerId", metadata.bannerCard.providerId); xml->writeCharacters(metadata.bannerCard.name); xml->writeEndElement(); - if (!metadata.playmat.card.isEmpty()) { - xml->writeStartElement("playmatCard"); - xml->writeAttribute("providerId", metadata.playmat.card.providerId); - xml->writeAttribute("marginPctL", QString::number(metadata.playmat.params.marginPctL, 'f', 4)); - xml->writeAttribute("marginPctR", QString::number(metadata.playmat.params.marginPctR, 'f', 4)); - xml->writeAttribute("verticalOffset", QString::number(metadata.playmat.params.verticalOffset, 'f', 4)); - xml->writeAttribute("zoom", QString::number(metadata.playmat.params.zoom, 'f', 4)); - xml->writeCharacters(metadata.playmat.card.name); - xml->writeEndElement(); - } xml->writeTextElement("comments", metadata.comments); // Write tags @@ -405,10 +365,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 475d99560..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,7 +70,6 @@ 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. @@ -116,10 +115,6 @@ public: { metadata.bannerCard = _bannerCard; } - void setPlaymat(const PlaymatInfo &_playmat = {}) - { - metadata.playmat = _playmat; - } void setLastLoadedTimestamp(const QString &_lastLoadedTimestamp = QString()) { metadata.lastLoadedTimestamp = _lastLoadedTimestamp; @@ -175,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_node_tree.cpp b/libcockatrice_deck_list/libcockatrice/deck_list/deck_list_node_tree.cpp index 21f628f9d..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 @@ -41,19 +41,13 @@ QList DecklistNodeTree::getCardNodes(const QSet result; - std::function collectCards = [&collectCards, - &result](const InnerDecklistNode *node) { - 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))) { - collectCards(inner); + for (auto *zoneNode : getZoneNodes(restrictToZones)) { + for (auto *cardNode : *zoneNode) { + auto *cardCardNode = dynamic_cast(cardNode); + if (cardCardNode) { + result.append(cardCardNode); } } - }; - - for (auto *zoneNode : getZoneNodes(restrictToZones)) { - collectCards(zoneNode); } return result; @@ -145,8 +139,7 @@ bool DecklistNodeTree::deleteNode(AbstractDecklistNode *node, InnerDecklistNode if (index != -1) { delete rootNode->takeAt(index); - // Empty custom zones are kept while empty board zones get pruned. - if (rootNode->empty() && rootNode->getParent() == root) { + if (rootNode->empty()) { deleteNode(rootNode, rootNode->getParent()); } @@ -167,179 +160,28 @@ bool DecklistNodeTree::deleteNode(AbstractDecklistNode *node, InnerDecklistNode void DecklistNodeTree::forEachCard(const std::function &func) const { - // Cards nested in custom zones are reported with their top-level board zone - // so that callers can classify cards by board (main/side/maybeboard/tokens). - std::function walk = [&func, &walk](InnerDecklistNode *boardZone, - InnerDecklistNode *node) { - 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))) { - walk(boardZone, inner); - } - } - }; - + // 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))) { - walk(zone, zone); + 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) 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 1012d5919..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 @@ -78,57 +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 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) const; - InnerDecklistNode *findBoardZone(const QString &boardZoneName) const; - InnerDecklistNode *findOrCreateBoardZone(const QString &boardZoneName); - InnerDecklistNode *findCustomZoneByName(const QString &zoneName) const; - bool hasZoneName(const QString &zoneName) const; }; #endif // COCKATRICE_DECKLIST_NODE_TREE_H 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/tree/abstract_deck_list_node.h b/libcockatrice_deck_list/libcockatrice/deck_list/tree/abstract_deck_list_node.h index c5cb25d8f..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.). 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 1f470695d..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; } 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 f8fdedf30..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 @@ -24,8 +24,6 @@ #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 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 3f2cbbe8e..3ee3d2aef 100644 --- a/libcockatrice_interfaces/libcockatrice/interfaces/interface_cards_display_settings_provider.h +++ b/libcockatrice_interfaces/libcockatrice/interfaces/interface_cards_display_settings_provider.h @@ -15,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; diff --git a/libcockatrice_interfaces/libcockatrice/interfaces/interface_interface_settings_provider.h b/libcockatrice_interfaces/libcockatrice/interfaces/interface_interface_settings_provider.h index bc2118cb3..ab2caa0d7 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 { @@ -72,24 +39,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 a81616cb0..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,8 +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; }; #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 621f28983..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,62 +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; } - const QString lowerName = card->getName().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}); - } + int distance = levenshteinDistance(query.toLower(), card->getName().toLower()); + searchResults.append({card, distance}); } - auto sortByDistanceThenLength = [](const SearchResult &a, const SearchResult &b) { - if (a.distance != b.distance) { - return a.distance < b.distance; - } - return a.card->getName().size() < b.card->getName().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(); } 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 646bf7e61..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; 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_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 d6316deb3..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; diff --git a/libcockatrice_network/libcockatrice/network/client/abstract/abstract_client.h b/libcockatrice_network/libcockatrice/network/client/abstract/abstract_client.h index 1ef9a31e4..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; 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 957a89792..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 @@ -1631,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()); } @@ -1653,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_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 43209e994..4761199e5 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/game/server_game.cpp +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_game.cpp @@ -53,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); @@ -331,11 +343,6 @@ void Server_Game::doStartGameIfReady(bool forceStartGame) } 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(); } @@ -393,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) { @@ -414,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; @@ -531,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); } @@ -590,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); } @@ -846,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()); @@ -906,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 1b9f651bd..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 { @@ -218,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 0ded27afa..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(); 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 899df6529..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; @@ -564,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; @@ -586,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)); @@ -616,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; } @@ -924,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_protocol/libcockatrice/protocol/pb/CMakeLists.txt b/libcockatrice_protocol/libcockatrice/protocol/pb/CMakeLists.txt index 3a193ae3c..20a4cb08d 100644 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/CMakeLists.txt +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/CMakeLists.txt @@ -35,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 @@ -56,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 @@ -148,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 @@ -176,17 +154,13 @@ set(PROTO_FILES 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 f8b34b3f8..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; } @@ -38,10 +37,3 @@ message Command_AdjustMod { optional bool should_be_mod = 2; optional bool should_be_judge = 3; } - -message Command_ResetUserPassword { - extend AdminCommand { - optional Command_ResetUserPassword ext = 1016; - } - optional string user_name = 1; -} 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/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 685408830..ca46e4dd7 100644 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/moderator_commands.proto +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/moderator_commands.proto @@ -14,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; } @@ -146,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 14ba737b5..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 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_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_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 fee8c36a8..9d207c711 100644 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/session_commands.proto +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/session_commands.proto @@ -34,11 +34,6 @@ message SessionCommand { 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_settings/libcockatrice/settings/appearance_settings.cpp b/libcockatrice_settings/libcockatrice/settings/appearance_settings.cpp index 2f19d6224..45a02299e 100644 --- a/libcockatrice_settings/libcockatrice/settings/appearance_settings.cpp +++ b/libcockatrice_settings/libcockatrice/settings/appearance_settings.cpp @@ -69,14 +69,3 @@ void AppearanceSettings::setHomeTabDisplayCardName(bool _displayCardName) setValue(_displayCardName, "homeTabDisplayCardName"); emit homeTabDisplayCardNameChanged(); } - -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 3a63f0df0..d9b326bee 100644 --- a/libcockatrice_settings/libcockatrice/settings/appearance_settings.h +++ b/libcockatrice_settings/libcockatrice/settings/appearance_settings.h @@ -27,8 +27,6 @@ public: void setHomeTabBackgroundShuffleFrequency(int _frequency); [[nodiscard]] bool getHomeTabDisplayCardName() const; void setHomeTabDisplayCardName(bool _displayCardName); - [[nodiscard]] int getHomeTabButtonColorSourceIndex() const; - void setHomeTabButtonColorSourceIndex(int index); signals: void themeNameChanged(); @@ -36,7 +34,6 @@ signals: void homeTabBackgroundSourceChanged(); void homeTabBackgroundShuffleFrequencyChanged(); void homeTabDisplayCardNameChanged(); - 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 f528a7c4b..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(); @@ -164,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"); diff --git a/libcockatrice_settings/libcockatrice/settings/cards_display_settings.h b/libcockatrice_settings/libcockatrice/settings/cards_display_settings.h index dbafa32ae..15a3e3ff4 100644 --- a/libcockatrice_settings/libcockatrice/settings/cards_display_settings.h +++ b/libcockatrice_settings/libcockatrice/settings/cards_display_settings.h @@ -20,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; @@ -41,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); 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/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..29c57c57e 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) { @@ -190,51 +160,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"); @@ -401,60 +326,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..982976310 100644 --- a/libcockatrice_settings/libcockatrice/settings/interface_settings.h +++ b/libcockatrice_settings/libcockatrice/settings/interface_settings.h @@ -42,13 +42,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); @@ -81,13 +74,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 +85,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/tabs_settings.cpp b/libcockatrice_settings/libcockatrice/settings/tabs_settings.cpp index 85a1424a6..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,52 +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(); -} - void TabsSettings::setTabVisualDeckStorageOpen(bool value) { setValue(value, "visualDeckStorage"); @@ -140,13 +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"); -} diff --git a/libcockatrice_settings/libcockatrice/settings/tabs_settings.h b/libcockatrice_settings/libcockatrice/settings/tabs_settings.h index 365d91af7..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,13 +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; - 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); @@ -55,14 +26,6 @@ public: void setTabReplaysOpen(bool value); void setTabAdminOpen(bool value); void setTabLogOpen(bool value); - void setTabReportOpen(bool value); - void setTabModerationOpen(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_utility/CMakeLists.txt b/libcockatrice_utility/CMakeLists.txt index c6411ea76..79f5a11e4 100644 --- a/libcockatrice_utility/CMakeLists.txt +++ b/libcockatrice_utility/CMakeLists.txt @@ -5,19 +5,16 @@ set(CMAKE_AUTOMOC ON) set(CMAKE_AUTOUIC ON) set(CMAKE_AUTORCC ON) -set(UTILITY_SOURCES - 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/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 @@ -25,7 +22,6 @@ 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}) 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/src/oracleimporter.cpp b/oracle/src/oracleimporter.cpp index fdb32bb8d..a9008c4da 100644 --- a/oracle/src/oracleimporter.cpp +++ b/oracle/src/oracleimporter.cpp @@ -237,11 +237,8 @@ int OracleImporter::importCardsFromSet(const CardSetPtr ¤tSet, const QList }; // 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"}}; @@ -319,7 +316,7 @@ int OracleImporter::importCardsFromSet(const CardSetPtr ¤tSet, const QList } } - PrintingInfo printingInfo(currentSet, LazyPropertiesHash(printingProps)); + PrintingInfo printingInfo(currentSet, printingProps); QString numComponent; const QString numProperty = printingInfo.getProperty("num"); 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/servatrice.ini.example b/servatrice/servatrice.ini.example index c1940c22f..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,13 +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 - [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. diff --git a/servatrice/servatrice.sql b/servatrice/servatrice.sql index 5dbf69cbc..7f530063c 100644 --- a/servatrice/servatrice.sql +++ b/servatrice/servatrice.sql @@ -20,7 +20,7 @@ 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(36); +INSERT INTO cockatrice_schema_version VALUES(35); -- users and user data tables CREATE TABLE IF NOT EXISTS `cockatrice_users` ( @@ -41,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, @@ -234,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/servatrice.cpp b/servatrice/src/servatrice.cpp index db8751658..aa50e068a 100644 --- a/servatrice/src/servatrice.cpp +++ b/servatrice/src/servatrice.cpp @@ -514,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; @@ -690,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; @@ -719,7 +683,6 @@ void Servatrice::scheduleShutdown(const QString &reason, int minutes) connect(shutdownTimer, SIGNAL(timeout()), this, SLOT(shutdownTimeout())); shutdownTimer->start(60000); } - shutdownStateMutex.unlock(); shutdownTimeout(); } @@ -739,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) { @@ -748,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); diff --git a/servatrice/src/servatrice.h b/servatrice/src/servatrice.h index 8b0f5ad60..62fb382cb 100644 --- a/servatrice/src/servatrice.h +++ b/servatrice/src/servatrice.h @@ -20,7 +20,6 @@ #ifndef SERVATRICE_H #define SERVATRICE_H -#include #include #include #include @@ -30,8 +29,6 @@ #include #include #include -#include -#include #include #include @@ -161,7 +158,6 @@ private: Servatrice_IslServer *islServer; mutable QMutex loginMessageMutex; QString loginMessage; - mutable QMutex shutdownStateMutex; QString dbPrefix; QMap serverRequiredFeatureList; QString officialWarnings; @@ -176,11 +172,6 @@ private: 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(); @@ -225,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; @@ -291,11 +280,6 @@ public: 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 847be61da..d5e1f13ef 100644 --- a/servatrice/src/servatrice_database_interface.cpp +++ b/servatrice/src/servatrice_database_interface.cpp @@ -14,7 +14,6 @@ #include #include #include -#include #include inline Q_LOGGING_CATEGORY(DatabaseInterfaceLog, "database_interface"); @@ -340,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"; @@ -351,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; @@ -363,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 { @@ -1090,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, @@ -1331,169 +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 & 7) <> 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; - } - loginDetails.set_user_level(userLevel); - - if (!query->value(2).isNull()) { - loginDetails.set_last_login(query->value(2).toLongLong()); - } - results << loginDetails; - } - - return results; -} - -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 cd76ae288..1e3501ec7 100644 --- a/servatrice/src/servatrice_database_interface.h +++ b/servatrice/src/servatrice_database_interface.h @@ -6,14 +6,11 @@ #include #include #include -#include -#include -#include #include #include #include -#define DATABASE_SCHEMA_VERSION 36 +#define DATABASE_SCHEMA_VERSION 35 class Servatrice; @@ -122,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); @@ -137,10 +133,6 @@ public: bool &room, int &range, int &maxresults); - QList getUserSessions(const QString &userName, int limit); - QList getUserAlts(const QString &userName); - QList getModeratorLastLogins(); - 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 2a8b5f0a4..842ddb4c8 100644 --- a/servatrice/src/serversocketinterface.cpp +++ b/servatrice/src/serversocketinterface.cpp @@ -49,20 +49,10 @@ #include #include #include -#include #include #include #include #include -#include -#include -#include -#include -#include -#include -#include -#include -#include #include #include #include @@ -80,36 +70,20 @@ #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 @@ -249,14 +223,6 @@ Response::ResponseCode AbstractServerSocketInterface::processExtendedSessionComm 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; } @@ -277,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); 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: @@ -301,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; } @@ -330,8 +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; } @@ -952,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("-"); @@ -1123,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()); @@ -1310,1010 +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::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) { @@ -2846,8 +1781,6 @@ Response::ResponseCode AbstractServerSocketInterface::cmdAccountPassword(const C return Response::RespWrongPassword; } - databaseInterface->setForcePasswordChange(userName, false); - return Response::RespOk; } @@ -2984,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; } @@ -3053,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. diff --git a/servatrice/src/serversocketinterface.h b/servatrice/src/serversocketinterface.h index 600796b5f..0d66ae78f 100644 --- a/servatrice/src/serversocketinterface.h +++ b/servatrice/src/serversocketinterface.h @@ -24,16 +24,6 @@ #include #include #include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include #include class Servatrice; @@ -60,7 +50,6 @@ class Command_BanFromServer; class Command_UpdateServerMessage; class Command_ShutdownServer; class Command_ReloadConfig; -class Command_ReplayDownloadByGameId; class Command_AccountEdit; class Command_AccountImage; @@ -78,7 +67,7 @@ signals: void incTxBytes(qint64 amount); protected: - void logDebugMessage(const QString &message) override; + void logDebugMessage(const QString &message); bool tooManyRegistrationAttempts(const QString &ipAddress); virtual void writeToSocket(QByteArray &data) = 0; @@ -113,7 +102,6 @@ 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); Response::ResponseCode cmdGetBanHistory(const Command_GetBanHistory &cmd, ResponseContainer &rc); @@ -121,10 +109,6 @@ private: 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*/); @@ -138,19 +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; + 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); @@ -166,12 +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); - bool addAdminFlagToUser(const QString &user, int flag); bool removeAdminFlagFromUser(const QString &user, int flag); @@ -188,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 a28f671c9..1c3f4c2c6 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -7,13 +7,9 @@ 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 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 deck_hash_performance_test COMMAND deck_hash_performance_test) set_tests_properties(deck_hash_performance_test PROPERTIES TIMEOUT 5) @@ -25,15 +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(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) find_package(GTest) @@ -65,14 +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(warning_categories_test gtest) - add_dependencies(lag_monitor_test gtest) - add_dependencies(latency_tracker_test gtest) endif() include_directories(${GTEST_INCLUDE_DIRS}) @@ -87,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} @@ -104,17 +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( - 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} -) add_subdirectory(card_zone_algorithms) add_subdirectory(carddatabase) -add_subdirectory(deck_list_zones) add_subdirectory(loading_from_clipboard) add_subdirectory(movecard_tests) add_subdirectory(oracle) 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 a5148621d..000000000 --- a/tests/deck_list_zones/deck_list_zones_test.cpp +++ /dev/null @@ -1,356 +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, 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/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/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/settings/settings_defaults_test.cpp b/tests/settings/settings_defaults_test.cpp index 6c79d5227..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,12 +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); -} - // --- ChatSettings --- TEST_F(SettingsDefaultsTest, Chat_Mention_Default) @@ -438,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) @@ -531,12 +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); -} - // --- VisualDeckStorageSettings --- TEST_F(SettingsDefaultsTest, VisualDeckStorage_SortingOrder_Default) 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(); -}