mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-11-10 03:53:26 -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
|
if [[ $MAKE_PACKAGE ]]; then
|
||||||
# Workaround https://github.com/actions/runner-images/issues/7522
|
# 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"
|
hdiutil_script="/tmp/hdiutil.sh"
|
||||||
# shellcheck disable=SC2016
|
# shellcheck disable=SC2016
|
||||||
echo '#!/bin/bash
|
echo '#!/bin/bash
|
||||||
|
attempts=10
|
||||||
i=0
|
i=0
|
||||||
while ! hdiutil "$@"; do
|
while true; do
|
||||||
if (( ++i >= 10 )); then
|
hdiutil "$@" && exit 0
|
||||||
echo "Error: hdiutil failed $i times!" >&2
|
rc=$?
|
||||||
break
|
if (( ++i >= attempts )); then
|
||||||
|
echo "::error::hdiutil $1 failed $i times (last status $rc), giving up" >&2
|
||||||
|
exit "$rc"
|
||||||
fi
|
fi
|
||||||
|
echo "hdiutil $1 attempt $i/$attempts failed (status $rc), retrying" >&2
|
||||||
sleep 1
|
sleep 1
|
||||||
done' >"$hdiutil_script"
|
done' >"$hdiutil_script"
|
||||||
chmod +x "$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" \
|
--title "$release_name" \
|
||||||
--notes-file "$body_path"
|
--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:
|
build-linux:
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
|
|
@ -466,7 +496,7 @@ jobs:
|
||||||
vcpkg_upload: 1 # only one leg per vcpkg_scope hands its archives over
|
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' || '' }}
|
name: ${{ matrix.os }} ${{ matrix.target }}${{ matrix.soc == 'Intel' && ' Intel' || '' }}${{ matrix.type == 'Debug' && ' Debug' || '' }}
|
||||||
needs: configure
|
needs: [configure, vcpkg-baseline]
|
||||||
runs-on: ${{ matrix.runner }}
|
runs-on: ${{ matrix.runner }}
|
||||||
timeout-minutes: 100
|
timeout-minutes: 100
|
||||||
env:
|
env:
|
||||||
|
|
|
||||||
3
.gitmodules
vendored
3
.gitmodules
vendored
|
|
@ -4,3 +4,6 @@
|
||||||
[submodule "doxygen-awesome-css"]
|
[submodule "doxygen-awesome-css"]
|
||||||
path = doc/doxygen/theme
|
path = doc/doxygen/theme
|
||||||
url = https://github.com/jothepro/doxygen-awesome-css.git
|
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)
|
option(WITH_ORACLE "Build Cockatrice card database tool (Oracle)" ON)
|
||||||
# Compile Servatrice
|
# Compile Servatrice
|
||||||
option(WITH_SERVER "Build Cockatrice server (Servatrice)" OFF)
|
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
|
# Compile tests
|
||||||
option(TEST "Build tests" OFF)
|
option(TEST "Build tests" OFF)
|
||||||
# Build with AddressSanitizer and UndefinedBehaviorSanitizer
|
# Build with AddressSanitizer and UndefinedBehaviorSanitizer
|
||||||
|
|
@ -29,6 +31,14 @@ option(ENABLE_COVERAGE "Build instrumented for GCC line coverage" OFF)
|
||||||
# Use vcpkg regardless of OS
|
# Use vcpkg regardless of OS
|
||||||
option(USE_VCPKG "Use vcpkg regardless of OS" OFF)
|
option(USE_VCPKG "Use vcpkg regardless of OS" OFF)
|
||||||
|
|
||||||
|
# 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
|
# Default to "Release" build type
|
||||||
# User-provided value for CMAKE_BUILD_TYPE must be checked before the PROJECT() call
|
# User-provided value for CMAKE_BUILD_TYPE must be checked before the PROJECT() call
|
||||||
if(DEFINED CMAKE_BUILD_TYPE)
|
if(DEFINED CMAKE_BUILD_TYPE)
|
||||||
|
|
@ -90,7 +100,7 @@ endif()
|
||||||
|
|
||||||
# A project name is needed for CPack
|
# A project name is needed for CPack
|
||||||
# Version can be overriden by git tags, see cmake/getversion.cmake
|
# 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
|
# Set release name if not provided via env/cmake var
|
||||||
if(NOT DEFINED GIT_TAG_RELEASENAME)
|
if(NOT DEFINED GIT_TAG_RELEASENAME)
|
||||||
|
|
@ -259,7 +269,13 @@ else()
|
||||||
add_compile_definitions(QT_DISABLE_DEPRECATED_BEFORE=0x060400)
|
add_compile_definitions(QT_DISABLE_DEPRECATED_BEFORE=0x060400)
|
||||||
endif()
|
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_AUTOMOC TRUE)
|
||||||
|
set(CMAKE_AUTOUIC TRUE)
|
||||||
|
set(CMAKE_AUTORCC TRUE)
|
||||||
|
|
||||||
# Find other needed libraries
|
# Find other needed libraries
|
||||||
find_package(Protobuf CONFIG)
|
find_package(Protobuf CONFIG)
|
||||||
|
|
@ -455,6 +471,34 @@ if(WITH_ORACLE)
|
||||||
set(CPACK_INSTALL_CMAKE_PROJECTS "Oracle;Oracle;ALL;/" ${CPACK_INSTALL_CMAKE_PROJECTS})
|
set(CPACK_INSTALL_CMAKE_PROJECTS "Oracle;Oracle;ALL;/" ${CPACK_INSTALL_CMAKE_PROJECTS})
|
||||||
endif()
|
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)
|
if(TEST)
|
||||||
include(CTest)
|
include(CTest)
|
||||||
add_subdirectory(tests)
|
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_widget.cpp
|
||||||
src/interface/widgets/cards/card_info_picture_with_text_overlay_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_info_text_widget.cpp
|
||||||
|
src/interface/widgets/cards/card_picture_delegate.cpp
|
||||||
src/interface/widgets/cards/card_size_widget.cpp
|
src/interface/widgets/cards/card_size_widget.cpp
|
||||||
src/interface/widgets/cards/deck_card_zone_display_widget.cpp
|
src/interface/widgets/cards/deck_card_zone_display_widget.cpp
|
||||||
src/interface/widgets/cards/deck_preview_card_picture_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/sequence_edit.cpp
|
||||||
src/interface/widgets/utility/visibility_change_listener.cpp
|
src/interface/widgets/utility/visibility_change_listener.cpp
|
||||||
src/interface/widgets/utility/visibility_change_listener.h
|
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_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_save_load_widget.cpp
|
||||||
src/interface/widgets/visual_database_display/visual_database_display_filter_toolbar_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)
|
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(UNIX)
|
||||||
if(APPLE)
|
if(APPLE)
|
||||||
set(MACOSX_BUNDLE_INFO_STRING "${PROJECT_NAME}")
|
set(MACOSX_BUNDLE_INFO_STRING "${PROJECT_NAME}")
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@
|
||||||
# or set <category> = false to disable logging
|
# or set <category> = false to disable logging
|
||||||
|
|
||||||
#main = true
|
#main = true
|
||||||
|
#recording = true
|
||||||
#qt_translator = true
|
#qt_translator = true
|
||||||
#window_main.* = true
|
#window_main.* = true
|
||||||
#release_channel = true
|
#release_channel = true
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
#include "../../../interface/pixel_map_generator.h"
|
#include "../../../interface/pixel_map_generator.h"
|
||||||
#include "../../settings/cache_settings.h"
|
#include "../../settings/cache_settings.h"
|
||||||
#include "../interface/widgets/dialogs/dlg_connect.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_challenge.h"
|
||||||
#include "../interface/widgets/dialogs/dlg_forgot_password_request.h"
|
#include "../interface/widgets/dialogs/dlg_forgot_password_request.h"
|
||||||
#include "../interface/widgets/dialogs/dlg_forgot_password_reset.h"
|
#include "../interface/widgets/dialogs/dlg_forgot_password_reset.h"
|
||||||
|
|
@ -99,6 +100,10 @@ void ConnectionController::wireClientSignals()
|
||||||
|
|
||||||
connect(remoteClient, &RemoteClient::sigPasswordVerifierReady, this,
|
connect(remoteClient, &RemoteClient::sigPasswordVerifierReady, this,
|
||||||
&ConnectionController::onPasswordVerifierReady);
|
&ConnectionController::onPasswordVerifierReady);
|
||||||
|
|
||||||
|
connect(remoteClient, &RemoteClient::sigPasswordChangeSuccess, this,
|
||||||
|
&ConnectionController::onPasswordChangeSuccess);
|
||||||
|
connect(remoteClient, &RemoteClient::sigPasswordChangeError, this, &ConnectionController::onPasswordChangeError);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ConnectionController::connectToServer()
|
void ConnectionController::connectToServer()
|
||||||
|
|
@ -351,10 +356,16 @@ void ConnectionController::onLoginError(int r,
|
||||||
}
|
}
|
||||||
|
|
||||||
case Response::RespPasswordChangeRequired: {
|
case Response::RespPasswordChangeRequired: {
|
||||||
QMessageBox::information(
|
// The credentials were right, but the account may not be used until a new password
|
||||||
dialogParent, tr("Password Change Required"),
|
// is chosen. Let the user complete the change here: sending them off to "ask the
|
||||||
tr("An administrator has reset your password. Please contact your server administrator to obtain "
|
// administrator for your temporary password" sends them in circles, since they just
|
||||||
"your temporary password, then log in and change it via Account -> Change Password."));
|
// 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();
|
remoteClient->disconnectFromServer();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -517,6 +528,43 @@ void ConnectionController::onNotifyUserAboutUpdate()
|
||||||
"To update your client, go to Help -> Check for Updates."));
|
"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()
|
void ConnectionController::onForgotPasswordSuccess()
|
||||||
{
|
{
|
||||||
QMessageBox::information(
|
QMessageBox::information(
|
||||||
|
|
|
||||||
|
|
@ -94,6 +94,10 @@ private slots:
|
||||||
// Persists the derived scrypt verifier after a successful challenge-response login
|
// Persists the derived scrypt verifier after a successful challenge-response login
|
||||||
void onPasswordVerifierReady(const QString &verifier);
|
void onPasswordVerifierReady(const QString &verifier);
|
||||||
|
|
||||||
|
// Completes the forced password change the server demanded at login
|
||||||
|
void onPasswordChangeSuccess();
|
||||||
|
void onPasswordChangeError(int responseCode);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void wireClientSignals();
|
void wireClientSignals();
|
||||||
void updateWindowTitle();
|
void updateWindowTitle();
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,27 @@
|
||||||
|
|
||||||
class QObject;
|
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)
|
FilterTreeModel::FilterTreeModel(QObject *parent) : QAbstractItemModel(parent)
|
||||||
{
|
{
|
||||||
fTree = new FilterTree;
|
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 *> FilterTreeModel::getFiltersOfType(CardFilter::Attr filterType) const
|
||||||
{
|
{
|
||||||
QList<const CardFilter *> filters;
|
QList<const CardFilter *> filters;
|
||||||
if (!fTree) {
|
forEachFilter(fTree, [&](const FilterItem *filterItem) {
|
||||||
return filters;
|
if (filterItem->attr() == filterType) {
|
||||||
}
|
filters.append(new CardFilter(filterItem->text(), filterItem->type(), filterItem->attr()));
|
||||||
|
|
||||||
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()));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
for (int i = 0; i < node->childCount(); ++i) {
|
});
|
||||||
traverse(node->nodeAt(i));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
traverse(fTree);
|
|
||||||
return filters;
|
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 *> FilterTreeModel::allFilters() const
|
||||||
{
|
{
|
||||||
QList<const CardFilter *> filters;
|
QList<const CardFilter *> filters;
|
||||||
if (!fTree) {
|
forEachFilter(fTree, [&](const FilterItem *filterItem) {
|
||||||
return filters;
|
filters.append(new CardFilter(filterItem->text(), filterItem->type(), filterItem->attr()));
|
||||||
}
|
});
|
||||||
|
|
||||||
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);
|
|
||||||
return filters;
|
return filters;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,14 @@ public slots:
|
||||||
void addFilter(const CardFilter *f);
|
void addFilter(const CardFilter *f);
|
||||||
void removeFilter(const CardFilter *f);
|
void removeFilter(const CardFilter *f);
|
||||||
void clearFiltersOfType(CardFilter::Attr filterType);
|
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;
|
[[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;
|
[[nodiscard]] QList<const CardFilter *> allFilters() const;
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
|
|
@ -48,6 +55,13 @@ public:
|
||||||
{
|
{
|
||||||
return fTree;
|
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 rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||||
[[nodiscard]] int columnCount(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;
|
[[nodiscard]] QVariant data(const QModelIndex &index, int role) const override;
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
#include <QBrush>
|
#include <QBrush>
|
||||||
#include <QColor>
|
#include <QColor>
|
||||||
#include <QCursor>
|
#include <QCursor>
|
||||||
|
#include <QGraphicsScene>
|
||||||
#include <QGraphicsSceneMouseEvent>
|
#include <QGraphicsSceneMouseEvent>
|
||||||
#include <QPainter>
|
#include <QPainter>
|
||||||
#include <QPainterPath>
|
#include <QPainterPath>
|
||||||
|
|
@ -51,7 +52,20 @@ AbstractCardDragItem::AbstractCardDragItem(AbstractCardItem *_item,
|
||||||
update();
|
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
|
QPainterPath AbstractCardDragItem::shape() const
|
||||||
|
|
@ -65,6 +79,10 @@ QPainterPath AbstractCardDragItem::shape() const
|
||||||
|
|
||||||
void AbstractCardDragItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget)
|
void AbstractCardDragItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget)
|
||||||
{
|
{
|
||||||
|
if (item.isNull()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
item->paint(painter, option, widget);
|
item->paint(painter, option, widget);
|
||||||
|
|
||||||
// adds a mask to the card so it looks like the card hasnt been placed yet
|
// adds a mask to the card so it looks like the card hasnt been placed yet
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@
|
||||||
#include <QList>
|
#include <QList>
|
||||||
#include <QObject>
|
#include <QObject>
|
||||||
#include <QPointF>
|
#include <QPointF>
|
||||||
|
#include <QPointer>
|
||||||
#include <QRectF>
|
#include <QRectF>
|
||||||
#include <qtmetamacros.h>
|
#include <qtmetamacros.h>
|
||||||
|
|
||||||
|
|
@ -24,7 +25,8 @@ class AbstractCardDragItem : public QObject, public QGraphicsItem
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
Q_INTERFACES(QGraphicsItem)
|
Q_INTERFACES(QGraphicsItem)
|
||||||
protected:
|
protected:
|
||||||
AbstractCardItem *item;
|
/// Guarded pointer: the source card may be destroyed while this drag item still exists.
|
||||||
|
QPointer<AbstractCardItem> item;
|
||||||
QPointF hotSpot;
|
QPointF hotSpot;
|
||||||
QList<AbstractCardDragItem *> childDrags;
|
QList<AbstractCardDragItem *> childDrags;
|
||||||
|
|
||||||
|
|
@ -44,10 +46,8 @@ public:
|
||||||
}
|
}
|
||||||
[[nodiscard]] QPainterPath shape() const override;
|
[[nodiscard]] QPainterPath shape() const override;
|
||||||
void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override;
|
void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override;
|
||||||
[[nodiscard]] AbstractCardItem *getItem() const
|
/// @return the source card, or nullptr if it has already been destroyed.
|
||||||
{
|
[[nodiscard]] AbstractCardItem *getItem() const;
|
||||||
return item;
|
|
||||||
}
|
|
||||||
[[nodiscard]] QPointF getHotSpot() const
|
[[nodiscard]] QPointF getHotSpot() const
|
||||||
{
|
{
|
||||||
return hotSpot;
|
return hotSpot;
|
||||||
|
|
|
||||||
|
|
@ -130,8 +130,9 @@ void CardDragItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
|
||||||
sc->removeItem(this);
|
sc->removeItem(this);
|
||||||
|
|
||||||
QList<CardDragItem *> dragItemList;
|
QList<CardDragItem *> dragItemList;
|
||||||
CardZoneLogic *startZone = static_cast<CardItem *>(item)->getZone();
|
CardZoneLogic *startZone = static_cast<CardItem *>(item.data())->getZone();
|
||||||
if (currentZone && !(static_cast<CardItem *>(item)->getAttachedTo() && (startZone == currentZone->getLogic()))) {
|
if (currentZone &&
|
||||||
|
!(static_cast<CardItem *>(item.data())->getAttachedTo() && (startZone == currentZone->getLogic()))) {
|
||||||
if (!occupied) {
|
if (!occupied) {
|
||||||
dragItemList.append(this);
|
dragItemList.append(this);
|
||||||
}
|
}
|
||||||
|
|
@ -139,7 +140,7 @@ void CardDragItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
|
||||||
for (int i = 0; i < childDrags.size(); i++) {
|
for (int i = 0; i < childDrags.size(); i++) {
|
||||||
CardDragItem *c = static_cast<CardDragItem *>(childDrags[i]);
|
CardDragItem *c = static_cast<CardDragItem *>(childDrags[i]);
|
||||||
if (!occupied &&
|
if (!occupied &&
|
||||||
!(static_cast<CardItem *>(c->item)->getAttachedTo() && (startZone == currentZone->getLogic())) &&
|
!(static_cast<CardItem *>(c->item.data())->getAttachedTo() && (startZone == currentZone->getLogic())) &&
|
||||||
!c->occupied) {
|
!c->occupied) {
|
||||||
dragItemList.append(c);
|
dragItemList.append(c);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -77,7 +77,7 @@ void DeckViewCardDragItem::updatePosition(const QPointF &cursorScenePos)
|
||||||
|
|
||||||
void DeckViewCardDragItem::handleDrop(DeckViewCardContainer *target)
|
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());
|
auto *start = static_cast<DeckViewCardContainer *>(item->parentItem());
|
||||||
start->removeCard(card);
|
start->removeCard(card);
|
||||||
target->addCard(card);
|
target->addCard(card);
|
||||||
|
|
|
||||||
|
|
@ -99,9 +99,13 @@ void ZoneViewZone::initializeCards(const QList<const ServerInfo_Card *> &cardLis
|
||||||
}
|
}
|
||||||
reorganizeCards();
|
reorganizeCards();
|
||||||
} else if (!qobject_cast<ZoneViewZoneLogic *>(getLogic())->getOriginalZone()->contentsKnown()) {
|
} 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) {
|
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);
|
connect(pend, &PendingCommand::finished, this, &ZoneViewZone::zoneDumpReceived);
|
||||||
|
viewLogic->sendZoneDump(pend);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const CardList &c = qobject_cast<ZoneViewZoneLogic *>(getLogic())->getOriginalZone()->getCards();
|
const CardList &c = qobject_cast<ZoneViewZoneLogic *>(getLogic())->getOriginalZone()->getCards();
|
||||||
|
|
|
||||||
|
|
@ -238,6 +238,7 @@ void CardPictureLoader::imageLoaded(const ExactCard &card, const QImage &image)
|
||||||
});
|
});
|
||||||
|
|
||||||
card.emitPixmapUpdated();
|
card.emitPixmapUpdated();
|
||||||
|
emit getInstance().pixmapLoaded(card);
|
||||||
}
|
}
|
||||||
|
|
||||||
void CardPictureLoader::deleteAllLocalOverrides(const ExactCard &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. */
|
/** @brief Emitted after the worker has finished clearing the network and redirect caches. */
|
||||||
void networkCacheCleared();
|
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:
|
private slots:
|
||||||
/**
|
/**
|
||||||
* @brief Triggered when the user changes the picture download settings.
|
* @brief Triggered when the user changes the picture download settings.
|
||||||
|
|
|
||||||
|
|
@ -27,9 +27,9 @@ void IntentGetLoginCredentials::onPreconditionSatisfied()
|
||||||
ServersSettings &servers = SettingsCache::instance().servers();
|
ServersSettings &servers = SettingsCache::instance().servers();
|
||||||
const int index = servers.findServerIndex(context->hostname, context->port);
|
const int index = servers.findServerIndex(context->hostname, context->port);
|
||||||
|
|
||||||
if (index >= 0) {
|
if (const auto server = servers.getServer(index)) {
|
||||||
context->username = servers.getValue(QString("username%1").arg(index), "server", "server_details").toString();
|
context->username = server->username;
|
||||||
context->password = servers.getValue(QString("password%1").arg(index), "server", "server_details").toString();
|
context->password = server->password;
|
||||||
emitFinished();
|
emitFinished();
|
||||||
} else {
|
} else {
|
||||||
emitFailed(tr("No saved credentials for this server"));
|
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.
|
// it with the raw hostname when addNewServer updates the entry in place.
|
||||||
QString saveName = context->hostname;
|
QString saveName = context->hostname;
|
||||||
const int existingIndex = servers.findServerIndex(context->hostname, context->port);
|
const int existingIndex = servers.findServerIndex(context->hostname, context->port);
|
||||||
if (existingIndex >= 0) {
|
if (const auto existing = servers.getServer(existingIndex)) {
|
||||||
saveName =
|
saveName = existing->saveName;
|
||||||
servers.getValue(QString("saveName%1").arg(existingIndex), "server", "server_details").toString();
|
|
||||||
if (saveName.isEmpty()) {
|
if (saveName.isEmpty()) {
|
||||||
saveName = context->hostname;
|
saveName = context->hostname;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -418,12 +418,10 @@ void IntentUrlParser::restoreToPreviousServer(const PendingIntentChain &chain)
|
||||||
|
|
||||||
ServersSettings &servers = SettingsCache::instance().servers();
|
ServersSettings &servers = SettingsCache::instance().servers();
|
||||||
const int index = servers.findServerIndex(chain.previousServerHost, chain.previousServerPort);
|
const int index = servers.findServerIndex(chain.previousServerHost, chain.previousServerPort);
|
||||||
if (index >= 0 && servers.hasLoginData(chain.previousServerHost, chain.previousServerPort)) {
|
const auto server = servers.getServer(index);
|
||||||
const QString username =
|
if (server && servers.hasLoginData(chain.previousServerHost, chain.previousServerPort)) {
|
||||||
servers.getValue(QString("username%1").arg(index), "server", "server_details").toString();
|
client->connectToServer(chain.previousServerHost, chain.previousServerPort.toUInt(), server->username,
|
||||||
const QString password =
|
server->password);
|
||||||
servers.getValue(QString("password%1").arg(index), "server", "server_details").toString();
|
|
||||||
client->connectToServer(chain.previousServerHost, chain.previousServerPort.toUInt(), username, password);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
client->disconnectFromServer();
|
client->disconnectFromServer();
|
||||||
|
|
@ -438,11 +436,9 @@ void IntentUrlParser::restoreToPreviousServer(const PendingIntentChain &chain)
|
||||||
// when credentials are saved, otherwise stay offline.
|
// when credentials are saved, otherwise stay offline.
|
||||||
ServersSettings &servers = SettingsCache::instance().servers();
|
ServersSettings &servers = SettingsCache::instance().servers();
|
||||||
const int index = servers.findServerIndex(chain.previousServerHost, chain.previousServerPort);
|
const int index = servers.findServerIndex(chain.previousServerHost, chain.previousServerPort);
|
||||||
if (index >= 0 && servers.hasLoginData(chain.previousServerHost, chain.previousServerPort)) {
|
const auto server = servers.getServer(index);
|
||||||
const QString username =
|
if (server && servers.hasLoginData(chain.previousServerHost, chain.previousServerPort)) {
|
||||||
servers.getValue(QString("username%1").arg(index), "server", "server_details").toString();
|
client->connectToServer(chain.previousServerHost, chain.previousServerPort.toUInt(), server->username,
|
||||||
const QString password =
|
server->password);
|
||||||
servers.getValue(QString("password%1").arg(index), "server", "server_details").toString();
|
|
||||||
client->connectToServer(chain.previousServerHost, chain.previousServerPort.toUInt(), username, 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 <qlogging.h>
|
||||||
#include <qnamespace.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 BASE_HEIGHT = 200;
|
||||||
|
|
||||||
static constexpr int ENLARGED_PIXMAP_OFFSET = 10;
|
static constexpr int ENLARGED_PIXMAP_OFFSET = 10;
|
||||||
|
|
@ -162,12 +157,11 @@ void CardInfoPictureWidget::resizeEvent(QResizeEvent *event)
|
||||||
*/
|
*/
|
||||||
void CardInfoPictureWidget::setScaleFactor(const int scale)
|
void CardInfoPictureWidget::setScaleFactor(const int scale)
|
||||||
{
|
{
|
||||||
const int newWidth = BASE_WIDTH * scale / 100;
|
const QSize newSize = sizeForScaleFactor(scale);
|
||||||
const int newHeight = static_cast<int>(newWidth * ASPECT_RATIO);
|
|
||||||
|
|
||||||
scaleFactor = scale;
|
scaleFactor = scale;
|
||||||
|
|
||||||
setFixedSize(newWidth, newHeight);
|
setFixedSize(newSize);
|
||||||
updatePixmap();
|
updatePixmap();
|
||||||
|
|
||||||
emit cardScaleFactorChanged(scale);
|
emit cardScaleFactorChanged(scale);
|
||||||
|
|
@ -266,8 +260,13 @@ void CardInfoPictureWidget::paintEvent(QPaintEvent *event)
|
||||||
*/
|
*/
|
||||||
QSize CardInfoPictureWidget::sizeHint() const
|
QSize CardInfoPictureWidget::sizeHint() const
|
||||||
{
|
{
|
||||||
return {static_cast<int>(BASE_WIDTH * scaleFactor / 100.0),
|
return sizeForScaleFactor(scaleFactor);
|
||||||
static_cast<int>(BASE_WIDTH * scaleFactor / 100.0 * ASPECT_RATIO)};
|
}
|
||||||
|
|
||||||
|
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
|
Q_OBJECT
|
||||||
|
|
||||||
public:
|
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,
|
explicit CardInfoPictureWidget(QWidget *parent = nullptr,
|
||||||
bool hoverToZoomEnabled = false,
|
bool hoverToZoomEnabled = false,
|
||||||
bool raiseOnEnter = false);
|
bool raiseOnEnter = false);
|
||||||
|
|
@ -37,6 +42,12 @@ public:
|
||||||
}
|
}
|
||||||
[[nodiscard]] QSize sizeHint() const override;
|
[[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:
|
public slots:
|
||||||
void setCard(const ExactCard &card);
|
void setCard(const ExactCard &card);
|
||||||
void setScaleFactor(int scale); // New slot for scaling
|
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 "../general/layout_containers/flow_widget.h"
|
||||||
|
|
||||||
|
#include <QAbstractScrollArea>
|
||||||
#include <QEvent>
|
#include <QEvent>
|
||||||
#include <QFlags>
|
#include <QFlags>
|
||||||
#include <QHBoxLayout>
|
#include <QHBoxLayout>
|
||||||
|
|
@ -33,8 +34,8 @@ CardSizeWidget::CardSizeWidget(QWidget *parent, FlowWidget *_flowWidget, int def
|
||||||
cardSizeLabel->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum);
|
cardSizeLabel->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum);
|
||||||
|
|
||||||
cardSizeSlider = new QSlider(Qt::Horizontal, this);
|
cardSizeSlider = new QSlider(Qt::Horizontal, this);
|
||||||
cardSizeSlider->setRange(50, 250); ///< Slider range for card size adjustment.
|
cardSizeSlider->setRange(MIN_CARD_SIZE, MAX_CARD_SIZE); ///< Slider range for card size adjustment.
|
||||||
cardSizeSlider->setValue(defaultValue); ///< Initial slider value.
|
cardSizeSlider->setValue(defaultValue); ///< Initial slider value.
|
||||||
|
|
||||||
cardSizeLayout->addWidget(cardSizeLabel);
|
cardSizeLayout->addWidget(cardSizeLabel);
|
||||||
cardSizeLayout->addWidget(cardSizeSlider);
|
cardSizeLayout->addWidget(cardSizeSlider);
|
||||||
|
|
@ -73,13 +74,26 @@ QSlider *CardSizeWidget::getSlider() const
|
||||||
|
|
||||||
void CardSizeWidget::enableCtrlScrollResize(QWidget *host)
|
void CardSizeWidget::enableCtrlScrollResize(QWidget *host)
|
||||||
{
|
{
|
||||||
host->installEventFilter(this);
|
const auto watchScrollArea = [this](QAbstractScrollArea *scrollArea) {
|
||||||
|
if (scrollArea == nullptr) {
|
||||||
const auto scrollAreas = host->findChildren<QScrollArea *>();
|
return;
|
||||||
for (QScrollArea *scrollArea : scrollAreas) {
|
|
||||||
if (QWidget *content = scrollArea->widget()) {
|
|
||||||
content->installEventFilter(this);
|
|
||||||
}
|
}
|
||||||
|
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
|
Q_OBJECT
|
||||||
|
|
||||||
public:
|
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);
|
explicit CardSizeWidget(QWidget *parent, FlowWidget *flowWidget = nullptr, int defaultValue = 100);
|
||||||
[[nodiscard]] QSlider *getSlider() const;
|
[[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.
|
* @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
|
* 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
|
* scroll area the filter is also installed on the scroll area's content widget, and on the
|
||||||
* intercepts the wheel event before the scroll area would scroll the view.
|
* 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);
|
void enableCtrlScrollResize(QWidget *host);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@ class QWidget;
|
||||||
|
|
||||||
DlgConnect::DlgConnect(QWidget *parent) : QDialog(parent)
|
DlgConnect::DlgConnect(QWidget *parent) : QDialog(parent)
|
||||||
{
|
{
|
||||||
|
setObjectName("connectDialog");
|
||||||
previousHostButton = new QRadioButton(tr("Known Hosts"), this);
|
previousHostButton = new QRadioButton(tr("Known Hosts"), this);
|
||||||
previousHosts = new QComboBox(this);
|
previousHosts = new QComboBox(this);
|
||||||
|
|
||||||
|
|
@ -85,6 +86,11 @@ DlgConnect::DlgConnect(QWidget *parent) : QDialog(parent)
|
||||||
passwordLabel->setBuddy(passwordEdit);
|
passwordLabel->setBuddy(passwordEdit);
|
||||||
passwordEdit->setEchoMode(QLineEdit::Password);
|
passwordEdit->setEchoMode(QLineEdit::Password);
|
||||||
|
|
||||||
|
hostEdit->setObjectName("connectHostEdit");
|
||||||
|
portEdit->setObjectName("connectPortEdit");
|
||||||
|
playernameEdit->setObjectName("connectPlayernameEdit");
|
||||||
|
passwordEdit->setObjectName("connectPasswordEdit");
|
||||||
|
|
||||||
savePasswordCheckBox = new QCheckBox(tr("&Save password"));
|
savePasswordCheckBox = new QCheckBox(tr("&Save password"));
|
||||||
|
|
||||||
autoConnectCheckBox = new QCheckBox(tr("A&uto connect"));
|
autoConnectCheckBox = new QCheckBox(tr("A&uto connect"));
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,7 @@ class QWidget;
|
||||||
|
|
||||||
DlgEditTokens::DlgEditTokens(QWidget *parent) : QDialog(parent), currentCard(nullptr)
|
DlgEditTokens::DlgEditTokens(QWidget *parent) : QDialog(parent), currentCard(nullptr)
|
||||||
{
|
{
|
||||||
|
setObjectName("editTokensDialog");
|
||||||
nameLabel = new QLabel(tr("&Name:"));
|
nameLabel = new QLabel(tr("&Name:"));
|
||||||
nameEdit = new QLineEdit;
|
nameEdit = new QLineEdit;
|
||||||
nameEdit->setMaxLength(MAX_NAME_LENGTH);
|
nameEdit->setMaxLength(MAX_NAME_LENGTH);
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,7 @@
|
||||||
|
|
||||||
WndSets::WndSets(QWidget *parent) : QMainWindow(parent)
|
WndSets::WndSets(QWidget *parent) : QMainWindow(parent)
|
||||||
{
|
{
|
||||||
|
setObjectName("manageSetsWindow");
|
||||||
setOrderIsSorted = false;
|
setOrderIsSorted = false;
|
||||||
|
|
||||||
// left toolbar
|
// left toolbar
|
||||||
|
|
@ -221,6 +222,7 @@ WndSets::WndSets(QWidget *parent) : QMainWindow(parent)
|
||||||
|
|
||||||
WndSets::~WndSets()
|
WndSets::~WndSets()
|
||||||
{
|
{
|
||||||
|
disconnect(view->header(), &QHeaderView::geometriesChanged, this, &WndSets::saveHeaderState);
|
||||||
}
|
}
|
||||||
|
|
||||||
void WndSets::closeEvent(QCloseEvent * /*ev*/)
|
void WndSets::closeEvent(QCloseEvent * /*ev*/)
|
||||||
|
|
|
||||||
|
|
@ -73,6 +73,8 @@ DlgSettings::DlgSettings(QWidget *parent) : QDialog(parent)
|
||||||
resize(calculateDialogSize(hostSize, available.size(), minimum));
|
resize(calculateDialogSize(hostSize, available.size(), minimum));
|
||||||
setSizeGripEnabled(true);
|
setSizeGripEnabled(true);
|
||||||
|
|
||||||
|
setObjectName("settingsDialog");
|
||||||
|
|
||||||
connect(&SettingsCache::instance().personal(), &PersonalSettings::langChanged, this, &DlgSettings::updateLanguage);
|
connect(&SettingsCache::instance().personal(), &PersonalSettings::langChanged, this, &DlgSettings::updateLanguage);
|
||||||
|
|
||||||
settingsView = new SettingsView(this);
|
settingsView = new SettingsView(this);
|
||||||
|
|
@ -82,6 +84,7 @@ DlgSettings::DlgSettings(QWidget *parent) : QDialog(parent)
|
||||||
buttonBox->addStretch();
|
buttonBox->addStretch();
|
||||||
okButton = new QPushButton;
|
okButton = new QPushButton;
|
||||||
okButton->setDefault(true);
|
okButton->setDefault(true);
|
||||||
|
okButton->setObjectName("settingsOkButton");
|
||||||
connect(okButton, &QPushButton::clicked, this, &DlgSettings::close);
|
connect(okButton, &QPushButton::clicked, this, &DlgSettings::close);
|
||||||
buttonBox->addWidget(okButton);
|
buttonBox->addWidget(okButton);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@
|
||||||
|
|
||||||
DlgTipOfTheDay::DlgTipOfTheDay(QWidget *parent) : QDialog(parent)
|
DlgTipOfTheDay::DlgTipOfTheDay(QWidget *parent) : QDialog(parent)
|
||||||
{
|
{
|
||||||
|
setObjectName("tipOfTheDayDialog");
|
||||||
successfulInit = false;
|
successfulInit = false;
|
||||||
QString xmlPath = "theme:tips/tips_of_the_day.xml";
|
QString xmlPath = "theme:tips/tips_of_the_day.xml";
|
||||||
tipDatabase = new TipsOfTheDay(xmlPath, this);
|
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)
|
DlgUpdate::DlgUpdate(QWidget *parent) : QDialog(parent)
|
||||||
{
|
{
|
||||||
|
setObjectName("updateDialog");
|
||||||
|
|
||||||
// Handle layout
|
// Handle layout
|
||||||
statusLabel = new QLabel(this);
|
statusLabel = new QLabel(this);
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ class QWidget;
|
||||||
|
|
||||||
DlgViewLog::DlgViewLog(QWidget *parent) : QDialog(parent)
|
DlgViewLog::DlgViewLog(QWidget *parent) : QDialog(parent)
|
||||||
{
|
{
|
||||||
|
setObjectName("viewLogDialog");
|
||||||
logArea = new QPlainTextEdit;
|
logArea = new QPlainTextEdit;
|
||||||
logArea->setReadOnly(true);
|
logArea->setReadOnly(true);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -119,6 +119,8 @@ QWidget *fromWidgetId(WidgetId id)
|
||||||
|
|
||||||
SettingsView::SettingsView(QWidget *parent) : QWidget(parent), currentTabIndex(0), searchActive(false)
|
SettingsView::SettingsView(QWidget *parent) : QWidget(parent), currentTabIndex(0), searchActive(false)
|
||||||
{
|
{
|
||||||
|
setObjectName("settingsView");
|
||||||
|
|
||||||
setupUi();
|
setupUi();
|
||||||
|
|
||||||
connect(&SettingsCache::instance().personal(), &PersonalSettings::langChanged, this, &SettingsView::retranslateUi);
|
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->addAction(themePixmap(QStringLiteral("icons/search")), QLineEdit::LeadingPosition);
|
||||||
searchEdit->installEventFilter(this);
|
searchEdit->installEventFilter(this);
|
||||||
connect(searchEdit, &QLineEdit::textChanged, this, &SettingsView::onSearchTextChanged);
|
connect(searchEdit, &QLineEdit::textChanged, this, &SettingsView::onSearchTextChanged);
|
||||||
|
searchEdit->setObjectName("settingsSearchEdit");
|
||||||
|
|
||||||
auto *searchLayout = new QHBoxLayout;
|
auto *searchLayout = new QHBoxLayout;
|
||||||
searchLayout->addWidget(searchEdit);
|
searchLayout->addWidget(searchEdit);
|
||||||
|
|
|
||||||
|
|
@ -68,6 +68,7 @@
|
||||||
HomeWidget::HomeWidget(QWidget *parent, TabSupervisor *_tabSupervisor)
|
HomeWidget::HomeWidget(QWidget *parent, TabSupervisor *_tabSupervisor)
|
||||||
: QWidget(parent), tabSupervisor(_tabSupervisor), background(themePixmap(QStringLiteral("backgrounds/home")))
|
: QWidget(parent), tabSupervisor(_tabSupervisor), background(themePixmap(QStringLiteral("backgrounds/home")))
|
||||||
{
|
{
|
||||||
|
setObjectName("homeWidget");
|
||||||
layout = new QGridLayout(this);
|
layout = new QGridLayout(this);
|
||||||
|
|
||||||
backgroundSourceCard = new CardInfoPictureArtCropWidget(this);
|
backgroundSourceCard = new CardInfoPictureArtCropWidget(this);
|
||||||
|
|
@ -287,6 +288,7 @@ void HomeWidget::updateButtonsToBackgroundColor()
|
||||||
QGroupBox *HomeWidget::createButtons()
|
QGroupBox *HomeWidget::createButtons()
|
||||||
{
|
{
|
||||||
QGroupBox *box = new QGroupBox(this);
|
QGroupBox *box = new QGroupBox(this);
|
||||||
|
box->setObjectName("homeButtonsBox");
|
||||||
box->setStyleSheet(R"(
|
box->setStyleSheet(R"(
|
||||||
QGroupBox {
|
QGroupBox {
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
|
|
@ -311,27 +313,34 @@ QGroupBox *HomeWidget::createButtons()
|
||||||
boxLayout->addSpacing(25);
|
boxLayout->addSpacing(25);
|
||||||
|
|
||||||
connectButton = new HomeStyledButton(QString(), gradientColors);
|
connectButton = new HomeStyledButton(QString(), gradientColors);
|
||||||
|
connectButton->setObjectName("homeConnectButton");
|
||||||
boxLayout->addWidget(connectButton);
|
boxLayout->addWidget(connectButton);
|
||||||
|
|
||||||
visualDeckEditorButton = new HomeStyledButton(QString(), gradientColors);
|
visualDeckEditorButton = new HomeStyledButton(QString(), gradientColors);
|
||||||
|
visualDeckEditorButton->setObjectName("homeCreateNewDeckButton");
|
||||||
connect(visualDeckEditorButton, &QPushButton::clicked, tabSupervisor,
|
connect(visualDeckEditorButton, &QPushButton::clicked, tabSupervisor,
|
||||||
[this] { tabSupervisor->openDeckInNewTab(LoadedDeck()); });
|
[this] { tabSupervisor->openDeckInNewTab(LoadedDeck()); });
|
||||||
boxLayout->addWidget(visualDeckEditorButton);
|
boxLayout->addWidget(visualDeckEditorButton);
|
||||||
visualDeckStorageButton = new HomeStyledButton(QString(), gradientColors);
|
visualDeckStorageButton = new HomeStyledButton(QString(), gradientColors);
|
||||||
|
visualDeckStorageButton->setObjectName("homeBrowseDecksButton");
|
||||||
connect(visualDeckStorageButton, &QPushButton::clicked, tabSupervisor,
|
connect(visualDeckStorageButton, &QPushButton::clicked, tabSupervisor,
|
||||||
[this] { tabSupervisor->actTabVisualDeckStorage(true); });
|
[this] { tabSupervisor->actTabVisualDeckStorage(true); });
|
||||||
boxLayout->addWidget(visualDeckStorageButton);
|
boxLayout->addWidget(visualDeckStorageButton);
|
||||||
visualDatabaseDisplayButton = new HomeStyledButton(QString(), gradientColors);
|
visualDatabaseDisplayButton = new HomeStyledButton(QString(), gradientColors);
|
||||||
|
visualDatabaseDisplayButton->setObjectName("homeBrowseCardDatabaseButton");
|
||||||
connect(visualDatabaseDisplayButton, &QPushButton::clicked, tabSupervisor,
|
connect(visualDatabaseDisplayButton, &QPushButton::clicked, tabSupervisor,
|
||||||
&TabSupervisor::addVisualDatabaseDisplayTab);
|
&TabSupervisor::addVisualDatabaseDisplayTab);
|
||||||
boxLayout->addWidget(visualDatabaseDisplayButton);
|
boxLayout->addWidget(visualDatabaseDisplayButton);
|
||||||
edhrecButton = new HomeStyledButton(QString(), gradientColors);
|
edhrecButton = new HomeStyledButton(QString(), gradientColors);
|
||||||
|
edhrecButton->setObjectName("homeBrowseEdhrecButton");
|
||||||
connect(edhrecButton, &QPushButton::clicked, tabSupervisor, &TabSupervisor::addEdhrecMainTab);
|
connect(edhrecButton, &QPushButton::clicked, tabSupervisor, &TabSupervisor::addEdhrecMainTab);
|
||||||
boxLayout->addWidget(edhrecButton);
|
boxLayout->addWidget(edhrecButton);
|
||||||
archidektButton = new HomeStyledButton(QString(), gradientColors);
|
archidektButton = new HomeStyledButton(QString(), gradientColors);
|
||||||
|
archidektButton->setObjectName("homeBrowseArchidektButton");
|
||||||
connect(archidektButton, &QPushButton::clicked, tabSupervisor, &TabSupervisor::addArchidektTab);
|
connect(archidektButton, &QPushButton::clicked, tabSupervisor, &TabSupervisor::addArchidektTab);
|
||||||
boxLayout->addWidget(archidektButton);
|
boxLayout->addWidget(archidektButton);
|
||||||
replayButton = new HomeStyledButton(QString(), gradientColors);
|
replayButton = new HomeStyledButton(QString(), gradientColors);
|
||||||
|
replayButton->setObjectName("homeViewReplaysButton");
|
||||||
connect(replayButton, &QPushButton::clicked, tabSupervisor, [this] { tabSupervisor->actTabReplays(true); });
|
connect(replayButton, &QPushButton::clicked, tabSupervisor, [this] { tabSupervisor->actTabReplays(true); });
|
||||||
boxLayout->addWidget(replayButton);
|
boxLayout->addWidget(replayButton);
|
||||||
if (qobject_cast<MainWindow *>(tabSupervisor->parentWidget())) {
|
if (qobject_cast<MainWindow *>(tabSupervisor->parentWidget())) {
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,6 @@
|
||||||
#include <libcockatrice/protocol/pb/serverinfo_user.pb.h>
|
#include <libcockatrice/protocol/pb/serverinfo_user.pb.h>
|
||||||
#include <libcockatrice/protocol/pb/session_commands.pb.h>
|
#include <libcockatrice/protocol/pb/session_commands.pb.h>
|
||||||
#include <libcockatrice/protocol/pending_command.h>
|
#include <libcockatrice/protocol/pending_command.h>
|
||||||
#include <libcockatrice/utility/passwordhasher.h>
|
|
||||||
#include <libcockatrice/utility/qt_utils.h>
|
#include <libcockatrice/utility/qt_utils.h>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <sys/types.h>
|
#include <sys/types.h>
|
||||||
|
|
@ -299,17 +298,7 @@ void UserInfoBox::actPassword()
|
||||||
|
|
||||||
void UserInfoBox::changePassword(const QString &oldPassword, const QString &newPassword)
|
void UserInfoBox::changePassword(const QString &oldPassword, const QString &newPassword)
|
||||||
{
|
{
|
||||||
Command_AccountPassword cmd;
|
const Command_AccountPassword cmd = client->makeAccountPasswordCommand(oldPassword, newPassword);
|
||||||
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());
|
|
||||||
}
|
|
||||||
|
|
||||||
PendingCommand *pend = client->prepareSessionCommand(cmd);
|
PendingCommand *pend = client->prepareSessionCommand(cmd);
|
||||||
connect(pend, &PendingCommand::finished, this, &UserInfoBox::processPasswordResponse);
|
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();
|
ServersSettings &servers = SettingsCache::instance().servers();
|
||||||
|
|
||||||
int size = servers.getValue("totalServers", "server", "server_details").toInt() + 1;
|
for (const auto &entry : servers.getServers()) {
|
||||||
|
UserConnection_Information userInfo(entry.saveName, entry.host, entry.port, entry.username, entry.password,
|
||||||
for (int i = 0; i < size; i++) {
|
entry.savePassword, entry.site);
|
||||||
QString _saveName = servers.getValue(QString("saveName%1").arg(i), "server", "server_details").toString();
|
serverList.insert(entry.saveName, std::make_pair(entry.host, userInfo));
|
||||||
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));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return serverList;
|
return serverList;
|
||||||
|
|
@ -53,28 +44,18 @@ QStringList UserConnection_Information::getServerInfo(const QString &find)
|
||||||
|
|
||||||
ServersSettings &servers = SettingsCache::instance().servers();
|
ServersSettings &servers = SettingsCache::instance().servers();
|
||||||
|
|
||||||
int size = servers.getValue("totalServers", "server", "server_details").toInt() + 1;
|
for (const auto &entry : servers.getServers()) {
|
||||||
for (int i = 0; i < size; i++) {
|
if (find != entry.saveName) {
|
||||||
QString _saveName = servers.getValue(QString("saveName%1").arg(i), "server", "server_details").toString();
|
|
||||||
|
|
||||||
if (find != _saveName) {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
QString serverName = servers.getValue(QString("server%1").arg(i), "server", "server_details").toString();
|
_server.append(entry.saveName);
|
||||||
QString portNum = servers.getValue(QString("port%1").arg(i), "server", "server_details").toString();
|
_server.append(entry.host);
|
||||||
QString userName = servers.getValue(QString("username%1").arg(i), "server", "server_details").toString();
|
_server.append(entry.port);
|
||||||
QString pass = servers.getValue(QString("password%1").arg(i), "server", "server_details").toString();
|
_server.append(entry.username);
|
||||||
bool savePass = servers.getValue(QString("savePassword%1").arg(i), "server", "server_details").toBool();
|
_server.append(entry.password);
|
||||||
QString _site = servers.getValue(QString("site%1").arg(i), "server", "server_details").toString();
|
_server.append(entry.savePassword ? "1" : "0");
|
||||||
|
_server.append(entry.site);
|
||||||
_server.append(_saveName);
|
|
||||||
_server.append(serverName);
|
|
||||||
_server.append(portNum);
|
|
||||||
_server.append(userName);
|
|
||||||
_server.append(pass);
|
|
||||||
_server.append(savePass ? "1" : "0");
|
|
||||||
_server.append(_site);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ class TabSupervisor;
|
||||||
|
|
||||||
TabHome::TabHome(TabSupervisor *_tabSupervisor, AbstractClient *_client) : Tab(_tabSupervisor), client(_client)
|
TabHome::TabHome(TabSupervisor *_tabSupervisor, AbstractClient *_client) : Tab(_tabSupervisor), client(_client)
|
||||||
{
|
{
|
||||||
|
setObjectName("TabHome");
|
||||||
homeWidget = new HomeWidget(this, tabSupervisor);
|
homeWidget = new HomeWidget(this, tabSupervisor);
|
||||||
setCentralWidget(homeWidget);
|
setCentralWidget(homeWidget);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@
|
||||||
#include <QMessageBox>
|
#include <QMessageBox>
|
||||||
#include <QPushButton>
|
#include <QPushButton>
|
||||||
#include <QRadioButton>
|
#include <QRadioButton>
|
||||||
|
#include <QScrollBar>
|
||||||
#include <QSharedPointer>
|
#include <QSharedPointer>
|
||||||
#include <QSizePolicy>
|
#include <QSizePolicy>
|
||||||
#include <QSpinBox>
|
#include <QSpinBox>
|
||||||
|
|
@ -21,6 +22,7 @@
|
||||||
#include <QTableWidgetItem>
|
#include <QTableWidgetItem>
|
||||||
#include <QVBoxLayout>
|
#include <QVBoxLayout>
|
||||||
#include <QWidget>
|
#include <QWidget>
|
||||||
|
#include <QtGlobal>
|
||||||
#include <libcockatrice/network/client/abstract/abstract_client.h>
|
#include <libcockatrice/network/client/abstract/abstract_client.h>
|
||||||
#include <libcockatrice/protocol/pb/moderator_commands.pb.h>
|
#include <libcockatrice/protocol/pb/moderator_commands.pb.h>
|
||||||
#include <libcockatrice/protocol/pb/response_viewlog_history.pb.h>
|
#include <libcockatrice/protocol/pb/response_viewlog_history.pb.h>
|
||||||
|
|
@ -31,6 +33,17 @@
|
||||||
|
|
||||||
class TabSupervisor;
|
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)
|
TabLog::TabLog(TabSupervisor *_tabSupervisor, AbstractClient *_client, bool _canUseDeveloperCommands)
|
||||||
: Tab(_tabSupervisor), client(_client), canUseDeveloperCommands(_canUseDeveloperCommands)
|
: Tab(_tabSupervisor), client(_client), canUseDeveloperCommands(_canUseDeveloperCommands)
|
||||||
{
|
{
|
||||||
|
|
@ -52,12 +65,20 @@ TabLog::TabLog(TabSupervisor *_tabSupervisor, AbstractClient *_client, bool _can
|
||||||
chatTable->setHorizontalHeaderLabels(
|
chatTable->setHorizontalHeaderLabels(
|
||||||
QString(tr("Time;SenderName;SenderIP;Message;TargetID;TargetName")).split(";"));
|
QString(tr("Time;SenderName;SenderIP;Message;TargetID;TargetName")).split(";"));
|
||||||
|
|
||||||
QTabWidget *tabManager = new QTabWidget();
|
tabManager = new QTabWidget();
|
||||||
tabManager->addTab(roomTable, tr("Room Logs"));
|
tabManager->addTab(roomTable, tr("Room Logs"));
|
||||||
tabManager->addTab(gameTable, tr("Game Logs"));
|
tabManager->addTab(gameTable, tr("Game Logs"));
|
||||||
tabManager->addTab(chatTable, tr("Chat Logs"));
|
tabManager->addTab(chatTable, tr("Chat Logs"));
|
||||||
setCentralWidget(tabManager);
|
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();
|
createDock();
|
||||||
restartLayout();
|
restartLayout();
|
||||||
clearClicked();
|
clearClicked();
|
||||||
|
|
@ -74,12 +95,9 @@ void TabLog::retranslateUi()
|
||||||
|
|
||||||
void TabLog::getClicked()
|
void TabLog::getClicked()
|
||||||
{
|
{
|
||||||
if (findUsername->text().isEmpty() && findIPAddress->text().isEmpty() && findGameName->text().isEmpty() &&
|
// Empty filters are a valid browse: room/game history going back from
|
||||||
findGameID->text().isEmpty() && findMessage->text().isEmpty()) {
|
// today, paginated. Only require a date range + location, both of which
|
||||||
QMessageBox::critical(this, tr("Error"), tr("You must select at least one filter."));
|
// get sensible defaults below so a bare "Get" just works.
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!lastHour->isChecked() && !today->isChecked() && !pastDays->isChecked()) {
|
if (!lastHour->isChecked() && !today->isChecked() && !pastDays->isChecked()) {
|
||||||
pastDays->setChecked(true);
|
pastDays->setChecked(true);
|
||||||
pastXDays->setValue(20);
|
pastXDays->setValue(20);
|
||||||
|
|
@ -99,39 +117,71 @@ void TabLog::getClicked()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (maximumResults->value() == 0) {
|
if (maximumResults->value() == 0) {
|
||||||
maximumResults->setValue(1000);
|
maximumResults->setValue(200);
|
||||||
}
|
}
|
||||||
|
pageSize = qBound(1, maximumResults->value(), 1000);
|
||||||
|
|
||||||
int dateRange = 0;
|
// Build the command once, here, and let every page copy it: rebuilding it from the
|
||||||
if (lastHour->isChecked()) {
|
// widgets per page would run page 2+ against filters edited after Get was clicked,
|
||||||
dateRange = 1;
|
// appending rows selected by a different query under the first page's results.
|
||||||
}
|
currentQuery.Clear();
|
||||||
|
currentQuery.set_user_name(findUsername->text().toStdString());
|
||||||
if (today->isChecked()) {
|
currentQuery.set_ip_address(findIPAddress->text().toStdString());
|
||||||
dateRange = 24;
|
currentQuery.set_game_name(findGameName->text().toStdString());
|
||||||
}
|
currentQuery.set_game_id(findGameID->text().toStdString());
|
||||||
|
currentQuery.set_message(findMessage->text().toStdString());
|
||||||
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());
|
|
||||||
if (mainRoom->isChecked()) {
|
if (mainRoom->isChecked()) {
|
||||||
cmd.add_log_location("room");
|
currentQuery.add_log_location("room");
|
||||||
};
|
}
|
||||||
if (gameRoom->isChecked()) {
|
if (gameRoom->isChecked()) {
|
||||||
cmd.add_log_location("game");
|
currentQuery.add_log_location("game");
|
||||||
};
|
}
|
||||||
if (privateChat->isChecked()) {
|
if (privateChat->isChecked()) {
|
||||||
cmd.add_log_location("chat");
|
currentQuery.add_log_location("chat");
|
||||||
};
|
}
|
||||||
cmd.set_date_range(dateRange);
|
currentQuery.set_date_range(dateRangeForSelection());
|
||||||
cmd.set_maximum_results(maximumResults->value());
|
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;
|
PendingCommand *pend;
|
||||||
if (canUseDeveloperCommands) {
|
if (canUseDeveloperCommands) {
|
||||||
|
|
@ -141,10 +191,42 @@ void TabLog::getClicked()
|
||||||
pend = client->prepareModeratorCommand(cmd);
|
pend = client->prepareModeratorCommand(cmd);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isFetching = true;
|
||||||
|
getButton->setEnabled(false);
|
||||||
|
|
||||||
connect(pend, &PendingCommand::finished, this, &TabLog::viewLogHistory_processResponse);
|
connect(pend, &PendingCommand::finished, this, &TabLog::viewLogHistory_processResponse);
|
||||||
client->sendCommand(pend);
|
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()
|
void TabLog::clearClicked()
|
||||||
{
|
{
|
||||||
findUsername->clear();
|
findUsername->clear();
|
||||||
|
|
@ -166,6 +248,10 @@ void TabLog::clearClicked()
|
||||||
pastDays->setAutoExclusive(true);
|
pastDays->setAutoExclusive(true);
|
||||||
today->setAutoExclusive(true);
|
today->setAutoExclusive(true);
|
||||||
lastHour->setAutoExclusive(true);
|
lastHour->setAutoExclusive(true);
|
||||||
|
currentOffset = 0;
|
||||||
|
hasMoreResults = false;
|
||||||
|
hasCurrentQuery = false;
|
||||||
|
snapshotBefore.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
void TabLog::createDock()
|
void TabLog::createDock()
|
||||||
|
|
@ -209,14 +295,15 @@ void TabLog::createDock()
|
||||||
pastXDays = new QSpinBox;
|
pastXDays = new QSpinBox;
|
||||||
pastXDays->setMaximum(20);
|
pastXDays->setMaximum(20);
|
||||||
|
|
||||||
labelMaximum = new QLabel(tr("Maximum Results: "));
|
labelMaximum = new QLabel(tr("Results per page: "));
|
||||||
maximumResults = new QSpinBox;
|
maximumResults = new QSpinBox;
|
||||||
maximumResults->setMaximum(1000);
|
maximumResults->setMaximum(1000);
|
||||||
|
maximumResults->setToolTip(tr("Page size for smooth scrolling: more rows load automatically as you scroll."));
|
||||||
|
|
||||||
labelDescription = new QLabel(tr(
|
labelDescription = new QLabel(tr("Leave filters empty to browse recent room and game messages.\n"
|
||||||
"At least one filter is required.\nThe more information you put in, the more specific your results will be."));
|
"More rows load automatically as you scroll."));
|
||||||
|
|
||||||
getButton = new QPushButton(tr("Get User Logs"));
|
getButton = new QPushButton(tr("Get Logs"));
|
||||||
getButton->setAutoDefault(true);
|
getButton->setAutoDefault(true);
|
||||||
connect(getButton, &QPushButton::clicked, this, &TabLog::getClicked);
|
connect(getButton, &QPushButton::clicked, this, &TabLog::getClicked);
|
||||||
|
|
||||||
|
|
@ -262,7 +349,7 @@ void TabLog::createDock()
|
||||||
maxResultsGrid->addWidget(labelMaximum, 0, 0);
|
maxResultsGrid->addWidget(labelMaximum, 0, 0);
|
||||||
maxResultsGrid->addWidget(maximumResults, 0, 1);
|
maxResultsGrid->addWidget(maximumResults, 0, 1);
|
||||||
|
|
||||||
maxResultsGroupBox = new QGroupBox(tr("Maximum Results"));
|
maxResultsGroupBox = new QGroupBox(tr("Results per page"));
|
||||||
maxResultsGroupBox->setLayout(maxResultsGrid);
|
maxResultsGroupBox->setLayout(maxResultsGrid);
|
||||||
|
|
||||||
descriptionGrid = new QGridLayout;
|
descriptionGrid = new QGridLayout;
|
||||||
|
|
@ -295,84 +382,134 @@ void TabLog::createDock()
|
||||||
searchDock->setWidget(searchDockContents);
|
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)
|
void TabLog::viewLogHistory_processResponse(const Response &resp)
|
||||||
{
|
{
|
||||||
const Response_ViewLogHistory &response = resp.GetExtension(Response_ViewLogHistory::ext);
|
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) {
|
if (resp.response_code() != Response::RespOk) {
|
||||||
QMessageBox::critical(static_cast<QWidget *>(parent()), tr("Message History"),
|
QMessageBox::critical(static_cast<QWidget *>(parent()), tr("Message History"),
|
||||||
tr("Failed to collect message history information."));
|
tr("Failed to collect message history information."));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (response.log_message_size() == 0) {
|
if (isFirstPage && response.log_message_size() == 0) {
|
||||||
QMessageBox::information(static_cast<QWidget *>(parent()), tr("Message History"),
|
QMessageBox::information(static_cast<QWidget *>(parent()), tr("Message History"),
|
||||||
tr("There are no messages for the selected filters."));
|
tr("There are no messages for the selected filters."));
|
||||||
|
hasMoreResults = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
int roomCounter = 0, gameCounter = 0, chatCounter = 0;
|
const QString thisPageStart = response.log_message_size() > 0 ? pageStartKey(response) : QString();
|
||||||
roomTable->setRowCount(roomCounter);
|
|
||||||
gameTable->setRowCount(gameCounter);
|
if (isFirstPage) {
|
||||||
chatTable->setRowCount(chatCounter);
|
// 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) {
|
for (int i = 0; i < response.log_message_size(); ++i) {
|
||||||
ServerInfo_ChatMessage message = response.log_message(i);
|
ServerInfo_ChatMessage message = response.log_message(i);
|
||||||
const QString targetType = QString::fromStdString(message.target_type());
|
const QString targetType = QString::fromStdString(message.target_type());
|
||||||
if (targetType == "room" || targetType == "room_channel") {
|
if (targetType == "room" || targetType == "room_channel") {
|
||||||
roomTable->insertRow(roomCounter);
|
appendChatMessage(roomTable, roomCounter, QString::fromStdString(message.time()),
|
||||||
roomTable->setItem(roomCounter, 0, new QTableWidgetItem(QString::fromStdString(message.time())));
|
QString::fromStdString(message.sender_name()),
|
||||||
roomTable->setItem(roomCounter, 1, new QTableWidgetItem(QString::fromStdString(message.sender_name())));
|
QString::fromStdString(message.sender_ip()), QString::fromStdString(message.message()),
|
||||||
roomTable->setItem(roomCounter, 2, new QTableWidgetItem(QString::fromStdString(message.sender_ip())));
|
QString::fromStdString(message.target_id()),
|
||||||
roomTable->setItem(roomCounter, 3, new QTableWidgetItem(QString::fromStdString(message.message())));
|
QString::fromStdString(message.target_name()), QString::fromStdString(message.channel()));
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (targetType == "game") {
|
if (targetType == "game") {
|
||||||
gameTable->insertRow(gameCounter);
|
appendChatMessage(gameTable, gameCounter, QString::fromStdString(message.time()),
|
||||||
gameTable->setItem(gameCounter, 0, new QTableWidgetItem(QString::fromStdString(message.time())));
|
QString::fromStdString(message.sender_name()),
|
||||||
gameTable->setItem(gameCounter, 1, new QTableWidgetItem(QString::fromStdString(message.sender_name())));
|
QString::fromStdString(message.sender_ip()), QString::fromStdString(message.message()),
|
||||||
gameTable->setItem(gameCounter, 2, new QTableWidgetItem(QString::fromStdString(message.sender_ip())));
|
QString::fromStdString(message.target_id()),
|
||||||
gameTable->setItem(gameCounter, 3, new QTableWidgetItem(QString::fromStdString(message.message())));
|
QString::fromStdString(message.target_name()));
|
||||||
gameTable->setItem(gameCounter, 4, new QTableWidgetItem(QString::fromStdString(message.target_id())));
|
|
||||||
gameTable->setItem(gameCounter, 5, new QTableWidgetItem(QString::fromStdString(message.target_name())));
|
|
||||||
++gameCounter;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (targetType == "chat") {
|
if (targetType == "chat") {
|
||||||
chatTable->insertRow(chatCounter);
|
appendChatMessage(chatTable, chatCounter, QString::fromStdString(message.time()),
|
||||||
chatTable->setItem(chatCounter, 0, new QTableWidgetItem(QString::fromStdString(message.time())));
|
QString::fromStdString(message.sender_name()),
|
||||||
chatTable->setItem(chatCounter, 1, new QTableWidgetItem(QString::fromStdString(message.sender_name())));
|
QString::fromStdString(message.sender_ip()), QString::fromStdString(message.message()),
|
||||||
chatTable->setItem(chatCounter, 2, new QTableWidgetItem(QString::fromStdString(message.sender_ip())));
|
QString::fromStdString(message.target_id()),
|
||||||
chatTable->setItem(chatCounter, 3, new QTableWidgetItem(QString::fromStdString(message.message())));
|
QString::fromStdString(message.target_name()));
|
||||||
chatTable->setItem(chatCounter, 4, new QTableWidgetItem(QString::fromStdString(message.target_id())));
|
|
||||||
chatTable->setItem(chatCounter, 5, new QTableWidgetItem(QString::fromStdString(message.target_name())));
|
|
||||||
++chatCounter;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
if (roomCounter) {
|
||||||
roomTable->show();
|
roomTable->show();
|
||||||
roomTable->resizeColumnsToContents();
|
if (isFirstPage) {
|
||||||
|
roomTable->resizeColumnsToContents();
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
roomTable->hide();
|
roomTable->hide();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (gameCounter) {
|
if (gameCounter) {
|
||||||
gameTable->resizeColumnsToContents();
|
if (isFirstPage) {
|
||||||
|
gameTable->resizeColumnsToContents();
|
||||||
|
}
|
||||||
gameTable->show();
|
gameTable->show();
|
||||||
} else {
|
} else {
|
||||||
gameTable->hide();
|
gameTable->hide();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (chatCounter) {
|
if (chatCounter) {
|
||||||
chatTable->resizeColumnsToContents();
|
if (isFirstPage) {
|
||||||
|
chatTable->resizeColumnsToContents();
|
||||||
|
}
|
||||||
chatTable->show();
|
chatTable->show();
|
||||||
} else {
|
} else {
|
||||||
chatTable->hide();
|
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()
|
void TabLog::restartLayout()
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@
|
||||||
#include "tab.h"
|
#include "tab.h"
|
||||||
|
|
||||||
#include <QString>
|
#include <QString>
|
||||||
|
#include <libcockatrice/protocol/pb/moderator_commands.pb.h>
|
||||||
#include <qtmetamacros.h>
|
#include <qtmetamacros.h>
|
||||||
|
|
||||||
class AbstractClient;
|
class AbstractClient;
|
||||||
|
|
@ -25,6 +26,7 @@ class QWidget;
|
||||||
class QGridLayout;
|
class QGridLayout;
|
||||||
class QVBoxLayout;
|
class QVBoxLayout;
|
||||||
class QTableWidget;
|
class QTableWidget;
|
||||||
|
class QTabWidget;
|
||||||
class Response;
|
class Response;
|
||||||
class TabSupervisor;
|
class TabSupervisor;
|
||||||
|
|
||||||
|
|
@ -48,8 +50,37 @@ private:
|
||||||
*buttonGroupBox;
|
*buttonGroupBox;
|
||||||
QVBoxLayout *mainLayout;
|
QVBoxLayout *mainLayout;
|
||||||
QTableWidget *roomTable, *gameTable, *chatTable;
|
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();
|
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:
|
signals:
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,7 @@
|
||||||
#include <QPainter>
|
#include <QPainter>
|
||||||
#include <QRect>
|
#include <QRect>
|
||||||
#include <QSharedPointer>
|
#include <QSharedPointer>
|
||||||
|
#include <QStackedWidget>
|
||||||
#include <QStringLiteral>
|
#include <QStringLiteral>
|
||||||
#include <QStyle>
|
#include <QStyle>
|
||||||
#include <QStyleOption>
|
#include <QStyleOption>
|
||||||
|
|
@ -157,6 +158,15 @@ TabSupervisor::TabSupervisor(AbstractClient *_client, QMenu *tabsMenu, QWidget *
|
||||||
tabReplays(nullptr), tabAdmin(nullptr), tabCardArtRules(nullptr), tabLog(nullptr), tabReport(nullptr),
|
tabReplays(nullptr), tabAdmin(nullptr), tabCardArtRules(nullptr), tabLog(nullptr), tabReport(nullptr),
|
||||||
tabModeration(nullptr), tabDeveloper(nullptr), isLocalGame(false)
|
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);
|
setElideMode(Qt::ElideRight);
|
||||||
setMovable(true);
|
setMovable(true);
|
||||||
setIconSize(QSize(15, 15));
|
setIconSize(QSize(15, 15));
|
||||||
|
|
@ -462,6 +472,9 @@ void TabSupervisor::addCloseButtonToTab(Tab *tab, int tabIndex, QAction *manager
|
||||||
auto closeSide = static_cast<QTabBar::ButtonPosition>(
|
auto closeSide = static_cast<QTabBar::ButtonPosition>(
|
||||||
tabBar()->style()->styleHint(QStyle::SH_TabBar_CloseButtonPosition, nullptr, tabBar()));
|
tabBar()->style()->styleHint(QStyle::SH_TabBar_CloseButtonPosition, nullptr, tabBar()));
|
||||||
auto *closeButton = new CloseButton(tab);
|
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 (manager) {
|
||||||
// If managed, all close requests should go through the menu action
|
// If managed, all close requests should go through the menu action
|
||||||
connect(closeButton, &CloseButton::clicked, this, [manager] { checkAndTrigger(manager, false); });
|
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 "../../../filters/syntax_help.h"
|
||||||
#include "../../key_signals.h"
|
#include "../../key_signals.h"
|
||||||
#include "../../pixel_map_generator.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 "../cards/card_size_widget.h"
|
||||||
#include "../deck_editor/card_database_view.h"
|
#include "../deck_editor/card_database_view.h"
|
||||||
#include "../general/layout_containers/flow_widget.h"
|
#include "../general/layout_containers/flow_widget.h"
|
||||||
#include "../utility/custom_line_edit.h"
|
#include "../utility/custom_line_edit.h"
|
||||||
#include "card_database_display_model.h"
|
#include "card_database_display_model.h"
|
||||||
#include "card_database_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 "deck_list_model.h"
|
||||||
#include "libcockatrice/card/card_localization.h"
|
#include "libcockatrice/card/card_localization.h"
|
||||||
#include "libcockatrice/card/database/card_database_loader.h"
|
#include "libcockatrice/card/database/card_database_loader.h"
|
||||||
#include "libcockatrice/card/database/card_database_querier.h"
|
#include "libcockatrice/card/database/card_database_querier.h"
|
||||||
#include "libcockatrice/card/printing/exact_card.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_card.h"
|
||||||
#include "libcockatrice/filters/filter_tree.h"
|
#include "libcockatrice/filters/filter_tree.h"
|
||||||
#include "visual_database_display_color_filter_widget.h"
|
#include "visual_database_display_color_filter_widget.h"
|
||||||
#include "visual_database_display_filter_toolbar_widget.h"
|
#include "visual_database_display_filter_toolbar_widget.h"
|
||||||
|
|
||||||
#include <QAbstractItemModel>
|
|
||||||
#include <QAction>
|
#include <QAction>
|
||||||
#include <QDebug>
|
|
||||||
#include <QIcon>
|
#include <QIcon>
|
||||||
#include <QLabel>
|
#include <QLabel>
|
||||||
#include <QLineEdit>
|
#include <QLineEdit>
|
||||||
#include <QList>
|
|
||||||
#include <QMessageLogger>
|
|
||||||
#include <QModelIndex>
|
|
||||||
#include <QPair>
|
|
||||||
#include <QPersistentModelIndex>
|
|
||||||
#include <QPushButton>
|
#include <QPushButton>
|
||||||
#include <QScrollArea>
|
|
||||||
#include <QScrollBar>
|
|
||||||
#include <QSharedPointer>
|
|
||||||
#include <QSizePolicy>
|
#include <QSizePolicy>
|
||||||
#include <QSlider>
|
#include <QSlider>
|
||||||
#include <QStringLiteral>
|
#include <QStringLiteral>
|
||||||
|
|
@ -48,14 +37,11 @@
|
||||||
#include <QVBoxLayout>
|
#include <QVBoxLayout>
|
||||||
#include <QVariant>
|
#include <QVariant>
|
||||||
#include <QtGlobal>
|
#include <QtGlobal>
|
||||||
#include <algorithm>
|
|
||||||
#include <libcockatrice/card/card_info_comparator.h>
|
|
||||||
#include <libcockatrice/card/database/card_database.h>
|
#include <libcockatrice/card/database/card_database.h>
|
||||||
#include <libcockatrice/card/database/card_database_manager.h>
|
#include <libcockatrice/card/database/card_database_manager.h>
|
||||||
#include <libcockatrice/deck_list/tree/inner_deck_list_node.h>
|
#include <libcockatrice/deck_list/tree/inner_deck_list_node.h>
|
||||||
#include <libcockatrice/settings/cards_display_settings.h>
|
#include <libcockatrice/settings/cards_display_settings.h>
|
||||||
#include <libcockatrice/utility/qt_utils.h>
|
#include <libcockatrice/utility/qt_utils.h>
|
||||||
#include <utility>
|
|
||||||
|
|
||||||
class QMouseEvent;
|
class QMouseEvent;
|
||||||
|
|
||||||
|
|
@ -64,11 +50,6 @@ VisualDatabaseDisplayWidget::VisualDatabaseDisplayWidget(QWidget *parent,
|
||||||
DeckListModel *deckListModel)
|
DeckListModel *deckListModel)
|
||||||
: QWidget(parent)
|
: 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
|
// Create display model
|
||||||
databaseDisplayModel = new CardDatabaseDisplayModel(this);
|
databaseDisplayModel = new CardDatabaseDisplayModel(this);
|
||||||
databaseDisplayModel->setObjectName("databaseDisplayModel");
|
databaseDisplayModel->setObjectName("databaseDisplayModel");
|
||||||
|
|
@ -86,23 +67,12 @@ VisualDatabaseDisplayWidget::VisualDatabaseDisplayWidget(QWidget *parent,
|
||||||
connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::cardSearchLanguageChanged, this,
|
connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::cardSearchLanguageChanged, this,
|
||||||
applyCardSearchLanguage);
|
applyCardSearchLanguage);
|
||||||
|
|
||||||
cards = new QList<ExactCard>;
|
|
||||||
connect(databaseDisplayModel, &CardDatabaseDisplayModel::modelDirty, this,
|
|
||||||
&VisualDatabaseDisplayWidget::modelDirty);
|
|
||||||
|
|
||||||
// Set up main layout and widgets
|
// Set up main layout and widgets
|
||||||
setMinimumSize(0, 0);
|
setMinimumSize(0, 0);
|
||||||
setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
|
setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
|
||||||
mainLayout = QtUtils::makeLayout<QVBoxLayout>(this, 0, 1);
|
mainLayout = QtUtils::makeLayout<QVBoxLayout>(this, 0, 1);
|
||||||
setLayout(mainLayout);
|
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);
|
searchContainer = new FlowWidget(this, Qt::Horizontal, Qt::ScrollBarAlwaysOff, Qt::ScrollBarAlwaysOff);
|
||||||
|
|
||||||
searchEdit = new SearchLineEdit();
|
searchEdit = new SearchLineEdit();
|
||||||
|
|
@ -159,6 +129,25 @@ VisualDatabaseDisplayWidget::VisualDatabaseDisplayWidget(QWidget *parent,
|
||||||
connect(databaseView, &CardDatabaseView::relatedCardClicked, this,
|
connect(databaseView, &CardDatabaseView::relatedCardClicked, this,
|
||||||
&VisualDatabaseDisplayWidget::onRelatedCardClicked);
|
&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);
|
colorFilterWidget = new VisualDatabaseDisplayColorFilterWidget(this, filterModel);
|
||||||
|
|
||||||
filterContainer = new VisualDatabaseDisplayFilterToolbarWidget(this, deckListModel);
|
filterContainer = new VisualDatabaseDisplayFilterToolbarWidget(this, deckListModel);
|
||||||
|
|
@ -190,8 +179,6 @@ VisualDatabaseDisplayWidget::VisualDatabaseDisplayWidget(QWidget *parent,
|
||||||
databaseLoadIndicator->setVisible(false);
|
databaseLoadIndicator->setVisible(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
QScrollBar *scrollBar = flowWidget->scrollArea->verticalScrollBar();
|
|
||||||
connect(scrollBar, &QScrollBar::valueChanged, [this](int /*value*/) { loadCurrentPage(); });
|
|
||||||
retranslateUi();
|
retranslateUi();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -210,7 +197,7 @@ void VisualDatabaseDisplayWidget::initialize()
|
||||||
|
|
||||||
mainLayout->addWidget(databaseView);
|
mainLayout->addWidget(databaseView);
|
||||||
|
|
||||||
mainLayout->addWidget(flowWidget);
|
mainLayout->addWidget(cardView);
|
||||||
|
|
||||||
mainLayout->addWidget(cardSizeWidget);
|
mainLayout->addWidget(cardSizeWidget);
|
||||||
|
|
||||||
|
|
@ -241,8 +228,6 @@ void VisualDatabaseDisplayWidget::initializeFilters()
|
||||||
|
|
||||||
databaseDisplayModel->setFilterTree(filterModel->filterTree());
|
databaseDisplayModel->setFilterTree(filterModel->filterTree());
|
||||||
|
|
||||||
QTimer::singleShot(5000, this, [this] { loadCurrentPage(); });
|
|
||||||
|
|
||||||
retranslateUi();
|
retranslateUi();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -278,71 +263,36 @@ void VisualDatabaseDisplayWidget::updateDisplayModeButton()
|
||||||
displayModeButton->setText(displayModeButton->isChecked() ? tr("Table") : tr("Visual"));
|
displayModeButton->setText(displayModeButton->isChecked() ? tr("Table") : tr("Visual"));
|
||||||
}
|
}
|
||||||
|
|
||||||
void VisualDatabaseDisplayWidget::resizeEvent(QResizeEvent *event)
|
|
||||||
{
|
|
||||||
QWidget::resizeEvent(event);
|
|
||||||
loadCurrentPage();
|
|
||||||
}
|
|
||||||
|
|
||||||
void VisualDatabaseDisplayWidget::onDisplayModeChanged(bool checked)
|
void VisualDatabaseDisplayWidget::onDisplayModeChanged(bool checked)
|
||||||
{
|
{
|
||||||
updateDisplayModeButton();
|
updateDisplayModeButton();
|
||||||
if (checked) {
|
if (checked) {
|
||||||
// Table mode
|
// Table mode
|
||||||
flowWidget->setVisible(false);
|
cardView->setVisible(false);
|
||||||
cardSizeWidget->setVisible(false);
|
cardSizeWidget->setVisible(false);
|
||||||
databaseView->setItemDelegate(new QStyledItemDelegate(databaseView));
|
databaseView->setItemDelegate(new QStyledItemDelegate(databaseView));
|
||||||
databaseView->setVisible(true);
|
databaseView->setVisible(true);
|
||||||
} else {
|
} else {
|
||||||
// Visual mode
|
// Visual mode
|
||||||
flowWidget->setVisible(true);
|
cardView->setVisible(true);
|
||||||
cardSizeWidget->setVisible(true);
|
cardSizeWidget->setVisible(true);
|
||||||
databaseView->setVisible(false);
|
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
|
bool VisualDatabaseDisplayWidget::isVisualDisplayMode() const
|
||||||
{
|
{
|
||||||
return !displayModeButton->isChecked();
|
return !displayModeButton->isChecked();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QStringList VisualDatabaseDisplayWidget::collectSetFilterTerms() const
|
||||||
|
{
|
||||||
|
return filterModel->getFilterTermsOfType(CardFilter::AttrSet);
|
||||||
|
}
|
||||||
|
|
||||||
void VisualDatabaseDisplayWidget::onSearchModelChanged()
|
void VisualDatabaseDisplayWidget::onSearchModelChanged()
|
||||||
{
|
{
|
||||||
if (flowWidget->isVisible()) {
|
visualDisplayModel->setSetFilterTerms(collectSetFilterTerms());
|
||||||
// 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";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void VisualDatabaseDisplayWidget::onSelectedCardChanged(const QString &cardName)
|
void VisualDatabaseDisplayWidget::onSelectedCardChanged(const QString &cardName)
|
||||||
|
|
@ -368,113 +318,3 @@ void VisualDatabaseDisplayWidget::onRelatedCardClicked(const QString &relatedCar
|
||||||
ExactCard exactCard = CardDatabaseManager::query()->guessCard({relatedCard});
|
ExactCard exactCard = CardDatabaseManager::query()->guessCard({relatedCard});
|
||||||
emit cardInfoRequested(exactCard);
|
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 "libcockatrice/card/card_info.h"
|
||||||
|
|
||||||
#include <QLoggingCategory>
|
|
||||||
#include <QMouseEvent>
|
|
||||||
#include <QString>
|
#include <QString>
|
||||||
#include <QStringList>
|
#include <QStringList>
|
||||||
#include <QWidget>
|
#include <QWidget>
|
||||||
|
|
@ -21,26 +19,21 @@
|
||||||
class CardDatabaseDisplayModel;
|
class CardDatabaseDisplayModel;
|
||||||
class CardDatabaseModel;
|
class CardDatabaseModel;
|
||||||
class CardDatabaseView;
|
class CardDatabaseView;
|
||||||
|
class CardDatabaseVisualDisplayModel;
|
||||||
|
class CardDatabaseVisualView;
|
||||||
class CardSizeWidget;
|
class CardSizeWidget;
|
||||||
class DeckList;
|
|
||||||
class DeckListModel;
|
class DeckListModel;
|
||||||
class ExactCard;
|
class ExactCard;
|
||||||
class FilterTreeModel;
|
class FilterTreeModel;
|
||||||
class FlowWidget;
|
class FlowWidget;
|
||||||
class OverlapControlWidget;
|
|
||||||
class QLabel;
|
class QLabel;
|
||||||
class QModelIndex;
|
class QMouseEvent;
|
||||||
class QPushButton;
|
class QPushButton;
|
||||||
class QScrollArea;
|
|
||||||
class QTimer;
|
|
||||||
class QToolButton;
|
class QToolButton;
|
||||||
class QVBoxLayout;
|
class QVBoxLayout;
|
||||||
class SearchLineEdit;
|
class SearchLineEdit;
|
||||||
class VisualDatabaseDisplayColorFilterWidget;
|
class VisualDatabaseDisplayColorFilterWidget;
|
||||||
class VisualDatabaseDisplayFilterToolbarWidget;
|
class VisualDatabaseDisplayFilterToolbarWidget;
|
||||||
template <typename T> class QList;
|
|
||||||
|
|
||||||
inline Q_LOGGING_CATEGORY(VisualDatabaseDisplayLog, "visual_database_display");
|
|
||||||
|
|
||||||
class VisualDatabaseDisplayWidget : public QWidget
|
class VisualDatabaseDisplayWidget : public QWidget
|
||||||
{
|
{
|
||||||
|
|
@ -52,14 +45,6 @@ public:
|
||||||
DeckListModel *deckListModel = nullptr);
|
DeckListModel *deckListModel = nullptr);
|
||||||
void retranslateUi();
|
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.
|
* @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.
|
* 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;
|
bool isVisualDisplayMode() const;
|
||||||
|
|
||||||
public slots:
|
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();
|
void onSearchModelChanged();
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
|
|
@ -101,11 +93,6 @@ signals:
|
||||||
|
|
||||||
protected slots:
|
protected slots:
|
||||||
void initialize();
|
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 onDisplayModeChanged(bool checked);
|
||||||
|
|
||||||
void onSelectedCardChanged(const QString &cardName);
|
void onSelectedCardChanged(const QString &cardName);
|
||||||
|
|
@ -125,31 +112,21 @@ private:
|
||||||
QToolButton *clearFilterWidget;
|
QToolButton *clearFilterWidget;
|
||||||
VisualDatabaseDisplayFilterToolbarWidget *filterContainer;
|
VisualDatabaseDisplayFilterToolbarWidget *filterContainer;
|
||||||
CardDatabaseDisplayModel *databaseDisplayModel;
|
CardDatabaseDisplayModel *databaseDisplayModel;
|
||||||
|
CardDatabaseVisualDisplayModel *visualDisplayModel;
|
||||||
CardDatabaseView *databaseView;
|
CardDatabaseView *databaseView;
|
||||||
|
CardDatabaseVisualView *cardView;
|
||||||
std::function<QString()> newZoneCreator;
|
std::function<QString()> newZoneCreator;
|
||||||
QList<ExactCard> *cards;
|
|
||||||
QVBoxLayout *mainLayout;
|
QVBoxLayout *mainLayout;
|
||||||
QScrollArea *scrollArea;
|
|
||||||
FlowWidget *flowWidget;
|
|
||||||
QWidget *overlapCategories;
|
|
||||||
QVBoxLayout *overlapCategoriesLayout;
|
|
||||||
OverlapControlWidget *overlapControlWidget;
|
|
||||||
CardSizeWidget *cardSizeWidget;
|
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 filtersInitialized = false;
|
||||||
bool initialLoadScheduled = false;
|
|
||||||
|
|
||||||
void initializeFilters();
|
void initializeFilters();
|
||||||
void highlightAllSearchEdit();
|
void highlightAllSearchEdit();
|
||||||
void updateDisplayModeButton();
|
void updateDisplayModeButton();
|
||||||
bool nearEndOfPage() const;
|
[[nodiscard]] QStringList collectSetFilterTerms() const;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
void resizeEvent(QResizeEvent *event) override;
|
|
||||||
void showEvent(QShowEvent *event) override;
|
void showEvent(QShowEvent *event) override;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -55,6 +55,13 @@
|
||||||
#include "widgets/server/handle_public_servers.h"
|
#include "widgets/server/handle_public_servers.h"
|
||||||
#include "widgets/settings_page/abstract_settings_page.h"
|
#include "widgets/settings_page/abstract_settings_page.h"
|
||||||
#include "widgets/tabs/api/commander_spellbook/handle_commander_brackets.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 <QAction>
|
||||||
#include <QApplication>
|
#include <QApplication>
|
||||||
|
|
@ -254,7 +261,11 @@ void MainWindow::actWatchReplay()
|
||||||
|
|
||||||
void MainWindow::localGameEnded()
|
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;
|
localServer = nullptr;
|
||||||
|
|
||||||
aConnect->setEnabled(true);
|
aConnect->setEnabled(true);
|
||||||
|
|
@ -306,6 +317,7 @@ void MainWindow::actAbout()
|
||||||
mb.setIconPixmap(
|
mb.setIconPixmap(
|
||||||
themePixmap(QStringLiteral("cockatrice")).scaled(64, 64, Qt::KeepAspectRatio, Qt::SmoothTransformation));
|
themePixmap(QStringLiteral("cockatrice")).scaled(64, 64, Qt::KeepAspectRatio, Qt::SmoothTransformation));
|
||||||
mb.setTextInteractionFlags(Qt::TextBrowserInteraction);
|
mb.setTextInteractionFlags(Qt::TextBrowserInteraction);
|
||||||
|
mb.setObjectName("aboutDialog");
|
||||||
mb.exec();
|
mb.exec();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -344,6 +356,24 @@ void MainWindow::actOpenSettingsFolder()
|
||||||
QtUtils::openPathInFileManager(dir);
|
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()
|
void MainWindow::retranslateUi()
|
||||||
{
|
{
|
||||||
connectionController->refreshWindowTitle();
|
connectionController->refreshWindowTitle();
|
||||||
|
|
@ -388,6 +418,10 @@ void MainWindow::retranslateUi()
|
||||||
aViewLog->setText(tr("View &Debug Log"));
|
aViewLog->setText(tr("View &Debug Log"));
|
||||||
aOpenSettingsFolder->setText(tr("Open Settings Folder"));
|
aOpenSettingsFolder->setText(tr("Open Settings Folder"));
|
||||||
aFirstRunWizard->setText(tr("Re-run Onboarding Wizard..."));
|
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"));
|
aShow->setText(tr("Show/Hide"));
|
||||||
|
|
||||||
|
|
@ -397,24 +431,33 @@ void MainWindow::retranslateUi()
|
||||||
void MainWindow::createActions()
|
void MainWindow::createActions()
|
||||||
{
|
{
|
||||||
aConnect = new QAction(this);
|
aConnect = new QAction(this);
|
||||||
|
aConnect->setObjectName("actionConnect");
|
||||||
connect(aConnect, &QAction::triggered, this, &MainWindow::actConnect);
|
connect(aConnect, &QAction::triggered, this, &MainWindow::actConnect);
|
||||||
aDisconnect = new QAction(this);
|
aDisconnect = new QAction(this);
|
||||||
|
aDisconnect->setObjectName("actionDisconnect");
|
||||||
aDisconnect->setEnabled(false);
|
aDisconnect->setEnabled(false);
|
||||||
connect(aDisconnect, &QAction::triggered, this, &MainWindow::actDisconnect);
|
connect(aDisconnect, &QAction::triggered, this, &MainWindow::actDisconnect);
|
||||||
aSinglePlayer = new QAction(this);
|
aSinglePlayer = new QAction(this);
|
||||||
|
aSinglePlayer->setObjectName("actionSinglePlayer");
|
||||||
connect(aSinglePlayer, &QAction::triggered, this, &MainWindow::actSinglePlayer);
|
connect(aSinglePlayer, &QAction::triggered, this, &MainWindow::actSinglePlayer);
|
||||||
aWatchReplay = new QAction(this);
|
aWatchReplay = new QAction(this);
|
||||||
|
aWatchReplay->setObjectName("actionWatchReplay");
|
||||||
connect(aWatchReplay, &QAction::triggered, this, &MainWindow::actWatchReplay);
|
connect(aWatchReplay, &QAction::triggered, this, &MainWindow::actWatchReplay);
|
||||||
aFullScreen = new QAction(this);
|
aFullScreen = new QAction(this);
|
||||||
|
aFullScreen->setObjectName("actionFullScreen");
|
||||||
aFullScreen->setCheckable(true);
|
aFullScreen->setCheckable(true);
|
||||||
connect(aFullScreen, &QAction::toggled, this, &MainWindow::actFullScreen);
|
connect(aFullScreen, &QAction::toggled, this, &MainWindow::actFullScreen);
|
||||||
aRegister = new QAction(this);
|
aRegister = new QAction(this);
|
||||||
|
aRegister->setObjectName("actionRegister");
|
||||||
connect(aRegister, &QAction::triggered, connectionController, &ConnectionController::registerToServer);
|
connect(aRegister, &QAction::triggered, connectionController, &ConnectionController::registerToServer);
|
||||||
aForgotPassword = new QAction(this);
|
aForgotPassword = new QAction(this);
|
||||||
|
aForgotPassword->setObjectName("actionForgotPassword");
|
||||||
connect(aForgotPassword, &QAction::triggered, connectionController, &ConnectionController::forgotPasswordRequest);
|
connect(aForgotPassword, &QAction::triggered, connectionController, &ConnectionController::forgotPasswordRequest);
|
||||||
aSettings = new QAction(this);
|
aSettings = new QAction(this);
|
||||||
|
aSettings->setObjectName("actionSettings");
|
||||||
connect(aSettings, &QAction::triggered, this, &MainWindow::actSettings);
|
connect(aSettings, &QAction::triggered, this, &MainWindow::actSettings);
|
||||||
aExit = new QAction(this);
|
aExit = new QAction(this);
|
||||||
|
aExit->setObjectName("actionExit");
|
||||||
connect(aExit, &QAction::triggered, this, &MainWindow::actExit);
|
connect(aExit, &QAction::triggered, this, &MainWindow::actExit);
|
||||||
|
|
||||||
aManageSets = new QAction(QString(), this);
|
aManageSets = new QAction(QString(), this);
|
||||||
|
|
@ -451,6 +494,12 @@ void MainWindow::createActions()
|
||||||
connect(aOpenSettingsFolder, &QAction::triggered, this, &MainWindow::actOpenSettingsFolder);
|
connect(aOpenSettingsFolder, &QAction::triggered, this, &MainWindow::actOpenSettingsFolder);
|
||||||
aFirstRunWizard = new QAction(this);
|
aFirstRunWizard = new QAction(this);
|
||||||
connect(aFirstRunWizard, &QAction::triggered, this, [this] { runFirstRunWizard(); });
|
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);
|
aShow = new QAction(this);
|
||||||
connect(aShow, &QAction::triggered, this, &MainWindow::actShow);
|
connect(aShow, &QAction::triggered, this, &MainWindow::actShow);
|
||||||
|
|
@ -491,7 +540,9 @@ void MainWindow::createActions()
|
||||||
|
|
||||||
void MainWindow::createMenus()
|
void MainWindow::createMenus()
|
||||||
{
|
{
|
||||||
|
menuBar()->setObjectName("menubar");
|
||||||
cockatriceMenu = menuBar()->addMenu(QString());
|
cockatriceMenu = menuBar()->addMenu(QString());
|
||||||
|
cockatriceMenu->setObjectName("cockatriceMenu");
|
||||||
cockatriceMenu->addAction(aConnect);
|
cockatriceMenu->addAction(aConnect);
|
||||||
cockatriceMenu->addAction(aDisconnect);
|
cockatriceMenu->addAction(aDisconnect);
|
||||||
cockatriceMenu->addAction(aRegister);
|
cockatriceMenu->addAction(aRegister);
|
||||||
|
|
@ -507,6 +558,7 @@ void MainWindow::createMenus()
|
||||||
cockatriceMenu->addAction(aExit);
|
cockatriceMenu->addAction(aExit);
|
||||||
|
|
||||||
dbMenu = menuBar()->addMenu(QString());
|
dbMenu = menuBar()->addMenu(QString());
|
||||||
|
dbMenu->setObjectName("dbMenu");
|
||||||
dbMenu->addAction(aManageSets);
|
dbMenu->addAction(aManageSets);
|
||||||
dbMenu->addAction(aEditTokens);
|
dbMenu->addAction(aEditTokens);
|
||||||
dbMenu->addSeparator();
|
dbMenu->addSeparator();
|
||||||
|
|
@ -517,8 +569,10 @@ void MainWindow::createMenus()
|
||||||
dbMenu->addAction(aReloadCardDatabase);
|
dbMenu->addAction(aReloadCardDatabase);
|
||||||
|
|
||||||
tabsMenu = menuBar()->addMenu(QString());
|
tabsMenu = menuBar()->addMenu(QString());
|
||||||
|
tabsMenu->setObjectName("tabsMenu");
|
||||||
|
|
||||||
helpMenu = menuBar()->addMenu(QString());
|
helpMenu = menuBar()->addMenu(QString());
|
||||||
|
helpMenu->setObjectName("helpMenu");
|
||||||
helpMenu->addAction(aAbout);
|
helpMenu->addAction(aAbout);
|
||||||
helpMenu->addAction(aTips);
|
helpMenu->addAction(aTips);
|
||||||
helpMenu->addSeparator();
|
helpMenu->addSeparator();
|
||||||
|
|
@ -531,12 +585,19 @@ void MainWindow::createMenus()
|
||||||
helpMenu->addAction(aOpenSettingsFolder);
|
helpMenu->addAction(aOpenSettingsFolder);
|
||||||
helpMenu->addSeparator();
|
helpMenu->addSeparator();
|
||||||
helpMenu->addAction(aFirstRunWizard);
|
helpMenu->addAction(aFirstRunWizard);
|
||||||
|
#ifdef WITH_GUI_TEST
|
||||||
|
helpMenu->addSeparator();
|
||||||
|
helpMenu->addAction(aRecord);
|
||||||
|
helpMenu->addAction(aPlayRecording);
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
MainWindow::MainWindow(QWidget *parent)
|
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)
|
cardUpdateProcess(nullptr), logviewDialog(nullptr)
|
||||||
{
|
{
|
||||||
|
setObjectName("MainWindow");
|
||||||
|
|
||||||
connect(&SettingsCache::instance().cacheStorage(), &CacheStorageSettings::pixmapCacheSizeChanged, this,
|
connect(&SettingsCache::instance().cacheStorage(), &CacheStorageSettings::pixmapCacheSizeChanged, this,
|
||||||
&MainWindow::pixmapCacheSizeChanged);
|
&MainWindow::pixmapCacheSizeChanged);
|
||||||
pixmapCacheSizeChanged(SettingsCache::instance().cacheStorage().getPixmapCacheSize());
|
pixmapCacheSizeChanged(SettingsCache::instance().cacheStorage().getPixmapCacheSize());
|
||||||
|
|
@ -622,6 +683,13 @@ MainWindow::MainWindow(QWidget *parent)
|
||||||
// run startup check async
|
// run startup check async
|
||||||
QTimer::singleShot(0, this, &MainWindow::startupConfigCheck);
|
QTimer::singleShot(0, this, &MainWindow::startupConfigCheck);
|
||||||
QTimer::singleShot(0, this, &MainWindow::applyStartupDestination);
|
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()
|
void MainWindow::startupConfigCheck()
|
||||||
|
|
@ -776,6 +844,11 @@ void MainWindow::runFirstRunWizard(bool firstRun)
|
||||||
*/
|
*/
|
||||||
void MainWindow::applyStartupDestination()
|
void MainWindow::applyStartupDestination()
|
||||||
{
|
{
|
||||||
|
if (noConnect) {
|
||||||
|
qCInfo(WindowMainStartupAutoconnectLog) << "Startup destination suppressed (--no-connect)";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// An explicit command-line connect takes precedence over the startup destination.
|
// An explicit command-line connect takes precedence over the startup destination.
|
||||||
if (!connectTo.isEmpty()) {
|
if (!connectTo.isEmpty()) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -1045,6 +1118,10 @@ void MainWindow::handleCockatriceLink(const QString &url)
|
||||||
|
|
||||||
void MainWindow::attemptStartupAutoConnect()
|
void MainWindow::attemptStartupAutoConnect()
|
||||||
{
|
{
|
||||||
|
if (noConnect) {
|
||||||
|
qCInfo(WindowMainStartupAutoconnectLog) << "Auto-connect suppressed (--no-connect)";
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (startupAutoConnectAttempted || skipStartupAutoConnect) {
|
if (startupAutoConnectAttempted || skipStartupAutoConnect) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,7 @@
|
||||||
class QAction;
|
class QAction;
|
||||||
class QToolButton;
|
class QToolButton;
|
||||||
class QWidget;
|
class QWidget;
|
||||||
|
class pqTestUtility;
|
||||||
struct LocalGameOptions;
|
struct LocalGameOptions;
|
||||||
|
|
||||||
inline Q_LOGGING_CATEGORY(WindowMainLog, "window_main");
|
inline Q_LOGGING_CATEGORY(WindowMainLog, "window_main");
|
||||||
|
|
@ -104,6 +105,11 @@ private slots:
|
||||||
void actUpdate();
|
void actUpdate();
|
||||||
void actViewLog();
|
void actViewLog();
|
||||||
void actOpenSettingsFolder();
|
void actOpenSettingsFolder();
|
||||||
|
#ifdef WITH_GUI_TEST
|
||||||
|
void actRecord();
|
||||||
|
void actPlayRecording();
|
||||||
|
#endif
|
||||||
|
|
||||||
void actShow();
|
void actShow();
|
||||||
void showWindowIfHidden();
|
void showWindowIfHidden();
|
||||||
void onUrlChainFinished(bool connected);
|
void onUrlChainFinished(bool connected);
|
||||||
|
|
@ -185,6 +191,9 @@ private:
|
||||||
*aReloadCardDatabase;
|
*aReloadCardDatabase;
|
||||||
QAction *aTips, *aUpdate, *aCheckCardUpdates, *aCheckCardUpdatesBackground, *aFirstRunWizard, *aStatusBar,
|
QAction *aTips, *aUpdate, *aCheckCardUpdates, *aCheckCardUpdatesBackground, *aFirstRunWizard, *aStatusBar,
|
||||||
*aViewLog, *aOpenSettingsFolder;
|
*aViewLog, *aOpenSettingsFolder;
|
||||||
|
#ifdef WITH_GUI_TEST
|
||||||
|
QAction *aRecord, *aPlayRecording;
|
||||||
|
#endif
|
||||||
|
|
||||||
TabSupervisor *tabSupervisor;
|
TabSupervisor *tabSupervisor;
|
||||||
IntentUrlParser *urlParser;
|
IntentUrlParser *urlParser;
|
||||||
|
|
@ -208,6 +217,7 @@ private:
|
||||||
bool skipStartupAutoConnect = false;
|
bool skipStartupAutoConnect = false;
|
||||||
bool startupAutoConnectAttempted = false;
|
bool startupAutoConnectAttempted = false;
|
||||||
bool firstRunWizardActive = false;
|
bool firstRunWizardActive = false;
|
||||||
|
bool noConnect = false; ///< --no-connect: skip every startup connection
|
||||||
QProcess *cardUpdateProcess;
|
QProcess *cardUpdateProcess;
|
||||||
QByteArray cardUpdateOutputBuffer;
|
QByteArray cardUpdateOutputBuffer;
|
||||||
QByteArray cardUpdateErrorOutput; ///< diagnostic output collected from the updater's stderr
|
QByteArray cardUpdateErrorOutput; ///< diagnostic output collected from the updater's stderr
|
||||||
|
|
@ -219,6 +229,9 @@ private:
|
||||||
GameReplay *replay;
|
GameReplay *replay;
|
||||||
DlgTipOfTheDay *tip;
|
DlgTipOfTheDay *tip;
|
||||||
QUrl connectTo;
|
QUrl connectTo;
|
||||||
|
#ifdef WITH_GUI_TEST
|
||||||
|
pqTestUtility *testUtility;
|
||||||
|
#endif
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit MainWindow(QWidget *parent = nullptr);
|
explicit MainWindow(QWidget *parent = nullptr);
|
||||||
|
|
@ -226,6 +239,16 @@ public:
|
||||||
{
|
{
|
||||||
connectTo = QUrl(QString("cockatrice://%1").arg(url));
|
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
|
// When set, the window's own startup connection (--connect or auto-connect
|
||||||
// on first activation) is skipped. Used for activation launches: the intent
|
// on first activation) is skipped. Used for activation launches: the intent
|
||||||
// chain triggered by a cockatrice:// URL owns the connection, and letting
|
// chain triggered by a cockatrice:// URL owns the connection, and letting
|
||||||
|
|
@ -266,6 +289,13 @@ public:
|
||||||
*/
|
*/
|
||||||
void requestInstallerClose();
|
void requestInstallerClose();
|
||||||
|
|
||||||
|
#ifdef WITH_GUI_TEST
|
||||||
|
pqTestUtility *getTestUtility() const
|
||||||
|
{
|
||||||
|
return testUtility;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
void closeEvent(QCloseEvent *event) override;
|
void closeEvent(QCloseEvent *event) override;
|
||||||
void changeEvent(QEvent *event) override;
|
void changeEvent(QEvent *event) override;
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,10 @@
|
||||||
#include "single_instance_manager.h"
|
#include "single_instance_manager.h"
|
||||||
#include "version_string.h"
|
#include "version_string.h"
|
||||||
|
|
||||||
|
#ifdef WITH_GUI_TEST
|
||||||
|
#include "pqTestUtility.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
#include <QApplication>
|
#include <QApplication>
|
||||||
#include <QByteArray>
|
#include <QByteArray>
|
||||||
#include <QByteArrayView>
|
#include <QByteArrayView>
|
||||||
|
|
@ -62,10 +66,12 @@
|
||||||
#include <QString>
|
#include <QString>
|
||||||
#include <QStringList>
|
#include <QStringList>
|
||||||
#include <QStringLiteral>
|
#include <QStringLiteral>
|
||||||
|
#include <QThread>
|
||||||
#include <QTranslator>
|
#include <QTranslator>
|
||||||
#include <QUrl>
|
#include <QUrl>
|
||||||
#include <QtGlobal>
|
#include <QtGlobal>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <cstdlib>
|
||||||
#include <libcockatrice/card/database/card_database_manager.h>
|
#include <libcockatrice/card/database/card_database_manager.h>
|
||||||
#include <libcockatrice/rng/rng_sfmt.h>
|
#include <libcockatrice/rng/rng_sfmt.h>
|
||||||
#include <libcockatrice/settings/card_database_settings.h>
|
#include <libcockatrice/settings/card_database_settings.h>
|
||||||
|
|
@ -343,9 +349,16 @@ int main(int argc, char *argv[])
|
||||||
parser.addOptions(
|
parser.addOptions(
|
||||||
{{{"c", "connect"}, QCoreApplication::translate("main", "Connect on startup"), "user:pass@host:port"},
|
{{{"c", "connect"}, QCoreApplication::translate("main", "Connect on startup"), "user:pass@host:port"},
|
||||||
{{"d", "debug-output"}, QCoreApplication::translate("main", "Debug to file")},
|
{{"d", "debug-output"}, QCoreApplication::translate("main", "Debug to file")},
|
||||||
{"simulate-crash",
|
{"simulate-crash", QCoreApplication::translate(
|
||||||
QCoreApplication::translate("main",
|
"main", "Show the crash-report dialog (saves a mock file when no DSN is configured)")},
|
||||||
"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);
|
parser.process(app);
|
||||||
|
|
||||||
|
|
@ -483,6 +496,9 @@ int main(int argc, char *argv[])
|
||||||
if (parser.isSet("connect")) {
|
if (parser.isSet("connect")) {
|
||||||
ui.setConnectTo(parser.value("connect"));
|
ui.setConnectTo(parser.value("connect"));
|
||||||
}
|
}
|
||||||
|
if (parser.isSet("no-connect")) {
|
||||||
|
ui.setNoConnect(true);
|
||||||
|
}
|
||||||
qCInfo(MainLog) << "MainWindow constructor finished";
|
qCInfo(MainLog) << "MainWindow constructor finished";
|
||||||
|
|
||||||
ui.setWindowIcon(themePixmap(QStringLiteral("cockatrice")));
|
ui.setWindowIcon(themePixmap(QStringLiteral("cockatrice")));
|
||||||
|
|
@ -523,6 +539,27 @@ int main(int argc, char *argv[])
|
||||||
handleActivation(file);
|
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
|
// Connect to future file/URL events from other instances
|
||||||
QObject::connect(&instance, &SingleInstanceManager::filesReceived, [&handleActivation](const QStringList &files) {
|
QObject::connect(&instance, &SingleInstanceManager::filesReceived, [&handleActivation](const QStringList &files) {
|
||||||
for (const QString &file : 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"
|
cmakedir="cmake/.*\\.cmake"
|
||||||
cmakeinclude=("cmake/gtest-CMakeLists.txt.in")
|
cmakeinclude=("cmake/gtest-CMakeLists.txt.in")
|
||||||
scripts="*.sh"
|
scripts="*.sh"
|
||||||
|
scriptsrx="[^/]*\\.sh$"
|
||||||
color="--"
|
color="--"
|
||||||
verbosity=0
|
verbosity=0
|
||||||
sep="----------"
|
sep="----------"
|
||||||
|
|
@ -254,7 +255,7 @@ if [[ $branch ]]; then
|
||||||
if [[ $do_shell ]]; then
|
if [[ $do_shell ]]; then
|
||||||
shell_names=()
|
shell_names=()
|
||||||
for name in "${basenames[@]}"; do
|
for name in "${basenames[@]}"; do
|
||||||
filerx="(^|/)$scripts$"
|
filerx="(^|/)$scriptsrx"
|
||||||
if [[ $name =~ $filerx ]]; then
|
if [[ $name =~ $filerx ]]; then
|
||||||
shell_names+=("$name")
|
shell_names+=("$name")
|
||||||
fi
|
fi
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,3 @@
|
||||||
set(CMAKE_AUTOMOC ON)
|
|
||||||
set(CMAKE_AUTOUIC ON)
|
|
||||||
set(CMAKE_AUTORCC ON)
|
|
||||||
|
|
||||||
set(HEADERS
|
set(HEADERS
|
||||||
libcockatrice/card/card_info.h
|
libcockatrice/card/card_info.h
|
||||||
libcockatrice/card/card_info_comparator.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
|
set(HEADERS
|
||||||
libcockatrice/deck_list/tree/abstract_deck_list_card_node.h
|
libcockatrice/deck_list/tree/abstract_deck_list_card_node.h
|
||||||
libcockatrice/deck_list/tree/abstract_deck_list_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
|
set(HEADERS libcockatrice/filters/filter_card.h libcockatrice/filters/filter_string.h
|
||||||
libcockatrice/filters/filter_tree.h
|
libcockatrice/filters/filter_tree.h
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,7 @@ private:
|
||||||
enum Attr a;
|
enum Attr a;
|
||||||
|
|
||||||
public:
|
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
|
set(HEADERS
|
||||||
libcockatrice/game/abstract_game.h
|
libcockatrice/game/abstract_game.h
|
||||||
libcockatrice/game/board/arrow_data.h
|
libcockatrice/game/board/arrow_data.h
|
||||||
|
|
|
||||||
|
|
@ -192,12 +192,14 @@ void ZoneViewZoneLogic::setWriteableRevealZone(bool _writeableRevealZone)
|
||||||
writeableRevealZone = _writeableRevealZone;
|
writeableRevealZone = _writeableRevealZone;
|
||||||
}
|
}
|
||||||
|
|
||||||
PendingCommand *ZoneViewZoneLogic::requestZoneDump()
|
PendingCommand *ZoneViewZoneLogic::prepareZoneDump()
|
||||||
{
|
{
|
||||||
PlayerActions *actions = getPlayer()->getPlayerActions();
|
return getPlayer()->getPlayerActions()->prepareZoneDump(this, numberCards);
|
||||||
PendingCommand *pend = actions->prepareZoneDump(this, numberCards);
|
}
|
||||||
if (pend != nullptr) {
|
|
||||||
actions->sendGameCommand(pend);
|
void ZoneViewZoneLogic::sendZoneDump(PendingCommand *pendingCommand)
|
||||||
}
|
{
|
||||||
return pend;
|
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.
|
* @return The pending command, or nullptr when there is no zone to dump.
|
||||||
*
|
*
|
||||||
* Keeps command construction in game/ so graphics views don't reach
|
* Keeps command construction in game/ so graphics views don't reach
|
||||||
* through the player into PlayerActions to set it up. The command is sent
|
* through the player into PlayerActions to set it up. The response can
|
||||||
* asynchronously, so callers connect to finished() after calling this.
|
* 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:
|
protected:
|
||||||
void addCardImpl(CardState *card, int x, int y) override;
|
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
|
set(HEADERS
|
||||||
libcockatrice/interfaces/interface_cache_storage_settings_provider.h
|
libcockatrice/interfaces/interface_cache_storage_settings_provider.h
|
||||||
libcockatrice/interfaces/interface_card_database_path_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/database)
|
||||||
add_subdirectory(libcockatrice/models/deck_list)
|
add_subdirectory(libcockatrice/models/deck_list)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,7 @@
|
||||||
set(CMAKE_AUTOMOC ON)
|
|
||||||
set(CMAKE_AUTOUIC ON)
|
|
||||||
set(CMAKE_AUTORCC ON)
|
|
||||||
|
|
||||||
set(HEADERS
|
set(HEADERS
|
||||||
card_database_model.h
|
card_database_model.h
|
||||||
card_database_display_model.h
|
card_database_display_model.h
|
||||||
|
card_database_visual_display_model.h
|
||||||
card/card_completer_proxy_model.h
|
card/card_completer_proxy_model.h
|
||||||
card/card_search_model.h
|
card/card_search_model.h
|
||||||
card_set/card_sets_model.h
|
card_set/card_sets_model.h
|
||||||
|
|
@ -17,6 +14,7 @@ add_library(
|
||||||
${HEADERS}
|
${HEADERS}
|
||||||
card_database_model.cpp
|
card_database_model.cpp
|
||||||
card_database_display_model.cpp
|
card_database_display_model.cpp
|
||||||
|
card_database_visual_display_model.cpp
|
||||||
card/card_completer_proxy_model.cpp
|
card/card_completer_proxy_model.cpp
|
||||||
card/card_search_model.cpp
|
card/card_search_model.cpp
|
||||||
card_set/card_sets_model.cpp
|
card_set/card_sets_model.cpp
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@
|
||||||
class QObject;
|
class QObject;
|
||||||
|
|
||||||
CardDatabaseDisplayModel::CardDatabaseDisplayModel(QObject *parent)
|
CardDatabaseDisplayModel::CardDatabaseDisplayModel(QObject *parent)
|
||||||
: QSortFilterProxyModel(parent), isToken(ShowAll), filterString(nullptr),
|
: QSortFilterProxyModel(parent), isToken(ShowAll), filterString(nullptr), cardModel(nullptr),
|
||||||
collator(NaturalSort::makeNaturalSortCollator())
|
collator(NaturalSort::makeNaturalSortCollator())
|
||||||
{
|
{
|
||||||
filterTree = nullptr;
|
filterTree = nullptr;
|
||||||
|
|
@ -33,22 +33,37 @@ CardDatabaseDisplayModel::CardDatabaseDisplayModel(QObject *parent)
|
||||||
invalidate();
|
invalidate();
|
||||||
emit modelDirty();
|
emit modelDirty();
|
||||||
});
|
});
|
||||||
|
|
||||||
loadedRowCount = 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void CardDatabaseDisplayModel::setSourceModel(QAbstractItemModel *model)
|
void CardDatabaseDisplayModel::setSourceModel(QAbstractItemModel *model)
|
||||||
{
|
{
|
||||||
|
cardModel = qobject_cast<CardDatabaseModel *>(model);
|
||||||
QSortFilterProxyModel::setSourceModel(model);
|
QSortFilterProxyModel::setSourceModel(model);
|
||||||
|
|
||||||
connect(model, &QAbstractItemModel::rowsInserted, this, [this]() { dirty(); });
|
connect(model, &QAbstractItemModel::rowsInserted, this, [this]() { dirty(); });
|
||||||
|
|
||||||
connect(model, &QAbstractItemModel::rowsRemoved, this, [this]() { dirty(); });
|
connect(model, &QAbstractItemModel::rowsRemoved, this, [this]() { dirty(); });
|
||||||
|
|
||||||
connect(model, &QAbstractItemModel::modelReset, this, [this]() {
|
connect(model, &QAbstractItemModel::modelReset, this, [this]() { dirty(); });
|
||||||
loadedRowCount = 0;
|
}
|
||||||
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'\"'},
|
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'\''},
|
||||||
{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
|
bool CardDatabaseDisplayModel::lessThan(const QModelIndex &left, const QModelIndex &right) const
|
||||||
{
|
{
|
||||||
const QString leftString = sourceModel()->data(left, CardDatabaseModel::SortRole).toString();
|
const QString leftString = sourceModel()->data(left, CardDatabaseModel::SortRole).toString();
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@
|
||||||
#include <libcockatrice/filters/filter_string.h>
|
#include <libcockatrice/filters/filter_string.h>
|
||||||
#include <qtmetamacros.h>
|
#include <qtmetamacros.h>
|
||||||
|
|
||||||
|
class CardDatabaseModel;
|
||||||
class FilterTree;
|
class FilterTree;
|
||||||
class QObject;
|
class QObject;
|
||||||
template <class Key, class T> class QMap;
|
template <class Key, class T> class QMap;
|
||||||
|
|
@ -36,13 +37,25 @@ public:
|
||||||
ShowAll
|
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:
|
private:
|
||||||
FilterBool isToken;
|
FilterBool isToken;
|
||||||
QString cardName, cardText;
|
QString cardName, cardText;
|
||||||
QSet<QString> cardNameSet, cardTypes, cardColors;
|
QSet<QString> cardNameSet, cardTypes, cardColors;
|
||||||
FilterTree *filterTree;
|
FilterTree *filterTree;
|
||||||
FilterString *filterString;
|
FilterString *filterString;
|
||||||
int loadedRowCount;
|
/// The source model, kept typed so CardInfoRole does not cast per row.
|
||||||
|
CardDatabaseModel *cardModel;
|
||||||
QTimer dirtyTimer;
|
QTimer dirtyTimer;
|
||||||
CardSearchLanguage searchLanguage;
|
CardSearchLanguage searchLanguage;
|
||||||
QString searchText;
|
QString searchText;
|
||||||
|
|
@ -85,9 +98,16 @@ public:
|
||||||
dirtyTimer.start(20);
|
dirtyTimer.start(20);
|
||||||
}
|
}
|
||||||
void clearFilterAll();
|
void clearFilterAll();
|
||||||
[[nodiscard]] int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
[[nodiscard]] QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
||||||
[[nodiscard]] bool canFetchMore(const QModelIndex &parent) const override;
|
|
||||||
void fetchMore(const QModelIndex &parent) 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:
|
signals:
|
||||||
void modelDirty();
|
void modelDirty();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -27,14 +27,18 @@ CardDatabaseModel::CardDatabaseModel(CardDatabase *_db, bool _showOnlyCardsFromE
|
||||||
|
|
||||||
CardDatabaseModel::~CardDatabaseModel() = default;
|
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
|
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)
|
set(HEADERS deck_list_model.h deck_list_sort_filter_proxy_model.h)
|
||||||
|
|
||||||
add_library(
|
add_library(
|
||||||
|
|
|
||||||
|
|
@ -134,6 +134,13 @@ void DeckListModel::rebuildTree()
|
||||||
|
|
||||||
int DeckListModel::rowCount(const QModelIndex &parent) const
|
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);
|
// debugIndexInfo("rowCount", parent);
|
||||||
auto *node = getNode<InnerDecklistNode *>(parent);
|
auto *node = getNode<InnerDecklistNode *>(parent);
|
||||||
if (node) {
|
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
|
QModelIndex DeckListModel::index(int row, int column, const QModelIndex &parent) const
|
||||||
{
|
{
|
||||||
// debugIndexInfo("index", parent);
|
// debugIndexInfo("index", parent);
|
||||||
if (!hasIndex(row, column, parent)) {
|
if ((parent.isValid() && parent.column() != 0) || !hasIndex(row, column, parent)) {
|
||||||
return {};
|
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/client)
|
||||||
add_subdirectory(libcockatrice/network/server)
|
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(abstract)
|
||||||
add_subdirectory(local)
|
add_subdirectory(local)
|
||||||
add_subdirectory(remote)
|
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(HEADERS abstract_client.h latency_tracker.h)
|
||||||
|
|
||||||
set(SOURCES abstract_client.cpp latency_tracker.cpp)
|
set(SOURCES abstract_client.cpp latency_tracker.cpp)
|
||||||
|
|
@ -15,7 +11,7 @@ target_include_directories(libcockatrice_network_client_abstract PUBLIC .)
|
||||||
target_link_libraries(
|
target_link_libraries(
|
||||||
libcockatrice_network_client_abstract
|
libcockatrice_network_client_abstract
|
||||||
PUBLIC ${COCKATRICE_QT_VERSION_NAME}::Network ${COCKATRICE_QT_VERSION_NAME}::WebSockets libcockatrice_protocol
|
PUBLIC ${COCKATRICE_QT_VERSION_NAME}::Network ${COCKATRICE_QT_VERSION_NAME}::WebSockets libcockatrice_protocol
|
||||||
libcockatrice_network_server_remote
|
libcockatrice_network_server_remote libcockatrice_utility
|
||||||
)
|
)
|
||||||
|
|
||||||
target_precompile_headers(libcockatrice_network_client_abstract PRIVATE "${CMAKE_SOURCE_DIR}/cmake/pch/qtcore_pch.h")
|
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/serverinfo_user.pb.h"
|
||||||
#include "libcockatrice/protocol/pb/session_commands.pb.h"
|
#include "libcockatrice/protocol/pb/session_commands.pb.h"
|
||||||
#include "libcockatrice/protocol/pb/session_event.pb.h"
|
#include "libcockatrice/protocol/pb/session_event.pb.h"
|
||||||
|
#include "libcockatrice/utility/passwordhasher.h"
|
||||||
|
|
||||||
#include <QDebug>
|
#include <QDebug>
|
||||||
#include <QList>
|
#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)
|
void AbstractClient::processProtocolItem(const ServerMessage &item)
|
||||||
{
|
{
|
||||||
switch (item.message_type()) {
|
switch (item.message_type()) {
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,7 @@ class Event_UserJoined;
|
||||||
class Event_UserLeft;
|
class Event_UserLeft;
|
||||||
class Event_ServerMessage;
|
class Event_ServerMessage;
|
||||||
class Event_ListRooms;
|
class Event_ListRooms;
|
||||||
|
class Command_AccountPassword;
|
||||||
class Event_GameJoined;
|
class Event_GameJoined;
|
||||||
class Event_UserMessage;
|
class Event_UserMessage;
|
||||||
class Event_NotifyUser;
|
class Event_NotifyUser;
|
||||||
|
|
@ -67,6 +68,9 @@ enum ClientStatus
|
||||||
StatusSubmitForgotPasswordReset,
|
StatusSubmitForgotPasswordReset,
|
||||||
StatusSubmitForgotPasswordChallenge,
|
StatusSubmitForgotPasswordChallenge,
|
||||||
StatusGettingPasswordSalt,
|
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
|
class AbstractClient : public QObject
|
||||||
|
|
@ -171,6 +175,16 @@ public:
|
||||||
{
|
{
|
||||||
return serverSupportsChallengeResponse;
|
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
|
const QString &getUserName() const
|
||||||
{
|
{
|
||||||
return userName;
|
return userName;
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,3 @@
|
||||||
set(CMAKE_AUTOMOC ON)
|
|
||||||
set(CMAKE_AUTOUIC ON)
|
|
||||||
set(CMAKE_AUTORCC ON)
|
|
||||||
|
|
||||||
set(HEADERS local_client.h)
|
set(HEADERS local_client.h)
|
||||||
|
|
||||||
set(SOURCES local_client.cpp)
|
set(SOURCES local_client.cpp)
|
||||||
|
|
|
||||||
|
|
@ -33,16 +33,18 @@ LocalClient::LocalClient(LocalServerInterface *_lsi,
|
||||||
sendCommand(prepareSessionCommand(joinCmd));
|
sendCommand(prepareSessionCommand(joinCmd));
|
||||||
}
|
}
|
||||||
|
|
||||||
LocalClient::~LocalClient()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
void LocalClient::sendCommandContainer(const CommandContainer &cont)
|
void LocalClient::sendCommandContainer(const CommandContainer &cont)
|
||||||
{
|
{
|
||||||
if (LocalClientLog().isDebugEnabled()) {
|
if (LocalClientLog().isDebugEnabled()) {
|
||||||
qCDebug(LocalClientLog).noquote() << userName << "OUT" << getSafeDebugString(cont);
|
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);
|
lsi->itemFromClient(cont);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@
|
||||||
#include "../abstract/abstract_client.h"
|
#include "../abstract/abstract_client.h"
|
||||||
|
|
||||||
#include <QLoggingCategory>
|
#include <QLoggingCategory>
|
||||||
|
#include <QPointer>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
#include <qtmetamacros.h>
|
#include <qtmetamacros.h>
|
||||||
|
|
||||||
|
|
@ -24,14 +25,15 @@ class LocalClient : public AbstractClient
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
private:
|
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:
|
public:
|
||||||
LocalClient(LocalServerInterface *_lsi,
|
LocalClient(LocalServerInterface *_lsi,
|
||||||
const QString &_playerName,
|
const QString &_playerName,
|
||||||
const QString &_clientId,
|
const QString &_clientId,
|
||||||
QObject *parent = nullptr);
|
QObject *parent = nullptr);
|
||||||
~LocalClient() override;
|
|
||||||
|
|
||||||
void sendCommandContainer(const CommandContainer &cont) override;
|
void sendCommandContainer(const CommandContainer &cont) override;
|
||||||
private slots:
|
private slots:
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,3 @@
|
||||||
set(CMAKE_AUTOMOC ON)
|
|
||||||
set(CMAKE_AUTOUIC ON)
|
|
||||||
set(CMAKE_AUTORCC ON)
|
|
||||||
|
|
||||||
set(HEADERS remote_client.h)
|
set(HEADERS remote_client.h)
|
||||||
|
|
||||||
set(SOURCES remote_client.cpp)
|
set(SOURCES remote_client.cpp)
|
||||||
|
|
|
||||||
|
|
@ -125,6 +125,7 @@ RemoteClient::RemoteClient(QObject *parent, INetworkSettingsProvider *_networkSe
|
||||||
&RemoteClient::doSubmitForgotPasswordResetToServer);
|
&RemoteClient::doSubmitForgotPasswordResetToServer);
|
||||||
connect(this, &RemoteClient::sigSubmitForgotPasswordChallengeToServer, this,
|
connect(this, &RemoteClient::sigSubmitForgotPasswordChallengeToServer, this,
|
||||||
&RemoteClient::doSubmitForgotPasswordChallengeToServer);
|
&RemoteClient::doSubmitForgotPasswordChallengeToServer);
|
||||||
|
connect(this, &RemoteClient::sigChangePasswordToServer, this, &RemoteClient::doChangePasswordToServer);
|
||||||
}
|
}
|
||||||
|
|
||||||
RemoteClient::~RemoteClient()
|
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*/)
|
void RemoteClient::processConnectionClosedEvent(const Event_ConnectionClosed & /*event*/)
|
||||||
{
|
{
|
||||||
doDisconnectFromServer();
|
doDisconnectFromServer();
|
||||||
|
|
@ -475,6 +499,23 @@ void RemoteClient::loginResponse(const Response &response)
|
||||||
emit notifyUserAboutUpdate();
|
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) {
|
} else if (response.response_code() != Response::RespNotConnected) {
|
||||||
QList<QString> missingFeatures;
|
QList<QString> missingFeatures;
|
||||||
if (resp.missing_features_size() > 0) {
|
if (resp.missing_features_size() > 0) {
|
||||||
|
|
|
||||||
|
|
@ -71,6 +71,11 @@ signals:
|
||||||
//! should only store it for the connection it is currently negotiating, so
|
//! should only store it for the connection it is currently negotiating, so
|
||||||
//! the hostname and user name are intentionally not part of the signal.
|
//! the hostname and user name are intentionally not part of the signal.
|
||||||
void sigPasswordVerifierReady(const QString &verifier);
|
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:
|
private slots:
|
||||||
void slotConnected();
|
void slotConnected();
|
||||||
void readData();
|
void readData();
|
||||||
|
|
@ -99,6 +104,8 @@ private slots:
|
||||||
Command_Login generateCommandLogin();
|
Command_Login generateCommandLogin();
|
||||||
void doSubmitPasswordVerifier();
|
void doSubmitPasswordVerifier();
|
||||||
void submitPasswordVerifierResponse(const Response &response);
|
void submitPasswordVerifierResponse(const Response &response);
|
||||||
|
void accountPasswordResponse(const Response &response);
|
||||||
|
void doChangePasswordToServer(const QString &oldPassword, const QString &newPassword);
|
||||||
void doDisconnectFromServer();
|
void doDisconnectFromServer();
|
||||||
void doActivateToServer(const QString &_token);
|
void doActivateToServer(const QString &_token);
|
||||||
void doRequestForgotPasswordToServer(const QString &hostname, unsigned int port, const QString &_userName);
|
void doRequestForgotPasswordToServer(const QString &hostname, unsigned int port, const QString &_userName);
|
||||||
|
|
@ -201,6 +208,10 @@ public:
|
||||||
unsigned int port,
|
unsigned int port,
|
||||||
const QString &_userName,
|
const QString &_userName,
|
||||||
const QString &_email);
|
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
|
#endif
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,3 @@
|
||||||
set(CMAKE_AUTOMOC ON)
|
|
||||||
set(CMAKE_AUTOUIC ON)
|
|
||||||
set(CMAKE_AUTORCC ON)
|
|
||||||
|
|
||||||
add_subdirectory(local)
|
add_subdirectory(local)
|
||||||
add_subdirectory(remote)
|
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(HEADERS local_server.h local_server_interface.h)
|
||||||
|
|
||||||
set(SOURCES local_server.cpp local_server_interface.cpp)
|
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
|
set(HEADERS
|
||||||
game/dump_zone_range.h
|
game/dump_zone_range.h
|
||||||
game/server_abstract_participant.h
|
game/server_abstract_participant.h
|
||||||
|
|
@ -12,6 +8,7 @@ set(HEADERS
|
||||||
game/server_cardzone.h
|
game/server_cardzone.h
|
||||||
game/server_counter.h
|
game/server_counter.h
|
||||||
game/game_config.h
|
game/game_config.h
|
||||||
|
game/role_info.h
|
||||||
game/server_deck_validation_strategy.h
|
game/server_deck_validation_strategy.h
|
||||||
game/server_game.h
|
game/server_game.h
|
||||||
game/server_game_lifecycle_strategy.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,
|
Server_AbstractParticipant::Server_AbstractParticipant(Server_Game *_game,
|
||||||
int _playerId,
|
int _playerId,
|
||||||
const ServerInfo_User &_userInfo,
|
const ServerInfo_User &_userInfo,
|
||||||
bool _judge,
|
const RoleInfo &_roleInfo,
|
||||||
Server_AbstractUserInterface *_userInterface)
|
Server_AbstractUserInterface *_userInterface)
|
||||||
: ServerInfo_User_Container(_userInfo), game(_game), userInterface(_userInterface), pingTime(0),
|
: 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) {
|
if (withUserInfo) {
|
||||||
copyUserInfo(*(result.mutable_user_info()), true);
|
copyUserInfo(*(result.mutable_user_info()), true);
|
||||||
}
|
}
|
||||||
result.set_spectator(spectator);
|
result.set_spectator(isSpectator());
|
||||||
result.set_judge(judge);
|
result.set_judge(isJudge());
|
||||||
result.set_ping_seconds(pingTime);
|
result.set_ping_seconds(pingTime);
|
||||||
getPlayerProperties(result);
|
getPlayerProperties(result);
|
||||||
}
|
}
|
||||||
|
|
@ -176,7 +176,7 @@ Response::ResponseCode Server_AbstractParticipant::cmdUnconcede(const Command_Un
|
||||||
Response::ResponseCode
|
Response::ResponseCode
|
||||||
Server_AbstractParticipant::cmdJudge(const Command_Judge &cmd, ResponseContainer &rc, GameEventStorage &ges)
|
Server_AbstractParticipant::cmdJudge(const Command_Judge &cmd, ResponseContainer &rc, GameEventStorage &ges)
|
||||||
{
|
{
|
||||||
if (!judge) {
|
if (!isJudge()) {
|
||||||
return Response::RespFunctionNotAllowed;
|
return Response::RespFunctionNotAllowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -204,7 +204,7 @@ Response::ResponseCode Server_AbstractParticipant::cmdReadyStart(const Command_R
|
||||||
Response::ResponseCode
|
Response::ResponseCode
|
||||||
Server_AbstractParticipant::cmdGameSay(const Command_GameSay &cmd, ResponseContainer & /*rc*/, GameEventStorage &ges)
|
Server_AbstractParticipant::cmdGameSay(const Command_GameSay &cmd, ResponseContainer & /*rc*/, GameEventStorage &ges)
|
||||||
{
|
{
|
||||||
if (spectator) {
|
if (isSpectator()) {
|
||||||
/* Spectators can only talk if:
|
/* Spectators can only talk if:
|
||||||
* (a) the game creator allows it
|
* (a) the game creator allows it
|
||||||
* (b) the spectator is a moderator/administrator
|
* (b) the spectator is a moderator/administrator
|
||||||
|
|
@ -365,7 +365,7 @@ Response::ResponseCode Server_AbstractParticipant::cmdNextTurn(const Command_Nex
|
||||||
return Response::RespGameNotStarted;
|
return Response::RespGameNotStarted;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!judge) {
|
if (!isJudge()) {
|
||||||
return Response::RespFunctionNotAllowed;
|
return Response::RespFunctionNotAllowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -381,7 +381,7 @@ Response::ResponseCode Server_AbstractParticipant::cmdSetActivePhase(const Comma
|
||||||
return Response::RespGameNotStarted;
|
return Response::RespGameNotStarted;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!judge) {
|
if (!isJudge()) {
|
||||||
return Response::RespFunctionNotAllowed;
|
return Response::RespFunctionNotAllowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -415,8 +415,8 @@ Response::ResponseCode Server_AbstractParticipant::cmdReverseTurn(const Command_
|
||||||
ResponseContainer & /*rc*/,
|
ResponseContainer & /*rc*/,
|
||||||
GameEventStorage &ges)
|
GameEventStorage &ges)
|
||||||
{
|
{
|
||||||
if (!judge) {
|
if (!isJudge()) {
|
||||||
if (spectator) {
|
if (isSpectator()) {
|
||||||
return Response::RespFunctionNotAllowed;
|
return Response::RespFunctionNotAllowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -577,7 +577,7 @@ void Server_AbstractParticipant::setUserInterface(Server_AbstractUserInterface *
|
||||||
void Server_AbstractParticipant::disconnectClient()
|
void Server_AbstractParticipant::disconnectClient()
|
||||||
{
|
{
|
||||||
bool isRegistered = userInfo->user_level() & ServerInfo_User::IsRegistered;
|
bool isRegistered = userInfo->user_level() & ServerInfo_User::IsRegistered;
|
||||||
if (!isRegistered || spectator) {
|
if (!isRegistered || isSpectator()) {
|
||||||
game->removeParticipant(this, Event_Leave::USER_DISCONNECTED);
|
game->removeParticipant(this, Event_Leave::USER_DISCONNECTED);
|
||||||
} else {
|
} else {
|
||||||
setUserInterface(nullptr);
|
setUserInterface(nullptr);
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
#define ABSTRACT_PARTICIPANT_H
|
#define ABSTRACT_PARTICIPANT_H
|
||||||
|
|
||||||
#include "../serverinfo_user_container.h"
|
#include "../serverinfo_user_container.h"
|
||||||
|
#include "role_info.h"
|
||||||
#include "server_arrowtarget.h"
|
#include "server_arrowtarget.h"
|
||||||
|
|
||||||
#include <QMutex>
|
#include <QMutex>
|
||||||
|
|
@ -61,8 +62,7 @@ protected:
|
||||||
Server_AbstractUserInterface *userInterface;
|
Server_AbstractUserInterface *userInterface;
|
||||||
int pingTime;
|
int pingTime;
|
||||||
int playerId;
|
int playerId;
|
||||||
bool spectator;
|
RoleInfo roleInfo;
|
||||||
bool judge;
|
|
||||||
virtual void getPlayerProperties(ServerInfo_PlayerProperties &result);
|
virtual void getPlayerProperties(ServerInfo_PlayerProperties &result);
|
||||||
mutable QMutex playerMutex;
|
mutable QMutex playerMutex;
|
||||||
|
|
||||||
|
|
@ -70,7 +70,7 @@ public:
|
||||||
Server_AbstractParticipant(Server_Game *_game,
|
Server_AbstractParticipant(Server_Game *_game,
|
||||||
int _playerId,
|
int _playerId,
|
||||||
const ServerInfo_User &_userInfo,
|
const ServerInfo_User &_userInfo,
|
||||||
bool _judge,
|
const RoleInfo &_roleInfo,
|
||||||
Server_AbstractUserInterface *_handler);
|
Server_AbstractUserInterface *_handler);
|
||||||
~Server_AbstractParticipant() override;
|
~Server_AbstractParticipant() override;
|
||||||
virtual void prepareDestroy()
|
virtual void prepareDestroy()
|
||||||
|
|
@ -91,11 +91,11 @@ public:
|
||||||
}
|
}
|
||||||
bool isSpectator() const
|
bool isSpectator() const
|
||||||
{
|
{
|
||||||
return spectator;
|
return roleInfo.isSpectator;
|
||||||
}
|
}
|
||||||
bool isJudge() const
|
bool isJudge() const
|
||||||
{
|
{
|
||||||
return judge;
|
return roleInfo.isJudge;
|
||||||
}
|
}
|
||||||
Server_Game *getGame() const
|
Server_Game *getGame() const
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -81,10 +81,9 @@ Server_AbstractPlayer::Server_AbstractPlayer(Server_Game *_game,
|
||||||
const ServerInfo_User &_userInfo,
|
const ServerInfo_User &_userInfo,
|
||||||
bool _judge,
|
bool _judge,
|
||||||
Server_AbstractUserInterface *_userInterface)
|
Server_AbstractUserInterface *_userInterface)
|
||||||
: Server_AbstractParticipant(_game, _playerId, _userInfo, _judge, _userInterface), conceded(false), deck(nullptr),
|
: Server_AbstractParticipant(_game, _playerId, _userInfo, {.isJudge = _judge}, _userInterface), conceded(false),
|
||||||
sideboardLocked(true), readyStart(false), nextCardId(0)
|
deck(nullptr), sideboardLocked(true), readyStart(false), nextCardId(0)
|
||||||
{
|
{
|
||||||
spectator = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Server_AbstractPlayer::~Server_AbstractPlayer() = default;
|
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.
|
// Disallow controller change to other zones than the table.
|
||||||
if (((targetzone->getType() != ServerInfo_Zone::PublicZone) || !targetzone->hasCoords()) &&
|
if (((targetzone->getType() != ServerInfo_Zone::PublicZone) || !targetzone->hasCoords()) &&
|
||||||
(startzone->getPlayer() != targetzone->getPlayer()) && !judge) {
|
(startzone->getPlayer() != targetzone->getPlayer()) && !isJudge()) {
|
||||||
return Response::RespContextError;
|
return Response::RespContextError;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -808,7 +807,7 @@ Server_AbstractPlayer::cmdMoveCard(const Command_MoveCard &cmd, ResponseContaine
|
||||||
return Response::RespNameNotFound;
|
return Response::RespNameNotFound;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((startPlayer != this) && (!startZone->getPlayersWithWritePermission().contains(playerId)) && !judge) {
|
if ((startPlayer != this) && (!startZone->getPlayersWithWritePermission().contains(playerId)) && !isJudge()) {
|
||||||
return Response::RespContextError;
|
return Response::RespContextError;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -821,7 +820,7 @@ Server_AbstractPlayer::cmdMoveCard(const Command_MoveCard &cmd, ResponseContaine
|
||||||
return Response::RespNameNotFound;
|
return Response::RespNameNotFound;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((startPlayer != this) && (targetPlayer != this) && !judge) {
|
if ((startPlayer != this) && (targetPlayer != this) && !isJudge()) {
|
||||||
return Response::RespContextError;
|
return Response::RespContextError;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1598,7 +1597,7 @@ Server_AbstractPlayer::cmdRevealCards(const Command_RevealCards &cmd, ResponseCo
|
||||||
zone->addWritePermission(cmd.player_id());
|
zone->addWritePermission(cmd.player_id());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (judge) {
|
if (isJudge()) {
|
||||||
ges.setOverwriteOwnership(true);
|
ges.setOverwriteOwnership(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,7 @@
|
||||||
#include "libcockatrice/protocol/pb/serverinfo_replay_match.pb.h"
|
#include "libcockatrice/protocol/pb/serverinfo_replay_match.pb.h"
|
||||||
#include "libcockatrice/protocol/pb/serverinfo_user.pb.h"
|
#include "libcockatrice/protocol/pb/serverinfo_user.pb.h"
|
||||||
#include "libcockatrice/protocol/pb/session_event.pb.h"
|
#include "libcockatrice/protocol/pb/session_event.pb.h"
|
||||||
|
#include "role_info.h"
|
||||||
#include "server_abstract_player.h"
|
#include "server_abstract_player.h"
|
||||||
#include "server_abstractuserinterface.h"
|
#include "server_abstractuserinterface.h"
|
||||||
#include "server_arrow.h"
|
#include "server_arrow.h"
|
||||||
|
|
@ -479,9 +480,8 @@ void Server_Game::stopGameIfFinished()
|
||||||
|
|
||||||
Response::ResponseCode Server_Game::checkJoin(ServerInfo_User *user,
|
Response::ResponseCode Server_Game::checkJoin(ServerInfo_User *user,
|
||||||
const QString &_password,
|
const QString &_password,
|
||||||
bool spectator,
|
const RoleInfo &roleInfo,
|
||||||
bool overrideRestrictions,
|
bool overrideRestrictions)
|
||||||
bool asJudge)
|
|
||||||
{
|
{
|
||||||
Server_DatabaseInterface *databaseInterface = room->getServer()->getDatabaseInterface();
|
Server_DatabaseInterface *databaseInterface = room->getServer()->getDatabaseInterface();
|
||||||
for (auto *participant : participants.values()) {
|
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;
|
return Response::RespUserLevelTooLow;
|
||||||
}
|
}
|
||||||
if (!(overrideRestrictions && (user->user_level() & (ServerInfo_User::IsModerator | ServerInfo_User::IsJudge)))) {
|
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;
|
return Response::RespWrongPassword;
|
||||||
}
|
}
|
||||||
if (!(user->user_level() & ServerInfo_User::IsRegistered) && onlyRegistered) {
|
if (!(user->user_level() & ServerInfo_User::IsRegistered) && onlyRegistered) {
|
||||||
|
|
@ -510,13 +510,13 @@ Response::ResponseCode Server_Game::checkJoin(ServerInfo_User *user,
|
||||||
QString::fromStdString(user->name()))) {
|
QString::fromStdString(user->name()))) {
|
||||||
return Response::RespInIgnoreList;
|
return Response::RespInIgnoreList;
|
||||||
}
|
}
|
||||||
if (spectator) {
|
if (roleInfo.isSpectator) {
|
||||||
if (!spectatorsAllowed) {
|
if (!spectatorsAllowed) {
|
||||||
return Response::RespSpectatorsNotAllowed;
|
return Response::RespSpectatorsNotAllowed;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!spectator && (gameStarted || (getPlayerCount() >= getMaxPlayers()))) {
|
if (!roleInfo.isSpectator && (gameStarted || (getPlayerCount() >= getMaxPlayers()))) {
|
||||||
return Response::RespGameFull;
|
return Response::RespGameFull;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -537,19 +537,18 @@ bool Server_Game::containsUser(const QString &userName) const
|
||||||
|
|
||||||
void Server_Game::addPlayer(Server_AbstractUserInterface *userInterface,
|
void Server_Game::addPlayer(Server_AbstractUserInterface *userInterface,
|
||||||
ResponseContainer &rc,
|
ResponseContainer &rc,
|
||||||
bool spectator,
|
const RoleInfo &roleInfo,
|
||||||
bool judge,
|
|
||||||
bool broadcastUpdate)
|
bool broadcastUpdate)
|
||||||
{
|
{
|
||||||
QMutexLocker locker(&gameMutex);
|
QMutexLocker locker(&gameMutex);
|
||||||
|
|
||||||
Server_AbstractParticipant *newParticipant;
|
Server_AbstractParticipant *newParticipant;
|
||||||
if (spectator) {
|
if (roleInfo.isSpectator) {
|
||||||
newParticipant = new Server_Spectator(this, nextPlayerId++, userInterface->copyUserInfo(true, true, true),
|
newParticipant = new Server_Spectator(this, nextPlayerId++, userInterface->copyUserInfo(true, true, true),
|
||||||
judge, userInterface);
|
roleInfo.isJudge, userInterface);
|
||||||
} else {
|
} else {
|
||||||
newParticipant = new Server_Player(this, nextPlayerId++, userInterface->copyUserInfo(true, true, true), judge,
|
newParticipant = new Server_Player(this, nextPlayerId++, userInterface->copyUserInfo(true, true, true),
|
||||||
userInterface);
|
roleInfo.isJudge, userInterface);
|
||||||
}
|
}
|
||||||
|
|
||||||
newParticipant->moveToThread(thread());
|
newParticipant->moveToThread(thread());
|
||||||
|
|
@ -560,7 +559,7 @@ void Server_Game::addPlayer(Server_AbstractUserInterface *userInterface,
|
||||||
|
|
||||||
const QString playerName = QString::fromStdString(newParticipant->getUserInfo()->name());
|
const QString playerName = QString::fromStdString(newParticipant->getUserInfo()->name());
|
||||||
participants.insert(newParticipant->getPlayerId(), newParticipant);
|
participants.insert(newParticipant->getPlayerId(), newParticipant);
|
||||||
if (spectator) {
|
if (roleInfo.isSpectator) {
|
||||||
allSpectatorsEver.insert(playerName);
|
allSpectatorsEver.insert(playerName);
|
||||||
} else {
|
} else {
|
||||||
allPlayersEver.insert(playerName);
|
allPlayersEver.insert(playerName);
|
||||||
|
|
@ -579,7 +578,7 @@ void Server_Game::addPlayer(Server_AbstractUserInterface *userInterface,
|
||||||
emit gameInfoChanged(gameInfo);
|
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());
|
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,
|
void Server_Game::sendGameEventContainer(GameEventContainer *cont,
|
||||||
GameEventStorageItem::EventRecipients recipients,
|
GameEventStorageItem::EventRecipients recipients,
|
||||||
int privatePlayerId)
|
int privatePlayerId)
|
||||||
|
|
@ -879,8 +885,7 @@ void Server_Game::sendGameEventContainer(GameEventContainer *cont,
|
||||||
|
|
||||||
cont->set_game_id(gameId);
|
cont->set_game_id(gameId);
|
||||||
for (auto *participant : participants.values()) {
|
for (auto *participant : participants.values()) {
|
||||||
const bool playerPrivate = (participant->getPlayerId() == privatePlayerId) || participant->isJudge() ||
|
const bool playerPrivate = canSeePrivateEventsFor(participant, privatePlayerId);
|
||||||
(participant->isSpectator() && spectatorsSeeEverything);
|
|
||||||
if ((recipients.testFlag(GameEventStorageItem::SendToPrivate) && playerPrivate) ||
|
if ((recipients.testFlag(GameEventStorageItem::SendToPrivate) && playerPrivate) ||
|
||||||
(recipients.testFlag(GameEventStorageItem::SendToOthers) && !playerPrivate)) {
|
(recipients.testFlag(GameEventStorageItem::SendToOthers) && !playerPrivate)) {
|
||||||
participant->sendGameEvent(*cont);
|
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