mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-11-09 19:43:26 -08:00
Compare commits
15 commits
64f58535dd
...
77a0253242
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
77a0253242 | ||
|
|
c08b345b71 | ||
|
|
1a4d89ff8f | ||
|
|
4feb31bd1a | ||
|
|
4c5a5e3e55 | ||
|
|
5d06bb903d | ||
|
|
57a6a0d9cb | ||
|
|
f83a61979f | ||
|
|
07eb9f0103 | ||
|
|
38acddeede | ||
|
|
74a6acde91 | ||
|
|
4a010bd17f | ||
|
|
315cbae345 | ||
|
|
bdda3dedfa | ||
|
|
8822ade49d |
71 changed files with 2344 additions and 1000 deletions
|
|
@ -9,6 +9,7 @@ RUN apt-get update && \
|
||||||
cmake \
|
cmake \
|
||||||
file \
|
file \
|
||||||
g++ \
|
g++ \
|
||||||
|
gcovr \
|
||||||
git \
|
git \
|
||||||
libgl-dev \
|
libgl-dev \
|
||||||
liblzma-dev \
|
liblzma-dev \
|
||||||
|
|
|
||||||
|
|
@ -8,14 +8,17 @@
|
||||||
# --suffix <suffix> renames package with this suffix, requires arg
|
# --suffix <suffix> renames package with this suffix, requires arg
|
||||||
# --server compiles servatrice
|
# --server compiles servatrice
|
||||||
# --test runs tests
|
# --test runs tests
|
||||||
|
# --sanitize builds with AddressSanitizer and UndefinedBehaviorSanitizer, gcc/clang only
|
||||||
|
# --coverage instruments the test run for line coverage and prints a gcovr summary, gcc only
|
||||||
|
|
||||||
# --debug or --release sets the build type ie CMAKE_BUILD_TYPE
|
# --debug or --release sets the build type ie CMAKE_BUILD_TYPE
|
||||||
# --ccache [<size>] uses ccache and shows stats, optionally provide size
|
# --ccache [<size>] uses ccache and shows stats, optionally provide size
|
||||||
# --evict-ccache <age> runs ccache eviction based on given age after build
|
# --evict-ccache <age> runs ccache eviction based on given age after build
|
||||||
# --dir <dir> sets the name of the build dir, default is "build"
|
# --dir <dir> sets the name of the build dir, default is "build"
|
||||||
# --cmake-generator <generator> sets CMAKE_GENERATOR as used by cmake
|
# --cmake-generator <generator> sets CMAKE_GENERATOR as used by cmake
|
||||||
# --target-macos-version <version> sets the min os version - only used for macOS builds
|
# --target-macos-version <version> sets the min os version - only used for macOS builds
|
||||||
# uses env: BUILDTYPE MAKE_INSTALL MAKE_PACKAGE PACKAGE_TYPE PACKAGE_SUFFIX MAKE_SERVER MAKE_NO_CLIENT MAKE_TEST USE_CCACHE CCACHE_SIZE CCACHE_EVICTION_AGE BUILD_DIR CMAKE_GENERATOR TARGET_MACOS_VERSION
|
# uses env: BUILDTYPE MAKE_INSTALL MAKE_PACKAGE PACKAGE_TYPE PACKAGE_SUFFIX MAKE_SERVER MAKE_NO_CLIENT MAKE_TEST USE_CCACHE CCACHE_SIZE CCACHE_EVICTION_AGE BUILD_DIR CMAKE_GENERATOR TARGET_MACOS_VERSION MAKE_SANITIZE MAKE_COVERAGE
|
||||||
# (correspond to args: --debug/--release --install --package <package type> --suffix <suffix> --server --test --ccache <ccache_size> --dir <dir>)
|
# (correspond to args: --debug/--release --install --package <package type> --suffix <suffix> --server --test --sanitize --coverage --ccache <ccache_size> --dir <dir>)
|
||||||
# exitcode: 1 for failure, 3 for invalid arguments
|
# exitcode: 1 for failure, 3 for invalid arguments
|
||||||
|
|
||||||
# Read arguments
|
# Read arguments
|
||||||
|
|
@ -57,6 +60,14 @@ while [[ $# != 0 ]]; do
|
||||||
MAKE_TEST=1
|
MAKE_TEST=1
|
||||||
shift
|
shift
|
||||||
;;
|
;;
|
||||||
|
'--sanitize')
|
||||||
|
MAKE_SANITIZE=1
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
'--coverage')
|
||||||
|
MAKE_COVERAGE=1
|
||||||
|
shift
|
||||||
|
;;
|
||||||
'--debug')
|
'--debug')
|
||||||
BUILDTYPE="Debug"
|
BUILDTYPE="Debug"
|
||||||
shift
|
shift
|
||||||
|
|
@ -131,6 +142,15 @@ if [[ ! $BUILD_DIR ]]; then
|
||||||
BUILD_DIR="build"
|
BUILD_DIR="build"
|
||||||
fi
|
fi
|
||||||
mkdir -p "$BUILD_DIR"
|
mkdir -p "$BUILD_DIR"
|
||||||
|
|
||||||
|
if [[ $MAKE_COVERAGE ]]; then
|
||||||
|
# Resolved while the working directory is still the source root. $0 is relative to the
|
||||||
|
# directory the script was invoked from, so after the cd below it no longer resolves, and
|
||||||
|
# under `set -e` the failed subshell would abort the build rather than just yielding an empty
|
||||||
|
# variable. Only the coverage report needs this, so only coverage pays for it.
|
||||||
|
SRC_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
|
fi
|
||||||
|
|
||||||
cd "$BUILD_DIR"
|
cd "$BUILD_DIR"
|
||||||
|
|
||||||
# Set minimum CMake Version
|
# Set minimum CMake Version
|
||||||
|
|
@ -147,6 +167,17 @@ fi
|
||||||
if [[ $MAKE_TEST ]]; then
|
if [[ $MAKE_TEST ]]; then
|
||||||
flags+=("-DTEST=1")
|
flags+=("-DTEST=1")
|
||||||
fi
|
fi
|
||||||
|
if [[ $MAKE_SANITIZE ]]; then
|
||||||
|
flags+=("-DENABLE_SANITIZERS=ON")
|
||||||
|
else
|
||||||
|
# The release-package step re-runs cmake in the same build dir, so an
|
||||||
|
# ENABLE_SANITIZERS=ON left over from the debug step would otherwise leak
|
||||||
|
# into the shipped binaries (libasan/libubsan Depends in the DEB).
|
||||||
|
flags+=("-DENABLE_SANITIZERS=OFF")
|
||||||
|
fi
|
||||||
|
if [[ $MAKE_COVERAGE ]]; then
|
||||||
|
flags+=("-DENABLE_COVERAGE=ON")
|
||||||
|
fi
|
||||||
if [[ $USE_CCACHE ]]; then
|
if [[ $USE_CCACHE ]]; then
|
||||||
flags+=("-DUSE_CCACHE=1")
|
flags+=("-DUSE_CCACHE=1")
|
||||||
# PCH-aware caching is required or ccache refuses to cache any TU that
|
# PCH-aware caching is required or ccache refuses to cache any TU that
|
||||||
|
|
@ -306,7 +337,50 @@ fi
|
||||||
|
|
||||||
if [[ $MAKE_TEST ]]; then
|
if [[ $MAKE_TEST ]]; then
|
||||||
echo "::group::Run tests"
|
echo "::group::Run tests"
|
||||||
ctest -C "$BUILDTYPE" --output-on-failure
|
if [[ $MAKE_SANITIZE ]]; then
|
||||||
|
# detect_leaks=0: Qt and the sanitizers' own runtimes both leak at exit by design, and
|
||||||
|
# LeakSanitizer's default is aggressive enough to bury our real findings under hundreds
|
||||||
|
# of process-teardown reports. Malloc/free misuse and use-after-free are what ASan is for
|
||||||
|
# and they stay fully on.
|
||||||
|
# abort_on_error=1: a bad access leaves the process in an unusable state, so continuing
|
||||||
|
# only produces a second, more confusing report from the wreckage.
|
||||||
|
# UBSAN halt_on_error=1: without it UBSan prints "runtime error: ..." but the
|
||||||
|
# process exits 0 (recover mode), so gtest/ctest report a pass and the job
|
||||||
|
# stays green. Combined with -fno-sanitize-recover=undefined at compile time,
|
||||||
|
# the first finding aborts the test process with a non-zero exit.
|
||||||
|
export ASAN_OPTIONS="detect_leaks=0:abort_on_error=1:print_stacktrace=1:symbolize=1"
|
||||||
|
export UBSAN_OPTIONS="print_stacktrace=1:halt_on_error=1:symbolize=1"
|
||||||
|
fi
|
||||||
|
# Every TEST() is its own CTest entry, so the suite is process-spawn bound rather than CPU
|
||||||
|
# bound and -j recovers nearly all of the per-process overhead. The test binaries are safe to
|
||||||
|
# run concurrently: the only one that claims a fixed resource is single_instance_manager_test,
|
||||||
|
# which already scopes its local socket name to its own pid so parallel copies cannot collide.
|
||||||
|
ctest -C "$BUILDTYPE" --output-on-failure -j "${CTEST_JOBS:-4}"
|
||||||
|
echo "::endgroup::"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ $MAKE_COVERAGE ]]; then
|
||||||
|
# gcovr is not a build dependency and is not available everywhere, so a missing one is reported
|
||||||
|
# and skipped rather than failing an otherwise green build.
|
||||||
|
echo "::group::Coverage report"
|
||||||
|
GCOVR="$(command -v gcovr || true)"
|
||||||
|
if [[ -z $GCOVR ]]; then
|
||||||
|
echo "gcovr not found; install it to produce a coverage report"
|
||||||
|
else
|
||||||
|
"$GCOVR" --version
|
||||||
|
# This script configures with `cmake ..`, so the build dir it was started from is the object
|
||||||
|
# directory and the source root is one level up. Both are passed explicitly: gcovr otherwise
|
||||||
|
# walks the current directory and trips over in-tree build directories left by other tooling,
|
||||||
|
# whose stale .gcno files cannot be resolved. --config is named too, so the filters apply no
|
||||||
|
# matter what directory gcovr happens to start from.
|
||||||
|
GCOVR_ARGS=("." --root "$SRC_ROOT" --object-directory "." --config "$SRC_ROOT/gcovr.cfg")
|
||||||
|
"$GCOVR" "${GCOVR_ARGS[@]}" --txt --decisions
|
||||||
|
"$GCOVR" "${GCOVR_ARGS[@]}" --html-details coverage-details.html --html-title "Cockatrice line coverage"
|
||||||
|
"$GCOVR" "${GCOVR_ARGS[@]}" --lcov -o coverage.lcov
|
||||||
|
"$GCOVR" "${GCOVR_ARGS[@]}" --cobertura-pretty -o coverage.xml
|
||||||
|
"$GCOVR" "${GCOVR_ARGS[@]}" --json-summary-pretty -o coverage-summary.json
|
||||||
|
echo "Coverage artifacts written to coverage-details.html, coverage.lcov, coverage.xml and coverage-summary.json"
|
||||||
|
fi
|
||||||
echo "::endgroup::"
|
echo "::endgroup::"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,11 +13,17 @@
|
||||||
# --set-cache <location> sets the location to cache the image or for ccache
|
# --set-cache <location> sets the location to cache the image or for ccache
|
||||||
#
|
#
|
||||||
# requires: docker
|
# requires: docker
|
||||||
# uses env: NAME CACHE BUILD GET SAVE INTERACTIVE
|
# uses env: NAME IMAGE_SUFFIX CACHE BUILD GET SAVE INTERACTIVE
|
||||||
# (correspond to args: <name> --set-cache <cache> --build --get --save --interactive)
|
# (correspond to args: <name> --set-cache <cache> --build --get --save --interactive)
|
||||||
# sets env: RUN CCACHE_DIR IMAGE_NAME RUN_ARGS RUN_OPTS BUILD_SCRIPT
|
# sets env: RUN CCACHE_DIR IMAGE_NAME RUN_ARGS RUN_OPTS BUILD_SCRIPT
|
||||||
# exitcode: 1 for failure, 2 for missing dockerfile, 3 for invalid arguments
|
# exitcode: 1 for failure, 2 for missing dockerfile, 3 for invalid arguments
|
||||||
#
|
#
|
||||||
|
# IMAGE_SUFFIX is optional and only appended to the image name, never to NAME, so that a matrix
|
||||||
|
# leg can have its own image and image cache without needing its own directory under .ci. Two legs
|
||||||
|
# building and tagging the same image concurrently would race on `docker build --tag`.
|
||||||
|
# It is deliberately not called SUFFIX: compile.sh already uses SUFFIX for output file names, and
|
||||||
|
# a job-level SUFFIX would be silently overridden by those per-step env blocks.
|
||||||
|
#
|
||||||
# exported RUN function will run the BUILD_SCRIPT inside of the docker container.
|
# exported RUN function will run the BUILD_SCRIPT inside of the docker container.
|
||||||
# note that the docker container will not inherit any environment variables!
|
# note that the docker container will not inherit any environment variables!
|
||||||
#
|
#
|
||||||
|
|
@ -81,7 +87,7 @@ if ! [[ $NAME ]]; then
|
||||||
return 3
|
return 3
|
||||||
fi
|
fi
|
||||||
|
|
||||||
export IMAGE_NAME="${project_name,,}_${NAME,,}" # lower case
|
export IMAGE_NAME="${project_name,,}_${NAME,,}${IMAGE_SUFFIX:+_${IMAGE_SUFFIX,,}}" # lower case
|
||||||
|
|
||||||
docker_dir=".ci/$NAME"
|
docker_dir=".ci/$NAME"
|
||||||
if ! [[ -r $docker_dir/Dockerfile ]]; then
|
if ! [[ -r $docker_dir/Dockerfile ]]; then
|
||||||
|
|
|
||||||
56
.github/workflows/desktop-build.yml
vendored
56
.github/workflows/desktop-build.yml
vendored
|
|
@ -144,17 +144,36 @@ jobs:
|
||||||
|
|
||||||
package: DEB
|
package: DEB
|
||||||
|
|
||||||
name: ${{ matrix.distro }} ${{ matrix.version }}
|
# The one leg that runs the suite under AddressSanitizer and
|
||||||
|
# UndefinedBehaviorSanitizer. Debian 13 and Fedora 44 keep covering the
|
||||||
|
# uninstrumented suite, so nothing is lost by instrumenting this one.
|
||||||
|
sanitize: yes
|
||||||
|
|
||||||
|
- distro: Ubuntu
|
||||||
|
version: 26.04
|
||||||
|
variant: cover # separate image and ccache, same distro
|
||||||
|
|
||||||
|
# The one leg that runs instrumented for line coverage and publishes the report as
|
||||||
|
# a workflow artifact. It is a separate leg rather than a second flag on the
|
||||||
|
# sanitizer leg because the two instruments answer different questions: the
|
||||||
|
# sanitizer leg reports what the run did wrong, the coverage leg reports what the
|
||||||
|
# run never touched at all. Combining them would also slow both down. Packaging is
|
||||||
|
# skipped because the leg above already publishes the DEB for this distro.
|
||||||
|
coverage: yes
|
||||||
|
package: skip
|
||||||
|
|
||||||
|
name: ${{ matrix.distro }} ${{ matrix.version }}${{ matrix.variant && format(' {0}', matrix.variant) || '' }}
|
||||||
needs: configure
|
needs: configure
|
||||||
runs-on: ubuntu-latest # https://github.com/actions/runner-images
|
runs-on: ubuntu-latest # https://github.com/actions/runner-images
|
||||||
continue-on-error: ${{ matrix.allow-failure == 'yes' }}
|
continue-on-error: ${{ matrix.allow-failure == 'yes' }}
|
||||||
timeout-minutes: 70
|
timeout-minutes: 70
|
||||||
env:
|
env:
|
||||||
CACHE: ${{ github.workspace }}/.cache/${{ matrix.distro }}${{ matrix.version }} # directory for caching docker image and ccache
|
CACHE: ${{ github.workspace }}/.cache/${{ matrix.distro }}${{ matrix.version }}${{ matrix.variant }} # directory for caching docker image and ccache
|
||||||
CCACHE_EVICTION_AGE: 7d
|
CCACHE_EVICTION_AGE: 7d
|
||||||
CCACHE_SIZE: 600M # space of all repo is 10Gi: https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#usage-limits-and-eviction-policy
|
CCACHE_SIZE: 600M # space of all repo is 10Gi: https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#usage-limits-and-eviction-policy
|
||||||
CMAKE_GENERATOR: 'Ninja'
|
CMAKE_GENERATOR: 'Ninja'
|
||||||
NAME: ${{ matrix.distro }}${{ matrix.version }}
|
NAME: ${{ matrix.distro }}${{ matrix.version }}
|
||||||
|
IMAGE_SUFFIX: ${{ matrix.variant }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: "Checkout"
|
- name: "Checkout"
|
||||||
|
|
@ -164,7 +183,7 @@ jobs:
|
||||||
id: restore_ccache
|
id: restore_ccache
|
||||||
uses: actions/cache/restore@v6
|
uses: actions/cache/restore@v6
|
||||||
with:
|
with:
|
||||||
key: ccache-${{ matrix.distro }}${{ matrix.version }}
|
key: ccache-${{ matrix.distro }}${{ matrix.version }}${{ matrix.variant }}
|
||||||
path: ${{ env.CACHE }}
|
path: ${{ env.CACHE }}
|
||||||
|
|
||||||
- name: "Build ${{ matrix.distro }} ${{ matrix.version }} Docker image"
|
- name: "Build ${{ matrix.distro }} ${{ matrix.version }} Docker image"
|
||||||
|
|
@ -174,6 +193,9 @@ jobs:
|
||||||
- name: "Build debug and test"
|
- name: "Build debug and test"
|
||||||
if: matrix.test != 'skip'
|
if: matrix.test != 'skip'
|
||||||
shell: bash
|
shell: bash
|
||||||
|
env:
|
||||||
|
sanitize: '${{ matrix.sanitize }}'
|
||||||
|
coverage: '${{ matrix.coverage }}'
|
||||||
run: |
|
run: |
|
||||||
source .ci/docker.sh
|
source .ci/docker.sh
|
||||||
args=()
|
args=()
|
||||||
|
|
@ -181,7 +203,33 @@ jobs:
|
||||||
args+=(--ccache "$CCACHE_SIZE")
|
args+=(--ccache "$CCACHE_SIZE")
|
||||||
args+=(--cmake-generator "$CMAKE_GENERATOR")
|
args+=(--cmake-generator "$CMAKE_GENERATOR")
|
||||||
|
|
||||||
RUN --server --debug --test "${args[@]}"
|
extra=()
|
||||||
|
[[ $sanitize == yes ]] && extra+=(--sanitize)
|
||||||
|
[[ $coverage == yes ]] && extra+=(--coverage)
|
||||||
|
|
||||||
|
RUN --server --debug --test "${extra[@]}" "${args[@]}"
|
||||||
|
|
||||||
|
# RUN bind-mounts the workspace into the container, so the report the build wrote into
|
||||||
|
# build/ is already on the host and no second container is needed to collect it.
|
||||||
|
- name: "Collect coverage report"
|
||||||
|
if: matrix.coverage == 'yes'
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
mkdir -p coverage
|
||||||
|
# --html-details writes one page per source file plus a stylesheet and script, and the
|
||||||
|
# index links to all of them by relative name. Copying the index alone would publish a
|
||||||
|
# report where every link 404s, so the whole family has to travel together.
|
||||||
|
cp build/coverage-details* coverage/
|
||||||
|
cp build/coverage.lcov build/coverage.xml build/coverage-summary.json coverage/
|
||||||
|
ls -la coverage/
|
||||||
|
|
||||||
|
- name: "Upload coverage artifact"
|
||||||
|
if: matrix.coverage == 'yes'
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: coverage-report
|
||||||
|
path: coverage/
|
||||||
|
if-no-files-found: warn
|
||||||
|
|
||||||
- name: "Build release package"
|
- name: "Build release package"
|
||||||
id: build
|
id: build
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,10 @@ option(WITH_ORACLE "Build Cockatrice card database tool (Oracle)" ON)
|
||||||
option(WITH_SERVER "Build Cockatrice server (Servatrice)" OFF)
|
option(WITH_SERVER "Build Cockatrice server (Servatrice)" OFF)
|
||||||
# Compile tests
|
# Compile tests
|
||||||
option(TEST "Build tests" OFF)
|
option(TEST "Build tests" OFF)
|
||||||
|
# Build with AddressSanitizer and UndefinedBehaviorSanitizer
|
||||||
|
option(ENABLE_SANITIZERS "Build with AddressSanitizer and UndefinedBehaviorSanitizer" OFF)
|
||||||
|
# Build instrumented for GCC line coverage, reported with gcovr
|
||||||
|
option(ENABLE_COVERAGE "Build instrumented for GCC line coverage" OFF)
|
||||||
# Use vcpkg regardless of OS
|
# Use vcpkg regardless of OS
|
||||||
option(USE_VCPKG "Use vcpkg regardless of OS" OFF)
|
option(USE_VCPKG "Use vcpkg regardless of OS" OFF)
|
||||||
|
|
||||||
|
|
@ -211,6 +215,12 @@ else()
|
||||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -pipe")
|
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -pipe")
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
# Always included, because they define cockatrice_disable_sanitizers_for() and
|
||||||
|
# cockatrice_disable_coverage_for() for the vendored trees. They only touch the global flags
|
||||||
|
# when ENABLE_SANITIZERS or ENABLE_COVERAGE is on.
|
||||||
|
include(Sanitizers)
|
||||||
|
include(Coverage)
|
||||||
|
|
||||||
# GNU systems need to define the Mersenne exponent for the RNG to compile w/o warning
|
# GNU systems need to define the Mersenne exponent for the RNG to compile w/o warning
|
||||||
if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
|
if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
|
||||||
add_definitions("-DSFMT_MEXP=19937")
|
add_definitions("-DSFMT_MEXP=19937")
|
||||||
|
|
|
||||||
75
cmake/Coverage.cmake
Normal file
75
cmake/Coverage.cmake
Normal file
|
|
@ -0,0 +1,75 @@
|
||||||
|
# Line-coverage instrumentation opt-in, plus per-source opt-out
|
||||||
|
# Input: ENABLE_COVERAGE
|
||||||
|
# Defines: cockatrice_disable_coverage_for
|
||||||
|
#
|
||||||
|
# Included once from the root CMakeLists.txt, immediately after the per-compiler flag
|
||||||
|
# block, so add_compile_options() reaches every library and executable defined below.
|
||||||
|
#
|
||||||
|
# Note the name deliberately shadows CMake's own built-in Modules/Coverage.cmake. It resolves
|
||||||
|
# to this file only because the root CMakeLists.txt puts cmake/ at the front of
|
||||||
|
# CMAKE_MODULE_PATH; if that ever moves, include(Coverage) would silently pick up the built-in
|
||||||
|
# module, which only defines a `COVERAGE` custom target and touches no global flags, so
|
||||||
|
# instrumentation would quietly stop happening rather than erroring.
|
||||||
|
|
||||||
|
include(CheckCXXSourceCompiles)
|
||||||
|
|
||||||
|
# Opt a set of vendored sources out of instrumentation.
|
||||||
|
#
|
||||||
|
# cockatrice_disable_coverage_for(<source> ...)
|
||||||
|
#
|
||||||
|
# Third-party code drags the total down with lines no test can ever reach, and a total that
|
||||||
|
# cannot move is not a useful signal. This is the same carve-out the sanitizer build uses.
|
||||||
|
#
|
||||||
|
# The sources must be named relative to the directory that compiles them. CMake resolves a
|
||||||
|
# source-file property against the directory scope the target was declared in, so calling
|
||||||
|
# this from the root with absolute paths silently does nothing. That is why the call sites
|
||||||
|
# live next to the add_library/add_executable that pull the vendored code in.
|
||||||
|
#
|
||||||
|
# SKIP_PRECOMPILE_HEADERS mirrors the sanitizer helper. The precompiled header carries the
|
||||||
|
# instrumentation flags, and mixing an instrumented PCH with an uninstrumented translation
|
||||||
|
# unit makes GCC reject it.
|
||||||
|
function(cockatrice_disable_coverage_for)
|
||||||
|
if(NOT ENABLE_COVERAGE)
|
||||||
|
return()
|
||||||
|
endif()
|
||||||
|
# There is no -fno-coverage: --coverage is shorthand for -fprofile-arcs -ftest-coverage, so both
|
||||||
|
# halves have to be negated. CC rejects "-fno-coverage" outright, which is how the vendored C
|
||||||
|
# files below hit it first.
|
||||||
|
set_source_files_properties(
|
||||||
|
${ARGN} PROPERTIES COMPILE_OPTIONS "-fno-profile-arcs;-fno-test-coverage" SKIP_PRECOMPILE_HEADERS ON
|
||||||
|
)
|
||||||
|
endfunction()
|
||||||
|
|
||||||
|
if(ENABLE_COVERAGE)
|
||||||
|
# Line coverage needs the compiler's own toolchain, so only GCC is supported. Clang's
|
||||||
|
# equivalent is llvm-cov, which reads a different intermediate format and would need a
|
||||||
|
# separate report path; rejecting it is better than emitting .gcno files that gcovr cannot
|
||||||
|
# read.
|
||||||
|
if(NOT CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
|
||||||
|
message(FATAL_ERROR "ENABLE_COVERAGE requires GCC, but the compiler is ${CMAKE_CXX_COMPILER_ID}. "
|
||||||
|
"Clang coverage needs llvm-cov and is not wired up."
|
||||||
|
)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# Probe the way the flag will actually be used: compile AND link. A .gcno file comes from the
|
||||||
|
# compile step, but the runtime that writes .gcda files comes from the link step, so probing
|
||||||
|
# only the compile step would pass on a toolchain that cannot actually produce coverage.
|
||||||
|
set(_cockatrice_saved_required_flags "${CMAKE_REQUIRED_FLAGS}")
|
||||||
|
set(_cockatrice_saved_required_link_options "${CMAKE_REQUIRED_LINK_OPTIONS}")
|
||||||
|
set(CMAKE_REQUIRED_FLAGS "-O0 -g")
|
||||||
|
set(CMAKE_REQUIRED_LINK_OPTIONS -fprofile-arcs -ftest-coverage)
|
||||||
|
check_cxx_source_compiles("int main() { return 0; }" COCKATRICE_HAVE_COVERAGE_FLAGS)
|
||||||
|
set(CMAKE_REQUIRED_FLAGS "${_cockatrice_saved_required_flags}")
|
||||||
|
set(CMAKE_REQUIRED_LINK_OPTIONS "${_cockatrice_saved_required_link_options}")
|
||||||
|
|
||||||
|
if(NOT COCKATRICE_HAVE_COVERAGE_FLAGS)
|
||||||
|
message(FATAL_ERROR "ENABLE_COVERAGE is on, but this toolchain rejected the coverage flags.")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# -O0 keeps the line mapping honest: at higher optimisation levels the compiler merges and
|
||||||
|
# relocates lines, and the report describes code that no longer exists in that form.
|
||||||
|
add_compile_options(--coverage -O0 -g)
|
||||||
|
add_link_options(--coverage)
|
||||||
|
|
||||||
|
message(STATUS "Coverage instrumentation enabled (--coverage). Report with gcovr over the build dir.")
|
||||||
|
endif()
|
||||||
|
|
@ -40,7 +40,9 @@ endif()
|
||||||
if(TEST)
|
if(TEST)
|
||||||
# Union of Qt modules required across all test targets (independent of application targets).
|
# Union of Qt modules required across all test targets (independent of application targets).
|
||||||
# When adding a new test that needs additional Qt modules, add them here rather than in the test's CMakeLists.txt.
|
# When adding a new test that needs additional Qt modules, add them here rather than in the test's CMakeLists.txt.
|
||||||
set(_TEST_NEEDED Concurrent Network Svg Widgets)
|
# Test is QtTest: it brings QAbstractItemModelTester and QSignalSpy, which are how the Qt model
|
||||||
|
# and signal contracts get verified without standing up a GUI.
|
||||||
|
set(_TEST_NEEDED Concurrent Network Svg Widgets Test)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
set(REQUIRED_QT_COMPONENTS ${REQUIRED_QT_COMPONENTS} ${_SERVATRICE_NEEDED} ${_COCKATRICE_NEEDED} ${_ORACLE_NEEDED}
|
set(REQUIRED_QT_COMPONENTS ${REQUIRED_QT_COMPONENTS} ${_SERVATRICE_NEEDED} ${_COCKATRICE_NEEDED} ${_ORACLE_NEEDED}
|
||||||
|
|
|
||||||
|
|
@ -304,10 +304,11 @@ SetShellVarContext all
|
||||||
; _?=$INSTDIR keeps the old uninstaller running in place. An NSIS uninstaller
|
; _?=$INSTDIR keeps the old uninstaller running in place. An NSIS uninstaller
|
||||||
; normally copies itself to %TEMP% and runs from there so that it can delete its
|
; normally copies itself to %TEMP% and runs from there so that it can delete its
|
||||||
; own directory, and the process we started exits at once. ExecWait then returns
|
; own directory, and the process we started exits at once. ExecWait then returns
|
||||||
; before the uninstall has run, and the temp copy's "RMDir /r $INSTDIR" executes
|
; before the uninstall has run, and the temp copy keeps deleting files - from
|
||||||
; while the section below is still writing the new payload. _?= disables the copy
|
; 3.1.0 and 3.1.1 on disk a blanket "RMDir /r $INSTDIR" - while the section
|
||||||
; and makes ExecWait block until the uninstall finishes. It must be the last
|
; below is still writing the new payload. _?= disables the copy and makes
|
||||||
; argument and must not be quoted, even when the path contains spaces.
|
; ExecWait block until the uninstall finishes. It must be the last argument
|
||||||
|
; and must not be quoted, even when the path contains spaces.
|
||||||
;
|
;
|
||||||
; --- 32-bit uninstall ---
|
; --- 32-bit uninstall ---
|
||||||
SetRegView 32
|
SetRegView 32
|
||||||
|
|
@ -1107,9 +1108,14 @@ ${AndIf} ${FileExists} "$INSTDIR\portable.dat"
|
||||||
; upgrade portable mode
|
; upgrade portable mode
|
||||||
!insertmacro CockLog "section: portable upgrade purge"
|
!insertmacro CockLog "section: portable upgrade purge"
|
||||||
!insertmacro CockLogListing "before-portable-purge" "*.dll"
|
!insertmacro CockLogListing "before-portable-purge" "*.dll"
|
||||||
|
; plugins and translations are payload and are rewritten by the payload
|
||||||
|
; below, so removing them here is safe. sounds and themes are payload
|
||||||
|
; too - the default packs are rewritten below as well - but a portable
|
||||||
|
; user's custom sounds and themes live in exactly those folders, so a
|
||||||
|
; recursive purge here would delete user content along with the defaults.
|
||||||
|
; Stale default files from an older release are overwritten or replaced
|
||||||
|
; by the payload, never by a purge.
|
||||||
RMDir /r "$INSTDIR\plugins"
|
RMDir /r "$INSTDIR\plugins"
|
||||||
RMDir /r "$INSTDIR\sounds"
|
|
||||||
RMDir /r "$INSTDIR\themes"
|
|
||||||
RMDir /r "$INSTDIR\translations"
|
RMDir /r "$INSTDIR\translations"
|
||||||
Delete "$INSTDIR\uninstall.exe"
|
Delete "$INSTDIR\uninstall.exe"
|
||||||
Delete "$INSTDIR\cockatrice.exe"
|
Delete "$INSTDIR\cockatrice.exe"
|
||||||
|
|
@ -1263,16 +1269,32 @@ Section "un.Application" UnSecApplication
|
||||||
SetShellVarContext all
|
SetShellVarContext all
|
||||||
StrCpy $R1 0
|
StrCpy $R1 0
|
||||||
|
|
||||||
; Record whether this was a portable install before the directory (and the
|
; Record whether this was a portable install before portable.dat is
|
||||||
; portable.dat marker inside it) is removed below.
|
; removed below.
|
||||||
IfFileExists "$INSTDIR\portable.dat" 0 +2
|
IfFileExists "$INSTDIR\portable.dat" 0 +2
|
||||||
StrCpy $R1 1
|
StrCpy $R1 1
|
||||||
|
|
||||||
; Remove the entire application directory so any file that is not part of
|
; Remove only what the installer wrote. The two lists below are generated
|
||||||
; the installed payload (e.g. build-tree artifacts such as *.dir folders,
|
; by CPack from the staged install tree: one Delete per installed file and
|
||||||
; *_autogen and *.tlog files from a build) cannot survive between an
|
; one non-recursive RMDir per installed directory, children first.
|
||||||
; uninstall and a fresh reinstall.
|
; Anything the user put in $INSTDIR - decks, pictures, portable data
|
||||||
RMDir /r "$INSTDIR"
|
; folders - is not on those lists and survives, and $INSTDIR itself is
|
||||||
|
; only removed when nothing else is left in it. An earlier version of this
|
||||||
|
; section ran a blanket "RMDir /r $INSTDIR" to keep build-tree artifacts
|
||||||
|
; out of a fresh reinstall; that also deleted user decks whenever a user
|
||||||
|
; had pointed their data folders into the install directory, so payload
|
||||||
|
; purity may never be bought at that price again.
|
||||||
|
@CPACK_NSIS_DELETE_FILES@
|
||||||
|
@CPACK_NSIS_DELETE_DIRECTORIES@
|
||||||
|
|
||||||
|
; Written by the installer itself, so absent from the staged payload
|
||||||
|
; above; qdebug.txt is app-generated log output next to the executables.
|
||||||
|
Delete "$INSTDIR\uninstall.exe"
|
||||||
|
Delete "$INSTDIR\portable.dat"
|
||||||
|
Delete "$INSTDIR\qdebug.txt"
|
||||||
|
|
||||||
|
; Succeeds only when no foreign files remain.
|
||||||
|
RMDir "$INSTDIR"
|
||||||
|
|
||||||
RMDir /r "$SMPROGRAMS\Cockatrice"
|
RMDir /r "$SMPROGRAMS\Cockatrice"
|
||||||
|
|
||||||
|
|
|
||||||
109
cmake/Sanitizers.cmake
Normal file
109
cmake/Sanitizers.cmake
Normal file
|
|
@ -0,0 +1,109 @@
|
||||||
|
# AddressSanitizer + UndefinedBehaviorSanitizer opt-in, plus per-source opt-out
|
||||||
|
# Input: ENABLE_SANITIZERS
|
||||||
|
# Defines: cockatrice_disable_sanitizers_for
|
||||||
|
#
|
||||||
|
# Included once from the root CMakeLists.txt, immediately after the per-compiler flag
|
||||||
|
# block, so add_compile_options() reaches every library and executable defined below.
|
||||||
|
|
||||||
|
include(CheckCXXSourceCompiles)
|
||||||
|
|
||||||
|
# Opt a set of vendored sources out of instrumentation.
|
||||||
|
#
|
||||||
|
# cockatrice_disable_sanitizers_for(<source> ...)
|
||||||
|
#
|
||||||
|
# Sanitizer findings inside vendored code are not actionable for us, and letting them
|
||||||
|
# through buries our own bugs in noise until everyone learns to skip the job.
|
||||||
|
#
|
||||||
|
# The sources must be named relative to the directory that compiles them. CMake resolves a
|
||||||
|
# source-file property against the directory scope the target was declared in, so calling
|
||||||
|
# this from the root with absolute paths silently does nothing. That is why the call sites
|
||||||
|
# live next to the add_library/add_executable that pull the vendored code in.
|
||||||
|
#
|
||||||
|
# SKIP_PRECOMPILE_HEADERS is not optional. The precompiled header is built with the
|
||||||
|
# sanitizer flags, so it defines __SANITIZE_ADDRESS__; a translation unit compiled with
|
||||||
|
# -fno-sanitize=all does not, and GCC then refuses the PCH with
|
||||||
|
# "not used because __SANITIZE_ADDRESS__ not defined", which is a fatal -Werror=invalid-pch
|
||||||
|
# in the servatrice and oracle builds.
|
||||||
|
function(cockatrice_disable_sanitizers_for)
|
||||||
|
if(NOT ENABLE_SANITIZERS)
|
||||||
|
return()
|
||||||
|
endif()
|
||||||
|
set_source_files_properties(${ARGN} PROPERTIES COMPILE_OPTIONS "-fno-sanitize=all" SKIP_PRECOMPILE_HEADERS ON)
|
||||||
|
endfunction()
|
||||||
|
|
||||||
|
if(ENABLE_SANITIZERS)
|
||||||
|
# Probe a flag the way it will actually be used: on the compile step AND the link step.
|
||||||
|
# check_cxx_compiler_flag() only probes the compile step, which makes every sanitizer
|
||||||
|
# flag look unsupported because linking an ASan object without -fsanitize=address leaves
|
||||||
|
# __asan_init undefined. That is a silent misconfiguration rather than an error, so the
|
||||||
|
# probe has to link.
|
||||||
|
function(cockatrice_probe_flag FLAG VARNAME)
|
||||||
|
set(CMAKE_REQUIRED_FLAGS "${FLAG}")
|
||||||
|
set(CMAKE_REQUIRED_LINK_OPTIONS "${FLAG}")
|
||||||
|
check_cxx_source_compiles("int main() { return 0; }" ${VARNAME})
|
||||||
|
unset(CMAKE_REQUIRED_FLAGS)
|
||||||
|
unset(CMAKE_REQUIRED_LINK_OPTIONS)
|
||||||
|
endfunction()
|
||||||
|
|
||||||
|
set(SANITIZER_FLAGS
|
||||||
|
-fsanitize=address,undefined
|
||||||
|
-fno-omit-frame-pointer
|
||||||
|
# UBSan defaults to recover mode: it prints "runtime error: ..." but the
|
||||||
|
# process exits 0, so gtest/ctest report a pass and CI stays green.
|
||||||
|
# -fno-sanitize-recover makes the first finding abort instead. This is
|
||||||
|
# probed like the other cosmetic flags, but CI also sets
|
||||||
|
# UBSAN_OPTIONS=halt_on_error=1 as the runtime-side guarantee.
|
||||||
|
-fno-sanitize-recover=undefined
|
||||||
|
# Qt builds QObject subclasses into raw storage and writes the vptr afterwards, which
|
||||||
|
# the vptr check reports on essentially every QObject instantiation. The check has no
|
||||||
|
# value for us because nothing outside moc-generated code inherits QObject, so it is
|
||||||
|
# off outright rather than suppressed piecemeal.
|
||||||
|
-fno-sanitize=vptr
|
||||||
|
)
|
||||||
|
|
||||||
|
# MSVC has no ASan/UBSan equivalent, and clang-cl would need /fsanitize=address plus a
|
||||||
|
# matching runtime link, which is a separate piece of work.
|
||||||
|
if(MSVC)
|
||||||
|
message(FATAL_ERROR "ENABLE_SANITIZERS is not supported with MSVC")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# Probe rather than assume, so an older toolchain gets the sanitizers it can support
|
||||||
|
# instead of failing the build on a flag it does not recognize.
|
||||||
|
set(SANITIZER_SUPPORTED_FLAGS)
|
||||||
|
foreach(FLAG ${SANITIZER_FLAGS})
|
||||||
|
string(MAKE_C_IDENTIFIER "COCKATRICE_HAS${FLAG}" _FLAG_VAR)
|
||||||
|
cockatrice_probe_flag("${FLAG}" ${_FLAG_VAR})
|
||||||
|
if(${_FLAG_VAR})
|
||||||
|
list(APPEND SANITIZER_SUPPORTED_FLAGS ${FLAG})
|
||||||
|
else()
|
||||||
|
message(STATUS "Sanitizers: ${FLAG} unsupported by ${CMAKE_CXX_COMPILER_ID}, skipping")
|
||||||
|
endif()
|
||||||
|
endforeach()
|
||||||
|
|
||||||
|
# The whole point of the option is the sanitizers, so an unsupported pair is fatal even
|
||||||
|
# though the cosmetic flags above are allowed to degrade.
|
||||||
|
if(NOT "-fsanitize=address,undefined" IN_LIST SANITIZER_SUPPORTED_FLAGS)
|
||||||
|
message(FATAL_ERROR "ENABLE_SANITIZERS requires -fsanitize=address,undefined, which "
|
||||||
|
"${CMAKE_CXX_COMPILER_ID} does not accept"
|
||||||
|
)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
add_compile_options(${SANITIZER_SUPPORTED_FLAGS})
|
||||||
|
add_link_options(-fsanitize=address,undefined)
|
||||||
|
|
||||||
|
# Recorded so the test tree can label its entries and a plain `make` at the same time
|
||||||
|
# cannot quietly disagree with what CI ran.
|
||||||
|
set(COCKATRICE_SANITIZERS
|
||||||
|
ON
|
||||||
|
CACHE INTERNAL "AddressSanitizer and UBSan are enabled"
|
||||||
|
)
|
||||||
|
else()
|
||||||
|
# ENABLE_SANITIZERS is OFF, but COCKATRICE_SANITIZERS is CACHE INTERNAL, so a
|
||||||
|
# reused build dir (e.g. CI's release-package step reconfiguring the same dir
|
||||||
|
# after the sanitized debug step) would otherwise keep skipping NO_SANITIZE
|
||||||
|
# tests and keep the 300s discovery timeout. Reset it explicitly.
|
||||||
|
set(COCKATRICE_SANITIZERS
|
||||||
|
OFF
|
||||||
|
CACHE INTERNAL "AddressSanitizer and UBSan are enabled" FORCE
|
||||||
|
)
|
||||||
|
endif()
|
||||||
|
|
@ -3,22 +3,53 @@
|
||||||
*
|
*
|
||||||
* Safe for every target that links Qt Core, including the headless
|
* Safe for every target that links Qt Core, including the headless
|
||||||
* Servatrice binary. Keep this header free of any widget/gui types.
|
* Servatrice binary. Keep this header free of any widget/gui types.
|
||||||
|
*
|
||||||
|
* Candidates are chosen from IWYU include statistics: a header earns a
|
||||||
|
* spot when it is not already reachable from the rest of this list and
|
||||||
|
* enough translation units re-parse it. Qt headers are stable across
|
||||||
|
* builds, so the precompiled header rarely invalidates.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include <QBasicTimer>
|
#include <QBasicTimer>
|
||||||
#include <QByteArray>
|
#include <QByteArray>
|
||||||
|
#include <QCommandLineParser>
|
||||||
|
#include <QCoreApplication>
|
||||||
|
#include <QCryptographicHash>
|
||||||
#include <QDateTime>
|
#include <QDateTime>
|
||||||
#include <QDebug>
|
#include <QDebug>
|
||||||
|
#include <QDir>
|
||||||
|
#include <QElapsedTimer>
|
||||||
#include <QFile>
|
#include <QFile>
|
||||||
|
#include <QFileInfo>
|
||||||
#include <QHash>
|
#include <QHash>
|
||||||
|
#include <QItemSelectionModel>
|
||||||
|
#include <QJsonArray>
|
||||||
|
#include <QJsonDocument>
|
||||||
|
#include <QJsonObject>
|
||||||
|
#include <QJsonValue>
|
||||||
#include <QList>
|
#include <QList>
|
||||||
#include <QLoggingCategory>
|
#include <QLoggingCategory>
|
||||||
#include <QMap>
|
#include <QMap>
|
||||||
|
#include <QMetaEnum>
|
||||||
#include <QMetaObject>
|
#include <QMetaObject>
|
||||||
|
#include <QMutex>
|
||||||
|
#include <QMutexLocker>
|
||||||
#include <QObject>
|
#include <QObject>
|
||||||
|
#include <QPointer>
|
||||||
|
#include <QPropertyAnimation>
|
||||||
#include <QRandomGenerator>
|
#include <QRandomGenerator>
|
||||||
|
#include <QReadWriteLock>
|
||||||
|
#include <QRegularExpression>
|
||||||
|
#include <QSet>
|
||||||
|
#include <QSettings>
|
||||||
#include <QSharedPointer>
|
#include <QSharedPointer>
|
||||||
|
#include <QSortFilterProxyModel>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
#include <QStringList>
|
#include <QStringList>
|
||||||
|
#include <QTemporaryDir>
|
||||||
|
#include <QThread>
|
||||||
#include <QTimer>
|
#include <QTimer>
|
||||||
|
#include <QUuid>
|
||||||
#include <QVariant>
|
#include <QVariant>
|
||||||
|
#include <QXmlStreamReader>
|
||||||
|
#include <QXmlStreamWriter>
|
||||||
|
|
|
||||||
|
|
@ -4,27 +4,56 @@
|
||||||
* Includes the Qt Core precompiled header plus the heavy Gui, Widgets and
|
* Includes the Qt Core precompiled header plus the heavy Gui, Widgets and
|
||||||
* Network layers that virtually every client translation unit re-parses.
|
* Network layers that virtually every client translation unit re-parses.
|
||||||
* Do not use on Servatrice (headless, QT_DONT_USE_QTGUI).
|
* Do not use on Servatrice (headless, QT_DONT_USE_QTGUI).
|
||||||
|
*
|
||||||
|
* Candidates are chosen from IWYU include statistics: a header earns a
|
||||||
|
* spot when it is not already reachable from the rest of this list and
|
||||||
|
* enough translation units re-parse it. Qt headers are stable across
|
||||||
|
* builds, so the precompiled header rarely invalidates.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include "qtcore_pch.h"
|
#include "qtcore_pch.h"
|
||||||
|
|
||||||
#include <QAction>
|
#include <QAction>
|
||||||
#include <QApplication>
|
#include <QApplication>
|
||||||
|
#include <QCheckBox>
|
||||||
|
#include <QComboBox>
|
||||||
|
#include <QCompleter>
|
||||||
|
#include <QDialogButtonBox>
|
||||||
|
#include <QDockWidget>
|
||||||
|
#include <QFileDialog>
|
||||||
|
#include <QFormLayout>
|
||||||
#include <QFrame>
|
#include <QFrame>
|
||||||
#include <QGraphicsItem>
|
#include <QGraphicsItem>
|
||||||
#include <QGraphicsScene>
|
#include <QGraphicsScene>
|
||||||
|
#include <QGraphicsSceneMouseEvent>
|
||||||
#include <QGraphicsView>
|
#include <QGraphicsView>
|
||||||
|
#include <QGridLayout>
|
||||||
|
#include <QGroupBox>
|
||||||
|
#include <QHBoxLayout>
|
||||||
|
#include <QHeaderView>
|
||||||
#include <QImage>
|
#include <QImage>
|
||||||
|
#include <QImageReader>
|
||||||
|
#include <QInputDialog>
|
||||||
#include <QLabel>
|
#include <QLabel>
|
||||||
#include <QLayout>
|
#include <QLayout>
|
||||||
|
#include <QListWidget>
|
||||||
#include <QMainWindow>
|
#include <QMainWindow>
|
||||||
#include <QMenu>
|
#include <QMenu>
|
||||||
|
#include <QMessageBox>
|
||||||
#include <QNetworkAccessManager>
|
#include <QNetworkAccessManager>
|
||||||
#include <QNetworkReply>
|
#include <QNetworkReply>
|
||||||
#include <QPainter>
|
#include <QPainter>
|
||||||
|
#include <QPlainTextEdit>
|
||||||
#include <QPushButton>
|
#include <QPushButton>
|
||||||
#include <QScrollArea>
|
#include <QScrollArea>
|
||||||
|
#include <QSpinBox>
|
||||||
|
#include <QSplitter>
|
||||||
|
#include <QStandardItemModel>
|
||||||
#include <QTabWidget>
|
#include <QTabWidget>
|
||||||
|
#include <QTableWidget>
|
||||||
|
#include <QTableWidgetItem>
|
||||||
#include <QToolBar>
|
#include <QToolBar>
|
||||||
|
#include <QToolButton>
|
||||||
#include <QTreeWidget>
|
#include <QTreeWidget>
|
||||||
|
#include <QVBoxLayout>
|
||||||
#include <QWidget>
|
#include <QWidget>
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,8 @@ set(cockatrice_SOURCES
|
||||||
src/interface/widgets/dialogs/dlg_update.cpp
|
src/interface/widgets/dialogs/dlg_update.cpp
|
||||||
src/interface/widgets/dialogs/dlg_view_log.cpp
|
src/interface/widgets/dialogs/dlg_view_log.cpp
|
||||||
src/interface/widgets/dialogs/override_printing_warning.cpp
|
src/interface/widgets/dialogs/override_printing_warning.cpp
|
||||||
|
src/interface/widgets/dialogs/path_safety_warning.cpp
|
||||||
|
src/interface/widgets/dialogs/settings_view.cpp
|
||||||
src/interface/widgets/dialogs/tip_of_the_day.cpp
|
src/interface/widgets/dialogs/tip_of_the_day.cpp
|
||||||
src/interface/widgets/deck_share/deck_share_utils.cpp
|
src/interface/widgets/deck_share/deck_share_utils.cpp
|
||||||
src/interface/widgets/deck_share/shared_deck_preview_widget.cpp
|
src/interface/widgets/deck_share/shared_deck_preview_widget.cpp
|
||||||
|
|
|
||||||
|
|
@ -277,8 +277,6 @@ void GameEventHandler::eventSpectatorLeave(const Event_Leave &event,
|
||||||
emit spectatorLeft(eventPlayerId);
|
emit spectatorLeft(eventPlayerId);
|
||||||
|
|
||||||
game->getPlayerManager()->removeSpectator(eventPlayerId);
|
game->getPlayerManager()->removeSpectator(eventPlayerId);
|
||||||
|
|
||||||
emitUserEvent();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void GameEventHandler::eventGameStateChanged(const Event_GameStateChanged &event,
|
void GameEventHandler::eventGameStateChanged(const Event_GameStateChanged &event,
|
||||||
|
|
@ -465,9 +463,9 @@ void GameEventHandler::eventJoin(const Event_Join &event, int /*eventPlayerId*/,
|
||||||
PlayerLogic *newPlayer = game->getPlayerManager()->addPlayer(playerId, playerInfo.user_info());
|
PlayerLogic *newPlayer = game->getPlayerManager()->addPlayer(playerId, playerInfo.user_info());
|
||||||
emit logJoinPlayer(newPlayer);
|
emit logJoinPlayer(newPlayer);
|
||||||
emit playerJoined(playerInfo);
|
emit playerJoined(playerInfo);
|
||||||
}
|
|
||||||
|
|
||||||
emitUserEvent();
|
emitUserEvent();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
QString GameEventHandler::getLeaveReason(Event_Leave::LeaveReason reason)
|
QString GameEventHandler::getLeaveReason(Event_Leave::LeaveReason reason)
|
||||||
|
|
|
||||||
|
|
@ -1,205 +1,41 @@
|
||||||
/**
|
/**
|
||||||
* @file dlg_settings.cpp
|
* @file dlg_settings.cpp
|
||||||
* @brief Implementation of the main settings dialog
|
* @brief Implementation of the main settings dialog (thin frame around SettingsView)
|
||||||
* @ingroup Dialogs
|
* @ingroup Dialogs
|
||||||
*/
|
*/
|
||||||
#include "dlg_settings.h"
|
#include "dlg_settings.h"
|
||||||
|
|
||||||
#include "../../../client/settings/cache_settings.h"
|
#include "../../../client/settings/cache_settings.h"
|
||||||
#include "../../pixel_map_generator.h"
|
|
||||||
#include "../main.h"
|
#include "../main.h"
|
||||||
#include "../settings_page/abstract_settings_page.h"
|
#include "../settings_page/abstract_settings_page.h"
|
||||||
#include "../settings_page/appearance_settings_page.h"
|
|
||||||
#include "../settings_page/deck_editor_settings_page.h"
|
|
||||||
#include "../settings_page/general_settings_page.h"
|
|
||||||
#include "../settings_page/messages_settings_page.h"
|
|
||||||
#include "../settings_page/settings_search_delegate.h"
|
|
||||||
#include "../settings_page/settings_search_model.h"
|
|
||||||
#include "../settings_page/shortcut_settings_page.h"
|
|
||||||
#include "../settings_page/sound_settings_page.h"
|
|
||||||
#include "../settings_page/storage_settings_page.h"
|
|
||||||
#include "../settings_page/user_interface_settings_page.h"
|
|
||||||
#include "libcockatrice/card/database/card_database.h"
|
#include "libcockatrice/card/database/card_database.h"
|
||||||
#include "libcockatrice/card/database/card_database_loader.h"
|
#include "libcockatrice/card/database/card_database_loader.h"
|
||||||
#include "libcockatrice/card/database/card_database_manager.h"
|
#include "libcockatrice/card/database/card_database_manager.h"
|
||||||
|
|
||||||
#include <QAbstractAnimation>
|
|
||||||
#include <QAbstractItemView>
|
|
||||||
#include <QApplication>
|
#include <QApplication>
|
||||||
#include <QBrush>
|
|
||||||
#include <QByteArray>
|
|
||||||
#include <QCloseEvent>
|
#include <QCloseEvent>
|
||||||
#include <QColor>
|
|
||||||
#include <QDebug>
|
|
||||||
#include <QDir>
|
#include <QDir>
|
||||||
#include <QEasingCurve>
|
|
||||||
#include <QEvent>
|
|
||||||
#include <QFlags>
|
|
||||||
#include <QFrame>
|
|
||||||
#include <QGraphicsOpacityEffect>
|
|
||||||
#include <QGuiApplication>
|
#include <QGuiApplication>
|
||||||
#include <QHBoxLayout>
|
#include <QHBoxLayout>
|
||||||
#include <QIcon>
|
|
||||||
#include <QKeyEvent>
|
|
||||||
#include <QKeySequence>
|
|
||||||
#include <QLineEdit>
|
|
||||||
#include <QListView>
|
|
||||||
#include <QMessageBox>
|
#include <QMessageBox>
|
||||||
#include <QMessageLogger>
|
#include <QMessageLogger>
|
||||||
#include <QModelIndex>
|
|
||||||
#include <QObject>
|
|
||||||
#include <QPalette>
|
|
||||||
#include <QPixmap>
|
|
||||||
#include <QPropertyAnimation>
|
|
||||||
#include <QPushButton>
|
#include <QPushButton>
|
||||||
#include <QRect>
|
|
||||||
#include <QScreen>
|
#include <QScreen>
|
||||||
#include <QScrollArea>
|
|
||||||
#include <QScrollBar>
|
|
||||||
#include <QSequentialAnimationGroup>
|
|
||||||
#include <QShortcut>
|
|
||||||
#include <QSize>
|
#include <QSize>
|
||||||
#include <QSizePolicy>
|
|
||||||
#include <QStackedLayout>
|
|
||||||
#include <QStackedWidget>
|
|
||||||
#include <QStringLiteral>
|
|
||||||
#include <QToolButton>
|
|
||||||
#include <QVBoxLayout>
|
#include <QVBoxLayout>
|
||||||
#include <QWidget>
|
#include <QWidget>
|
||||||
#include <QtGlobal>
|
#include <QtGlobal>
|
||||||
#include <libcockatrice/settings/paths_settings.h>
|
#include <libcockatrice/settings/paths_settings.h>
|
||||||
#include <libcockatrice/settings/personal_settings.h>
|
#include <libcockatrice/settings/personal_settings.h>
|
||||||
#include <qnamespace.h>
|
|
||||||
|
|
||||||
class QModelIndex;
|
DlgSettings::DlgSettings(QWidget *parent) : QDialog(parent)
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Wraps a widget in a scroll area for long settings pages
|
|
||||||
* @param widget The widget to wrap
|
|
||||||
* @return The scroll area containing the widget
|
|
||||||
*/
|
|
||||||
static QScrollArea *makeScrollable(QWidget *widget)
|
|
||||||
{
|
|
||||||
widget->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Maximum);
|
|
||||||
|
|
||||||
auto *scrollArea = new QScrollArea;
|
|
||||||
scrollArea->setWidgetResizable(true);
|
|
||||||
scrollArea->setContentsMargins(0, 0, 0, 0);
|
|
||||||
scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
|
||||||
scrollArea->horizontalScrollBar()->setEnabled(false);
|
|
||||||
scrollArea->setWidget(widget);
|
|
||||||
return scrollArea;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Returns the theme icon resources for each settings page, indexed by SettingsPage order
|
|
||||||
*/
|
|
||||||
static QStringList pageIconResources()
|
|
||||||
{
|
|
||||||
return {QStringLiteral("theme:config/general"), QStringLiteral("theme:config/appearance"),
|
|
||||||
QStringLiteral("theme:config/interface"), QStringLiteral("theme:config/deckeditor"),
|
|
||||||
QStringLiteral("theme:config/storage"), QStringLiteral("theme:config/messages"),
|
|
||||||
QStringLiteral("theme:config/sound"), QStringLiteral("theme:config/shorcuts")};
|
|
||||||
}
|
|
||||||
|
|
||||||
DlgSettings::DlgSettings(QWidget *parent) : QDialog(parent), currentTabIndex(0), searchActive(false)
|
|
||||||
{
|
{
|
||||||
auto rec = QGuiApplication::primaryScreen()->availableGeometry();
|
auto rec = QGuiApplication::primaryScreen()->availableGeometry();
|
||||||
setMinimumSize(qMin(750, rec.width()), qMin(700, rec.height()));
|
setMinimumSize(qMin(750, rec.width()), qMin(700, rec.height()));
|
||||||
|
|
||||||
connect(&SettingsCache::instance().personal(), &PersonalSettings::langChanged, this, &DlgSettings::updateLanguage);
|
connect(&SettingsCache::instance().personal(), &PersonalSettings::langChanged, this, &DlgSettings::updateLanguage);
|
||||||
|
|
||||||
setupUi();
|
settingsView = new SettingsView(this);
|
||||||
|
|
||||||
connect(&SettingsCache::instance().personal(), &PersonalSettings::langChanged, this, &DlgSettings::retranslateUi);
|
|
||||||
retranslateUi();
|
|
||||||
|
|
||||||
searchEdit->setFocus();
|
|
||||||
|
|
||||||
adjustSize();
|
|
||||||
}
|
|
||||||
|
|
||||||
void DlgSettings::setupUi()
|
|
||||||
{
|
|
||||||
// Search bar
|
|
||||||
searchEdit = new QLineEdit;
|
|
||||||
searchEdit->setClearButtonEnabled(true);
|
|
||||||
searchEdit->addAction(themePixmap(QStringLiteral("icons/search")), QLineEdit::LeadingPosition);
|
|
||||||
searchEdit->installEventFilter(this);
|
|
||||||
connect(searchEdit, &QLineEdit::textChanged, this, &DlgSettings::onSearchTextChanged);
|
|
||||||
|
|
||||||
auto *searchLayout = new QHBoxLayout;
|
|
||||||
searchLayout->addWidget(searchEdit);
|
|
||||||
|
|
||||||
// Tab bar (built in setupTabBar)
|
|
||||||
setupTabBar();
|
|
||||||
|
|
||||||
// Pages stacked widget
|
|
||||||
pagesWidget = new QStackedWidget;
|
|
||||||
|
|
||||||
auto *generalPage = new GeneralSettingsPage;
|
|
||||||
auto *appearancePage = new AppearanceSettingsPage;
|
|
||||||
auto *userInterfacePage = new UserInterfaceSettingsPage;
|
|
||||||
auto *deckEditorPage = new DeckEditorSettingsPage;
|
|
||||||
auto *storagePage = new StorageSettingsPage;
|
|
||||||
auto *messagesPage = new MessagesSettingsPage;
|
|
||||||
auto *soundPage = new SoundSettingsPage;
|
|
||||||
auto *shortcutsPage = new ShortcutSettingsPage;
|
|
||||||
|
|
||||||
pages.append(generalPage);
|
|
||||||
pages.append(appearancePage);
|
|
||||||
pages.append(userInterfacePage);
|
|
||||||
pages.append(deckEditorPage);
|
|
||||||
pages.append(storagePage);
|
|
||||||
pages.append(messagesPage);
|
|
||||||
pages.append(soundPage);
|
|
||||||
pages.append(shortcutsPage);
|
|
||||||
|
|
||||||
pagesWidget->addWidget(makeScrollable(generalPage));
|
|
||||||
pagesWidget->addWidget(makeScrollable(appearancePage));
|
|
||||||
pagesWidget->addWidget(makeScrollable(userInterfacePage));
|
|
||||||
pagesWidget->addWidget(makeScrollable(deckEditorPage));
|
|
||||||
pagesWidget->addWidget(makeScrollable(storagePage));
|
|
||||||
pagesWidget->addWidget(makeScrollable(messagesPage));
|
|
||||||
pagesWidget->addWidget(soundPage);
|
|
||||||
pagesWidget->addWidget(shortcutsPage);
|
|
||||||
|
|
||||||
Q_ASSERT(pages.size() == NumPages);
|
|
||||||
|
|
||||||
// Search results view (hidden by default)
|
|
||||||
searchResultsView = new QListView;
|
|
||||||
searchResultsView->setUniformItemSizes(false);
|
|
||||||
searchResultsView->setSelectionMode(QAbstractItemView::SingleSelection);
|
|
||||||
searchResultsView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
|
||||||
searchResultsView->setVisible(false);
|
|
||||||
searchResultsView->setStyleSheet(
|
|
||||||
"QListView::item:selected { background: palette(highlight); color: palette(highlighted-text); }");
|
|
||||||
|
|
||||||
searchModel = new SettingsSearchModel(this);
|
|
||||||
searchDelegate = new SettingsSearchDelegate(this);
|
|
||||||
searchResultsView->setModel(searchModel);
|
|
||||||
searchResultsView->setItemDelegate(searchDelegate);
|
|
||||||
connect(searchResultsView, &QListView::clicked, this, &DlgSettings::onSearchResultClicked);
|
|
||||||
|
|
||||||
connect(&SettingsCache::instance(), &SettingsCache::themeChanged, this, [this] {
|
|
||||||
const QStringList icons = pageIconResources();
|
|
||||||
for (int i = 0; i < tabButtons.size() && i < icons.size(); ++i) {
|
|
||||||
tabButtons[i]->setIcon(QPixmap(icons[i]));
|
|
||||||
}
|
|
||||||
searchDelegate->setPageIcons(icons);
|
|
||||||
searchResultsView->viewport()->update();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Build search index after pages are created
|
|
||||||
buildSearchIndex();
|
|
||||||
|
|
||||||
// Pages container (stacked widget + search results overlay)
|
|
||||||
pagesContainer = new QWidget;
|
|
||||||
auto *containerLayout = new QStackedLayout;
|
|
||||||
containerLayout->setStackingMode(QStackedLayout::StackAll);
|
|
||||||
containerLayout->addWidget(pagesWidget);
|
|
||||||
containerLayout->addWidget(searchResultsView);
|
|
||||||
pagesContainer->setLayout(containerLayout);
|
|
||||||
|
|
||||||
// Bottom buttons
|
// Bottom buttons
|
||||||
auto *buttonBox = new QHBoxLayout;
|
auto *buttonBox = new QHBoxLayout;
|
||||||
|
|
@ -211,241 +47,25 @@ void DlgSettings::setupUi()
|
||||||
|
|
||||||
// Main layout
|
// Main layout
|
||||||
auto *mainLayout = new QVBoxLayout;
|
auto *mainLayout = new QVBoxLayout;
|
||||||
mainLayout->addLayout(searchLayout);
|
mainLayout->addWidget(settingsView);
|
||||||
mainLayout->addWidget(tabBarWidget);
|
|
||||||
auto *separator = new QFrame;
|
|
||||||
separator->setFrameShape(QFrame::HLine);
|
|
||||||
separator->setFrameShadow(QFrame::Sunken);
|
|
||||||
mainLayout->addWidget(separator);
|
|
||||||
mainLayout->addWidget(pagesContainer);
|
|
||||||
mainLayout->addSpacing(4);
|
mainLayout->addSpacing(4);
|
||||||
mainLayout->addLayout(buttonBox);
|
mainLayout->addLayout(buttonBox);
|
||||||
setLayout(mainLayout);
|
setLayout(mainLayout);
|
||||||
|
|
||||||
// Keyboard shortcuts
|
connect(&SettingsCache::instance().personal(), &PersonalSettings::langChanged, this, &DlgSettings::retranslateUi);
|
||||||
auto *searchShortcut = new QShortcut(QKeySequence(Qt::CTRL | Qt::Key_F), this);
|
retranslateUi();
|
||||||
connect(searchShortcut, &QShortcut::activated, searchEdit, qOverload<>(&QLineEdit::setFocus));
|
|
||||||
|
|
||||||
auto *nextTabShortcut = new QShortcut(QKeySequence(Qt::CTRL | Qt::Key_Tab), this);
|
adjustSize();
|
||||||
connect(nextTabShortcut, &QShortcut::activated, this, [this] {
|
|
||||||
int next = (currentTabIndex + 1) % tabButtons.size();
|
|
||||||
setActiveTab(next);
|
|
||||||
});
|
|
||||||
|
|
||||||
auto *prevTabShortcut = new QShortcut(QKeySequence(Qt::CTRL | Qt::SHIFT | Qt::Key_Tab), this);
|
|
||||||
connect(prevTabShortcut, &QShortcut::activated, this, [this] {
|
|
||||||
int prev = (currentTabIndex - 1 + tabButtons.size()) % tabButtons.size();
|
|
||||||
setActiveTab(prev);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Initialize to first tab
|
|
||||||
setActiveTab(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
void DlgSettings::setupTabBar()
|
|
||||||
{
|
|
||||||
tabBarWidget = new QWidget;
|
|
||||||
auto *tabLayout = new QHBoxLayout;
|
|
||||||
tabLayout->setContentsMargins(0, 0, 0, 0);
|
|
||||||
tabLayout->setSpacing(2);
|
|
||||||
|
|
||||||
const QStringList iconResources = pageIconResources();
|
|
||||||
|
|
||||||
for (int i = 0; i < iconResources.size(); ++i) {
|
|
||||||
auto *tabButton = new QToolButton;
|
|
||||||
tabButton->setCheckable(true);
|
|
||||||
tabButton->setIcon(QPixmap(iconResources[i]));
|
|
||||||
tabButton->setIconSize(QSize(48, 48));
|
|
||||||
tabButton->setToolButtonStyle(Qt::ToolButtonTextUnderIcon);
|
|
||||||
tabButton->setAutoExclusive(true);
|
|
||||||
tabButton->setMinimumHeight(85);
|
|
||||||
tabButton->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
|
|
||||||
|
|
||||||
connect(tabButton, &QToolButton::clicked, this, [this, idx = i] { onTabClicked(idx); });
|
|
||||||
|
|
||||||
tabButtons.append(tabButton);
|
|
||||||
tabLayout->addWidget(tabButton);
|
|
||||||
}
|
|
||||||
|
|
||||||
tabBarWidget->setLayout(tabLayout);
|
|
||||||
}
|
|
||||||
|
|
||||||
void DlgSettings::buildSearchIndex()
|
|
||||||
{
|
|
||||||
QList<SettingsSearchEntry> allEntries;
|
|
||||||
|
|
||||||
const QStringList pageNames = translatedPageNames();
|
|
||||||
searchDelegate->setPageNames(pageNames);
|
|
||||||
searchDelegate->setPageIcons(pageIconResources());
|
|
||||||
|
|
||||||
for (int i = 0; i < pages.size(); ++i) {
|
|
||||||
QList<SettingsSearchEntry> pageEntries = pages[i]->getSearchEntries();
|
|
||||||
for (auto &entry : pageEntries) {
|
|
||||||
if (entry.pageIndex == -1) {
|
|
||||||
entry.pageIndex = i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
allEntries.append(pageEntries);
|
|
||||||
}
|
|
||||||
|
|
||||||
searchModel->setSourceEntries(allEntries);
|
|
||||||
}
|
|
||||||
|
|
||||||
void DlgSettings::onTabClicked(int index)
|
|
||||||
{
|
|
||||||
if (searchActive) {
|
|
||||||
switchToTabMode();
|
|
||||||
}
|
|
||||||
setActiveTab(index);
|
|
||||||
}
|
|
||||||
|
|
||||||
void DlgSettings::setActiveTab(int index)
|
|
||||||
{
|
|
||||||
if (index < 0 || index >= tabButtons.size()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
currentTabIndex = index;
|
|
||||||
pagesWidget->setCurrentIndex(index);
|
|
||||||
|
|
||||||
for (int i = 0; i < tabButtons.size(); ++i) {
|
|
||||||
tabButtons[i]->setChecked(i == index);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Style active tab with a thick accent border + subtle background tint
|
|
||||||
for (int i = 0; i < tabButtons.size(); ++i) {
|
|
||||||
if (i == index) {
|
|
||||||
tabButtons[i]->setStyleSheet("QToolButton { border: none; border-bottom: 3px solid palette(highlight); "
|
|
||||||
"border-top-left-radius: 4px; border-top-right-radius: 4px; "
|
|
||||||
"background: palette(window); padding-bottom: 1px; }");
|
|
||||||
} else {
|
|
||||||
tabButtons[i]->setStyleSheet("QToolButton { border: none; border-bottom: 1px solid transparent; "
|
|
||||||
"border-top-left-radius: 4px; border-top-right-radius: 4px; "
|
|
||||||
"background: transparent; }");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void DlgSettings::flashWidget(QWidget *widget)
|
|
||||||
{
|
|
||||||
auto *overlay = new QWidget(widget);
|
|
||||||
overlay->setGeometry(widget->rect());
|
|
||||||
overlay->setAttribute(Qt::WA_TransparentForMouseEvents, true);
|
|
||||||
|
|
||||||
QPalette pal = overlay->palette();
|
|
||||||
QColor flashColor = pal.color(QPalette::Highlight);
|
|
||||||
flashColor.setAlpha(100);
|
|
||||||
pal.setBrush(QPalette::Window, flashColor);
|
|
||||||
overlay->setPalette(pal);
|
|
||||||
overlay->setAutoFillBackground(true);
|
|
||||||
|
|
||||||
auto *effect = new QGraphicsOpacityEffect(overlay);
|
|
||||||
effect->setOpacity(0.0);
|
|
||||||
overlay->setGraphicsEffect(effect);
|
|
||||||
overlay->show();
|
|
||||||
overlay->raise();
|
|
||||||
|
|
||||||
auto *flashIn = new QPropertyAnimation(effect, "opacity");
|
|
||||||
flashIn->setDuration(120);
|
|
||||||
flashIn->setStartValue(0.0);
|
|
||||||
flashIn->setEndValue(0.6);
|
|
||||||
flashIn->setEasingCurve(QEasingCurve::OutCubic);
|
|
||||||
|
|
||||||
auto *fadeOut = new QPropertyAnimation(effect, "opacity");
|
|
||||||
fadeOut->setDuration(900);
|
|
||||||
fadeOut->setStartValue(0.6);
|
|
||||||
fadeOut->setEndValue(0.0);
|
|
||||||
fadeOut->setEasingCurve(QEasingCurve::InCubic);
|
|
||||||
|
|
||||||
auto *group = new QSequentialAnimationGroup(overlay);
|
|
||||||
group->addAnimation(flashIn);
|
|
||||||
group->addAnimation(fadeOut);
|
|
||||||
|
|
||||||
connect(group, &QSequentialAnimationGroup::finished, overlay, &QWidget::deleteLater);
|
|
||||||
|
|
||||||
group->start(QAbstractAnimation::DeleteWhenStopped);
|
|
||||||
}
|
|
||||||
|
|
||||||
void DlgSettings::onSearchTextChanged(const QString &text)
|
|
||||||
{
|
|
||||||
searchModel->setFilterString(text);
|
|
||||||
|
|
||||||
if (searchModel->isFilterActive() && !text.trimmed().isEmpty()) {
|
|
||||||
if (!searchActive) {
|
|
||||||
switchToSearchMode();
|
|
||||||
}
|
|
||||||
if (searchModel->rowCount(QModelIndex()) > 0) {
|
|
||||||
searchResultsView->setCurrentIndex(searchModel->index(0));
|
|
||||||
}
|
|
||||||
} else if (searchActive) {
|
|
||||||
switchToTabMode();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void DlgSettings::switchToSearchMode()
|
|
||||||
{
|
|
||||||
searchActive = true;
|
|
||||||
tabBarWidget->setVisible(false);
|
|
||||||
pagesWidget->setVisible(false);
|
|
||||||
searchResultsView->setVisible(true);
|
|
||||||
if (searchModel->rowCount(QModelIndex()) > 0) {
|
|
||||||
searchResultsView->setCurrentIndex(searchModel->index(0));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void DlgSettings::switchToTabMode()
|
|
||||||
{
|
|
||||||
searchActive = false;
|
|
||||||
tabBarWidget->setVisible(true);
|
|
||||||
pagesWidget->setVisible(true);
|
|
||||||
searchResultsView->setVisible(false);
|
|
||||||
searchEdit->blockSignals(true);
|
|
||||||
searchEdit->clear();
|
|
||||||
searchEdit->blockSignals(false);
|
|
||||||
setActiveTab(currentTabIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
void DlgSettings::onSearchResultClicked(const QModelIndex &index)
|
|
||||||
{
|
|
||||||
navigateToSearchResult(index);
|
|
||||||
}
|
|
||||||
|
|
||||||
void DlgSettings::navigateToSearchResult(const QModelIndex &index)
|
|
||||||
{
|
|
||||||
SettingsSearchEntry entry = searchModel->entryForIndex(index);
|
|
||||||
if (entry.pageIndex < 0 || entry.pageIndex >= pages.size()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Switch to the page
|
|
||||||
switchToTabMode();
|
|
||||||
setActiveTab(entry.pageIndex);
|
|
||||||
|
|
||||||
// Scroll to the widget, focus it, and flash to highlight it
|
|
||||||
if (entry.widget) {
|
|
||||||
QWidget *widget = entry.widget;
|
|
||||||
while (widget) {
|
|
||||||
if (auto *scrollArea = qobject_cast<QScrollArea *>(widget)) {
|
|
||||||
scrollArea->ensureWidgetVisible(entry.widget);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
widget = widget->parentWidget();
|
|
||||||
}
|
|
||||||
entry.widget->setFocus();
|
|
||||||
flashWidget(entry.widget);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void DlgSettings::setTab(int index)
|
void DlgSettings::setTab(int index)
|
||||||
{
|
{
|
||||||
if (index >= 0 && index < tabButtons.size()) {
|
settingsView->setTab(index);
|
||||||
setActiveTab(index);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
AbstractSettingsPage *DlgSettings::page(SettingsPage which) const
|
AbstractSettingsPage *DlgSettings::page(SettingsPage which) const
|
||||||
{
|
{
|
||||||
return pages.value(static_cast<int>(which));
|
return settingsView->page(which);
|
||||||
}
|
}
|
||||||
|
|
||||||
void DlgSettings::updateLanguage()
|
void DlgSettings::updateLanguage()
|
||||||
|
|
@ -454,49 +74,6 @@ void DlgSettings::updateLanguage()
|
||||||
installNewTranslator();
|
installNewTranslator();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool DlgSettings::eventFilter(QObject *watched, QEvent *event)
|
|
||||||
{
|
|
||||||
if (watched == searchEdit && event->type() == QEvent::KeyPress) {
|
|
||||||
auto *keyEvent = static_cast<QKeyEvent *>(event);
|
|
||||||
if (keyEvent->key() == Qt::Key_Escape) {
|
|
||||||
if (searchActive) {
|
|
||||||
switchToTabMode();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
} else if (keyEvent->key() == Qt::Key_Return || keyEvent->key() == Qt::Key_Enter) {
|
|
||||||
if (searchActive) {
|
|
||||||
if (searchResultsView->currentIndex().isValid()) {
|
|
||||||
navigateToSearchResult(searchResultsView->currentIndex());
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
} else if (keyEvent->key() == Qt::Key_Down) {
|
|
||||||
if (searchActive) {
|
|
||||||
int nextRow = searchResultsView->currentIndex().row() + 1;
|
|
||||||
if (nextRow >= searchModel->rowCount()) {
|
|
||||||
nextRow = 0;
|
|
||||||
}
|
|
||||||
searchResultsView->setCurrentIndex(searchModel->index(nextRow));
|
|
||||||
searchResultsView->scrollTo(searchModel->index(nextRow));
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
} else if (keyEvent->key() == Qt::Key_Up) {
|
|
||||||
if (searchActive) {
|
|
||||||
int prevRow = searchResultsView->currentIndex().row() - 1;
|
|
||||||
if (prevRow < 0) {
|
|
||||||
prevRow = searchModel->rowCount() - 1;
|
|
||||||
}
|
|
||||||
if (prevRow >= 0) {
|
|
||||||
searchResultsView->setCurrentIndex(searchModel->index(prevRow));
|
|
||||||
searchResultsView->scrollTo(searchModel->index(prevRow));
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return QDialog::eventFilter(watched, event);
|
|
||||||
}
|
|
||||||
|
|
||||||
void DlgSettings::closeEvent(QCloseEvent *event)
|
void DlgSettings::closeEvent(QCloseEvent *event)
|
||||||
{
|
{
|
||||||
bool showLoadError = true;
|
bool showLoadError = true;
|
||||||
|
|
@ -576,26 +153,5 @@ void DlgSettings::closeEvent(QCloseEvent *event)
|
||||||
void DlgSettings::retranslateUi()
|
void DlgSettings::retranslateUi()
|
||||||
{
|
{
|
||||||
setWindowTitle(tr("Settings"));
|
setWindowTitle(tr("Settings"));
|
||||||
retranslateTabNames();
|
|
||||||
|
|
||||||
searchEdit->setPlaceholderText(tr("Search settings..."));
|
|
||||||
okButton->setText(tr("OK"));
|
okButton->setText(tr("OK"));
|
||||||
|
|
||||||
// Rebuild search index for translated text
|
|
||||||
buildSearchIndex();
|
|
||||||
}
|
|
||||||
|
|
||||||
QStringList DlgSettings::translatedPageNames()
|
|
||||||
{
|
|
||||||
return {tr("General"), tr("Appearance"), tr("User Interface"), tr("Card Sources"),
|
|
||||||
tr("Storage"), tr("Chat"), tr("Sound"), tr("Shortcuts")};
|
|
||||||
}
|
|
||||||
|
|
||||||
void DlgSettings::retranslateTabNames()
|
|
||||||
{
|
|
||||||
const QStringList tabLabels = translatedPageNames();
|
|
||||||
|
|
||||||
for (int i = 0; i < tabButtons.size() && i < tabLabels.size(); ++i) {
|
|
||||||
tabButtons[i]->setText(tabLabels[i]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,105 +1,59 @@
|
||||||
/**
|
/**
|
||||||
* @file dlg_settings.h
|
* @file dlg_settings.h
|
||||||
* @brief Main settings dialog for the Cockatrice client
|
* @brief Main settings dialog for the Cockatrice client (thin frame around SettingsView)
|
||||||
* @ingroup Dialogs
|
* @ingroup Dialogs
|
||||||
*/
|
*/
|
||||||
#ifndef DLG_SETTINGS_H
|
#ifndef DLG_SETTINGS_H
|
||||||
#define DLG_SETTINGS_H
|
#define DLG_SETTINGS_H
|
||||||
|
|
||||||
|
#include "settings_view.h"
|
||||||
|
|
||||||
#include <QDialog>
|
#include <QDialog>
|
||||||
#include <QList>
|
|
||||||
#include <QLoggingCategory>
|
#include <QLoggingCategory>
|
||||||
#include <QString>
|
|
||||||
#include <QStringList>
|
|
||||||
#include <qtmetamacros.h>
|
#include <qtmetamacros.h>
|
||||||
|
|
||||||
class QPushButton;
|
class QPushButton;
|
||||||
class QModelIndex;
|
class AbstractSettingsPage;
|
||||||
class QWidget;
|
|
||||||
|
|
||||||
inline Q_LOGGING_CATEGORY(DlgSettingsLog, "dlg_settings");
|
inline Q_LOGGING_CATEGORY(DlgSettingsLog, "dlg_settings");
|
||||||
|
|
||||||
class QStackedWidget;
|
|
||||||
class QToolButton;
|
|
||||||
class QListView;
|
|
||||||
class QLineEdit;
|
|
||||||
class AbstractSettingsPage;
|
|
||||||
class SettingsSearchModel;
|
|
||||||
class SettingsSearchDelegate;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Main application settings dialog with tabbed navigation and search
|
* @brief Main application settings dialog hosting the shared SettingsView
|
||||||
*
|
*
|
||||||
* Provides a modern settings interface organized into tabbed pages. Users can
|
* Thin QDialog frame around SettingsView: only the window title, the OK button
|
||||||
* either navigate by clicking tabs or search for specific settings using the
|
* and the close validation live here. All pages, navigation and search are
|
||||||
* built-in search bar. Search results are filtered and ranked by relevance.
|
* owned by SettingsView so other hosts (e.g. a settings tab) can reuse them.
|
||||||
*/
|
*/
|
||||||
class DlgSettings : public QDialog
|
class DlgSettings : public QDialog
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
public:
|
public:
|
||||||
/**
|
using SettingsPage = SettingsView::SettingsPage;
|
||||||
* @brief Page order in the tab bar, matching the order pages are added in setupUi()
|
static constexpr SettingsPage GeneralPage = SettingsView::GeneralPage;
|
||||||
*
|
static constexpr SettingsPage AppearancePage = SettingsView::AppearancePage;
|
||||||
* Use these values instead of raw indices so reordering pages never silently
|
static constexpr SettingsPage UserInterfacePage = SettingsView::UserInterfacePage;
|
||||||
* breaks external callers like tab_room.cpp.
|
static constexpr SettingsPage DeckEditorPage = SettingsView::DeckEditorPage;
|
||||||
*/
|
static constexpr SettingsPage StoragePage = SettingsView::StoragePage;
|
||||||
enum SettingsPage
|
static constexpr SettingsPage MessagesPage = SettingsView::MessagesPage;
|
||||||
{
|
static constexpr SettingsPage SoundPage = SettingsView::SoundPage;
|
||||||
GeneralPage = 0,
|
static constexpr SettingsPage ShortcutsPage = SettingsView::ShortcutsPage;
|
||||||
AppearancePage,
|
static constexpr SettingsPage NumPages = SettingsView::NumPages;
|
||||||
UserInterfacePage,
|
|
||||||
DeckEditorPage,
|
|
||||||
StoragePage,
|
|
||||||
MessagesPage,
|
|
||||||
SoundPage,
|
|
||||||
ShortcutsPage,
|
|
||||||
NumPages
|
|
||||||
};
|
|
||||||
|
|
||||||
explicit DlgSettings(QWidget *parent = nullptr);
|
explicit DlgSettings(QWidget *parent = nullptr);
|
||||||
void setTab(int index);
|
void setTab(int index);
|
||||||
AbstractSettingsPage *page(SettingsPage which) const;
|
AbstractSettingsPage *page(SettingsPage which) const;
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void onTabClicked(int index);
|
|
||||||
void onSearchTextChanged(const QString &text);
|
|
||||||
void onSearchResultClicked(const QModelIndex &index);
|
|
||||||
void updateLanguage();
|
void updateLanguage();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
// UI elements
|
SettingsView *settingsView; ///< Shared settings content
|
||||||
QLineEdit *searchEdit; ///< Search bar for filtering settings
|
QPushButton *okButton; ///< Button to close the dialog
|
||||||
QWidget *tabBarWidget; ///< Container widget for the tab buttons
|
|
||||||
QList<QToolButton *> tabButtons; ///< Navigation tab buttons
|
|
||||||
QStackedWidget *pagesWidget; ///< Stacked widget containing settings pages
|
|
||||||
QListView *searchResultsView; ///< Search results list view
|
|
||||||
QWidget *pagesContainer; ///< Container stacking pages and search results
|
|
||||||
QPushButton *okButton; ///< Button to close the dialog
|
|
||||||
|
|
||||||
// Data
|
|
||||||
QList<AbstractSettingsPage *> pages; ///< All settings page instances
|
|
||||||
SettingsSearchModel *searchModel; ///< Model for search results
|
|
||||||
SettingsSearchDelegate *searchDelegate; ///< Delegate for search result rendering
|
|
||||||
int currentTabIndex; ///< Currently active tab index
|
|
||||||
bool searchActive; ///< Whether search mode is active
|
|
||||||
|
|
||||||
void setupUi();
|
|
||||||
void setupTabBar();
|
|
||||||
void buildSearchIndex();
|
|
||||||
void switchToTabMode();
|
|
||||||
void switchToSearchMode();
|
|
||||||
void navigateToSearchResult(const QModelIndex &index);
|
|
||||||
void setActiveTab(int index);
|
|
||||||
static void flashWidget(QWidget *widget);
|
|
||||||
static QStringList translatedPageNames();
|
|
||||||
|
|
||||||
void retranslateUi();
|
void retranslateUi();
|
||||||
void retranslateTabNames();
|
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
void closeEvent(QCloseEvent *event) override;
|
void closeEvent(QCloseEvent *event) override;
|
||||||
bool eventFilter(QObject *watched, QEvent *event) override;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // DLG_SETTINGS_H
|
#endif // DLG_SETTINGS_H
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
#include "path_safety_warning.h"
|
||||||
|
|
||||||
|
#include "../../client/settings/cache_settings.h"
|
||||||
|
|
||||||
|
#include <QCoreApplication>
|
||||||
|
#include <QMessageBox>
|
||||||
|
#include <QObject>
|
||||||
|
#include <QString>
|
||||||
|
#include <libcockatrice/settings/paths_settings.h>
|
||||||
|
|
||||||
|
class QWidget;
|
||||||
|
|
||||||
|
bool PathSafetyWarning::confirmOutsideProgramDir(QWidget *parent, const QString &path)
|
||||||
|
{
|
||||||
|
if (SettingsCache::instance().getIsPortableBuild()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!PathsSettings::isInsideDir(path, QCoreApplication::applicationDirPath())) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
QMessageBox box(QMessageBox::Warning, QObject::tr("Path inside the program directory"),
|
||||||
|
QObject::tr("'%1' is inside the Cockatrice program directory.\n\n"
|
||||||
|
"Files stored there can be deleted by an update or by uninstalling "
|
||||||
|
"Cockatrice, so this change was not applied. Choose a location outside "
|
||||||
|
"the program directory.")
|
||||||
|
.arg(path),
|
||||||
|
QMessageBox::Ok, parent);
|
||||||
|
box.setTextFormat(Qt::PlainText);
|
||||||
|
box.exec();
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
#ifndef COCKATRICE_PATH_SAFETY_WARNING_H
|
||||||
|
#define COCKATRICE_PATH_SAFETY_WARNING_H
|
||||||
|
|
||||||
|
class QString;
|
||||||
|
class QWidget;
|
||||||
|
|
||||||
|
namespace PathSafetyWarning
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Warns when the user tries to configure a data path inside the program directory.
|
||||||
|
*
|
||||||
|
* Shows a message box explaining that files kept there can be deleted by an
|
||||||
|
* update or by uninstalling Cockatrice. Portable builds are always exempt:
|
||||||
|
* their data lives beside the executables by design.
|
||||||
|
*
|
||||||
|
* @param parent The parent widget
|
||||||
|
* @param path The path the user is trying to configure
|
||||||
|
* @return true when the path is outside the program directory and safe to apply
|
||||||
|
*/
|
||||||
|
bool confirmOutsideProgramDir(QWidget *parent, const QString &path);
|
||||||
|
|
||||||
|
} // namespace PathSafetyWarning
|
||||||
|
|
||||||
|
#endif // COCKATRICE_PATH_SAFETY_WARNING_H
|
||||||
497
cockatrice/src/interface/widgets/dialogs/settings_view.cpp
Normal file
497
cockatrice/src/interface/widgets/dialogs/settings_view.cpp
Normal file
|
|
@ -0,0 +1,497 @@
|
||||||
|
/**
|
||||||
|
* @file settings_view.cpp
|
||||||
|
* @brief Implementation of the settings content widget
|
||||||
|
* @ingroup Dialogs
|
||||||
|
*/
|
||||||
|
#include "settings_view.h"
|
||||||
|
|
||||||
|
#include "../../../client/settings/cache_settings.h"
|
||||||
|
#include "../../pixel_map_generator.h"
|
||||||
|
#include "../settings_page/abstract_settings_page.h"
|
||||||
|
#include "../settings_page/appearance_settings_page.h"
|
||||||
|
#include "../settings_page/deck_editor_settings_page.h"
|
||||||
|
#include "../settings_page/general_settings_page.h"
|
||||||
|
#include "../settings_page/messages_settings_page.h"
|
||||||
|
#include "../settings_page/settings_search_delegate.h"
|
||||||
|
#include "../settings_page/settings_search_model.h"
|
||||||
|
#include "../settings_page/shortcut_settings_page.h"
|
||||||
|
#include "../settings_page/sound_settings_page.h"
|
||||||
|
#include "../settings_page/storage_settings_page.h"
|
||||||
|
#include "../settings_page/user_interface_settings_page.h"
|
||||||
|
|
||||||
|
#include <QAbstractAnimation>
|
||||||
|
#include <QAbstractItemView>
|
||||||
|
#include <QBrush>
|
||||||
|
#include <QColor>
|
||||||
|
#include <QCoreApplication>
|
||||||
|
#include <QDebug>
|
||||||
|
#include <QEasingCurve>
|
||||||
|
#include <QEvent>
|
||||||
|
#include <QFlags>
|
||||||
|
#include <QFrame>
|
||||||
|
#include <QGraphicsOpacityEffect>
|
||||||
|
#include <QHBoxLayout>
|
||||||
|
#include <QIcon>
|
||||||
|
#include <QKeyEvent>
|
||||||
|
#include <QKeySequence>
|
||||||
|
#include <QLineEdit>
|
||||||
|
#include <QListView>
|
||||||
|
#include <QMessageLogger>
|
||||||
|
#include <QModelIndex>
|
||||||
|
#include <QObject>
|
||||||
|
#include <QPalette>
|
||||||
|
#include <QPixmap>
|
||||||
|
#include <QPropertyAnimation>
|
||||||
|
#include <QRect>
|
||||||
|
#include <QScreen>
|
||||||
|
#include <QScrollArea>
|
||||||
|
#include <QScrollBar>
|
||||||
|
#include <QSequentialAnimationGroup>
|
||||||
|
#include <QShortcut>
|
||||||
|
#include <QSize>
|
||||||
|
#include <QSizePolicy>
|
||||||
|
#include <QStackedLayout>
|
||||||
|
#include <QStackedWidget>
|
||||||
|
#include <QStringLiteral>
|
||||||
|
#include <QToolButton>
|
||||||
|
#include <QVBoxLayout>
|
||||||
|
#include <QWidget>
|
||||||
|
#include <QtGlobal>
|
||||||
|
#include <libcockatrice/settings/personal_settings.h>
|
||||||
|
#include <qnamespace.h>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Wraps a widget in a scroll area for long settings pages
|
||||||
|
* @param widget The widget to wrap
|
||||||
|
* @return The scroll area containing the widget
|
||||||
|
*/
|
||||||
|
static QScrollArea *makeScrollable(QWidget *widget)
|
||||||
|
{
|
||||||
|
widget->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Maximum);
|
||||||
|
|
||||||
|
auto *scrollArea = new QScrollArea;
|
||||||
|
scrollArea->setWidgetResizable(true);
|
||||||
|
scrollArea->setContentsMargins(0, 0, 0, 0);
|
||||||
|
scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
||||||
|
scrollArea->horizontalScrollBar()->setEnabled(false);
|
||||||
|
scrollArea->setWidget(widget);
|
||||||
|
return scrollArea;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Returns the theme icon resources for each settings page, indexed by SettingsPage order
|
||||||
|
*/
|
||||||
|
static QStringList pageIconResources()
|
||||||
|
{
|
||||||
|
return {QStringLiteral("theme:config/general"), QStringLiteral("theme:config/appearance"),
|
||||||
|
QStringLiteral("theme:config/interface"), QStringLiteral("theme:config/deckeditor"),
|
||||||
|
QStringLiteral("theme:config/storage"), QStringLiteral("theme:config/messages"),
|
||||||
|
QStringLiteral("theme:config/sound"), QStringLiteral("theme:config/shorcuts")};
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsView::SettingsView(QWidget *parent) : QWidget(parent), currentTabIndex(0), searchActive(false)
|
||||||
|
{
|
||||||
|
setupUi();
|
||||||
|
|
||||||
|
connect(&SettingsCache::instance().personal(), &PersonalSettings::langChanged, this, &SettingsView::retranslateUi);
|
||||||
|
retranslateUi();
|
||||||
|
|
||||||
|
searchEdit->setFocus();
|
||||||
|
}
|
||||||
|
|
||||||
|
void SettingsView::setupUi()
|
||||||
|
{
|
||||||
|
// Search bar
|
||||||
|
searchEdit = new QLineEdit;
|
||||||
|
searchEdit->setClearButtonEnabled(true);
|
||||||
|
searchEdit->addAction(themePixmap(QStringLiteral("icons/search")), QLineEdit::LeadingPosition);
|
||||||
|
searchEdit->installEventFilter(this);
|
||||||
|
connect(searchEdit, &QLineEdit::textChanged, this, &SettingsView::onSearchTextChanged);
|
||||||
|
|
||||||
|
auto *searchLayout = new QHBoxLayout;
|
||||||
|
searchLayout->addWidget(searchEdit);
|
||||||
|
|
||||||
|
// Tab bar (built in setupTabBar)
|
||||||
|
setupTabBar();
|
||||||
|
|
||||||
|
// Pages stacked widget
|
||||||
|
pagesWidget = new QStackedWidget;
|
||||||
|
|
||||||
|
auto *generalPage = new GeneralSettingsPage;
|
||||||
|
auto *appearancePage = new AppearanceSettingsPage;
|
||||||
|
auto *userInterfacePage = new UserInterfaceSettingsPage;
|
||||||
|
auto *deckEditorPage = new DeckEditorSettingsPage;
|
||||||
|
auto *storagePage = new StorageSettingsPage;
|
||||||
|
auto *messagesPage = new MessagesSettingsPage;
|
||||||
|
auto *soundPage = new SoundSettingsPage;
|
||||||
|
auto *shortcutsPage = new ShortcutSettingsPage;
|
||||||
|
|
||||||
|
pages.append(generalPage);
|
||||||
|
pages.append(appearancePage);
|
||||||
|
pages.append(userInterfacePage);
|
||||||
|
pages.append(deckEditorPage);
|
||||||
|
pages.append(storagePage);
|
||||||
|
pages.append(messagesPage);
|
||||||
|
pages.append(soundPage);
|
||||||
|
pages.append(shortcutsPage);
|
||||||
|
|
||||||
|
pagesWidget->addWidget(makeScrollable(generalPage));
|
||||||
|
pagesWidget->addWidget(makeScrollable(appearancePage));
|
||||||
|
pagesWidget->addWidget(makeScrollable(userInterfacePage));
|
||||||
|
pagesWidget->addWidget(makeScrollable(deckEditorPage));
|
||||||
|
pagesWidget->addWidget(makeScrollable(storagePage));
|
||||||
|
pagesWidget->addWidget(makeScrollable(messagesPage));
|
||||||
|
pagesWidget->addWidget(soundPage);
|
||||||
|
pagesWidget->addWidget(shortcutsPage);
|
||||||
|
|
||||||
|
Q_ASSERT(pages.size() == NumPages);
|
||||||
|
|
||||||
|
// Search results view (hidden by default)
|
||||||
|
searchResultsView = new QListView;
|
||||||
|
searchResultsView->setUniformItemSizes(false);
|
||||||
|
searchResultsView->setSelectionMode(QAbstractItemView::SingleSelection);
|
||||||
|
searchResultsView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
||||||
|
searchResultsView->setVisible(false);
|
||||||
|
searchResultsView->setStyleSheet(
|
||||||
|
"QListView::item:selected { background: palette(highlight); color: palette(highlighted-text); }");
|
||||||
|
|
||||||
|
searchModel = new SettingsSearchModel(this);
|
||||||
|
searchDelegate = new SettingsSearchDelegate(this);
|
||||||
|
searchResultsView->setModel(searchModel);
|
||||||
|
searchResultsView->setItemDelegate(searchDelegate);
|
||||||
|
connect(searchResultsView, &QListView::clicked, this, &SettingsView::onSearchResultClicked);
|
||||||
|
|
||||||
|
connect(&SettingsCache::instance(), &SettingsCache::themeChanged, this, [this] {
|
||||||
|
const QStringList icons = pageIconResources();
|
||||||
|
for (int i = 0; i < tabButtons.size() && i < icons.size(); ++i) {
|
||||||
|
tabButtons[i]->setIcon(QPixmap(icons[i]));
|
||||||
|
}
|
||||||
|
searchDelegate->setPageIcons(icons);
|
||||||
|
searchResultsView->viewport()->update();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Build search index after pages are created
|
||||||
|
buildSearchIndex();
|
||||||
|
|
||||||
|
// Pages container (stacked widget + search results overlay)
|
||||||
|
pagesContainer = new QWidget;
|
||||||
|
auto *containerLayout = new QStackedLayout;
|
||||||
|
containerLayout->setStackingMode(QStackedLayout::StackAll);
|
||||||
|
containerLayout->addWidget(pagesWidget);
|
||||||
|
containerLayout->addWidget(searchResultsView);
|
||||||
|
pagesContainer->setLayout(containerLayout);
|
||||||
|
|
||||||
|
// Main layout
|
||||||
|
auto *mainLayout = new QVBoxLayout;
|
||||||
|
mainLayout->addLayout(searchLayout);
|
||||||
|
mainLayout->addWidget(tabBarWidget);
|
||||||
|
auto *separator = new QFrame;
|
||||||
|
separator->setFrameShape(QFrame::HLine);
|
||||||
|
separator->setFrameShadow(QFrame::Sunken);
|
||||||
|
mainLayout->addWidget(separator);
|
||||||
|
mainLayout->addWidget(pagesContainer);
|
||||||
|
mainLayout->addSpacing(4);
|
||||||
|
setLayout(mainLayout);
|
||||||
|
|
||||||
|
// Keyboard shortcuts
|
||||||
|
auto *searchShortcut = new QShortcut(QKeySequence(Qt::CTRL | Qt::Key_F), this);
|
||||||
|
connect(searchShortcut, &QShortcut::activated, searchEdit, qOverload<>(&QLineEdit::setFocus));
|
||||||
|
|
||||||
|
auto *nextTabShortcut = new QShortcut(QKeySequence(Qt::CTRL | Qt::Key_Tab), this);
|
||||||
|
connect(nextTabShortcut, &QShortcut::activated, this, [this] {
|
||||||
|
int next = (currentTabIndex + 1) % tabButtons.size();
|
||||||
|
setActiveTab(next);
|
||||||
|
});
|
||||||
|
|
||||||
|
auto *prevTabShortcut = new QShortcut(QKeySequence(Qt::CTRL | Qt::SHIFT | Qt::Key_Tab), this);
|
||||||
|
connect(prevTabShortcut, &QShortcut::activated, this, [this] {
|
||||||
|
int prev = (currentTabIndex - 1 + tabButtons.size()) % tabButtons.size();
|
||||||
|
setActiveTab(prev);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initialize to first tab
|
||||||
|
setActiveTab(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void SettingsView::setupTabBar()
|
||||||
|
{
|
||||||
|
tabBarWidget = new QWidget;
|
||||||
|
auto *tabLayout = new QHBoxLayout;
|
||||||
|
tabLayout->setContentsMargins(0, 0, 0, 0);
|
||||||
|
tabLayout->setSpacing(2);
|
||||||
|
|
||||||
|
const QStringList iconResources = pageIconResources();
|
||||||
|
|
||||||
|
for (int i = 0; i < iconResources.size(); ++i) {
|
||||||
|
auto *tabButton = new QToolButton;
|
||||||
|
tabButton->setCheckable(true);
|
||||||
|
tabButton->setIcon(QPixmap(iconResources[i]));
|
||||||
|
tabButton->setIconSize(QSize(48, 48));
|
||||||
|
tabButton->setToolButtonStyle(Qt::ToolButtonTextUnderIcon);
|
||||||
|
tabButton->setAutoExclusive(true);
|
||||||
|
tabButton->setMinimumHeight(85);
|
||||||
|
tabButton->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
|
||||||
|
|
||||||
|
connect(tabButton, &QToolButton::clicked, this, [this, idx = i] { onTabClicked(idx); });
|
||||||
|
|
||||||
|
tabButtons.append(tabButton);
|
||||||
|
tabLayout->addWidget(tabButton);
|
||||||
|
}
|
||||||
|
|
||||||
|
tabBarWidget->setLayout(tabLayout);
|
||||||
|
}
|
||||||
|
|
||||||
|
void SettingsView::buildSearchIndex()
|
||||||
|
{
|
||||||
|
QList<SettingsSearchEntry> allEntries;
|
||||||
|
|
||||||
|
const QStringList pageNames = translatedPageNames();
|
||||||
|
searchDelegate->setPageNames(pageNames);
|
||||||
|
searchDelegate->setPageIcons(pageIconResources());
|
||||||
|
|
||||||
|
for (int i = 0; i < pages.size(); ++i) {
|
||||||
|
QList<SettingsSearchEntry> pageEntries = pages[i]->getSearchEntries();
|
||||||
|
for (auto &entry : pageEntries) {
|
||||||
|
if (entry.pageIndex == -1) {
|
||||||
|
entry.pageIndex = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
allEntries.append(pageEntries);
|
||||||
|
}
|
||||||
|
|
||||||
|
searchModel->setSourceEntries(allEntries);
|
||||||
|
}
|
||||||
|
|
||||||
|
void SettingsView::onTabClicked(int index)
|
||||||
|
{
|
||||||
|
if (searchActive) {
|
||||||
|
switchToTabMode();
|
||||||
|
}
|
||||||
|
setActiveTab(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
void SettingsView::setActiveTab(int index)
|
||||||
|
{
|
||||||
|
if (index < 0 || index >= tabButtons.size()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentTabIndex = index;
|
||||||
|
pagesWidget->setCurrentIndex(index);
|
||||||
|
|
||||||
|
for (int i = 0; i < tabButtons.size(); ++i) {
|
||||||
|
tabButtons[i]->setChecked(i == index);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Style active tab with a thick accent border + subtle background tint
|
||||||
|
for (int i = 0; i < tabButtons.size(); ++i) {
|
||||||
|
if (i == index) {
|
||||||
|
tabButtons[i]->setStyleSheet("QToolButton { border: none; border-bottom: 3px solid palette(highlight); "
|
||||||
|
"border-top-left-radius: 4px; border-top-right-radius: 4px; "
|
||||||
|
"background: palette(window); padding-bottom: 1px; }");
|
||||||
|
} else {
|
||||||
|
tabButtons[i]->setStyleSheet("QToolButton { border: none; border-bottom: 1px solid transparent; "
|
||||||
|
"border-top-left-radius: 4px; border-top-right-radius: 4px; "
|
||||||
|
"background: transparent; }");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SettingsView::flashWidget(QWidget *widget)
|
||||||
|
{
|
||||||
|
auto *overlay = new QWidget(widget);
|
||||||
|
overlay->setGeometry(widget->rect());
|
||||||
|
overlay->setAttribute(Qt::WA_TransparentForMouseEvents, true);
|
||||||
|
|
||||||
|
QPalette pal = overlay->palette();
|
||||||
|
QColor flashColor = pal.color(QPalette::Highlight);
|
||||||
|
flashColor.setAlpha(100);
|
||||||
|
pal.setBrush(QPalette::Window, flashColor);
|
||||||
|
overlay->setPalette(pal);
|
||||||
|
overlay->setAutoFillBackground(true);
|
||||||
|
|
||||||
|
auto *effect = new QGraphicsOpacityEffect(overlay);
|
||||||
|
effect->setOpacity(0.0);
|
||||||
|
overlay->setGraphicsEffect(effect);
|
||||||
|
overlay->show();
|
||||||
|
overlay->raise();
|
||||||
|
|
||||||
|
auto *flashIn = new QPropertyAnimation(effect, "opacity");
|
||||||
|
flashIn->setDuration(120);
|
||||||
|
flashIn->setStartValue(0.0);
|
||||||
|
flashIn->setEndValue(0.6);
|
||||||
|
flashIn->setEasingCurve(QEasingCurve::OutCubic);
|
||||||
|
|
||||||
|
auto *fadeOut = new QPropertyAnimation(effect, "opacity");
|
||||||
|
fadeOut->setDuration(900);
|
||||||
|
fadeOut->setStartValue(0.6);
|
||||||
|
fadeOut->setEndValue(0.0);
|
||||||
|
fadeOut->setEasingCurve(QEasingCurve::InCubic);
|
||||||
|
|
||||||
|
auto *group = new QSequentialAnimationGroup(overlay);
|
||||||
|
group->addAnimation(flashIn);
|
||||||
|
group->addAnimation(fadeOut);
|
||||||
|
|
||||||
|
connect(group, &QSequentialAnimationGroup::finished, overlay, &QWidget::deleteLater);
|
||||||
|
|
||||||
|
group->start(QAbstractAnimation::DeleteWhenStopped);
|
||||||
|
}
|
||||||
|
|
||||||
|
void SettingsView::onSearchTextChanged(const QString &text)
|
||||||
|
{
|
||||||
|
searchModel->setFilterString(text);
|
||||||
|
|
||||||
|
if (searchModel->isFilterActive() && !text.trimmed().isEmpty()) {
|
||||||
|
if (!searchActive) {
|
||||||
|
switchToSearchMode();
|
||||||
|
}
|
||||||
|
if (searchModel->rowCount(QModelIndex()) > 0) {
|
||||||
|
searchResultsView->setCurrentIndex(searchModel->index(0));
|
||||||
|
}
|
||||||
|
} else if (searchActive) {
|
||||||
|
switchToTabMode();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SettingsView::switchToSearchMode()
|
||||||
|
{
|
||||||
|
searchActive = true;
|
||||||
|
tabBarWidget->setVisible(false);
|
||||||
|
pagesWidget->setVisible(false);
|
||||||
|
searchResultsView->setVisible(true);
|
||||||
|
if (searchModel->rowCount(QModelIndex()) > 0) {
|
||||||
|
searchResultsView->setCurrentIndex(searchModel->index(0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SettingsView::switchToTabMode()
|
||||||
|
{
|
||||||
|
searchActive = false;
|
||||||
|
tabBarWidget->setVisible(true);
|
||||||
|
pagesWidget->setVisible(true);
|
||||||
|
searchResultsView->setVisible(false);
|
||||||
|
searchEdit->blockSignals(true);
|
||||||
|
searchEdit->clear();
|
||||||
|
searchEdit->blockSignals(false);
|
||||||
|
setActiveTab(currentTabIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
void SettingsView::onSearchResultClicked(const QModelIndex &index)
|
||||||
|
{
|
||||||
|
navigateToSearchResult(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
void SettingsView::navigateToSearchResult(const QModelIndex &index)
|
||||||
|
{
|
||||||
|
SettingsSearchEntry entry = searchModel->entryForIndex(index);
|
||||||
|
if (entry.pageIndex < 0 || entry.pageIndex >= pages.size()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Switch to the page
|
||||||
|
switchToTabMode();
|
||||||
|
setActiveTab(entry.pageIndex);
|
||||||
|
|
||||||
|
// Scroll to the widget, focus it, and flash to highlight it
|
||||||
|
if (entry.widget) {
|
||||||
|
QWidget *widget = entry.widget;
|
||||||
|
while (widget) {
|
||||||
|
if (auto *scrollArea = qobject_cast<QScrollArea *>(widget)) {
|
||||||
|
scrollArea->ensureWidgetVisible(entry.widget);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
widget = widget->parentWidget();
|
||||||
|
}
|
||||||
|
entry.widget->setFocus();
|
||||||
|
flashWidget(entry.widget);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SettingsView::setTab(int index)
|
||||||
|
{
|
||||||
|
if (index >= 0 && index < tabButtons.size()) {
|
||||||
|
setActiveTab(index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AbstractSettingsPage *SettingsView::page(SettingsPage which) const
|
||||||
|
{
|
||||||
|
return pages.value(static_cast<int>(which));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SettingsView::eventFilter(QObject *watched, QEvent *event)
|
||||||
|
{
|
||||||
|
if (watched == searchEdit && event->type() == QEvent::KeyPress) {
|
||||||
|
auto *keyEvent = static_cast<QKeyEvent *>(event);
|
||||||
|
if (keyEvent->key() == Qt::Key_Escape) {
|
||||||
|
if (searchActive) {
|
||||||
|
switchToTabMode();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} else if (keyEvent->key() == Qt::Key_Return || keyEvent->key() == Qt::Key_Enter) {
|
||||||
|
if (searchActive) {
|
||||||
|
if (searchResultsView->currentIndex().isValid()) {
|
||||||
|
navigateToSearchResult(searchResultsView->currentIndex());
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} else if (keyEvent->key() == Qt::Key_Down) {
|
||||||
|
if (searchActive) {
|
||||||
|
int nextRow = searchResultsView->currentIndex().row() + 1;
|
||||||
|
if (nextRow >= searchModel->rowCount()) {
|
||||||
|
nextRow = 0;
|
||||||
|
}
|
||||||
|
searchResultsView->setCurrentIndex(searchModel->index(nextRow));
|
||||||
|
searchResultsView->scrollTo(searchModel->index(nextRow));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} else if (keyEvent->key() == Qt::Key_Up) {
|
||||||
|
if (searchActive) {
|
||||||
|
int prevRow = searchResultsView->currentIndex().row() - 1;
|
||||||
|
if (prevRow < 0) {
|
||||||
|
prevRow = searchModel->rowCount() - 1;
|
||||||
|
}
|
||||||
|
if (prevRow >= 0) {
|
||||||
|
searchResultsView->setCurrentIndex(searchModel->index(prevRow));
|
||||||
|
searchResultsView->scrollTo(searchModel->index(prevRow));
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return QWidget::eventFilter(watched, event);
|
||||||
|
}
|
||||||
|
|
||||||
|
void SettingsView::retranslateUi()
|
||||||
|
{
|
||||||
|
retranslateTabNames();
|
||||||
|
|
||||||
|
// NOTE: the "DlgSettings" context is deliberate — these strings lived in DlgSettings
|
||||||
|
// before the view was extracted, and keeping the context preserves existing translations.
|
||||||
|
searchEdit->setPlaceholderText(QCoreApplication::translate("DlgSettings", "Search settings..."));
|
||||||
|
|
||||||
|
// Rebuild search index for translated text
|
||||||
|
buildSearchIndex();
|
||||||
|
}
|
||||||
|
|
||||||
|
QStringList SettingsView::translatedPageNames()
|
||||||
|
{
|
||||||
|
// NOTE: same "DlgSettings" context as above (translation continuity, see retranslateUi).
|
||||||
|
return {QCoreApplication::translate("DlgSettings", "General"),
|
||||||
|
QCoreApplication::translate("DlgSettings", "Appearance"),
|
||||||
|
QCoreApplication::translate("DlgSettings", "User Interface"),
|
||||||
|
QCoreApplication::translate("DlgSettings", "Card Sources"),
|
||||||
|
QCoreApplication::translate("DlgSettings", "Storage"),
|
||||||
|
QCoreApplication::translate("DlgSettings", "Chat"),
|
||||||
|
QCoreApplication::translate("DlgSettings", "Sound"),
|
||||||
|
QCoreApplication::translate("DlgSettings", "Shortcuts")};
|
||||||
|
}
|
||||||
|
|
||||||
|
void SettingsView::retranslateTabNames()
|
||||||
|
{
|
||||||
|
const QStringList tabLabels = translatedPageNames();
|
||||||
|
|
||||||
|
for (int i = 0; i < tabButtons.size() && i < tabLabels.size(); ++i) {
|
||||||
|
tabButtons[i]->setText(tabLabels[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
98
cockatrice/src/interface/widgets/dialogs/settings_view.h
Normal file
98
cockatrice/src/interface/widgets/dialogs/settings_view.h
Normal file
|
|
@ -0,0 +1,98 @@
|
||||||
|
/**
|
||||||
|
* @file settings_view.h
|
||||||
|
* @brief Settings content widget hosted by the settings dialog (and, later, the settings tab)
|
||||||
|
* @ingroup Dialogs
|
||||||
|
*/
|
||||||
|
#ifndef SETTINGS_VIEW_H
|
||||||
|
#define SETTINGS_VIEW_H
|
||||||
|
|
||||||
|
#include <QList>
|
||||||
|
#include <QString>
|
||||||
|
#include <QStringList>
|
||||||
|
#include <QWidget>
|
||||||
|
#include <qtmetamacros.h>
|
||||||
|
|
||||||
|
class QModelIndex;
|
||||||
|
class QObject;
|
||||||
|
class QEvent;
|
||||||
|
|
||||||
|
class QStackedWidget;
|
||||||
|
class QToolButton;
|
||||||
|
class QListView;
|
||||||
|
class QLineEdit;
|
||||||
|
class AbstractSettingsPage;
|
||||||
|
class SettingsSearchModel;
|
||||||
|
class SettingsSearchDelegate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Settings pages with tabbed navigation and search, without any dialog chrome
|
||||||
|
*
|
||||||
|
* Owns the search bar, the tab bar, the stacked settings pages and the search
|
||||||
|
* results view. Dialog/tab hosts only add their own frame (buttons, titles).
|
||||||
|
*/
|
||||||
|
class SettingsView : public QWidget
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
/**
|
||||||
|
* @brief Page order in the tab bar, matching the order pages are added in setupUi()
|
||||||
|
*
|
||||||
|
* Use these values instead of raw indices so reordering pages never silently
|
||||||
|
* breaks external callers like tab_room.cpp.
|
||||||
|
*/
|
||||||
|
enum SettingsPage
|
||||||
|
{
|
||||||
|
GeneralPage = 0,
|
||||||
|
AppearancePage,
|
||||||
|
UserInterfacePage,
|
||||||
|
DeckEditorPage,
|
||||||
|
StoragePage,
|
||||||
|
MessagesPage,
|
||||||
|
SoundPage,
|
||||||
|
ShortcutsPage,
|
||||||
|
NumPages
|
||||||
|
};
|
||||||
|
|
||||||
|
explicit SettingsView(QWidget *parent = nullptr);
|
||||||
|
void setTab(int index);
|
||||||
|
AbstractSettingsPage *page(SettingsPage which) const;
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
void onTabClicked(int index);
|
||||||
|
void onSearchTextChanged(const QString &text);
|
||||||
|
void onSearchResultClicked(const QModelIndex &index);
|
||||||
|
|
||||||
|
private:
|
||||||
|
// UI elements
|
||||||
|
QLineEdit *searchEdit; ///< Search bar for filtering settings
|
||||||
|
QWidget *tabBarWidget; ///< Container widget for the tab buttons
|
||||||
|
QList<QToolButton *> tabButtons; ///< Navigation tab buttons
|
||||||
|
QStackedWidget *pagesWidget; ///< Stacked widget containing settings pages
|
||||||
|
QListView *searchResultsView; ///< Search results list view
|
||||||
|
QWidget *pagesContainer; ///< Container stacking pages and search results
|
||||||
|
|
||||||
|
// Data
|
||||||
|
QList<AbstractSettingsPage *> pages; ///< All settings page instances
|
||||||
|
SettingsSearchModel *searchModel; ///< Model for search results
|
||||||
|
SettingsSearchDelegate *searchDelegate; ///< Delegate for search result rendering
|
||||||
|
int currentTabIndex; ///< Currently active tab index
|
||||||
|
bool searchActive; ///< Whether search mode is active
|
||||||
|
|
||||||
|
void setupUi();
|
||||||
|
void setupTabBar();
|
||||||
|
void buildSearchIndex();
|
||||||
|
void switchToTabMode();
|
||||||
|
void switchToSearchMode();
|
||||||
|
void navigateToSearchResult(const QModelIndex &index);
|
||||||
|
void setActiveTab(int index);
|
||||||
|
static void flashWidget(QWidget *widget);
|
||||||
|
static QStringList translatedPageNames();
|
||||||
|
|
||||||
|
void retranslateUi();
|
||||||
|
void retranslateTabNames();
|
||||||
|
|
||||||
|
protected:
|
||||||
|
bool eventFilter(QObject *watched, QEvent *event) override;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // SETTINGS_VIEW_H
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
#include "../../../client/settings/cache_settings.h"
|
#include "../../../client/settings/cache_settings.h"
|
||||||
#include "../../pixel_map_generator.h"
|
#include "../../pixel_map_generator.h"
|
||||||
|
#include "../dialogs/path_safety_warning.h"
|
||||||
#include "update/card_spoiler/spoiler_background_updater.h"
|
#include "update/card_spoiler/spoiler_background_updater.h"
|
||||||
|
|
||||||
#include <QAbstractItemModel>
|
#include <QAbstractItemModel>
|
||||||
|
|
@ -363,6 +364,9 @@ void DeckEditorSettingsPage::spoilerPathButtonClicked()
|
||||||
if (lsPath.isEmpty()) {
|
if (lsPath.isEmpty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (!PathSafetyWarning::confirmOutsideProgramDir(this, lsPath + "/spoiler.xml")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
mpSpoilerSavePathLineEdit->setText(lsPath + "/spoiler.xml");
|
mpSpoilerSavePathLineEdit->setText(lsPath + "/spoiler.xml");
|
||||||
SettingsCache::instance().paths().setSpoilerDatabasePath(lsPath + "/spoiler.xml");
|
SettingsCache::instance().paths().setSpoilerDatabasePath(lsPath + "/spoiler.xml");
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
#include "general_settings_page.h"
|
#include "general_settings_page.h"
|
||||||
|
|
||||||
#include "../../../client/settings/cache_settings.h"
|
#include "../../../client/settings/cache_settings.h"
|
||||||
|
#include "../dialogs/path_safety_warning.h"
|
||||||
#include "../interface/card_picture_loader/card_picture_loader.h"
|
#include "../interface/card_picture_loader/card_picture_loader.h"
|
||||||
#include "../main.h"
|
#include "../main.h"
|
||||||
#include "../server/user/user_info_connection.h"
|
#include "../server/user/user_info_connection.h"
|
||||||
|
|
@ -378,6 +379,9 @@ void GeneralSettingsPage::deckPathButtonClicked()
|
||||||
if (path.isEmpty()) {
|
if (path.isEmpty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (!PathSafetyWarning::confirmOutsideProgramDir(this, path)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
deckPathEdit->setText(path);
|
deckPathEdit->setText(path);
|
||||||
SettingsCache::instance().paths().setDeckPath(path);
|
SettingsCache::instance().paths().setDeckPath(path);
|
||||||
|
|
@ -389,6 +393,9 @@ void GeneralSettingsPage::filtersPathButtonClicked()
|
||||||
if (path.isEmpty()) {
|
if (path.isEmpty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (!PathSafetyWarning::confirmOutsideProgramDir(this, path)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
filtersPathEdit->setText(path);
|
filtersPathEdit->setText(path);
|
||||||
SettingsCache::instance().paths().setFiltersPath(path);
|
SettingsCache::instance().paths().setFiltersPath(path);
|
||||||
|
|
@ -400,6 +407,9 @@ void GeneralSettingsPage::replaysPathButtonClicked()
|
||||||
if (path.isEmpty()) {
|
if (path.isEmpty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (!PathSafetyWarning::confirmOutsideProgramDir(this, path)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
replaysPathEdit->setText(path);
|
replaysPathEdit->setText(path);
|
||||||
SettingsCache::instance().paths().setReplaysPath(path);
|
SettingsCache::instance().paths().setReplaysPath(path);
|
||||||
|
|
@ -411,6 +421,9 @@ void GeneralSettingsPage::picsPathButtonClicked()
|
||||||
if (path.isEmpty()) {
|
if (path.isEmpty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (!PathSafetyWarning::confirmOutsideProgramDir(this, path)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
picsPathEdit->setText(path);
|
picsPathEdit->setText(path);
|
||||||
SettingsCache::instance().paths().setPicsPath(path);
|
SettingsCache::instance().paths().setPicsPath(path);
|
||||||
|
|
@ -422,6 +435,9 @@ void GeneralSettingsPage::cardDatabasePathButtonClicked()
|
||||||
if (path.isEmpty()) {
|
if (path.isEmpty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (!PathSafetyWarning::confirmOutsideProgramDir(this, path)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
cardDatabasePathEdit->setText(path);
|
cardDatabasePathEdit->setText(path);
|
||||||
SettingsCache::instance().paths().setCardDatabasePath(path);
|
SettingsCache::instance().paths().setCardDatabasePath(path);
|
||||||
|
|
@ -433,6 +449,9 @@ void GeneralSettingsPage::customCardDatabaseButtonClicked()
|
||||||
if (path.isEmpty()) {
|
if (path.isEmpty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (!PathSafetyWarning::confirmOutsideProgramDir(this, path)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
customCardDatabasePathEdit->setText(path);
|
customCardDatabasePathEdit->setText(path);
|
||||||
SettingsCache::instance().paths().setCustomCardDatabasePath(path);
|
SettingsCache::instance().paths().setCustomCardDatabasePath(path);
|
||||||
|
|
@ -444,6 +463,9 @@ void GeneralSettingsPage::tokenDatabasePathButtonClicked()
|
||||||
if (path.isEmpty()) {
|
if (path.isEmpty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (!PathSafetyWarning::confirmOutsideProgramDir(this, path)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
tokenDatabasePathEdit->setText(path);
|
tokenDatabasePathEdit->setText(path);
|
||||||
SettingsCache::instance().paths().setTokenDatabasePath(path);
|
SettingsCache::instance().paths().setTokenDatabasePath(path);
|
||||||
|
|
|
||||||
|
|
@ -1060,6 +1060,12 @@ void TabGame::setCardMenu(CardMenu *menu)
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (aCardMenu->menu() && aCardMenu->menu()->isVisible()) {
|
||||||
|
aCardMenu->menu()->hide();
|
||||||
|
if (gameView && gameView->viewport()) {
|
||||||
|
gameView->viewport()->update();
|
||||||
|
}
|
||||||
|
}
|
||||||
if (menu) {
|
if (menu) {
|
||||||
aCardMenu->setMenu(menu);
|
aCardMenu->setMenu(menu);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
#include "tab_report.h"
|
#include "tab_report.h"
|
||||||
|
|
||||||
|
#include "../server/user/user_context_menu.h"
|
||||||
#include "../utility/report_utils.h"
|
#include "../utility/report_utils.h"
|
||||||
#include "abstract_client.h"
|
#include "abstract_client.h"
|
||||||
#include "libcockatrice/protocol/pb/response.pb.h"
|
#include "libcockatrice/protocol/pb/response.pb.h"
|
||||||
|
|
@ -33,11 +34,13 @@
|
||||||
#include <QVariant>
|
#include <QVariant>
|
||||||
#include <QWidget>
|
#include <QWidget>
|
||||||
#include <QtGlobal>
|
#include <QtGlobal>
|
||||||
|
#include <libcockatrice/network/server/remote/user_level.h>
|
||||||
#include <libcockatrice/protocol/pb/command_replay_download_by_game_id.pb.h>
|
#include <libcockatrice/protocol/pb/command_replay_download_by_game_id.pb.h>
|
||||||
#include <libcockatrice/protocol/pb/command_report_add_comment.pb.h>
|
#include <libcockatrice/protocol/pb/command_report_add_comment.pb.h>
|
||||||
#include <libcockatrice/protocol/pb/command_report_assign.pb.h>
|
#include <libcockatrice/protocol/pb/command_report_assign.pb.h>
|
||||||
#include <libcockatrice/protocol/pb/command_report_details.pb.h>
|
#include <libcockatrice/protocol/pb/command_report_details.pb.h>
|
||||||
#include <libcockatrice/protocol/pb/command_report_list.pb.h>
|
#include <libcockatrice/protocol/pb/command_report_list.pb.h>
|
||||||
|
#include <libcockatrice/protocol/pb/command_report_reopen.pb.h>
|
||||||
#include <libcockatrice/protocol/pb/command_report_resolve.pb.h>
|
#include <libcockatrice/protocol/pb/command_report_resolve.pb.h>
|
||||||
#include <libcockatrice/protocol/pb/command_report_stats.pb.h>
|
#include <libcockatrice/protocol/pb/command_report_stats.pb.h>
|
||||||
#include <libcockatrice/protocol/pb/command_report_user_info.pb.h>
|
#include <libcockatrice/protocol/pb/command_report_user_info.pb.h>
|
||||||
|
|
@ -120,6 +123,10 @@ TabReport::TabReport(TabSupervisor *_tabSupervisor, AbstractClient *_client) : T
|
||||||
table->horizontalHeader()->setSectionResizeMode(COL_REPORTED, QHeaderView::Stretch);
|
table->horizontalHeader()->setSectionResizeMode(COL_REPORTED, QHeaderView::Stretch);
|
||||||
table->horizontalHeader()->setSectionResizeMode(COL_REPORTER, QHeaderView::ResizeToContents);
|
table->horizontalHeader()->setSectionResizeMode(COL_REPORTER, QHeaderView::ResizeToContents);
|
||||||
connect(table, &QTableWidget::itemSelectionChanged, this, &TabReport::onSelectionChanged);
|
connect(table, &QTableWidget::itemSelectionChanged, this, &TabReport::onSelectionChanged);
|
||||||
|
table->setContextMenuPolicy(Qt::CustomContextMenu);
|
||||||
|
connect(table, &QTableWidget::customContextMenuRequested, this, &TabReport::showTableContextMenu);
|
||||||
|
userContextMenu = new UserContextMenu(tabSupervisor, this);
|
||||||
|
connect(userContextMenu, &UserContextMenu::openMessageDialog, this, &TabReport::openMessageDialog);
|
||||||
|
|
||||||
descGroup = new QGroupBox;
|
descGroup = new QGroupBox;
|
||||||
descriptionEdit = new QTextEdit;
|
descriptionEdit = new QTextEdit;
|
||||||
|
|
@ -231,14 +238,14 @@ TabReport::TabReport(TabSupervisor *_tabSupervisor, AbstractClient *_client) : T
|
||||||
|
|
||||||
assignButton = new QPushButton;
|
assignButton = new QPushButton;
|
||||||
resolveButton = new QPushButton;
|
resolveButton = new QPushButton;
|
||||||
resolveWithNoteButton = new QPushButton;
|
|
||||||
dismissButton = new QPushButton;
|
dismissButton = new QPushButton;
|
||||||
|
reopenButton = new QPushButton;
|
||||||
viewReplayButton = new QPushButton;
|
viewReplayButton = new QPushButton;
|
||||||
joinGameButton = new QPushButton;
|
joinGameButton = new QPushButton;
|
||||||
connect(assignButton, &QPushButton::clicked, this, &TabReport::assignReport);
|
connect(assignButton, &QPushButton::clicked, this, &TabReport::assignReport);
|
||||||
connect(resolveButton, &QPushButton::clicked, this, [this]() { resolveReport(false, false); });
|
connect(resolveButton, &QPushButton::clicked, this, [this]() { resolveReport(false); });
|
||||||
connect(resolveWithNoteButton, &QPushButton::clicked, this, [this]() { resolveReport(false, true); });
|
connect(dismissButton, &QPushButton::clicked, this, [this]() { resolveReport(true); });
|
||||||
connect(dismissButton, &QPushButton::clicked, this, [this]() { resolveReport(true, true); });
|
connect(reopenButton, &QPushButton::clicked, this, &TabReport::reopenReport);
|
||||||
connect(viewReplayButton, &QPushButton::clicked, this, &TabReport::viewReplay);
|
connect(viewReplayButton, &QPushButton::clicked, this, &TabReport::viewReplay);
|
||||||
connect(joinGameButton, &QPushButton::clicked, this, &TabReport::joinGame);
|
connect(joinGameButton, &QPushButton::clicked, this, &TabReport::joinGame);
|
||||||
|
|
||||||
|
|
@ -247,8 +254,8 @@ TabReport::TabReport(TabSupervisor *_tabSupervisor, AbstractClient *_client) : T
|
||||||
auto *actionBar = new QHBoxLayout;
|
auto *actionBar = new QHBoxLayout;
|
||||||
actionBar->addWidget(assignButton);
|
actionBar->addWidget(assignButton);
|
||||||
actionBar->addWidget(resolveButton);
|
actionBar->addWidget(resolveButton);
|
||||||
actionBar->addWidget(resolveWithNoteButton);
|
|
||||||
actionBar->addWidget(dismissButton);
|
actionBar->addWidget(dismissButton);
|
||||||
|
actionBar->addWidget(reopenButton);
|
||||||
actionBar->addSpacing(20);
|
actionBar->addSpacing(20);
|
||||||
actionBar->addWidget(viewReplayButton);
|
actionBar->addWidget(viewReplayButton);
|
||||||
actionBar->addWidget(joinGameButton);
|
actionBar->addWidget(joinGameButton);
|
||||||
|
|
@ -272,6 +279,7 @@ TabReport::TabReport(TabSupervisor *_tabSupervisor, AbstractClient *_client) : T
|
||||||
|
|
||||||
void TabReport::retranslateUi()
|
void TabReport::retranslateUi()
|
||||||
{
|
{
|
||||||
|
userContextMenu->retranslateUi();
|
||||||
searchEdit->setPlaceholderText(tr("Search by username, category..."));
|
searchEdit->setPlaceholderText(tr("Search by username, category..."));
|
||||||
statusFilter->clear();
|
statusFilter->clear();
|
||||||
statusFilter->addItem(tr("All Statuses"), "");
|
statusFilter->addItem(tr("All Statuses"), "");
|
||||||
|
|
@ -312,9 +320,10 @@ void TabReport::retranslateUi()
|
||||||
statsGroup->setTitle(tr("Report Statistics"));
|
statsGroup->setTitle(tr("Report Statistics"));
|
||||||
|
|
||||||
assignButton->setText(tr("Assign to Me"));
|
assignButton->setText(tr("Assign to Me"));
|
||||||
resolveButton->setText(tr("Resolve"));
|
resolveButton->setText(tr("Resolve..."));
|
||||||
resolveWithNoteButton->setText(tr("Resolve with note..."));
|
|
||||||
dismissButton->setText(tr("Dismiss..."));
|
dismissButton->setText(tr("Dismiss..."));
|
||||||
|
reopenButton->setText(tr("Reopen..."));
|
||||||
|
reopenButton->setToolTip(tr("Available for resolved or dismissed reports."));
|
||||||
viewReplayButton->setText(tr("View Replay"));
|
viewReplayButton->setText(tr("View Replay"));
|
||||||
joinGameButton->setText(tr("Join Game"));
|
joinGameButton->setText(tr("Join Game"));
|
||||||
}
|
}
|
||||||
|
|
@ -346,6 +355,7 @@ void TabReport::reportListResponse(const Response &response)
|
||||||
refreshButton->setEnabled(true);
|
refreshButton->setEnabled(true);
|
||||||
|
|
||||||
if (response.response_code() != Response::RespOk) {
|
if (response.response_code() != Response::RespOk) {
|
||||||
|
pendingStatusMessage.clear();
|
||||||
statusLabel->setText(tr("Failed to load reports."));
|
statusLabel->setText(tr("Failed to load reports."));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -359,6 +369,11 @@ void TabReport::reportListResponse(const Response &response)
|
||||||
applyFilters();
|
applyFilters();
|
||||||
updateStats();
|
updateStats();
|
||||||
|
|
||||||
|
if (!pendingStatusMessage.isEmpty()) {
|
||||||
|
statusLabel->setText(pendingStatusMessage);
|
||||||
|
pendingStatusMessage.clear();
|
||||||
|
}
|
||||||
|
|
||||||
if (selectedReportIdBeforeRefresh >= 0) {
|
if (selectedReportIdBeforeRefresh >= 0) {
|
||||||
bool found = false;
|
bool found = false;
|
||||||
for (int row = 0; row < table->rowCount(); ++row) {
|
for (int row = 0; row < table->rowCount(); ++row) {
|
||||||
|
|
@ -494,6 +509,36 @@ void TabReport::onSelectionChanged()
|
||||||
updateActionStates();
|
updateActionStates();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void TabReport::showTableContextMenu(const QPoint &pos)
|
||||||
|
{
|
||||||
|
QTableWidgetItem *clickedItem = table->itemAt(pos);
|
||||||
|
if (!clickedItem || clickedItem->row() < 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const int row = clickedItem->row();
|
||||||
|
const QTableWidgetItem *reportedItem = table->item(row, COL_REPORTED);
|
||||||
|
const QTableWidgetItem *idItem = table->item(row, COL_ID);
|
||||||
|
if (!reportedItem || !idItem || reportedItem->text().isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int userLevel = ServerInfo_User::IsUser;
|
||||||
|
const int reportId = idItem->data(Qt::UserRole).toInt();
|
||||||
|
for (const ServerInfo_Report &r : filteredReports) {
|
||||||
|
if (r.report_id() == reportId) {
|
||||||
|
if (r.reported_user_level() != 0) {
|
||||||
|
userLevel = r.reported_user_level();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
table->selectRow(row);
|
||||||
|
userContextMenu->showContextMenu(table->viewport()->mapToGlobal(pos), reportedItem->text(),
|
||||||
|
UserLevelFlags(userLevel));
|
||||||
|
}
|
||||||
|
|
||||||
void TabReport::loadReportDetails(int reportId)
|
void TabReport::loadReportDetails(int reportId)
|
||||||
{
|
{
|
||||||
detailsRequestedReportId = reportId;
|
detailsRequestedReportId = reportId;
|
||||||
|
|
@ -577,8 +622,8 @@ void TabReport::updateActionStates()
|
||||||
const QString status = QString::fromStdString(report.status());
|
const QString status = QString::fromStdString(report.status());
|
||||||
assignButton->setEnabled(status == "open");
|
assignButton->setEnabled(status == "open");
|
||||||
resolveButton->setEnabled(status == "open" || status == "assigned");
|
resolveButton->setEnabled(status == "open" || status == "assigned");
|
||||||
resolveWithNoteButton->setEnabled(status == "open" || status == "assigned");
|
|
||||||
dismissButton->setEnabled(status == "open" || status == "assigned");
|
dismissButton->setEnabled(status == "open" || status == "assigned");
|
||||||
|
reopenButton->setEnabled(status == "resolved" || status == "dismissed");
|
||||||
|
|
||||||
const bool hasGameId = report.game_id() > 0;
|
const bool hasGameId = report.game_id() > 0;
|
||||||
const bool hasReplay = hasGameId && report.has_replay_id() && report.replay_id() > 0;
|
const bool hasReplay = hasGameId && report.has_replay_id() && report.replay_id() > 0;
|
||||||
|
|
@ -590,8 +635,8 @@ void TabReport::setActionsEnabled(bool enabled)
|
||||||
{
|
{
|
||||||
assignButton->setEnabled(enabled);
|
assignButton->setEnabled(enabled);
|
||||||
resolveButton->setEnabled(enabled);
|
resolveButton->setEnabled(enabled);
|
||||||
resolveWithNoteButton->setEnabled(enabled);
|
|
||||||
dismissButton->setEnabled(enabled);
|
dismissButton->setEnabled(enabled);
|
||||||
|
reopenButton->setEnabled(enabled);
|
||||||
viewReplayButton->setEnabled(false);
|
viewReplayButton->setEnabled(false);
|
||||||
joinGameButton->setEnabled(false);
|
joinGameButton->setEnabled(false);
|
||||||
commentButton->setEnabled(enabled);
|
commentButton->setEnabled(enabled);
|
||||||
|
|
@ -645,7 +690,7 @@ void TabReport::assignReport()
|
||||||
void TabReport::assignResponse(const Response &response)
|
void TabReport::assignResponse(const Response &response)
|
||||||
{
|
{
|
||||||
if (response.response_code() == Response::RespOk) {
|
if (response.response_code() == Response::RespOk) {
|
||||||
statusLabel->setText(tr("Assigned."));
|
pendingStatusMessage = tr("Assigned.");
|
||||||
refreshList();
|
refreshList();
|
||||||
} else {
|
} else {
|
||||||
statusLabel->setText(tr("Assignment failed."));
|
statusLabel->setText(tr("Assignment failed."));
|
||||||
|
|
@ -653,22 +698,19 @@ void TabReport::assignResponse(const Response &response)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void TabReport::resolveReport(bool dismissed, bool promptNote)
|
void TabReport::resolveReport(bool dismissed)
|
||||||
{
|
{
|
||||||
const int reportId = selectedReportId();
|
const int reportId = selectedReportId();
|
||||||
if (reportId < 0) {
|
if (reportId < 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
QString note;
|
bool ok;
|
||||||
if (promptNote) {
|
QString note = QInputDialog::getText(this, dismissed ? tr("Dismiss Report") : tr("Resolve Report"),
|
||||||
bool ok;
|
dismissed ? tr("Optional note:") : tr("Resolution note (optional):"),
|
||||||
note = QInputDialog::getText(this, dismissed ? tr("Dismiss Report") : tr("Resolve Report"),
|
QLineEdit::Normal, QString(), &ok);
|
||||||
dismissed ? tr("Optional note:") : tr("Resolution note (optional):"),
|
if (!ok) {
|
||||||
QLineEdit::Normal, QString(), &ok);
|
return;
|
||||||
if (!ok) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setActionsEnabled(false);
|
setActionsEnabled(false);
|
||||||
|
|
@ -689,7 +731,46 @@ void TabReport::resolveReport(bool dismissed, bool promptNote)
|
||||||
void TabReport::resolveResponse(const Response &response)
|
void TabReport::resolveResponse(const Response &response)
|
||||||
{
|
{
|
||||||
if (response.response_code() == Response::RespOk) {
|
if (response.response_code() == Response::RespOk) {
|
||||||
statusLabel->setText(tr("Done."));
|
pendingStatusMessage = tr("Done.");
|
||||||
|
refreshList();
|
||||||
|
} else {
|
||||||
|
statusLabel->setText(tr("Action failed."));
|
||||||
|
updateActionStates();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void TabReport::reopenReport()
|
||||||
|
{
|
||||||
|
const int reportId = selectedReportId();
|
||||||
|
if (reportId < 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ok;
|
||||||
|
QString note = QInputDialog::getText(this, tr("Reopen Report"), tr("Reopening note (optional):"), QLineEdit::Normal,
|
||||||
|
QString(), &ok);
|
||||||
|
if (!ok) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setActionsEnabled(false);
|
||||||
|
statusLabel->setText(tr("Reopening..."));
|
||||||
|
|
||||||
|
Command_ReportReopen cmd;
|
||||||
|
cmd.set_report_id(reportId);
|
||||||
|
if (!note.isEmpty()) {
|
||||||
|
cmd.set_reopen_note(note.toStdString());
|
||||||
|
}
|
||||||
|
|
||||||
|
PendingCommand *pend = client->prepareModeratorCommand(cmd);
|
||||||
|
connect(pend, &PendingCommand::finished, this, &TabReport::reopenResponse);
|
||||||
|
client->sendCommand(pend);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TabReport::reopenResponse(const Response &response)
|
||||||
|
{
|
||||||
|
if (response.response_code() == Response::RespOk) {
|
||||||
|
pendingStatusMessage = tr("Reopened.");
|
||||||
refreshList();
|
refreshList();
|
||||||
} else {
|
} else {
|
||||||
statusLabel->setText(tr("Action failed."));
|
statusLabel->setText(tr("Action failed."));
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
#include "tab.h"
|
#include "tab.h"
|
||||||
|
|
||||||
#include <QList>
|
#include <QList>
|
||||||
|
#include <QPoint>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
#include <libcockatrice/protocol/pb/serverinfo_report.pb.h>
|
#include <libcockatrice/protocol/pb/serverinfo_report.pb.h>
|
||||||
#include <qtmetamacros.h>
|
#include <qtmetamacros.h>
|
||||||
|
|
@ -22,6 +23,7 @@ class QTimer;
|
||||||
class GameReplay;
|
class GameReplay;
|
||||||
class Response;
|
class Response;
|
||||||
class TabSupervisor;
|
class TabSupervisor;
|
||||||
|
class UserContextMenu;
|
||||||
|
|
||||||
class TabReport : public Tab
|
class TabReport : public Tab
|
||||||
{
|
{
|
||||||
|
|
@ -37,15 +39,17 @@ public:
|
||||||
signals:
|
signals:
|
||||||
void openReplay(GameReplay *replay);
|
void openReplay(GameReplay *replay);
|
||||||
void requestJoinGame(int gameId, int roomId);
|
void requestJoinGame(int gameId, int roomId);
|
||||||
|
void openMessageDialog(const QString &userName, bool focus);
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void refreshList();
|
void refreshList();
|
||||||
void reportListResponse(const Response &response);
|
void reportListResponse(const Response &response);
|
||||||
void assignReport();
|
void assignReport();
|
||||||
void assignResponse(const Response &response);
|
void assignResponse(const Response &response);
|
||||||
void resolveReport(bool dismissed, bool promptNote);
|
void resolveReport(bool dismissed);
|
||||||
void resolveResponse(const Response &response);
|
void resolveResponse(const Response &response);
|
||||||
void onSelectionChanged();
|
void onSelectionChanged();
|
||||||
|
void showTableContextMenu(const QPoint &pos);
|
||||||
void viewReplay();
|
void viewReplay();
|
||||||
void viewReplayResponse(const Response &response);
|
void viewReplayResponse(const Response &response);
|
||||||
void joinGame();
|
void joinGame();
|
||||||
|
|
@ -56,6 +60,8 @@ private slots:
|
||||||
void reportDetailsResponse(const Response &response);
|
void reportDetailsResponse(const Response &response);
|
||||||
void requestStats();
|
void requestStats();
|
||||||
void statsResponse(const Response &response);
|
void statsResponse(const Response &response);
|
||||||
|
void reopenReport();
|
||||||
|
void reopenResponse(const Response &response);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
int selectedReportId() const;
|
int selectedReportId() const;
|
||||||
|
|
@ -85,12 +91,13 @@ private:
|
||||||
QPushButton *commentButton;
|
QPushButton *commentButton;
|
||||||
QPushButton *assignButton;
|
QPushButton *assignButton;
|
||||||
QPushButton *resolveButton;
|
QPushButton *resolveButton;
|
||||||
QPushButton *resolveWithNoteButton;
|
|
||||||
QPushButton *dismissButton;
|
QPushButton *dismissButton;
|
||||||
|
QPushButton *reopenButton;
|
||||||
QPushButton *viewReplayButton;
|
QPushButton *viewReplayButton;
|
||||||
QPushButton *joinGameButton;
|
QPushButton *joinGameButton;
|
||||||
QLabel *statusLabel;
|
QLabel *statusLabel;
|
||||||
QTimer *refreshTimer;
|
QTimer *refreshTimer;
|
||||||
|
UserContextMenu *userContextMenu;
|
||||||
|
|
||||||
QGroupBox *userContextGroup;
|
QGroupBox *userContextGroup;
|
||||||
QLabel *userContextName;
|
QLabel *userContextName;
|
||||||
|
|
@ -108,6 +115,7 @@ private:
|
||||||
QLabel *userContextNotesLabel;
|
QLabel *userContextNotesLabel;
|
||||||
QLabel *userContextRecentReportsLabel;
|
QLabel *userContextRecentReportsLabel;
|
||||||
QString lastRequestedUser;
|
QString lastRequestedUser;
|
||||||
|
QString pendingStatusMessage;
|
||||||
QGroupBox *statsGroup;
|
QGroupBox *statsGroup;
|
||||||
QLabel *statsTotalLabel;
|
QLabel *statsTotalLabel;
|
||||||
QLabel *statsTrendLabel;
|
QLabel *statsTrendLabel;
|
||||||
|
|
|
||||||
|
|
@ -919,6 +919,7 @@ void TabSupervisor::openTabReport()
|
||||||
myAddTab(tabReport, aTabReport);
|
myAddTab(tabReport, aTabReport);
|
||||||
connect(tabReport, &TabReport::openReplay, this, &TabSupervisor::openReplay);
|
connect(tabReport, &TabReport::openReplay, this, &TabSupervisor::openReplay);
|
||||||
connect(tabReport, &TabReport::requestJoinGame, this, &TabSupervisor::joinReportGame);
|
connect(tabReport, &TabReport::requestJoinGame, this, &TabSupervisor::joinReportGame);
|
||||||
|
connect(tabReport, &TabReport::openMessageDialog, this, &TabSupervisor::addMessageTab);
|
||||||
connect(tabReport, &QObject::destroyed, this, [this] {
|
connect(tabReport, &QObject::destroyed, this, [this] {
|
||||||
tabReport = nullptr;
|
tabReport = nullptr;
|
||||||
aTabReport->setChecked(false);
|
aTabReport->setChecked(false);
|
||||||
|
|
|
||||||
|
|
@ -130,10 +130,19 @@ void LineEditCompleter::keyPressEvent(QKeyEvent *event)
|
||||||
CompleterInfo *active = nullptr;
|
CompleterInfo *active = nullptr;
|
||||||
QString prefix;
|
QString prefix;
|
||||||
|
|
||||||
|
// An open "[[" card link takes precedence over a mention: while the cursor is
|
||||||
|
// inside it, an earlier "@" must not shadow the card completer.
|
||||||
|
const int openCardPos = textValue.lastIndexOf("[[", cursorPos - 1);
|
||||||
|
const int closeCardPos = openCardPos == -1 ? -1 : textValue.indexOf("]]", openCardPos + 2);
|
||||||
|
const bool insideCardLink = openCardPos != -1 && (closeCardPos == -1 || closeCardPos >= cursorPos);
|
||||||
|
|
||||||
for (auto &info : completers) {
|
for (auto &info : completers) {
|
||||||
bool triggered = false;
|
bool triggered = false;
|
||||||
switch (info.trigger) {
|
switch (info.trigger) {
|
||||||
case CompleterTrigger::Mention: {
|
case CompleterTrigger::Mention: {
|
||||||
|
if (insideCardLink) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
int triggerPos = textValue.lastIndexOf("@", cursorPos - 1);
|
int triggerPos = textValue.lastIndexOf("@", cursorPos - 1);
|
||||||
if (triggerPos != -1 && (triggerPos == 0 || textValue[triggerPos - 1].isSpace())) {
|
if (triggerPos != -1 && (triggerPos == 0 || textValue[triggerPos - 1].isSpace())) {
|
||||||
triggered = true;
|
triggered = true;
|
||||||
|
|
|
||||||
|
|
@ -100,10 +100,27 @@ void renderReportDetails(QTextEdit *chatLogEdit,
|
||||||
const QString &moderatorPrefix,
|
const QString &moderatorPrefix,
|
||||||
const QString &nonModeratorPrefix)
|
const QString &nonModeratorPrefix)
|
||||||
{
|
{
|
||||||
|
QString details;
|
||||||
|
if (report.has_resolution_note() && !report.resolution_note().empty()) {
|
||||||
|
details = QObject::tr("Resolution note: %1").arg(QString::fromStdString(report.resolution_note()));
|
||||||
|
}
|
||||||
|
if (report.has_resolution_time()) {
|
||||||
|
QString resolvedBy = QObject::tr("unknown");
|
||||||
|
if (report.has_resolved_by_name() && !report.resolved_by_name().empty()) {
|
||||||
|
resolvedBy = QString::fromStdString(report.resolved_by_name());
|
||||||
|
}
|
||||||
|
if (!details.isEmpty()) {
|
||||||
|
details += "\n";
|
||||||
|
}
|
||||||
|
details += QObject::tr("Resolved at %1 by %2").arg(formatReportTime(report.resolution_time()), resolvedBy);
|
||||||
|
}
|
||||||
|
|
||||||
if (report.has_chat_log() && !report.chat_log().empty()) {
|
if (report.has_chat_log() && !report.chat_log().empty()) {
|
||||||
chatLogEdit->setPlainText(QString::fromStdString(report.chat_log()));
|
const QString chatLog = QString::fromStdString(report.chat_log());
|
||||||
|
chatLogEdit->setPlainText(
|
||||||
|
details.isEmpty() ? chatLog : details + "\n\n" + QObject::tr("=== Chat Log ===") + "\n" + chatLog);
|
||||||
} else {
|
} else {
|
||||||
chatLogEdit->clear();
|
chatLogEdit->setPlainText(details);
|
||||||
}
|
}
|
||||||
|
|
||||||
commentsEdit->clear();
|
commentsEdit->clear();
|
||||||
|
|
|
||||||
|
|
@ -58,7 +58,9 @@
|
||||||
#include <QAction>
|
#include <QAction>
|
||||||
#include <QApplication>
|
#include <QApplication>
|
||||||
#include <QButtonGroup>
|
#include <QButtonGroup>
|
||||||
|
#include <QCheckBox>
|
||||||
#include <QCloseEvent>
|
#include <QCloseEvent>
|
||||||
|
#include <QCoreApplication>
|
||||||
#include <QDateTime>
|
#include <QDateTime>
|
||||||
#include <QDebug>
|
#include <QDebug>
|
||||||
#include <QDesktopServices>
|
#include <QDesktopServices>
|
||||||
|
|
@ -603,8 +605,39 @@ MainWindow::MainWindow(QWidget *parent)
|
||||||
QTimer::singleShot(0, this, &MainWindow::applyStartupDestination);
|
QTimer::singleShot(0, this, &MainWindow::applyStartupDestination);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void MainWindow::warnAboutDataPathsInProgramDir()
|
||||||
|
{
|
||||||
|
auto &paths = SettingsCache::instance().paths();
|
||||||
|
if (SettingsCache::instance().getIsPortableBuild() || paths.getAppDirWarningAcknowledged()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QStringList offenders = paths.pathsInsideDir(QCoreApplication::applicationDirPath());
|
||||||
|
if (offenders.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QMessageBox box(QMessageBox::Warning, tr("Data paths inside the program directory"),
|
||||||
|
tr("These paths are configured inside the Cockatrice program directory:\n\n%1\n\n"
|
||||||
|
"Cockatrice keeps its own data outside the program directory by default. Files "
|
||||||
|
"stored inside it can be deleted by an update or by uninstalling Cockatrice. "
|
||||||
|
"Change them in the Settings dialog - most are on the General page - to keep "
|
||||||
|
"them safe.")
|
||||||
|
.arg(offenders.join(QLatin1Char('\n'))),
|
||||||
|
QMessageBox::Ok, this);
|
||||||
|
box.setTextFormat(Qt::PlainText);
|
||||||
|
auto *dontShowAgain = new QCheckBox(tr("Don't show this again"));
|
||||||
|
box.setCheckBox(dontShowAgain);
|
||||||
|
box.exec();
|
||||||
|
if (dontShowAgain->isChecked()) {
|
||||||
|
paths.setAppDirWarningAcknowledged(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void MainWindow::startupConfigCheck()
|
void MainWindow::startupConfigCheck()
|
||||||
{
|
{
|
||||||
|
warnAboutDataPathsInProgramDir();
|
||||||
|
|
||||||
const bool isCleanInstall = SettingsCache::instance().network().getClientVersion() == CLIENT_INFO_NOT_SET;
|
const bool isCleanInstall = SettingsCache::instance().network().getClientVersion() == CLIENT_INFO_NOT_SET;
|
||||||
|
|
||||||
// checkUnknownSets() is intentionally deferred from the card database load
|
// checkUnknownSets() is intentionally deferred from the card database load
|
||||||
|
|
|
||||||
|
|
@ -149,6 +149,9 @@ private:
|
||||||
|
|
||||||
void runFirstRunWizard(bool firstRun = false);
|
void runFirstRunWizard(bool firstRun = false);
|
||||||
|
|
||||||
|
/** @brief Warns once when a non-portable install keeps its data paths inside the program directory. */
|
||||||
|
void warnAboutDataPathsInProgramDir();
|
||||||
|
|
||||||
inline QString getCardUpdaterBinaryName()
|
inline QString getCardUpdaterBinaryName()
|
||||||
{
|
{
|
||||||
return "oracle";
|
return "oracle";
|
||||||
|
|
|
||||||
63
gcovr.cfg
Normal file
63
gcovr.cfg
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
# gcovr configuration for Cockatrice line coverage.
|
||||||
|
#
|
||||||
|
# Used by .ci/compile.sh --coverage and by hand:
|
||||||
|
#
|
||||||
|
# cmake -S . -B build/cover -DCMAKE_BUILD_TYPE=Debug -DWITH_SERVER=1 -DTEST=1 -DENABLE_COVERAGE=ON
|
||||||
|
# cmake --build build/cover -j 24
|
||||||
|
# ctest --test-dir build/cover -LE bench -j 24
|
||||||
|
# gcovr build/cover --root . --object-directory build/cover --txt --decisions
|
||||||
|
#
|
||||||
|
# The point of most of this file is subtraction. A raw report is dominated by code no test can
|
||||||
|
# reach: vendored third-party trees, build directories left over from earlier experiments, and
|
||||||
|
# the client's main() and window classes, which have no headless test. Those all dilute the one
|
||||||
|
# number worth watching, so they are filtered out here rather than in every invocation.
|
||||||
|
|
||||||
|
# root is deliberately not set here. This file is normally invoked from the build directory
|
||||||
|
# (compile.sh runs there, because it configures with `cmake ..`), so a `root` of `.` would
|
||||||
|
# resolve to the build tree and every filter below would exclude everything. compile.sh passes
|
||||||
|
# --root explicitly instead.
|
||||||
|
|
||||||
|
# Our own code only. This keeps generated Qt MOC output, protobuf output and anything CMake
|
||||||
|
# built under the source tree out of the report.
|
||||||
|
filter = ^cockatrice/
|
||||||
|
filter = ^libcockatrice_[^/]+/
|
||||||
|
filter = ^servatrice/
|
||||||
|
filter = ^oracle/
|
||||||
|
|
||||||
|
# The tests are the vehicle, not the subject. They are compiled with the same instrumentation,
|
||||||
|
# so leaving them in would add their near-perfect coverage to the numerator and quietly report a
|
||||||
|
# healthier project than we have: test code is only ever executed by the run that is measuring
|
||||||
|
# it. Helpers under tests/ are excluded on the same grounds.
|
||||||
|
exclude = ^tests/
|
||||||
|
|
||||||
|
# Vendored third-party code is not ours to cover and is never going to move the number.
|
||||||
|
exclude = ^vcpkg/
|
||||||
|
exclude = ^doc/doxygen/theme/
|
||||||
|
exclude = ^libcockatrice_rng/libcockatrice/rng/sfmt/
|
||||||
|
exclude = ^libcockatrice_utility/libcockatrice/utility/peglib\.h$
|
||||||
|
exclude = ^oracle/src/lzma/
|
||||||
|
exclude = ^oracle/src/qt-json/
|
||||||
|
exclude = ^oracle/src/zip/
|
||||||
|
exclude = ^servatrice/src/smtp/
|
||||||
|
|
||||||
|
# Qt's own moc/pch/uic output is generated into the build tree, not the source tree, so the
|
||||||
|
# filters above already keep it out. These cover CLion's directories, which sit in-tree and are
|
||||||
|
# picked up by the object-directory walk.
|
||||||
|
exclude = .*cmake-build-.*
|
||||||
|
exclude = .*\.cache/
|
||||||
|
|
||||||
|
# Process entry points and the client's window chrome. Each of these needs a display, an
|
||||||
|
# interactive terminal, or a live database before it does anything a test could observe, so no
|
||||||
|
# headless run will ever move them. Left in, they contribute thousands of lines at 0% and drag
|
||||||
|
# the total down by a fixed amount that hides movement in the code we can actually test. The
|
||||||
|
# window header is excluded with its .cpp because gcovr attributes its inline template code
|
||||||
|
# there, which is where most of its line count lives.
|
||||||
|
exclude = ^cockatrice/src/main\.cpp$
|
||||||
|
exclude = ^cockatrice/src/interface/window_main\.
|
||||||
|
exclude = ^oracle/src/main\.cpp$
|
||||||
|
exclude = ^servatrice/src/main\.cpp$
|
||||||
|
|
||||||
|
exclude-throw-branches = yes
|
||||||
|
exclude-function-lines = yes
|
||||||
|
|
||||||
|
print-summary = yes
|
||||||
|
|
@ -60,3 +60,5 @@ target_link_libraries(
|
||||||
PUBLIC libcockatrice_utility
|
PUBLIC libcockatrice_utility
|
||||||
PUBLIC ${QT_CORE_MODULE}
|
PUBLIC ${QT_CORE_MODULE}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
target_precompile_headers(libcockatrice_card PRIVATE "${CMAKE_SOURCE_DIR}/cmake/pch/qtcore_pch.h")
|
||||||
|
|
|
||||||
|
|
@ -41,3 +41,5 @@ target_link_libraries(
|
||||||
libcockatrice_deck_list PUBLIC libcockatrice_interfaces libcockatrice_protocol libcockatrice_utility
|
libcockatrice_deck_list PUBLIC libcockatrice_interfaces libcockatrice_protocol libcockatrice_utility
|
||||||
${QT_CORE_MODULE}
|
${QT_CORE_MODULE}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
target_precompile_headers(libcockatrice_deck_list PRIVATE "${CMAKE_SOURCE_DIR}/cmake/pch/qtcore_pch.h")
|
||||||
|
|
|
||||||
|
|
@ -18,3 +18,5 @@ add_dependencies(libcockatrice_filters libcockatrice_card)
|
||||||
target_include_directories(libcockatrice_filters PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
|
target_include_directories(libcockatrice_filters PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
|
||||||
|
|
||||||
target_link_libraries(libcockatrice_filters PUBLIC libcockatrice_card ${QT_CORE_MODULE})
|
target_link_libraries(libcockatrice_filters PUBLIC libcockatrice_card ${QT_CORE_MODULE})
|
||||||
|
|
||||||
|
target_precompile_headers(libcockatrice_filters PRIVATE "${CMAKE_SOURCE_DIR}/cmake/pch/qtcore_pch.h")
|
||||||
|
|
|
||||||
|
|
@ -31,3 +31,5 @@ add_library(libcockatrice_interfaces STATIC ${MOC_SOURCES})
|
||||||
target_include_directories(libcockatrice_interfaces PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
|
target_include_directories(libcockatrice_interfaces PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
|
||||||
|
|
||||||
target_link_libraries(libcockatrice_interfaces PUBLIC libcockatrice_utility ${QT_CORE_MODULE})
|
target_link_libraries(libcockatrice_interfaces PUBLIC libcockatrice_utility ${QT_CORE_MODULE})
|
||||||
|
|
||||||
|
target_precompile_headers(libcockatrice_interfaces PRIVATE "${CMAKE_SOURCE_DIR}/cmake/pch/qtcore_pch.h")
|
||||||
|
|
|
||||||
|
|
@ -29,3 +29,5 @@ add_library(
|
||||||
target_include_directories(libcockatrice_models_database PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
|
target_include_directories(libcockatrice_models_database PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
|
||||||
|
|
||||||
target_link_libraries(libcockatrice_models_database PUBLIC libcockatrice_card libcockatrice_filters ${QT_CORE_MODULE})
|
target_link_libraries(libcockatrice_models_database PUBLIC libcockatrice_card libcockatrice_filters ${QT_CORE_MODULE})
|
||||||
|
|
||||||
|
target_precompile_headers(libcockatrice_models_database PRIVATE "${CMAKE_SOURCE_DIR}/cmake/pch/qtcore_pch.h")
|
||||||
|
|
|
||||||
|
|
@ -16,3 +16,5 @@ target_include_directories(libcockatrice_models_deck_list PUBLIC ${CMAKE_CURRENT
|
||||||
target_link_libraries(
|
target_link_libraries(
|
||||||
libcockatrice_models_deck_list PUBLIC libcockatrice_card libcockatrice_deck_list ${QT_CORE_MODULE}
|
libcockatrice_models_deck_list PUBLIC libcockatrice_card libcockatrice_deck_list ${QT_CORE_MODULE}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
target_precompile_headers(libcockatrice_models_deck_list PRIVATE "${CMAKE_SOURCE_DIR}/cmake/pch/qtcore_pch.h")
|
||||||
|
|
|
||||||
|
|
@ -19,3 +19,5 @@ target_link_libraries(
|
||||||
PUBLIC ${COCKATRICE_QT_VERSION_NAME}::Network ${COCKATRICE_QT_VERSION_NAME}::WebSockets libcockatrice_protocol
|
PUBLIC ${COCKATRICE_QT_VERSION_NAME}::Network ${COCKATRICE_QT_VERSION_NAME}::WebSockets libcockatrice_protocol
|
||||||
libcockatrice_network_server_remote
|
libcockatrice_network_server_remote
|
||||||
)
|
)
|
||||||
|
|
||||||
|
target_precompile_headers(libcockatrice_network_client_abstract PRIVATE "${CMAKE_SOURCE_DIR}/cmake/pch/qtcore_pch.h")
|
||||||
|
|
|
||||||
|
|
@ -19,3 +19,5 @@ target_link_libraries(
|
||||||
PUBLIC ${COCKATRICE_QT_VERSION_NAME}::Network ${COCKATRICE_QT_VERSION_NAME}::WebSockets
|
PUBLIC ${COCKATRICE_QT_VERSION_NAME}::Network ${COCKATRICE_QT_VERSION_NAME}::WebSockets
|
||||||
libcockatrice_network_client_abstract
|
libcockatrice_network_client_abstract
|
||||||
)
|
)
|
||||||
|
|
||||||
|
target_precompile_headers(libcockatrice_network_client_local PRIVATE "${CMAKE_SOURCE_DIR}/cmake/pch/qtcore_pch.h")
|
||||||
|
|
|
||||||
|
|
@ -19,3 +19,5 @@ target_link_libraries(
|
||||||
PUBLIC ${COCKATRICE_QT_VERSION_NAME}::Network ${COCKATRICE_QT_VERSION_NAME}::WebSockets
|
PUBLIC ${COCKATRICE_QT_VERSION_NAME}::Network ${COCKATRICE_QT_VERSION_NAME}::WebSockets
|
||||||
libcockatrice_network_client_abstract libcockatrice_interfaces libcockatrice_utility libcockatrice_protocol
|
libcockatrice_network_client_abstract libcockatrice_interfaces libcockatrice_utility libcockatrice_protocol
|
||||||
)
|
)
|
||||||
|
|
||||||
|
target_precompile_headers(libcockatrice_network_client_remote PRIVATE "${CMAKE_SOURCE_DIR}/cmake/pch/qtcore_pch.h")
|
||||||
|
|
|
||||||
|
|
@ -15,3 +15,5 @@ add_dependencies(libcockatrice_network_server_local libcockatrice_protocol)
|
||||||
target_include_directories(libcockatrice_network_server_local PUBLIC .)
|
target_include_directories(libcockatrice_network_server_local PUBLIC .)
|
||||||
|
|
||||||
target_link_libraries(libcockatrice_network_server_local PUBLIC ${COCKATRICE_QT_MODULES} libcockatrice_protocol)
|
target_link_libraries(libcockatrice_network_server_local PUBLIC ${COCKATRICE_QT_MODULES} libcockatrice_protocol)
|
||||||
|
|
||||||
|
target_precompile_headers(libcockatrice_network_server_local PRIVATE "${CMAKE_SOURCE_DIR}/cmake/pch/qtcore_pch.h")
|
||||||
|
|
|
||||||
|
|
@ -62,3 +62,5 @@ target_link_libraries(
|
||||||
libcockatrice_network_server_remote PUBLIC libcockatrice_protocol libcockatrice_utility libcockatrice_rng
|
libcockatrice_network_server_remote PUBLIC libcockatrice_protocol libcockatrice_utility libcockatrice_rng
|
||||||
libcockatrice_deck_list ${COCKATRICE_QT_MODULES}
|
libcockatrice_deck_list ${COCKATRICE_QT_MODULES}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
target_precompile_headers(libcockatrice_network_server_remote PRIVATE "${CMAKE_SOURCE_DIR}/cmake/pch/qtcore_pch.h")
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,10 @@ public:
|
||||||
{
|
{
|
||||||
return deck;
|
return deck;
|
||||||
}
|
}
|
||||||
|
bool getSideboardLocked() const
|
||||||
|
{
|
||||||
|
return sideboardLocked;
|
||||||
|
}
|
||||||
bool getReadyStart() const
|
bool getReadyStart() const
|
||||||
{
|
{
|
||||||
return readyStart;
|
return readyStart;
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,8 @@
|
||||||
#include <QTimer>
|
#include <QTimer>
|
||||||
#include <google/protobuf/descriptor.h>
|
#include <google/protobuf/descriptor.h>
|
||||||
#include <google/protobuf/message.h>
|
#include <google/protobuf/message.h>
|
||||||
|
#include <libcockatrice/deck_list/deck_list.h>
|
||||||
|
#include <libcockatrice/protocol/pb/context_deck_select.pb.h>
|
||||||
#include <libcockatrice/protocol/pb/context_ping_changed.pb.h>
|
#include <libcockatrice/protocol/pb/context_ping_changed.pb.h>
|
||||||
#include <libcockatrice/protocol/pb/event_delete_arrow.pb.h>
|
#include <libcockatrice/protocol/pb/event_delete_arrow.pb.h>
|
||||||
#include <libcockatrice/protocol/pb/event_game_closed.pb.h>
|
#include <libcockatrice/protocol/pb/event_game_closed.pb.h>
|
||||||
|
|
@ -825,6 +827,48 @@ void Server_Game::createGameJoinedEvent(Server_AbstractParticipant *joiningParti
|
||||||
}
|
}
|
||||||
|
|
||||||
rc.enqueuePostResponseItem(ServerMessage::GAME_EVENT_CONTAINER, prepareGameEvent(event2, -1));
|
rc.enqueuePostResponseItem(ServerMessage::GAME_EVENT_CONTAINER, prepareGameEvent(event2, -1));
|
||||||
|
|
||||||
|
// A newly joined player never saw the "Player X has loaded a deck" messages
|
||||||
|
// (they are only broadcast when a deck is selected), so replay a deck-select
|
||||||
|
// context for every player that already has a deck loaded. It is delivered
|
||||||
|
// privately to the joining participant, whose client renders it as a log
|
||||||
|
// entry in the same way as a fresh deck selection. Skipped on reconnect
|
||||||
|
// (resuming) — the player already saw these — and once the game has started,
|
||||||
|
// when the message is no longer useful and would only spam the log.
|
||||||
|
if (!resuming && !gameStarted) {
|
||||||
|
for (auto *participant : participants.values()) {
|
||||||
|
if (participant == joiningParticipant) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
auto *deckOwner = qobject_cast<Server_Player *>(participant);
|
||||||
|
if (deckOwner == nullptr) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const DeckList *deckList = deckOwner->getDeckList();
|
||||||
|
if (deckList == nullptr) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Event_PlayerPropertiesChanged event;
|
||||||
|
event.mutable_player_properties()->set_sideboard_locked(deckOwner->getSideboardLocked());
|
||||||
|
event.mutable_player_properties()->set_deck_hash(deckList->getDeckHash().toStdString());
|
||||||
|
|
||||||
|
Context_DeckSelect deckSelect;
|
||||||
|
deckSelect.set_deck_hash(deckList->getDeckHash().toStdString());
|
||||||
|
deckSelect.set_sideboard_size(deckList->getSideboardSize());
|
||||||
|
if (getShareDecklistsOnLoad()) {
|
||||||
|
deckSelect.set_deck_list(deckList->writeToString_Native().toStdString());
|
||||||
|
}
|
||||||
|
|
||||||
|
GameEventContext context;
|
||||||
|
context.GetReflection()
|
||||||
|
->MutableMessage(&context, deckSelect.GetDescriptor()->FindExtensionByName("ext"))
|
||||||
|
->CopyFrom(deckSelect);
|
||||||
|
|
||||||
|
rc.enqueuePostResponseItem(ServerMessage::GAME_EVENT_CONTAINER,
|
||||||
|
prepareGameEvent(event, deckOwner->getPlayerId(), &context));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Server_Game::sendGameEventContainer(GameEventContainer *cont,
|
void Server_Game::sendGameEventContainer(GameEventContainer *cont,
|
||||||
|
|
|
||||||
|
|
@ -24,3 +24,5 @@ target_include_directories(
|
||||||
libcockatrice_protocol PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}
|
libcockatrice_protocol PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}
|
||||||
${CMAKE_CURRENT_BINARY_DIR} # points to the generated headers
|
${CMAKE_CURRENT_BINARY_DIR} # points to the generated headers
|
||||||
)
|
)
|
||||||
|
|
||||||
|
target_precompile_headers(libcockatrice_protocol PRIVATE "${CMAKE_SOURCE_DIR}/cmake/pch/qtcore_pch.h")
|
||||||
|
|
|
||||||
|
|
@ -55,6 +55,7 @@ set(PROTO_FILES
|
||||||
command_report_details.proto
|
command_report_details.proto
|
||||||
command_report_list.proto
|
command_report_list.proto
|
||||||
command_report_my_list.proto
|
command_report_my_list.proto
|
||||||
|
command_report_reopen.proto
|
||||||
command_report_resolve.proto
|
command_report_resolve.proto
|
||||||
command_report_stats.proto
|
command_report_stats.proto
|
||||||
command_report_user_info.proto
|
command_report_user_info.proto
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
syntax = "proto2";
|
||||||
|
import "moderator_commands.proto";
|
||||||
|
|
||||||
|
message Command_ReportReopen {
|
||||||
|
extend ModeratorCommand {
|
||||||
|
optional Command_ReportReopen ext = 1206;
|
||||||
|
}
|
||||||
|
required int32 report_id = 1;
|
||||||
|
optional string reopen_note = 2;
|
||||||
|
}
|
||||||
|
|
@ -26,6 +26,7 @@ message ModeratorCommand {
|
||||||
REPLAY_DOWNLOAD_BY_GAME_ID = 1203;
|
REPLAY_DOWNLOAD_BY_GAME_ID = 1203;
|
||||||
REPORT_USER_INFO = 1204;
|
REPORT_USER_INFO = 1204;
|
||||||
REPORT_STATS = 1205;
|
REPORT_STATS = 1205;
|
||||||
|
REPORT_REOPEN = 1206;
|
||||||
}
|
}
|
||||||
extensions 100 to max;
|
extensions 100 to max;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -33,4 +33,7 @@ message ServerInfo_Report {
|
||||||
optional string chat_log = 14;
|
optional string chat_log = 14;
|
||||||
|
|
||||||
optional string resolution_note = 15;
|
optional string resolution_note = 15;
|
||||||
|
optional string resolved_by_name = 16;
|
||||||
|
|
||||||
|
optional int32 reported_user_level = 17;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,10 @@ add_library(
|
||||||
libcockatrice/rng/sfmt/SFMT.c
|
libcockatrice/rng/sfmt/SFMT.c
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# SFMT is vendored and its own headers trip UBSan's alignment and vptr checks.
|
||||||
|
cockatrice_disable_sanitizers_for(libcockatrice/rng/sfmt/SFMT.c)
|
||||||
|
cockatrice_disable_coverage_for(libcockatrice/rng/sfmt/SFMT.c)
|
||||||
|
|
||||||
target_include_directories(libcockatrice_rng PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
|
target_include_directories(libcockatrice_rng PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
|
||||||
|
|
||||||
target_link_libraries(libcockatrice_rng PUBLIC ${QT_CORE_MODULE})
|
target_link_libraries(libcockatrice_rng PUBLIC ${QT_CORE_MODULE})
|
||||||
|
|
|
||||||
|
|
@ -71,3 +71,5 @@ target_include_directories(
|
||||||
)
|
)
|
||||||
|
|
||||||
target_link_libraries(libcockatrice_settings PUBLIC libcockatrice_card libcockatrice_utility ${QT_CORE_MODULE})
|
target_link_libraries(libcockatrice_settings PUBLIC libcockatrice_card libcockatrice_utility ${QT_CORE_MODULE})
|
||||||
|
|
||||||
|
target_precompile_headers(libcockatrice_settings PRIVATE "${CMAKE_SOURCE_DIR}/cmake/pch/qtcore_pch.h")
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@
|
||||||
|
|
||||||
#include "libcockatrice/settings/settings_manager.h"
|
#include "libcockatrice/settings/settings_manager.h"
|
||||||
|
|
||||||
|
#include <QDir>
|
||||||
|
#include <QStringList>
|
||||||
#include <QVariant>
|
#include <QVariant>
|
||||||
|
|
||||||
class QObject;
|
class QObject;
|
||||||
|
|
@ -121,3 +123,52 @@ void PathsSettings::setSpoilerDatabasePath(const QString &_spoilerDatabasePath)
|
||||||
setValue(_spoilerDatabasePath, "spoilerDatabase");
|
setValue(_spoilerDatabasePath, "spoilerDatabase");
|
||||||
emit cardDatabasePathChanged();
|
emit cardDatabasePathChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QStringList PathsSettings::pathsInsideDir(const QString &baseDir) const
|
||||||
|
{
|
||||||
|
QStringList offenders;
|
||||||
|
const auto check = [&offenders, &baseDir](const QString &path) {
|
||||||
|
if (!path.isEmpty() && isInsideDir(path, baseDir)) {
|
||||||
|
offenders.append(QDir::cleanPath(path));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
check(getDeckPath());
|
||||||
|
check(getFiltersPath());
|
||||||
|
check(getReplaysPath());
|
||||||
|
check(getPicsPath());
|
||||||
|
check(getCustomPicsPath());
|
||||||
|
check(getThemesPath());
|
||||||
|
check(getCardDatabasePath());
|
||||||
|
check(getCustomCardDatabasePath());
|
||||||
|
check(getTokenDatabasePath());
|
||||||
|
check(getSpoilerCardDatabasePath());
|
||||||
|
check(getRedirectCachePath());
|
||||||
|
|
||||||
|
offenders.removeDuplicates();
|
||||||
|
return offenders;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PathsSettings::isInsideDir(const QString &path, const QString &baseDir)
|
||||||
|
{
|
||||||
|
#if defined(Q_OS_WIN) || defined(Q_OS_MACOS)
|
||||||
|
// Windows and default macOS volumes are case-insensitive filesystems.
|
||||||
|
constexpr Qt::CaseSensitivity caseSensitivity = Qt::CaseInsensitive;
|
||||||
|
#else
|
||||||
|
constexpr Qt::CaseSensitivity caseSensitivity = Qt::CaseSensitive;
|
||||||
|
#endif
|
||||||
|
const QString base = QDir::cleanPath(baseDir);
|
||||||
|
const QString cleaned = QDir::cleanPath(path);
|
||||||
|
const QString prefix = base.endsWith(QLatin1Char('/')) ? base : base + QLatin1Char('/');
|
||||||
|
return cleaned == base || cleaned.startsWith(prefix, caseSensitivity);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PathsSettings::getAppDirWarningAcknowledged() const
|
||||||
|
{
|
||||||
|
return getValue("appDirWarningAcknowledged").toBool();
|
||||||
|
}
|
||||||
|
|
||||||
|
void PathsSettings::setAppDirWarningAcknowledged(bool _appDirWarningAcknowledged)
|
||||||
|
{
|
||||||
|
setValue(_appDirWarningAcknowledged, "appDirWarningAcknowledged");
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
#include "settings_manager.h"
|
#include "settings_manager.h"
|
||||||
|
|
||||||
#include <QString>
|
#include <QString>
|
||||||
|
#include <QStringList>
|
||||||
#include <libcockatrice/interfaces/interface_paths_settings_provider.h>
|
#include <libcockatrice/interfaces/interface_paths_settings_provider.h>
|
||||||
#include <qtmetamacros.h>
|
#include <qtmetamacros.h>
|
||||||
|
|
||||||
|
|
@ -27,6 +28,24 @@ public:
|
||||||
[[nodiscard]] QString getSpoilerCardDatabasePath() const override;
|
[[nodiscard]] QString getSpoilerCardDatabasePath() const override;
|
||||||
[[nodiscard]] QString getRedirectCachePath() const override;
|
[[nodiscard]] QString getRedirectCachePath() const override;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Returns the configured paths that resolve inside baseDir.
|
||||||
|
*
|
||||||
|
* Comparison is lexical (QDir::cleanPath); symlinks are not resolved, so a
|
||||||
|
* path that only reaches baseDir through a symlink is not reported.
|
||||||
|
*/
|
||||||
|
[[nodiscard]] QStringList pathsInsideDir(const QString &baseDir) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Whether path resolves inside baseDir, as a single-path check.
|
||||||
|
*
|
||||||
|
* A path counts as inside when it equals baseDir or is nested below it;
|
||||||
|
* a sibling sharing the name prefix (baseDir + "2") does not.
|
||||||
|
*/
|
||||||
|
[[nodiscard]] static bool isInsideDir(const QString &path, const QString &baseDir);
|
||||||
|
|
||||||
|
[[nodiscard]] bool getAppDirWarningAcknowledged() const;
|
||||||
|
|
||||||
void setDeckPath(const QString &_deckPath);
|
void setDeckPath(const QString &_deckPath);
|
||||||
void setFiltersPath(const QString &_filtersPath);
|
void setFiltersPath(const QString &_filtersPath);
|
||||||
void setReplaysPath(const QString &_replaysPath);
|
void setReplaysPath(const QString &_replaysPath);
|
||||||
|
|
@ -37,14 +56,17 @@ public:
|
||||||
void setCustomCardDatabasePath(const QString &_customCardDatabasePath);
|
void setCustomCardDatabasePath(const QString &_customCardDatabasePath);
|
||||||
void setTokenDatabasePath(const QString &_tokenDatabasePath);
|
void setTokenDatabasePath(const QString &_tokenDatabasePath);
|
||||||
void setSpoilerDatabasePath(const QString &_spoilerDatabasePath);
|
void setSpoilerDatabasePath(const QString &_spoilerDatabasePath);
|
||||||
|
void setAppDirWarningAcknowledged(bool _appDirWarningAcknowledged);
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void cardDatabasePathChanged();
|
void cardDatabasePathChanged();
|
||||||
void picsPathChanged();
|
void picsPathChanged();
|
||||||
void themeChanged();
|
void themeChanged();
|
||||||
|
|
||||||
private:
|
public:
|
||||||
explicit PathsSettings(const QString &settingPath, QObject *parent = nullptr);
|
explicit PathsSettings(const QString &settingPath, QObject *parent = nullptr);
|
||||||
|
|
||||||
|
private:
|
||||||
PathsSettings(const PathsSettings & /*other*/);
|
PathsSettings(const PathsSettings & /*other*/);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -49,3 +49,5 @@ target_link_libraries(libcockatrice_utility PUBLIC libcockatrice_rng OpenSSL::Cr
|
||||||
set(ORACLE_LIBS)
|
set(ORACLE_LIBS)
|
||||||
|
|
||||||
include_directories(${${COCKATRICE_QT_VERSION_NAME}Core_INCLUDE_DIRS})
|
include_directories(${${COCKATRICE_QT_VERSION_NAME}Core_INCLUDE_DIRS})
|
||||||
|
|
||||||
|
target_precompile_headers(libcockatrice_utility PRIVATE "${CMAKE_SOURCE_DIR}/cmake/pch/qtcore_pch.h")
|
||||||
|
|
|
||||||
|
|
@ -79,6 +79,9 @@ if(ZLIB_FOUND)
|
||||||
include_directories(${ZLIB_INCLUDE_DIRS})
|
include_directories(${ZLIB_INCLUDE_DIRS})
|
||||||
add_definitions("-DHAS_ZLIB")
|
add_definitions("-DHAS_ZLIB")
|
||||||
list(APPEND oracle_SOURCES src/zip/unzip.cpp src/zip/zipglobal.cpp)
|
list(APPEND oracle_SOURCES src/zip/unzip.cpp src/zip/zipglobal.cpp)
|
||||||
|
# minizip and zlib are vendored.
|
||||||
|
cockatrice_disable_sanitizers_for(src/zip/unzip.cpp src/zip/zipglobal.cpp)
|
||||||
|
cockatrice_disable_coverage_for(src/zip/unzip.cpp src/zip/zipglobal.cpp)
|
||||||
else()
|
else()
|
||||||
message(STATUS "Oracle: zlib not found; ZIP support disabled")
|
message(STATUS "Oracle: zlib not found; ZIP support disabled")
|
||||||
endif()
|
endif()
|
||||||
|
|
@ -89,6 +92,9 @@ if(LIBLZMA_FOUND)
|
||||||
include_directories(${LIBLZMA_INCLUDE_DIRS})
|
include_directories(${LIBLZMA_INCLUDE_DIRS})
|
||||||
add_definitions("-DHAS_LZMA")
|
add_definitions("-DHAS_LZMA")
|
||||||
list(APPEND oracle_SOURCES src/lzma/decompress.cpp)
|
list(APPEND oracle_SOURCES src/lzma/decompress.cpp)
|
||||||
|
# The LZMA SDK is vendored.
|
||||||
|
cockatrice_disable_sanitizers_for(src/lzma/decompress.cpp)
|
||||||
|
cockatrice_disable_coverage_for(src/lzma/decompress.cpp)
|
||||||
else()
|
else()
|
||||||
message(STATUS "Oracle: LibLZMA not found; xz support disabled")
|
message(STATUS "Oracle: LibLZMA not found; xz support disabled")
|
||||||
endif()
|
endif()
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,14 @@ set(servatrice_SOURCES
|
||||||
|
|
||||||
set(servatrice_RESOURCES servatrice.qrc)
|
set(servatrice_RESOURCES servatrice.qrc)
|
||||||
|
|
||||||
|
# Qxt SMTP is vendored.
|
||||||
|
cockatrice_disable_sanitizers_for(
|
||||||
|
src/smtp/qxthmac.cpp src/smtp/qxtmailattachment.cpp src/smtp/qxtmailmessage.cpp src/smtp/qxtsmtp.cpp
|
||||||
|
)
|
||||||
|
cockatrice_disable_coverage_for(
|
||||||
|
src/smtp/qxthmac.cpp src/smtp/qxtmailattachment.cpp src/smtp/qxtmailmessage.cpp src/smtp/qxtsmtp.cpp
|
||||||
|
)
|
||||||
|
|
||||||
if(WIN32)
|
if(WIN32)
|
||||||
set(servatrice_SOURCES ${servatrice_SOURCES} servatrice.rc)
|
set(servatrice_SOURCES ${servatrice_SOURCES} servatrice.rc)
|
||||||
endif(WIN32)
|
endif(WIN32)
|
||||||
|
|
|
||||||
|
|
@ -110,6 +110,7 @@
|
||||||
#include <libcockatrice/protocol/pb/command_report_details.pb.h>
|
#include <libcockatrice/protocol/pb/command_report_details.pb.h>
|
||||||
#include <libcockatrice/protocol/pb/command_report_list.pb.h>
|
#include <libcockatrice/protocol/pb/command_report_list.pb.h>
|
||||||
#include <libcockatrice/protocol/pb/command_report_my_list.pb.h>
|
#include <libcockatrice/protocol/pb/command_report_my_list.pb.h>
|
||||||
|
#include <libcockatrice/protocol/pb/command_report_reopen.pb.h>
|
||||||
#include <libcockatrice/protocol/pb/command_report_resolve.pb.h>
|
#include <libcockatrice/protocol/pb/command_report_resolve.pb.h>
|
||||||
#include <libcockatrice/protocol/pb/command_report_stats.pb.h>
|
#include <libcockatrice/protocol/pb/command_report_stats.pb.h>
|
||||||
#include <libcockatrice/protocol/pb/command_report_user_info.pb.h>
|
#include <libcockatrice/protocol/pb/command_report_user_info.pb.h>
|
||||||
|
|
@ -434,6 +435,8 @@ Response::ResponseCode AbstractServerSocketInterface::processExtendedModeratorCo
|
||||||
return cmdReportAssign(cmd.GetExtension(Command_ReportAssign::ext), rc);
|
return cmdReportAssign(cmd.GetExtension(Command_ReportAssign::ext), rc);
|
||||||
case ModeratorCommand::REPORT_RESOLVE:
|
case ModeratorCommand::REPORT_RESOLVE:
|
||||||
return cmdReportResolve(cmd.GetExtension(Command_ReportResolve::ext), rc);
|
return cmdReportResolve(cmd.GetExtension(Command_ReportResolve::ext), rc);
|
||||||
|
case ModeratorCommand::REPORT_REOPEN:
|
||||||
|
return cmdReportReopen(cmd.GetExtension(Command_ReportReopen::ext), rc);
|
||||||
case ModeratorCommand::VIEWLOG_HISTORY:
|
case ModeratorCommand::VIEWLOG_HISTORY:
|
||||||
return cmdGetLogHistory(cmd.GetExtension(Command_ViewLogHistory::ext), rc, true);
|
return cmdGetLogHistory(cmd.GetExtension(Command_ViewLogHistory::ext), rc, true);
|
||||||
case ModeratorCommand::GRANT_REPLAY_ACCESS:
|
case ModeratorCommand::GRANT_REPLAY_ACCESS:
|
||||||
|
|
@ -1983,6 +1986,29 @@ Response::ResponseCode AbstractServerSocketInterface::cmdBanFromServer(const Com
|
||||||
return Response::RespOk;
|
return Response::RespOk;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
int reportUserLevel(bool userExists, int adminFlags)
|
||||||
|
{
|
||||||
|
int userLevel = ServerInfo_User::IsUser;
|
||||||
|
if (userExists) {
|
||||||
|
userLevel |= ServerInfo_User::IsRegistered;
|
||||||
|
if (adminFlags & 1) {
|
||||||
|
userLevel |= ServerInfo_User::IsAdmin | ServerInfo_User::IsModerator;
|
||||||
|
} else if (adminFlags & 2) {
|
||||||
|
userLevel |= ServerInfo_User::IsModerator;
|
||||||
|
}
|
||||||
|
if (adminFlags & 4) {
|
||||||
|
userLevel |= ServerInfo_User::IsJudge;
|
||||||
|
}
|
||||||
|
if (adminFlags & 8) {
|
||||||
|
userLevel |= ServerInfo_User::IsDeveloper;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return userLevel;
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
Response::ResponseCode AbstractServerSocketInterface::cmdReportList(const Command_ReportList &cmd,
|
Response::ResponseCode AbstractServerSocketInterface::cmdReportList(const Command_ReportList &cmd,
|
||||||
ResponseContainer &rc)
|
ResponseContainer &rc)
|
||||||
{
|
{
|
||||||
|
|
@ -1996,8 +2022,9 @@ Response::ResponseCode AbstractServerSocketInterface::cmdReportList(const Comman
|
||||||
|
|
||||||
// Columns: 0=id, 1=reporter_name, 2=reported_user_name, 3=game_id,
|
// Columns: 0=id, 1=reporter_name, 2=reported_user_name, 3=game_id,
|
||||||
// 4=category, 5=description, 6=created_at, 7=status,
|
// 4=category, 5=description, 6=created_at, 7=status,
|
||||||
// 8=resolution_note, 9=assigned_mod_name,
|
// 8=resolution_note, 9=assigned_mod_name, 10=resolved_by_name,
|
||||||
// 10=room_id, 11=replay_id
|
// 11=room_id, 12=replay_id,
|
||||||
|
// 13=reported_user_id, 14=reported_user_admin
|
||||||
QString whereClause;
|
QString whereClause;
|
||||||
if (unresolvedOnly) {
|
if (unresolvedOnly) {
|
||||||
whereClause = "WHERE r.status = 'open' OR r.status = 'assigned' ";
|
whereClause = "WHERE r.status = 'open' OR r.status = 'assigned' ";
|
||||||
|
|
@ -2012,11 +2039,13 @@ Response::ResponseCode AbstractServerSocketInterface::cmdReportList(const Comman
|
||||||
|
|
||||||
QString queryStr = "SELECT r.id, r.reporter_name, r.reported_user_name, r.game_id, "
|
QString queryStr = "SELECT r.id, r.reporter_name, r.reported_user_name, r.game_id, "
|
||||||
"r.category, r.description, r.created_at, r.status, "
|
"r.category, r.description, r.created_at, r.status, "
|
||||||
"r.resolution_note, u.name AS assigned_mod_name, r.room_id, "
|
"r.resolution_note, u.name AS assigned_mod_name, ru.name AS resolved_by_name, r.room_id, "
|
||||||
"(SELECT id FROM {prefix}_replays WHERE id_game = r.game_id ORDER BY id DESC LIMIT 1) "
|
"(SELECT id FROM {prefix}_replays WHERE id_game = r.game_id ORDER BY id DESC LIMIT 1) "
|
||||||
"AS replay_id "
|
"AS replay_id, u2.id AS reported_user_id, u2.admin AS reported_user_admin "
|
||||||
"FROM {prefix}_reports r "
|
"FROM {prefix}_reports r "
|
||||||
"LEFT JOIN {prefix}_users u ON r.assigned_to = u.id ";
|
"LEFT JOIN {prefix}_users u ON r.assigned_to = u.id "
|
||||||
|
"LEFT JOIN {prefix}_users ru ON r.resolved_by = ru.id "
|
||||||
|
"LEFT JOIN {prefix}_users u2 ON u2.name = r.reported_user_name ";
|
||||||
|
|
||||||
if (unresolvedOnly) {
|
if (unresolvedOnly) {
|
||||||
queryStr += "WHERE r.status = 'open' OR r.status = 'assigned' ";
|
queryStr += "WHERE r.status = 'open' OR r.status = 'assigned' ";
|
||||||
|
|
@ -2057,14 +2086,19 @@ Response::ResponseCode AbstractServerSocketInterface::cmdReportList(const Comman
|
||||||
if (!query->value(9).isNull()) {
|
if (!query->value(9).isNull()) {
|
||||||
info->set_assigned_mod_name(query->value(9).toString().toStdString());
|
info->set_assigned_mod_name(query->value(9).toString().toStdString());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!query->value(10).isNull()) {
|
if (!query->value(10).isNull()) {
|
||||||
info->set_room_id(query->value(10).toInt());
|
info->set_resolved_by_name(query->value(10).toString().toStdString());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!query->value(11).isNull()) {
|
if (!query->value(11).isNull()) {
|
||||||
info->set_replay_id(query->value(11).toInt());
|
info->set_room_id(query->value(11).toInt());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!query->value(12).isNull()) {
|
||||||
|
info->set_replay_id(query->value(12).toInt());
|
||||||
|
}
|
||||||
|
|
||||||
|
info->set_reported_user_level(reportUserLevel(!query->value(13).isNull(), query->value(14).toInt()));
|
||||||
}
|
}
|
||||||
|
|
||||||
rc.setResponseExtension(re);
|
rc.setResponseExtension(re);
|
||||||
|
|
@ -2737,11 +2771,13 @@ Response::ResponseCode AbstractServerSocketInterface::cmdReportMyList(const Comm
|
||||||
|
|
||||||
QString queryStr = "SELECT r.id, r.reporter_name, r.reported_user_name, r.game_id, "
|
QString queryStr = "SELECT r.id, r.reporter_name, r.reported_user_name, r.game_id, "
|
||||||
"r.category, r.description, r.created_at, r.status, "
|
"r.category, r.description, r.created_at, r.status, "
|
||||||
"r.resolution_note, u.name AS assigned_mod_name, r.room_id, "
|
"r.resolution_note, u.name AS assigned_mod_name, ru.name AS resolved_by_name, r.room_id, "
|
||||||
"(SELECT id FROM {prefix}_replays WHERE id_game = r.game_id ORDER BY id DESC LIMIT 1) "
|
"(SELECT id FROM {prefix}_replays WHERE id_game = r.game_id ORDER BY id DESC LIMIT 1) "
|
||||||
"AS replay_id "
|
"AS replay_id, u2.id AS reported_user_id, u2.admin AS reported_user_admin "
|
||||||
"FROM {prefix}_reports r "
|
"FROM {prefix}_reports r "
|
||||||
"LEFT JOIN {prefix}_users u ON r.assigned_to = u.id "
|
"LEFT JOIN {prefix}_users u ON r.assigned_to = u.id "
|
||||||
|
"LEFT JOIN {prefix}_users ru ON r.resolved_by = ru.id "
|
||||||
|
"LEFT JOIN {prefix}_users u2 ON u2.name = r.reported_user_name "
|
||||||
"WHERE r.reporter_id = :reporter_id "
|
"WHERE r.reporter_id = :reporter_id "
|
||||||
"ORDER BY r.created_at DESC LIMIT 200";
|
"ORDER BY r.created_at DESC LIMIT 200";
|
||||||
|
|
||||||
|
|
@ -2777,20 +2813,61 @@ Response::ResponseCode AbstractServerSocketInterface::cmdReportMyList(const Comm
|
||||||
if (!query->value(9).isNull()) {
|
if (!query->value(9).isNull()) {
|
||||||
info->set_assigned_mod_name(query->value(9).toString().toStdString());
|
info->set_assigned_mod_name(query->value(9).toString().toStdString());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!query->value(10).isNull()) {
|
if (!query->value(10).isNull()) {
|
||||||
info->set_room_id(query->value(10).toInt());
|
info->set_resolved_by_name(query->value(10).toString().toStdString());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!query->value(11).isNull()) {
|
if (!query->value(11).isNull()) {
|
||||||
info->set_replay_id(query->value(11).toInt());
|
info->set_room_id(query->value(11).toInt());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!query->value(12).isNull()) {
|
||||||
|
info->set_replay_id(query->value(12).toInt());
|
||||||
|
}
|
||||||
|
|
||||||
|
info->set_reported_user_level(reportUserLevel(!query->value(13).isNull(), query->value(14).toInt()));
|
||||||
}
|
}
|
||||||
|
|
||||||
rc.setResponseExtension(re);
|
rc.setResponseExtension(re);
|
||||||
return Response::RespOk;
|
return Response::RespOk;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Response::ResponseCode AbstractServerSocketInterface::cmdReportReopen(const Command_ReportReopen &cmd,
|
||||||
|
ResponseContainer & /*rc*/)
|
||||||
|
{
|
||||||
|
if (!sqlInterface->checkSql()) {
|
||||||
|
return Response::RespInternalError;
|
||||||
|
}
|
||||||
|
|
||||||
|
int reportId = cmd.report_id();
|
||||||
|
QString note = textFromStdString(cmd.reopen_note());
|
||||||
|
|
||||||
|
QSqlQuery *query = sqlInterface->prepareQuery("UPDATE {prefix}_reports "
|
||||||
|
"SET status = 'open', resolution_note = NULL, "
|
||||||
|
"resolution_time = NULL, resolved_by = NULL, assigned_to = NULL, "
|
||||||
|
"notified = 0 "
|
||||||
|
"WHERE id = :id AND status IN ('resolved', 'dismissed')");
|
||||||
|
|
||||||
|
query->bindValue(":id", reportId);
|
||||||
|
|
||||||
|
if (!sqlInterface->execSqlQuery(query)) {
|
||||||
|
return Response::RespInternalError;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query->numRowsAffected() == 0) {
|
||||||
|
return Response::RespInvalidData;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString auditDetails = QString("Report #%1 reopened").arg(reportId);
|
||||||
|
if (!note.isEmpty()) {
|
||||||
|
auditDetails += QString(". Reason: %1").arg(note.left(200));
|
||||||
|
}
|
||||||
|
sqlInterface->addAuditRecord(QString::number(reportId), this->getAddress(),
|
||||||
|
QString::fromStdString(userInfo->clientid()), "REPORT_REOPENED", auditDetails, true);
|
||||||
|
|
||||||
|
return Response::RespOk;
|
||||||
|
}
|
||||||
|
|
||||||
Response::ResponseCode AbstractServerSocketInterface::cmdReportDetails(const Command_ReportDetails &cmd,
|
Response::ResponseCode AbstractServerSocketInterface::cmdReportDetails(const Command_ReportDetails &cmd,
|
||||||
ResponseContainer &rc)
|
ResponseContainer &rc)
|
||||||
{
|
{
|
||||||
|
|
@ -2806,17 +2883,19 @@ Response::ResponseCode AbstractServerSocketInterface::cmdReportDetails(const Com
|
||||||
|
|
||||||
// Columns: 0=id, 1=reporter_id, 2=reporter_name, 3=reported_user_name, 4=game_id,
|
// Columns: 0=id, 1=reporter_id, 2=reporter_name, 3=reported_user_name, 4=game_id,
|
||||||
// 5=category, 6=description, 7=created_at, 8=status,
|
// 5=category, 6=description, 7=created_at, 8=status,
|
||||||
// 9=resolution_note, 10=assigned_mod_name, 11=chat_log,
|
// 9=resolution_note, 10=assigned_mod_name, 11=resolved_by_name,
|
||||||
// 12=room_id, 13=resolution_time, 14=replay_id
|
// 12=chat_log, 13=room_id, 14=resolution_time, 15=replay_id
|
||||||
QString queryStr = "SELECT r.id, r.reporter_id, r.reporter_name, r.reported_user_name, r.game_id, "
|
QString queryStr =
|
||||||
"r.category, r.description, r.created_at, r.status, "
|
"SELECT r.id, r.reporter_id, r.reporter_name, r.reported_user_name, r.game_id, "
|
||||||
"r.resolution_note, u.name AS assigned_mod_name, r.chat_log, r.room_id, "
|
"r.category, r.description, r.created_at, r.status, "
|
||||||
"r.resolution_time, "
|
"r.resolution_note, u.name AS assigned_mod_name, ru.name AS resolved_by_name, r.chat_log, r.room_id, "
|
||||||
"(SELECT id FROM {prefix}_replays WHERE id_game = r.game_id ORDER BY id DESC LIMIT 1) "
|
"r.resolution_time, "
|
||||||
"AS replay_id "
|
"(SELECT id FROM {prefix}_replays WHERE id_game = r.game_id ORDER BY id DESC LIMIT 1) "
|
||||||
"FROM {prefix}_reports r "
|
"AS replay_id "
|
||||||
"LEFT JOIN {prefix}_users u ON r.assigned_to = u.id "
|
"FROM {prefix}_reports r "
|
||||||
"WHERE r.id = :id";
|
"LEFT JOIN {prefix}_users u ON r.assigned_to = u.id "
|
||||||
|
"LEFT JOIN {prefix}_users ru ON r.resolved_by = ru.id "
|
||||||
|
"WHERE r.id = :id";
|
||||||
QSqlQuery *query = sqlInterface->prepareQuery(queryStr);
|
QSqlQuery *query = sqlInterface->prepareQuery(queryStr);
|
||||||
query->bindValue(":id", reportId);
|
query->bindValue(":id", reportId);
|
||||||
|
|
||||||
|
|
@ -2855,21 +2934,24 @@ Response::ResponseCode AbstractServerSocketInterface::cmdReportDetails(const Com
|
||||||
if (!query->value(10).isNull()) {
|
if (!query->value(10).isNull()) {
|
||||||
info->set_assigned_mod_name(query->value(10).toString().toStdString());
|
info->set_assigned_mod_name(query->value(10).toString().toStdString());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!query->value(11).isNull()) {
|
if (!query->value(11).isNull()) {
|
||||||
info->set_chat_log(query->value(11).toString().toStdString());
|
info->set_resolved_by_name(query->value(11).toString().toStdString());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!query->value(12).isNull()) {
|
if (!query->value(12).isNull()) {
|
||||||
info->set_room_id(query->value(12).toInt());
|
info->set_chat_log(query->value(12).toString().toStdString());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!query->value(13).isNull()) {
|
if (!query->value(13).isNull()) {
|
||||||
info->set_resolution_time(query->value(13).toDateTime().toSecsSinceEpoch());
|
info->set_room_id(query->value(13).toInt());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!query->value(14).isNull()) {
|
if (!query->value(14).isNull()) {
|
||||||
info->set_replay_id(query->value(14).toInt());
|
info->set_resolution_time(query->value(14).toDateTime().toSecsSinceEpoch());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!query->value(15).isNull()) {
|
||||||
|
info->set_replay_id(query->value(15).toInt());
|
||||||
}
|
}
|
||||||
|
|
||||||
QSqlQuery *commentQuery =
|
QSqlQuery *commentQuery =
|
||||||
|
|
|
||||||
|
|
@ -96,6 +96,7 @@ class Command_ReportAssign;
|
||||||
class Command_ReportDetails;
|
class Command_ReportDetails;
|
||||||
class Command_ReportList;
|
class Command_ReportList;
|
||||||
class Command_ReportMyList;
|
class Command_ReportMyList;
|
||||||
|
class Command_ReportReopen;
|
||||||
class Command_ReportResolve;
|
class Command_ReportResolve;
|
||||||
class Command_ReportStats;
|
class Command_ReportStats;
|
||||||
class Command_ReportUserInfo;
|
class Command_ReportUserInfo;
|
||||||
|
|
@ -181,6 +182,7 @@ private:
|
||||||
Response::ResponseCode cmdUpdateServerMessage(const Command_UpdateServerMessage &cmd, ResponseContainer &rc);
|
Response::ResponseCode cmdUpdateServerMessage(const Command_UpdateServerMessage &cmd, ResponseContainer &rc);
|
||||||
Response::ResponseCode cmdReportAssign(const Command_ReportAssign &cmd, ResponseContainer &);
|
Response::ResponseCode cmdReportAssign(const Command_ReportAssign &cmd, ResponseContainer &);
|
||||||
Response::ResponseCode cmdReportResolve(const Command_ReportResolve &cmd, ResponseContainer &);
|
Response::ResponseCode cmdReportResolve(const Command_ReportResolve &cmd, ResponseContainer &);
|
||||||
|
Response::ResponseCode cmdReportReopen(const Command_ReportReopen &cmd, ResponseContainer &);
|
||||||
Response::ResponseCode cmdReportUserInfo(const Command_ReportUserInfo &cmd, ResponseContainer &rc);
|
Response::ResponseCode cmdReportUserInfo(const Command_ReportUserInfo &cmd, ResponseContainer &rc);
|
||||||
Response::ResponseCode cmdReportStats(const Command_ReportStats &cmd, ResponseContainer &rc);
|
Response::ResponseCode cmdReportStats(const Command_ReportStats &cmd, ResponseContainer &rc);
|
||||||
Response::ResponseCode cmdRegisterAccount(const Command_Register &cmd, ResponseContainer &rc);
|
Response::ResponseCode cmdRegisterAccount(const Command_Register &cmd, ResponseContainer &rc);
|
||||||
|
|
|
||||||
|
|
@ -2,109 +2,10 @@
|
||||||
# If a new test needs additional Qt modules, add them there — not in individual test CMakeLists.txt files.
|
# If a new test needs additional Qt modules, add them there — not in individual test CMakeLists.txt files.
|
||||||
enable_testing()
|
enable_testing()
|
||||||
|
|
||||||
add_test(NAME dummy_test COMMAND dummy_test)
|
include(GoogleTest)
|
||||||
add_test(NAME expression_test COMMAND expression_test)
|
|
||||||
add_test(NAME clamped_arithmetic_test COMMAND clamped_arithmetic_test)
|
|
||||||
add_test(NAME test_age_formatting COMMAND test_age_formatting)
|
|
||||||
add_test(NAME password_hash_test COMMAND password_hash_test)
|
|
||||||
add_test(NAME playmat_resolver_test COMMAND playmat_resolver_test)
|
|
||||||
add_test(NAME server_card_counter_test COMMAND server_card_counter_test)
|
|
||||||
add_test(NAME server_counter_test COMMAND server_counter_test)
|
|
||||||
add_test(NAME server_rate_limiter_test COMMAND server_rate_limiter_test)
|
|
||||||
add_test(NAME server_developer_role_test COMMAND server_developer_role_test)
|
|
||||||
add_test(NAME server_game_join_test COMMAND server_game_join_test)
|
|
||||||
add_test(NAME warning_categories_test COMMAND warning_categories_test)
|
|
||||||
# The translation loader test compiles a .qm fixture, which needs lrelease from Qt Linguist.
|
|
||||||
if(TARGET Qt6::lrelease)
|
|
||||||
add_test(NAME translation_loader_test COMMAND translation_loader_test)
|
|
||||||
endif()
|
|
||||||
add_test(NAME report_categories_test COMMAND report_categories_test)
|
|
||||||
add_test(NAME lag_monitor_test COMMAND lag_monitor_test)
|
|
||||||
add_test(NAME latency_tracker_test COMMAND latency_tracker_test)
|
|
||||||
add_test(NAME metrics_registry_test COMMAND metrics_registry_test)
|
|
||||||
add_test(NAME card_update_progress_test COMMAND card_update_progress_test)
|
|
||||||
add_test(NAME loader_local_matching_test COMMAND loader_local_matching_test)
|
|
||||||
|
|
||||||
add_test(NAME deck_hash_performance_test COMMAND deck_hash_performance_test)
|
|
||||||
set_tests_properties(deck_hash_performance_test PROPERTIES TIMEOUT 15)
|
|
||||||
|
|
||||||
# Find GTest
|
# Find GTest
|
||||||
|
|
||||||
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(playmat_resolver_test playmat_resolver_test.cpp)
|
|
||||||
add_executable(deck_hash_performance_test deck_hash_performance_test.cpp)
|
|
||||||
add_executable(server_card_counter_test server_card_counter_test.cpp)
|
|
||||||
add_executable(server_counter_test server_counter_test.cpp)
|
|
||||||
add_executable(server_rate_limiter_test server_rate_limiter_test.cpp)
|
|
||||||
add_executable(server_developer_role_test server_developer_role_test.cpp)
|
|
||||||
add_executable(server_game_join_test server_game_join_test.cpp)
|
|
||||||
add_executable(warning_categories_test warning_categories_test.cpp)
|
|
||||||
if(TARGET Qt6::lrelease)
|
|
||||||
add_executable(translation_loader_test translation_loader_test.cpp)
|
|
||||||
|
|
||||||
# QTranslator only accepts real .qm files, so the translation loader test gets one compiled from its
|
|
||||||
# own .ts fixture rather than a stub.
|
|
||||||
set(TRANSLATION_LOADER_TEST_QM "${CMAKE_CURRENT_BINARY_DIR}/translation_loader_test_de.qm")
|
|
||||||
add_custom_command(
|
|
||||||
OUTPUT "${TRANSLATION_LOADER_TEST_QM}"
|
|
||||||
COMMAND Qt6::lrelease "${CMAKE_CURRENT_SOURCE_DIR}/translation_loader_test.ts" -qm "${TRANSLATION_LOADER_TEST_QM}"
|
|
||||||
DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/translation_loader_test.ts"
|
|
||||||
COMMENT "Compiling translation_loader_test.ts"
|
|
||||||
)
|
|
||||||
add_custom_target(translation_loader_test_qm DEPENDS "${TRANSLATION_LOADER_TEST_QM}")
|
|
||||||
add_dependencies(translation_loader_test translation_loader_test_qm)
|
|
||||||
target_compile_definitions(translation_loader_test PRIVATE TRANSLATION_LOADER_TEST_DIR="${CMAKE_CURRENT_BINARY_DIR}")
|
|
||||||
|
|
||||||
# A multi-config generator (MSBuild, Xcode) puts the executable in a per-config subdirectory while
|
|
||||||
# the .qm stays in the current binary directory, so copy the fixture next to the binary: that is the
|
|
||||||
# layout ABuildTreeFindsTheTranslationsNextToTheExecutable is about, and the only one it finds on
|
|
||||||
# those generators.
|
|
||||||
add_custom_command(
|
|
||||||
TARGET translation_loader_test
|
|
||||||
POST_BUILD
|
|
||||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different "${TRANSLATION_LOADER_TEST_QM}"
|
|
||||||
"$<TARGET_FILE_DIR:translation_loader_test>/"
|
|
||||||
VERBATIM
|
|
||||||
)
|
|
||||||
endif()
|
|
||||||
add_executable(report_categories_test report_categories_test.cpp)
|
|
||||||
add_executable(lag_monitor_test ${CMAKE_SOURCE_DIR}/cockatrice/src/client/lag_monitor.cpp lag_monitor_test.cpp)
|
|
||||||
target_include_directories(lag_monitor_test PRIVATE ${CMAKE_SOURCE_DIR}/cockatrice/src)
|
|
||||||
add_executable(latency_tracker_test latency_tracker_test.cpp)
|
|
||||||
add_executable(metrics_registry_test ../servatrice/src/metrics_registry.cpp metrics_registry_test.cpp)
|
|
||||||
add_executable(
|
|
||||||
card_update_progress_test ${CMAKE_SOURCE_DIR}/cockatrice/src/interface/card_database_update/card_update_progress.cpp
|
|
||||||
card_update_progress_test.cpp
|
|
||||||
)
|
|
||||||
target_include_directories(card_update_progress_test PRIVATE ${CMAKE_SOURCE_DIR}/cockatrice/src)
|
|
||||||
add_executable(
|
|
||||||
loader_local_matching_test
|
|
||||||
${CMAKE_SOURCE_DIR}/cockatrice/src/interface/card_picture_loader/card_picture_loader_local.cpp
|
|
||||||
${CMAKE_SOURCE_DIR}/cockatrice/src/client/settings/cache_settings.cpp
|
|
||||||
${CMAKE_SOURCE_DIR}/cockatrice/src/client/settings/card_counter_settings.cpp
|
|
||||||
${CMAKE_SOURCE_DIR}/cockatrice/src/client/settings/shortcuts_settings.cpp
|
|
||||||
${CMAKE_SOURCE_DIR}/cockatrice/src/client/network/update/client/release_channel.cpp
|
|
||||||
${VERSION_STRING_CPP}
|
|
||||||
loader_local_matching_test.cpp
|
|
||||||
)
|
|
||||||
target_include_directories(loader_local_matching_test PRIVATE ${CMAKE_SOURCE_DIR}/cockatrice/src)
|
|
||||||
|
|
||||||
# Guard must match the condition for src/single_instance_manager.cpp in cockatrice/CMakeLists.txt.
|
|
||||||
if(WITH_CLIENT)
|
|
||||||
add_executable(
|
|
||||||
single_instance_manager_test ${CMAKE_SOURCE_DIR}/cockatrice/src/single_instance_manager.cpp
|
|
||||||
single_instance_manager_test.cpp
|
|
||||||
)
|
|
||||||
target_include_directories(single_instance_manager_test PRIVATE ${CMAKE_SOURCE_DIR}/cockatrice/src)
|
|
||||||
target_include_directories(single_instance_manager_test PRIVATE ${CMAKE_SOURCE_DIR}/libcockatrice_utility)
|
|
||||||
|
|
||||||
add_test(NAME single_instance_manager_test COMMAND single_instance_manager_test)
|
|
||||||
endif()
|
|
||||||
|
|
||||||
find_package(GTest)
|
find_package(GTest)
|
||||||
|
|
||||||
if(NOT GTEST_FOUND)
|
if(NOT GTEST_FOUND)
|
||||||
|
|
@ -129,93 +30,342 @@ if(NOT GTEST_FOUND)
|
||||||
target_include_directories(gtest INTERFACE "$<BUILD_INTERFACE:${CMAKE_BINARY_DIR}/gtest-src/include>")
|
target_include_directories(gtest INTERFACE "$<BUILD_INTERFACE:${CMAKE_BINARY_DIR}/gtest-src/include>")
|
||||||
|
|
||||||
set(GTEST_INCLUDE_DIRS "${CMAKE_BINARY_DIR}/gtest-src/include")
|
set(GTEST_INCLUDE_DIRS "${CMAKE_BINARY_DIR}/gtest-src/include")
|
||||||
set(GTEST_BOTH_LIBRARIES gtest)
|
# gtest_main is not optional here. FindGTest sets GTEST_BOTH_LIBRARIES to both halves, so this
|
||||||
add_dependencies(dummy_test gtest)
|
# has to match it or the two paths disagree about what provides main(). A test source that
|
||||||
add_dependencies(expression_test gtest)
|
# defines its own main() gets away with the omission; one that does not fails at link with
|
||||||
add_dependencies(clamped_arithmetic_test gtest)
|
# `undefined reference to main` pointing at crt1.o, which names neither gtest nor this line.
|
||||||
add_dependencies(test_age_formatting gtest)
|
set(GTEST_BOTH_LIBRARIES gtest gtest_main)
|
||||||
add_dependencies(password_hash_test gtest)
|
|
||||||
add_dependencies(playmat_resolver_test gtest)
|
|
||||||
add_dependencies(deck_hash_performance_test gtest)
|
|
||||||
add_dependencies(server_card_counter_test gtest)
|
|
||||||
add_dependencies(server_counter_test gtest)
|
|
||||||
add_dependencies(server_rate_limiter_test gtest)
|
|
||||||
add_dependencies(server_developer_role_test gtest)
|
|
||||||
add_dependencies(server_game_join_test gtest)
|
|
||||||
add_dependencies(warning_categories_test gtest)
|
|
||||||
if(TARGET translation_loader_test)
|
|
||||||
add_dependencies(translation_loader_test gtest)
|
|
||||||
endif()
|
|
||||||
add_dependencies(report_categories_test gtest)
|
|
||||||
add_dependencies(lag_monitor_test gtest)
|
|
||||||
add_dependencies(latency_tracker_test gtest)
|
|
||||||
add_dependencies(metrics_registry_test gtest)
|
|
||||||
add_dependencies(card_update_progress_test gtest)
|
|
||||||
add_dependencies(loader_local_matching_test gtest)
|
|
||||||
add_dependencies(single_instance_manager_test gtest)
|
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
# Directory-scoped on purpose: the test subdirectories below add targets that include the
|
||||||
|
# fetched gtest headers and do not repeat this path themselves.
|
||||||
include_directories(${GTEST_INCLUDE_DIRS})
|
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})
|
# --- Test registration ----------------------------------------------------------
|
||||||
target_link_libraries(
|
#
|
||||||
clamped_arithmetic_test libcockatrice_utility Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES}
|
# Every test target needs the same four steps: build the executable, depend on gtest when it
|
||||||
)
|
# was fetched from source, link the shared test libraries, and hand the cases to CTest. The
|
||||||
target_link_libraries(
|
# `if(NOT GTEST_FOUND)` branch is the easy one to forget — omit it and the link fails with
|
||||||
test_age_formatting libcockatrice_utility Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES}
|
# undefined gtest symbols — so it lives here once instead of being repeated per test.
|
||||||
)
|
#
|
||||||
target_link_libraries(
|
# This also replaces `add_test(NAME x COMMAND x)`. Naming the binary as a single CTest entry
|
||||||
password_hash_test libcockatrice_utility Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES}
|
# hides which case failed and makes `ctest -R Suite.Case` impossible; discovery exposes each
|
||||||
)
|
# TEST() as its own CTest entry instead.
|
||||||
target_link_libraries(
|
#
|
||||||
playmat_resolver_test libcockatrice_deck_list libcockatrice_utility Threads::Threads ${GTEST_BOTH_LIBRARIES}
|
# Labels: exactly one per test, because gtest_discover_tests() cannot carry more than one.
|
||||||
${TEST_QT_MODULES}
|
# GoogleTestAddTests.cmake accumulates the generated script with `foreach(arg ${ARGN})`, so a
|
||||||
)
|
# label list serialises as bare `LABELS ci server` in the CTestTestfile, and CTest reads only
|
||||||
target_link_libraries(
|
# the first value of a property and silently discards the rest. A label therefore classifies a
|
||||||
deck_hash_performance_test libcockatrice_deck_list libcockatrice_utility Threads::Threads ${GTEST_BOTH_LIBRARIES}
|
# test into exactly one group, and the three groups cover every selection worth making:
|
||||||
${TEST_QT_MODULES}
|
#
|
||||||
)
|
# ci the default hermetic set, so `ctest -LE bench` runs everything but benchmarks
|
||||||
target_link_libraries(
|
# server servatrice-facing tests, so `ctest -L server` works
|
||||||
server_card_counter_test libcockatrice_network Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES}
|
# bench the slow suites, kept out of the default run
|
||||||
)
|
#
|
||||||
target_link_libraries(
|
# A second label is a configure-time FATAL_ERROR rather than a line CTest quietly reads as one.
|
||||||
server_counter_test libcockatrice_network Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES}
|
# Anything orthogonal to a test's kind belongs in a helper flag beside the label, so it
|
||||||
)
|
# composes with the group instead of competing for the single slot. NO_SANITIZE is such a flag.
|
||||||
target_link_libraries(
|
#
|
||||||
server_rate_limiter_test libcockatrice_utility Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES}
|
# TIMEOUT is bounded per test because CTest's implicit default is 1500s: one hung socket wait
|
||||||
)
|
# would otherwise stall CI for 25 minutes.
|
||||||
target_link_libraries(
|
#
|
||||||
server_developer_role_test libcockatrice_network libcockatrice_rng Threads::Threads ${GTEST_BOTH_LIBRARIES}
|
# EXTRA_LIBS is appended after the shared libraries so a caller adding a static archive (zlib,
|
||||||
${TEST_QT_MODULES}
|
# lzma) keeps it last, which is where the linker needs it.
|
||||||
)
|
|
||||||
target_link_libraries(
|
function(cockatrice_add_test)
|
||||||
server_game_join_test libcockatrice_network_server_remote libcockatrice_rng Threads::Threads ${GTEST_BOTH_LIBRARIES}
|
cmake_parse_arguments(
|
||||||
${TEST_QT_MODULES}
|
PARSE_ARGV 0 ARG "MANUAL;NO_SANITIZE" "NAME" "SOURCES;LIBS;LABELS;DEFINITIONS;INCLUDE_DIRS;EXTRA_LIBS"
|
||||||
)
|
)
|
||||||
target_link_libraries(
|
|
||||||
warning_categories_test libcockatrice_utility Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES}
|
if(NOT ARG_NAME OR NOT ARG_SOURCES)
|
||||||
)
|
message(FATAL_ERROR "cockatrice_add_test requires at least NAME and SOURCES")
|
||||||
if(TARGET translation_loader_test)
|
endif()
|
||||||
|
|
||||||
|
add_executable(${ARG_NAME} ${ARG_SOURCES})
|
||||||
|
|
||||||
|
if(ARG_INCLUDE_DIRS)
|
||||||
|
target_include_directories(${ARG_NAME} PRIVATE ${ARG_INCLUDE_DIRS})
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if(ARG_DEFINITIONS)
|
||||||
|
target_compile_definitions(${ARG_NAME} PRIVATE ${ARG_DEFINITIONS})
|
||||||
|
endif()
|
||||||
|
|
||||||
target_link_libraries(
|
target_link_libraries(
|
||||||
translation_loader_test libcockatrice_utility Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES}
|
${ARG_NAME} ${ARG_LIBS} Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES} ${ARG_EXTRA_LIBS}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Only needed when gtest was downloaded and add_subdirectory'd above. An imported gtest
|
||||||
|
# target already carries its own dependency, so this must stay conditional.
|
||||||
|
if(NOT GTEST_FOUND)
|
||||||
|
add_dependencies(${ARG_NAME} gtest gtest_main)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# MANUAL targets are built but deliberately kept out of ctest: benchmarks that need a card
|
||||||
|
# database, hit the network, or measure RAM. Leaving them unregistered keeps CI hermetic
|
||||||
|
# and fast. They still link gtest so they can use TEST() without a second code path.
|
||||||
|
if(ARG_MANUAL)
|
||||||
|
return()
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# NO_SANITIZE marks a target the sanitizer leg cannot run. Excluded by not handing its cases
|
||||||
|
# to CTest at all, rather than by a label, so the exclusion cannot rot into a line CTest
|
||||||
|
# quietly reads as something else. The target still builds, and still runs on the other legs.
|
||||||
|
if(ARG_NO_SANITIZE AND COCKATRICE_SANITIZERS)
|
||||||
|
return()
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# One label only, per the note above. Rejecting a second loudly beats emitting a
|
||||||
|
# CTestTestfile line that CTest will quietly read as one label.
|
||||||
|
list(LENGTH ARG_LABELS _label_count)
|
||||||
|
if(_label_count GREATER 1)
|
||||||
|
message(FATAL_ERROR "${ARG_NAME}: pass at most one label, got '${ARG_LABELS}'")
|
||||||
|
endif()
|
||||||
|
if(NOT ARG_LABELS)
|
||||||
|
set(ARG_LABELS ci)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# GoogleTestAddTests defaults DISCOVERY_TIMEOUT to 5s, which is ample for a plain debug
|
||||||
|
# binary (--gtest_list_tests costs 0.04s) but not for an instrumented one (~2.8s cold, and
|
||||||
|
# discovery runs several binaries concurrently behind a -j build). Left at the default, a
|
||||||
|
# sanitizer build fails at the *link* step with a bare "Process terminated due to timeout"
|
||||||
|
# and no hint that discovery, not the test, is what is slow.
|
||||||
|
if(COCKATRICE_SANITIZERS)
|
||||||
|
set(_discovery_timeout 300)
|
||||||
|
else()
|
||||||
|
set(_discovery_timeout 60)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
gtest_discover_tests(
|
||||||
|
${ARG_NAME} DISCOVERY_MODE POST_BUILD DISCOVERY_TIMEOUT ${_discovery_timeout}
|
||||||
|
PROPERTIES LABELS "${ARG_LABELS}"
|
||||||
|
TIMEOUT 60
|
||||||
|
)
|
||||||
|
endfunction()
|
||||||
|
|
||||||
|
# Same wiring for targets that are built but deliberately kept out of ctest: benchmarks that
|
||||||
|
# need a card database, hit the network, or measure RAM. A separate name rather than a MANUAL
|
||||||
|
# flag spelled out at each call site, so "this is not in the CI suite" is visible in the CMake
|
||||||
|
# file. Note the unquoted expansions: an unset multi-value keyword expands to zero arguments,
|
||||||
|
# which is what lets this forward a caller's keywords verbatim without tripping the FATAL_ERROR
|
||||||
|
# guard above on an empty list.
|
||||||
|
function(cockatrice_add_manual_test)
|
||||||
|
cmake_parse_arguments(PARSE_ARGV 0 ARG "" "NAME" "SOURCES;LIBS;DEFINITIONS;INCLUDE_DIRS;EXTRA_LIBS")
|
||||||
|
|
||||||
|
cockatrice_add_test(
|
||||||
|
NAME
|
||||||
|
${ARG_NAME}
|
||||||
|
SOURCES
|
||||||
|
${ARG_SOURCES}
|
||||||
|
LIBS
|
||||||
|
${ARG_LIBS}
|
||||||
|
DEFINITIONS
|
||||||
|
${ARG_DEFINITIONS}
|
||||||
|
INCLUDE_DIRS
|
||||||
|
${ARG_INCLUDE_DIRS}
|
||||||
|
EXTRA_LIBS
|
||||||
|
${ARG_EXTRA_LIBS}
|
||||||
|
MANUAL
|
||||||
|
)
|
||||||
|
endfunction()
|
||||||
|
|
||||||
|
# --- Unit tests ------------------------------------------------------------------
|
||||||
|
|
||||||
|
cockatrice_add_test(NAME expression_test SOURCES expression_test.cpp LIBS libcockatrice_utility)
|
||||||
|
cockatrice_add_test(NAME clamped_arithmetic_test SOURCES clamped_arithmetic_test.cpp LIBS libcockatrice_utility)
|
||||||
|
cockatrice_add_test(NAME test_age_formatting SOURCES test_age_formatting.cpp LIBS libcockatrice_utility)
|
||||||
|
cockatrice_add_test(NAME password_hash_test SOURCES password_hash_test.cpp LIBS libcockatrice_utility)
|
||||||
|
cockatrice_add_test(NAME report_categories_test SOURCES report_categories_test.cpp LIBS libcockatrice_utility)
|
||||||
|
cockatrice_add_test(NAME warning_categories_test SOURCES warning_categories_test.cpp LIBS libcockatrice_utility)
|
||||||
|
cockatrice_add_test(
|
||||||
|
NAME
|
||||||
|
playmat_resolver_test
|
||||||
|
SOURCES
|
||||||
|
playmat_resolver_test.cpp
|
||||||
|
LIBS
|
||||||
|
libcockatrice_deck_list
|
||||||
|
libcockatrice_utility
|
||||||
|
)
|
||||||
|
cockatrice_add_test(
|
||||||
|
NAME
|
||||||
|
server_rate_limiter_test
|
||||||
|
SOURCES
|
||||||
|
server_rate_limiter_test.cpp
|
||||||
|
LIBS
|
||||||
|
libcockatrice_utility
|
||||||
|
LABELS
|
||||||
|
server
|
||||||
|
)
|
||||||
|
cockatrice_add_test(
|
||||||
|
NAME
|
||||||
|
server_card_counter_test
|
||||||
|
SOURCES
|
||||||
|
server_card_counter_test.cpp
|
||||||
|
LIBS
|
||||||
|
libcockatrice_network
|
||||||
|
LABELS
|
||||||
|
server
|
||||||
|
)
|
||||||
|
cockatrice_add_test(
|
||||||
|
NAME
|
||||||
|
server_counter_test
|
||||||
|
SOURCES
|
||||||
|
server_counter_test.cpp
|
||||||
|
LIBS
|
||||||
|
libcockatrice_network
|
||||||
|
LABELS
|
||||||
|
server
|
||||||
|
)
|
||||||
|
cockatrice_add_test(
|
||||||
|
NAME
|
||||||
|
server_developer_role_test
|
||||||
|
NO_SANITIZE
|
||||||
|
SOURCES
|
||||||
|
server_developer_role_test.cpp
|
||||||
|
LIBS
|
||||||
|
libcockatrice_network
|
||||||
|
libcockatrice_rng
|
||||||
|
LABELS
|
||||||
|
server
|
||||||
|
)
|
||||||
|
cockatrice_add_test(
|
||||||
|
NAME
|
||||||
|
server_moderator_role_test
|
||||||
|
NO_SANITIZE
|
||||||
|
SOURCES
|
||||||
|
server_moderator_role_test.cpp
|
||||||
|
LIBS
|
||||||
|
libcockatrice_network
|
||||||
|
libcockatrice_rng
|
||||||
|
LABELS
|
||||||
|
server
|
||||||
|
)
|
||||||
|
cockatrice_add_test(
|
||||||
|
NAME
|
||||||
|
server_game_join_test
|
||||||
|
SOURCES
|
||||||
|
server_game_join_test.cpp
|
||||||
|
LIBS
|
||||||
|
libcockatrice_network_server_remote
|
||||||
|
libcockatrice_rng
|
||||||
|
LABELS
|
||||||
|
server
|
||||||
|
)
|
||||||
|
cockatrice_add_test(
|
||||||
|
NAME
|
||||||
|
latency_tracker_test
|
||||||
|
SOURCES
|
||||||
|
latency_tracker_test.cpp
|
||||||
|
LIBS
|
||||||
|
libcockatrice_network
|
||||||
|
LABELS
|
||||||
|
server
|
||||||
|
)
|
||||||
|
cockatrice_add_test(
|
||||||
|
NAME
|
||||||
|
lag_monitor_test
|
||||||
|
SOURCES
|
||||||
|
${CMAKE_SOURCE_DIR}/cockatrice/src/client/lag_monitor.cpp
|
||||||
|
lag_monitor_test.cpp
|
||||||
|
INCLUDE_DIRS
|
||||||
|
${CMAKE_SOURCE_DIR}/cockatrice/src
|
||||||
|
)
|
||||||
|
cockatrice_add_test(
|
||||||
|
NAME
|
||||||
|
metrics_registry_test
|
||||||
|
SOURCES
|
||||||
|
${CMAKE_SOURCE_DIR}/servatrice/src/metrics_registry.cpp
|
||||||
|
metrics_registry_test.cpp
|
||||||
|
INCLUDE_DIRS
|
||||||
|
${CMAKE_SOURCE_DIR}/servatrice/src
|
||||||
|
LABELS
|
||||||
|
server
|
||||||
|
)
|
||||||
|
cockatrice_add_test(
|
||||||
|
NAME
|
||||||
|
card_update_progress_test
|
||||||
|
SOURCES
|
||||||
|
${CMAKE_SOURCE_DIR}/cockatrice/src/interface/card_database_update/card_update_progress.cpp
|
||||||
|
card_update_progress_test.cpp
|
||||||
|
INCLUDE_DIRS
|
||||||
|
${CMAKE_SOURCE_DIR}/cockatrice/src
|
||||||
|
)
|
||||||
|
cockatrice_add_test(
|
||||||
|
NAME
|
||||||
|
loader_local_matching_test
|
||||||
|
SOURCES
|
||||||
|
${CMAKE_SOURCE_DIR}/cockatrice/src/interface/card_picture_loader/card_picture_loader_local.cpp
|
||||||
|
${CMAKE_SOURCE_DIR}/cockatrice/src/client/settings/cache_settings.cpp
|
||||||
|
${CMAKE_SOURCE_DIR}/cockatrice/src/client/settings/card_counter_settings.cpp
|
||||||
|
${CMAKE_SOURCE_DIR}/cockatrice/src/client/settings/shortcuts_settings.cpp
|
||||||
|
${CMAKE_SOURCE_DIR}/cockatrice/src/client/network/update/client/release_channel.cpp
|
||||||
|
${VERSION_STRING_CPP}
|
||||||
|
loader_local_matching_test.cpp
|
||||||
|
LIBS
|
||||||
|
libcockatrice_settings
|
||||||
|
INCLUDE_DIRS
|
||||||
|
${CMAKE_SOURCE_DIR}/cockatrice/src
|
||||||
|
)
|
||||||
|
cockatrice_add_test(
|
||||||
|
NAME
|
||||||
|
deck_hash_performance_test
|
||||||
|
SOURCES
|
||||||
|
deck_hash_performance_test.cpp
|
||||||
|
LIBS
|
||||||
|
libcockatrice_deck_list
|
||||||
|
libcockatrice_utility
|
||||||
|
LABELS
|
||||||
|
bench
|
||||||
|
)
|
||||||
|
|
||||||
|
# The translation loader test compiles a .qm fixture, which needs lrelease from Qt Linguist.
|
||||||
|
if(TARGET Qt6::lrelease)
|
||||||
|
set(TRANSLATION_LOADER_TEST_QM "${CMAKE_CURRENT_BINARY_DIR}/translation_loader_test_de.qm")
|
||||||
|
add_custom_command(
|
||||||
|
OUTPUT "${TRANSLATION_LOADER_TEST_QM}"
|
||||||
|
COMMAND Qt6::lrelease "${CMAKE_CURRENT_SOURCE_DIR}/translation_loader_test.ts" -qm "${TRANSLATION_LOADER_TEST_QM}"
|
||||||
|
DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/translation_loader_test.ts"
|
||||||
|
COMMENT "Compiling translation_loader_test.ts"
|
||||||
|
)
|
||||||
|
add_custom_target(translation_loader_test_qm DEPENDS "${TRANSLATION_LOADER_TEST_QM}")
|
||||||
|
|
||||||
|
cockatrice_add_test(
|
||||||
|
NAME
|
||||||
|
translation_loader_test
|
||||||
|
SOURCES
|
||||||
|
translation_loader_test.cpp
|
||||||
|
LIBS
|
||||||
|
libcockatrice_utility
|
||||||
|
DEFINITIONS
|
||||||
|
TRANSLATION_LOADER_TEST_DIR="${CMAKE_CURRENT_BINARY_DIR}"
|
||||||
|
)
|
||||||
|
add_dependencies(translation_loader_test translation_loader_test_qm)
|
||||||
|
|
||||||
|
# A multi-config generator (MSBuild, Xcode) puts the executable in a per-config subdirectory while
|
||||||
|
# the .qm stays in the current binary directory, so copy the fixture next to the binary: that is the
|
||||||
|
# layout ABuildTreeFindsTheTranslationsNextToTheExecutable is about, and the only one it finds on
|
||||||
|
# those generators.
|
||||||
|
add_custom_command(
|
||||||
|
TARGET translation_loader_test
|
||||||
|
POST_BUILD
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E copy_if_different "${TRANSLATION_LOADER_TEST_QM}"
|
||||||
|
"$<TARGET_FILE_DIR:translation_loader_test>/"
|
||||||
|
VERBATIM
|
||||||
)
|
)
|
||||||
endif()
|
endif()
|
||||||
target_link_libraries(
|
|
||||||
report_categories_test libcockatrice_utility Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES}
|
|
||||||
)
|
|
||||||
target_link_libraries(lag_monitor_test Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES})
|
|
||||||
target_link_libraries(
|
|
||||||
latency_tracker_test libcockatrice_network Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES}
|
|
||||||
)
|
|
||||||
target_include_directories(metrics_registry_test PRIVATE ${CMAKE_SOURCE_DIR}/servatrice/src)
|
|
||||||
target_link_libraries(metrics_registry_test ${TEST_QT_MODULES} Threads::Threads ${GTEST_BOTH_LIBRARIES})
|
|
||||||
target_link_libraries(card_update_progress_test Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES})
|
|
||||||
target_link_libraries(
|
|
||||||
loader_local_matching_test libcockatrice_settings Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
# Guard must match the condition for src/single_instance_manager.cpp in cockatrice/CMakeLists.txt.
|
||||||
if(WITH_CLIENT)
|
if(WITH_CLIENT)
|
||||||
target_link_libraries(single_instance_manager_test Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES})
|
cockatrice_add_test(
|
||||||
|
NAME
|
||||||
|
single_instance_manager_test
|
||||||
|
SOURCES
|
||||||
|
${CMAKE_SOURCE_DIR}/cockatrice/src/single_instance_manager.cpp
|
||||||
|
single_instance_manager_test.cpp
|
||||||
|
INCLUDE_DIRS
|
||||||
|
${CMAKE_SOURCE_DIR}/cockatrice/src
|
||||||
|
${CMAKE_SOURCE_DIR}/libcockatrice_utility
|
||||||
|
)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
add_subdirectory(card_zone_algorithms)
|
add_subdirectory(card_zone_algorithms)
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,4 @@
|
||||||
add_executable(card_zone_algorithms_test card_zone_algorithms_test.cpp)
|
cockatrice_add_test(
|
||||||
|
NAME card_zone_algorithms_test SOURCES card_zone_algorithms_test.cpp INCLUDE_DIRS
|
||||||
target_include_directories(card_zone_algorithms_test PRIVATE ${CMAKE_SOURCE_DIR}/cockatrice/src/game/zones)
|
${CMAKE_SOURCE_DIR}/cockatrice/src/game/zones
|
||||||
|
|
||||||
target_link_libraries(
|
|
||||||
card_zone_algorithms_test
|
|
||||||
PRIVATE Threads::Threads
|
|
||||||
PRIVATE ${GTEST_BOTH_LIBRARIES}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
add_test(NAME card_zone_algorithms_test COMMAND card_zone_algorithms_test)
|
|
||||||
|
|
||||||
if(NOT GTEST_FOUND)
|
|
||||||
add_dependencies(card_zone_algorithms_test gtest)
|
|
||||||
endif()
|
|
||||||
|
|
|
||||||
|
|
@ -9,61 +9,46 @@ add_definitions("-DCARDDB_DATADIR=\"${CMAKE_CURRENT_SOURCE_DIR}/data/\"")
|
||||||
# ------------------------
|
# ------------------------
|
||||||
# Card Database Test
|
# Card Database Test
|
||||||
# ------------------------
|
# ------------------------
|
||||||
add_executable(carddatabase_test ${MOCKS_SOURCES} ${VERSION_STRING_CPP} carddatabase_test.cpp mocks.cpp)
|
cockatrice_add_test(
|
||||||
|
NAME
|
||||||
target_link_libraries(
|
|
||||||
carddatabase_test
|
carddatabase_test
|
||||||
PRIVATE libcockatrice_card
|
SOURCES
|
||||||
PRIVATE Threads::Threads
|
${VERSION_STRING_CPP}
|
||||||
PRIVATE ${GTEST_BOTH_LIBRARIES}
|
carddatabase_test.cpp
|
||||||
PRIVATE ${TEST_QT_MODULES}
|
mocks.cpp
|
||||||
|
LIBS
|
||||||
|
libcockatrice_card
|
||||||
)
|
)
|
||||||
|
|
||||||
add_test(NAME carddatabase_test COMMAND carddatabase_test)
|
|
||||||
|
|
||||||
# ------------------------
|
|
||||||
# Load Benchmark (manual, not run in CI)
|
|
||||||
# ------------------------
|
|
||||||
add_executable(load_benchmark ${MOCKS_SOURCES} ${VERSION_STRING_CPP} load_benchmark.cpp mocks.cpp)
|
|
||||||
|
|
||||||
target_link_libraries(
|
|
||||||
load_benchmark
|
|
||||||
PRIVATE libcockatrice_card
|
|
||||||
PRIVATE libcockatrice_models
|
|
||||||
PRIVATE Threads::Threads
|
|
||||||
PRIVATE ${GTEST_BOTH_LIBRARIES}
|
|
||||||
PRIVATE ${TEST_QT_MODULES}
|
|
||||||
)
|
|
||||||
|
|
||||||
if(NOT GTEST_FOUND)
|
|
||||||
add_dependencies(load_benchmark gtest)
|
|
||||||
endif()
|
|
||||||
|
|
||||||
# ------------------------
|
# ------------------------
|
||||||
# Filter String Test
|
# Filter String Test
|
||||||
# (guard must match the condition for libcockatrice_filters in the root CMakeLists.txt)
|
# (guard must match the condition for libcockatrice_filters in the root CMakeLists.txt)
|
||||||
# ------------------------
|
# ------------------------
|
||||||
if(WITH_ORACLE OR WITH_CLIENT)
|
if(WITH_ORACLE OR WITH_CLIENT)
|
||||||
add_executable(filter_string_test ${MOCKS_SOURCES} ${VERSION_STRING_CPP} filter_string_test.cpp mocks.cpp)
|
cockatrice_add_test(
|
||||||
|
NAME
|
||||||
target_link_libraries(
|
|
||||||
filter_string_test
|
filter_string_test
|
||||||
PRIVATE libcockatrice_filters
|
SOURCES
|
||||||
PRIVATE Threads::Threads
|
${VERSION_STRING_CPP}
|
||||||
PRIVATE ${GTEST_BOTH_LIBRARIES}
|
filter_string_test.cpp
|
||||||
PRIVATE ${TEST_QT_MODULES}
|
mocks.cpp
|
||||||
|
LIBS
|
||||||
|
libcockatrice_filters
|
||||||
)
|
)
|
||||||
|
|
||||||
add_test(NAME filter_string_test COMMAND filter_string_test)
|
|
||||||
|
|
||||||
if(NOT GTEST_FOUND)
|
|
||||||
add_dependencies(filter_string_test gtest)
|
|
||||||
endif()
|
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
# ------------------------
|
# ------------------------
|
||||||
# Dependencies on gtest
|
# Load Benchmark (manual, not run in CI)
|
||||||
# ------------------------
|
# ------------------------
|
||||||
if(NOT GTEST_FOUND)
|
cockatrice_add_manual_test(
|
||||||
add_dependencies(carddatabase_test gtest)
|
NAME
|
||||||
endif()
|
load_benchmark
|
||||||
|
SOURCES
|
||||||
|
${VERSION_STRING_CPP}
|
||||||
|
load_benchmark.cpp
|
||||||
|
mocks.cpp
|
||||||
|
LIBS
|
||||||
|
libcockatrice_card
|
||||||
|
libcockatrice_models
|
||||||
|
MANUAL
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,33 +1,23 @@
|
||||||
add_executable(deck_list_model_custom_zones_test ${VERSION_STRING_CPP} deck_list_model_custom_zones_test.cpp)
|
cockatrice_add_test(
|
||||||
|
NAME
|
||||||
if(NOT GTEST_FOUND)
|
|
||||||
add_dependencies(deck_list_model_custom_zones_test gtest)
|
|
||||||
endif()
|
|
||||||
|
|
||||||
target_link_libraries(
|
|
||||||
deck_list_model_custom_zones_test
|
deck_list_model_custom_zones_test
|
||||||
|
SOURCES
|
||||||
|
${VERSION_STRING_CPP}
|
||||||
|
deck_list_model_custom_zones_test.cpp
|
||||||
|
LIBS
|
||||||
libcockatrice_models
|
libcockatrice_models
|
||||||
libcockatrice_card
|
libcockatrice_card
|
||||||
libcockatrice_deck_list
|
libcockatrice_deck_list
|
||||||
Threads::Threads
|
|
||||||
${GTEST_BOTH_LIBRARIES}
|
|
||||||
${TEST_QT_MODULES}
|
|
||||||
)
|
)
|
||||||
add_test(NAME deck_list_model_custom_zones_test COMMAND deck_list_model_custom_zones_test)
|
|
||||||
|
|
||||||
add_executable(deck_list_model_zone_integration_test ${VERSION_STRING_CPP} deck_list_model_zone_integration_test.cpp)
|
cockatrice_add_test(
|
||||||
|
NAME
|
||||||
if(NOT GTEST_FOUND)
|
|
||||||
add_dependencies(deck_list_model_zone_integration_test gtest)
|
|
||||||
endif()
|
|
||||||
|
|
||||||
target_link_libraries(
|
|
||||||
deck_list_model_zone_integration_test
|
deck_list_model_zone_integration_test
|
||||||
|
SOURCES
|
||||||
|
${VERSION_STRING_CPP}
|
||||||
|
deck_list_model_zone_integration_test.cpp
|
||||||
|
LIBS
|
||||||
libcockatrice_models
|
libcockatrice_models
|
||||||
libcockatrice_card
|
libcockatrice_card
|
||||||
libcockatrice_deck_list
|
libcockatrice_deck_list
|
||||||
Threads::Threads
|
|
||||||
${GTEST_BOTH_LIBRARIES}
|
|
||||||
${TEST_QT_MODULES}
|
|
||||||
)
|
)
|
||||||
add_test(NAME deck_list_model_zone_integration_test COMMAND deck_list_model_zone_integration_test)
|
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1 @@
|
||||||
add_executable(deck_list_zones_test deck_list_zones_test.cpp)
|
cockatrice_add_test(NAME deck_list_zones_test SOURCES deck_list_zones_test.cpp LIBS libcockatrice_deck_list)
|
||||||
|
|
||||||
if(NOT GTEST_FOUND)
|
|
||||||
add_dependencies(deck_list_zones_test gtest)
|
|
||||||
endif()
|
|
||||||
|
|
||||||
target_link_libraries(
|
|
||||||
deck_list_zones_test libcockatrice_deck_list Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES}
|
|
||||||
)
|
|
||||||
add_test(NAME deck_list_zones_test COMMAND deck_list_zones_test)
|
|
||||||
|
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
#include "gtest/gtest.h"
|
|
||||||
#include <string>
|
|
||||||
|
|
||||||
namespace
|
|
||||||
{
|
|
||||||
class FooTest : public ::testing::Test
|
|
||||||
{
|
|
||||||
};
|
|
||||||
|
|
||||||
TEST(DummyTest, Works)
|
|
||||||
{
|
|
||||||
ASSERT_EQ(1, 1) << "One is not equal to one";
|
|
||||||
}
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
int main(int argc, char **argv)
|
|
||||||
{
|
|
||||||
::testing::InitGoogleTest(&argc, argv);
|
|
||||||
return RUN_ALL_TESTS();
|
|
||||||
}
|
|
||||||
|
|
@ -1,12 +1,13 @@
|
||||||
add_definitions("-DCARDDB_DATADIR=\"${CMAKE_CURRENT_SOURCE_DIR}/data/\"")
|
add_definitions("-DCARDDB_DATADIR=\"${CMAKE_CURRENT_SOURCE_DIR}/data/\"")
|
||||||
add_executable(loading_from_clipboard_test ${VERSION_STRING_CPP} clipboard_testing.cpp loading_from_clipboard_test.cpp)
|
|
||||||
|
|
||||||
if(NOT GTEST_FOUND)
|
cockatrice_add_test(
|
||||||
add_dependencies(loading_from_clipboard_test gtest)
|
NAME
|
||||||
endif()
|
loading_from_clipboard_test
|
||||||
|
SOURCES
|
||||||
target_link_libraries(
|
${VERSION_STRING_CPP}
|
||||||
loading_from_clipboard_test libcockatrice_deck_list libcockatrice_card Threads::Threads ${GTEST_BOTH_LIBRARIES}
|
clipboard_testing.cpp
|
||||||
${TEST_QT_MODULES}
|
loading_from_clipboard_test.cpp
|
||||||
|
LIBS
|
||||||
|
libcockatrice_deck_list
|
||||||
|
libcockatrice_card
|
||||||
)
|
)
|
||||||
add_test(NAME loading_from_clipboard_test COMMAND loading_from_clipboard_test)
|
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,11 @@
|
||||||
add_executable(reverse_card_move_test reverse_card_move_test.cpp)
|
cockatrice_add_test(
|
||||||
|
NAME
|
||||||
if(NOT GTEST_FOUND)
|
|
||||||
add_dependencies(reverse_card_move_test gtest)
|
|
||||||
endif()
|
|
||||||
|
|
||||||
target_link_libraries(
|
|
||||||
reverse_card_move_test
|
reverse_card_move_test
|
||||||
PRIVATE libcockatrice_network_server_remote
|
SOURCES
|
||||||
PRIVATE libcockatrice_rng
|
reverse_card_move_test.cpp
|
||||||
PRIVATE Threads::Threads
|
LIBS
|
||||||
PRIVATE ${GTEST_BOTH_LIBRARIES}
|
libcockatrice_network_server_remote
|
||||||
PRIVATE ${TEST_QT_MODULES}
|
libcockatrice_rng
|
||||||
|
LABELS
|
||||||
|
server
|
||||||
)
|
)
|
||||||
|
|
||||||
add_test(NAME reverse_card_move_test COMMAND reverse_card_move_test)
|
|
||||||
|
|
|
||||||
|
|
@ -1,37 +1,32 @@
|
||||||
add_executable(parse_cipt_test ../../oracle/src/parsehelpers.cpp parse_cipt_test.cpp)
|
cockatrice_add_test(NAME parse_cipt_test SOURCES ../../oracle/src/parsehelpers.cpp parse_cipt_test.cpp)
|
||||||
|
|
||||||
if(NOT GTEST_FOUND)
|
|
||||||
add_dependencies(parse_cipt_test gtest)
|
|
||||||
endif()
|
|
||||||
|
|
||||||
target_link_libraries(parse_cipt_test Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES})
|
|
||||||
|
|
||||||
add_test(NAME parse_cipt_test COMMAND parse_cipt_test)
|
|
||||||
|
|
||||||
# Oracle importer unit tests
|
# Oracle importer unit tests
|
||||||
add_executable(
|
cockatrice_add_test(
|
||||||
oracle_importer_test ${VERSION_STRING_CPP} ../../oracle/src/oracleimporter.cpp ../../oracle/src/parsehelpers.cpp
|
NAME
|
||||||
../../oracle/src/raw_json_scanner.cpp oracle_importer_test.cpp
|
oracle_importer_test
|
||||||
|
SOURCES
|
||||||
|
${VERSION_STRING_CPP}
|
||||||
|
../../oracle/src/oracleimporter.cpp
|
||||||
|
../../oracle/src/parsehelpers.cpp
|
||||||
|
../../oracle/src/raw_json_scanner.cpp
|
||||||
|
oracle_importer_test.cpp
|
||||||
|
LIBS
|
||||||
|
libcockatrice_card
|
||||||
|
libcockatrice_interfaces
|
||||||
)
|
)
|
||||||
|
|
||||||
if(NOT GTEST_FOUND)
|
|
||||||
add_dependencies(oracle_importer_test gtest)
|
|
||||||
endif()
|
|
||||||
|
|
||||||
target_link_libraries(
|
|
||||||
oracle_importer_test libcockatrice_card libcockatrice_interfaces Threads::Threads ${GTEST_BOTH_LIBRARIES}
|
|
||||||
${TEST_QT_MODULES}
|
|
||||||
)
|
|
||||||
|
|
||||||
add_test(NAME oracle_importer_test COMMAND oracle_importer_test)
|
|
||||||
|
|
||||||
# Oracle importer benchmark tests (manual, not run in CI, incl. RAM benchmark)
|
# Oracle importer benchmark tests (manual, not run in CI, incl. RAM benchmark)
|
||||||
# Optional compression libs, mirrored from oracle/CMakeLists.txt, so the benchmark
|
# Optional compression libs, mirrored from oracle/CMakeLists.txt, so the benchmark
|
||||||
# can download and decompress whatever AllPrintings format the default URL selects.
|
# can download and decompress whatever AllPrintings format the default URL selects.
|
||||||
|
# add_definitions() is directory-scoped and order-independent, so HAS_ZLIB/HAS_LZMA also
|
||||||
|
# reach the two unit tests above. That is harmless: those macros gate code only in
|
||||||
|
# oracle/src/pages.cpp and oracle/src/pagetemplates.cpp, which no test target compiles.
|
||||||
find_package(ZLIB)
|
find_package(ZLIB)
|
||||||
if(ZLIB_FOUND)
|
if(ZLIB_FOUND)
|
||||||
add_definitions("-DHAS_ZLIB")
|
add_definitions("-DHAS_ZLIB")
|
||||||
set(_ORACLE_BENCH_EXTRA_SOURCES ../../oracle/src/zip/unzip.cpp ../../oracle/src/zip/zipglobal.cpp)
|
set(_ORACLE_BENCH_EXTRA_SOURCES ../../oracle/src/zip/unzip.cpp ../../oracle/src/zip/zipglobal.cpp)
|
||||||
|
cockatrice_disable_sanitizers_for(${_ORACLE_BENCH_EXTRA_SOURCES})
|
||||||
|
cockatrice_disable_coverage_for(${_ORACLE_BENCH_EXTRA_SOURCES})
|
||||||
set(_ORACLE_BENCH_EXTRA_LIBRARIES ${ZLIB_LIBRARIES})
|
set(_ORACLE_BENCH_EXTRA_LIBRARIES ${ZLIB_LIBRARIES})
|
||||||
include_directories(${ZLIB_INCLUDE_DIRS})
|
include_directories(${ZLIB_INCLUDE_DIRS})
|
||||||
else()
|
else()
|
||||||
|
|
@ -42,28 +37,28 @@ find_package(LibLZMA)
|
||||||
if(LIBLZMA_FOUND)
|
if(LIBLZMA_FOUND)
|
||||||
add_definitions("-DHAS_LZMA")
|
add_definitions("-DHAS_LZMA")
|
||||||
list(APPEND _ORACLE_BENCH_EXTRA_SOURCES ../../oracle/src/lzma/decompress.cpp)
|
list(APPEND _ORACLE_BENCH_EXTRA_SOURCES ../../oracle/src/lzma/decompress.cpp)
|
||||||
|
cockatrice_disable_sanitizers_for(../../oracle/src/lzma/decompress.cpp)
|
||||||
|
cockatrice_disable_coverage_for(../../oracle/src/lzma/decompress.cpp)
|
||||||
list(APPEND _ORACLE_BENCH_EXTRA_LIBRARIES ${LIBLZMA_LIBRARIES})
|
list(APPEND _ORACLE_BENCH_EXTRA_LIBRARIES ${LIBLZMA_LIBRARIES})
|
||||||
include_directories(${LIBLZMA_INCLUDE_DIRS})
|
include_directories(${LIBLZMA_INCLUDE_DIRS})
|
||||||
else()
|
else()
|
||||||
message(STATUS "Oracle tests: LibLZMA not found; xz download benchmark disabled")
|
message(STATUS "Oracle tests: LibLZMA not found; xz download benchmark disabled")
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
add_executable(
|
cockatrice_add_manual_test(
|
||||||
oracle_importer_benchmark_test
|
NAME
|
||||||
${VERSION_STRING_CPP} ../../oracle/src/oracleimporter.cpp ../../oracle/src/parsehelpers.cpp
|
|
||||||
../../oracle/src/raw_json_scanner.cpp oracle_importer_benchmark_test.cpp ${_ORACLE_BENCH_EXTRA_SOURCES}
|
|
||||||
)
|
|
||||||
|
|
||||||
if(NOT GTEST_FOUND)
|
|
||||||
add_dependencies(oracle_importer_benchmark_test gtest)
|
|
||||||
endif()
|
|
||||||
|
|
||||||
target_link_libraries(
|
|
||||||
oracle_importer_benchmark_test
|
oracle_importer_benchmark_test
|
||||||
|
SOURCES
|
||||||
|
${VERSION_STRING_CPP}
|
||||||
|
../../oracle/src/oracleimporter.cpp
|
||||||
|
../../oracle/src/parsehelpers.cpp
|
||||||
|
../../oracle/src/raw_json_scanner.cpp
|
||||||
|
oracle_importer_benchmark_test.cpp
|
||||||
|
${_ORACLE_BENCH_EXTRA_SOURCES}
|
||||||
|
LIBS
|
||||||
libcockatrice_card
|
libcockatrice_card
|
||||||
libcockatrice_interfaces
|
libcockatrice_interfaces
|
||||||
Threads::Threads
|
EXTRA_LIBS
|
||||||
${GTEST_BOTH_LIBRARIES}
|
|
||||||
${TEST_QT_MODULES}
|
|
||||||
${_ORACLE_BENCH_EXTRA_LIBRARIES}
|
${_ORACLE_BENCH_EXTRA_LIBRARIES}
|
||||||
|
MANUAL
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
# Manual benchmark hitting the real card image hosts. Not registered with
|
# Manual benchmark hitting the real card image hosts. Not registered with CTest: it requires a
|
||||||
# add_test(): it requires a cards.xml, touches the network for minutes at a
|
# cards.xml, touches the network for minutes at a time, and needs network access. Build it
|
||||||
# time, and needs network access. Build it explicitly and run by hand.
|
# explicitly and run by hand.
|
||||||
add_executable(
|
cockatrice_add_manual_test(
|
||||||
|
NAME
|
||||||
picture_loader_benchmark_test
|
picture_loader_benchmark_test
|
||||||
|
SOURCES
|
||||||
${VERSION_STRING_CPP}
|
${VERSION_STRING_CPP}
|
||||||
../../cockatrice/src/interface/card_picture_loader/card_picture_loader_local.cpp
|
../../cockatrice/src/interface/card_picture_loader/card_picture_loader_local.cpp
|
||||||
../../cockatrice/src/interface/card_picture_loader/card_picture_to_load.cpp
|
../../cockatrice/src/interface/card_picture_loader/card_picture_to_load.cpp
|
||||||
|
|
@ -11,16 +13,12 @@ add_executable(
|
||||||
../../cockatrice/src/client/settings/cache_settings.h
|
../../cockatrice/src/client/settings/cache_settings.h
|
||||||
picture_loader_benchmark.cpp
|
picture_loader_benchmark.cpp
|
||||||
settings_cache_mock.cpp
|
settings_cache_mock.cpp
|
||||||
)
|
LIBS
|
||||||
|
|
||||||
target_include_directories(picture_loader_benchmark_test PRIVATE ${CMAKE_SOURCE_DIR}/cockatrice/src)
|
|
||||||
|
|
||||||
target_link_libraries(
|
|
||||||
picture_loader_benchmark_test
|
|
||||||
libcockatrice_card
|
libcockatrice_card
|
||||||
libcockatrice_settings
|
libcockatrice_settings
|
||||||
libcockatrice_interfaces
|
libcockatrice_interfaces
|
||||||
libcockatrice_utility
|
libcockatrice_utility
|
||||||
Threads::Threads
|
INCLUDE_DIRS
|
||||||
${TEST_QT_MODULES}
|
${CMAKE_SOURCE_DIR}/cockatrice/src
|
||||||
|
MANUAL
|
||||||
)
|
)
|
||||||
|
|
|
||||||
156
tests/server_moderator_role_test.cpp
Normal file
156
tests/server_moderator_role_test.cpp
Normal file
|
|
@ -0,0 +1,156 @@
|
||||||
|
/** @file server_moderator_role_test.cpp
|
||||||
|
* @brief Tests for the moderator staff role authorization and dispatch.
|
||||||
|
* @ingroup Tests
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "libcockatrice/protocol/pb/response.pb.h"
|
||||||
|
#include "libcockatrice/protocol/pb/server_message.pb.h"
|
||||||
|
|
||||||
|
#include <QString>
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
#include <libcockatrice/network/server/remote/server.h>
|
||||||
|
#include <libcockatrice/network/server/remote/server_protocolhandler.h>
|
||||||
|
#include <libcockatrice/protocol/pb/command_report_reopen.pb.h>
|
||||||
|
#include <libcockatrice/protocol/pb/commands.pb.h>
|
||||||
|
#include <libcockatrice/protocol/pb/moderator_commands.pb.h>
|
||||||
|
#include <libcockatrice/protocol/pb/serverinfo_user.pb.h>
|
||||||
|
#include <libcockatrice/rng/rng_abstract.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
// The server_remote library references the global RNG, which is normally
|
||||||
|
// defined by the servatrice/client executable main(). Provide a stub so the
|
||||||
|
// unit test can link against it.
|
||||||
|
RNG_Abstract *rng = nullptr;
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
|
||||||
|
class TestModeratorHandler : public Server_ProtocolHandler
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
explicit TestModeratorHandler(Server *_server) : Server_ProtocolHandler(_server, nullptr)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
QString getAddress() const override
|
||||||
|
{
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
QString getConnectionType() const override
|
||||||
|
{
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Buffer the last response code sent to the client so tests can assert on
|
||||||
|
// the outcome of processCommandContainer().
|
||||||
|
Response::ResponseCode lastResponseCode = Response::RespNothing;
|
||||||
|
int dispatchCount = 0;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void transmitProtocolItem(const ServerMessage &item) override
|
||||||
|
{
|
||||||
|
if (item.message_type() == ServerMessage::RESPONSE) {
|
||||||
|
lastResponseCode = item.response().response_code();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Response::ResponseCode
|
||||||
|
processExtendedModeratorCommand(int cmdType, const ModeratorCommand &, ResponseContainer &) override
|
||||||
|
{
|
||||||
|
++dispatchCount;
|
||||||
|
// Fail closed for anything not explicitly handled.
|
||||||
|
if (cmdType != ModeratorCommand::REPORT_REOPEN) {
|
||||||
|
return Response::RespFunctionNotAllowed;
|
||||||
|
}
|
||||||
|
return Response::RespOk;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
class ModeratorRoleTest : public ::testing::Test
|
||||||
|
{
|
||||||
|
protected:
|
||||||
|
Server server;
|
||||||
|
TestModeratorHandler handler{&server};
|
||||||
|
|
||||||
|
void setUserLevel(uint32_t level)
|
||||||
|
{
|
||||||
|
ServerInfo_User user;
|
||||||
|
user.set_user_level(level);
|
||||||
|
handler.setUserInfo(user);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
TEST_F(ModeratorRoleTest, RejectsWhenNotLoggedIn)
|
||||||
|
{
|
||||||
|
CommandContainer cont;
|
||||||
|
cont.add_moderator_command();
|
||||||
|
handler.processCommandContainer(cont);
|
||||||
|
EXPECT_EQ(handler.lastResponseCode, Response::RespLoginNeeded);
|
||||||
|
EXPECT_EQ(handler.dispatchCount, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(ModeratorRoleTest, RejectsPlainUser)
|
||||||
|
{
|
||||||
|
setUserLevel(ServerInfo_User::IsUser | ServerInfo_User::IsRegistered);
|
||||||
|
|
||||||
|
CommandContainer cont;
|
||||||
|
cont.add_moderator_command();
|
||||||
|
handler.processCommandContainer(cont);
|
||||||
|
EXPECT_EQ(handler.lastResponseCode, Response::RespLoginNeeded);
|
||||||
|
EXPECT_EQ(handler.dispatchCount, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(ModeratorRoleTest, DispatchesToModeratorCommandForModerator)
|
||||||
|
{
|
||||||
|
setUserLevel(ServerInfo_User::IsModerator);
|
||||||
|
|
||||||
|
CommandContainer cont;
|
||||||
|
ModeratorCommand *cmd = cont.add_moderator_command();
|
||||||
|
cmd->MutableExtension(Command_ReportReopen::ext);
|
||||||
|
handler.processCommandContainer(cont);
|
||||||
|
EXPECT_EQ(handler.lastResponseCode, Response::RespOk);
|
||||||
|
EXPECT_EQ(handler.dispatchCount, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(ModeratorRoleTest, DispatchesToModeratorCommandForAdmin)
|
||||||
|
{
|
||||||
|
setUserLevel(ServerInfo_User::IsModerator | ServerInfo_User::IsAdmin);
|
||||||
|
|
||||||
|
CommandContainer cont;
|
||||||
|
ModeratorCommand *cmd = cont.add_moderator_command();
|
||||||
|
cmd->MutableExtension(Command_ReportReopen::ext);
|
||||||
|
handler.processCommandContainer(cont);
|
||||||
|
EXPECT_EQ(handler.lastResponseCode, Response::RespOk);
|
||||||
|
EXPECT_EQ(handler.dispatchCount, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(ModeratorRoleTest, RejectsDeveloperThatIsNotModerator)
|
||||||
|
{
|
||||||
|
setUserLevel(ServerInfo_User::IsDeveloper);
|
||||||
|
|
||||||
|
CommandContainer cont;
|
||||||
|
cont.add_moderator_command();
|
||||||
|
handler.processCommandContainer(cont);
|
||||||
|
EXPECT_EQ(handler.lastResponseCode, Response::RespLoginNeeded);
|
||||||
|
EXPECT_EQ(handler.dispatchCount, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(ModeratorRoleTest, FailClosedForUnknownModeratorCommand)
|
||||||
|
{
|
||||||
|
setUserLevel(ServerInfo_User::IsModerator);
|
||||||
|
|
||||||
|
CommandContainer cont;
|
||||||
|
cont.add_moderator_command(); // no extension set -> getPbExtension() returns -1
|
||||||
|
handler.processCommandContainer(cont);
|
||||||
|
EXPECT_EQ(handler.lastResponseCode, Response::RespFunctionNotAllowed);
|
||||||
|
EXPECT_EQ(handler.dispatchCount, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
int main(int argc, char **argv)
|
||||||
|
{
|
||||||
|
::testing::InitGoogleTest(&argc, argv);
|
||||||
|
return RUN_ALL_TESTS();
|
||||||
|
}
|
||||||
|
|
@ -1,26 +1,14 @@
|
||||||
add_executable(settings_manager_test settings_manager_test.cpp)
|
cockatrice_add_test(NAME settings_manager_test SOURCES settings_manager_test.cpp LIBS libcockatrice_settings)
|
||||||
add_executable(settings_defaults_test settings_defaults_test.cpp)
|
|
||||||
add_executable(settings_migration_test settings_migration_test.cpp)
|
|
||||||
|
|
||||||
target_link_libraries(
|
cockatrice_add_test(
|
||||||
settings_manager_test libcockatrice_settings Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES}
|
NAME
|
||||||
|
settings_defaults_test
|
||||||
|
SOURCES
|
||||||
|
settings_defaults_test.cpp
|
||||||
|
LIBS
|
||||||
|
libcockatrice_settings
|
||||||
|
DEFINITIONS
|
||||||
|
SETTINGS_UNIT_TEST
|
||||||
)
|
)
|
||||||
|
|
||||||
target_link_libraries(
|
cockatrice_add_test(NAME settings_migration_test SOURCES settings_migration_test.cpp LIBS libcockatrice_settings)
|
||||||
settings_defaults_test libcockatrice_settings Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES}
|
|
||||||
)
|
|
||||||
target_compile_definitions(settings_defaults_test PRIVATE SETTINGS_UNIT_TEST)
|
|
||||||
|
|
||||||
target_link_libraries(
|
|
||||||
settings_migration_test libcockatrice_settings Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES}
|
|
||||||
)
|
|
||||||
|
|
||||||
add_test(NAME settings_manager_test COMMAND settings_manager_test)
|
|
||||||
add_test(NAME settings_defaults_test COMMAND settings_defaults_test)
|
|
||||||
add_test(NAME settings_migration_test COMMAND settings_migration_test)
|
|
||||||
|
|
||||||
if(NOT GTEST_FOUND)
|
|
||||||
add_dependencies(settings_manager_test gtest)
|
|
||||||
add_dependencies(settings_defaults_test gtest)
|
|
||||||
add_dependencies(settings_migration_test gtest)
|
|
||||||
endif()
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
#include "libcockatrice/card/card_localization.h"
|
#include "libcockatrice/card/card_localization.h"
|
||||||
|
|
||||||
#include "gtest/gtest.h"
|
#include "gtest/gtest.h"
|
||||||
|
#include <QDir>
|
||||||
#include <QHash>
|
#include <QHash>
|
||||||
#include <QList>
|
#include <QList>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
|
|
@ -15,6 +16,7 @@
|
||||||
#include <libcockatrice/settings/game_settings.h>
|
#include <libcockatrice/settings/game_settings.h>
|
||||||
#include <libcockatrice/settings/interface_settings.h>
|
#include <libcockatrice/settings/interface_settings.h>
|
||||||
#include <libcockatrice/settings/network_settings.h>
|
#include <libcockatrice/settings/network_settings.h>
|
||||||
|
#include <libcockatrice/settings/paths_settings.h>
|
||||||
#include <libcockatrice/settings/personal_settings.h>
|
#include <libcockatrice/settings/personal_settings.h>
|
||||||
#include <libcockatrice/settings/sound_settings.h>
|
#include <libcockatrice/settings/sound_settings.h>
|
||||||
#include <libcockatrice/settings/tabs_settings.h>
|
#include <libcockatrice/settings/tabs_settings.h>
|
||||||
|
|
@ -695,6 +697,55 @@ TEST_F(SettingsDefaultsTest, VisualDeckStorage_ShowUploadTime_SetAndGet)
|
||||||
ASSERT_EQ(s.getVisualDeckStorageShowUploadTime(), false);
|
ASSERT_EQ(s.getVisualDeckStorageShowUploadTime(), false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_F(SettingsDefaultsTest, Paths_AppDirWarningAcknowledged_Default)
|
||||||
|
{
|
||||||
|
PathsSettings s(settingsPath, nullptr);
|
||||||
|
ASSERT_EQ(s.getAppDirWarningAcknowledged(), false);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(SettingsDefaultsTest, Paths_AppDirWarningAcknowledged_SetAndGet)
|
||||||
|
{
|
||||||
|
PathsSettings s(settingsPath, nullptr);
|
||||||
|
s.setAppDirWarningAcknowledged(true);
|
||||||
|
ASSERT_EQ(s.getAppDirWarningAcknowledged(), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(SettingsDefaultsTest, Paths_PathsInsideDir_DetectsNestedAndIgnoresSiblings)
|
||||||
|
{
|
||||||
|
PathsSettings s(settingsPath, nullptr);
|
||||||
|
const QString base = QDir::cleanPath(settingsPath);
|
||||||
|
|
||||||
|
s.setDeckPath(base + "/decks/");
|
||||||
|
s.setPicsPath(base + "/pics");
|
||||||
|
s.setReplaysPath(base + "2"); // sibling sharing the name prefix, not nested
|
||||||
|
|
||||||
|
const QStringList offenders = s.pathsInsideDir(base);
|
||||||
|
ASSERT_EQ(offenders.size(), 2);
|
||||||
|
ASSERT_TRUE(offenders.contains(base + "/decks"));
|
||||||
|
ASSERT_TRUE(offenders.contains(base + "/pics"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(SettingsDefaultsTest, Paths_PathsInsideDir_EmptyWhenNothingMatches)
|
||||||
|
{
|
||||||
|
PathsSettings s(settingsPath, nullptr);
|
||||||
|
s.setDeckPath("/somewhere/else/decks");
|
||||||
|
ASSERT_TRUE(s.pathsInsideDir(QDir::cleanPath(settingsPath)).isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(SettingsDefaultsTest, Paths_PathsInsideDir_MatchesExactBaseAndNormalizesDots)
|
||||||
|
{
|
||||||
|
PathsSettings s(settingsPath, nullptr);
|
||||||
|
const QString base = QDir::cleanPath(settingsPath);
|
||||||
|
|
||||||
|
s.setCustomPicsPath(base); // exactly the base directory
|
||||||
|
s.setThemesPath(base + "/pics/../decks"); // needs cleanPath normalization
|
||||||
|
|
||||||
|
const QStringList offenders = s.pathsInsideDir(base);
|
||||||
|
ASSERT_EQ(offenders.size(), 2);
|
||||||
|
ASSERT_TRUE(offenders.contains(base));
|
||||||
|
ASSERT_TRUE(offenders.contains(base + "/decks"));
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
int main(int argc, char **argv)
|
int main(int argc, char **argv)
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
add_executable(
|
cockatrice_add_test(
|
||||||
|
NAME
|
||||||
theme_migration_test
|
theme_migration_test
|
||||||
|
SOURCES
|
||||||
${CMAKE_SOURCE_DIR}/cockatrice/src/interface/theme_manager.cpp
|
${CMAKE_SOURCE_DIR}/cockatrice/src/interface/theme_manager.cpp
|
||||||
${CMAKE_SOURCE_DIR}/cockatrice/src/interface/theme_config.cpp
|
${CMAKE_SOURCE_DIR}/cockatrice/src/interface/theme_config.cpp
|
||||||
${CMAKE_SOURCE_DIR}/cockatrice/src/interface/pixel_map_generator.cpp
|
${CMAKE_SOURCE_DIR}/cockatrice/src/interface/pixel_map_generator.cpp
|
||||||
|
|
@ -9,22 +11,11 @@ add_executable(
|
||||||
${CMAKE_SOURCE_DIR}/cockatrice/src/client/network/update/client/release_channel.cpp
|
${CMAKE_SOURCE_DIR}/cockatrice/src/client/network/update/client/release_channel.cpp
|
||||||
${VERSION_STRING_CPP}
|
${VERSION_STRING_CPP}
|
||||||
theme_migration_test.cpp
|
theme_migration_test.cpp
|
||||||
)
|
LIBS
|
||||||
target_include_directories(theme_migration_test PRIVATE ${CMAKE_SOURCE_DIR}/cockatrice/src)
|
|
||||||
|
|
||||||
target_link_libraries(
|
|
||||||
theme_migration_test
|
|
||||||
libcockatrice_settings
|
libcockatrice_settings
|
||||||
libcockatrice_network
|
libcockatrice_network
|
||||||
libcockatrice_network_server_remote
|
libcockatrice_network_server_remote
|
||||||
libcockatrice_protocol
|
libcockatrice_protocol
|
||||||
Threads::Threads
|
INCLUDE_DIRS
|
||||||
${GTEST_BOTH_LIBRARIES}
|
${CMAKE_SOURCE_DIR}/cockatrice/src
|
||||||
${TEST_QT_MODULES}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
add_test(NAME theme_migration_test COMMAND theme_migration_test)
|
|
||||||
|
|
||||||
if(NOT GTEST_FOUND)
|
|
||||||
add_dependencies(theme_migration_test gtest)
|
|
||||||
endif()
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue