mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-11-05 09:33:25 -08:00
Compare commits
22 commits
e6847b0813
...
7c8b696f9d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7c8b696f9d | ||
|
|
12e03099c8 | ||
|
|
8519082124 | ||
|
|
7c03e4467a | ||
|
|
9f30892dff | ||
|
|
8b0c980e85 | ||
|
|
62e590d300 | ||
|
|
a1a8a59f4a | ||
|
|
94e6281d87 | ||
|
|
3cf8bdb00e | ||
|
|
5134e72eb4 | ||
|
|
b37c8b828f | ||
|
|
719863e43d | ||
|
|
d021f20d14 | ||
|
|
ce543896f5 | ||
|
|
429eb2a1fd | ||
|
|
a74dfeef6e | ||
|
|
5fe7f186d3 | ||
|
|
50a2ae9cc8 | ||
|
|
ef4574d2b9 | ||
|
|
e08402ac94 | ||
|
|
ff393b8990 |
151 changed files with 5555 additions and 1061 deletions
|
|
@ -311,16 +311,24 @@ if [[ $RUNNER_OS == macOS ]]; then
|
|||
|
||||
if [[ $MAKE_PACKAGE ]]; then
|
||||
# Workaround https://github.com/actions/runner-images/issues/7522
|
||||
# have hdiutil repeat the command 10 times in hope of success
|
||||
# have hdiutil repeat the command 10 times in hope of success.
|
||||
# The wrapper has to exit non-zero once it gives up: CPack reads that status to
|
||||
# decide whether the disk image was created, so a zero exit turns a failed hdiutil
|
||||
# into a much later, misleading "Problem copying the package" error that hides
|
||||
# hdiutil's own message.
|
||||
hdiutil_script="/tmp/hdiutil.sh"
|
||||
# shellcheck disable=SC2016
|
||||
echo '#!/bin/bash
|
||||
attempts=10
|
||||
i=0
|
||||
while ! hdiutil "$@"; do
|
||||
if (( ++i >= 10 )); then
|
||||
echo "Error: hdiutil failed $i times!" >&2
|
||||
break
|
||||
while true; do
|
||||
hdiutil "$@" && exit 0
|
||||
rc=$?
|
||||
if (( ++i >= attempts )); then
|
||||
echo "::error::hdiutil $1 failed $i times (last status $rc), giving up" >&2
|
||||
exit "$rc"
|
||||
fi
|
||||
echo "hdiutil $1 attempt $i/$attempts failed (status $rc), retrying" >&2
|
||||
sleep 1
|
||||
done' >"$hdiutil_script"
|
||||
chmod +x "$hdiutil_script"
|
||||
|
|
|
|||
32
.github/workflows/desktop-build.yml
vendored
32
.github/workflows/desktop-build.yml
vendored
|
|
@ -101,6 +101,36 @@ jobs:
|
|||
--title "$release_name" \
|
||||
--notes-file "$body_path"
|
||||
|
||||
vcpkg-baseline:
|
||||
name: vcpkg baseline
|
||||
runs-on: ubuntu-slim
|
||||
steps:
|
||||
- name: "Checkout"
|
||||
uses: actions/checkout@v7
|
||||
|
||||
# vcpkg-tool reads versions/baseline.json out of the commit named by
|
||||
# `builtin-baseline`, through the `vcpkg` submodule's git history. A submodule
|
||||
# pointer that lags the manifest - e.g. a stale `vcpkg/` worktree captured by
|
||||
# `git add` in a PR - makes every vcpkg leg fail in configure with "failed to
|
||||
# `git show` versions/baseline.json". That reads like a baseline or network
|
||||
# problem, when the pointer is what has to move. Check the two agree here in
|
||||
# seconds instead of on five build legs.
|
||||
- name: "Check the submodule pointer against the manifest baseline"
|
||||
shell: bash
|
||||
run: |
|
||||
pointer="$(git rev-parse HEAD:vcpkg 2>/dev/null || true)"
|
||||
baseline="$(sed -nE 's/.*"builtin-baseline"[[:space:]]*:[[:space:]]*"([0-9a-f]{40})".*/\1/p' vcpkg.json)"
|
||||
if [[ -z "$pointer" || -z "$baseline" ]]; then
|
||||
echo "::error file=vcpkg.json::could not read the vcpkg submodule pointer or the builtin-baseline"
|
||||
exit 1
|
||||
fi
|
||||
if [[ "$pointer" != "$baseline" ]]; then
|
||||
echo "::error file=vcpkg.json::vcpkg submodule is at $pointer but builtin-baseline is $baseline"
|
||||
echo "Move them together: git -C vcpkg checkout $baseline && git add vcpkg"
|
||||
exit 1
|
||||
fi
|
||||
echo "vcpkg submodule and builtin-baseline agree on $baseline"
|
||||
|
||||
build-linux:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
|
|
@ -466,7 +496,7 @@ jobs:
|
|||
vcpkg_upload: 1 # only one leg per vcpkg_scope hands its archives over
|
||||
|
||||
name: ${{ matrix.os }} ${{ matrix.target }}${{ matrix.soc == 'Intel' && ' Intel' || '' }}${{ matrix.type == 'Debug' && ' Debug' || '' }}
|
||||
needs: configure
|
||||
needs: [configure, vcpkg-baseline]
|
||||
runs-on: ${{ matrix.runner }}
|
||||
timeout-minutes: 100
|
||||
env:
|
||||
|
|
|
|||
3
.gitmodules
vendored
3
.gitmodules
vendored
|
|
@ -4,3 +4,6 @@
|
|||
[submodule "doxygen-awesome-css"]
|
||||
path = doc/doxygen/theme
|
||||
url = https://github.com/jothepro/doxygen-awesome-css.git
|
||||
[submodule "external/qttesting"]
|
||||
path = external/qttesting
|
||||
url = https://github.com/Kitware/QtTesting.git
|
||||
|
|
|
|||
|
|
@ -20,6 +20,8 @@ option(WITH_CLIENT "Build Cockatrice client" ON)
|
|||
option(WITH_ORACLE "Build Cockatrice card database tool (Oracle)" ON)
|
||||
# Compile Servatrice
|
||||
option(WITH_SERVER "Build Cockatrice server (Servatrice)" OFF)
|
||||
# Compile GUI tests (QtTesting widget-level playback)
|
||||
option(WITH_GUI_TEST "Build the QtTesting-based GUI test harness" OFF)
|
||||
# Compile tests
|
||||
option(TEST "Build tests" OFF)
|
||||
# Build with AddressSanitizer and UndefinedBehaviorSanitizer
|
||||
|
|
@ -29,6 +31,14 @@ option(ENABLE_COVERAGE "Build instrumented for GCC line coverage" OFF)
|
|||
# Use vcpkg regardless of OS
|
||||
option(USE_VCPKG "Use vcpkg regardless of OS" OFF)
|
||||
|
||||
# The GUI test harness lives in a submodule; a checkout without it must still configure.
|
||||
if(WITH_GUI_TEST AND NOT EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/external/qttesting/CMakeLists.txt")
|
||||
message(WARNING "WITH_GUI_TEST is ON but external/qttesting is missing, so GUI tests are disabled.\n"
|
||||
"Run 'git submodule update --init external/qttesting' to enable them."
|
||||
)
|
||||
set(WITH_GUI_TEST OFF)
|
||||
endif()
|
||||
|
||||
# Default to "Release" build type
|
||||
# User-provided value for CMAKE_BUILD_TYPE must be checked before the PROJECT() call
|
||||
if(DEFINED CMAKE_BUILD_TYPE)
|
||||
|
|
@ -90,7 +100,7 @@ endif()
|
|||
|
||||
# A project name is needed for CPack
|
||||
# Version can be overriden by git tags, see cmake/getversion.cmake
|
||||
project("Cockatrice" VERSION 3.1.2)
|
||||
project("Cockatrice" VERSION 3.2.0)
|
||||
|
||||
# Set release name if not provided via env/cmake var
|
||||
if(NOT DEFINED GIT_TAG_RELEASENAME)
|
||||
|
|
@ -259,7 +269,13 @@ else()
|
|||
add_compile_definitions(QT_DISABLE_DEPRECATED_BEFORE=0x060400)
|
||||
endif()
|
||||
|
||||
# Set once here for every target: these variables are inherited by the
|
||||
# subdirectories, so the per-directory copies were redundant. Only the
|
||||
# variables are set; qt_standard_project_setup would also change the runtime
|
||||
# output directories and the install RPATH.
|
||||
set(CMAKE_AUTOMOC TRUE)
|
||||
set(CMAKE_AUTOUIC TRUE)
|
||||
set(CMAKE_AUTORCC TRUE)
|
||||
|
||||
# Find other needed libraries
|
||||
find_package(Protobuf CONFIG)
|
||||
|
|
@ -455,6 +471,34 @@ if(WITH_ORACLE)
|
|||
set(CPACK_INSTALL_CMAKE_PROJECTS "Oracle;Oracle;ALL;/" ${CPACK_INSTALL_CMAKE_PROJECTS})
|
||||
endif()
|
||||
|
||||
if(WITH_GUI_TEST)
|
||||
set(QtTesting_QT_VERSION "6")
|
||||
# QtTesting registers its own unit tests when BUILD_TESTING is on. They target the
|
||||
# Qt version the harness was written for and fail on Qt 6.11 (toolbutton
|
||||
# long-press recording), and they are not Cockatrice's tests, so suppress them
|
||||
# for the duration of the subdirectory instead of painting ctest red.
|
||||
if(DEFINED BUILD_TESTING)
|
||||
set(_cockatrice_saved_build_testing ${BUILD_TESTING})
|
||||
endif()
|
||||
set(BUILD_TESTING
|
||||
OFF
|
||||
CACHE BOOL "Build the testing tree." FORCE
|
||||
)
|
||||
add_subdirectory(external/qttesting)
|
||||
if(DEFINED _cockatrice_saved_build_testing)
|
||||
set(BUILD_TESTING
|
||||
${_cockatrice_saved_build_testing}
|
||||
CACHE BOOL "Build the testing tree." FORCE
|
||||
)
|
||||
else()
|
||||
unset(BUILD_TESTING CACHE)
|
||||
endif()
|
||||
# cl.exe has no warnings-as-errors here, and it rejects -Wno-error with D8021.
|
||||
if(NOT MSVC)
|
||||
target_compile_options(qttesting PRIVATE -Wno-error)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if(TEST)
|
||||
include(CTest)
|
||||
add_subdirectory(tests)
|
||||
|
|
|
|||
52
Testing/README.md
Normal file
52
Testing/README.md
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
# GUI tests
|
||||
|
||||
Widget-level playback tests for the Cockatrice client, driven by
|
||||
[QtTesting](https://github.com/Kitware/QtTesting) (`external/qttesting`, a submodule).
|
||||
Each `*_test.xml` file is a list of events that QtTesting replays against the running
|
||||
application; a test passes when every event is handled without error.
|
||||
|
||||
## Running
|
||||
|
||||
```bash
|
||||
git submodule update --init external/qttesting # once
|
||||
cmake -S . -B build/debug -DCMAKE_BUILD_TYPE=Debug -DWITH_SERVER=0 -DWITH_GUI_TEST=ON
|
||||
cmake --build build/debug -j 24
|
||||
|
||||
./Testing/run_test.sh --all # headless, offscreen
|
||||
./Testing/run_test.sh Testing/smoke_test.xml # one file
|
||||
./Testing/run_test.sh --visual Testing/smoke_test.xml # shows the window
|
||||
```
|
||||
|
||||
`WITH_GUI_TEST` is on by default; a checkout without the submodule configures with a
|
||||
warning and no harness. `COCKATRICE_BIN` points the runner at another build, and
|
||||
`TEST_TIMEOUT` (seconds, default 180) bounds a single test — a failing event inside a
|
||||
modal dialog can otherwise leave the process alive. The client also hard-exits a few
|
||||
seconds after playback returns, whatever the result.
|
||||
|
||||
The tests are **not** part of `ctest`: they need a card database and a developer's
|
||||
settings, so they stay a local loop for now.
|
||||
|
||||
## Caveats
|
||||
|
||||
- **Not hermetic.** The client reads the local settings and card database, so a test can
|
||||
pass on one machine and fail on another. Avoid asserting things that restored
|
||||
workspace state or user preferences decide (for example the number of open tabs).
|
||||
- **English labels only.** Menu entries are addressed by their (translated) text, so the
|
||||
tests expect the source language. Widgets are addressed by objectName paths, which are
|
||||
stable across translations.
|
||||
- Actions that open a dialog are followed by a `pause` event: playback continues in the
|
||||
dialog's own event loop, and the pause gives the UI time to build itself.
|
||||
- Widget paths include Qt-internal names where a QTabWidget is involved
|
||||
(`qt_tabwidget_stackedwidget`), and the deck views in a game are indexed
|
||||
(`tabbedDeckView_0`, `tabbedDeckView_1`).
|
||||
|
||||
## Writing a test
|
||||
|
||||
A test is a `<events>` list. Widgets are named by their path from the main window, e.g.
|
||||
`MainWindow/tabSupervisor/stackedWidget/TabGame/phasesMenu`. Useful commands:
|
||||
|
||||
| Command | Arguments | Effect |
|
||||
|---------|-----------|--------|
|
||||
| `activate` | action or menu name | triggers a QAction (menus are opened first) |
|
||||
| `key` | Qt key code | presses that key on the focused widget |
|
||||
| `pause` | milliseconds | waits, letting the UI settle |
|
||||
4
Testing/deck_editor_open_test.xml
Normal file
4
Testing/deck_editor_open_test.xml
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<events>
|
||||
<!-- Open a new deck editor tab from the home page -->
|
||||
<event widget="MainWindow/tabSupervisor/stackedWidget/TabHome/homeWidget/homeButtonsBox/homeCreateNewDeckButton" command="activate" arguments="" />
|
||||
</events>
|
||||
14
Testing/deck_editor_search_test.xml
Normal file
14
Testing/deck_editor_search_test.xml
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
<events>
|
||||
<!-- Open a new visual deck editor tab from the home page -->
|
||||
<event widget="MainWindow/tabSupervisor/stackedWidget/TabHome/homeWidget/homeButtonsBox/homeCreateNewDeckButton" command="activate" arguments="" />
|
||||
<!-- Type "Lightning" into the search box (keycodes for L-i-g-h-t-n-i-n-g) -->
|
||||
<event widget="MainWindow/tabSupervisor/stackedWidget/TabDeckEditorVisual/databaseDisplayDock/databaseDisplayDockContents/databaseDisplayWidget/searchEdit" command="key" arguments="76" />
|
||||
<event widget="MainWindow/tabSupervisor/stackedWidget/TabDeckEditorVisual/databaseDisplayDock/databaseDisplayDockContents/databaseDisplayWidget/searchEdit" command="key" arguments="105" />
|
||||
<event widget="MainWindow/tabSupervisor/stackedWidget/TabDeckEditorVisual/databaseDisplayDock/databaseDisplayDockContents/databaseDisplayWidget/searchEdit" command="key" arguments="103" />
|
||||
<event widget="MainWindow/tabSupervisor/stackedWidget/TabDeckEditorVisual/databaseDisplayDock/databaseDisplayDockContents/databaseDisplayWidget/searchEdit" command="key" arguments="104" />
|
||||
<event widget="MainWindow/tabSupervisor/stackedWidget/TabDeckEditorVisual/databaseDisplayDock/databaseDisplayDockContents/databaseDisplayWidget/searchEdit" command="key" arguments="116" />
|
||||
<event widget="MainWindow/tabSupervisor/stackedWidget/TabDeckEditorVisual/databaseDisplayDock/databaseDisplayDockContents/databaseDisplayWidget/searchEdit" command="key" arguments="110" />
|
||||
<event widget="MainWindow/tabSupervisor/stackedWidget/TabDeckEditorVisual/databaseDisplayDock/databaseDisplayDockContents/databaseDisplayWidget/searchEdit" command="key" arguments="105" />
|
||||
<event widget="MainWindow/tabSupervisor/stackedWidget/TabDeckEditorVisual/databaseDisplayDock/databaseDisplayDockContents/databaseDisplayWidget/searchEdit" command="key" arguments="110" />
|
||||
<event widget="MainWindow/tabSupervisor/stackedWidget/TabDeckEditorVisual/databaseDisplayDock/databaseDisplayDockContents/databaseDisplayWidget/searchEdit" command="key" arguments="103" />
|
||||
</events>
|
||||
11
Testing/dialog_lifecycle_test.xml
Normal file
11
Testing/dialog_lifecycle_test.xml
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<events>
|
||||
<!-- Open and close Settings dialog (modal exec) -->
|
||||
<event widget="MainWindow/menubar" command="activate" arguments="cockatriceMenu" />
|
||||
<event widget="MainWindow/menubar/cockatriceMenu" command="activate" arguments="actionSettings" />
|
||||
<event widget="MainWindow/settingsDialog" command="key" arguments="16777216" />
|
||||
|
||||
<!-- Open and close About dialog (modal exec) -->
|
||||
<event widget="MainWindow/menubar" command="activate" arguments="helpMenu" />
|
||||
<event widget="MainWindow/menubar/helpMenu" command="activate" arguments="&About Cockatrice" />
|
||||
<event widget="MainWindow/aboutDialog" command="key" arguments="16777216" />
|
||||
</events>
|
||||
45
Testing/integration_test.xml
Normal file
45
Testing/integration_test.xml
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
<events>
|
||||
<!-- Integration test: exercises multiple tiers in sequence -->
|
||||
|
||||
<!-- Tier 2: Open and close all menus -->
|
||||
<event widget="MainWindow/menubar" command="activate" arguments="cockatriceMenu" />
|
||||
<event widget="MainWindow" command="key" arguments="16777216" />
|
||||
<event widget="MainWindow/menubar" command="activate" arguments="dbMenu" />
|
||||
<event widget="MainWindow" command="key" arguments="16777216" />
|
||||
<event widget="MainWindow/menubar" command="activate" arguments="tabsMenu" />
|
||||
<event widget="MainWindow" command="key" arguments="16777216" />
|
||||
<event widget="MainWindow/menubar" command="activate" arguments="helpMenu" />
|
||||
<event widget="MainWindow" command="key" arguments="16777216" />
|
||||
|
||||
<!-- Tier 2: Open and close About (modal) -->
|
||||
<event widget="MainWindow/menubar" command="activate" arguments="helpMenu" />
|
||||
<event widget="MainWindow/menubar/helpMenu" command="activate" arguments="&About Cockatrice" />
|
||||
<event widget="MainWindow/aboutDialog" command="key" arguments="16777216" />
|
||||
|
||||
<!-- Tier 4: Open and close ViewLog (non-modal) -->
|
||||
<event widget="MainWindow/menubar" command="activate" arguments="helpMenu" />
|
||||
<event widget="MainWindow/menubar/helpMenu" command="activate" arguments="View &Debug Log" />
|
||||
<event widget="MainWindow/viewLogDialog" command="key" arguments="16777216" />
|
||||
|
||||
<!-- Tier 4: Open and close Tip of the Day (non-modal) -->
|
||||
<event widget="MainWindow/menubar" command="activate" arguments="helpMenu" />
|
||||
<event widget="MainWindow/menubar/helpMenu" command="activate" arguments="&Tip of the Day" />
|
||||
<event widget="MainWindow/tipOfTheDayDialog" command="key" arguments="16777216" />
|
||||
|
||||
<!-- Tier 5: Settings search (state interaction) -->
|
||||
<event widget="MainWindow/menubar" command="activate" arguments="cockatriceMenu" />
|
||||
<event widget="MainWindow/menubar/cockatriceMenu" command="activate" arguments="actionSettings" />
|
||||
<event widget="MainWindow/settingsDialog/settingsView/settingsSearchEdit" command="key" arguments="100" />
|
||||
<event widget="MainWindow/settingsDialog/settingsView/settingsSearchEdit" command="key" arguments="101" />
|
||||
<event widget="MainWindow/settingsDialog/settingsView/settingsSearchEdit" command="key" arguments="98" />
|
||||
<event widget="MainWindow/settingsDialog/settingsView/settingsSearchEdit" command="key" arguments="117" />
|
||||
<event widget="MainWindow/settingsDialog/settingsOkButton" command="activate" arguments="" />
|
||||
|
||||
<!-- Tier 3: Open deck editor from home and type in search -->
|
||||
<event widget="MainWindow/tabSupervisor/stackedWidget/TabHome/homeWidget/homeButtonsBox/homeCreateNewDeckButton" command="activate" arguments="" />
|
||||
<event widget="MainWindow/tabSupervisor/stackedWidget/TabDeckEditorVisual/databaseDisplayDock/databaseDisplayDockContents/databaseDisplayWidget/searchEdit" command="key" arguments="76" />
|
||||
<event widget="MainWindow/tabSupervisor/stackedWidget/TabDeckEditorVisual/databaseDisplayDock/databaseDisplayDockContents/databaseDisplayWidget/searchEdit" command="key" arguments="105" />
|
||||
<event widget="MainWindow/tabSupervisor/stackedWidget/TabDeckEditorVisual/databaseDisplayDock/databaseDisplayDockContents/databaseDisplayWidget/searchEdit" command="key" arguments="103" />
|
||||
<event widget="MainWindow/tabSupervisor/stackedWidget/TabDeckEditorVisual/databaseDisplayDock/databaseDisplayDockContents/databaseDisplayWidget/searchEdit" command="key" arguments="104" />
|
||||
<event widget="MainWindow/tabSupervisor/stackedWidget/TabDeckEditorVisual/databaseDisplayDock/databaseDisplayDockContents/databaseDisplayWidget/searchEdit" command="key" arguments="116" />
|
||||
</events>
|
||||
19
Testing/menu_open_close_test.xml
Normal file
19
Testing/menu_open_close_test.xml
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
<events>
|
||||
<!-- Test 1: Open and close each top-level menu -->
|
||||
|
||||
<!-- Cockatrice menu -->
|
||||
<event widget="MainWindow/menubar" command="activate" arguments="cockatriceMenu" />
|
||||
<event widget="MainWindow" command="key" arguments="16777216" />
|
||||
|
||||
<!-- Database menu -->
|
||||
<event widget="MainWindow/menubar" command="activate" arguments="dbMenu" />
|
||||
<event widget="MainWindow" command="key" arguments="16777216" />
|
||||
|
||||
<!-- Tabs menu -->
|
||||
<event widget="MainWindow/menubar" command="activate" arguments="tabsMenu" />
|
||||
<event widget="MainWindow" command="key" arguments="16777216" />
|
||||
|
||||
<!-- Help menu -->
|
||||
<event widget="MainWindow/menubar" command="activate" arguments="helpMenu" />
|
||||
<event widget="MainWindow" command="key" arguments="16777216" />
|
||||
</events>
|
||||
6
Testing/nonmodal_manage_sets_test.xml
Normal file
6
Testing/nonmodal_manage_sets_test.xml
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
<events>
|
||||
<!-- Test non-modal dialog lifecycle: open and close Manage Sets (QMainWindow) -->
|
||||
<event widget="MainWindow/menubar" command="activate" arguments="dbMenu" />
|
||||
<event widget="MainWindow/menubar/dbMenu" command="activate" arguments="&Manage sets..." />
|
||||
<event widget="MainWindow/manageSetsWindow" command="key" arguments="16777216" />
|
||||
</events>
|
||||
6
Testing/nonmodal_tip_test.xml
Normal file
6
Testing/nonmodal_tip_test.xml
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
<events>
|
||||
<!-- Test non-modal dialog lifecycle: open and close Tip of the Day -->
|
||||
<event widget="MainWindow/menubar" command="activate" arguments="helpMenu" />
|
||||
<event widget="MainWindow/menubar/helpMenu" command="activate" arguments="&Tip of the Day" />
|
||||
<event widget="MainWindow/tipOfTheDayDialog" command="key" arguments="16777216" />
|
||||
</events>
|
||||
6
Testing/nonmodal_view_log_test.xml
Normal file
6
Testing/nonmodal_view_log_test.xml
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
<events>
|
||||
<!-- Test non-modal dialog lifecycle: open and close ViewLog -->
|
||||
<event widget="MainWindow/menubar" command="activate" arguments="helpMenu" />
|
||||
<event widget="MainWindow/menubar/helpMenu" command="activate" arguments="View &Debug Log" />
|
||||
<event widget="MainWindow/viewLogDialog" command="key" arguments="16777216" />
|
||||
</events>
|
||||
118
Testing/run_test.sh
Executable file
118
Testing/run_test.sh
Executable file
|
|
@ -0,0 +1,118 @@
|
|||
#!/bin/bash
|
||||
# Run QtTesting XML test files against the Cockatrice build.
|
||||
#
|
||||
# Usage:
|
||||
# ./Testing/run_test.sh [--visual] [--all | <test-file.xml> ...]
|
||||
#
|
||||
# Options:
|
||||
# --visual Run with native display (shows the GUI window).
|
||||
# Default is headless (QT_QPA_PLATFORM=offscreen).
|
||||
# --all Run every *.xml file in Testing/.
|
||||
#
|
||||
# Environment:
|
||||
# COCKATRICE_BIN Path to the client binary (default: build/debug/cockatrice/cockatrice,
|
||||
# or the .app bundle binary on macOS).
|
||||
# TEST_TIMEOUT Seconds a single test may run before it is killed (default: 180).
|
||||
# Uses timeout, or gtimeout from coreutils on macOS.
|
||||
#
|
||||
# Requires the debug build to exist at build/debug/cockatrice/cockatrice.
|
||||
|
||||
set -euo pipefail
|
||||
shopt -s nullglob
|
||||
|
||||
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
||||
if [ -n "${COCKATRICE_BIN:-}" ]; then
|
||||
COCKATRICE="$COCKATRICE_BIN"
|
||||
elif [ "$(uname -s)" = "Darwin" ]; then
|
||||
# A macOS build produces an app bundle, not a bare executable.
|
||||
COCKATRICE="$REPO_ROOT/build/debug/cockatrice/cockatrice.app/Contents/MacOS/cockatrice"
|
||||
else
|
||||
COCKATRICE="$REPO_ROOT/build/debug/cockatrice/cockatrice"
|
||||
fi
|
||||
TEST_DIR="$REPO_ROOT/Testing"
|
||||
TEST_TIMEOUT="${TEST_TIMEOUT:-180}"
|
||||
|
||||
# Stock macOS has no `timeout`; coreutils provides `gtimeout`. Without either the
|
||||
# runner still works, but a hung test is not killed.
|
||||
TIMEOUT=()
|
||||
if command -v timeout >/dev/null 2>&1; then
|
||||
TIMEOUT=(timeout --foreground "$TEST_TIMEOUT")
|
||||
elif command -v gtimeout >/dev/null 2>&1; then
|
||||
TIMEOUT=(gtimeout --foreground "$TEST_TIMEOUT")
|
||||
else
|
||||
echo "Warning: neither timeout nor gtimeout found; tests are not time-limited." >&2
|
||||
fi
|
||||
|
||||
if [ ! -f "$COCKATRICE" ]; then
|
||||
echo "Error: $COCKATRICE not found. Build first:" >&2
|
||||
echo " cmake --build build/debug" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
VISUAL=0
|
||||
if [ "${1:-}" = "--visual" ]; then
|
||||
VISUAL=1
|
||||
shift
|
||||
fi
|
||||
|
||||
if [ "${1:-}" = "--all" ]; then
|
||||
shift
|
||||
TEST_FILES=("$TEST_DIR"/*.xml)
|
||||
if [ ${#TEST_FILES[@]} -eq 0 ]; then
|
||||
echo "No *.xml files found in $TEST_DIR" >&2
|
||||
exit 1
|
||||
fi
|
||||
elif [ $# -ge 1 ]; then
|
||||
TEST_FILES=("$@")
|
||||
else
|
||||
echo "Usage: $0 [--visual] [--all | <test-file.xml> ...]" >&2
|
||||
echo " Run a single test: $0 Testing/smoke_test.xml" >&2
|
||||
echo " Run all tests: $0 --all" >&2
|
||||
echo " Run visually: $0 --visual Testing/smoke_test.xml" >&2
|
||||
echo " Run all visually: $0 --visual --all" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
PASS=0
|
||||
FAIL=0
|
||||
FAILED_TESTS=()
|
||||
|
||||
run_one() {
|
||||
local file="$1"
|
||||
|
||||
local -a cmd=()
|
||||
if [ "${#TIMEOUT[@]}" -gt 0 ]; then
|
||||
cmd=("${TIMEOUT[@]}")
|
||||
fi
|
||||
cmd+=("$COCKATRICE" --no-connect --test "$file")
|
||||
|
||||
if [ "$VISUAL" -eq 1 ]; then
|
||||
"${cmd[@]}"
|
||||
else
|
||||
env QT_QPA_PLATFORM=offscreen "${cmd[@]}"
|
||||
fi
|
||||
}
|
||||
|
||||
for f in "${TEST_FILES[@]}"; do
|
||||
name="$(basename "$f")"
|
||||
printf "%-40s " "$name"
|
||||
if run_one "$f"; then
|
||||
echo "PASS"
|
||||
PASS=$((PASS + 1))
|
||||
else
|
||||
echo "FAIL"
|
||||
FAIL=$((FAIL + 1))
|
||||
FAILED_TESTS+=("$name")
|
||||
fi
|
||||
done
|
||||
|
||||
TOTAL=$((PASS + FAIL))
|
||||
echo ""
|
||||
echo "Results: $PASS/$TOTAL passed, $FAIL failed"
|
||||
if [ "$FAIL" -gt 0 ]; then
|
||||
echo "Failed:"
|
||||
for t in "${FAILED_TESTS[@]}"; do
|
||||
echo " $t"
|
||||
done
|
||||
exit 1
|
||||
fi
|
||||
6
Testing/settings_dialog_test.xml
Normal file
6
Testing/settings_dialog_test.xml
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
<events>
|
||||
<!-- Open Settings dialog via Cockatrice menu, then close with Escape -->
|
||||
<event widget="MainWindow/menubar" command="activate" arguments="cockatriceMenu" />
|
||||
<event widget="MainWindow/menubar/cockatriceMenu" command="activate" arguments="actionSettings" />
|
||||
<event widget="MainWindow/settingsDialog" command="key" arguments="16777216" />
|
||||
</events>
|
||||
13
Testing/settings_search_test.xml
Normal file
13
Testing/settings_search_test.xml
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<events>
|
||||
<!-- Tier 5: State verification — open Settings, search for a page, close -->
|
||||
<event widget="MainWindow/menubar" command="activate" arguments="cockatriceMenu" />
|
||||
<event widget="MainWindow/menubar/cockatriceMenu" command="activate" arguments="actionSettings" />
|
||||
<!-- Type "sound" in the settings search to filter pages -->
|
||||
<event widget="MainWindow/settingsDialog/settingsView/settingsSearchEdit" command="key" arguments="115" />
|
||||
<event widget="MainWindow/settingsDialog/settingsView/settingsSearchEdit" command="key" arguments="111" />
|
||||
<event widget="MainWindow/settingsDialog/settingsView/settingsSearchEdit" command="key" arguments="117" />
|
||||
<event widget="MainWindow/settingsDialog/settingsView/settingsSearchEdit" command="key" arguments="110" />
|
||||
<event widget="MainWindow/settingsDialog/settingsView/settingsSearchEdit" command="key" arguments="100" />
|
||||
<!-- Close settings -->
|
||||
<event widget="MainWindow/settingsDialog/settingsOkButton" command="activate" arguments="" />
|
||||
</events>
|
||||
8
Testing/smoke_test.xml
Normal file
8
Testing/smoke_test.xml
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
<events>
|
||||
<!-- Open Help menu by activating it on the menu bar -->
|
||||
<event widget="MainWindow/menubar" command="activate" arguments="helpMenu" />
|
||||
<!-- Click About Cockatrice (opens modal QMessageBox) -->
|
||||
<event widget="MainWindow/menubar/helpMenu" command="activate" arguments="&About Cockatrice" />
|
||||
<!-- Close the modal dialog by pressing Escape (Qt::Key_Escape = 16777216) -->
|
||||
<event widget="MainWindow/aboutDialog" command="key" arguments="16777216" />
|
||||
</events>
|
||||
|
|
@ -171,6 +171,7 @@ set(cockatrice_SOURCES
|
|||
src/interface/widgets/cards/card_info_picture_widget.cpp
|
||||
src/interface/widgets/cards/card_info_picture_with_text_overlay_widget.cpp
|
||||
src/interface/widgets/cards/card_info_text_widget.cpp
|
||||
src/interface/widgets/cards/card_picture_delegate.cpp
|
||||
src/interface/widgets/cards/card_size_widget.cpp
|
||||
src/interface/widgets/cards/deck_card_zone_display_widget.cpp
|
||||
src/interface/widgets/cards/deck_preview_card_picture_widget.cpp
|
||||
|
|
@ -290,6 +291,7 @@ set(cockatrice_SOURCES
|
|||
src/interface/widgets/utility/sequence_edit.cpp
|
||||
src/interface/widgets/utility/visibility_change_listener.cpp
|
||||
src/interface/widgets/utility/visibility_change_listener.h
|
||||
src/interface/widgets/visual_database_display/card_database_visual_view.cpp
|
||||
src/interface/widgets/visual_database_display/visual_database_display_color_filter_widget.cpp
|
||||
src/interface/widgets/visual_database_display/visual_database_display_filter_save_load_widget.cpp
|
||||
src/interface/widgets/visual_database_display/visual_database_display_filter_toolbar_widget.cpp
|
||||
|
|
@ -562,6 +564,14 @@ target_link_libraries(
|
|||
|
||||
cockatrice_target_enable_crash_reporting(cockatrice)
|
||||
|
||||
if(WITH_GUI_TEST)
|
||||
# recording.cpp translates between XML test files and the QtTesting event pipeline;
|
||||
# it needs the QtTesting headers, so it only exists with the harness.
|
||||
target_sources(cockatrice PRIVATE src/interface/recording.cpp)
|
||||
target_link_libraries(cockatrice PRIVATE QtTesting)
|
||||
target_compile_definitions(cockatrice PRIVATE WITH_GUI_TEST)
|
||||
endif()
|
||||
|
||||
if(UNIX)
|
||||
if(APPLE)
|
||||
set(MACOSX_BUNDLE_INFO_STRING "${PROJECT_NAME}")
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
# or set <category> = false to disable logging
|
||||
|
||||
#main = true
|
||||
#recording = true
|
||||
#qt_translator = true
|
||||
#window_main.* = true
|
||||
#release_channel = true
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
#include "../../../interface/pixel_map_generator.h"
|
||||
#include "../../settings/cache_settings.h"
|
||||
#include "../interface/widgets/dialogs/dlg_connect.h"
|
||||
#include "../interface/widgets/dialogs/dlg_edit_password.h"
|
||||
#include "../interface/widgets/dialogs/dlg_forgot_password_challenge.h"
|
||||
#include "../interface/widgets/dialogs/dlg_forgot_password_request.h"
|
||||
#include "../interface/widgets/dialogs/dlg_forgot_password_reset.h"
|
||||
|
|
@ -99,6 +100,10 @@ void ConnectionController::wireClientSignals()
|
|||
|
||||
connect(remoteClient, &RemoteClient::sigPasswordVerifierReady, this,
|
||||
&ConnectionController::onPasswordVerifierReady);
|
||||
|
||||
connect(remoteClient, &RemoteClient::sigPasswordChangeSuccess, this,
|
||||
&ConnectionController::onPasswordChangeSuccess);
|
||||
connect(remoteClient, &RemoteClient::sigPasswordChangeError, this, &ConnectionController::onPasswordChangeError);
|
||||
}
|
||||
|
||||
void ConnectionController::connectToServer()
|
||||
|
|
@ -351,10 +356,16 @@ void ConnectionController::onLoginError(int r,
|
|||
}
|
||||
|
||||
case Response::RespPasswordChangeRequired: {
|
||||
QMessageBox::information(
|
||||
dialogParent, tr("Password Change Required"),
|
||||
tr("An administrator has reset your password. Please contact your server administrator to obtain "
|
||||
"your temporary password, then log in and change it via Account -> Change Password."));
|
||||
// The credentials were right, but the account may not be used until a new password
|
||||
// is chosen. Let the user complete the change here: sending them off to "ask the
|
||||
// administrator for your temporary password" sends them in circles, since they just
|
||||
// logged in with it and the session ends without a way to comply.
|
||||
DlgEditPassword dlg(dialogParent);
|
||||
dlg.setWindowTitle(tr("Password Change Required"));
|
||||
if (dlg.exec()) {
|
||||
remoteClient->changePasswordToServer(dlg.getOldPassword(), dlg.getNewPassword());
|
||||
return; // onPasswordChangeSuccess/onPasswordChangeError decides the next step
|
||||
}
|
||||
remoteClient->disconnectFromServer();
|
||||
return;
|
||||
}
|
||||
|
|
@ -517,6 +528,43 @@ void ConnectionController::onNotifyUserAboutUpdate()
|
|||
"To update your client, go to Help -> Check for Updates."));
|
||||
}
|
||||
|
||||
void ConnectionController::onPasswordChangeSuccess()
|
||||
{
|
||||
// The credential saved for this profile is stale now: drop it so the next login asks for
|
||||
// the new password instead of replaying the old verifier.
|
||||
if (!pendingSaveName.isEmpty()) {
|
||||
SettingsCache::instance().servers().setServerPassword(pendingSaveName, "");
|
||||
}
|
||||
|
||||
QMessageBox::information(dialogParent, tr("Password Changed"),
|
||||
tr("Your password has been changed. Please log in again with your new password."));
|
||||
remoteClient->disconnectFromServer();
|
||||
connectToServer();
|
||||
}
|
||||
|
||||
void ConnectionController::onPasswordChangeError(int responseCode)
|
||||
{
|
||||
QString reason;
|
||||
switch (static_cast<Response::ResponseCode>(responseCode)) {
|
||||
case Response::RespWrongPassword:
|
||||
reason = tr("The current password is incorrect.");
|
||||
break;
|
||||
case Response::RespPasswordTooShort:
|
||||
reason = tr("The new password is too short.");
|
||||
break;
|
||||
case Response::RespClientUpdateRequired:
|
||||
reason = tr("The server does not accept the new password format.");
|
||||
break;
|
||||
default:
|
||||
reason = tr("The password could not be changed (error %1).").arg(responseCode);
|
||||
break;
|
||||
}
|
||||
|
||||
QMessageBox::critical(dialogParent, tr("Password Change Failed"), reason);
|
||||
remoteClient->disconnectFromServer();
|
||||
connectToServer();
|
||||
}
|
||||
|
||||
void ConnectionController::onForgotPasswordSuccess()
|
||||
{
|
||||
QMessageBox::information(
|
||||
|
|
|
|||
|
|
@ -94,6 +94,10 @@ private slots:
|
|||
// Persists the derived scrypt verifier after a successful challenge-response login
|
||||
void onPasswordVerifierReady(const QString &verifier);
|
||||
|
||||
// Completes the forced password change the server demanded at login
|
||||
void onPasswordChangeSuccess();
|
||||
void onPasswordChangeError(int responseCode);
|
||||
|
||||
private:
|
||||
void wireClientSignals();
|
||||
void updateWindowTitle();
|
||||
|
|
|
|||
|
|
@ -11,6 +11,27 @@
|
|||
|
||||
class QObject;
|
||||
|
||||
namespace
|
||||
{
|
||||
/**
|
||||
* @brief Visits every filter item below @p node, depth first.
|
||||
*/
|
||||
template <typename Callback> void forEachFilter(const FilterTreeNode *node, const Callback &callback)
|
||||
{
|
||||
if (node == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (const auto *filterItem = dynamic_cast<const FilterItem *>(node)) {
|
||||
callback(filterItem);
|
||||
}
|
||||
|
||||
for (int i = 0; i < node->childCount(); ++i) {
|
||||
forEachFilter(node->nodeAt(i), callback);
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
FilterTreeModel::FilterTreeModel(QObject *parent) : QAbstractItemModel(parent)
|
||||
{
|
||||
fTree = new FilterTree;
|
||||
|
|
@ -110,46 +131,31 @@ void FilterTreeModel::clearFiltersOfType(CardFilter::Attr filterType)
|
|||
QList<const CardFilter *> FilterTreeModel::getFiltersOfType(CardFilter::Attr filterType) const
|
||||
{
|
||||
QList<const CardFilter *> filters;
|
||||
if (!fTree) {
|
||||
return filters;
|
||||
}
|
||||
|
||||
std::function<void(const FilterTreeNode *)> traverse;
|
||||
traverse = [&](const FilterTreeNode *node) {
|
||||
if (const auto *filterItem = dynamic_cast<const FilterItem *>(node)) {
|
||||
if (filterItem->attr() == filterType) {
|
||||
QString text = filterItem->text();
|
||||
filters.append(new CardFilter(text, filterItem->type(), filterItem->attr()));
|
||||
}
|
||||
forEachFilter(fTree, [&](const FilterItem *filterItem) {
|
||||
if (filterItem->attr() == filterType) {
|
||||
filters.append(new CardFilter(filterItem->text(), filterItem->type(), filterItem->attr()));
|
||||
}
|
||||
for (int i = 0; i < node->childCount(); ++i) {
|
||||
traverse(node->nodeAt(i));
|
||||
}
|
||||
};
|
||||
|
||||
traverse(fTree);
|
||||
});
|
||||
return filters;
|
||||
}
|
||||
|
||||
QStringList FilterTreeModel::getFilterTermsOfType(CardFilter::Attr filterType) const
|
||||
{
|
||||
QStringList terms;
|
||||
forEachFilter(fTree, [&](const FilterItem *filterItem) {
|
||||
if (filterItem->attr() == filterType) {
|
||||
terms.append(filterItem->text());
|
||||
}
|
||||
});
|
||||
return terms;
|
||||
}
|
||||
|
||||
QList<const CardFilter *> FilterTreeModel::allFilters() const
|
||||
{
|
||||
QList<const CardFilter *> filters;
|
||||
if (!fTree) {
|
||||
return filters;
|
||||
}
|
||||
|
||||
std::function<void(const FilterTreeNode *)> traverse;
|
||||
traverse = [&](const FilterTreeNode *node) {
|
||||
if (const auto *filterItem = dynamic_cast<const FilterItem *>(node)) {
|
||||
QString text = filterItem->text();
|
||||
filters.append(new CardFilter(text, filterItem->type(), filterItem->attr()));
|
||||
}
|
||||
for (int i = 0; i < node->childCount(); ++i) {
|
||||
traverse(node->nodeAt(i));
|
||||
}
|
||||
};
|
||||
|
||||
traverse(fTree);
|
||||
forEachFilter(fTree, [&](const FilterItem *filterItem) {
|
||||
filters.append(new CardFilter(filterItem->text(), filterItem->type(), filterItem->attr()));
|
||||
});
|
||||
return filters;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -28,7 +28,14 @@ public slots:
|
|||
void addFilter(const CardFilter *f);
|
||||
void removeFilter(const CardFilter *f);
|
||||
void clearFiltersOfType(CardFilter::Attr filterType);
|
||||
/**
|
||||
* @brief Copies every active filter of one attribute into new CardFilter objects.
|
||||
*
|
||||
* The caller owns the returned filters. Prefer getFilterTermsOfType() when only
|
||||
* the terms are needed.
|
||||
*/
|
||||
[[nodiscard]] QList<const CardFilter *> getFiltersOfType(CardFilter::Attr filterType) const;
|
||||
/// @brief Copies every active filter into new CardFilter objects. The caller owns them.
|
||||
[[nodiscard]] QList<const CardFilter *> allFilters() const;
|
||||
|
||||
private slots:
|
||||
|
|
@ -48,6 +55,13 @@ public:
|
|||
{
|
||||
return fTree;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief The texts of every active filter of one attribute, without allocating filters.
|
||||
*
|
||||
* Prefer this over getFiltersOfType() when only the filter terms are needed.
|
||||
*/
|
||||
[[nodiscard]] QStringList getFilterTermsOfType(CardFilter::Attr filterType) const;
|
||||
[[nodiscard]] int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||
[[nodiscard]] int columnCount(const QModelIndex & /*parent*/ = QModelIndex()) const override;
|
||||
[[nodiscard]] QVariant data(const QModelIndex &index, int role) const override;
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
#include <QBrush>
|
||||
#include <QColor>
|
||||
#include <QCursor>
|
||||
#include <QGraphicsScene>
|
||||
#include <QGraphicsSceneMouseEvent>
|
||||
#include <QPainter>
|
||||
#include <QPainterPath>
|
||||
|
|
@ -51,7 +52,20 @@ AbstractCardDragItem::AbstractCardDragItem(AbstractCardItem *_item,
|
|||
update();
|
||||
});
|
||||
|
||||
connect(item, &QObject::destroyed, this, &AbstractCardDragItem::deleteLater);
|
||||
// The source card can be destroyed mid-drag (e.g. moved by a server event). deleteLater() alone is not enough:
|
||||
// a scene repaint can run before the deferred delete and paint the dead card. Detach from the scene immediately.
|
||||
connect(_item, &QObject::destroyed, this, [this]() {
|
||||
hide();
|
||||
if (QGraphicsScene *sc = scene()) {
|
||||
sc->removeItem(this);
|
||||
}
|
||||
deleteLater();
|
||||
});
|
||||
}
|
||||
|
||||
AbstractCardItem *AbstractCardDragItem::getItem() const
|
||||
{
|
||||
return item.data();
|
||||
}
|
||||
|
||||
QPainterPath AbstractCardDragItem::shape() const
|
||||
|
|
@ -65,6 +79,10 @@ QPainterPath AbstractCardDragItem::shape() const
|
|||
|
||||
void AbstractCardDragItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget)
|
||||
{
|
||||
if (item.isNull()) {
|
||||
return;
|
||||
}
|
||||
|
||||
item->paint(painter, option, widget);
|
||||
|
||||
// adds a mask to the card so it looks like the card hasnt been placed yet
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@
|
|||
#include <QList>
|
||||
#include <QObject>
|
||||
#include <QPointF>
|
||||
#include <QPointer>
|
||||
#include <QRectF>
|
||||
#include <qtmetamacros.h>
|
||||
|
||||
|
|
@ -24,7 +25,8 @@ class AbstractCardDragItem : public QObject, public QGraphicsItem
|
|||
Q_OBJECT
|
||||
Q_INTERFACES(QGraphicsItem)
|
||||
protected:
|
||||
AbstractCardItem *item;
|
||||
/// Guarded pointer: the source card may be destroyed while this drag item still exists.
|
||||
QPointer<AbstractCardItem> item;
|
||||
QPointF hotSpot;
|
||||
QList<AbstractCardDragItem *> childDrags;
|
||||
|
||||
|
|
@ -44,10 +46,8 @@ public:
|
|||
}
|
||||
[[nodiscard]] QPainterPath shape() const override;
|
||||
void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override;
|
||||
[[nodiscard]] AbstractCardItem *getItem() const
|
||||
{
|
||||
return item;
|
||||
}
|
||||
/// @return the source card, or nullptr if it has already been destroyed.
|
||||
[[nodiscard]] AbstractCardItem *getItem() const;
|
||||
[[nodiscard]] QPointF getHotSpot() const
|
||||
{
|
||||
return hotSpot;
|
||||
|
|
|
|||
|
|
@ -130,8 +130,9 @@ void CardDragItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
|
|||
sc->removeItem(this);
|
||||
|
||||
QList<CardDragItem *> dragItemList;
|
||||
CardZoneLogic *startZone = static_cast<CardItem *>(item)->getZone();
|
||||
if (currentZone && !(static_cast<CardItem *>(item)->getAttachedTo() && (startZone == currentZone->getLogic()))) {
|
||||
CardZoneLogic *startZone = static_cast<CardItem *>(item.data())->getZone();
|
||||
if (currentZone &&
|
||||
!(static_cast<CardItem *>(item.data())->getAttachedTo() && (startZone == currentZone->getLogic()))) {
|
||||
if (!occupied) {
|
||||
dragItemList.append(this);
|
||||
}
|
||||
|
|
@ -139,7 +140,7 @@ void CardDragItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
|
|||
for (int i = 0; i < childDrags.size(); i++) {
|
||||
CardDragItem *c = static_cast<CardDragItem *>(childDrags[i]);
|
||||
if (!occupied &&
|
||||
!(static_cast<CardItem *>(c->item)->getAttachedTo() && (startZone == currentZone->getLogic())) &&
|
||||
!(static_cast<CardItem *>(c->item.data())->getAttachedTo() && (startZone == currentZone->getLogic())) &&
|
||||
!c->occupied) {
|
||||
dragItemList.append(c);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@ void DeckViewCardDragItem::updatePosition(const QPointF &cursorScenePos)
|
|||
|
||||
void DeckViewCardDragItem::handleDrop(DeckViewCardContainer *target)
|
||||
{
|
||||
auto *card = static_cast<DeckViewCard *>(item);
|
||||
auto *card = static_cast<DeckViewCard *>(item.data());
|
||||
auto *start = static_cast<DeckViewCardContainer *>(item->parentItem());
|
||||
start->removeCard(card);
|
||||
target->addCard(card);
|
||||
|
|
|
|||
|
|
@ -99,9 +99,13 @@ void ZoneViewZone::initializeCards(const QList<const ServerInfo_Card *> &cardLis
|
|||
}
|
||||
reorganizeCards();
|
||||
} else if (!qobject_cast<ZoneViewZoneLogic *>(getLogic())->getOriginalZone()->contentsKnown()) {
|
||||
PendingCommand *pend = qobject_cast<ZoneViewZoneLogic *>(getLogic())->requestZoneDump();
|
||||
auto *viewLogic = qobject_cast<ZoneViewZoneLogic *>(getLogic());
|
||||
PendingCommand *pend = viewLogic->prepareZoneDump();
|
||||
if (pend != nullptr) {
|
||||
// A local game answers synchronously, so the handler has to be connected before the
|
||||
// command is sent or the response would be delivered to nobody.
|
||||
connect(pend, &PendingCommand::finished, this, &ZoneViewZone::zoneDumpReceived);
|
||||
viewLogic->sendZoneDump(pend);
|
||||
}
|
||||
} else {
|
||||
const CardList &c = qobject_cast<ZoneViewZoneLogic *>(getLogic())->getOriginalZone()->getCards();
|
||||
|
|
|
|||
|
|
@ -238,6 +238,7 @@ void CardPictureLoader::imageLoaded(const ExactCard &card, const QImage &image)
|
|||
});
|
||||
|
||||
card.emitPixmapUpdated();
|
||||
emit getInstance().pixmapLoaded(card);
|
||||
}
|
||||
|
||||
void CardPictureLoader::deleteAllLocalOverrides(const ExactCard &card)
|
||||
|
|
|
|||
|
|
@ -146,6 +146,14 @@ signals:
|
|||
/** @brief Emitted after the worker has finished clearing the network and redirect caches. */
|
||||
void networkCacheCleared();
|
||||
|
||||
/**
|
||||
* @brief Emitted once an image has been loaded and put into the pixmap cache.
|
||||
*
|
||||
* Views that paint cards from the loader without owning a widget per card can
|
||||
* listen to this to repaint the rows whose picture just became available.
|
||||
*/
|
||||
void pixmapLoaded(const ExactCard &card);
|
||||
|
||||
private slots:
|
||||
/**
|
||||
* @brief Triggered when the user changes the picture download settings.
|
||||
|
|
|
|||
|
|
@ -27,9 +27,9 @@ void IntentGetLoginCredentials::onPreconditionSatisfied()
|
|||
ServersSettings &servers = SettingsCache::instance().servers();
|
||||
const int index = servers.findServerIndex(context->hostname, context->port);
|
||||
|
||||
if (index >= 0) {
|
||||
context->username = servers.getValue(QString("username%1").arg(index), "server", "server_details").toString();
|
||||
context->password = servers.getValue(QString("password%1").arg(index), "server", "server_details").toString();
|
||||
if (const auto server = servers.getServer(index)) {
|
||||
context->username = server->username;
|
||||
context->password = server->password;
|
||||
emitFinished();
|
||||
} else {
|
||||
emitFailed(tr("No saved credentials for this server"));
|
||||
|
|
@ -69,9 +69,8 @@ void IntentGetLoginCredentials::onPreconditionNotSatisfied()
|
|||
// it with the raw hostname when addNewServer updates the entry in place.
|
||||
QString saveName = context->hostname;
|
||||
const int existingIndex = servers.findServerIndex(context->hostname, context->port);
|
||||
if (existingIndex >= 0) {
|
||||
saveName =
|
||||
servers.getValue(QString("saveName%1").arg(existingIndex), "server", "server_details").toString();
|
||||
if (const auto existing = servers.getServer(existingIndex)) {
|
||||
saveName = existing->saveName;
|
||||
if (saveName.isEmpty()) {
|
||||
saveName = context->hostname;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -418,12 +418,10 @@ void IntentUrlParser::restoreToPreviousServer(const PendingIntentChain &chain)
|
|||
|
||||
ServersSettings &servers = SettingsCache::instance().servers();
|
||||
const int index = servers.findServerIndex(chain.previousServerHost, chain.previousServerPort);
|
||||
if (index >= 0 && servers.hasLoginData(chain.previousServerHost, chain.previousServerPort)) {
|
||||
const QString username =
|
||||
servers.getValue(QString("username%1").arg(index), "server", "server_details").toString();
|
||||
const QString password =
|
||||
servers.getValue(QString("password%1").arg(index), "server", "server_details").toString();
|
||||
client->connectToServer(chain.previousServerHost, chain.previousServerPort.toUInt(), username, password);
|
||||
const auto server = servers.getServer(index);
|
||||
if (server && servers.hasLoginData(chain.previousServerHost, chain.previousServerPort)) {
|
||||
client->connectToServer(chain.previousServerHost, chain.previousServerPort.toUInt(), server->username,
|
||||
server->password);
|
||||
return;
|
||||
}
|
||||
client->disconnectFromServer();
|
||||
|
|
@ -438,11 +436,9 @@ void IntentUrlParser::restoreToPreviousServer(const PendingIntentChain &chain)
|
|||
// when credentials are saved, otherwise stay offline.
|
||||
ServersSettings &servers = SettingsCache::instance().servers();
|
||||
const int index = servers.findServerIndex(chain.previousServerHost, chain.previousServerPort);
|
||||
if (index >= 0 && servers.hasLoginData(chain.previousServerHost, chain.previousServerPort)) {
|
||||
const QString username =
|
||||
servers.getValue(QString("username%1").arg(index), "server", "server_details").toString();
|
||||
const QString password =
|
||||
servers.getValue(QString("password%1").arg(index), "server", "server_details").toString();
|
||||
client->connectToServer(chain.previousServerHost, chain.previousServerPort.toUInt(), username, password);
|
||||
const auto server = servers.getServer(index);
|
||||
if (server && servers.hasLoginData(chain.previousServerHost, chain.previousServerPort)) {
|
||||
client->connectToServer(chain.previousServerHost, chain.previousServerPort.toUInt(), server->username,
|
||||
server->password);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
102
cockatrice/src/interface/recording.cpp
Normal file
102
cockatrice/src/interface/recording.cpp
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
#include "recording.h"
|
||||
|
||||
#include "pqEventTypes.h"
|
||||
|
||||
#include <QFile>
|
||||
#include <QTextStream>
|
||||
#include <QXmlStreamReader>
|
||||
#include <QXmlStreamWriter>
|
||||
|
||||
XMLEventObserver::XMLEventObserver(QObject *parent) : pqEventObserver(parent)
|
||||
{
|
||||
}
|
||||
|
||||
XMLEventObserver::~XMLEventObserver() = default;
|
||||
|
||||
void XMLEventObserver::setStream(QTextStream *stream)
|
||||
{
|
||||
if (xmlStream) {
|
||||
xmlStream->writeEndElement();
|
||||
xmlStream->writeEndDocument();
|
||||
xmlStream.reset();
|
||||
}
|
||||
if (Stream) {
|
||||
*Stream << xmlString;
|
||||
}
|
||||
xmlString.clear();
|
||||
pqEventObserver::setStream(stream);
|
||||
if (Stream) {
|
||||
xmlStream.reset(new QXmlStreamWriter(&xmlString));
|
||||
xmlStream->setAutoFormatting(true);
|
||||
xmlStream->writeStartDocument();
|
||||
xmlStream->writeStartElement(QStringLiteral("events"));
|
||||
}
|
||||
}
|
||||
|
||||
void XMLEventObserver::onRecordEvent(const QString &widget,
|
||||
const QString &command,
|
||||
const QString &arguments,
|
||||
const int &eventType)
|
||||
{
|
||||
if (!xmlStream) {
|
||||
return;
|
||||
}
|
||||
|
||||
xmlStream->writeStartElement(QStringLiteral("event"));
|
||||
xmlStream->writeAttribute(QStringLiteral("widget"), widget);
|
||||
if (eventType == pqEventTypes::ACTION_EVENT) {
|
||||
xmlStream->writeAttribute(QStringLiteral("command"), command);
|
||||
} else { // pqEventTypes::CHECK_EVENT
|
||||
xmlStream->writeAttribute(QStringLiteral("property"), command);
|
||||
}
|
||||
xmlStream->writeAttribute(QStringLiteral("arguments"), arguments);
|
||||
xmlStream->writeEndElement();
|
||||
}
|
||||
|
||||
XMLEventSource::XMLEventSource(QObject *parent) : pqEventSource(parent)
|
||||
{
|
||||
}
|
||||
|
||||
XMLEventSource::~XMLEventSource() = default;
|
||||
|
||||
void XMLEventSource::setContent(const QString &xmlFileName)
|
||||
{
|
||||
xmlStream.reset();
|
||||
|
||||
QFile xml(xmlFileName);
|
||||
if (!xml.open(QIODevice::ReadOnly)) {
|
||||
qCWarning(RecordingLog) << "Could not read test file" << xmlFileName;
|
||||
return;
|
||||
}
|
||||
|
||||
xmlStream.reset(new QXmlStreamReader(xml.readAll()));
|
||||
}
|
||||
|
||||
int XMLEventSource::getNextEvent(QString &widget, QString &command, QString &arguments, int &eventType)
|
||||
{
|
||||
if (!xmlStream) {
|
||||
return DONE;
|
||||
}
|
||||
|
||||
while (!xmlStream->atEnd()) {
|
||||
if (xmlStream->readNext() != QXmlStreamReader::StartElement || xmlStream->name() != QStringLiteral("event")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const QXmlStreamAttributes attributes = xmlStream->attributes();
|
||||
eventType = pqEventTypes::ACTION_EVENT;
|
||||
widget = attributes.value(QStringLiteral("widget")).toString();
|
||||
command = attributes.value(QStringLiteral("command")).toString();
|
||||
if (command.isEmpty()) {
|
||||
// A recorded check event carries a property instead of a command.
|
||||
command = attributes.value(QStringLiteral("property")).toString();
|
||||
}
|
||||
arguments = attributes.value(QStringLiteral("arguments")).toString();
|
||||
return SUCCESS;
|
||||
}
|
||||
|
||||
if (xmlStream->hasError()) {
|
||||
qCWarning(RecordingLog) << "Malformed test file:" << xmlStream->errorString();
|
||||
}
|
||||
return DONE;
|
||||
}
|
||||
54
cockatrice/src/interface/recording.h
Normal file
54
cockatrice/src/interface/recording.h
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
#ifndef COCKATRICE_RECORDING_H
|
||||
#define COCKATRICE_RECORDING_H
|
||||
|
||||
#include "pqEventObserver.h"
|
||||
#include "pqEventSource.h"
|
||||
|
||||
#include <QLoggingCategory>
|
||||
#include <QScopedPointer>
|
||||
|
||||
inline Q_LOGGING_CATEGORY(RecordingLog, "recording");
|
||||
|
||||
class QXmlStreamReader;
|
||||
class QXmlStreamWriter;
|
||||
|
||||
/**
|
||||
* @brief Writes a session recording as an XML test file.
|
||||
*
|
||||
* The events are buffered in memory and flushed to the stream when recording
|
||||
* ends, because the stream is handed over once the file name is known.
|
||||
*/
|
||||
class XMLEventObserver : public pqEventObserver
|
||||
{
|
||||
QScopedPointer<QXmlStreamWriter> xmlStream;
|
||||
QString xmlString;
|
||||
|
||||
public:
|
||||
explicit XMLEventObserver(QObject *parent);
|
||||
~XMLEventObserver() override;
|
||||
|
||||
protected:
|
||||
void setStream(QTextStream *stream) override;
|
||||
void onRecordEvent(const QString &widget,
|
||||
const QString &command,
|
||||
const QString &arguments,
|
||||
const int &eventType) override;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Plays the events of an XML test file back, one at a time.
|
||||
*/
|
||||
class XMLEventSource : public pqEventSource
|
||||
{
|
||||
QScopedPointer<QXmlStreamReader> xmlStream;
|
||||
|
||||
public:
|
||||
explicit XMLEventSource(QObject *parent);
|
||||
~XMLEventSource() override;
|
||||
|
||||
protected:
|
||||
void setContent(const QString &xmlFileName) override;
|
||||
int getNextEvent(QString &widget, QString &command, QString &arguments, int &eventType) override;
|
||||
};
|
||||
|
||||
#endif // COCKATRICE_RECORDING_H
|
||||
|
|
@ -43,11 +43,6 @@
|
|||
#include <qlogging.h>
|
||||
#include <qnamespace.h>
|
||||
|
||||
static constexpr qreal MTG_CARD_ASPECT_RATIO = 1.396;
|
||||
// static constexpr qreal YUGIOH_CARD_ASPECT_RATIO = 1.457;
|
||||
static constexpr qreal ASPECT_RATIO = MTG_CARD_ASPECT_RATIO;
|
||||
|
||||
static constexpr int BASE_WIDTH = 200;
|
||||
static constexpr int BASE_HEIGHT = 200;
|
||||
|
||||
static constexpr int ENLARGED_PIXMAP_OFFSET = 10;
|
||||
|
|
@ -162,12 +157,11 @@ void CardInfoPictureWidget::resizeEvent(QResizeEvent *event)
|
|||
*/
|
||||
void CardInfoPictureWidget::setScaleFactor(const int scale)
|
||||
{
|
||||
const int newWidth = BASE_WIDTH * scale / 100;
|
||||
const int newHeight = static_cast<int>(newWidth * ASPECT_RATIO);
|
||||
const QSize newSize = sizeForScaleFactor(scale);
|
||||
|
||||
scaleFactor = scale;
|
||||
|
||||
setFixedSize(newWidth, newHeight);
|
||||
setFixedSize(newSize);
|
||||
updatePixmap();
|
||||
|
||||
emit cardScaleFactorChanged(scale);
|
||||
|
|
@ -266,8 +260,13 @@ void CardInfoPictureWidget::paintEvent(QPaintEvent *event)
|
|||
*/
|
||||
QSize CardInfoPictureWidget::sizeHint() const
|
||||
{
|
||||
return {static_cast<int>(BASE_WIDTH * scaleFactor / 100.0),
|
||||
static_cast<int>(BASE_WIDTH * scaleFactor / 100.0 * ASPECT_RATIO)};
|
||||
return sizeForScaleFactor(scaleFactor);
|
||||
}
|
||||
|
||||
QSize CardInfoPictureWidget::sizeForScaleFactor(int scaleFactor)
|
||||
{
|
||||
const int width = BASE_WIDTH * scaleFactor / 100;
|
||||
return {width, static_cast<int>(width * ASPECT_RATIO)};
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -28,6 +28,11 @@ class CardInfoPictureWidget : public QWidget
|
|||
Q_OBJECT
|
||||
|
||||
public:
|
||||
/// Width of a card picture at 100 % scale, in pixels.
|
||||
static constexpr int BASE_WIDTH = 200;
|
||||
/// Height / width of a card picture.
|
||||
static constexpr qreal ASPECT_RATIO = 1.396;
|
||||
|
||||
explicit CardInfoPictureWidget(QWidget *parent = nullptr,
|
||||
bool hoverToZoomEnabled = false,
|
||||
bool raiseOnEnter = false);
|
||||
|
|
@ -37,6 +42,12 @@ public:
|
|||
}
|
||||
[[nodiscard]] QSize sizeHint() const override;
|
||||
|
||||
/**
|
||||
* @brief The size a card picture has at @p scaleFactor percent.
|
||||
* @param scaleFactor Percentage of BASE_WIDTH, as set by the card size slider.
|
||||
*/
|
||||
[[nodiscard]] static QSize sizeForScaleFactor(int scaleFactor);
|
||||
|
||||
public slots:
|
||||
void setCard(const ExactCard &card);
|
||||
void setScaleFactor(int scale); // New slot for scaling
|
||||
|
|
|
|||
|
|
@ -0,0 +1,83 @@
|
|||
#include "card_picture_delegate.h"
|
||||
|
||||
#include "../../../client/settings/cache_settings.h"
|
||||
#include "../../card_picture_loader/card_picture_loader.h"
|
||||
#include "card_art_utils.h"
|
||||
#include "card_database_visual_display_model.h"
|
||||
#include "libcockatrice/card/printing/exact_card.h"
|
||||
#include "libcockatrice/settings/cards_display_settings.h"
|
||||
|
||||
#include <QPainter>
|
||||
#include <QPainterPath>
|
||||
#include <QPixmap>
|
||||
#include <QRect>
|
||||
#include <QStyleOptionViewItem>
|
||||
#include <libcockatrice/card/printing/printing_info.h>
|
||||
|
||||
CardPictureDelegate::CardPictureDelegate(CardDatabaseVisualDisplayModel *_model, QObject *parent)
|
||||
: QStyledItemDelegate(parent), model(_model)
|
||||
{
|
||||
}
|
||||
|
||||
void CardPictureDelegate::setCardSize(const QSize &size)
|
||||
{
|
||||
if (cardSize == size) {
|
||||
return;
|
||||
}
|
||||
|
||||
cardSize = size;
|
||||
}
|
||||
|
||||
void CardPictureDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
|
||||
{
|
||||
if (model == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
const ExactCard card = model->cardAt(index.row());
|
||||
if (!card) {
|
||||
return;
|
||||
}
|
||||
|
||||
// The grid cell is the card plus SPACING; give the picture the rest.
|
||||
const int inset = SPACING / 2;
|
||||
const QRect cardArea = option.rect.adjusted(inset, inset, -inset, -inset);
|
||||
|
||||
QPixmap pixmap;
|
||||
CardPictureLoader::getCardBackLoadingInProgressPixmap(pixmap, cardArea.size());
|
||||
CardPictureLoader::getPixmap(pixmap, card, cardArea.size());
|
||||
|
||||
if (pixmap.isNull()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (SettingsCache::instance().cardsDisplay().getAutoRotateSidewaysLayoutCards()) {
|
||||
pixmap = CardArtUtils::rotateSidewaysLayoutArt(pixmap, card);
|
||||
}
|
||||
|
||||
// Fit the picture into the cell and center it, like CardInfoPictureWidget does.
|
||||
const QSize pictureSize = pixmap.deviceIndependentSize().toSize().scaled(cardArea.size(), Qt::KeepAspectRatio);
|
||||
const QRect pictureRect(cardArea.center().x() - pictureSize.width() / 2,
|
||||
cardArea.center().y() - pictureSize.height() / 2, pictureSize.width(),
|
||||
pictureSize.height());
|
||||
|
||||
const qreal radius = SettingsCache::instance().cardsDisplay().getRoundCardCorners()
|
||||
? 0.05 * static_cast<qreal>(pictureRect.width())
|
||||
: 0.0;
|
||||
|
||||
painter->save();
|
||||
painter->setRenderHint(QPainter::Antialiasing);
|
||||
painter->setRenderHint(QPainter::SmoothPixmapTransform);
|
||||
QPainterPath shape;
|
||||
shape.addRoundedRect(pictureRect, radius, radius);
|
||||
painter->setClipPath(shape);
|
||||
// Source rect is in device pixels, target in logical ones: drawPixmap scales
|
||||
// between them without the device pixel ratio being applied twice.
|
||||
painter->drawPixmap(pictureRect, pixmap, QRectF(pixmap.rect()));
|
||||
painter->restore();
|
||||
}
|
||||
|
||||
QSize CardPictureDelegate::sizeHint(const QStyleOptionViewItem & /*option*/, const QModelIndex & /*index*/) const
|
||||
{
|
||||
return cardSize + QSize(SPACING, SPACING);
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
/**
|
||||
* @file card_picture_delegate.h
|
||||
* @ingroup CardWidgets
|
||||
* @brief Paints a card picture for a visual card grid item.
|
||||
*/
|
||||
|
||||
#ifndef CARD_PICTURE_DELEGATE_H
|
||||
#define CARD_PICTURE_DELEGATE_H
|
||||
|
||||
#include <QSize>
|
||||
#include <QStyledItemDelegate>
|
||||
#include <qtmetamacros.h>
|
||||
|
||||
class CardDatabaseVisualDisplayModel;
|
||||
|
||||
/**
|
||||
* @brief Draws the card of a CardDatabaseVisualDisplayModel row as a picture.
|
||||
*
|
||||
* The delegate only paints rows the view asks for, so a grid of the whole card
|
||||
* database stays cheap: no widget per card and no pixmap loaded for items that
|
||||
* are off screen. Card images come from CardPictureLoader, which caches them and
|
||||
* loads missing ones asynchronously.
|
||||
*/
|
||||
class CardPictureDelegate : public QStyledItemDelegate
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
/// Gap left around a card picture inside its grid cell, in pixels.
|
||||
static constexpr int SPACING = 4;
|
||||
|
||||
explicit CardPictureDelegate(CardDatabaseVisualDisplayModel *_model, QObject *parent = nullptr);
|
||||
|
||||
/// The picture size inside a grid cell. The cell is this size plus SPACING.
|
||||
void setCardSize(const QSize &size);
|
||||
|
||||
void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override;
|
||||
[[nodiscard]] QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override;
|
||||
|
||||
private:
|
||||
CardDatabaseVisualDisplayModel *model;
|
||||
QSize cardSize;
|
||||
};
|
||||
|
||||
#endif // CARD_PICTURE_DELEGATE_H
|
||||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
#include "../general/layout_containers/flow_widget.h"
|
||||
|
||||
#include <QAbstractScrollArea>
|
||||
#include <QEvent>
|
||||
#include <QFlags>
|
||||
#include <QHBoxLayout>
|
||||
|
|
@ -33,8 +34,8 @@ CardSizeWidget::CardSizeWidget(QWidget *parent, FlowWidget *_flowWidget, int def
|
|||
cardSizeLabel->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum);
|
||||
|
||||
cardSizeSlider = new QSlider(Qt::Horizontal, this);
|
||||
cardSizeSlider->setRange(50, 250); ///< Slider range for card size adjustment.
|
||||
cardSizeSlider->setValue(defaultValue); ///< Initial slider value.
|
||||
cardSizeSlider->setRange(MIN_CARD_SIZE, MAX_CARD_SIZE); ///< Slider range for card size adjustment.
|
||||
cardSizeSlider->setValue(defaultValue); ///< Initial slider value.
|
||||
|
||||
cardSizeLayout->addWidget(cardSizeLabel);
|
||||
cardSizeLayout->addWidget(cardSizeSlider);
|
||||
|
|
@ -73,13 +74,26 @@ QSlider *CardSizeWidget::getSlider() const
|
|||
|
||||
void CardSizeWidget::enableCtrlScrollResize(QWidget *host)
|
||||
{
|
||||
host->installEventFilter(this);
|
||||
|
||||
const auto scrollAreas = host->findChildren<QScrollArea *>();
|
||||
for (QScrollArea *scrollArea : scrollAreas) {
|
||||
if (QWidget *content = scrollArea->widget()) {
|
||||
content->installEventFilter(this);
|
||||
const auto watchScrollArea = [this](QAbstractScrollArea *scrollArea) {
|
||||
if (scrollArea == nullptr) {
|
||||
return;
|
||||
}
|
||||
if (QWidget *viewport = scrollArea->viewport()) {
|
||||
viewport->installEventFilter(this);
|
||||
}
|
||||
if (auto *contentScrollArea = qobject_cast<QScrollArea *>(scrollArea)) {
|
||||
if (QWidget *content = contentScrollArea->widget()) {
|
||||
content->installEventFilter(this);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
host->installEventFilter(this);
|
||||
watchScrollArea(qobject_cast<QAbstractScrollArea *>(host));
|
||||
|
||||
const auto scrollAreas = host->findChildren<QAbstractScrollArea *>();
|
||||
for (QAbstractScrollArea *scrollArea : scrollAreas) {
|
||||
watchScrollArea(scrollArea);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -24,6 +24,10 @@ class CardSizeWidget : public QWidget
|
|||
Q_OBJECT
|
||||
|
||||
public:
|
||||
/// Range of the card size slider, as a percentage of the base card width.
|
||||
static constexpr int MIN_CARD_SIZE = 50;
|
||||
static constexpr int MAX_CARD_SIZE = 250;
|
||||
|
||||
explicit CardSizeWidget(QWidget *parent, FlowWidget *flowWidget = nullptr, int defaultValue = 100);
|
||||
[[nodiscard]] QSlider *getSlider() const;
|
||||
|
||||
|
|
@ -31,8 +35,9 @@ public:
|
|||
* @brief Resizes the cards when the user Ctrl + scrolls over @p host or any of its descendants.
|
||||
*
|
||||
* Installs this widget as an event filter on the given host widget. If the host contains a
|
||||
* scroll area the filter is also installed on the scroll area's content widget, so the resize
|
||||
* intercepts the wheel event before the scroll area would scroll the view.
|
||||
* scroll area the filter is also installed on the scroll area's content widget, and on the
|
||||
* viewport of every scroll area base (QListView, QTableView, ...), so the resize intercepts
|
||||
* the wheel event before the scroll area would scroll the view.
|
||||
*/
|
||||
void enableCtrlScrollResize(QWidget *host);
|
||||
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ class QWidget;
|
|||
|
||||
DlgConnect::DlgConnect(QWidget *parent) : QDialog(parent)
|
||||
{
|
||||
setObjectName("connectDialog");
|
||||
previousHostButton = new QRadioButton(tr("Known Hosts"), this);
|
||||
previousHosts = new QComboBox(this);
|
||||
|
||||
|
|
@ -85,6 +86,11 @@ DlgConnect::DlgConnect(QWidget *parent) : QDialog(parent)
|
|||
passwordLabel->setBuddy(passwordEdit);
|
||||
passwordEdit->setEchoMode(QLineEdit::Password);
|
||||
|
||||
hostEdit->setObjectName("connectHostEdit");
|
||||
portEdit->setObjectName("connectPortEdit");
|
||||
playernameEdit->setObjectName("connectPlayernameEdit");
|
||||
passwordEdit->setObjectName("connectPasswordEdit");
|
||||
|
||||
savePasswordCheckBox = new QCheckBox(tr("&Save password"));
|
||||
|
||||
autoConnectCheckBox = new QCheckBox(tr("A&uto connect"));
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ class QWidget;
|
|||
|
||||
DlgEditTokens::DlgEditTokens(QWidget *parent) : QDialog(parent), currentCard(nullptr)
|
||||
{
|
||||
setObjectName("editTokensDialog");
|
||||
nameLabel = new QLabel(tr("&Name:"));
|
||||
nameEdit = new QLineEdit;
|
||||
nameEdit->setMaxLength(MAX_NAME_LENGTH);
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@
|
|||
|
||||
WndSets::WndSets(QWidget *parent) : QMainWindow(parent)
|
||||
{
|
||||
setObjectName("manageSetsWindow");
|
||||
setOrderIsSorted = false;
|
||||
|
||||
// left toolbar
|
||||
|
|
@ -221,6 +222,7 @@ WndSets::WndSets(QWidget *parent) : QMainWindow(parent)
|
|||
|
||||
WndSets::~WndSets()
|
||||
{
|
||||
disconnect(view->header(), &QHeaderView::geometriesChanged, this, &WndSets::saveHeaderState);
|
||||
}
|
||||
|
||||
void WndSets::closeEvent(QCloseEvent * /*ev*/)
|
||||
|
|
|
|||
|
|
@ -73,6 +73,8 @@ DlgSettings::DlgSettings(QWidget *parent) : QDialog(parent)
|
|||
resize(calculateDialogSize(hostSize, available.size(), minimum));
|
||||
setSizeGripEnabled(true);
|
||||
|
||||
setObjectName("settingsDialog");
|
||||
|
||||
connect(&SettingsCache::instance().personal(), &PersonalSettings::langChanged, this, &DlgSettings::updateLanguage);
|
||||
|
||||
settingsView = new SettingsView(this);
|
||||
|
|
@ -82,6 +84,7 @@ DlgSettings::DlgSettings(QWidget *parent) : QDialog(parent)
|
|||
buttonBox->addStretch();
|
||||
okButton = new QPushButton;
|
||||
okButton->setDefault(true);
|
||||
okButton->setObjectName("settingsOkButton");
|
||||
connect(okButton, &QPushButton::clicked, this, &DlgSettings::close);
|
||||
buttonBox->addWidget(okButton);
|
||||
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@
|
|||
|
||||
DlgTipOfTheDay::DlgTipOfTheDay(QWidget *parent) : QDialog(parent)
|
||||
{
|
||||
setObjectName("tipOfTheDayDialog");
|
||||
successfulInit = false;
|
||||
QString xmlPath = "theme:tips/tips_of_the_day.xml";
|
||||
tipDatabase = new TipsOfTheDay(xmlPath, this);
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ static const QString UPDATE_INSTALLER_OVERRIDE = "Cockatrice-Update-Override.exe
|
|||
|
||||
DlgUpdate::DlgUpdate(QWidget *parent) : QDialog(parent)
|
||||
{
|
||||
setObjectName("updateDialog");
|
||||
|
||||
// Handle layout
|
||||
statusLabel = new QLabel(this);
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ class QWidget;
|
|||
|
||||
DlgViewLog::DlgViewLog(QWidget *parent) : QDialog(parent)
|
||||
{
|
||||
setObjectName("viewLogDialog");
|
||||
logArea = new QPlainTextEdit;
|
||||
logArea->setReadOnly(true);
|
||||
|
||||
|
|
|
|||
|
|
@ -119,6 +119,8 @@ QWidget *fromWidgetId(WidgetId id)
|
|||
|
||||
SettingsView::SettingsView(QWidget *parent) : QWidget(parent), currentTabIndex(0), searchActive(false)
|
||||
{
|
||||
setObjectName("settingsView");
|
||||
|
||||
setupUi();
|
||||
|
||||
connect(&SettingsCache::instance().personal(), &PersonalSettings::langChanged, this, &SettingsView::retranslateUi);
|
||||
|
|
@ -135,6 +137,7 @@ void SettingsView::setupUi()
|
|||
searchEdit->addAction(themePixmap(QStringLiteral("icons/search")), QLineEdit::LeadingPosition);
|
||||
searchEdit->installEventFilter(this);
|
||||
connect(searchEdit, &QLineEdit::textChanged, this, &SettingsView::onSearchTextChanged);
|
||||
searchEdit->setObjectName("settingsSearchEdit");
|
||||
|
||||
auto *searchLayout = new QHBoxLayout;
|
||||
searchLayout->addWidget(searchEdit);
|
||||
|
|
|
|||
|
|
@ -68,6 +68,7 @@
|
|||
HomeWidget::HomeWidget(QWidget *parent, TabSupervisor *_tabSupervisor)
|
||||
: QWidget(parent), tabSupervisor(_tabSupervisor), background(themePixmap(QStringLiteral("backgrounds/home")))
|
||||
{
|
||||
setObjectName("homeWidget");
|
||||
layout = new QGridLayout(this);
|
||||
|
||||
backgroundSourceCard = new CardInfoPictureArtCropWidget(this);
|
||||
|
|
@ -287,6 +288,7 @@ void HomeWidget::updateButtonsToBackgroundColor()
|
|||
QGroupBox *HomeWidget::createButtons()
|
||||
{
|
||||
QGroupBox *box = new QGroupBox(this);
|
||||
box->setObjectName("homeButtonsBox");
|
||||
box->setStyleSheet(R"(
|
||||
QGroupBox {
|
||||
font-size: 20px;
|
||||
|
|
@ -311,27 +313,34 @@ QGroupBox *HomeWidget::createButtons()
|
|||
boxLayout->addSpacing(25);
|
||||
|
||||
connectButton = new HomeStyledButton(QString(), gradientColors);
|
||||
connectButton->setObjectName("homeConnectButton");
|
||||
boxLayout->addWidget(connectButton);
|
||||
|
||||
visualDeckEditorButton = new HomeStyledButton(QString(), gradientColors);
|
||||
visualDeckEditorButton->setObjectName("homeCreateNewDeckButton");
|
||||
connect(visualDeckEditorButton, &QPushButton::clicked, tabSupervisor,
|
||||
[this] { tabSupervisor->openDeckInNewTab(LoadedDeck()); });
|
||||
boxLayout->addWidget(visualDeckEditorButton);
|
||||
visualDeckStorageButton = new HomeStyledButton(QString(), gradientColors);
|
||||
visualDeckStorageButton->setObjectName("homeBrowseDecksButton");
|
||||
connect(visualDeckStorageButton, &QPushButton::clicked, tabSupervisor,
|
||||
[this] { tabSupervisor->actTabVisualDeckStorage(true); });
|
||||
boxLayout->addWidget(visualDeckStorageButton);
|
||||
visualDatabaseDisplayButton = new HomeStyledButton(QString(), gradientColors);
|
||||
visualDatabaseDisplayButton->setObjectName("homeBrowseCardDatabaseButton");
|
||||
connect(visualDatabaseDisplayButton, &QPushButton::clicked, tabSupervisor,
|
||||
&TabSupervisor::addVisualDatabaseDisplayTab);
|
||||
boxLayout->addWidget(visualDatabaseDisplayButton);
|
||||
edhrecButton = new HomeStyledButton(QString(), gradientColors);
|
||||
edhrecButton->setObjectName("homeBrowseEdhrecButton");
|
||||
connect(edhrecButton, &QPushButton::clicked, tabSupervisor, &TabSupervisor::addEdhrecMainTab);
|
||||
boxLayout->addWidget(edhrecButton);
|
||||
archidektButton = new HomeStyledButton(QString(), gradientColors);
|
||||
archidektButton->setObjectName("homeBrowseArchidektButton");
|
||||
connect(archidektButton, &QPushButton::clicked, tabSupervisor, &TabSupervisor::addArchidektTab);
|
||||
boxLayout->addWidget(archidektButton);
|
||||
replayButton = new HomeStyledButton(QString(), gradientColors);
|
||||
replayButton->setObjectName("homeViewReplaysButton");
|
||||
connect(replayButton, &QPushButton::clicked, tabSupervisor, [this] { tabSupervisor->actTabReplays(true); });
|
||||
boxLayout->addWidget(replayButton);
|
||||
if (qobject_cast<MainWindow *>(tabSupervisor->parentWidget())) {
|
||||
|
|
|
|||
|
|
@ -32,7 +32,6 @@
|
|||
#include <libcockatrice/protocol/pb/serverinfo_user.pb.h>
|
||||
#include <libcockatrice/protocol/pb/session_commands.pb.h>
|
||||
#include <libcockatrice/protocol/pending_command.h>
|
||||
#include <libcockatrice/utility/passwordhasher.h>
|
||||
#include <libcockatrice/utility/qt_utils.h>
|
||||
#include <string>
|
||||
#include <sys/types.h>
|
||||
|
|
@ -299,17 +298,7 @@ void UserInfoBox::actPassword()
|
|||
|
||||
void UserInfoBox::changePassword(const QString &oldPassword, const QString &newPassword)
|
||||
{
|
||||
Command_AccountPassword cmd;
|
||||
cmd.set_old_password(oldPassword.toStdString());
|
||||
if (client->getServerSupportsChallengeResponse()) {
|
||||
cmd.set_hashed_new_password(PasswordHasher::generatePasswordVerifier(newPassword).toStdString());
|
||||
} else if (client->getServerSupportsPasswordHash()) {
|
||||
auto passwordSalt = PasswordHasher::generateRandomSalt();
|
||||
QString hashedPassword = PasswordHasher::computeHash(newPassword, passwordSalt);
|
||||
cmd.set_hashed_new_password(hashedPassword.toStdString());
|
||||
} else {
|
||||
cmd.set_new_password(newPassword.toStdString());
|
||||
}
|
||||
const Command_AccountPassword cmd = client->makeAccountPasswordCommand(oldPassword, newPassword);
|
||||
|
||||
PendingCommand *pend = client->prepareSessionCommand(cmd);
|
||||
connect(pend, &PendingCommand::finished, this, &UserInfoBox::processPasswordResponse);
|
||||
|
|
|
|||
|
|
@ -29,19 +29,10 @@ QMap<QString, std::pair<QString, UserConnection_Information>> UserConnection_Inf
|
|||
|
||||
ServersSettings &servers = SettingsCache::instance().servers();
|
||||
|
||||
int size = servers.getValue("totalServers", "server", "server_details").toInt() + 1;
|
||||
|
||||
for (int i = 0; i < size; i++) {
|
||||
QString _saveName = servers.getValue(QString("saveName%1").arg(i), "server", "server_details").toString();
|
||||
QString serverName = servers.getValue(QString("server%1").arg(i), "server", "server_details").toString();
|
||||
QString portNum = servers.getValue(QString("port%1").arg(i), "server", "server_details").toString();
|
||||
QString userName = servers.getValue(QString("username%1").arg(i), "server", "server_details").toString();
|
||||
QString pass = servers.getValue(QString("password%1").arg(i), "server", "server_details").toString();
|
||||
bool savePass = servers.getValue(QString("savePassword%1").arg(i), "server", "server_details").toBool();
|
||||
QString _site = servers.getValue(QString("site%1").arg(i), "server", "server_details").toString();
|
||||
|
||||
UserConnection_Information userInfo(_saveName, serverName, portNum, userName, pass, savePass, _site);
|
||||
serverList.insert(_saveName, std::make_pair(serverName, userInfo));
|
||||
for (const auto &entry : servers.getServers()) {
|
||||
UserConnection_Information userInfo(entry.saveName, entry.host, entry.port, entry.username, entry.password,
|
||||
entry.savePassword, entry.site);
|
||||
serverList.insert(entry.saveName, std::make_pair(entry.host, userInfo));
|
||||
}
|
||||
|
||||
return serverList;
|
||||
|
|
@ -53,28 +44,18 @@ QStringList UserConnection_Information::getServerInfo(const QString &find)
|
|||
|
||||
ServersSettings &servers = SettingsCache::instance().servers();
|
||||
|
||||
int size = servers.getValue("totalServers", "server", "server_details").toInt() + 1;
|
||||
for (int i = 0; i < size; i++) {
|
||||
QString _saveName = servers.getValue(QString("saveName%1").arg(i), "server", "server_details").toString();
|
||||
|
||||
if (find != _saveName) {
|
||||
for (const auto &entry : servers.getServers()) {
|
||||
if (find != entry.saveName) {
|
||||
continue;
|
||||
}
|
||||
|
||||
QString serverName = servers.getValue(QString("server%1").arg(i), "server", "server_details").toString();
|
||||
QString portNum = servers.getValue(QString("port%1").arg(i), "server", "server_details").toString();
|
||||
QString userName = servers.getValue(QString("username%1").arg(i), "server", "server_details").toString();
|
||||
QString pass = servers.getValue(QString("password%1").arg(i), "server", "server_details").toString();
|
||||
bool savePass = servers.getValue(QString("savePassword%1").arg(i), "server", "server_details").toBool();
|
||||
QString _site = servers.getValue(QString("site%1").arg(i), "server", "server_details").toString();
|
||||
|
||||
_server.append(_saveName);
|
||||
_server.append(serverName);
|
||||
_server.append(portNum);
|
||||
_server.append(userName);
|
||||
_server.append(pass);
|
||||
_server.append(savePass ? "1" : "0");
|
||||
_server.append(_site);
|
||||
_server.append(entry.saveName);
|
||||
_server.append(entry.host);
|
||||
_server.append(entry.port);
|
||||
_server.append(entry.username);
|
||||
_server.append(entry.password);
|
||||
_server.append(entry.savePassword ? "1" : "0");
|
||||
_server.append(entry.site);
|
||||
break;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ class TabSupervisor;
|
|||
|
||||
TabHome::TabHome(TabSupervisor *_tabSupervisor, AbstractClient *_client) : Tab(_tabSupervisor), client(_client)
|
||||
{
|
||||
setObjectName("TabHome");
|
||||
homeWidget = new HomeWidget(this, tabSupervisor);
|
||||
setCentralWidget(homeWidget);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@
|
|||
#include <QMessageBox>
|
||||
#include <QPushButton>
|
||||
#include <QRadioButton>
|
||||
#include <QScrollBar>
|
||||
#include <QSharedPointer>
|
||||
#include <QSizePolicy>
|
||||
#include <QSpinBox>
|
||||
|
|
@ -21,6 +22,7 @@
|
|||
#include <QTableWidgetItem>
|
||||
#include <QVBoxLayout>
|
||||
#include <QWidget>
|
||||
#include <QtGlobal>
|
||||
#include <libcockatrice/network/client/abstract/abstract_client.h>
|
||||
#include <libcockatrice/protocol/pb/moderator_commands.pb.h>
|
||||
#include <libcockatrice/protocol/pb/response_viewlog_history.pb.h>
|
||||
|
|
@ -31,6 +33,17 @@
|
|||
|
||||
class TabSupervisor;
|
||||
|
||||
namespace
|
||||
{
|
||||
/** @brief Identifies the newest row of a page well enough to recognise a repeated page. */
|
||||
QString pageStartKey(const Response_ViewLogHistory &response)
|
||||
{
|
||||
const ServerInfo_ChatMessage &first = response.log_message(0);
|
||||
return QString::fromStdString(first.time()) + QChar(0x1f) + QString::fromStdString(first.sender_name()) +
|
||||
QChar(0x1f) + QString::fromStdString(first.message());
|
||||
}
|
||||
} // namespace
|
||||
|
||||
TabLog::TabLog(TabSupervisor *_tabSupervisor, AbstractClient *_client, bool _canUseDeveloperCommands)
|
||||
: Tab(_tabSupervisor), client(_client), canUseDeveloperCommands(_canUseDeveloperCommands)
|
||||
{
|
||||
|
|
@ -52,12 +65,20 @@ TabLog::TabLog(TabSupervisor *_tabSupervisor, AbstractClient *_client, bool _can
|
|||
chatTable->setHorizontalHeaderLabels(
|
||||
QString(tr("Time;SenderName;SenderIP;Message;TargetID;TargetName")).split(";"));
|
||||
|
||||
QTabWidget *tabManager = new QTabWidget();
|
||||
tabManager = new QTabWidget();
|
||||
tabManager->addTab(roomTable, tr("Room Logs"));
|
||||
tabManager->addTab(gameTable, tr("Game Logs"));
|
||||
tabManager->addTab(chatTable, tr("Chat Logs"));
|
||||
setCentralWidget(tabManager);
|
||||
|
||||
connect(roomTable->verticalScrollBar(), &QScrollBar::valueChanged, this, &TabLog::checkFetchMore);
|
||||
connect(gameTable->verticalScrollBar(), &QScrollBar::valueChanged, this, &TabLog::checkFetchMore);
|
||||
connect(chatTable->verticalScrollBar(), &QScrollBar::valueChanged, this, &TabLog::checkFetchMore);
|
||||
// A tab that gains only a few rows has nothing to scroll, so its scrollbar never fires:
|
||||
// switching to it would leave its next page unfetched until the moderator scrolled
|
||||
// another tab. Re-check on every tab switch so the visible table can continue the browse.
|
||||
connect(tabManager, &QTabWidget::currentChanged, this, &TabLog::checkFetchMore);
|
||||
|
||||
createDock();
|
||||
restartLayout();
|
||||
clearClicked();
|
||||
|
|
@ -74,12 +95,9 @@ void TabLog::retranslateUi()
|
|||
|
||||
void TabLog::getClicked()
|
||||
{
|
||||
if (findUsername->text().isEmpty() && findIPAddress->text().isEmpty() && findGameName->text().isEmpty() &&
|
||||
findGameID->text().isEmpty() && findMessage->text().isEmpty()) {
|
||||
QMessageBox::critical(this, tr("Error"), tr("You must select at least one filter."));
|
||||
return;
|
||||
}
|
||||
|
||||
// Empty filters are a valid browse: room/game history going back from
|
||||
// today, paginated. Only require a date range + location, both of which
|
||||
// get sensible defaults below so a bare "Get" just works.
|
||||
if (!lastHour->isChecked() && !today->isChecked() && !pastDays->isChecked()) {
|
||||
pastDays->setChecked(true);
|
||||
pastXDays->setValue(20);
|
||||
|
|
@ -99,39 +117,71 @@ void TabLog::getClicked()
|
|||
}
|
||||
|
||||
if (maximumResults->value() == 0) {
|
||||
maximumResults->setValue(1000);
|
||||
maximumResults->setValue(200);
|
||||
}
|
||||
pageSize = qBound(1, maximumResults->value(), 1000);
|
||||
|
||||
int dateRange = 0;
|
||||
if (lastHour->isChecked()) {
|
||||
dateRange = 1;
|
||||
}
|
||||
|
||||
if (today->isChecked()) {
|
||||
dateRange = 24;
|
||||
}
|
||||
|
||||
if (pastDays->isChecked()) {
|
||||
dateRange = pastXDays->value() * 24;
|
||||
}
|
||||
|
||||
Command_ViewLogHistory cmd;
|
||||
cmd.set_user_name(findUsername->text().toStdString());
|
||||
cmd.set_ip_address(findIPAddress->text().toStdString());
|
||||
cmd.set_game_name(findGameName->text().toStdString());
|
||||
cmd.set_game_id(findGameID->text().toStdString());
|
||||
cmd.set_message(findMessage->text().toStdString());
|
||||
// Build the command once, here, and let every page copy it: rebuilding it from the
|
||||
// widgets per page would run page 2+ against filters edited after Get was clicked,
|
||||
// appending rows selected by a different query under the first page's results.
|
||||
currentQuery.Clear();
|
||||
currentQuery.set_user_name(findUsername->text().toStdString());
|
||||
currentQuery.set_ip_address(findIPAddress->text().toStdString());
|
||||
currentQuery.set_game_name(findGameName->text().toStdString());
|
||||
currentQuery.set_game_id(findGameID->text().toStdString());
|
||||
currentQuery.set_message(findMessage->text().toStdString());
|
||||
if (mainRoom->isChecked()) {
|
||||
cmd.add_log_location("room");
|
||||
};
|
||||
currentQuery.add_log_location("room");
|
||||
}
|
||||
if (gameRoom->isChecked()) {
|
||||
cmd.add_log_location("game");
|
||||
};
|
||||
currentQuery.add_log_location("game");
|
||||
}
|
||||
if (privateChat->isChecked()) {
|
||||
cmd.add_log_location("chat");
|
||||
};
|
||||
cmd.set_date_range(dateRange);
|
||||
cmd.set_maximum_results(maximumResults->value());
|
||||
currentQuery.add_log_location("chat");
|
||||
}
|
||||
currentQuery.set_date_range(dateRangeForSelection());
|
||||
currentQuery.set_maximum_results(pageSize);
|
||||
hasCurrentQuery = true;
|
||||
snapshotBefore.clear();
|
||||
lastPageStartKey.clear();
|
||||
|
||||
// Fresh search: reset pagination state and clear previous pages.
|
||||
currentOffset = 0;
|
||||
roomCounter = 0;
|
||||
gameCounter = 0;
|
||||
chatCounter = 0;
|
||||
roomTable->setRowCount(0);
|
||||
gameTable->setRowCount(0);
|
||||
chatTable->setRowCount(0);
|
||||
|
||||
fetchPage(0);
|
||||
}
|
||||
|
||||
int TabLog::dateRangeForSelection() const
|
||||
{
|
||||
if (lastHour->isChecked()) {
|
||||
return 1;
|
||||
}
|
||||
if (today->isChecked()) {
|
||||
return 24;
|
||||
}
|
||||
if (pastDays->isChecked()) {
|
||||
return pastXDays->value() * 24;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
void TabLog::fetchPage(int offset)
|
||||
{
|
||||
if (isFetching || !hasCurrentQuery) {
|
||||
return;
|
||||
}
|
||||
|
||||
Command_ViewLogHistory cmd = currentQuery;
|
||||
cmd.set_offset(offset);
|
||||
if (!snapshotBefore.isEmpty()) {
|
||||
cmd.set_before_time(snapshotBefore.toStdString());
|
||||
}
|
||||
|
||||
PendingCommand *pend;
|
||||
if (canUseDeveloperCommands) {
|
||||
|
|
@ -141,10 +191,42 @@ void TabLog::getClicked()
|
|||
pend = client->prepareModeratorCommand(cmd);
|
||||
}
|
||||
|
||||
isFetching = true;
|
||||
getButton->setEnabled(false);
|
||||
|
||||
connect(pend, &PendingCommand::finished, this, &TabLog::viewLogHistory_processResponse);
|
||||
client->sendCommand(pend);
|
||||
}
|
||||
|
||||
void TabLog::checkFetchMore()
|
||||
{
|
||||
if (isFetching || !hasMoreResults) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only the table the moderator is looking at may pull the next page. Checking all
|
||||
// three would make an empty or hidden table nearBottom by definition (maximum() and
|
||||
// value() are both zero), so a room-only browse chained page after page without any
|
||||
// scrolling - pulling the whole range with a growing OFFSET scan per page, and
|
||||
// looping forever against a server that ignores `offset`.
|
||||
auto *table = qobject_cast<QTableWidget *>(tabManager->currentWidget());
|
||||
if (table == nullptr || table->rowCount() == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// ScrollPerItem (the default) counts rows, not pixels: `value` is the top visible row
|
||||
// and `maximum` the last one that can be scrolled to. Prefetch half a page ahead so
|
||||
// scrolling never visibly stalls, without the fixed 300-row threshold firing the next
|
||||
// page the moment a page loads.
|
||||
const QScrollBar *bar = table->verticalScrollBar();
|
||||
const int prefetchRows = qMax(1, pageSize / 2);
|
||||
if (bar->maximum() - bar->value() > prefetchRows) {
|
||||
return;
|
||||
}
|
||||
|
||||
fetchPage(currentOffset);
|
||||
}
|
||||
|
||||
void TabLog::clearClicked()
|
||||
{
|
||||
findUsername->clear();
|
||||
|
|
@ -166,6 +248,10 @@ void TabLog::clearClicked()
|
|||
pastDays->setAutoExclusive(true);
|
||||
today->setAutoExclusive(true);
|
||||
lastHour->setAutoExclusive(true);
|
||||
currentOffset = 0;
|
||||
hasMoreResults = false;
|
||||
hasCurrentQuery = false;
|
||||
snapshotBefore.clear();
|
||||
}
|
||||
|
||||
void TabLog::createDock()
|
||||
|
|
@ -209,14 +295,15 @@ void TabLog::createDock()
|
|||
pastXDays = new QSpinBox;
|
||||
pastXDays->setMaximum(20);
|
||||
|
||||
labelMaximum = new QLabel(tr("Maximum Results: "));
|
||||
labelMaximum = new QLabel(tr("Results per page: "));
|
||||
maximumResults = new QSpinBox;
|
||||
maximumResults->setMaximum(1000);
|
||||
maximumResults->setToolTip(tr("Page size for smooth scrolling: more rows load automatically as you scroll."));
|
||||
|
||||
labelDescription = new QLabel(tr(
|
||||
"At least one filter is required.\nThe more information you put in, the more specific your results will be."));
|
||||
labelDescription = new QLabel(tr("Leave filters empty to browse recent room and game messages.\n"
|
||||
"More rows load automatically as you scroll."));
|
||||
|
||||
getButton = new QPushButton(tr("Get User Logs"));
|
||||
getButton = new QPushButton(tr("Get Logs"));
|
||||
getButton->setAutoDefault(true);
|
||||
connect(getButton, &QPushButton::clicked, this, &TabLog::getClicked);
|
||||
|
||||
|
|
@ -262,7 +349,7 @@ void TabLog::createDock()
|
|||
maxResultsGrid->addWidget(labelMaximum, 0, 0);
|
||||
maxResultsGrid->addWidget(maximumResults, 0, 1);
|
||||
|
||||
maxResultsGroupBox = new QGroupBox(tr("Maximum Results"));
|
||||
maxResultsGroupBox = new QGroupBox(tr("Results per page"));
|
||||
maxResultsGroupBox->setLayout(maxResultsGrid);
|
||||
|
||||
descriptionGrid = new QGridLayout;
|
||||
|
|
@ -295,84 +382,134 @@ void TabLog::createDock()
|
|||
searchDock->setWidget(searchDockContents);
|
||||
}
|
||||
|
||||
void TabLog::appendChatMessage(QTableWidget *table,
|
||||
int &counter,
|
||||
const QString &time,
|
||||
const QString &senderName,
|
||||
const QString &senderIp,
|
||||
const QString &messageText,
|
||||
const QString &targetId,
|
||||
const QString &targetName,
|
||||
const QString &channel)
|
||||
{
|
||||
table->insertRow(counter);
|
||||
table->setItem(counter, 0, new QTableWidgetItem(time));
|
||||
table->setItem(counter, 1, new QTableWidgetItem(senderName));
|
||||
table->setItem(counter, 2, new QTableWidgetItem(senderIp));
|
||||
table->setItem(counter, 3, new QTableWidgetItem(messageText));
|
||||
table->setItem(counter, 4, new QTableWidgetItem(targetId));
|
||||
table->setItem(counter, 5, new QTableWidgetItem(targetName));
|
||||
if (table->columnCount() > 6) {
|
||||
table->setItem(counter, 6, new QTableWidgetItem(channel));
|
||||
}
|
||||
++counter;
|
||||
}
|
||||
|
||||
void TabLog::viewLogHistory_processResponse(const Response &resp)
|
||||
{
|
||||
const Response_ViewLogHistory &response = resp.GetExtension(Response_ViewLogHistory::ext);
|
||||
const bool isFirstPage = (currentOffset == 0);
|
||||
isFetching = false;
|
||||
getButton->setEnabled(true);
|
||||
|
||||
if (resp.response_code() != Response::RespOk) {
|
||||
QMessageBox::critical(static_cast<QWidget *>(parent()), tr("Message History"),
|
||||
tr("Failed to collect message history information."));
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.log_message_size() == 0) {
|
||||
if (isFirstPage && response.log_message_size() == 0) {
|
||||
QMessageBox::information(static_cast<QWidget *>(parent()), tr("Message History"),
|
||||
tr("There are no messages for the selected filters."));
|
||||
hasMoreResults = false;
|
||||
return;
|
||||
}
|
||||
|
||||
int roomCounter = 0, gameCounter = 0, chatCounter = 0;
|
||||
roomTable->setRowCount(roomCounter);
|
||||
gameTable->setRowCount(gameCounter);
|
||||
chatTable->setRowCount(chatCounter);
|
||||
const QString thisPageStart = response.log_message_size() > 0 ? pageStartKey(response) : QString();
|
||||
|
||||
if (isFirstPage) {
|
||||
// Rows come newest first, so the first one fixes the snapshot every later page is
|
||||
// bounded by. Without it, each message logged between two page fetches pushes the
|
||||
// window down by one and repeats a row the moderator already has.
|
||||
snapshotBefore = QString::fromStdString(response.log_message(0).time());
|
||||
} else if (!response.has_has_more() && !thisPageStart.isEmpty() && thisPageStart == lastPageStartKey) {
|
||||
// A server that predates `offset` ignores it and returns the same page again. It
|
||||
// also predates `has_more`, so the full-page heuristic below can never prove
|
||||
// exhaustion and every scroll would append the same rows forever. The identical
|
||||
// first row is the proof; stop instead of looping.
|
||||
hasMoreResults = false;
|
||||
return;
|
||||
}
|
||||
lastPageStartKey = thisPageStart;
|
||||
|
||||
for (int i = 0; i < response.log_message_size(); ++i) {
|
||||
ServerInfo_ChatMessage message = response.log_message(i);
|
||||
const QString targetType = QString::fromStdString(message.target_type());
|
||||
if (targetType == "room" || targetType == "room_channel") {
|
||||
roomTable->insertRow(roomCounter);
|
||||
roomTable->setItem(roomCounter, 0, new QTableWidgetItem(QString::fromStdString(message.time())));
|
||||
roomTable->setItem(roomCounter, 1, new QTableWidgetItem(QString::fromStdString(message.sender_name())));
|
||||
roomTable->setItem(roomCounter, 2, new QTableWidgetItem(QString::fromStdString(message.sender_ip())));
|
||||
roomTable->setItem(roomCounter, 3, new QTableWidgetItem(QString::fromStdString(message.message())));
|
||||
roomTable->setItem(roomCounter, 4, new QTableWidgetItem(QString::fromStdString(message.target_id())));
|
||||
roomTable->setItem(roomCounter, 5, new QTableWidgetItem(QString::fromStdString(message.target_name())));
|
||||
roomTable->setItem(roomCounter, 6, new QTableWidgetItem(QString::fromStdString(message.channel())));
|
||||
++roomCounter;
|
||||
appendChatMessage(roomTable, roomCounter, QString::fromStdString(message.time()),
|
||||
QString::fromStdString(message.sender_name()),
|
||||
QString::fromStdString(message.sender_ip()), QString::fromStdString(message.message()),
|
||||
QString::fromStdString(message.target_id()),
|
||||
QString::fromStdString(message.target_name()), QString::fromStdString(message.channel()));
|
||||
}
|
||||
|
||||
if (targetType == "game") {
|
||||
gameTable->insertRow(gameCounter);
|
||||
gameTable->setItem(gameCounter, 0, new QTableWidgetItem(QString::fromStdString(message.time())));
|
||||
gameTable->setItem(gameCounter, 1, new QTableWidgetItem(QString::fromStdString(message.sender_name())));
|
||||
gameTable->setItem(gameCounter, 2, new QTableWidgetItem(QString::fromStdString(message.sender_ip())));
|
||||
gameTable->setItem(gameCounter, 3, new QTableWidgetItem(QString::fromStdString(message.message())));
|
||||
gameTable->setItem(gameCounter, 4, new QTableWidgetItem(QString::fromStdString(message.target_id())));
|
||||
gameTable->setItem(gameCounter, 5, new QTableWidgetItem(QString::fromStdString(message.target_name())));
|
||||
++gameCounter;
|
||||
appendChatMessage(gameTable, gameCounter, QString::fromStdString(message.time()),
|
||||
QString::fromStdString(message.sender_name()),
|
||||
QString::fromStdString(message.sender_ip()), QString::fromStdString(message.message()),
|
||||
QString::fromStdString(message.target_id()),
|
||||
QString::fromStdString(message.target_name()));
|
||||
}
|
||||
|
||||
if (targetType == "chat") {
|
||||
chatTable->insertRow(chatCounter);
|
||||
chatTable->setItem(chatCounter, 0, new QTableWidgetItem(QString::fromStdString(message.time())));
|
||||
chatTable->setItem(chatCounter, 1, new QTableWidgetItem(QString::fromStdString(message.sender_name())));
|
||||
chatTable->setItem(chatCounter, 2, new QTableWidgetItem(QString::fromStdString(message.sender_ip())));
|
||||
chatTable->setItem(chatCounter, 3, new QTableWidgetItem(QString::fromStdString(message.message())));
|
||||
chatTable->setItem(chatCounter, 4, new QTableWidgetItem(QString::fromStdString(message.target_id())));
|
||||
chatTable->setItem(chatCounter, 5, new QTableWidgetItem(QString::fromStdString(message.target_name())));
|
||||
++chatCounter;
|
||||
appendChatMessage(chatTable, chatCounter, QString::fromStdString(message.time()),
|
||||
QString::fromStdString(message.sender_name()),
|
||||
QString::fromStdString(message.sender_ip()), QString::fromStdString(message.message()),
|
||||
QString::fromStdString(message.target_id()),
|
||||
QString::fromStdString(message.target_name()));
|
||||
}
|
||||
}
|
||||
|
||||
currentOffset += response.log_message_size();
|
||||
if (response.has_has_more()) {
|
||||
hasMoreResults = response.has_more();
|
||||
} else {
|
||||
// Legacy server without has_more: a full page implies more may exist.
|
||||
hasMoreResults = (response.log_message_size() >= pageSize);
|
||||
}
|
||||
|
||||
if (roomCounter) {
|
||||
roomTable->show();
|
||||
roomTable->resizeColumnsToContents();
|
||||
if (isFirstPage) {
|
||||
roomTable->resizeColumnsToContents();
|
||||
}
|
||||
} else {
|
||||
roomTable->hide();
|
||||
}
|
||||
|
||||
if (gameCounter) {
|
||||
gameTable->resizeColumnsToContents();
|
||||
if (isFirstPage) {
|
||||
gameTable->resizeColumnsToContents();
|
||||
}
|
||||
gameTable->show();
|
||||
} else {
|
||||
gameTable->hide();
|
||||
}
|
||||
|
||||
if (chatCounter) {
|
||||
chatTable->resizeColumnsToContents();
|
||||
if (isFirstPage) {
|
||||
chatTable->resizeColumnsToContents();
|
||||
}
|
||||
chatTable->show();
|
||||
} else {
|
||||
chatTable->hide();
|
||||
}
|
||||
|
||||
// The new rows may already fill less than a viewport (or the user is
|
||||
// still parked at the bottom), so chain the next page without waiting
|
||||
// for another scroll event. The prefetch stays invisible: rows simply
|
||||
// keep appearing as you scroll.
|
||||
checkFetchMore();
|
||||
}
|
||||
|
||||
void TabLog::restartLayout()
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
#include "tab.h"
|
||||
|
||||
#include <QString>
|
||||
#include <libcockatrice/protocol/pb/moderator_commands.pb.h>
|
||||
#include <qtmetamacros.h>
|
||||
|
||||
class AbstractClient;
|
||||
|
|
@ -25,6 +26,7 @@ class QWidget;
|
|||
class QGridLayout;
|
||||
class QVBoxLayout;
|
||||
class QTableWidget;
|
||||
class QTabWidget;
|
||||
class Response;
|
||||
class TabSupervisor;
|
||||
|
||||
|
|
@ -48,8 +50,37 @@ private:
|
|||
*buttonGroupBox;
|
||||
QVBoxLayout *mainLayout;
|
||||
QTableWidget *roomTable, *gameTable, *chatTable;
|
||||
QTabWidget *tabManager = nullptr;
|
||||
|
||||
//! The filters of the search in flight, captured when Get was clicked so that
|
||||
//! later pages cannot pick up edits the moderator made after starting it.
|
||||
Command_ViewLogHistory currentQuery;
|
||||
bool hasCurrentQuery = false;
|
||||
//! Newest `log_time` of the first page, sent back as the upper bound of every
|
||||
//! later page so messages logged in the meantime do not shift the window.
|
||||
QString snapshotBefore;
|
||||
//! Identity of the previous page's first row, used to spot a server that ignores
|
||||
//! `offset` and hands back the same page forever.
|
||||
QString lastPageStartKey;
|
||||
int currentOffset = 0;
|
||||
int pageSize = 200;
|
||||
int roomCounter = 0, gameCounter = 0, chatCounter = 0;
|
||||
bool isFetching = false;
|
||||
bool hasMoreResults = false;
|
||||
|
||||
void createDock();
|
||||
[[nodiscard]] int dateRangeForSelection() const;
|
||||
void fetchPage(int offset);
|
||||
void appendChatMessage(QTableWidget *table,
|
||||
int &counter,
|
||||
const QString &time,
|
||||
const QString &senderName,
|
||||
const QString &senderIp,
|
||||
const QString &messageText,
|
||||
const QString &targetId,
|
||||
const QString &targetName,
|
||||
const QString &channel = QString());
|
||||
void checkFetchMore();
|
||||
signals:
|
||||
|
||||
private slots:
|
||||
|
|
|
|||
|
|
@ -54,6 +54,7 @@
|
|||
#include <QPainter>
|
||||
#include <QRect>
|
||||
#include <QSharedPointer>
|
||||
#include <QStackedWidget>
|
||||
#include <QStringLiteral>
|
||||
#include <QStyle>
|
||||
#include <QStyleOption>
|
||||
|
|
@ -157,6 +158,15 @@ TabSupervisor::TabSupervisor(AbstractClient *_client, QMenu *tabsMenu, QWidget *
|
|||
tabReplays(nullptr), tabAdmin(nullptr), tabCardArtRules(nullptr), tabLog(nullptr), tabReport(nullptr),
|
||||
tabModeration(nullptr), tabDeveloper(nullptr), isLocalGame(false)
|
||||
{
|
||||
setObjectName("tabSupervisor");
|
||||
|
||||
// QTabWidget keeps tab pages in an internal stacked widget. Give it a stable
|
||||
// name so the GUI tests can address the pages by a readable path
|
||||
// (tabSupervisor/stackedWidget/<TabName>).
|
||||
if (auto *stackedWidget = findChild<QStackedWidget *>()) {
|
||||
stackedWidget->setObjectName("stackedWidget");
|
||||
}
|
||||
|
||||
setElideMode(Qt::ElideRight);
|
||||
setMovable(true);
|
||||
setIconSize(QSize(15, 15));
|
||||
|
|
@ -462,6 +472,9 @@ void TabSupervisor::addCloseButtonToTab(Tab *tab, int tabIndex, QAction *manager
|
|||
auto closeSide = static_cast<QTabBar::ButtonPosition>(
|
||||
tabBar()->style()->styleHint(QStyle::SH_TabBar_CloseButtonPosition, nullptr, tabBar()));
|
||||
auto *closeButton = new CloseButton(tab);
|
||||
// No object name: the tab bar index shifts when tabs move or close, so an
|
||||
// index-derived name would go stale (or collide). Callers find the button
|
||||
// through the tab widget instead.
|
||||
if (manager) {
|
||||
// If managed, all close requests should go through the menu action
|
||||
connect(closeButton, &CloseButton::clicked, this, [manager] { checkAndTrigger(manager, false); });
|
||||
|
|
|
|||
|
|
@ -0,0 +1,203 @@
|
|||
#include "card_database_visual_view.h"
|
||||
|
||||
#include "../../card_picture_loader/card_picture_loader.h"
|
||||
#include "../cards/card_info_picture_widget.h"
|
||||
#include "../cards/card_picture_delegate.h"
|
||||
#include "../cards/card_size_widget.h"
|
||||
#include "card_database_visual_display_model.h"
|
||||
#include "libcockatrice/card/printing/exact_card.h"
|
||||
|
||||
#include <QAbstractItemModel>
|
||||
#include <QAbstractItemView>
|
||||
#include <QApplication>
|
||||
#include <QCursor>
|
||||
#include <QEvent>
|
||||
#include <QFrame>
|
||||
#include <QModelIndex>
|
||||
#include <QMouseEvent>
|
||||
#include <QScrollBar>
|
||||
#include <QSize>
|
||||
#include <QWheelEvent>
|
||||
#include <QWidget>
|
||||
#include <QtGlobal>
|
||||
#include <qnamespace.h>
|
||||
|
||||
CardDatabaseVisualView::CardDatabaseVisualView(CardDatabaseVisualDisplayModel *_model, QWidget *parent)
|
||||
: QListView(parent), model(_model)
|
||||
{
|
||||
auto *cardDelegate = new CardPictureDelegate(model, this);
|
||||
setItemDelegate(cardDelegate);
|
||||
setModel(model);
|
||||
|
||||
setViewMode(QListView::IconMode);
|
||||
setFlow(QListView::LeftToRight);
|
||||
setWrapping(true);
|
||||
setResizeMode(QListView::Adjust);
|
||||
setMovement(QListView::Static);
|
||||
setUniformItemSizes(true);
|
||||
setLayoutMode(QListView::SinglePass);
|
||||
setVerticalScrollMode(QAbstractItemView::ScrollPerPixel);
|
||||
setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
||||
setFrameShape(QFrame::NoFrame);
|
||||
setWordWrap(false);
|
||||
|
||||
// QListView hands the delegate's item height to the scroll bar as its single
|
||||
// step under ScrollPerPixel, so one wheel notch would jump wheelScrollLines()
|
||||
// whole card rows. Give the bar a text line instead; the wheel handler below
|
||||
// scales that line to the grid. A positive step also stops Qt from taking the
|
||||
// item height back on the next layout.
|
||||
verticalScrollBar()->setSingleStep(qMax(1, fontMetrics().height()));
|
||||
|
||||
// The grid is a read-only surface: clicks are actions on a card, not a
|
||||
// selection, and they must not steal focus from the search field.
|
||||
setSelectionMode(QAbstractItemView::NoSelection);
|
||||
setEditTriggers(QAbstractItemView::NoEditTriggers);
|
||||
setFocusPolicy(Qt::NoFocus);
|
||||
|
||||
// Hover drives the card preview.
|
||||
setMouseTracking(true);
|
||||
viewport()->setMouseTracking(true);
|
||||
|
||||
connect(&CardPictureLoader::getInstance(), &CardPictureLoader::pixmapLoaded, this,
|
||||
[this] { viewport()->update(); });
|
||||
|
||||
// A new search result starts at the top, like the widget-per-card grid did.
|
||||
connect(model, &QAbstractItemModel::modelReset, this, [this] {
|
||||
verticalScrollBar()->setValue(0);
|
||||
// The rows under a stationary cursor changed with the reset: re-resolve the
|
||||
// hover so the preview follows the grid, as enterEvent did per card widget.
|
||||
hoveredRow = -1;
|
||||
updateHoveredRow(viewport()->mapFromGlobal(QCursor::pos()));
|
||||
});
|
||||
|
||||
setCardSizeFactor(cardSizeFactor);
|
||||
}
|
||||
|
||||
void CardDatabaseVisualView::setCardSizeFactor(int factor)
|
||||
{
|
||||
cardSizeFactor = qBound(CardSizeWidget::MIN_CARD_SIZE, factor, CardSizeWidget::MAX_CARD_SIZE);
|
||||
|
||||
const QSize cardSize = CardInfoPictureWidget::sizeForScaleFactor(cardSizeFactor);
|
||||
if (auto *cardDelegate = qobject_cast<CardPictureDelegate *>(itemDelegate())) {
|
||||
cardDelegate->setCardSize(cardSize);
|
||||
}
|
||||
// The delegate's sizeHint does not emit sizeHintChanged, so the grid size is what
|
||||
// makes a resize take effect. Keep the two in step.
|
||||
setGridSize(cardSize + QSize(CardPictureDelegate::SPACING, CardPictureDelegate::SPACING));
|
||||
}
|
||||
|
||||
void CardDatabaseVisualView::mousePressEvent(QMouseEvent *event)
|
||||
{
|
||||
const QModelIndex index = indexAt(event->pos());
|
||||
if (index.isValid() && model != nullptr) {
|
||||
const ExactCard card = model->cardAt(index.row());
|
||||
if (card) {
|
||||
emit cardClicked(event, card);
|
||||
event->accept();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
QListView::mousePressEvent(event);
|
||||
}
|
||||
|
||||
void CardDatabaseVisualView::mouseMoveEvent(QMouseEvent *event)
|
||||
{
|
||||
QListView::mouseMoveEvent(event);
|
||||
updateHoveredRow(event->pos());
|
||||
}
|
||||
|
||||
bool CardDatabaseVisualView::viewportEvent(QEvent *event)
|
||||
{
|
||||
// A per-card widget reported its hover from enterEvent, which also fires when the
|
||||
// grid changes under a stationary cursor. A QListView only reports moves, so
|
||||
// re-resolve the hover on enter and clear it on leave.
|
||||
if (event->type() == QEvent::Enter) {
|
||||
updateHoveredRow(viewport()->mapFromGlobal(QCursor::pos()));
|
||||
} else if (event->type() == QEvent::Leave) {
|
||||
hoveredRow = -1;
|
||||
}
|
||||
|
||||
return QListView::viewportEvent(event);
|
||||
}
|
||||
|
||||
void CardDatabaseVisualView::wheelEvent(QWheelEvent *event)
|
||||
{
|
||||
// Ctrl+wheel is the card-size gesture and is filtered out before it reaches
|
||||
// the view; any other modifier keeps QAbstractSlider's own page scrolling.
|
||||
if (event->modifiers() != Qt::NoModifier) {
|
||||
QListView::wheelEvent(event);
|
||||
return;
|
||||
}
|
||||
|
||||
QScrollBar *bar = verticalScrollBar();
|
||||
if (!bar->isVisible()) {
|
||||
event->ignore();
|
||||
return;
|
||||
}
|
||||
|
||||
// Touchpads and high-resolution wheels already report pixels: follow them.
|
||||
if (!event->pixelDelta().isNull()) {
|
||||
bar->setValue(bar->value() - event->pixelDelta().y());
|
||||
event->accept();
|
||||
return;
|
||||
}
|
||||
|
||||
if (event->angleDelta().y() == 0) {
|
||||
QListView::wheelEvent(event);
|
||||
return;
|
||||
}
|
||||
|
||||
// Accumulate wheel deltas smaller than a notch, like QAbstractSlider does, so
|
||||
// a fine-grained wheel still moves.
|
||||
wheelRemainder += static_cast<qreal>(event->angleDelta().y()) / 120.0;
|
||||
const int notches = static_cast<int>(wheelRemainder);
|
||||
if (notches == 0) {
|
||||
event->accept();
|
||||
return;
|
||||
}
|
||||
wheelRemainder -= notches;
|
||||
|
||||
const int pixels = -notches * wheelStep();
|
||||
bar->setValue(qBound(bar->minimum(), bar->value() + pixels, bar->maximum()));
|
||||
event->accept();
|
||||
}
|
||||
|
||||
int CardDatabaseVisualView::wheelStep() const
|
||||
{
|
||||
// A card row is far too coarse for one wheel line, and a text line is too
|
||||
// fine, so a line is an eighth of the grid cell. A notch then moves about a
|
||||
// third of a card row at any card size, and the OS line count still applies.
|
||||
const int lineHeight = qMax(1, gridSize().height() / 8);
|
||||
return qMax(1, QApplication::wheelScrollLines()) * lineHeight;
|
||||
}
|
||||
|
||||
void CardDatabaseVisualView::updateHoveredRow(const QPoint &position)
|
||||
{
|
||||
// indexAt() also answers for points outside the viewport, so a cursor parked
|
||||
// below the grid (on the size slider, say) would resolve to the row just under
|
||||
// the visible area. Only a point inside the viewport can hover a card.
|
||||
if (model == nullptr || !viewport()->rect().contains(position)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const QModelIndex index = indexAt(position);
|
||||
const int row = index.isValid() ? index.row() : -1;
|
||||
if (row == hoveredRow) {
|
||||
return;
|
||||
}
|
||||
|
||||
hoveredRow = row;
|
||||
if (row >= 0) {
|
||||
const ExactCard card = model->cardAt(row);
|
||||
if (card) {
|
||||
emit cardHovered(card);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void CardDatabaseVisualView::leaveEvent(QEvent *event)
|
||||
{
|
||||
hoveredRow = -1;
|
||||
QListView::leaveEvent(event);
|
||||
}
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
/**
|
||||
* @file card_database_visual_view.h
|
||||
* @ingroup VisualCardDatabaseWidgets
|
||||
* @brief Icon-mode view of the card database, one card picture per item.
|
||||
*/
|
||||
|
||||
#ifndef CARD_DATABASE_VISUAL_VIEW_H
|
||||
#define CARD_DATABASE_VISUAL_VIEW_H
|
||||
|
||||
#include <QListView>
|
||||
#include <QSize>
|
||||
#include <qtmetamacros.h>
|
||||
|
||||
class CardDatabaseVisualDisplayModel;
|
||||
class ExactCard;
|
||||
class QEvent;
|
||||
class QMouseEvent;
|
||||
class QPoint;
|
||||
class QWheelEvent;
|
||||
|
||||
/**
|
||||
* @brief A wrapping grid of card pictures.
|
||||
*
|
||||
* The view virtualizes painting: it only asks the delegate for the items that are
|
||||
* on screen, so the whole card database can sit in the model without a widget (or
|
||||
* pixmap) per card. Clicks and hover are reported with the exact card under the
|
||||
* cursor, which is what the deck editor needs to add, decrement or preview a card.
|
||||
*/
|
||||
class CardDatabaseVisualView : public QListView
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit CardDatabaseVisualView(CardDatabaseVisualDisplayModel *_model, QWidget *parent = nullptr);
|
||||
|
||||
/**
|
||||
* @brief Sets the card size as a percentage of the base card width.
|
||||
* @param factor Percentage in the range of the card size slider.
|
||||
*/
|
||||
void setCardSizeFactor(int factor);
|
||||
|
||||
signals:
|
||||
/** @brief A card was pressed. The event is valid for the duration of the emission. */
|
||||
void cardClicked(QMouseEvent *event, const ExactCard &card);
|
||||
/** @brief The cursor moved onto a different card. */
|
||||
void cardHovered(const ExactCard &card);
|
||||
|
||||
protected:
|
||||
void mousePressEvent(QMouseEvent *event) override;
|
||||
void mouseMoveEvent(QMouseEvent *event) override;
|
||||
void leaveEvent(QEvent *event) override;
|
||||
bool viewportEvent(QEvent *event) override;
|
||||
void wheelEvent(QWheelEvent *event) override;
|
||||
|
||||
private:
|
||||
void updateHoveredRow(const QPoint &position);
|
||||
/// Pixels one wheel notch moves, scaled to the grid and the OS line count.
|
||||
[[nodiscard]] int wheelStep() const;
|
||||
|
||||
CardDatabaseVisualDisplayModel *model;
|
||||
/// Fraction of a line left over from a wheel event smaller than one notch.
|
||||
qreal wheelRemainder = 0.0;
|
||||
int cardSizeFactor = 100;
|
||||
int hoveredRow = -1;
|
||||
};
|
||||
|
||||
#endif // CARD_DATABASE_VISUAL_VIEW_H
|
||||
|
|
@ -5,40 +5,29 @@
|
|||
#include "../../../filters/syntax_help.h"
|
||||
#include "../../key_signals.h"
|
||||
#include "../../pixel_map_generator.h"
|
||||
#include "../cards/card_info_picture_widget.h"
|
||||
#include "../cards/card_info_picture_with_text_overlay_widget.h"
|
||||
#include "../cards/card_size_widget.h"
|
||||
#include "../deck_editor/card_database_view.h"
|
||||
#include "../general/layout_containers/flow_widget.h"
|
||||
#include "../utility/custom_line_edit.h"
|
||||
#include "card_database_display_model.h"
|
||||
#include "card_database_model.h"
|
||||
#include "card_database_visual_display_model.h"
|
||||
#include "card_database_visual_view.h"
|
||||
#include "deck_list_model.h"
|
||||
#include "libcockatrice/card/card_localization.h"
|
||||
#include "libcockatrice/card/database/card_database_loader.h"
|
||||
#include "libcockatrice/card/database/card_database_querier.h"
|
||||
#include "libcockatrice/card/printing/exact_card.h"
|
||||
#include "libcockatrice/card/printing/printing_info.h"
|
||||
#include "libcockatrice/filters/filter_card.h"
|
||||
#include "libcockatrice/filters/filter_tree.h"
|
||||
#include "visual_database_display_color_filter_widget.h"
|
||||
#include "visual_database_display_filter_toolbar_widget.h"
|
||||
|
||||
#include <QAbstractItemModel>
|
||||
#include <QAction>
|
||||
#include <QDebug>
|
||||
#include <QIcon>
|
||||
#include <QLabel>
|
||||
#include <QLineEdit>
|
||||
#include <QList>
|
||||
#include <QMessageLogger>
|
||||
#include <QModelIndex>
|
||||
#include <QPair>
|
||||
#include <QPersistentModelIndex>
|
||||
#include <QPushButton>
|
||||
#include <QScrollArea>
|
||||
#include <QScrollBar>
|
||||
#include <QSharedPointer>
|
||||
#include <QSizePolicy>
|
||||
#include <QSlider>
|
||||
#include <QStringLiteral>
|
||||
|
|
@ -48,14 +37,11 @@
|
|||
#include <QVBoxLayout>
|
||||
#include <QVariant>
|
||||
#include <QtGlobal>
|
||||
#include <algorithm>
|
||||
#include <libcockatrice/card/card_info_comparator.h>
|
||||
#include <libcockatrice/card/database/card_database.h>
|
||||
#include <libcockatrice/card/database/card_database_manager.h>
|
||||
#include <libcockatrice/deck_list/tree/inner_deck_list_node.h>
|
||||
#include <libcockatrice/settings/cards_display_settings.h>
|
||||
#include <libcockatrice/utility/qt_utils.h>
|
||||
#include <utility>
|
||||
|
||||
class QMouseEvent;
|
||||
|
||||
|
|
@ -64,11 +50,6 @@ VisualDatabaseDisplayWidget::VisualDatabaseDisplayWidget(QWidget *parent,
|
|||
DeckListModel *deckListModel)
|
||||
: QWidget(parent)
|
||||
{
|
||||
debounceTimer = new QTimer(this);
|
||||
debounceTimer->setSingleShot(true); // Ensure it only fires once after the timeout
|
||||
|
||||
connect(debounceTimer, &QTimer::timeout, this, &VisualDatabaseDisplayWidget::onSearchModelChanged);
|
||||
|
||||
// Create display model
|
||||
databaseDisplayModel = new CardDatabaseDisplayModel(this);
|
||||
databaseDisplayModel->setObjectName("databaseDisplayModel");
|
||||
|
|
@ -86,23 +67,12 @@ VisualDatabaseDisplayWidget::VisualDatabaseDisplayWidget(QWidget *parent,
|
|||
connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::cardSearchLanguageChanged, this,
|
||||
applyCardSearchLanguage);
|
||||
|
||||
cards = new QList<ExactCard>;
|
||||
connect(databaseDisplayModel, &CardDatabaseDisplayModel::modelDirty, this,
|
||||
&VisualDatabaseDisplayWidget::modelDirty);
|
||||
|
||||
// Set up main layout and widgets
|
||||
setMinimumSize(0, 0);
|
||||
setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
|
||||
mainLayout = QtUtils::makeLayout<QVBoxLayout>(this, 0, 1);
|
||||
setLayout(mainLayout);
|
||||
|
||||
flowWidget = new FlowWidget(this, Qt::Horizontal, Qt::ScrollBarAlwaysOff, Qt::ScrollBarPolicy::ScrollBarAsNeeded);
|
||||
cardSizeWidget = new CardSizeWidget(this, flowWidget,
|
||||
SettingsCache::instance().cardsDisplay().getVisualDatabaseDisplayCardSize());
|
||||
connect(cardSizeWidget, &CardSizeWidget::cardSizeSettingUpdated, &SettingsCache::instance().cardsDisplay(),
|
||||
&CardsDisplaySettings::setVisualDatabaseDisplayCardSize);
|
||||
cardSizeWidget->enableCtrlScrollResize(flowWidget);
|
||||
|
||||
searchContainer = new FlowWidget(this, Qt::Horizontal, Qt::ScrollBarAlwaysOff, Qt::ScrollBarAlwaysOff);
|
||||
|
||||
searchEdit = new SearchLineEdit();
|
||||
|
|
@ -159,6 +129,25 @@ VisualDatabaseDisplayWidget::VisualDatabaseDisplayWidget(QWidget *parent,
|
|||
connect(databaseView, &CardDatabaseView::relatedCardClicked, this,
|
||||
&VisualDatabaseDisplayWidget::onRelatedCardClicked);
|
||||
|
||||
// The visual grid is a real view on a flattened model: one row per displayed
|
||||
// card picture, painted on demand.
|
||||
visualDisplayModel = new CardDatabaseVisualDisplayModel(databaseDisplayModel, CardDatabaseManager::query(), this);
|
||||
|
||||
cardView = new CardDatabaseVisualView(visualDisplayModel, this);
|
||||
cardView->setObjectName("visualCardView");
|
||||
connect(cardView, &CardDatabaseVisualView::cardClicked, this,
|
||||
&VisualDatabaseDisplayWidget::cardClickedDatabaseDisplay);
|
||||
connect(cardView, &CardDatabaseVisualView::cardHovered, this,
|
||||
&VisualDatabaseDisplayWidget::cardHoveredDatabaseDisplay);
|
||||
|
||||
const int cardSize = SettingsCache::instance().cardsDisplay().getVisualDatabaseDisplayCardSize();
|
||||
cardSizeWidget = new CardSizeWidget(this, nullptr, cardSize);
|
||||
connect(cardSizeWidget, &CardSizeWidget::cardSizeSettingUpdated, &SettingsCache::instance().cardsDisplay(),
|
||||
&CardsDisplaySettings::setVisualDatabaseDisplayCardSize);
|
||||
connect(cardSizeWidget->getSlider(), &QSlider::valueChanged, cardView, &CardDatabaseVisualView::setCardSizeFactor);
|
||||
cardView->setCardSizeFactor(cardSize);
|
||||
cardSizeWidget->enableCtrlScrollResize(cardView);
|
||||
|
||||
colorFilterWidget = new VisualDatabaseDisplayColorFilterWidget(this, filterModel);
|
||||
|
||||
filterContainer = new VisualDatabaseDisplayFilterToolbarWidget(this, deckListModel);
|
||||
|
|
@ -190,8 +179,6 @@ VisualDatabaseDisplayWidget::VisualDatabaseDisplayWidget(QWidget *parent,
|
|||
databaseLoadIndicator->setVisible(false);
|
||||
}
|
||||
|
||||
QScrollBar *scrollBar = flowWidget->scrollArea->verticalScrollBar();
|
||||
connect(scrollBar, &QScrollBar::valueChanged, [this](int /*value*/) { loadCurrentPage(); });
|
||||
retranslateUi();
|
||||
}
|
||||
|
||||
|
|
@ -210,7 +197,7 @@ void VisualDatabaseDisplayWidget::initialize()
|
|||
|
||||
mainLayout->addWidget(databaseView);
|
||||
|
||||
mainLayout->addWidget(flowWidget);
|
||||
mainLayout->addWidget(cardView);
|
||||
|
||||
mainLayout->addWidget(cardSizeWidget);
|
||||
|
||||
|
|
@ -241,8 +228,6 @@ void VisualDatabaseDisplayWidget::initializeFilters()
|
|||
|
||||
databaseDisplayModel->setFilterTree(filterModel->filterTree());
|
||||
|
||||
QTimer::singleShot(5000, this, [this] { loadCurrentPage(); });
|
||||
|
||||
retranslateUi();
|
||||
});
|
||||
}
|
||||
|
|
@ -278,71 +263,36 @@ void VisualDatabaseDisplayWidget::updateDisplayModeButton()
|
|||
displayModeButton->setText(displayModeButton->isChecked() ? tr("Table") : tr("Visual"));
|
||||
}
|
||||
|
||||
void VisualDatabaseDisplayWidget::resizeEvent(QResizeEvent *event)
|
||||
{
|
||||
QWidget::resizeEvent(event);
|
||||
loadCurrentPage();
|
||||
}
|
||||
|
||||
void VisualDatabaseDisplayWidget::onDisplayModeChanged(bool checked)
|
||||
{
|
||||
updateDisplayModeButton();
|
||||
if (checked) {
|
||||
// Table mode
|
||||
flowWidget->setVisible(false);
|
||||
cardView->setVisible(false);
|
||||
cardSizeWidget->setVisible(false);
|
||||
databaseView->setItemDelegate(new QStyledItemDelegate(databaseView));
|
||||
databaseView->setVisible(true);
|
||||
} else {
|
||||
// Visual mode
|
||||
flowWidget->setVisible(true);
|
||||
cardView->setVisible(true);
|
||||
cardSizeWidget->setVisible(true);
|
||||
databaseView->setVisible(false);
|
||||
populateCards();
|
||||
}
|
||||
}
|
||||
|
||||
void VisualDatabaseDisplayWidget::onClick(QMouseEvent *event, const ExactCard &card)
|
||||
{
|
||||
emit cardClickedDatabaseDisplay(event, card);
|
||||
}
|
||||
|
||||
void VisualDatabaseDisplayWidget::onHover(const ExactCard &hoveredCard)
|
||||
{
|
||||
emit cardHoveredDatabaseDisplay(hoveredCard);
|
||||
}
|
||||
|
||||
void VisualDatabaseDisplayWidget::addCardToDisplay(const ExactCard &cardToAdd)
|
||||
{
|
||||
cards->append(cardToAdd);
|
||||
auto *display = new CardInfoPictureWithTextOverlayWidget(flowWidget, false);
|
||||
display->setScaleFactor(cardSizeWidget->getSlider()->value());
|
||||
display->setCard(cardToAdd);
|
||||
flowWidget->addWidget(display);
|
||||
connect(display, &CardInfoPictureWithTextOverlayWidget::cardClicked, this, &VisualDatabaseDisplayWidget::onClick);
|
||||
connect(display, &CardInfoPictureWithTextOverlayWidget::hoveredOnCard, this, &VisualDatabaseDisplayWidget::onHover);
|
||||
connect(cardSizeWidget->getSlider(), &QSlider::valueChanged, display, &CardInfoPictureWidget::setScaleFactor);
|
||||
}
|
||||
|
||||
bool VisualDatabaseDisplayWidget::isVisualDisplayMode() const
|
||||
{
|
||||
return !displayModeButton->isChecked();
|
||||
}
|
||||
|
||||
QStringList VisualDatabaseDisplayWidget::collectSetFilterTerms() const
|
||||
{
|
||||
return filterModel->getFilterTermsOfType(CardFilter::AttrSet);
|
||||
}
|
||||
|
||||
void VisualDatabaseDisplayWidget::onSearchModelChanged()
|
||||
{
|
||||
if (flowWidget->isVisible()) {
|
||||
// Clear the current page and prepare for new data
|
||||
flowWidget->clearLayout(); // Clear existing cards
|
||||
cards->clear(); // Clear the card list
|
||||
// Reset scrollbar position to the top after loading new cards
|
||||
QScrollBar *scrollBar = flowWidget->scrollArea->verticalScrollBar();
|
||||
scrollBar->setValue(0); // Reset scrollbar to top
|
||||
|
||||
currentPage = 0;
|
||||
loadCurrentPage();
|
||||
qCDebug(VisualDatabaseDisplayLog) << "Search model changed";
|
||||
}
|
||||
visualDisplayModel->setSetFilterTerms(collectSetFilterTerms());
|
||||
}
|
||||
|
||||
void VisualDatabaseDisplayWidget::onSelectedCardChanged(const QString &cardName)
|
||||
|
|
@ -368,113 +318,3 @@ void VisualDatabaseDisplayWidget::onRelatedCardClicked(const QString &relatedCar
|
|||
ExactCard exactCard = CardDatabaseManager::query()->guessCard({relatedCard});
|
||||
emit cardInfoRequested(exactCard);
|
||||
}
|
||||
|
||||
bool VisualDatabaseDisplayWidget::nearEndOfPage() const
|
||||
{
|
||||
if (!flowWidget->isVisible()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
QScrollBar *scrollBar = flowWidget->scrollArea->verticalScrollBar();
|
||||
if (scrollBar->value() + scrollBar->pageStep() * 2 < scrollBar->maximum()) {
|
||||
return false; // there is at least two pages of space to scroll remaining
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void VisualDatabaseDisplayWidget::loadCurrentPage()
|
||||
{
|
||||
// Ensure only the initial page is loaded
|
||||
if (currentPage == 0) {
|
||||
if (!initialLoadScheduled) {
|
||||
initialLoadScheduled = true;
|
||||
qCDebug(VisualDatabaseDisplayLog) << "Loading the first page";
|
||||
// Defer the first page so the tab switch stays responsive. The card
|
||||
// grid builds one event loop turn later. This also applies to
|
||||
// search-driven reloads, which reset currentPage back to 0.
|
||||
QTimer::singleShot(0, this, [this] {
|
||||
initialLoadScheduled = false;
|
||||
populateCards();
|
||||
});
|
||||
}
|
||||
} else if (nearEndOfPage()) {
|
||||
// If not the first page, just load the next page and append to the flow widget
|
||||
loadNextPage();
|
||||
}
|
||||
}
|
||||
|
||||
void VisualDatabaseDisplayWidget::populateCards()
|
||||
{
|
||||
cards->clear();
|
||||
loadNextPage();
|
||||
}
|
||||
|
||||
void VisualDatabaseDisplayWidget::loadNextPage()
|
||||
{
|
||||
// Calculate the start and end indices for the next page
|
||||
int rowCount = databaseDisplayModel->rowCount();
|
||||
int start = currentPage * cardsPerPage;
|
||||
int end = qMin(start + cardsPerPage, rowCount);
|
||||
|
||||
// Load more cards if we are at the end of the current list and can fetch more
|
||||
if (end >= rowCount && databaseDisplayModel->canFetchMore(QModelIndex())) {
|
||||
databaseDisplayModel->fetchMore(QModelIndex());
|
||||
}
|
||||
|
||||
// Load the next page of cards and add them to the flow widget
|
||||
flowWidget->setUpdatesEnabled(false);
|
||||
loadPage(start, end);
|
||||
flowWidget->setUpdatesEnabled(true);
|
||||
}
|
||||
|
||||
void VisualDatabaseDisplayWidget::loadPage(int start, int end)
|
||||
{
|
||||
QList<const CardFilter *> setFilters = filterModel->getFiltersOfType(CardFilter::AttrSet);
|
||||
for (int row = start; row < end; ++row) {
|
||||
qCDebug(VisualDatabaseDisplayLog) << "Adding " << row;
|
||||
QModelIndex index = databaseDisplayModel->index(row, CardDatabaseModel::NameColumn);
|
||||
QVariant name = databaseDisplayModel->data(index, Qt::DisplayRole);
|
||||
qCDebug(VisualDatabaseDisplayLog) << name.toString();
|
||||
|
||||
if (CardInfoPtr info = CardDatabaseManager::query()->getCardInfo(name.toString())) {
|
||||
if (!setFilters.empty()) {
|
||||
const auto &setMap = info->getSets();
|
||||
for (const CardFilter *setFilter : setFilters) {
|
||||
const auto it = setMap.constFind(setFilter->term());
|
||||
if (it != setMap.constEnd()) {
|
||||
for (const PrintingInfo &printing : it.value()) {
|
||||
addCardToDisplay(ExactCard(info, printing));
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
addCardToDisplay(CardDatabaseManager::query()->getPreferredCard(info));
|
||||
}
|
||||
} else {
|
||||
qCDebug(VisualDatabaseDisplayLog) << "Card not found in database!";
|
||||
}
|
||||
}
|
||||
currentPage++;
|
||||
}
|
||||
|
||||
void VisualDatabaseDisplayWidget::modelDirty() const
|
||||
{
|
||||
debounceTimer->start(debounceTime);
|
||||
}
|
||||
|
||||
void VisualDatabaseDisplayWidget::sortCardList(const QStringList &properties,
|
||||
Qt::SortOrder order = Qt::AscendingOrder) const
|
||||
{
|
||||
CardInfoComparator comparator(properties, order);
|
||||
std::sort(cards->begin(), cards->end(), [comparator](const ExactCard &a, const ExactCard &b) {
|
||||
return comparator(a.getCardPtr(), b.getCardPtr());
|
||||
});
|
||||
}
|
||||
|
||||
void VisualDatabaseDisplayWidget::databaseDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight)
|
||||
{
|
||||
(void)topLeft;
|
||||
(void)bottomRight;
|
||||
qCDebug(VisualDatabaseDisplayLog) << "Database Data changed";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,8 +9,6 @@
|
|||
|
||||
#include "libcockatrice/card/card_info.h"
|
||||
|
||||
#include <QLoggingCategory>
|
||||
#include <QMouseEvent>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <QWidget>
|
||||
|
|
@ -21,26 +19,21 @@
|
|||
class CardDatabaseDisplayModel;
|
||||
class CardDatabaseModel;
|
||||
class CardDatabaseView;
|
||||
class CardDatabaseVisualDisplayModel;
|
||||
class CardDatabaseVisualView;
|
||||
class CardSizeWidget;
|
||||
class DeckList;
|
||||
class DeckListModel;
|
||||
class ExactCard;
|
||||
class FilterTreeModel;
|
||||
class FlowWidget;
|
||||
class OverlapControlWidget;
|
||||
class QLabel;
|
||||
class QModelIndex;
|
||||
class QMouseEvent;
|
||||
class QPushButton;
|
||||
class QScrollArea;
|
||||
class QTimer;
|
||||
class QToolButton;
|
||||
class QVBoxLayout;
|
||||
class SearchLineEdit;
|
||||
class VisualDatabaseDisplayColorFilterWidget;
|
||||
class VisualDatabaseDisplayFilterToolbarWidget;
|
||||
template <typename T> class QList;
|
||||
|
||||
inline Q_LOGGING_CATEGORY(VisualDatabaseDisplayLog, "visual_database_display");
|
||||
|
||||
class VisualDatabaseDisplayWidget : public QWidget
|
||||
{
|
||||
|
|
@ -52,14 +45,6 @@ public:
|
|||
DeckListModel *deckListModel = nullptr);
|
||||
void retranslateUi();
|
||||
|
||||
void adjustCardsPerPage();
|
||||
void populateCards();
|
||||
void loadPage(int start, int end);
|
||||
void loadNextPage();
|
||||
void loadCurrentPage();
|
||||
void sortCardList(const QStringList &properties, Qt::SortOrder order) const;
|
||||
void setDeckList(const DeckList &new_deck_list_model);
|
||||
|
||||
/**
|
||||
* @brief Sets the callback used to create a custom zone from the add-to-zone menu.
|
||||
* The callback returns the name of the created zone, or an empty string if creation was cancelled.
|
||||
|
|
@ -87,6 +72,13 @@ public:
|
|||
bool isVisualDisplayMode() const;
|
||||
|
||||
public slots:
|
||||
/**
|
||||
* @brief Pushes the active set filters to the visual grid.
|
||||
*
|
||||
* The grid's rows follow the display model's filter and sort signals on their
|
||||
* own; this only forwards the set filters, which decide which printings the
|
||||
* grid shows.
|
||||
*/
|
||||
void onSearchModelChanged();
|
||||
|
||||
signals:
|
||||
|
|
@ -101,11 +93,6 @@ signals:
|
|||
|
||||
protected slots:
|
||||
void initialize();
|
||||
void onClick(QMouseEvent *event, const ExactCard &card);
|
||||
void onHover(const ExactCard &hoveredCard);
|
||||
void addCardToDisplay(const ExactCard &cardToAdd);
|
||||
void databaseDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight);
|
||||
void modelDirty() const;
|
||||
void onDisplayModeChanged(bool checked);
|
||||
|
||||
void onSelectedCardChanged(const QString &cardName);
|
||||
|
|
@ -125,31 +112,21 @@ private:
|
|||
QToolButton *clearFilterWidget;
|
||||
VisualDatabaseDisplayFilterToolbarWidget *filterContainer;
|
||||
CardDatabaseDisplayModel *databaseDisplayModel;
|
||||
CardDatabaseVisualDisplayModel *visualDisplayModel;
|
||||
CardDatabaseView *databaseView;
|
||||
CardDatabaseVisualView *cardView;
|
||||
std::function<QString()> newZoneCreator;
|
||||
QList<ExactCard> *cards;
|
||||
QVBoxLayout *mainLayout;
|
||||
QScrollArea *scrollArea;
|
||||
FlowWidget *flowWidget;
|
||||
QWidget *overlapCategories;
|
||||
QVBoxLayout *overlapCategoriesLayout;
|
||||
OverlapControlWidget *overlapControlWidget;
|
||||
CardSizeWidget *cardSizeWidget;
|
||||
QTimer *debounceTimer;
|
||||
|
||||
int debounceTime = 300; // in Ms
|
||||
int currentPage = 0; // Current page index
|
||||
int cardsPerPage = 100; // Number of cards per page
|
||||
bool filtersInitialized = false;
|
||||
bool initialLoadScheduled = false;
|
||||
|
||||
void initializeFilters();
|
||||
void highlightAllSearchEdit();
|
||||
void updateDisplayModeButton();
|
||||
bool nearEndOfPage() const;
|
||||
[[nodiscard]] QStringList collectSetFilterTerms() const;
|
||||
|
||||
protected:
|
||||
void resizeEvent(QResizeEvent *event) override;
|
||||
void showEvent(QShowEvent *event) override;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -55,6 +55,13 @@
|
|||
#include "widgets/server/handle_public_servers.h"
|
||||
#include "widgets/settings_page/abstract_settings_page.h"
|
||||
#include "widgets/tabs/api/commander_spellbook/handle_commander_brackets.h"
|
||||
#ifdef WITH_GUI_TEST
|
||||
#include "pqEventObserver.h"
|
||||
#include "pqEventSource.h"
|
||||
#include "pqEventTypes.h"
|
||||
#include "pqTestUtility.h"
|
||||
#include "recording.h"
|
||||
#endif
|
||||
|
||||
#include <QAction>
|
||||
#include <QApplication>
|
||||
|
|
@ -254,7 +261,11 @@ void MainWindow::actWatchReplay()
|
|||
|
||||
void MainWindow::localGameEnded()
|
||||
{
|
||||
delete localServer;
|
||||
// Defer deletion so the LocalClients (already scheduled with deleteLater() in TabSupervisor::stop)
|
||||
// and any queued commands are processed before the server and its interfaces are destroyed.
|
||||
if (localServer) {
|
||||
localServer->deleteLater();
|
||||
}
|
||||
localServer = nullptr;
|
||||
|
||||
aConnect->setEnabled(true);
|
||||
|
|
@ -306,6 +317,7 @@ void MainWindow::actAbout()
|
|||
mb.setIconPixmap(
|
||||
themePixmap(QStringLiteral("cockatrice")).scaled(64, 64, Qt::KeepAspectRatio, Qt::SmoothTransformation));
|
||||
mb.setTextInteractionFlags(Qt::TextBrowserInteraction);
|
||||
mb.setObjectName("aboutDialog");
|
||||
mb.exec();
|
||||
}
|
||||
|
||||
|
|
@ -344,6 +356,24 @@ void MainWindow::actOpenSettingsFolder()
|
|||
QtUtils::openPathInFileManager(dir);
|
||||
}
|
||||
|
||||
#ifdef WITH_GUI_TEST
|
||||
void MainWindow::actRecord()
|
||||
{
|
||||
QString filename = QFileDialog::getSaveFileName(this, tr("Test File Name"), QString(), tr("XML Files (*.xml)"));
|
||||
if (!filename.isEmpty()) {
|
||||
testUtility->recordTests(filename);
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::actPlayRecording()
|
||||
{
|
||||
QString filename = QFileDialog::getOpenFileName(this, tr("Test File Name"), QString(), tr("XML Files (*.xml)"));
|
||||
if (!filename.isEmpty()) {
|
||||
testUtility->playTests(filename);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
void MainWindow::retranslateUi()
|
||||
{
|
||||
connectionController->refreshWindowTitle();
|
||||
|
|
@ -388,6 +418,10 @@ void MainWindow::retranslateUi()
|
|||
aViewLog->setText(tr("View &Debug Log"));
|
||||
aOpenSettingsFolder->setText(tr("Open Settings Folder"));
|
||||
aFirstRunWizard->setText(tr("Re-run Onboarding Wizard..."));
|
||||
#ifdef WITH_GUI_TEST
|
||||
aRecord->setText(tr("&Record Test..."));
|
||||
aPlayRecording->setText(tr("&Play Test..."));
|
||||
#endif
|
||||
|
||||
aShow->setText(tr("Show/Hide"));
|
||||
|
||||
|
|
@ -397,24 +431,33 @@ void MainWindow::retranslateUi()
|
|||
void MainWindow::createActions()
|
||||
{
|
||||
aConnect = new QAction(this);
|
||||
aConnect->setObjectName("actionConnect");
|
||||
connect(aConnect, &QAction::triggered, this, &MainWindow::actConnect);
|
||||
aDisconnect = new QAction(this);
|
||||
aDisconnect->setObjectName("actionDisconnect");
|
||||
aDisconnect->setEnabled(false);
|
||||
connect(aDisconnect, &QAction::triggered, this, &MainWindow::actDisconnect);
|
||||
aSinglePlayer = new QAction(this);
|
||||
aSinglePlayer->setObjectName("actionSinglePlayer");
|
||||
connect(aSinglePlayer, &QAction::triggered, this, &MainWindow::actSinglePlayer);
|
||||
aWatchReplay = new QAction(this);
|
||||
aWatchReplay->setObjectName("actionWatchReplay");
|
||||
connect(aWatchReplay, &QAction::triggered, this, &MainWindow::actWatchReplay);
|
||||
aFullScreen = new QAction(this);
|
||||
aFullScreen->setObjectName("actionFullScreen");
|
||||
aFullScreen->setCheckable(true);
|
||||
connect(aFullScreen, &QAction::toggled, this, &MainWindow::actFullScreen);
|
||||
aRegister = new QAction(this);
|
||||
aRegister->setObjectName("actionRegister");
|
||||
connect(aRegister, &QAction::triggered, connectionController, &ConnectionController::registerToServer);
|
||||
aForgotPassword = new QAction(this);
|
||||
aForgotPassword->setObjectName("actionForgotPassword");
|
||||
connect(aForgotPassword, &QAction::triggered, connectionController, &ConnectionController::forgotPasswordRequest);
|
||||
aSettings = new QAction(this);
|
||||
aSettings->setObjectName("actionSettings");
|
||||
connect(aSettings, &QAction::triggered, this, &MainWindow::actSettings);
|
||||
aExit = new QAction(this);
|
||||
aExit->setObjectName("actionExit");
|
||||
connect(aExit, &QAction::triggered, this, &MainWindow::actExit);
|
||||
|
||||
aManageSets = new QAction(QString(), this);
|
||||
|
|
@ -451,6 +494,12 @@ void MainWindow::createActions()
|
|||
connect(aOpenSettingsFolder, &QAction::triggered, this, &MainWindow::actOpenSettingsFolder);
|
||||
aFirstRunWizard = new QAction(this);
|
||||
connect(aFirstRunWizard, &QAction::triggered, this, [this] { runFirstRunWizard(); });
|
||||
#ifdef WITH_GUI_TEST
|
||||
aRecord = new QAction(this);
|
||||
connect(aRecord, &QAction::triggered, this, &MainWindow::actRecord);
|
||||
aPlayRecording = new QAction(this);
|
||||
connect(aPlayRecording, &QAction::triggered, this, &MainWindow::actPlayRecording);
|
||||
#endif
|
||||
|
||||
aShow = new QAction(this);
|
||||
connect(aShow, &QAction::triggered, this, &MainWindow::actShow);
|
||||
|
|
@ -491,7 +540,9 @@ void MainWindow::createActions()
|
|||
|
||||
void MainWindow::createMenus()
|
||||
{
|
||||
menuBar()->setObjectName("menubar");
|
||||
cockatriceMenu = menuBar()->addMenu(QString());
|
||||
cockatriceMenu->setObjectName("cockatriceMenu");
|
||||
cockatriceMenu->addAction(aConnect);
|
||||
cockatriceMenu->addAction(aDisconnect);
|
||||
cockatriceMenu->addAction(aRegister);
|
||||
|
|
@ -507,6 +558,7 @@ void MainWindow::createMenus()
|
|||
cockatriceMenu->addAction(aExit);
|
||||
|
||||
dbMenu = menuBar()->addMenu(QString());
|
||||
dbMenu->setObjectName("dbMenu");
|
||||
dbMenu->addAction(aManageSets);
|
||||
dbMenu->addAction(aEditTokens);
|
||||
dbMenu->addSeparator();
|
||||
|
|
@ -517,8 +569,10 @@ void MainWindow::createMenus()
|
|||
dbMenu->addAction(aReloadCardDatabase);
|
||||
|
||||
tabsMenu = menuBar()->addMenu(QString());
|
||||
tabsMenu->setObjectName("tabsMenu");
|
||||
|
||||
helpMenu = menuBar()->addMenu(QString());
|
||||
helpMenu->setObjectName("helpMenu");
|
||||
helpMenu->addAction(aAbout);
|
||||
helpMenu->addAction(aTips);
|
||||
helpMenu->addSeparator();
|
||||
|
|
@ -531,12 +585,19 @@ void MainWindow::createMenus()
|
|||
helpMenu->addAction(aOpenSettingsFolder);
|
||||
helpMenu->addSeparator();
|
||||
helpMenu->addAction(aFirstRunWizard);
|
||||
#ifdef WITH_GUI_TEST
|
||||
helpMenu->addSeparator();
|
||||
helpMenu->addAction(aRecord);
|
||||
helpMenu->addAction(aPlayRecording);
|
||||
#endif
|
||||
}
|
||||
|
||||
MainWindow::MainWindow(QWidget *parent)
|
||||
: QMainWindow(parent), localServer(nullptr), bHasActivated(false), askedForDbUpdater(false),
|
||||
: QMainWindow(parent), localServer(nullptr), bHasActivated(false), askedForDbUpdater(false), noConnect(false),
|
||||
cardUpdateProcess(nullptr), logviewDialog(nullptr)
|
||||
{
|
||||
setObjectName("MainWindow");
|
||||
|
||||
connect(&SettingsCache::instance().cacheStorage(), &CacheStorageSettings::pixmapCacheSizeChanged, this,
|
||||
&MainWindow::pixmapCacheSizeChanged);
|
||||
pixmapCacheSizeChanged(SettingsCache::instance().cacheStorage().getPixmapCacheSize());
|
||||
|
|
@ -622,6 +683,13 @@ MainWindow::MainWindow(QWidget *parent)
|
|||
// run startup check async
|
||||
QTimer::singleShot(0, this, &MainWindow::startupConfigCheck);
|
||||
QTimer::singleShot(0, this, &MainWindow::applyStartupDestination);
|
||||
|
||||
#ifdef WITH_GUI_TEST
|
||||
testUtility = new pqTestUtility(this);
|
||||
testUtility->setRecordWithDialog(true);
|
||||
testUtility->addEventObserver("xml", new XMLEventObserver(this));
|
||||
testUtility->addEventSource("xml", new XMLEventSource(this));
|
||||
#endif
|
||||
}
|
||||
|
||||
void MainWindow::startupConfigCheck()
|
||||
|
|
@ -776,6 +844,11 @@ void MainWindow::runFirstRunWizard(bool firstRun)
|
|||
*/
|
||||
void MainWindow::applyStartupDestination()
|
||||
{
|
||||
if (noConnect) {
|
||||
qCInfo(WindowMainStartupAutoconnectLog) << "Startup destination suppressed (--no-connect)";
|
||||
return;
|
||||
}
|
||||
|
||||
// An explicit command-line connect takes precedence over the startup destination.
|
||||
if (!connectTo.isEmpty()) {
|
||||
return;
|
||||
|
|
@ -1045,6 +1118,10 @@ void MainWindow::handleCockatriceLink(const QString &url)
|
|||
|
||||
void MainWindow::attemptStartupAutoConnect()
|
||||
{
|
||||
if (noConnect) {
|
||||
qCInfo(WindowMainStartupAutoconnectLog) << "Auto-connect suppressed (--no-connect)";
|
||||
return;
|
||||
}
|
||||
if (startupAutoConnectAttempted || skipStartupAutoConnect) {
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@
|
|||
class QAction;
|
||||
class QToolButton;
|
||||
class QWidget;
|
||||
class pqTestUtility;
|
||||
struct LocalGameOptions;
|
||||
|
||||
inline Q_LOGGING_CATEGORY(WindowMainLog, "window_main");
|
||||
|
|
@ -104,6 +105,11 @@ private slots:
|
|||
void actUpdate();
|
||||
void actViewLog();
|
||||
void actOpenSettingsFolder();
|
||||
#ifdef WITH_GUI_TEST
|
||||
void actRecord();
|
||||
void actPlayRecording();
|
||||
#endif
|
||||
|
||||
void actShow();
|
||||
void showWindowIfHidden();
|
||||
void onUrlChainFinished(bool connected);
|
||||
|
|
@ -185,6 +191,9 @@ private:
|
|||
*aReloadCardDatabase;
|
||||
QAction *aTips, *aUpdate, *aCheckCardUpdates, *aCheckCardUpdatesBackground, *aFirstRunWizard, *aStatusBar,
|
||||
*aViewLog, *aOpenSettingsFolder;
|
||||
#ifdef WITH_GUI_TEST
|
||||
QAction *aRecord, *aPlayRecording;
|
||||
#endif
|
||||
|
||||
TabSupervisor *tabSupervisor;
|
||||
IntentUrlParser *urlParser;
|
||||
|
|
@ -208,6 +217,7 @@ private:
|
|||
bool skipStartupAutoConnect = false;
|
||||
bool startupAutoConnectAttempted = false;
|
||||
bool firstRunWizardActive = false;
|
||||
bool noConnect = false; ///< --no-connect: skip every startup connection
|
||||
QProcess *cardUpdateProcess;
|
||||
QByteArray cardUpdateOutputBuffer;
|
||||
QByteArray cardUpdateErrorOutput; ///< diagnostic output collected from the updater's stderr
|
||||
|
|
@ -219,6 +229,9 @@ private:
|
|||
GameReplay *replay;
|
||||
DlgTipOfTheDay *tip;
|
||||
QUrl connectTo;
|
||||
#ifdef WITH_GUI_TEST
|
||||
pqTestUtility *testUtility;
|
||||
#endif
|
||||
|
||||
public:
|
||||
explicit MainWindow(QWidget *parent = nullptr);
|
||||
|
|
@ -226,6 +239,16 @@ public:
|
|||
{
|
||||
connectTo = QUrl(QString("cockatrice://%1").arg(url));
|
||||
}
|
||||
/**
|
||||
* @brief Suppresses every startup connection: --connect, auto-connect and the
|
||||
* configured startup destination. Used by the GUI test harness so a
|
||||
* playback run never dials out.
|
||||
*/
|
||||
void setNoConnect(bool value)
|
||||
{
|
||||
noConnect = value;
|
||||
}
|
||||
|
||||
// When set, the window's own startup connection (--connect or auto-connect
|
||||
// on first activation) is skipped. Used for activation launches: the intent
|
||||
// chain triggered by a cockatrice:// URL owns the connection, and letting
|
||||
|
|
@ -266,6 +289,13 @@ public:
|
|||
*/
|
||||
void requestInstallerClose();
|
||||
|
||||
#ifdef WITH_GUI_TEST
|
||||
pqTestUtility *getTestUtility() const
|
||||
{
|
||||
return testUtility;
|
||||
}
|
||||
#endif
|
||||
|
||||
protected:
|
||||
void closeEvent(QCloseEvent *event) override;
|
||||
void changeEvent(QEvent *event) override;
|
||||
|
|
|
|||
|
|
@ -39,6 +39,10 @@
|
|||
#include "single_instance_manager.h"
|
||||
#include "version_string.h"
|
||||
|
||||
#ifdef WITH_GUI_TEST
|
||||
#include "pqTestUtility.h"
|
||||
#endif
|
||||
|
||||
#include <QApplication>
|
||||
#include <QByteArray>
|
||||
#include <QByteArrayView>
|
||||
|
|
@ -62,10 +66,12 @@
|
|||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <QStringLiteral>
|
||||
#include <QThread>
|
||||
#include <QTranslator>
|
||||
#include <QUrl>
|
||||
#include <QtGlobal>
|
||||
#include <algorithm>
|
||||
#include <cstdlib>
|
||||
#include <libcockatrice/card/database/card_database_manager.h>
|
||||
#include <libcockatrice/rng/rng_sfmt.h>
|
||||
#include <libcockatrice/settings/card_database_settings.h>
|
||||
|
|
@ -343,9 +349,16 @@ int main(int argc, char *argv[])
|
|||
parser.addOptions(
|
||||
{{{"c", "connect"}, QCoreApplication::translate("main", "Connect on startup"), "user:pass@host:port"},
|
||||
{{"d", "debug-output"}, QCoreApplication::translate("main", "Debug to file")},
|
||||
{"simulate-crash",
|
||||
QCoreApplication::translate("main",
|
||||
"Show the crash-report dialog (saves a mock file when no DSN is configured)")}});
|
||||
{"simulate-crash", QCoreApplication::translate(
|
||||
"main", "Show the crash-report dialog (saves a mock file when no DSN is configured)")},
|
||||
{"no-connect", QCoreApplication::translate("main", "Do not connect to any server on startup")}
|
||||
#ifdef WITH_GUI_TEST
|
||||
,
|
||||
{{"t", "test"},
|
||||
QCoreApplication::translate("main", "Play back a QtTesting XML test file and exit"),
|
||||
"test-file.xml"}
|
||||
#endif
|
||||
});
|
||||
|
||||
parser.process(app);
|
||||
|
||||
|
|
@ -483,6 +496,9 @@ int main(int argc, char *argv[])
|
|||
if (parser.isSet("connect")) {
|
||||
ui.setConnectTo(parser.value("connect"));
|
||||
}
|
||||
if (parser.isSet("no-connect")) {
|
||||
ui.setNoConnect(true);
|
||||
}
|
||||
qCInfo(MainLog) << "MainWindow constructor finished";
|
||||
|
||||
ui.setWindowIcon(themePixmap(QStringLiteral("cockatrice")));
|
||||
|
|
@ -523,6 +539,27 @@ int main(int argc, char *argv[])
|
|||
handleActivation(file);
|
||||
}
|
||||
|
||||
#ifdef WITH_GUI_TEST
|
||||
if (parser.isSet("test")) {
|
||||
QString testFile = parser.value("test");
|
||||
qCInfo(MainLog) << "Playing test file:" << testFile;
|
||||
QTimer::singleShot(0, &ui, [&ui, testFile]() {
|
||||
bool success = ui.getTestUtility()->playTests(testFile);
|
||||
qCInfo(MainLog) << "Test playback" << (success ? "succeeded" : "FAILED");
|
||||
qApp->exit(success ? 0 : 1);
|
||||
|
||||
// An event that fails while a modal dialog is open aborts the dispatcher,
|
||||
// but the dialog's nested event loop can then keep the application alive
|
||||
// forever. Hard-exit after a grace period so a red test always terminates.
|
||||
auto *watchdog = QThread::create([success] {
|
||||
QThread::sleep(10);
|
||||
::_Exit(success ? 0 : 1);
|
||||
});
|
||||
watchdog->start();
|
||||
});
|
||||
}
|
||||
#endif
|
||||
|
||||
// Connect to future file/URL events from other instances
|
||||
QObject::connect(&instance, &SingleInstanceManager::filesReceived, [&handleActivation](const QStringList &files) {
|
||||
for (const QString &file : files) {
|
||||
|
|
|
|||
1
external/qttesting
vendored
Submodule
1
external/qttesting
vendored
Submodule
|
|
@ -0,0 +1 @@
|
|||
Subproject commit c7bb5d372133e94c75cbdc70b04e039b864ea379
|
||||
|
|
@ -33,6 +33,7 @@ cmakefile="CMakeLists.txt"
|
|||
cmakedir="cmake/.*\\.cmake"
|
||||
cmakeinclude=("cmake/gtest-CMakeLists.txt.in")
|
||||
scripts="*.sh"
|
||||
scriptsrx="[^/]*\\.sh$"
|
||||
color="--"
|
||||
verbosity=0
|
||||
sep="----------"
|
||||
|
|
@ -254,7 +255,7 @@ if [[ $branch ]]; then
|
|||
if [[ $do_shell ]]; then
|
||||
shell_names=()
|
||||
for name in "${basenames[@]}"; do
|
||||
filerx="(^|/)$scripts$"
|
||||
filerx="(^|/)$scriptsrx"
|
||||
if [[ $name =~ $filerx ]]; then
|
||||
shell_names+=("$name")
|
||||
fi
|
||||
|
|
|
|||
|
|
@ -1,7 +1,3 @@
|
|||
set(CMAKE_AUTOMOC ON)
|
||||
set(CMAKE_AUTOUIC ON)
|
||||
set(CMAKE_AUTORCC ON)
|
||||
|
||||
set(HEADERS
|
||||
libcockatrice/card/card_info.h
|
||||
libcockatrice/card/card_info_comparator.h
|
||||
|
|
|
|||
|
|
@ -1,7 +1,3 @@
|
|||
set(CMAKE_AUTOMOC ON)
|
||||
set(CMAKE_AUTOUIC ON)
|
||||
set(CMAKE_AUTORCC ON)
|
||||
|
||||
set(HEADERS
|
||||
libcockatrice/deck_list/tree/abstract_deck_list_card_node.h
|
||||
libcockatrice/deck_list/tree/abstract_deck_list_node.h
|
||||
|
|
|
|||
|
|
@ -1,7 +1,3 @@
|
|||
set(CMAKE_AUTOMOC ON)
|
||||
set(CMAKE_AUTOUIC ON)
|
||||
set(CMAKE_AUTORCC ON)
|
||||
|
||||
set(HEADERS libcockatrice/filters/filter_card.h libcockatrice/filters/filter_string.h
|
||||
libcockatrice/filters/filter_tree.h
|
||||
)
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ private:
|
|||
enum Attr a;
|
||||
|
||||
public:
|
||||
CardFilter(QString &term, Type type, Attr attr) : trm(term), t(type), a(attr)
|
||||
CardFilter(const QString &term, Type type, Attr attr) : trm(term), t(type), a(attr)
|
||||
{
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,3 @@
|
|||
set(CMAKE_AUTOMOC ON)
|
||||
set(CMAKE_AUTOUIC ON)
|
||||
set(CMAKE_AUTORCC ON)
|
||||
|
||||
set(HEADERS
|
||||
libcockatrice/game/abstract_game.h
|
||||
libcockatrice/game/board/arrow_data.h
|
||||
|
|
|
|||
|
|
@ -192,12 +192,14 @@ void ZoneViewZoneLogic::setWriteableRevealZone(bool _writeableRevealZone)
|
|||
writeableRevealZone = _writeableRevealZone;
|
||||
}
|
||||
|
||||
PendingCommand *ZoneViewZoneLogic::requestZoneDump()
|
||||
PendingCommand *ZoneViewZoneLogic::prepareZoneDump()
|
||||
{
|
||||
PlayerActions *actions = getPlayer()->getPlayerActions();
|
||||
PendingCommand *pend = actions->prepareZoneDump(this, numberCards);
|
||||
if (pend != nullptr) {
|
||||
actions->sendGameCommand(pend);
|
||||
}
|
||||
return pend;
|
||||
return getPlayer()->getPlayerActions()->prepareZoneDump(this, numberCards);
|
||||
}
|
||||
|
||||
void ZoneViewZoneLogic::sendZoneDump(PendingCommand *pendingCommand)
|
||||
{
|
||||
if (pendingCommand != nullptr) {
|
||||
getPlayer()->getPlayerActions()->sendGameCommand(pendingCommand);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -88,14 +88,21 @@ public:
|
|||
}
|
||||
|
||||
/**
|
||||
* @brief Prepares and sends the zone-dump command for this view.
|
||||
* @brief Builds the zone-dump command for this view without sending it.
|
||||
* @return The pending command, or nullptr when there is no zone to dump.
|
||||
*
|
||||
* Keeps command construction in game/ so graphics views don't reach
|
||||
* through the player into PlayerActions to set it up. The command is sent
|
||||
* asynchronously, so callers connect to finished() after calling this.
|
||||
* through the player into PlayerActions to set it up. The response can
|
||||
* arrive synchronously (local games), so callers must connect to
|
||||
* PendingCommand::finished before calling sendZoneDump().
|
||||
*/
|
||||
PendingCommand *requestZoneDump();
|
||||
PendingCommand *prepareZoneDump();
|
||||
|
||||
/**
|
||||
* @brief Sends a command returned by prepareZoneDump().
|
||||
* @param pendingCommand The prepared command; nullptr is ignored.
|
||||
*/
|
||||
void sendZoneDump(PendingCommand *pendingCommand);
|
||||
|
||||
protected:
|
||||
void addCardImpl(CardState *card, int x, int y) override;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,3 @@
|
|||
set(CMAKE_AUTOMOC ON)
|
||||
set(CMAKE_AUTOUIC ON)
|
||||
set(CMAKE_AUTORCC ON)
|
||||
|
||||
set(HEADERS
|
||||
libcockatrice/interfaces/interface_cache_storage_settings_provider.h
|
||||
libcockatrice/interfaces/interface_card_database_path_provider.h
|
||||
|
|
|
|||
|
|
@ -1,7 +1,3 @@
|
|||
set(CMAKE_AUTOMOC ON)
|
||||
set(CMAKE_AUTOUIC ON)
|
||||
set(CMAKE_AUTORCC ON)
|
||||
|
||||
add_subdirectory(libcockatrice/models/database)
|
||||
add_subdirectory(libcockatrice/models/deck_list)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,7 @@
|
|||
set(CMAKE_AUTOMOC ON)
|
||||
set(CMAKE_AUTOUIC ON)
|
||||
set(CMAKE_AUTORCC ON)
|
||||
|
||||
set(HEADERS
|
||||
card_database_model.h
|
||||
card_database_display_model.h
|
||||
card_database_visual_display_model.h
|
||||
card/card_completer_proxy_model.h
|
||||
card/card_search_model.h
|
||||
card_set/card_sets_model.h
|
||||
|
|
@ -17,6 +14,7 @@ add_library(
|
|||
${HEADERS}
|
||||
card_database_model.cpp
|
||||
card_database_display_model.cpp
|
||||
card_database_visual_display_model.cpp
|
||||
card/card_completer_proxy_model.cpp
|
||||
card/card_search_model.cpp
|
||||
card_set/card_sets_model.cpp
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@
|
|||
class QObject;
|
||||
|
||||
CardDatabaseDisplayModel::CardDatabaseDisplayModel(QObject *parent)
|
||||
: QSortFilterProxyModel(parent), isToken(ShowAll), filterString(nullptr),
|
||||
: QSortFilterProxyModel(parent), isToken(ShowAll), filterString(nullptr), cardModel(nullptr),
|
||||
collator(NaturalSort::makeNaturalSortCollator())
|
||||
{
|
||||
filterTree = nullptr;
|
||||
|
|
@ -33,22 +33,37 @@ CardDatabaseDisplayModel::CardDatabaseDisplayModel(QObject *parent)
|
|||
invalidate();
|
||||
emit modelDirty();
|
||||
});
|
||||
|
||||
loadedRowCount = 0;
|
||||
}
|
||||
|
||||
void CardDatabaseDisplayModel::setSourceModel(QAbstractItemModel *model)
|
||||
{
|
||||
cardModel = qobject_cast<CardDatabaseModel *>(model);
|
||||
QSortFilterProxyModel::setSourceModel(model);
|
||||
|
||||
connect(model, &QAbstractItemModel::rowsInserted, this, [this]() { dirty(); });
|
||||
|
||||
connect(model, &QAbstractItemModel::rowsRemoved, this, [this]() { dirty(); });
|
||||
|
||||
connect(model, &QAbstractItemModel::modelReset, this, [this]() {
|
||||
loadedRowCount = 0;
|
||||
dirty();
|
||||
});
|
||||
connect(model, &QAbstractItemModel::modelReset, this, [this]() { dirty(); });
|
||||
}
|
||||
|
||||
QVariant CardDatabaseDisplayModel::data(const QModelIndex &index, int role) const
|
||||
{
|
||||
if (role == CardInfoRole) {
|
||||
return QVariant::fromValue(cardInfoAt(index));
|
||||
}
|
||||
|
||||
return QSortFilterProxyModel::data(index, role);
|
||||
}
|
||||
|
||||
CardInfoPtr CardDatabaseDisplayModel::cardInfoAt(const QModelIndex &index) const
|
||||
{
|
||||
if (!index.isValid() || cardModel == nullptr) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const QModelIndex sourceIndex = mapToSource(index);
|
||||
return sourceIndex.isValid() ? cardModel->getCard(sourceIndex.row()) : CardInfoPtr();
|
||||
}
|
||||
|
||||
QMap<wchar_t, wchar_t> CardDatabaseDisplayModel::characterTranslation = {{L'“', L'\"'},
|
||||
|
|
@ -56,32 +71,6 @@ QMap<wchar_t, wchar_t> CardDatabaseDisplayModel::characterTranslation = {{L'“'
|
|||
{L'‘', L'\''},
|
||||
{L'’', L'\''}};
|
||||
|
||||
bool CardDatabaseDisplayModel::canFetchMore(const QModelIndex &index) const
|
||||
{
|
||||
return loadedRowCount < sourceModel()->rowCount(index);
|
||||
}
|
||||
|
||||
void CardDatabaseDisplayModel::fetchMore(const QModelIndex &index)
|
||||
{
|
||||
int remainder = sourceModel()->rowCount(index) - loadedRowCount;
|
||||
int itemsToFetch = qMin(100, remainder);
|
||||
|
||||
if (itemsToFetch == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto startIndex = qMin(rowCount(QModelIndex()), loadedRowCount);
|
||||
beginInsertRows(QModelIndex(), startIndex, startIndex + itemsToFetch - 1);
|
||||
|
||||
loadedRowCount += itemsToFetch;
|
||||
endInsertRows();
|
||||
}
|
||||
|
||||
int CardDatabaseDisplayModel::rowCount(const QModelIndex &parent) const
|
||||
{
|
||||
return QSortFilterProxyModel::rowCount(parent);
|
||||
}
|
||||
|
||||
bool CardDatabaseDisplayModel::lessThan(const QModelIndex &left, const QModelIndex &right) const
|
||||
{
|
||||
const QString leftString = sourceModel()->data(left, CardDatabaseModel::SortRole).toString();
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@
|
|||
#include <libcockatrice/filters/filter_string.h>
|
||||
#include <qtmetamacros.h>
|
||||
|
||||
class CardDatabaseModel;
|
||||
class FilterTree;
|
||||
class QObject;
|
||||
template <class Key, class T> class QMap;
|
||||
|
|
@ -36,13 +37,25 @@ public:
|
|||
ShowAll
|
||||
};
|
||||
|
||||
enum Role
|
||||
{
|
||||
/**
|
||||
* @brief The CardInfoPtr behind a proxy row.
|
||||
*
|
||||
* Saves views from looking the card up again through its name, and keeps
|
||||
* them from depending on the source model's column layout.
|
||||
*/
|
||||
CardInfoRole = Qt::UserRole + 1
|
||||
};
|
||||
|
||||
private:
|
||||
FilterBool isToken;
|
||||
QString cardName, cardText;
|
||||
QSet<QString> cardNameSet, cardTypes, cardColors;
|
||||
FilterTree *filterTree;
|
||||
FilterString *filterString;
|
||||
int loadedRowCount;
|
||||
/// The source model, kept typed so CardInfoRole does not cast per row.
|
||||
CardDatabaseModel *cardModel;
|
||||
QTimer dirtyTimer;
|
||||
CardSearchLanguage searchLanguage;
|
||||
QString searchText;
|
||||
|
|
@ -85,9 +98,16 @@ public:
|
|||
dirtyTimer.start(20);
|
||||
}
|
||||
void clearFilterAll();
|
||||
[[nodiscard]] int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||
[[nodiscard]] bool canFetchMore(const QModelIndex &parent) const override;
|
||||
void fetchMore(const QModelIndex &parent) override;
|
||||
[[nodiscard]] QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
||||
|
||||
/**
|
||||
* @brief Returns the card behind a proxy row, without the QVariant wrapper of CardInfoRole.
|
||||
*
|
||||
* The visual grid calls this for every row on each rebuild; CardInfoRole stays the
|
||||
* generic way in for consumers that only see the model through data().
|
||||
*/
|
||||
[[nodiscard]] CardInfoPtr cardInfoAt(const QModelIndex &index) const;
|
||||
|
||||
signals:
|
||||
void modelDirty();
|
||||
|
||||
|
|
|
|||
|
|
@ -27,14 +27,18 @@ CardDatabaseModel::CardDatabaseModel(CardDatabase *_db, bool _showOnlyCardsFromE
|
|||
|
||||
CardDatabaseModel::~CardDatabaseModel() = default;
|
||||
|
||||
int CardDatabaseModel::rowCount(const QModelIndex & /*parent*/) const
|
||||
int CardDatabaseModel::rowCount(const QModelIndex &parent) const
|
||||
{
|
||||
return cardList.size();
|
||||
// A list model's card rows are leaves; returning the full card count for a
|
||||
// valid parent makes every row claim all cards as children.
|
||||
return parent.isValid() ? 0 : cardList.size();
|
||||
}
|
||||
|
||||
int CardDatabaseModel::columnCount(const QModelIndex & /*parent*/) const
|
||||
int CardDatabaseModel::columnCount(const QModelIndex &parent) const
|
||||
{
|
||||
return CARDDBMODEL_COLUMNS;
|
||||
// Card rows are leaves, so a valid parent holds no columns. Reporting the
|
||||
// row width for a card index would claim child cells that do not exist.
|
||||
return parent.isValid() ? 0 : CARDDBMODEL_COLUMNS;
|
||||
}
|
||||
|
||||
QVariant CardDatabaseModel::data(const QModelIndex &index, int role) const
|
||||
|
|
|
|||
|
|
@ -0,0 +1,123 @@
|
|||
#include "card_database_visual_display_model.h"
|
||||
|
||||
#include "card_database_display_model.h"
|
||||
#include "libcockatrice/card/database/card_database_querier.h"
|
||||
|
||||
#include <QAbstractItemModel>
|
||||
#include <QList>
|
||||
#include <QStringList>
|
||||
#include <QVariant>
|
||||
|
||||
/// Coalesces the bursts of proxy signals a filter, sort or database load produces.
|
||||
static constexpr int REBUILD_COALESCE_MS = 30;
|
||||
|
||||
CardDatabaseVisualDisplayModel::CardDatabaseVisualDisplayModel(CardDatabaseDisplayModel *_displayModel,
|
||||
CardDatabaseQuerier *_querier,
|
||||
QObject *parent)
|
||||
: QAbstractListModel(parent), displayModel(_displayModel), querier(_querier)
|
||||
{
|
||||
rebuildTimer.setSingleShot(true);
|
||||
rebuildTimer.setInterval(REBUILD_COALESCE_MS);
|
||||
connect(&rebuildTimer, &QTimer::timeout, this, &CardDatabaseVisualDisplayModel::rebuild);
|
||||
|
||||
if (displayModel == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
connect(displayModel, &QAbstractItemModel::modelReset, this, &CardDatabaseVisualDisplayModel::scheduleRebuild);
|
||||
connect(displayModel, &QAbstractItemModel::layoutChanged, this, &CardDatabaseVisualDisplayModel::scheduleRebuild);
|
||||
connect(displayModel, &QAbstractItemModel::rowsInserted, this, &CardDatabaseVisualDisplayModel::scheduleRebuild);
|
||||
connect(displayModel, &QAbstractItemModel::rowsRemoved, this, &CardDatabaseVisualDisplayModel::scheduleRebuild);
|
||||
connect(displayModel, &QAbstractItemModel::dataChanged, this, &CardDatabaseVisualDisplayModel::scheduleRebuild);
|
||||
}
|
||||
|
||||
void CardDatabaseVisualDisplayModel::setSetFilterTerms(const QStringList &terms)
|
||||
{
|
||||
if (setFilterTerms == terms) {
|
||||
return;
|
||||
}
|
||||
|
||||
setFilterTerms = terms;
|
||||
refresh();
|
||||
}
|
||||
|
||||
void CardDatabaseVisualDisplayModel::refresh()
|
||||
{
|
||||
rebuildTimer.stop();
|
||||
rebuild();
|
||||
}
|
||||
|
||||
void CardDatabaseVisualDisplayModel::scheduleRebuild()
|
||||
{
|
||||
rebuildTimer.start();
|
||||
}
|
||||
|
||||
void CardDatabaseVisualDisplayModel::rebuild()
|
||||
{
|
||||
beginResetModel();
|
||||
entries.clear();
|
||||
|
||||
if (displayModel != nullptr) {
|
||||
const int proxyRows = displayModel->rowCount();
|
||||
entries.reserve(proxyRows);
|
||||
for (int row = 0; row < proxyRows; ++row) {
|
||||
const CardInfoPtr info = displayModel->cardInfoAt(displayModel->index(row, 0));
|
||||
if (info) {
|
||||
appendPrintingsFor(info);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
endResetModel();
|
||||
}
|
||||
|
||||
void CardDatabaseVisualDisplayModel::appendPrintingsFor(const CardInfoPtr &info)
|
||||
{
|
||||
if (setFilterTerms.isEmpty()) {
|
||||
entries.append({info, PrintingInfo()});
|
||||
return;
|
||||
}
|
||||
|
||||
const auto &printingsBySet = info->getSets();
|
||||
for (const QString &term : setFilterTerms) {
|
||||
const auto printings = printingsBySet.constFind(term);
|
||||
if (printings == printingsBySet.constEnd()) {
|
||||
continue;
|
||||
}
|
||||
for (const PrintingInfo &printing : printings.value()) {
|
||||
entries.append({info, printing});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int CardDatabaseVisualDisplayModel::rowCount(const QModelIndex &parent) const
|
||||
{
|
||||
return parent.isValid() ? 0 : static_cast<int>(entries.size());
|
||||
}
|
||||
|
||||
QVariant CardDatabaseVisualDisplayModel::data(const QModelIndex &index, int role) const
|
||||
{
|
||||
if (!index.isValid() || index.row() >= entries.size()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
if (role == Qt::DisplayRole || role == Qt::AccessibleTextRole) {
|
||||
return cardAt(index.row()).getName();
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
ExactCard CardDatabaseVisualDisplayModel::cardAt(int row) const
|
||||
{
|
||||
if (row < 0 || row >= entries.size()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const Entry &entry = entries.at(row);
|
||||
if (entry.printing.isEmpty() && setFilterTerms.isEmpty()) {
|
||||
return querier != nullptr ? querier->getPreferredCard(entry.info) : ExactCard(entry.info);
|
||||
}
|
||||
|
||||
return ExactCard(entry.info, entry.printing);
|
||||
}
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
/**
|
||||
* @file card_database_visual_display_model.h
|
||||
* @ingroup CardDatabaseModels
|
||||
* @brief A list model of the exact cards a visual card grid paints.
|
||||
*
|
||||
* The filtered and sorted CardDatabaseDisplayModel holds one row per card name.
|
||||
* A visual grid shows one picture per *printing* instead, so this model flattens
|
||||
* the proxy rows into ExactCards: the preferred printing per card normally, and
|
||||
* every printing of the selected sets while set filters are active.
|
||||
*/
|
||||
|
||||
#ifndef COCKATRICE_CARD_DATABASE_VISUAL_DISPLAY_MODEL_H
|
||||
#define COCKATRICE_CARD_DATABASE_VISUAL_DISPLAY_MODEL_H
|
||||
|
||||
#include "libcockatrice/card/card_info.h"
|
||||
#include "libcockatrice/card/printing/exact_card.h"
|
||||
#include "libcockatrice/card/printing/printing_info.h"
|
||||
|
||||
#include <QAbstractListModel>
|
||||
#include <QList>
|
||||
#include <QModelIndex>
|
||||
#include <QPointer>
|
||||
#include <QStringList>
|
||||
#include <QTimer>
|
||||
#include <qtmetamacros.h>
|
||||
|
||||
class CardDatabaseDisplayModel;
|
||||
class CardDatabaseQuerier;
|
||||
|
||||
class CardDatabaseVisualDisplayModel : public QAbstractListModel
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Flattens _displayModel into the cards a visual grid displays.
|
||||
* @param _displayModel The filtered and sorted proxy model of the card database.
|
||||
* @param _querier Resolves the preferred printing of a card.
|
||||
* @param parent Parent object.
|
||||
*/
|
||||
explicit CardDatabaseVisualDisplayModel(CardDatabaseDisplayModel *_displayModel,
|
||||
CardDatabaseQuerier *_querier,
|
||||
QObject *parent = nullptr);
|
||||
|
||||
/**
|
||||
* @brief Sets the set shorthand names of the active set filters.
|
||||
*
|
||||
* With no terms every card is shown with its preferred printing. With terms
|
||||
* every printing belonging to one of the named sets is shown instead, in term
|
||||
* order. Rebuilds immediately when the terms change.
|
||||
*/
|
||||
void setSetFilterTerms(const QStringList &terms);
|
||||
|
||||
/**
|
||||
* @brief Rebuilds the row list from the proxy model right away.
|
||||
*
|
||||
* Proxy model changes normally rebuild on a short coalescing timer; call this
|
||||
* when the caller needs the new rows before returning.
|
||||
*/
|
||||
void refresh();
|
||||
|
||||
[[nodiscard]] int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||
[[nodiscard]] QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
||||
|
||||
/**
|
||||
* @brief Returns the card painted at @p row, with its preferred printing resolved.
|
||||
* @param row Row in this model, not in the proxy model.
|
||||
*/
|
||||
[[nodiscard]] ExactCard cardAt(int row) const;
|
||||
|
||||
private:
|
||||
struct Entry
|
||||
{
|
||||
CardInfoPtr info;
|
||||
/// Empty when the entry follows the preferred printing rather than a set filter.
|
||||
PrintingInfo printing;
|
||||
};
|
||||
|
||||
void scheduleRebuild();
|
||||
void rebuild();
|
||||
void appendPrintingsFor(const CardInfoPtr &info);
|
||||
|
||||
QPointer<CardDatabaseDisplayModel> displayModel;
|
||||
CardDatabaseQuerier *querier;
|
||||
QStringList setFilterTerms;
|
||||
QList<Entry> entries;
|
||||
QTimer rebuildTimer;
|
||||
};
|
||||
|
||||
#endif // COCKATRICE_CARD_DATABASE_VISUAL_DISPLAY_MODEL_H
|
||||
|
|
@ -1,7 +1,3 @@
|
|||
set(CMAKE_AUTOMOC ON)
|
||||
set(CMAKE_AUTOUIC ON)
|
||||
set(CMAKE_AUTORCC ON)
|
||||
|
||||
set(HEADERS deck_list_model.h deck_list_sort_filter_proxy_model.h)
|
||||
|
||||
add_library(
|
||||
|
|
|
|||
|
|
@ -134,6 +134,13 @@ void DeckListModel::rebuildTree()
|
|||
|
||||
int DeckListModel::rowCount(const QModelIndex &parent) const
|
||||
{
|
||||
// Only column 0 carries children. Column 1 and later indexes are siblings
|
||||
// used by views for the remaining cells of a row, and a model that hangs
|
||||
// children off them breaks QAbstractItemModelTester and proxy models.
|
||||
if (parent.isValid() && parent.column() != 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// debugIndexInfo("rowCount", parent);
|
||||
auto *node = getNode<InnerDecklistNode *>(parent);
|
||||
if (node) {
|
||||
|
|
@ -282,7 +289,7 @@ QVariant DeckListModel::headerData(const int section, const Qt::Orientation orie
|
|||
QModelIndex DeckListModel::index(int row, int column, const QModelIndex &parent) const
|
||||
{
|
||||
// debugIndexInfo("index", parent);
|
||||
if (!hasIndex(row, column, parent)) {
|
||||
if ((parent.isValid() && parent.column() != 0) || !hasIndex(row, column, parent)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,3 @@
|
|||
set(CMAKE_AUTOMOC ON)
|
||||
set(CMAKE_AUTOUIC ON)
|
||||
set(CMAKE_AUTORCC ON)
|
||||
|
||||
add_subdirectory(libcockatrice/network/client)
|
||||
add_subdirectory(libcockatrice/network/server)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,3 @@
|
|||
set(CMAKE_AUTOMOC ON)
|
||||
set(CMAKE_AUTOUIC ON)
|
||||
set(CMAKE_AUTORCC ON)
|
||||
|
||||
add_subdirectory(abstract)
|
||||
add_subdirectory(local)
|
||||
add_subdirectory(remote)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,3 @@
|
|||
set(CMAKE_AUTOMOC ON)
|
||||
set(CMAKE_AUTOUIC ON)
|
||||
set(CMAKE_AUTORCC ON)
|
||||
|
||||
set(HEADERS abstract_client.h latency_tracker.h)
|
||||
|
||||
set(SOURCES abstract_client.cpp latency_tracker.cpp)
|
||||
|
|
@ -15,7 +11,7 @@ target_include_directories(libcockatrice_network_client_abstract PUBLIC .)
|
|||
target_link_libraries(
|
||||
libcockatrice_network_client_abstract
|
||||
PUBLIC ${COCKATRICE_QT_VERSION_NAME}::Network ${COCKATRICE_QT_VERSION_NAME}::WebSockets libcockatrice_protocol
|
||||
libcockatrice_network_server_remote
|
||||
libcockatrice_network_server_remote libcockatrice_utility
|
||||
)
|
||||
|
||||
target_precompile_headers(libcockatrice_network_client_abstract PRIVATE "${CMAKE_SOURCE_DIR}/cmake/pch/qtcore_pch.h")
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
#include "libcockatrice/protocol/pb/serverinfo_user.pb.h"
|
||||
#include "libcockatrice/protocol/pb/session_commands.pb.h"
|
||||
#include "libcockatrice/protocol/pb/session_event.pb.h"
|
||||
#include "libcockatrice/utility/passwordhasher.h"
|
||||
|
||||
#include <QDebug>
|
||||
#include <QList>
|
||||
|
|
@ -80,6 +81,26 @@ AbstractClient::~AbstractClient()
|
|||
{
|
||||
}
|
||||
|
||||
Command_AccountPassword AbstractClient::makeAccountPasswordCommand(const QString &oldPassword,
|
||||
const QString &newPassword) const
|
||||
{
|
||||
Command_AccountPassword cmd;
|
||||
cmd.set_old_password(oldPassword.toStdString());
|
||||
if (getServerSupportsChallengeResponse()) {
|
||||
// A verifier lets the server upgrade the stored credential without ever seeing the
|
||||
// password, and it is what a challenge-response server expects for a new password.
|
||||
cmd.set_hashed_new_password(PasswordHasher::generatePasswordVerifier(newPassword).toStdString());
|
||||
} else if (getServerSupportsPasswordHash()) {
|
||||
// The server hashes the password itself with its own salt; a salted hash is the
|
||||
// strongest form it accepts, so the plaintext never goes on the wire.
|
||||
const QString passwordSalt = PasswordHasher::generateRandomSalt();
|
||||
cmd.set_hashed_new_password(PasswordHasher::computeHash(newPassword, passwordSalt).toStdString());
|
||||
} else {
|
||||
cmd.set_new_password(newPassword.toStdString());
|
||||
}
|
||||
return cmd;
|
||||
}
|
||||
|
||||
void AbstractClient::processProtocolItem(const ServerMessage &item)
|
||||
{
|
||||
switch (item.message_type()) {
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ class Event_UserJoined;
|
|||
class Event_UserLeft;
|
||||
class Event_ServerMessage;
|
||||
class Event_ListRooms;
|
||||
class Command_AccountPassword;
|
||||
class Event_GameJoined;
|
||||
class Event_UserMessage;
|
||||
class Event_NotifyUser;
|
||||
|
|
@ -67,6 +68,9 @@ enum ClientStatus
|
|||
StatusSubmitForgotPasswordReset,
|
||||
StatusSubmitForgotPasswordChallenge,
|
||||
StatusGettingPasswordSalt,
|
||||
//! The server accepted the credentials but keeps the session open until the account
|
||||
//! password is replaced; the connection must survive for `Command_AccountPassword`.
|
||||
StatusPasswordChangeRequired,
|
||||
};
|
||||
|
||||
class AbstractClient : public QObject
|
||||
|
|
@ -171,6 +175,16 @@ public:
|
|||
{
|
||||
return serverSupportsChallengeResponse;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Builds the account-password command in the form this server understands.
|
||||
*
|
||||
* Challenge-response servers take a scrypt verifier; servers that only advertise
|
||||
* SupportsPasswordHash take a salted SHA-512 hash; anything older gets the plaintext,
|
||||
* which is the only form it understands. Both password-change paths go through here so
|
||||
* the two cannot drift apart and hand a plaintext password to a server that can hash it.
|
||||
*/
|
||||
Command_AccountPassword makeAccountPasswordCommand(const QString &oldPassword, const QString &newPassword) const;
|
||||
const QString &getUserName() const
|
||||
{
|
||||
return userName;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,3 @@
|
|||
set(CMAKE_AUTOMOC ON)
|
||||
set(CMAKE_AUTOUIC ON)
|
||||
set(CMAKE_AUTORCC ON)
|
||||
|
||||
set(HEADERS local_client.h)
|
||||
|
||||
set(SOURCES local_client.cpp)
|
||||
|
|
|
|||
|
|
@ -33,16 +33,18 @@ LocalClient::LocalClient(LocalServerInterface *_lsi,
|
|||
sendCommand(prepareSessionCommand(joinCmd));
|
||||
}
|
||||
|
||||
LocalClient::~LocalClient()
|
||||
{
|
||||
}
|
||||
|
||||
void LocalClient::sendCommandContainer(const CommandContainer &cont)
|
||||
{
|
||||
if (LocalClientLog().isDebugEnabled()) {
|
||||
qCDebug(LocalClientLog).noquote() << userName << "OUT" << getSafeDebugString(cont);
|
||||
}
|
||||
|
||||
if (lsi.isNull()) {
|
||||
qCWarning(LocalClientLog).noquote()
|
||||
<< userName << "dropping command, local server interface no longer exists:" << getSafeDebugString(cont);
|
||||
return;
|
||||
}
|
||||
|
||||
lsi->itemFromClient(cont);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
#include "../abstract/abstract_client.h"
|
||||
|
||||
#include <QLoggingCategory>
|
||||
#include <QPointer>
|
||||
#include <QString>
|
||||
#include <qtmetamacros.h>
|
||||
|
||||
|
|
@ -24,14 +25,15 @@ class LocalClient : public AbstractClient
|
|||
{
|
||||
Q_OBJECT
|
||||
private:
|
||||
LocalServerInterface *lsi;
|
||||
// Guarded pointer: the LocalServer (and its interfaces) may be destroyed while this client
|
||||
// still has pending commands queued on the event loop.
|
||||
QPointer<LocalServerInterface> lsi;
|
||||
|
||||
public:
|
||||
LocalClient(LocalServerInterface *_lsi,
|
||||
const QString &_playerName,
|
||||
const QString &_clientId,
|
||||
QObject *parent = nullptr);
|
||||
~LocalClient() override;
|
||||
|
||||
void sendCommandContainer(const CommandContainer &cont) override;
|
||||
private slots:
|
||||
|
|
|
|||
|
|
@ -1,7 +1,3 @@
|
|||
set(CMAKE_AUTOMOC ON)
|
||||
set(CMAKE_AUTOUIC ON)
|
||||
set(CMAKE_AUTORCC ON)
|
||||
|
||||
set(HEADERS remote_client.h)
|
||||
|
||||
set(SOURCES remote_client.cpp)
|
||||
|
|
|
|||
|
|
@ -125,6 +125,7 @@ RemoteClient::RemoteClient(QObject *parent, INetworkSettingsProvider *_networkSe
|
|||
&RemoteClient::doSubmitForgotPasswordResetToServer);
|
||||
connect(this, &RemoteClient::sigSubmitForgotPasswordChallengeToServer, this,
|
||||
&RemoteClient::doSubmitForgotPasswordChallengeToServer);
|
||||
connect(this, &RemoteClient::sigChangePasswordToServer, this, &RemoteClient::doChangePasswordToServer);
|
||||
}
|
||||
|
||||
RemoteClient::~RemoteClient()
|
||||
|
|
@ -354,6 +355,29 @@ void RemoteClient::submitPasswordVerifierResponse(const Response &response)
|
|||
}
|
||||
}
|
||||
|
||||
void RemoteClient::changePasswordToServer(const QString &oldPassword, const QString &newPassword)
|
||||
{
|
||||
emit sigChangePasswordToServer(oldPassword, newPassword);
|
||||
}
|
||||
|
||||
void RemoteClient::doChangePasswordToServer(const QString &oldPassword, const QString &newPassword)
|
||||
{
|
||||
const Command_AccountPassword cmd = makeAccountPasswordCommand(oldPassword, newPassword);
|
||||
|
||||
PendingCommand *pend = prepareSessionCommand(cmd);
|
||||
connect(pend, &PendingCommand::finished, this, &RemoteClient::accountPasswordResponse);
|
||||
sendCommand(pend);
|
||||
}
|
||||
|
||||
void RemoteClient::accountPasswordResponse(const Response &response)
|
||||
{
|
||||
if (response.response_code() == Response::RespOk) {
|
||||
emit sigPasswordChangeSuccess();
|
||||
} else {
|
||||
emit sigPasswordChangeError(response.response_code());
|
||||
}
|
||||
}
|
||||
|
||||
void RemoteClient::processConnectionClosedEvent(const Event_ConnectionClosed & /*event*/)
|
||||
{
|
||||
doDisconnectFromServer();
|
||||
|
|
@ -475,6 +499,23 @@ void RemoteClient::loginResponse(const Response &response)
|
|||
emit notifyUserAboutUpdate();
|
||||
}
|
||||
|
||||
} else if (response.response_code() == Response::RespPasswordChangeRequired) {
|
||||
// The credentials were accepted: the server keeps this session open and serves
|
||||
// nothing but ping and Command_AccountPassword until the password is replaced.
|
||||
// Treating it as a failed login would disconnect before the dialog that collects
|
||||
// the new password (queued to the UI thread behind this slot) can send anything:
|
||||
// the command would sit in pendingCommands forever, because the ping timer that
|
||||
// times it out stops with the connection. Keep the session and let the controller
|
||||
// report the outcome through accountPasswordResponse().
|
||||
QList<QString> missingFeatures;
|
||||
if (resp.missing_features_size() > 0) {
|
||||
for (int i = 0; i < resp.missing_features_size(); ++i) {
|
||||
missingFeatures << QString::fromStdString(resp.missing_features(i));
|
||||
}
|
||||
}
|
||||
setStatus(StatusPasswordChangeRequired);
|
||||
emit loginError(response.response_code(), QString::fromStdString(resp.denied_reason_str()),
|
||||
static_cast<quint32>(resp.denied_end_time()), missingFeatures);
|
||||
} else if (response.response_code() != Response::RespNotConnected) {
|
||||
QList<QString> missingFeatures;
|
||||
if (resp.missing_features_size() > 0) {
|
||||
|
|
|
|||
|
|
@ -71,6 +71,11 @@ signals:
|
|||
//! should only store it for the connection it is currently negotiating, so
|
||||
//! the hostname and user name are intentionally not part of the signal.
|
||||
void sigPasswordVerifierReady(const QString &verifier);
|
||||
//! \brief Outcome of the password change the server demanded at login.
|
||||
void sigPasswordChangeSuccess();
|
||||
void sigPasswordChangeError(int responseCode);
|
||||
//! \brief Request a password change; queued to the client thread like the other requests.
|
||||
void sigChangePasswordToServer(const QString &oldPassword, const QString &newPassword);
|
||||
private slots:
|
||||
void slotConnected();
|
||||
void readData();
|
||||
|
|
@ -99,6 +104,8 @@ private slots:
|
|||
Command_Login generateCommandLogin();
|
||||
void doSubmitPasswordVerifier();
|
||||
void submitPasswordVerifierResponse(const Response &response);
|
||||
void accountPasswordResponse(const Response &response);
|
||||
void doChangePasswordToServer(const QString &oldPassword, const QString &newPassword);
|
||||
void doDisconnectFromServer();
|
||||
void doActivateToServer(const QString &_token);
|
||||
void doRequestForgotPasswordToServer(const QString &hostname, unsigned int port, const QString &_userName);
|
||||
|
|
@ -201,6 +208,10 @@ public:
|
|||
unsigned int port,
|
||||
const QString &_userName,
|
||||
const QString &_email);
|
||||
//! \brief Change the password of the account that just logged in, following the
|
||||
//! server's forced-password-change reply. Emits sigPasswordChangeSuccess or
|
||||
//! sigPasswordChangeError when the server answers.
|
||||
void changePasswordToServer(const QString &oldPassword, const QString &newPassword);
|
||||
};
|
||||
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -1,7 +1,3 @@
|
|||
set(CMAKE_AUTOMOC ON)
|
||||
set(CMAKE_AUTOUIC ON)
|
||||
set(CMAKE_AUTORCC ON)
|
||||
|
||||
add_subdirectory(local)
|
||||
add_subdirectory(remote)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,3 @@
|
|||
set(CMAKE_AUTOMOC ON)
|
||||
set(CMAKE_AUTOUIC ON)
|
||||
set(CMAKE_AUTORCC ON)
|
||||
|
||||
set(HEADERS local_server.h local_server_interface.h)
|
||||
|
||||
set(SOURCES local_server.cpp local_server_interface.cpp)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,3 @@
|
|||
set(CMAKE_AUTOMOC ON)
|
||||
set(CMAKE_AUTOUIC ON)
|
||||
set(CMAKE_AUTORCC ON)
|
||||
|
||||
set(HEADERS
|
||||
game/dump_zone_range.h
|
||||
game/server_abstract_participant.h
|
||||
|
|
@ -12,6 +8,7 @@ set(HEADERS
|
|||
game/server_cardzone.h
|
||||
game/server_counter.h
|
||||
game/game_config.h
|
||||
game/role_info.h
|
||||
game/server_deck_validation_strategy.h
|
||||
game/server_game.h
|
||||
game/server_game_lifecycle_strategy.h
|
||||
|
|
|
|||
|
|
@ -0,0 +1,13 @@
|
|||
#ifndef COCKATRICE_ROLE_INFO_H
|
||||
#define COCKATRICE_ROLE_INFO_H
|
||||
|
||||
/**
|
||||
* @brief Represents information about a participant's roles.
|
||||
*/
|
||||
struct RoleInfo
|
||||
{
|
||||
bool isJudge = false; ///< Whether the participant joined as a judge
|
||||
bool isSpectator = false; ///< Whether the participant joined as a spectator
|
||||
};
|
||||
|
||||
#endif // COCKATRICE_ROLE_INFO_H
|
||||
|
|
@ -61,10 +61,10 @@
|
|||
Server_AbstractParticipant::Server_AbstractParticipant(Server_Game *_game,
|
||||
int _playerId,
|
||||
const ServerInfo_User &_userInfo,
|
||||
bool _judge,
|
||||
const RoleInfo &_roleInfo,
|
||||
Server_AbstractUserInterface *_userInterface)
|
||||
: ServerInfo_User_Container(_userInfo), game(_game), userInterface(_userInterface), pingTime(0),
|
||||
playerId(_playerId), judge(_judge)
|
||||
playerId(_playerId), roleInfo(_roleInfo)
|
||||
{
|
||||
}
|
||||
|
||||
|
|
@ -98,8 +98,8 @@ void Server_AbstractParticipant::getProperties(ServerInfo_PlayerProperties &resu
|
|||
if (withUserInfo) {
|
||||
copyUserInfo(*(result.mutable_user_info()), true);
|
||||
}
|
||||
result.set_spectator(spectator);
|
||||
result.set_judge(judge);
|
||||
result.set_spectator(isSpectator());
|
||||
result.set_judge(isJudge());
|
||||
result.set_ping_seconds(pingTime);
|
||||
getPlayerProperties(result);
|
||||
}
|
||||
|
|
@ -176,7 +176,7 @@ Response::ResponseCode Server_AbstractParticipant::cmdUnconcede(const Command_Un
|
|||
Response::ResponseCode
|
||||
Server_AbstractParticipant::cmdJudge(const Command_Judge &cmd, ResponseContainer &rc, GameEventStorage &ges)
|
||||
{
|
||||
if (!judge) {
|
||||
if (!isJudge()) {
|
||||
return Response::RespFunctionNotAllowed;
|
||||
}
|
||||
|
||||
|
|
@ -204,7 +204,7 @@ Response::ResponseCode Server_AbstractParticipant::cmdReadyStart(const Command_R
|
|||
Response::ResponseCode
|
||||
Server_AbstractParticipant::cmdGameSay(const Command_GameSay &cmd, ResponseContainer & /*rc*/, GameEventStorage &ges)
|
||||
{
|
||||
if (spectator) {
|
||||
if (isSpectator()) {
|
||||
/* Spectators can only talk if:
|
||||
* (a) the game creator allows it
|
||||
* (b) the spectator is a moderator/administrator
|
||||
|
|
@ -365,7 +365,7 @@ Response::ResponseCode Server_AbstractParticipant::cmdNextTurn(const Command_Nex
|
|||
return Response::RespGameNotStarted;
|
||||
}
|
||||
|
||||
if (!judge) {
|
||||
if (!isJudge()) {
|
||||
return Response::RespFunctionNotAllowed;
|
||||
}
|
||||
|
||||
|
|
@ -381,7 +381,7 @@ Response::ResponseCode Server_AbstractParticipant::cmdSetActivePhase(const Comma
|
|||
return Response::RespGameNotStarted;
|
||||
}
|
||||
|
||||
if (!judge) {
|
||||
if (!isJudge()) {
|
||||
return Response::RespFunctionNotAllowed;
|
||||
}
|
||||
|
||||
|
|
@ -415,8 +415,8 @@ Response::ResponseCode Server_AbstractParticipant::cmdReverseTurn(const Command_
|
|||
ResponseContainer & /*rc*/,
|
||||
GameEventStorage &ges)
|
||||
{
|
||||
if (!judge) {
|
||||
if (spectator) {
|
||||
if (!isJudge()) {
|
||||
if (isSpectator()) {
|
||||
return Response::RespFunctionNotAllowed;
|
||||
}
|
||||
|
||||
|
|
@ -577,7 +577,7 @@ void Server_AbstractParticipant::setUserInterface(Server_AbstractUserInterface *
|
|||
void Server_AbstractParticipant::disconnectClient()
|
||||
{
|
||||
bool isRegistered = userInfo->user_level() & ServerInfo_User::IsRegistered;
|
||||
if (!isRegistered || spectator) {
|
||||
if (!isRegistered || isSpectator()) {
|
||||
game->removeParticipant(this, Event_Leave::USER_DISCONNECTED);
|
||||
} else {
|
||||
setUserInterface(nullptr);
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
#define ABSTRACT_PARTICIPANT_H
|
||||
|
||||
#include "../serverinfo_user_container.h"
|
||||
#include "role_info.h"
|
||||
#include "server_arrowtarget.h"
|
||||
|
||||
#include <QMutex>
|
||||
|
|
@ -61,8 +62,7 @@ protected:
|
|||
Server_AbstractUserInterface *userInterface;
|
||||
int pingTime;
|
||||
int playerId;
|
||||
bool spectator;
|
||||
bool judge;
|
||||
RoleInfo roleInfo;
|
||||
virtual void getPlayerProperties(ServerInfo_PlayerProperties &result);
|
||||
mutable QMutex playerMutex;
|
||||
|
||||
|
|
@ -70,7 +70,7 @@ public:
|
|||
Server_AbstractParticipant(Server_Game *_game,
|
||||
int _playerId,
|
||||
const ServerInfo_User &_userInfo,
|
||||
bool _judge,
|
||||
const RoleInfo &_roleInfo,
|
||||
Server_AbstractUserInterface *_handler);
|
||||
~Server_AbstractParticipant() override;
|
||||
virtual void prepareDestroy()
|
||||
|
|
@ -91,11 +91,11 @@ public:
|
|||
}
|
||||
bool isSpectator() const
|
||||
{
|
||||
return spectator;
|
||||
return roleInfo.isSpectator;
|
||||
}
|
||||
bool isJudge() const
|
||||
{
|
||||
return judge;
|
||||
return roleInfo.isJudge;
|
||||
}
|
||||
Server_Game *getGame() const
|
||||
{
|
||||
|
|
|
|||
|
|
@ -81,10 +81,9 @@ Server_AbstractPlayer::Server_AbstractPlayer(Server_Game *_game,
|
|||
const ServerInfo_User &_userInfo,
|
||||
bool _judge,
|
||||
Server_AbstractUserInterface *_userInterface)
|
||||
: Server_AbstractParticipant(_game, _playerId, _userInfo, _judge, _userInterface), conceded(false), deck(nullptr),
|
||||
sideboardLocked(true), readyStart(false), nextCardId(0)
|
||||
: Server_AbstractParticipant(_game, _playerId, _userInfo, {.isJudge = _judge}, _userInterface), conceded(false),
|
||||
deck(nullptr), sideboardLocked(true), readyStart(false), nextCardId(0)
|
||||
{
|
||||
spectator = false;
|
||||
}
|
||||
|
||||
Server_AbstractPlayer::~Server_AbstractPlayer() = default;
|
||||
|
|
@ -295,7 +294,7 @@ Response::ResponseCode Server_AbstractPlayer::moveCard(GameEventStorage &ges,
|
|||
{
|
||||
// Disallow controller change to other zones than the table.
|
||||
if (((targetzone->getType() != ServerInfo_Zone::PublicZone) || !targetzone->hasCoords()) &&
|
||||
(startzone->getPlayer() != targetzone->getPlayer()) && !judge) {
|
||||
(startzone->getPlayer() != targetzone->getPlayer()) && !isJudge()) {
|
||||
return Response::RespContextError;
|
||||
}
|
||||
|
||||
|
|
@ -808,7 +807,7 @@ Server_AbstractPlayer::cmdMoveCard(const Command_MoveCard &cmd, ResponseContaine
|
|||
return Response::RespNameNotFound;
|
||||
}
|
||||
|
||||
if ((startPlayer != this) && (!startZone->getPlayersWithWritePermission().contains(playerId)) && !judge) {
|
||||
if ((startPlayer != this) && (!startZone->getPlayersWithWritePermission().contains(playerId)) && !isJudge()) {
|
||||
return Response::RespContextError;
|
||||
}
|
||||
|
||||
|
|
@ -821,7 +820,7 @@ Server_AbstractPlayer::cmdMoveCard(const Command_MoveCard &cmd, ResponseContaine
|
|||
return Response::RespNameNotFound;
|
||||
}
|
||||
|
||||
if ((startPlayer != this) && (targetPlayer != this) && !judge) {
|
||||
if ((startPlayer != this) && (targetPlayer != this) && !isJudge()) {
|
||||
return Response::RespContextError;
|
||||
}
|
||||
|
||||
|
|
@ -1598,7 +1597,7 @@ Server_AbstractPlayer::cmdRevealCards(const Command_RevealCards &cmd, ResponseCo
|
|||
zone->addWritePermission(cmd.player_id());
|
||||
}
|
||||
|
||||
if (judge) {
|
||||
if (isJudge()) {
|
||||
ges.setOverwriteOwnership(true);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@
|
|||
#include "libcockatrice/protocol/pb/serverinfo_replay_match.pb.h"
|
||||
#include "libcockatrice/protocol/pb/serverinfo_user.pb.h"
|
||||
#include "libcockatrice/protocol/pb/session_event.pb.h"
|
||||
#include "role_info.h"
|
||||
#include "server_abstract_player.h"
|
||||
#include "server_abstractuserinterface.h"
|
||||
#include "server_arrow.h"
|
||||
|
|
@ -479,9 +480,8 @@ void Server_Game::stopGameIfFinished()
|
|||
|
||||
Response::ResponseCode Server_Game::checkJoin(ServerInfo_User *user,
|
||||
const QString &_password,
|
||||
bool spectator,
|
||||
bool overrideRestrictions,
|
||||
bool asJudge)
|
||||
const RoleInfo &roleInfo,
|
||||
bool overrideRestrictions)
|
||||
{
|
||||
Server_DatabaseInterface *databaseInterface = room->getServer()->getDatabaseInterface();
|
||||
for (auto *participant : participants.values()) {
|
||||
|
|
@ -490,11 +490,11 @@ Response::ResponseCode Server_Game::checkJoin(ServerInfo_User *user,
|
|||
}
|
||||
}
|
||||
|
||||
if (asJudge && !(user->user_level() & ServerInfo_User::IsJudge)) {
|
||||
if (roleInfo.isJudge && !(user->user_level() & ServerInfo_User::IsJudge)) {
|
||||
return Response::RespUserLevelTooLow;
|
||||
}
|
||||
if (!(overrideRestrictions && (user->user_level() & (ServerInfo_User::IsModerator | ServerInfo_User::IsJudge)))) {
|
||||
if ((_password != password) && !(spectator && !spectatorsNeedPassword)) {
|
||||
if ((_password != password) && !(roleInfo.isSpectator && !spectatorsNeedPassword)) {
|
||||
return Response::RespWrongPassword;
|
||||
}
|
||||
if (!(user->user_level() & ServerInfo_User::IsRegistered) && onlyRegistered) {
|
||||
|
|
@ -510,13 +510,13 @@ Response::ResponseCode Server_Game::checkJoin(ServerInfo_User *user,
|
|||
QString::fromStdString(user->name()))) {
|
||||
return Response::RespInIgnoreList;
|
||||
}
|
||||
if (spectator) {
|
||||
if (roleInfo.isSpectator) {
|
||||
if (!spectatorsAllowed) {
|
||||
return Response::RespSpectatorsNotAllowed;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!spectator && (gameStarted || (getPlayerCount() >= getMaxPlayers()))) {
|
||||
if (!roleInfo.isSpectator && (gameStarted || (getPlayerCount() >= getMaxPlayers()))) {
|
||||
return Response::RespGameFull;
|
||||
}
|
||||
|
||||
|
|
@ -537,19 +537,18 @@ bool Server_Game::containsUser(const QString &userName) const
|
|||
|
||||
void Server_Game::addPlayer(Server_AbstractUserInterface *userInterface,
|
||||
ResponseContainer &rc,
|
||||
bool spectator,
|
||||
bool judge,
|
||||
const RoleInfo &roleInfo,
|
||||
bool broadcastUpdate)
|
||||
{
|
||||
QMutexLocker locker(&gameMutex);
|
||||
|
||||
Server_AbstractParticipant *newParticipant;
|
||||
if (spectator) {
|
||||
if (roleInfo.isSpectator) {
|
||||
newParticipant = new Server_Spectator(this, nextPlayerId++, userInterface->copyUserInfo(true, true, true),
|
||||
judge, userInterface);
|
||||
roleInfo.isJudge, userInterface);
|
||||
} else {
|
||||
newParticipant = new Server_Player(this, nextPlayerId++, userInterface->copyUserInfo(true, true, true), judge,
|
||||
userInterface);
|
||||
newParticipant = new Server_Player(this, nextPlayerId++, userInterface->copyUserInfo(true, true, true),
|
||||
roleInfo.isJudge, userInterface);
|
||||
}
|
||||
|
||||
newParticipant->moveToThread(thread());
|
||||
|
|
@ -560,7 +559,7 @@ void Server_Game::addPlayer(Server_AbstractUserInterface *userInterface,
|
|||
|
||||
const QString playerName = QString::fromStdString(newParticipant->getUserInfo()->name());
|
||||
participants.insert(newParticipant->getPlayerId(), newParticipant);
|
||||
if (spectator) {
|
||||
if (roleInfo.isSpectator) {
|
||||
allSpectatorsEver.insert(playerName);
|
||||
} else {
|
||||
allPlayersEver.insert(playerName);
|
||||
|
|
@ -579,7 +578,7 @@ void Server_Game::addPlayer(Server_AbstractUserInterface *userInterface,
|
|||
emit gameInfoChanged(gameInfo);
|
||||
}
|
||||
|
||||
if ((newParticipant->getUserInfo()->user_level() & ServerInfo_User::IsRegistered) && !spectator) {
|
||||
if ((newParticipant->getUserInfo()->user_level() & ServerInfo_User::IsRegistered) && !roleInfo.isSpectator) {
|
||||
room->getServer()->addPersistentPlayer(playerName, room->getId(), gameId, newParticipant->getPlayerId());
|
||||
}
|
||||
|
||||
|
|
@ -871,6 +870,13 @@ void Server_Game::createGameJoinedEvent(Server_AbstractParticipant *joiningParti
|
|||
}
|
||||
}
|
||||
|
||||
bool Server_Game::canSeePrivateEventsFor(const Server_AbstractParticipant *participant, int privatePlayerId) const
|
||||
{
|
||||
return participant->getPlayerId() == privatePlayerId // same player
|
||||
|| participant->isJudge() // judges see everything
|
||||
|| (participant->isSpectator() && spectatorsSeeEverything); // spectators see everything
|
||||
}
|
||||
|
||||
void Server_Game::sendGameEventContainer(GameEventContainer *cont,
|
||||
GameEventStorageItem::EventRecipients recipients,
|
||||
int privatePlayerId)
|
||||
|
|
@ -879,8 +885,7 @@ void Server_Game::sendGameEventContainer(GameEventContainer *cont,
|
|||
|
||||
cont->set_game_id(gameId);
|
||||
for (auto *participant : participants.values()) {
|
||||
const bool playerPrivate = (participant->getPlayerId() == privatePlayerId) || participant->isJudge() ||
|
||||
(participant->isSpectator() && spectatorsSeeEverything);
|
||||
const bool playerPrivate = canSeePrivateEventsFor(participant, privatePlayerId);
|
||||
if ((recipients.testFlag(GameEventStorageItem::SendToPrivate) && playerPrivate) ||
|
||||
(recipients.testFlag(GameEventStorageItem::SendToOthers) && !playerPrivate)) {
|
||||
participant->sendGameEvent(*cont);
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue