diff --git a/.ci/lint_cpp.sh b/.ci/lint_cpp.sh index cfb1e1f07..9786a83fc 100755 --- a/.ci/lint_cpp.sh +++ b/.ci/lint_cpp.sh @@ -13,17 +13,9 @@ fi # Check formatting using format.sh echo "Checking your code using format.sh..." -diff="$(./format.sh --diff --cmake --shell --print-version --branch origin/master)" +./format.sh --color-diff --cmake --shell --print-version --branch origin/master err=$? -sep=" ----------- -" -used_version="${diff%%"$sep"*}" -diff="${diff#*"$sep"}" -changes_to_make="${diff%%"$sep"*}" -files_to_edit="${diff#*"$sep"}" - case $err in 1) cat <>"$GITHUB_OUTPUT" - else # push to branch - sha="$GITHUB_SHA" + if [[ "$GITHUB_REF_TYPE" == 'tag' ]]; then # release + echo "tag=$GITHUB_REF_NAME" >> "$GITHUB_OUTPUT" fi - echo "sha=$sha" >>"$GITHUB_OUTPUT" + echo "sha=$RESOLVED_SHA" >> "$GITHUB_OUTPUT" - - name: Checkout + - name: "Checkout" if: steps.configure.outputs.tag != null - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: - fetch-depth: 0 + fetch-depth: 0 # fetch all history for all branches and tags - - name: Prepare release parameters + - name: "Prepare release parameters" id: prepare if: steps.configure.outputs.tag != null shell: bash env: - TAG: ${{steps.configure.outputs.tag}} + TAG: ${{ steps.configure.outputs.tag }} run: .ci/prep_release.sh - - name: Create release + - name: "Create release" if: steps.configure.outputs.tag != null id: create_release shell: bash env: - GH_TOKEN: ${{github.token}} - tag_name: ${{steps.configure.outputs.tag}} - target: ${{steps.configure.outputs.sha}} - release_name: ${{steps.prepare.outputs.title}} - body_path: ${{steps.prepare.outputs.body_path}} - prerelease: ${{steps.prepare.outputs.is_beta}} + GH_TOKEN: ${{ github.token }} + tag_name: ${{ steps.configure.outputs.tag }} + target: ${{ steps.configure.outputs.sha }} + release_name: ${{ steps.prepare.outputs.title }} + body_path: ${{ steps.prepare.outputs.body_path }} + prerelease: ${{ steps.prepare.outputs.is_beta }} run: | - if [[ $prerelease == yes ]]; then - args="--prerelease" - fi - gh release create "$tag_name" --draft --verify-tag $args \ - --target "$target" --title "$release_name" \ - --notes-file "$body_path" + args=() + [[ $prerelease == yes ]] && args+=(--prerelease) + + gh release create "$tag_name" --verify-tag --draft "${args[@]}" \ + --target "$target" \ + --title "$release_name" \ + --notes-file "$body_path" build-linux: strategy: fail-fast: false matrix: - # These names correspond to the files in ".ci/$distro$version" + # The files in ".ci/$distro$version" correspond to the values given here include: - distro: Arch - package: skip # We are packaged in Arch already + allow-failure: yes + package: skip # We are packaged in Arch already - distro: Servatrice_Debian version: 12 + package: DEB - test: skip server_only: yes + test: skip - distro: Debian version: 12 + package: DEB test: skip # Running tests on all distros is superfluous - distro: Debian version: 13 + package: DEB - distro: Fedora version: 43 + package: RPM test: skip # Running tests on all distros is superfluous - distro: Fedora version: 44 + package: RPM - distro: Ubuntu version: 24.04 + package: DEB test: skip # Running tests on all distros is superfluous - distro: Ubuntu version: 26.04 + package: DEB - name: ${{matrix.distro}} ${{matrix.version}} + name: ${{ matrix.distro }} ${{ matrix.version }} needs: configure runs-on: ubuntu-latest - continue-on-error: ${{matrix.allow-failure == 'yes'}} + continue-on-error: ${{ matrix.allow-failure == 'yes' }} timeout-minutes: 70 env: - NAME: ${{matrix.distro}}${{matrix.version}} - CACHE: ${{github.workspace}}/.cache/${{matrix.distro}}${{matrix.version}} # directory for caching docker image and ccache - # Cache size over the entire repo is 10Gi: - # https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#usage-limits-and-eviction-policy - CCACHE_SIZE: 550M + CACHE: ${{ github.workspace }}/.cache/${{ matrix.distro }}${{ matrix.version }} # directory for caching docker image and ccache CCACHE_EVICTION_AGE: 7d + CCACHE_SIZE: 550M # space of all repo is 10Gi: https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#usage-limits-and-eviction-policy CMAKE_GENERATOR: 'Ninja' + NAME: ${{ matrix.distro }}${{ matrix.version }} steps: - - name: Checkout - uses: actions/checkout@v6 + - name: "Checkout" + uses: actions/checkout@v7 - - name: Restore compiler cache (ccache) + - name: "Restore compiler cache (ccache)" id: ccache_restore - uses: actions/cache/restore@v5 + uses: actions/cache/restore@v6 env: BRANCH_NAME: ${{ github.head_ref || github.ref_name }} with: - path: ${{env.CACHE}} - key: ccache-${{matrix.distro}}${{matrix.version}}-${{env.BRANCH_NAME}} - restore-keys: 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 + - name: "Build ${{ matrix.distro }} ${{ matrix.version }} Docker image" shell: bash run: source .ci/docker.sh --build - - name: Build debug and test + - name: "Build debug and test" if: matrix.test != 'skip' shell: bash run: | source .ci/docker.sh RUN --server --debug --test --ccache "$CCACHE_SIZE" \ - --cmake-generator "$CMAKE_GENERATOR" + --cmake-generator "$CMAKE_GENERATOR" - - name: Build release package + - name: "Build release package" id: build if: matrix.package != 'skip' shell: bash env: - SUFFIX: '-${{matrix.distro}}${{matrix.version}}' - package: '${{matrix.package}}' - server_only: '${{matrix.server_only}}' + SUFFIX: '-${{ matrix.distro }}${{ matrix.version }}' + package: '${{ matrix.package }}' + server_only: '${{ matrix.server_only }}' run: | source .ci/docker.sh args=() - if [[ $server_only == yes ]]; then - args+=(--no-client) - fi - if [[ $GITHUB_REF == "refs/heads/master" ]]; then - args+=(--evict-ccache "$CCACHE_EVICTION_AGE") - fi + [[ $server_only == yes ]] && args+=(--no-client) + [[ $GITHUB_REF == "refs/heads/master" ]] && args+=(--evict-ccache "$CCACHE_EVICTION_AGE") args+=(--ccache "$CCACHE_SIZE") args+=(--cmake-generator "$CMAKE_GENERATOR") args+=(--suffix "$SUFFIX") + RUN --server --release --package "$package" "${args[@]}" # Delete used cache to emulate a ccache update. See https://github.com/actions/cache/issues/342 - - name: Delete remote compiler cache (ccache) + - name: "Delete remote compiler cache (ccache)" if: github.ref == 'refs/heads/master' && steps.ccache_restore.outputs.cache-hit continue-on-error: true env: + CACHE_PRIMARY_KEY: ${{ steps.ccache_restore.outputs.cache-primary-key }} GH_TOKEN: ${{ github.token }} run: | - if gh cache delete --repo ${{ github.repository }} ${{ steps.ccache_restore.outputs.cache-primary-key }}; then + if gh cache delete --repo "$GITHUB_REPOSITORY" "$CACHE_PRIMARY_KEY"; then echo "Cache deleted successfully" fi - - name: Save updated compiler cache (ccache) + - name: "Save updated compiler cache (ccache)" if: github.ref == 'refs/heads/master' - uses: actions/cache/save@v5 + uses: actions/cache/save@v6 with: - path: ${{env.CACHE}} key: ${{ steps.ccache_restore.outputs.cache-primary-key }} + path: ${{ env.CACHE }} - - name: Upload artifact + - name: "Upload artifact" id: upload_artifact if: matrix.package != 'skip' uses: actions/upload-artifact@v7 with: - path: ${{steps.build.outputs.path}} archive: false if-no-files-found: error + path: ${{ steps.build.outputs.path }} - - name: Upload to release + - name: "Upload to release" id: upload_release if: matrix.package != 'skip' && needs.configure.outputs.tag != null shell: bash env: - GH_TOKEN: ${{github.token}} - tag_name: ${{needs.configure.outputs.tag}} - asset_name: ${{steps.build.outputs.fullname}} - asset_path: ${{steps.build.outputs.path}} + asset_name: ${{ steps.build.outputs.fullname }} + asset_path: ${{ steps.build.outputs.path }} + GH_TOKEN: ${{ github.token }} + tag_name: ${{ needs.configure.outputs.tag }} run: gh release upload "$tag_name" "$asset_path#$asset_name" - - name: Attest binary provenance + - name: "Attest binary provenance" id: attestation if: steps.upload_release.outcome == 'success' uses: actions/attest@v4 with: - subject-path: ${{steps.build.outputs.path}} show-summary: false + subject-path: ${{ steps.build.outputs.path }} - - name: Verify binary attestation + - name: "Verify binary attestation" if: steps.attestation.outcome == 'success' shell: bash env: - GH_TOKEN: ${{github.token}} - run: gh attestation verify "${{steps.build.outputs.path}}" --repo Cockatrice/Cockatrice + BUILD_PATH: ${{ steps.build.outputs.path }} + GH_TOKEN: ${{ github.token }} + run: gh attestation verify "$BUILD_PATH" --repo Cockatrice/Cockatrice build-vcpkg: strategy: @@ -263,234 +264,241 @@ jobs: - os: macOS target: 13 runner: macos-15-intel - soc: Intel - xcode: "16.4" - type: Release - override_target: 13 + + ccache_eviction_age: 7d + cmake_generator: Ninja make_package: 1 + override_target: 13 package_suffix: "-macOS13_Intel" qt_version: 6.11.0 qt_arch: clang_64 qt_modules: qtimageformats qtmultimedia qtwebsockets - cmake_generator: Ninja + soc: Intel + type: Release use_ccache: 1 - ccache_eviction_age: 7d + xcode: "16.4" - os: macOS target: 14 runner: macos-14 - soc: Apple - xcode: "15.4" - type: Release + + ccache_eviction_age: 7d + cmake_generator: Ninja make_package: 1 package_suffix: "-macOS14" qt_version: 6.11.0 qt_arch: clang_64 qt_modules: qtimageformats qtmultimedia qtwebsockets - cmake_generator: Ninja + soc: Apple + type: Release use_ccache: 1 - ccache_eviction_age: 7d + xcode: "15.4" - os: macOS target: 15 runner: macos-15 - soc: Apple - xcode: "16.4" - type: Release + + ccache_eviction_age: 7d + cmake_generator: Ninja make_package: 1 package_suffix: "-macOS15" qt_version: 6.11.0 qt_arch: clang_64 qt_modules: qtimageformats qtmultimedia qtwebsockets - cmake_generator: Ninja + soc: Apple + type: Release use_ccache: 1 - ccache_eviction_age: 7d + xcode: "16.4" - os: macOS target: 15 runner: macos-15 - soc: Apple - xcode: "16.4" - type: Debug + + ccache_eviction_age: 7d + cmake_generator: Ninja qt_version: 6.11.0 qt_arch: clang_64 qt_modules: qtimageformats qtmultimedia qtwebsockets - cmake_generator: Ninja + soc: Apple + type: Debug use_ccache: 1 - ccache_eviction_age: 7d + xcode: "16.4" - os: Windows target: 10 runner: windows-2025 - type: Release + + cmake_generator: "Visual Studio 18 2026" + cmake_generator_platform: x64 make_package: 1 package_suffix: "-Win10" qt_version: 6.11.0 qt_arch: win64_msvc2022_64 qt_modules: qtimageformats qtmultimedia qtwebsockets - cmake_generator: "Visual Studio 17 2022" - cmake_generator_platform: x64 + type: Release - name: ${{matrix.os}} ${{matrix.target}}${{ matrix.soc == 'Intel' && ' Intel' || '' }}${{ matrix.type == 'Debug' && ' Debug' || '' }} + name: ${{ matrix.os }} ${{ matrix.target }}${{ matrix.soc == 'Intel' && ' Intel' || '' }}${{ matrix.type == 'Debug' && ' Debug' || '' }} needs: configure - runs-on: ${{matrix.runner}} + runs-on: ${{ matrix.runner }} timeout-minutes: 100 env: - CCACHE_DIR: ${{github.workspace}}/.cache/ - # Cache size over the entire repo is 10Gi: - # https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#usage-limits-and-eviction-policy - CCACHE_SIZE: 550M + CCACHE_DIR: ${{ github.workspace }}/.cache/ + CCACHE_SIZE: 550M # space of all repo is 10Gi: https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#usage-limits-and-eviction-policy steps: - - name: Checkout - uses: actions/checkout@v6 + - name: "Checkout" + uses: actions/checkout@v7 with: submodules: recursive - - name: Add msbuild to PATH + - name: "[Windows] Add msbuild to PATH" if: matrix.os == 'Windows' id: add-msbuild uses: microsoft/setup-msbuild@v3 with: msbuild-architecture: x64 - - name: Setup ccache - if: matrix.use_ccache == 1 && matrix.os == 'macOS' + - name: "[macOS] Setup ccache" + if: matrix.os == 'macOS' && matrix.use_ccache == 1 run: brew install ccache - - name: Restore compiler cache (ccache) - if: matrix.use_ccache == 1 + - name: "[macOS] Restore compiler cache (ccache)" + if: matrix.os == 'macOS' && matrix.use_ccache == 1 id: ccache_restore - uses: actions/cache/restore@v5 + uses: actions/cache/restore@v6 env: BRANCH_NAME: ${{ github.head_ref || github.ref_name }} with: - path: ${{env.CCACHE_DIR}} - key: ccache-${{matrix.runner}}-${{matrix.soc}}-${{matrix.type}}-${{env.BRANCH_NAME}} - restore-keys: ccache-${{matrix.runner}}-${{matrix.soc}}-${{matrix.type}}- + 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 + - name: "Install aqtinstall" run: pipx install aqtinstall # Resolve given wildcard versions (e.g. Qt 6.6.*) to latest version via aqtinstall to avoid stale caches on new releases - - name: Resolve latest Qt patch version + - name: "Resolve latest Qt patch version" + env: + QT_VERSION: ${{ matrix.qt_version }} id: resolve_qt_version shell: bash - run: .ci/resolve_latest_aqt_qt_version.sh "${{matrix.qt_version}}" + run: .ci/resolve_latest_aqt_qt_version.sh "$QT_VERSION" - - name: Restore thin Qt ${{ steps.resolve_qt_version.outputs.version }} libraries (${{ matrix.soc }} macOS) + - name: "[macOS] Restore thin Qt ${{ steps.resolve_qt_version.outputs.version }} libraries" if: matrix.os == 'macOS' id: restore_qt - uses: actions/cache/restore@v5 + uses: actions/cache/restore@v6 with: - path: ${{ github.workspace }}/Qt key: thin-qt-macos-${{ matrix.soc }}-${{ steps.resolve_qt_version.outputs.version }} + path: ${{ github.workspace }}/Qt # Using jurplel/install-qt-action to install Qt without using brew - # qt build using vcpkg either just fails or takes too long to build - - name: Install fat Qt ${{ steps.resolve_qt_version.outputs.version }} (${{ matrix.soc }} macOS) + # Qt build using vcpkg either just fails or takes too long to build + - name: "[macOS] Install fat Qt ${{ steps.resolve_qt_version.outputs.version }}" if: matrix.os == 'macOS' && steps.restore_qt.outputs.cache-hit != 'true' uses: jurplel/install-qt-action@v4 with: - version: ${{ steps.resolve_qt_version.outputs.version }} - arch: ${{matrix.qt_arch}} - modules: ${{matrix.qt_modules}} + arch: ${{ matrix.qt_arch }} cache: false - dir: ${{github.workspace}} + dir: ${{ github.workspace }} + modules: ${{ matrix.qt_modules }} + version: ${{ steps.resolve_qt_version.outputs.version }} - - name: Thin Qt libraries (${{ matrix.soc }} macOS) + - name: "[macOS] Create thin Qt libraries" if: matrix.os == 'macOS' && steps.restore_qt.outputs.cache-hit != 'true' run: .ci/thin_macos_qtlib.sh - - name: Cache thin Qt libraries (${{ matrix.soc }} macOS) + - name: "[macOS] Cache thin Qt libraries" if: matrix.os == 'macOS' && steps.restore_qt.outputs.cache-hit != 'true' - uses: actions/cache/save@v5 + uses: actions/cache/save@v6 with: - path: ${{ github.workspace }}/Qt key: thin-qt-macos-${{ matrix.soc }}-${{ steps.resolve_qt_version.outputs.version }} + path: ${{ github.workspace }}/Qt - - name: Install Qt ${{matrix.qt_version}} (Windows) + - name: "[Windows] Install Qt ${{ matrix.qt_version }}" if: matrix.os == 'Windows' uses: jurplel/install-qt-action@v4 with: - # qt 6.11.0 only works with aqtinstall directly from git until aqtinstall 3.4 is released + # Qt 6.11.0 only works with aqtinstall directly from git until aqtinstall 3.4 is released aqtsource: git+https://github.com/miurahr/aqtinstall.git - version: ${{ steps.resolve_qt_version.outputs.version }} - arch: ${{matrix.qt_arch}} - modules: ${{matrix.qt_modules}} + arch: ${{ matrix.qt_arch }} cache: true + modules: ${{ matrix.qt_modules }} + version: ${{ steps.resolve_qt_version.outputs.version }} - - name: Install NSIS + - name: "[Windows] Install NSIS" if: matrix.os == 'Windows' shell: bash run: choco install nsis - - name: Setup vcpkg cache + - name: "Setup vcpkg cache" id: vcpkg-cache uses: TAServers/vcpkg-cache@v3 with: token: ${{ secrets.GITHUB_TOKEN }} - # uses environment variables, see compile.sh for more details - - name: Build Cockatrice + # Uses environment variables, see compile.sh for more details + - name: "Build Cockatrice" id: build shell: bash env: - BUILDTYPE: '${{matrix.type}}' - MAKE_PACKAGE: '${{matrix.make_package}}' - PACKAGE_SUFFIX: '${{matrix.package_suffix}}' - CMAKE_GENERATOR: ${{matrix.cmake_generator}} - CMAKE_GENERATOR_PLATFORM: ${{matrix.cmake_generator_platform}} - USE_CCACHE: ${{matrix.use_ccache}} - VCPKG_DISABLE_METRICS: 1 - VCPKG_BINARY_SOURCES: 'clear;files,${{ steps.vcpkg-cache.outputs.path }},readwrite' - # macOS-specific environment variables, will be ignored on Windows - MACOS_CERTIFICATE: ${{ secrets.PROD_MACOS_CERTIFICATE }} - MACOS_CERTIFICATE_PWD: ${{ secrets.PROD_MACOS_CERTIFICATE_PWD }} - MACOS_CERTIFICATE_NAME: ${{ secrets.PROD_MACOS_CERTIFICATE_NAME }} - MACOS_CI_KEYCHAIN_PWD: ${{ secrets.PROD_MACOS_CI_KEYCHAIN_PWD }} - DEVELOPER_DIR: '/Applications/Xcode_${{matrix.xcode}}.app/Contents/Developer' - TARGET_MACOS_VERSION: ${{ matrix.override_target }} + BUILDTYPE: '${{ matrix.type }}' CCACHE_EVICTION_AGE: ${{ matrix.ccache_eviction_age }} + CMAKE_GENERATOR: ${{ matrix.cmake_generator }} + CMAKE_GENERATOR_PLATFORM: ${{ matrix.cmake_generator_platform }} + DEVELOPER_DIR: '/Applications/Xcode_${{ matrix.xcode }}.app/Contents/Developer' + MACOS_CERTIFICATE: ${{ secrets.PROD_MACOS_CERTIFICATE }} + MACOS_CERTIFICATE_NAME: ${{ secrets.PROD_MACOS_CERTIFICATE_NAME }} + MACOS_CERTIFICATE_PWD: ${{ secrets.PROD_MACOS_CERTIFICATE_PWD }} + MACOS_CI_KEYCHAIN_PWD: ${{ secrets.PROD_MACOS_CI_KEYCHAIN_PWD }} + MAKE_PACKAGE: '${{ matrix.make_package }}' + PACKAGE_SUFFIX: '${{ matrix.package_suffix }}' + TARGET_MACOS_VERSION: ${{ matrix.override_target }} + USE_CCACHE: ${{ matrix.use_ccache }} + VCPKG_BINARY_SOURCES: 'clear;files,${{ steps.vcpkg-cache.outputs.path }},readwrite' + VCPKG_DISABLE_METRICS: 1 run: .ci/compile.sh --server --test --vcpkg # 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' && matrix.use_ccache == 1 && steps.ccache_restore.outputs.cache-hit + - name: "[macOS] Delete remote compiler cache (ccache)" + 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.ccache_restore.outputs.cache-primary-key }} GH_TOKEN: ${{ github.token }} run: | - if gh cache delete --repo ${{ github.repository }} ${{ steps.ccache_restore.outputs.cache-primary-key }}; then + if gh cache delete --repo "$GITHUB_REPOSITORY" "$CACHE_PRIMARY_KEY"; then echo "Cache deleted successfully" fi - - name: Save updated compiler cache (ccache) - if: github.ref == 'refs/heads/master' && matrix.use_ccache == 1 - uses: actions/cache/save@v5 + - 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: - path: ${{env.CCACHE_DIR}} key: ${{ steps.ccache_restore.outputs.cache-primary-key }} + path: ${{ env.CCACHE_DIR }} - - name: Sign app bundle + - name: "[macOS] Sign app bundle" if: matrix.os == 'macOS' && matrix.make_package && needs.configure.outputs.tag != null id: sign_macos env: + BUILD_PATH: ${{ steps.build.outputs.path }} MACOS_CERTIFICATE_NAME: ${{ secrets.PROD_MACOS_CERTIFICATE_NAME }} MACOS_CI_KEYCHAIN_PWD: ${{ secrets.PROD_MACOS_CI_KEYCHAIN_PWD }} run: | if [[ -n "$MACOS_CERTIFICATE_NAME" ]] then security unlock-keychain -p "$MACOS_CI_KEYCHAIN_PWD" build.keychain - /usr/bin/codesign --sign="$MACOS_CERTIFICATE_NAME" --entitlements=".ci/macos.entitlements" --options=runtime --force --deep --timestamp --verbose "${{steps.build.outputs.path}}" + /usr/bin/codesign --sign="$MACOS_CERTIFICATE_NAME" --entitlements=".ci/macos.entitlements" --options=runtime --force --deep --timestamp --verbose "$BUILD_PATH" fi - - name: Notarize app bundle - if: steps.sign_macos.outcome == 'success' + - name: "[macOS] Notarize app bundle" + if: matrix.os == 'macOS' && steps.sign_macos.outcome == 'success' env: + BUILD_PATH: ${{ steps.build.outputs.path }} MACOS_NOTARIZATION_APPLE_ID: ${{ secrets.PROD_MACOS_NOTARIZATION_APPLE_ID }} - MACOS_NOTARIZATION_TEAM_ID: ${{ secrets.PROD_MACOS_NOTARIZATION_TEAM_ID }} MACOS_NOTARIZATION_PWD: ${{ secrets.PROD_MACOS_NOTARIZATION_PWD }} + MACOS_NOTARIZATION_TEAM_ID: ${{ secrets.PROD_MACOS_NOTARIZATION_TEAM_ID }} run: | if [[ -n "$MACOS_NOTARIZATION_APPLE_ID" ]] then @@ -502,7 +510,7 @@ jobs: # Therefore, we create a zip file containing our app bundle, so that we can send it to the # notarization service echo "Creating temp notarization archive" - ditto -c -k --keepParent "${{steps.build.outputs.path}}" "notarization.zip" + ditto -c -k --keepParent "$BUILD_PATH" "notarization.zip" # Here we send the notarization request to the Apple's Notarization service, waiting for the result. # This typically takes a few seconds inside a CI environment, but it might take more depending on the App @@ -514,51 +522,52 @@ jobs: # Finally, we need to "attach the staple" to our executable, which will allow our app to be # validated by macOS even when an internet connection is not available. echo "Attach staple" - xcrun stapler staple "${{steps.build.outputs.path}}" + xcrun stapler staple "$BUILD_PATH" fi - - name: Upload artifact + - name: "Upload artifact" if: matrix.make_package id: upload_artifact uses: actions/upload-artifact@v7 with: - path: ${{steps.build.outputs.path}} archive: false if-no-files-found: error + path: ${{ steps.build.outputs.path }} - - name: Upload PDBs (Program Databases) + - name: "[Windows] Upload PDBs (Program Databases)" if: matrix.os == 'Windows' && github.ref_type != 'tag' uses: actions/upload-artifact@v7 with: - name: ${{steps.build.outputs.name}}-PDBs + if-no-files-found: error + name: ${{ steps.build.outputs.name }}-PDBs path: | build/cockatrice/Release/*.pdb build/oracle/Release/*.pdb build/servatrice/Release/*.pdb - if-no-files-found: error - - name: Upload to release + - name: "Upload to release" if: needs.configure.outputs.tag != null && matrix.make_package == '1' id: upload_release shell: bash env: - GH_TOKEN: ${{github.token}} - tag_name: ${{needs.configure.outputs.tag}} - asset_name: ${{steps.build.outputs.fullname}} - asset_path: ${{steps.build.outputs.path}} + asset_name: ${{ steps.build.outputs.fullname }} + asset_path: ${{ steps.build.outputs.path }} + GH_TOKEN: ${{ github.token }} + tag_name: ${{ needs.configure.outputs.tag }} run: gh release upload "$tag_name" "$asset_path#$asset_name" - - name: Attest binary provenance + - name: "Attest binary provenance" if: steps.upload_release.outcome == 'success' id: attestation uses: actions/attest@v4 with: - subject-path: ${{steps.build.outputs.path}} show-summary: false + subject-path: ${{ steps.build.outputs.path }} - - name: Verify binary attestation + - name: "Verify binary attestation" if: steps.attestation.outcome == 'success' shell: bash env: - GH_TOKEN: ${{github.token}} - run: gh attestation verify "${{steps.build.outputs.path}}" --repo Cockatrice/Cockatrice + BUILD_PATH: ${{ steps.build.outputs.path }} + GH_TOKEN: ${{ github.token }} + run: gh attestation verify "$BUILD_PATH" --repo Cockatrice/Cockatrice diff --git a/.github/workflows/desktop-lint.yml b/.github/workflows/desktop-lint.yml index df8b9f89e..5f31ea59c 100644 --- a/.github/workflows/desktop-lint.yml +++ b/.github/workflows/desktop-lint.yml @@ -1,7 +1,7 @@ name: Code Style (C++) on: - # push trigger not needed for linting, we do not allow direct pushes to master + # Push trigger not needed for linting, we do not allow direct pushes to master pull_request: paths: - '*/**' # matches all files not in root @@ -21,17 +21,20 @@ jobs: runs-on: ubuntu-slim steps: - - name: Checkout - uses: actions/checkout@v6 + - name: "Checkout" + uses: actions/checkout@v7 with: fetch-depth: 20 # should be enough to find merge base - - name: Install dependencies + - name: "Install dependencies" shell: bash run: | sudo apt-get update - sudo apt-get install -y --no-install-recommends clang-format cmake-format shellcheck + sudo apt-get install -y --no-install-recommends \ + clang-format \ + cmake-format \ + shellcheck - - name: Check code formatting + - name: "Check code formatting" shell: bash run: ./.ci/lint_cpp.sh diff --git a/.github/workflows/docker-release.yml b/.github/workflows/docker-release.yml index b869d1fa9..b479322d0 100644 --- a/.github/workflows/docker-release.yml +++ b/.github/workflows/docker-release.yml @@ -1,9 +1,10 @@ name: Build Docker Image +permissions: + contents: read + packages: write + on: - release: - types: - - released # publishing of stable releases push: branches: - master @@ -13,6 +14,9 @@ on: paths: - '.github/workflows/docker-release.yml' - 'Dockerfile' + release: + types: + - released # publishing of stable releases # Cancel earlier, unfinished runs of this workflow on the same branch (unless on release) concurrency: @@ -24,54 +28,50 @@ jobs: name: amd64 & arm64 if: ${{ github.repository_owner == 'Cockatrice' }} runs-on: ubuntu-latest - - permissions: - contents: read - packages: write steps: - - name: Checkout - uses: actions/checkout@v6 + - name: "Checkout" + uses: actions/checkout@v7 - - name: Docker metadata + - 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 - 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 - - name: Set up QEMU + - name: "Set up QEMU" uses: docker/setup-qemu-action@v4 - - name: Set up Docker buildx + - name: "Set up Docker buildx" uses: docker/setup-buildx-action@v4 - - name: Login to GitHub Container Registry + - name: "Login to GitHub Container Registry" if: contains(github.event.release.tag_name, 'Release') && github.event.release.target_commitish == 'master' uses: docker/login-action@v4 with: + password: ${{ github.token }} registry: ghcr.io username: ${{ github.actor }} - password: ${{ github.token }} - - name: Build and push Docker image + - name: "Build and push Docker image" uses: docker/build-push-action@v7 with: - context: . - platforms: linux/amd64,linux/arm64 - push: ${{ github.ref_type == 'tag' }} - tags: ${{ steps.metadata.outputs.tags }} - labels: ${{ steps.metadata.outputs.labels }} annotations: ${{ steps.metadata.outputs.annotations }} cache-from: type=gha,scope=servatrice cache-to: type=gha,mode=max,scope=servatrice + context: . + labels: ${{ steps.metadata.outputs.labels }} + platforms: linux/amd64,linux/arm64 + push: ${{ github.ref_type == 'tag' }} + tags: ${{ steps.metadata.outputs.tags }} diff --git a/.github/workflows/documentation-build.yml b/.github/workflows/documentation-build.yml index b0093d6b1..4b9ca79ab 100644 --- a/.github/workflows/documentation-build.yml +++ b/.github/workflows/documentation-build.yml @@ -1,18 +1,18 @@ name: Generate Docs on: - release: - types: - - published # publishing of stable releases and pre-releases pull_request: paths: - 'doc/doxygen/**' - '.github/workflows/documentation-build.yml' - 'Doxyfile' + release: + types: + - published # publishing of stable releases and pre-releases workflow_dispatch: env: - COCKATRICE_REF: ${{ github.ref_name }} # Tag name if the commit is tagged, otherwise branch name + COCKATRICE_REF: ${{ github.ref_name }} # tag name if the commit is tagged, otherwise branch name jobs: docs: @@ -20,22 +20,22 @@ jobs: runs-on: ubuntu-latest steps: - - name: Checkout code - uses: actions/checkout@v6 + - name: "Checkout code" + uses: actions/checkout@v7 with: submodules: recursive - - name: Install Graphviz + - name: "Install Graphviz" run: | sudo apt-get install -y graphviz dot -V - - name: Install Doxygen + - name: "Install Doxygen" uses: ssciwr/doxygen-install@v2 with: version: "1.16.1" - - name: Update Doxygen Configuration + - name: "Update Doxygen Configuration" run: | git diff Doxyfile doxygen -u Doxyfile @@ -48,16 +48,16 @@ jobs: exit 1 fi - - name: Generate Documentation + - name: "Generate Documentation" if: always() run: doxygen Doxyfile - - name: Deploy to cockatrice.github.io + - name: "Deploy to cockatrice.github.io" if: github.event_name == 'release' || github.event_name == 'workflow_dispatch' uses: peaceiris/actions-gh-pages@v4 with: deploy_key: ${{ secrets.DOCS_DEPLOY_KEY }} + destination_dir: docs # docs will be available at https://cockatrice.github.io/docs/ external_repository: Cockatrice/cockatrice.github.io publish_branch: master publish_dir: ./docs/html - destination_dir: docs # Docs will live under https://cockatrice.github.io/docs/ diff --git a/.github/workflows/translations-pull.yml b/.github/workflows/translations-pull.yml index 8f673fce9..a3db5f86d 100644 --- a/.github/workflows/translations-pull.yml +++ b/.github/workflows/translations-pull.yml @@ -1,14 +1,14 @@ name: Update Translations on: - workflow_dispatch: - schedule: - # runs in the middle of each month starting a quarter (UTC) = two weeks after new strings are built - - cron: '0 0 15 1,4,7,10 *' pull_request: paths: - '.tx/**' - '.github/workflows/translations-pull.yml' + schedule: + # Runs in the middle of each month starting a quarter (UTC) = two weeks after new strings are built + - cron: '0 0 15 1,4,7,10 *' + workflow_dispatch: jobs: translations: @@ -19,18 +19,20 @@ jobs: runs-on: ubuntu-slim steps: - - name: Checkout repo - uses: actions/checkout@v6 + - name: "Checkout repo" + uses: actions/checkout@v7 - - name: Pull translated strings from Transifex + - name: "Pull translated strings from Transifex" + # Do not run this step for PR's from forks, they don't have access to the secret + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false uses: transifex/cli-action@v2 with: - # used config file: https://github.com/Cockatrice/Cockatrice/blob/master/.tx/config - # https://github.com/transifex/cli#pulling-files-from-transifex - token: ${{ secrets.TX_TOKEN }} + # Used config file: https://github.com/Cockatrice/Cockatrice/blob/master/.tx/config + # Docs: https://github.com/transifex/cli#pulling-files-from-transifex args: pull --force --all + token: ${{ secrets.TX_TOKEN }} - - name: Create pull request + - name: "Create pull request" if: github.event_name != 'pull_request' id: create_pr uses: peter-evans/create-pull-request@v8 @@ -38,35 +40,32 @@ jobs: add-paths: | cockatrice/translations/*.ts oracle/translations/*.ts - commit-message: Update translation files - # author is the owner of the commit - author: github-actions - branch: ci-update_translations - delete-branch: true - title: 'Update translations' + author: github-actions # owner of the commit body: | Pulled all translated strings from [Transifex][1]. - + --- *This PR is automatically generated and updated by the workflow at `.github/workflows/translations-pull.yml`. Review [action runs][2].*
*After merging, all new languages and translations are available in the next build.* - + [1]: https://explore.transifex.com/cockatrice/cockatrice/ [2]: https://github.com/Cockatrice/Cockatrice/actions/workflows/translations-pull.yml?query=branch%3Amaster + branch: ci-update_translations + commit-message: Update translation files + delete-branch: true + draft: false labels: | CI Translation - draft: false + title: 'Update translations' - - name: PR Status + - name: "PR Status" if: github.event_name != 'pull_request' shell: bash env: - STATUS: ${{ steps.create_pr.outputs.pull-request-operation }} + PR_NUMBER: ${{ steps.create_pr.outputs.pull-request-number }} + PR_URL: ${{ steps.create_pr.outputs.pull-request-url }} + STATUS: ${{ case(steps.create_pr.outputs.pull-request-operation == 'none', 'unchanged', steps.create_pr.outputs.pull-request-operation) }} run: | - if [[ "$STATUS" == "none" ]]; then - echo "PR #${{ steps.create_pr.outputs.pull-request-number }} unchanged!" >> $GITHUB_STEP_SUMMARY - else - echo "PR #${{ steps.create_pr.outputs.pull-request-number }} $STATUS!" >> $GITHUB_STEP_SUMMARY - fi - echo "URL: ${{ steps.create_pr.outputs.pull-request-url }}" >> $GITHUB_STEP_SUMMARY + echo "PR #$PR_NUMBER $STATUS!" >> "$GITHUB_STEP_SUMMARY" + echo "URL: $PR_URL" >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/translations-push.yml b/.github/workflows/translations-push.yml index 9e236c621..e90dc0151 100644 --- a/.github/workflows/translations-push.yml +++ b/.github/workflows/translations-push.yml @@ -1,14 +1,14 @@ name: Update Translation Source on: - workflow_dispatch: - schedule: - # runs at the start of each quarter (UTC) - - cron: '0 0 1 1,4,7,10 *' pull_request: paths: - '.ci/update_translation_source_strings.sh' - '.github/workflows/translations-push.yml' + schedule: + # Runs at the start of each quarter (UTC) + - cron: '0 0 1 1,4,7,10 *' + workflow_dispatch: jobs: translations: @@ -19,34 +19,33 @@ jobs: runs-on: ubuntu-slim steps: - - name: Checkout repo - uses: actions/checkout@v6 + - name: "Checkout repo" + uses: actions/checkout@v7 - - name: Install lupdate + - name: "Install lupdate" shell: bash run: | sudo apt-get update sudo apt-get install -y --no-install-recommends qt6-l10n-tools - - name: Update Cockatrice translation source + - name: "Update Cockatrice translation source" id: cockatrice shell: bash env: - FILE: 'cockatrice/cockatrice_en@source.ts' - run: | + FILE: cockatrice/cockatrice_en@source.ts + run: > DIRS="cockatrice/src $(find . -maxdepth 1 -type d -name 'libcockatrice_*')" - export DIRS .ci/update_translation_source_strings.sh - - name: Update Oracle translation source + - name: "Update Oracle translation source" id: oracle shell: bash env: - FILE: 'oracle/oracle_en@source.ts' DIRS: 'oracle/src' + FILE: 'oracle/oracle_en@source.ts' run: .ci/update_translation_source_strings.sh - - name: Render template + - name: "Render template" id: template uses: chuhlomin/render-template/binary@v1 with: @@ -56,7 +55,7 @@ jobs: oracle_output: ${{ steps.oracle.outputs.output }} commit: ${{ github.sha }} - - name: Create pull request + - name: "Create pull request" if: github.event_name != 'pull_request' id: create_pr uses: peter-evans/create-pull-request@v8 @@ -64,27 +63,24 @@ jobs: add-paths: | cockatrice/cockatrice_en@source.ts oracle/oracle_en@source.ts - commit-message: Update translation source strings - # author is the owner of the commit - author: github-actions - branch: ci-update_translation_source - delete-branch: true - title: 'Update source strings' + author: github-actions # owner of the commit body: ${{ steps.template.outputs.result }} + branch: ci-update_translation_source + commit-message: Update translation source strings + delete-branch: true + draft: false labels: | CI Translation - draft: false + title: 'Update source strings' - - name: PR Status + - name: "PR Status" if: github.event_name != 'pull_request' shell: bash env: - STATUS: ${{ steps.create_pr.outputs.pull-request-operation }} + PR_NUMBER: ${{ steps.create_pr.outputs.pull-request-number }} + PR_URL: ${{ steps.create_pr.outputs.pull-request-url }} + STATUS: ${{ case(steps.create_pr.outputs.pull-request-operation == 'none', 'unchanged', steps.create_pr.outputs.pull-request-operation) }} run: | - if [[ "$STATUS" == "none" ]]; then - echo "PR #${{ steps.create_pr.outputs.pull-request-number }} unchanged!" >> $GITHUB_STEP_SUMMARY - else - echo "PR #${{ steps.create_pr.outputs.pull-request-number }} $STATUS!" >> $GITHUB_STEP_SUMMARY - fi - echo "URL: ${{ steps.create_pr.outputs.pull-request-url }}" >> $GITHUB_STEP_SUMMARY + echo "PR #$PR_NUMBER $STATUS!" >> "$GITHUB_STEP_SUMMARY" + echo "URL: $PR_URL" >> "$GITHUB_STEP_SUMMARY" diff --git a/cmake/NSIS.template.in b/cmake/NSIS.template.in index 8fae83a79..24392c6ef 100644 --- a/cmake/NSIS.template.in +++ b/cmake/NSIS.template.in @@ -109,21 +109,22 @@ ${If} $InstDir == "" ; we need to set a default based on the install mode StrCpy $InstDir $0 ${EndIf} -Call SetModeDestinationFromInstdir -; --- Detect portable install when using /R --- +; --- Detect portable install when using /R (must come BEFORE SetModeDestinationFromInstdir) --- ${If} $ReinstallMode = 1 IfFileExists "$InstDir\portable.dat" 0 not_portable StrCpy $PortableMode 1 Goto portable_done - not_portable: StrCpy $PortableMode 0 - portable_done: ${EndIf} +; Now that $PortableMode reflects reality, commit InstDir into the correct slot +Call SetModeDestinationFromInstdir + ${If} $ReinstallMode = 1 +${AndIf} $PortableMode = 0 Call AutoUninstallIfNeeded ${EndIf} diff --git a/cockatrice/CMakeLists.txt b/cockatrice/CMakeLists.txt index 7ec717315..5fe2ce163 100644 --- a/cockatrice/CMakeLists.txt +++ b/cockatrice/CMakeLists.txt @@ -56,56 +56,59 @@ set(cockatrice_SOURCES src/filters/filter_tree_model.cpp src/filters/syntax_help.cpp src/game/abstract_game.cpp - src/game/board/abstract_card_drag_item.cpp - src/game/board/abstract_card_item.cpp - src/game/board/abstract_counter.cpp + src/game/arrow_registry.cpp + src/game_graphics/board/abstract_card_drag_item.cpp + src/game_graphics/board/abstract_card_item.cpp + src/game_graphics/board/abstract_counter.cpp src/game/board/arrow_data.cpp - src/game/board/arrow_item.cpp - src/game/board/arrow_target.cpp - src/game/board/card_drag_item.cpp - src/game/board/card_item.cpp + src/game_graphics/board/arrow_item.cpp + src/game_graphics/board/arrow_target.cpp + src/game_graphics/board/card_drag_item.cpp + src/game_graphics/board/card_item.cpp src/game/board/card_list.cpp src/game/board/card_state.cpp - src/game/board/counter_general.cpp + src/game_graphics/board/counter_general.cpp src/game/board/counter_state.cpp - src/game/board/translate_counter_name.cpp - src/game/deckview/deck_view.cpp - src/game/deckview/deck_view_container.cpp - src/game/deckview/tabbed_deck_view_container.cpp - src/game/dialogs/dlg_create_token.cpp - src/game/dialogs/dlg_move_top_cards_until.cpp - src/game/dialogs/dlg_roll_dice.cpp + src/game_graphics/board/translate_counter_name.cpp + src/game_graphics/deckview/deck_view.cpp + src/game_graphics/deckview/deck_view_container.cpp + src/game_graphics/deckview/tabbed_deck_view_container.cpp + src/game_graphics/dialogs/dlg_create_token.cpp + src/game_graphics/dialogs/dlg_move_top_cards_until.cpp + src/game_graphics/dialogs/dlg_roll_dice.cpp src/game/game.cpp src/game/game_event_handler.cpp src/game/game_meta_info.cpp - src/game/game_scene.cpp + src/game_graphics/game_scene.cpp src/game/game_state.cpp - src/game/game_view.cpp - src/game/hand_counter.cpp - src/game/log/message_log_widget.cpp + src/game_graphics/game_view.cpp + src/game_graphics/hand_counter.cpp + src/game/selection_subtype_tally.cpp + src/game_graphics/log/message_log_widget.cpp src/game/phase.cpp - src/game/phases_toolbar.cpp - src/game/player/menu/card_menu.cpp - src/game/player/menu/custom_zone_menu.cpp - src/game/player/menu/grave_menu.cpp - src/game/player/menu/hand_menu.cpp - src/game/player/menu/library_menu.cpp - src/game/player/menu/move_menu.cpp - src/game/player/menu/player_menu.cpp - src/game/player/menu/pt_menu.cpp - src/game/player/menu/rfg_menu.cpp - src/game/player/menu/say_menu.cpp - src/game/player/menu/sideboard_menu.cpp - src/game/player/menu/utility_menu.cpp + src/game_graphics/phases_toolbar.cpp + src/game_graphics/player/menu/card_menu.cpp + src/game_graphics/player/menu/custom_zone_menu.cpp + src/game_graphics/player/menu/grave_menu.cpp + src/game_graphics/player/menu/hand_menu.cpp + src/game_graphics/player/menu/library_menu.cpp + src/game_graphics/player/menu/move_menu.cpp + src/game_graphics/player/menu/player_menu.cpp + src/game_graphics/player/menu/pt_menu.cpp + src/game_graphics/player/menu/rfg_menu.cpp + src/game_graphics/player/menu/say_menu.cpp + src/game_graphics/player/menu/sideboard_menu.cpp + src/game_graphics/player/menu/utility_menu.cpp src/game/player/player_actions.cpp - src/game/player/player_area.cpp + src/game_graphics/player/player_area.cpp + src/game_graphics/player/player_dialogs.cpp src/game/player/player_event_handler.cpp - src/game/player/player_graphics_item.cpp + src/game_graphics/player/player_graphics_item.cpp src/game/player/player_info.cpp - src/game/player/player_list_widget.cpp + src/game_graphics/player/player_list_widget.cpp src/game/player/player_logic.cpp src/game/player/player_manager.cpp - src/game/player/player_target.cpp + src/game_graphics/player/player_target.cpp src/game/replay.cpp src/game/zones/card_zone_logic.cpp src/game/zones/hand_zone_logic.cpp @@ -181,6 +184,7 @@ set(cockatrice_SOURCES src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_single_display_widget.cpp src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_total_widget.cpp src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_category_widget.cpp + src/interface/widgets/deck_editor/card_database_view.cpp src/interface/widgets/deck_editor/deck_list_history_manager_widget.cpp src/interface/widgets/deck_editor/deck_editor_card_database_dock_widget.cpp src/interface/widgets/deck_editor/deck_editor_card_info_dock_widget.cpp @@ -233,10 +237,14 @@ set(cockatrice_SOURCES src/interface/widgets/server/handle_public_servers.cpp src/interface/widgets/server/remote/remote_decklist_tree_widget.cpp src/interface/widgets/server/remote/remote_replay_list_tree_widget.cpp + src/interface/widgets/server/user/user_avatar_provider.cpp + src/interface/widgets/server/user/user_card_art_provider.cpp + src/interface/widgets/server/user/user_card_settings_dialog.cpp src/interface/widgets/server/user/user_context_menu.cpp src/interface/widgets/server/user/user_info_box.cpp src/interface/widgets/server/user/user_info_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_widget.cpp src/interface/widgets/settings_page/appearance_settings_page.cpp src/interface/widgets/settings_page/deck_editor_settings_page.cpp @@ -324,6 +332,7 @@ set(cockatrice_SOURCES src/interface/widgets/tabs/tab.cpp src/interface/widgets/tabs/tab_account.cpp src/interface/widgets/tabs/tab_admin.cpp + src/interface/widgets/tabs/tab_card_art_rules.cpp src/interface/widgets/tabs/tab_deck_editor.cpp src/interface/widgets/tabs/tab_deck_storage.cpp src/interface/widgets/tabs/tab_game.cpp @@ -346,6 +355,8 @@ set(cockatrice_SOURCES src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_budget_navigation_widget.h src/interface/widgets/utility/compact_push_button.cpp src/interface/widgets/utility/compact_push_button.h + src/interface/widgets/server/user/user_info_popup.cpp + src/interface/widgets/server/user/user_info_popup.h ) add_subdirectory(sounds) diff --git a/cockatrice/cockatrice_en@source.ts b/cockatrice/cockatrice_en@source.ts index 810b345df..e06972ddc 100644 --- a/cockatrice/cockatrice_en@source.ts +++ b/cockatrice/cockatrice_en@source.ts @@ -4,7 +4,7 @@ AbstractCounter - + &Set counter... @@ -12,12 +12,12 @@ AbstractCounterDialog - + Set counter - + New value for counter '%1': @@ -38,60 +38,60 @@ AbstractTabDeckEditor - + Open in new tab - + Are you sure? - + The decklist has been modified. Do you want to save the changes? - - - - - - + + + + + + Error - + Could not open deck at %1 - + Could not save remote deck - - + + The deck could not be saved. Please check that the directory is writable and try again. - + Save deck - + The deck could not be saved. - + There are no cards in your deck to be exported @@ -107,12 +107,12 @@ Please check that the directory is writable and try again. AdminNotesDialog - + Update Notes - + Admin Notes for %1 @@ -129,172 +129,227 @@ Please check that the directory is writable and try again. Sideboard + + + Tokens + + AppearanceSettingsPage - + seconds - + Error - + Could not create themes directory at '%1'. - + Theme settings - + Current theme: - + Open themes folder - + Home tab background source: - + Home tab background shuffle frequency: - + Disabled - - Display card name of background in bottom right: - - - - + Menu settings - + Show keyboard shortcuts in right-click menus - + Show game filter toolbar above list in room tab - + Card rendering - + Display card names on cards having a picture - + Auto-Rotate cards with sideways layout - + Override all card art with personal set preference (Pre-ProviderID change behavior) - + + Light + + + + + Dark + + + + + System + + + + + Active theme palette: + + + + + Edit theme palette + + + + + Home tab settings + + + + + Display card name of background in bottom right + + + + + Styling settings + + + + + Style user list + + + + + Card printings + + + + Bump sets that the deck contains cards from to the top in the printing selector - + Scale cards on mouse over - + Use rounded card corners - + + Card layout + + + + Minimum overlap percentage of cards on the stack and in vertical hand - + Maximum initial height for card view window: - - + + rows - + Maximum expanded height for card view window: - + Card counters - + Counter %1 - + Hand layout - + Display hand horizontally (wastes space) - + Enable left justification - + Table grid layout - + Invert vertical coordinate - + Minimum player count for multi-column layout: - + Maximum font size for information displayed on cards: @@ -302,12 +357,12 @@ Please check that the directory is writable and try again. ArchidektApiResponseDeckDisplayWidget - + Back to results - + Open Deck in Deck Editor @@ -333,111 +388,111 @@ Please check that the directory is writable and try again. BanDialog - + ban &user name - + ban &IP address - + ban client I&D - + Ban type - + &permanent ban - + &temporary ban - + &Days: - + &Hours: - + &Minutes: - + Duration of the ban - + Please enter the reason for the ban. This is only saved for moderators and cannot be seen by the banned person. - + Please enter the reason for the ban that will be visible to the banned person. - + Redact all messages from this user in all rooms - + &OK - + &Cancel - + Ban user from server - - - - + + + + Error - + You have to select a name-based, IP-based, clientId based, or some combination of the three to place a ban. - + You must have a value in the name ban when selecting the name ban checkbox. - + You must have a value in the ip ban when selecting the ip ban checkbox. - + You must have a value in the clientid ban when selecting the clientid ban checkbox. @@ -445,59 +500,123 @@ This is only saved for moderators and cannot be seen by the banned person. BetaReleaseChannel - + Beta - + No reply received from the release update server. - + Invalid reply received from the release update server. - + No reply received from the file update server. + + CardArtPreviewWidget + + + No card selected + + + + + CardArtRulesModel + + + Card + + + + + ProviderId + + + + + Mode + + + + + Reason + + + CardDatabaseModel - + Name - + Sets - + Mana cost - + Card type - + P/T - + Color(s) + + CardDatabaseView + + + Add to Deck + + + + + Add to Sideboard + + + + + Select Printing + + + + + Show on EDHRec (Commander) + + + + + Show on EDHRec (Card) + + + + + Show Related cards + + + CardFilter @@ -626,22 +745,22 @@ This is only saved for moderators and cannot be seen by the banned person. CardInfoPictureWidget - + View related cards - + Add card to deck - + Mainboard - + Sideboard @@ -664,12 +783,12 @@ This is only saved for moderators and cannot be seen by the banned person. - + Related cards: - + Unknown card: @@ -677,128 +796,146 @@ This is only saved for moderators and cannot be seen by the banned person. CardMenu - + Re&veal to... - + &All players - + View related cards - + Token: - + All tokens - + &Select All - + S&elect Row - + S&elect Column - + &Play - + &Hide - + Play &Face Down - + &Tap / Untap Turn sideways or back again - + Skip &untapping - + T&urn Over Turn face up/face down - + &Peek at card face - + &Clone - + Attac&h to card... - + Unattac&h - + &Draw arrow... - + &Set annotation... - + + Reduce life by power + + + + Ca&rd counters - + &Add counter (%1) - + &Remove counter (%1) - + &Set counters (%1)... + + CardPictureLoaderCacheMethod + + + Network Cache + + + + + Filesystem + + + CardSizeWidget @@ -810,133 +947,133 @@ This is only saved for moderators and cannot be seen by the banned person. CardZoneLogic - + their hand nominative - + %1's hand nominative - + their library look at zone - + %1's library look at zone - + of their library top cards of zone, - + of %1's library top cards of zone - + their library reveal zone - + %1's library reveal zone - + their library shuffle - + %1's library shuffle - + their library nominative - + %1's library nominative - + their graveyard nominative - + %1's graveyard nominative - + their exile nominative - + %1's exile nominative - + their sideboard look at zone - + %1's sideboard look at zone - + their sideboard nominative - + %1's sideboard nominative - + their custom zone '%1' nominative - + %1's custom zone '%2' nominative @@ -958,16 +1095,424 @@ This is only saved for moderators and cannot be seen by the banned person. + + ColorButton + + + Click to pick a color + + + + + Pick colour + + + + + ConnectionController + + + + The server has reached its maximum user capacity, please check back later. + + + + + There are too many concurrent connections from your address. + + + + + Banned by moderator + + + + + Expected end time: %1 + + + + + This ban lasts indefinitely. + + + + + Scheduled server shutdown. + + + + + + Invalid username. + + + + + You have been logged out due to logging in at another location. + + + + + Connection closed + + + + + The server has terminated your connection. +Reason: %1 + + + + + The server is going to be restarted in %n minute(s). +All running games will be lost. +Reason for shutdown: %1 + + + + + + + + Scheduled server shutdown + + + + + Failed Login + + + + + Your client seems to be missing features this server requires for connection. + + + + + To update your client, go to 'Help -> Check for Client Updates'. + + + + + + + + + + + + + + + + + + + + + + Error + + + + + Incorrect username or password. Please check your authentication information and try again. + + + + + There is already an active session using this user name. +Please close that session first and re-login. + + + + + + You are banned until %1. + + + + + + You are banned indefinitely. + + + + + This server requires user registration. Do you want to register now? + + + + + This server requires client IDs. Your client is either failing to generate an ID or you are running a modified client. +Please close and reopen your client to try again. + + + + + An internal error has occurred, please close and reopen Cockatrice before trying again. +If the error persists, ensure you are running the latest version of the software and if needed contact the software developers. + + + + + Account activation + + + + + Your account has not been activated yet. +You need to provide the activation token received in the activation email. + + + + + Server Full + + + + + Unknown login error: %1 + + + + + + +This usually means that your client version is out of date, and the server sent a reply your client doesn't understand. + + + + + + + + + + Registration denied + + + + + Registration is currently disabled on this server + + + + + There is already an existing account with the same user name. + + + + + It's mandatory to specify a valid email address when registering. + + + + + It appears you are attempting to register a new account on this server yet you already have an account registered with the email provided. This server restricts the number of accounts a user can register per address. Please contact the server operator for further assistance or to obtain your credential information. + + + + + Password too short. + + + + + Registration failed for a technical problem on the server. + + + + + The connection to the server has been lost. + + + + + Unknown registration error: %1 + + + + + Account activation failed + + + + + Socket error: %1 + + + + + Server timeout + + + + + You are trying to connect to an obsolete server. Please downgrade your Cockatrice version or connect to a suitable server. +Local version is %1, remote version is %2. + + + + + Your Cockatrice client is obsolete. Please update your Cockatrice version. +Local version is %1, remote version is %2. + + + + + + Success + + + + + Registration accepted. +Will now login. + + + + + Account activation accepted. +Will now login. + + + + + Information + + + + + This server supports additional features that your client doesn't have. +This is most likely not a problem, but this message might mean there is a new version of Cockatrice available or this server is running a custom or pre-release version. + +To update your client, go to Help -> Check for Updates. + + + + + + + Reset Password + + + + + Your password has been reset successfully, you can now log in using the new credentials. + + + + + Failed to reset user account password, please contact the server operator to reset your password. + + + + + Activation request received, please check your email for an activation token. + + + + + Connecting to %1... + + + + + Registering to %1 as %2... + + + + + Disconnected + + + + + Connected, logging in at %1 + + + + + Requesting forgotten password to %1 as %2... + + + + + Your username must respect these rules: + + + + + is %1 - %2 characters long + + + + + can %1 contain lowercase characters + + + + + + + + NOT + + + + + can %1 contain uppercase characters + + + + + can %1 contain numeric characters + + + + + can contain the following punctuation: %1 + + + + + first character can %1 be a punctuation mark + + + + + no unacceptable language as specified by these server rules: + note that the following lines will not be translated + + + + + can not contain any of the following words: %1 + + + + + can not match any of the following expressions: %1 + + + + + You may only use A-Z, a-z, 0-9, _, ., and - in your username. + + + CustomZoneMenu - + C&ustom Zones - - + + View custom zone '%1' @@ -975,30 +1520,35 @@ This is only saved for moderators and cannot be seen by the banned person. DeckAnalyticsWidget - + Add Panel - + Remove Panel - + Save Layout - + Load Layout + + + Include Sideboard + + DeckEditorCardDatabaseDockWidget - + Card Database @@ -1014,47 +1564,17 @@ This is only saved for moderators and cannot be seen by the banned person. DeckEditorDatabaseDisplayWidget - + Search by card name (or search expressions) - - Add to Deck - - - - - Add to Sideboard - - - - - Select Printing - - - - - Show on EDHRec (Commander) - - - - - Show on EDHRec (Card) - - - - - Show Related cards - - - - + Add card to &maindeck - + Add card to &sideboard @@ -1087,72 +1607,72 @@ This is only saved for moderators and cannot be seen by the banned person. - + Select Printing - + Deck - + Deck &name: - + Banner Card/Tags Visibility Settings - + Show banner card selection menu - + Show tags selection menu - + &Comments: - + Group by: - + Format: - + Hash: - + &Increment number - + &Decrement number - + &Remove row - + Swap card to/from sideboard @@ -1160,17 +1680,17 @@ This is only saved for moderators and cannot be seen by the banned person. DeckEditorFilterDockWidget - + Filters - + &Clear all filters - + Delete selected @@ -1301,169 +1821,113 @@ This is only saved for moderators and cannot be seen by the banned person. DeckEditorSettingsPage - - + + Update Spoilers - - + Success - + Download URLs have been reset. - - Downloaded card pictures have been reset. - - - - - Error - - - - - One or more downloaded card pictures could not be cleared. - - - - + Add URL - - + + URL: - - + + Edit URL - - Network Cache Size: - - - - - Redirect Cache TTL: - - - - - How long cached redirects for urls are valid for. - - - - - Picture Cache Size: - - - - + Add New URL - + Remove URL - - Day(s) - - - - + Updating... - + Choose path - + URL Download Priority - + Spoilers - + Download Spoilers Automatically - + Spoiler Location: - + Last Change - + Spoilers download automatically on launch - + Press the button to manually update without relaunching - + Do not close settings until manual update is complete - + Download card pictures on the fly - + How to add a custom URL - - Delete Downloaded Images - - - - + Reset Download URLs - - - On-disk cache for downloaded pictures - - - - - In-memory cache for pictures not currently on screen - - DeckListHistoryManagerWidget @@ -1529,12 +1993,12 @@ This is only saved for moderators and cannot be seen by the banned person. DeckLoader - + Common deck formats (%1) - + All files (*.*) @@ -1756,27 +2220,27 @@ This is only saved for moderators and cannot be seen by the banned person. - + Moved to %1 1 × "%2" (%3) - + Removed "%1" (all copies) - + %1 1 × "%2" (%3) - + Added - + Removed @@ -1798,74 +2262,74 @@ This is only saved for moderators and cannot be seen by the banned person. DeckViewContainer - + Load deck... - + Load remote deck... - + Load from clipboard... - + Load from website... - + Unload deck - + Ready to start - + Force start - + Sideboard unlocked - + Sideboard locked - - + + Error - + The selected file could not be loaded. - + Deck is greater than maximum file size. - + Are you sure you want to force start? This will kick all non-ready players from the game. - + Cockatrice @@ -1964,7 +2428,7 @@ This will kick all non-ready players from the game. - + Webpage @@ -2004,37 +2468,37 @@ This will kick all non-ready players from the game. - + Server URL - + Communication Port - + Unique Server Name - + Connection Warning - + You need to name your new connection profile. - + Connect Warning - + The player name can't be empty. @@ -2147,17 +2611,17 @@ This will kick all non-ready players from the game. - + Game information - + Error - + Server error. @@ -2165,97 +2629,97 @@ This will kick all non-ready players from the game. DlgCreateToken - + &Name: - + Token - + C&olor: - + white - + blue - + black - + red - + green - + multicolor - + colorless - + &P/T: - + &Annotation: - + &Destroy token when it leaves the table - + Create face-down (Only hides name) - + Token data - + Show &all tokens - + Show tokens from this &deck - + Choose token from list - + Create token @@ -2289,7 +2753,7 @@ This will kick all non-ready players from the game. - + @@ -2304,12 +2768,12 @@ This will kick all non-ready players from the game. - + Duplicate Tag - + This tag already exists. @@ -2495,12 +2959,12 @@ To remove your current avatar, confirm without choosing a new image. - + Error - + The chosen name conflicts with an existing card or token. Make sure to enable the 'Token' set in the "Manage sets" dialog to display them correctly. @@ -2524,12 +2988,12 @@ Make sure to enable the 'Token' set in the "Manage sets" dia - + Real name: - + Edit user profile @@ -2567,113 +3031,113 @@ Make sure to enable the 'Token' set in the "Manage sets" dia - + Hide 'buddies only' games - + Hide full games - + Hide games that have started - + Hide password protected games - + Hide 'ignored user' games - + Hide games not created by buddies Hide games not created by buddy - + Hide games with forced open decklists - + &Newer than: - + Game &description: - + &Creator name: - + General - + &Game types - + at &least: - + at &most: - + Maximum player count - + Restrictions - + Show games only if &spectators can watch - + Show spectator password p&rotected games - + Show only if spectators can ch&at - + Show only if spectators can see &hands - + Spectators - + Filter games @@ -2976,37 +3440,37 @@ https://tappedout.net/mtg-decks/your-deck-name/ DlgMoveTopCardsUntil - + Card name (or search expressions): - + Number of hits: - + Auto play hits - + Put top cards on stack until... - + No cards matching the search expression exists in the card database. Proceed anyways? - + Cockatrice - + Invalid filter @@ -3065,40 +3529,40 @@ Your email will be used to verify your account. - + Real name: - + Register to server - - - - + + + + Registration Warning - + Your password is too short. - + Your passwords do not match, please try again. - + Your email addresses do not match, please try again. - + The player name can't be empty. @@ -3106,17 +3570,17 @@ Your email will be used to verify your account. DlgRollDice - + Number of sides: - + Number of dice: - + Roll Dice @@ -3167,12 +3631,12 @@ Your email will be used to verify your account. DlgSettings - + Unknown Error loading card database - + Your card database is invalid. Cockatrice may not function correctly with an invalid database @@ -3183,7 +3647,7 @@ Would you like to change your database location setting? - + Your card database version is too old. This can cause problems loading card information or images @@ -3194,7 +3658,7 @@ Would you like to change your database location setting? - + Your card database did not finish loading Please file a ticket at https://github.com/Cockatrice/Cockatrice/issues with your cards.xml attached @@ -3203,21 +3667,21 @@ Would you like to change your database location setting? - + File Error loading your card database. Would you like to change your database location setting? - + Your card database was loaded but contains no cards. Would you like to change your database location setting? - + Unknown card database load status Please file a ticket at https://github.com/Cockatrice/Cockatrice/issues @@ -3226,59 +3690,64 @@ Would you like to change your database location setting? - - - + + + Error - + The path to your deck directory is invalid. Would you like to go back and set the correct path? - + The path to your card pictures directory is invalid. Would you like to go back and set the correct path? - + Settings - + General - + Appearance - + User Interface - + + Storage + + + + Card Sources - + Chat - + Sound - + Shortcuts @@ -3370,9 +3839,9 @@ You can always change this behavior in the 'General' settings tab. - - - + + + Error @@ -3424,31 +3893,31 @@ Please visit the download page to update manually. - + Update Available - + A new version of Cockatrice is available! - + New version - + Released - + Changelog @@ -3458,50 +3927,50 @@ Please visit the download page to update manually. - + Unfortunately, the automatic updater failed to find a compatible download. You may have to manually download the new version. - + Please check the <a href="%1">releases page</a> on our Github and download the build for your system. - - - + + + Update Error - + An error occurred while checking for updates: - + An error occurred while downloading an update: - + Installing... - + Cockatrice is unable to open the installer. - + Try to update manually by closing Cockatrice and running the installer. - + Download location @@ -3591,67 +4060,67 @@ You may have to manually download the new version. DrawProbabilityWidget - + Draw Probability - + Probability of drawing - + Card Name - + Type - + Subtype - + Mana Value - + At least - + Exactly - + card(s) having drawn at least - + cards - + Category - + Qty - + Odds (%) @@ -3765,7 +4234,7 @@ You may have to manually download the new version. FilterBuilder - + Type your filter here @@ -3796,22 +4265,22 @@ You may have to manually download the new version. GameEventHandler - + kicked by game host or moderator - + player left the game - + player disconnected from server - + reason unknown @@ -3819,140 +4288,140 @@ You may have to manually download the new version. GameSelector - - - - - - - - - + + + + + + + + + Error - + Please join the appropriate room first. - + Wrong password. - + Spectators are not allowed in this game. - + The game is already full. - + The game does not exist any more. - + This game is only open to registered users. - + This game is only open to its creator's buddies. - + You are being ignored by the creator of this game. - + Join Game - + Spectate Game - + Game Information - + Join Game as Judge - + Spectate Game as Judge - + Join game - + Password: - + Please join the respective room first. - + &Filter games - + C&lear filter - + C&reate - + &Join - + Join as judge - + J&oin as spectator - + Join as judge spectator - + Games shown: %1 / %2 - + Games @@ -3960,32 +4429,32 @@ You may have to manually download the new version. GameSelectorQuickFilterToolBar - + All types - + Filter by game name... - + Filter by game type/format - + Hide games not created by buddies - + Hide full games - + Hide started games @@ -3993,12 +4462,12 @@ You may have to manually download the new version. GamesModel - + >1 day - + %1%2 hr short age in hours @@ -4007,12 +4476,12 @@ You may have to manually download the new version. - + new - + %1%2 min short age in minutes @@ -4021,83 +4490,83 @@ You may have to manually download the new version. - + password - + buddies only - + reg. users only - + open decklists - + can chat - + see hands - + can see hands - + not allowed - + Room - + Age - + Description - + Creator - + Type - + Restrictions - + Players - + Spectators @@ -4105,143 +4574,158 @@ You may have to manually download the new version. GeneralSettingsPage - + Reset all paths - + All paths have been reset - - - - - - - + + + + + + + Choose path - - Personal settings + + Language settings - + Language: - + + Version settings + + + + + Card database + + + + + Startup settings + + + + Paths (editing disabled in portable mode) - + Paths - + How to help with translations - + Decks directory: - + Filters directory: - + Replays directory: - + Pictures directory: - + Card database: - + Custom database directory: - + Token database: - + Update channel - + Check for client updates on startup - + Check for card database updates on startup - + Don't check - + Prompt for update - + Always update in the background - + Check for card database updates every - + days - + Notify if a feature supported by the server is missing in my client - + Automatically run Oracle when running a new version of Cockatrice - + Show tips on startup - + Last update check on %1 (%2 days ago) @@ -4249,47 +4733,47 @@ You may have to manually download the new version. GraveyardMenu - + &Graveyard - + &View graveyard - + &Move graveyard to... - + &Top of library - + &Bottom of library - + &All players - + &Hand - + &Exile - + Reveal random card to... @@ -4297,88 +4781,88 @@ You may have to manually download the new version. HandMenu - + &Hand - + &View hand - + Sort hand by... - + Name - + Type - + Mana Value - + Take &mulligan (Choose hand size) - + Take mulligan (Same hand size) - + Take mulligan (Hand size - 1) - + &Move hand to... - + &Top of library - + &Bottom of library - + &Graveyard - + &Exile - + &Reveal hand to... - - + + All players - + Reveal r&andom card to... @@ -4386,52 +4870,52 @@ You may have to manually download the new version. HomeWidget - + Create New Deck - + Browse Decks - + Browse Card Database - + Browse EDHRec - + Browse Archidekt - + View Replays - + Quit - + Connecting... - + Connect - + Play @@ -4439,213 +4923,213 @@ You may have to manually download the new version. LibraryMenu - + &Library - + &View library - + View &top cards of library... - + View bottom cards of library... - + Reveal &library to... - + Lend library to... - + Reveal &top cards to... - + &Top of library... - + &Bottom of library... - + &Always reveal top card - + &Always look at top card - + &Open deck in deck editor - + &Draw card - + D&raw cards... - + &Undo last draw - + Shuffle - + &Play top card - + Play top card &face down - + Put top card on &bottom - + Move top card to grave&yard - + Move top card to e&xile - + Move top cards to &graveyard... - + Move top cards to graveyard face down... - + Move top cards to &exile... - + Move top cards to exile face down... - + Put top cards on stack &until... - + Shuffle top cards... - + &Draw bottom card - + D&raw bottom cards... - + &Play bottom card - + Play bottom card &face down - + Move bottom card to grave&yard - + Move bottom card to e&xile - + Move bottom cards to &graveyard... - + Move bottom cards to graveyard face down... - + Move bottom cards to &exile... - + Move bottom cards to exile face down... - + Put bottom card on &top - + Shuffle bottom cards... - - + + &All players - + Reveal top cards of library - + Number of cards: (max. %1) @@ -4653,655 +5137,300 @@ You may have to manually download the new version. MainWindow - - - The server has reached its maximum user capacity, please check back later. - - - - - There are too many concurrent connections from your address. - - - - - Banned by moderator - - - - - Expected end time: %1 - - - - - This ban lasts indefinitely. - - - - - Scheduled server shutdown. - - - - - - Invalid username. - - - - - You have been logged out due to logging in at another location. - - - - - Connection closed - - - - - The server has terminated your connection. -Reason: %1 - - - - - The server is going to be restarted in %n minute(s). -All running games will be lost. -Reason for shutdown: %1 - - - - - - - - Scheduled server shutdown - - - - - - Success - - - - - Registration accepted. -Will now login. - - - - - Account activation accepted. -Will now login. - - - - - + + Player %1 - + Load replay - + About Cockatrice - + Version - + Cockatrice Webpage - + Project Manager: - + Past Project Managers: - + Developers: - + Our Developers - + Help Develop! - + Translators: - + Our Translators - + Help Translate! - + Support: - + Report an Issue - + Troubleshooting - + F.A.Q. - - - - - - - - - - - - - - - - - - - - + + Error - - Server timeout - - - - - Failed Login - - - - - Your client seems to be missing features this server requires for connection. - - - - - To update your client, go to 'Help -> Check for Client Updates'. - - - - - Incorrect username or password. Please check your authentication information and try again. - - - - - There is already an active session using this user name. -Please close that session first and re-login. - - - - - - You are banned until %1. - - - - - - You are banned indefinitely. - - - - - This server requires user registration. Do you want to register now? - - - - - This server requires client IDs. Your client is either failing to generate an ID or you are running a modified client. -Please close and reopen your client to try again. - - - - - An internal error has occurred, please close and reopen Cockatrice before trying again. -If the error persists, ensure you are running the latest version of the software and if needed contact the software developers. - - - - - Account activation - - - - - Your account has not been activated yet. -You need to provide the activation token received in the activation email. - - - - - Server Full - - - - - Unknown login error: %1 - - - - - - -This usually means that your client version is out of date, and the server sent a reply your client doesn't understand. - - - - - Your username must respect these rules: - - - - - is %1 - %2 characters long - - - - - can %1 contain lowercase characters - - - - - - - - NOT - - - - - can %1 contain uppercase characters - - - - - can %1 contain numeric characters - - - - - can contain the following punctuation: %1 - - - - - first character can %1 be a punctuation mark - - - - - no unacceptable language as specified by these server rules: - note that the following lines will not be translated - - - - - can not contain any of the following words: %1 - - - - - can not match any of the following expressions: %1 - - - - - You may only use A-Z, a-z, 0-9, _, ., and - in your username. - - - - - - - - - - Registration denied - - - - - Registration is currently disabled on this server - - - - - There is already an existing account with the same user name. - - - - - It's mandatory to specify a valid email address when registering. - - - - - It appears you are attempting to register a new account on this server yet you already have an account registered with the email provided. This server restricts the number of accounts a user can register per address. Please contact the server operator for further assistance or to obtain your credential information. - - - - - Password too short. - - - - - Registration failed for a technical problem on the server. - - - - - The connection to the server has been lost. - - - - - Unknown registration error: %1 - - - - - Account activation failed - - - - - Socket error: %1 - - - - - You are trying to connect to an obsolete server. Please downgrade your Cockatrice version or connect to a suitable server. -Local version is %1, remote version is %2. - - - - - Your Cockatrice client is obsolete. Please update your Cockatrice version. -Local version is %1, remote version is %2. - - - - - Connecting to %1... - - - - - Registering to %1 as %2... - - - - - Disconnected - - - - - Connected, logging in at %1 - - - - - - - Requesting forgotten password to %1 as %2... - - - - + &Connect... - + &Disconnect - + Start &local game... - + &Watch replay... - + &Full screen - + &Register to server... - + &Restore password... - + &Settings... - + &Exit - + A&ctions - + &Cockatrice - + C&ard Database - + &Manage sets... - + Edit custom &tokens... - + Open custom image folder - + Open custom sets folder - + Add custom sets/cards - + Reload card database - + Tabs - + &Help - + &About Cockatrice - + &Tip of the Day - + Check for Client Updates - + Check for Card Updates... - + Check for Card Updates (Automatic) - + Show Status Bar - + View &Debug Log - + Open Settings Folder - + Show/Hide - + New Version - + Congratulations on updating to Cockatrice %1! Oracle will now launch to update your card database. - + Cockatrice installed - + Congratulations on installing Cockatrice %1! Oracle will now launch to install the initial card database. - + Card database - + Cockatrice is unable to load the card database. Do you want to update your card database now? If unsure or first time user, choose "Yes" - - + + Yes - - + + No - + Open settings - + New sets found - + %n new set(s) found in the card database Set code(s): %1 Do you want to enable it/them? @@ -5311,179 +5440,153 @@ Do you want to enable it/them? - + + Yes, always enable + + + + View sets - + Welcome - + Hi! It seems like you're running this version of Cockatrice for the first time. All the sets in the card database have been enabled. Read more about changing the set order or disabling specific sets and consequent effects in the "Manage Sets" dialog. - - + Information - + A card database update is already running. - + Unable to run the card database updater: - + Card database update running. - + Failed to start. The file might be missing, or permissions might be incorrect. - + The process crashed some time after starting successfully. - + Timed out. The process took too long to respond. The last waitFor...() function timed out. - + An error occurred when attempting to write to the process. For example, the process may not be running, or it may have closed its input channel. - + An error occurred when attempting to read from the process. For example, the process may not be running. - + Unknown error occurred. - + The card database updater exited with an error: %1 - - This server supports additional features that your client doesn't have. -This is most likely not a problem, but this message might mean there is a new version of Cockatrice available or this server is running a custom or pre-release version. - -To update your client, go to Help -> Check for Updates. - - - - - - - - + + + + + Load sets/cards - + Selected file cannot be found. - + You can only import XML databases at this time. - + The new sets/cards have been added successfully. Cockatrice will now reload the card database. - + Sets/cards failed to import. - - - - - Reset Password - - - - - Your password has been reset successfully, you can now log in using the new credentials. - - - - - Failed to reset user account password, please contact the server operator to reset your password. - - - - - Activation request received, please check your email for an activation token. - - ManaBaseConfigDialog - + Mana Base Configuration - + Display type: - + pie - + bar - + combinedBar - + Filter Colors (optional): - + OK - + Cancel @@ -5555,27 +5658,27 @@ Cockatrice will now reload the card database. ManaDevotionConfigDialog - + Display type: - + pie - + bar - + combinedBar - + Filter Colors (optional): @@ -5645,297 +5748,297 @@ Cockatrice will now reload the card database. MessageLogWidget - + from play - + from their graveyard - + from exile - + from their hand - + the top card of %1's library - + the top card of their library - + from the top of %1's library - + from the top of their library - + the bottom card of %1's library - + the bottom card of their library - + from the bottom of %1's library - + from the bottom of their library - + from %1's library - + from their library - + from sideboard - + from the stack - + from custom zone '%1' - + %1 is now keeping the top card %2 revealed. - + %1 is not revealing the top card %2 any longer. - + %1 can now look at top card %2 at any time. - + %1 no longer can look at top card %2 at any time. - + %1 attaches %2 to %3's %4. - + %1 has conceded the game. - + %1 has unconceded the game. - + %1 has restored connection to the game. - + %1 has lost connection to the game. - + %1 points from their %2 to themselves. - + %1 points from their %2 to %3. - + %1 points from %2's %3 to themselves. - + %1 points from %2's %3 to %4. - + %1 points from their %2 to their %3. - + %1 points from their %2 to %3's %4. - + %1 points from %2's %3 to their own %4. - + %1 points from %2's %3 to %4's %5. - + %1 creates a face down token. - + %1 creates token: %2%3. - + %1 has loaded a deck (%2). - + %1 has loaded a deck with %2 sideboard cards (%3). - + %1 destroys %2. - + a card - + %1 gives %2 control over %3. - + %1 puts %2 into play%3 face down. - + %1 puts %2 into play%3. - + %1 puts %2%3 into their graveyard face down. - + %1 puts %2%3 into their graveyard. - + %1 exiles %2%3 face down. - + %1 exiles %2%3. - + %1 moves %2%3 to their hand. - + %1 puts %2%3 into their library. - + %1 puts %2%3 onto the bottom of their library. - + %1 puts %2%3 on top of their library. - + %1 puts %2%3 into their library %4 cards from the top. - + %1 moves %2%3 to sideboard. - + %1 plays %2%3 face down. - + %1 plays %2%3. - + %1 moves %2%3 to custom zone '%4' face down. - + %1 moves %2%3 to custom zone '%4'. - + %1 tries to draw from an empty library - + %1 draws %2 card(s). @@ -5943,12 +6046,12 @@ Cockatrice will now reload the card database. - + %1 is looking at %2. - + %1 is looking at the %4 %3 card(s) %2. top card for singular, top %3 cards for plural @@ -5957,72 +6060,72 @@ Cockatrice will now reload the card database. - + bottom - + top - + %1 turns %2 face-down. - + %1 turns %2 face-up. - + The game has been closed. - + The game has started. - + You are flooding the game. Please wait a couple of seconds. - + %1 has joined the game. - + %1 is now watching the game. - + You have been kicked out of the game. - + %1 has left the game (%2). - + %1 is not watching the game any more (%2). - + %1 is not ready to start the game any more. - + %1 shuffles their deck and draws a new hand of %2 card(s). @@ -6030,28 +6133,28 @@ Cockatrice will now reload the card database. - + %1 shuffles their deck and draws a new hand. - + You are watching a replay of game #%1. - + %1 is ready to start the game. - + cards an unknown amount of cards - + %1 card(s) a card for singular, %1 cards for plural @@ -6060,213 +6163,218 @@ Cockatrice will now reload the card database. - + %1 lends %2 to %3. - + %1 reveals %2 to %3. - + %1 reveals %2. - + %1 randomly reveals %2%3 to %4. - + %1 randomly reveals %2%3. - + %1 peeks at face down card #%2. - + %1 peeks at face down card #%2: %3. - + %1 reveals %2%3 to %4. - + %1 reveals %2%3. - + %1 reversed turn order, now it's %2. - + reversed - + normal - + Heads - + Tails - + %1 flipped a coin. It landed as %2. - + %1 rolls a %2 with a %3-sided die. - + %1 flips %2 coins. There are %3 heads and %4 tails. - + %1 rolls a %2-sided dice %3 times: %4. - + %1's turn. - + %1 sets annotation of %2 to %3. - - %1 places %2 "%3" counter(s) on %4 (now %5). + + %1 places %2 %3%4 counter(s) on %5 (now %6). - - %1 removes %2 "%3" counter(s) from %4 (now %5). + + %1 removes %2 %3%4 counter(s) from %5 (now %6). - + + %1 failed to undo their last draw. + + + + %1 sets counter %2 to %3 (%4%5). - + %1 sets %2 to not untap normally. - + %1 sets %2 to untap normally. - + %1 removes the PT of %2. - + %1 changes the PT of %2 from nothing to %4. - + %1 changes the PT of %2 from %3 to %4. - + %1 has locked their sideboard. - + %1 has unlocked their sideboard. - + %1 taps their permanents. - + %1 untaps their permanents. - + %1 taps %2. - + %1 untaps %2. - + %1 shuffles %2. - + %1 shuffles the bottom %3 cards of %2. - + %1 shuffles the top %3 cards of %2. - + %1 shuffles cards %3 - %4 of %2. - + %1 unattaches %2. - + %1 undoes their last draw. - + %1 undoes their last draw (%2). @@ -6274,110 +6382,115 @@ Cockatrice will now reload the card database. MessagesSettingsPage - + Word1 Word2 Word3 - + Add New Message - + Edit Message - + Remove Message - + Add message - - + + Message: - + Edit message - + Chat settings - + Custom alert words - + Enable chat mentions - + Enable mention completer - + In-game message macros - + How to use in-game message macros - + Ignore chat room messages sent by unregistered users - + Ignore private messages sent by unregistered users - - + + Ignore private messages sent by non-buddy users + + + + + Invert text color - + Enable desktop notifications for private messages - + Enable desktop notification for mentions - + Enable room message history on join - - + + (Color is hexadecimal) - + Separate words with a space, alphanumeric characters only @@ -6385,42 +6498,42 @@ Cockatrice will now reload the card database. MoveMenu - + Move to - + &Top of library in random order - + X cards from the top of library... - + &Bottom of library in random order - + T&able - + &Hand - + &Graveyard - + &Exile @@ -6433,47 +6546,47 @@ Cockatrice will now reload the card database. - + Mana Value - + Color(s) - + Loyalty - + Main Card Type - + Mana Cost - + P/T - + Side - + Layout - + Color Identity @@ -6496,6 +6609,152 @@ Cockatrice will now reload the card database. + + PaletteEditorDialog + + + + Reset + + + + + + Apply + + + + + + Save && Apply + + + + + ▼ Edit Palette + + + + + + ▶ Edit Palette + + + + + Palette Editor — %1 + + + + + <b>Palette Editor</b> &nbsp;·&nbsp; %1 + + + + + This theme ships no default palette files + + + + + Replace current colours with the theme author's defaults + + + + + Switch between the light and dark palette files + + + + + Editing: + + + + + Show or hide the per-role colour grid for manual tweaks + + + + + ↺ Revert to theme default + + + + + Discard unsaved edits and restore the last saved palette + + + + + Preview this palette without saving to disk + + + + + Write palette-%1.toml and reload the theme + + + + + Cannot save: this theme has no directory on disk + + + + + Save failed + + + + + Could not write %1 to: +%2 + + + + + No default found + + + + + No default palette file found for the "%1" scheme. + + + + + PaletteGridWidget + + + Active + + + + + Disabled + + + + + Inactive + + + + + Normal interactive state + + + + + Widget is disabled / not interactive + + + + + Window is in background / unfocused + + + Phase @@ -6562,57 +6821,57 @@ Cockatrice will now reload the card database. PhasesToolbar - + Untap step - + Upkeep step - + Draw step - + First main phase - + Beginning of combat step - + Declare attackers step - + Declare blockers step - + Combat damage step - + End of combat step - + Second main phase - + End of turn step @@ -6620,7 +6879,7 @@ Cockatrice will now reload the card database. PictureLoader - + en code for scryfall's language property, not available for all languages @@ -6629,151 +6888,142 @@ Cockatrice will now reload the card database. PlayerActions - - View top cards of library - - - - - - - - - - - - - Number of cards: (max. %1) - - - - - View bottom cards of library - - - - - Shuffle top cards of library - - - - - Shuffle bottom cards of library - - - - - Draw hand - - - - - 0 and lower are in comparison to current hand size - - - - - Draw cards - - - - - - - + + + + grave - - - - + + + + exile + + + PlayerDialogs - + + View top cards of library + + + + + + + + + + + + + Number of cards: (max. %1) + + + + + View bottom cards of library + + + + + Shuffle top cards of library + + + + + Shuffle bottom cards of library + + + + + Draw hand + + + + + 0 and lower are in comparison to current hand size + + + + + Draw cards + + + + Move top cards to %1 - + Move bottom cards to %1 - + Draw bottom cards - - - C&reate another %1 token - - - - + Create tokens - - + Number: - + Place card X cards from top of library - + Which position should this card be placed: - + (max. %1) - + Change power/toughness - + Change stats to: - + Set annotation - + Please enter the new annotation: - - - Set counters - - PlayerMenu - + Player "%1" - + &Counters @@ -6803,7 +7053,7 @@ This setting means you'll only see the default printing for each card, inst - + Printing Selector @@ -6811,22 +7061,22 @@ This setting means you'll only see the default printing for each card, inst PrintingSelectorCardOverlayWidget - + Preference - + Pin Printing - + Unpin Printing - + Show Related cards @@ -6897,57 +7147,57 @@ This setting means you'll only see the default printing for each card, inst PtMenu - + Power / toughness - + &Increase power - + &Decrease power - + I&ncrease toughness - + D&ecrease toughness - + In&crease power and toughness - + Dec&rease power and toughness - + Increase power and decrease toughness - + Decrease power and increase toughness - + Set &power and toughness... - + Reset p&ower and toughness @@ -6955,37 +7205,37 @@ This setting means you'll only see the default printing for each card, inst QMenuBar - + Services - + Hide %1 - + Hide Others - + Show All - + Preferences... - + Quit %1 - + About %1 @@ -6993,17 +7243,17 @@ This setting means you'll only see the default printing for each card, inst QObject - + Cockatrice card database (*.xml) - + All files (*.*) - + Cockatrice replays (*.cor) @@ -7063,110 +7313,177 @@ Are you sure you would like to disable this feature? QPlatformTheme - + OK - + Save - + Save All - + Open - + &Yes - + Yes to &All - + &No - + N&o to All - + Abort - + Retry - + Ignore - + Close - + Cancel - + Discard - + Help - + Apply - + Reset - + Restore Defaults + + QuickSetupPanel + + + %1% + + + + + <b>Quick Setup</b> + + + + + Generate all palette roles automatically from a single accent colour + + + + + Accent: + + + + + Primary hue. Used directly for highlights and links. +At high intensity it also tints buttons and backgrounds. + + + + + Intensity: + + + + + Subtle + + + + + Full colour + + + + + 0–30 Subtle tint — only highlights and links change hue +30–70 Accented — buttons, tooltips, and borders join in +70–100 Full colour — backgrounds, everything + + + + + 70% + + + + + Generate ↓ + + + + + Derive all palette roles from the accent colour above. +Fine-tune individual colours in the grid afterwards. + + + RemoteDeckList_TreeModel - + Name - + ID - + Upload time @@ -7174,32 +7491,32 @@ Are you sure you would like to disable this feature? RemoteReplayList_TreeModel - + ID - + Name - + Players - + Keep - + Time started - + Duration (sec) @@ -7207,37 +7524,37 @@ Are you sure you would like to disable this feature? RfgMenu - + &Exile - + &View exile - + &Move exile to... - + &Top of library - + &Bottom of library - + &Hand - + &Graveyard @@ -7283,7 +7600,7 @@ Are you sure you would like to disable this feature? SayMenu - + S&ay @@ -7324,27 +7641,27 @@ Are you sure you would like to disable this feature? SetsModel - + Enabled - + Set type - + Set code - + Long name - + Release date @@ -7352,53 +7669,53 @@ Are you sure you would like to disable this feature? ShortcutSettingsPage - - + + Restore all default shortcuts - + Do you really want to restore all default shortcuts? - + Clear all default shortcuts - + Do you really want to clear all shortcuts? - + Section: - + Action: - + Shortcut: - + How to set custom shortcuts - + Clear all shortcuts - + Search by shortcut name @@ -7452,12 +7769,12 @@ Please check your shortcut settings! SideboardMenu - + &Sideboard - + &View sideboard @@ -7465,27 +7782,27 @@ Please check your shortcut settings! SoundSettingsPage - + Enable &sounds - + Current sounds theme: - + Test system sound engine - + Sound settings - + Master volume @@ -7557,16 +7874,146 @@ Please check your shortcut settings! - + No reply received from the tag update server. - + Invalid reply received from the tag update server. + + StorageSettingsPage + + + + + Success + + + + + Cached card pictures have been reset. + + + + + Downloaded card pictures have been reset. + + + + + Error + + + + + One or more downloaded card pictures could not be cleared. + + + + + In-memory (currently loaded) card pictures have been reset. + + + + + Card Picture Loader Caching Method: + + + + + The network cache is the preferred way of storing images. Downloaded images are stored here until the size of the cache exceeds the configured size. Cockatrice automatically monitors this cache and deletes the least recently seen card images to ensure the cache does not exceed the configured size. + + + + + Writing card images directly to a folder on your hard drive is another way of storing images. This does not change how Cockatrice accesses or downloads images. Cockatrice will NOT automatically monitor and clear this folder, so if you enable this option, it is up to you to ensure sufficient available space. It should also be noted that if a provider outage causes you to download the wrong picture (i.e. wrong printing) you will be stuck with it until you manually delete the file, as opposed to using the network cache, which automatically rotates and thus correct errors after a while. + + + + + This is the in-memory picture cache used by the application at runtime. It determines how much memory (RAM) Cockatrice can use before it has to fetch card images from the hard disk again. Increasing this will allow more card images to be displayed at once but shouldn't be necessary. Clearing this will make Cockatrice reload all images from the network cache or the disk. + + + + + Delete Cached Images + + + + + Delete Saved Images + + + + + Clear In-Memory Images + + + + + Card Picture Loader Cache Method + + + + + Network Cache + + + + + Filesystem + + + + + In-Memory Picture Cache + + + + + Network Cache Size: + + + + + On-disk cache for downloaded pictures + + + + + Redirect Cache TTL: + + + + + How long cached redirects for urls are valid for. + + + + + Picture Cache Size: + + + + + In-memory cache for pictures not currently on screen + + + + + Naming scheme: + + + + + Day(s) + + + TabAccount @@ -7702,117 +8149,117 @@ Please check your shortcut settings! TabArchidekt - - + + Desc. - - + + AND - - + + Require ALL selected colors - - + + Deck name... - - + + Owner... - - + + Packages - - + + Advanced Filters - + Bracket: - - + + Any - - + + Contains card... - - + + Commander... - - + + Tag... - - + + Deck Size - + Cards: - - + + Asc. - + Sort by: - + Filter by: - + Display Settings - - + + Search - - + + Formats @@ -7822,6 +8269,54 @@ Please check your shortcut settings! + + TabCardArtRules + + + Card: + + + + + ProviderId: + + + + + Mode: + + + + + Reason: + + + + + Type a card name... + + + + + Add rule + + + + + Remove rule + + + + + Refresh + + + + + Card Art Rules + + + TabDeckEditor @@ -7870,7 +8365,7 @@ Please check your shortcut settings! - + Deck: %1 @@ -7878,55 +8373,49 @@ Please check your shortcut settings! TabDeckEditorVisual - + Visual Deck: %1 - + &Visual Deck Editor - - + + Card Info - - + + Deck - - - Filters - - - - + &View - + Printing - + Visible - + Floating - + Reset layout @@ -7934,22 +8423,22 @@ Please check your shortcut settings! TabDeckEditorVisualTabWidget - + Visual Deck View - + Visual Database Display - + Deck Analytics - + Sample Hand @@ -7957,133 +8446,133 @@ Please check your shortcut settings! TabDeckStorage - + Local file system - + Server deck storage - - + + Open in deck editor - + Rename deck or folder - + Upload deck - + Download deck - - - + + + New folder - + Delete - + Open decks folder - + Rename local folder - + Rename local file - + New name: - - - - + + + + Error - + Rename failed - - + + Invalid deck file - + Enter deck name - + This decklist does not have a name. Please enter a name: - + Unnamed deck - + Failed to upload deck to server - + Delete local file - + Are you sure you want to delete the selected files? - + Delete remote decks - + Are you sure you want to delete the selected decks? - - + + Name of new folder: @@ -8155,191 +8644,191 @@ Please enter a name: TabGame - - - + + + Replay - - + + Game - - + + Player List - - + + Card Info - - + + Messages - - + + Replay Timeline - + &Phases - + &Game - + Next &phase - + Next phase with &action - + Next &turn - + Reverse turn order - + &Remove all local arrows - + Rotate View Cl&ockwise - + Rotate View Co&unterclockwise - + Game &information - + Un&concede - - - + + + &Concede - + &Leave game - + C&lose replay - + &Focus Chat - + &Say: - + Selected cards - + &View - + Visible - + Floating - + Reset layout - + Concede - + Are you sure you want to concede this game? - + Unconcede - + You have already conceded. Do you want to return to this game? - + Leave game - + Are you sure you want to leave this game? - + A player has joined game #%1 - + %1 has joined the game - + You have been kicked out of the game. @@ -8398,114 +8887,114 @@ Please enter a name: - + Username: - + IP Address: - + Game Name: - + GameID: - + Message: - + Main Room - + Game Room - + Private Chat - + Past X Days: - + Today - + Last Hour - + Maximum Results: - + At least one filter is required. The more information you put in, the more specific your results will be. - + Get User Logs - + Clear Filters - + Filters - + Log Locations - + Date Range - + Maximum Results - - + + Message History - + Failed to collect message history information. - + There are no messages for the selected filters. @@ -8523,27 +9012,27 @@ The more information you put in, the more specific your results will be. - + %1 - Private chat - + This user is ignoring you, they cannot see your messages in main chat and you cannot join their games. - + Private message from - + %1 has left the server. - + %1 has joined the server. @@ -8551,180 +9040,180 @@ The more information you put in, the more specific your results will be. TabReplays - + Local file system - + Server replay storage - - + + Watch replay - + Rename - - + + New folder - - + + Delete - + Open replays folder - + Download replay - + Toggle expiration lock - + Get replay share code - - + + Look up replay by share code - + Rename local folder - + Rename local file - + New name: - + Error - + Rename failed - + Name of new folder: - + Delete local file - + Are you sure you want to delete the selected files? - + Are you sure you want to delete the selected replays? - + Failed to get code - - + + Either this server does not support replay sharing, or does not permit replay sharing for you. - - - + + + Failed - + Could not get replay code - + Replay Share Code - + Others can use this code to add the replay to their list of remote replays: %1 - + Copy to clipboard - + Replay share code - + Replay code found - + Replay was added, or you already had access to it. - + Replay code not found - + Failed to submit code - + Unexpected error - + Delete remote replay @@ -8737,47 +9226,62 @@ The more information you put in, the more specific your results will be. TabRoom - + + Friends + + + + + Online + + + + + Ignored + + + + &Say: - + Chat - + &Room - + &Leave room - + &Clear chat - + Chat Settings... - + mentioned you. - + Click to view - + You are flooding the chat. Please wait a couple of seconds. @@ -8790,30 +9294,30 @@ The more information you put in, the more specific your results will be. - - - - + + + + Error - + Failed to join the server room: it doesn't exist on the server. - + The server thinks you are in the server room but your client is unable to display it. Try restarting your client. - + You do not have the required permission to join this server room. - + Failed to join the server room due to an unknown error: %1. @@ -8821,97 +9325,97 @@ The more information you put in, the more specific your results will be. TabSupervisor - + Deck Editor - + Visual Deck Editor - + EDHRec - + Archidekt - + Home - + &Visual Deck Storage - + Visual Database Display - + Server - + Account - + Deck Storage - + Game Replays - + Administration - + Logs - + Are you sure? - + There are still open games. Are you sure you want to quit? - + Click to view - + Your buddy %1 has signed on! - + Unknown Event - + The server has sent you a message that your client does not understand. This message might mean there is a new version of Cockatrice available or this server is running a custom or pre-release version. @@ -8919,38 +9423,38 @@ To update your client, go to Help -> Check for Updates. - + Idle Timeout - + You are about to be logged out due to inactivity. - + Promotion - + You have been promoted. Please log out and back in for changes to take effect. - + Warned - + You have received a warning due to %1. Please refrain from engaging in this activity or further actions may be taken against you. If you have any questions, please private message a moderator. - + You have received the following message from the server. (custom messages like these could be untranslated) @@ -8959,12 +9463,12 @@ Please refrain from engaging in this activity or further actions may be taken ag TabVisualDatabaseDisplay - + Database Display - + Visual Database Display @@ -9001,42 +9505,42 @@ Please refrain from engaging in this activity or further actions may be taken ag TranslateCounterName - + Life - + White - + Blue - + Black - + Red - + Green - + Colorless - + Other @@ -9044,11 +9548,69 @@ Please refrain from engaging in this activity or further actions may be taken ag UpdateDownloader - + Could not open the file for reading. + + UserCardArtSettingsDialog + + + Card Art Settings + + + + + Type a card name... + + + + + Card name: + + + + + Card ProviderId: + + + + + Left margin (%): + + + + + Right margin (%): + + + + + Vertical offset: + + + + + Zoom: + + + + + Parameters + + + + + Preview + + + + + Remove Banner Card + + + UserContextMenu @@ -9166,7 +9728,7 @@ Please refrain from engaging in this activity or further actions may be taken ag - + Ban History @@ -9181,77 +9743,87 @@ Please refrain from engaging in this activity or further actions may be taken ag - + Failed to collect ban information. - - - + + + Warning History - + Warning Time;Moderator;User Name;Reason - + User has never been warned. - + Failed to collect warning information. - + Failed to get admin notes. - - + + Success - + Successfully promoted user. - + Successfully demoted user. - - - + + Kick Player + + + + + Are you sure you want to kick this player from the game? + + + + + + Failed - + Failed to promote user. - + Failed to demote user. - + Copy hash to clipboard - + Remove this user's messages @@ -9259,109 +9831,116 @@ Please refrain from engaging in this activity or further actions may be taken ag UserInfoBox - + Location: - + Account Age: - + Edit - + Change password - + Change avatar - + Administrator - + Moderator - + Registered user - - + + Unregistered user - + Judge - + Unknown - + The entered password does not match your account. - - - + + + + Information - + User information updated. - - - - - - - - - - + + + + + + + + + + + Error - + User Information - + Real Name: - + User Level: + + + Edit Banner Card + + - + %n Year(s), amount of years (only shown if more than 0) @@ -9370,7 +9949,7 @@ Please refrain from engaging in this activity or further actions may be taken ag - + %10%n Day(s) %20 amount of years (if more than 0), amount of days, date in local short format @@ -9379,212 +9958,433 @@ Please refrain from engaging in this activity or further actions may be taken ag - + Enter Password - + Password verification is required in order to change your email address - - - + + + An error occurred while trying to update your user information. - - This server does not permit you to update your user informations. + + The selected card is blacklisted on this server or another error occurred. - - Password changed. + + Banner card removed. - - This server does not permit you to change your password. - - - - - The new password is too short. - - - - - The old password is incorrect. - - - - - Avatar updated. - - - - - This server does not permit you to update your avatar. + + Banner card updated. - An error occured while trying to updater your avatar. + This server does not permit you to update your user informations. + + + + + Password changed. + + + + + This server does not permit you to change your password. + + + + + The new password is too short. + + + + + The old password is incorrect. + + + + + Avatar updated. + + + + + This server does not permit you to update your avatar. + + + + + An error occured while trying to update your avatar. + + + + + This server does not permit you to update your user informations. + An error occured while trying to updater your avatar. + + + + + UserInfoPopup + + + + Games + + + + + Chat + + + + + Open private chat + + + + + Profile + + + + + View user profile + + + + + Show this user's games + + + + + − Buddy + + + + + Remove from buddy list + + + + + + Buddy + + + + + Add to buddy list + + + + + − Ignore + + + + + Remove from ignore list + + + + + + Ignore + + + + + Add to ignore list + + + + + Ban + + + + + Ban from server + + + + + Warn + + + + + Warn user + + + + + Ban log + + + + + View ban history + + + + + Warn log + + + + + View warning history + + + + + Notes + + + + + View admin notes + + + + + − Mod + + + + + Demote from moderator + + + + + + Mod + + + + + Promote to moderator + + + + + − Judge + + + + + Demote from judge + + + + + + Judge + + + + + Promote to judge + + + + + Join game + + + + + Spectate + + + + + + Loading games… + + + + + Could not load games. + + + + + No active games. UserInterfaceSettingsPage - + General interface settings - + &Double-click cards to play them (instead of single-click) - + &Clicking plays all selected cards (instead of just the clicked card) - + &Play all nonlands onto the stack (not the battlefield) by default - + Do not delete &arrows inside of subphases - + Close card view window when last card is removed - + Auto focus search bar when card view window is opened - + Annotate card text on tokens - - Show selection counter during drag selection + + Show selection count during drag selection - - Show total selection counter + + Show total selection count - + + Show subtype breakdown in selection tally + + + + Use tear-off menus, allowing right click menus to persist on screen - + + Keep game chat focused when clicking in game (Note: disables card view search bar) + + + + Notifications settings - + Enable notifications in taskbar - + Notify in the taskbar for game events while you are spectating - + Notify in the taskbar when users in your buddy list connect - + Animation settings - + &Tap/untap animation - + Deck editor/storage settings - + Open deck in new tab by default - + Use visual deck storage in game lobby - + Use selection animation for Visual Deck Storage - + When adding a tag in the visual deck storage to a .txt deck: - + do nothing - + ask to convert to .cod - + always convert to .cod - + Default deck editor type - + Classic Deck Editor - + Visual Deck Editor - + Replay settings - + Buffer time for backwards skip via shortcut: @@ -9592,22 +10392,22 @@ Please refrain from engaging in this activity or further actions may be taken ag UserListWidget - + Users connected to server: %1 - + Users in this room: %1 - + Buddies online: %1 / %2 - + Ignored users online: %1 / %2 @@ -9615,35 +10415,45 @@ Please refrain from engaging in this activity or further actions may be taken ag UtilityMenu - + Increment all card counters - + &Untap all permanents - + R&oll die... - + + Flip coin + + + + &Create token... - + C&reate another token - + Cr&eate predefined token + + + C&reate another %1 token + + VisualDatabaseDisplayColorFilterWidget @@ -9695,72 +10505,72 @@ Please refrain from engaging in this activity or further actions may be taken ag VisualDatabaseDisplayFilterToolbarWidget - + Sort by - + Filter by - + Save and load filters - + Filter by exact card name - + Filter by card main-type - + Filter by card sub-type - + Filter by set - + Filter by format legality - + Save/Load - + Name - + Main Type - + Sub Type - + Sets - + Formats @@ -9768,22 +10578,22 @@ Please refrain from engaging in this activity or further actions may be taken ag VisualDatabaseDisplayFormatLegalityFilterWidget - + Show formats with at least: - + cards - + Do not display formats with less than this amount of cards in the database - + Filter mode (AND/OR/NOT conjunctions of filters) @@ -9801,32 +10611,32 @@ Please refrain from engaging in this activity or further actions may be taken ag VisualDatabaseDisplayMainTypeFilterWidget - + Show main types with at least: - + cards - + Do not display card main-types with less than this amount of cards in the database - + Filter mode (AND/OR/NOT conjunctions of filters) - + Mode: Exact Match - + Mode: Includes @@ -9834,27 +10644,27 @@ Please refrain from engaging in this activity or further actions may be taken ag VisualDatabaseDisplayNameFilterWidget - + Filter by name... (Exact match) - + Load from Deck - + Apply all card names in currently loaded deck as exact match name filters - + Load from Clipboard - + Apply all card names in clipboard as exact match name filters @@ -9862,7 +10672,7 @@ Please refrain from engaging in this activity or further actions may be taken ag VisualDatabaseDisplayRecentSetFilterSettingsWidget - + Filter to most recent sets @@ -9870,19 +10680,19 @@ Please refrain from engaging in this activity or further actions may be taken ag VisualDatabaseDisplaySetFilterWidget - + Search sets... - - + + Mode: Exact Match - - + + Mode: Includes @@ -9890,37 +10700,37 @@ Please refrain from engaging in this activity or further actions may be taken ag VisualDatabaseDisplaySubTypeFilterWidget - + Search subtypes... - + Show sub types with at least: - + cards - + Do not display card sub-types with less than this amount of cards in the database - + Filter mode (AND/OR/NOT conjunctions of filters) - + Mode: Exact Match - + Mode: Includes @@ -9928,23 +10738,23 @@ Please refrain from engaging in this activity or further actions may be taken ag VisualDatabaseDisplayWidget - + Search by card name (or search expressions) - + Visual - + Loading database ... - + Clear all filters @@ -9983,17 +10793,17 @@ Please refrain from engaging in this activity or further actions may be taken ag - + Toggle Layout: Overlap - + Change how cards are displayed within zones (i.e. overlapped or fully visible.) - + Toggle Layout: Flat @@ -10022,17 +10832,17 @@ Please refrain from engaging in this activity or further actions may be taken ag VisualDeckEditorWidget - + Type a card name here for suggestions from the database... - + Quick search and add card - + Search for closest match in the database (with auto-suggestions) and add preferred printing to the deck on pressing enter @@ -10150,43 +10960,43 @@ Please refrain from engaging in this activity or further actions may be taken ag WarningDialog - + Which warning would you like to send? - + Redact all messages from this user in all rooms - + &OK - + &Cancel - + Warn user for misconduct - - + + Error - + User name to send a warning to can not be blank, please specify a user to warn. - + Warning to use can not be blank, please select a valid warning to send. @@ -10194,133 +11004,133 @@ Please refrain from engaging in this activity or further actions may be taken ag WndSets - + Move selected set to the top - + Move selected set up - + Move selected set down - + Move selected set to the bottom - + Search by set name, code, or type - + Default order - + Restore original art priority order - + Enable all sets - + Disable all sets - + Enable selected set(s) - + Disable selected set(s) - + Deck Editor - + Use CTRL+A to select all sets in the view. - + Only cards in enabled sets will appear in the card list of the deck editor. - + Image priority is decided in the following order: - + first the CUSTOM Folder (%1), then the Enabled Sets in this dialog (Top to Bottom) %1 is a link to the wiki - + Include cards rebalanced for Alchemy [requires restart] - + Card Art - + How to use custom card art - + Hints - + Note - + Sorting by column allows you to find a set while not changing set priority. - + To enable ordering again, click the column header until this message disappears. - + Use the current sorting as the set priority instead - + Sorts the set priority using the same column - + Manage sets @@ -10328,72 +11138,72 @@ Please refrain from engaging in this activity or further actions may be taken ag ZoneViewWidget - + Search by card name (or search expressions) - + Ungrouped - + Group by Type - + Group by Mana Value - + Group by Color - + Unsorted - + Sort by Name - + Sort by Type - + Sort by Mana Cost - + Sort by Colors - + Sort by P/T - + Sort by Set - + shuffle when closing - + pile view @@ -10401,7 +11211,7 @@ Please refrain from engaging in this activity or further actions may be taken ag i18n - + English @@ -10409,12 +11219,12 @@ Please refrain from engaging in this activity or further actions may be taken ag main - + Connect on startup - + Debug to file @@ -10428,7 +11238,7 @@ Please refrain from engaging in this activity or further actions may be taken ag - + Deck Editor @@ -10509,7 +11319,7 @@ Please refrain from engaging in this activity or further actions may be taken ag - + Replays @@ -10624,129 +11434,129 @@ Please refrain from engaging in this activity or further actions may be taken ag - - + + Load Deck from Clipboard... - + Edit Deck in Clipboard, Annotated - + Edit Deck in Clipboard - + New Deck - + Open Custom Pictures Folder - + Print Deck... - + Delete Card - - + + Reset Layout - + Save Deck - + Save Deck as... - + Save Deck to Clipboard, Annotated - + Save Deck to Clipboard, Annotated (No Set Info) - + Save Deck to Clipboard - + Save Deck to Clipboard (No Set Info) - + Load Local Deck... - + Load Remote Deck... - + Set Ready to Start - + Toggle Sideboard Lock - + Add Green Counter - + Remove Green Counter - + Set Green Counters... - + Add Red Counter - + Remove Red Counter - + Set Red Counters... - + Add Life Counter @@ -10756,724 +11566,744 @@ Please refrain from engaging in this activity or further actions may be taken ag - + + Load deck from online service... + + + + + Load from website... + + + + Unload Deck - + Force Start - + Add Card Counter (F) - + Remove Card Counter (F) - + Set Card Counters (F)... - + Add Card Counter (E) - + Remove Card Counter (E) - + Set Card Counters (E)... - + Add Card Counter(D) - + Remove Card Counter (D) - + Set Card Counters (D)... - + Add Card Counter (C) - + Remove Card Counter (C) - + Set Card Counters (C)... - + Add Card Counter (B) - + Remove Card Counter (B) - + Set Card Counters (B)... - + Add Card Counter (A) - + Remove Card Counter (A) - + Set Card Counters (A)... - + Remove Life Counter - + Set Life Counters... - + Add White Counter - + Remove White Counter - + Set White Counters... - + Add Blue Counter - + Remove Blue Counter - + Set Blue Counters... - + Add Black Counter - + Remove Black Counter - + Set Black Counters... - + Add Colorless Counter - + Remove Colorless Counter - + Set Colorless Counters... - + Add Other Counter - + Remove Other Counter - + Set Other Counters... - + Increment all card counters - + Add Power (+1/+0) - + Remove Power (-1/-0) - + Move Toughness to Power (+1/-1) - + Add Toughness (+0/+1) - + Remove Toughness (-0/-1) - + Move Power to Toughness (-1/+1) - + Add Power and Toughness (+1/+1) - + Remove Power and Toughness (-1/-1) - + Set Power and Toughness... - + Reset Power and Toughness - + Untap - + Upkeep - + Draw - + First Main Phase - + Start Combat - + Attack - + Block - + Damage - + End Combat - + Second Main Phase - + End - + Next Phase - + Next Phase Action - + Next Turn - + Hide Card in Reveal Window - + Tap / Untap Card - + Untap All - + Toggle Skip Untapping Toggle Untap - + Turn Card Over - + Peek Card - + Play Card - + Play Card, Face Down - + Attach Card... - + Unattach Card - + Clone Card - + Create Token... - + Create All Related Tokens - + Create Another Token - + Set Annotation... - + + Reduce Life by Power + + + + Select All Cards in Zone - + Select All Cards in Row - + Select All Cards in Column - + Reveal Selected Cards to All Players - - + + Bottom of Library - - - - + + + + Exile - - - - + + + + Graveyard - - + + Hand - - + + Top of Library - - + + Battlefield, Face Down - + Battlefield - + Library - + Sideboard - + Top Cards of Library - + Bottom Cards of Library - + Close Recent View - - + + Stack - - + + Graveyard (Multiple) - - + + Graveyard (Multiple), Face Down - - + + Exile (Multiple) - - + + Exile (Multiple), Face Down - + Stack Until Found - + Draw Bottom Card - + Draw Multiple Cards from Bottom... - + Draw Arrow... - + Remove Local Arrows - + Leave Game - + Concede - + Roll Dice... - + + Flip Coin + + + + Shuffle Library - + Shuffle Top Cards of Library - + Shuffle Bottom Cards of Library - + Mulligan - + Mulligan (Same hand size) - + Mulligan (Hand size - 1) - + Draw a Card - + Draw Multiple Cards... - + Undo Draw - + Always Reveal Top Card - + Always Look At Top Card - + Sort Hand by Name - + Sort Hand by Type - + Sort Hand by Mana Value - + Reveal Hand to All Players - + Reveal Random Card to All Players - + Rotate View Clockwise - + Rotate View Counterclockwise - + Unfocus Text Box - + Focus Chat - + Clear Chat - + Refresh - + Skip Forward - + Skip Backward - + Skip Forward by a lot - + Skip Backward by a lot - + Play/Pause - + Toggle Fast Forward - + Home - + Visual Deck Storage - + Deck Storage - + Server - + Account - + Administration - + Logs diff --git a/cockatrice/src/client/network/interfaces/deck_stats_interface.cpp b/cockatrice/src/client/network/interfaces/deck_stats_interface.cpp index 0298daa6b..8689a19e9 100644 --- a/cockatrice/src/client/network/interfaces/deck_stats_interface.cpp +++ b/cockatrice/src/client/network/interfaces/deck_stats_interface.cpp @@ -6,12 +6,12 @@ #include #include #include +#include #include #include #include -DeckStatsInterface::DeckStatsInterface(CardDatabase &_cardDatabase, QObject *parent) - : QObject(parent), cardDatabase(_cardDatabase) +DeckStatsInterface::DeckStatsInterface(QObject *parent) : QObject(parent) { manager = new QNetworkAccessManager(this); connect(manager, &QNetworkAccessManager::finished, this, &DeckStatsInterface::queryFinished); @@ -70,8 +70,8 @@ void DeckStatsInterface::analyzeDeck(const DeckList &deck) void DeckStatsInterface::copyDeckWithoutTokens(const DeckList &source, DeckList &destination) { - auto copyIfNotAToken = [this, &destination](const auto node, const auto card) { - CardInfoPtr dbCard = cardDatabase.query()->getCardInfo(card->getName()); + auto copyIfNotAToken = [&destination](const auto node, const auto card) { + CardInfoPtr dbCard = CardDatabaseManager::query()->getCardInfo(card->getName()); 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/deck_stats_interface.h b/cockatrice/src/client/network/interfaces/deck_stats_interface.h index 2ec67a5a7..09bf998de 100644 --- a/cockatrice/src/client/network/interfaces/deck_stats_interface.h +++ b/cockatrice/src/client/network/interfaces/deck_stats_interface.h @@ -7,7 +7,6 @@ #ifndef DECKSTATS_INTERFACE_H #define DECKSTATS_INTERFACE_H -#include #include class QByteArray; @@ -21,8 +20,6 @@ class DeckStatsInterface : public QObject private: QNetworkAccessManager *manager; - CardDatabase &cardDatabase; - /** * Deckstats doesn't recognize token cards, and instead tries to find the * closest non-token card instead. So we construct a new deck which has no @@ -35,7 +32,7 @@ private slots: void getAnalyzeRequestData(const DeckList &deck, QByteArray &data); public: - explicit DeckStatsInterface(CardDatabase &_cardDatabase, QObject *parent = nullptr); + explicit DeckStatsInterface(QObject *parent = nullptr); void analyzeDeck(const DeckList &deck); }; diff --git a/cockatrice/src/client/network/interfaces/tapped_out_interface.cpp b/cockatrice/src/client/network/interfaces/tapped_out_interface.cpp index cd39ea251..5dc77fa2c 100644 --- a/cockatrice/src/client/network/interfaces/tapped_out_interface.cpp +++ b/cockatrice/src/client/network/interfaces/tapped_out_interface.cpp @@ -6,12 +6,12 @@ #include #include #include +#include #include #include #include -TappedOutInterface::TappedOutInterface(CardDatabase &_cardDatabase, QObject *parent) - : QObject(parent), cardDatabase(_cardDatabase) +TappedOutInterface::TappedOutInterface(QObject *parent) : QObject(parent) { manager = new QNetworkAccessManager(this); connect(manager, &QNetworkAccessManager::finished, this, &TappedOutInterface::queryFinished); @@ -97,8 +97,8 @@ void TappedOutInterface::analyzeDeck(const DeckList &deck) void TappedOutInterface::copyDeckSplitMainAndSide(const DeckList &source, DeckList &mainboard, DeckList &sideboard) { - auto copyMainOrSide = [this, &mainboard, &sideboard](const auto node, const auto card) { - CardInfoPtr dbCard = cardDatabase.query()->getCardInfo(card->getName()); + auto copyMainOrSide = [&mainboard, &sideboard](const auto node, const auto card) { + CardInfoPtr dbCard = CardDatabaseManager::query()->getCardInfo(card->getName()); if (!dbCard || dbCard->getIsToken()) { return; } diff --git a/cockatrice/src/client/network/interfaces/tapped_out_interface.h b/cockatrice/src/client/network/interfaces/tapped_out_interface.h index f1cc1cbeb..32f9369d5 100644 --- a/cockatrice/src/client/network/interfaces/tapped_out_interface.h +++ b/cockatrice/src/client/network/interfaces/tapped_out_interface.h @@ -7,8 +7,8 @@ #ifndef TAPPEDOUT_INTERFACE_H #define TAPPEDOUT_INTERFACE_H -#include -#include +#include +#include inline Q_LOGGING_CATEGORY(TappedOutInterfaceLog, "tapped_out_interface"); @@ -29,14 +29,13 @@ class TappedOutInterface : public QObject private: QNetworkAccessManager *manager; - CardDatabase &cardDatabase; void copyDeckSplitMainAndSide(const DeckList &source, DeckList &mainboard, DeckList &sideboard); private slots: void queryFinished(QNetworkReply *reply); void getAnalyzeRequestData(const DeckList &deck, QByteArray &data); public: - explicit TappedOutInterface(CardDatabase &_cardDatabase, QObject *parent = nullptr); + explicit TappedOutInterface(QObject *parent = nullptr); void analyzeDeck(const DeckList &deck); }; diff --git a/cockatrice/src/client/settings/cache_settings.cpp b/cockatrice/src/client/settings/cache_settings.cpp index 64416e5ee..b6bc8a47d 100644 --- a/cockatrice/src/client/settings/cache_settings.cpp +++ b/cockatrice/src/client/settings/cache_settings.cpp @@ -309,9 +309,11 @@ SettingsCache::SettingsCache() cardViewExpandedRowsMax = settings->value("interface/cardViewExpandedRowsMax", 20).toInt(); closeEmptyCardView = settings->value("interface/closeEmptyCardView", true).toBool(); focusCardViewSearchBar = settings->value("interface/focusCardViewSearchBar", true).toBool(); + keepGameChatFocus = settings->value("interface/keepGameChatFocus", false).toBool(); showDragSelectionCount = settings->value("interface/showlassoselectioncount", true).toBool(); showTotalSelectionCount = settings->value("interface/showpersistentselectioncount", true).toBool(); + showSubtypeSelectionTally = settings->value("interface/showsubtypeselectiontally", true).toBool(); showShortcuts = settings->value("menu/showshortcuts", true).toBool(); showGameSelectorFilterToolbar = settings->value("menu/showgameselectorfiltertoolbar", true).toBool(); @@ -370,6 +372,7 @@ SettingsCache::SettingsCache() openDeckInNewTab = settings->value("editor/openDeckInNewTab", false).toBool(); rewindBufferingMs = settings->value("replay/rewindBufferingMs", 200).toInt(); + styleUserList = settings->value("appearance/styleUserList", true).toBool(); chatMention = settings->value("chat/mention", true).toBool(); chatMentionCompleter = settings->value("chat/mentioncompleter", true).toBool(); chatMentionForeground = settings->value("chat/mentionforeground", true).toBool(); @@ -388,6 +391,7 @@ SettingsCache::SettingsCache() ignoreUnregisteredUsers = settings->value("chat/ignore_unregistered", false).toBool(); ignoreUnregisteredUserMessages = settings->value("chat/ignore_unregistered_messages", false).toBool(); + ignoreNonBuddyUserMessages = settings->value("chat/ignore_nonbuddy_messages", false).toBool(); scaleCards = settings->value("cards/scaleCards", true).toBool(); verticalCardOverlapPercent = settings->value("cards/verticalCardOverlapPercent", 33).toInt(); @@ -456,6 +460,13 @@ void SettingsCache::setFocusCardViewSearchBar(QT_STATE_CHANGED_T value) settings->setValue("interface/focusCardViewSearchBar", focusCardViewSearchBar); } +void SettingsCache::setKeepGameChatFocus(QT_STATE_CHANGED_T value) +{ + keepGameChatFocus = value; + settings->setValue("interface/keepGameChatFocus", keepGameChatFocus); + emit keepGameChatFocusChanged(keepGameChatFocus); +} + void SettingsCache::setKnownMissingFeatures(const QString &_knownMissingFeatures) { knownMissingFeatures = _knownMissingFeatures; @@ -1036,6 +1047,13 @@ void SettingsCache::setRewindBufferingMs(int _rewindBufferingMs) settings->setValue("replay/rewindBufferingMs", rewindBufferingMs); } +void SettingsCache::setStyleUserList(QT_STATE_CHANGED_T _styleUserList) +{ + styleUserList = static_cast(_styleUserList); + settings->setValue("appearance/styleUserList", styleUserList); + emit styleUserListChanged(); +} + void SettingsCache::setChatMention(QT_STATE_CHANGED_T _chatMention) { chatMention = static_cast(_chatMention); @@ -1117,6 +1135,12 @@ void SettingsCache::setIgnoreUnregisteredUserMessages(QT_STATE_CHANGED_T _ignore settings->setValue("chat/ignore_unregistered_messages", ignoreUnregisteredUserMessages); } +void SettingsCache::setIgnoreNonBuddyUserMessages(QT_STATE_CHANGED_T _ignoreNonBuddyUserMessages) +{ + ignoreNonBuddyUserMessages = static_cast(_ignoreNonBuddyUserMessages); + settings->setValue("chat/ignore_nonbuddy_messages", ignoreNonBuddyUserMessages); +} + void SettingsCache::setPixmapCacheSize(const int _pixmapCacheSize) { pixmapCacheSize = _pixmapCacheSize; @@ -1372,6 +1396,12 @@ void SettingsCache::setShowTotalSelectionCount(QT_STATE_CHANGED_T _showTotalSele settings->setValue("interface/showpersistentselectioncount", showTotalSelectionCount); } +void SettingsCache::setShowSubtypeSelectionTally(QT_STATE_CHANGED_T _showSubtypeSelectionTally) +{ + showSubtypeSelectionTally = static_cast(_showSubtypeSelectionTally); + settings->setValue("interface/showsubtypeselectiontally", showSubtypeSelectionTally); +} + void SettingsCache::loadPaths() { QString dataPath = getDataPath(); diff --git a/cockatrice/src/client/settings/cache_settings.h b/cockatrice/src/client/settings/cache_settings.h index b1197e267..29af89587 100644 --- a/cockatrice/src/client/settings/cache_settings.h +++ b/cockatrice/src/client/settings/cache_settings.h @@ -183,17 +183,20 @@ signals: void soundThemeChanged(); void ignoreUnregisteredUsersChanged(); void ignoreUnregisteredUserMessagesChanged(); + void ignoreNonBuddyUserMessagesChanged(); void pixmapCacheSizeChanged(int newSizeInMBs); void networkCacheSizeChanged(int newSizeInMBs); void redirectCacheTtlChanged(int newTtl); void cardPictureLoaderCacheMethodChanged(int cardPictureLoaderCacheMethod); void localCardImageStorageNamingSchemeChanged(int localCardImageStorageNamingScheme); void masterVolumeChanged(int value); + void styleUserListChanged(); void chatMentionCompleterChanged(); void downloadSpoilerTimeIndexChanged(); void downloadSpoilerStatusChanged(); void useTearOffMenusChanged(bool state); void roundCardCornersChanged(bool roundCardCorners); + void keepGameChatFocusChanged(bool value); private: QSettings *settings; @@ -282,6 +285,7 @@ private: bool autoRotateSidewaysLayoutCards; bool openDeckInNewTab; int rewindBufferingMs; + bool styleUserList; bool chatMention; bool chatMentionCompleter; QString chatMentionColor; @@ -294,6 +298,7 @@ private: QString soundThemeName; bool ignoreUnregisteredUsers; bool ignoreUnregisteredUserMessages; + bool ignoreNonBuddyUserMessages; QString picUrl; QString picUrlFallback; QString clientID; @@ -304,6 +309,7 @@ private: int cardViewExpandedRowsMax; bool closeEmptyCardView; bool focusCardViewSearchBar; + bool keepGameChatFocus; int pixmapCacheSize; int networkCacheSize; int redirectCacheTtl; @@ -349,6 +355,7 @@ private: bool showStatusBar; bool showDragSelectionCount; bool showTotalSelectionCount; + bool showSubtypeSelectionTally; public: SettingsCache(); @@ -472,6 +479,10 @@ public: { return showTotalSelectionCount; } + [[nodiscard]] bool getShowSubtypeSelectionTally() const + { + return showSubtypeSelectionTally; + } [[nodiscard]] bool getNotificationsEnabled() const { return notificationsEnabled; @@ -734,6 +745,10 @@ public: { return rewindBufferingMs; } + [[nodiscard]] bool getStyleUserList() const + { + return styleUserList; + } [[nodiscard]] bool getChatMention() const { return chatMention; @@ -788,6 +803,10 @@ public: { return ignoreUnregisteredUserMessages; } + [[nodiscard]] bool getIgnoreNonBuddyUserMessages() const + { + return ignoreNonBuddyUserMessages; + } [[nodiscard]] int getPixmapCacheSize() const { return pixmapCacheSize; @@ -929,6 +948,7 @@ public: void setCardViewExpandedRowsMax(int value); void setCloseEmptyCardView(QT_STATE_CHANGED_T value); void setFocusCardViewSearchBar(QT_STATE_CHANGED_T value); + void setKeepGameChatFocus(QT_STATE_CHANGED_T value); QString getClientID() override { return clientID; @@ -961,6 +981,10 @@ public: { return focusCardViewSearchBar; } + [[nodiscard]] bool getKeepGameChatFocus() const + { + return keepGameChatFocus; + } [[nodiscard]] ShortcutsSettings &shortcuts() const { return *shortcutsSettings; @@ -1100,6 +1124,7 @@ public slots: void setAutoRotateSidewaysLayoutCards(QT_STATE_CHANGED_T _autoRotateSidewaysLayoutCards); void setOpenDeckInNewTab(QT_STATE_CHANGED_T _openDeckInNewTab); void setRewindBufferingMs(int _rewindBufferingMs); + void setStyleUserList(QT_STATE_CHANGED_T _styleUserList); void setChatMention(QT_STATE_CHANGED_T _chatMention); void setChatMentionCompleter(QT_STATE_CHANGED_T _chatMentionCompleter); void setChatMentionForeground(QT_STATE_CHANGED_T _chatMentionForeground); @@ -1111,6 +1136,7 @@ public slots: void setSoundThemeName(const QString &_soundThemeName); void setIgnoreUnregisteredUsers(QT_STATE_CHANGED_T _ignoreUnregisteredUsers); void setIgnoreUnregisteredUserMessages(QT_STATE_CHANGED_T _ignoreUnregisteredUserMessages); + void setIgnoreNonBuddyUserMessages(QT_STATE_CHANGED_T _ignoreNonBuddyUserMessages); void setPixmapCacheSize(const int _pixmapCacheSize); void setCardImageCacheMethod(CardPictureLoaderCacheMethod::CacheMethod _cardImageCachingMethod); void setNetworkCacheSizeInMB(const int _networkCacheSize); @@ -1155,5 +1181,6 @@ public slots: void setRoundCardCorners(bool _roundCardCorners); void setShowDragSelectionCount(QT_STATE_CHANGED_T _showDragSelectionCount); void setShowTotalSelectionCount(QT_STATE_CHANGED_T _showTotalSelectionCount); + void setShowSubtypeSelectionTally(QT_STATE_CHANGED_T _showSubtypeSelectionTally); }; #endif diff --git a/cockatrice/src/client/settings/shortcuts_settings.h b/cockatrice/src/client/settings/shortcuts_settings.h index 45e2c4fca..95155b8d1 100644 --- a/cockatrice/src/client/settings/shortcuts_settings.h +++ b/cockatrice/src/client/settings/shortcuts_settings.h @@ -223,6 +223,10 @@ private: {"TabDeckEditor/aLoadDeck", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Load Deck..."), parseSequenceString("Ctrl+O"), ShortcutGroup::Deck_Editor)}, + {"TabDeckEditor/aLoadDeckFromWebsite", + ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Load deck from online service..."), + parseSequenceString("Ctrl+Shift+O"), + ShortcutGroup::Deck_Editor)}, {"TabDeckEditor/aLoadDeckFromClipboard", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Load Deck from Clipboard..."), parseSequenceString("Ctrl+Shift+V"), @@ -283,6 +287,10 @@ private: ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Load Deck from Clipboard..."), parseSequenceString("Ctrl+Shift+V"), ShortcutGroup::Game_Lobby)}, + {"DeckViewContainer/loadFromWebsiteButton", + ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Load from website..."), + parseSequenceString("Ctrl+Shift+O"), + ShortcutGroup::Game_Lobby)}, {"DeckViewContainer/unloadDeckButton", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Unload Deck"), parseSequenceString("Ctrl+Alt+U"), ShortcutGroup::Game_Lobby)}, diff --git a/cockatrice/src/game/abstract_game.cpp b/cockatrice/src/game/abstract_game.cpp index 5b1b4bff2..c20003ece 100644 --- a/cockatrice/src/game/abstract_game.cpp +++ b/cockatrice/src/game/abstract_game.cpp @@ -3,7 +3,7 @@ #include "../interface/widgets/tabs/tab_game.h" #include "player/player_logic.h" -AbstractGame::AbstractGame(TabGame *_tab) : QObject(_tab), tab(_tab) +AbstractGame::AbstractGame(QObject *_parent) : QObject(_parent) { gameMetaInfo = new GameMetaInfo(this); gameEventHandler = new GameEventHandler(this); diff --git a/cockatrice/src/game/abstract_game.h b/cockatrice/src/game/abstract_game.h index 2441bac2d..5115ed5ca 100644 --- a/cockatrice/src/game/abstract_game.h +++ b/cockatrice/src/game/abstract_game.h @@ -16,26 +16,19 @@ #include class CardItem; -class TabGame; class AbstractGame : public QObject { Q_OBJECT public: - explicit AbstractGame(TabGame *tab); + explicit AbstractGame(QObject *parent); - TabGame *tab; GameMetaInfo *gameMetaInfo; GameState *gameState; GameEventHandler *gameEventHandler; PlayerManager *playerManager; CardItem *activeCard; - TabGame *getTab() const - { - return tab; - } - GameMetaInfo *getGameMetaInfo() { return gameMetaInfo; diff --git a/cockatrice/src/game/arrow_registry.cpp b/cockatrice/src/game/arrow_registry.cpp new file mode 100644 index 000000000..286764b3b --- /dev/null +++ b/cockatrice/src/game/arrow_registry.cpp @@ -0,0 +1,48 @@ +#include "arrow_registry.h" + +#include "../game_graphics/board/arrow_item.h" + +void ArrowRegistry::insert(QSharedPointer data, ArrowItem *arrow) +{ + const ArrowKey key{data->creatorId, data->id}; + + if (auto *existing = take(data->creatorId, data->id)) { + existing->delArrow(); + } + + dataStore.insert(key, data); + items.insert(key, arrow); + byPlayer[data->creatorId].insert(data->id); +} + +ArrowItem *ArrowRegistry::take(int creatorId, int arrowId) +{ + const ArrowKey key{creatorId, arrowId}; + dataStore.remove(key); + auto &playerSet = byPlayer[creatorId]; + playerSet.remove(arrowId); + if (playerSet.isEmpty()) { + byPlayer.remove(creatorId); + } + return items.take(key); +} + +ArrowItem *ArrowRegistry::get(int creatorId, int arrowId) const +{ + return items.value(ArrowKey{creatorId, arrowId}, nullptr); +} + +bool ArrowRegistry::contains(int creatorId, int arrowId) const +{ + return items.contains(ArrowKey{creatorId, arrowId}); +} + +QSet ArrowRegistry::idsForPlayer(int playerId) const +{ + return byPlayer.value(playerId); +} + +QList ArrowRegistry::all() const +{ + return items.values(); +} \ No newline at end of file diff --git a/cockatrice/src/game/arrow_registry.h b/cockatrice/src/game/arrow_registry.h new file mode 100644 index 000000000..ef98229a2 --- /dev/null +++ b/cockatrice/src/game/arrow_registry.h @@ -0,0 +1,43 @@ +#ifndef COCKATRICE_ARROW_REGISTRY_H +#define COCKATRICE_ARROW_REGISTRY_H + +#include "board/arrow_data.h" + +#include +#include +#include + +class ArrowItem; + +struct ArrowKey +{ + int creatorId; + int arrowId; + + bool operator<(const ArrowKey &other) const + { + if (creatorId != other.creatorId) { + return creatorId < other.creatorId; + } + return arrowId < other.arrowId; + } +}; + +class ArrowRegistry +{ +public: + void insert(QSharedPointer data, ArrowItem *arrow); + ArrowItem *take(int creatorId, int arrowId); + + [[nodiscard]] ArrowItem *get(int creatorId, int arrowId) const; + [[nodiscard]] bool contains(int creatorId, int arrowId) const; + [[nodiscard]] QSet idsForPlayer(int playerId) const; + [[nodiscard]] QList all() const; + +private: + QMap> dataStore; + QMap items; + QMap> byPlayer; +}; + +#endif \ No newline at end of file diff --git a/cockatrice/src/game/board/arrow_data.cpp b/cockatrice/src/game/board/arrow_data.cpp index bbb70f474..9e89deed0 100644 --- a/cockatrice/src/game/board/arrow_data.cpp +++ b/cockatrice/src/game/board/arrow_data.cpp @@ -1,8 +1,10 @@ #include "arrow_data.h" -ArrowData ArrowData::fromProto(const ServerInfo_Arrow &arrow) +ArrowData ArrowData::fromProto(const ServerInfo_Arrow &arrow, int creatorId, bool isLocalCreator) { ArrowData data; + data.creatorId = creatorId; + data.isLocalCreator = isLocalCreator; data.id = arrow.id(); data.startPlayerId = arrow.start_player_id(); data.startZone = QString::fromStdString(arrow.start_zone()); diff --git a/cockatrice/src/game/board/arrow_data.h b/cockatrice/src/game/board/arrow_data.h index a8b35dad6..2752f97e3 100644 --- a/cockatrice/src/game/board/arrow_data.h +++ b/cockatrice/src/game/board/arrow_data.h @@ -8,16 +8,18 @@ struct ArrowData { - int id; - int startPlayerId; - QString startZone; - int startCardId; - int targetPlayerId; - QString targetZone; // empty = targeting a player - int targetCardId = -1; // -1 = targeting a player - QColor color; + int creatorId = -1; + bool isLocalCreator = false; + int id = -1; + int startPlayerId = -1; + QString startZone = ""; + int startCardId = -1; + int targetPlayerId = -1; + QString targetZone = ""; + int targetCardId = -1; + QColor color = ""; - static ArrowData fromProto(const ServerInfo_Arrow &arrow); + static ArrowData fromProto(const ServerInfo_Arrow &arrow, int creatorId, bool isLocalCreator); bool isPlayerTargeted() const { diff --git a/cockatrice/src/game/board/card_list.cpp b/cockatrice/src/game/board/card_list.cpp index c324ca10a..0080b5ae6 100644 --- a/cockatrice/src/game/board/card_list.cpp +++ b/cockatrice/src/game/board/card_list.cpp @@ -1,6 +1,6 @@ #include "card_list.h" -#include "card_item.h" +#include "../../game_graphics/board/card_item.h" #include #include diff --git a/cockatrice/src/game/game.cpp b/cockatrice/src/game/game.cpp index 38477f7f7..4c8b109c2 100644 --- a/cockatrice/src/game/game.cpp +++ b/cockatrice/src/game/game.cpp @@ -4,16 +4,16 @@ #include -Game::Game(TabGame *_tab, +Game::Game(QObject *_parent, + bool isLocalGame, QList &_clients, const Event_GameJoined &event, const QMap &_roomGameTypes) - : AbstractGame(_tab) + : AbstractGame(_parent) { gameMetaInfo->setFromProto(event.game_info()); gameMetaInfo->setRoomGameTypes(_roomGameTypes); - gameState = new GameState(this, 0, event.host_id(), tab->getTabSupervisor()->getIsLocalGame(), _clients, false, - event.resuming(), -1, false); + gameState = new GameState(this, 0, event.host_id(), isLocalGame, _clients, false, event.resuming(), -1, false); connect(gameMetaInfo, &GameMetaInfo::startedChanged, gameState, &GameState::onStartedChanged); playerManager = new PlayerManager(this, event.player_id(), event.judge(), event.spectator()); gameMetaInfo->setStarted(false); diff --git a/cockatrice/src/game/game.h b/cockatrice/src/game/game.h index ccdb679df..4f912664c 100644 --- a/cockatrice/src/game/game.h +++ b/cockatrice/src/game/game.h @@ -16,7 +16,8 @@ class Game : public AbstractGame Q_OBJECT public: - Game(TabGame *tab, + Game(QObject *parent, + bool isLocalGame, QList &_clients, const Event_GameJoined &event, const QMap &_roomGameTypes); diff --git a/cockatrice/src/game/game_event_handler.cpp b/cockatrice/src/game/game_event_handler.cpp index cff80a1ec..4a96eebdb 100644 --- a/cockatrice/src/game/game_event_handler.cpp +++ b/cockatrice/src/game/game_event_handler.cpp @@ -1,8 +1,8 @@ #include "game_event_handler.h" +#include "../game_graphics/log/message_log_widget.h" #include "../interface/widgets/tabs/tab_game.h" #include "abstract_game.h" -#include "log/message_log_widget.h" #include #include @@ -213,23 +213,24 @@ void GameEventHandler::handleChatMessageSent(const QString &chatMessage) sendGameCommand(cmd); } -void GameEventHandler::handleArrowDeletion(int arrowId) +void GameEventHandler::handleArrowDeletion(int creatorId, int arrowId) { Command_DeleteArrow cmd; cmd.set_arrow_id(arrowId); auto preparedCommand = prepareGameCommand(cmd); - connect(preparedCommand, &PendingCommand::finished, this, - [arrowId, this](const Response &response) { handleArrowDeletionFinished(response, arrowId); }); + connect(preparedCommand, &PendingCommand::finished, this, [creatorId, arrowId, this](const Response &response) { + handleArrowDeletionFinished(response, creatorId, arrowId); + }); sendGameCommand(preparedCommand); } -void GameEventHandler::handleArrowDeletionFinished(const Response &response, int arrowId) +void GameEventHandler::handleArrowDeletionFinished(const Response &response, int creatorId, int arrowId) { if (response.response_code() == Response::RespNameNotFound) { - emit arrowDeleted(arrowId); + emit arrowDeleted(creatorId, arrowId); } } diff --git a/cockatrice/src/game/game_event_handler.h b/cockatrice/src/game/game_event_handler.h index bc4812aa4..f47116949 100644 --- a/cockatrice/src/game/game_event_handler.h +++ b/cockatrice/src/game/game_event_handler.h @@ -60,8 +60,8 @@ public: void handleActivePhaseChanged(int phase); void handleGameLeft(); void handleChatMessageSent(const QString &chatMessage); - void handleArrowDeletion(int arrowId); - void handleArrowDeletionFinished(const Response &response, int arrowId); + void handleArrowDeletion(int creatorId, int arrowId); + void handleArrowDeletionFinished(const Response &response, int creatorId, int arrowId); void eventSpectatorSay(const Event_GameSay &event, int eventPlayerId, const GameEventContext &context); void eventSpectatorLeave(const Event_Leave &event, int eventPlayerId, const GameEventContext &context); @@ -113,7 +113,7 @@ signals: void containerProcessingStarted(GameEventContext context); void setContextJudgeName(QString judgeName); void containerProcessingDone(); - void arrowDeleted(int arrowId); + void arrowDeleted(int creatorId, int arrowId); void logSpectatorSay(ServerInfo_User userInfo, QString message); void logSpectatorLeave(QString name, QString reason); void logGameStart(); diff --git a/cockatrice/src/game/game_view.cpp b/cockatrice/src/game/game_view.cpp deleted file mode 100644 index 4ba41cffb..000000000 --- a/cockatrice/src/game/game_view.cpp +++ /dev/null @@ -1,188 +0,0 @@ -#include "game_view.h" - -#include "../client/settings/cache_settings.h" -#include "game_scene.h" - -#include -#include -#include -#include - -// QRubberBand calls raise() in showEvent() and changeEvent() to stay on top of siblings. -// This subclass disables that behavior so dragCountLabel can appear above it. -class SelectionRubberBand : public QRubberBand -{ -public: - using QRubberBand::QRubberBand; - -protected: - void showEvent(QShowEvent *event) override - { - QWidget::showEvent(event); // Skip QRubberBand's raise() - } - - void changeEvent(QEvent *event) override - { - if (event->type() == QEvent::ZOrderChange) { - return; // Skip QRubberBand's raise() on z-order changes - } - QRubberBand::changeEvent(event); - } -}; - -GameView::GameView(GameScene *scene, QWidget *parent) : QGraphicsView(scene, parent), rubberBand(0) -{ - setBackgroundBrush(QBrush(QColor(0, 0, 0))); - setRenderHints(QPainter::TextAntialiasing | QPainter::Antialiasing); - setFocusPolicy(Qt::ClickFocus); - setViewportUpdateMode(BoundingRectViewportUpdate); - - connect(scene, &GameScene::sceneRectChanged, this, &GameView::updateSceneRect); - - connect(scene, &GameScene::sigStartRubberBand, this, &GameView::startRubberBand); - connect(scene, &GameScene::sigResizeRubberBand, this, &GameView::resizeRubberBand); - connect(scene, &GameScene::sigStopRubberBand, this, &GameView::stopRubberBand); - connect(scene, &QGraphicsScene::selectionChanged, this, [this]() { updateTotalSelectionCount(); }); - - aCloseMostRecentZoneView = new QAction(this); - - connect(aCloseMostRecentZoneView, &QAction::triggered, scene, &GameScene::closeMostRecentZoneView); - addAction(aCloseMostRecentZoneView); - connect(&SettingsCache::instance().shortcuts(), &ShortcutsSettings::shortCutChanged, this, - &GameView::refreshShortcuts); - refreshShortcuts(); - rubberBand = new SelectionRubberBand(QRubberBand::Rectangle, this); - - const QString countLabelStyle = "color: white; " - "font-size: 14px; " - "font-weight: bold; " - "background-color: rgba(0, 0, 0, 160); " - "border-radius: 3px; " - "padding: 1px 2px;"; - - dragCountLabel = new QLabel(this); - dragCountLabel->setStyleSheet(countLabelStyle); - dragCountLabel->hide(); - dragCountLabel->raise(); - - totalCountLabel = new QLabel(this); - totalCountLabel->setStyleSheet(countLabelStyle); - totalCountLabel->hide(); -} - -void GameView::resizeEvent(QResizeEvent *event) -{ - QGraphicsView::resizeEvent(event); - - GameScene *s = dynamic_cast(scene()); - if (s) { - s->processViewSizeChange(event->size()); - } - - updateSceneRect(scene()->sceneRect()); - updateTotalSelectionCount(event->size()); -} - -void GameView::updateSceneRect(const QRectF &rect) -{ - fitInView(rect, Qt::KeepAspectRatio); -} - -void GameView::startRubberBand(const QPointF &_selectionOrigin) -{ - if (!rubberBand) { - return; - } - - selectionOrigin = _selectionOrigin; - rubberBand->setGeometry(QRect(mapFromScene(selectionOrigin), QSize(0, 0))); - rubberBand->show(); -} - -void GameView::resizeRubberBand(const QPointF &cursorPoint, int selectedCount) -{ - if (!rubberBand) { - return; - } - - constexpr int kLabelPaddingInPixels = 4; - - QPoint cursor = cursorPoint.toPoint(); - QRect rect = QRect(mapFromScene(selectionOrigin), cursor).normalized(); - rubberBand->setGeometry(rect); - - if (!SettingsCache::instance().getShowDragSelectionCount()) { - dragCountLabel->hide(); - return; - } - - if (selectedCount > 0) { - dragCountLabel->setText(QString::number(selectedCount)); - dragCountLabel->adjustSize(); - QSize labelSize = dragCountLabel->size(); - - if (rect.width() < labelSize.width() + 2 * kLabelPaddingInPixels || - rect.height() < labelSize.height() + 2 * kLabelPaddingInPixels) { - dragCountLabel->hide(); - return; - } - - const int minX = rect.left() + kLabelPaddingInPixels; - const int minY = rect.top() + kLabelPaddingInPixels; - - int x = qMax(minX, cursor.x() - labelSize.width() - kLabelPaddingInPixels); - int y = qMax(minY, cursor.y() - labelSize.height() - kLabelPaddingInPixels); - - bool isAtTopLeftCorner = (x == minX) && (y == minY); - if (isAtTopLeftCorner) { - constexpr int kCursorClearanceInPixels = 16; - x = qMin(cursor.x() + kCursorClearanceInPixels, rect.right() - labelSize.width() - kLabelPaddingInPixels); - } - - dragCountLabel->move(x, y); - dragCountLabel->show(); - } else { - dragCountLabel->hide(); - } -} - -void GameView::stopRubberBand() -{ - if (!rubberBand) { - return; - } - - rubberBand->hide(); - dragCountLabel->hide(); -} - -void GameView::refreshShortcuts() -{ - aCloseMostRecentZoneView->setShortcuts( - SettingsCache::instance().shortcuts().getShortcut("Player/aCloseMostRecentZoneView")); -} - -void GameView::updateTotalSelectionCount(const QSize &viewSize) -{ - if (!SettingsCache::instance().getShowTotalSelectionCount()) { - totalCountLabel->hide(); - return; - } - - int count = scene()->selectedItems().count(); - - if (count > 1) { - totalCountLabel->setText(QString::number(count)); - totalCountLabel->adjustSize(); - - constexpr int kMarginInPixels = 10; - int availableWidth = viewSize.isValid() ? viewSize.width() : viewport()->width(); - int availableHeight = viewSize.isValid() ? viewSize.height() : viewport()->height(); - int x = availableWidth - totalCountLabel->width() - kMarginInPixels; - int y = availableHeight - totalCountLabel->height() - kMarginInPixels; - totalCountLabel->move(x, y); - totalCountLabel->show(); - } else { - totalCountLabel->hide(); - } -} diff --git a/cockatrice/src/game/player/player_actions.cpp b/cockatrice/src/game/player/player_actions.cpp index f137e5036..9c644a26c 100644 --- a/cockatrice/src/game/player/player_actions.cpp +++ b/cockatrice/src/game/player/player_actions.cpp @@ -1,15 +1,13 @@ #include "player_actions.h" +#include "../../game_graphics/dialogs/dlg_move_top_cards_until.h" +#include "../../game_graphics/dialogs/dlg_roll_dice.h" +#include "../../game_graphics/player/card_menu_action_type.h" #include "../../game_graphics/zones/hand_zone.h" #include "../../game_graphics/zones/table_zone.h" #include "../../interface/widgets/tabs/tab_game.h" #include "../../interface/widgets/utility/get_text_with_max.h" -#include "../board/card_item.h" -#include "../client/settings/card_counter_settings.h" -#include "../dialogs/dlg_move_top_cards_until.h" -#include "../dialogs/dlg_roll_dice.h" #include "../zones/view_zone_logic.h" -#include "card_menu_action_type.h" #include #include @@ -29,8 +27,9 @@ #include #include #include +#include +#include #include -#include #include // milliseconds in between triggers of the move top cards until action @@ -39,6 +38,8 @@ static constexpr int MOVE_TOP_CARD_UNTIL_INTERVAL = 100; PlayerActions::PlayerActions(PlayerLogic *_player) : QObject(_player), player(_player), lastTokenTableRow(0), movingCardsUntil(false) { + connect(this, &PlayerActions::requestZoneViewToggle, player, &PlayerLogic::onRequestZoneViewToggle); + moveTopCardTimer = new QTimer(this); moveTopCardTimer->setInterval(MOVE_TOP_CARD_UNTIL_INTERVAL); moveTopCardTimer->setSingleShot(true); @@ -133,12 +134,12 @@ void PlayerActions::playCardToTable(const CardItem *card, bool faceDown) void PlayerActions::actViewLibrary() { - player->getGameScene()->toggleZoneView(player, ZoneNames::DECK, -1); + emit requestZoneViewToggle(ZoneNames::DECK, -1); } void PlayerActions::actViewHand() { - player->getGameScene()->toggleZoneView(player, ZoneNames::HAND, -1); + emit requestZoneViewToggle(ZoneNames::HAND, -1); } /** @@ -170,49 +171,45 @@ void PlayerActions::actSortHand() static QList defaultOptions = {CardList::SortByName, CardList::SortByPrinting}; - player->getGraphicsItem()->getHandZoneGraphicsItem()->sortHand(sortOptions + defaultOptions); + emit requestSortHand(sortOptions + defaultOptions); } -void PlayerActions::actViewTopCards() +void PlayerActions::actRequestViewTopCardsDialog() { - int deckSize = player->getDeckZone()->getCards().size(); - bool ok; - int number = QInputDialog::getInt(player->getGame()->getTab(), tr("View top cards of library"), - tr("Number of cards: (max. %1)").arg(deckSize), defaultNumberTopCards, 1, - deckSize, 1, &ok); - if (ok) { - defaultNumberTopCards = number; - player->getGameScene()->toggleZoneView(player, ZoneNames::DECK, number); - } + emit requestViewTopCardsDialog(defaultNumberTopCards, player->getDeckZone()->getCards().size()); } -void PlayerActions::actViewBottomCards() +void PlayerActions::actViewTopCards(int number) { - int deckSize = player->getDeckZone()->getCards().size(); - bool ok; - int number = QInputDialog::getInt(player->getGame()->getTab(), tr("View bottom cards of library"), - tr("Number of cards: (max. %1)").arg(deckSize), defaultNumberBottomCards, 1, - deckSize, 1, &ok); - if (ok) { - defaultNumberBottomCards = number; - player->getGameScene()->toggleZoneView(player, ZoneNames::DECK, number, true); - } + defaultNumberTopCards = number; + emit requestZoneViewToggle(ZoneNames::DECK, number); } -void PlayerActions::actAlwaysRevealTopCard() +void PlayerActions::actRequestViewBottomCardsDialog() +{ + emit requestViewBottomCardsDialog(defaultNumberBottomCards, player->getDeckZone()->getCards().size()); +} + +void PlayerActions::actViewBottomCards(int number) +{ + defaultNumberBottomCards = number; + emit requestZoneViewToggle(ZoneNames::DECK, number, true); +} + +void PlayerActions::actAlwaysRevealTopCard(bool alwaysRevealTopCard) { Command_ChangeZoneProperties cmd; cmd.set_zone_name(ZoneNames::DECK); - cmd.set_always_reveal_top_card(player->getPlayerMenu()->getLibraryMenu()->isAlwaysRevealTopCardChecked()); + cmd.set_always_reveal_top_card(alwaysRevealTopCard); sendGameCommand(cmd); } -void PlayerActions::actAlwaysLookAtTopCard() +void PlayerActions::actAlwaysLookAtTopCard(bool alwaysRevealTopCard) { Command_ChangeZoneProperties cmd; cmd.set_zone_name(ZoneNames::DECK); - cmd.set_always_look_at_top_card(player->getPlayerMenu()->getLibraryMenu()->isAlwaysLookAtTopCardChecked()); + cmd.set_always_look_at_top_card(alwaysRevealTopCard); sendGameCommand(cmd); } @@ -224,17 +221,17 @@ void PlayerActions::actOpenDeckInDeckEditor() void PlayerActions::actViewGraveyard() { - player->getGameScene()->toggleZoneView(player, ZoneNames::GRAVE, -1); + emit requestZoneViewToggle(ZoneNames::GRAVE, -1); } void PlayerActions::actViewRfg() { - player->getGameScene()->toggleZoneView(player, ZoneNames::EXILE, -1); + emit requestZoneViewToggle(ZoneNames::EXILE, -1); } void PlayerActions::actViewSideboard() { - player->getGameScene()->toggleZoneView(player, ZoneNames::SIDEBOARD, -1); + emit requestZoneViewToggle(ZoneNames::SIDEBOARD, -1); } void PlayerActions::actShuffle() @@ -242,18 +239,20 @@ void PlayerActions::actShuffle() sendGameCommand(Command_Shuffle()); } -void PlayerActions::actShuffleTop() +void PlayerActions::actRequestShuffleTopDialog() { const int maxCards = player->getDeckZone()->getCards().size(); if (maxCards == 0) { return; } - bool ok; - int number = QInputDialog::getInt(player->getGame()->getTab(), tr("Shuffle top cards of library"), - tr("Number of cards: (max. %1)").arg(maxCards), defaultNumberTopCards, 1, - maxCards, 1, &ok); - if (!ok) { + emit requestShuffleTopDialog(defaultNumberTopCards, maxCards); +} + +void PlayerActions::actShuffleTop(int number) +{ + const int maxCards = player->getDeckZone()->getCards().size(); + if (maxCards == 0) { return; } @@ -271,18 +270,20 @@ void PlayerActions::actShuffleTop() sendGameCommand(cmd); } -void PlayerActions::actShuffleBottom() +void PlayerActions::actRequestShuffleBottomDialog() { const int maxCards = player->getDeckZone()->getCards().size(); if (maxCards == 0) { return; } - bool ok; - int number = QInputDialog::getInt(player->getGame()->getTab(), tr("Shuffle bottom cards of library"), - tr("Number of cards: (max. %1)").arg(maxCards), defaultNumberBottomCards, 1, - maxCards, 1, &ok); - if (!ok) { + emit requestShuffleBottomDialog(defaultNumberBottomCards, maxCards); +} + +void PlayerActions::actShuffleBottom(int number) +{ + const int maxCards = player->getDeckZone()->getCards().size(); + if (maxCards == 0) { return; } @@ -307,21 +308,18 @@ void PlayerActions::actDrawCard() sendGameCommand(cmd); } -void PlayerActions::actMulligan() +void PlayerActions::actRequestMulliganDialog() { int startSize = SettingsCache::instance().getStartingHandSize(); int handSize = player->getHandZone()->getCards().size(); int deckSize = player->getDeckZone()->getCards().size() + handSize; - bool ok; - int number = QInputDialog::getInt(player->getGame()->getTab(), tr("Draw hand"), - tr("Number of cards: (max. %1)").arg(deckSize) + '\n' + - tr("0 and lower are in comparison to current hand size"), - startSize, -handSize, deckSize, 1, &ok); + emit requestMulliganDialog(startSize, handSize, deckSize); +} - if (!ok) { - return; - } +void PlayerActions::actMulligan(int number) +{ + int handSize = player->getHandZone()->getCards().size(); if (number < 1) { number = handSize + number; @@ -355,19 +353,19 @@ void PlayerActions::doMulligan(int number) sendGameCommand(cmd); } -void PlayerActions::actDrawCards() +void PlayerActions::actRequestDrawCardsDialog() { int deckSize = player->getDeckZone()->getCards().size(); - bool ok; - int number = QInputDialog::getInt(player->getGame()->getTab(), tr("Draw cards"), - tr("Number of cards: (max. %1)").arg(deckSize), defaultNumberTopCards, 1, - deckSize, 1, &ok); - if (ok) { - defaultNumberTopCards = number; - Command_DrawCards cmd; - cmd.set_number(static_cast(number)); - sendGameCommand(cmd); - } + + emit requestDrawCardsDialog(defaultNumberTopCards, deckSize); +} + +void PlayerActions::actDrawCards(int number) +{ + defaultNumberTopCards = number; + Command_DrawCards cmd; + cmd.set_number(static_cast(number)); + sendGameCommand(cmd); } void PlayerActions::actUndoDraw() @@ -425,36 +423,40 @@ void PlayerActions::actMoveTopCardToExile() void PlayerActions::actMoveTopCardsToGrave() { - moveTopCardsTo(ZoneNames::GRAVE, tr("grave"), false); + actRequestMoveTopCardsToDialog(ZoneNames::GRAVE, tr("grave"), false); } void PlayerActions::actMoveTopCardsToGraveFaceDown() { - moveTopCardsTo(ZoneNames::GRAVE, tr("grave"), true); + actRequestMoveTopCardsToDialog(ZoneNames::GRAVE, tr("grave"), true); } void PlayerActions::actMoveTopCardsToExile() { - moveTopCardsTo(ZoneNames::EXILE, tr("exile"), false); + actRequestMoveTopCardsToDialog(ZoneNames::EXILE, tr("exile"), false); } void PlayerActions::actMoveTopCardsToExileFaceDown() { - moveTopCardsTo(ZoneNames::EXILE, tr("exile"), true); + actRequestMoveTopCardsToDialog(ZoneNames::EXILE, tr("exile"), true); } -void PlayerActions::moveTopCardsTo(const QString &targetZone, const QString &zoneDisplayName, bool faceDown) +void PlayerActions::actRequestMoveTopCardsToDialog(const QString &targetZone, + const QString &zoneDisplayName, + bool faceDown) { const int maxCards = player->getDeckZone()->getCards().size(); if (maxCards == 0) { return; } - bool ok; - int number = QInputDialog::getInt(player->getGame()->getTab(), tr("Move top cards to %1").arg(zoneDisplayName), - tr("Number of cards: (max. %1)").arg(maxCards), defaultNumberTopCards, 1, - maxCards, 1, &ok); - if (!ok) { + emit requestMoveTopCardsToDialog(defaultNumberTopCards, maxCards, targetZone, zoneDisplayName, faceDown); +} + +void PlayerActions::moveTopCardsTo(int number, const QString &targetZone, bool faceDown) +{ + const int maxCards = player->getDeckZone()->getCards().size(); + if (maxCards == 0) { return; } @@ -481,17 +483,16 @@ void PlayerActions::moveTopCardsTo(const QString &targetZone, const QString &zon sendGameCommand(cmd); } -void PlayerActions::actMoveTopCardsUntil() +void PlayerActions::actRequestMoveTopCardsUntilDialog() { stopMoveTopCardsUntil(); - DlgMoveTopCardsUntil dlg(player->getGame()->getTab(), movingCardsUntilOptions); - if (!dlg.exec()) { - return; - } + emit requestMoveTopCardsUntilDialog(movingCardsUntilOptions); +} - auto expr = dlg.getExpr(); - movingCardsUntilOptions = dlg.getOptions(); +void PlayerActions::moveTopCardsUntil(const QString &expr, MoveTopCardsUntilOptions options) +{ + movingCardsUntilOptions = options; if (player->getDeckZone()->getCards().empty()) { stopMoveTopCardsUntil(); @@ -620,36 +621,40 @@ void PlayerActions::actMoveBottomCardToExile() void PlayerActions::actMoveBottomCardsToGrave() { - moveBottomCardsTo(ZoneNames::GRAVE, tr("grave"), false); + actRequestMoveBottomCardsToDialog(ZoneNames::GRAVE, tr("grave"), false); } void PlayerActions::actMoveBottomCardsToGraveFaceDown() { - moveBottomCardsTo(ZoneNames::GRAVE, tr("grave"), true); + actRequestMoveBottomCardsToDialog(ZoneNames::GRAVE, tr("grave"), true); } void PlayerActions::actMoveBottomCardsToExile() { - moveBottomCardsTo(ZoneNames::EXILE, tr("exile"), false); + actRequestMoveBottomCardsToDialog(ZoneNames::EXILE, tr("exile"), false); } void PlayerActions::actMoveBottomCardsToExileFaceDown() { - moveBottomCardsTo(ZoneNames::EXILE, tr("exile"), true); + actRequestMoveBottomCardsToDialog(ZoneNames::EXILE, tr("exile"), true); } -void PlayerActions::moveBottomCardsTo(const QString &targetZone, const QString &zoneDisplayName, bool faceDown) +void PlayerActions::actRequestMoveBottomCardsToDialog(const QString &targetZone, + const QString &zoneDisplayName, + bool faceDown) { const int maxCards = player->getDeckZone()->getCards().size(); if (maxCards == 0) { return; } - bool ok; - int number = QInputDialog::getInt(player->getGame()->getTab(), tr("Move bottom cards to %1").arg(zoneDisplayName), - tr("Number of cards: (max. %1)").arg(maxCards), defaultNumberBottomCards, 1, - maxCards, 1, &ok); - if (!ok) { + emit requestMoveBottomCardsToDialog(defaultNumberBottomCards, maxCards, targetZone, zoneDisplayName, faceDown); +} + +void PlayerActions::moveBottomCardsTo(int number, const QString &targetZone, bool faceDown) +{ + const int maxCards = player->getDeckZone()->getCards().size(); + if (maxCards == 0) { return; } @@ -761,20 +766,24 @@ void PlayerActions::actDrawBottomCard() sendGameCommand(cmd); } -void PlayerActions::actDrawBottomCards() +void PlayerActions::actRequestDrawBottomCardsDialog() { const int maxCards = player->getDeckZone()->getCards().size(); if (maxCards == 0) { return; } - bool ok; - int number = QInputDialog::getInt(player->getGame()->getTab(), tr("Draw bottom cards"), - tr("Number of cards: (max. %1)").arg(maxCards), defaultNumberBottomCards, 1, - maxCards, 1, &ok); - if (!ok) { + emit requestDrawBottomCardsDialog(defaultNumberBottomCards, maxCards); +} + +void PlayerActions::actDrawBottomCards(int number) +{ + const int maxCards = player->getDeckZone()->getCards().size(); + if (maxCards == 0) { return; - } else if (number > maxCards) { + } + + if (number > maxCards) { number = maxCards; } defaultNumberBottomCards = number; @@ -841,16 +850,16 @@ void PlayerActions::actUntapAll() sendGameCommand(cmd); } -void PlayerActions::actRollDie() +void PlayerActions::actRequestRollDieDialog() { - DlgRollDice dlg(player->getGame()->getTab()); - if (!dlg.exec()) { - return; - } + emit requestRollDieDialog(); +} +void PlayerActions::actRollDie(int sides, int count) +{ Command_RollDie cmd; - cmd.set_sides(dlg.getDieSideCount()); - cmd.set_count(dlg.getDiceToRollCount()); + cmd.set_sides(sides); + cmd.set_count(count); sendGameCommand(cmd); } @@ -862,26 +871,26 @@ void PlayerActions::actFlipCoin() sendGameCommand(cmd); } -void PlayerActions::actCreateToken() +void PlayerActions::actRequestCreateTokenDialog(const QStringList &predefinedTokens) { - DlgCreateToken dlg(player->getPlayerMenu()->getUtilityMenu()->getPredefinedTokens(), player->getGame()->getTab()); - if (!dlg.exec()) { - return; - } + emit requestCreateTokenDialog(predefinedTokens); +} - lastTokenInfo = dlg.getTokenInfo(); +void PlayerActions::actCreateToken(TokenInfo tokenToCreate) +{ + lastTokenInfo = tokenToCreate; ExactCard correctedCard = CardDatabaseManager::query()->guessCard({lastTokenInfo.name, lastTokenInfo.providerId}); if (correctedCard) { lastTokenInfo.name = correctedCard.getName(); - lastTokenTableRow = TableZone::tableRowToGridY(correctedCard.getInfo().getUiAttributes().tableRow); + int tableRow = lastTokenInfo.faceDown ? 2 : correctedCard.getInfo().getUiAttributes().tableRow; + lastTokenTableRow = TableZone::tableRowToGridY(tableRow); if (lastTokenInfo.pt.isEmpty()) { lastTokenInfo.pt = correctedCard.getInfo().getPowTough(); } } - player->getPlayerMenu()->getUtilityMenu()->setAndEnableCreateAnotherTokenAction( - tr("C&reate another %1 token").arg(lastTokenInfo.name)); + emit requestEnableAndSetCreateAnotherTokenAction(lastTokenInfo.name); actCreateAnotherToken(); } @@ -912,8 +921,12 @@ void PlayerActions::setLastToken(CardInfoPtr cardInfo) return; } - UtilityMenu *utilityMenu = player->getPlayerMenu()->getUtilityMenu(); - if (utilityMenu == nullptr || !utilityMenu->createAnotherTokenActionExists()) { + emit requestSetLastToken(cardInfo); +} + +void PlayerActions::setLastTokenInfo(CardInfoPtr cardInfo) +{ + if (cardInfo == nullptr) { return; } @@ -927,7 +940,7 @@ void PlayerActions::setLastToken(CardInfoPtr cardInfo) lastTokenTableRow = TableZone::tableRowToGridY(cardInfo->getUiAttributes().tableRow); - utilityMenu->setAndEnableCreateAnotherTokenAction(tr("C&reate another %1 token").arg(lastTokenInfo.name)); + emit requestEnableAndSetCreateAnotherTokenAction(lastTokenInfo.name); } void PlayerActions::actCreatePredefinedToken() @@ -946,23 +959,17 @@ void PlayerActions::actCreatePredefinedToken() void PlayerActions::actCreateRelatedCard() { const CardItem *sourceCard = player->getGame()->getActiveCard(); + if (!sourceCard) { return; } + auto *action = static_cast(sender()); // If there is a better way of passing a CardRelation through a QAction, please add it here. auto relatedCards = sourceCard->getCardInfo().getAllRelatedCards(); - CardRelation *cardRelation = relatedCards.at(action->data().toInt()); - /* - * If we make a token via "Token: TokenName" - * then let's allow it to be created via "create another token" - */ - if (createRelatedFromRelation(sourceCard, cardRelation) && cardRelation->getCanCreateAnother()) { - ExactCard relatedCard = CardDatabaseManager::query()->getCardFromSameSet(cardRelation->getName(), - sourceCard->getCard().getPrinting()); - setLastToken(relatedCard.getCardPtr()); - } + CardRelation *cardRelation = relatedCards.at(action->data().toInt()); + actRequestCreateRelatedFromRelationDialog(sourceCard, cardRelation); } void PlayerActions::actCreateAllRelatedCards() @@ -982,7 +989,9 @@ void PlayerActions::actCreateAllRelatedCards() if (relatedCards.length() == 1) { cardRelation = relatedCards.at(0); - if (createRelatedFromRelation(sourceCard, cardRelation)) { + lastRelatedCreationSucceeded = false; // reset before emit + actRequestCreateRelatedFromRelationDialog(sourceCard, cardRelation); + if (lastRelatedCreationSucceeded) { ++tokensTypesCreated; } } else { @@ -994,21 +1003,25 @@ void PlayerActions::actCreateAllRelatedCards() } } switch (nonExcludedRelatedCards.length()) { - case 1: // if nonExcludedRelatedCards == 1 + case 1: cardRelation = nonExcludedRelatedCards.at(0); - if (createRelatedFromRelation(sourceCard, cardRelation)) { + lastRelatedCreationSucceeded = false; // reset before emit + actRequestCreateRelatedFromRelationDialog(sourceCard, cardRelation); + if (lastRelatedCreationSucceeded) { ++tokensTypesCreated; } break; + // If all are marked "Exclude", then treat the situation as if none of them are. // We won't accept "garbage in, garbage out", here. - case 0: // else if nonExcludedRelatedCards == 0 + case 0: for (CardRelation *cardRelationAll : relatedCards) { if (!cardRelationAll->getDoesAttach() && !cardRelationAll->getIsVariable()) { dbName = cardRelationAll->getName(); bool persistent = cardRelationAll->getIsPersistent(); + bool faceDown = cardRelationAll->getIsFaceDown(); for (int i = 0; i < cardRelationAll->getDefaultCount(); ++i) { - createCard(sourceCard, dbName, CardRelationType::DoesNotAttach, persistent); + createCard(sourceCard, dbName, CardRelationType::DoesNotAttach, persistent, faceDown); } ++tokensTypesCreated; if (tokensTypesCreated == 1) { @@ -1017,13 +1030,15 @@ void PlayerActions::actCreateAllRelatedCards() } } break; - default: // else + + default: for (CardRelation *cardRelationNotExcluded : nonExcludedRelatedCards) { if (!cardRelationNotExcluded->getDoesAttach() && !cardRelationNotExcluded->getIsVariable()) { dbName = cardRelationNotExcluded->getName(); bool persistent = cardRelationNotExcluded->getIsPersistent(); + bool faceDown = cardRelationNotExcluded->getIsFaceDown(); for (int i = 0; i < cardRelationNotExcluded->getDefaultCount(); ++i) { - createCard(sourceCard, dbName, CardRelationType::DoesNotAttach, persistent); + createCard(sourceCard, dbName, CardRelationType::DoesNotAttach, persistent, faceDown); } ++tokensTypesCreated; if (tokensTypesCreated == 1) { @@ -1045,54 +1060,89 @@ void PlayerActions::actCreateAllRelatedCards() } } -bool PlayerActions::createRelatedFromRelation(const CardItem *sourceCard, const CardRelation *cardRelation) +void PlayerActions::actRequestCreateRelatedFromRelationDialog(const CardItem *sourceCard, + const CardRelation *cardRelation) +{ + emit requestCreateRelatedFromRelationDialog(sourceCard, cardRelation); +} + +bool PlayerActions::createRelatedFromRelation(const CardItem *sourceCard, + const CardRelation *cardRelation, + int variableCount) { if (sourceCard == nullptr || cardRelation == nullptr) { return false; } - QString dbName = cardRelation->getName(); - bool persistent = cardRelation->getIsPersistent(); + + const QString dbName = cardRelation->getName(); + const bool persistent = cardRelation->getIsPersistent(); + const bool faceDown = cardRelation->getIsFaceDown(); + + // Variable relations always use DoesNotAttach, regardless of the count the user + // entered. if (cardRelation->getIsVariable()) { - bool ok; - player->setDialogSemaphore(true); - int count = QInputDialog::getInt(player->getGame()->getTab(), tr("Create tokens"), tr("Number:"), - cardRelation->getDefaultCount(), 1, MAX_TOKENS_PER_DIALOG, 1, &ok); - player->setDialogSemaphore(false); - if (!ok) { + if (variableCount <= 0) { return false; } - for (int i = 0; i < count; ++i) { - createCard(sourceCard, dbName, CardRelationType::DoesNotAttach, persistent); + for (int i = 0; i < variableCount; ++i) { + createCard(sourceCard, dbName, CardRelationType::DoesNotAttach, persistent, faceDown); } - } else if (cardRelation->getDefaultCount() > 1) { - for (int i = 0; i < cardRelation->getDefaultCount(); ++i) { - createCard(sourceCard, dbName, CardRelationType::DoesNotAttach, persistent); - } - } else { - CardRelationType attachType; - // do not attempt to attach to another player's cards, this causes the card to attempt to attach to the same - // cardid on the local player's field instead, which is an entirely different card! - if (player->getPlayerInfo()->getLocalOrJudge()) { - attachType = cardRelation->getAttachType(); - } else { - attachType = CardRelationType::DoesNotAttach; - } - - // move card onto table first if attaching from some other zone - // we only do this for AttachTo because cross-zone TransformInto is already handled server-side - if (attachType == CardRelationType::AttachTo && sourceCard->getZone()->getName() != ZoneNames::TABLE) { - playCardToTable(sourceCard, false); - } - - createCard(sourceCard, dbName, attachType, persistent); + return true; } + + const int count = cardRelation->getDefaultCount(); + + if (count > 1) { + for (int i = 0; i < count; ++i) { + createCard(sourceCard, dbName, CardRelationType::DoesNotAttach, persistent, faceDown); + } + return true; + } + + CardRelationType attachType; + // do not attempt to attach to another player's cards, this causes the card to attempt to attach to the same + // cardid on the local player's field instead, which is an entirely different card! + if (player->getPlayerInfo()->getLocalOrJudge()) { + attachType = cardRelation->getAttachType(); + } else { + attachType = CardRelationType::DoesNotAttach; + } + + // move card onto table first if attaching from some other zone + // we only do this for AttachTo because cross-zone TransformInto is already handled server-side + if (attachType == CardRelationType::AttachTo && sourceCard->getZone()->getName() != ZoneNames::TABLE) { + playCardToTable(sourceCard, false); + } + + createCard(sourceCard, dbName, attachType, persistent, faceDown); return true; } +void PlayerActions::onRelatedCardCreated(const CardItem *sourceCard, const CardRelation *cardRelation) +{ + if (sourceCard == nullptr || cardRelation == nullptr) { + return; + } + + /* + * If we make a token via "Token: TokenName" + * then let's allow it to be created via "create another token" + */ + if (!cardRelation->getCanCreateAnother()) { + return; + } + + ExactCard relatedCard = + CardDatabaseManager::query()->getCardFromSameSet(cardRelation->getName(), sourceCard->getCard().getPrinting()); + + setLastToken(relatedCard.getCardPtr()); +} + void PlayerActions::createCard(const CardItem *sourceCard, const QString &dbCardName, CardRelationType attachType, - bool persistent) + bool persistent, + bool faceDown) { CardInfoPtr cardInfo = CardDatabaseManager::query()->getCardInfo(dbCardName); @@ -1127,6 +1177,7 @@ void PlayerActions::createCard(const CardItem *sourceCard, cmd.set_destroy_on_zone_change(!persistent); cmd.set_x(gridPoint.x()); cmd.set_y(gridPoint.y()); + cmd.set_face_down(faceDown); ExactCard relatedCard = CardDatabaseManager::query()->getCardFromSameSet(cardInfo->getName(), sourceCard->getCard().getPrinting()); @@ -1166,35 +1217,29 @@ void PlayerActions::actSayMessage() sendGameCommand(cmd); } -void PlayerActions::actMoveCardXCardsFromTop() +void PlayerActions::actRequestMoveCardXCardsFromTopDialog() { int deckSize = player->getDeckZone()->getCards().size() + 1; // add the card to move to the deck - bool ok; - int number = - QInputDialog::getInt(player->getGame()->getTab(), tr("Place card X cards from top of library"), - tr("Which position should this card be placed:") + "\n" + tr("(max. %1)").arg(deckSize), - defaultNumberTopCardsToPlaceBelow, 1, deckSize, 1, &ok); - number -= 1; // indexes start at 0 - if (!ok) { - return; - } + emit requestMoveCardXCardsFromTopDialog(defaultNumberTopCardsToPlaceBelow, deckSize); +} +void PlayerActions::actMoveCardXCardsFromTop(QList selectedCards, int number) +{ defaultNumberTopCardsToPlaceBelow = number; - QList cardList = player->getGameScene()->selectedCards(); - if (cardList.isEmpty()) { + if (selectedCards.isEmpty()) { return; } QList commandList; ListOfCardsToMove idList; - for (const auto &i : cardList) { + for (const auto &i : selectedCards) { idList.add_card()->set_card_id(i->getId()); } - int startPlayerId = cardList[0]->getZone()->getPlayer()->getPlayerInfo()->getId(); - QString startZone = cardList[0]->getZone()->getName(); + int startPlayerId = selectedCards[0]->getZone()->getPlayer()->getPlayerInfo()->getId(); + QString startZone = selectedCards[0]->getZone()->getName(); auto *cmd = new Command_MoveCard; cmd->set_start_player_id(startPlayerId); @@ -1213,12 +1258,12 @@ void PlayerActions::actMoveCardXCardsFromTop() } } -void PlayerActions::actIncPT(int deltaP, int deltaT) +void PlayerActions::actIncPT(QList selectedCards, int deltaP, int deltaT) { int playerid = player->getPlayerInfo()->getId(); QList commandList; - for (auto card : player->getGameScene()->selectedCards()) { + for (auto card : selectedCards) { QString pt = card->getPT(); const auto ptList = CardItem::parsePT(pt); QString newpt; @@ -1246,11 +1291,11 @@ void PlayerActions::actIncPT(int deltaP, int deltaT) player->getGame()->getGameEventHandler()->sendGameCommand(prepareGameCommand(commandList), playerid); } -void PlayerActions::actResetPT() +void PlayerActions::actResetPT(QList selectedCards) { int playerid = player->getPlayerInfo()->getId(); QList commandList; - for (auto card : player->getGameScene()->selectedCards()) { + for (auto card : selectedCards) { QString ptString; if (!card->getFaceDown()) { // leave the pt empty if the card is face down ExactCard ec = card->getCard(); @@ -1279,31 +1324,28 @@ void PlayerActions::actResetPT() } } -void PlayerActions::actSetPT() +void PlayerActions::actRequestSetPTDialog(QList selectedCards) { QString oldPT; - int playerid = player->getPlayerInfo()->getId(); - auto cards = player->getGameScene()->selectedCards(); - for (auto card : cards) { + for (auto card : selectedCards) { if (!card->getPT().isEmpty()) { oldPT = card->getPT(); } } - bool ok; - player->setDialogSemaphore(true); - QString pt = getTextWithMax(player->getGame()->getTab(), tr("Change power/toughness"), tr("Change stats to:"), - QLineEdit::Normal, oldPT, &ok); - player->setDialogSemaphore(false); - if (player->clearCardsToDelete() || !ok) { - return; - } + + emit requestSetPTDialog(oldPT); +} + +void PlayerActions::actSetPT(QList selectedCards, const QString &pt) +{ + int playerid = player->getPlayerInfo()->getId(); const auto ptList = CardItem::parsePT(pt); bool empty = ptList.isEmpty(); QList commandList; - for (auto card : cards) { + for (auto card : selectedCards) { auto *cmd = new Command_SetCardAttr; QString newpt = QString(); if (!empty) { @@ -1343,47 +1385,47 @@ void PlayerActions::actDrawArrow() } } -void PlayerActions::actIncP() +void PlayerActions::actIncP(QList selectedCards) { - actIncPT(1, 0); + actIncPT(selectedCards, 1, 0); } -void PlayerActions::actDecP() +void PlayerActions::actDecP(QList selectedCards) { - actIncPT(-1, 0); + actIncPT(selectedCards, -1, 0); } -void PlayerActions::actIncT() +void PlayerActions::actIncT(QList selectedCards) { - actIncPT(0, 1); + actIncPT(selectedCards, 0, 1); } -void PlayerActions::actDecT() +void PlayerActions::actDecT(QList selectedCards) { - actIncPT(0, -1); + actIncPT(selectedCards, 0, -1); } -void PlayerActions::actIncPT() +void PlayerActions::actIncPT(QList selectedCards) { - actIncPT(1, 1); + actIncPT(selectedCards, 1, 1); } -void PlayerActions::actDecPT() +void PlayerActions::actDecPT(QList selectedCards) { - actIncPT(-1, -1); + actIncPT(selectedCards, -1, -1); } -void PlayerActions::actFlowP() +void PlayerActions::actFlowP(QList selectedCards) { - actIncPT(1, -1); + actIncPT(selectedCards, 1, -1); } -void PlayerActions::actFlowT() +void PlayerActions::actFlowT(QList selectedCards) { - actIncPT(-1, 1); + actIncPT(selectedCards, -1, 1); } -void PlayerActions::actReduceLifeByPower() +void PlayerActions::actReduceLifeByPower(QList selectedCards) { // find life counter auto lifeCounter = player->getLifeCounter(); @@ -1391,10 +1433,9 @@ void PlayerActions::actReduceLifeByPower() return; } - // calculate total power - auto cards = player->getGameScene()->selectedCards(); + // calculate total power; int total = 0; - for (auto card : cards) { + for (auto card : selectedCards) { QVariantList parsed = CardItem::parsePT(card->getPT()); if (!parsed.isEmpty()) { int power = parsed.first().toInt(); // toInt will default to 0 if it's not an int @@ -1419,31 +1460,22 @@ void AnnotationDialog::keyPressEvent(QKeyEvent *event) QInputDialog::keyPressEvent(event); } -void PlayerActions::actSetAnnotation() +void PlayerActions::actRequestSetAnnotationDialog(QList selectedCards) { QString oldAnnotation; - auto cards = player->getGameScene()->selectedCards(); - for (auto card : cards) { + for (auto card : selectedCards) { if (!card->getAnnotation().isEmpty()) { oldAnnotation = card->getAnnotation(); } } - player->setDialogSemaphore(true); - AnnotationDialog *dialog = new AnnotationDialog(player->getGame()->getTab()); - dialog->setOptions(QInputDialog::UsePlainTextEditForTextInput); - dialog->setWindowTitle(tr("Set annotation")); - dialog->setLabelText(tr("Please enter the new annotation:")); - dialog->setTextValue(oldAnnotation); - bool ok = dialog->exec(); - player->setDialogSemaphore(false); - if (player->clearCardsToDelete() || !ok) { - return; - } - QString annotation = dialog->textValue().left(MAX_NAME_LENGTH); + emit requestSetAnnotationDialog(oldAnnotation); +} +void PlayerActions::actSetAnnotation(QList selectedCards, const QString &annotation) +{ QList commandList; - for (auto card : cards) { + for (auto card : selectedCards) { auto *cmd = new Command_SetCardAttr; cmd->set_zone(card->getZone()->getName().toStdString()); cmd->set_card_id(card->getId()); @@ -1464,10 +1496,10 @@ void PlayerActions::actAttach() card->drawAttachArrow(); } -void PlayerActions::actUnattach() +void PlayerActions::actUnattach(QList selectedCards) { QList commandList; - for (auto card : player->getGameScene()->selectedCards()) { + for (auto card : selectedCards) { if (!card->getAttachedTo()) { continue; } @@ -1480,27 +1512,30 @@ void PlayerActions::actUnattach() sendGameCommand(prepareGameCommand(commandList)); } -void PlayerActions::actAddCardCounter(int counterId) +void PlayerActions::actAddCardCounter(QList selectedCards, int counterId) { - offsetCardCounter(counterId, 1); + offsetCardCounter(selectedCards, counterId, 1); } -void PlayerActions::actRemoveCardCounter(int counterId) +void PlayerActions::actRemoveCardCounter(QList selectedCards, int counterId) { - offsetCardCounter(counterId, -1); + offsetCardCounter(selectedCards, counterId, -1); } -void PlayerActions::offsetCardCounter(int counterId, int offset) +void PlayerActions::offsetCardCounter(QList selectedCards, int counterId, int offset) { QList commandList; - for (auto card : player->getGameScene()->selectedCards()) { + for (auto card : selectedCards) { int oldValue = card->getCounters().value(counterId, 0); - int newValue = oldValue + offset; - // Early exit optimization: server enforces [0, MAX_COUNTERS_ON_CARD]. - // Compare clamped value to allow recovery from invalid states. - int clampedValue = qBound(0, newValue, MAX_COUNTERS_ON_CARD); - if (clampedValue != oldValue) { + // Overflow-safe clamp to the server-enforced range [0, MAX_COUNTER_VALUE]; + // a result differing from oldValue also corrects an out-of-range cached value. + // Callers only ever pass offset == ±1 (actAddCardCounter / actRemoveCardCounter). + // This client-side clamp is a defense-in-depth UX check, consistent with + // actSetCardCounter and actIncrementAllCardCounters; the server remains the + // authoritative enforcer of the bounds. + int newValue = addClamped(oldValue, offset, 0, MAX_COUNTER_VALUE); + if (newValue != oldValue) { auto *cmd = new Command_SetCardCounter; cmd->set_zone(card->getZone()->getName().toStdString()); cmd->set_card_id(card->getId()); @@ -1513,36 +1548,27 @@ void PlayerActions::offsetCardCounter(int counterId, int offset) sendGameCommand(prepareGameCommand(commandList)); } -void PlayerActions::actSetCardCounter(int counterId) +void PlayerActions::actRequestSetCardCounterDialog(QList selectedCards, int counterId) { - player->setDialogSemaphore(true); - // If a single card is selected, we show the old value in the dialog. Otherwise, we show "x" - QList sel = player->getGameScene()->selectedCards(); QString oldValueForDlg = "x"; - if (sel.size() == 1) { - auto *card = sel.first(); + if (selectedCards.size() == 1) { + auto *card = selectedCards.first(); oldValueForDlg = QString::number(card->getCounters().value(counterId, 0)); } - auto &cardCounterSettings = SettingsCache::instance().cardCounters(); - QString counterName = cardCounterSettings.displayName(counterId); - - AbstractCounterDialog dialog(counterName, oldValueForDlg, player->getGame()->getTab()); - int ok = dialog.exec(); - - player->setDialogSemaphore(false); - if (player->clearCardsToDelete() || !ok) { - return; - } + emit requestSetCardCounterDialog(counterId, oldValueForDlg); +} +void PlayerActions::actSetCardCounter(QList selectedCards, int counterId, const QString &counterValue) +{ QList commandList; - for (auto card : sel) { + for (auto card : selectedCards) { int oldValue = card->getCounters().value(counterId, 0); Expression exp(oldValue); - double parsed = exp.parse(dialog.textValue()); + double parsed = exp.parse(counterValue); // Clamp in double precision first to avoid UB, then cast - int number = static_cast(qBound(0.0, parsed, static_cast(MAX_COUNTERS_ON_CARD))); + int number = static_cast(qBound(0.0, parsed, static_cast(MAX_COUNTER_VALUE))); auto *cmd = new Command_SetCardCounter; cmd->set_zone(card->getZone()->getName().toStdString()); @@ -1555,9 +1581,8 @@ void PlayerActions::actSetCardCounter(int counterId) sendGameCommand(prepareGameCommand(commandList)); } -void PlayerActions::actIncrementAllCardCounters() +void PlayerActions::actIncrementAllCardCounters(QList cardsToUpdate) { - auto cardsToUpdate = player->getGameScene()->selectedCards(); if (cardsToUpdate.isEmpty()) { // If no cards selected, update all cards on table cardsToUpdate = static_cast>(player->getTableZone()->getCards()); @@ -1573,7 +1598,7 @@ void PlayerActions::actIncrementAllCardCounters() counterIterator.next(); int counterId = counterIterator.key(); int currentValue = counterIterator.value(); - if (currentValue >= MAX_COUNTERS_ON_CARD) { + if (currentValue >= MAX_COUNTER_VALUE) { continue; } @@ -1603,10 +1628,8 @@ static bool isUnwritableRevealZone(CardZoneLogic *zone) return false; } -void PlayerActions::playSelectedCards(const bool faceDown) +void PlayerActions::playSelectedCards(QList selectedCards, const bool faceDown) { - QList selectedCards = player->getGameScene()->selectedCards(); - // CardIds will get shuffled downwards when cards leave the deck. // We need to iterate through the cards in reverse order so cardIds don't get changed out from under us as we play // out the cards one-by-one. @@ -1620,19 +1643,19 @@ void PlayerActions::playSelectedCards(const bool faceDown) } } -void PlayerActions::actPlay() +void PlayerActions::actPlay(QList selectedCards) { - playSelectedCards(false); + playSelectedCards(selectedCards, false); } -void PlayerActions::actPlayFacedown() +void PlayerActions::actPlayFacedown(QList selectedCards) { - playSelectedCards(true); + playSelectedCards(selectedCards, true); } -void PlayerActions::actHide() +void PlayerActions::actHide(QList selectedCards) { - for (const auto &item : player->getGameScene()->selectedCards()) { + for (const auto &item : selectedCards) { auto *card = static_cast(item); if (card && isUnwritableRevealZone(card->getZone())) { card->getZone()->removeCard(card); @@ -1640,7 +1663,7 @@ void PlayerActions::actHide() } } -void PlayerActions::actReveal(QAction *action) +void PlayerActions::actReveal(QList selectedCards, QAction *action) { const int otherPlayerId = action->data().toInt(); @@ -1649,7 +1672,7 @@ void PlayerActions::actReveal(QAction *action) cmd.set_player_id(otherPlayerId); } - for (auto card : player->getGameScene()->selectedCards()) { + for (auto card : selectedCards) { if (!cmd.has_zone_name()) { cmd.set_zone_name(card->getZone()->getName().toStdString()); } @@ -1731,15 +1754,14 @@ void PlayerActions::actRevealRandomGraveyardCard(int revealToPlayerId) sendGameCommand(cmd); } -void PlayerActions::cardMenuAction() +void PlayerActions::cardMenuAction(QList selectedCards, CardMenuActionType type) { - auto *a = dynamic_cast(sender()); - QList cardList = player->getGameScene()->selectedCards(); + QList cardList = selectedCards; QList commandList; - if (a->data().toInt() <= (int)cmClone) { + if (type <= cmClone) { for (const auto &card : cardList) { - switch (static_cast(a->data().toInt())) { + switch (type) { // Leaving both for compatibility with server case cmUntap: // fallthrough @@ -1820,7 +1842,7 @@ void PlayerActions::cardMenuAction() idList.add_card()->set_card_id(i->getId()); } - switch (static_cast(a->data().toInt())) { + switch (type) { case cmMoveToTopLibrary: { auto *cmd = new Command_MoveCard; cmd->set_start_player_id(startPlayerId); diff --git a/cockatrice/src/game/player/player_actions.h b/cockatrice/src/game/player/player_actions.h index 3b822b61a..fa4d54110 100644 --- a/cockatrice/src/game/player/player_actions.h +++ b/cockatrice/src/game/player/player_actions.h @@ -7,8 +7,11 @@ #ifndef COCKATRICE_PLAYER_ACTIONS_H #define COCKATRICE_PLAYER_ACTIONS_H -#include "../dialogs/dlg_create_token.h" -#include "../dialogs/dlg_move_top_cards_until.h" + +#include "../../game_graphics/board/card_item.h" +#include "../../game_graphics/dialogs/dlg_create_token.h" +#include "../../game_graphics/dialogs/dlg_move_top_cards_until.h" +#include "../../game_graphics/player/card_menu_action_type.h" #include "event_processing_options.h" #include "player_logic.h" @@ -25,7 +28,6 @@ class Message; } } // namespace google -class CardItem; class Command_MoveCard; class GameEventContext; class PendingCommand; @@ -56,30 +58,75 @@ public: return movingCardsUntil; } +signals: + void requestViewTopCardsDialog(int defaultNumberTopCards, int deckSize); + void requestViewBottomCardsDialog(int defaultNumberBottomCards, int deckSize); + void requestShuffleTopDialog(int defaultNumberTopCards, int maxCards); + void requestShuffleBottomDialog(int defaultNumberBottomCards, int maxCards); + void requestMulliganDialog(int startSize, int handSize, int deckSize); + void requestDrawCardsDialog(int defaultNumberTopCards, int deckSize); + void requestMoveTopCardsToDialog(int defaultNumberTopCards, + int maxCards, + const QString &targetZone, + const QString &zoneDisplayName, + bool faceDown); + void requestMoveTopCardsUntilDialog(MoveTopCardsUntilOptions options); + void requestMoveBottomCardsToDialog(int defaultNumberBottomCards, + int maxCards, + const QString &targetZone, + const QString &zoneDisplayName, + bool faceDown); + void requestDrawBottomCardsDialog(int defaultNumberBottomCards, int maxCards); + void requestRollDieDialog(); + void requestCreateTokenDialog(const QStringList &predefinedTokens); + void requestCreateRelatedFromRelationDialog(const CardItem *sourceCard, const CardRelation *cardRelation); + void requestMoveCardXCardsFromTopDialog(int defaultNumberTopCardsToPlaceBelow, int deckSize); + void requestSetPTDialog(const QString &oldPT); + void requestSetAnnotationDialog(const QString &oldAnnotation); + void requestSetCardCounterDialog(int counterId, const QString &oldValueForDlg); + void requestZoneViewToggle(const QString &zoneName, int numberCards, bool isReversed = false); + void requestSortHand(const QList &options); + void requestEnableAndSetCreateAnotherTokenAction(const QString &lastTokenName); + void requestSetLastToken(CardInfoPtr lastToken); + public slots: void setLastToken(CardInfoPtr cardInfo); + void setLastTokenInfo(CardInfoPtr cardInfo); void playCard(CardItem *c, bool faceDown); void playCardToTable(const CardItem *c, bool faceDown); void actUntapAll(); - void actRollDie(); + void actRequestRollDieDialog(); + void actRollDie(int sides, int count); void actFlipCoin(); - void actCreateToken(); + void actRequestCreateTokenDialog(const QStringList &predefinedTokens); + void actCreateToken(TokenInfo tokenToCreate); void actCreateAnotherToken(); + void actRequestCreateRelatedFromRelationDialog(const CardItem *sourceCard, const CardRelation *cardRelation); + bool createRelatedFromRelation(const CardItem *sourceCard, const CardRelation *cardRelation, int variableCount); + void onRelatedCardCreated(const CardItem *sourceCard, const CardRelation *cardRelation); + void setLastRelatedCreationSucceeded(bool succeeded) + { + lastRelatedCreationSucceeded = succeeded; + } void actShuffle(); - void actShuffleTop(); - void actShuffleBottom(); + void actRequestShuffleTopDialog(); + void actShuffleTop(int number); + void actRequestShuffleBottomDialog(); + void actShuffleBottom(int number); void actDrawCard(); - void actDrawCards(); + void actRequestDrawCardsDialog(); + void actDrawCards(int number); void actUndoDraw(); - void actMulligan(); + void actRequestMulliganDialog(); + void actMulligan(int number); void actMulliganSameSize(); void actMulliganMinusOne(); void doMulligan(int number); - void actPlay(); - void actPlayFacedown(); - void actHide(); + void actPlay(QList selectedCards); + void actPlayFacedown(QList selectedCards); + void actHide(QList selectedCards); void actMoveTopCardToPlay(); void actMoveTopCardToPlayFaceDown(); @@ -89,10 +136,14 @@ public slots: void actMoveTopCardsToGraveFaceDown(); void actMoveTopCardsToExile(); void actMoveTopCardsToExileFaceDown(); - void actMoveTopCardsUntil(); + void actRequestMoveTopCardsUntilDialog(); + void moveTopCardsUntil(const QString &expr, MoveTopCardsUntilOptions options); void actMoveTopCardToBottom(); + void actRequestMoveTopCardsToDialog(const QString &targetZone, const QString &zoneDisplayName, bool faceDown); + void moveTopCardsTo(int number, const QString &targetZone, bool faceDown); void actDrawBottomCard(); - void actDrawBottomCards(); + void actRequestDrawBottomCardsDialog(); + void actDrawBottomCards(int number); void actMoveBottomCardToPlay(); void actMoveBottomCardToPlayFaceDown(); void actMoveBottomCardToGrave(); @@ -102,6 +153,8 @@ public slots: void actMoveBottomCardsToExile(); void actMoveBottomCardsToExileFaceDown(); void actMoveBottomCardToTop(); + void actRequestMoveBottomCardsToDialog(const QString &targetZone, const QString &zoneDisplayName, bool faceDown); + void moveBottomCardsTo(int number, const QString &targetZone, bool faceDown); void actSelectAll(); void actSelectRow(); @@ -109,10 +162,12 @@ public slots: void actViewLibrary(); void actViewHand(); - void actViewTopCards(); - void actViewBottomCards(); - void actAlwaysRevealTopCard(); - void actAlwaysLookAtTopCard(); + void actRequestViewTopCardsDialog(); + void actViewTopCards(int number); + void actRequestViewBottomCardsDialog(); + void actViewBottomCards(int number); + void actAlwaysRevealTopCard(bool alwaysRevealTopCard); + void actAlwaysLookAtTopCard(bool alwaysRevealTopCard); void actViewGraveyard(); void actLendLibrary(int lendToPlayerId); void actRevealTopCards(int revealToPlayerId, int amount); @@ -127,37 +182,41 @@ public slots: void actCreateRelatedCard(); void actCreateAllRelatedCards(); - void actMoveCardXCardsFromTop(); - void actRemoveCardCounter(int counterId); - void actAddCardCounter(int counterId); - void actSetCardCounter(int counterId); - void actIncrementAllCardCounters(); + void actRequestMoveCardXCardsFromTopDialog(); + void actMoveCardXCardsFromTop(QList selectedCards, int number); + void actRemoveCardCounter(QList selectedCards, int counterId); + void actAddCardCounter(QList selectedCards, int counterId); + void actRequestSetCardCounterDialog(QList selectedCards, int counterId); + void actSetCardCounter(QList selectedCards, int counterId, const QString &counterValue); + void actIncrementAllCardCounters(QList cardsToUpdate); void actAttach(); - void actUnattach(); + void actUnattach(QList selectedCards); void actDrawArrow(); - void actIncPT(int deltaP, int deltaT); - void actResetPT(); - void actSetPT(); - void actIncP(); - void actDecP(); - void actIncT(); - void actDecT(); - void actIncPT(); - void actDecPT(); - void actFlowP(); - void actFlowT(); + void actIncPT(QList selectedCards, int deltaP, int deltaT); + void actResetPT(QList selectedCards); + void actRequestSetPTDialog(QList selectedCards); + void actSetPT(QList selectedCards, const QString &pt); + void actIncP(QList selectedCards); + void actDecP(QList selectedCards); + void actIncT(QList selectedCards); + void actDecT(QList selectedCards); + void actIncPT(QList selectedCards); + void actDecPT(QList selectedCards); + void actFlowP(QList selectedCards); + void actFlowT(QList selectedCards); - void actReduceLifeByPower(); + void actReduceLifeByPower(QList selectedCards); - void actSetAnnotation(); - void actReveal(QAction *action); + void actRequestSetAnnotationDialog(QList selectedCards); + void actSetAnnotation(QList selectedCards, const QString &annotation); + void actReveal(QList selectedCards, QAction *action); void actRevealHand(int revealToPlayerId); void actRevealRandomHandCard(int revealToPlayerId); void actRevealLibrary(int revealToPlayerId); void actSortHand(); - void cardMenuAction(); + void cardMenuAction(QList selectedCards, CardMenuActionType type); private: PlayerLogic *player; @@ -176,21 +235,20 @@ private: int movingCardsUntilCounter = 0; MoveTopCardsUntilOptions movingCardsUntilOptions; - void moveTopCardsTo(const QString &targetZone, const QString &zoneDisplayName, bool faceDown); - void moveBottomCardsTo(const QString &targetZone, const QString &zoneDisplayName, bool faceDown); + bool lastRelatedCreationSucceeded = false; void createCard(const CardItem *sourceCard, const QString &dbCardName, CardRelationType attach = CardRelationType::DoesNotAttach, - bool persistent = false); - bool createRelatedFromRelation(const CardItem *sourceCard, const CardRelation *cardRelation); + bool persistent = false, + bool faceDown = false); - void playSelectedCards(bool faceDown = false); + void playSelectedCards(QList selectedCards, bool faceDown = false); void cmdSetTopCard(Command_MoveCard &cmd); void cmdSetBottomCard(Command_MoveCard &cmd); - void offsetCardCounter(int counterId, int offset); + void offsetCardCounter(QList selectedCards, int counterId, int offset); }; #endif // COCKATRICE_PLAYER_ACTIONS_H diff --git a/cockatrice/src/game/player/player_event_handler.cpp b/cockatrice/src/game/player/player_event_handler.cpp index 3a7d0345b..bc48298f7 100644 --- a/cockatrice/src/game/player/player_event_handler.cpp +++ b/cockatrice/src/game/player/player_event_handler.cpp @@ -1,12 +1,11 @@ #include "player_event_handler.h" +#include "../../game_graphics/board/arrow_item.h" +#include "../../game_graphics/board/card_item.h" #include "../../game_graphics/zones/view_zone.h" #include "../../interface/widgets/tabs/tab_game.h" #include "../board/arrow_data.h" -#include "../board/arrow_item.h" -#include "../board/card_item.h" #include "../board/card_list.h" -#include "libcockatrice/utility/color.h" #include "player_actions.h" #include "player_logic.h" @@ -33,10 +32,12 @@ #include #include #include +#include #include PlayerEventHandler::PlayerEventHandler(PlayerLogic *_player) : QObject(_player), player(_player) { + connect(this, &PlayerEventHandler::requestCardMenuUpdate, player, &PlayerLogic::requestCardMenuUpdate); } void PlayerEventHandler::eventGameSay(const Event_GameSay &event) @@ -92,26 +93,24 @@ void PlayerEventHandler::eventRollDie(const Event_RollDie &event) void PlayerEventHandler::eventCreateArrow(const Event_CreateArrow &event) { - const ArrowData data = ArrowData::fromProto(event.arrow_info()); + auto data = QSharedPointer::create(ArrowData::fromProto( + event.arrow_info(), player->getPlayerInfo()->getId(), player->getPlayerInfo()->getLocal())); - // Resolve names for logging const auto &playerList = player->getGame()->getPlayerManager()->getPlayers(); - PlayerLogic *startPlayer = playerList.value(data.startPlayerId); - PlayerLogic *targetPlayer = playerList.value(data.targetPlayerId); + PlayerLogic *startPlayer = playerList.value(data->startPlayerId); + PlayerLogic *targetPlayer = playerList.value(data->targetPlayerId); QString startCardName, targetCardName; if (startPlayer) { - auto *zone = startPlayer->getZones().value(data.startZone); - if (zone) { - if (auto *card = zone->getCard(data.startCardId)) { + if (auto *zone = startPlayer->getZones().value(data->startZone)) { + if (auto *card = zone->getCard(data->startCardId)) { startCardName = card->getName(); } } } - if (!data.isPlayerTargeted() && targetPlayer) { - auto *zone = targetPlayer->getZones().value(data.targetZone); - if (zone) { - if (auto *card = zone->getCard(data.targetCardId)) { + if (!data->isPlayerTargeted() && targetPlayer) { + if (auto *zone = targetPlayer->getZones().value(data->targetZone)) { + if (auto *card = zone->getCard(data->targetCardId)) { targetCardName = card->getName(); } } @@ -119,16 +118,15 @@ void PlayerEventHandler::eventCreateArrow(const Event_CreateArrow &event) emit player->arrowCreateRequested(data); - const bool validForLogging = !startCardName.isEmpty() && (data.isPlayerTargeted() || !targetCardName.isEmpty()); - - if (startPlayer && targetPlayer && validForLogging) { - emit logCreateArrow(player, startPlayer, startCardName, targetPlayer, targetCardName, data.isPlayerTargeted()); + if (startPlayer && targetPlayer && !startCardName.isEmpty() && + (data->isPlayerTargeted() || !targetCardName.isEmpty())) { + emit logCreateArrow(player, startPlayer, startCardName, targetPlayer, targetCardName, data->isPlayerTargeted()); } } void PlayerEventHandler::eventDeleteArrow(const Event_DeleteArrow &event) { - emit player->arrowDeleted(event.arrow_id()); + emit player->arrowDeleted(player->getPlayerInfo()->getId(), event.arrow_id()); } void PlayerEventHandler::eventCreateToken(const Event_CreateToken &event) @@ -255,7 +253,7 @@ void PlayerEventHandler::eventSetCardCounter(const Event_SetCardCounter &event) int oldValue = card->getCounters().value(event.counter_id(), 0); card->setCounter(event.counter_id(), event.counter_value()); - player->getPlayerMenu()->updateCardMenu(card); + emit requestCardMenuUpdate(card); emit logSetCardCounter(player, card->getName(), event.counter_id(), event.counter_value(), oldValue); } @@ -373,7 +371,7 @@ void PlayerEventHandler::eventMoveCard(const Event_MoveCard &event, const GameEv targetZone->addCard(card, true, x, y); emit cardZoneChanged(card, startZone == targetZone); - player->getPlayerMenu()->updateCardMenu(card); + emit requestCardMenuUpdate(card); if (player->getPlayerActions()->isMovingCardsUntil() && startZoneString == ZoneNames::DECK && targetZone->getName() == ZoneNames::STACK) { @@ -400,7 +398,7 @@ void PlayerEventHandler::eventFlipCard(const Event_FlipCard &event) emit logFlipCard(player, card->getName(), event.face_down()); card->setFaceDown(event.face_down()); - player->getPlayerMenu()->updateCardMenu(card); + emit requestCardMenuUpdate(card); } void PlayerEventHandler::eventDestroyCard(const Event_DestroyCard &event) @@ -469,7 +467,7 @@ void PlayerEventHandler::eventAttachCard(const Event_AttachCard &event) } else { emit logUnattachCard(player, startCard->getName()); } - player->getPlayerMenu()->updateCardMenu(startCard); + emit requestCardMenuUpdate(startCard); } void PlayerEventHandler::eventDrawCards(const Event_DrawCards &event) @@ -555,7 +553,7 @@ void PlayerEventHandler::eventRevealCards(const Event_RevealCards &event, EventP } if (!options.testFlag(SKIP_REVEAL_WINDOW) && showZoneView && !cardList.isEmpty()) { - player->getGameScene()->addRevealedZoneView(player, zone, cardList, event.grant_write_access()); + emit player->requestRevealedZoneView(player, zone, cardList, event.grant_write_access()); } emit logRevealCards(player, zone, cardId, cardName, otherPlayer, false, diff --git a/cockatrice/src/game/player/player_event_handler.h b/cockatrice/src/game/player/player_event_handler.h index 958dee16b..cfd82933f 100644 --- a/cockatrice/src/game/player/player_event_handler.h +++ b/cockatrice/src/game/player/player_event_handler.h @@ -83,6 +83,7 @@ signals: void logAlwaysRevealTopCard(PlayerLogic *player, CardZoneLogic *zone, bool reveal); void logAlwaysLookAtTopCard(PlayerLogic *player, CardZoneLogic *zone, bool reveal); void cardZoneChanged(CardItem *card, bool sameZone); + void requestCardMenuUpdate(const CardItem *card); public: PlayerEventHandler(PlayerLogic *player); diff --git a/cockatrice/src/game/player/player_info.h b/cockatrice/src/game/player/player_info.h index e67131ceb..4ec39edbd 100644 --- a/cockatrice/src/game/player/player_info.h +++ b/cockatrice/src/game/player/player_info.h @@ -7,7 +7,7 @@ #ifndef COCKATRICE_PLAYER_INFO_H #define COCKATRICE_PLAYER_INFO_H -#include "player_target.h" +#include "../../game_graphics/player/player_target.h" #include #include diff --git a/cockatrice/src/game/player/player_logic.cpp b/cockatrice/src/game/player/player_logic.cpp index 67c6e9519..485e2fc5c 100644 --- a/cockatrice/src/game/player/player_logic.cpp +++ b/cockatrice/src/game/player/player_logic.cpp @@ -1,18 +1,18 @@ #include "player_logic.h" +#include "../../game_graphics/board/arrow_item.h" +#include "../../game_graphics/board/card_item.h" +#include "../../game_graphics/board/counter_general.h" +#include "../../game_graphics/game_scene.h" +#include "../../game_graphics/player/player_target.h" #include "../../game_graphics/zones/hand_zone.h" #include "../../game_graphics/zones/pile_zone.h" #include "../../game_graphics/zones/stack_zone.h" #include "../../game_graphics/zones/table_zone.h" #include "../../interface/theme_manager.h" #include "../../interface/widgets/tabs/tab_game.h" -#include "../board/arrow_item.h" -#include "../board/card_item.h" #include "../board/card_list.h" -#include "../board/counter_general.h" -#include "../game_scene.h" #include "player_actions.h" -#include "player_target.h" #include #include @@ -35,14 +35,6 @@ PlayerLogic::PlayerLogic(const ServerInfo_User &info, int _id, bool _local, bool conceded(false), zoneId(0), dialogSemaphore(false) { initializeZones(); - - playerMenu = new PlayerMenu(this); - graphicsItem = new PlayerGraphicsItem(this); - playerMenu->setMenusForGraphicItems(); - - connect(this, &PlayerLogic::activeChanged, graphicsItem, &PlayerGraphicsItem::onPlayerActiveChanged); - - connect(this, &PlayerLogic::openDeckEditor, game->getTab(), &TabGame::openDeckEditor); } void PlayerLogic::initializeZones() @@ -68,13 +60,12 @@ PlayerLogic::~PlayerLogic() } zones.clear(); - delete playerMenu; delete getPlayerInfo()->userInfo; } void PlayerLogic::clear() { - emit arrowsCleared(); + emit arrowsClearedLocally(); QMapIterator i(zones); while (i.hasNext()) { @@ -115,7 +106,7 @@ void PlayerLogic::processPlayerInfo(const ServerInfo_Player &info) /* HandZone */ ZoneNames::HAND}; clearCounters(); - emit arrowsCleared(); + emit arrowsClearedLocally(); QMutableMapIterator zoneIt(zones); while (zoneIt.hasNext()) { @@ -231,7 +222,8 @@ void PlayerLogic::processCardAttachment(const ServerInfo_Player &info) const int arrowListSize = info.arrow_list_size(); for (int i = 0; i < arrowListSize; ++i) { - emit arrowCreateRequested(ArrowData::fromProto(info.arrow_list(i))); + emit arrowCreateRequested(QSharedPointer::create( + ArrowData::fromProto(info.arrow_list(i), getPlayerInfo()->getId(), getPlayerInfo()->getLocal()))); } } @@ -325,22 +317,16 @@ void PlayerLogic::setActive(bool _active) active = _active; emit activeChanged(active); } +void PlayerLogic::onRequestZoneViewToggle(const QString &zoneName, int numberCards, bool isReversed) +{ + emit requestZoneViewToggle(this, zoneName, numberCards, isReversed); +} void PlayerLogic::updateZones() { getTableZone()->reorganizeCards(); } -PlayerGraphicsItem *PlayerLogic::getGraphicsItem() -{ - return graphicsItem; -} - -GameScene *PlayerLogic::getGameScene() -{ - return getGraphicsItem()->getGameScene(); -} - void PlayerLogic::setGameStarted() { if (playerInfo->local) { diff --git a/cockatrice/src/game/player/player_logic.h b/cockatrice/src/game/player/player_logic.h index 20d7597b4..a89cb6eed 100644 --- a/cockatrice/src/game/player/player_logic.h +++ b/cockatrice/src/game/player/player_logic.h @@ -7,6 +7,7 @@ #ifndef PLAYER_H #define PLAYER_H +#include "../../game_graphics/player/player_area.h" #include "../../interface/widgets/menus/tearoff_menu.h" #include "../board/arrow_data.h" #include "../interface/deck_loader/loaded_deck.h" @@ -14,10 +15,7 @@ #include "../zones/pile_zone_logic.h" #include "../zones/stack_zone_logic.h" #include "../zones/table_zone_logic.h" -#include "menu/player_menu.h" -#include "player_area.h" #include "player_event_handler.h" -#include "player_graphics_item.h" #include "player_info.h" #include @@ -54,6 +52,7 @@ class PlayerMenu; class QAction; class QMenu; class ServerInfo_Arrow; +class ServerInfo_Card; class ServerInfo_Counter; class ServerInfo_Player; class ServerInfo_User; @@ -67,8 +66,14 @@ class PlayerLogic : public QObject signals: void openDeckEditor(const LoadedDeck &deck); + void requestZoneViewToggle(PlayerLogic *player, const QString &zoneName, int numberCards, bool isReversed); + void requestRevealedZoneView(PlayerLogic *player, + CardZoneLogic *zone, + const QList &cardList, + bool withWritePermission); void deckChanged(); void newCardAdded(AbstractCardItem *card); + void requestCardMenuUpdate(const CardItem *card); void counterAdded(CounterState *state); void counterRemoved(int counterId); void rearrangeCounters(); @@ -78,13 +83,14 @@ signals: void clearCustomZonesMenu(); void addViewCustomZoneActionToCustomZoneMenu(QString zoneName); void resetTopCardMenuActions(); - void arrowCreateRequested(ArrowData data); - void arrowDeleteRequested(int arrowId); - void arrowDeleted(int arrowId); - void arrowsCleared(); // fires on clear() and processPlayerInfo + void arrowCreateRequested(QSharedPointer data); + void arrowDeleteRequested(int creatorId, int arrowId); + void arrowDeleted(int creatorId, int arrowId); + void arrowsClearedLocally(); // fires on clear() and processPlayerInfo public slots: void setActive(bool _active); + void onRequestZoneViewToggle(const QString &zoneName, int numberCards, bool isReversed); public: PlayerLogic(const ServerInfo_User &info, int _id, bool _local, bool _judge, AbstractGame *_parent); @@ -112,10 +118,6 @@ public: return game; } - GameScene *getGameScene(); - - [[nodiscard]] PlayerGraphicsItem *getGraphicsItem(); - [[nodiscard]] PlayerActions *getPlayerActions() const { return playerActions; @@ -131,11 +133,6 @@ public: return playerInfo; } - [[nodiscard]] PlayerMenu *getPlayerMenu() const - { - return playerMenu; - } - void setDeck(const DeckList &_deck); [[nodiscard]] const DeckList &getDeck() const @@ -234,8 +231,6 @@ private: PlayerInfo *playerInfo; PlayerEventHandler *playerEventHandler; PlayerActions *playerActions; - PlayerMenu *playerMenu; - PlayerGraphicsItem *graphicsItem; bool active; bool conceded; diff --git a/cockatrice/src/game/replay.cpp b/cockatrice/src/game/replay.cpp index 6886f817a..69f9d8b20 100644 --- a/cockatrice/src/game/replay.cpp +++ b/cockatrice/src/game/replay.cpp @@ -2,9 +2,9 @@ #include "../interface/widgets/tabs/tab_game.h" -Replay::Replay(TabGame *_tab, GameReplay *_replay) : AbstractGame(_tab) +Replay::Replay(QObject *_parent, GameReplay *_replay, bool isLocalGame) : AbstractGame(_parent) { - gameState = new GameState(this, 0, -1, tab->getTabSupervisor()->getIsLocalGame(), {}, false, false, -1, false); + gameState = new GameState(this, 0, -1, isLocalGame, {}, false, false, -1, false); connect(gameMetaInfo, &GameMetaInfo::startedChanged, gameState, &GameState::onStartedChanged); playerManager = new PlayerManager(this, -1, false, true); loadReplay(_replay); diff --git a/cockatrice/src/game/replay.h b/cockatrice/src/game/replay.h index b837e4b8c..ecb3a10d0 100644 --- a/cockatrice/src/game/replay.h +++ b/cockatrice/src/game/replay.h @@ -15,7 +15,7 @@ class Replay : public AbstractGame Q_OBJECT public: - explicit Replay(TabGame *_tab, GameReplay *_replay); + explicit Replay(QObject *_parent, GameReplay *_replay, bool isLocalGame); }; #endif // COCKATRICE_REPLAY_H diff --git a/cockatrice/src/game/selection_subtype_tally.cpp b/cockatrice/src/game/selection_subtype_tally.cpp new file mode 100644 index 000000000..e9f87fab9 --- /dev/null +++ b/cockatrice/src/game/selection_subtype_tally.cpp @@ -0,0 +1,64 @@ +#include "selection_subtype_tally.h" + +#include "../game_graphics/board/card_item.h" + +#include +#include + +namespace +{ + +/** @brief Extracts subtypes from a single card face's type line. */ +QStringList extractSubtypesFromFace(const QString &faceType) +{ + // Card type format: "Creature — Goblin Warrior" or "Legendary Enchantment — Saga" + QStringList parts = faceType.split(QStringLiteral(" — ")); + if (parts.size() > 1) { + return parts[1].split(QStringLiteral(" "), Qt::SkipEmptyParts); + } + return {}; +} + +} // anonymous namespace + +namespace SelectionSubtypeTally +{ + +QList countSubtypes(const QList &cards) +{ + QMap subtypeCounts; + + for (CardItem *card : cards) { + if (card->getFaceDown() || card->getCard().isEmpty()) { + continue; + } + + QString cardType = card->getCardInfo().getCardType(); + // Handle double-faced cards: "Creature — Human // Creature — Werewolf" + QStringList cardFaces = cardType.split(QStringLiteral(" // ")); + + for (const QString &face : cardFaces) { + QStringList subtypes = extractSubtypesFromFace(face); + for (const QString &subtype : subtypes) { + subtypeCounts[subtype]++; + } + } + } + + QList entries; + for (auto it = subtypeCounts.constBegin(); it != subtypeCounts.constEnd(); ++it) { + entries.append({it.key(), it.value()}); + } + + // Sort by count ascending, then alphabetically (lowest counts at bottom of display) + std::sort(entries.begin(), entries.end(), [](const SubtypeEntry &a, const SubtypeEntry &b) { + if (a.count != b.count) { + return a.count < b.count; + } + return a.name < b.name; + }); + + return entries; +} + +} // namespace SelectionSubtypeTally diff --git a/cockatrice/src/game/selection_subtype_tally.h b/cockatrice/src/game/selection_subtype_tally.h new file mode 100644 index 000000000..9038653f6 --- /dev/null +++ b/cockatrice/src/game/selection_subtype_tally.h @@ -0,0 +1,36 @@ +#ifndef SELECTION_SUBTYPE_TALLY_H +#define SELECTION_SUBTYPE_TALLY_H + +#include +#include + +class CardItem; + +/** @brief A single subtype (e.g., "Goblin", "Warrior") with its occurrence count. */ +struct SubtypeEntry +{ + QString name; ///< The subtype name + int count; ///< Number of selected cards with this subtype + + bool operator==(const SubtypeEntry &other) const + { + return name == other.name && count == other.count; + } +}; + +/** + * @brief Extracts and tallies subtypes from selected cards. + */ +namespace SelectionSubtypeTally +{ +/** + * @brief Parses card type lines and counts each subtype occurrence. + * + * Skips face-down cards and cards without type info. + * @param cards The list of selected card items to analyze. + * @return Entries sorted by count ascending, then alphabetically. + */ +QList countSubtypes(const QList &cards); +} // namespace SelectionSubtypeTally + +#endif diff --git a/cockatrice/src/game/zones/card_zone_logic.cpp b/cockatrice/src/game/zones/card_zone_logic.cpp index aace7097e..7e0585f4e 100644 --- a/cockatrice/src/game/zones/card_zone_logic.cpp +++ b/cockatrice/src/game/zones/card_zone_logic.cpp @@ -1,7 +1,7 @@ #include "card_zone_logic.h" +#include "../../game_graphics/board/card_item.h" #include "../../game_graphics/zones/view_zone.h" -#include "../board/card_item.h" #include "../player/player_actions.h" #include "../player/player_logic.h" #include "view_zone_logic.h" diff --git a/cockatrice/src/game/zones/hand_zone_logic.cpp b/cockatrice/src/game/zones/hand_zone_logic.cpp index 36af11131..3bdd15902 100644 --- a/cockatrice/src/game/zones/hand_zone_logic.cpp +++ b/cockatrice/src/game/zones/hand_zone_logic.cpp @@ -1,6 +1,6 @@ #include "hand_zone_logic.h" -#include "../board/card_item.h" +#include "../../game_graphics/board/card_item.h" #include "card_zone_algorithms.h" HandZoneLogic::HandZoneLogic(PlayerLogic *_player, diff --git a/cockatrice/src/game/zones/pile_zone_logic.cpp b/cockatrice/src/game/zones/pile_zone_logic.cpp index 66edde4b7..0f374fb84 100644 --- a/cockatrice/src/game/zones/pile_zone_logic.cpp +++ b/cockatrice/src/game/zones/pile_zone_logic.cpp @@ -1,6 +1,6 @@ #include "pile_zone_logic.h" -#include "../board/card_item.h" +#include "../../game_graphics/board/card_item.h" PileZoneLogic::PileZoneLogic(PlayerLogic *_player, const QString &_name, diff --git a/cockatrice/src/game/zones/stack_zone_logic.cpp b/cockatrice/src/game/zones/stack_zone_logic.cpp index 2120b9a1d..341d4b0e4 100644 --- a/cockatrice/src/game/zones/stack_zone_logic.cpp +++ b/cockatrice/src/game/zones/stack_zone_logic.cpp @@ -1,6 +1,6 @@ #include "stack_zone_logic.h" -#include "../board/card_item.h" +#include "../../game_graphics/board/card_item.h" #include "card_zone_algorithms.h" StackZoneLogic::StackZoneLogic(PlayerLogic *_player, diff --git a/cockatrice/src/game/zones/table_zone_logic.cpp b/cockatrice/src/game/zones/table_zone_logic.cpp index 3d7ac4297..a4f033819 100644 --- a/cockatrice/src/game/zones/table_zone_logic.cpp +++ b/cockatrice/src/game/zones/table_zone_logic.cpp @@ -1,6 +1,6 @@ #include "table_zone_logic.h" -#include "../board/card_item.h" +#include "../../game_graphics/board/card_item.h" TableZoneLogic::TableZoneLogic(PlayerLogic *_player, const QString &_name, diff --git a/cockatrice/src/game/zones/view_zone_logic.cpp b/cockatrice/src/game/zones/view_zone_logic.cpp index fa4a73d38..8782a1762 100644 --- a/cockatrice/src/game/zones/view_zone_logic.cpp +++ b/cockatrice/src/game/zones/view_zone_logic.cpp @@ -1,7 +1,7 @@ #include "view_zone_logic.h" #include "../../client/settings/cache_settings.h" -#include "../board/card_item.h" +#include "../../game_graphics/board/card_item.h" /** * @param _player the player that the cards are revealed to. diff --git a/cockatrice/src/game/board/abstract_card_drag_item.cpp b/cockatrice/src/game_graphics/board/abstract_card_drag_item.cpp similarity index 100% rename from cockatrice/src/game/board/abstract_card_drag_item.cpp rename to cockatrice/src/game_graphics/board/abstract_card_drag_item.cpp diff --git a/cockatrice/src/game/board/abstract_card_drag_item.h b/cockatrice/src/game_graphics/board/abstract_card_drag_item.h similarity index 100% rename from cockatrice/src/game/board/abstract_card_drag_item.h rename to cockatrice/src/game_graphics/board/abstract_card_drag_item.h diff --git a/cockatrice/src/game/board/abstract_card_item.cpp b/cockatrice/src/game_graphics/board/abstract_card_item.cpp similarity index 100% rename from cockatrice/src/game/board/abstract_card_item.cpp rename to cockatrice/src/game_graphics/board/abstract_card_item.cpp diff --git a/cockatrice/src/game/board/abstract_card_item.h b/cockatrice/src/game_graphics/board/abstract_card_item.h similarity index 91% rename from cockatrice/src/game/board/abstract_card_item.h rename to cockatrice/src/game_graphics/board/abstract_card_item.h index ed545e1ab..bdb5f7cf1 100644 --- a/cockatrice/src/game/board/abstract_card_item.h +++ b/cockatrice/src/game_graphics/board/abstract_card_item.h @@ -7,9 +7,9 @@ #ifndef ABSTRACTCARDITEM_H #define ABSTRACTCARDITEM_H -#include "../../game_graphics/board/graphics_item_type.h" #include "../card_dimensions.h" #include "arrow_target.h" +#include "graphics_item_type.h" #include #include @@ -44,6 +44,11 @@ signals: void deleteCardInfoPopup(QString cardName); void sigPixmapUpdated(); void cardShiftClicked(QString cardName); + void rightClicked(AbstractCardItem *card, QPoint screenPos); + void playSelected(AbstractCardItem *card); + void playSelectedFaceDown(AbstractCardItem *card); + void hideSelected(AbstractCardItem *card); + void selectionChanged(AbstractCardItem *card, bool selected); public: enum diff --git a/cockatrice/src/game/board/abstract_counter.cpp b/cockatrice/src/game_graphics/board/abstract_counter.cpp similarity index 97% rename from cockatrice/src/game/board/abstract_counter.cpp rename to cockatrice/src/game_graphics/board/abstract_counter.cpp index 18787a0bc..219dd456e 100644 --- a/cockatrice/src/game/board/abstract_counter.cpp +++ b/cockatrice/src/game_graphics/board/abstract_counter.cpp @@ -1,10 +1,10 @@ #include "abstract_counter.h" #include "../../client/settings/cache_settings.h" +#include "../../game/player/player_actions.h" +#include "../../game/player/player_logic.h" +#include "../../game_graphics/board/translate_counter_name.h" #include "../../interface/widgets/tabs/tab_game.h" -#include "../player/player_actions.h" -#include "../player/player_logic.h" -#include "translate_counter_name.h" #include #include diff --git a/cockatrice/src/game/board/abstract_counter.h b/cockatrice/src/game_graphics/board/abstract_counter.h similarity index 98% rename from cockatrice/src/game/board/abstract_counter.h rename to cockatrice/src/game_graphics/board/abstract_counter.h index b31bd1aa3..b319a722d 100644 --- a/cockatrice/src/game/board/abstract_counter.h +++ b/cockatrice/src/game_graphics/board/abstract_counter.h @@ -7,9 +7,9 @@ #ifndef COUNTER_H #define COUNTER_H +#include "../../game/board/counter_state.h" #include "../../interface/widgets/menus/tearoff_menu.h" #include "../player/menu/abstract_player_component.h" -#include "counter_state.h" #include #include diff --git a/cockatrice/src/game/board/arrow_item.cpp b/cockatrice/src/game_graphics/board/arrow_item.cpp similarity index 89% rename from cockatrice/src/game/board/arrow_item.cpp rename to cockatrice/src/game_graphics/board/arrow_item.cpp index 430477d76..af6a6bf36 100644 --- a/cockatrice/src/game/board/arrow_item.cpp +++ b/cockatrice/src/game_graphics/board/arrow_item.cpp @@ -2,11 +2,11 @@ #include "arrow_item.h" #include "../../client/settings/cache_settings.h" -#include "../../game_graphics/zones/card_zone.h" -#include "../player/player_actions.h" -#include "../player/player_logic.h" +#include "../../game/player/player_actions.h" +#include "../../game/player/player_logic.h" #include "../player/player_target.h" #include "../z_values.h" +#include "../zones/card_zone.h" #include "card_item.h" #include @@ -21,12 +21,8 @@ #include #include -ArrowItem::ArrowItem(PlayerLogic *_player, - int _id, - ArrowTarget *_startItem, - ArrowTarget *_targetItem, - const QColor &_color) - : player(_player), id(_id), startItem(_startItem), targetItem(_targetItem), color(_color) +ArrowItem::ArrowItem(QSharedPointer _data, ArrowTarget *_startItem, ArrowTarget *_targetItem) + : data(std::move(_data)), startItem(_startItem), targetItem(_targetItem) { setZValue(ZValues::ARROWS); @@ -52,7 +48,7 @@ ArrowItem::ArrowItem(PlayerLogic *_player, void ArrowItem::onTargetDestroyed() { - emit requestDeletion(id); + emit requestDeletion(data->creatorId, data->id); } void ArrowItem::delArrow() @@ -130,7 +126,7 @@ void ArrowItem::updatePath(const QPointF &endPoint) void ArrowItem::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*option*/, QWidget * /*widget*/) { - QColor paintColor(color); + QColor paintColor(data->color); if (fullColor) { paintColor.setAlpha(200); } else { @@ -142,7 +138,7 @@ void ArrowItem::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*opti void ArrowItem::mousePressEvent(QGraphicsSceneMouseEvent *event) { - if (!player->getPlayerInfo()->getLocal()) { + if (!data->isLocalCreator) { event->ignore(); return; } @@ -156,14 +152,20 @@ void ArrowItem::mousePressEvent(QGraphicsSceneMouseEvent *event) event->accept(); if (event->button() == Qt::RightButton) { - emit requestDeletion(id); + emit requestDeletion(data->creatorId, data->id); } } // ArrowDragItem ArrowDragItem::ArrowDragItem(PlayerLogic *_owner, ArrowTarget *_startItem, const QColor &_color, int _deleteInPhase) - : ArrowItem(_owner, -1, _startItem, nullptr, _color), deleteInPhase(_deleteInPhase) + : ArrowItem(QSharedPointer::create(ArrowData{.creatorId = _owner->getPlayerInfo()->getId(), + .isLocalCreator = true, + .id = -1, + .color = _color}), + _startItem, + nullptr), + player(_owner), deleteInPhase(_deleteInPhase) { } @@ -238,7 +240,7 @@ void ArrowDragItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) CardZoneLogic *startZone = startCard->getZone(); Command_CreateArrow cmd; - cmd.mutable_arrow_color()->CopyFrom(convertQColorToColor(color)); + cmd.mutable_arrow_color()->CopyFrom(convertQColorToColor(data->color)); cmd.set_start_player_id(startZone->getPlayer()->getPlayerInfo()->getId()); cmd.set_start_zone(startZone->getName().toStdString()); cmd.set_start_card_id(startCard->getId()); @@ -284,7 +286,14 @@ void ArrowDragItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) // ArrowAttachItem ArrowAttachItem::ArrowAttachItem(ArrowTarget *_startItem) - : ArrowItem(_startItem->getOwner(), -1, _startItem, nullptr, Qt::green) + : ArrowItem( + QSharedPointer::create(ArrowData{.creatorId = _startItem->getOwner()->getPlayerInfo()->getId(), + .isLocalCreator = true, + .id = -1, + .color = Qt::green}), + _startItem, + nullptr), + player(_startItem->getOwner()) { } diff --git a/cockatrice/src/game/board/arrow_item.h b/cockatrice/src/game_graphics/board/arrow_item.h similarity index 85% rename from cockatrice/src/game/board/arrow_item.h rename to cockatrice/src/game_graphics/board/arrow_item.h index 7dc0f9477..1c306e065 100644 --- a/cockatrice/src/game/board/arrow_item.h +++ b/cockatrice/src/game_graphics/board/arrow_item.h @@ -1,20 +1,15 @@ -/** - * @file arrow_item.h - * @ingroup GameGraphics - */ -//! \todo Document this file. - #ifndef ARROWITEM_H #define ARROWITEM_H +#include "../../game/board/arrow_data.h" #include "arrow_target.h" #include #include +#include class CardItem; class QGraphicsSceneMouseEvent; -class QMenu; class PlayerLogic; class ArrowItem : public QObject, public QGraphicsItem @@ -22,25 +17,27 @@ class ArrowItem : public QObject, public QGraphicsItem Q_OBJECT Q_INTERFACES(QGraphicsItem) signals: - void requestDeletion(int id); + void requestDeletion(int creatorId, int id); private: QPainterPath path; protected: - PlayerLogic *player; - int id; + QSharedPointer data; QPointer startItem; QPointer targetItem; bool targetLocked = false; - QColor color; bool fullColor = true; void mousePressEvent(QGraphicsSceneMouseEvent *event) override; public: - ArrowItem(PlayerLogic *_player, int _id, ArrowTarget *_startItem, ArrowTarget *_targetItem, const QColor &_color); + ArrowItem(QSharedPointer _data, ArrowTarget *_startItem, ArrowTarget *_targetItem); + void onTargetDestroyed(); + void delArrow(); + void updatePath(); + void updatePath(const QPointF &endPoint); void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override; [[nodiscard]] QRectF boundingRect() const override @@ -51,17 +48,13 @@ public: { return path; } - - void updatePath(); - void updatePath(const QPointF &endPoint); - [[nodiscard]] int getId() const { - return id; + return data->id; } - [[nodiscard]] PlayerLogic *getPlayer() const + [[nodiscard]] int getCreatorId() const { - return player; + return data->creatorId; } [[nodiscard]] ArrowTarget *getStartItem() const { @@ -75,14 +68,13 @@ public: { targetLocked = _targetLocked; } - - void delArrow(); }; class ArrowDragItem : public ArrowItem { Q_OBJECT private: + PlayerLogic *player; int deleteInPhase; QList childArrows; QMetaObject::Connection positionConnection; @@ -100,6 +92,7 @@ class ArrowAttachItem : public ArrowItem { Q_OBJECT private: + PlayerLogic *player; QList childArrows; QMetaObject::Connection positionConnection; void attachCards(CardItem *startCard, const CardItem *targetCard); @@ -113,4 +106,4 @@ protected: void mouseReleaseEvent(QGraphicsSceneMouseEvent *event) override; }; -#endif // ARROWITEM_H +#endif \ No newline at end of file diff --git a/cockatrice/src/game/board/arrow_target.cpp b/cockatrice/src/game_graphics/board/arrow_target.cpp similarity index 92% rename from cockatrice/src/game/board/arrow_target.cpp rename to cockatrice/src/game_graphics/board/arrow_target.cpp index edf526e4e..79b21d921 100644 --- a/cockatrice/src/game/board/arrow_target.cpp +++ b/cockatrice/src/game_graphics/board/arrow_target.cpp @@ -1,6 +1,6 @@ #include "arrow_target.h" -#include "../player/player_logic.h" +#include "../../game/player/player_logic.h" #include "arrow_item.h" ArrowTarget::ArrowTarget(PlayerLogic *_owner, QGraphicsItem *parent) : AbstractGraphicsItem(parent), owner(_owner) diff --git a/cockatrice/src/game/board/arrow_target.h b/cockatrice/src/game_graphics/board/arrow_target.h similarity index 93% rename from cockatrice/src/game/board/arrow_target.h rename to cockatrice/src/game_graphics/board/arrow_target.h index 664572705..bf89c5456 100644 --- a/cockatrice/src/game/board/arrow_target.h +++ b/cockatrice/src/game_graphics/board/arrow_target.h @@ -7,7 +7,7 @@ #ifndef ARROWTARGET_H #define ARROWTARGET_H -#include "../../game_graphics/board/abstract_graphics_item.h" +#include "abstract_graphics_item.h" #include diff --git a/cockatrice/src/game/board/card_drag_item.cpp b/cockatrice/src/game_graphics/board/card_drag_item.cpp similarity index 96% rename from cockatrice/src/game/board/card_drag_item.cpp rename to cockatrice/src/game_graphics/board/card_drag_item.cpp index 39fb9a390..49467c5c9 100644 --- a/cockatrice/src/game/board/card_drag_item.cpp +++ b/cockatrice/src/game_graphics/board/card_drag_item.cpp @@ -1,9 +1,9 @@ #include "card_drag_item.h" -#include "../../game_graphics/zones/card_zone.h" -#include "../../game_graphics/zones/table_zone.h" -#include "../../game_graphics/zones/view_zone.h" #include "../game_scene.h" +#include "../zones/card_zone.h" +#include "../zones/table_zone.h" +#include "../zones/view_zone.h" #include "card_item.h" #include diff --git a/cockatrice/src/game/board/card_drag_item.h b/cockatrice/src/game_graphics/board/card_drag_item.h similarity index 100% rename from cockatrice/src/game/board/card_drag_item.h rename to cockatrice/src/game_graphics/board/card_drag_item.h diff --git a/cockatrice/src/game/board/card_item.cpp b/cockatrice/src/game_graphics/board/card_item.cpp similarity index 92% rename from cockatrice/src/game/board/card_item.cpp rename to cockatrice/src/game_graphics/board/card_item.cpp index a08194540..cabe988c2 100644 --- a/cockatrice/src/game/board/card_item.cpp +++ b/cockatrice/src/game_graphics/board/card_item.cpp @@ -1,14 +1,14 @@ #include "card_item.h" #include "../../client/settings/cache_settings.h" -#include "../../game_graphics/zones/table_zone.h" -#include "../../game_graphics/zones/view_zone.h" +#include "../../game/phase.h" +#include "../../game/player/player_actions.h" +#include "../../game/player/player_logic.h" +#include "../../game/zones/view_zone_logic.h" #include "../../interface/widgets/tabs/tab_game.h" #include "../game_scene.h" -#include "../phase.h" -#include "../player/player_actions.h" -#include "../player/player_logic.h" -#include "../zones/view_zone_logic.h" +#include "../zones/table_zone.h" +#include "../zones/view_zone.h" #include "arrow_item.h" #include "card_drag_item.h" @@ -40,7 +40,7 @@ void CardItem::prepareDelete() { if (owner != nullptr) { if (owner->getGame()->getActiveCard() == this) { - owner->getPlayerMenu()->updateCardMenu(nullptr); + emit owner->requestCardMenuUpdate(nullptr); owner->getGame()->setActiveCard(nullptr); } owner = nullptr; @@ -399,8 +399,11 @@ void CardItem::playCard(bool faceDown) emit tz->toggleTapped(); } else { if (SettingsCache::instance().getClickPlaysAllSelected()) { - faceDown ? state->getZone()->getPlayer()->getPlayerActions()->actPlayFacedown() - : state->getZone()->getPlayer()->getPlayerActions()->actPlay(); + if (faceDown) { + emit playSelectedFaceDown(this); + } else { + emit playSelected(this); + } } else { state->getZone()->getPlayer()->getPlayerActions()->playCard(this, faceDown); } @@ -460,7 +463,7 @@ void CardItem::handleClickedToPlay(bool shiftHeld) { if (isUnwritableRevealZone(state->getZone())) { if (SettingsCache::instance().getClickPlaysAllSelected()) { - state->getZone()->getPlayer()->getPlayerActions()->actHide(); + emit hideSelected(this); } else { state->getZone()->removeCard(this); } @@ -471,21 +474,15 @@ void CardItem::handleClickedToPlay(bool shiftHeld) void CardItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) { - if (event->button() == Qt::RightButton) { - - if (owner != nullptr) { - owner->getGame()->setActiveCard(this); - if (QMenu *cardMenu = owner->getPlayerMenu()->updateCardMenu(this)) { - cardMenu->popup(event->screenPos()); - return; - } - } - } else if ((event->modifiers() != Qt::AltModifier) && (event->button() == Qt::LeftButton) && - (!SettingsCache::instance().getDoubleClickToPlay())) { + if (event->button() == Qt::RightButton && owner != nullptr) { + emit rightClicked(this, event->screenPos()); + return; + } + if ((event->modifiers() != Qt::AltModifier) && (event->button() == Qt::LeftButton) && + (!SettingsCache::instance().getDoubleClickToPlay())) { handleClickedToPlay(event->modifiers().testFlag(Qt::ShiftModifier)); } - - if (owner != nullptr) { // cards without owner will be deleted + if (owner != nullptr) { setCursor(Qt::OpenHandCursor); } AbstractCardItem::mouseReleaseEvent(event); @@ -531,14 +528,14 @@ bool CardItem::animationEvent() QVariant CardItem::itemChange(GraphicsItemChange change, const QVariant &value) { if ((change == ItemSelectedHasChanged) && owner != nullptr) { - if (value == true) { - owner->getGame()->setActiveCard(this); - owner->getPlayerMenu()->updateCardMenu(this); - } else if (owner->getGameScene()->selectedItems().isEmpty()) { + bool selected = value.toBool(); - owner->getGame()->setActiveCard(nullptr); - owner->getPlayerMenu()->updateCardMenu(nullptr); + if (selected) { + owner->getGame()->setActiveCard(this); } + + emit selectionChanged(this, selected); } + return AbstractCardItem::itemChange(change, value); -} +} \ No newline at end of file diff --git a/cockatrice/src/game/board/card_item.h b/cockatrice/src/game_graphics/board/card_item.h similarity index 97% rename from cockatrice/src/game/board/card_item.h rename to cockatrice/src/game_graphics/board/card_item.h index 87f9667de..37f3bab50 100644 --- a/cockatrice/src/game/board/card_item.h +++ b/cockatrice/src/game_graphics/board/card_item.h @@ -7,12 +7,11 @@ #ifndef CARDITEM_H #define CARDITEM_H -#include "../zones/card_zone_logic.h" +#include "../../game/board/card_state.h" +#include "../../game/zones/card_zone_logic.h" #include "abstract_card_item.h" -#include "card_state.h" #include -#include class CardDatabase; class CardDragItem; diff --git a/cockatrice/src/game/board/counter_general.cpp b/cockatrice/src/game_graphics/board/counter_general.cpp similarity index 95% rename from cockatrice/src/game/board/counter_general.cpp rename to cockatrice/src/game_graphics/board/counter_general.cpp index 5147ede6b..379c6f837 100644 --- a/cockatrice/src/game/board/counter_general.cpp +++ b/cockatrice/src/game_graphics/board/counter_general.cpp @@ -1,7 +1,7 @@ #include "counter_general.h" -#include "../../game_graphics/board/abstract_graphics_item.h" #include "../../interface/pixel_map_generator.h" +#include "abstract_graphics_item.h" #include diff --git a/cockatrice/src/game/board/counter_general.h b/cockatrice/src/game_graphics/board/counter_general.h similarity index 100% rename from cockatrice/src/game/board/counter_general.h rename to cockatrice/src/game_graphics/board/counter_general.h diff --git a/cockatrice/src/game/board/translate_counter_name.cpp b/cockatrice/src/game_graphics/board/translate_counter_name.cpp similarity index 100% rename from cockatrice/src/game/board/translate_counter_name.cpp rename to cockatrice/src/game_graphics/board/translate_counter_name.cpp diff --git a/cockatrice/src/game/board/translate_counter_name.h b/cockatrice/src/game_graphics/board/translate_counter_name.h similarity index 100% rename from cockatrice/src/game/board/translate_counter_name.h rename to cockatrice/src/game_graphics/board/translate_counter_name.h diff --git a/cockatrice/src/game/card_dimensions.h b/cockatrice/src/game_graphics/card_dimensions.h similarity index 100% rename from cockatrice/src/game/card_dimensions.h rename to cockatrice/src/game_graphics/card_dimensions.h diff --git a/cockatrice/src/game/deckview/deck_view.cpp b/cockatrice/src/game_graphics/deckview/deck_view.cpp similarity index 98% rename from cockatrice/src/game/deckview/deck_view.cpp rename to cockatrice/src/game_graphics/deckview/deck_view.cpp index ced02c8db..a5d0fa3bc 100644 --- a/cockatrice/src/game/deckview/deck_view.cpp +++ b/cockatrice/src/game_graphics/deckview/deck_view.cpp @@ -360,6 +360,16 @@ void DeckViewScene::rebuildTree() return; } + QStringList requiredZones = {DECK_ZONE_MAIN, DECK_ZONE_SIDE}; + + for (const QString &zoneName : requiredZones) { + if (!cardContainers.contains(zoneName)) { + auto *container = new DeckViewCardContainer(zoneName); + cardContainers.insert(zoneName, container); + addItem(container); + } + } + for (auto *currentZone : deck->getZoneNodes()) { DeckViewCardContainer *container = cardContainers.value(currentZone->getName(), 0); if (!container) { diff --git a/cockatrice/src/game/deckview/deck_view.h b/cockatrice/src/game_graphics/deckview/deck_view.h similarity index 100% rename from cockatrice/src/game/deckview/deck_view.h rename to cockatrice/src/game_graphics/deckview/deck_view.h diff --git a/cockatrice/src/game/deckview/deck_view_container.cpp b/cockatrice/src/game_graphics/deckview/deck_view_container.cpp similarity index 98% rename from cockatrice/src/game/deckview/deck_view_container.cpp rename to cockatrice/src/game_graphics/deckview/deck_view_container.cpp index cbd6c2bad..d476a5012 100644 --- a/cockatrice/src/game/deckview/deck_view_container.cpp +++ b/cockatrice/src/game_graphics/deckview/deck_view_container.cpp @@ -19,7 +19,7 @@ #include #include #include -#include +#include ToggleButton::ToggleButton(QWidget *parent) : QPushButton(parent), state(false) { @@ -209,6 +209,7 @@ void DeckViewContainer::refreshShortcuts() loadLocalButton->setShortcut(shortcuts.getSingleShortcut("DeckViewContainer/loadLocalButton")); loadRemoteButton->setShortcut(shortcuts.getSingleShortcut("DeckViewContainer/loadRemoteButton")); loadFromClipboardButton->setShortcut(shortcuts.getSingleShortcut("DeckViewContainer/loadFromClipboardButton")); + loadFromWebsiteButton->setShortcut(shortcuts.getSingleShortcut("DeckViewContainer/loadFromWebsiteButton")); unloadDeckButton->setShortcut(shortcuts.getSingleShortcut("DeckViewContainer/unloadDeckButton")); readyStartButton->setShortcut(shortcuts.getSingleShortcut("DeckViewContainer/readyStartButton")); sideboardLockButton->setShortcut(shortcuts.getSingleShortcut("DeckViewContainer/sideboardLockButton")); diff --git a/cockatrice/src/game/deckview/deck_view_container.h b/cockatrice/src/game_graphics/deckview/deck_view_container.h similarity index 100% rename from cockatrice/src/game/deckview/deck_view_container.h rename to cockatrice/src/game_graphics/deckview/deck_view_container.h diff --git a/cockatrice/src/game/deckview/tabbed_deck_view_container.cpp b/cockatrice/src/game_graphics/deckview/tabbed_deck_view_container.cpp similarity index 100% rename from cockatrice/src/game/deckview/tabbed_deck_view_container.cpp rename to cockatrice/src/game_graphics/deckview/tabbed_deck_view_container.cpp diff --git a/cockatrice/src/game/deckview/tabbed_deck_view_container.h b/cockatrice/src/game_graphics/deckview/tabbed_deck_view_container.h similarity index 100% rename from cockatrice/src/game/deckview/tabbed_deck_view_container.h rename to cockatrice/src/game_graphics/deckview/tabbed_deck_view_container.h diff --git a/cockatrice/src/game/dialogs/dlg_create_token.cpp b/cockatrice/src/game_graphics/dialogs/dlg_create_token.cpp similarity index 99% rename from cockatrice/src/game/dialogs/dlg_create_token.cpp rename to cockatrice/src/game_graphics/dialogs/dlg_create_token.cpp index 11c24b72e..1a9dc59b2 100644 --- a/cockatrice/src/game/dialogs/dlg_create_token.cpp +++ b/cockatrice/src/game_graphics/dialogs/dlg_create_token.cpp @@ -20,7 +20,7 @@ #include #include #include -#include +#include DlgCreateToken::DlgCreateToken(const QStringList &_predefinedTokens, QWidget *parent) : QDialog(parent), predefinedTokens(_predefinedTokens) diff --git a/cockatrice/src/game/dialogs/dlg_create_token.h b/cockatrice/src/game_graphics/dialogs/dlg_create_token.h similarity index 100% rename from cockatrice/src/game/dialogs/dlg_create_token.h rename to cockatrice/src/game_graphics/dialogs/dlg_create_token.h diff --git a/cockatrice/src/game/dialogs/dlg_move_top_cards_until.cpp b/cockatrice/src/game_graphics/dialogs/dlg_move_top_cards_until.cpp similarity index 100% rename from cockatrice/src/game/dialogs/dlg_move_top_cards_until.cpp rename to cockatrice/src/game_graphics/dialogs/dlg_move_top_cards_until.cpp diff --git a/cockatrice/src/game/dialogs/dlg_move_top_cards_until.h b/cockatrice/src/game_graphics/dialogs/dlg_move_top_cards_until.h similarity index 100% rename from cockatrice/src/game/dialogs/dlg_move_top_cards_until.h rename to cockatrice/src/game_graphics/dialogs/dlg_move_top_cards_until.h diff --git a/cockatrice/src/game/dialogs/dlg_roll_dice.cpp b/cockatrice/src/game_graphics/dialogs/dlg_roll_dice.cpp similarity index 97% rename from cockatrice/src/game/dialogs/dlg_roll_dice.cpp rename to cockatrice/src/game_graphics/dialogs/dlg_roll_dice.cpp index dfb3d0bc5..4f5d5b861 100644 --- a/cockatrice/src/game/dialogs/dlg_roll_dice.cpp +++ b/cockatrice/src/game_graphics/dialogs/dlg_roll_dice.cpp @@ -5,7 +5,7 @@ #include #include #include -#include +#include DlgRollDice::DlgRollDice(QWidget *parent) : QDialog(parent) { diff --git a/cockatrice/src/game/dialogs/dlg_roll_dice.h b/cockatrice/src/game_graphics/dialogs/dlg_roll_dice.h similarity index 100% rename from cockatrice/src/game/dialogs/dlg_roll_dice.h rename to cockatrice/src/game_graphics/dialogs/dlg_roll_dice.h diff --git a/cockatrice/src/game/game_scene.cpp b/cockatrice/src/game_graphics/game_scene.cpp similarity index 81% rename from cockatrice/src/game/game_scene.cpp rename to cockatrice/src/game_graphics/game_scene.cpp index 1b4f0d461..b9816a602 100644 --- a/cockatrice/src/game/game_scene.cpp +++ b/cockatrice/src/game_graphics/game_scene.cpp @@ -1,13 +1,17 @@ #include "game_scene.h" #include "../client/settings/cache_settings.h" -#include "../game_graphics/zones/select_zone.h" -#include "../game_graphics/zones/view_zone.h" -#include "../game_graphics/zones/view_zone_widget.h" +#include "../game/abstract_game.h" +#include "../game/player/player_actions.h" +#include "../game/player/player_logic.h" +#include "../game_graphics/player/player_graphics_item.h" #include "board/card_item.h" #include "phases_toolbar.h" +#include "player/menu/player_menu.h" #include "player/player_graphics_item.h" -#include "player/player_logic.h" +#include "zones/select_zone.h" +#include "zones/view_zone.h" +#include "zones/view_zone_widget.h" #include #include @@ -72,6 +76,80 @@ QList GameScene::selectedCards() const return selectedCards; } +void GameScene::onCardSelectionChanged(AbstractCardItem *abstractCard, bool selected) +{ + CardItem *card = qobject_cast(abstractCard); + if (!card || !card->getOwner()) { + return; + } + + auto *owner = card->getOwner(); + + if (selected) { + owner->requestCardMenuUpdate(card); + return; + } + + if (selectedItems().isEmpty()) { + owner->getGame()->setActiveCard(nullptr); + owner->requestCardMenuUpdate(nullptr); + } +} + +void GameScene::onCardRightClicked(AbstractCardItem *abstractCard, QPoint screenPos) +{ + auto *card = qobject_cast(abstractCard); + if (!card) { + return; + } + if (!card->getOwner()) { + return; + } + auto *view = playerViews.value(card->getOwner()->getPlayerInfo()->getId()); + if (!view) { + return; + } + + card->getOwner()->getGame()->setActiveCard(card); + + if (auto *menu = view->getPlayerMenu()->updateCardMenu(card)) { + menu->popup(screenPos); + } +} + +void GameScene::playSelected(AbstractCardItem *card) +{ + if (!card) { + return; + } + if (!card->getOwner()) { + return; + } + card->getOwner()->getPlayerActions()->actPlay(selectedCards()); +} + +void GameScene::playSelectedFaceDown(AbstractCardItem *card) +{ + if (!card) { + return; + } + if (!card->getOwner()) { + return; + } + card->getOwner()->getPlayerActions()->actPlayFacedown(selectedCards()); +} + +void GameScene::hideSelected(AbstractCardItem *card) +{ + if (!card) { + return; + } + if (!card->getOwner()) { + return; + } + card->getOwner()->getPlayerActions()->actHide(selectedCards()); +} + /** * @brief Adds a player to the scene and stores their graphics item. * @param player Player to add. @@ -82,9 +160,11 @@ void GameScene::addPlayer(PlayerLogic *player) { qCInfo(GameScenePlayerAdditionRemovalLog) << "GameScene::addPlayer name=" << player->getPlayerInfo()->getName(); - playerViews.insert(player->getPlayerInfo()->getId(), player->getGraphicsItem()); - addItem(player->getGraphicsItem()); - connect(player->getGraphicsItem(), &PlayerGraphicsItem::sizeChanged, this, &GameScene::rearrange); + auto *view = new PlayerGraphicsItem(player); + + playerViews.insert(player->getPlayerInfo()->getId(), view); + addItem(view); + connect(view, &PlayerGraphicsItem::sizeChanged, this, &GameScene::rearrange); connect(player, &PlayerLogic::concededChanged, this, [this](int id, bool conceded) { if (conceded) { @@ -93,11 +173,13 @@ void GameScene::addPlayer(PlayerLogic *player) rearrange(); }); + connect(player, &PlayerLogic::requestZoneViewToggle, this, &GameScene::toggleZoneView); + connect(player, &PlayerLogic::requestRevealedZoneView, this, &GameScene::addRevealedZoneView); connect(player, &PlayerLogic::arrowDeleted, this, &GameScene::deleteArrow); connect(player, &PlayerLogic::arrowCreateRequested, this, &GameScene::addArrow); connect(player, &PlayerLogic::arrowDeleteRequested, this, &GameScene::requestArrowDeletion); - connect(player, &PlayerLogic::arrowsCleared, this, - [this, id = player->getPlayerInfo()->getId()]() { clearArrowsForPlayer(id); }); + connect(player, &PlayerLogic::arrowsClearedLocally, this, + [this, id = player->getPlayerInfo()->getId()]() { clearArrowsForPlayerLocally(id); }); connect(player->getPlayerEventHandler(), &PlayerEventHandler::cardZoneChanged, this, &GameScene::onCardZoneChanged); @@ -123,6 +205,7 @@ void GameScene::removePlayer(PlayerLogic *player) } auto *view = playerViews.take(player->getPlayerInfo()->getId()); removeItem(view); + view->deleteLater(); rearrange(); } @@ -204,7 +287,7 @@ QList GameScene::collectActivePlayers(int &firstPlayerIndex) cons bool firstPlayerFound = false; for (auto *pgItem : playerViews.values()) { - PlayerLogic *p = pgItem->getPlayer(); + PlayerLogic *p = pgItem->getLogic(); if (p && !p->getConceded()) { activePlayers.append(p); if (!firstPlayerFound && p->getPlayerInfo()->getLocal()) { @@ -275,12 +358,12 @@ QSizeF GameScene::computeSceneSizeAndPlayerLayout(const QList &pl for (int j = 0; j < rowsInColumn; ++j) { PlayerLogic *player = playersIter.next(); if (col == 0) { - playersByColumn[col].prepend(player->getGraphicsItem()); + playersByColumn[col].prepend(playerViews.value(player->getPlayerInfo()->getId())); } else { - playersByColumn[col].append(player->getGraphicsItem()); + playersByColumn[col].append(playerViews.value(player->getPlayerInfo()->getId())); } - auto *pgItem = player->getGraphicsItem(); + auto *pgItem = playerViews.value(player->getPlayerInfo()->getId()); thisColumnHeight += pgItem->boundingRect().height() + playerAreaSpacing; columnWidth[col] = std::max(columnWidth[col], (int)pgItem->boundingRect().width()); } @@ -367,86 +450,62 @@ void GameScene::resizeColumnsAndPlayers(const QList &minWidthByColumn, qr } } -void GameScene::addArrow(const ArrowData &data) +void GameScene::addArrow(QSharedPointer data) { - auto *startView = playerViews.value(data.startPlayerId); - auto *targetView = playerViews.value(data.targetPlayerId); + auto *startView = playerViews.value(data->startPlayerId); + auto *targetView = playerViews.value(data->targetPlayerId); if (!startView || !targetView) { return; } - PlayerLogic *startLogic = startView->getPlayer(); - auto *startZone = startLogic->getZones().value(data.startZone); + PlayerLogic *startLogic = startView->getLogic(); + auto *startZone = startLogic->getZones().value(data->startZone); if (!startZone) { return; } - CardItem *startCard = startZone->getCard(data.startCardId); + CardItem *startCard = startZone->getCard(data->startCardId); if (!startCard) { return; } ArrowTarget *targetItem = nullptr; - if (data.isPlayerTargeted()) { + if (data->isPlayerTargeted()) { targetItem = targetView->getPlayerTarget(); } else { - auto *zone = targetView->getPlayer()->getZones().value(data.targetZone); + auto *zone = targetView->getLogic()->getZones().value(data->targetZone); if (zone) { - targetItem = zone->getCard(data.targetCardId); + targetItem = zone->getCard(data->targetCardId); } } if (!targetItem) { return; } - auto *arrow = new ArrowItem(startView->getPlayer(), data.id, startCard, targetItem, data.color); + auto *arrow = new ArrowItem(data, startCard, targetItem); addItem(arrow); - arrowRegistry.insert(data.id, arrow); + arrowRegistry.insert(data, arrow); connect(arrow, &ArrowItem::requestDeletion, this, &GameScene::requestArrowDeletion); } -void GameScene::deleteArrow(int arrowId) +void GameScene::deleteArrow(int playerId, int arrowId) { - if (arrowRegistry.contains(arrowId)) { - arrowRegistry.take(arrowId)->delArrow(); + if (auto *arrow = arrowRegistry.take(playerId, arrowId)) { + arrow->delArrow(); } } -void GameScene::clearArrowsForPlayer(int playerId) +void GameScene::requestArrowDeletion(int playerId, int arrowId) { - QList toDelete; - for (auto i = arrowRegistry.cbegin(); i != arrowRegistry.cend(); ++i) { - auto *arrow = i.value(); - if (arrow->getPlayer()->getPlayerInfo()->getId() == playerId) { - toDelete.append(i.key()); - } - } - - for (int arrowId : toDelete) { - arrowRegistry.take(arrowId)->delArrow(); - } -} - -void GameScene::requestArrowDeletion(int arrowId) -{ - if (arrowRegistry.contains(arrowId)) { - emit arrowDeletionRequested(arrowId); - } -} - -void GameScene::requestClearArrowsForPlayer(int playerId) -{ - for (auto *arrow : arrowRegistry.values()) { - if (arrow->getPlayer()->getPlayerInfo()->getId() == playerId) { - emit requestArrowDeletion(arrow->getId()); - } + if (arrowRegistry.contains(playerId, arrowId)) { + emit arrowDeletionRequested(playerId, arrowId); } } void GameScene::onCardZoneChanged(CardItem *card, bool sameZone) { QList toDelete; - for (auto *arrow : arrowRegistry.values()) { + for (auto *arrow : arrowRegistry.all()) { if (arrow->getStartItem() == card || arrow->getTargetItem() == card) { if (sameZone) { arrow->updatePath(); @@ -456,7 +515,21 @@ void GameScene::onCardZoneChanged(CardItem *card, bool sameZone) } } for (auto *arrow : toDelete) { - deleteArrow(arrow->getId()); + deleteArrow(arrow->getCreatorId(), arrow->getId()); + } +} + +void GameScene::clearArrowsForPlayer(int playerId) +{ + for (int arrowId : arrowRegistry.idsForPlayer(playerId)) { + emit requestArrowDeletion(playerId, arrowId); + } +} + +void GameScene::clearArrowsForPlayerLocally(int playerId) +{ + for (int arrowId : arrowRegistry.idsForPlayer(playerId)) { + arrowRegistry.take(playerId, arrowId)->delArrow(); } } diff --git a/cockatrice/src/game/game_scene.h b/cockatrice/src/game_graphics/game_scene.h similarity index 89% rename from cockatrice/src/game/game_scene.h rename to cockatrice/src/game_graphics/game_scene.h index 1551c8365..74e979556 100644 --- a/cockatrice/src/game/game_scene.h +++ b/cockatrice/src/game_graphics/game_scene.h @@ -1,9 +1,10 @@ #ifndef GAMESCENE_H #define GAMESCENE_H -#include "board/arrow_data.h" +#include "../game/arrow_registry.h" +#include "../game/board/arrow_data.h" +#include "../game/zones/card_zone_logic.h" #include "board/arrow_item.h" -#include "zones/card_zone_logic.h" #include #include @@ -45,7 +46,7 @@ private: PhasesToolbar *phasesToolbar; ///< Toolbar showing game phases QMap playerViews; ///< ID lookup for player graphics items QList> playersByColumn; ///< Players organized by column - QMap arrowRegistry; ///< ID registry for arrow graphics items + ArrowRegistry arrowRegistry; ///< ID registry for arrow graphics items QList zoneViews; ///< Active zone view widgets QSize viewSize; ///< Current view size QPointer hoveredCard; ///< Currently hovered card @@ -96,6 +97,16 @@ public: */ void removePlayer(PlayerLogic *player); + QMap getPlayers() const + { + return playerViews; + } + + PlayerGraphicsItem *viewForPlayer(int playerId) + { + return playerViews.value(playerId); + } + /** * @brief Adjusts the global rotation offset for player layout. * @param rotationAdjustment Number of positions to rotate. @@ -181,6 +192,11 @@ public: void stopRubberBand(); public slots: + void onCardSelectionChanged(AbstractCardItem *card, bool selected); + void onCardRightClicked(AbstractCardItem *card, QPoint screenPos); + void playSelected(AbstractCardItem *card); + void playSelectedFaceDown(AbstractCardItem *card); + void hideSelected(AbstractCardItem *card); /** @brief Toggles a zone view for a player. */ void toggleZoneView(PlayerLogic *player, const QString &zoneName, int numberCards, bool isReversed = false); @@ -202,13 +218,13 @@ public slots: QTransform getViewportTransform() const; /// Directly modifies the scene - void addArrow(const ArrowData &data); - void deleteArrow(int arrowId); + void addArrow(QSharedPointer data); + void deleteArrow(int playerId, int arrowId); void clearArrowsForPlayer(int playerId); + void clearArrowsForPlayerLocally(int playerId); /// Queues up arrow deletion but doesn't directly modify the scene - void requestArrowDeletion(int arrowId); - void requestClearArrowsForPlayer(int playerId); + void requestArrowDeletion(int playerId, int arrowId); void onCardZoneChanged(CardItem *card, bool sameZone); @@ -223,7 +239,7 @@ signals: void sigStartRubberBand(const QPointF &selectionOrigin); void sigResizeRubberBand(const QPointF &cursorPoint, int selectedCount); void sigStopRubberBand(); - void arrowDeletionRequested(int arrowId); + void arrowDeletionRequested(int creatorId, int arrowId); }; #endif diff --git a/cockatrice/src/game_graphics/game_view.cpp b/cockatrice/src/game_graphics/game_view.cpp new file mode 100644 index 000000000..c2d9b2b3b --- /dev/null +++ b/cockatrice/src/game_graphics/game_view.cpp @@ -0,0 +1,297 @@ +#include "game_view.h" + +#include "../client/settings/cache_settings.h" +#include "../game/selection_subtype_tally.h" +#include "game_scene.h" + +#include +#include +#include +#include +#include +#include +#include + +// QRubberBand calls raise() in showEvent() and changeEvent() to stay on top of siblings. +// This subclass disables that behavior so dragCountLabel can appear above it. +class SelectionRubberBand : public QRubberBand +{ +public: + using QRubberBand::QRubberBand; + +protected: + void showEvent(QShowEvent *event) override + { + QWidget::showEvent(event); // Skip QRubberBand's raise() + } + + void changeEvent(QEvent *event) override + { + if (event->type() == QEvent::ZOrderChange) { + return; // Skip QRubberBand's raise() on z-order changes + } + QRubberBand::changeEvent(event); + } +}; + +GameView::GameView(GameScene *scene, QWidget *parent) : QGraphicsView(scene, parent), rubberBand(0) +{ + setBackgroundBrush(QBrush(QColor(0, 0, 0))); + setRenderHints(QPainter::TextAntialiasing | QPainter::Antialiasing); + setViewportUpdateMode(BoundingRectViewportUpdate); + + connect(scene, &GameScene::sceneRectChanged, this, &GameView::updateSceneRect); + + connect(scene, &GameScene::sigStartRubberBand, this, &GameView::startRubberBand); + connect(scene, &GameScene::sigResizeRubberBand, this, &GameView::resizeRubberBand); + connect(scene, &GameScene::sigStopRubberBand, this, &GameView::stopRubberBand); + connect(scene, &QGraphicsScene::selectionChanged, this, [this]() { updateTotalSelectionCount(); }); + + setFocusDisabled(SettingsCache::instance().getKeepGameChatFocus()); + connect(&SettingsCache::instance(), &SettingsCache::keepGameChatFocusChanged, this, &GameView::setFocusDisabled); + + aCloseMostRecentZoneView = new QAction(this); + + connect(aCloseMostRecentZoneView, &QAction::triggered, scene, &GameScene::closeMostRecentZoneView); + addAction(aCloseMostRecentZoneView); + connect(&SettingsCache::instance().shortcuts(), &ShortcutsSettings::shortCutChanged, this, + &GameView::refreshShortcuts); + refreshShortcuts(); + rubberBand = new SelectionRubberBand(QRubberBand::Rectangle, this); + + const QString baseProperties = "color: white; " + "font-family: monospace; " + "background-color: rgba(0, 0, 0, 160); " + "border-radius: 3px; " + "padding: 1px 2px; " + "white-space: pre;"; + + const QString dragCountLabelStyle = baseProperties + "font-size: 14px; font-weight: bold;"; + const QString totalCountLabelStyle = baseProperties + "font-size: 16px; font-weight: bold;"; + const QString subtypeTallyLabelStyle = baseProperties + "font-size: 12px;"; + + dragCountLabel = new QLabel(this); + dragCountLabel->setStyleSheet(dragCountLabelStyle); + dragCountLabel->hide(); + dragCountLabel->raise(); + + totalCountLabel = new QLabel(this); + totalCountLabel->setStyleSheet(totalCountLabelStyle); + totalCountLabel->hide(); + + subtypeTallyContainer = new QWidget(this); + subtypeTallyContainer->setStyleSheet(subtypeTallyLabelStyle); + subtypeTallyLayout = new QGridLayout(subtypeTallyContainer); + subtypeTallyLayout->setContentsMargins(2, 2, 2, 2); + subtypeTallyLayout->setSpacing(2); + subtypeTallyContainer->hide(); +} + +void GameView::resizeEvent(QResizeEvent *event) +{ + QGraphicsView::resizeEvent(event); + + GameScene *s = static_cast(scene()); + s->processViewSizeChange(event->size()); + + updateSceneRect(scene()->sceneRect()); + updateTotalSelectionCount(event->size()); +} + +void GameView::updateSceneRect(const QRectF &rect) +{ + fitInView(rect, Qt::KeepAspectRatio); +} + +void GameView::startRubberBand(const QPointF &_selectionOrigin) +{ + if (!rubberBand) { + return; + } + + selectionOrigin = _selectionOrigin; + rubberBand->setGeometry(QRect(mapFromScene(selectionOrigin), QSize(0, 0))); + rubberBand->show(); +} + +void GameView::resizeRubberBand(const QPointF &cursorPoint, int selectedCount) +{ + if (!rubberBand) { + return; + } + + constexpr int kLabelPaddingInPixels = 4; + + QPoint cursor = cursorPoint.toPoint(); + QRect rect = QRect(mapFromScene(selectionOrigin), cursor).normalized(); + rubberBand->setGeometry(rect); + + if (!SettingsCache::instance().getShowDragSelectionCount()) { + dragCountLabel->hide(); + return; + } + + if (selectedCount > 0) { + dragCountLabel->setText(QString::number(selectedCount)); + dragCountLabel->adjustSize(); + QSize labelSize = dragCountLabel->size(); + + if (rect.width() < labelSize.width() + 2 * kLabelPaddingInPixels || + rect.height() < labelSize.height() + 2 * kLabelPaddingInPixels) { + dragCountLabel->hide(); + return; + } + + const int minX = rect.left() + kLabelPaddingInPixels; + const int minY = rect.top() + kLabelPaddingInPixels; + + int x = qMax(minX, cursor.x() - labelSize.width() - kLabelPaddingInPixels); + int y = qMax(minY, cursor.y() - labelSize.height() - kLabelPaddingInPixels); + + bool isAtTopLeftCorner = (x == minX) && (y == minY); + if (isAtTopLeftCorner) { + constexpr int kCursorClearanceInPixels = 16; + x = qMin(cursor.x() + kCursorClearanceInPixels, rect.right() - labelSize.width() - kLabelPaddingInPixels); + } + + dragCountLabel->move(x, y); + dragCountLabel->show(); + } else { + dragCountLabel->hide(); + } +} + +void GameView::stopRubberBand() +{ + if (!rubberBand) { + return; + } + + rubberBand->hide(); + dragCountLabel->hide(); +} + +void GameView::refreshShortcuts() +{ + aCloseMostRecentZoneView->setShortcuts( + SettingsCache::instance().shortcuts().getShortcut("Player/aCloseMostRecentZoneView")); +} + +void GameView::clearSubtypeLabels() +{ + QtUtils::clearLayoutRec(subtypeTallyLayout); +} + +QSize GameView::rebuildSubtypeLabels(const QList &entries) +{ + clearSubtypeLabels(); + + const QString nameStyle = QStringLiteral("color: white; font-size: 12px; background: transparent;"); + const QString countStyle = + QStringLiteral("color: white; font-size: 14px; font-weight: bold; background: transparent;"); + + int totalHeight = 0; + int maxNameWidth = 0; + int maxCountWidth = 0; + + int row = 0; + for (const SubtypeEntry &entry : entries) { + auto *nameLabel = new QLabel(entry.name, subtypeTallyContainer); + nameLabel->setStyleSheet(nameStyle); + nameLabel->setAlignment(Qt::AlignRight | Qt::AlignVCenter); + subtypeTallyLayout->addWidget(nameLabel, row, 0); + + auto *countLabel = new QLabel(QString::number(entry.count), subtypeTallyContainer); + countLabel->setStyleSheet(countStyle); + countLabel->setAlignment(Qt::AlignRight | Qt::AlignVCenter); + subtypeTallyLayout->addWidget(countLabel, row, 1); + + QSize nameSize = nameLabel->sizeHint(); + QSize countSize = countLabel->sizeHint(); + maxNameWidth = qMax(maxNameWidth, nameSize.width()); + maxCountWidth = qMax(maxCountWidth, countSize.width()); + totalHeight += qMax(nameSize.height(), countSize.height()); + + ++row; + } + + int spacing = subtypeTallyLayout->spacing(); + int margins = subtypeTallyLayout->contentsMargins().left() + subtypeTallyLayout->contentsMargins().right(); + int verticalMargins = subtypeTallyLayout->contentsMargins().top() + subtypeTallyLayout->contentsMargins().bottom(); + + int width = maxNameWidth + spacing + maxCountWidth + margins; + int height = totalHeight + (row - 1) * spacing + verticalMargins; + + return QSize(width, height); +} + +void GameView::updateTotalSelectionCount(const QSize &viewSize) +{ + constexpr int kMarginInPixels = 10; + constexpr int kSpacingBetweenLabels = 4; + + int availableWidth = viewSize.isValid() ? viewSize.width() : viewport()->width(); + int availableHeight = viewSize.isValid() ? viewSize.height() : viewport()->height(); + + int count = scene()->selectedItems().count(); + + if (!SettingsCache::instance().getShowTotalSelectionCount() || count <= 1) { + totalCountLabel->hide(); + } else { + totalCountLabel->setText(QString::number(count)); + totalCountLabel->adjustSize(); + + int x = availableWidth - totalCountLabel->width() - kMarginInPixels; + int y = availableHeight - totalCountLabel->height() - kMarginInPixels; + totalCountLabel->move(x, y); + totalCountLabel->show(); + } + + if (!SettingsCache::instance().getShowSubtypeSelectionTally() || count <= 1) { + subtypeTallyContainer->hide(); + cachedSubtypeEntries.clear(); + return; + } + + GameScene *gameScene = static_cast(scene()); + QList entries = SelectionSubtypeTally::countSubtypes(gameScene->selectedCards()); + + if (entries.isEmpty()) { + subtypeTallyContainer->hide(); + cachedSubtypeEntries.clear(); + return; + } + + // Only rebuild labels if entries changed + QSize containerSize; + if (entries != cachedSubtypeEntries) { + cachedSubtypeEntries = entries; + containerSize = rebuildSubtypeLabels(entries); + subtypeTallyContainer->resize(containerSize); + } else { + containerSize = subtypeTallyContainer->size(); + } + + int x = availableWidth - containerSize.width() - kMarginInPixels; + int y; + + if (totalCountLabel->isVisible()) { + y = totalCountLabel->y() - containerSize.height() - kSpacingBetweenLabels; + } else { + y = availableHeight - containerSize.height() - kMarginInPixels; + } + + y = qMax(kMarginInPixels, y); + + subtypeTallyContainer->move(x, y); + subtypeTallyContainer->show(); +} + +/** + * Disabling focus on the game view will allow chat to maintain the autofocusing behavior of pre 2.10.3, + * at the cost of disabling the zone view search bar. + */ +void GameView::setFocusDisabled(bool disabled) +{ + setFocusPolicy(disabled ? Qt::NoFocus : Qt::ClickFocus); +} diff --git a/cockatrice/src/game/game_view.h b/cockatrice/src/game_graphics/game_view.h similarity index 70% rename from cockatrice/src/game/game_view.h rename to cockatrice/src/game_graphics/game_view.h index 15abad9af..4047c87ab 100644 --- a/cockatrice/src/game/game_view.h +++ b/cockatrice/src/game_graphics/game_view.h @@ -7,9 +7,12 @@ #ifndef GAMEVIEW_H #define GAMEVIEW_H +#include "../game/selection_subtype_tally.h" + #include class GameScene; +class QGridLayout; class QLabel; class QRubberBand; @@ -21,7 +24,13 @@ private: QRubberBand *rubberBand; QLabel *dragCountLabel; QLabel *totalCountLabel; + QWidget *subtypeTallyContainer; + QGridLayout *subtypeTallyLayout; QPointF selectionOrigin; + QList cachedSubtypeEntries; ///< Cached entries to avoid redundant rebuilds + + QSize rebuildSubtypeLabels(const QList &entries); + void clearSubtypeLabels(); protected: void resizeEvent(QResizeEvent *event) override; @@ -31,6 +40,7 @@ private slots: void stopRubberBand(); void refreshShortcuts(); void updateTotalSelectionCount(const QSize &viewSize = QSize()); + void setFocusDisabled(bool disabled); public slots: void updateSceneRect(const QRectF &rect); diff --git a/cockatrice/src/game/hand_counter.cpp b/cockatrice/src/game_graphics/hand_counter.cpp similarity index 96% rename from cockatrice/src/game/hand_counter.cpp rename to cockatrice/src/game_graphics/hand_counter.cpp index a853ae2de..35989ff38 100644 --- a/cockatrice/src/game/hand_counter.cpp +++ b/cockatrice/src/game_graphics/hand_counter.cpp @@ -1,6 +1,6 @@ #include "hand_counter.h" -#include "../game_graphics/zones/card_zone.h" +#include "zones/card_zone.h" #include #include diff --git a/cockatrice/src/game/hand_counter.h b/cockatrice/src/game_graphics/hand_counter.h similarity index 87% rename from cockatrice/src/game/hand_counter.h rename to cockatrice/src/game_graphics/hand_counter.h index 41ab3b5b2..9aa65d514 100644 --- a/cockatrice/src/game/hand_counter.h +++ b/cockatrice/src/game_graphics/hand_counter.h @@ -7,8 +7,8 @@ #ifndef HANDCOUNTER_H #define HANDCOUNTER_H -#include "../game_graphics/board/abstract_graphics_item.h" -#include "../game_graphics/board/graphics_item_type.h" +#include "board/abstract_graphics_item.h" +#include "board/graphics_item_type.h" #include diff --git a/cockatrice/src/game/log/message_log_widget.cpp b/cockatrice/src/game_graphics/log/message_log_widget.cpp similarity index 99% rename from cockatrice/src/game/log/message_log_widget.cpp rename to cockatrice/src/game_graphics/log/message_log_widget.cpp index 906f15c2e..ccd903b04 100644 --- a/cockatrice/src/game/log/message_log_widget.cpp +++ b/cockatrice/src/game_graphics/log/message_log_widget.cpp @@ -1,13 +1,13 @@ #include "message_log_widget.h" +#include "../../client/settings/card_counter_settings.h" #include "../../client/sound_engine.h" +#include "../../game/phase.h" +#include "../../game/player/player_logic.h" #include "../../interface/widgets/tabs/tab_game.h" #include "../board/card_item.h" #include "../board/translate_counter_name.h" -#include "../phase.h" -#include "../player/player_logic.h" -#include <../../client/settings/card_counter_settings.h> #include #include #include diff --git a/cockatrice/src/game/log/message_log_widget.h b/cockatrice/src/game_graphics/log/message_log_widget.h similarity index 99% rename from cockatrice/src/game/log/message_log_widget.h rename to cockatrice/src/game_graphics/log/message_log_widget.h index 9f1990ac4..a145d358d 100644 --- a/cockatrice/src/game/log/message_log_widget.h +++ b/cockatrice/src/game_graphics/log/message_log_widget.h @@ -7,8 +7,8 @@ #ifndef MESSAGELOGWIDGET_H #define MESSAGELOGWIDGET_H +#include "../../game/zones/card_zone_logic.h" #include "../../interface/widgets/server/chat_view/chat_view.h" -#include "../zones/card_zone_logic.h" class AbstractGame; class CardItem; diff --git a/cockatrice/src/game/phases_toolbar.cpp b/cockatrice/src/game_graphics/phases_toolbar.cpp similarity index 100% rename from cockatrice/src/game/phases_toolbar.cpp rename to cockatrice/src/game_graphics/phases_toolbar.cpp diff --git a/cockatrice/src/game/phases_toolbar.h b/cockatrice/src/game_graphics/phases_toolbar.h similarity index 97% rename from cockatrice/src/game/phases_toolbar.h rename to cockatrice/src/game_graphics/phases_toolbar.h index 6f0931d61..39884ef75 100644 --- a/cockatrice/src/game/phases_toolbar.h +++ b/cockatrice/src/game_graphics/phases_toolbar.h @@ -8,7 +8,7 @@ #ifndef PHASESTOOLBAR_H #define PHASESTOOLBAR_H -#include "../game_graphics/board/abstract_graphics_item.h" +#include "board/abstract_graphics_item.h" #include #include diff --git a/cockatrice/src/game/player/card_menu_action_type.h b/cockatrice/src/game_graphics/player/card_menu_action_type.h similarity index 100% rename from cockatrice/src/game/player/card_menu_action_type.h rename to cockatrice/src/game_graphics/player/card_menu_action_type.h diff --git a/cockatrice/src/game/player/menu/abstract_player_component.h b/cockatrice/src/game_graphics/player/menu/abstract_player_component.h similarity index 100% rename from cockatrice/src/game/player/menu/abstract_player_component.h rename to cockatrice/src/game_graphics/player/menu/abstract_player_component.h diff --git a/cockatrice/src/game/player/menu/card_menu.cpp b/cockatrice/src/game_graphics/player/menu/card_menu.cpp similarity index 78% rename from cockatrice/src/game/player/menu/card_menu.cpp rename to cockatrice/src/game_graphics/player/menu/card_menu.cpp index 3b866d4e0..aa94c3be7 100644 --- a/cockatrice/src/game/player/menu/card_menu.cpp +++ b/cockatrice/src/game_graphics/player/menu/card_menu.cpp @@ -3,10 +3,11 @@ #include "../../../client/settings/card_counter_settings.h" #include "../../../interface/widgets/tabs/tab_game.h" #include "../../board/card_item.h" -#include "../../zones/view_zone_logic.h" +#include "../../game/player/player_actions.h" +#include "../../game/player/player_logic.h" +#include "../../game/zones/view_zone_logic.h" #include "../card_menu_action_type.h" -#include "../player_actions.h" -#include "../player_logic.h" +#include "../player_graphics_item.h" #include "move_menu.h" #include "pt_menu.h" @@ -31,93 +32,92 @@ static QIcon createCircleIcon(const QColor &color) return QIcon(pixmap); } -CardMenu::CardMenu(PlayerLogic *_player, const CardItem *_card, bool _shortcutsActive) +template +static QAction *makeAction(QObject *parent, Slot &&slot, bool checkable = false, bool checked = false) +{ + auto *a = new QAction(parent); + a->setCheckable(checkable); + if (checkable) { + a->setChecked(checked); + } + QObject::connect(a, &QAction::triggered, parent, std::forward(slot)); + return a; +} + +CardMenu::CardMenu(PlayerGraphicsItem *_player, const CardItem *_card, bool _shortcutsActive) : player(_player), card(_card), shortcutsActive(_shortcutsActive) { - auto playerActions = player->getPlayerActions(); - - const QList &players = player->getGame()->getPlayerManager()->getPlayers().values(); + const QList &players = player->getLogic()->getGame()->getPlayerManager()->getPlayers().values(); for (auto playerToAdd : players) { - if (playerToAdd == player) { + if (playerToAdd == player->getLogic()) { continue; } playersInfo.append(qMakePair(playerToAdd->getPlayerInfo()->getName(), playerToAdd->getPlayerInfo()->getId())); } - connect(player->getGame()->getPlayerManager(), &PlayerManager::playerRemoved, this, &CardMenu::removePlayer); + connect(player->getLogic()->getGame()->getPlayerManager(), &PlayerManager::playerRemoved, this, + &CardMenu::removePlayer); - aTap = new QAction(this); - aTap->setData(cmTap); - connect(aTap, &QAction::triggered, playerActions, &PlayerActions::cardMenuAction); - aDoesntUntap = new QAction(this); - aDoesntUntap->setData(cmDoesntUntap); - aDoesntUntap->setCheckable(true); - aDoesntUntap->setChecked(card != nullptr && card->getDoesntUntap()); - connect(aDoesntUntap, &QAction::triggered, playerActions, &PlayerActions::cardMenuAction); + auto *actions = player->getLogic()->getPlayerActions(); + auto *gameScene = player->getGameScene(); + + // Single selection resolver used by all lambdas — called at trigger time + auto sel = [gameScene]() { return gameScene->selectedCards(); }; + + // Unified dispatcher for card menu actions + auto invoke = [actions, sel](CardMenuActionType type) { + return [actions, sel, type]() { actions->cardMenuAction(sel(), type); }; + }; + + // Actions using invoke (type dispatch, need selection) + aTap = makeAction(this, invoke(cmTap)); + aDoesntUntap = makeAction(this, invoke(cmDoesntUntap), /*checkable=*/true, card && card->getDoesntUntap()); + aFlip = makeAction(this, invoke(cmFlip)); + aPeek = makeAction(this, invoke(cmPeek)); + aClone = makeAction(this, invoke(cmClone)); + + // Actions using selection directly + aUnattach = makeAction(this, [actions, sel]() { actions->actUnattach(sel()); }); + aSetAnnotation = makeAction(this, [actions, sel]() { actions->actRequestSetAnnotationDialog(sel()); }); + aPlay = makeAction(this, [actions, sel]() { actions->actPlay(sel()); }); + aPlayFacedown = makeAction(this, [actions, sel]() { actions->actPlayFacedown(sel()); }); + aHide = makeAction(this, [actions, sel]() { actions->actHide(sel()); }); + aReduceLifeByPower = makeAction(this, [actions, sel]() { actions->actReduceLifeByPower(sel()); }); + + // Actions that use activeCard, not selection — direct connection aAttach = new QAction(this); - connect(aAttach, &QAction::triggered, playerActions, &PlayerActions::actAttach); - aUnattach = new QAction(this); - connect(aUnattach, &QAction::triggered, playerActions, &PlayerActions::actUnattach); aDrawArrow = new QAction(this); - connect(aDrawArrow, &QAction::triggered, playerActions, &PlayerActions::actDrawArrow); - aSetAnnotation = new QAction(this); - connect(aSetAnnotation, &QAction::triggered, playerActions, &PlayerActions::actSetAnnotation); - aFlip = new QAction(this); - aFlip->setData(cmFlip); - connect(aFlip, &QAction::triggered, player->getPlayerActions(), &PlayerActions::cardMenuAction); - aPeek = new QAction(this); - aPeek->setData(cmPeek); - connect(aPeek, &QAction::triggered, player->getPlayerActions(), &PlayerActions::cardMenuAction); - aClone = new QAction(this); - aClone->setData(cmClone); - connect(aClone, &QAction::triggered, player->getPlayerActions(), &PlayerActions::cardMenuAction); aSelectAll = new QAction(this); - connect(aSelectAll, &QAction::triggered, playerActions, &PlayerActions::actSelectAll); aSelectRow = new QAction(this); - connect(aSelectRow, &QAction::triggered, playerActions, &PlayerActions::actSelectRow); aSelectColumn = new QAction(this); - connect(aSelectColumn, &QAction::triggered, playerActions, &PlayerActions::actSelectColumn); - aReduceLifeByPower = new QAction(this); - connect(aReduceLifeByPower, &QAction::triggered, playerActions, &PlayerActions::actReduceLifeByPower); - - aPlay = new QAction(this); - connect(aPlay, &QAction::triggered, playerActions, &PlayerActions::actPlay); - aHide = new QAction(this); - connect(aHide, &QAction::triggered, playerActions, &PlayerActions::actHide); - aPlayFacedown = new QAction(this); - connect(aPlayFacedown, &QAction::triggered, playerActions, &PlayerActions::actPlayFacedown); + connect(aAttach, &QAction::triggered, actions, &PlayerActions::actAttach); + connect(aDrawArrow, &QAction::triggered, actions, &PlayerActions::actDrawArrow); + connect(aSelectAll, &QAction::triggered, actions, &PlayerActions::actSelectAll); + connect(aSelectRow, &QAction::triggered, actions, &PlayerActions::actSelectRow); + connect(aSelectColumn, &QAction::triggered, actions, &PlayerActions::actSelectColumn); aRevealToAll = new QAction(this); mCardCounters = new QMenu; + // Card counters for (int i = 0; i < 6; ++i) { QColor color = SettingsCache::instance().cardCounters().color(i); QIcon circleIcon = createCircleIcon(color); - auto *tempAddCounter = new QAction(this); - tempAddCounter->setIconVisibleInMenu(true); - tempAddCounter->setIcon(circleIcon); + auto *addAction = makeAction(this, [actions, sel, i]() { actions->actAddCardCounter(sel(), i); }); + addAction->setIcon(circleIcon); + aAddCounter.append(addAction); - auto *tempRemoveCounter = new QAction(this); - tempRemoveCounter->setIconVisibleInMenu(true); - tempRemoveCounter->setIcon(circleIcon); + auto *removeAction = makeAction(this, [actions, sel, i]() { actions->actRemoveCardCounter(sel(), i); }); + removeAction->setIcon(circleIcon); + aRemoveCounter.append(removeAction); - auto *tempSetCounter = new QAction(this); - tempSetCounter->setIconVisibleInMenu(true); - tempSetCounter->setIcon(circleIcon); - - aAddCounter.append(tempAddCounter); - aRemoveCounter.append(tempRemoveCounter); - aSetCounter.append(tempSetCounter); - connect(tempAddCounter, &QAction::triggered, playerActions, - [playerActions, i] { playerActions->actAddCardCounter(i); }); - connect(tempRemoveCounter, &QAction::triggered, playerActions, - [playerActions, i] { playerActions->actRemoveCardCounter(i); }); - connect(tempSetCounter, &QAction::triggered, playerActions, - [playerActions, i] { playerActions->actSetCardCounter(i); }); + auto *setAction = makeAction(this, [actions, sel, i]() { actions->actRequestSetCardCounterDialog(sel(), i); }); + setAction->setIcon(circleIcon); + aSetCounter.append(setAction); } setShortcutsActive(); @@ -129,7 +129,7 @@ CardMenu::CardMenu(PlayerLogic *_player, const CardItem *_card, bool _shortcutsA } bool revealedCard = false; - bool writeableCard = player->getPlayerInfo()->getLocalOrJudge(); + bool writeableCard = player->getLogic()->getPlayerInfo()->getLocalOrJudge(); if (auto *view = qobject_cast(card->getZone())) { if (view->getRevealZone()) { if (view->getWriteableRevealZone()) { @@ -313,7 +313,9 @@ void CardMenu::createHandOrCustomZoneMenu(bool canModifyCard) initContextualPlayersMenu(revealMenu, aRevealToAll); - connect(revealMenu, &QMenu::triggered, player->getPlayerActions(), &PlayerActions::actReveal); + connect(revealMenu, &QMenu::triggered, this, [this](QAction *action) { + player->getLogic()->getPlayerActions()->actReveal(player->getGameScene()->selectedCards(), action); + }); addSeparator(); addAction(aClone); @@ -398,8 +400,7 @@ void CardMenu::addRelatedCardView() QAction *viewCard = viewRelatedCards->addAction(relatedCardName); Q_UNUSED(viewCard); - connect(viewCard, &QAction::triggered, player->getGame(), - [this, cardRef] { player->getGame()->getTab()->viewCardInfo(cardRef); }); + connect(viewCard, &QAction::triggered, this, [this, cardRef] { emit cardInfoRequested(cardRef); }); } } @@ -461,7 +462,8 @@ void CardMenu::addRelatedCardActions() auto *createRelated = new QAction(text, this); createRelated->setData(QVariant(index++)); - connect(createRelated, &QAction::triggered, player->getPlayerActions(), &PlayerActions::actCreateRelatedCard); + connect(createRelated, &QAction::triggered, player->getLogic()->getPlayerActions(), + &PlayerActions::actCreateRelatedCard); addAction(createRelated); } @@ -470,7 +472,7 @@ void CardMenu::addRelatedCardActions() createRelatedCards->setShortcuts( SettingsCache::instance().shortcuts().getShortcut("Player/aCreateRelatedTokens")); } - connect(createRelatedCards, &QAction::triggered, player->getPlayerActions(), + connect(createRelatedCards, &QAction::triggered, player->getLogic()->getPlayerActions(), &PlayerActions::actCreateAllRelatedCards); addAction(createRelatedCards); } diff --git a/cockatrice/src/game/player/menu/card_menu.h b/cockatrice/src/game_graphics/player/menu/card_menu.h similarity index 83% rename from cockatrice/src/game/player/menu/card_menu.h rename to cockatrice/src/game_graphics/player/menu/card_menu.h index ad3962caf..d67ef3876 100644 --- a/cockatrice/src/game/player/menu/card_menu.h +++ b/cockatrice/src/game_graphics/player/menu/card_menu.h @@ -8,15 +8,20 @@ #define COCKATRICE_CARD_MENU_H #include +#include class CardItem; +class PlayerGraphicsItem; class PlayerLogic; class CardMenu : public QMenu { Q_OBJECT +signals: + void cardInfoRequested(const CardRef &cardRef); + public: - explicit CardMenu(PlayerLogic *player, const CardItem *card, bool shortcutsActive); + explicit CardMenu(PlayerGraphicsItem *player, const CardItem *card, bool shortcutsActive); void removePlayer(PlayerLogic *playerToRemove); void createTableMenu(bool canModifyCard); void createStackMenu(bool canModifyCard); @@ -41,7 +46,7 @@ public: QList aAddCounter, aSetCounter, aRemoveCounter; private: - PlayerLogic *player; + PlayerGraphicsItem *player; const CardItem *card; QList> playersInfo; bool shortcutsActive; diff --git a/cockatrice/src/game/player/menu/custom_zone_menu.cpp b/cockatrice/src/game_graphics/player/menu/custom_zone_menu.cpp similarity index 63% rename from cockatrice/src/game/player/menu/custom_zone_menu.cpp rename to cockatrice/src/game_graphics/player/menu/custom_zone_menu.cpp index 88b7f3710..743746cc8 100644 --- a/cockatrice/src/game/player/menu/custom_zone_menu.cpp +++ b/cockatrice/src/game_graphics/player/menu/custom_zone_menu.cpp @@ -1,13 +1,14 @@ #include "custom_zone_menu.h" -#include "../player_logic.h" +#include "../../game/player/player_logic.h" +#include "../player_graphics_item.h" -CustomZoneMenu::CustomZoneMenu(PlayerLogic *_player) : player(_player) +CustomZoneMenu::CustomZoneMenu(PlayerGraphicsItem *_player) : player(_player) { menuAction()->setVisible(false); - connect(player, &PlayerLogic::clearCustomZonesMenu, this, &CustomZoneMenu::clearCustomZonesMenu); - connect(player, &PlayerLogic::addViewCustomZoneActionToCustomZoneMenu, this, + connect(player->getLogic(), &PlayerLogic::clearCustomZonesMenu, this, &CustomZoneMenu::clearCustomZonesMenu); + connect(player->getLogic(), &PlayerLogic::addViewCustomZoneActionToCustomZoneMenu, this, &CustomZoneMenu::addViewCustomZoneActionToCustomZoneMenu); retranslateUi(); @@ -17,7 +18,7 @@ void CustomZoneMenu::retranslateUi() { setTitle(tr("C&ustom Zones")); - if (player->getPlayerInfo()->getLocalOrJudge()) { + if (player->getLogic()->getPlayerInfo()->getLocalOrJudge()) { for (auto aViewZone : actions()) { aViewZone->setText(tr("View custom zone '%1'").arg(aViewZone->data().toString())); @@ -37,5 +38,5 @@ void CustomZoneMenu::addViewCustomZoneActionToCustomZoneMenu(QString zoneName) QAction *aViewZone = addAction(tr("View custom zone '%1'").arg(zoneName)); aViewZone->setData(zoneName); connect(aViewZone, &QAction::triggered, this, - [zoneName, this]() { player->getGameScene()->toggleZoneView(player, zoneName, -1); }); + [zoneName, this]() { player->getGameScene()->toggleZoneView(player->getLogic(), zoneName, -1); }); } \ No newline at end of file diff --git a/cockatrice/src/game/player/menu/custom_zone_menu.h b/cockatrice/src/game_graphics/player/menu/custom_zone_menu.h similarity index 84% rename from cockatrice/src/game/player/menu/custom_zone_menu.h rename to cockatrice/src/game_graphics/player/menu/custom_zone_menu.h index e10f6a4f0..46dd58db6 100644 --- a/cockatrice/src/game/player/menu/custom_zone_menu.h +++ b/cockatrice/src/game_graphics/player/menu/custom_zone_menu.h @@ -11,12 +11,12 @@ #include -class PlayerLogic; +class PlayerGraphicsItem; class CustomZoneMenu : public QMenu, public AbstractPlayerComponent { Q_OBJECT public: - explicit CustomZoneMenu(PlayerLogic *player); + explicit CustomZoneMenu(PlayerGraphicsItem *player); void retranslateUi() override; void setShortcutsActive() override { @@ -26,7 +26,7 @@ public: } private: - PlayerLogic *player; + PlayerGraphicsItem *player; private slots: void clearCustomZonesMenu(); void addViewCustomZoneActionToCustomZoneMenu(QString zoneName); diff --git a/cockatrice/src/game/player/menu/grave_menu.cpp b/cockatrice/src/game_graphics/player/menu/grave_menu.cpp similarity index 79% rename from cockatrice/src/game/player/menu/grave_menu.cpp rename to cockatrice/src/game_graphics/player/menu/grave_menu.cpp index 16a5858ca..698481f7a 100644 --- a/cockatrice/src/game/player/menu/grave_menu.cpp +++ b/cockatrice/src/game_graphics/player/menu/grave_menu.cpp @@ -1,21 +1,22 @@ #include "grave_menu.h" -#include "../../abstract_game.h" -#include "../player_actions.h" -#include "../player_logic.h" +#include "../../game/abstract_game.h" +#include "../../game/player/player_actions.h" +#include "../../game/player/player_logic.h" +#include "../player_graphics_item.h" #include #include #include -GraveyardMenu::GraveyardMenu(PlayerLogic *_player, QWidget *parent) : TearOffMenu(parent), player(_player) +GraveyardMenu::GraveyardMenu(PlayerGraphicsItem *_player, QWidget *parent) : TearOffMenu(parent), player(_player) { createMoveActions(); createViewActions(); addAction(aViewGraveyard); - if (player->getPlayerInfo()->local || player->getPlayerInfo()->judge) { + if (player->getLogic()->getPlayerInfo()->local || player->getLogic()->getPlayerInfo()->judge) { mRevealRandomGraveyardCard = addMenu(QString()); connect(mRevealRandomGraveyardCard, &QMenu::aboutToShow, this, &GraveyardMenu::populateRevealRandomMenuWithActivePlayers); @@ -36,9 +37,9 @@ GraveyardMenu::GraveyardMenu(PlayerLogic *_player, QWidget *parent) : TearOffMen void GraveyardMenu::createMoveActions() { - auto grave = player->getGraveZone(); + auto grave = player->getLogic()->getGraveZone(); - if (player->getPlayerInfo()->local || player->getPlayerInfo()->judge) { + if (player->getLogic()->getPlayerInfo()->local || player->getLogic()->getPlayerInfo()->judge) { aMoveGraveToTopLibrary = new QAction(this); aMoveGraveToTopLibrary->setData(QList() << ZoneNames::DECK << 0); @@ -60,7 +61,7 @@ void GraveyardMenu::createMoveActions() void GraveyardMenu::createViewActions() { - PlayerActions *playerActions = player->getPlayerActions(); + PlayerActions *playerActions = player->getLogic()->getPlayerActions(); aViewGraveyard = new QAction(this); connect(aViewGraveyard, &QAction::triggered, playerActions, &PlayerActions::actViewGraveyard); @@ -76,9 +77,9 @@ void GraveyardMenu::populateRevealRandomMenuWithActivePlayers() mRevealRandomGraveyardCard->addSeparator(); - const auto &players = player->getGame()->getPlayerManager()->getPlayers().values(); + const auto &players = player->getLogic()->getGame()->getPlayerManager()->getPlayers().values(); for (auto *other : players) { - if (other == player) { + if (other == player->getLogic()) { continue; } QAction *a = mRevealRandomGraveyardCard->addAction(other->getPlayerInfo()->getName()); @@ -90,7 +91,7 @@ void GraveyardMenu::populateRevealRandomMenuWithActivePlayers() void GraveyardMenu::onRevealRandomTriggered() { if (auto *a = qobject_cast(sender())) { - player->getPlayerActions()->actRevealRandomGraveyardCard(a->data().toInt()); + player->getLogic()->getPlayerActions()->actRevealRandomGraveyardCard(a->data().toInt()); } } @@ -100,7 +101,7 @@ void GraveyardMenu::retranslateUi() aViewGraveyard->setText(tr("&View graveyard")); - if (player->getPlayerInfo()->getLocalOrJudge()) { + if (player->getLogic()->getPlayerInfo()->getLocalOrJudge()) { moveGraveMenu->setTitle(tr("&Move graveyard to...")); aMoveGraveToTopLibrary->setText(tr("&Top of library")); aMoveGraveToBottomLibrary->setText(tr("&Bottom of library")); diff --git a/cockatrice/src/game/player/menu/grave_menu.h b/cockatrice/src/game_graphics/player/menu/grave_menu.h similarity index 88% rename from cockatrice/src/game/player/menu/grave_menu.h rename to cockatrice/src/game_graphics/player/menu/grave_menu.h index d3d98802d..116261e9b 100644 --- a/cockatrice/src/game/player/menu/grave_menu.h +++ b/cockatrice/src/game_graphics/player/menu/grave_menu.h @@ -13,7 +13,7 @@ #include #include -class PlayerLogic; +class PlayerGraphicsItem; class GraveyardMenu : public TearOffMenu, public AbstractPlayerComponent { Q_OBJECT @@ -21,7 +21,7 @@ signals: void newPlayerActionCreated(QAction *action); public: - explicit GraveyardMenu(PlayerLogic *player, QWidget *parent = nullptr); + explicit GraveyardMenu(PlayerGraphicsItem *player, QWidget *parent = nullptr); void createMoveActions(); void createViewActions(); void populateRevealRandomMenuWithActivePlayers(); @@ -40,7 +40,7 @@ public: QAction *aMoveGraveToRfg = nullptr; private: - PlayerLogic *player; + PlayerGraphicsItem *player; }; #endif // COCKATRICE_GRAVE_MENU_H diff --git a/cockatrice/src/game/player/menu/hand_menu.cpp b/cockatrice/src/game_graphics/player/menu/hand_menu.cpp similarity index 87% rename from cockatrice/src/game/player/menu/hand_menu.cpp rename to cockatrice/src/game_graphics/player/menu/hand_menu.cpp index 6ff177655..ba0702f07 100644 --- a/cockatrice/src/game/player/menu/hand_menu.cpp +++ b/cockatrice/src/game_graphics/player/menu/hand_menu.cpp @@ -3,18 +3,22 @@ #include "../../../client/settings/cache_settings.h" #include "../../../client/settings/shortcuts_settings.h" #include "../../../game_graphics/zones/hand_zone.h" -#include "../../abstract_game.h" -#include "../player_actions.h" -#include "../player_logic.h" +#include "../../game/abstract_game.h" +#include "../../game/player/player_actions.h" +#include "../../game/player/player_logic.h" +#include "../player_graphics_item.h" #include #include #include -HandMenu::HandMenu(PlayerLogic *_player, PlayerActions *actions, QWidget *parent) : TearOffMenu(parent), player(_player) +HandMenu::HandMenu(PlayerGraphicsItem *_player, QWidget *parent) : TearOffMenu(parent), player(_player) { - if (player->getPlayerInfo()->local || player->getPlayerInfo()->judge) { + auto *actions = player->getLogic()->getPlayerActions(); + + if (player->getLogic()->getPlayerInfo()->local || player->getLogic()->getPlayerInfo()->judge) { aViewHand = new QAction(this); + connect(aViewHand, &QAction::triggered, actions, &PlayerActions::actViewHand); addAction(aViewHand); @@ -58,7 +62,7 @@ HandMenu::HandMenu(PlayerLogic *_player, PlayerActions *actions, QWidget *parent addSeparator(); aMulligan = new QAction(this); - connect(aMulligan, &QAction::triggered, actions, &PlayerActions::actMulligan); + connect(aMulligan, &QAction::triggered, actions, &PlayerActions::actRequestMulliganDialog); addAction(aMulligan); // Mulligan same size @@ -75,7 +79,7 @@ HandMenu::HandMenu(PlayerLogic *_player, PlayerActions *actions, QWidget *parent mMoveHandMenu = addTearOffMenu(QString()); - if (player->getPlayerInfo()->local || player->getPlayerInfo()->judge) { + if (player->getLogic()->getPlayerInfo()->local || player->getLogic()->getPlayerInfo()->judge) { aMoveHandToTopLibrary = new QAction(this); aMoveHandToTopLibrary->setData(QList() << ZoneNames::DECK << 0); aMoveHandToBottomLibrary = new QAction(this); @@ -85,7 +89,7 @@ HandMenu::HandMenu(PlayerLogic *_player, PlayerActions *actions, QWidget *parent aMoveHandToRfg = new QAction(this); aMoveHandToRfg->setData(QList() << ZoneNames::EXILE << 0); - auto hand = player->getHandZone(); + auto hand = player->getLogic()->getHandZone(); connect(aMoveHandToTopLibrary, &QAction::triggered, hand, &HandZoneLogic::moveAllToZone); connect(aMoveHandToBottomLibrary, &QAction::triggered, hand, &HandZoneLogic::moveAllToZone); @@ -107,7 +111,7 @@ void HandMenu::retranslateUi() { setTitle(tr("&Hand")); - if (player->getPlayerInfo()->getLocalOrJudge()) { + if (player->getLogic()->getPlayerInfo()->getLocalOrJudge()) { aViewHand->setText(tr("&View hand")); mSortHand->setTitle(tr("Sort hand by...")); @@ -166,9 +170,9 @@ void HandMenu::populateRevealHandMenuWithActivePlayers() mRevealHand->addSeparator(); - const auto &players = player->getGame()->getPlayerManager()->getPlayers().values(); + const auto &players = player->getLogic()->getGame()->getPlayerManager()->getPlayers().values(); for (auto *other : players) { - if (other == player) { + if (other == player->getLogic()) { continue; } QAction *a = mRevealHand->addAction(other->getPlayerInfo()->getName()); @@ -185,9 +189,9 @@ void HandMenu::populateRevealRandomHandCardMenuWithActivePlayers() mRevealRandomHandCard->addSeparator(); - const auto &players = player->getGame()->getPlayerManager()->getPlayers().values(); + const auto &players = player->getLogic()->getGame()->getPlayerManager()->getPlayers().values(); for (auto *other : players) { - if (other == player) { + if (other == player->getLogic()) { continue; } QAction *a = mRevealRandomHandCard->addAction(other->getPlayerInfo()->getName()); @@ -204,7 +208,7 @@ void HandMenu::onRevealHandTriggered() } const int targetId = action->data().toInt(); - player->getPlayerActions()->actRevealHand(targetId); + player->getLogic()->getPlayerActions()->actRevealHand(targetId); } void HandMenu::onRevealRandomHandCardTriggered() @@ -215,5 +219,5 @@ void HandMenu::onRevealRandomHandCardTriggered() } const int targetId = action->data().toInt(); - player->getPlayerActions()->actRevealRandomHandCard(targetId); + player->getLogic()->getPlayerActions()->actRevealRandomHandCard(targetId); } diff --git a/cockatrice/src/game/player/menu/hand_menu.h b/cockatrice/src/game_graphics/player/menu/hand_menu.h similarity index 92% rename from cockatrice/src/game/player/menu/hand_menu.h rename to cockatrice/src/game_graphics/player/menu/hand_menu.h index 1e2ddd95a..d5204612b 100644 --- a/cockatrice/src/game/player/menu/hand_menu.h +++ b/cockatrice/src/game_graphics/player/menu/hand_menu.h @@ -13,7 +13,7 @@ #include #include -class PlayerLogic; +class PlayerGraphicsItem; class PlayerActions; class HandMenu : public TearOffMenu, public AbstractPlayerComponent @@ -21,7 +21,7 @@ class HandMenu : public TearOffMenu, public AbstractPlayerComponent Q_OBJECT public: - HandMenu(PlayerLogic *player, PlayerActions *actions, QWidget *parent = nullptr); + HandMenu(PlayerGraphicsItem *player, QWidget *parent = nullptr); QMenu *revealHandMenu() const { @@ -43,7 +43,7 @@ private slots: void onRevealRandomHandCardTriggered(); private: - PlayerLogic *player; + PlayerGraphicsItem *player; QAction *aViewHand = nullptr; QAction *aMulligan = nullptr; diff --git a/cockatrice/src/game/player/menu/library_menu.cpp b/cockatrice/src/game_graphics/player/menu/library_menu.cpp similarity index 87% rename from cockatrice/src/game/player/menu/library_menu.cpp rename to cockatrice/src/game_graphics/player/menu/library_menu.cpp index 8449af05a..4c15e09ec 100644 --- a/cockatrice/src/game/player/menu/library_menu.cpp +++ b/cockatrice/src/game_graphics/player/menu/library_menu.cpp @@ -3,14 +3,16 @@ #include "../../../client/settings/cache_settings.h" #include "../../../client/settings/shortcuts_settings.h" #include "../../../interface/widgets/tabs/tab_game.h" -#include "../../abstract_game.h" -#include "../player_actions.h" -#include "../player_logic.h" +#include "../../game/abstract_game.h" +#include "../../game/player/player_actions.h" +#include "../../game/player/player_logic.h" +#include "../player_graphics_item.h" #include +#include #include -LibraryMenu::LibraryMenu(PlayerLogic *_player, QWidget *parent) : TearOffMenu(parent), player(_player) +LibraryMenu::LibraryMenu(PlayerGraphicsItem *_player, QWidget *parent) : TearOffMenu(parent), player(_player) { createDrawActions(); createShuffleActions(); @@ -75,8 +77,8 @@ LibraryMenu::LibraryMenu(PlayerLogic *_player, QWidget *parent) : TearOffMenu(pa bottomLibraryMenu->addSeparator(); bottomLibraryMenu->addAction(aShuffleBottomCards); - connect(player, &PlayerLogic::resetTopCardMenuActions, this, &LibraryMenu::resetTopCardMenuActions); - connect(player, &PlayerLogic::deckChanged, this, &LibraryMenu::enableOpenInDeckEditorAction); + connect(player->getLogic(), &PlayerLogic::resetTopCardMenuActions, this, &LibraryMenu::resetTopCardMenuActions); + connect(player->getLogic(), &PlayerLogic::deckChanged, this, &LibraryMenu::enableOpenInDeckEditorAction); retranslateUi(); } @@ -94,41 +96,41 @@ void LibraryMenu::resetTopCardMenuActions() void LibraryMenu::createDrawActions() { - PlayerActions *playerActions = player->getPlayerActions(); + PlayerActions *playerActions = player->getLogic()->getPlayerActions(); - if (player->getPlayerInfo()->local || player->getPlayerInfo()->judge) { + if (player->getLogic()->getPlayerInfo()->local || player->getLogic()->getPlayerInfo()->judge) { aDrawCard = new QAction(this); connect(aDrawCard, &QAction::triggered, playerActions, &PlayerActions::actDrawCard); aDrawCards = new QAction(this); - connect(aDrawCards, &QAction::triggered, playerActions, &PlayerActions::actDrawCards); + connect(aDrawCards, &QAction::triggered, playerActions, &PlayerActions::actRequestDrawCardsDialog); aUndoDraw = new QAction(this); connect(aUndoDraw, &QAction::triggered, playerActions, &PlayerActions::actUndoDraw); aDrawBottomCard = new QAction(this); connect(aDrawBottomCard, &QAction::triggered, playerActions, &PlayerActions::actDrawBottomCard); aDrawBottomCards = new QAction(this); - connect(aDrawBottomCards, &QAction::triggered, playerActions, &PlayerActions::actDrawBottomCards); + connect(aDrawBottomCards, &QAction::triggered, playerActions, &PlayerActions::actRequestDrawBottomCardsDialog); } } void LibraryMenu::createShuffleActions() { - PlayerActions *playerActions = player->getPlayerActions(); + PlayerActions *playerActions = player->getLogic()->getPlayerActions(); - if (player->getPlayerInfo()->local || player->getPlayerInfo()->judge) { + if (player->getLogic()->getPlayerInfo()->local || player->getLogic()->getPlayerInfo()->judge) { aShuffle = new QAction(this); connect(aShuffle, &QAction::triggered, playerActions, &PlayerActions::actShuffle); aShuffleTopCards = new QAction(this); - connect(aShuffleTopCards, &QAction::triggered, playerActions, &PlayerActions::actShuffleTop); + connect(aShuffleTopCards, &QAction::triggered, playerActions, &PlayerActions::actRequestShuffleTopDialog); aShuffleBottomCards = new QAction(this); - connect(aShuffleBottomCards, &QAction::triggered, playerActions, &PlayerActions::actShuffleBottom); + connect(aShuffleBottomCards, &QAction::triggered, playerActions, &PlayerActions::actRequestShuffleBottomDialog); } } void LibraryMenu::createMoveActions() { - PlayerActions *playerActions = player->getPlayerActions(); + PlayerActions *playerActions = player->getLogic()->getPlayerActions(); - if (player->getPlayerInfo()->local || player->getPlayerInfo()->judge) { + if (player->getLogic()->getPlayerInfo()->local || player->getLogic()->getPlayerInfo()->judge) { aMoveTopToPlay = new QAction(this); connect(aMoveTopToPlay, &QAction::triggered, playerActions, &PlayerActions::actMoveTopCardToPlay); aMoveTopToPlayFaceDown = new QAction(this); @@ -149,7 +151,8 @@ void LibraryMenu::createMoveActions() connect(aMoveTopCardsToExileFaceDown, &QAction::triggered, playerActions, &PlayerActions::actMoveTopCardsToExileFaceDown); aMoveTopCardsUntil = new QAction(this); - connect(aMoveTopCardsUntil, &QAction::triggered, playerActions, &PlayerActions::actMoveTopCardsUntil); + connect(aMoveTopCardsUntil, &QAction::triggered, playerActions, + &PlayerActions::actRequestMoveTopCardsUntilDialog); aMoveTopCardToBottom = new QAction(this); connect(aMoveTopCardToBottom, &QAction::triggered, playerActions, &PlayerActions::actMoveTopCardToBottom); @@ -181,16 +184,16 @@ void LibraryMenu::createMoveActions() void LibraryMenu::createViewActions() { - PlayerActions *playerActions = player->getPlayerActions(); + PlayerActions *playerActions = player->getLogic()->getPlayerActions(); - if (player->getPlayerInfo()->local || player->getPlayerInfo()->judge) { + if (player->getLogic()->getPlayerInfo()->local || player->getLogic()->getPlayerInfo()->judge) { aViewLibrary = new QAction(this); connect(aViewLibrary, &QAction::triggered, playerActions, &PlayerActions::actViewLibrary); aViewTopCards = new QAction(this); - connect(aViewTopCards, &QAction::triggered, playerActions, &PlayerActions::actViewTopCards); + connect(aViewTopCards, &QAction::triggered, playerActions, &PlayerActions::actRequestViewTopCardsDialog); aViewBottomCards = new QAction(this); - connect(aViewBottomCards, &QAction::triggered, playerActions, &PlayerActions::actViewBottomCards); + connect(aViewBottomCards, &QAction::triggered, playerActions, &PlayerActions::actRequestViewBottomCardsDialog); aAlwaysRevealTopCard = new QAction(this); aAlwaysRevealTopCard->setCheckable(true); connect(aAlwaysRevealTopCard, &QAction::triggered, playerActions, &PlayerActions::actAlwaysRevealTopCard); @@ -207,7 +210,7 @@ void LibraryMenu::retranslateUi() { setTitle(tr("&Library")); - if (player->getPlayerInfo()->getLocalOrJudge()) { + if (player->getLogic()->getPlayerInfo()->getLocalOrJudge()) { aViewLibrary->setText(tr("&View library")); aViewTopCards->setText(tr("View &top cards of library...")); aViewBottomCards->setText(tr("View bottom cards of library...")); @@ -263,9 +266,9 @@ void LibraryMenu::populateRevealLibraryMenuWithActivePlayers() mRevealLibrary->addSeparator(); - const auto &players = player->getGame()->getPlayerManager()->getPlayers().values(); + const auto &players = player->getLogic()->getGame()->getPlayerManager()->getPlayers().values(); for (auto *other : players) { - if (other == player) { + if (other == player->getLogic()) { continue; } QAction *a = mRevealLibrary->addAction(other->getPlayerInfo()->getName()); @@ -278,9 +281,9 @@ void LibraryMenu::populateLendLibraryMenuWithActivePlayers() { mLendLibrary->clear(); - const auto &players = player->getGame()->getPlayerManager()->getPlayers().values(); + const auto &players = player->getLogic()->getGame()->getPlayerManager()->getPlayers().values(); for (auto *other : players) { - if (other == player) { + if (other == player->getLogic()) { continue; } QAction *a = mLendLibrary->addAction(other->getPlayerInfo()->getName()); @@ -299,9 +302,9 @@ void LibraryMenu::populateRevealTopCardMenuWithActivePlayers() mRevealTopCard->addSeparator(); - const auto &players = player->getGame()->getPlayerManager()->getPlayers().values(); + const auto &players = player->getLogic()->getGame()->getPlayerManager()->getPlayers().values(); for (auto *other : players) { - if (other == player) { + if (other == player->getLogic()) { continue; } QAction *a = mRevealTopCard->addAction(other->getPlayerInfo()->getName()); @@ -313,27 +316,33 @@ void LibraryMenu::populateRevealTopCardMenuWithActivePlayers() void LibraryMenu::onRevealLibraryTriggered() { if (auto *a = qobject_cast(sender())) { - player->getPlayerActions()->actRevealLibrary(a->data().toInt()); + player->getLogic()->getPlayerActions()->actRevealLibrary(a->data().toInt()); } } void LibraryMenu::onLendLibraryTriggered() { if (auto *a = qobject_cast(sender())) { - player->getPlayerActions()->actLendLibrary(a->data().toInt()); + player->getLogic()->getPlayerActions()->actLendLibrary(a->data().toInt()); } } void LibraryMenu::onRevealTopCardTriggered() { + QWidget *parent = nullptr; + if (auto *view = player->scene() ? player->scene()->views().value(0) : nullptr) { + parent = view->window(); + } if (auto *a = qobject_cast(sender())) { - int deckSize = player->getDeckZone()->getCards().size(); - bool ok; - int number = QInputDialog::getInt(player->getGame()->getTab(), tr("Reveal top cards of library"), + + int deckSize = player->getLogic()->getDeckZone()->getCards().size(); + bool ok = true; + int number = QInputDialog::getInt(parent, tr("Reveal top cards of library"), tr("Number of cards: (max. %1)").arg(deckSize), defaultNumberTopCards, 1, deckSize, 1, &ok); + if (ok) { - player->getPlayerActions()->actRevealTopCards(a->data().toInt(), number); + player->getLogic()->getPlayerActions()->actRevealTopCards(a->data().toInt(), number); defaultNumberTopCards = number; } } diff --git a/cockatrice/src/game/player/menu/library_menu.h b/cockatrice/src/game_graphics/player/menu/library_menu.h similarity index 96% rename from cockatrice/src/game/player/menu/library_menu.h rename to cockatrice/src/game_graphics/player/menu/library_menu.h index a941c54b1..bc0e6fb8e 100644 --- a/cockatrice/src/game/player/menu/library_menu.h +++ b/cockatrice/src/game_graphics/player/menu/library_menu.h @@ -13,6 +13,7 @@ #include #include +class PlayerGraphicsItem; class PlayerLogic; class PlayerActions; @@ -24,7 +25,7 @@ public slots: void resetTopCardMenuActions(); public: - LibraryMenu(PlayerLogic *player, QWidget *parent = nullptr); + LibraryMenu(PlayerGraphicsItem *player, QWidget *parent = nullptr); void createDrawActions(); void createShuffleActions(); void createMoveActions(); @@ -111,7 +112,7 @@ public: int defaultNumberTopCards = 1; private: - PlayerLogic *player; + PlayerGraphicsItem *player; }; #endif // COCKATRICE_LIBRARY_MENU_H diff --git a/cockatrice/src/game/player/menu/move_menu.cpp b/cockatrice/src/game_graphics/player/menu/move_menu.cpp similarity index 64% rename from cockatrice/src/game/player/menu/move_menu.cpp rename to cockatrice/src/game_graphics/player/menu/move_menu.cpp index 3a5ad4da3..5b7209a9f 100644 --- a/cockatrice/src/game/player/menu/move_menu.cpp +++ b/cockatrice/src/game_graphics/player/menu/move_menu.cpp @@ -1,10 +1,11 @@ #include "move_menu.h" +#include "../../game/player/player_actions.h" +#include "../../game/player/player_logic.h" #include "../card_menu_action_type.h" -#include "../player_actions.h" -#include "../player_logic.h" +#include "../player_graphics_item.h" -MoveMenu::MoveMenu(PlayerLogic *player) : QMenu(tr("Move to")) +MoveMenu::MoveMenu(PlayerGraphicsItem *player) : QMenu(tr("Move to")) { aMoveToTopLibrary = new QAction(this); aMoveToTopLibrary->setData(cmMoveToTopLibrary); @@ -20,14 +21,22 @@ MoveMenu::MoveMenu(PlayerLogic *player) : QMenu(tr("Move to")) aMoveToExile = new QAction(this); aMoveToExile->setData(cmMoveToExile); - connect(aMoveToTopLibrary, &QAction::triggered, player->getPlayerActions(), &PlayerActions::cardMenuAction); - connect(aMoveToBottomLibrary, &QAction::triggered, player->getPlayerActions(), &PlayerActions::cardMenuAction); - connect(aMoveToXfromTopOfLibrary, &QAction::triggered, player->getPlayerActions(), - &PlayerActions::actMoveCardXCardsFromTop); - connect(aMoveToTable, &QAction::triggered, player->getPlayerActions(), &PlayerActions::cardMenuAction); - connect(aMoveToHand, &QAction::triggered, player->getPlayerActions(), &PlayerActions::cardMenuAction); - connect(aMoveToGraveyard, &QAction::triggered, player->getPlayerActions(), &PlayerActions::cardMenuAction); - connect(aMoveToExile, &QAction::triggered, player->getPlayerActions(), &PlayerActions::cardMenuAction); + auto *actions = player->getLogic()->getPlayerActions(); + + auto invoke = [player](CardMenuActionType type) { + return [type, player]() { + player->getLogic()->getPlayerActions()->cardMenuAction(player->getGameScene()->selectedCards(), type); + }; + }; + + connect(aMoveToTopLibrary, &QAction::triggered, actions, invoke(cmMoveToTopLibrary)); + connect(aMoveToBottomLibrary, &QAction::triggered, actions, invoke(cmMoveToBottomLibrary)); + connect(aMoveToXfromTopOfLibrary, &QAction::triggered, actions, + &PlayerActions::actRequestMoveCardXCardsFromTopDialog); + connect(aMoveToTable, &QAction::triggered, actions, invoke(cmMoveToTable)); + connect(aMoveToHand, &QAction::triggered, actions, invoke(cmMoveToHand)); + connect(aMoveToGraveyard, &QAction::triggered, actions, invoke(cmMoveToGraveyard)); + connect(aMoveToExile, &QAction::triggered, actions, invoke(cmMoveToExile)); addAction(aMoveToTopLibrary); addAction(aMoveToXfromTopOfLibrary); diff --git a/cockatrice/src/game/player/menu/move_menu.h b/cockatrice/src/game_graphics/player/menu/move_menu.h similarity index 88% rename from cockatrice/src/game/player/menu/move_menu.h rename to cockatrice/src/game_graphics/player/menu/move_menu.h index 4e257b7fb..150bdbd3c 100644 --- a/cockatrice/src/game/player/menu/move_menu.h +++ b/cockatrice/src/game_graphics/player/menu/move_menu.h @@ -8,13 +8,13 @@ #define COCKATRICE_MOVE_MENU_H #include -class PlayerLogic; +class PlayerGraphicsItem; class MoveMenu : public QMenu { Q_OBJECT public: - explicit MoveMenu(PlayerLogic *player); + explicit MoveMenu(PlayerGraphicsItem *player); void setShortcutsActive(); void retranslateUi(); diff --git a/cockatrice/src/game/player/menu/player_menu.cpp b/cockatrice/src/game_graphics/player/menu/player_menu.cpp similarity index 62% rename from cockatrice/src/game/player/menu/player_menu.cpp rename to cockatrice/src/game_graphics/player/menu/player_menu.cpp index 9e7b91923..17b791222 100644 --- a/cockatrice/src/game/player/menu/player_menu.cpp +++ b/cockatrice/src/game_graphics/player/menu/player_menu.cpp @@ -5,17 +5,21 @@ #include "../../../game_graphics/zones/table_zone.h" #include "../../../interface/widgets/tabs/tab_game.h" #include "../../board/card_item.h" +#include "../player_graphics_item.h" #include "card_menu.h" #include "hand_menu.h" #include -PlayerMenu::PlayerMenu(PlayerLogic *_player) : QObject(_player), player(_player) +PlayerMenu::PlayerMenu(PlayerGraphicsItem *_player) : QObject(_player), player(_player) { + connect(player->getLogic(), &PlayerLogic::requestCardMenuUpdate, this, &PlayerMenu::updateCardMenu); + connect(this, &PlayerMenu::cardInfoRequested, player, &PlayerGraphicsItem::cardInfoRequested); + playerMenu = new TearOffMenu(); - if (player->getPlayerInfo()->getLocalOrJudge()) { - handMenu = addManagedMenu(player, player->getPlayerActions(), playerMenu); + if (player->getLogic()->getPlayerInfo()->getLocalOrJudge()) { + handMenu = addManagedMenu(player, playerMenu); libraryMenu = addManagedMenu(player, playerMenu); } else { handMenu = nullptr; @@ -25,7 +29,7 @@ PlayerMenu::PlayerMenu(PlayerLogic *_player) : QObject(_player), player(_player) graveMenu = addManagedMenu(player, playerMenu); rfgMenu = addManagedMenu(player, playerMenu); - if (player->getPlayerInfo()->getLocalOrJudge()) { + if (player->getLogic()->getPlayerInfo()->getLocalOrJudge()) { sideboardMenu = addManagedMenu(player, playerMenu); customZonesMenu = addManagedMenu(player); playerMenu->addSeparator(); @@ -40,7 +44,7 @@ PlayerMenu::PlayerMenu(PlayerLogic *_player) : QObject(_player), player(_player) utilityMenu = nullptr; } - if (player->getPlayerInfo()->getLocal()) { + if (player->getLogic()->getPlayerInfo()->getLocal()) { sayMenu = addManagedMenu(player); } else { sayMenu = nullptr; @@ -55,13 +59,13 @@ PlayerMenu::PlayerMenu(PlayerLogic *_player) : QObject(_player), player(_player) void PlayerMenu::setMenusForGraphicItems() { - player->getGraphicsItem()->getTableZoneGraphicsItem()->setMenu(playerMenu); - player->getGraphicsItem()->getGraveyardZoneGraphicsItem()->setMenu(graveMenu, graveMenu->aViewGraveyard); - player->getGraphicsItem()->getRfgZoneGraphicsItem()->setMenu(rfgMenu, rfgMenu->aViewRfg); - if (player->getPlayerInfo()->getLocalOrJudge()) { - player->getGraphicsItem()->getHandZoneGraphicsItem()->setMenu(handMenu); - player->getGraphicsItem()->getDeckZoneGraphicsItem()->setMenu(libraryMenu, libraryMenu->aDrawCard); - player->getGraphicsItem()->getSideboardZoneGraphicsItem()->setMenu(sideboardMenu); + player->getTableZoneGraphicsItem()->setMenu(playerMenu); + player->getGraveyardZoneGraphicsItem()->setMenu(graveMenu, graveMenu->aViewGraveyard); + player->getRfgZoneGraphicsItem()->setMenu(rfgMenu, rfgMenu->aViewRfg); + if (player->getLogic()->getPlayerInfo()->getLocalOrJudge()) { + player->getHandZoneGraphicsItem()->setMenu(handMenu); + player->getDeckZoneGraphicsItem()->setMenu(libraryMenu, libraryMenu->aDrawCard); + player->getSideboardZoneGraphicsItem()->setMenu(sideboardMenu); } } @@ -74,12 +78,14 @@ QMenu *PlayerMenu::updateCardMenu(const CardItem *card) // If is spectator (as spectators don't need card menus), return // only update the menu if the card is actually selected - if ((player->getGame()->getPlayerManager()->isSpectator() && !player->getGame()->getPlayerManager()->isJudge()) || - player->getGame()->getActiveCard() != card) { + if ((player->getLogic()->getGame()->getPlayerManager()->isSpectator() && + !player->getLogic()->getGame()->getPlayerManager()->isJudge()) || + player->getLogic()->getGame()->getActiveCard() != card) { return nullptr; } - QMenu *menu = new CardMenu(player, card, shortcutsActive); + CardMenu *menu = new CardMenu(player, card, shortcutsActive); + connect(menu, &CardMenu::cardInfoRequested, this, &PlayerMenu::cardInfoRequested); emit cardMenuUpdated(menu); return menu; @@ -87,7 +93,7 @@ QMenu *PlayerMenu::updateCardMenu(const CardItem *card) void PlayerMenu::retranslateUi() { - playerMenu->setTitle(tr("Player \"%1\"").arg(player->getPlayerInfo()->getName())); + playerMenu->setTitle(tr("Player \"%1\"").arg(player->getLogic()->getPlayerInfo()->getName())); for (auto *component : managedComponents) { component->retranslateUi(); @@ -104,7 +110,8 @@ void PlayerMenu::refreshShortcuts() { if (shortcutsActive) { // Judges get access to every player's menus but only want shortcuts to be set for their own. - if (player->getPlayerInfo()->getLocalOrJudge() && !player->getPlayerInfo()->getLocal()) { + if (player->getLogic()->getPlayerInfo()->getLocalOrJudge() && + !player->getLogic()->getPlayerInfo()->getLocal()) { setShortcutsInactive(); } else { setShortcutsActive(); diff --git a/cockatrice/src/game/player/menu/player_menu.h b/cockatrice/src/game_graphics/player/menu/player_menu.h similarity index 92% rename from cockatrice/src/game/player/menu/player_menu.h rename to cockatrice/src/game_graphics/player/menu/player_menu.h index d5c19df58..62ba66df7 100644 --- a/cockatrice/src/game/player/menu/player_menu.h +++ b/cockatrice/src/game_graphics/player/menu/player_menu.h @@ -8,7 +8,6 @@ #define COCKATRICE_PLAYER_MENU_H #include "../../../interface/widgets/menus/tearoff_menu.h" -#include "../player_logic.h" #include "custom_zone_menu.h" #include "grave_menu.h" #include "hand_menu.h" @@ -23,29 +22,31 @@ #include class CardItem; +class CardMenu; +class PlayerGraphicsItem; class PlayerMenu : public QObject { Q_OBJECT signals: - void cardMenuUpdated(QMenu *cardMenu); + void cardMenuUpdated(CardMenu *cardMenu); + void cardInfoRequested(const CardRef &cardRef); void shortcutsActivated(); void shortcutsDeactivated(); void retranslateRequested(); public slots: void setMenusForGraphicItems(); + QMenu *updateCardMenu(const CardItem *card); private slots: void refreshShortcuts(); public: - explicit PlayerMenu(PlayerLogic *player); + explicit PlayerMenu(PlayerGraphicsItem *player); /** @brief Retranslate all user-visible strings. Called on language change. */ void retranslateUi(); - QMenu *updateCardMenu(const CardItem *card); - [[nodiscard]] QMenu *getPlayerMenu() const { return playerMenu; @@ -77,7 +78,7 @@ public: void setShortcutsInactive(); private: - PlayerLogic *player; + PlayerGraphicsItem *player; TearOffMenu *playerMenu; QMenu *countersMenu; HandMenu *handMenu; diff --git a/cockatrice/src/game/player/menu/pt_menu.cpp b/cockatrice/src/game_graphics/player/menu/pt_menu.cpp similarity index 51% rename from cockatrice/src/game/player/menu/pt_menu.cpp rename to cockatrice/src/game_graphics/player/menu/pt_menu.cpp index 7dc3035c1..a01be9424 100644 --- a/cockatrice/src/game/player/menu/pt_menu.cpp +++ b/cockatrice/src/game_graphics/player/menu/pt_menu.cpp @@ -1,32 +1,43 @@ #include "pt_menu.h" -#include "../player_actions.h" -#include "../player_logic.h" +#include "../../game/player/player_actions.h" +#include "../../game/player/player_logic.h" +#include "../player_graphics_item.h" -PtMenu::PtMenu(PlayerLogic *player) : QMenu(tr("Power / toughness")) +PtMenu::PtMenu(PlayerGraphicsItem *player) : QMenu(tr("Power / toughness")) { - PlayerActions *playerActions = player->getPlayerActions(); + PlayerActions *playerActions = player->getLogic()->getPlayerActions(); aIncP = new QAction(this); - connect(aIncP, &QAction::triggered, playerActions, &PlayerActions::actIncP); + connect(aIncP, &QAction::triggered, playerActions, + [player, playerActions] { playerActions->actIncP(player->getGameScene()->selectedCards()); }); aDecP = new QAction(this); - connect(aDecP, &QAction::triggered, playerActions, &PlayerActions::actDecP); + connect(aDecP, &QAction::triggered, playerActions, + [player, playerActions] { playerActions->actDecP(player->getGameScene()->selectedCards()); }); aIncT = new QAction(this); - connect(aIncT, &QAction::triggered, playerActions, &PlayerActions::actIncT); + connect(aIncT, &QAction::triggered, playerActions, + [player, playerActions] { playerActions->actIncT(player->getGameScene()->selectedCards()); }); aDecT = new QAction(this); - connect(aDecT, &QAction::triggered, playerActions, &PlayerActions::actDecT); + connect(aDecT, &QAction::triggered, playerActions, + [player, playerActions] { playerActions->actDecT(player->getGameScene()->selectedCards()); }); aIncPT = new QAction(this); - connect(aIncPT, &QAction::triggered, playerActions, [playerActions] { playerActions->actIncPT(); }); + connect(aIncPT, &QAction::triggered, playerActions, + [player, playerActions] { playerActions->actIncPT(player->getGameScene()->selectedCards()); }); aDecPT = new QAction(this); - connect(aDecPT, &QAction::triggered, playerActions, &PlayerActions::actDecPT); + connect(aDecPT, &QAction::triggered, playerActions, + [player, playerActions] { playerActions->actDecPT(player->getGameScene()->selectedCards()); }); aFlowP = new QAction(this); - connect(aFlowP, &QAction::triggered, playerActions, &PlayerActions::actFlowP); + connect(aFlowP, &QAction::triggered, playerActions, + [player, playerActions] { playerActions->actFlowP(player->getGameScene()->selectedCards()); }); aFlowT = new QAction(this); - connect(aFlowT, &QAction::triggered, playerActions, &PlayerActions::actFlowT); + connect(aFlowT, &QAction::triggered, playerActions, + [player, playerActions] { playerActions->actFlowT(player->getGameScene()->selectedCards()); }); aSetPT = new QAction(this); - connect(aSetPT, &QAction::triggered, playerActions, &PlayerActions::actSetPT); + connect(aSetPT, &QAction::triggered, playerActions, + [player, playerActions] { playerActions->actRequestSetPTDialog(player->getGameScene()->selectedCards()); }); aResetPT = new QAction(this); - connect(aResetPT, &QAction::triggered, playerActions, &PlayerActions::actResetPT); + connect(aResetPT, &QAction::triggered, playerActions, + [player, playerActions] { playerActions->actResetPT(player->getGameScene()->selectedCards()); }); addAction(aIncP); addAction(aDecP); diff --git a/cockatrice/src/game/player/menu/pt_menu.h b/cockatrice/src/game_graphics/player/menu/pt_menu.h similarity index 89% rename from cockatrice/src/game/player/menu/pt_menu.h rename to cockatrice/src/game_graphics/player/menu/pt_menu.h index 645449586..72f828801 100644 --- a/cockatrice/src/game/player/menu/pt_menu.h +++ b/cockatrice/src/game_graphics/player/menu/pt_menu.h @@ -8,14 +8,14 @@ #define COCKATRICE_PT_MENU_H #include -class PlayerLogic; +class PlayerGraphicsItem; class PtMenu : public QMenu { Q_OBJECT public: - explicit PtMenu(PlayerLogic *player); + explicit PtMenu(PlayerGraphicsItem *player); void retranslateUi(); void setShortcutsActive(); diff --git a/cockatrice/src/game/player/menu/rfg_menu.cpp b/cockatrice/src/game_graphics/player/menu/rfg_menu.cpp similarity index 78% rename from cockatrice/src/game/player/menu/rfg_menu.cpp rename to cockatrice/src/game_graphics/player/menu/rfg_menu.cpp index e8aca00cb..45abadbf7 100644 --- a/cockatrice/src/game/player/menu/rfg_menu.cpp +++ b/cockatrice/src/game_graphics/player/menu/rfg_menu.cpp @@ -1,18 +1,19 @@ #include "rfg_menu.h" -#include "../player_actions.h" -#include "../player_logic.h" +#include "../../game/player/player_actions.h" +#include "../../game/player/player_logic.h" +#include "../player_graphics_item.h" #include -RfgMenu::RfgMenu(PlayerLogic *_player, QWidget *parent) : TearOffMenu(parent), player(_player) +RfgMenu::RfgMenu(PlayerGraphicsItem *_player, QWidget *parent) : TearOffMenu(parent), player(_player) { createMoveActions(); createViewActions(); addAction(aViewRfg); - if (player->getPlayerInfo()->getLocalOrJudge()) { + if (player->getLogic()->getPlayerInfo()->getLocalOrJudge()) { addSeparator(); moveRfgMenu = addTearOffMenu(QString()); moveRfgMenu->addAction(aMoveRfgToTopLibrary); @@ -28,8 +29,8 @@ RfgMenu::RfgMenu(PlayerLogic *_player, QWidget *parent) : TearOffMenu(parent), p void RfgMenu::createMoveActions() { - if (player->getPlayerInfo()->getLocalOrJudge()) { - auto rfg = player->getRfgZone(); + if (player->getLogic()->getPlayerInfo()->getLocalOrJudge()) { + auto rfg = player->getLogic()->getRfgZone(); aMoveRfgToTopLibrary = new QAction(this); aMoveRfgToTopLibrary->setData(QList() << ZoneNames::DECK << 0); @@ -49,7 +50,7 @@ void RfgMenu::createMoveActions() void RfgMenu::createViewActions() { - PlayerActions *playerActions = player->getPlayerActions(); + PlayerActions *playerActions = player->getLogic()->getPlayerActions(); aViewRfg = new QAction(this); connect(aViewRfg, &QAction::triggered, playerActions, &PlayerActions::actViewRfg); @@ -61,7 +62,7 @@ void RfgMenu::retranslateUi() aViewRfg->setText(tr("&View exile")); - if (player->getPlayerInfo()->getLocalOrJudge()) { + if (player->getLogic()->getPlayerInfo()->getLocalOrJudge()) { moveRfgMenu->setTitle(tr("&Move exile to...")); aMoveRfgToTopLibrary->setText(tr("&Top of library")); aMoveRfgToBottomLibrary->setText(tr("&Bottom of library")); diff --git a/cockatrice/src/game/player/menu/rfg_menu.h b/cockatrice/src/game_graphics/player/menu/rfg_menu.h similarity index 86% rename from cockatrice/src/game/player/menu/rfg_menu.h rename to cockatrice/src/game_graphics/player/menu/rfg_menu.h index 9e179f8fd..f5dd888e4 100644 --- a/cockatrice/src/game/player/menu/rfg_menu.h +++ b/cockatrice/src/game_graphics/player/menu/rfg_menu.h @@ -13,12 +13,12 @@ #include #include -class PlayerLogic; +class PlayerGraphicsItem; class RfgMenu : public TearOffMenu, public AbstractPlayerComponent { Q_OBJECT public: - explicit RfgMenu(PlayerLogic *player, QWidget *parent = nullptr); + explicit RfgMenu(PlayerGraphicsItem *player, QWidget *parent = nullptr); void createMoveActions(); void createViewActions(); void retranslateUi() override; @@ -38,7 +38,7 @@ public: QAction *aMoveRfgToGrave = nullptr; private: - PlayerLogic *player; + PlayerGraphicsItem *player; }; #endif // COCKATRICE_RFG_MENU_H diff --git a/cockatrice/src/game/player/menu/say_menu.cpp b/cockatrice/src/game_graphics/player/menu/say_menu.cpp similarity index 78% rename from cockatrice/src/game/player/menu/say_menu.cpp rename to cockatrice/src/game_graphics/player/menu/say_menu.cpp index a2d5ab982..336b70f0d 100644 --- a/cockatrice/src/game/player/menu/say_menu.cpp +++ b/cockatrice/src/game_graphics/player/menu/say_menu.cpp @@ -1,10 +1,11 @@ #include "say_menu.h" #include "../../../client/settings/cache_settings.h" -#include "../player_actions.h" -#include "../player_logic.h" +#include "../../game/player/player_actions.h" +#include "../../game/player/player_logic.h" +#include "../player_graphics_item.h" -SayMenu::SayMenu(PlayerLogic *_player) : player(_player) +SayMenu::SayMenu(PlayerGraphicsItem *_player) : player(_player) { connect(&SettingsCache::instance().messages(), &MessageSettings::messageMacrosChanged, this, &SayMenu::initSayMenu); initSayMenu(); @@ -44,7 +45,7 @@ void SayMenu::initSayMenu() for (int i = 0; i < count; ++i) { auto *newAction = new QAction(SettingsCache::instance().messages().getMessageAt(i), this); - connect(newAction, &QAction::triggered, player->getPlayerActions(), &PlayerActions::actSayMessage); + connect(newAction, &QAction::triggered, player->getLogic()->getPlayerActions(), &PlayerActions::actSayMessage); addAction(newAction); } diff --git a/cockatrice/src/game/player/menu/say_menu.h b/cockatrice/src/game_graphics/player/menu/say_menu.h similarity index 83% rename from cockatrice/src/game/player/menu/say_menu.h rename to cockatrice/src/game_graphics/player/menu/say_menu.h index 3de70e85c..3ff160d05 100644 --- a/cockatrice/src/game/player/menu/say_menu.h +++ b/cockatrice/src/game_graphics/player/menu/say_menu.h @@ -11,12 +11,12 @@ #include -class PlayerLogic; +class PlayerGraphicsItem; class SayMenu : public QMenu, public AbstractPlayerComponent { Q_OBJECT public: - explicit SayMenu(PlayerLogic *player); + explicit SayMenu(PlayerGraphicsItem *player); void retranslateUi() override; void setShortcutsActive() override; @@ -26,7 +26,7 @@ private slots: void initSayMenu(); private: - PlayerLogic *player; + PlayerGraphicsItem *player; bool shortcutsActive = false; }; diff --git a/cockatrice/src/game/player/menu/sideboard_menu.cpp b/cockatrice/src/game_graphics/player/menu/sideboard_menu.cpp similarity index 56% rename from cockatrice/src/game/player/menu/sideboard_menu.cpp rename to cockatrice/src/game_graphics/player/menu/sideboard_menu.cpp index f88625a1f..0dd7894d2 100644 --- a/cockatrice/src/game/player/menu/sideboard_menu.cpp +++ b/cockatrice/src/game_graphics/player/menu/sideboard_menu.cpp @@ -1,14 +1,16 @@ #include "sideboard_menu.h" -#include "../player_actions.h" -#include "../player_logic.h" +#include "../../game/player/player_actions.h" +#include "../../game/player/player_logic.h" +#include "../player_graphics_item.h" -SideboardMenu::SideboardMenu(PlayerLogic *player, QMenu *playerMenu) : QMenu(playerMenu) +SideboardMenu::SideboardMenu(PlayerGraphicsItem *player, QMenu *playerMenu) : QMenu(playerMenu) { aViewSideboard = new QAction(this); - connect(aViewSideboard, &QAction::triggered, player->getPlayerActions(), &PlayerActions::actViewSideboard); + connect(aViewSideboard, &QAction::triggered, player->getLogic()->getPlayerActions(), + &PlayerActions::actViewSideboard); - if (player->getPlayerInfo()->getLocalOrJudge()) { + if (player->getLogic()->getPlayerInfo()->getLocalOrJudge()) { addAction(aViewSideboard); } diff --git a/cockatrice/src/game/player/menu/sideboard_menu.h b/cockatrice/src/game_graphics/player/menu/sideboard_menu.h similarity index 79% rename from cockatrice/src/game/player/menu/sideboard_menu.h rename to cockatrice/src/game_graphics/player/menu/sideboard_menu.h index 20a206782..b3b547291 100644 --- a/cockatrice/src/game/player/menu/sideboard_menu.h +++ b/cockatrice/src/game_graphics/player/menu/sideboard_menu.h @@ -11,19 +11,19 @@ #include -class PlayerLogic; +class PlayerGraphicsItem; class SideboardMenu : public QMenu, public AbstractPlayerComponent { Q_OBJECT public: - explicit SideboardMenu(PlayerLogic *player, QMenu *playerMenu); + explicit SideboardMenu(PlayerGraphicsItem *player, QMenu *playerMenu); void retranslateUi() override; void setShortcutsActive() override; void setShortcutsInactive() override; private: - PlayerLogic *player; + PlayerGraphicsItem *player; QAction *aViewSideboard; }; diff --git a/cockatrice/src/game/player/menu/utility_menu.cpp b/cockatrice/src/game_graphics/player/menu/utility_menu.cpp similarity index 70% rename from cockatrice/src/game/player/menu/utility_menu.cpp rename to cockatrice/src/game_graphics/player/menu/utility_menu.cpp index 6b33d7bde..61a822b21 100644 --- a/cockatrice/src/game/player/menu/utility_menu.cpp +++ b/cockatrice/src/game_graphics/player/menu/utility_menu.cpp @@ -1,41 +1,49 @@ #include "utility_menu.h" #include "../../../interface/deck_loader/deck_loader.h" -#include "../player_actions.h" -#include "../player_logic.h" +#include "../../game/player/player_actions.h" +#include "../../game/player/player_logic.h" +#include "../player_graphics_item.h" #include "player_menu.h" #include #include -UtilityMenu::UtilityMenu(PlayerLogic *_player, QMenu *playerMenu) : QMenu(playerMenu), player(_player) +UtilityMenu::UtilityMenu(PlayerGraphicsItem *_player, QMenu *playerMenu) : QMenu(playerMenu), player(_player) { - PlayerActions *playerActions = player->getPlayerActions(); + PlayerActions *playerActions = player->getLogic()->getPlayerActions(); + connect(playerActions, &PlayerActions::requestEnableAndSetCreateAnotherTokenAction, this, + &UtilityMenu::setAndEnableCreateAnotherTokenAction); + connect(playerActions, &PlayerActions::requestSetLastToken, this, &UtilityMenu::setLastToken); - if (player->getPlayerInfo()->getLocalOrJudge()) { + if (player->getLogic()->getPlayerInfo()->getLocalOrJudge()) { aUntapAll = new QAction(this); connect(aUntapAll, &QAction::triggered, playerActions, &PlayerActions::actUntapAll); aRollDie = new QAction(this); - connect(aRollDie, &QAction::triggered, playerActions, &PlayerActions::actRollDie); + connect(aRollDie, &QAction::triggered, playerActions, &PlayerActions::actRequestRollDieDialog); aFlipCoin = new QAction(this); connect(aFlipCoin, &QAction::triggered, playerActions, &PlayerActions::actFlipCoin); aCreateToken = new QAction(this); - connect(aCreateToken, &QAction::triggered, playerActions, &PlayerActions::actCreateToken); + connect(aCreateToken, &QAction::triggered, playerActions, [this]() { + player->getLogic()->getPlayerActions()->actRequestCreateTokenDialog(getPredefinedTokens()); + }); aCreateAnotherToken = new QAction(this); connect(aCreateAnotherToken, &QAction::triggered, playerActions, &PlayerActions::actCreateAnotherToken); aCreateAnotherToken->setEnabled(false); aIncrementAllCardCounters = new QAction(this); - connect(aIncrementAllCardCounters, &QAction::triggered, playerActions, - &PlayerActions::actIncrementAllCardCounters); + connect(aIncrementAllCardCounters, &QAction::triggered, playerActions, [this]() { + player->getLogic()->getPlayerActions()->actIncrementAllCardCounters( + player->getGameScene()->selectedCards()); + }); createPredefinedTokenMenu = new QMenu(QString()); createPredefinedTokenMenu->setEnabled(false); - connect(player, &PlayerLogic::deckChanged, this, &UtilityMenu::populatePredefinedTokensMenu); + connect(player->getLogic(), &PlayerLogic::deckChanged, this, &UtilityMenu::populatePredefinedTokensMenu); playerMenu->addAction(aIncrementAllCardCounters); playerMenu->addSeparator(); @@ -66,7 +74,7 @@ void UtilityMenu::populatePredefinedTokensMenu() clear(); setEnabled(false); predefinedTokens.clear(); - const DeckList &deckList = player->getDeck(); + const DeckList &deckList = player->getLogic()->getDeck(); if (deckList.isEmpty()) { return; @@ -84,14 +92,24 @@ void UtilityMenu::populatePredefinedTokensMenu() if (i < 10) { a->setShortcut(QKeySequence("Alt+" + QString::number((i + 1) % 10))); } - connect(a, &QAction::triggered, player->getPlayerActions(), &PlayerActions::actCreatePredefinedToken); + connect(a, &QAction::triggered, player->getLogic()->getPlayerActions(), + &PlayerActions::actCreatePredefinedToken); } } } +void UtilityMenu::setLastToken(CardInfoPtr lastToken) +{ + if (!createAnotherTokenActionExists()) { + return; + } + + player->getLogic()->getPlayerActions()->setLastTokenInfo(lastToken); +} + void UtilityMenu::retranslateUi() { - if (player->getPlayerInfo()->getLocalOrJudge()) { + if (player->getLogic()->getPlayerInfo()->getLocalOrJudge()) { aIncrementAllCardCounters->setText(tr("Increment all card counters")); aUntapAll->setText(tr("&Untap all permanents")); aRollDie->setText(tr("R&oll die...")); @@ -106,7 +124,7 @@ void UtilityMenu::setShortcutsActive() { ShortcutsSettings &shortcuts = SettingsCache::instance().shortcuts(); - if (player->getPlayerInfo()->getLocalOrJudge()) { + if (player->getLogic()->getPlayerInfo()->getLocalOrJudge()) { aIncrementAllCardCounters->setShortcuts(shortcuts.getShortcut("Player/aIncrementAllCardCounters")); aUntapAll->setShortcuts(shortcuts.getShortcut("Player/aUntapAll")); aRollDie->setShortcuts(shortcuts.getShortcut("Player/aRollDie")); @@ -118,7 +136,7 @@ void UtilityMenu::setShortcutsActive() void UtilityMenu::setShortcutsInactive() { - if (player->getPlayerInfo()->getLocalOrJudge()) { + if (player->getLogic()->getPlayerInfo()->getLocalOrJudge()) { aUntapAll->setShortcut(QKeySequence()); aRollDie->setShortcut(QKeySequence()); aFlipCoin->setShortcut(QKeySequence()); diff --git a/cockatrice/src/game/player/menu/utility_menu.h b/cockatrice/src/game_graphics/player/menu/utility_menu.h similarity index 78% rename from cockatrice/src/game/player/menu/utility_menu.h rename to cockatrice/src/game_graphics/player/menu/utility_menu.h index fab3211ca..bdc2a81a5 100644 --- a/cockatrice/src/game/player/menu/utility_menu.h +++ b/cockatrice/src/game_graphics/player/menu/utility_menu.h @@ -10,19 +10,21 @@ #include "abstract_player_component.h" #include +#include -class PlayerLogic; +class PlayerGraphicsItem; class UtilityMenu : public QMenu, public AbstractPlayerComponent { Q_OBJECT public slots: void populatePredefinedTokensMenu(); + void setLastToken(CardInfoPtr lastToken); void retranslateUi() override; void setShortcutsActive() override; void setShortcutsInactive() override; public: - explicit UtilityMenu(PlayerLogic *player, QMenu *playerMenu); + explicit UtilityMenu(PlayerGraphicsItem *player, QMenu *playerMenu); [[nodiscard]] bool createAnotherTokenActionExists() const { @@ -31,7 +33,7 @@ public: void setAndEnableCreateAnotherTokenAction(QString text) { - aCreateAnotherToken->setText(text); + aCreateAnotherToken->setText(tr("C&reate another %1 token").arg(text)); aCreateAnotherToken->setEnabled(true); } @@ -41,7 +43,7 @@ public: } private: - PlayerLogic *player; + PlayerGraphicsItem *player; QStringList predefinedTokens; QMenu *createPredefinedTokenMenu; diff --git a/cockatrice/src/game/player/player_area.cpp b/cockatrice/src/game_graphics/player/player_area.cpp similarity index 100% rename from cockatrice/src/game/player/player_area.cpp rename to cockatrice/src/game_graphics/player/player_area.cpp diff --git a/cockatrice/src/game/player/player_area.h b/cockatrice/src/game_graphics/player/player_area.h similarity index 94% rename from cockatrice/src/game/player/player_area.h rename to cockatrice/src/game_graphics/player/player_area.h index 6ffaf4958..d73547f81 100644 --- a/cockatrice/src/game/player/player_area.h +++ b/cockatrice/src/game_graphics/player/player_area.h @@ -7,7 +7,7 @@ #ifndef COCKATRICE_PLAYER_AREA_H #define COCKATRICE_PLAYER_AREA_H -#include "../../game_graphics/board/graphics_item_type.h" +#include "../board/graphics_item_type.h" #include "QGraphicsItem" /** diff --git a/cockatrice/src/game_graphics/player/player_dialogs.cpp b/cockatrice/src/game_graphics/player/player_dialogs.cpp new file mode 100644 index 000000000..95c225812 --- /dev/null +++ b/cockatrice/src/game_graphics/player/player_dialogs.cpp @@ -0,0 +1,299 @@ +#include "player_dialogs.h" + +#include "../../client/settings/card_counter_settings.h" +#include "../../interface/widgets/utility/get_text_with_max.h" +#include "../board/card_item.h" +#include "../dialogs/dlg_roll_dice.h" +#include "../player/player_graphics_item.h" + +#include +#include +#include + +PlayerDialogs::PlayerDialogs(PlayerGraphicsItem *_player, PlayerActions *_playerActions) + : QObject(_player), player(_player), playerActions(_playerActions) +{ + connect(playerActions, &PlayerActions::requestViewTopCardsDialog, this, + &PlayerDialogs::onViewTopCardsDialogRequested); + + connect(playerActions, &PlayerActions::requestViewBottomCardsDialog, this, + &PlayerDialogs::onViewBottomCardsDialogRequested); + + connect(playerActions, &PlayerActions::requestShuffleTopDialog, this, &PlayerDialogs::onShuffleTopDialogRequested); + + connect(playerActions, &PlayerActions::requestShuffleBottomDialog, this, + &PlayerDialogs::onShuffleBottomDialogRequested); + + connect(playerActions, &PlayerActions::requestMulliganDialog, this, &PlayerDialogs::onMulliganDialogRequested); + + connect(playerActions, &PlayerActions::requestDrawCardsDialog, this, &PlayerDialogs::onDrawCardsDialogRequested); + + connect(playerActions, &PlayerActions::requestMoveTopCardsToDialog, this, + &PlayerDialogs::onMoveTopCardsToDialogRequested); + + connect(playerActions, &PlayerActions::requestMoveTopCardsUntilDialog, this, + &PlayerDialogs::onMoveTopCardsUntilDialogRequested); + + connect(playerActions, &PlayerActions::requestMoveBottomCardsToDialog, this, + &PlayerDialogs::onMoveBottomCardsToDialogRequested); + + connect(playerActions, &PlayerActions::requestDrawBottomCardsDialog, this, + &PlayerDialogs::onDrawBottomCardsDialogRequested); + + connect(playerActions, &PlayerActions::requestRollDieDialog, this, &PlayerDialogs::onRollDieDialogRequested); + + connect(playerActions, &PlayerActions::requestCreateTokenDialog, this, + &PlayerDialogs::onCreateTokenDialogRequested); + + connect(playerActions, &PlayerActions::requestCreateRelatedFromRelationDialog, this, + &PlayerDialogs::onCreateRelatedFromRelationDialogRequested); + + connect(playerActions, &PlayerActions::requestMoveCardXCardsFromTopDialog, this, + &PlayerDialogs::onMoveCardXCardsFromTopDialogRequested); + + connect(playerActions, &PlayerActions::requestSetPTDialog, this, &PlayerDialogs::onSetPTDialogRequested); + + connect(playerActions, &PlayerActions::requestSetAnnotationDialog, this, + &PlayerDialogs::onSetAnnotationDialogRequested); + + connect(playerActions, &PlayerActions::requestSetCardCounterDialog, this, + &PlayerDialogs::onSetCardCounterDialogRequested); +} + +void PlayerDialogs::onViewTopCardsDialogRequested(int defaultNumberTopCards, int deckSize) +{ + bool ok; + int number = QInputDialog::getInt(dialogParent(), tr("View top cards of library"), + tr("Number of cards: (max. %1)").arg(deckSize), defaultNumberTopCards, 1, + deckSize, 1, &ok); + if (ok) { + playerActions->actViewTopCards(number); + } +} + +void PlayerDialogs::onViewBottomCardsDialogRequested(int defaultNumberBottomCards, int deckSize) +{ + bool ok; + int number = QInputDialog::getInt(dialogParent(), tr("View bottom cards of library"), + tr("Number of cards: (max. %1)").arg(deckSize), defaultNumberBottomCards, 1, + deckSize, 1, &ok); + if (ok) { + playerActions->actViewBottomCards(number); + } +} + +void PlayerDialogs::onShuffleTopDialogRequested(int defaultNumberTopCards, int maxCards) +{ + bool ok; + int number = QInputDialog::getInt(dialogParent(), tr("Shuffle top cards of library"), + tr("Number of cards: (max. %1)").arg(maxCards), defaultNumberTopCards, 1, + maxCards, 1, &ok); + if (ok) { + playerActions->actShuffleTop(number); + } +} + +void PlayerDialogs::onShuffleBottomDialogRequested(int defaultNumberBottomCards, int maxCards) +{ + bool ok; + int number = QInputDialog::getInt(dialogParent(), tr("Shuffle bottom cards of library"), + tr("Number of cards: (max. %1)").arg(maxCards), defaultNumberBottomCards, 1, + maxCards, 1, &ok); + if (ok) { + playerActions->actShuffleBottom(number); + } +} + +void PlayerDialogs::onMulliganDialogRequested(int startSize, int handSize, int deckSize) +{ + bool ok; + int number = QInputDialog::getInt(dialogParent(), tr("Draw hand"), + tr("Number of cards: (max. %1)").arg(deckSize) + '\n' + + tr("0 and lower are in comparison to current hand size"), + startSize, -handSize, deckSize, 1, &ok); + + if (ok) { + playerActions->actMulligan(number); + } +} + +void PlayerDialogs::onDrawCardsDialogRequested(int defaultNumberTopCards, int deckSize) +{ + bool ok; + int number = QInputDialog::getInt(dialogParent(), tr("Draw cards"), tr("Number of cards: (max. %1)").arg(deckSize), + defaultNumberTopCards, 1, deckSize, 1, &ok); + + if (ok) { + playerActions->actDrawCards(number); + } +} + +void PlayerDialogs::onMoveTopCardsToDialogRequested(int defaultNumberTopCards, + int maxCards, + const QString &targetZone, + const QString &zoneDisplayName, + bool faceDown) +{ + bool ok; + int number = QInputDialog::getInt(dialogParent(), tr("Move top cards to %1").arg(zoneDisplayName), + tr("Number of cards: (max. %1)").arg(maxCards), defaultNumberTopCards, 1, + maxCards, 1, &ok); + if (ok) { + playerActions->moveTopCardsTo(number, targetZone, faceDown); + } +} + +void PlayerDialogs::onMoveTopCardsUntilDialogRequested(MoveTopCardsUntilOptions options) +{ + DlgMoveTopCardsUntil dlg(dialogParent(), options); + if (!dlg.exec()) { + return; + } + playerActions->moveTopCardsUntil(dlg.getExpr(), dlg.getOptions()); +} + +void PlayerDialogs::onMoveBottomCardsToDialogRequested(int defaultNumberBottomCards, + int maxCards, + const QString &targetZone, + const QString &zoneDisplayName, + bool faceDown) +{ + bool ok; + int number = QInputDialog::getInt(dialogParent(), tr("Move bottom cards to %1").arg(zoneDisplayName), + tr("Number of cards: (max. %1)").arg(maxCards), defaultNumberBottomCards, 1, + maxCards, 1, &ok); + if (ok) { + playerActions->moveBottomCardsTo(number, targetZone, faceDown); + } +} + +void PlayerDialogs::onDrawBottomCardsDialogRequested(int defaultNumberBottomCards, int maxCards) +{ + bool ok; + int number = + QInputDialog::getInt(dialogParent(), tr("Draw bottom cards"), tr("Number of cards: (max. %1)").arg(maxCards), + defaultNumberBottomCards, 1, maxCards, 1, &ok); + if (ok) { + playerActions->actDrawBottomCards(number); + } +} + +void PlayerDialogs::onRollDieDialogRequested() +{ + DlgRollDice dlg(dialogParent()); + if (!dlg.exec()) { + return; + } + playerActions->actRollDie(dlg.getDieSideCount(), dlg.getDiceToRollCount()); +} + +void PlayerDialogs::onCreateRelatedFromRelationDialogRequested(const CardItem *sourceCard, + const CardRelation *cardRelation) +{ + if (sourceCard == nullptr || cardRelation == nullptr) { + playerActions->setLastRelatedCreationSucceeded(false); + return; + } + + int variableCount = cardRelation->getDefaultCount(); + + if (cardRelation->getIsVariable()) { + bool ok; + + emit requestDialogSemaphore(true); + + variableCount = QInputDialog::getInt(dialogParent(), tr("Create tokens"), tr("Number:"), + cardRelation->getDefaultCount(), 1, MAX_TOKENS_PER_DIALOG, 1, &ok); + + emit requestDialogSemaphore(false); + + if (!ok) { + playerActions->setLastRelatedCreationSucceeded(false); // cancelled + return; + } + } + + const bool succeeded = playerActions->createRelatedFromRelation(sourceCard, cardRelation, variableCount); + + playerActions->setLastRelatedCreationSucceeded(succeeded); + + if (succeeded) { + playerActions->onRelatedCardCreated(sourceCard, cardRelation); // only on confirmed success + } +} + +void PlayerDialogs::onCreateTokenDialogRequested(const QStringList &predefinedTokens) +{ + DlgCreateToken dlg(predefinedTokens, dialogParent()); + if (!dlg.exec()) { + return; + } + + playerActions->actCreateToken(dlg.getTokenInfo()); +} + +void PlayerDialogs::onMoveCardXCardsFromTopDialogRequested(int defaultNumberTopCardsToPlaceBelow, int deckSize) +{ + bool ok; + int number = + QInputDialog::getInt(dialogParent(), tr("Place card X cards from top of library"), + tr("Which position should this card be placed:") + "\n" + tr("(max. %1)").arg(deckSize), + defaultNumberTopCardsToPlaceBelow, 1, deckSize, 1, &ok); + number -= 1; // indexes start at 0 + + if (ok) { + playerActions->actMoveCardXCardsFromTop(player->getGameScene()->selectedCards(), number); + } +} + +void PlayerDialogs::onSetPTDialogRequested(const QString &oldPT) +{ + bool ok; + auto cards = player->getGameScene()->selectedCards(); + emit requestDialogSemaphore(true); + QString pt = getTextWithMax(dialogParent(), tr("Change power/toughness"), tr("Change stats to:"), QLineEdit::Normal, + oldPT, &ok); + emit requestDialogSemaphore(false); + + if (!ok || player->getLogic()->clearCardsToDelete()) { + return; + } + + playerActions->actSetPT(cards, pt); +} + +void PlayerDialogs::onSetAnnotationDialogRequested(const QString &oldAnnotation) +{ + auto cards = player->getGameScene()->selectedCards(); + emit requestDialogSemaphore(true); + AnnotationDialog *dialog = new AnnotationDialog(dialogParent()); + dialog->setOptions(QInputDialog::UsePlainTextEditForTextInput); + dialog->setWindowTitle(tr("Set annotation")); + dialog->setLabelText(tr("Please enter the new annotation:")); + dialog->setTextValue(oldAnnotation); + bool ok = dialog->exec(); + emit requestDialogSemaphore(false); + if (!ok || player->getLogic()->clearCardsToDelete()) { + return; + } + QString annotation = dialog->textValue().left(MAX_NAME_LENGTH); + playerActions->actSetAnnotation(cards, annotation); +} + +void PlayerDialogs::onSetCardCounterDialogRequested(int counterId, const QString &oldValueForDlg) +{ + auto cards = player->getGameScene()->selectedCards(); + emit requestDialogSemaphore(true); + + auto &cardCounterSettings = SettingsCache::instance().cardCounters(); + QString counterName = cardCounterSettings.displayName(counterId); + + AbstractCounterDialog dialog(counterName, oldValueForDlg, dialogParent()); + int ok = dialog.exec(); + + emit requestDialogSemaphore(false); + if (!ok || player->getLogic()->clearCardsToDelete()) { + return; + } + playerActions->actSetCardCounter(cards, counterId, dialog.textValue()); +} \ No newline at end of file diff --git a/cockatrice/src/game_graphics/player/player_dialogs.h b/cockatrice/src/game_graphics/player/player_dialogs.h new file mode 100644 index 000000000..f87704f2d --- /dev/null +++ b/cockatrice/src/game_graphics/player/player_dialogs.h @@ -0,0 +1,63 @@ +#ifndef COCKATRICE_PLAYER_DIALOGS_H +#define COCKATRICE_PLAYER_DIALOGS_H +#include "../../game/player/player_actions.h" +#include "player_graphics_item.h" + +#include +#include + +class PlayerGraphicsItem; +class PlayerDialogs : public QObject +{ + + Q_OBJECT + +public: + explicit PlayerDialogs(PlayerGraphicsItem *player, PlayerActions *playerActions); + +signals: + void requestDialogSemaphore(bool active); + +public slots: + void onViewTopCardsDialogRequested(int defaultNumberTopCards, int deckSize); + void onViewBottomCardsDialogRequested(int defaultNumberBottomCards, int deckSize); + void onShuffleTopDialogRequested(int defaultNumberTopCards, int maxCards); + void onShuffleBottomDialogRequested(int defaultNumberBottomCards, int maxCards); + void onMulliganDialogRequested(int startSize, int handSize, int deckSize); + void onDrawCardsDialogRequested(int defaultNumberTopCards, int deckSize); + void onMoveTopCardsToDialogRequested(int defaultNumberTopCards, + int maxCards, + const QString &targetZone, + const QString &zoneDisplayName, + bool faceDown); + void onMoveTopCardsUntilDialogRequested(MoveTopCardsUntilOptions options); + void onMoveBottomCardsToDialogRequested(int defaultNumberBottomCards, + int maxCards, + const QString &targetZone, + const QString &zoneDisplayName, + bool faceDown); + void onDrawBottomCardsDialogRequested(int defaultNumberBottomCards, int maxCards); + void onRollDieDialogRequested(); + void onCreateRelatedFromRelationDialogRequested(const CardItem *sourceCard, const CardRelation *cardRelation); + void onCreateTokenDialogRequested(const QStringList &predefinedTokens); + void onMoveCardXCardsFromTopDialogRequested(int defaultNumberTopCardsToPlaceBelow, int deckSize); + void onSetPTDialogRequested(const QString &oldPT); + void onSetAnnotationDialogRequested(const QString &oldAnnotation); + void onSetCardCounterDialogRequested(int counterId, const QString &oldValueForDlg); + +private: + PlayerGraphicsItem *player; + PlayerActions *playerActions; + + QWidget *dialogParent() const + { + if (auto *s = player->scene()) { + if (auto *v = s->views().value(0)) { + return v->window(); + } + } + return nullptr; + } +}; + +#endif // COCKATRICE_PLAYER_DIALOGS_H diff --git a/cockatrice/src/game/player/player_graphics_item.cpp b/cockatrice/src/game_graphics/player/player_graphics_item.cpp similarity index 78% rename from cockatrice/src/game/player/player_graphics_item.cpp rename to cockatrice/src/game_graphics/player/player_graphics_item.cpp index 0d4f8c3ed..e0194abda 100644 --- a/cockatrice/src/game/player/player_graphics_item.cpp +++ b/cockatrice/src/game_graphics/player/player_graphics_item.cpp @@ -1,13 +1,18 @@ #include "player_graphics_item.h" -#include "../../game_graphics/zones/hand_zone.h" -#include "../../game_graphics/zones/pile_zone.h" -#include "../../game_graphics/zones/stack_zone.h" -#include "../../game_graphics/zones/table_zone.h" +#include "../../game/player/player_actions.h" #include "../../interface/widgets/tabs/tab_game.h" #include "../board/abstract_card_item.h" #include "../board/counter_general.h" #include "../hand_counter.h" +#include "../zones/hand_zone.h" +#include "../zones/pile_zone.h" +#include "../zones/stack_zone.h" +#include "../zones/table_zone.h" +#include "menu/player_menu.h" +#include "player_dialogs.h" + +#include PlayerGraphicsItem::PlayerGraphicsItem(PlayerLogic *_player) : player(_player) { @@ -16,28 +21,35 @@ PlayerGraphicsItem::PlayerGraphicsItem(PlayerLogic *_player) : player(_player) connect(&SettingsCache::instance(), &SettingsCache::handJustificationChanged, this, &PlayerGraphicsItem::rearrangeZones); connect(player, &PlayerLogic::rearrangeCounters, this, &PlayerGraphicsItem::rearrangeCounters); + connect(player, &PlayerLogic::activeChanged, this, &PlayerGraphicsItem::onPlayerActiveChanged); connect(player, &PlayerLogic::concededChanged, this, [this](int, bool c) { setVisible(!c); }); connect(player, &PlayerLogic::zoneIdChanged, this, [this](int id) { playerArea->setPlayerZoneId(id); }); connect(player, &PlayerLogic::counterAdded, this, &PlayerGraphicsItem::onCounterAdded); connect(player, &PlayerLogic::counterRemoved, this, &PlayerGraphicsItem::onCounterRemoved); - connect(player->getPlayerMenu(), &PlayerMenu::shortcutsActivated, this, [this]() { + playerMenu = new PlayerMenu(this); + + connect(playerMenu, &PlayerMenu::shortcutsActivated, this, [this]() { for (auto *ctr : counterWidgets) { ctr->setShortcutsActive(); } }); - connect(player->getPlayerMenu(), &PlayerMenu::shortcutsDeactivated, this, [this]() { + connect(playerMenu, &PlayerMenu::shortcutsDeactivated, this, [this]() { for (auto *ctr : counterWidgets) { ctr->setShortcutsInactive(); } }); - connect(player->getPlayerMenu(), &PlayerMenu::retranslateRequested, this, [this]() { + connect(playerMenu, &PlayerMenu::retranslateRequested, this, [this]() { for (auto *ctr : counterWidgets) { ctr->retranslateUi(); } }); + playerDialogs = new PlayerDialogs(this, player->getPlayerActions()); + + connect(playerDialogs, &PlayerDialogs::requestDialogSemaphore, player, &PlayerLogic::setDialogSemaphore); + playerArea = new PlayerArea(this); playerTarget = new PlayerTarget(player, playerArea); @@ -47,6 +59,11 @@ PlayerGraphicsItem::PlayerGraphicsItem(PlayerLogic *_player) : player(_player) initializeZones(); + connect(player, &PlayerLogic::addViewCustomZoneActionToCustomZoneMenu, this, + &PlayerGraphicsItem::onCustomZoneAdded); + + playerMenu->setMenusForGraphicItems(); + connect(tableZoneGraphicsItem, &TableZone::sizeChanged, this, &PlayerGraphicsItem::updateBoundingRect); updateBoundingRect(); @@ -57,7 +74,7 @@ PlayerGraphicsItem::PlayerGraphicsItem(PlayerLogic *_player) : player(_player) void PlayerGraphicsItem::retranslateUi() { - player->getPlayerMenu()->retranslateUi(); + playerMenu->retranslateUi(); QMapIterator zoneIterator(player->getZones()); while (zoneIterator.hasNext()) { @@ -93,18 +110,33 @@ void PlayerGraphicsItem::initializeZones() rfgZoneGraphicsItem = new PileZone(player->getRfgZone(), this); rfgZoneGraphicsItem->setPos(base + QPointF(0, 2 * h + h2 + 10)); - tableZoneGraphicsItem = new TableZone(player->getTableZone(), this); + tableZoneGraphicsItem = new TableZone(player->getTableZone(), mirrored, this); connect(tableZoneGraphicsItem, &TableZone::sizeChanged, this, &PlayerGraphicsItem::updateBoundingRect); + connect(this, &PlayerGraphicsItem::mirroredChanged, tableZoneGraphicsItem, &TableZone::setMirrored); stackZoneGraphicsItem = new StackZone(player->getStackZone(), static_cast(tableZoneGraphicsItem->boundingRect().height()), this); handZoneGraphicsItem = new HandZone(player->getHandZone(), static_cast(tableZoneGraphicsItem->boundingRect().height()), this); + connect(player->getPlayerActions(), &PlayerActions::requestSortHand, handZoneGraphicsItem, &HandZone::sortHand); connect(handZoneGraphicsItem->getLogic(), &HandZoneLogic::cardCountChanged, handCounter, &HandCounter::updateNumber); connect(handCounter, &HandCounter::showContextMenu, handZoneGraphicsItem, &HandZone::showContextMenu); + + zoneGraphicsItems.insert(player->getDeckZone()->getName(), deckZoneGraphicsItem); + zoneGraphicsItems.insert(player->getGraveZone()->getName(), graveyardZoneGraphicsItem); + zoneGraphicsItems.insert(player->getRfgZone()->getName(), rfgZoneGraphicsItem); + zoneGraphicsItems.insert(player->getSideboardZone()->getName(), sideboardGraphicsItem); + zoneGraphicsItems.insert(player->getTableZone()->getName(), tableZoneGraphicsItem); + zoneGraphicsItems.insert(player->getStackZone()->getName(), stackZoneGraphicsItem); + zoneGraphicsItems.insert(player->getHandZone()->getName(), handZoneGraphicsItem); +} + +void PlayerGraphicsItem::onCustomZoneAdded(QString customZoneName) +{ + zoneGraphicsItems.insert(customZoneName, nullptr); // Custom zone view goes here, if we ever implement it. } QRectF PlayerGraphicsItem::boundingRect() const @@ -145,6 +177,7 @@ void PlayerGraphicsItem::setMirrored(bool _mirrored) { if (mirrored != _mirrored) { mirrored = _mirrored; + emit mirroredChanged(mirrored); rearrangeZones(); } } @@ -159,11 +192,11 @@ void PlayerGraphicsItem::onCounterAdded(CounterState *state) } counterWidgets.insert(state->getId(), widget); - if (player->getPlayerMenu()->getCountersMenu() && widget->getMenu()) { - player->getPlayerMenu()->getCountersMenu()->addMenu(widget->getMenu()); + if (playerMenu->getCountersMenu() && widget->getMenu()) { + playerMenu->getCountersMenu()->addMenu(widget->getMenu()); } - if (player->getPlayerMenu()->getShortcutsActive()) { + if (playerMenu->getShortcutsActive()) { widget->setShortcutsActive(); } @@ -176,8 +209,8 @@ void PlayerGraphicsItem::onCounterRemoved(int counterId) if (!widget) { return; } - if (player->getPlayerMenu()->getCountersMenu() && widget->getMenu()) { - player->getPlayerMenu()->getCountersMenu()->removeAction(widget->getMenu()->menuAction()); + if (playerMenu->getCountersMenu() && widget->getMenu()) { + playerMenu->getCountersMenu()->removeAction(widget->getMenu()->menuAction()); } widget->delCounter(); rearrangeCounters(); diff --git a/cockatrice/src/game/player/player_graphics_item.h b/cockatrice/src/game_graphics/player/player_graphics_item.h similarity index 83% rename from cockatrice/src/game/player/player_graphics_item.h rename to cockatrice/src/game_graphics/player/player_graphics_item.h index e37fe7290..d02234ded 100644 --- a/cockatrice/src/game/player/player_graphics_item.h +++ b/cockatrice/src/game_graphics/player/player_graphics_item.h @@ -6,14 +6,16 @@ #ifndef COCKATRICE_PLAYER_GRAPHICS_ITEM_H #define COCKATRICE_PLAYER_GRAPHICS_ITEM_H +#include "../../game/player/player_logic.h" #include "../board/abstract_counter.h" #include "../game_scene.h" -#include "player_logic.h" #include class HandZone; class PileZone; +class PlayerDialogs; +class PlayerMenu; class PlayerTarget; class StackZone; class TableZone; @@ -55,11 +57,16 @@ public: return static_cast(scene()); } - PlayerLogic *getPlayer() const + PlayerLogic *getLogic() const { return player; } + [[nodiscard]] PlayerMenu *getPlayerMenu() const + { + return playerMenu; + } + PlayerArea *getPlayerArea() const { return playerArea; @@ -70,6 +77,11 @@ public: return playerTarget; } + CardZone *getZoneGraphicsItem(const QString &name) const + { + return zoneGraphicsItems.value(name, nullptr); + } + [[nodiscard]] PileZone *getDeckZoneGraphicsItem() const { return deckZoneGraphicsItem; @@ -103,6 +115,7 @@ public: public slots: void onPlayerActiveChanged(bool _active); + void onCustomZoneAdded(QString customZoneName); void onCounterAdded(CounterState *state); void onCounterRemoved(int counterId); void rearrangeCounters(); @@ -111,12 +124,17 @@ public slots: signals: void sizeChanged(); void playerCountChanged(); + void mirroredChanged(bool isMirrored); + void cardInfoRequested(const CardRef &cardRef); private: PlayerLogic *player; + PlayerMenu *playerMenu; + PlayerDialogs *playerDialogs; PlayerArea *playerArea; PlayerTarget *playerTarget; QMap counterWidgets; + QMap zoneGraphicsItems; PileZone *deckZoneGraphicsItem; PileZone *sideboardGraphicsItem; PileZone *graveyardZoneGraphicsItem; diff --git a/cockatrice/src/game/player/player_list_widget.cpp b/cockatrice/src/game_graphics/player/player_list_widget.cpp similarity index 100% rename from cockatrice/src/game/player/player_list_widget.cpp rename to cockatrice/src/game_graphics/player/player_list_widget.cpp diff --git a/cockatrice/src/game/player/player_list_widget.h b/cockatrice/src/game_graphics/player/player_list_widget.h similarity index 97% rename from cockatrice/src/game/player/player_list_widget.h rename to cockatrice/src/game_graphics/player/player_list_widget.h index 842c45873..a53cfa989 100644 --- a/cockatrice/src/game/player/player_list_widget.h +++ b/cockatrice/src/game_graphics/player/player_list_widget.h @@ -7,7 +7,7 @@ #ifndef PLAYERLISTWIDGET_H #define PLAYERLISTWIDGET_H -#include "player_logic.h" +#include "../../game/player/player_logic.h" #include #include diff --git a/cockatrice/src/game/player/player_target.cpp b/cockatrice/src/game_graphics/player/player_target.cpp similarity index 99% rename from cockatrice/src/game/player/player_target.cpp rename to cockatrice/src/game_graphics/player/player_target.cpp index 97fd51998..567f3d44d 100644 --- a/cockatrice/src/game/player/player_target.cpp +++ b/cockatrice/src/game_graphics/player/player_target.cpp @@ -1,7 +1,7 @@ #include "player_target.h" +#include "../../game/player/player_logic.h" #include "../../interface/pixel_map_generator.h" -#include "player_logic.h" #include #include diff --git a/cockatrice/src/game/player/player_target.h b/cockatrice/src/game_graphics/player/player_target.h similarity index 95% rename from cockatrice/src/game/player/player_target.h rename to cockatrice/src/game_graphics/player/player_target.h index d3facc60d..67e155660 100644 --- a/cockatrice/src/game/player/player_target.h +++ b/cockatrice/src/game_graphics/player/player_target.h @@ -7,9 +7,9 @@ #ifndef PLAYERTARGET_H #define PLAYERTARGET_H -#include "../../game_graphics/board/graphics_item_type.h" #include "../board/abstract_counter.h" #include "../board/arrow_target.h" +#include "../board/graphics_item_type.h" #include diff --git a/cockatrice/src/game/z_value_layer_manager.h b/cockatrice/src/game_graphics/z_value_layer_manager.h similarity index 100% rename from cockatrice/src/game/z_value_layer_manager.h rename to cockatrice/src/game_graphics/z_value_layer_manager.h diff --git a/cockatrice/src/game/z_values.h b/cockatrice/src/game_graphics/z_values.h similarity index 100% rename from cockatrice/src/game/z_values.h rename to cockatrice/src/game_graphics/z_values.h diff --git a/cockatrice/src/game_graphics/zones/card_zone.cpp b/cockatrice/src/game_graphics/zones/card_zone.cpp index 6ba8abe42..3457b681e 100644 --- a/cockatrice/src/game_graphics/zones/card_zone.cpp +++ b/cockatrice/src/game_graphics/zones/card_zone.cpp @@ -1,6 +1,6 @@ #include "card_zone.h" -#include "../../game/board/card_item.h" +#include "../board/card_item.h" #include "view_zone.h" #include diff --git a/cockatrice/src/game_graphics/zones/hand_zone.cpp b/cockatrice/src/game_graphics/zones/hand_zone.cpp index 09e9a5091..5885e3630 100644 --- a/cockatrice/src/game_graphics/zones/hand_zone.cpp +++ b/cockatrice/src/game_graphics/zones/hand_zone.cpp @@ -1,11 +1,11 @@ #include "hand_zone.h" #include "../../client/settings/cache_settings.h" -#include "../../game/board/card_drag_item.h" -#include "../../game/board/card_item.h" #include "../../game/player/player_actions.h" #include "../../game/player/player_logic.h" #include "../../interface/theme_manager.h" +#include "../board/card_drag_item.h" +#include "../board/card_item.h" #include #include diff --git a/cockatrice/src/game_graphics/zones/pile_zone.cpp b/cockatrice/src/game_graphics/zones/pile_zone.cpp index 302b983d8..7bb0e695a 100644 --- a/cockatrice/src/game_graphics/zones/pile_zone.cpp +++ b/cockatrice/src/game_graphics/zones/pile_zone.cpp @@ -1,10 +1,10 @@ #include "pile_zone.h" -#include "../../game/board/card_drag_item.h" -#include "../../game/board/card_item.h" #include "../../game/player/player_actions.h" #include "../../game/player/player_logic.h" #include "../../game/zones/pile_zone_logic.h" +#include "../board/card_drag_item.h" +#include "../board/card_item.h" #include "view_zone.h" #include diff --git a/cockatrice/src/game_graphics/zones/select_zone.cpp b/cockatrice/src/game_graphics/zones/select_zone.cpp index 90d53b464..f2e720686 100644 --- a/cockatrice/src/game_graphics/zones/select_zone.cpp +++ b/cockatrice/src/game_graphics/zones/select_zone.cpp @@ -1,8 +1,8 @@ #include "select_zone.h" #include "../../client/settings/cache_settings.h" -#include "../../game/board/card_item.h" -#include "../../game/game_scene.h" +#include "../board/card_item.h" +#include "../game_scene.h" #include #include diff --git a/cockatrice/src/game_graphics/zones/stack_zone.cpp b/cockatrice/src/game_graphics/zones/stack_zone.cpp index 9b0545b1d..46ff099ab 100644 --- a/cockatrice/src/game_graphics/zones/stack_zone.cpp +++ b/cockatrice/src/game_graphics/zones/stack_zone.cpp @@ -1,12 +1,12 @@ #include "stack_zone.h" -#include "../../game/board/card_drag_item.h" -#include "../../game/board/card_item.h" -#include "../../game/card_dimensions.h" #include "../../game/player/player_actions.h" #include "../../game/player/player_logic.h" #include "../../game/zones/stack_zone_logic.h" #include "../../interface/theme_manager.h" +#include "../board/card_drag_item.h" +#include "../board/card_item.h" +#include "../card_dimensions.h" #include #include diff --git a/cockatrice/src/game_graphics/zones/table_zone.cpp b/cockatrice/src/game_graphics/zones/table_zone.cpp index ffb4adf5c..e886f62e9 100644 --- a/cockatrice/src/game_graphics/zones/table_zone.cpp +++ b/cockatrice/src/game_graphics/zones/table_zone.cpp @@ -1,14 +1,14 @@ #include "table_zone.h" #include "../../client/settings/cache_settings.h" -#include "../../game/board/arrow_item.h" -#include "../../game/board/card_drag_item.h" -#include "../../game/board/card_item.h" #include "../../game/player/player_actions.h" #include "../../game/player/player_logic.h" -#include "../../game/z_values.h" #include "../../game/zones/table_zone_logic.h" #include "../../interface/theme_manager.h" +#include "../board/arrow_item.h" +#include "../board/card_drag_item.h" +#include "../board/card_item.h" +#include "../z_values.h" #include #include @@ -22,7 +22,8 @@ const QColor TableZone::FADE_MASK = QColor(0, 0, 0, 80); const QColor TableZone::GRADIENT_COLOR = QColor(255, 255, 255, 150); const QColor TableZone::GRADIENT_COLORLESS = QColor(255, 255, 255, 0); -TableZone::TableZone(TableZoneLogic *_logic, QGraphicsItem *parent) : SelectZone(_logic, parent), active(false) +TableZone::TableZone(TableZoneLogic *_logic, bool _mirrored, QGraphicsItem *parent) + : SelectZone(_logic, parent), active(false), mirrored(_mirrored) { connect(_logic, &TableZoneLogic::contentSizeChanged, this, &TableZone::resizeToContents); connect(_logic, &TableZoneLogic::toggleTapped, this, &TableZone::toggleTapped); @@ -50,12 +51,16 @@ QRectF TableZone::boundingRect() const return QRectF(0, 0, width, height); } +void TableZone::setMirrored(bool isMirrored) +{ + mirrored = isMirrored; + update(); +} + bool TableZone::isInverted() const { - return ((getLogic()->getPlayer()->getGraphicsItem()->getMirrored() && - !SettingsCache::instance().getInvertVerticalCoordinate()) || - (!getLogic()->getPlayer()->getGraphicsItem()->getMirrored() && - SettingsCache::instance().getInvertVerticalCoordinate())); + return ((mirrored && !SettingsCache::instance().getInvertVerticalCoordinate()) || + (!mirrored && SettingsCache::instance().getInvertVerticalCoordinate())); } void TableZone::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*option*/, QWidget * /*widget*/) diff --git a/cockatrice/src/game_graphics/zones/table_zone.h b/cockatrice/src/game_graphics/zones/table_zone.h index f46531520..0d7e58206 100644 --- a/cockatrice/src/game_graphics/zones/table_zone.h +++ b/cockatrice/src/game_graphics/zones/table_zone.h @@ -7,8 +7,8 @@ #ifndef TABLEZONE_H #define TABLEZONE_H -#include "../../game/board/abstract_card_item.h" #include "../../game/zones/table_zone_logic.h" +#include "../board/abstract_card_item.h" #include "select_zone.h" /** @@ -82,6 +82,7 @@ private: If this TableZone is currently active */ bool active = false; + bool mirrored = false; [[nodiscard]] bool isInverted() const; @@ -96,6 +97,7 @@ public slots: Reorganizes CardItems in the TableZone */ void reorganizeCards() override; + void setMirrored(bool isMirrored); public: /** @@ -104,7 +106,7 @@ public: @param _p the Player @param parent defaults to null */ - explicit TableZone(TableZoneLogic *_logic, QGraphicsItem *parent = nullptr); + explicit TableZone(TableZoneLogic *_logic, bool mirrored, QGraphicsItem *parent = nullptr); /** @return a QRectF of the TableZone bounding box. diff --git a/cockatrice/src/game_graphics/zones/view_zone.cpp b/cockatrice/src/game_graphics/zones/view_zone.cpp index 805c60638..baf7b8b30 100644 --- a/cockatrice/src/game_graphics/zones/view_zone.cpp +++ b/cockatrice/src/game_graphics/zones/view_zone.cpp @@ -1,10 +1,10 @@ #include "view_zone.h" -#include "../../game/board/card_drag_item.h" -#include "../../game/board/card_item.h" #include "../../game/player/player_actions.h" #include "../../game/player/player_logic.h" #include "../../game/zones/view_zone_logic.h" +#include "../board/card_drag_item.h" +#include "../board/card_item.h" #include #include diff --git a/cockatrice/src/game_graphics/zones/view_zone_widget.cpp b/cockatrice/src/game_graphics/zones/view_zone_widget.cpp index 03c6d8925..14537a826 100644 --- a/cockatrice/src/game_graphics/zones/view_zone_widget.cpp +++ b/cockatrice/src/game_graphics/zones/view_zone_widget.cpp @@ -2,12 +2,12 @@ #include "../../client/settings/cache_settings.h" #include "../../filters/syntax_help.h" -#include "../../game/board/card_item.h" -#include "../../game/game_scene.h" #include "../../game/player/player_actions.h" #include "../../game/player/player_logic.h" -#include "../../game/z_values.h" #include "../../interface/pixel_map_generator.h" +#include "../board/card_item.h" +#include "../game_scene.h" +#include "../z_values.h" #include "view_zone.h" #include @@ -75,6 +75,11 @@ ZoneViewWidget::ZoneViewWidget(PlayerLogic *_player, searchEditProxy->setZValue(ZValues::DRAG_ITEM); vbox->addItem(searchEditProxy); + // hide search bar if chat autofocus setting is enabled, since typing into it will no longer work anyway + searchEditProxy->setVisible(!SettingsCache::instance().getKeepGameChatFocus()); + connect(&SettingsCache::instance(), &SettingsCache::keepGameChatFocusChanged, searchEditProxy, + [searchEditProxy](bool keepFocus) { searchEditProxy->setVisible(!keepFocus); }); + // top row QGraphicsLinearLayout *hTopRow = new QGraphicsLinearLayout(Qt::Horizontal); diff --git a/cockatrice/src/interface/deck_loader/deck_loader.cpp b/cockatrice/src/interface/deck_loader/deck_loader.cpp index e616c5eb5..39a0c1071 100644 --- a/cockatrice/src/interface/deck_loader/deck_loader.cpp +++ b/cockatrice/src/interface/deck_loader/deck_loader.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -129,7 +130,10 @@ std::optional DeckLoader::loadFromRemote(const QString &nativeString std::optional DeckLoader::saveToFile(const DeckList &deck, const QString &fileName, DeckFileFormat::Format fmt) { - QFile file(fileName); + // Use QSaveFile so that a failed write (e.g. a full disk) leaves the existing deck untouched + // instead of truncating it to a 0-byte file. The target is only replaced once every byte has + // been flushed successfully in commit(). + QSaveFile file(fileName); if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { qCWarning(DeckLoaderLog) << "Could not create or open file:" << fileName; return std::nullopt; @@ -145,15 +149,19 @@ DeckLoader::saveToFile(const DeckList &deck, const QString &fileName, DeckFileFo break; } - file.flush(); - file.close(); - - qCInfo(DeckLoaderLog) << "Saved deck to " << fileName << "with format" << fmt << "-" << success; - if (!success) { + file.cancelWriting(); + qCWarning(DeckLoaderLog) << "Failed to serialize deck for file:" << fileName; return std::nullopt; } + if (!file.commit()) { + qCWarning(DeckLoaderLog) << "Failed to save deck to " << fileName << ":" << file.errorString(); + return std::nullopt; + } + + qCInfo(DeckLoaderLog) << "Saved deck to " << fileName << "with format" << fmt; + LoadedDeck::LoadInfo lastLoadInfo = {fileName, fmt}; return lastLoadInfo; } @@ -196,38 +204,44 @@ bool DeckLoader::updateLastLoadedTimestamp(LoadedDeck &deck) QDateTime originalTimestamp = fileInfo.lastModified(); - // Open the file for writing - QFile file(fileName); + // Use QSaveFile so that a failed write (e.g. a full disk) cannot truncate an existing deck to a + // 0-byte file while merely bumping its timestamp. + QSaveFile file(fileName); if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { qCWarning(DeckLoaderLog) << "Failed to open file for writing:" << fileName; return false; } - bool result = false; - // Perform file modifications deck.deckList.setLastLoadedTimestamp(QDateTime::currentDateTime().toString()); - result = deck.deckList.saveToFile_Native(&file); - file.close(); // Close the file to ensure changes are flushed - - if (result) { - // Re-open the file and set the original timestamp - if (!file.open(QIODevice::ReadWrite)) { - qCWarning(DeckLoaderLog) << "Failed to re-open file to set timestamp:" << fileName; - return false; - } - - if (!file.setFileTime(originalTimestamp, QFileDevice::FileModificationTime)) { - qCWarning(DeckLoaderLog) << "Failed to set modification time for file:" << fileName; - file.close(); - return false; - } - - file.close(); + if (!deck.deckList.saveToFile_Native(&file)) { + file.cancelWriting(); + qCWarning(DeckLoaderLog) << "Failed to serialize deck for file:" << fileName; + return false; } - return result; + if (!file.commit()) { + qCWarning(DeckLoaderLog) << "Failed to update timestamp for file:" << fileName << ":" << file.errorString(); + return false; + } + + // Re-open the file and restore the original timestamp, so that updating the lastLoadedTimestamp + // does not change the file's modification time. + QFile timestampFile(fileName); + if (!timestampFile.open(QIODevice::ReadWrite)) { + qCWarning(DeckLoaderLog) << "Failed to re-open file to set timestamp:" << fileName; + return false; + } + + if (!timestampFile.setFileTime(originalTimestamp, QFileDevice::FileModificationTime)) { + qCWarning(DeckLoaderLog) << "Failed to set modification time for file:" << fileName; + timestampFile.close(); + return false; + } + + timestampFile.close(); + return true; } static QString getDomainForWebsite(DeckLoader::DecklistWebsite website) @@ -444,51 +458,54 @@ bool DeckLoader::convertToCockatriceFormat(LoadedDeck &deck) return false; } + // Determine the format before touching any file, so an already-converted or + // unsupported deck never truncates or deletes anything. + switch (DeckFileFormat::getFormatFromName(fileName)) { + case DeckFileFormat::PlainText: + break; + case DeckFileFormat::Cockatrice: + qCInfo(DeckLoaderLog) << "File is already in Cockatrice format. No conversion needed."; + return true; + default: + qCWarning(DeckLoaderLog) << "Unsupported file format for conversion:" << fileName; + return false; + } + // Change the file extension to .cod QFileInfo fileInfo(fileName); QString newFileName = QDir::toNativeSeparators(fileInfo.path() + "/" + fileInfo.completeBaseName() + ".cod"); - // Open the new file for writing - QFile file(newFileName); + // Use QSaveFile so a failed write (e.g. a full disk) cannot leave a 0-byte .cod + // behind and then delete the original deck. + QSaveFile file(newFileName); if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { qCWarning(DeckLoaderLog) << "Failed to open file for writing:" << newFileName; return false; } - bool result = false; - - // Perform file modifications based on the detected format - switch (DeckFileFormat::getFormatFromName(fileName)) { - case DeckFileFormat::PlainText: - // Save in Cockatrice's native format - result = deck.deckList.saveToFile_Native(&file); - break; - case DeckFileFormat::Cockatrice: - qCInfo(DeckLoaderLog) << "File is already in Cockatrice format. No conversion needed."; - result = true; - break; - default: - qCWarning(DeckLoaderLog) << "Unsupported file format for conversion:" << fileName; - result = false; - break; + if (!deck.deckList.saveToFile_Native(&file)) { + file.cancelWriting(); + qCWarning(DeckLoaderLog) << "Failed to serialize deck for file:" << newFileName; + return false; } - file.close(); - - // Delete the old file if conversion was successful - if (result) { - if (!QFile::remove(fileName)) { - qCWarning(DeckLoaderLog) << "Failed to delete original file:" << fileName; - } else { - qCInfo(DeckLoaderLog) << "Original file deleted successfully:" << fileName; - } - deck.lastLoadInfo = { - .fileName = newFileName, - .fileFormat = DeckFileFormat::Cockatrice, - }; + if (!file.commit()) { + qCWarning(DeckLoaderLog) << "Failed to convert deck to " << newFileName << ":" << file.errorString(); + return false; } - return result; + // Conversion succeeded: delete the original file. + if (!QFile::remove(fileName)) { + qCWarning(DeckLoaderLog) << "Failed to delete original file:" << fileName; + } else { + qCInfo(DeckLoaderLog) << "Original file deleted successfully:" << fileName; + } + deck.lastLoadInfo = { + .fileName = newFileName, + .fileFormat = DeckFileFormat::Cockatrice, + }; + + return true; } void DeckLoader::printDeckListNode(QTextCursor *cursor, const InnerDecklistNode *node) diff --git a/cockatrice/src/interface/theme_manager.cpp b/cockatrice/src/interface/theme_manager.cpp index 086845fe6..7dc757062 100644 --- a/cockatrice/src/interface/theme_manager.cpp +++ b/cockatrice/src/interface/theme_manager.cpp @@ -271,6 +271,9 @@ void ThemeManager::applyStyleAndPalette(const QString &themeName, const PaletteConfig &palCfg, const QString &activeScheme) { +#if (QT_VERSION < QT_VERSION_CHECK(6, 5, 0)) + Q_UNUSED(activeScheme) +#endif QString styleName = themeCfg.styleName; if (styleName.isEmpty() || styleName.compare("Default", Qt::CaseInsensitive) == 0) { if (themeName == FUSION_THEME_NAME) { @@ -396,6 +399,7 @@ static QString roleBgName(ThemeManager::Role role) default: Q_ASSERT(false); + return {}; } } diff --git a/cockatrice/src/interface/widgets/cards/card_group_display_widgets/card_group_display_widget.cpp b/cockatrice/src/interface/widgets/cards/card_group_display_widgets/card_group_display_widget.cpp index 5fb0cb343..3f36e559c 100644 --- a/cockatrice/src/interface/widgets/cards/card_group_display_widgets/card_group_display_widget.cpp +++ b/cockatrice/src/interface/widgets/cards/card_group_display_widgets/card_group_display_widget.cpp @@ -58,16 +58,6 @@ void CardGroupDisplayWidget::mousePressEvent(QMouseEvent *event) } } -void CardGroupDisplayWidget::onClick(QMouseEvent *event, CardInfoPictureWithTextOverlayWidget *card) -{ - emit cardClicked(event, card); -} - -void CardGroupDisplayWidget::onHover(const ExactCard &card) -{ - emit cardHovered(card); -} - void CardGroupDisplayWidget::onSelectionChanged(const QItemSelection &selected, const QItemSelection &deselected) { auto proxyModel = qobject_cast(selectionModel->model()); @@ -154,8 +144,8 @@ QWidget *CardGroupDisplayWidget::constructWidgetForIndex(QPersistentModelIndex i widget->setScaleFactor(cardSizeWidget->getSlider()->value()); widget->setCard(CardDatabaseManager::query()->getCard({cardName, cardProviderId})); - connect(widget, &CardInfoPictureWithTextOverlayWidget::imageClicked, this, &CardGroupDisplayWidget::onClick); - connect(widget, &CardInfoPictureWithTextOverlayWidget::hoveredOnCard, this, &CardGroupDisplayWidget::onHover); + connect(widget, &CardInfoPictureWithTextOverlayWidget::cardClicked, this, &CardGroupDisplayWidget::cardClicked); + connect(widget, &CardInfoPictureWithTextOverlayWidget::hoveredOnCard, this, &CardGroupDisplayWidget::cardHovered); connect(cardSizeWidget->getSlider(), &QSlider::valueChanged, widget, &CardInfoPictureWidget::setScaleFactor); indexToWidgetMap[index].append(widget); diff --git a/cockatrice/src/interface/widgets/cards/card_group_display_widgets/card_group_display_widget.h b/cockatrice/src/interface/widgets/cards/card_group_display_widgets/card_group_display_widget.h index 848bebb7e..2308ccf8d 100644 --- a/cockatrice/src/interface/widgets/cards/card_group_display_widgets/card_group_display_widget.h +++ b/cockatrice/src/interface/widgets/cards/card_group_display_widgets/card_group_display_widget.h @@ -48,8 +48,6 @@ public: public slots: void mousePressEvent(QMouseEvent *event) override; - void onClick(QMouseEvent *event, CardInfoPictureWithTextOverlayWidget *card); - void onHover(const ExactCard &card); virtual QWidget *constructWidgetForIndex(QPersistentModelIndex index); virtual void updateCardDisplays(); virtual void onCardAddition(const QModelIndex &parent, int first, int last); @@ -59,7 +57,7 @@ public slots: void resizeEvent(QResizeEvent *event) override; signals: - void cardClicked(QMouseEvent *event, CardInfoPictureWithTextOverlayWidget *card); + void cardClicked(QMouseEvent *event, const ExactCard &card); void cardHovered(const ExactCard &card); void cleanupRequested(CardGroupDisplayWidget *cardGroupDisplayWidget); diff --git a/cockatrice/src/interface/widgets/cards/card_info_display_widget.cpp b/cockatrice/src/interface/widgets/cards/card_info_display_widget.cpp index 509a2d92f..577dafe0a 100644 --- a/cockatrice/src/interface/widgets/cards/card_info_display_widget.cpp +++ b/cockatrice/src/interface/widgets/cards/card_info_display_widget.cpp @@ -1,6 +1,6 @@ #include "card_info_display_widget.h" -#include "../../../game/board/card_item.h" +#include "../../../game_graphics/board/card_item.h" #include "card_info_picture_widget.h" #include "card_info_text_widget.h" diff --git a/cockatrice/src/interface/widgets/cards/card_info_frame_widget.cpp b/cockatrice/src/interface/widgets/cards/card_info_frame_widget.cpp index 21bee8f54..2e7c62461 100644 --- a/cockatrice/src/interface/widgets/cards/card_info_frame_widget.cpp +++ b/cockatrice/src/interface/widgets/cards/card_info_frame_widget.cpp @@ -1,7 +1,7 @@ #include "card_info_frame_widget.h" #include "../../../client/settings/cache_settings.h" -#include "../../../game/board/card_item.h" +#include "../../../game_graphics/board/card_item.h" #include "card_info_display_widget.h" #include "card_info_picture_widget.h" #include "card_info_text_widget.h" diff --git a/cockatrice/src/interface/widgets/cards/card_info_picture_widget.cpp b/cockatrice/src/interface/widgets/cards/card_info_picture_widget.cpp index ecb84af36..69b8301a9 100644 --- a/cockatrice/src/interface/widgets/cards/card_info_picture_widget.cpp +++ b/cockatrice/src/interface/widgets/cards/card_info_picture_widget.cpp @@ -1,7 +1,7 @@ #include "card_info_picture_widget.h" #include "../../../client/settings/cache_settings.h" -#include "../../../game/board/card_item.h" +#include "../../../game_graphics/board/card_item.h" #include "../../../interface/card_picture_loader/card_picture_loader.h" #include "../../../interface/widgets/tabs/tab_supervisor.h" #include "../../window_main.h" @@ -341,7 +341,7 @@ void CardInfoPictureWidget::mousePressEvent(QMouseEvent *event) createRightClickMenu()->popup(QCursor::pos()); } - emit cardClicked(event); + emit cardClicked(event, exactCard); } void CardInfoPictureWidget::hideEvent(QHideEvent *event) @@ -427,13 +427,13 @@ QMenu *CardInfoPictureWidget::createAddToOpenDeckMenu() QAction *addCard = addCardMenu->addAction(tr("Mainboard")); connect(addCard, &QAction::triggered, this, [this, deckEditorTab] { deckEditorTab->updateCard(exactCard); - deckEditorTab->actAddCard(exactCard); + deckEditorTab->addCard(exactCard, DECK_ZONE_MAIN); }); QAction *addCardSideboard = addCardMenu->addAction(tr("Sideboard")); connect(addCardSideboard, &QAction::triggered, this, [this, deckEditorTab] { deckEditorTab->updateCard(exactCard); - deckEditorTab->actAddCardToSideboard(exactCard); + deckEditorTab->addCard(exactCard, DECK_ZONE_SIDE); }); } diff --git a/cockatrice/src/interface/widgets/cards/card_info_picture_widget.h b/cockatrice/src/interface/widgets/cards/card_info_picture_widget.h index 96e813594..4fe84ed0b 100644 --- a/cockatrice/src/interface/widgets/cards/card_info_picture_widget.h +++ b/cockatrice/src/interface/widgets/cards/card_info_picture_widget.h @@ -43,7 +43,7 @@ signals: void hoveredOnCard(const ExactCard &hoveredCard); void cardScaleFactorChanged(int _scale); void cardChanged(const ExactCard &card); - void cardClicked(QMouseEvent *event); + void cardClicked(QMouseEvent *event, const ExactCard &card); protected: void resizeEvent(QResizeEvent *event) override; diff --git a/cockatrice/src/interface/widgets/cards/card_info_picture_with_text_overlay_widget.cpp b/cockatrice/src/interface/widgets/cards/card_info_picture_with_text_overlay_widget.cpp index 2f0aeccfd..c5cb59b3b 100644 --- a/cockatrice/src/interface/widgets/cards/card_info_picture_with_text_overlay_widget.cpp +++ b/cockatrice/src/interface/widgets/cards/card_info_picture_with_text_overlay_widget.cpp @@ -93,7 +93,7 @@ void CardInfoPictureWithTextOverlayWidget::setHighlighted(bool _highlighted) void CardInfoPictureWithTextOverlayWidget::mousePressEvent(QMouseEvent *event) { - emit imageClicked(event, this); + emit cardClicked(event, getCard()); } /** diff --git a/cockatrice/src/interface/widgets/cards/card_info_picture_with_text_overlay_widget.h b/cockatrice/src/interface/widgets/cards/card_info_picture_with_text_overlay_widget.h index 0cc7e501c..ba978498d 100644 --- a/cockatrice/src/interface/widgets/cards/card_info_picture_with_text_overlay_widget.h +++ b/cockatrice/src/interface/widgets/cards/card_info_picture_with_text_overlay_widget.h @@ -35,8 +35,6 @@ public: void setHighlighted(bool _highlighted); [[nodiscard]] QSize sizeHint() const override; -signals: - void imageClicked(QMouseEvent *event, CardInfoPictureWithTextOverlayWidget *instance); protected: void paintEvent(QPaintEvent *event) override; diff --git a/cockatrice/src/interface/widgets/cards/card_info_text_widget.cpp b/cockatrice/src/interface/widgets/cards/card_info_text_widget.cpp index 345eb9909..c6af5320b 100644 --- a/cockatrice/src/interface/widgets/cards/card_info_text_widget.cpp +++ b/cockatrice/src/interface/widgets/cards/card_info_text_widget.cpp @@ -1,6 +1,6 @@ #include "card_info_text_widget.h" -#include "../../../game/board/card_item.h" +#include "../../../game_graphics/board/card_item.h" #include #include diff --git a/cockatrice/src/interface/widgets/cards/deck_card_zone_display_widget.cpp b/cockatrice/src/interface/widgets/cards/deck_card_zone_display_widget.cpp index a8a97a4ca..eaf3a67b0 100644 --- a/cockatrice/src/interface/widgets/cards/deck_card_zone_display_widget.cpp +++ b/cockatrice/src/interface/widgets/cards/deck_card_zone_display_widget.cpp @@ -51,7 +51,7 @@ DeckCardZoneDisplayWidget::DeckCardZoneDisplayWidget(QWidget *parent, // User Interaction // ===================================================================================================================== -void DeckCardZoneDisplayWidget::onClick(QMouseEvent *event, CardInfoPictureWithTextOverlayWidget *card) +void DeckCardZoneDisplayWidget::onClick(QMouseEvent *event, const ExactCard &card) { emit cardClicked(event, card, zoneName); } diff --git a/cockatrice/src/interface/widgets/cards/deck_card_zone_display_widget.h b/cockatrice/src/interface/widgets/cards/deck_card_zone_display_widget.h index 074a77e53..b426fca30 100644 --- a/cockatrice/src/interface/widgets/cards/deck_card_zone_display_widget.h +++ b/cockatrice/src/interface/widgets/cards/deck_card_zone_display_widget.h @@ -42,7 +42,7 @@ public: void addCardsToOverlapWidget(); public slots: - void onClick(QMouseEvent *event, CardInfoPictureWithTextOverlayWidget *card); + void onClick(QMouseEvent *event, const ExactCard &card); void onHover(const ExactCard &card); void cleanupInvalidCardGroup(CardGroupDisplayWidget *displayWidget); void constructAppropriateWidget(QPersistentModelIndex index); @@ -55,7 +55,7 @@ public slots: void onCategoryRemoval(const QModelIndex &parent, int first, int last); signals: - void cardClicked(QMouseEvent *event, CardInfoPictureWithTextOverlayWidget *card, QString zoneName); + void cardClicked(QMouseEvent *event, const ExactCard &card, const QString &zoneName); void cardHovered(const ExactCard &card); void activeSortCriteriaChanged(QStringList activeSortCriteria); void requestCleanup(DeckCardZoneDisplayWidget *displayWidget); diff --git a/cockatrice/src/interface/widgets/deck_editor/card_database_view.cpp b/cockatrice/src/interface/widgets/deck_editor/card_database_view.cpp new file mode 100644 index 000000000..a1c29e241 --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_editor/card_database_view.cpp @@ -0,0 +1,167 @@ +#include "card_database_view.h" + +#include "../../../client/settings/cache_settings.h" +#include "card_database_display_model.h" +#include "card_database_model.h" + +#include +#include +#include +#include +#include +#include +#include + +static bool canBeCommander(const CardInfo &cardInfo) +{ + return (cardInfo.getCardType().contains("Legendary", Qt::CaseInsensitive) && + cardInfo.getCardType().contains("Creature", Qt::CaseInsensitive)) || + cardInfo.getText().contains("can be your commander", Qt::CaseInsensitive); +} + +CardDatabaseView::CardDatabaseView(QWidget *parent, CardDatabaseDisplayModel *model) + : QTreeView(parent), databaseDisplayModel(model) +{ + // set up object + setUniformRowHeights(true); + setRootIsDecorated(false); + setAlternatingRowColors(true); + setSortingEnabled(true); + sortByColumn(0, Qt::AscendingOrder); + QTreeView::setModel(databaseDisplayModel); + setContextMenuPolicy(Qt::CustomContextMenu); + + connect(databaseDisplayModel, &CardDatabaseDisplayModel::modelDirty, this, + &CardDatabaseView::resetSelectionIfEmpty); + + connect(this, &QTreeView::customContextMenuRequested, this, &CardDatabaseView::openCustomMenu); + connect(selectionModel(), &QItemSelectionModel::currentRowChanged, this, &CardDatabaseView::updateCard); + connect(this, &QTreeView::doubleClicked, this, &CardDatabaseView::actDoubleClick); + + // layout settings + QByteArray dbHeaderState = SettingsCache::instance().layouts().getDeckEditorDbHeaderState(); + if (dbHeaderState.isNull()) { + // first run + setColumnWidth(0, 200); + } else { + header()->restoreState(dbHeaderState); + } + connect(header(), &QHeaderView::geometriesChanged, this, &CardDatabaseView::saveDbHeaderState); + + // create key filters + searchKeySignals.setObjectName("searchKeySignals"); + connect(&searchKeySignals, &KeySignals::onEnter, this, [this] { addCard(DECK_ZONE_MAIN); }); + connect(&searchKeySignals, &KeySignals::onCtrlAltEqual, this, [this] { addCard(DECK_ZONE_MAIN); }); + connect(&searchKeySignals, &KeySignals::onCtrlAltRBracket, this, [this] { addCard(DECK_ZONE_SIDE); }); + connect(&searchKeySignals, &KeySignals::onCtrlAltMinus, this, [this] { decrementCard(DECK_ZONE_MAIN); }); + connect(&searchKeySignals, &KeySignals::onCtrlAltLBracket, this, [this] { decrementCard(DECK_ZONE_SIDE); }); + connect(&searchKeySignals, &KeySignals::onCtrlAltEnter, this, [this] { addCard(DECK_ZONE_SIDE); }); + connect(&searchKeySignals, &KeySignals::onCtrlEnter, this, [this] { addCard(DECK_ZONE_SIDE); }); + connect(&searchKeySignals, &KeySignals::onCtrlC, this, &CardDatabaseView::copyDatabaseCellContents); +} + +QString CardDatabaseView::currentCardName() const +{ + const QModelIndex currentIndex = selectionModel()->currentIndex(); + if (!currentIndex.isValid()) { + return {}; + } + + return currentIndex.siblingAtColumn(CardDatabaseModel::NameColumn).data().toString(); +} + +void CardDatabaseView::actDoubleClick() +{ + if (QApplication::keyboardModifiers() & Qt::ControlModifier) { + addCard(DECK_ZONE_SIDE); + } else { + addCard(DECK_ZONE_MAIN); + } +} + +void CardDatabaseView::addCard(const QString &zoneName) +{ + emit cardAdded(currentCardName(), zoneName); +} + +void CardDatabaseView::decrementCard(const QString &zoneName) +{ + emit cardDecremented(currentCardName(), zoneName); +} + +void CardDatabaseView::updateCard(const QModelIndex ¤t, const QModelIndex & /*previous*/) +{ + if (!current.isValid()) { + return; + } + + const QString cardName = current.siblingAtColumn(CardDatabaseModel::NameColumn).data().toString(); + + if (!current.model()->hasChildren(current.siblingAtColumn(CardDatabaseModel::NameColumn))) { + emit cardChanged(cardName); + } +} + +void CardDatabaseView::resetSelectionIfEmpty() +{ + QModelIndexList sel = selectionModel()->selectedRows(); + if (sel.isEmpty() && databaseDisplayModel->rowCount() > 0) { + selectionModel()->setCurrentIndex(databaseDisplayModel->index(0, 0), + QItemSelectionModel::SelectCurrent | QItemSelectionModel::Rows); + } +} + +void CardDatabaseView::copyDatabaseCellContents() const +{ + auto _data = selectionModel()->currentIndex().data(); + QApplication::clipboard()->setText(_data.toString()); +} + +void CardDatabaseView::saveDbHeaderState() +{ + SettingsCache::instance().layouts().setDeckEditorDbHeaderState(header()->saveState()); +} + +void CardDatabaseView::openCustomMenu(QPoint point) +{ + CardInfoPtr card = CardDatabaseManager::query()->getCardInfo(currentCardName()); + + if (!card) { + return; + } + + QMenu menu; + // add to deck and sideboard options + QAction *addToDeck = menu.addAction(tr("Add to Deck")); + QAction *addToSideboard = menu.addAction(tr("Add to Sideboard")); + QAction *selectPrinting = menu.addAction(tr("Select Printing")); + + connect(addToDeck, &QAction::triggered, this, [this, card] { emit cardAdded(card->getName(), DECK_ZONE_MAIN); }); + connect(addToSideboard, &QAction::triggered, this, + [this, card] { emit cardAdded(card->getName(), DECK_ZONE_SIDE); }); + connect(selectPrinting, &QAction::triggered, this, &CardDatabaseView::selectPrintingClicked); + + if (canBeCommander(*card)) { + QAction *edhRecCommander = menu.addAction(tr("Show on EDHRec (Commander)")); + connect(edhRecCommander, &QAction::triggered, this, [this, card] { emit edhrecClicked(card, true); }); + } + QAction *edhRecCard = menu.addAction(tr("Show on EDHRec (Card)")); + connect(edhRecCard, &QAction::triggered, this, [this, card] { emit edhrecClicked(card, false); }); + + // filling out the related cards submenu + auto *relatedMenu = new QMenu(tr("Show Related cards")); + menu.addMenu(relatedMenu); + auto relatedCards = card->getAllRelatedCards(); + if (relatedCards.isEmpty()) { + relatedMenu->setDisabled(true); + } else { + for (const CardRelation *rel : relatedCards) { + const QString &relatedCardName = rel->getName(); + QAction *relatedCard = relatedMenu->addAction(relatedCardName); + connect(relatedCard, &QAction::triggered, this, + [this, relatedCardName] { emit relatedCardClicked(relatedCardName); }); + } + } + + menu.exec(mapToGlobal(point)); +} diff --git a/cockatrice/src/interface/widgets/deck_editor/card_database_view.h b/cockatrice/src/interface/widgets/deck_editor/card_database_view.h new file mode 100644 index 000000000..175ec12b9 --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_editor/card_database_view.h @@ -0,0 +1,59 @@ +#ifndef COCKATRICE_CARD_DATABASE_VIEW_H +#define COCKATRICE_CARD_DATABASE_VIEW_H + +#include "../../key_signals.h" + +#include +#include + +class CardDatabaseModel; +class CardDatabaseDisplayModel; + +/** + * @brief The card database table. + */ +class CardDatabaseView : public QTreeView +{ + Q_OBJECT + + KeySignals searchKeySignals; + CardDatabaseDisplayModel *databaseDisplayModel; + +public: + explicit CardDatabaseView(QWidget *parent, CardDatabaseDisplayModel *model); + + QString currentCardName() const; + + /** + * @brief Get the KeySignals that are connected to this view. + * You can install the KeySignals as an eventFilter to capture keyboard shortcuts for adding and decrementing cards. + */ + KeySignals *getKeySignals() + { + return &searchKeySignals; + } + +signals: + void cardChanged(const QString &cardName); + + void cardAdded(const QString &cardName, const QString &zoneName); + void cardDecremented(const QString &cardName, const QString &zoneName); + + void edhrecClicked(const CardInfoPtr &cardInfo, bool isCommander); + void selectPrintingClicked(); + void relatedCardClicked(const QString &relatedCard); + +private slots: + void actDoubleClick(); + + void addCard(const QString &zoneName); + void decrementCard(const QString &zoneName); + void updateCard(const QModelIndex ¤t, const QModelIndex &); + + void resetSelectionIfEmpty(); + void copyDatabaseCellContents() const; + void saveDbHeaderState(); + void openCustomMenu(QPoint point); +}; + +#endif // COCKATRICE_CARD_DATABASE_VIEW_H diff --git a/cockatrice/src/interface/widgets/deck_editor/deck_editor_card_database_dock_widget.cpp b/cockatrice/src/interface/widgets/deck_editor/deck_editor_card_database_dock_widget.cpp index f2a2ab4ea..2a491de4f 100644 --- a/cockatrice/src/interface/widgets/deck_editor/deck_editor_card_database_dock_widget.cpp +++ b/cockatrice/src/interface/widgets/deck_editor/deck_editor_card_database_dock_widget.cpp @@ -13,7 +13,7 @@ DeckEditorCardDatabaseDockWidget::DeckEditorCardDatabaseDockWidget(AbstractTabDe void DeckEditorCardDatabaseDockWidget::createDatabaseDisplayDock(AbstractTabDeckEditor *deckEditor) { - databaseDisplayWidget = new DeckEditorDatabaseDisplayWidget(this, deckEditor); + databaseDisplayWidget = new DeckEditorDatabaseDisplayWidget(this, deckEditor->databaseModel); auto *frame = new QVBoxLayout; frame->setObjectName("databaseDisplayFrame"); @@ -29,19 +29,16 @@ void DeckEditorCardDatabaseDockWidget::createDatabaseDisplayDock(AbstractTabDeck // connect signals connect(databaseDisplayWidget, &DeckEditorDatabaseDisplayWidget::cardChanged, deckEditor, &AbstractTabDeckEditor::updateCard); - connect(databaseDisplayWidget, &DeckEditorDatabaseDisplayWidget::addCardToMainDeck, deckEditor, - &AbstractTabDeckEditor::actAddCard); - connect(databaseDisplayWidget, &DeckEditorDatabaseDisplayWidget::addCardToSideboard, deckEditor, - &AbstractTabDeckEditor::actAddCardToSideboard); - connect(databaseDisplayWidget, &DeckEditorDatabaseDisplayWidget::decrementCardFromMainDeck, deckEditor, - &AbstractTabDeckEditor::actDecrementCard); - connect(databaseDisplayWidget, &DeckEditorDatabaseDisplayWidget::decrementCardFromSideboard, deckEditor, - &AbstractTabDeckEditor::actDecrementCardFromSideboard); -} - -CardDatabase *DeckEditorCardDatabaseDockWidget::getDatabase() const -{ - return databaseDisplayWidget->databaseModel->getDatabase(); + connect(databaseDisplayWidget, &DeckEditorDatabaseDisplayWidget::cardAdded, deckEditor, + &AbstractTabDeckEditor::addCard); + connect(databaseDisplayWidget, &DeckEditorDatabaseDisplayWidget::cardDecremented, deckEditor, + &AbstractTabDeckEditor::decrementCard); + connect(databaseDisplayWidget, &DeckEditorDatabaseDisplayWidget::edhrecRequested, deckEditor, + &AbstractTabDeckEditor::openEdhrecTab); + connect(databaseDisplayWidget, &DeckEditorDatabaseDisplayWidget::printingSelectorRequested, deckEditor, + &AbstractTabDeckEditor::showPrintingSelector); + connect(databaseDisplayWidget, &DeckEditorDatabaseDisplayWidget::cardInfoRequested, deckEditor, + &AbstractTabDeckEditor::updateCardInfo); } void DeckEditorCardDatabaseDockWidget::retranslateUi() diff --git a/cockatrice/src/interface/widgets/deck_editor/deck_editor_card_database_dock_widget.h b/cockatrice/src/interface/widgets/deck_editor/deck_editor_card_database_dock_widget.h index bff9ee36f..6af2e4432 100644 --- a/cockatrice/src/interface/widgets/deck_editor/deck_editor_card_database_dock_widget.h +++ b/cockatrice/src/interface/widgets/deck_editor/deck_editor_card_database_dock_widget.h @@ -17,7 +17,6 @@ public: DeckEditorDatabaseDisplayWidget *databaseDisplayWidget; - CardDatabase *getDatabase() const; void setFilterTree(FilterTree *filterTree); public slots: diff --git a/cockatrice/src/interface/widgets/deck_editor/deck_editor_database_display_widget.cpp b/cockatrice/src/interface/widgets/deck_editor/deck_editor_database_display_widget.cpp index 580db67f4..9da821813 100644 --- a/cockatrice/src/interface/widgets/deck_editor/deck_editor_database_display_widget.cpp +++ b/cockatrice/src/interface/widgets/deck_editor/deck_editor_database_display_widget.cpp @@ -5,24 +5,17 @@ #include "../../../interface/widgets/tabs/abstract_tab_deck_editor.h" #include "../../../interface/widgets/tabs/tab_supervisor.h" #include "../../pixel_map_generator.h" +#include "card_database_view.h" #include #include -#include #include #include #include #include -static bool canBeCommander(const CardInfo &cardInfo) -{ - return (cardInfo.getCardType().contains("Legendary", Qt::CaseInsensitive) && - cardInfo.getCardType().contains("Creature", Qt::CaseInsensitive)) || - cardInfo.getText().contains("can be your commander", Qt::CaseInsensitive); -} - -DeckEditorDatabaseDisplayWidget::DeckEditorDatabaseDisplayWidget(QWidget *parent, AbstractTabDeckEditor *deckEditor) - : QWidget(parent), deckEditor(deckEditor) +DeckEditorDatabaseDisplayWidget::DeckEditorDatabaseDisplayWidget(QWidget *parent, CardDatabaseModel *databaseModel) + : QWidget(parent) { setObjectName("databaseDisplayWidget"); @@ -36,62 +29,34 @@ DeckEditorDatabaseDisplayWidget::DeckEditorDatabaseDisplayWidget(QWidget *parent searchEdit->setClearButtonEnabled(true); searchEdit->addAction(loadColorAdjustedPixmap("theme:icons/search"), QLineEdit::LeadingPosition); auto help = searchEdit->addAction(QPixmap("theme:icons/info"), QLineEdit::TrailingPosition); - searchEdit->installEventFilter(&searchKeySignals); setFocusProxy(searchEdit); setFocusPolicy(Qt::ClickFocus); - searchKeySignals.setObjectName("searchKeySignals"); - connect(searchEdit, &SearchLineEdit::textChanged, this, &DeckEditorDatabaseDisplayWidget::updateSearch); - connect(&searchKeySignals, &KeySignals::onEnter, this, &DeckEditorDatabaseDisplayWidget::actAddCardToMainDeck); - connect(&searchKeySignals, &KeySignals::onCtrlAltEqual, this, - &DeckEditorDatabaseDisplayWidget::actAddCardToMainDeck); - connect(&searchKeySignals, &KeySignals::onCtrlAltRBracket, this, - &DeckEditorDatabaseDisplayWidget::actAddCardToSideboard); - connect(&searchKeySignals, &KeySignals::onCtrlAltMinus, this, - &DeckEditorDatabaseDisplayWidget::actDecrementCardFromMainDeck); - connect(&searchKeySignals, &KeySignals::onCtrlAltLBracket, this, - &DeckEditorDatabaseDisplayWidget::actDecrementCardFromSideboard); - connect(&searchKeySignals, &KeySignals::onCtrlAltEnter, this, - &DeckEditorDatabaseDisplayWidget::actAddCardToSideboard); - connect(&searchKeySignals, &KeySignals::onCtrlEnter, this, &DeckEditorDatabaseDisplayWidget::actAddCardToSideboard); - connect(&searchKeySignals, &KeySignals::onCtrlC, this, &DeckEditorDatabaseDisplayWidget::copyDatabaseCellContents); connect(help, &QAction::triggered, this, [this] { createSearchSyntaxHelpWindow(searchEdit); }); - databaseModel = new CardDatabaseModel(CardDatabaseManager::getInstance(), true, this); - databaseModel->setObjectName("databaseModel"); databaseDisplayModel = new CardDatabaseDisplayModel(this); databaseDisplayModel->setObjectName("databaseDisplayModel"); databaseDisplayModel->setSourceModel(databaseModel); databaseDisplayModel->setFilterKeyColumn(0); - databaseView = new QTreeView(this); + databaseView = new CardDatabaseView(this, databaseDisplayModel); databaseView->setObjectName("databaseView"); databaseView->setFocusProxy(searchEdit); - databaseView->setUniformRowHeights(true); - databaseView->setRootIsDecorated(false); - databaseView->setAlternatingRowColors(true); - databaseView->setSortingEnabled(true); - databaseView->sortByColumn(0, Qt::AscendingOrder); - databaseView->setModel(databaseDisplayModel); - databaseView->setContextMenuPolicy(Qt::CustomContextMenu); - connect(databaseView, &QTreeView::customContextMenuRequested, this, - &DeckEditorDatabaseDisplayWidget::databaseCustomMenu); - connect(databaseView->selectionModel(), &QItemSelectionModel::currentRowChanged, this, - &DeckEditorDatabaseDisplayWidget::updateCard); - connect(databaseView, &QTreeView::doubleClicked, this, &DeckEditorDatabaseDisplayWidget::actAddCardToMainDeck); - - QByteArray dbHeaderState = SettingsCache::instance().layouts().getDeckEditorDbHeaderState(); - if (dbHeaderState.isNull()) { - // first run - databaseView->setColumnWidth(0, 200); - } else { - databaseView->header()->restoreState(dbHeaderState); - } - connect(databaseView->header(), &QHeaderView::geometriesChanged, this, - &DeckEditorDatabaseDisplayWidget::saveDbHeaderState); searchEdit->setTreeView(databaseView); + searchEdit->installEventFilter(databaseView->getKeySignals()); + + connect(searchEdit, &SearchLineEdit::textChanged, databaseDisplayModel, &CardDatabaseDisplayModel::setStringFilter); + connect(databaseView, &CardDatabaseView::cardAdded, this, &DeckEditorDatabaseDisplayWidget::addCard); + connect(databaseView, &CardDatabaseView::cardDecremented, this, &DeckEditorDatabaseDisplayWidget::decrementCard); + connect(databaseView, &CardDatabaseView::cardChanged, this, &DeckEditorDatabaseDisplayWidget::updateCard); + + connect(databaseView, &CardDatabaseView::edhrecClicked, this, &DeckEditorDatabaseDisplayWidget::edhrecRequested); + connect(databaseView, &CardDatabaseView::selectPrintingClicked, this, + &DeckEditorDatabaseDisplayWidget::printingSelectorRequested); + connect(databaseView, &CardDatabaseView::relatedCardClicked, this, + &DeckEditorDatabaseDisplayWidget::onRelatedCardClicked); aAddCard = new QAction(QString(), this); aAddCard->setIcon(QPixmap("theme:icons/arrow_right_green")); @@ -117,121 +82,39 @@ DeckEditorDatabaseDisplayWidget::DeckEditorDatabaseDisplayWidget(QWidget *parent retranslateUi(); } -void DeckEditorDatabaseDisplayWidget::updateSearch(const QString &search) -{ - databaseDisplayModel->setStringFilter(search); - QModelIndexList sel = databaseView->selectionModel()->selectedRows(); - if (sel.isEmpty() && databaseDisplayModel->rowCount()) { - databaseView->selectionModel()->setCurrentIndex(databaseDisplayModel->index(0, 0), - QItemSelectionModel::SelectCurrent | QItemSelectionModel::Rows); - } -} - void DeckEditorDatabaseDisplayWidget::clearAllDatabaseFilters() { databaseDisplayModel->clearFilterAll(); searchEdit->setText(""); } -void DeckEditorDatabaseDisplayWidget::updateCard(const QModelIndex ¤t, const QModelIndex & /*previous*/) -{ - if (!current.isValid()) { - return; - } - - const QString cardName = current.siblingAtColumn(CardDatabaseModel::NameColumn).data().toString(); - - if (!current.model()->hasChildren(current.siblingAtColumn(CardDatabaseModel::NameColumn))) { - emit cardChanged(CardDatabaseManager::query()->getPreferredCard(cardName)); - } -} - void DeckEditorDatabaseDisplayWidget::actAddCardToMainDeck() { - highlightAllSearchEdit(); - emit addCardToMainDeck(currentCard()); + addCard(databaseView->currentCardName(), DECK_ZONE_MAIN); } void DeckEditorDatabaseDisplayWidget::actAddCardToSideboard() +{ + addCard(databaseView->currentCardName(), DECK_ZONE_SIDE); +} + +void DeckEditorDatabaseDisplayWidget::addCard(const QString &cardName, const QString &zoneName) { highlightAllSearchEdit(); - emit addCardToSideboard(currentCard()); + ExactCard exactCard = CardDatabaseManager::query()->getPreferredCard(cardName); + emit cardAdded(exactCard, zoneName); } -void DeckEditorDatabaseDisplayWidget::actDecrementCardFromMainDeck() +void DeckEditorDatabaseDisplayWidget::decrementCard(const QString &cardName, const QString &zoneName) { - emit decrementCardFromMainDeck(currentCard()); + ExactCard exactCard = CardDatabaseManager::query()->getPreferredCard(cardName); + emit cardDecremented(exactCard, zoneName); } -void DeckEditorDatabaseDisplayWidget::actDecrementCardFromSideboard() +void DeckEditorDatabaseDisplayWidget::updateCard(const QString &cardName) { - emit decrementCardFromSideboard(currentCard()); -} - -ExactCard DeckEditorDatabaseDisplayWidget::currentCard() const -{ - const QModelIndex currentIndex = databaseView->selectionModel()->currentIndex(); - if (!currentIndex.isValid()) { - return {}; - } - - const QString cardName = currentIndex.siblingAtColumn(CardDatabaseModel::NameColumn).data().toString(); - - return CardDatabaseManager::query()->getPreferredCard(cardName); -} - -void DeckEditorDatabaseDisplayWidget::databaseCustomMenu(QPoint point) -{ - QMenu menu; - ExactCard card = currentCard(); - - if (card) { - // add to deck and sideboard options - QAction *addToDeck, *addToSideboard, *selectPrinting, *edhRecCommander, *edhRecCard; - addToDeck = menu.addAction(tr("Add to Deck")); - addToSideboard = menu.addAction(tr("Add to Sideboard")); - selectPrinting = menu.addAction(tr("Select Printing")); - connect(selectPrinting, &QAction::triggered, this, [this, card] { deckEditor->showPrintingSelector(); }); - if (canBeCommander(card.getInfo())) { - edhRecCommander = menu.addAction(tr("Show on EDHRec (Commander)")); - connect(edhRecCommander, &QAction::triggered, this, - [this, card] { deckEditor->getTabSupervisor()->addEdhrecTab(card.getCardPtr(), true); }); - } - edhRecCard = menu.addAction(tr("Show on EDHRec (Card)")); - - connect(addToDeck, &QAction::triggered, this, &DeckEditorDatabaseDisplayWidget::actAddCardToMainDeck); - connect(addToSideboard, &QAction::triggered, this, &DeckEditorDatabaseDisplayWidget::actAddCardToSideboard); - connect(edhRecCard, &QAction::triggered, this, - [this, card] { deckEditor->getTabSupervisor()->addEdhrecTab(card.getCardPtr()); }); - - // filling out the related cards submenu - auto *relatedMenu = new QMenu(tr("Show Related cards")); - menu.addMenu(relatedMenu); - auto relatedCards = card.getInfo().getAllRelatedCards(); - if (relatedCards.isEmpty()) { - relatedMenu->setDisabled(true); - } else { - for (const CardRelation *rel : relatedCards) { - const QString &relatedCardName = rel->getName(); - QAction *relatedCard = relatedMenu->addAction(relatedCardName); - connect( - relatedCard, &QAction::triggered, deckEditor->cardInfoDockWidget->cardInfo, - [this, relatedCardName] { deckEditor->cardInfoDockWidget->cardInfo->setCard(relatedCardName); }); - } - } - menu.exec(databaseView->mapToGlobal(point)); - } -} - -void DeckEditorDatabaseDisplayWidget::copyDatabaseCellContents() -{ - auto _data = databaseView->selectionModel()->currentIndex().data(); - QApplication::clipboard()->setText(_data.toString()); -} - -void DeckEditorDatabaseDisplayWidget::saveDbHeaderState() -{ - SettingsCache::instance().layouts().setDeckEditorDbHeaderState(databaseView->header()->saveState()); + ExactCard exactCard = CardDatabaseManager::query()->getPreferredCard(cardName); + emit cardChanged(exactCard); } void DeckEditorDatabaseDisplayWidget::setFilterTree(FilterTree *filterTree) @@ -248,4 +131,10 @@ void DeckEditorDatabaseDisplayWidget::retranslateUi() void DeckEditorDatabaseDisplayWidget::highlightAllSearchEdit() { searchEdit->setSelection(0, searchEdit->text().length()); +} + +void DeckEditorDatabaseDisplayWidget::onRelatedCardClicked(const QString &relatedCard) +{ + ExactCard exactCard = CardDatabaseManager::query()->guessCard({relatedCard}); + emit cardInfoRequested(exactCard); } \ No newline at end of file diff --git a/cockatrice/src/interface/widgets/deck_editor/deck_editor_database_display_widget.h b/cockatrice/src/interface/widgets/deck_editor/deck_editor_database_display_widget.h index 0f62998ef..5de4d211d 100644 --- a/cockatrice/src/interface/widgets/deck_editor/deck_editor_database_display_widget.h +++ b/cockatrice/src/interface/widgets/deck_editor/deck_editor_database_display_widget.h @@ -9,7 +9,6 @@ #define DECK_EDITOR_DATABASE_DISPLAY_WIDGET_H #include "../../../interface/widgets/tabs/abstract_tab_deck_editor.h" -#include "../../key_signals.h" #include "../utility/custom_line_edit.h" #include @@ -17,45 +16,44 @@ #include #include +class CardDatabaseView; class AbstractTabDeckEditor; + class DeckEditorDatabaseDisplayWidget : public QWidget { Q_OBJECT public: - explicit DeckEditorDatabaseDisplayWidget(QWidget *parent, AbstractTabDeckEditor *deckEditor); - AbstractTabDeckEditor *deckEditor; - CardDatabaseModel *databaseModel; - CardDatabaseDisplayModel *databaseDisplayModel; + explicit DeckEditorDatabaseDisplayWidget(QWidget *parent, CardDatabaseModel *databaseModel); - QTreeView *getDatabaseView() + CardDatabaseView *getDatabaseView() const { return databaseView; } public slots: - ExactCard currentCard() const; void setFilterTree(FilterTree *filterTree); void clearAllDatabaseFilters(); - void updateSearch(const QString &search); - void updateCard(const QModelIndex ¤t, const QModelIndex &); + void actAddCardToMainDeck(); void actAddCardToSideboard(); - void actDecrementCardFromMainDeck(); - void actDecrementCardFromSideboard(); - void databaseCustomMenu(QPoint point); - void copyDatabaseCellContents(); + + void addCard(const QString &cardName, const QString &zoneName); + void decrementCard(const QString &cardName, const QString &zoneName); + void updateCard(const QString &cardName); signals: - void addCardToMainDeck(const ExactCard &card); - void addCardToSideboard(const ExactCard &card); - void decrementCardFromMainDeck(const ExactCard &card); - void decrementCardFromSideboard(const ExactCard &card); + void cardAdded(const ExactCard &card, const QString &zoneName); + void cardDecremented(const ExactCard &card, const QString &zoneName); void cardChanged(const ExactCard &_card); + void edhrecRequested(const CardInfoPtr &cardInfo, bool isCommander); + void printingSelectorRequested(); + void cardInfoRequested(const ExactCard &card); + private: - KeySignals searchKeySignals; - QTreeView *databaseView; + CardDatabaseDisplayModel *databaseDisplayModel; + CardDatabaseView *databaseView; QHBoxLayout *searchLayout; SearchLineEdit *searchEdit; QAction *aAddCard, *aAddCardToSideboard; @@ -66,7 +64,8 @@ private: private slots: void retranslateUi(); - void saveDbHeaderState(); + + void onRelatedCardClicked(const QString &relatedCard); }; #endif // DECK_EDITOR_DATABASE_DISPLAY_WIDGET_H 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 f751fa225..f61a01168 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 @@ -11,7 +11,7 @@ #include #include #include -#include +#include static int findRestoreIndex(const CardRef &wanted, const QComboBox *combo) { 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 6db8e5623..f8fb450ce 100644 --- a/cockatrice/src/interface/widgets/deck_editor/deck_state_manager.cpp +++ b/cockatrice/src/interface/widgets/deck_editor/deck_state_manager.cpp @@ -255,6 +255,10 @@ bool DeckStateManager::swapCardAtIndex(const QModelIndex &idx) } QString zoneName = gparent.siblingAtColumn(DeckListModelColumns::CARD_NAME).data(Qt::EditRole).toString(); + // tokens have no swap target + if (zoneName == DECK_ZONE_TOKENS) { + return false; + } QString otherZoneName = zoneName == DECK_ZONE_MAIN ? DECK_ZONE_SIDE : DECK_ZONE_MAIN; QString reason = tr("Moved to %1 1 × \"%2\" (%3)") // diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_connect.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_connect.cpp index fdbc90542..2a7fc6c06 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_connect.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_connect.cpp @@ -12,7 +12,7 @@ #include #include #include -#include +#include DlgConnect::DlgConnect(QWidget *parent) : QDialog(parent) { diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_create_game.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_create_game.cpp index 30364f242..f892801f1 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_create_game.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_create_game.cpp @@ -17,7 +17,7 @@ #include #include #include -#include +#include void DlgCreateGame::sharedCtor() { diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_edit_avatar.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_edit_avatar.cpp index db5f21701..b5710b208 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_edit_avatar.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_edit_avatar.cpp @@ -8,7 +8,7 @@ #include #include #include -#include +#include DlgEditAvatar::DlgEditAvatar(QWidget *parent) : QDialog(parent), image() { diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_edit_password.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_edit_password.cpp index cdd4433a7..63181b44f 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_edit_password.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_edit_password.cpp @@ -7,7 +7,7 @@ #include #include #include -#include +#include DlgEditPassword::DlgEditPassword(QWidget *parent) : QDialog(parent) { diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_edit_tokens.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_edit_tokens.cpp index 381aa2b11..f249976c2 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_edit_tokens.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_edit_tokens.cpp @@ -19,7 +19,7 @@ #include #include #include -#include +#include DlgEditTokens::DlgEditTokens(QWidget *parent) : QDialog(parent), currentCard(nullptr) { diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_edit_user.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_edit_user.cpp index 7015f9d47..85b95f335 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_edit_user.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_edit_user.cpp @@ -6,7 +6,7 @@ #include #include #include -#include +#include DlgEditUser::DlgEditUser(QWidget *parent, QString email, QString country, QString realName) : QDialog(parent) { diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_forgot_password_challenge.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_forgot_password_challenge.cpp index 24e9030e0..14c3cda47 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_forgot_password_challenge.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_forgot_password_challenge.cpp @@ -7,7 +7,7 @@ #include #include #include -#include +#include DlgForgotPasswordChallenge::DlgForgotPasswordChallenge(QWidget *parent) : QDialog(parent) { diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_forgot_password_request.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_forgot_password_request.cpp index c33a41bed..fc934d082 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_forgot_password_request.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_forgot_password_request.cpp @@ -7,7 +7,7 @@ #include #include #include -#include +#include DlgForgotPasswordRequest::DlgForgotPasswordRequest(QWidget *parent) : QDialog(parent) { diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_forgot_password_reset.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_forgot_password_reset.cpp index d2eb081d1..d30887988 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_forgot_password_reset.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_forgot_password_reset.cpp @@ -7,7 +7,7 @@ #include #include #include -#include +#include DlgForgotPasswordReset::DlgForgotPasswordReset(QWidget *parent) : QDialog(parent) { diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_manage_sets.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_manage_sets.cpp index c693fb02e..b17a9306f 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_manage_sets.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_manage_sets.cpp @@ -62,7 +62,7 @@ WndSets::WndSets(QWidget *parent) : QMainWindow(parent) // search field searchField = new LineEditUnfocusable; searchField->setObjectName("searchEdit"); - searchField->setPlaceholderText(tr("Search by set name, code, or type")); + searchField->setPlaceholderText(tr("Search by set name, code, type, or release date")); searchField->addAction(QPixmap("theme:icons/search"), LineEditUnfocusable::LeadingPosition); searchField->setClearButtonEnabled(true); setFocusProxy(searchField); diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_register.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_register.cpp index 0f7c17b18..942782403 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_register.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_register.cpp @@ -8,7 +8,7 @@ #include #include #include -#include +#include DlgRegister::DlgRegister(QWidget *parent) : QDialog(parent) { diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_update.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_update.cpp index 15735168f..ee2149309 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_update.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_update.cpp @@ -219,10 +219,25 @@ void DlgUpdate::downloadError(const QString &errorString) void DlgUpdate::downloadSuccessful(const QUrl &filepath) { setLabel(tr("Installing...")); + + QString installerPath = filepath.toLocalFile(); + + QString appDir = QDir::toNativeSeparators(QCoreApplication::applicationDirPath()); + QProcess process; + process.setProgram(installerPath); + + // NSIS needs the /D= argument to be an UNQUOTED string, even if it contains spaces. Qt likes to quote arguments if + // they contain spaces, so we use the windows exclusive QProcess::setNativeArguments in the only case where this is + // relevant, which preserves the argument unquoted. +#ifdef Q_OS_WIN + process.setNativeArguments(QString("/R /D=%1").arg(appDir)); +#else + // Linux/macOS: normal argument passing (not relevant since they update differently.) + process.setArguments({"/R", QString("/D=%1").arg(appDir)}); +#endif + // Try to open the installer. If it opens, quit Cockatrice - if (QProcess::startDetached(filepath.toLocalFile(), - QStringList() - << "/R" << QString("/D=%1").arg(QCoreApplication::applicationDirPath()))) { + if (process.startDetached()) { QMetaObject::invokeMethod(static_cast(parent()), "close", Qt::QueuedConnection); qCInfo(DlgUpdateLog) << "Opened downloaded update file successfully - closing Cockatrice"; close(); diff --git a/cockatrice/src/interface/widgets/menus/deck_editor_menu.cpp b/cockatrice/src/interface/widgets/menus/deck_editor_menu.cpp index 23d19abbb..d6df694df 100644 --- a/cockatrice/src/interface/widgets/menus/deck_editor_menu.cpp +++ b/cockatrice/src/interface/widgets/menus/deck_editor_menu.cpp @@ -193,6 +193,8 @@ void DeckEditorMenu::refreshShortcuts() aEditDeckInClipboardRaw->setShortcuts(shortcuts.getShortcut("TabDeckEditor/aEditDeckInClipboardRaw")); aPrintDeck->setShortcuts(shortcuts.getShortcut("TabDeckEditor/aPrintDeck")); + aLoadDeckFromWebsite->setShortcuts(shortcuts.getShortcut("TabDeckEditor/aLoadDeckFromWebsite")); + aExportDeckDecklist->setShortcuts(shortcuts.getShortcut("TabDeckEditor/aExportDeckDecklist")); aExportDeckDecklistXyz->setShortcuts(shortcuts.getShortcut("TabDeckEditor/aExportDeckDecklistXyz")); aAnalyzeDeckDeckstats->setShortcuts(shortcuts.getShortcut("TabDeckEditor/aAnalyzeDeck")); diff --git a/cockatrice/src/interface/widgets/printing_selector/all_zones_card_amount_widget.cpp b/cockatrice/src/interface/widgets/printing_selector/all_zones_card_amount_widget.cpp index 2eac13d94..579106540 100644 --- a/cockatrice/src/interface/widgets/printing_selector/all_zones_card_amount_widget.cpp +++ b/cockatrice/src/interface/widgets/printing_selector/all_zones_card_amount_widget.cpp @@ -8,7 +8,7 @@ * @brief Constructor for the AllZonesCardAmountWidget class. * * Initializes the widget with its layout and sets up the connections and necessary - * UI elements for managing card counts in both the mainboard and sideboard zones. + * UI elements for managing card counts in all the mainboard, tokensboard and sideboard zones. * * @param parent The parent widget. * @param deckStateManager Pointer to the DeckStateManager @@ -31,13 +31,28 @@ AllZonesCardAmountWidget::AllZonesCardAmountWidget(QWidget *parent, buttonBoxMainboard = new CardAmountWidget(this, deckStateManager, cardSizeSlider, rootCard, DECK_ZONE_MAIN); zoneLabelSideboard = new ShadowBackgroundLabel(this, tr("Sideboard")); buttonBoxSideboard = new CardAmountWidget(this, deckStateManager, cardSizeSlider, rootCard, DECK_ZONE_SIDE); + zoneLabelTokensboard = new ShadowBackgroundLabel(this, tr("Tokens")); + buttonBoxTokensboard = new CardAmountWidget(this, deckStateManager, cardSizeSlider, rootCard, DECK_ZONE_TOKENS); layout->addWidget(zoneLabelMainboard, 0, Qt::AlignHCenter | Qt::AlignBottom); layout->addWidget(buttonBoxMainboard, 0, Qt::AlignHCenter | Qt::AlignTop); - layout->addSpacing(25); + layout->addSpacing(12); + layout->addWidget(zoneLabelTokensboard, 0, Qt::AlignHCenter | Qt::AlignBottom); + layout->addWidget(buttonBoxTokensboard, 0, Qt::AlignHCenter | Qt::AlignTop); + layout->addSpacing(13); layout->addWidget(zoneLabelSideboard, 0, Qt::AlignHCenter | Qt::AlignBottom); layout->addWidget(buttonBoxSideboard, 0, Qt::AlignHCenter | Qt::AlignTop); + // Show Tokens buttons for token cards, Mainboard/Sideboard for non-token cards + bool isToken = rootCard.getInfo().getIsToken(); + + zoneLabelMainboard->setVisible(!isToken); + buttonBoxMainboard->setVisible(!isToken); + zoneLabelTokensboard->setVisible(isToken); + buttonBoxTokensboard->setVisible(isToken); + zoneLabelSideboard->setVisible(!isToken); + buttonBoxSideboard->setVisible(!isToken); + connect(cardSizeSlider, &QSlider::valueChanged, this, &AllZonesCardAmountWidget::adjustFontSize); QTimer::singleShot(10, this, [this]() { adjustFontSize(this->cardSizeSlider->value()); }); @@ -67,15 +82,17 @@ void AllZonesCardAmountWidget::adjustFontSize(int scalePercentage) zoneLabelFont.setPointSize(newFontSize); zoneLabelMainboard->setFont(zoneLabelFont); zoneLabelSideboard->setFont(zoneLabelFont); + zoneLabelTokensboard->setFont(zoneLabelFont); // Repaint the widget (if necessary) repaint(); } -void AllZonesCardAmountWidget::setAmounts(int mainboardAmount, int sideboardAmount) +void AllZonesCardAmountWidget::setAmounts(int mainboardAmount, int sideboardAmount, int tokensboardAmount) { buttonBoxMainboard->setAmount(mainboardAmount); buttonBoxSideboard->setAmount(sideboardAmount); + buttonBoxTokensboard->setAmount(tokensboardAmount); } /** @@ -99,11 +116,21 @@ int AllZonesCardAmountWidget::getSideboardAmount() } /** - * @brief Checks if the amount is at least one in either the mainboard or sideboard. + * @brief Gets the card count in the tokensboard zone. + * + * @return The number of cards in the tokensboard. + */ +int AllZonesCardAmountWidget::getTokensboardAmount() +{ + return buttonBoxTokensboard->getAmount(); +} + +/** + * @brief Checks if the amount is at least one in either the mainboard or sideboard or tokensboard. */ bool AllZonesCardAmountWidget::isNonZero() { - return getMainboardAmount() > 0 || getSideboardAmount() > 0; + return getMainboardAmount() > 0 || getSideboardAmount() > 0 || getTokensboardAmount() > 0; } /** diff --git a/cockatrice/src/interface/widgets/printing_selector/all_zones_card_amount_widget.h b/cockatrice/src/interface/widgets/printing_selector/all_zones_card_amount_widget.h index a86333f6a..325dd1c1b 100644 --- a/cockatrice/src/interface/widgets/printing_selector/all_zones_card_amount_widget.h +++ b/cockatrice/src/interface/widgets/printing_selector/all_zones_card_amount_widget.h @@ -23,13 +23,14 @@ public: const ExactCard &rootCard); int getMainboardAmount(); int getSideboardAmount(); + int getTokensboardAmount(); bool isNonZero(); void enterEvent(QEnterEvent *event) override; public slots: void adjustFontSize(int scalePercentage); - void setAmounts(int mainboardAmount, int sideboardAmount); + void setAmounts(int mainboardAmount, int sideboardAmount, int tokensboardAmount); private: QVBoxLayout *layout; @@ -38,6 +39,8 @@ private: CardAmountWidget *buttonBoxMainboard; QLabel *zoneLabelSideboard; CardAmountWidget *buttonBoxSideboard; + QLabel *zoneLabelTokensboard; + CardAmountWidget *buttonBoxTokensboard; }; #endif // ALL_ZONES_CARD_AMOUNT_WIDGET_H diff --git a/cockatrice/src/interface/widgets/printing_selector/card_amount_widget.cpp b/cockatrice/src/interface/widgets/printing_selector/card_amount_widget.cpp index 25222f437..ff47e7b9c 100644 --- a/cockatrice/src/interface/widgets/printing_selector/card_amount_widget.cpp +++ b/cockatrice/src/interface/widgets/printing_selector/card_amount_widget.cpp @@ -11,7 +11,7 @@ * @param parent The parent widget. * @param cardSizeSlider Pointer to the QSlider for adjusting font size. * @param rootCard The root card to manage within the widget. - * @param zoneName The zone name (e.g., DECK_ZONE_MAIN or DECK_ZONE_SIDE). + * @param zoneName The zone name (e.g., DECK_ZONE_MAIN , DECK_ZONE_SIDE, or DECK_ZONE_TOKENS). */ CardAmountWidget::CardAmountWidget(QWidget *parent, DeckStateManager *deckStateManager, @@ -36,13 +36,16 @@ CardAmountWidget::CardAmountWidget(QWidget *parent, incrementButton->setFixedSize(parentWidget()->size().width() / 3, parentWidget()->size().height() / 9); decrementButton->setFixedSize(parentWidget()->size().width() / 3, parentWidget()->size().height() / 9); - // Set up connections based on the zone (Mainboard or Sideboard) + // Set up connections based on the zone (Mainboard, Sideboard, or Tokensboard) if (zoneName == DECK_ZONE_MAIN) { connect(incrementButton, &QPushButton::clicked, this, &CardAmountWidget::addPrintingMainboard); connect(decrementButton, &QPushButton::clicked, this, &CardAmountWidget::removePrintingMainboard); } else if (zoneName == DECK_ZONE_SIDE) { connect(incrementButton, &QPushButton::clicked, this, &CardAmountWidget::addPrintingSideboard); connect(decrementButton, &QPushButton::clicked, this, &CardAmountWidget::removePrintingSideboard); + } else if (zoneName == DECK_ZONE_TOKENS) { + connect(incrementButton, &QPushButton::clicked, this, &CardAmountWidget::addPrintingTokensboard); + connect(decrementButton, &QPushButton::clicked, this, &CardAmountWidget::removePrintingTokensboard); } cardCountInZone = new QLabel(QString::number(amount), this); @@ -137,6 +140,19 @@ void CardAmountWidget::updateCardCount() layout->activate(); } +static QString zoneLogName(const QString &zone) +{ + if (zone == DECK_ZONE_MAIN) { + return "mainboard"; + } else if (zone == DECK_ZONE_SIDE) { + return "sideboard"; + } else if (zone == DECK_ZONE_TOKENS) { + return "tokens"; + } else { + return "unknown"; + } +} + static QModelIndex addAndReplacePrintings(DeckListModel *model, const QModelIndex &existing, const ExactCard &rootCard, @@ -161,9 +177,9 @@ static QModelIndex addAndReplacePrintings(DeckListModel *model, } /** - * @brief Adds a printing of the card to the specified zone (Mainboard or Sideboard). + * @brief Adds a printing of the card to the specified zone (Mainboard, Sideboard, or Tokensboard). * - * @param zone The zone to add the card to (DECK_ZONE_MAIN or DECK_ZONE_SIDE). + * @param zone The zone to add the card to (DECK_ZONE_MAIN, DECK_ZONE_SIDE, or DECK_ZONE_TOKENS). */ void CardAmountWidget::addPrinting(const QString &zone) { @@ -183,12 +199,13 @@ void CardAmountWidget::addPrinting(const QString &zone) } } + QString zoneName = zoneLogName(zone); QString reason = QString("Added %1 copies of '%2 (%3) %4' to %5 [ProviderID: %6]%7") .arg(1 + extraCopies) .arg(rootCard.getName()) .arg(rootCard.getPrinting().getSet()->getShortName()) .arg(rootCard.getPrinting().getProperty("num")) - .arg(zone == DECK_ZONE_MAIN ? "mainboard" : "sideboard") + .arg(zoneName) .arg(rootCard.getPrinting().getUuid()) .arg(replacingProviderless ? " (replaced providerless printings)" : ""); @@ -218,6 +235,14 @@ void CardAmountWidget::addPrintingSideboard() addPrinting(DECK_ZONE_SIDE); } +/** + * @brief Adds a printing to the tokens zone. + */ +void CardAmountWidget::addPrintingTokensboard() +{ + addPrinting(DECK_ZONE_TOKENS); +} + /** * @brief Removes a printing from the mainboard zone. */ @@ -234,18 +259,27 @@ void CardAmountWidget::removePrintingSideboard() decrementCardHelper(DECK_ZONE_SIDE); } +/** + * @brief Removes a printing from the tokens zone. + */ +void CardAmountWidget::removePrintingTokensboard() +{ + decrementCardHelper(DECK_ZONE_TOKENS); +} + /** * @brief Helper function to decrement the card count for a given zone. * - * @param zone The zone from which to remove the card (DECK_ZONE_MAIN or DECK_ZONE_SIDE). + * @param zone The zone from which to remove the card (DECK_ZONE_MAIN, DECK_ZONE_SIDE, or DECK_ZONE_TOKENS). */ void CardAmountWidget::decrementCardHelper(const QString &zone) { + QString zoneName = zoneLogName(zone); QString reason = QString("Removed 1 copy of '%1 (%2) %3' from %4 [ProviderID: %5]") .arg(rootCard.getName()) .arg(rootCard.getPrinting().getSet()->getShortName()) .arg(rootCard.getPrinting().getProperty("num")) - .arg(zone == DECK_ZONE_MAIN ? "mainboard" : "sideboard") + .arg(zoneName) .arg(rootCard.getPrinting().getUuid()); deckStateManager->modifyDeck(reason, [this, &zone](auto model) { diff --git a/cockatrice/src/interface/widgets/printing_selector/card_amount_widget.h b/cockatrice/src/interface/widgets/printing_selector/card_amount_widget.h index f0f2128f0..2780e3ad2 100644 --- a/cockatrice/src/interface/widgets/printing_selector/card_amount_widget.h +++ b/cockatrice/src/interface/widgets/printing_selector/card_amount_widget.h @@ -60,8 +60,10 @@ private: private slots: void addPrintingMainboard(); void addPrintingSideboard(); + void addPrintingTokensboard(); void removePrintingMainboard(); void removePrintingSideboard(); + void removePrintingTokensboard(); void adjustFontSize(int scalePercentage); }; diff --git a/cockatrice/src/interface/widgets/printing_selector/printing_selector.cpp b/cockatrice/src/interface/widgets/printing_selector/printing_selector.cpp index 71b93b297..76a416587 100644 --- a/cockatrice/src/interface/widgets/printing_selector/printing_selector.cpp +++ b/cockatrice/src/interface/widgets/printing_selector/printing_selector.cpp @@ -105,23 +105,30 @@ void PrintingSelector::printingsInDeckChanged() } /** - * @return A map of uuid to amounts (main, side). + * @return A map of uuid to amounts (main, side, tokens). */ -static QMap> tallyUuidCounts(const DeckListModel *model, const QString &cardName) +static QMap tallyUuidCounts(const DeckListModel *model, const QString &cardName) { - QMap> map; + QMap map; auto mainNodes = model->getCardNodesForZone(DECK_ZONE_MAIN); for (auto &node : mainNodes) { if (node->getName() == cardName) { - map[node->getCardProviderId()].first += node->getNumber(); + map[node->getCardProviderId()].mainboard += node->getNumber(); } } auto sideNodes = model->getCardNodesForZone(DECK_ZONE_SIDE); for (auto &node : sideNodes) { if (node->getName() == cardName) { - map[node->getCardProviderId()].second += node->getNumber(); + map[node->getCardProviderId()].sideboard += node->getNumber(); + } + } + + auto tokensNodes = model->getCardNodesForZone(DECK_ZONE_TOKENS); + for (auto &node : tokensNodes) { + if (node->getName() == cardName) { + map[node->getCardProviderId()].tokensboard += node->getNumber(); } } diff --git a/cockatrice/src/interface/widgets/printing_selector/printing_selector.h b/cockatrice/src/interface/widgets/printing_selector/printing_selector.h index b9e6723f2..14d73f836 100644 --- a/cockatrice/src/interface/widgets/printing_selector/printing_selector.h +++ b/cockatrice/src/interface/widgets/printing_selector/printing_selector.h @@ -22,6 +22,13 @@ #define BATCH_SIZE 10 +struct ZoneCounts +{ + int mainboard = 0; + int sideboard = 0; + int tokensboard = 0; +}; + class DeckStateManager; class PrintingSelectorCardSearchWidget; class PrintingSelectorCardSelectionWidget; @@ -59,9 +66,9 @@ signals: /** * The amounts of the printings in the deck has changed - * @param uuidToAmounts Map of uuids to the amounts (maindeck, sideboard) in the deck + * @param uuidToAmounts Map of uuids to the amounts (maindeck, sideboard, tokensboard) in the deck */ - void cardAmountsChanged(const QMap> &uuidToAmounts); + void cardAmountsChanged(const QMap &uuidToAmounts); private: QVBoxLayout *layout; diff --git a/cockatrice/src/interface/widgets/printing_selector/printing_selector_card_display_widget.cpp b/cockatrice/src/interface/widgets/printing_selector/printing_selector_card_display_widget.cpp index 7d0b4882f..edeba86d1 100644 --- a/cockatrice/src/interface/widgets/printing_selector/printing_selector_card_display_widget.cpp +++ b/cockatrice/src/interface/widgets/printing_selector/printing_selector_card_display_widget.cpp @@ -67,10 +67,10 @@ void PrintingSelectorCardDisplayWidget::clampSetNameToPicture() update(); } -void PrintingSelectorCardDisplayWidget::updateCardAmounts(const QMap> &uuidToAmounts) +void PrintingSelectorCardDisplayWidget::updateCardAmounts(const QMap &uuidToAmounts) { - auto [main, side] = uuidToAmounts.value(rootCard.getPrinting().getUuid()); - overlayWidget->updateCardAmounts(main, side); + auto counts = uuidToAmounts.value(rootCard.getPrinting().getUuid()); + overlayWidget->updateCardAmounts(counts.mainboard, counts.sideboard, counts.tokensboard); } void PrintingSelectorCardDisplayWidget::resizeEvent(QResizeEvent *event) diff --git a/cockatrice/src/interface/widgets/printing_selector/printing_selector_card_display_widget.h b/cockatrice/src/interface/widgets/printing_selector/printing_selector_card_display_widget.h index b708bd973..4de561f4f 100644 --- a/cockatrice/src/interface/widgets/printing_selector/printing_selector_card_display_widget.h +++ b/cockatrice/src/interface/widgets/printing_selector/printing_selector_card_display_widget.h @@ -27,7 +27,7 @@ public: public slots: void clampSetNameToPicture(); - void updateCardAmounts(const QMap> &uuidToAmounts); + void updateCardAmounts(const QMap &uuidToAmounts); void resizeEvent(QResizeEvent *event) override; 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 69b84e085..b7144735e 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 @@ -112,9 +112,11 @@ void PrintingSelectorCardOverlayWidget::enterEvent(QEnterEvent *event) updateVisibility(); } -void PrintingSelectorCardOverlayWidget::updateCardAmounts(int mainboardAmount, int sideboardAmount) +void PrintingSelectorCardOverlayWidget::updateCardAmounts(int mainboardAmount, + int sideboardAmount, + int tokensboardAmount) { - allZonesCardAmountWidget->setAmounts(mainboardAmount, sideboardAmount); + allZonesCardAmountWidget->setAmounts(mainboardAmount, sideboardAmount, tokensboardAmount); updateVisibility(); } @@ -169,8 +171,8 @@ void PrintingSelectorCardOverlayWidget::updatePinBadgeVisibility() /** * @brief Handles the mouse leave event when the cursor leaves the overlay widget area. * - * When the cursor leaves the widget, the card amount widget is hidden if both the mainboard and sideboard - * amounts are zero. + * When the cursor leaves the widget, the card amount widget is hidden if all of the mainboard, sideboard, and + * tokensboard amounts are zero. * * @param event The event triggered when the mouse leaves the widget. */ diff --git a/cockatrice/src/interface/widgets/printing_selector/printing_selector_card_overlay_widget.h b/cockatrice/src/interface/widgets/printing_selector/printing_selector_card_overlay_widget.h index 0a5ed20b8..228393c9c 100644 --- a/cockatrice/src/interface/widgets/printing_selector/printing_selector_card_overlay_widget.h +++ b/cockatrice/src/interface/widgets/printing_selector/printing_selector_card_overlay_widget.h @@ -35,7 +35,7 @@ signals: void cardPreferenceChanged(); public slots: - void updateCardAmounts(int mainboardAmount, int sideboardAmount); + void updateCardAmounts(int mainboardAmount, int sideboardAmount, int tokensboardAmount); private slots: void updateVisibility(); diff --git a/cockatrice/src/interface/widgets/replay/replay_manager.cpp b/cockatrice/src/interface/widgets/replay/replay_manager.cpp index 525b703db..a1330a82d 100644 --- a/cockatrice/src/interface/widgets/replay/replay_manager.cpp +++ b/cockatrice/src/interface/widgets/replay/replay_manager.cpp @@ -98,8 +98,7 @@ ReplayManager::ReplayManager(TabGame *parent, GameReplay *_replay) void ReplayManager::replayNextEvent(EventProcessingOptions options) { - game->getGame()->getGameEventHandler()->processGameEventContainer( - replay->event_list(timelineWidget->getCurrentEvent()), nullptr, options); + emit eventReplayed(replay->event_list(timelineWidget->getCurrentEvent()), options); } void ReplayManager::replayFinished() diff --git a/cockatrice/src/interface/widgets/replay/replay_manager.h b/cockatrice/src/interface/widgets/replay/replay_manager.h index d67ae5a90..a3e0126c7 100644 --- a/cockatrice/src/interface/widgets/replay/replay_manager.h +++ b/cockatrice/src/interface/widgets/replay/replay_manager.h @@ -27,6 +27,7 @@ public: signals: void requestChatAndPhaseReset(); + void eventReplayed(const GameEventContainer &cont, EventProcessingOptions options); private: // Replay related members diff --git a/cockatrice/src/interface/widgets/server/user/user_avatar_provider.cpp b/cockatrice/src/interface/widgets/server/user/user_avatar_provider.cpp new file mode 100644 index 000000000..c115caa47 --- /dev/null +++ b/cockatrice/src/interface/widgets/server/user/user_avatar_provider.cpp @@ -0,0 +1,48 @@ +#include "user_avatar_provider.h" + +#include +#include +#include + +UserAvatarProvider::UserAvatarProvider(AbstractClient *client, QObject *parent) : QObject(parent), client(client) +{ +} + +const QMap &UserAvatarProvider::cache() const +{ + return avatarCache; +} + +void UserAvatarProvider::requestAvatar(const QString &userName) +{ + if (avatarCache.contains(userName) || pending.contains(userName)) { + return; + } + + pending.insert(userName); + + Command_GetUserInfo cmd; + cmd.set_user_name(userName.toStdString()); + + PendingCommand *pend = client->prepareSessionCommand(cmd); + + connect(pend, &PendingCommand::finished, this, [this, userName](const Response &r) { + pending.remove(userName); + + const auto &response = r.GetExtension(Response_GetUserInfo::ext); + const auto &user = response.user_info(); + const std::string &bmp = user.avatar_bmp(); + + QPixmap avatar; + if (!bmp.empty() && + avatar.loadFromData(reinterpret_cast(bmp.data()), static_cast(bmp.size()))) { + avatarCache.insert(userName, avatar); + } else { + avatarCache.insert(userName, QPixmap()); + } + + emit avatarUpdated(userName); + }); + + client->sendCommand(pend); +} \ No newline at end of file diff --git a/cockatrice/src/interface/widgets/server/user/user_avatar_provider.h b/cockatrice/src/interface/widgets/server/user/user_avatar_provider.h new file mode 100644 index 000000000..44491e544 --- /dev/null +++ b/cockatrice/src/interface/widgets/server/user/user_avatar_provider.h @@ -0,0 +1,30 @@ +#ifndef COCKATRICE_USER_AVATAR_PROVIDER_H +#define COCKATRICE_USER_AVATAR_PROVIDER_H + +#include +#include +#include +#include + +class AbstractClient; + +class UserAvatarProvider : public QObject +{ + Q_OBJECT + +public: + explicit UserAvatarProvider(AbstractClient *client, QObject *parent = nullptr); + + void requestAvatar(const QString &userName); + const QMap &cache() const; + +signals: + void avatarUpdated(const QString &userName); + +private: + AbstractClient *client; + QMap avatarCache; + QSet pending; +}; + +#endif // COCKATRICE_USER_AVATAR_PROVIDER_H \ No newline at end of file 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 new file mode 100644 index 000000000..67fb4f684 --- /dev/null +++ b/cockatrice/src/interface/widgets/server/user/user_card_art_provider.cpp @@ -0,0 +1,147 @@ +#include "user_card_art_provider.h" + +#include "../../../card_picture_loader/card_picture_loader.h" + +#include +#include + +static QString makeKey(const QString &user, const QString &card, const QString &providerId) +{ + return user + u'|' + card + u'|' + providerId; +} + +UserCardArtProvider::UserCardArtProvider(QObject *parent) : QObject(parent) +{ + dbReady = (CardDatabaseManager::getInstance()->getLoadStatus() == LoadStatus::Ok); + + if (!dbReady) { + connect(CardDatabaseManager::getInstance(), &CardDatabase::cardDatabaseLoadingFinished, this, + &UserCardArtProvider::onDatabaseReady); + } +} + +void UserCardArtProvider::onDatabaseReady() +{ + dbReady = true; + processQueue(); +} + +const QMap &UserCardArtProvider::cache() const +{ + return cardArtCache; +} + +void UserCardArtProvider::requestCardArt(const QString &userName, const QString &cardName, const QString &providerId) +{ + if (cardName.isEmpty()) { + return; + } + + const QString key = makeKey(userName, cardName, providerId); + + if (cardArtCache.contains(key) || pending.contains(key)) { + return; + } + + pending.insert(key); + queue.enqueue(key); + + processQueue(); +} + +QPixmap UserCardArtProvider::cropCardArt(const QPixmap &fullRes) +{ + const QSize sz = fullRes.size(); + const int marginX = sz.width() * 0.07; + const int topMargin = sz.height() * 0.11; + const int bottomMargin = sz.height() * 0.45; + + const QRect foilRect(marginX, topMargin, sz.width() - 2 * marginX, sz.height() - topMargin - bottomMargin); + + return fullRes.copy(foilRect.intersected(fullRes.rect())); +} + +void UserCardArtProvider::insertIntoCache(const QString &key, const QPixmap &pixmap) +{ + if (!cardArtCache.contains(key)) { + cacheInsertionOrder.append(key); + while (cacheInsertionOrder.size() > MaxCacheEntries) { + const QString evicted = cacheInsertionOrder.takeFirst(); + cardArtCache.remove(evicted); + } + } + cardArtCache.insert(key, pixmap); +} + +void UserCardArtProvider::processQueue() +{ + if (!dbReady) { + return; + } + + while (!queue.isEmpty()) { + const QString key = queue.dequeue(); + + const QStringList parts = key.split(u'|'); + if (parts.size() != 3) { + pending.remove(key); + continue; + } + + const QString userName = parts.at(0); + const QString cardName = parts.at(1); + const QString providerId = parts.at(2); + + ExactCard card = CardDatabaseManager::query()->getCard({cardName, providerId}); + + if (!card) { + pending.remove(key); + continue; + } + + QPixmap fullRes; + CardPictureLoader::getPixmap(fullRes, card, QSize(745, 1040)); + + // Synchronous hit (already loaded/on disk) + if (!fullRes.isNull()) { + insertIntoCache(key, cropCardArt(fullRes)); + pending.remove(key); + + emit cardArtUpdated(userName); + continue; + } + + // Async load required. + QPointer self(this); + + auto conn = std::make_shared(); + + *conn = connect(card.getCardPtr().data(), &CardInfo::pixmapUpdated, this, + [self, key, userName, card, conn]() mutable { + if (!self) { + return; + } + + QObject::disconnect(*conn); + + QPixmap fullRes; + CardPictureLoader::getPixmap(fullRes, card, QSize(745, 1040)); + + if (!fullRes.isNull()) { + self->insertIntoCache(key, self->cropCardArt(fullRes)); + } else { + self->insertIntoCache(key, QPixmap()); + } + + self->pending.remove(key); + + emit self->cardArtUpdated(userName); + + // Resume processing remaining queued items. + self->processQueue(); + }); + + // Stop here. We'll continue when the async load finishes. + return; + } +} \ No newline at end of file diff --git a/cockatrice/src/interface/widgets/server/user/user_card_art_provider.h b/cockatrice/src/interface/widgets/server/user/user_card_art_provider.h new file mode 100644 index 000000000..fb2f37812 --- /dev/null +++ b/cockatrice/src/interface/widgets/server/user/user_card_art_provider.h @@ -0,0 +1,39 @@ +#ifndef COCKATRICE_USER_CARD_ART_PROVIDER_H +#define COCKATRICE_USER_CARD_ART_PROVIDER_H + +#include +#include +#include +#include +#include + +class UserCardArtProvider : public QObject +{ + Q_OBJECT + +public: + explicit UserCardArtProvider(QObject *parent = nullptr); + + void requestCardArt(const QString &userName, const QString &cardName, const QString &providerId); + const QMap &cache() const; + static QPixmap cropCardArt(const QPixmap &fullRes); + +signals: + void cardArtUpdated(const QString &userName); + +public slots: + void onDatabaseReady(); + +private: + bool dbReady = false; + static constexpr int MaxCacheEntries = 300; + QList cacheInsertionOrder; // FIFO eviction + QMap cardArtCache; + QSet pending; + QQueue queue; + + void processQueue(); + void insertIntoCache(const QString &key, const QPixmap &pixmap); +}; + +#endif // COCKATRICE_USER_CARD_ART_PROVIDER_H \ No newline at end of file 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 new file mode 100644 index 000000000..56c9600a0 --- /dev/null +++ b/cockatrice/src/interface/widgets/server/user/user_card_settings_dialog.cpp @@ -0,0 +1,339 @@ +#include "user_card_settings_dialog.h" + +#include "../../../card_picture_loader/card_picture_loader.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 "user_card_art_provider.h" +#include "user_list_painter.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +CardArtPreviewWidget::CardArtPreviewWidget(QWidget *parent) : QWidget(parent) +{ + setMinimumSize(400, 72); + setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); +} + +void CardArtPreviewWidget::setPixmap(const QPixmap &pixmap) +{ + sourcePixmap = pixmap; + update(); +} + +void CardArtPreviewWidget::setParams(const CardArtParams &p) +{ + params = p; + update(); +} + +void CardArtPreviewWidget::paintEvent(QPaintEvent *) +{ + QPainter painter(this); + painter.setRenderHints(QPainter::Antialiasing | QPainter::SmoothPixmapTransform | QPainter::TextAntialiasing); + + const QRect rect = this->rect(); + + const QColor accentColor(100, 116, 139); + 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; + } + + UserListPainter::drawCardArt(&painter, rect, rect.right() - 4, + QString(), // userName not needed for override path + nullptr, // no cache + params, + &sourcePixmap // direct pixmap + ); + + // 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); + + QPainterPath clip; + clip.addEllipse(avatarRect); + painter.save(); + painter.setClipPath(clip); + painter.setBrush(accentColor.darker(200)); + painter.setPen(Qt::NoPen); + painter.drawEllipse(avatarRect); + painter.restore(); + + painter.setPen(QPen(QColor(70, 80, 95), 2)); + painter.setBrush(Qt::NoBrush); + painter.drawEllipse(avatarRect.adjusted(-1, -1, 1, 1)); +} + +UserCardArtSettingsDialog::UserCardArtSettingsDialog(const CardArtParams &initial, QWidget *parent) + : QDialog(parent), currentParams(initial) +{ + setWindowTitle(tr("Card Art Settings")); + setMinimumWidth(500); + setupUi(); + + // Seed UI from initial params + if (!initial.cardName.isEmpty()) { + searchBar->setText(initial.cardName); + onCardNameChanged(initial.cardName); + } + marginLSpin->setValue(initial.marginPctL); + marginRSpin->setValue(initial.marginPctR); + verticalOffsetSpin->setValue(initial.verticalOffset); + zoomSpin->setValue(initial.zoom); +} + +CardArtParams UserCardArtSettingsDialog::params() const +{ + return currentParams; +} + +QDoubleSpinBox *UserCardArtSettingsDialog::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 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); + + 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); + + 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) { + if (searchBar->text() != completion) { + searchBar->setText(completion); + searchBar->setCursorPosition(searchBar->text().length()); + } + onCardNameChanged(completion); + }); + + // Also trigger a load when the user hits Return on a typed name + connect(searchBar, &QLineEdit::returnPressed, this, [this]() { onCardNameChanged(searchBar->text()); }); +} + +void UserCardArtSettingsDialog::setupUi() +{ + initializeSearchBar(); + + providerComboBox = new QComboBox; + connect(providerComboBox, &QComboBox::currentIndexChanged, this, [this]() { + currentParams.cardProviderId = providerComboBox->currentData().toString(); + reloadPreview(); + onParamChanged(); + }); + + 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; + 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); + + auto *controlsGroup = new QGroupBox(tr("Parameters")); + controlsGroup->setLayout(form); + + preview = new CardArtPreviewWidget; + + auto *previewLayout = new QVBoxLayout; + previewLayout->addWidget(preview); + auto *previewGroup = new QGroupBox(tr("Preview")); + previewGroup->setLayout(previewLayout); + + auto *buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); + auto *removeBtn = new QPushButton(tr("Remove Banner Card")); + buttons->addButton(removeBtn, QDialogButtonBox::ResetRole); + + connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept); + connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject); + connect(removeBtn, &QPushButton::clicked, this, [this]() { + currentParams = CardArtParams{}; // empty cardName signals removal + accept(); + }); + + auto *root = new QVBoxLayout; + 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); + connect(verticalOffsetSpin, &QDoubleSpinBox::valueChanged, this, &UserCardArtSettingsDialog::onParamChanged); + connect(zoomSpin, &QDoubleSpinBox::valueChanged, this, &UserCardArtSettingsDialog::onParamChanged); +} + +void UserCardArtSettingsDialog::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 UserCardArtSettingsDialog::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; + } + + currentParams.cardName = name; + + populateProviderCombo(name); + + if (providerComboBox->count() == 0) { + // No printings found for this card; nothing to preview. + currentPixmap = QPixmap(); + preview->setPixmap(currentPixmap); + currentParams.cardProviderId.clear(); + return; + } + + currentParams.cardProviderId = providerComboBox->currentData().toString(); + reloadPreview(); +} + +void UserCardArtSettingsDialog::reloadPreview() +{ + if (currentParams.cardName.isEmpty()) { + return; + } + + ExactCard card = CardDatabaseManager::query()->getCard({currentParams.cardName, currentParams.cardProviderId}); + if (!card) { + return; + } + + // CardPictureLoader::getPixmap() is async on a cache miss: it enqueues a + // background download and returns a null pixmap immediately. When that + // download finishes, CardPictureLoader::imageLoaded() caches the result + // and calls card.emitPixmapUpdated(), which emits pixmapUpdated() on the + // underlying CardInfo (see exact_card.h). Listen for that, scoped to + // 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 + // repeatedly stacks up connections to old CardInfo objects, each of + // which would still fire reloadPreview() (harmlessly, but wastefully) + // whenever ITS art finishes loading later. + disconnect(pixmapUpdatedConnection); + + QPixmap fullRes; + 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); + // .data() gives the raw QObject* needed for connect(). + CardInfo *cardInfo = card.getCardPtr().data(); + if (cardInfo) { + pixmapUpdatedConnection = connect(cardInfo, &CardInfo::pixmapUpdated, this, [this]() { reloadPreview(); }); + } + return; + } + + currentPixmap = UserCardArtProvider::cropCardArt(fullRes); + preview->setPixmap(currentPixmap); + preview->setParams(currentParams); +} + +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 new file mode 100644 index 000000000..018043278 --- /dev/null +++ b/cockatrice/src/interface/widgets/server/user/user_card_settings_dialog.h @@ -0,0 +1,77 @@ +#ifndef COCKATRICE_USER_CARD_ART_SETTINGS_DIALOG_H +#define COCKATRICE_USER_CARD_ART_SETTINGS_DIALOG_H + +#include "user_list_painter.h" + +#include +#include +#include + +class QCompleter; +class QLineEdit; +class QDoubleSpinBox; +class CardDatabaseModel; +class CardDatabaseDisplayModel; +class CardSearchModel; +class CardCompleterProxyModel; + +class CardArtPreviewWidget : public QWidget +{ + Q_OBJECT + +public: + explicit CardArtPreviewWidget(QWidget *parent = nullptr); + + void setPixmap(const QPixmap &pixmap); + void setParams(const CardArtParams ¶ms); + +protected: + void paintEvent(QPaintEvent *event) override; + +private: + QPixmap sourcePixmap; + CardArtParams params; +}; + +class UserCardArtSettingsDialog : public QDialog +{ + Q_OBJECT + +public: + explicit UserCardArtSettingsDialog(const CardArtParams &initial = {}, QWidget *parent = nullptr); + + CardArtParams params() const; + +private slots: + void onCardNameChanged(const QString &name); + void reloadPreview(); + void onParamChanged(); + +private: + void setupUi(); + void populateProviderCombo(const QString &cardName); + void initializeSearchBar(); + 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; + + QDoubleSpinBox *marginLSpin; + QDoubleSpinBox *marginRSpin; + QDoubleSpinBox *verticalOffsetSpin; + QDoubleSpinBox *zoomSpin; + CardArtPreviewWidget *preview; + + QPixmap currentPixmap; + CardArtParams currentParams; +}; + +#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 195b1cc8d..11fd02d80 100644 --- a/cockatrice/src/interface/widgets/server/user/user_context_menu.cpp +++ b/cockatrice/src/interface/widgets/server/user/user_context_menu.cpp @@ -476,10 +476,15 @@ void UserContextMenu::showContextMenu(const QPoint &pos, client->sendCommand(client->prepareSessionCommand(cmd)); } else if (actionClicked == aKick) { - Command_KickFromGame cmd; - cmd.set_player_id(playerId); + auto result = QMessageBox::question(static_cast(parent()), tr("Kick Player"), + tr("Are you sure you want to kick this player from the game?"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + if (result == QMessageBox::Yes) { + Command_KickFromGame cmd; + cmd.set_player_id(playerId); - game->getGameEventHandler()->sendGameCommand(cmd); + game->getGameEventHandler()->sendGameCommand(cmd); + } } else if (actionClicked == aBan) { Command_GetUserInfo cmd; cmd.set_user_name(userName.toStdString()); @@ -537,3 +542,113 @@ void UserContextMenu::showContextMenu(const QPoint &pos, delete menu; } + +void UserContextMenu::execChat(const QString &userName) +{ + emit openMessageDialog(userName, true); +} + +void UserContextMenu::execDetails(const QString &userName) +{ + auto *w = new UserInfoBox(client, false, static_cast(parent()), + Qt::Dialog | Qt::WindowTitleHint | Qt::CustomizeWindowHint | Qt::WindowCloseButtonHint); + w->setAttribute(Qt::WA_DeleteOnClose); + w->updateInfo(userName); +} + +void UserContextMenu::execShowGames(const QString &userName) +{ + Command_GetGamesOfUser cmd; + cmd.set_user_name(userName.toStdString()); + PendingCommand *pend = client->prepareSessionCommand(cmd); + connect(pend, &PendingCommand::finished, this, &UserContextMenu::gamesOfUserReceived); + client->sendCommand(pend); +} + +void UserContextMenu::execAddToBuddy(const QString &userName) +{ + Command_AddToList cmd; + cmd.set_list("buddy"); + cmd.set_user_name(userName.toStdString()); + client->sendCommand(client->prepareSessionCommand(cmd)); +} + +void UserContextMenu::execRemoveFromBuddy(const QString &userName) +{ + Command_RemoveFromList cmd; + cmd.set_list("buddy"); + cmd.set_user_name(userName.toStdString()); + client->sendCommand(client->prepareSessionCommand(cmd)); +} + +void UserContextMenu::execAddToIgnore(const QString &userName) +{ + Command_AddToList cmd; + cmd.set_list("ignore"); + cmd.set_user_name(userName.toStdString()); + client->sendCommand(client->prepareSessionCommand(cmd)); +} + +void UserContextMenu::execRemoveFromIgnore(const QString &userName) +{ + Command_RemoveFromList cmd; + cmd.set_list("ignore"); + cmd.set_user_name(userName.toStdString()); + client->sendCommand(client->prepareSessionCommand(cmd)); +} + +void UserContextMenu::execBan(const QString &userName) +{ + Command_GetUserInfo cmd; + cmd.set_user_name(userName.toStdString()); + PendingCommand *pend = client->prepareSessionCommand(cmd); + connect(pend, &PendingCommand::finished, this, &UserContextMenu::banUser_processUserInfoResponse); + client->sendCommand(pend); +} + +void UserContextMenu::execWarn(const QString &userName) +{ + Command_GetUserInfo cmd; + cmd.set_user_name(userName.toStdString()); + PendingCommand *pend = client->prepareSessionCommand(cmd); + connect(pend, &PendingCommand::finished, this, &UserContextMenu::warnUser_processUserInfoResponse); + client->sendCommand(pend); +} + +void UserContextMenu::execBanHistory(const QString &userName) +{ + Command_GetBanHistory cmd; + cmd.set_user_name(userName.toStdString()); + PendingCommand *pend = client->prepareModeratorCommand(cmd); + connect(pend, &PendingCommand::finished, this, &UserContextMenu::banUserHistory_processResponse); + client->sendCommand(pend); +} + +void UserContextMenu::execWarnHistory(const QString &userName) +{ + Command_GetWarnHistory cmd; + cmd.set_user_name(userName.toStdString()); + PendingCommand *pend = client->prepareModeratorCommand(cmd); + connect(pend, &PendingCommand::finished, this, &UserContextMenu::warnUserHistory_processResponse); + client->sendCommand(pend); +} + +void UserContextMenu::execAdminNotes(const QString &userName) +{ + Command_GetAdminNotes cmd; + cmd.set_user_name(userName.toStdString()); + auto *pend = client->prepareModeratorCommand(cmd); + connect(pend, &PendingCommand::finished, this, &UserContextMenu::getAdminNotes_processResponse); + client->sendCommand(pend); +} + +void UserContextMenu::execAdjustMod(const QString &userName, bool shouldBeMod, bool shouldBeJudge) +{ + Command_AdjustMod cmd; + cmd.set_user_name(userName.toStdString()); + cmd.set_should_be_mod(shouldBeMod); + cmd.set_should_be_judge(shouldBeJudge); + PendingCommand *pend = client->prepareAdminCommand(cmd); + connect(pend, &PendingCommand::finished, this, &UserContextMenu::adjustMod_processUserResponse); + client->sendCommand(pend); +} \ No newline at end of file diff --git a/cockatrice/src/interface/widgets/server/user/user_context_menu.h b/cockatrice/src/interface/widgets/server/user/user_context_menu.h index b0ff89816..28173bfbc 100644 --- a/cockatrice/src/interface/widgets/server/user/user_context_menu.h +++ b/cockatrice/src/interface/widgets/server/user/user_context_menu.h @@ -74,6 +74,27 @@ public: int playerId, const QString &deckHash, ChatView *chatView = nullptr); + + const UserListProxy *getUserListProxy() const + { + return userListProxy; + } + + // Individual action entry points — used by UserInfoPopup to trigger + // actions without re-running the full context menu flow. + void execChat(const QString &userName); + void execDetails(const QString &userName); + void execShowGames(const QString &userName); + void execAddToBuddy(const QString &userName); + void execRemoveFromBuddy(const QString &userName); + void execAddToIgnore(const QString &userName); + void execRemoveFromIgnore(const QString &userName); + void execBan(const QString &userName); + void execWarn(const QString &userName); + void execBanHistory(const QString &userName); + void execWarnHistory(const QString &userName); + void execAdminNotes(const QString &userName); + void execAdjustMod(const QString &userName, bool shouldBeMod, bool shouldBeJudge); }; #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 a9955ff3d..416cd42e3 100644 --- a/cockatrice/src/interface/widgets/server/user/user_info_box.cpp +++ b/cockatrice/src/interface/widgets/server/user/user_info_box.cpp @@ -5,6 +5,7 @@ #include "../../interface/widgets/dialogs/dlg_edit_password.h" #include "../../interface/widgets/dialogs/dlg_edit_user.h" #include "../../interface/widgets/utility/get_text_with_max.h" +#include "user_card_settings_dialog.h" #include #include @@ -61,11 +62,13 @@ UserInfoBox::UserInfoBox(AbstractClient *_client, bool _editable, QWidget *paren buttonsLayout->addWidget(&editButton); buttonsLayout->addWidget(&passwordButton); buttonsLayout->addWidget(&avatarButton); + buttonsLayout->addWidget(&bannerCardButton); mainLayout->addLayout(buttonsLayout, 7, 0, 1, 3); connect(&editButton, &QPushButton::clicked, this, &UserInfoBox::actEdit); connect(&passwordButton, &QPushButton::clicked, this, &UserInfoBox::actPassword); connect(&avatarButton, &QPushButton::clicked, this, &UserInfoBox::actAvatar); + connect(&bannerCardButton, &QPushButton::clicked, this, &UserInfoBox::actBannerCard); } setWindowTitle(tr("User Information")); @@ -83,26 +86,21 @@ void UserInfoBox::retranslateUi() editButton.setText(tr("Edit")); passwordButton.setText(tr("Change password")); avatarButton.setText(tr("Change avatar")); -} - -/** - * Creates the default profile pic that is used when the user doesn't have a custom pic - */ -static QPixmap createDefaultAvatar(int height, const ServerInfo_User &user) -{ - return UserLevelPixmapGenerator::generatePixmap(height, UserLevelFlags(user.user_level()), user.pawn_colors(), - false, QString::fromStdString(user.privlevel())); + bannerCardButton.setText(tr("Edit Banner Card")); } void UserInfoBox::updateInfo(const ServerInfo_User &user) { - currentUserInfo = &user; + currentUserInfo = user; + hasUserInfo = true; const UserLevelFlags userLevel(user.user_level()); + pawnColors = user.pawn_colors(); + privLevel = QString::fromStdString(user.privlevel()); const std::string &bmp = user.avatar_bmp(); if (!avatarPixmap.loadFromData((const uchar *)bmp.data(), static_cast(bmp.size()))) { - avatarPixmap = createDefaultAvatar(64, user); + avatarPixmap = UserLevelPixmapGenerator::generatePixmap(64, userLevel, pawnColors, false, privLevel); hasAvatar = false; } else { hasAvatar = true; @@ -120,8 +118,7 @@ void UserInfoBox::updateInfo(const ServerInfo_User &user) countryLabel3.setText(""); } - userLevelIcon.setPixmap(UserLevelPixmapGenerator::generatePixmap(15, userLevel, user.pawn_colors(), false, - QString::fromStdString(user.privlevel()))); + userLevelIcon.setPixmap(UserLevelPixmapGenerator::generatePixmap(15, userLevel, pawnColors, false, privLevel)); QString userLevelText; if (userLevel.testFlag(ServerInfo_User::IsAdmin)) { userLevelText = tr("Administrator"); @@ -316,6 +313,49 @@ void UserInfoBox::actAvatar() client->sendCommand(pend); } +void UserInfoBox::actBannerCard() +{ + CardArtParams initial; + if (hasUserInfo && currentUserInfo.has_card_art_params()) { + const auto &cap = currentUserInfo.card_art_params(); + initial.cardName = QString::fromStdString(cap.card_name()); + initial.marginPctL = cap.margin_pct_l(); + initial.marginPctR = cap.margin_pct_r(); + initial.verticalOffset = cap.vertical_offset(); + initial.zoom = cap.zoom(); + } + + UserCardArtSettingsDialog dlg(initial, this); + if (dlg.exec() != QDialog::Accepted) { + return; + } + + const CardArtParams p = dlg.params(); + + Command_SetCardArtParams cmd; + cmd.set_card_name(p.cardName.toStdString()); + if (!p.cardName.isEmpty()) { + cmd.set_card_provider_id(p.cardProviderId.toStdString()); + cmd.set_margin_pct_l(p.marginPctL); + cmd.set_margin_pct_r(p.marginPctR); + cmd.set_vertical_offset(p.verticalOffset); + cmd.set_zoom(p.zoom); + } + + PendingCommand *pend = client->prepareSessionCommand(cmd); + connect(pend, &PendingCommand::finished, this, [p, this](const Response &r) { + if (r.response_code() != Response::RespOk) { + QMessageBox::critical(this, tr("Error"), + tr("The selected card is blacklisted on this server or another error occurred.")); + } else { + updateInfo(nameLabel.text()); // re-fetch so currentUserInfo reflects the change + QMessageBox::information(this, tr("Information"), + p.cardName.isEmpty() ? tr("Banner card removed.") : tr("Banner card updated.")); + } + }); + client->sendCommand(pend); +} + void UserInfoBox::processEditResponse(const Response &r) { switch (r.response_code()) { @@ -373,7 +413,7 @@ void UserInfoBox::processAvatarResponse(const Response &r) break; case Response::RespInternalError: default: - QMessageBox::critical(this, tr("Error"), tr("An error occured while trying to updater your avatar.")); + QMessageBox::critical(this, tr("Error"), tr("An error occured while trying to update your avatar.")); break; } } @@ -385,7 +425,7 @@ void UserInfoBox::resizeEvent(QResizeEvent *event) resizedPixmap = avatarPixmap.scaled(avatarPic.size(), Qt::KeepAspectRatio, Qt::SmoothTransformation); } else { int height = qMin(avatarPic.size().width(), avatarPic.size().height()); - resizedPixmap = createDefaultAvatar(height, *currentUserInfo); + resizedPixmap = UserLevelPixmapGenerator::generatePixmap(height, userLevel, pawnColors, false, privLevel); } avatarPic.setPixmap(resizedPixmap); diff --git a/cockatrice/src/interface/widgets/server/user/user_info_box.h b/cockatrice/src/interface/widgets/server/user/user_info_box.h index 299deed2f..955cb9d3d 100644 --- a/cockatrice/src/interface/widgets/server/user/user_info_box.h +++ b/cockatrice/src/interface/widgets/server/user/user_info_box.h @@ -11,8 +11,10 @@ #include #include #include +#include +#include +#include -class ServerInfo_User; class AbstractClient; class Response; @@ -24,10 +26,14 @@ private: bool editable; QLabel avatarPic, userLevelIcon, nameLabel, realNameLabel1, realNameLabel2, countryLabel1, countryLabel2, countryLabel3, userLevelLabel1, userLevelLabel2, accountAgeLabel1, accountAgeLabel2; - QPushButton editButton, passwordButton, avatarButton; + QPushButton editButton, passwordButton, avatarButton, bannerCardButton; QPixmap avatarPixmap; bool hasAvatar; - const ServerInfo_User *currentUserInfo; + ServerInfo_User currentUserInfo; + bool hasUserInfo = false; + UserLevelFlags userLevel; + ServerInfo_User::PawnColorsOverride pawnColors; + QString privLevel; static QString getAgeString(int ageSeconds); @@ -35,12 +41,6 @@ public: UserInfoBox(AbstractClient *_client, bool editable, QWidget *parent = nullptr, Qt::WindowFlags flags = {}); void retranslateUi(); - inline static QPair getDaysAndYearsBetween(const QDate &then, const QDate &now) - { - int years = now.addDays(1 - then.dayOfYear()).year() - then.year(); // there is no yearsTo - int days = then.addYears(years).daysTo(now); - return {days, years}; - } private slots: void processResponse(const Response &r); void processEditResponse(const Response &r); @@ -51,6 +51,7 @@ private slots: void actEditInternal(const Response &r); void actPassword(); void actAvatar(); + void actBannerCard(); public slots: void updateInfo(const ServerInfo_User &user); void updateInfo(const QString &userName); diff --git a/cockatrice/src/interface/widgets/server/user/user_info_popup.cpp b/cockatrice/src/interface/widgets/server/user/user_info_popup.cpp new file mode 100644 index 000000000..2b4dcb8ed --- /dev/null +++ b/cockatrice/src/interface/widgets/server/user/user_info_popup.cpp @@ -0,0 +1,656 @@ +#include "user_info_popup.h" + +#include "../../interface/pixel_map_generator.h" +#include "../../interface/widgets/tabs/tab_supervisor.h" +#include "user_list_painter.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// ── Compact game row delegate ───────────────────────────────────────────────── + +class PopupGameDelegate : public QStyledItemDelegate +{ +public: + using QStyledItemDelegate::QStyledItemDelegate; + + QSize sizeHint(const QStyleOptionViewItem &, const QModelIndex &) const override + { + return QSize(0, 38); + } + + void paint(QPainter *p, const QStyleOptionViewItem &option, const QModelIndex &index) const override + { + const QVariant var = index.data(PopupRoles::GameData); + if (!var.isValid()) { + QStyledItemDelegate::paint(p, option, index); + return; + } + + p->save(); + p->setRenderHints(QPainter::Antialiasing | QPainter::TextAntialiasing); + + const QRect rect = option.rect; + const ServerInfo_Game game = var.value(); + const bool selected = option.state & QStyle::State_Selected; + + p->fillRect(rect, selected ? QColor(35, 45, 62) : QColor(14, 18, 26)); + + // State colour dot + const QColor dot = game.started() ? QColor(239, 68, 68) + : (game.player_count() >= game.max_players()) ? QColor(249, 115, 22) + : game.with_password() ? QColor(59, 130, 246) + : QColor(34, 197, 94); + p->setPen(Qt::NoPen); + p->setBrush(dot); + p->drawEllipse(QRectF(rect.left() + 9, rect.top() + (rect.height() - 8) / 2.0, 8, 8)); + + // Game title (bold, elided) + QFont tf = option.font; + tf.setBold(true); + p->setFont(tf); + p->setPen(QColor(205, 215, 230)); + const int textX = rect.left() + 26; + const int countW = 52; + const int titleW = rect.width() - textX - countW - 6; + p->drawText(QRect(textX, rect.top(), titleW, rect.height()), Qt::AlignVCenter | Qt::AlignLeft, + QFontMetrics(tf).elidedText(QString::fromStdString(game.description()), Qt::ElideRight, titleW)); + + // Player count + const bool full = game.player_count() >= game.max_players(); + p->setFont(option.font); + 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 + p->setPen(QColor(24, 32, 44)); + p->drawLine(rect.bottomLeft(), rect.bottomRight()); + + p->restore(); + } +}; + +// ── UserInfoHeaderWidget ────────────────────────────────────────────────────── + +UserInfoHeaderWidget::UserInfoHeaderWidget(QWidget *parent) : QWidget(parent) +{ + setFixedHeight(HeaderHeight); + setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); +} + +void UserInfoHeaderWidget::setUserData(const ServerInfo_User &user, + bool online, + const QPixmap &avatar, + const QPixmap &cardArt, + const CardArtParams ¶ms) +{ + m_user = user; + m_online = online; + m_avatar = avatar; + m_cardArt = cardArt; + m_params = params; + update(); +} + +void UserInfoHeaderWidget::paintEvent(QPaintEvent *) +{ + QPainter p(this); + p.setRenderHints(QPainter::Antialiasing | QPainter::SmoothPixmapTransform | QPainter::TextAntialiasing); + + const QRect rect = this->rect(); + const UserLevelFlags level(m_user.user_level()); + const QString userName = QString::fromStdString(m_user.name()); + const QString privLevel = QString::fromStdString(m_user.privlevel()); + + // Dark base + p.fillRect(rect, QColor(14, 18, 26)); + + // ── Card art background ─────────────────────────────────────────────────── + if (!m_cardArt.isNull()) { + const int w = rect.width(); + const int h = rect.height(); + 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) / 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 = 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(srcX, srcY, dW, h).toImage().convertToFormat(QImage::Format_ARGB32_Premultiplied); + { + QPainter mask(&img); + mask.setCompositionMode(QPainter::CompositionMode_DestinationIn); + QLinearGradient g(0, 0, img.width(), 0); + g.setColorAt(0.00, Qt::transparent); + g.setColorAt(0.18, Qt::white); + g.setColorAt(0.82, Qt::white); + g.setColorAt(1.00, Qt::transparent); + mask.fillRect(img.rect(), g); + } + p.setOpacity(0.48); + p.drawImage(mL, 0, img); + p.setOpacity(1.0); + } + + // Bottom gradient overlay so avatar and text are always legible + { + QLinearGradient ov(0, 0, 0, rect.height()); + 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); + } + + // ── Avatar ──────────────────────────────────────────────────────────────── + const QColor accent = [&]() -> QColor { + if (level.testFlag(ServerInfo_User::IsAdmin)) { + return QColor(245, 158, 11); + } + if (level.testFlag(ServerInfo_User::IsModerator)) { + return QColor(59, 130, 246); + } + if (level.testFlag(ServerInfo_User::IsJudge)) { + return QColor(168, 85, 247); + } + return QColor(100, 116, 139); + }(); + + const int ax = LeftPad; + const int ay = rect.height() - AvatarSize - 10; + const QRect ar(ax, ay, AvatarSize, AvatarSize); + + QPainterPath clip; + clip.addEllipse(ar); + p.save(); + p.setClipPath(clip); + + if (!m_avatar.isNull()) { + p.drawPixmap(ar, m_avatar.scaled(ar.size(), Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation)); + } else { + p.setPen(Qt::NoPen); + p.setBrush(accent.darker(200)); + p.drawEllipse(ar); + const QPixmap pawn = + 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(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)); + + // ── Username + badge ────────────────────────────────────────────────────── + const int tx = ax + AvatarSize + AvatarToTextGap; + const int tw = rect.width() - tx - 8; + + QFont nf = font(); + nf.setBold(true); + nf.setPointSizeF(nf.pointSizeF() * 1.12); + p.setFont(nf); + 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)); + + // Level / priv badge + struct + { + QString text; + QColor color; + } badge; + if (level.testFlag(ServerInfo_User::IsAdmin)) { + badge = {"ADMIN", QColor(245, 158, 11)}; + } else if (level.testFlag(ServerInfo_User::IsModerator)) { + badge = {"MOD", QColor(59, 130, 246)}; + } else if (level.testFlag(ServerInfo_User::IsJudge)) { + badge = {"JUDGE", QColor(168, 85, 247)}; + } else if (privLevel == "VIP") { + badge = {"VIP", QColor(20, 184, 166)}; + } else if (privLevel == "DONATOR") { + badge = {"DONATOR", QColor(249, 115, 22)}; + } + + if (!badge.text.isEmpty()) { + QFont bf = font(); + bf.setPointSizeF(bf.pointSizeF() * 0.70); + bf.setBold(true); + p.setFont(bf); + const QFontMetrics bfm(bf); + const int bw = bfm.horizontalAdvance(badge.text) + 10; + const QRect br(tx, ay + AvatarSize / 2 + 6, bw, 15); + p.setPen(Qt::NoPen); + p.setBrush(badge.color.darker(160)); + p.drawRoundedRect(br, 3, 3); + p.setPen(badge.color.lighter(150)); + p.drawText(br, Qt::AlignCenter, badge.text); + } +} + +// ── UserInfoPopup ───────────────────────────────────────────────────────────── + +UserInfoPopup::UserInfoPopup(TabSupervisor *ts, + AbstractClient *client, + const QMap *avatarCache, + const QMap *cardArtCache, + const QMap *cardArtParamsMap, + QWidget *parent) + : 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(); +} + +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 + m_header = new UserInfoHeaderWidget(this); + root->addWidget(m_header); + + // Action area — rebuilt per user + m_actionArea = new QWidget(this); + m_actionArea->setStyleSheet(QStringLiteral("background:#0e1218;")); + root->addWidget(m_actionArea); + + // Thin 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); + 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(); + 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 + 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 + 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(m_gamesView); + + // 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) +{ + auto *b = new QPushButton(label, p); + b->setToolTip(tip); + b->setFixedHeight(26); + b->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); + b->setStyleSheet(QStringLiteral("QPushButton{" + " background:#192030;color:#b8c8de;border:1px solid #263040;" + " border-radius:4px;font-size:11px;padding:0 4px;" + "}" + "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 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(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(m_actionArea); + grid->setContentsMargins(8, 6, 8, 6); + grid->setSpacing(4); + + int row = 0, col = 0; + const int cols = 3; + auto add = [&](QPushButton *btn) { + grid->addWidget(btn, row, col); + if (++col == cols) { + col = 0; + ++row; + } + }; + + // ── Always visible ──────────────────────────────────────────────────────── + 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"), 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"), m_actionArea); + games->setEnabled(!isSelf && online); + connect(games, &QPushButton::clicked, this, [this, name] { emit showGamesRequested(name); }); + add(games); + + // ── Buddy / ignore (registered users only) ──────────────────────────────── + if (!isSelf && isReg) { + if (isBuddy) { + 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"), 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"), 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"), m_actionArea); + connect(b, &QPushButton::clicked, this, [this, name] { emit addIgnoreRequested(name); }); + add(b); + } + } + + // ── Moderator actions ───────────────────────────────────────────────────── + if (!isSelf && (isMod || isAdmin)) { + if (col != 0) { + ++row; + col = 0; + } // start mod section on a fresh row + + 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); }); + connect(wLog, &QPushButton::clicked, this, [this, name] { emit warnHistoryRequested(name); }); + add(ban); + add(warn); + add(bLog); + add(wLog); + } + + // ── Admin actions ───────────────────────────────────────────────────────── + if (!isSelf && isAdmin) { + 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"), 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"), 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"), 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"), m_actionArea); + connect(b, &QPushButton::clicked, this, [this, name] { emit promoteToJudgeRequested(name); }); + add(b); + } + } + + m_actionArea->adjustSize(); +} + +void UserInfoPopup::updateActionButtons(const ServerInfo_User &userInfo, bool online, bool isBuddy, bool isIgnored) +{ + rebuildActionButtons(userInfo, online, isBuddy, isIgnored); + adjustSize(); +} + +void UserInfoPopup::onGamesContextMenu(const QPoint &pos) +{ + const QModelIndex idx = m_gamesView->indexAt(pos); + if (!idx.isValid()) { + return; + } + + const QVariant var = idx.data(PopupRoles::GameData); + if (!var.isValid()) { + return; + } + const ServerInfo_Game game = var.value(); + + QMenu menu(this); + menu.setStyleSheet( + 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")); + join->setEnabled(canJoin); + + QAction *spec = nullptr; + if (game.spectators_allowed()) { + spec = menu.addAction(tr("Spectate")); + } + + const QAction *chosen = menu.exec(m_gamesView->viewport()->mapToGlobal(pos)); + if (!chosen) { + return; + } + + if (chosen == join) { + emit joinGameRequested(game.game_id(), game.room_id(), false); + } else if (spec && chosen == spec) { + emit joinGameRequested(game.game_id(), game.room_id(), true); + } +} + +// ── showForUser ─────────────────────────────────────────────────────────────── + +void UserInfoPopup::showForUser(const QString &userName, + const ServerInfo_User &userInfo, + bool online, + bool isBuddy, + bool isIgnored) +{ + m_currentUser = userName; + m_currentUserInfo = userInfo; + m_currentOnline = online; + + // Header + 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 + m_gamesModel->clear(); + m_gamesView->hide(); + m_gamesStatus->setText(tr("Loading games…")); + m_gamesStatus->show(); + + // Close button — top-right corner, above everything + m_closeBtn->move(PopupWidth - m_closeBtn->width() - 6, 6); + m_closeBtn->raise(); + + adjustSize(); + fetchGames(); +} + +// ── Games fetch ─────────────────────────────────────────────────────────────── + +void UserInfoPopup::fetchGames() +{ + if (!m_client || m_currentUser.isEmpty()) { + return; + } + + Command_GetGamesOfUser cmd; + cmd.set_user_name(m_currentUser.toStdString()); + + const QString snapshot = m_currentUser; + PendingCommand *pend = m_client->prepareSessionCommand(cmd); + connect(pend, &PendingCommand::finished, this, + [this, snapshot](const Response &r) { onGamesReceived(r, snapshot); }); + m_client->sendCommand(pend); +} + +void UserInfoPopup::onGamesReceived(const Response &r, const QString &forUser) +{ + if (forUser != m_currentUser) { + return; // stale response — different user showing now + } + + m_gamesModel->clear(); + + if (r.response_code() != Response::RespOk) { + 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) { + m_gamesStatus->setText(tr("No active games.")); + m_gamesStatus->show(); + m_gamesView->hide(); + return; + } + + for (int i = 0; i < resp.game_list_size(); ++i) { + auto *item = new QStandardItem; + item->setData(QVariant::fromValue(resp.game_list(i)), PopupRoles::GameData); + item->setEditable(false); + m_gamesModel->appendRow(item); + } + + 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 = m_gamesModel->rowCount(); + const int visible = qMin(count, maxRows); + m_gamesView->setFixedHeight(visible * rowH + 2); + m_gamesView->setVerticalScrollBarPolicy(count > maxRows ? Qt::ScrollBarAlwaysOn : Qt::ScrollBarAlwaysOff); + + adjustSize(); +} + +void UserInfoPopup::refreshGames() +{ + m_gamesModel->clear(); + m_gamesView->hide(); + m_gamesStatus->setText(tr("Loading games…")); + m_gamesStatus->show(); + fetchGames(); +} + +// ── Mouse events ────────────────────────────────────────────────────────────── + +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) +void UserInfoPopup::enterEvent(QEnterEvent *e) +{ + QFrame::enterEvent(e); + emit mouseEnteredPopup(); +} +#else +void UserInfoPopup::enterEvent(QEvent *e) +{ + QFrame::enterEvent(e); + emit mouseEnteredPopup(); +} +#endif +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 new file mode 100644 index 000000000..0e03147c4 --- /dev/null +++ b/cockatrice/src/interface/widgets/server/user/user_info_popup.h @@ -0,0 +1,181 @@ +#ifndef COCKATRICE_USER_INFO_POPUP_H +#define COCKATRICE_USER_INFO_POPUP_H + +#include "../../interface/widgets/server/game_type_map.h" +#include "user_list_painter.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +class AbstractClient; +class QLabel; +class QPushButton; +class TabSupervisor; + +// ── Roles ───────────────────────────────────────────────────────────────────── + +namespace PopupRoles +{ +constexpr int GameData = Qt::UserRole + 10; +} + +// ── Header widget ───────────────────────────────────────────────────────────── + +/** + * @class UserInfoHeaderWidget + * @brief Paints the enlarged banner card art + circular avatar section at the + * top of the UserInfoPopup. + * + * Layout mirrors UserListPainter but at a larger scale: the card art fills the + * full width as a semi-transparent background, a bottom gradient ensures the + * avatar and username text remain legible, and the status ring colour matches + * the UserListPainter convention. + */ +class UserInfoHeaderWidget : public QWidget +{ + Q_OBJECT + + static constexpr int HeaderHeight = 130; + static constexpr int AvatarSize = 68; + static constexpr int AvatarPawnSize = 46; + static constexpr int LeftPad = 14; + static constexpr int AvatarToTextGap = 10; + +public: + explicit UserInfoHeaderWidget(QWidget *parent = nullptr); + + 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 m_user; + bool m_online = false; + QPixmap m_avatar; + QPixmap m_cardArt; + CardArtParams m_params; +}; + +// ── Main popup ──────────────────────────────────────────────────────────────── + +/** + * @class UserInfoPopup + * @brief Floating panel showing an enlarged user card, quick action buttons, + * and a live scrollable games list. + * + * Lifecycle (mirrors DeckEditorDeckDockWidget): + * - showForUser() — populate, position externally, call show() + * - mouseEnteredPopup / mouseLeftPopup — caller manages hide timer + * - closeRequested() — emitted by the internal close button + * + * The popup is a Qt::Tool frameless child so windowOpacity animations and + * move() in screen coordinates work identically to CardInfoPictureEnlargedWidget. + * + * Action signals map 1-to-1 to UserContextMenu::exec*() methods so all action + * logic stays in one place. + */ +class UserInfoPopup : public QFrame +{ + Q_OBJECT + + static constexpr int PopupWidth = 316; + +public: + explicit UserInfoPopup(TabSupervisor *tabSupervisor, + AbstractClient *client, + const QMap *avatarCache, + const QMap *cardArtCache, + const QMap *cardArtParamsMap, + QWidget *parent); + + /** + * Populate the popup for @p userName and kick off a game list fetch. + * Call show() / move() externally after this. + */ + void + showForUser(const QString &userName, const ServerInfo_User &userInfo, bool online, bool isBuddy, bool isIgnored); + void fetchGames(); + + [[nodiscard]] QString currentUser() const + { + 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); + +signals: + void mouseEnteredPopup(); + void mouseLeftPopup(); + void closeRequested(); + + /** Emitted when the user requests joining or spectating a game in the list. */ + void joinGameRequested(int gameId, int roomId, bool asSpectator); + + // ── Action signals — connect to UserContextMenu::exec*() ────────────────── + void chatRequested(const QString &userName); + void detailsRequested(const QString &userName); + void showGamesRequested(const QString &userName); + void addBuddyRequested(const QString &userName); + void removeBuddyRequested(const QString &userName); + void addIgnoreRequested(const QString &userName); + void removeIgnoreRequested(const QString &userName); + void banRequested(const QString &userName); + void warnRequested(const QString &userName); + void banHistoryRequested(const QString &userName); + void warnHistoryRequested(const QString &userName); + void adminNotesRequested(const QString &userName); + void promoteToModRequested(const QString &userName); + void demoteFromModRequested(const QString &userName); + void promoteToJudgeRequested(const QString &userName); + void demoteFromJudgeRequested(const QString &userName); + +protected: +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + void enterEvent(QEnterEvent *e) override; +#else + void enterEvent(QEvent *e) override; +#endif + void leaveEvent(QEvent *e) override; + +private slots: + void refreshGames(); + void onGamesReceived(const Response &r, const QString &forUser); + void onGamesContextMenu(const QPoint &pos); + +private: + void buildUi(); + void rebuildActionButtons(const ServerInfo_User &userInfo, bool online, bool isBuddy, bool isIgnored); + + TabSupervisor *m_ts; + AbstractClient *m_client; + const QMap *m_avatarCache; + const QMap *m_cardArtCache; + const QMap *m_cardArtParamsMap; + + QString m_currentUser; + ServerInfo_User m_currentUserInfo; + bool m_currentOnline = false; + + 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_manager.cpp b/cockatrice/src/interface/widgets/server/user/user_list_manager.cpp index 4bc2c84d6..216420006 100644 --- a/cockatrice/src/interface/widgets/server/user/user_list_manager.cpp +++ b/cockatrice/src/interface/widgets/server/user/user_list_manager.cpp @@ -42,6 +42,9 @@ void UserListManager::handleDisconnect() delete ownUserInfo; ownUserInfo = nullptr; + + // Full rebuild — all lists are gone + emit listReset(); } void UserListManager::setOwnUserInfo(const ServerInfo_User &userInfo) @@ -63,74 +66,77 @@ void UserListManager::processListUsersResponse(const Response &response) const int userListSize = resp.user_list_size(); for (int i = 0; i < userListSize; ++i) { const ServerInfo_User &info = resp.user_list(i); - const QString &userName = QString::fromStdString(info.name()); - onlineUsers.insert(userName, info); + onlineUsers.insert(QString::fromStdString(info.name()), info); } + + // Bulk load complete — widgets rebuild once from the now-populated map + emit listReset(); } void UserListManager::processUserJoinedEvent(const Event_UserJoined &event) { const auto &info = event.user_info(); - const QString &userName = QString::fromStdString(info.name()); - onlineUsers.insert(userName, info); + const QString name = QString::fromStdString(info.name()); + onlineUsers.insert(name, info); + + emit userJoinedOnline(info); } void UserListManager::processUserLeftEvent(const Event_UserLeft &event) { - const auto &userName = QString::fromStdString(event.name()); - onlineUsers.remove(userName); + const QString name = QString::fromStdString(event.name()); + onlineUsers.remove(name); + + emit userLeftOnline(name); } void UserListManager::buddyListReceived(const QList &_buddyList) { for (const auto &user : _buddyList) { - const auto &userName = QString::fromStdString(user.name()); - buddyUsers.insert(userName, user); + buddyUsers.insert(QString::fromStdString(user.name()), user); } + + // Bulk load — one reset covers all newly added entries + emit listReset(); } void UserListManager::ignoreListReceived(const QList &_ignoreList) { for (const auto &user : _ignoreList) { - const auto &userName = QString::fromStdString(user.name()); - ignoredUsers.insert(userName, user); + ignoredUsers.insert(QString::fromStdString(user.name()), user); } + + // Bulk load — one reset covers all newly added entries + emit listReset(); } void UserListManager::processAddToListEvent(const Event_AddToList &event) { const auto &user = event.user_info(); - const auto &userName = QString::fromStdString(user.name()); + const QString userName = QString::fromStdString(user.name()); + const QString listType = QString::fromStdString(event.list_name()); - const auto &userListType = QString::fromStdString(event.list_name()); - - QMap *userMap; - if (userListType == "buddy") { - userMap = &buddyUsers; - } else if (userListType == "ignore") { - userMap = &ignoredUsers; - } else { - return; + if (listType == "buddy") { + buddyUsers.insert(userName, user); + emit addedToBuddyList(user); + } else if (listType == "ignore") { + ignoredUsers.insert(userName, user); + emit addedToIgnoreList(user); } - - userMap->insert(userName, user); } void UserListManager::processRemoveFromListEvent(const Event_RemoveFromList &event) { - const auto &userListType = QString::fromStdString(event.list_name()); - const auto &userName = QString::fromStdString(event.user_name()); + const QString listType = QString::fromStdString(event.list_name()); + const QString userName = QString::fromStdString(event.user_name()); - QMap *userMap; - if (userListType == "buddy") { - userMap = &buddyUsers; - } else if (userListType == "ignore") { - userMap = &ignoredUsers; - } else { - return; + if (listType == "buddy") { + buddyUsers.remove(userName); + emit removedFromBuddyList(userName); + } else if (listType == "ignore") { + ignoredUsers.remove(userName); + emit removedFromIgnoreList(userName); } - - userMap->remove(userName); } bool UserListManager::isOwnUserRegistered() const @@ -155,16 +161,9 @@ bool UserListManager::isUserIgnored(const QString &userName) const const ServerInfo_User *UserListManager::getOnlineUser(const QString &userName) const { - const QString &userNameToMatchLower = userName.toLower(); - - const auto it = - std::find_if(onlineUsers.begin(), onlineUsers.end(), [&userNameToMatchLower](const ServerInfo_User &user) { - return userNameToMatchLower == QString::fromStdString(user.name()).toLower(); - }); - - if (it != onlineUsers.end()) { - return &*it; - } - - return nullptr; + const QString lower = userName.toLower(); + const auto it = std::find_if(onlineUsers.begin(), onlineUsers.end(), [&lower](const ServerInfo_User &user) { + return lower == QString::fromStdString(user.name()).toLower(); + }); + return it != onlineUsers.end() ? &*it : nullptr; } \ No newline at end of file diff --git a/cockatrice/src/interface/widgets/server/user/user_list_manager.h b/cockatrice/src/interface/widgets/server/user/user_list_manager.h index f09284bd0..6238f0799 100644 --- a/cockatrice/src/interface/widgets/server/user/user_list_manager.h +++ b/cockatrice/src/interface/widgets/server/user/user_list_manager.h @@ -47,15 +47,17 @@ public: explicit UserListManager(AbstractClient *_client, QObject *parent = nullptr); ~UserListManager() override; - [[nodiscard]] QMap getAllUsersList() const + [[nodiscard]] const QMap &getAllUsersList() const { return onlineUsers; } - [[nodiscard]] QMap getBuddyList() const + + [[nodiscard]] const QMap &getBuddyList() const { return buddyUsers; } - [[nodiscard]] QMap getIgnoreList() const + + [[nodiscard]] const QMap &getIgnoreList() const { return ignoredUsers; } @@ -71,8 +73,26 @@ public slots: void handleDisconnect(); signals: - void userLeft(const QString &userName); - void userJoined(const ServerInfo_User &userInfo); + /** + * The entire list needs to be rebuilt from scratch. + * Fired on disconnect, reconnect, and initial bulk loads + * (Command_ListUsers response, initial buddy/ignore lists). + */ + void listReset(); + + // ── Online user presence ────────────────────────────────────────────────── + /** A user came online (or joined the room). Full ServerInfo_User available. */ + void userJoinedOnline(const ServerInfo_User &user); + /** A user went offline (or left the room). */ + void userLeftOnline(const QString &userName); + + // ── Buddy list mutations (individual, post-login) ───────────────────────── + void addedToBuddyList(const ServerInfo_User &user); + void removedFromBuddyList(const QString &userName); + + // ── Ignore list mutations (individual, post-login) ──────────────────────── + void addedToIgnoreList(const ServerInfo_User &user); + void removedFromIgnoreList(const QString &userName); }; #endif // COCKATRICE_USER_LIST_MANAGER_H diff --git a/cockatrice/src/interface/widgets/server/user/user_list_painter.cpp b/cockatrice/src/interface/widgets/server/user/user_list_painter.cpp new file mode 100644 index 000000000..8891ff268 --- /dev/null +++ b/cockatrice/src/interface/widgets/server/user/user_list_painter.cpp @@ -0,0 +1,342 @@ +#include "user_list_painter.h" + +#include "../../interface/pixel_map_generator.h" + +#include +#include +#include +#include +#include +#include + +static constexpr int RowHeight = 72; +static constexpr int AvatarSize = 36; +static constexpr int LeftPadding = 14; +static constexpr int TextSpacing = 10; + +QSize UserListPainter::sizeHint() +{ + return QSize(0, RowHeight); +} + +QColor UserListPainter::getAccentColor(const UserLevelFlags &userLevel, bool online) +{ + QColor accentColor; + + if (userLevel.testFlag(ServerInfo_User::IsAdmin)) { + accentColor = QColor(245, 158, 11); + } else if (userLevel.testFlag(ServerInfo_User::IsModerator)) { + accentColor = QColor(59, 130, 246); + } else if (userLevel.testFlag(ServerInfo_User::IsJudge)) { + accentColor = QColor(168, 85, 247); + } else { + accentColor = QColor(100, 116, 139); + } + + if (!online) { + accentColor = accentColor.darker(160); + } + + return accentColor; +} + +int UserListPainter::getCardRight(const QStyleOptionViewItem &option, const QRect &rect) +{ + int scrollBarWidth = 0; + + if (const auto *scrollArea = qobject_cast(option.widget)) { + const QScrollBar *sb = scrollArea->verticalScrollBar(); + if (sb && sb->isVisible()) { + scrollBarWidth = sb->width(); + } + } + + const int viewportRight = option.widget ? option.widget->width() - scrollBarWidth : rect.right(); + + return qMin(rect.right(), viewportRight - 4); +} + +void UserListPainter::drawBackground(QPainter *painter, + const QRectF &cardRect, + const QColor &accentColor, + bool selected) +{ + QLinearGradient bg(cardRect.topLeft(), cardRect.topRight()); + 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); + + painter->setBrush(accentColor); + painter->drawRoundedRect(QRectF(cardRect.left(), cardRect.top(), 3, cardRect.height()), 2, 2); +} + +static QString makeKey(const QString &user, const QString &card, const QString &providerId) +{ + return user + u'|' + card + u'|' + providerId; +} + +void UserListPainter::drawCardArt(QPainter *painter, + const QRect &rect, + int cardRight, + const QString &userName, + const QMap *cardArtCache, + const CardArtParams ¶ms, + const QPixmap *overridePixmap = nullptr) +{ + QPixmap art; + + if (overridePixmap && !overridePixmap->isNull()) { + art = *overridePixmap; + } else { + if (!cardArtCache) { + return; + } + + const QString key = makeKey(userName, params.cardName, params.cardProviderId); + + if (!cardArtCache->contains(key)) { + return; + } + + art = cardArtCache->value(key); + } + + if (art.isNull()) { + return; + } + + const int cardH = rect.height() - 4; + const int totalW = cardRight - 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) / art.width(), double(cardH) / art.height()); + const double scale = basescale * params.zoom; + + const int scaledW = qRound(art.width() * scale); + const int scaledH = qRound(art.height() * scale); + + const QPixmap scaled = art.scaled(scaledW, scaledH, Qt::IgnoreAspectRatio, Qt::SmoothTransformation); + + const int srcX = (scaledW - drawW) / 2; + const int srcY = qRound((scaledH - cardH) * params.verticalOffset); + + // Clamp srcY so we never copy outside the pixmap bounds + const int safeSrcY = qBound(0, srcY, qMax(0, scaledH - cardH)); + + QImage img = + scaled.copy(srcX, safeSrcY, drawW, cardH).toImage().convertToFormat(QImage::Format_ARGB32_Premultiplied); + + { + QPainter mask(&img); + mask.setCompositionMode(QPainter::CompositionMode_DestinationIn); + + QLinearGradient grad(0, 0, img.width(), 0); + grad.setColorAt(0.00, Qt::transparent); + grad.setColorAt(0.22, Qt::white); + grad.setColorAt(0.78, Qt::white); + grad.setColorAt(1.00, Qt::transparent); + + mask.fillRect(img.rect(), grad); + } + + painter->setOpacity(0.55); + painter->drawImage(rect.left() + marginL, rect.top() + 2, img); + painter->setOpacity(1.0); +} + +QRect UserListPainter::getAvatarRect(const QRect &rect) +{ + const int avatarX = rect.left() + LeftPadding; + const int avatarY = rect.top() + (rect.height() - AvatarSize) / 2; + return QRect(avatarX, avatarY, AvatarSize, AvatarSize); +} + +void UserListPainter::drawAvatar(QPainter *painter, + const QRect &avatarRect, + const QString &userName, + const QColor &accentColor, + const UserLevelFlags &userLevel, + const ServerInfo_User &userInfo, + const QString &privLevel, + const QMap *avatarCache) +{ + QPainterPath clipPath; + clipPath.addEllipse(avatarRect); + + painter->save(); + painter->setClipPath(clipPath); + + bool drewAvatar = false; + + if (avatarCache && avatarCache->contains(userName)) { + const QPixmap &avatar = avatarCache->value(userName); + if (!avatar.isNull()) { + painter->drawPixmap( + avatarRect, avatar.scaled(avatarRect.size(), Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation)); + drewAvatar = true; + } + } + + if (!drewAvatar) { + painter->setBrush(accentColor.darker(200)); + painter->setPen(Qt::NoPen); + painter->drawEllipse(avatarRect); + + const QPixmap pawn = + UserLevelPixmapGenerator::generatePixmap(24, userLevel, userInfo.pawn_colors(), false, privLevel); + + painter->drawPixmap(avatarRect.center().x() - 12, avatarRect.center().y() - 12, pawn); + } + + painter->restore(); +} + +void UserListPainter::drawStatusRing(QPainter *painter, const QRect &avatarRect, bool online) +{ + const QColor statusColor = online ? QColor(34, 197, 94) : QColor(70, 80, 95); + + painter->setPen(QPen(statusColor, 2)); + painter->setBrush(Qt::NoBrush); + painter->drawEllipse(avatarRect.adjusted(-1, -1, 1, 1)); +} + +void UserListPainter::drawUserName(QPainter *painter, + const QStyleOptionViewItem &option, + const QRect &rect, + int cardRight, + int textX, + const QString &userName, + bool online, + bool selected) +{ + QFont nameFont = option.font; + nameFont.setBold(true); + painter->setFont(nameFont); + + const QRect nameRect(textX, rect.top() + 8, cardRight - textX - 10, 20); + const QString elidedName = QFontMetrics(nameFont).elidedText(userName, Qt::ElideRight, cardRight - textX - 10); + + painter->setPen(QColor(0, 0, 0, 200)); + painter->drawText(nameRect.translated(1, 1), Qt::AlignVCenter | Qt::AlignLeft, elidedName); + + painter->setPen(online ? (selected ? Qt::white : QColor(226, 232, 240)) : QColor(90, 100, 115)); + painter->drawText(nameRect, Qt::AlignVCenter | Qt::AlignLeft, elidedName); +} + +void UserListPainter::drawCountryFlag(QPainter *painter, const QRect &rect, int textX, const ServerInfo_User &userInfo) +{ + const QPixmap flag = CountryPixmapGenerator::generatePixmap(13, QString::fromStdString(userInfo.country())); + if (!flag.isNull()) { + painter->drawPixmap(textX, rect.top() + 46, flag); + } +} + +QList UserListPainter::buildBadges(const UserLevelFlags &userLevel, const QString &privLevel) +{ + QList badges; + + if (userLevel.testFlag(ServerInfo_User::IsAdmin)) { + badges << Badge{"ADMIN", QColor(245, 158, 11)}; + } else if (userLevel.testFlag(ServerInfo_User::IsModerator)) { + badges << Badge{"MOD", QColor(59, 130, 246)}; + } else if (userLevel.testFlag(ServerInfo_User::IsJudge)) { + badges << Badge{"JUDGE", QColor(168, 85, 247)}; + } + + if (privLevel == "VIP") { + badges << Badge{"VIP", QColor(20, 184, 166)}; + } else if (privLevel == "DONATOR") { + badges << Badge{"DONATOR", QColor(249, 115, 22)}; + } + + return badges; +} + +void UserListPainter::drawBadges(QPainter *painter, + const QStyleOptionViewItem &option, + const QRect &rect, + int cardRight, + const QList &badges, + bool online) +{ + if (badges.isEmpty()) { + return; + } + + QFont badgeFont = option.font; + badgeFont.setPointSizeF(badgeFont.pointSizeF() * 0.68); + badgeFont.setBold(true); + painter->setFont(badgeFont); + + QFontMetrics fm(badgeFont); + + int totalBadgeW = 0; + for (const Badge &b : badges) { + totalBadgeW += fm.horizontalAdvance(b.text) + 8 + 4; + } + totalBadgeW -= 4; + + int bx = cardRight - 6 - totalBadgeW; + + for (const Badge &b : badges) { + 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(col.darker(online ? 160 : 220)); + painter->drawRoundedRect(br, 3, 3); + + painter->setPen(col.lighter(online ? 160 : 100)); + painter->drawText(br, Qt::AlignCenter, b.text); + + bx += bw + 4; + } +} + +void UserListPainter::paint(QPainter *painter, + const QStyleOptionViewItem &option, + const QModelIndex &index, + const ServerInfo_User &userInfo, + const QMap *avatarCache, + const QMap *cardArtCache, + const QMap *cardArtParamsMap) +{ + painter->save(); + painter->setRenderHints(QPainter::Antialiasing | QPainter::SmoothPixmapTransform | QPainter::TextAntialiasing); + + const QRect rect = option.rect; + const bool online = index.data(Qt::UserRole + 1).toBool(); + const bool selected = option.state & QStyle::State_Selected; + const UserLevelFlags userLevel(userInfo.user_level()); + const QString userName = QString::fromStdString(userInfo.name()); + const QString privLevel = QString::fromStdString(userInfo.privlevel()); + const QColor accentColor = getAccentColor(userLevel, online); + const QRectF cardRect = QRectF(rect).adjusted(3, 2, -3, -2); + const int cardRight = getCardRight(option, rect); + + const CardArtParams params = (cardArtParamsMap && cardArtParamsMap->contains(userName)) + ? cardArtParamsMap->value(userName) + : CardArtParams{}; + + 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); + drawStatusRing(painter, avatarRect, online); + + const int textX = avatarRect.right() + TextSpacing; + 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); + + 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 new file mode 100644 index 000000000..28cab9675 --- /dev/null +++ b/cockatrice/src/interface/widgets/server/user/user_list_painter.h @@ -0,0 +1,87 @@ +#ifndef COCKATRICE_USER_LIST_PAINTER_H +#define COCKATRICE_USER_LIST_PAINTER_H + +#include "user_level.h" + +#include +#include +#include +#include +#include +#include + +class QPainter; +class QModelIndex; +class QStyleOptionViewItem; +class ServerInfo_User; + +struct CardArtParams +{ + QString cardName = ""; + QString cardProviderId = ""; + double marginPctL = 0.33; + double marginPctR = 0.02; + double verticalOffset = 0.35; + double zoom = 1.0; +}; + +class UserListPainter +{ +public: + static void paint(QPainter *painter, + const QStyleOptionViewItem &option, + const QModelIndex &index, + const ServerInfo_User &userInfo, + const QMap *avatarCache, + const QMap *cardArtCache, + const QMap *cardArtParamsMap); + + static QSize sizeHint(); + + static void drawCardArt(QPainter *painter, + const QRect &rect, + int cardRight, + const QString &userName, + const QMap *cardArtCache, + const CardArtParams ¶ms, + const QPixmap *overridePixmap); + +private: + struct Badge + { + QString text; + QColor color; + }; + + 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); + static QRect getAvatarRect(const QRect &rect); + static void drawAvatar(QPainter *painter, + const QRect &avatarRect, + const QString &userName, + const QColor &accentColor, + const UserLevelFlags &userLevel, + const ServerInfo_User &userInfo, + const QString &privLevel, + const QMap *avatarCache); + static void drawStatusRing(QPainter *painter, const QRect &avatarRect, bool online); + static void drawUserName(QPainter *painter, + const QStyleOptionViewItem &option, + const QRect &rect, + int cardRight, + int textX, + const QString &userName, + bool online, + 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, + const QStyleOptionViewItem &option, + const QRect &rect, + int cardRight, + const QList &badges, + bool online); +}; + +#endif // COCKATRICE_USER_LIST_PAINTER_H \ No newline at end of file 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 a14567dd4..8e0235f61 100644 --- a/cockatrice/src/interface/widgets/server/user/user_list_widget.cpp +++ b/cockatrice/src/interface/widgets/server/user/user_list_widget.cpp @@ -1,10 +1,13 @@ #include "user_list_widget.h" +#include "../../../../client/settings/cache_settings.h" +#include "../../../card_picture_loader/card_picture_loader.h" #include "../../interface/pixel_map_generator.h" #include "../../interface/widgets/tabs/tab_account.h" #include "../../interface/widgets/tabs/tab_supervisor.h" #include "../game_selector.h" #include "user_context_menu.h" +#include "user_list_painter.h" #include #include @@ -15,14 +18,19 @@ #include #include #include +#include +#include #include #include #include #include #include +#include #include #include -#include +#include +#include +#include BanDialog::BanDialog(const ServerInfo_User &info, QWidget *parent) : QDialog(parent) { @@ -308,7 +316,18 @@ QString AdminNotesDialog::getNotes() const return notes->toPlainText(); } -UserListItemDelegate::UserListItemDelegate(QObject *const parent) : QStyledItemDelegate(parent) +namespace UserListRoles +{ +constexpr int Online = Qt::UserRole + 1; +constexpr int UserInfo = Qt::UserRole + 2; +} // namespace UserListRoles + +UserListItemDelegate::UserListItemDelegate(QObject *const parent, + const QMap *avatarCache, + const QMap *cardArtCache, + const QMap *cardArtParamsMap) + : QStyledItemDelegate(parent), avatarCache(avatarCache), cardArtCache(cardArtCache), + cardArtParamsMap(cardArtParamsMap) { } @@ -327,6 +346,32 @@ bool UserListItemDelegate::editorEvent(QEvent *event, return QStyledItemDelegate::editorEvent(event, model, option, index); } +QSize UserListItemDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const +{ + if (!SettingsCache::instance().getStyleUserList()) { + return QStyledItemDelegate::sizeHint(option, index); + } + return UserListPainter::sizeHint(); +} + +void UserListItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const +{ + if (!SettingsCache::instance().getStyleUserList()) { + QStyledItemDelegate::paint(painter, option, index); + return; + } + + const QVariant var = index.data(UserListRoles::UserInfo); + + if (!var.isValid()) { + QStyledItemDelegate::paint(painter, option, index); + return; + } + + UserListPainter::paint(painter, option, index, var.value(), avatarCache, cardArtCache, + cardArtParamsMap); +} + UserListTWI::UserListTWI(const ServerInfo_User &_userInfo) : QTreeWidgetItem(Type) { setUserInfo(_userInfo); @@ -343,11 +388,12 @@ void UserListTWI::setUserInfo(const ServerInfo_User &_userInfo) setData(2, Qt::UserRole, QString::fromStdString(userInfo.name())); setData(2, Qt::DisplayRole, QString::fromStdString(userInfo.name())); setData(3, Qt::InitialSortOrderRole, QString::fromStdString(userInfo.privlevel())); + setData(0, UserListRoles::UserInfo, QVariant::fromValue(userInfo)); } void UserListTWI::setOnline(bool online) { - setData(0, Qt::UserRole + 1, online); + setData(0, UserListRoles::Online, online); setData(2, Qt::ForegroundRole, online ? qApp->palette().brush(QPalette::WindowText) : QBrush(Qt::gray)); } @@ -366,8 +412,8 @@ void UserListTWI::setOnline(bool online) bool UserListTWI::operator<(const QTreeWidgetItem &other) const { // Sort by online/offline - if (data(0, Qt::UserRole + 1) != other.data(0, Qt::UserRole + 1)) { - return data(0, Qt::UserRole + 1).toBool(); + if (data(0, UserListRoles::Online) != other.data(0, UserListRoles::Online)) { + return data(0, UserListRoles::Online).toBool(); } const auto &lhsUserLevelFlags = UserLevelFlags(data(0, Qt::UserRole).toInt()); @@ -414,20 +460,100 @@ UserListWidget::UserListWidget(TabSupervisor *_tabSupervisor, QWidget *parent) : QGroupBox(parent), tabSupervisor(_tabSupervisor), client(_client), type(_type), onlineCount(0) { - itemDelegate = new UserListItemDelegate(this); + 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); userTree = new QTreeWidget; - userTree->setColumnCount(3); - userTree->header()->setSectionResizeMode(QHeaderView::ResizeToContents); + userTree->setColumnCount(4); // 0=display, 1=flag(hidden), 2=name(hidden), 3=privlevel(hidden) + userTree->header()->setSectionResizeMode(0, QHeaderView::Stretch); userTree->header()->setMinimumSectionSize(0); userTree->setHeaderHidden(true); userTree->setRootIsDecorated(false); userTree->setIconSize(QSize(20, 18)); userTree->setItemDelegate(itemDelegate); userTree->setAlternatingRowColors(true); + userTree->hideColumn(1); + userTree->hideColumn(2); + userTree->hideColumn(3); connect(userTree, &QTreeWidget::itemActivated, this, &UserListWidget::userClicked); + userTree->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + userTree->header()->setStretchLastSection(true); + + // ── 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); + } + }); + + 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(); + } + }); + + userTree->setMouseTracking(true); + userTree->viewport()->setMouseTracking(true); + userTree->viewport()->installEventFilter(this); + + // Pin on item click + connect(userTree, &QTreeWidget::itemClicked, this, [this](QTreeWidgetItem *item, int) { + if (!SettingsCache::instance().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 + }); + + 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(); + } + }); + + // Hide popup when list scrolls (reference row has moved) + connect(userTree->verticalScrollBar(), &QScrollBar::valueChanged, this, [this] { + m_showPopupTimer->stop(); + hidePopup(true); + }); + + // Forward join requests from popup upward + connect(m_userInfoPopup, &UserInfoPopup::joinGameRequested, this, &UserListWidget::joinGameRequested); + + 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(), &SettingsCache::styleUserListChanged, this, &UserListWidget::applyDisplayMode); + applyDisplayMode(); QVBoxLayout *vbox = new QVBoxLayout; vbox->addWidget(userTree); @@ -437,6 +563,296 @@ UserListWidget::UserListWidget(TabSupervisor *_tabSupervisor, retranslateUi(); } +void UserListWidget::bind(UserListManager *mgr) +{ + manager = mgr; + + // ── Full rebuild: disconnect / reconnect / bulk initial load ────────────── + connect(manager, &UserListManager::listReset, this, &UserListWidget::rebuild); + + // ── 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); }); + } + + // ── Popup button refresh ────────────────────────────────────────────────── + // Any buddy/ignore mutation while the popup is open refreshes its buttons + auto refreshIfPopupOpen = [this](const QString &name) { + if (m_userInfoPopup && m_userInfoPopup->isVisible() && m_userInfoPopup->currentUser() == name) { + refreshPopupButtons(name); + } + }; + auto refreshCurrentPopup = [refreshIfPopupOpen](const ServerInfo_User &u) { + refreshIfPopupOpen(QString::fromStdString(u.name())); + }; + + connect(manager, &UserListManager::addedToBuddyList, this, refreshCurrentPopup); + connect(manager, &UserListManager::removedFromBuddyList, this, refreshIfPopupOpen); + connect(manager, &UserListManager::addedToIgnoreList, this, refreshCurrentPopup); + connect(manager, &UserListManager::removedFromIgnoreList, this, refreshIfPopupOpen); + connect(manager, &UserListManager::userJoinedOnline, this, refreshCurrentPopup); + connect(manager, &UserListManager::userLeftOnline, this, refreshIfPopupOpen); + + rebuild(); +} + +void UserListWidget::refreshPopupButtons(const QString &userName) +{ + UserListTWI *item = users.value(userName); + if (!item) { + return; + } + + const UserListProxy *proxy = tabSupervisor->getUserListManager(); + const bool online = item->data(0, UserListRoles::Online).toBool(); + const bool isBuddy = proxy->isUserBuddy(userName); + const bool isIgn = proxy->isUserIgnored(userName); + + m_userInfoPopup->updateActionButtons(item->getUserInfo(), online, isBuddy, isIgn); + positionPopup(userName); // height may have changed — reposition +} + +void UserListWidget::hideEvent(QHideEvent *e) +{ + QGroupBox::hideEvent(e); + m_showPopupTimer->stop(); + m_hidePopupTimer->stop(); + hidePopup(true); +} + +void UserListWidget::applyDisplayMode() +{ + const bool styled = SettingsCache::instance().getStyleUserList(); + + if (styled) { + userTree->header()->setSectionResizeMode(0, QHeaderView::Stretch); + userTree->hideColumn(1); + userTree->hideColumn(2); + userTree->hideColumn(3); + } else { + userTree->header()->setSectionResizeMode(QHeaderView::ResizeToContents); + userTree->showColumn(1); + userTree->showColumn(2); + userTree->hideColumn(3); + } + + userTree->viewport()->update(); +} + +void UserListWidget::connectPopupSignals() +{ + connect(m_userInfoPopup, &UserInfoPopup::closeRequested, this, [this] { + m_popupPinned = false; + hidePopup(true); + }); + 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(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(m_userInfoPopup, &UserInfoPopup::addIgnoreRequested, userContextMenu, &UserContextMenu::execAddToIgnore); + connect(m_userInfoPopup, &UserInfoPopup::removeIgnoreRequested, userContextMenu, + &UserContextMenu::execRemoveFromIgnore); + 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, false); }); + connect(m_userInfoPopup, &UserInfoPopup::demoteFromModRequested, this, + [this](const QString &n) { userContextMenu->execAdjustMod(n, false, false); }); + connect(m_userInfoPopup, &UserInfoPopup::promoteToJudgeRequested, this, + [this](const QString &n) { userContextMenu->execAdjustMod(n, false, true); }); + connect(m_userInfoPopup, &UserInfoPopup::demoteFromJudgeRequested, this, + [this](const QString &n) { userContextMenu->execAdjustMod(n, false, false); }); +} + +bool UserListWidget::eventFilter(QObject *obj, QEvent *event) +{ + if (obj == userTree->viewport()) { + if (event->type() == QEvent::MouseMove) { + if (!SettingsCache::instance().getStyleUserList()) { + return QGroupBox::eventFilter(obj, event); + } + auto *me = static_cast(event); + auto *twi = static_cast(userTree->itemAt(me->pos())); + const QString hovName = twi ? QString::fromStdString(twi->getUserInfo().name()) : QString{}; + + if (hovName != m_hoveredUser) { + m_hoveredUser = hovName; + if (!hovName.isEmpty()) { + m_hidePopupTimer->stop(); + if (!m_popupPinned) { + m_showPopupTimer->start(); + } + } else { + m_showPopupTimer->stop(); + if (!m_popupPinned) { + m_hidePopupTimer->start(); + } + } + } + } else if (event->type() == QEvent::Leave) { + m_hoveredUser.clear(); + m_showPopupTimer->stop(); + if (!m_popupPinned) { + m_hidePopupTimer->start(); + } + } + } + + return QGroupBox::eventFilter(obj, event); +} + +void UserListWidget::showPopupForUser(const QString &userName) +{ + UserListTWI *item = users.value(userName); + if (!item) { + return; + } + + 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); + + m_userInfoPopup->showForUser(userName, info, online, isBuddy, isIgn); + + // 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(); + + positionPopup(userName); // geometry is now accurate; move() sticks + + 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(const QString &userName) +{ + UserListTWI *item = users.value(userName); + if (!item) { + return; + } + + QWidget *vp = userTree->viewport(); + const QRect itemR = userTree->visualItemRect(item); + const QPoint itemTL = vp->mapToGlobal(itemR.topLeft()); + const QPoint vpTL = vp->mapToGlobal(vp->rect().topLeft()); + const QPoint vpTR = vp->mapToGlobal(vp->rect().topRight()); + + m_userInfoPopup->adjustSize(); + const int popW = m_userInfoPopup->width(); + const int popH = m_userInfoPopup->height(); + const int margin = 12; + + const QRect screen = QGuiApplication::primaryScreen()->availableGeometry(); + + // ── X: prefer the side with more space ─────────────────────────────────── + const int spaceLeft = vpTL.x() - screen.left() - margin; + const int spaceRight = screen.right() - vpTR.x() - margin; + int x; + if (spaceLeft >= spaceRight) { + x = (spaceLeft >= popW) ? (vpTL.x() - margin - popW) : (vpTR.x() + margin); + } else { + x = (spaceRight >= popW) ? (vpTR.x() + margin) : (vpTL.x() - margin - popW); + } + x = qBound(screen.left() + margin, x, screen.right() - popW - margin); + + // ── Y: grow down if there's room, otherwise grow up ─────────────────────── + const int itemTopY = itemTL.y(); + const int spaceBelow = screen.bottom() - itemTopY - margin; + const int spaceAbove = itemTopY - screen.top() - margin; + + int y; + if (spaceBelow >= popH) { + y = itemTopY; // top edges align, popup grows downward + } else if (spaceAbove >= popH) { + y = itemTopY - popH; // bottom of popup meets top of item, grows upward + } else { + // Neither side fits cleanly — pick the roomier side and let clamp handle the rest + y = (spaceBelow >= spaceAbove) ? itemTopY : (itemTopY - popH); + } + y = qBound(screen.top() + margin, y, screen.bottom() - popH - margin); + + m_userInfoPopup->move(x, y); +} + +void UserListWidget::hidePopup(bool immediate) +{ + m_showPopupTimer->stop(); + m_hidePopupTimer->stop(); + if (!m_userInfoPopup->isVisible()) { + return; + } + + if (immediate) { + m_userInfoPopup->hide(); + return; + } + + // Fade out + auto *fade = new QPropertyAnimation(m_userInfoPopup, "windowOpacity", m_userInfoPopup); + fade->setDuration(100); + fade->setStartValue(m_userInfoPopup->windowOpacity()); + fade->setEndValue(0.0); + connect(fade, &QPropertyAnimation::finished, m_userInfoPopup, &QWidget::hide); + fade->start(QAbstractAnimation::DeleteWhenStopped); +} + void UserListWidget::retranslateUi() { userContextMenu->retranslateUi(); @@ -457,9 +873,60 @@ void UserListWidget::retranslateUi() updateCount(); } +void UserListWidget::rebuild() +{ + userTree->clear(); + users.clear(); + cardArtParamsMap.clear(); + onlineCount = 0; + + if (!manager) { + return; + } + + const QMap *source = nullptr; + + switch (type) { + case AllUsersList: + case RoomList: + source = &manager->getAllUsersList(); + break; + case BuddyList: + source = &manager->getBuddyList(); + break; + case IgnoreList: + source = &manager->getIgnoreList(); + break; + } + + for (auto it = source->cbegin(); it != source->cend(); ++it) { + processUserInfo(it.value(), manager->getOnlineUser(it.key()) != nullptr); + } + + sortItems(); +} + 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. + if (user.has_card_art_params()) { + const auto &cap = user.card_art_params(); + CardArtParams params; + params.cardName = QString::fromStdString(cap.card_name()); + params.cardProviderId = QString::fromStdString(cap.card_provider_id()); + params.marginPctL = cap.margin_pct_l(); + params.marginPctR = cap.margin_pct_r(); + params.verticalOffset = cap.vertical_offset(); + params.zoom = cap.zoom(); + cardArtParamsMap.insert(userName, params); + cardArtProvider->requestCardArt(userName, params.cardName, params.cardProviderId); + } else { + cardArtParamsMap.remove(userName); // clear stale params on removal + } + UserListTWI *item = users.value(userName); if (item) { item->setUserInfo(user); @@ -471,25 +938,28 @@ void UserListWidget::processUserInfo(const ServerInfo_User &user, bool online) ++onlineCount; } updateCount(); + avatarProvider->requestAvatar(userName); } item->setOnline(online); + sortItems(); + userTree->viewport()->update(); } bool UserListWidget::deleteUser(const QString &userName) { UserListTWI *twi = users.value(userName); - if (twi) { - users.remove(userName); - userTree->takeTopLevelItem(userTree->indexOfTopLevelItem(twi)); - if (twi->data(0, Qt::UserRole + 1).toBool()) { - --onlineCount; - } - delete twi; - updateCount(); - return true; + if (!twi) { + return false; } - return false; + users.remove(userName); + userTree->takeTopLevelItem(userTree->indexOfTopLevelItem(twi)); + if (twi->data(0, Qt::UserRole + 1).toBool()) { + --onlineCount; + } + delete twi; + updateCount(); + return true; } void UserListWidget::setUserOnline(const QString &userName, bool online) @@ -533,5 +1003,5 @@ void UserListWidget::showContextMenu(const QPoint &pos, const QModelIndex &index void UserListWidget::sortItems() { - userTree->sortItems(1, Qt::AscendingOrder); + userTree->sortItems(0, Qt::AscendingOrder); } 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 5a8c00d10..d70cdfbbd 100644 --- a/cockatrice/src/interface/widgets/server/user/user_list_widget.h +++ b/cockatrice/src/interface/widgets/server/user/user_list_widget.h @@ -7,9 +7,17 @@ #ifndef USERLIST_H #define USERLIST_H +#include "../../cards/card_info_picture_art_crop_widget.h" +#include "user_avatar_provider.h" +#include "user_card_art_provider.h" +#include "user_info_popup.h" +#include "user_list_manager.h" +#include "user_list_painter.h" + #include #include #include +#include #include #include #include @@ -94,12 +102,21 @@ public: class UserListItemDelegate : public QStyledItemDelegate { + const QMap *avatarCache; + const QMap *cardArtCache; + const QMap *cardArtParamsMap; + public: - explicit UserListItemDelegate(QObject *const parent); + explicit UserListItemDelegate(QObject *const parent, + const QMap *avatarCache, + const QMap *cardArtCache, + const QMap *cardArtParamsMap); bool editorEvent(QEvent *event, QAbstractItemModel *model, const QStyleOptionViewItem &option, const QModelIndex &index) override; + void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override; + QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override; }; class UserListTWI : public QTreeWidgetItem @@ -131,6 +148,22 @@ public: }; private: + UserListManager *manager = nullptr; + UserAvatarProvider *avatarProvider = nullptr; + UserCardArtProvider *cardArtProvider = nullptr; + QMap cardArtParamsMap; + // ── Hover popup ─────────────────────────────────────────────────────────── + UserInfoPopup *m_userInfoPopup = nullptr; + QTimer *m_showPopupTimer = nullptr; + QTimer *m_hidePopupTimer = nullptr; + QString m_hoveredUser; + bool m_popupPinned = false; + + void showPopupForUser(const QString &userName); + void hidePopup(bool immediate = false); + void positionPopup(const QString &userName); + void connectPopupSignals(); + QMap users; TabSupervisor *tabSupervisor; AbstractClient *client; @@ -141,6 +174,7 @@ private: int onlineCount; QString titleStr; void updateCount(); + void refreshPopupButtons(const QString &userName); private slots: void userClicked(QTreeWidgetItem *item, int column); signals: @@ -149,13 +183,18 @@ signals: void removeBuddy(const QString &userName); void addIgnore(const QString &userName); void removeIgnore(const QString &userName); + void joinGameRequested(int gameId, int roomId, bool asSpectator); public: UserListWidget(TabSupervisor *_tabSupervisor, AbstractClient *_client, UserListType _type, QWidget *parent = nullptr); + void bind(UserListManager *mgr); + void applyDisplayMode(); + 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); @@ -165,6 +204,9 @@ public: } void showContextMenu(const QPoint &pos, const QModelIndex &index); void sortItems(); + +protected: + void hideEvent(QHideEvent *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 2d32f3ce1..e00484ebf 100644 --- a/cockatrice/src/interface/widgets/settings_page/appearance_settings_page.cpp +++ b/cockatrice/src/interface/widgets/settings_page/appearance_settings_page.cpp @@ -111,6 +111,15 @@ AppearanceSettingsPage::AppearanceSettingsPage() homeTabGroupBox = new QGroupBox; homeTabGroupBox->setLayout(homeTabGrid); + styleUserListCheckBox.setChecked(settings.getStyleUserList()); + connect(&styleUserListCheckBox, &QCheckBox::QT_STATE_CHANGED, &settings, &SettingsCache::setStyleUserList); + + auto stylingTabGrid = new QGridLayout; + stylingTabGrid->addWidget(&styleUserListCheckBox, 0, 0, 1, 2); + + stylingGroupBox = new QGroupBox; + stylingGroupBox->setLayout(stylingTabGrid); + // Menu settings showShortcutsCheckBox.setChecked(settings.getShowShortcuts()); connect(&showShortcutsCheckBox, &QCheckBox::QT_STATE_CHANGED, this, &AppearanceSettingsPage::showShortcutsChanged); @@ -284,6 +293,7 @@ AppearanceSettingsPage::AppearanceSettingsPage() auto *mainLayout = new QVBoxLayout; mainLayout->addWidget(themeGroupBox); mainLayout->addWidget(homeTabGroupBox); + mainLayout->addWidget(stylingGroupBox); mainLayout->addWidget(menuGroupBox); mainLayout->addWidget(printingsGroupBox); mainLayout->addWidget(cardsGroupBox); @@ -398,6 +408,9 @@ void AppearanceSettingsPage::retranslateUi() homeTabBackgroundShuffleFrequencySpinBox.setSpecialValueText(tr("Disabled")); homeTabDisplayCardNameCheckBox.setText(tr("Display card name of background in bottom right")); + stylingGroupBox->setTitle(tr("Styling settings")); + styleUserListCheckBox.setText(tr("Style user list")); + menuGroupBox->setTitle(tr("Menu settings")); showShortcutsCheckBox.setText(tr("Show keyboard shortcuts in right-click menus")); showGameSelectorFilterToolbarCheckBox.setText(tr("Show game filter toolbar above list in room tab")); 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 9ed27be4d..e223d70f8 100644 --- a/cockatrice/src/interface/widgets/settings_page/appearance_settings_page.h +++ b/cockatrice/src/interface/widgets/settings_page/appearance_settings_page.h @@ -37,6 +37,7 @@ private: QLabel homeTabBackgroundShuffleFrequencyLabel; QSpinBox homeTabBackgroundShuffleFrequencySpinBox; QCheckBox homeTabDisplayCardNameCheckBox; + QCheckBox styleUserListCheckBox; QLabel minPlayersForMultiColumnLayoutLabel; QLabel maxFontSizeForCardsLabel; QCheckBox showShortcutsCheckBox; @@ -58,6 +59,7 @@ private: QCheckBox invertVerticalCoordinateCheckBox; QGroupBox *themeGroupBox; QGroupBox *homeTabGroupBox; + QGroupBox *stylingGroupBox; QGroupBox *menuGroupBox; QGroupBox *printingsGroupBox; QGroupBox *cardsGroupBox; diff --git a/cockatrice/src/interface/widgets/settings_page/messages_settings_page.cpp b/cockatrice/src/interface/widgets/settings_page/messages_settings_page.cpp index f64398fe5..831cb442c 100644 --- a/cockatrice/src/interface/widgets/settings_page/messages_settings_page.cpp +++ b/cockatrice/src/interface/widgets/settings_page/messages_settings_page.cpp @@ -6,6 +6,7 @@ #include #include #include +#include MessagesSettingsPage::MessagesSettingsPage() { @@ -22,10 +23,14 @@ MessagesSettingsPage::MessagesSettingsPage() ignoreUnregUsersMainChat.setChecked(SettingsCache::instance().getIgnoreUnregisteredUsers()); ignoreUnregUserMessages.setChecked(SettingsCache::instance().getIgnoreUnregisteredUserMessages()); + ignoreNonBuddyUserMessages.setChecked(SettingsCache::instance().getIgnoreNonBuddyUserMessages()); + connect(&ignoreUnregUsersMainChat, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance(), &SettingsCache::setIgnoreUnregisteredUsers); connect(&ignoreUnregUserMessages, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance(), &SettingsCache::setIgnoreUnregisteredUserMessages); + connect(&ignoreNonBuddyUserMessages, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance(), + &SettingsCache::setIgnoreNonBuddyUserMessages); invertMentionForeground.setChecked(SettingsCache::instance().getChatMentionForeground()); connect(&invertMentionForeground, &QCheckBox::QT_STATE_CHANGED, this, &MessagesSettingsPage::updateTextColor); @@ -62,9 +67,10 @@ MessagesSettingsPage::MessagesSettingsPage() chatGrid->addWidget(&ignoreUnregUsersMainChat, 2, 0); chatGrid->addWidget(&hexLabel, 1, 2); chatGrid->addWidget(&ignoreUnregUserMessages, 3, 0); - chatGrid->addWidget(&messagePopups, 4, 0); - chatGrid->addWidget(&mentionPopups, 5, 0); - chatGrid->addWidget(&roomHistory, 6, 0); + chatGrid->addWidget(&ignoreNonBuddyUserMessages, 4, 0); + chatGrid->addWidget(&messagePopups, 5, 0); + chatGrid->addWidget(&mentionPopups, 6, 0); + chatGrid->addWidget(&roomHistory, 7, 0); chatGroupBox = new QGroupBox; chatGroupBox->setLayout(chatGrid); @@ -237,6 +243,7 @@ void MessagesSettingsPage::retranslateUi() QString("%2").arg(WIKI_CUSTOM_SHORTCUTS).arg(tr("How to use in-game message macros"))); ignoreUnregUsersMainChat.setText(tr("Ignore chat room messages sent by unregistered users")); ignoreUnregUserMessages.setText(tr("Ignore private messages sent by unregistered users")); + ignoreNonBuddyUserMessages.setText(tr("Ignore private messages sent by non-buddy users")); invertMentionForeground.setText(tr("Invert text color")); invertHighlightForeground.setText(tr("Invert text color")); messagePopups.setText(tr("Enable desktop notifications for private messages")); diff --git a/cockatrice/src/interface/widgets/settings_page/messages_settings_page.h b/cockatrice/src/interface/widgets/settings_page/messages_settings_page.h index e8a4a8aa4..e98ae0592 100644 --- a/cockatrice/src/interface/widgets/settings_page/messages_settings_page.h +++ b/cockatrice/src/interface/widgets/settings_page/messages_settings_page.h @@ -36,6 +36,7 @@ private: QCheckBox invertHighlightForeground; QCheckBox ignoreUnregUsersMainChat; QCheckBox ignoreUnregUserMessages; + QCheckBox ignoreNonBuddyUserMessages; QCheckBox messagePopups; QCheckBox mentionPopups; QCheckBox roomHistory; 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 dfa736a1a..44b30d29c 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 @@ -68,10 +68,18 @@ UserInterfaceSettingsPage::UserInterfaceSettingsPage() connect(&showTotalSelectionCountCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance(), &SettingsCache::setShowTotalSelectionCount); + showSubtypeSelectionTallyCheckBox.setChecked(SettingsCache::instance().getShowSubtypeSelectionTally()); + connect(&showSubtypeSelectionTallyCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance(), + &SettingsCache::setShowSubtypeSelectionTally); + useTearOffMenusCheckBox.setChecked(SettingsCache::instance().getUseTearOffMenus()); connect(&useTearOffMenusCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance(), [](const QT_STATE_CHANGED_T state) { SettingsCache::instance().setUseTearOffMenus(state == Qt::Checked); }); + keepGameChatFocusCheckBox.setChecked(SettingsCache::instance().getKeepGameChatFocus()); + connect(&keepGameChatFocusCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance(), + &SettingsCache::setKeepGameChatFocus); + auto *generalGrid = new QGridLayout; generalGrid->addWidget(&doubleClickToPlayCheckBox, 0, 0); generalGrid->addWidget(&clickPlaysAllSelectedCheckBox, 1, 0); @@ -82,7 +90,9 @@ UserInterfaceSettingsPage::UserInterfaceSettingsPage() generalGrid->addWidget(&annotateTokensCheckBox, 6, 0); generalGrid->addWidget(&showDragSelectionCountCheckBox, 7, 0); generalGrid->addWidget(&showTotalSelectionCountCheckBox, 8, 0); - generalGrid->addWidget(&useTearOffMenusCheckBox, 9, 0); + generalGrid->addWidget(&showSubtypeSelectionTallyCheckBox, 9, 0); + generalGrid->addWidget(&useTearOffMenusCheckBox, 10, 0); + generalGrid->addWidget(&keepGameChatFocusCheckBox, 11, 0); generalGroupBox = new QGroupBox; generalGroupBox->setLayout(generalGrid); @@ -204,9 +214,13 @@ void UserInterfaceSettingsPage::retranslateUi() closeEmptyCardViewCheckBox.setText(tr("Close card view window when last card is removed")); focusCardViewSearchBarCheckBox.setText(tr("Auto focus search bar when card view window is opened")); annotateTokensCheckBox.setText(tr("Annotate card text on tokens")); - showDragSelectionCountCheckBox.setText(tr("Show selection counter during drag selection")); - showTotalSelectionCountCheckBox.setText(tr("Show total selection counter")); + showDragSelectionCountCheckBox.setText(tr("Show selection count during drag selection")); + showTotalSelectionCountCheckBox.setText(tr("Show total selection count")); + showSubtypeSelectionTallyCheckBox.setText(tr("Show subtype breakdown in selection tally")); useTearOffMenusCheckBox.setText(tr("Use tear-off menus, allowing right click menus to persist on screen")); + keepGameChatFocusCheckBox.setText( + tr("Keep game chat focused when clicking in game (Note: disables card view search bar)")); + notificationsGroupBox->setTitle(tr("Notifications settings")); notificationsEnabledCheckBox.setText(tr("Enable notifications in taskbar")); specNotificationsEnabledCheckBox.setText(tr("Notify in the taskbar for game events while you are spectating")); 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 6dd43ceae..06f0e6b83 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 @@ -29,7 +29,9 @@ private: QCheckBox annotateTokensCheckBox; QCheckBox showDragSelectionCountCheckBox; QCheckBox showTotalSelectionCountCheckBox; + QCheckBox showSubtypeSelectionTallyCheckBox; QCheckBox useTearOffMenusCheckBox; + QCheckBox keepGameChatFocusCheckBox; QCheckBox tapAnimationCheckBox; QCheckBox openDeckInNewTabCheckBox; QLabel visualDeckStoragePromptForConversionLabel; diff --git a/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.cpp b/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.cpp index 66609456e..4bb8ffca4 100644 --- a/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.cpp +++ b/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.cpp @@ -43,7 +43,7 @@ #include #include #include -#include +#include /** * @brief Constructs the AbstractTabDeckEditor. @@ -56,6 +56,9 @@ AbstractTabDeckEditor::AbstractTabDeckEditor(TabSupervisor *_tabSupervisor) : Ta deckStateManager = new DeckStateManager(this); + databaseModel = new CardDatabaseModel(CardDatabaseManager::getInstance(), true, this); + databaseModel->setObjectName("databaseModel"); + cardDatabaseDockWidget = new DeckEditorCardDatabaseDockWidget(this); deckDockWidget = new DeckEditorDeckDockWidget(this); cardInfoDockWidget = new DeckEditorCardInfoDockWidget(this); @@ -105,16 +108,17 @@ void AbstractTabDeckEditor::registerDockWidget(QMenu *_viewMenu, QDockWidget *wi dockToActions.insert(widget, {menu, aVisible, aFloating, defaultSize}); } -/** - * @brief Updates the card info dock and printing selector. - * @param card The card to display. - */ void AbstractTabDeckEditor::updateCard(const ExactCard &card) { cardInfoDockWidget->updateCard(card); printingSelectorDockWidget->printingSelector->setCard(card.getCardPtr()); } +void AbstractTabDeckEditor::updateCardInfo(const ExactCard &card) +{ + cardInfoDockWidget->updateCard(card); +} + /** @brief Placeholder: called when the deck changes. */ void AbstractTabDeckEditor::onDeckChanged() { @@ -129,47 +133,14 @@ void AbstractTabDeckEditor::onDeckModified() emit tabTextChanged(this, getTabText()); } -/** - * @brief Helper for adding a card to a deck zone. - * @param card Card to add. - * @param zoneName Zone to add the card to. - */ -void AbstractTabDeckEditor::addCardHelper(const ExactCard &card, const QString &zoneName) +void AbstractTabDeckEditor::addCard(const ExactCard &card, const QString &zoneName) { deckStateManager->addCard(card, zoneName); } -/** - * @brief Adds a card to the main deck or sideboard depending on Ctrl key. - */ -void AbstractTabDeckEditor::actAddCard(const ExactCard &card) +void AbstractTabDeckEditor::decrementCard(const ExactCard &card, const QString &zoneName) { - if (QApplication::keyboardModifiers() & Qt::ControlModifier) { - actAddCardToSideboard(card); - } else { - addCardHelper(card, DECK_ZONE_MAIN); - } - - deckMenu->setSaveStatus(true); -} - -/** @brief Adds a card to the sideboard explicitly. */ -void AbstractTabDeckEditor::actAddCardToSideboard(const ExactCard &card) -{ - addCardHelper(card, DECK_ZONE_SIDE); - deckMenu->setSaveStatus(true); -} - -/** @brief Decrements a card from the main deck. */ -void AbstractTabDeckEditor::actDecrementCard(const ExactCard &card) -{ - deckStateManager->decrementCard(card, DECK_ZONE_MAIN); -} - -/** @brief Decrements a card from the sideboard. */ -void AbstractTabDeckEditor::actDecrementCardFromSideboard(const ExactCard &card) -{ - deckStateManager->decrementCard(card, DECK_ZONE_SIDE); + deckStateManager->decrementCard(card, zoneName); } /** @@ -571,14 +542,14 @@ void AbstractTabDeckEditor::actExportDeckDecklistXyz() /** @brief Analyzes the deck using DeckStats. */ void AbstractTabDeckEditor::actAnalyzeDeckDeckstats() { - auto *interface = new DeckStatsInterface(*cardDatabaseDockWidget->getDatabase(), this); + auto *interface = new DeckStatsInterface(this); interface->analyzeDeck(deckStateManager->getDeckList()); } /** @brief Analyzes the deck using TappedOut. */ void AbstractTabDeckEditor::actAnalyzeDeckTappedout() { - auto *interface = new TappedOutInterface(*cardDatabaseDockWidget->getDatabase(), this); + auto *interface = new TappedOutInterface(this); interface->analyzeDeck(deckStateManager->getDeckList()); } @@ -621,3 +592,15 @@ bool AbstractTabDeckEditor::closeRequest() } return close(); } + +void AbstractTabDeckEditor::showPrintingSelector() +{ + printingSelectorDockWidget->printingSelector->setCard(cardInfoDockWidget->cardInfo->getCard().getCardPtr()); + printingSelectorDockWidget->printingSelector->updateDisplay(); + printingSelectorDockWidget->setVisible(true); +} + +void AbstractTabDeckEditor::openEdhrecTab(const CardInfoPtr &info, bool isCommander) +{ + getTabSupervisor()->addEdhrecTab(info, isCommander); +} 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 477c3f973..34c585597 100644 --- a/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.h +++ b/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.h @@ -77,8 +77,8 @@ class QAction; * * **Key Methods:** * - * - actAddCard(const ExactCard &card) — Adds a card to the deck. - * - actDecrementCard(const ExactCard &card) — Removes a single instance of a card from the deck. + * - addCard(const ExactCard &card, const QString &zoneName) — Adds a card to the deck. + * - decrementCard(const ExactCard &card, const QString &zoneName) — Removes a single instance of a card from the deck. * - actRemoveCard() — Removes the currently selected card from the deck. * - actSaveDeckAs() — Performs a "Save As" action for the deck. * - updateCard(const ExactCard &card) — Updates the currently displayed card info in the dock. @@ -126,6 +126,7 @@ public: // UI Elements DeckStateManager *deckStateManager; + CardDatabaseModel *databaseModel; ///< Card database DeckEditorMenu *deckMenu; ///< Menu for deck operations DeckEditorCardDatabaseDockWidget *cardDatabaseDockWidget; ///< Database dock DeckEditorCardInfoDockWidget *cardInfoDockWidget; ///< Card info dock @@ -140,22 +141,35 @@ public slots: /** @brief Called when the deck is modified. */ virtual void onDeckModified(); - /** @brief Updates the card info panel. - * @param card The card to display. + /** + * @brief Updates the card info dock and printing selector. + * @param card The card to display. */ void updateCard(const ExactCard &card); - /** @brief Adds a card to the main deck or sideboard based on Ctrl key. */ - void actAddCard(const ExactCard &card); + /** + * @brief Updates just the card info dock + * @param card The card to display + */ + void updateCardInfo(const ExactCard &card); - /** @brief Adds a card to the sideboard explicitly. */ - void actAddCardToSideboard(const ExactCard &card); + /** + * @brief Adds a card to the given zone + * @param card Card to add. + * @param zoneName Zone to add the card to. + */ + void addCard(const ExactCard &card, const QString &zoneName); - /** @brief Decrements a card from the main deck. */ - void actDecrementCard(const ExactCard &card); - - /** @brief Decrements a card from the sideboard. */ - void actDecrementCardFromSideboard(const ExactCard &card); + /** + * @brief Decrements a card from the given zone + * + * Use an ExactCard with empty PrintingInfo if you want to remove a card by name regardless of printing. + * Otherwise, it won't remove anything unless there's an exact printing match. + * + * @param card Card to decrement. + * @param zoneName Zone to decrement from. + */ + void decrementCard(const ExactCard &card, const QString &zoneName); /** @brief Opens a recently opened deck file. */ void actOpenRecent(const QString &fileName); @@ -166,8 +180,15 @@ public slots: /** @brief Requests closing the tab. */ bool closeRequest() override; - /** @brief Shows the printing selector dock. Pure virtual. */ - virtual void showPrintingSelector() = 0; + /** @brief Shows the printing selector dock and updates it with the current card. */ + void showPrintingSelector(); + + /** + * @brief Opens an EDHRec tab for the given card + * @param info The card + * @param isCommander The type of search + */ + void openEdhrecTab(const CardInfoPtr &info, bool isCommander); signals: /** @brief Emitted when a deck should be opened in a new editor tab. */ @@ -293,9 +314,6 @@ protected: */ QMessageBox *createSaveConfirmationWindow(); - /** @brief Helper function to add a card to a specific deck zone. */ - void addCardHelper(const ExactCard &card, const QString &zoneName); - /** @brief Opens a deck from a file. */ virtual void openDeckFromFile(const QString &fileName, DeckOpenLocation deckOpenLocation); diff --git a/cockatrice/src/interface/widgets/tabs/tab_account.cpp b/cockatrice/src/interface/widgets/tabs/tab_account.cpp index e61732f90..2cc8165e8 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_account.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_account.cpp @@ -17,7 +17,7 @@ #include #include #include -#include +#include TabAccount::TabAccount(TabSupervisor *_tabSupervisor, AbstractClient *_client, const ServerInfo_User &userInfo) : Tab(_tabSupervisor), client(_client) diff --git a/cockatrice/src/interface/widgets/tabs/tab_admin.cpp b/cockatrice/src/interface/widgets/tabs/tab_admin.cpp index 533e1cc83..16f17e03e 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_admin.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_admin.cpp @@ -13,7 +13,7 @@ #include #include #include -#include +#include ShutdownDialog::ShutdownDialog(QWidget *parent) : QDialog(parent) { diff --git a/cockatrice/src/interface/widgets/tabs/tab_card_art_rules.cpp b/cockatrice/src/interface/widgets/tabs/tab_card_art_rules.cpp new file mode 100644 index 000000000..3dc4de15f --- /dev/null +++ b/cockatrice/src/interface/widgets/tabs/tab_card_art_rules.cpp @@ -0,0 +1,293 @@ +#include "tab_card_art_rules.h" + +#include "libcockatrice/card/database/card_database_manager.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +CardArtRulesModel::CardArtRulesModel(AbstractClient *client, QObject *parent) + : QAbstractTableModel(parent), client(client) +{ +} + +int CardArtRulesModel::rowCount(const QModelIndex &parent) const +{ + if (parent.isValid()) { + return 0; + } + return static_cast(entries.size()); +} + +int CardArtRulesModel::columnCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent); + return 4; +} + +QVariant CardArtRulesModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) { + return {}; + } + + const auto &e = entries.at(index.row()); + + if (role == Qt::DisplayRole) { + switch (index.column()) { + case 0: + return e.cardName; + case 1: + return e.cardProviderId; + case 2: + return e.mode; + case 3: + return e.reason; + } + } + + return {}; +} + +QVariant CardArtRulesModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + if (orientation != Qt::Horizontal || role != Qt::DisplayRole) { + return {}; + } + + switch (section) { + case 0: + return tr("Card"); + case 1: + return tr("ProviderId"); + case 2: + return tr("Mode"); + case 3: + return tr("Reason"); + default: + return {}; + } +} + +void CardArtRulesModel::refresh() +{ + Command_ListCardArtRules cmd; + + PendingCommand *pend = client->prepareModeratorCommand(cmd); + + connect(pend, &PendingCommand::finished, this, &CardArtRulesModel::onRefreshFinished); + + client->sendCommand(pend); +} + +void CardArtRulesModel::clear() +{ + beginResetModel(); + entries.clear(); + endResetModel(); +} + +QString CardArtRulesModel::cardAt(int row) const +{ + if (row < 0 || row >= static_cast(entries.size())) { + return {}; + } + + return entries[row].cardName; +} + +const CardArtRulesModel::Entry *CardArtRulesModel::entryAt(int row) const +{ + if (row < 0 || row >= static_cast(entries.size())) { + return nullptr; + } + + return &entries[row]; +} + +void CardArtRulesModel::onRefreshFinished(const Response &r) +{ + if (r.response_code() != Response::RespOk) { + return; + } + + const auto &resp = r.GetExtension(Response_ListCardArtRules::ext); + + beginResetModel(); + entries.clear(); + + for (const auto &e : resp.entries()) { + entries.push_back({QString::fromStdString(e.card_name()), QString::fromStdString(e.card_provider_id()), + QString::fromStdString(e.mode()), QString::fromStdString(e.reason())}); + } + + endResetModel(); +} + +TabCardArtRules::TabCardArtRules(TabSupervisor *parent, AbstractClient *_client) : Tab(parent), client(_client) +{ + setupUi(); + refresh(); +} + +void TabCardArtRules::setupUi() +{ + auto *central = new QWidget(this); + + initSearchBar(); + + providerComboBox = new QComboBox; + modeBox = new QComboBox; + reasonEdit = new QLineEdit; + + addBtn = new QPushButton; + removeBtn = new QPushButton; + refreshBtn = new QPushButton; + + modeBox->addItems({"ALLOW", "DENY"}); + + tableModel = new CardArtRulesModel(client, this); + + table = new QTableView; + table->setModel(tableModel); + table->setSelectionBehavior(QAbstractItemView::SelectRows); + table->setSelectionMode(QAbstractItemView::SingleSelection); + + auto *form = new QFormLayout; + form->addRow(tr("Card:"), searchEdit); + form->addRow(tr("ProviderId:"), providerComboBox); + form->addRow(tr("Mode:"), modeBox); + form->addRow(tr("Reason:"), reasonEdit); + + auto *buttons = new QHBoxLayout; + buttons->addWidget(addBtn); + buttons->addWidget(removeBtn); + buttons->addWidget(refreshBtn); + + auto *layout = new QVBoxLayout; + layout->addLayout(form); + layout->addLayout(buttons); + layout->addWidget(table); + + central->setLayout(layout); + setCentralWidget(central); + + connect(addBtn, &QPushButton::clicked, this, &TabCardArtRules::addRule); + + connect(removeBtn, &QPushButton::clicked, this, &TabCardArtRules::removeSelected); + + connect(refreshBtn, &QPushButton::clicked, this, &TabCardArtRules::refresh); + + retranslateUi(); +} + +void TabCardArtRules::initSearchBar() +{ + searchEdit = new QLineEdit; + searchEdit->setPlaceholderText(tr("Type a card name...")); + + cardDbModel = new CardDatabaseModel(CardDatabaseManager::getInstance(), false, this); + cardDbDisplayModel = new CardDatabaseDisplayModel(this); + cardDbDisplayModel->setSourceModel(cardDbModel); + cardSearchModel = new CardSearchModel(cardDbDisplayModel, this); + + 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); + + 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, + [this]() { populateProviderCombo(searchEdit->text().trimmed()); }); +} + +void TabCardArtRules::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 TabCardArtRules::retranslateUi() +{ + addBtn->setText(tr("Add rule")); + removeBtn->setText(tr("Remove rule")); + refreshBtn->setText(tr("Refresh")); +} + +void TabCardArtRules::refresh() +{ + tableModel->refresh(); +} + +void TabCardArtRules::addRule() +{ + Command_AddCardArtRule cmd; + cmd.set_card_name(searchEdit->text().toStdString()); + cmd.set_card_provider_id(providerComboBox->currentData().toString().toStdString()); + cmd.set_mode(modeBox->currentText().toStdString()); + cmd.set_reason(reasonEdit->text().toStdString()); + + client->sendCommand(client->prepareModeratorCommand(cmd)); + + refresh(); +} + +void TabCardArtRules::removeSelected() +{ + QModelIndex idx = table->currentIndex(); + if (!idx.isValid()) { + return; + } + + Command_RemoveCardArtRule cmd; + const auto e = tableModel->entryAt(idx.row()); + + cmd.set_card_name(e->cardName.toStdString()); + cmd.set_card_provider_id(e->cardProviderId.toStdString()); + + client->sendCommand(client->prepareModeratorCommand(cmd)); + + refresh(); +} \ No newline at end of file diff --git a/cockatrice/src/interface/widgets/tabs/tab_card_art_rules.h b/cockatrice/src/interface/widgets/tabs/tab_card_art_rules.h new file mode 100644 index 000000000..b9ea2ca83 --- /dev/null +++ b/cockatrice/src/interface/widgets/tabs/tab_card_art_rules.h @@ -0,0 +1,93 @@ +#ifndef COCKATRICE_DLG_CARD_ART_RULES_H +#define COCKATRICE_DLG_CARD_ART_RULES_H + +#include "card/card_search_model.h" +#include "tab_supervisor.h" + +#include +#include +#include +#include +#include + +class AbstractClient; + +class CardArtRulesModel : public QAbstractTableModel +{ + Q_OBJECT + +public: + struct Entry + { + QString cardName; + QString cardProviderId; + QString mode; + QString reason; + }; + + explicit CardArtRulesModel(AbstractClient *client, QObject *parent = nullptr); + + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + int columnCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant data(const QModelIndex &index, int role) const override; + QVariant headerData(int section, Qt::Orientation orientation, int role) const override; + + void refresh(); + void clear(); + + QString cardAt(int row) const; + const Entry *entryAt(int row) const; + +private slots: + void onRefreshFinished(const Response &r); + +private: + AbstractClient *client; + std::vector entries; +}; + +class TabCardArtRules : public Tab +{ + Q_OBJECT + +public: + TabCardArtRules(TabSupervisor *parent, AbstractClient *client); + + QString getTabText() const override + { + return tr("Card Art Rules"); + } + void retranslateUi() override; + +private: + void setupUi(); + +private slots: + void addRule(); + void removeSelected(); + void refresh(); + +private: + AbstractClient *client; + + QLineEdit *searchEdit; + void initSearchBar(); + void populateProviderCombo(const QString &cardName); + QCompleter *searchCompleter; + CardDatabaseModel *cardDbModel; + CardDatabaseDisplayModel *cardDbDisplayModel; + CardSearchModel *cardSearchModel; + CardCompleterProxyModel *cardProxyModel; + QComboBox *providerComboBox; + QComboBox *modeBox; + QLineEdit *reasonEdit; + + QPushButton *addBtn; + QPushButton *removeBtn; + QPushButton *refreshBtn; + + QTableView *table; + CardArtRulesModel *tableModel; +}; + +#endif // COCKATRICE_DLG_CARD_ART_RULES_H diff --git a/cockatrice/src/interface/widgets/tabs/tab_deck_editor.cpp b/cockatrice/src/interface/widgets/tabs/tab_deck_editor.cpp index 77dfddb4a..3878e12e0 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_deck_editor.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_deck_editor.cpp @@ -21,7 +21,6 @@ #include #include #include -#include /** * @brief Constructs a new TabDeckEditor object. @@ -120,16 +119,6 @@ void TabDeckEditor::refreshShortcuts() aResetLayout->setShortcuts(shortcuts.getShortcut("TabDeckEditor/aResetLayout")); } -/** - * @brief Displays the printing selector dock with the current card. - */ -void TabDeckEditor::showPrintingSelector() -{ - printingSelectorDockWidget->printingSelector->setCard(cardInfoDockWidget->cardInfo->getCard().getCardPtr()); - printingSelectorDockWidget->printingSelector->updateDisplay(); - printingSelectorDockWidget->setVisible(true); -} - /** * @brief Loads deck editor layout from settings or resets to default. */ diff --git a/cockatrice/src/interface/widgets/tabs/tab_deck_editor.h b/cockatrice/src/interface/widgets/tabs/tab_deck_editor.h index ab7a0bfc5..14be59cd7 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_deck_editor.h +++ b/cockatrice/src/interface/widgets/tabs/tab_deck_editor.h @@ -83,10 +83,6 @@ public: /** @brief Creates menus for deck editing and view options. */ void createMenus() override; - -public slots: - /** @brief Shows the printing selector dock and updates it with current card. */ - void showPrintingSelector() override; }; #endif diff --git a/cockatrice/src/interface/widgets/tabs/tab_deck_storage.cpp b/cockatrice/src/interface/widgets/tabs/tab_deck_storage.cpp index bdf7901f1..8e2b114d9 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_deck_storage.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_deck_storage.cpp @@ -28,6 +28,7 @@ #include #include #include +#include TabDeckStorage::TabDeckStorage(TabSupervisor *_tabSupervisor, AbstractClient *_client, diff --git a/cockatrice/src/interface/widgets/tabs/tab_game.cpp b/cockatrice/src/interface/widgets/tabs/tab_game.cpp index 9fc123a8c..600647171 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_game.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_game.cpp @@ -1,18 +1,21 @@ #include "tab_game.h" #include "../../../client/settings/cache_settings.h" -#include "../game/board/arrow_item.h" -#include "../game/board/card_item.h" -#include "../game/deckview/deck_view_container.h" -#include "../game/deckview/tabbed_deck_view_container.h" #include "../game/game.h" -#include "../game/game_scene.h" -#include "../game/game_view.h" -#include "../game/log/message_log_widget.h" -#include "../game/phases_toolbar.h" -#include "../game/player/player_list_widget.h" #include "../game/player/player_logic.h" #include "../game/replay.h" +#include "../game_graphics/board/arrow_item.h" +#include "../game_graphics/board/card_item.h" +#include "../game_graphics/deckview/deck_view_container.h" +#include "../game_graphics/deckview/tabbed_deck_view_container.h" +#include "../game_graphics/game_scene.h" +#include "../game_graphics/game_view.h" +#include "../game_graphics/log/message_log_widget.h" +#include "../game_graphics/phases_toolbar.h" +#include "../game_graphics/player/menu/card_menu.h" +#include "../game_graphics/player/menu/player_menu.h" +#include "../game_graphics/player/player_graphics_item.h" +#include "../game_graphics/player/player_list_widget.h" #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" @@ -41,13 +44,13 @@ #include #include #include -#include +#include TabGame::TabGame(TabSupervisor *_tabSupervisor, GameReplay *_replay) : Tab(_tabSupervisor), sayLabel(nullptr), sayEdit(nullptr) { // THIS CTOR IS USED ON REPLAY - game = new Replay(this, _replay); + game = new Replay(this, _replay, tabSupervisor->getIsLocalGame()); createCardInfoDock(true); createPlayerListDock(true); @@ -91,7 +94,7 @@ TabGame::TabGame(TabSupervisor *_tabSupervisor, : Tab(_tabSupervisor), userListProxy(_tabSupervisor->getUserListManager()) { // THIS CTOR IS USED ON GAMES - game = new Game(this, _clients, event, _roomGameTypes); + game = new Game(this, tabSupervisor->getIsLocalGame(), _clients, event, _roomGameTypes); createCardInfoDock(); createPlayerListDock(); @@ -363,11 +366,10 @@ void TabGame::retranslateUi() cardInfoFrameWidget->retranslateUi(); - QMapIterator i(game->getPlayerManager()->getPlayers()); - - while (i.hasNext()) { - i.next().value()->getGraphicsItem()->retranslateUi(); + for (auto playerView : scene->getPlayers().values()) { + playerView->retranslateUi(); } + QMapIterator j(deckViewContainers); while (j.hasNext()) { j.next().value()->playerDeckView->retranslateUi(); @@ -608,7 +610,7 @@ void TabGame::actRemoveLocalArrows() { auto *local = game->getPlayerManager()->getActiveLocalPlayer(game->getGameState()->getActivePlayer()); if (local) { - scene->requestClearArrowsForPlayer(local->getPlayerInfo()->getId()); + scene->clearArrowsForPlayer(local->getPlayerInfo()->getId()); } } @@ -654,8 +656,12 @@ PlayerLogic *TabGame::addPlayer(PlayerLogic *newPlayer) scene->addPlayer(newPlayer); + auto *view = scene->viewForPlayer(newPlayer->getPlayerInfo()->getId()); + connect(newPlayer, &PlayerLogic::newCardAdded, this, &TabGame::newCardAdded); - connect(newPlayer->getPlayerMenu(), &PlayerMenu::cardMenuUpdated, this, &TabGame::setCardMenu); + connect(newPlayer, &PlayerLogic::openDeckEditor, this, &TabGame::openDeckEditor); + connect(view->getPlayerMenu(), &PlayerMenu::cardMenuUpdated, this, &TabGame::setCardMenu); + connect(view, &PlayerGraphicsItem::cardInfoRequested, this, &TabGame::viewCardInfo); messageLog->connectToPlayerEventHandler(newPlayer->getPlayerEventHandler()); @@ -668,7 +674,7 @@ PlayerLogic *TabGame::addPlayer(PlayerLogic *newPlayer) addLocalPlayer(newPlayer, newPlayer->getPlayerInfo()->getId()); } - gameMenu->insertMenu(playersSeparator, newPlayer->getPlayerMenu()->getPlayerMenu()); + gameMenu->insertMenu(playersSeparator, view->getPlayerMenu()->getPlayerMenu()); createZoneForPlayer(newPlayer, newPlayer->getPlayerInfo()->getId()); @@ -678,7 +684,7 @@ PlayerLogic *TabGame::addPlayer(PlayerLogic *newPlayer) void TabGame::addLocalPlayer(PlayerLogic *newPlayer, int playerId) { if (game->getGameState()->getClients().size() == 1) { - newPlayer->getPlayerMenu()->setShortcutsActive(); + scene->viewForPlayer(playerId)->getPlayerMenu()->setShortcutsActive(); } auto *deckView = new TabbedDeckViewContainer(playerId, this); @@ -698,27 +704,24 @@ void TabGame::addLocalPlayer(PlayerLogic *newPlayer, int playerId) void TabGame::processPlayerLeave(PlayerLogic *leavingPlayer) { - QString playerName = "@" + leavingPlayer->getPlayerInfo()->getName(); - removePlayerFromAutoCompleteList(playerName); - - scene->removePlayer(leavingPlayer); + removePlayerFromAutoCompleteList("@" + leavingPlayer->getPlayerInfo()->getName()); // When we inserted the playerMenu into the gameMenu earlier, Qt wrapped the playerMenu into a QAction*, which lives // independently and does not get cleaned up when the source menu gets destroyed. We have to manually clean here. - if (leavingPlayer->getPlayerMenu()) { - QMenu *menu = leavingPlayer->getPlayerMenu()->getPlayerMenu(); - if (menu) { - // Find and remove the QAction pointing to this menu - QList actions = gameMenu->actions(); - for (QAction *act : actions) { - if (act->menu() == menu) { - gameMenu->removeAction(act); - delete act; // deletes the QAction wrapper around the submenu - break; - } + auto *view = scene->viewForPlayer(leavingPlayer->getPlayerInfo()->getId()); + if (view) { + // Find and remove the QAction pointing to this menu + QMenu *menu = view->getPlayerMenu()->getPlayerMenu(); + for (QAction *act : gameMenu->actions()) { + if (act->menu() == menu) { + gameMenu->removeAction(act); + delete act; + break; } } } + + scene->removePlayer(leavingPlayer); } void TabGame::processRemotePlayerDeckSelect(QString deckList, int playerId, QString playerName) @@ -869,12 +872,12 @@ PlayerLogic *TabGame::setActivePlayer(int id) if (i.value() == player) { i.value()->setActive(true); if (game->getGameState()->getClients().size() > 1) { - i.value()->getPlayerMenu()->setShortcutsActive(); + scene->viewForPlayer(i.value()->getPlayerInfo()->getId())->getPlayerMenu()->setShortcutsActive(); } } else { i.value()->setActive(false); if (game->getGameState()->getClients().size() > 1) { - i.value()->getPlayerMenu()->setShortcutsInactive(); + scene->viewForPlayer(i.value()->getPlayerInfo()->getId())->getPlayerMenu()->setShortcutsInactive(); } } } @@ -890,16 +893,16 @@ void TabGame::setActivePhase(int phase) void TabGame::newCardAdded(AbstractCardItem *card) { + connect(card, &AbstractCardItem::rightClicked, scene, &GameScene::onCardRightClicked); + connect(card, &AbstractCardItem::playSelected, scene, &GameScene::playSelected); + connect(card, &AbstractCardItem::playSelectedFaceDown, scene, &GameScene::playSelectedFaceDown); + connect(card, &AbstractCardItem::hideSelected, scene, &GameScene::hideSelected); connect(card, &AbstractCardItem::hovered, cardInfoFrameWidget, qOverload(&CardInfoFrameWidget::setCard)); + connect(card, &AbstractCardItem::selectionChanged, scene, &GameScene::onCardSelectionChanged); connect(card, &AbstractCardItem::showCardInfoPopup, this, &TabGame::showCardInfoPopup); connect(card, SIGNAL(deleteCardInfoPopup(QString)), this, SLOT(deleteCardInfoPopup(QString))); connect(card, &AbstractCardItem::cardShiftClicked, this, &TabGame::linkCardToChat); - CardItem *cardItem = qobject_cast(card); - if (cardItem) { - connect(cardItem->getState(), &CardState::zoneChanged, scene, - [this, cardItem]() { scene->onCardZoneChanged(cardItem, false); }); - } } QString TabGame::getTabText() const @@ -940,7 +943,7 @@ QString TabGame::getTabText() const /** * @param menu The menu to set. Pass in nullptr to set the menu to empty. */ -void TabGame::setCardMenu(QMenu *menu) +void TabGame::setCardMenu(CardMenu *menu) { if (!aCardMenu) { return; @@ -1174,6 +1177,11 @@ void TabGame::createReplayDock(GameReplay *replay) QDockWidget::DockWidgetMovable); replayDock->setWidget(replayManager); replayDock->setFloating(false); + + connect(replayManager, &ReplayManager::eventReplayed, game->getGameEventHandler(), + [this](const auto &event, auto options) { + game->getGameEventHandler()->processGameEventContainer(event, nullptr, options); + }); } void TabGame::createDeckViewContainerWidget(bool bReplay) diff --git a/cockatrice/src/interface/widgets/tabs/tab_game.h b/cockatrice/src/interface/widgets/tabs/tab_game.h index 7f9392034..b9289432d 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_game.h +++ b/cockatrice/src/interface/widgets/tabs/tab_game.h @@ -10,8 +10,8 @@ #define TAB_GAME_H #include "../game/abstract_game.h" -#include "../game/log/message_log_widget.h" #include "../game/player/player_logic.h" +#include "../game_graphics/log/message_log_widget.h" #include "../interface/widgets/menus/tearoff_menu.h" #include "../interface/widgets/replay/replay_manager.h" #include "tab.h" @@ -20,6 +20,7 @@ #include #include +class CardMenu; class ServerInfo_PlayerProperties; class TabbedDeckViewContainer; inline Q_LOGGING_CATEGORY(TabGameLog, "tab_game"); @@ -141,7 +142,7 @@ signals: private slots: void adminLockChanged(bool lock); void newCardAdded(AbstractCardItem *card); - void setCardMenu(QMenu *menu); + void setCardMenu(CardMenu *menu); void actGameInfo(); void actConcede(); diff --git a/cockatrice/src/interface/widgets/tabs/tab_logs.cpp b/cockatrice/src/interface/widgets/tabs/tab_logs.cpp index 9a030e7d9..e3678a903 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_logs.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_logs.cpp @@ -17,7 +17,7 @@ #include #include #include -#include +#include TabLog::TabLog(TabSupervisor *_tabSupervisor, AbstractClient *_client) : Tab(_tabSupervisor), client(_client) { diff --git a/cockatrice/src/interface/widgets/tabs/tab_message.cpp b/cockatrice/src/interface/widgets/tabs/tab_message.cpp index d77cb0391..2cacab731 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_message.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_message.cpp @@ -17,7 +17,7 @@ #include #include #include -#include +#include TabMessage::TabMessage(TabSupervisor *_tabSupervisor, AbstractClient *_client, diff --git a/cockatrice/src/interface/widgets/tabs/tab_room.cpp b/cockatrice/src/interface/widgets/tabs/tab_room.cpp index 424742e9b..ec05e9ff6 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_room.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_room.cpp @@ -31,7 +31,7 @@ #include #include #include -#include +#include TabRoom::TabRoom(TabSupervisor *_tabSupervisor, AbstractClient *_client, @@ -49,10 +49,25 @@ TabRoom::TabRoom(TabSupervisor *_tabSupervisor, QMap tempMap; tempMap.insert(info.room_id(), gameTypes); gameSelector = new GameSelector(client, tabSupervisor, this, QMap(), tempMap, true, true); + + auto *tabs = new QTabWidget(this); + + 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); @@ -101,7 +116,7 @@ TabRoom::TabRoom(TabSupervisor *_tabSupervisor, auto *hbox = new QHBoxLayout; hbox->addWidget(splitter, 3); - hbox->addWidget(userList, 1); + hbox->addWidget(tabs, 1); aLeaveRoom = new QAction(this); connect(aLeaveRoom, &QAction::triggered, this, &TabRoom::closeRequest); @@ -112,10 +127,8 @@ TabRoom::TabRoom(TabSupervisor *_tabSupervisor, const int userListSize = info.user_list_size(); for (int i = 0; i < userListSize; ++i) { - userList->processUserInfo(info.user_list(i), true); autocompleteUserList.append("@" + QString::fromStdString(info.user_list(i).name())); } - userList->sortItems(); const int gameListSize = info.game_list_size(); for (int i = 0; i < gameListSize; ++i) { @@ -269,8 +282,6 @@ void TabRoom::processListGamesEvent(const Event_ListGames &event) void TabRoom::processJoinRoomEvent(const Event_JoinRoom &event) { - userList->processUserInfo(event.user_info(), true); - userList->sortItems(); if (!autocompleteUserList.contains("@" + QString::fromStdString(event.user_info().name()))) { autocompleteUserList << "@" + QString::fromStdString(event.user_info().name()); sayEdit->setCompletionList(autocompleteUserList); @@ -279,7 +290,6 @@ void TabRoom::processJoinRoomEvent(const Event_JoinRoom &event) void TabRoom::processLeaveRoomEvent(const Event_LeaveRoom &event) { - userList->deleteUser(QString::fromStdString(event.name())); autocompleteUserList.removeOne("@" + QString::fromStdString(event.name())); sayEdit->setCompletionList(autocompleteUserList); } diff --git a/cockatrice/src/interface/widgets/tabs/tab_room.h b/cockatrice/src/interface/widgets/tabs/tab_room.h index eeb5a9e14..d669b6107 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_room.h +++ b/cockatrice/src/interface/widgets/tabs/tab_room.h @@ -56,7 +56,9 @@ private: QMap gameTypes; GameSelector *gameSelector; + UserListWidget *friendsList; UserListWidget *userList; + UserListWidget *ignoreList; const UserListProxy *userListProxy; ChatView *chatView; QLabel *sayLabel; diff --git a/cockatrice/src/interface/widgets/tabs/tab_supervisor.cpp b/cockatrice/src/interface/widgets/tabs/tab_supervisor.cpp index 68ef1d0b1..45512ce68 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_supervisor.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_supervisor.cpp @@ -9,6 +9,7 @@ #include "api/edhrec/tab_edhrec_main.h" #include "tab_account.h" #include "tab_admin.h" +#include "tab_card_art_rules.h" #include "tab_deck_editor.h" #include "tab_deck_storage.h" #include "tab_game.h" @@ -157,6 +158,10 @@ TabSupervisor::TabSupervisor(AbstractClient *_client, QMenu *tabsMenu, QWidget * aTabAdmin->setCheckable(true); connect(aTabAdmin, &QAction::triggered, this, &TabSupervisor::actTabAdmin); + aTabCardArtRules = new QAction(this); + aTabCardArtRules->setCheckable(true); + connect(aTabCardArtRules, &QAction::triggered, this, &TabSupervisor::actTabCardArtRules); + aTabLog = new QAction(this); aTabLog->setCheckable(true); connect(aTabLog, &QAction::triggered, this, &TabSupervisor::actTabLog); @@ -413,6 +418,7 @@ void TabSupervisor::start(const ServerInfo_User &_userInfo) tabsMenu->addSeparator(); tabsMenu->addAction(aTabAdmin); tabsMenu->addAction(aTabLog); + tabsMenu->addAction(aTabCardArtRules); if (SettingsCache::instance().getTabAdminOpen()) { openTabAdmin(); @@ -420,6 +426,7 @@ void TabSupervisor::start(const ServerInfo_User &_userInfo) if (SettingsCache::instance().getTabLogOpen()) { openTabLog(); } + openTabCardArtRules(); } retranslateUi(); @@ -659,6 +666,30 @@ void TabSupervisor::openTabAdmin() aTabAdmin->setChecked(true); } +void TabSupervisor::actTabCardArtRules(bool checked) +{ + if (checked && !tabCardArtRules) { + openTabCardArtRules(); + setCurrentWidget(tabCardArtRules); + } else if (!checked && tabCardArtRules) { + tabCardArtRules->closeRequest(); + } +} + +void TabSupervisor::openTabCardArtRules() +{ + tabCardArtRules = new TabCardArtRules(this, client); + + myAddTab(tabCardArtRules, aTabCardArtRules); + + connect(tabCardArtRules, &QObject::destroyed, this, [this] { + tabCardArtRules = nullptr; + aTabCardArtRules->setChecked(false); + }); + + aTabCardArtRules->setChecked(true); +} + void TabSupervisor::actTabLog(bool checked) { SettingsCache::instance().setTabLogOpen(checked); @@ -997,6 +1028,12 @@ void TabSupervisor::processUserMessageEvent(const Event_UserMessage &event) !userLevel.testFlag(ServerInfo_User::IsRegistered)) { // Flags are additive, so reg/mod/admin are all IsRegistered return; + } else if (SettingsCache::instance().getIgnoreNonBuddyUserMessages() && + !userListManager->isUserBuddy(senderName) && !userLevel.testFlag(ServerInfo_User::IsModerator) && + !userLevel.testFlag(ServerInfo_User::IsAdmin)) { + // Ignore private messages from non-buddies + // Moderator/Admin messages are exempt to ensure warnings reach users + return; } } tab = addMessageTab(QString::fromStdString(event.sender_name()), false); diff --git a/cockatrice/src/interface/widgets/tabs/tab_supervisor.h b/cockatrice/src/interface/widgets/tabs/tab_supervisor.h index 7fd8c868e..d645bc7d7 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_supervisor.h +++ b/cockatrice/src/interface/widgets/tabs/tab_supervisor.h @@ -23,6 +23,7 @@ #include #include +class TabCardArtRules; inline Q_LOGGING_CATEGORY(TabSupervisorLog, "tab_supervisor"); class UserListManager; @@ -91,6 +92,7 @@ private: TabDeckStorage *tabDeckStorage; TabReplays *tabReplays; TabAdmin *tabAdmin; + TabCardArtRules *tabCardArtRules; TabLog *tabLog; QMap roomTabs; QMap gameTabs; @@ -100,7 +102,8 @@ private: bool isLocalGame; QAction *aTabHome, *aTabDeckEditor, *aTabVisualDeckEditor, *aTabEdhRec, *aTabArchidekt, *aTabVisualDeckStorage, - *aTabVisualDatabaseDisplay, *aTabServer, *aTabAccount, *aTabDeckStorage, *aTabReplays, *aTabAdmin, *aTabLog; + *aTabVisualDatabaseDisplay, *aTabServer, *aTabAccount, *aTabDeckStorage, *aTabReplays, *aTabAdmin, + *aTabCardArtRules, *aTabLog; int myAddTab(Tab *tab, QAction *manager = nullptr); void addCloseButtonToTab(Tab *tab, int tabIndex, QAction *manager); @@ -133,7 +136,7 @@ public: return userInfo; } [[nodiscard]] AbstractClient *getClient() const; - [[nodiscard]] const UserListManager *getUserListManager() const + [[nodiscard]] UserListManager *getUserListManager() const { return userListManager; } @@ -185,6 +188,8 @@ private slots: void openTabDeckStorage(); void openTabReplays(); void openTabAdmin(); + void actTabCardArtRules(bool checked); + void openTabCardArtRules(); void openTabLog(); void updateCurrent(int index); diff --git a/cockatrice/src/interface/widgets/tabs/tab_visual_database_display.cpp b/cockatrice/src/interface/widgets/tabs/tab_visual_database_display.cpp index 5e8fb8670..3112e7ada 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_visual_database_display.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_visual_database_display.cpp @@ -1,14 +1,19 @@ #include "tab_visual_database_display.h" #include "tab_deck_editor.h" +#include "tab_supervisor.h" + +#include TabVisualDatabaseDisplay::TabVisualDatabaseDisplay(TabSupervisor *_tabSupervisor) : Tab(_tabSupervisor) { - deckEditor = new TabDeckEditor(_tabSupervisor); - deckEditor->setHidden(true); - visualDatabaseDisplayWidget = new VisualDatabaseDisplayWidget( - this, deckEditor, deckEditor->cardDatabaseDockWidget->databaseDisplayWidget->databaseModel, - deckEditor->cardDatabaseDockWidget->databaseDisplayWidget->databaseDisplayModel); + auto databaseModel = new CardDatabaseModel(CardDatabaseManager::getInstance(), true, this); + databaseModel->setObjectName("databaseModel"); + + visualDatabaseDisplayWidget = new VisualDatabaseDisplayWidget(this, databaseModel); + + connect(visualDatabaseDisplayWidget, &VisualDatabaseDisplayWidget::edhrecRequested, this, + &TabVisualDatabaseDisplay::openEdhrecTab); setCentralWidget(visualDatabaseDisplayWidget); @@ -18,3 +23,8 @@ TabVisualDatabaseDisplay::TabVisualDatabaseDisplay(TabSupervisor *_tabSupervisor void TabVisualDatabaseDisplay::retranslateUi() { } + +void TabVisualDatabaseDisplay::openEdhrecTab(const CardInfoPtr &info, bool isCommander) const +{ + getTabSupervisor()->addEdhrecTab(info, isCommander); +} \ No newline at end of file diff --git a/cockatrice/src/interface/widgets/tabs/tab_visual_database_display.h b/cockatrice/src/interface/widgets/tabs/tab_visual_database_display.h index f5aef6d9b..3a4bcd6ea 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_visual_database_display.h +++ b/cockatrice/src/interface/widgets/tabs/tab_visual_database_display.h @@ -15,9 +15,11 @@ class TabVisualDatabaseDisplay : public Tab Q_OBJECT private: - TabDeckEditor *deckEditor; VisualDatabaseDisplayWidget *visualDatabaseDisplayWidget; +private slots: + void openEdhrecTab(const CardInfoPtr &info, bool isCommander) const; + public: TabVisualDatabaseDisplay(TabSupervisor *_tabSupervisor); void retranslateUi() override; 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 3cdad91fc..fd465ec21 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 @@ -1,6 +1,7 @@ #include "tab_deck_editor_visual.h" #include "../../../../client/settings/cache_settings.h" +#include "../../cards/card_info_display_widget.h" #include "../../deck_editor/deck_state_manager.h" #include "../../filters/filter_builder.h" #include "../../interface/pixel_map_generator.h" @@ -25,6 +26,7 @@ #include #include #include +#include #include #include #include @@ -63,9 +65,10 @@ void TabDeckEditorVisual::createCentralFrame() centralFrame = new QVBoxLayout; centralWidget->setLayout(centralFrame); - tabContainer = new TabDeckEditorVisualTabWidget( - centralWidget, this, deckStateManager->getModel(), cardDatabaseDockWidget->databaseDisplayWidget->databaseModel, - cardDatabaseDockWidget->databaseDisplayWidget->databaseDisplayModel); + auto databaseModel = new CardDatabaseModel(CardDatabaseManager::getInstance(), true, this); + databaseModel->setObjectName("databaseModel"); + + tabContainer = new TabDeckEditorVisualTabWidget(centralWidget, this, deckStateManager->getModel(), databaseModel); connect(tabContainer, &TabDeckEditorVisualTabWidget::cardChanged, this, &TabDeckEditorVisual::changeModelIndexAndCardInfo); @@ -74,7 +77,14 @@ void TabDeckEditorVisual::createCentralFrame() connect(tabContainer, &TabDeckEditorVisualTabWidget::cardClicked, this, &TabDeckEditorVisual::processMainboardCardClick); connect(tabContainer, &TabDeckEditorVisualTabWidget::cardClickedDatabaseDisplay, this, - &TabDeckEditorVisual::processCardClickDatabaseDisplay); + &TabDeckEditorVisual::processDatabaseCardClick); + + connect(tabContainer, &TabDeckEditorVisualTabWidget::cardAdded, this, &TabDeckEditorVisual::addCard); + connect(tabContainer, &TabDeckEditorVisualTabWidget::cardDecremented, this, &TabDeckEditorVisual::decrementCard); + connect(tabContainer, &TabDeckEditorVisualTabWidget::edhrecRequested, this, &TabDeckEditorVisual::openEdhrecTab); + connect(tabContainer, &TabDeckEditorVisualTabWidget::printingSelectorRequested, this, + &TabDeckEditorVisual::showPrintingSelector); + connect(tabContainer, &TabDeckEditorVisualTabWidget::cardInfoRequested, this, &TabDeckEditorVisual::updateCardInfo); centralFrame->addWidget(tabContainer); setCentralWidget(centralWidget); @@ -143,12 +153,10 @@ void TabDeckEditorVisual::changeModelIndexToCard(const ExactCard &activeCard) } } -void TabDeckEditorVisual::processMainboardCardClick(QMouseEvent *event, - CardInfoPictureWithTextOverlayWidget *instance, +void TabDeckEditorVisual::processMainboardCardClick(const QMouseEvent *event, + const ExactCard &card, const QString &zoneName) { - auto card = instance->getCard(); - // Get the model index for the card QModelIndex idx = deckStateManager->getModel()->findCard(card.getName(), zoneName); if (!idx.isValid()) { @@ -168,22 +176,14 @@ void TabDeckEditorVisual::processMainboardCardClick(QMouseEvent *event, // Alt + Right-click = decrement if (event->button() == Qt::RightButton && event->modifiers().testFlag(Qt::AltModifier)) { - if (zoneName == DECK_ZONE_MAIN) { - actDecrementCard(card); - } else { - actDecrementCardFromSideboard(card); - } + decrementCard(card, zoneName); // Keep selection intact. return; } // Alt + Left click = increment if (event->button() == Qt::LeftButton && event->modifiers().testFlag(Qt::AltModifier)) { - if (zoneName == DECK_ZONE_MAIN) { - actAddCard(card); - } else { - actAddCardToSideboard(card); - } + addCard(card, zoneName); // Keep selection intact. return; } @@ -219,13 +219,16 @@ void TabDeckEditorVisual::processMainboardCardClick(QMouseEvent *event, } /** @brief Handles clicks on cards in the database display. */ -void TabDeckEditorVisual::processCardClickDatabaseDisplay(QMouseEvent *event, - CardInfoPictureWithTextOverlayWidget *instance) +void TabDeckEditorVisual::processDatabaseCardClick(const QMouseEvent *event, const ExactCard &card) { if (event->button() == Qt::LeftButton) { - actAddCard(instance->getCard()); + if (QApplication::keyboardModifiers() & Qt::ControlModifier) { + addCard(card, DECK_ZONE_SIDE); + } else { + addCard(card, DECK_ZONE_MAIN); + } } else if (event->button() == Qt::RightButton) { - actDecrementCard(instance->getCard()); + decrementCard(card, DECK_ZONE_MAIN); } else if (event->button() == Qt::MiddleButton) { deckDockWidget->actRemoveCard(); } @@ -240,14 +243,6 @@ bool TabDeckEditorVisual::actSaveDeckAs() return result; } -/** @brief Shows the printing selector dock and updates it with the current card. */ -void TabDeckEditorVisual::showPrintingSelector() -{ - printingSelectorDockWidget->printingSelector->setCard(cardInfoDockWidget->cardInfo->getCard().getCardPtr()); - printingSelectorDockWidget->printingSelector->updateDisplay(); - printingSelectorDockWidget->setVisible(true); -} - /** @brief Refreshes keyboard shortcuts for this tab from settings. */ void TabDeckEditorVisual::refreshShortcuts() { diff --git a/cockatrice/src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual.h b/cockatrice/src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual.h index 8a0677c9d..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 @@ -41,7 +41,7 @@ * - changeModelIndexAndCardInfo(const ExactCard &card) — Updates deck model selection and card info. * - changeModelIndexToCard(const ExactCard &card) — Selects the card in the deck view. * - processMainboardCardClick(QMouseEvent *event, ...) — Handles clicks on mainboard cards. - * - processCardClickDatabaseDisplay(QMouseEvent *event, ...) — Handles clicks on database cards. + * - processDatabaseCardClick(QMouseEvent *event, ...) — Handles clicks on database cards. * - actSaveDeckAs() — Overrides save action with temporary UI adjustments. * - showPrintingSelector() — Opens the printing selector dock for the current card. * - freeDocksSize() — Frees constraints on dock widget sizes. @@ -144,27 +144,20 @@ public slots: */ void onDeckChanged() override; - /** - * @brief Show the printing selector dock for the currently active card. - */ - void showPrintingSelector() override; - /** * @brief Handle card clicks in the mainboard visual deck. * @param event Mouse event triggering the action. - * @param instance Widget representing the clicked card. + * @param card The clicked card. * @param zoneName Deck zone of the card. */ - void processMainboardCardClick(QMouseEvent *event, - CardInfoPictureWithTextOverlayWidget *instance, - const QString &zoneName); + void processMainboardCardClick(const QMouseEvent *event, const ExactCard &card, const QString &zoneName); /** * @brief Handle card clicks in the database visual display. * @param event Mouse event triggering the action. - * @param instance Widget representing the clicked card. + * @param card The clicked card. */ - void processCardClickDatabaseDisplay(QMouseEvent *event, CardInfoPictureWithTextOverlayWidget *instance); + void processDatabaseCardClick(const QMouseEvent *event, const ExactCard &card); /** * @brief Save the deck under a new name. 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 8a4d5903d..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 @@ -9,7 +9,6 @@ * @param _deckEditor Pointer to the associated deck editor. * @param _deckModel Pointer to the deck list model. * @param _cardDatabaseModel Pointer to the card database model. - * @param _cardDatabaseDisplayModel Pointer to the card database display model. * * Initializes all sub-widgets (visual deck view, database display, deck analytics, * sample hand) and sets up the tab layout and signal connections. @@ -17,10 +16,8 @@ TabDeckEditorVisualTabWidget::TabDeckEditorVisualTabWidget(QWidget *parent, AbstractTabDeckEditor *_deckEditor, DeckListModel *_deckModel, - CardDatabaseModel *_cardDatabaseModel, - CardDatabaseDisplayModel *_cardDatabaseDisplayModel) - : QTabWidget(parent), deckEditor(_deckEditor), deckModel(_deckModel), cardDatabaseModel(_cardDatabaseModel), - cardDatabaseDisplayModel(_cardDatabaseDisplayModel) + CardDatabaseModel *_cardDatabaseModel) + : QTabWidget(parent), deckEditor(_deckEditor), deckModel(_deckModel), cardDatabaseModel(_cardDatabaseModel) { this->setTabsClosable(true); // Enable tab closing connect(this, &QTabWidget::tabCloseRequested, this, &TabDeckEditorVisualTabWidget::handleTabClose); @@ -34,16 +31,25 @@ TabDeckEditorVisualTabWidget::TabDeckEditorVisualTabWidget(QWidget *parent, &TabDeckEditorVisualTabWidget::onCardChanged); connect(visualDeckView, &VisualDeckEditorWidget::cardClicked, this, &TabDeckEditorVisualTabWidget::onCardClickedDeckEditor); - connect(visualDeckView, &VisualDeckEditorWidget::cardAdditionRequested, deckEditor, - &AbstractTabDeckEditor::actAddCard); + connect(visualDeckView, &VisualDeckEditorWidget::cardAdditionRequested, this, + &TabDeckEditorVisualTabWidget::actAddCard); - visualDatabaseDisplay = - new VisualDatabaseDisplayWidget(this, deckEditor, _cardDatabaseModel, _cardDatabaseDisplayModel); + visualDatabaseDisplay = new VisualDatabaseDisplayWidget(this, _cardDatabaseModel, deckModel); visualDatabaseDisplay->setObjectName("visualDatabaseView"); connect(visualDatabaseDisplay, &VisualDatabaseDisplayWidget::cardHoveredDatabaseDisplay, this, &TabDeckEditorVisualTabWidget::onCardChangedDatabaseDisplay); connect(visualDatabaseDisplay, &VisualDatabaseDisplayWidget::cardClickedDatabaseDisplay, this, &TabDeckEditorVisualTabWidget::onCardClickedDatabaseDisplay); + connect(visualDatabaseDisplay, &VisualDatabaseDisplayWidget::cardAdded, this, + &TabDeckEditorVisualTabWidget::cardAdded); + connect(visualDatabaseDisplay, &VisualDatabaseDisplayWidget::cardDecremented, this, + &TabDeckEditorVisualTabWidget::cardDecremented); + connect(visualDatabaseDisplay, &VisualDatabaseDisplayWidget::edhrecRequested, this, + &TabDeckEditorVisualTabWidget::edhrecRequested); + connect(visualDatabaseDisplay, &VisualDatabaseDisplayWidget::printingSelectorRequested, this, + &TabDeckEditorVisualTabWidget::printingSelectorRequested); + connect(visualDatabaseDisplay, &VisualDatabaseDisplayWidget::cardInfoRequested, this, + &TabDeckEditorVisualTabWidget::cardInfoRequested); statsAnalyzer = new DeckListStatisticsAnalyzer(this, deckModel); statsAnalyzer->analyze(); @@ -82,25 +88,24 @@ void TabDeckEditorVisualTabWidget::onCardChangedDatabaseDisplay(const ExactCard /** * @brief Emits the cardClicked signal when a card is clicked in the visual deck view. * @param event The mouse event. - * @param instance The widget instance of the clicked card. + * @param card The clicked card. * @param zoneName The zone of the deck where the card is located. */ void TabDeckEditorVisualTabWidget::onCardClickedDeckEditor(QMouseEvent *event, - CardInfoPictureWithTextOverlayWidget *instance, - QString zoneName) + const ExactCard &card, + const QString &zoneName) { - emit cardClicked(event, instance, zoneName); + emit cardClicked(event, card, zoneName); } /** * @brief Emits the cardClickedDatabaseDisplay signal when a card is clicked in the database display. * @param event The mouse event. - * @param instance The widget instance of the clicked card. + * @param card The clicked card. */ -void TabDeckEditorVisualTabWidget::onCardClickedDatabaseDisplay(QMouseEvent *event, - CardInfoPictureWithTextOverlayWidget *instance) +void TabDeckEditorVisualTabWidget::onCardClickedDatabaseDisplay(QMouseEvent *event, const ExactCard &card) { - emit cardClickedDatabaseDisplay(event, instance); + emit cardClickedDatabaseDisplay(event, card); } /** @@ -166,3 +171,15 @@ void TabDeckEditorVisualTabWidget::handleTabClose(int index) this->removeTab(index); delete tab; } + +void TabDeckEditorVisualTabWidget::actAddCard(const ExactCard &card) +{ + QString zoneName; + if (QApplication::keyboardModifiers() & Qt::ControlModifier) { + zoneName = DECK_ZONE_SIDE; + } else { + zoneName = DECK_ZONE_MAIN; + } + + deckEditor->addCard(card, zoneName); +} 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 48dd8ea9d..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 @@ -55,13 +55,11 @@ public: * @param _deckEditor Pointer to the deck editor instance. * @param _deckModel Deck list model. * @param _cardDatabaseModel Card database model. - * @param _cardDatabaseDisplayModel Database display model. */ explicit TabDeckEditorVisualTabWidget(QWidget *parent, AbstractTabDeckEditor *_deckEditor, DeckListModel *_deckModel, - CardDatabaseModel *_cardDatabaseModel, - CardDatabaseDisplayModel *_cardDatabaseDisplayModel); + CardDatabaseModel *_cardDatabaseModel); /** @brief Add a new tab with a widget and title. */ void addNewTab(QWidget *widget, const QString &title); @@ -101,30 +99,35 @@ public slots: /** * @brief Emitted when a card is clicked in the deck view. * @param event Mouse event. - * @param instance Widget representing the clicked card. + * @param card The clicked card. * @param zoneName Deck zone of the card. */ - void onCardClickedDeckEditor(QMouseEvent *event, CardInfoPictureWithTextOverlayWidget *instance, QString zoneName); + void onCardClickedDeckEditor(QMouseEvent *event, const ExactCard &card, const QString &zoneName); /** * @brief Emitted when a card is clicked in the database display. * @param event Mouse event. - * @param instance Widget representing the clicked card. + * @param card The clicked card. */ - void onCardClickedDatabaseDisplay(QMouseEvent *event, CardInfoPictureWithTextOverlayWidget *instance); + void onCardClickedDatabaseDisplay(QMouseEvent *event, const ExactCard &card); signals: void cardChanged(const ExactCard &activeCard); void cardChangedDatabaseDisplay(const ExactCard &activeCard); - void cardClicked(QMouseEvent *event, CardInfoPictureWithTextOverlayWidget *instance, QString zoneName); - void cardClickedDatabaseDisplay(QMouseEvent *event, CardInfoPictureWithTextOverlayWidget *instance); + void cardClicked(QMouseEvent *event, const ExactCard &card, const QString &zoneName); + void cardClickedDatabaseDisplay(QMouseEvent *event, const ExactCard &card); + + void cardAdded(const ExactCard &card, const QString &zoneName); + void cardDecremented(const ExactCard &card, const QString &zoneName); + void edhrecRequested(const CardInfoPtr &cardInfo, bool isCommander); + void printingSelectorRequested(); + void cardInfoRequested(const ExactCard &cardName); private: - QVBoxLayout *layout; ///< Layout for tabs and controls. - AbstractTabDeckEditor *deckEditor; ///< Reference to the deck editor. - DeckListModel *deckModel; ///< Deck list model. - CardDatabaseModel *cardDatabaseModel; ///< Card database model. - CardDatabaseDisplayModel *cardDatabaseDisplayModel; ///< Card database display model. + QVBoxLayout *layout; ///< Layout for tabs and controls. + AbstractTabDeckEditor *deckEditor; ///< Reference to the deck editor. + DeckListModel *deckModel; ///< Deck list model. + CardDatabaseModel *cardDatabaseModel; ///< Card database model. private slots: /** @@ -132,6 +135,12 @@ private slots: * @param index Index of the tab to close. */ void handleTabClose(int index); + + /** + * @brief Adds card to maindeck or side depending on whether ctrl is held + * @param card + */ + void actAddCard(const ExactCard &card); }; #endif // TAB_DECK_EDITOR_VISUAL_TAB_WIDGET_H \ No newline at end of file diff --git a/cockatrice/src/interface/widgets/utility/get_text_with_max.h b/cockatrice/src/interface/widgets/utility/get_text_with_max.h index 923d6f427..424bb0c6a 100644 --- a/cockatrice/src/interface/widgets/utility/get_text_with_max.h +++ b/cockatrice/src/interface/widgets/utility/get_text_with_max.h @@ -9,7 +9,7 @@ #include #include -#include +#include QString getTextWithMax(QWidget *parent, const QString &title, 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 0c1280009..62e1bf5ba 100644 --- a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_filter_toolbar_widget.cpp +++ b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_filter_toolbar_widget.cpp @@ -1,12 +1,14 @@ #include "visual_database_display_filter_toolbar_widget.h" +#include "../deck_editor/card_database_view.h" #include "visual_database_display_widget.h" #include -VisualDatabaseDisplayFilterToolbarWidget::VisualDatabaseDisplayFilterToolbarWidget(VisualDatabaseDisplayWidget *_parent) +VisualDatabaseDisplayFilterToolbarWidget::VisualDatabaseDisplayFilterToolbarWidget(VisualDatabaseDisplayWidget *_parent, + DeckListModel *deckListModel) : FlowWidget(_parent, Qt::Horizontal, Qt::ScrollBarAlwaysOff, Qt::ScrollBarAlwaysOff), - visualDatabaseDisplay(_parent) + visualDatabaseDisplay(_parent), deckListModel(deckListModel) { connect(this, &VisualDatabaseDisplayFilterToolbarWidget::searchModelChanged, visualDatabaseDisplay, &VisualDatabaseDisplayWidget::onSearchModelChanged); @@ -97,8 +99,7 @@ void VisualDatabaseDisplayFilterToolbarWidget::initialize() auto filterModel = visualDatabaseDisplay->getFilterModel(); saveLoadWidget = new VisualDatabaseDisplayFilterSaveLoadWidget(this, filterModel); - nameFilterWidget = - new VisualDatabaseDisplayNameFilterWidget(this, visualDatabaseDisplay->getDeckEditor(), filterModel); + nameFilterWidget = new VisualDatabaseDisplayNameFilterWidget(this, filterModel, deckListModel); mainTypeFilterWidget = new VisualDatabaseDisplayMainTypeFilterWidget(this, filterModel); formatLegalityWidget = new VisualDatabaseDisplayFormatLegalityFilterWidget(this, filterModel); subTypeFilterWidget = new VisualDatabaseDisplaySubTypeFilterWidget(this, filterModel); diff --git a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_filter_toolbar_widget.h b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_filter_toolbar_widget.h index 5b55f4ba6..8a3555455 100644 --- a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_filter_toolbar_widget.h +++ b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_filter_toolbar_widget.h @@ -18,12 +18,14 @@ signals: void searchModelChanged(); public: - explicit VisualDatabaseDisplayFilterToolbarWidget(VisualDatabaseDisplayWidget *parent); + explicit VisualDatabaseDisplayFilterToolbarWidget(VisualDatabaseDisplayWidget *parent, + DeckListModel *deckListModel = nullptr); void initialize(); void retranslateUi(); private: VisualDatabaseDisplayWidget *visualDatabaseDisplay; + DeckListModel *deckListModel; QGroupBox *sortGroupBox; QLabel *sortByLabel; diff --git a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_name_filter_widget.cpp b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_name_filter_widget.cpp index fd03e17e6..3fa1a782a 100644 --- a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_name_filter_widget.cpp +++ b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_name_filter_widget.cpp @@ -8,9 +8,9 @@ #include VisualDatabaseDisplayNameFilterWidget::VisualDatabaseDisplayNameFilterWidget(QWidget *parent, - AbstractTabDeckEditor *_deckEditor, - FilterTreeModel *_filterModel) - : QWidget(parent), deckEditor(_deckEditor), filterModel(_filterModel) + FilterTreeModel *_filterModel, + DeckListModel *deckListModel) + : QWidget(parent), filterModel(_filterModel), deckListModel(deckListModel) { setMinimumWidth(300); setMaximumHeight(300); @@ -62,8 +62,6 @@ void VisualDatabaseDisplayNameFilterWidget::retranslateUi() void VisualDatabaseDisplayNameFilterWidget::actLoadFromDeck() { - DeckListModel *deckListModel = deckEditor->deckStateManager->getModel(); - if (!deckListModel) { return; } diff --git a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_name_filter_widget.h b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_name_filter_widget.h index 5a8438a05..0c10408ae 100644 --- a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_name_filter_widget.h +++ b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_name_filter_widget.h @@ -21,8 +21,8 @@ class VisualDatabaseDisplayNameFilterWidget : public QWidget Q_OBJECT public: explicit VisualDatabaseDisplayNameFilterWidget(QWidget *parent, - AbstractTabDeckEditor *deckEditor, - FilterTreeModel *filterModel); + FilterTreeModel *filterModel, + DeckListModel *deckListModel = nullptr); void createNameFilter(const QString &name); void removeNameFilter(const QString &name); @@ -34,8 +34,8 @@ public slots: void retranslateUi(); private: - AbstractTabDeckEditor *deckEditor; FilterTreeModel *filterModel; + DeckListModel *deckListModel; QVBoxLayout *layout; QLineEdit *searchBox; FlowWidget *flowWidget; 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 70218d478..cc4cce496 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 @@ -5,7 +5,7 @@ #include "../../../filters/syntax_help.h" #include "../../pixel_map_generator.h" #include "../cards/card_info_picture_with_text_overlay_widget.h" -#include "../quick_settings/settings_button_widget.h" +#include "../deck_editor/card_database_view.h" #include "../utility/custom_line_edit.h" #include "visual_database_display_color_filter_widget.h" #include "visual_database_display_filter_save_load_widget.h" @@ -23,17 +23,21 @@ #include VisualDatabaseDisplayWidget::VisualDatabaseDisplayWidget(QWidget *parent, - AbstractTabDeckEditor *_deckEditor, CardDatabaseModel *database_model, - CardDatabaseDisplayModel *database_display_model) - : QWidget(parent), deckEditor(_deckEditor), databaseModel(database_model), - databaseDisplayModel(database_display_model) + DeckListModel *deckListModel) + : QWidget(parent) { debounceTimer = new QTimer(this); debounceTimer->setSingleShot(true); // Ensure it only fires once after the timeout connect(debounceTimer, &QTimer::timeout, this, &VisualDatabaseDisplayWidget::onSearchModelChanged); + // Create display model + databaseDisplayModel = new CardDatabaseDisplayModel(this); + databaseDisplayModel->setObjectName("databaseDisplayModel"); + databaseDisplayModel->setSourceModel(database_model); + databaseDisplayModel->setFilterKeyColumn(0); + cards = new QList; connect(databaseDisplayModel, &CardDatabaseDisplayModel::modelDirty, this, &VisualDatabaseDisplayWidget::modelDirty); @@ -60,7 +64,6 @@ VisualDatabaseDisplayWidget::VisualDatabaseDisplayWidget(QWidget *parent, searchEdit->addAction(loadColorAdjustedPixmap("theme:icons/search"), QLineEdit::LeadingPosition); auto help = searchEdit->addAction(QPixmap("theme:icons/info"), QLineEdit::TrailingPosition); connect(help, &QAction::triggered, this, [this] { createSearchSyntaxHelpWindow(searchEdit); }); - searchEdit->installEventFilter(&searchKeySignals); setFocusProxy(searchEdit); setFocusPolicy(Qt::ClickFocus); @@ -75,43 +78,29 @@ VisualDatabaseDisplayWidget::VisualDatabaseDisplayWidget(QWidget *parent, filterModel = new FilterTreeModel(); filterModel->setObjectName("filterModel"); - searchKeySignals.setObjectName("searchKeySignals"); - connect(searchEdit, &SearchLineEdit::textChanged, this, &VisualDatabaseDisplayWidget::updateSearch); + connect(searchEdit, &SearchLineEdit::textChanged, databaseDisplayModel, &CardDatabaseDisplayModel::setStringFilter); - DeckEditorDatabaseDisplayWidget *databaseDisplayWidget = deckEditor->cardDatabaseDockWidget->databaseDisplayWidget; - connect(&searchKeySignals, &KeySignals::onEnter, databaseDisplayWidget, - &DeckEditorDatabaseDisplayWidget::actAddCardToMainDeck); - connect(&searchKeySignals, &KeySignals::onCtrlAltEqual, databaseDisplayWidget, - &DeckEditorDatabaseDisplayWidget::actAddCardToMainDeck); - connect(&searchKeySignals, &KeySignals::onCtrlAltRBracket, databaseDisplayWidget, - &DeckEditorDatabaseDisplayWidget::actAddCardToSideboard); - connect(&searchKeySignals, &KeySignals::onCtrlAltMinus, databaseDisplayWidget, - &DeckEditorDatabaseDisplayWidget::actDecrementCardFromMainDeck); - connect(&searchKeySignals, &KeySignals::onCtrlAltLBracket, databaseDisplayWidget, - &DeckEditorDatabaseDisplayWidget::actDecrementCardFromSideboard); - connect(&searchKeySignals, &KeySignals::onCtrlAltEnter, databaseDisplayWidget, - &DeckEditorDatabaseDisplayWidget::actAddCardToSideboard); - connect(&searchKeySignals, &KeySignals::onCtrlEnter, databaseDisplayWidget, - &DeckEditorDatabaseDisplayWidget::actAddCardToSideboard); - connect(&searchKeySignals, &KeySignals::onCtrlC, databaseDisplayWidget, - &DeckEditorDatabaseDisplayWidget::copyDatabaseCellContents); - connect(help, &QAction::triggered, this, [this] { createSearchSyntaxHelpWindow(searchEdit); }); - - connect(databaseDisplayWidget, &DeckEditorDatabaseDisplayWidget::addCardToMainDeck, this, - &VisualDatabaseDisplayWidget::highlightAllSearchEdit); - connect(databaseDisplayWidget, &DeckEditorDatabaseDisplayWidget::addCardToSideboard, this, - &VisualDatabaseDisplayWidget::highlightAllSearchEdit); - - databaseView = databaseDisplayWidget->getDatabaseView(); + databaseView = new CardDatabaseView(this, databaseDisplayModel); + databaseView->setObjectName("databaseView"); databaseView->setFocusProxy(searchEdit); databaseView->setItemDelegate(nullptr); databaseView->setVisible(false); searchEdit->setTreeView(databaseView); + searchEdit->installEventFilter(databaseView->getKeySignals()); + + connect(databaseView, &CardDatabaseView::cardChanged, this, &VisualDatabaseDisplayWidget::onSelectedCardChanged); + connect(databaseView, &CardDatabaseView::cardAdded, this, &VisualDatabaseDisplayWidget::actAddCard); + connect(databaseView, &CardDatabaseView::cardDecremented, this, &VisualDatabaseDisplayWidget::actDecrementCard); + connect(databaseView, &CardDatabaseView::edhrecClicked, this, &VisualDatabaseDisplayWidget::edhrecRequested); + connect(databaseView, &CardDatabaseView::selectPrintingClicked, this, + &VisualDatabaseDisplayWidget::printingSelectorRequested); + connect(databaseView, &CardDatabaseView::relatedCardClicked, this, + &VisualDatabaseDisplayWidget::onRelatedCardClicked); colorFilterWidget = new VisualDatabaseDisplayColorFilterWidget(this, filterModel); - filterContainer = new VisualDatabaseDisplayFilterToolbarWidget(this); + filterContainer = new VisualDatabaseDisplayFilterToolbarWidget(this, deckListModel); clearFilterWidget = new QToolButton(); clearFilterWidget->setFixedSize(32, 32); @@ -216,9 +205,9 @@ void VisualDatabaseDisplayWidget::onDisplayModeChanged(bool checked) } } -void VisualDatabaseDisplayWidget::onClick(QMouseEvent *event, CardInfoPictureWithTextOverlayWidget *instance) +void VisualDatabaseDisplayWidget::onClick(QMouseEvent *event, const ExactCard &card) { - emit cardClickedDatabaseDisplay(event, instance); + emit cardClickedDatabaseDisplay(event, card); } void VisualDatabaseDisplayWidget::onHover(const ExactCard &hoveredCard) @@ -226,28 +215,18 @@ void VisualDatabaseDisplayWidget::onHover(const ExactCard &hoveredCard) emit cardHoveredDatabaseDisplay(hoveredCard); } -void VisualDatabaseDisplayWidget::addCard(const ExactCard &cardToAdd) +void VisualDatabaseDisplayWidget::addCardToDisplay(const ExactCard &cardToAdd) { cards->append(cardToAdd); auto *display = new CardInfoPictureWithTextOverlayWidget(flowWidget, false); display->setScaleFactor(cardSizeWidget->getSlider()->value()); display->setCard(cardToAdd); flowWidget->addWidget(display); - connect(display, &CardInfoPictureWithTextOverlayWidget::imageClicked, this, &VisualDatabaseDisplayWidget::onClick); + connect(display, &CardInfoPictureWithTextOverlayWidget::cardClicked, this, &VisualDatabaseDisplayWidget::onClick); connect(display, &CardInfoPictureWithTextOverlayWidget::hoveredOnCard, this, &VisualDatabaseDisplayWidget::onHover); connect(cardSizeWidget->getSlider(), &QSlider::valueChanged, display, &CardInfoPictureWidget::setScaleFactor); } -void VisualDatabaseDisplayWidget::updateSearch(const QString &search) const -{ - databaseDisplayModel->setStringFilter(search); - QModelIndexList sel = databaseView->selectionModel()->selectedRows(); - if (sel.isEmpty() && databaseDisplayModel->rowCount()) { - databaseView->selectionModel()->setCurrentIndex(databaseDisplayModel->index(0, 0), - QItemSelectionModel::SelectCurrent | QItemSelectionModel::Rows); - } -} - bool VisualDatabaseDisplayWidget::isVisualDisplayMode() const { return !displayModeButton->isChecked(); @@ -269,6 +248,30 @@ void VisualDatabaseDisplayWidget::onSearchModelChanged() } } +void VisualDatabaseDisplayWidget::onSelectedCardChanged(const QString &cardName) +{ + emit cardHoveredDatabaseDisplay(CardDatabaseManager::query()->getPreferredCard(cardName)); +} + +void VisualDatabaseDisplayWidget::actAddCard(const QString &cardName, const QString &zoneName) +{ + highlightAllSearchEdit(); + ExactCard exactCard = CardDatabaseManager::query()->getPreferredCard(cardName); + emit cardAdded(exactCard, zoneName); +} + +void VisualDatabaseDisplayWidget::actDecrementCard(const QString &cardName, const QString &zoneName) +{ + ExactCard exactCard = CardDatabaseManager::query()->getPreferredCard(cardName); + emit cardDecremented(exactCard, zoneName); +} + +void VisualDatabaseDisplayWidget::onRelatedCardClicked(const QString &relatedCard) +{ + ExactCard exactCard = CardDatabaseManager::query()->guessCard({relatedCard}); + emit cardInfoRequested(exactCard); +} + bool VisualDatabaseDisplayWidget::nearEndOfPage() const { if (!flowWidget->isVisible()) { @@ -335,12 +338,12 @@ void VisualDatabaseDisplayWidget::loadPage(int start, int end) for (const CardFilter *setFilter : setFilters) { if (setMap.contains(setFilter->term())) { for (PrintingInfo printing : setMap[setFilter->term()]) { - addCard(ExactCard(info, printing)); + addCardToDisplay(ExactCard(info, printing)); } } } } else { - addCard(CardDatabaseManager::query()->getPreferredCard(info)); + addCardToDisplay(CardDatabaseManager::query()->getPreferredCard(info)); } } else { qCDebug(VisualDatabaseDisplayLog) << "Card not found in database!"; 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 48a026d11..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 @@ -34,9 +34,8 @@ class VisualDatabaseDisplayWidget : public QWidget public: explicit VisualDatabaseDisplayWidget(QWidget *parent, - AbstractTabDeckEditor *deckEditor, CardDatabaseModel *database_model, - CardDatabaseDisplayModel *database_display_model); + DeckListModel *deckListModel = nullptr); void retranslateUi(); void adjustCardsPerPage(); @@ -47,17 +46,12 @@ public: void sortCardList(const QStringList &properties, Qt::SortOrder order) const; void setDeckList(const DeckList &new_deck_list_model); - AbstractTabDeckEditor *getDeckEditor() - { - return deckEditor; - } - CardDatabaseDisplayModel *getDatabaseDisplayModel() { return databaseDisplayModel; } - QTreeView *getDatabaseView() + CardDatabaseView *getDatabaseView() { return databaseView; } @@ -76,19 +70,29 @@ public slots: void onSearchModelChanged(); signals: - void cardClickedDatabaseDisplay(QMouseEvent *event, CardInfoPictureWithTextOverlayWidget *instance); + void cardClickedDatabaseDisplay(QMouseEvent *event, const ExactCard &card); void cardHoveredDatabaseDisplay(const ExactCard &hoveredCard); + void cardAdded(const ExactCard &card, const QString &zoneName); + void cardDecremented(const ExactCard &card, const QString &zoneName); + void edhrecRequested(const CardInfoPtr &cardInfo, bool isCommander); + void printingSelectorRequested(); + void cardInfoRequested(const ExactCard &cardName); + protected slots: void initialize(); - void onClick(QMouseEvent *event, CardInfoPictureWithTextOverlayWidget *instance); + void onClick(QMouseEvent *event, const ExactCard &card); void onHover(const ExactCard &hoveredCard); - void addCard(const ExactCard &cardToAdd); + void addCardToDisplay(const ExactCard &cardToAdd); void databaseDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight); void modelDirty() const; - void updateSearch(const QString &search) const; void onDisplayModeChanged(bool checked); + void onSelectedCardChanged(const QString &cardName); + void actAddCard(const QString &cardName, const QString &zoneName); + void actDecrementCard(const QString &cardName, const QString &zoneName); + void onRelatedCardClicked(const QString &relatedCard); + private: FlowWidget *searchContainer; SearchLineEdit *searchEdit; @@ -100,11 +104,8 @@ private: QToolButton *clearFilterWidget; VisualDatabaseDisplayFilterToolbarWidget *filterContainer; - KeySignals searchKeySignals; - AbstractTabDeckEditor *deckEditor; - CardDatabaseModel *databaseModel; CardDatabaseDisplayModel *databaseDisplayModel; - QTreeView *databaseView; + CardDatabaseView *databaseView; QList *cards; QVBoxLayout *mainLayout; QScrollArea *scrollArea; 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 4a67edcd1..815892f4c 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 @@ -281,7 +281,7 @@ void VisualDeckEditorWidget::constructZoneWidgetForIndex(QPersistentModelIndex p displayOptionsWidget->getActiveGroupCriteria(), displayOptionsWidget->getActiveSortCriteria(), displayOptionsWidget->getDisplayType(), 20, 10, cardSizeWidget); connect(zoneDisplayWidget, &DeckCardZoneDisplayWidget::cardHovered, this, &VisualDeckEditorWidget::onHover); - connect(zoneDisplayWidget, &DeckCardZoneDisplayWidget::cardClicked, this, &VisualDeckEditorWidget::onCardClick); + connect(zoneDisplayWidget, &DeckCardZoneDisplayWidget::cardClicked, this, &VisualDeckEditorWidget::cardClicked); connect(zoneDisplayWidget, &DeckCardZoneDisplayWidget::requestCleanup, this, &VisualDeckEditorWidget::cleanupInvalidZones); connect(this, &VisualDeckEditorWidget::activeSortCriteriaChanged, zoneDisplayWidget, @@ -401,13 +401,6 @@ void VisualDeckEditorWidget::decklistDataChanged(QModelIndex topLeft, QModelInde // User Interaction // ===================================================================================================================== -void VisualDeckEditorWidget::onCardClick(QMouseEvent *event, - CardInfoPictureWithTextOverlayWidget *instance, - QString zoneName) -{ - emit cardClicked(event, instance, zoneName); -} - void VisualDeckEditorWidget::onHover(const ExactCard &hoveredCard) { // If user has any card selected, ignore hover 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 1af565b29..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 @@ -69,7 +69,7 @@ signals: void activeCardChanged(const ExactCard &activeCard); void activeGroupCriteriaChanged(QString activeGroupCriteria); void activeSortCriteriaChanged(QStringList activeSortCriteria); - void cardClicked(QMouseEvent *event, CardInfoPictureWithTextOverlayWidget *instance, QString zoneName); + void cardClicked(QMouseEvent *event, const ExactCard &card, const QString &zoneName); void cardAdditionRequested(const ExactCard &card); void displayTypeChanged(DisplayType displayType); @@ -82,7 +82,6 @@ protected: protected slots: void onHover(const ExactCard &hoveredCard); - void onCardClick(QMouseEvent *event, CardInfoPictureWithTextOverlayWidget *instance, QString zoneName); void decklistModelReset(); void resizeEvent(QResizeEvent *event) override; diff --git a/doc/carddatabase_v4/cards.xsd b/doc/carddatabase_v4/cards.xsd index 92d30b94f..59ca3e560 100644 --- a/doc/carddatabase_v4/cards.xsd +++ b/doc/carddatabase_v4/cards.xsd @@ -6,6 +6,7 @@ + diff --git a/format.sh b/format.sh index c43f537f5..83dee9e28 100755 --- a/format.sh +++ b/format.sh @@ -3,7 +3,7 @@ # This script will run clang-format on all modified, non-3rd-party C++/Header files. # Optionally runs cmake-format on all modified cmake files. # Optionally runs shellcheck on all modified shell files. -# Uses clang-format cmake-format git diff find shellcheck +# Uses clang-format, cmake-format, git, diff, find and shellcheck # Never, ever, should this receive a path with a newline in it. Don't bother proofing it for that. set -o pipefail @@ -103,11 +103,11 @@ OPTIONS: Do not check any source files for clang-format. --print-version - Print the version of clang-format being used before continuing. + Print the lint tool version being used before continuing. --shell - Use shellcheck to lint shell files. Not available in the default inline - mode. + Use shellcheck to lint shell files. + Not available in the default inline mode. -t, --test Do not edit files in place. Set exit code to 1 if changes are required. diff --git a/libcockatrice_card/libcockatrice/card/database/card_database.cpp b/libcockatrice_card/libcockatrice/card/database/card_database.cpp index 951381aa4..261b36690 100644 --- a/libcockatrice_card/libcockatrice/card/database/card_database.cpp +++ b/libcockatrice_card/libcockatrice/card/database/card_database.cpp @@ -85,14 +85,15 @@ void CardDatabase::refreshCachedReverseRelatedCards() for (auto *rel : card->getReverseRelatedCards()) { if (auto target = cards.value(rel->getName())) { auto *newRel = new CardRelation(card->getName(), rel->getAttachType(), rel->getIsCreateAllExclusion(), - rel->getIsVariable(), rel->getDefaultCount(), rel->getIsPersistent()); + rel->getIsVariable(), rel->getDefaultCount(), rel->getIsPersistent(), + rel->getIsFaceDown()); target->addReverseRelatedCards2Me(newRel); } } } } -void CardDatabase::addCard(CardInfoPtr card) +void CardDatabase::addCard(const CardInfoPtr &card) { if (card == nullptr) { qCWarning(CardDatabaseLog) << "CardDatabase::addCard(nullptr)"; @@ -118,7 +119,7 @@ void CardDatabase::addCard(CardInfoPtr card) emit cardAdded(card); } -void CardDatabase::removeCard(CardInfoPtr card) +void CardDatabase::removeCard(const CardInfoPtr &card) { if (card.isNull()) { qCWarning(CardDatabaseLog) << "CardDatabase::removeCard(nullptr)"; @@ -143,7 +144,7 @@ void CardDatabase::removeCard(CardInfoPtr card) emit cardRemoved(card); } -void CardDatabase::addSet(CardSetPtr set) +void CardDatabase::addSet(const CardSetPtr &set) { sets.insert(set->getShortName(), set); } @@ -215,7 +216,7 @@ void CardDatabase::notifyEnabledSetsChanged() emit cardDatabaseEnabledSetsChanged(); } -void CardDatabase::addFormat(FormatRulesPtr format) +void CardDatabase::addFormat(const FormatRulesPtr &format) { formats.insert(format->formatName.toLower(), format); -} \ No newline at end of file +} diff --git a/libcockatrice_card/libcockatrice/card/database/card_database.h b/libcockatrice_card/libcockatrice/card/database/card_database.h index 521be8fbc..44838962d 100644 --- a/libcockatrice_card/libcockatrice/card/database/card_database.h +++ b/libcockatrice_card/libcockatrice/card/database/card_database.h @@ -88,7 +88,7 @@ public: * @brief Removes a card from the database. * @param card Pointer to the card to remove. */ - void removeCard(CardInfoPtr card); + void removeCard(const CardInfoPtr &card); /** @brief Clears all cards, sets, and internal state. */ void clear(); @@ -140,15 +140,15 @@ public slots: * @brief Adds a card to the database. * @param card CardInfoPtr to add. */ - void addCard(CardInfoPtr card); + void addCard(const CardInfoPtr &card); /** * @brief Adds a set to the database. * @param set Pointer to CardSet to add. */ - void addSet(CardSetPtr set); + void addSet(const CardSetPtr &set); - void addFormat(FormatRulesPtr format); + void addFormat(const FormatRulesPtr &format); /** @brief Loads card databases from configured paths. */ void loadCardDatabases(); 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 ba27d63c4..b6c3afc57 100644 --- a/libcockatrice_card/libcockatrice/card/database/parser/cockatrice_xml_3.cpp +++ b/libcockatrice_card/libcockatrice/card/database/parser/cockatrice_xml_3.cpp @@ -217,27 +217,32 @@ void CockatriceXml3Parser::loadCardsFromXml(QXmlStreamReader &xml) // NOTE: attributes must be read before readElementText() QXmlStreamAttributes attrs = xml.attributes(); QString setName = xml.readElementText(QXmlStreamReader::IncludeChildElements); - PrintingInfo setInfo(internalAddSet(setName)); - if (attrs.hasAttribute("muId")) { - setInfo.setProperty("muid", attrs.value("muId").toString()); - } + auto set = internalAddSet(setName); + // Only load printings from sets the user has enabled, matching the v4 loader's + // behaviour. Without this check, disabling a set has no effect on v3 databases. + if (set->getEnabled()) { + PrintingInfo setInfo(set); + if (attrs.hasAttribute("muId")) { + setInfo.setProperty("muid", attrs.value("muId").toString()); + } - if (attrs.hasAttribute("muId")) { - setInfo.setProperty("uuid", attrs.value("uuId").toString()); - } + if (attrs.hasAttribute("uuId")) { + setInfo.setProperty("uuid", attrs.value("uuId").toString()); + } - if (attrs.hasAttribute("picURL")) { - setInfo.setProperty("picurl", attrs.value("picURL").toString()); - } + if (attrs.hasAttribute("picURL")) { + setInfo.setProperty("picurl", attrs.value("picURL").toString()); + } - if (attrs.hasAttribute("num")) { - setInfo.setProperty("num", attrs.value("num").toString()); - } + if (attrs.hasAttribute("num")) { + setInfo.setProperty("num", attrs.value("num").toString()); + } - if (attrs.hasAttribute("rarity")) { - setInfo.setProperty("rarity", attrs.value("rarity").toString()); + if (attrs.hasAttribute("rarity")) { + setInfo.setProperty("rarity", attrs.value("rarity").toString()); + } + _sets[setName].append(setInfo); } - _sets[setName].append(setInfo); // related cards } else if (xmlName == "related" || xmlName == "reverse-related") { CardRelationType attach = CardRelationType::DoesNotAttach; 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 96a5ac104..c242425ab 100644 --- a/libcockatrice_card/libcockatrice/card/database/parser/cockatrice_xml_4.cpp +++ b/libcockatrice_card/libcockatrice/card/database/parser/cockatrice_xml_4.cpp @@ -329,6 +329,7 @@ void CockatriceXml4Parser::loadCardsFromXml(QXmlStreamReader &xml) bool exclude = false; bool variable = false; bool persistent = false; + bool facedown = false; int count = 1; QXmlStreamAttributes attrs = xml.attributes(); QString cardName = xml.readElementText(QXmlStreamReader::IncludeChildElements); @@ -360,7 +361,12 @@ void CockatriceXml4Parser::loadCardsFromXml(QXmlStreamReader &xml) persistent = true; } - auto *relation = new CardRelation(cardName, attachType, exclude, variable, count, persistent); + if (attrs.hasAttribute("facedown")) { + facedown = true; + } + + auto *relation = + new CardRelation(cardName, attachType, exclude, variable, count, persistent, facedown); if (xmlName == "reverse-related") { reverseRelatedCards << relation; } else { @@ -510,6 +516,9 @@ static QXmlStreamWriter &operator<<(QXmlStreamWriter &xml, const CardInfoPtr &in if (i->getIsPersistent()) { xml.writeAttribute("persistent", "persistent"); } + if (i->getIsFaceDown()) { + xml.writeAttribute("facedown", "facedown"); + } if (i->getIsVariable()) { if (1 == i->getDefaultCount()) { xml.writeAttribute("count", "x"); diff --git a/libcockatrice_card/libcockatrice/card/relation/card_relation.cpp b/libcockatrice_card/libcockatrice/card/relation/card_relation.cpp index 90e59e439..8903c892d 100644 --- a/libcockatrice_card/libcockatrice/card/relation/card_relation.cpp +++ b/libcockatrice_card/libcockatrice/card/relation/card_relation.cpp @@ -7,8 +7,10 @@ CardRelation::CardRelation(const QString &_name, bool _isCreateAllExclusion, bool _isVariableCount, int _defaultCount, - bool _isPersistent) + bool _isPersistent, + bool _isFaceDown) : name(_name), attachType(_attachType), isCreateAllExclusion(_isCreateAllExclusion), - isVariableCount(_isVariableCount), defaultCount(_defaultCount), isPersistent(_isPersistent) + isVariableCount(_isVariableCount), defaultCount(_defaultCount), isPersistent(_isPersistent), + isFaceDown(_isFaceDown) { -} \ No newline at end of file +} diff --git a/libcockatrice_card/libcockatrice/card/relation/card_relation.h b/libcockatrice_card/libcockatrice/card/relation/card_relation.h index 9ff704097..a1864f5b2 100644 --- a/libcockatrice_card/libcockatrice/card/relation/card_relation.h +++ b/libcockatrice_card/libcockatrice/card/relation/card_relation.h @@ -31,6 +31,7 @@ private: bool isVariableCount; ///< True if the number of creations is variable. int defaultCount; ///< Default number of cards created or involved. bool isPersistent; ///< True if this relation persists (i.e. is not destroyed) on zone change. + bool isFaceDown; ///< True if this relation creates the tokens facedown public: /** @@ -42,13 +43,15 @@ public: * @param _isVariableCount Whether the count is variable. * @param _defaultCount Default number for creations or transformations. * @param _isPersistent Whether the relation persists across zone changes. + * @param _isFaceDown Whether the relation creates the token face down */ explicit CardRelation(const QString &_name = QString(), CardRelationType _attachType = CardRelationType::DoesNotAttach, bool _isCreateAllExclusion = false, bool _isVariableCount = false, int _defaultCount = 1, - bool _isPersistent = false); + bool _isPersistent = false, + bool _isFaceDown = false); /** * @brief Returns the name of the related card. @@ -151,6 +154,16 @@ public: { return isPersistent; } + + /** + * @brief Returns whether the relation creates the token facedown. + * + * @return True if facedown, false otherwise. + */ + [[nodiscard]] bool getIsFaceDown() const + { + return isFaceDown; + } }; #endif // COCKATRICE_CARD_RELATION_H diff --git a/libcockatrice_models/libcockatrice/models/database/card_set/card_sets_model.cpp b/libcockatrice_models/libcockatrice/models/database/card_set/card_sets_model.cpp index 5e0cc31d8..8fd5311f2 100644 --- a/libcockatrice_models/libcockatrice/models/database/card_set/card_sets_model.cpp +++ b/libcockatrice_models/libcockatrice/models/database/card_set/card_sets_model.cpp @@ -303,12 +303,14 @@ bool SetsDisplayModel::filterAcceptsRow(int sourceRow, const QModelIndex &source auto typeIndex = sourceModel()->index(sourceRow, SetsModel::SetTypeCol, sourceParent); auto nameIndex = sourceModel()->index(sourceRow, SetsModel::LongNameCol, sourceParent); auto shortNameIndex = sourceModel()->index(sourceRow, SetsModel::ShortNameCol, sourceParent); + auto dateIndex = sourceModel()->index(sourceRow, SetsModel::ReleaseDateCol, sourceParent); const auto filter = filterRegularExpression(); return (sourceModel()->data(typeIndex).toString().contains(filter) || sourceModel()->data(nameIndex).toString().contains(filter) || - sourceModel()->data(shortNameIndex).toString().contains(filter)); + sourceModel()->data(shortNameIndex).toString().contains(filter) || + sourceModel()->data(dateIndex).toString().contains(filter)); } bool SetsDisplayModel::lessThan(const QModelIndex &left, const QModelIndex &right) const 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 493b8e966..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 @@ -48,7 +48,7 @@ #include #include #include -#include +#include Server_AbstractParticipant::Server_AbstractParticipant(Server_Game *_game, int _playerId, 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 3e489233c..44e317bf7 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 @@ -47,7 +47,8 @@ #include #include #include -#include +#include +#include #include #include #include @@ -81,17 +82,6 @@ int Server_AbstractPlayer::newCardId() return nextCardId++; } -int Server_AbstractPlayer::newArrowId() const -{ - int id = 0; - for (Server_Arrow *a : arrows) { - if (a->getId() > id) { - id = a->getId(); - } - } - return id + 1; -} - void Server_AbstractPlayer::setupZones() { nextCardId = 0; @@ -1144,7 +1134,7 @@ Server_AbstractPlayer::cmdCreateToken(const Command_CreateToken &cmd, ResponseCo Event_CreateArrow createEvent; ServerInfo_Arrow *arrowInfo = createEvent.mutable_arrow_info(); - const int newId = player->newArrowId(); + const int newId = game->generateArrowId(); arrow->setId(newId); arrowInfo->set_id(newId); arrowInfo->set_start_player_id(player->getPlayerId()); @@ -1267,7 +1257,8 @@ Server_AbstractPlayer::cmdCreateArrow(const Command_CreateArrow &cmd, ResponseCo int currentPhase = game->getActivePhase(); int deletionPhase = cmd.has_delete_in_phase() ? cmd.delete_in_phase() : currentPhase; - auto arrow = new Server_Arrow(newArrowId(), startCard, targetItem, cmd.arrow_color(), currentPhase, deletionPhase); + auto arrow = new Server_Arrow(game->generateArrowId(), startCard, targetItem, cmd.arrow_color(), currentPhase, + deletionPhase); addArrow(arrow); Event_CreateArrow event; diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_player.h b/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_player.h index 9d9809298..85fbc0557 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_player.h +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_player.h @@ -74,7 +74,6 @@ public: } int newCardId(); - int newArrowId() const; void addZone(Server_CardZone *zone); void addArrow(Server_Arrow *arrow); diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/server_card.cpp b/libcockatrice_network/libcockatrice/network/server/remote/game/server_card.cpp index b858314c0..8c7feadba 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/game/server_card.cpp +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_card.cpp @@ -26,7 +26,8 @@ #include #include #include -#include +#include +#include #include Server_Card::Server_Card(const CardRef &cardRef, int _id, int _coord_x, int _coord_y, Server_CardZone *_zone) @@ -114,8 +115,8 @@ QString Server_Card::setAttribute(CardAttribute attribute, const QString &avalue bool Server_Card::setCounter(int _id, int value, Event_SetCardCounter *event) { - // Clamp to valid card counter range [0, MAX_COUNTERS_ON_CARD] - value = qBound(0, value, MAX_COUNTERS_ON_CARD); + // Clamp to valid card counter range [0, MAX_COUNTER_VALUE] + value = qBound(0, value, MAX_COUNTER_VALUE); const int oldValue = counters.value(_id, 0); if (value == oldValue) { @@ -139,10 +140,8 @@ bool Server_Card::setCounter(int _id, int value, Event_SetCardCounter *event) bool Server_Card::incrementCounter(int counterId, int delta, Event_SetCardCounter *event) { const int oldValue = counters.value(counterId, 0); - const auto result = static_cast(oldValue) + static_cast(delta); - // Clamp to [0, MAX_COUNTERS_ON_CARD] for card counters - const int newValue = - static_cast(qBound(static_cast(0), result, static_cast(MAX_COUNTERS_ON_CARD))); + // Clamp to [0, MAX_COUNTER_VALUE] for card counters + const int newValue = addClamped(oldValue, delta, 0, MAX_COUNTER_VALUE); if (newValue == oldValue) { return false; diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/server_card.h b/libcockatrice_network/libcockatrice/network/server/remote/game/server_card.h index 3d7e649b9..a2698ad61 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/game/server_card.h +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_card.h @@ -156,7 +156,7 @@ public: /** * @brief Sets a card counter to an exact value with clamping. * @param _id The counter ID. - * @param value The desired value (clamped to [0, MAX_COUNTERS_ON_CARD]; 0 removes the counter). + * @param value The desired value (clamped to [0, MAX_COUNTER_VALUE]; 0 removes the counter). * @param event Optional event to populate with counter state. * @return true if the value changed, false otherwise. */ @@ -168,7 +168,7 @@ public: * @param event Optional event to populate with counter state. * @return true if the value changed, false otherwise. * @note If counter does not exist, starts from 0. Counter is removed if result is 0. - * @note Clamps result to [0, MAX_COUNTERS_ON_CARD]. + * @note Clamps result to [0, MAX_COUNTER_VALUE]. */ [[nodiscard]] bool incrementCounter(int counterId, int delta, Event_SetCardCounter *event = nullptr); void setTapped(bool _tapped) diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/server_counter.cpp b/libcockatrice_network/libcockatrice/network/server/remote/game/server_counter.cpp index e65205cbb..b18e11c2b 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/game/server_counter.cpp +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_counter.cpp @@ -1,24 +1,12 @@ #include "server_counter.h" #include -#include Server_Counter::Server_Counter(int _id, const QString &_name, const color &_counterColor, int _radius, int _count) : id(_id), name(_name), counterColor(_counterColor), radius(_radius), count(_count) { } -//! \todo Extract overflow-safe arithmetic into shared helper. -//! Duplicated in Server_Card::incrementCounter() - keep in sync if modified. -bool Server_Counter::incrementCount(int delta) -{ - const int oldCount = count; - const auto result = static_cast(count) + static_cast(delta); - count = static_cast(qBound(static_cast(std::numeric_limits::min()), result, - static_cast(std::numeric_limits::max()))); - return count != oldCount; -} - void Server_Counter::getInfo(ServerInfo_Counter *info) { info->set_id(id); diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/server_counter.h b/libcockatrice_network/libcockatrice/network/server/remote/game/server_counter.h index 8226e663f..ca093b7cf 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/game/server_counter.h +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_counter.h @@ -22,6 +22,8 @@ #include #include +#include +#include class ServerInfo_Counter; @@ -92,7 +94,12 @@ public: * @return true if the value changed, false otherwise. * @note Clamps result to [INT_MIN, INT_MAX] to prevent overflow. */ - [[nodiscard]] bool incrementCount(int delta); + [[nodiscard]] bool incrementCount(int delta) + { + const int oldCount = count; + count = addClamped(count, delta, std::numeric_limits::min(), std::numeric_limits::max()); + return count != oldCount; + } /** * @brief Populates info with this counter's current state for network serialization. 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 50fff4812..4761199e5 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/game/server_game.cpp +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_game.cpp @@ -697,6 +697,11 @@ void Server_Game::setActivePhase(int newPhase) sendGameEventContainer(prepareGameEvent(event, -1)); } +qint64 Server_Game::generateArrowId() +{ + return nextArrowId++; +} + void Server_Game::removeArrows(int newPhase, bool force) { QMutexLocker locker(&gameMutex); 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 1c658f2ba..e0e7896b7 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/game/server_game.h +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_game.h @@ -49,6 +49,7 @@ class Server_Game : public QObject private: Server_Room *room; int nextPlayerId; + std::atomic nextArrowId = 1; int hostId; ServerInfo_User *creatorInfo; QMap participants; @@ -196,6 +197,7 @@ public: } void setActivePlayer(int newPlayer); void setActivePhase(int newPhase); + qint64 generateArrowId(); void removeArrows(int newPhase, bool force = false); void nextTurn(); int getSecondsElapsed() const 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 56e3f9f8e..d502fc7d6 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/game/server_player.cpp +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_player.cpp @@ -47,7 +47,7 @@ #include #include #include -#include +#include #include Server_Player::Server_Player(Server_Game *_game, diff --git a/libcockatrice_network/libcockatrice/network/server/remote/server.cpp b/libcockatrice_network/libcockatrice/network/server/remote/server.cpp index 3da9ddc73..7abccfca8 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/server.cpp +++ b/libcockatrice_network/libcockatrice/network/server/remote/server.cpp @@ -190,6 +190,25 @@ AuthenticationResult Server::loginUser(Server_ProtocolHandler *session, return authState; } +void Server::broadcastUserInfoUpdate(Server_ProtocolHandler *source) +{ + Event_UserJoined event; + event.mutable_user_info()->CopyFrom(source->copyUserInfo(false)); + + SessionEvent *se = Server_ProtocolHandler::prepareSessionEvent(event); + + clientsLock.lockForRead(); + for (auto &client : clients) { + if (client->getAcceptsUserListChanges()) { + client->sendProtocolItem(*se); + } + } + clientsLock.unlock(); + + sendIsl_SessionEvent(*se); + delete se; +} + void Server::addPersistentPlayer(const QString &userName, int roomId, int gameId, int playerId) { QWriteLocker locker(&persistentPlayersLock); diff --git a/libcockatrice_network/libcockatrice/network/server/remote/server.h b/libcockatrice_network/libcockatrice/network/server/remote/server.h index ab57fac4e..2fca46593 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/server.h +++ b/libcockatrice_network/libcockatrice/network/server/remote/server.h @@ -64,6 +64,7 @@ public: QString &clientid, QString &clientVersion, QString &connectionType); + void broadcastUserInfoUpdate(Server_ProtocolHandler *source); const QMap &getRooms() { diff --git a/libcockatrice_network/libcockatrice/network/server/remote/server_protocolhandler.cpp b/libcockatrice_network/libcockatrice/network/server/remote/server_protocolhandler.cpp index 27ebaf228..c441da781 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/server_protocolhandler.cpp +++ b/libcockatrice_network/libcockatrice/network/server/remote/server_protocolhandler.cpp @@ -26,7 +26,7 @@ #include #include #include -#include +#include Server_ProtocolHandler::Server_ProtocolHandler(Server *_server, Server_DatabaseInterface *_databaseInterface, diff --git a/libcockatrice_network/libcockatrice/network/server/remote/server_room.cpp b/libcockatrice_network/libcockatrice/network/server/remote/server_room.cpp index 1bd928e09..1f29e62fb 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/server_room.cpp +++ b/libcockatrice_network/libcockatrice/network/server/remote/server_room.cpp @@ -15,7 +15,7 @@ #include #include #include -#include +#include Server_Room::Server_Room(int _id, int _chatHistorySize, diff --git a/libcockatrice_protocol/libcockatrice/protocol/debug_pb_message.cpp b/libcockatrice_protocol/libcockatrice/protocol/debug_pb_message.cpp index c419a68d4..4d4889323 100644 --- a/libcockatrice_protocol/libcockatrice/protocol/debug_pb_message.cpp +++ b/libcockatrice_protocol/libcockatrice/protocol/debug_pb_message.cpp @@ -5,7 +5,7 @@ #include #include #include -#include +#include // FastFieldValuePrinter is added in protobuf 3.4, going out of our way to add the old FieldValuePrinter is not worth it #if GOOGLE_PROTOBUF_VERSION > 3004000 diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/CMakeLists.txt b/libcockatrice_protocol/libcockatrice/protocol/pb/CMakeLists.txt index b4c7b6ac8..20a4cb08d 100644 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/CMakeLists.txt +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/CMakeLists.txt @@ -122,6 +122,7 @@ set(PROTO_FILES response_activate.proto response_adjust_mod.proto response_ban_history.proto + response_card_art_rule_entry.proto response_deck_download.proto response_deck_list.proto response_deck_upload.proto diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/moderator_commands.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/moderator_commands.proto index 9d01b51d2..ca46e4dd7 100644 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/moderator_commands.proto +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/moderator_commands.proto @@ -11,6 +11,9 @@ message ModeratorCommand { FORCE_ACTIVATE_USER = 1007; GET_ADMIN_NOTES = 1008; UPDATE_ADMIN_NOTES = 1009; + ADD_CARD_ART_RULE = 1010; + REMOVE_CARD_ART_RULE = 1011; + LIST_CARD_ART_RULES = 1012; } extensions 100 to max; } @@ -106,3 +109,29 @@ message Command_UpdateAdminNotes { optional string user_name = 1; optional string notes = 2; } + +message Command_AddCardArtRule { + extend ModeratorCommand { + optional Command_AddCardArtRule ext = 1010; + } + + optional string card_name = 1; + optional string card_provider_id = 2; + optional string mode = 3; // "ALLOW" or "DENY" + optional string reason = 4; +} + +message Command_RemoveCardArtRule { + extend ModeratorCommand { + optional Command_RemoveCardArtRule ext = 1011; + } + + optional string card_name = 1; + optional string card_provider_id = 2; +} + +message Command_ListCardArtRules { + extend ModeratorCommand { + optional Command_ListCardArtRules ext = 1012; + } +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/response.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/response.proto index dece8ae17..e719f3e92 100644 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/response.proto +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/response.proto @@ -68,6 +68,7 @@ message Response { REPLAY_LIST = 1100; REPLAY_DOWNLOAD = 1101; REPLAY_GET_CODE = 1102; + CARD_ART_RULE_LIST = 1200; } required uint64 cmd_id = 1; optional ResponseCode response_code = 2; diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/response_card_art_rule_entry.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/response_card_art_rule_entry.proto new file mode 100644 index 000000000..25b76e09f --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/response_card_art_rule_entry.proto @@ -0,0 +1,16 @@ +syntax = "proto2"; +import "response.proto"; + +message Response_CardArtRuleEntry { + optional string card_name = 1; + optional string card_provider_id = 2; + optional string mode = 3; + optional string reason = 4; +} + +message Response_ListCardArtRules { + extend Response { + optional Response_ListCardArtRules ext = 1200; + } + repeated Response_CardArtRuleEntry entries = 1; +} \ No newline at end of file diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_user.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_user.proto index 10add611f..98cc3ce6a 100644 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_user.proto +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_user.proto @@ -1,4 +1,5 @@ syntax = "proto2"; + message ServerInfo_User { enum UserLevelFlag { IsNothing = 0; @@ -12,6 +13,14 @@ message ServerInfo_User { optional string left_side = 1; optional string right_side = 2; }; + message CardArtParams { + optional string card_name = 1; + optional string card_provider_id = 2; + optional double margin_pct_l = 3 [default = 0.33]; + optional double margin_pct_r = 4 [default = 0.02]; + optional double vertical_offset = 5 [default = 0.35]; + optional double zoom = 6 [default = 1.0]; + }; optional string name = 1; optional uint32 user_level = 2; @@ -28,4 +37,5 @@ message ServerInfo_User { optional string clientid = 13; optional string privlevel = 14; optional PawnColorsOverride pawn_colors = 15; -} + optional CardArtParams card_art_params = 16; +} \ No newline at end of file diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/session_commands.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/session_commands.proto index cecf87370..9d207c711 100644 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/session_commands.proto +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/session_commands.proto @@ -27,6 +27,7 @@ message SessionCommand { FORGOT_PASSWORD_RESET = 1022; FORGOT_PASSWORD_CHALLENGE = 1023; REQUEST_PASSWORD_SALT = 1024; + SET_CARD_ART_PARAMS = 1025; REPLAY_LIST = 1100; REPLAY_DOWNLOAD = 1101; REPLAY_MODIFY_MATCH = 1102; @@ -205,3 +206,15 @@ message Command_RequestPasswordSalt { } required string user_name = 1; } + +message Command_SetCardArtParams { + extend SessionCommand { + optional Command_SetCardArtParams ext = 1025; + } + optional string card_name = 1; + optional string card_provider_id = 2; + optional double margin_pct_l = 3; + optional double margin_pct_r = 4; + optional double vertical_offset = 5; + optional double zoom = 6; +} diff --git a/libcockatrice_utility/CMakeLists.txt b/libcockatrice_utility/CMakeLists.txt index c0c7d8cc9..2d34cad31 100644 --- a/libcockatrice_utility/CMakeLists.txt +++ b/libcockatrice_utility/CMakeLists.txt @@ -15,8 +15,12 @@ set(UTILITY_HEADERS libcockatrice/utility/levenshtein.h libcockatrice/utility/macros.h libcockatrice/utility/passwordhasher.h - libcockatrice/utility/trice_limits.h + libcockatrice/utility/string_limits.h + libcockatrice/utility/dice_limits.h + libcockatrice/utility/counter_limits.h + libcockatrice/utility/clamped_arithmetic.h libcockatrice/utility/zone_names.h + libcockatrice/utility/days_years_between.h ) add_library(libcockatrice_utility STATIC ${UTILITY_SOURCES} ${UTILITY_HEADERS}) diff --git a/libcockatrice_utility/libcockatrice/utility/clamped_arithmetic.h b/libcockatrice_utility/libcockatrice/utility/clamped_arithmetic.h new file mode 100644 index 000000000..1afac758c --- /dev/null +++ b/libcockatrice_utility/libcockatrice/utility/clamped_arithmetic.h @@ -0,0 +1,22 @@ +#ifndef CLAMPED_ARITHMETIC_H +#define CLAMPED_ARITHMETIC_H + +#include +#include + +/** + * @brief Overflow-safe clamped addition: returns value + delta bounded to [minValue, maxValue]. + * + * Uses a 64-bit intermediate so the addition cannot overflow int. Shared by the bounded + * counter arithmetic in both the client and the server. + * + * @note Requires minValue <= maxValue. Bounds come from trusted compile-time call sites; + * qBound() asserts this internally in debug builds. + */ +inline int addClamped(int value, int delta, int minValue, int maxValue) +{ + const auto result = static_cast(value) + static_cast(delta); + return static_cast(qBound(static_cast(minValue), result, static_cast(maxValue))); +} + +#endif // CLAMPED_ARITHMETIC_H diff --git a/libcockatrice_utility/libcockatrice/utility/counter_limits.h b/libcockatrice_utility/libcockatrice/utility/counter_limits.h new file mode 100644 index 000000000..1343dbb3f --- /dev/null +++ b/libcockatrice_utility/libcockatrice/utility/counter_limits.h @@ -0,0 +1,17 @@ +#ifndef COUNTER_LIMITS_H +#define COUNTER_LIMITS_H + +/** + * @brief Upper bound for a bounded counter's value: [0, MAX_COUNTER_VALUE]. + * + * Caps an individual counter's VALUE (e.g. a +1/+1 counter at 999), not how many counters + * something holds. Applies to counters that are constrained to a non-negative display range, + * such as card counters and commander tax. Unbounded counters (e.g. a player's life total) + * do not use this limit and may go negative, saturating only at the int range. + * + * The max of 999 is a display constraint (3-digit rendering) and a reasonable gameplay limit. + * The server enforces these bounds; the client may also check them for UX optimization. + */ +constexpr int MAX_COUNTER_VALUE = 999; + +#endif // COUNTER_LIMITS_H diff --git a/libcockatrice_utility/libcockatrice/utility/days_years_between.h b/libcockatrice_utility/libcockatrice/utility/days_years_between.h new file mode 100644 index 000000000..4b3b5bc0c --- /dev/null +++ b/libcockatrice_utility/libcockatrice/utility/days_years_between.h @@ -0,0 +1,13 @@ +#ifndef COCKATRICE_DAYS_YEARS_BETWEEN_H +#define COCKATRICE_DAYS_YEARS_BETWEEN_H + +#include + +inline static QPair getDaysAndYearsBetween(const QDate &then, const QDate &now) +{ + int years = now.addDays(1 - then.dayOfYear()).year() - then.year(); // there is no yearsTo + int days = then.addYears(years).daysTo(now); + return {days, years}; +} + +#endif // COCKATRICE_DAYS_YEARS_BETWEEN_H diff --git a/libcockatrice_utility/libcockatrice/utility/dice_limits.h b/libcockatrice_utility/libcockatrice/utility/dice_limits.h new file mode 100644 index 000000000..e1407c57e --- /dev/null +++ b/libcockatrice_utility/libcockatrice/utility/dice_limits.h @@ -0,0 +1,15 @@ +#ifndef DICE_LIMITS_H +#define DICE_LIMITS_H + +#include // for uint + +/** @brief Fewest sides a rollable die may have. */ +constexpr uint MINIMUM_DIE_SIDES = 2; +/** @brief Most sides a rollable die may have. */ +constexpr uint MAXIMUM_DIE_SIDES = 1000000; +/** @brief Fewest dice that may be rolled at once. */ +constexpr uint MINIMUM_DICE_TO_ROLL = 1; +/** @brief Most dice that may be rolled at once. */ +constexpr uint MAXIMUM_DICE_TO_ROLL = 100; + +#endif // DICE_LIMITS_H diff --git a/libcockatrice_utility/libcockatrice/utility/qt_utils.h b/libcockatrice_utility/libcockatrice/utility/qt_utils.h index 334e56027..8e5212031 100644 --- a/libcockatrice_utility/libcockatrice/utility/qt_utils.h +++ b/libcockatrice_utility/libcockatrice/utility/qt_utils.h @@ -1,5 +1,6 @@ #ifndef COCKATRICE_QT_UTILS_H #define COCKATRICE_QT_UTILS_H +#include #include namespace QtUtils diff --git a/libcockatrice_utility/libcockatrice/utility/string_limits.h b/libcockatrice_utility/libcockatrice/utility/string_limits.h new file mode 100644 index 000000000..cca804bf0 --- /dev/null +++ b/libcockatrice_utility/libcockatrice/utility/string_limits.h @@ -0,0 +1,31 @@ +#ifndef STRING_LIMITS_H +#define STRING_LIMITS_H + +#include +#include +#include + +/** @brief Max size for short strings, like names and things that are generally a single phrase. */ +constexpr int MAX_NAME_LENGTH = 0xff; +/** @brief Max size for chat messages and text contents. */ +constexpr int MAX_TEXT_LENGTH = 0xfff; +/** @brief Max size for deck files and pictures (about 2 megabytes). */ +constexpr int MAX_FILE_LENGTH = 0x1fffff; + +/** @brief Returns a QString from a std::string, truncated to at most MAX_NAME_LENGTH bytes. */ +inline QString nameFromStdString(const std::string &_string) +{ + return QString::fromUtf8(_string.data(), std::min(int(_string.size()), MAX_NAME_LENGTH)); +} +/** @brief Returns a QString from a std::string, truncated to at most MAX_TEXT_LENGTH bytes. */ +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_FILE_LENGTH bytes. */ +inline QString fileFromStdString(const std::string &_string) +{ + return QString::fromUtf8(_string.data(), std::min(int(_string.size()), MAX_FILE_LENGTH)); +} + +#endif // STRING_LIMITS_H diff --git a/libcockatrice_utility/libcockatrice/utility/trice_limits.h b/libcockatrice_utility/libcockatrice/utility/trice_limits.h deleted file mode 100644 index 833ce1b98..000000000 --- a/libcockatrice_utility/libcockatrice/utility/trice_limits.h +++ /dev/null @@ -1,38 +0,0 @@ -#ifndef TRICE_LIMITS_H -#define TRICE_LIMITS_H - -#include - -// max size for short strings, like names and things that are generally a single phrase -constexpr int MAX_NAME_LENGTH = 0xff; -// max size for chat messages and text contents -constexpr int MAX_TEXT_LENGTH = 0xfff; -// max size for deck files and pictures -constexpr int MAX_FILE_LENGTH = 0x1fffff; // about 2 megabytes - -constexpr uint MINIMUM_DIE_SIDES = 2; -constexpr uint MAXIMUM_DIE_SIDES = 1000000; -constexpr uint MINIMUM_DICE_TO_ROLL = 1; -constexpr uint MAXIMUM_DICE_TO_ROLL = 100; - -// Card counter value bounds [0, MAX_COUNTERS_ON_CARD]. -// Counters on cards (e.g., +1/+1 counters, charge counters) are non-negative physical game objects. -// The max of 999 is a display constraint (3-digit rendering) and reasonable gameplay limit. -// Server enforces these bounds; client may also check for UX optimization. -constexpr int MAX_COUNTERS_ON_CARD = 999; - -// optimized functions to get qstrings that are at most that long -static inline QString nameFromStdString(const std::string &_string) -{ - return QString::fromUtf8(_string.data(), std::min(int(_string.size()), MAX_NAME_LENGTH)); -} -static inline QString textFromStdString(const std::string &_string) -{ - return QString::fromUtf8(_string.data(), std::min(int(_string.size()), MAX_TEXT_LENGTH)); -} -static inline QString fileFromStdString(const std::string &_string) -{ - return QString::fromUtf8(_string.data(), std::min(int(_string.size()), MAX_FILE_LENGTH)); -} - -#endif // TRICE_LIMITS_H diff --git a/oracle/oracle_en@source.ts b/oracle/oracle_en@source.ts index 943b44a97..b3263f861 100644 --- a/oracle/oracle_en@source.ts +++ b/oracle/oracle_en@source.ts @@ -278,7 +278,7 @@ OracleImporter - + Dummy set containing tokens @@ -286,7 +286,7 @@ OracleWizard - + Oracle Importer diff --git a/oracle/translations/oracle_es.ts b/oracle/translations/oracle_es.ts index eeb9f71bd..33dbbdc3a 100644 --- a/oracle/translations/oracle_es.ts +++ b/oracle/translations/oracle_es.ts @@ -63,7 +63,7 @@ Sets file (%1) Sets JSON file (%1) - + Archivo de ediciones (%1) @@ -172,7 +172,7 @@ spoiler - + spoiler @@ -192,7 +192,7 @@ Local file: - + Archivo local: @@ -202,7 +202,7 @@ Choose file... - + Elegir archivo... @@ -230,7 +230,7 @@ tokens - + fichas @@ -250,7 +250,7 @@ Local file: - + Archivo local: @@ -260,7 +260,7 @@ Choose file... - + Elegir archivo... @@ -391,12 +391,12 @@ Load %1 file - + Cargar archivo de %1 %1 file (%1) - + archivo de %1 (%1) @@ -420,12 +420,12 @@ Please choose a file. - + Por favor elija un archivo. Cannot open file '%1'. - + No se puede abrir el archivo '%1'. @@ -602,7 +602,7 @@ Run in no-confirm background mode - + Ejecutar en modo del segundo plano sin confirmación \ No newline at end of file diff --git a/servatrice/migrations/servatrice_0034_to_0035.sql b/servatrice/migrations/servatrice_0034_to_0035.sql new file mode 100644 index 000000000..acaad9c8b --- /dev/null +++ b/servatrice/migrations/servatrice_0034_to_0035.sql @@ -0,0 +1,19 @@ +ALTER TABLE `cockatrice_users` ADD COLUMN `card_art_params` TEXT DEFAULT NULL, ALGORITHM=INSTANT; + +CREATE TABLE IF NOT EXISTS `cockatrice_card_art_name_rules` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `card_name` varchar(255) NOT NULL, + `card_provider_id` varchar(255) NOT NULL, + `mode` enum('ALLOW','DENY') NOT NULL, + `reason` varchar(255) DEFAULT NULL, + `created_by` int(7) unsigned DEFAULT NULL, + `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + UNIQUE KEY `uniq_provider_card_name` (`card_provider_id`, `card_name`), + KEY `idx_mode` (`mode`), + FOREIGN KEY (`created_by`) REFERENCES `cockatrice_users`(`id`) + ON DELETE SET NULL + ON UPDATE CASCADE + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE utf8mb4_unicode_ci; + +UPDATE cockatrice_schema_version SET version=35 WHERE version=34; diff --git a/servatrice/scripts/account_monitor/.gitignore b/servatrice/scripts/account_monitor/.gitignore new file mode 100644 index 000000000..40cd3839a --- /dev/null +++ b/servatrice/scripts/account_monitor/.gitignore @@ -0,0 +1,5 @@ +# Local state - never commit these +state.json +state.json.tmp +venv/ +__pycache__/ diff --git a/servatrice/scripts/account_monitor/README.md b/servatrice/scripts/account_monitor/README.md new file mode 100644 index 000000000..e2c01d0ba --- /dev/null +++ b/servatrice/scripts/account_monitor/README.md @@ -0,0 +1,163 @@ +# Account registration monitor + +Posts a Discord message whenever a new account is registered in Servatrice +(`cockatrice_users`). Each message includes the username, real name (if set), +email, and registration time. + +It runs as a periodic read-only query against the production database. It does +not modify the database and does not touch the running Servatrice process. + +## How it decides what is "new" + +Accounts get an auto-increment `id`, so "new since last time" is just +`id > last_seen_id`. The monitor stores that single high-water-mark id in its +state file. Each run it posts every account above the mark, oldest first, then +advances the mark to the highest id it posted. + +Because the mark only moves forward and an id is posted exactly once, there are +no duplicate messages and nothing is missed, even if the monitor is down for a +while. The state file holds a single number, so it never grows. + +The first run (when no state file exists yet) records the current maximum id as +the baseline and posts nothing. This prevents the entire existing user base from +being dumped into the channel. Only accounts registered after that baseline are +posted. + +If a post to Discord fails, the monitor stops there without advancing the mark +past it, so that account and everything after it are retried on the next run. + +## Privacy note + +Messages contain personal data (real name and email). Discord stores message +content on their servers, so post only to a private channel that the right +people can see, and treat the webhook URL as a secret. It lives in the config +ini alongside the database password, so keep that file readable only by the user +that runs the monitor. + +## Setup + +The monitor reads its database credentials and the webhook from a +servatrice-style ini file passed with `--config` (or the `CONFIG_FILE` env var). +You can point it at your existing `servatrice.ini`, or keep a small separate ini +just for the monitor. + +### 1. Create a read-only database user + +Run as a DB admin. Adjust the host (`'%'` allows any host; restrict it to the +machine running the monitor if you can) and the table prefix if yours is not the +default `cockatrice`. + +```sql +CREATE USER 'account_monitor'@'%' IDENTIFIED BY 'a-strong-password'; +GRANT SELECT (id, name, realname, email, registrationDate) + ON servatrice.cockatrice_users TO 'account_monitor'@'%'; +FLUSH PRIVILEGES; +``` + +Using a read-only user is recommended over pointing `--config` at the real +`servatrice.ini`, because Servatrice's own DB account usually has write access +the monitor does not need. + +### 2. Create the Discord webhook and add it to the config + +In Discord: open the target channel, then Edit Channel -> Integrations -> +Webhooks -> New Webhook. Name it, pick the channel, and copy the webhook URL. + +Add a `[discord]` section with the URL to the ini you will pass to `--config`. +If you want the read-only user above, set the `[database]` section to use it. A +small dedicated `monitor.ini` looks like this: + +```ini +[database] +hostname=127.0.0.1 +database=servatrice +user=account_monitor +password=a-strong-password +prefix=cockatrice + +[discord] +new_user_activation_webhook=https://discord.com/api/webhooks/XXXX/YYYY +``` + +If you would rather use one file, add the `[discord]` section to the real +`servatrice.ini` instead. Servatrice ignores sections it does not use. Note that +Servatrice (a Qt app) rewrites ini values it touches in quoted, backslash-escaped +form, for example `"https\://..."`. The monitor strips that encoding from the +webhook automatically, so either the plain or the escaped form works. + +### 3. Install + +```bash +cd servatrice/scripts/account_monitor +python3 -m venv venv +./venv/bin/pip install -r requirements.txt +``` + +### 4. Verify before scheduling + +```bash +# Confirm the webhook works (sends one test message to the channel) +./venv/bin/python ./account_monitor.py --config /path/to/monitor.ini --test-webhook + +# Confirm DB access and see what it would do, without posting or writing state +./venv/bin/python ./account_monitor.py --config /path/to/monitor.ini --dry-run --verbose +``` + +The first real run seeds the baseline and posts nothing: + +```bash +./venv/bin/python ./account_monitor.py --config /path/to/monitor.ini +``` + +After that, test it end to end by registering a throwaway account and confirming +a message appears on the next run. + +## Run it every 2 minutes with cron + +Edit the crontab of the user that owns the script directory (`crontab -e`) and +add one line. This runs the monitor every 2 minutes, using the venv's Python and +your config ini, and appends output to a log: + +```cron +*/2 * * * * cd /opt/cockatrice/servatrice/scripts/account_monitor && ./venv/bin/python ./account_monitor.py --config /etc/servatrice/servatrice.ini >> /var/log/account_monitor.log 2>&1 +``` + +Adjust the three paths to your install: the script directory after `cd`, and the +`--config` and log paths. The `*/2` field is what makes it run every 2 minutes; +change it to `*/5` for every 5, and so on. + +By default the high-water-mark is stored in `state.json` next to the script, so +the directory must be writable by the cron user. To put it elsewhere, set +`STATE_FILE`: + +```cron +*/2 * * * * STATE_FILE=/var/lib/account_monitor/state.json cd /opt/cockatrice/servatrice/scripts/account_monitor && ./venv/bin/python ./account_monitor.py --config /etc/servatrice/servatrice.ini >> /var/log/account_monitor.log 2>&1 +``` + +The interval only controls how often it checks; it is not a lookback window, so +a longer interval never causes missed accounts. The query is cheap: an indexed +range scan on the primary key for `id > last_seen`. + +## Options + +- `--config PATH` / `-c PATH` — read DB settings from `[database]` and the webhook from `[discord] new_user_activation_webhook` of a servatrice-style ini (falls back to the `CONFIG_FILE` env var). +- `--dry-run` — query and log what would be posted; no Discord posts, no state write. +- `--test-webhook` — send one test message to the webhook and exit (does not need DB credentials). +- `--verbose` — debug logging. + +## Configuration reference + +Settings come from the `--config` ini, with environment variables available as +overrides if you need them (env takes precedence over the ini). + +| Setting | ini (`--config`) | Environment override | +| --- | --- | --- | +| DB host | `[database] hostname` | `DB_HOST` | +| DB port | `[database] port` (optional) | `DB_PORT` | +| DB name | `[database] database` | `DB_NAME` | +| DB user | `[database] user` | `DB_USER` | +| DB password | `[database] password` | `DB_PASSWORD` | +| Table prefix | `[database] prefix` | `DB_TABLE_PREFIX` | +| Webhook URL | `[discord] new_user_activation_webhook` | `DISCORD_WEBHOOK_URL` | +| DB TLS | — | `DB_SSL` / `DB_SSL_CA` | +| State file path | — | `STATE_FILE` (default: `state.json` next to the script) | diff --git a/servatrice/scripts/account_monitor/account_monitor.py b/servatrice/scripts/account_monitor/account_monitor.py new file mode 100755 index 000000000..47cf23fd3 --- /dev/null +++ b/servatrice/scripts/account_monitor/account_monitor.py @@ -0,0 +1,350 @@ +#!/usr/bin/env python3 +"""Post a Discord message when a new Servatrice account is registered. + +Accounts get an auto-increment `id`, so "what is new since last time" is simply +`id > last_seen_id`. The monitor stores that single high-water-mark id in a +small state file. Each run it posts every account above the mark (oldest +first), then advances the mark. This means no duplicate posts, nothing missed +across downtime, and a state file that never grows (it holds one number). + +On the very first run (no state file yet) it records the current maximum id as +the baseline and posts nothing, so existing users are not dumped into the +channel. From then on only newly-registered accounts are posted. + +Intended to be run on a schedule (cron). Pass a servatrice-style ini with +--config (or CONFIG_FILE) for the database credentials and webhook; see +README.md. +""" + +import argparse +import configparser +import json +import logging +import os +import re +import sys +import time +import urllib.error +import urllib.request + +import pymysql + +log = logging.getLogger("account_monitor") + +# Columns we use. `id` drives the high-water-mark; `name` is the login/username, +# `realname` is the optional display name, `registrationDate` is when the account +# row was created. +NEW_ACCOUNTS_QUERY = ( + "SELECT id, name, realname, email, registrationDate " + "FROM `{prefix}_users` WHERE id > %s ORDER BY id ASC" +) + +EMBED_COLOR = 0x5865F2 # discord blurple +DISCORD_MAX_EMBED_FIELD = 1024 +POST_DELAY_SECONDS = 1.0 # gap between webhook posts to stay under rate limits +MAX_RATELIMIT_RETRIES = 5 + + +def _clean_ini_value(value): + """Undo Qt QSettings ini encoding of a value. + + Qt apps (including Servatrice) write ini values that contain special + characters wrapped in double quotes and backslash-escaped, e.g. a webhook + URL stored as "https\\://...". configparser returns that text literally, so + strip the wrapping quotes and remove the backslash escapes. This is applied + to the webhook URL only, where it is safe (URLs contain no quotes or + backslashes); DB values are left untouched so passwords are never altered. + """ + value = value.strip() + if len(value) >= 2 and value[0] == value[-1] and value[0] in "\"'": + value = value[1:-1] + return re.sub(r"\\(.)", r"\1", value) + + +def load_config_file(path): + """Read DB and Discord settings from a servatrice-style ini. + + Pulls the [database] section (hostname/database/user/password/prefix/port) + and the Discord webhook from [discord] new_user_activation_webhook. Returns + a dict using this module's internal config keys; only keys actually present + (and non-empty) in the file are returned, so missing values fall back to + defaults or the environment. + """ + # interpolation=None so a '%' in a password is not treated as a token. + parser = configparser.ConfigParser(interpolation=None) + if not parser.read(path): + log.error("Config file not found or unreadable: %s", path) + sys.exit(2) + + result = {} + if parser.has_section("database"): + db = parser["database"] + db_mapping = { + "hostname": "db_host", + "database": "db_name", + "user": "db_user", + "password": "db_password", + "prefix": "db_prefix", + "port": "db_port", # not in stock servatrice.ini, but honored if present + } + result.update({cfg_key: db[ini_key] for ini_key, cfg_key in db_mapping.items() if db.get(ini_key)}) + + if parser.has_section("discord") and parser["discord"].get("new_user_activation_webhook"): + result["webhook_url"] = _clean_ini_value(parser["discord"]["new_user_activation_webhook"]) + + return result + + +def get_config(config_path=None, require_db=True): + """Build configuration from defaults, an optional ini file, then env vars. + + Precedence, highest first: environment variables, the ini file, built-in + defaults. Database credentials and the Discord webhook may come from either + the ini file or the environment; the state file is environment-only. + """ + cfg = { + "db_host": "localhost", + "db_port": 3306, + "db_name": "servatrice", + "db_user": None, + "db_password": None, + "db_prefix": "cockatrice", + "db_ssl": False, + "db_ssl_ca": None, + "webhook_url": None, + "state_file": os.path.join(os.path.dirname(os.path.abspath(__file__)), "state.json"), + } + + if config_path: + cfg.update(load_config_file(config_path)) + + env_map = { + "DB_HOST": "db_host", + "DB_PORT": "db_port", + "DB_NAME": "db_name", + "DB_USER": "db_user", + "DB_PASSWORD": "db_password", + "DB_TABLE_PREFIX": "db_prefix", + "DB_SSL_CA": "db_ssl_ca", + "DISCORD_WEBHOOK_URL": "webhook_url", + "STATE_FILE": "state_file", + } + for env_key, cfg_key in env_map.items(): + if os.environ.get(env_key): + cfg[cfg_key] = os.environ[env_key] + if os.environ.get("DB_SSL"): + cfg["db_ssl"] = os.environ["DB_SSL"].lower() in ("1", "true", "yes") + + cfg["db_port"] = int(cfg["db_port"]) + + required = {"webhook_url": "DISCORD_WEBHOOK_URL or [discord] new_user_activation_webhook"} + if require_db: + required["db_user"] = "DB_USER or [database] user" + required["db_password"] = "DB_PASSWORD or [database] password" + missing = [label for key, label in required.items() if not cfg[key]] + if missing: + log.error("Missing required configuration: %s", "; ".join(missing)) + sys.exit(2) + + if cfg["webhook_url"] and not cfg["webhook_url"].lower().startswith(("http://", "https://")): + log.error("Webhook URL does not look like an http(s) URL: %r", cfg["webhook_url"]) + sys.exit(2) + return cfg + + +def connect(cfg): + """Open a read-only connection to the Servatrice database.""" + ssl = None + if cfg["db_ssl"]: + ssl = {"ca": cfg["db_ssl_ca"]} if cfg["db_ssl_ca"] else {} + return pymysql.connect( + host=cfg["db_host"], + port=cfg["db_port"], + user=cfg["db_user"], + password=cfg["db_password"], + database=cfg["db_name"], + charset="utf8mb4", + cursorclass=pymysql.cursors.DictCursor, + connect_timeout=15, + read_timeout=30, + ssl=ssl, + ) + + +def fetch_max_id(conn, prefix): + """Return the highest account id currently in the table, or 0 if empty.""" + with conn.cursor() as cur: + cur.execute("SELECT MAX(id) AS max_id FROM `{prefix}_users`".format(prefix=prefix)) + row = cur.fetchone() + return int(row["max_id"]) if row and row["max_id"] is not None else 0 + + +def fetch_new_accounts(conn, prefix, last_id): + """Return detail rows for accounts with id > last_id, oldest first.""" + with conn.cursor() as cur: + cur.execute(NEW_ACCOUNTS_QUERY.format(prefix=prefix), (last_id,)) + return cur.fetchall() + + +def load_state(path): + """Load the high-water-mark id. Returns (last_id, is_first_run).""" + if not os.path.exists(path): + return 0, True + with open(path, "r", encoding="utf-8") as fh: + data = json.load(fh) + return int(data.get("last_id", 0)), False + + +def save_state(path, last_id): + """Atomically persist the high-water-mark id.""" + directory = os.path.dirname(path) + if directory: + os.makedirs(directory, exist_ok=True) + tmp = path + ".tmp" + with open(tmp, "w", encoding="utf-8") as fh: + json.dump({"version": 2, "last_id": int(last_id)}, fh) + os.replace(tmp, path) + + +def build_embed(row): + """Build a Discord embed dict for one newly-registered account.""" + fields = [ + {"name": "Username", "value": str(row["name"]) or "(none)", "inline": False}, + ] + realname = (row.get("realname") or "").strip() + if realname: + fields.append({"name": "Real name", "value": realname[:DISCORD_MAX_EMBED_FIELD], "inline": False}) + fields.append({"name": "Email", "value": str(row.get("email") or "(none)"), "inline": False}) + reg = row.get("registrationDate") + fields.append({"name": "Reg time", "value": str(reg) if reg is not None else "(unknown)", "inline": False}) + return { + "title": "New account registered", + "color": EMBED_COLOR, + "fields": fields, + } + + +def post_embed(webhook_url, embed): + """POST a single embed to the Discord webhook, honoring 429 rate limits.""" + payload = json.dumps({"embeds": [embed]}).encode("utf-8") + for attempt in range(MAX_RATELIMIT_RETRIES): + req = urllib.request.Request( + webhook_url, + data=payload, + headers={"Content-Type": "application/json", "User-Agent": "servatrice-account-monitor/1.0"}, + method="POST", + ) + try: + with urllib.request.urlopen(req, timeout=30) as resp: + if resp.status in (200, 204): + return True + log.warning("Unexpected Discord status %s", resp.status) + return False + except urllib.error.HTTPError as err: + if err.code == 429: + retry_after = _retry_after_seconds(err) + log.warning("Rate limited by Discord; sleeping %.2fs", retry_after) + time.sleep(retry_after) + continue + log.error("Discord webhook HTTP %s: %s", err.code, err.read().decode("utf-8", "replace")[:500]) + return False + except urllib.error.URLError as err: + log.error("Discord webhook connection error: %s", err) + return False + log.error("Gave up posting after %d rate-limit retries", MAX_RATELIMIT_RETRIES) + return False + + +def _retry_after_seconds(err): + """Extract the retry delay (seconds) from a Discord 429 response.""" + header = err.headers.get("Retry-After") + if header: + try: + return float(header) + except ValueError: + pass + try: + body = json.loads(err.read().decode("utf-8", "replace")) + return float(body.get("retry_after", 1.0)) + except (ValueError, json.JSONDecodeError): + return 1.0 + + +def main(): + parser = argparse.ArgumentParser(description="Post new Servatrice account registrations to Discord.") + parser.add_argument( + "--config", "-c", + help="Path to a servatrice-style ini; reads DB settings from its [database] " + "section (hostname/database/user/password/prefix) and the webhook from " + "[discord] new_user_activation_webhook. Defaults to the CONFIG_FILE env " + "var if set.", + ) + parser.add_argument("--dry-run", action="store_true", help="Log what would be posted; do not post or write state.") + parser.add_argument("--test-webhook", action="store_true", help="Send a single test message to the webhook and exit.") + parser.add_argument("--verbose", action="store_true", help="Enable debug logging.") + args = parser.parse_args() + + logging.basicConfig( + level=logging.DEBUG if args.verbose else logging.INFO, + format="%(asctime)s %(levelname)s %(message)s", + ) + + config_path = args.config or os.environ.get("CONFIG_FILE") + + if args.test_webhook: + cfg = get_config(config_path, require_db=False) + ok = post_embed( + cfg["webhook_url"], + {"title": "Account monitor test", "color": EMBED_COLOR, + "description": "If you can see this, the webhook is configured correctly."}, + ) + sys.exit(0 if ok else 1) + + cfg = get_config(config_path) + + try: + conn = connect(cfg) + except pymysql.MySQLError as err: + log.error("Database connection failed: %s", err) + sys.exit(1) + + try: + last_id, first_run = load_state(cfg["state_file"]) + + if first_run: + baseline = fetch_max_id(conn, cfg["db_prefix"]) + log.info("First run: seeding high-water-mark at id=%d; posting nothing.", baseline) + if not args.dry_run: + save_state(cfg["state_file"], baseline) + return + + rows = fetch_new_accounts(conn, cfg["db_prefix"], last_id) + if not rows: + log.info("No new accounts since id=%d.", last_id) + return + + log.info("Found %d new account(s) since id=%d.", len(rows), last_id) + + # Post oldest first. Advance the mark only past accounts we successfully + # posted; on the first failure, stop so nothing after it is posted out of + # order or skipped. The failed account (and the rest) retry next run. + for row in rows: + if args.dry_run: + log.info("[dry-run] would post: id=%s name=%s email=%s reg=%s", + row["id"], row["name"], row.get("email"), row.get("registrationDate")) + continue + if not post_embed(cfg["webhook_url"], build_embed(row)): + log.error("Failed to post account id=%s; stopping. Will retry from here next run.", row["id"]) + break + last_id = row["id"] + log.info("Posted account id=%s (%s)", row["id"], row["name"]) + time.sleep(POST_DELAY_SECONDS) + + if not args.dry_run: + save_state(cfg["state_file"], last_id) + finally: + conn.close() + + +if __name__ == "__main__": + main() diff --git a/servatrice/scripts/account_monitor/requirements.txt b/servatrice/scripts/account_monitor/requirements.txt new file mode 100644 index 000000000..24d93c7a7 --- /dev/null +++ b/servatrice/scripts/account_monitor/requirements.txt @@ -0,0 +1 @@ +PyMySQL==1.2.0 diff --git a/servatrice/servatrice.sql b/servatrice/servatrice.sql index fa644dbc0..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(34); +INSERT INTO cockatrice_schema_version VALUES(35); -- users and user data tables CREATE TABLE IF NOT EXISTS `cockatrice_users` ( @@ -43,6 +43,7 @@ CREATE TABLE IF NOT EXISTS `cockatrice_users` ( `passwordLastChangedDate` datetime NOT NULL DEFAULT '0000-00-00 00:00:00', `leftPawnColorOverride` varchar(255), `rightPawnColorOverride` varchar(255), + `card_art_params` TEXT DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `name` (`name`), KEY `token` (`token`), @@ -300,3 +301,19 @@ CREATE TABLE IF NOT EXISTS `cockatrice_audit` ( PRIMARY KEY (`id`), KEY `user_name` (`name`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS `cockatrice_card_art_name_rules` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `card_name` varchar(255) NOT NULL, + `card_provider_id` varchar(255) NOT NULL, + `mode` enum('ALLOW','DENY') NOT NULL, + `reason` varchar(255) DEFAULT NULL, + `created_by` int(7) unsigned DEFAULT NULL, + `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + UNIQUE KEY `uniq_provider_card_name` (`card_provider_id`, `card_name`), + KEY `idx_mode` (`mode`), + FOREIGN KEY (`created_by`) REFERENCES `cockatrice_users`(`id`) + ON DELETE SET NULL + ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE utf8mb4_unicode_ci; diff --git a/servatrice/src/servatrice_database_interface.cpp b/servatrice/src/servatrice_database_interface.cpp index 73643825e..d5e1f13ef 100644 --- a/servatrice/src/servatrice_database_interface.cpp +++ b/servatrice/src/servatrice_database_interface.cpp @@ -7,6 +7,8 @@ #include #include #include +#include +#include #include #include #include @@ -681,6 +683,33 @@ ServerInfo_User Servatrice_DatabaseInterface::evalUserQueryResult(const QSqlQuer if (!clientid.isEmpty()) { result.set_clientid(clientid.toStdString()); } + + const QString cardArtParamsJson = query->value(12).toString(); + if (!cardArtParamsJson.isEmpty()) { + const QJsonDocument doc = QJsonDocument::fromJson(cardArtParamsJson.toUtf8()); + if (doc.isObject()) { + const QJsonObject obj = doc.object(); + auto *cap = result.mutable_card_art_params(); + if (obj.contains("card_name")) { + cap->set_card_name(obj["card_name"].toString().toStdString()); + } + if (obj.contains("card_provider_id")) { + cap->set_card_provider_id(obj["card_provider_id"].toString().toStdString()); + } + if (obj.contains("marginPctL")) { + cap->set_margin_pct_l(obj["marginPctL"].toDouble(0.33)); + } + if (obj.contains("marginPctR")) { + cap->set_margin_pct_r(obj["marginPctR"].toDouble(0.02)); + } + if (obj.contains("verticalOffset")) { + cap->set_vertical_offset(obj["verticalOffset"].toDouble(0.35)); + } + if (obj.contains("zoom")) { + cap->set_zoom(obj["zoom"].toDouble(1.0)); + } + } + } } return result; } @@ -698,7 +727,7 @@ ServerInfo_User Servatrice_DatabaseInterface::getUserData(const QString &name, b QSqlQuery *query = prepareQuery("select id, name, admin, country, privlevel, leftPawnColorOverride, " "rightPawnColorOverride, realname, avatar_bmp, registrationDate, " - "email, clientid from {prefix}_users where " + "email, clientid, card_art_params from {prefix}_users where " "name = :name and active = 1"); query->bindValue(":name", name); if (!execSqlQuery(query)) { diff --git a/servatrice/src/servatrice_database_interface.h b/servatrice/src/servatrice_database_interface.h index 68080404c..1e3501ec7 100644 --- a/servatrice/src/servatrice_database_interface.h +++ b/servatrice/src/servatrice_database_interface.h @@ -10,7 +10,7 @@ #include #include -#define DATABASE_SCHEMA_VERSION 34 +#define DATABASE_SCHEMA_VERSION 35 class Servatrice; diff --git a/servatrice/src/serversocketinterface.cpp b/servatrice/src/serversocketinterface.cpp index bc90a3ef1..6ceebfca9 100644 --- a/servatrice/src/serversocketinterface.cpp +++ b/servatrice/src/serversocketinterface.cpp @@ -31,6 +31,8 @@ #include #include #include +#include +#include #include #include #include @@ -59,8 +61,10 @@ #include #include #include +#include #include #include +#include #include #include #include @@ -79,7 +83,7 @@ #include #include #include -#include +#include #include #include #include @@ -212,6 +216,8 @@ Response::ResponseCode AbstractServerSocketInterface::processExtendedSessionComm return cmdAccountEdit(cmd.GetExtension(Command_AccountEdit::ext), rc); case SessionCommand::ACCOUNT_IMAGE: return cmdAccountImage(cmd.GetExtension(Command_AccountImage::ext), rc); + case SessionCommand::SET_CARD_ART_PARAMS: + return cmdSetCardArtParams(cmd.GetExtension(Command_SetCardArtParams::ext), rc); case SessionCommand::ACCOUNT_PASSWORD: return cmdAccountPassword(cmd.GetExtension(Command_AccountPassword::ext), rc); case SessionCommand::REQUEST_PASSWORD_SALT: @@ -247,6 +253,12 @@ Response::ResponseCode AbstractServerSocketInterface::processExtendedModeratorCo return cmdGetAdminNotes(cmd.GetExtension(Command_GetAdminNotes::ext), rc); case ModeratorCommand::UPDATE_ADMIN_NOTES: return cmdUpdateAdminNotes(cmd.GetExtension(Command_UpdateAdminNotes::ext), rc); + case ModeratorCommand::ADD_CARD_ART_RULE: + return cmdAddCardArtRule(cmd.GetExtension((Command_AddCardArtRule::ext)), rc); + case ModeratorCommand::REMOVE_CARD_ART_RULE: + return cmdRemoveCardArtRule(cmd.GetExtension((Command_RemoveCardArtRule::ext)), rc); + case ModeratorCommand::LIST_CARD_ART_RULES: + return cmdListCardArtRules(cmd.GetExtension((Command_ListCardArtRules::ext)), rc); default: return Response::RespFunctionNotAllowed; } @@ -1565,6 +1577,173 @@ Response::ResponseCode AbstractServerSocketInterface::cmdAccountImage(const Comm return Response::RespOk; } +bool AbstractServerSocketInterface::isCardNameAllowed(const QString &cardName, const QString &cardProviderId) +{ + QSqlQuery *q = sqlInterface->prepareQuery( + "SELECT mode FROM {prefix}_card_art_name_rules WHERE card_name = :name AND card_provider_id = :provider"); + + q->bindValue(":name", cardName); + q->bindValue(":provider", cardProviderId); + + if (!sqlInterface->execSqlQuery(q)) { + qWarning() << "Card art rule lookup failed; failing open for" << cardName; + return true; + } + + if (!q->next()) { + return true; // default allow + } + + return q->value(0).toString() != "DENY"; +} + +Response::ResponseCode AbstractServerSocketInterface::cmdSetCardArtParams(const Command_SetCardArtParams &cmd, + ResponseContainer & /* rc */) +{ + if (authState != PasswordRight) { + return Response::RespFunctionNotAllowed; + } + + const QString cardName = QString::fromStdString(cmd.card_name()); + const QString cardProviderId = QString::fromStdString(cmd.card_provider_id()); + + if (cardName.length() > MAX_NAME_LENGTH || cardProviderId.length() > MAX_NAME_LENGTH) { + return Response::RespInvalidData; + } + + if (cardName.isEmpty()) { + // Removal path + QSqlQuery *q = sqlInterface->prepareQuery("UPDATE {prefix}_users SET card_art_params = NULL WHERE id = :id"); + q->bindValue(":id", userInfo->id()); + if (!sqlInterface->execSqlQuery(q)) { + return Response::RespInternalError; + } + userInfo->clear_card_art_params(); + server->broadcastUserInfoUpdate(this); + return Response::RespOk; + } + + if (!isCardNameAllowed(cardName, cardProviderId)) { + return Response::RespFunctionNotAllowed; + } + + // Clamp everything to sane ranges server-side so a malicious client + // can't store garbage that breaks other clients' rendering. + const double marginPctL = qBound(0.0, cmd.margin_pct_l(), 0.95); + const double marginPctR = qBound(0.0, cmd.margin_pct_r(), 0.95); + const double verticalOffset = qBound(0.0, cmd.vertical_offset(), 1.0); + const double zoom = qBound(0.1, cmd.zoom(), 4.0); + + QJsonObject obj; + obj["card_name"] = cardName; + obj["card_provider_id"] = cardProviderId; + obj["marginPctL"] = marginPctL; + obj["marginPctR"] = marginPctR; + obj["verticalOffset"] = verticalOffset; + obj["zoom"] = zoom; + const QString json = QString::fromUtf8(QJsonDocument(obj).toJson(QJsonDocument::Compact)); + + QSqlQuery *query = sqlInterface->prepareQuery("update {prefix}_users set card_art_params=:params where id=:id"); + query->bindValue(":params", json); + query->bindValue(":id", userInfo->id()); + if (!sqlInterface->execSqlQuery(query)) { + return Response::RespInternalError; + } + + // Keep the in-memory userInfo in sync + auto *cap = userInfo->mutable_card_art_params(); + cap->set_card_name(cmd.card_name()); + cap->set_card_provider_id(cmd.card_provider_id()); + cap->set_margin_pct_l(marginPctL); + cap->set_margin_pct_r(marginPctR); + cap->set_vertical_offset(verticalOffset); + cap->set_zoom(zoom); + + const QString name = QString::fromStdString(userInfo->name()); + server->broadcastUserInfoUpdate(this); + + return Response::RespOk; +} + +Response::ResponseCode AbstractServerSocketInterface::cmdAddCardArtRule(const Command_AddCardArtRule &cmd, + ResponseContainer &) +{ + const QString cardName = QString::fromStdString(cmd.card_name()); + const QString cardProviderId = QString::fromStdString(cmd.card_provider_id()); + const QString mode = QString::fromStdString(cmd.mode()); + + if (mode != "ALLOW" && mode != "DENY") { + return Response::RespInvalidData; + } + if (cardName.isEmpty() || cardName.length() > MAX_NAME_LENGTH || cardProviderId.length() > MAX_NAME_LENGTH) { + return Response::RespInvalidData; + } + + QSqlQuery *q = sqlInterface->prepareQuery("INSERT INTO {prefix}_card_art_name_rules " + "(card_name, card_provider_id, mode, reason, created_by) " + "VALUES (:name, :provider, :mode, :reason, :uid) " + "ON DUPLICATE KEY UPDATE mode=:mode2, reason=:reason2"); + + q->bindValue(":name", cardName); + q->bindValue(":provider", cardProviderId); + q->bindValue(":mode", mode); + q->bindValue(":mode2", mode); + q->bindValue(":reason", QString::fromStdString(cmd.reason())); + q->bindValue(":reason2", QString::fromStdString(cmd.reason())); + q->bindValue(":uid", userInfo->id()); + + if (!sqlInterface->execSqlQuery(q)) { + return Response::RespInternalError; + } + + return Response::RespOk; +} + +Response::ResponseCode AbstractServerSocketInterface::cmdRemoveCardArtRule(const Command_RemoveCardArtRule &cmd, + ResponseContainer &) +{ + auto cardName = QString::fromStdString(cmd.card_name()); + auto cardProviderId = QString::fromStdString(cmd.card_provider_id()); + if (cardName.length() > MAX_NAME_LENGTH || cardProviderId.length() > MAX_NAME_LENGTH) { + return Response::RespInvalidData; + } + QSqlQuery *q = sqlInterface->prepareQuery( + "DELETE FROM {prefix}_card_art_name_rules WHERE card_name=:name AND card_provider_id=:provider"); + + q->bindValue(":name", cardName); + q->bindValue(":provider", cardProviderId); + + if (!sqlInterface->execSqlQuery(q)) { + return Response::RespInternalError; + } + + return Response::RespOk; +} + +Response::ResponseCode AbstractServerSocketInterface::cmdListCardArtRules(const Command_ListCardArtRules &, + ResponseContainer &rc) +{ + QSqlQuery *q = sqlInterface->prepareQuery( + "SELECT card_name, card_provider_id, mode, reason FROM {prefix}_card_art_name_rules"); + + if (!sqlInterface->execSqlQuery(q)) { + return Response::RespInternalError; + } + + auto *re = new Response_ListCardArtRules; + + while (q->next()) { + auto *entry = re->add_entries(); + entry->set_card_name(q->value(0).toString().toStdString()); + entry->set_card_provider_id(q->value(1).toString().toStdString()); + entry->set_mode(q->value(2).toString().toStdString()); + entry->set_reason(q->value(3).toString().toStdString()); + } + + rc.setResponseExtension(re); + return Response::RespOk; +} + Response::ResponseCode AbstractServerSocketInterface::cmdAccountPassword(const Command_AccountPassword &cmd, ResponseContainer & /* rc */) { diff --git a/servatrice/src/serversocketinterface.h b/servatrice/src/serversocketinterface.h index e10aa0dde..0d66ae78f 100644 --- a/servatrice/src/serversocketinterface.h +++ b/servatrice/src/serversocketinterface.h @@ -129,6 +129,11 @@ private: Response::ResponseCode cmdAccountEdit(const Command_AccountEdit &cmd, ResponseContainer &rc); Response::ResponseCode cmdAccountImage(const Command_AccountImage &cmd, ResponseContainer &rc); + bool isCardNameAllowed(const QString &cardName, const QString &cardProviderId); + Response::ResponseCode cmdSetCardArtParams(const Command_SetCardArtParams &cmd, ResponseContainer &); + Response::ResponseCode cmdAddCardArtRule(const Command_AddCardArtRule &cmd, ResponseContainer &); + Response::ResponseCode cmdRemoveCardArtRule(const Command_RemoveCardArtRule &cmd, ResponseContainer &); + Response::ResponseCode cmdListCardArtRules(const Command_ListCardArtRules &, ResponseContainer &rc); Response::ResponseCode cmdAccountPassword(const Command_AccountPassword &cmd, ResponseContainer &rc); Response::ResponseCode cmdGrantReplayAccess(const Command_GrantReplayAccess &cmd, ResponseContainer &rc); Response::ResponseCode cmdForceActivateUser(const Command_ForceActivateUser &cmd, ResponseContainer &rc); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 00eba288e..a179a3603 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -4,6 +4,7 @@ enable_testing() add_test(NAME dummy_test COMMAND dummy_test) 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 server_card_counter_test COMMAND server_card_counter_test) @@ -16,6 +17,7 @@ set_tests_properties(deck_hash_performance_test PROPERTIES TIMEOUT 5) add_executable(dummy_test dummy_test.cpp) 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(deck_hash_performance_test deck_hash_performance_test.cpp) @@ -49,6 +51,7 @@ if(NOT GTEST_FOUND) set(GTEST_BOTH_LIBRARIES gtest) add_dependencies(dummy_test gtest) add_dependencies(expression_test gtest) + add_dependencies(clamped_arithmetic_test gtest) add_dependencies(test_age_formatting gtest) add_dependencies(password_hash_test gtest) add_dependencies(deck_hash_performance_test gtest) @@ -59,7 +62,12 @@ endif() include_directories(${GTEST_INCLUDE_DIRS}) target_link_libraries(dummy_test Threads::Threads ${GTEST_BOTH_LIBRARIES}) target_link_libraries(expression_test libcockatrice_utility Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES}) -target_link_libraries(test_age_formatting Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES}) +target_link_libraries( + clamped_arithmetic_test libcockatrice_utility Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES} +) +target_link_libraries( + test_age_formatting libcockatrice_utility Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES} +) target_link_libraries( password_hash_test libcockatrice_utility Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES} ) diff --git a/tests/clamped_arithmetic_test.cpp b/tests/clamped_arithmetic_test.cpp new file mode 100644 index 000000000..2471d5870 --- /dev/null +++ b/tests/clamped_arithmetic_test.cpp @@ -0,0 +1,44 @@ +/** @file clamped_arithmetic_test.cpp + * @brief Tests for shared helpers in clamped_arithmetic.h. + * @ingroup Tests + */ + +#include +#include +#include + +TEST(AddClamped, AddsWithinBounds) +{ + EXPECT_EQ(addClamped(5, 3, 0, 100), 8); + EXPECT_EQ(addClamped(10, -3, 0, 100), 7); +} + +TEST(AddClamped, ClampsToUpperAndLowerBound) +{ + EXPECT_EQ(addClamped(99, 5, 0, 100), 100); // saturates at max + EXPECT_EQ(addClamped(2, -10, 0, 100), 0); // saturates at min + EXPECT_EQ(addClamped(999, 1, 0, 999), 999); // crossing the counter cap holds at the bound +} + +TEST(AddClamped, IntOverflowDoesNotWrap) +{ + // The 64-bit intermediate must prevent signed-int overflow UB. + constexpr int intMax = std::numeric_limits::max(); + constexpr int intMin = std::numeric_limits::min(); + EXPECT_EQ(addClamped(intMax, 1, intMin, intMax), intMax); + EXPECT_EQ(addClamped(intMax, intMax, intMin, intMax), intMax); +} + +TEST(AddClamped, IntUnderflowDoesNotWrap) +{ + constexpr int intMax = std::numeric_limits::max(); + constexpr int intMin = std::numeric_limits::min(); + EXPECT_EQ(addClamped(intMin, -1, intMin, intMax), intMin); + EXPECT_EQ(addClamped(intMin, intMin, intMin, intMax), intMin); +} + +int main(int argc, char **argv) +{ + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/tests/server_card_counter_test.cpp b/tests/server_card_counter_test.cpp index ff906b906..c8bc43f8f 100644 --- a/tests/server_card_counter_test.cpp +++ b/tests/server_card_counter_test.cpp @@ -7,7 +7,7 @@ #include #include #include -#include +#include #include TEST(ServerCardCounter, IncrementNewCounter) @@ -28,9 +28,9 @@ TEST(ServerCardCounter, IncrementExistingCounter) TEST(ServerCardCounter, IncrementOverflowProtection) { Server_Card card(CardRef{"TestCard", ""}, 1, 0, 0); - ASSERT_TRUE(card.setCounter(1, MAX_COUNTERS_ON_CARD)); + ASSERT_TRUE(card.setCounter(1, MAX_COUNTER_VALUE)); EXPECT_FALSE(card.incrementCounter(1, 1)); - EXPECT_EQ(card.getCounter(1), MAX_COUNTERS_ON_CARD); + EXPECT_EQ(card.getCounter(1), MAX_COUNTER_VALUE); } TEST(ServerCardCounter, DecrementUnderflowProtection) @@ -113,13 +113,13 @@ TEST(ServerCardCounter, IncrementCounterPopulatesEvent) TEST(ServerCardCounter, IncrementCounterEventReflectsClampedValue) { Server_Card card(CardRef{"TestCard", ""}, 1, 0, 0); - ASSERT_TRUE(card.setCounter(1, MAX_COUNTERS_ON_CARD - 5)); + ASSERT_TRUE(card.setCounter(1, MAX_COUNTER_VALUE - 5)); Event_SetCardCounter event; EXPECT_TRUE(card.incrementCounter(1, 10, &event)); EXPECT_EQ(event.counter_id(), 1); - EXPECT_EQ(event.counter_value(), MAX_COUNTERS_ON_CARD); + EXPECT_EQ(event.counter_value(), MAX_COUNTER_VALUE); } TEST(ServerCardCounter, IncrementCounterNoEventWhenNullptr) @@ -133,7 +133,7 @@ TEST(ServerCardCounter, IncrementCounterNoEventWhenNullptr) TEST(ServerCardCounter, IncrementCounterEventNotPopulatedWhenUnchanged) { Server_Card card(CardRef{"TestCard", ""}, 1, 0, 0); - ASSERT_TRUE(card.setCounter(1, MAX_COUNTERS_ON_CARD)); + ASSERT_TRUE(card.setCounter(1, MAX_COUNTER_VALUE)); Event_SetCardCounter event; event.set_counter_id(999); @@ -156,7 +156,7 @@ TEST(ServerCardCounter, SetCounterClampsAboveMaxToMax) { Server_Card card(CardRef{"TestCard", ""}, 1, 0, 0); EXPECT_TRUE(card.setCounter(1, 1500)); - EXPECT_EQ(card.getCounter(1), MAX_COUNTERS_ON_CARD); + EXPECT_EQ(card.getCounter(1), MAX_COUNTER_VALUE); } TEST(ServerCardCounter, IncrementDoesNotGoBelowZero) @@ -171,9 +171,9 @@ TEST(ServerCardCounter, IncrementDoesNotGoBelowZero) TEST(ServerCardCounter, IncrementDoesNotExceedMax) { Server_Card card(CardRef{"TestCard", ""}, 1, 0, 0); - ASSERT_TRUE(card.setCounter(1, MAX_COUNTERS_ON_CARD - 5)); + ASSERT_TRUE(card.setCounter(1, MAX_COUNTER_VALUE - 5)); EXPECT_TRUE(card.incrementCounter(1, 10)); - EXPECT_EQ(card.getCounter(1), MAX_COUNTERS_ON_CARD); + EXPECT_EQ(card.getCounter(1), MAX_COUNTER_VALUE); } int main(int argc, char **argv) diff --git a/tests/test_age_formatting.cpp b/tests/test_age_formatting.cpp index e4fc64cf9..6a9d5d4af 100644 --- a/tests/test_age_formatting.cpp +++ b/tests/test_age_formatting.cpp @@ -1,6 +1,5 @@ -#include "../cockatrice/src/interface/widgets/server/user/user_info_box.h" - #include "gtest/gtest.h" +#include namespace { @@ -8,31 +7,31 @@ using dayyear = QPair; TEST(AgeFormatting, Zero) { - auto got = UserInfoBox::getDaysAndYearsBetween(QDate(2000, 1, 1), QDate(2000, 1, 1)); + auto got = getDaysAndYearsBetween(QDate(2000, 1, 1), QDate(2000, 1, 1)); ASSERT_EQ(got, dayyear(0, 0)) << "these are the same day"; } TEST(AgeFormatting, LeapDay) { - auto got = UserInfoBox::getDaysAndYearsBetween(QDate(2000, 2, 28), QDate(2000, 3, 1)); + auto got = getDaysAndYearsBetween(QDate(2000, 2, 28), QDate(2000, 3, 1)); ASSERT_EQ(got, dayyear(2, 0)) << "there is a leap day in between these days"; } TEST(AgeFormatting, LeapYear) { - auto got = UserInfoBox::getDaysAndYearsBetween(QDate(2000, 1, 1), QDate(2001, 1, 1)); + auto got = getDaysAndYearsBetween(QDate(2000, 1, 1), QDate(2001, 1, 1)); ASSERT_EQ(got, dayyear(0, 1)) << "there is a leap day in between these dates, but that's fine"; } TEST(AgeFormatting, LeapDayWithYear) { - auto got = UserInfoBox::getDaysAndYearsBetween(QDate(2000, 2, 28), QDate(2001, 3, 1)); + auto got = getDaysAndYearsBetween(QDate(2000, 2, 28), QDate(2001, 3, 1)); ASSERT_EQ(got, dayyear(1, 1)) << "there is a leap day in between these days but not in the last year"; } TEST(AgeFormatting, LeapDayThisYear) { - auto got = UserInfoBox::getDaysAndYearsBetween(QDate(2003, 2, 28), QDate(2004, 3, 1)); + auto got = getDaysAndYearsBetween(QDate(2003, 2, 28), QDate(2004, 3, 1)); ASSERT_EQ(got, dayyear(2, 1)) << "there is a leap day in between these days this year"; } } // namespace