diff --git a/.ci/Arch/Dockerfile b/.ci/Arch/Dockerfile index ed46e08a9..36cf5c4ae 100644 --- a/.ci/Arch/Dockerfile +++ b/.ci/Arch/Dockerfile @@ -7,6 +7,7 @@ RUN pacman --sync --refresh --sysupgrade --needed --noconfirm \ git \ gtest \ mariadb-libs \ + ninja \ protobuf \ qt6-base \ qt6-imageformats \ diff --git a/.ci/Debian11/Dockerfile b/.ci/Debian11/Dockerfile index 8c4fbe724..b994863bf 100644 --- a/.ci/Debian11/Dockerfile +++ b/.ci/Debian11/Dockerfile @@ -16,6 +16,7 @@ RUN apt-get update && \ libqt5sql5-mysql \ libqt5svg5-dev \ libqt5websockets5-dev \ + ninja-build \ protobuf-compiler \ qt5-image-formats-plugins \ qtmultimedia5-dev \ diff --git a/.ci/Debian12/Dockerfile b/.ci/Debian12/Dockerfile index 21c4ffa29..202405b84 100644 --- a/.ci/Debian12/Dockerfile +++ b/.ci/Debian12/Dockerfile @@ -15,13 +15,14 @@ RUN apt-get update && \ libprotobuf-dev \ libqt6multimedia6 \ libqt6sql6-mysql \ - qt6-svg-dev \ - qt6-websockets-dev \ + ninja-build \ protobuf-compiler \ qt6-image-formats-plugins \ qt6-l10n-tools \ qt6-multimedia-dev \ + qt6-svg-dev \ qt6-tools-dev \ qt6-tools-dev-tools \ + qt6-websockets-dev \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* diff --git a/.ci/Fedora41/Dockerfile b/.ci/Fedora41/Dockerfile index ab23a6052..fc9e86c2e 100644 --- a/.ci/Fedora41/Dockerfile +++ b/.ci/Fedora41/Dockerfile @@ -6,6 +6,7 @@ RUN dnf install -y \ gcc-c++ \ git \ mariadb-devel \ + ninja-build \ protobuf-devel \ qt6-{qttools,qtsvg,qtmultimedia,qtwebsockets}-devel \ qt6-qtimageformats \ diff --git a/.ci/Fedora42/Dockerfile b/.ci/Fedora42/Dockerfile index cf56ff604..ee4a856f2 100644 --- a/.ci/Fedora42/Dockerfile +++ b/.ci/Fedora42/Dockerfile @@ -6,6 +6,7 @@ RUN dnf install -y \ gcc-c++ \ git \ mariadb-devel \ + ninja-build \ protobuf-devel \ qt6-{qttools,qtsvg,qtmultimedia,qtwebsockets}-devel \ qt6-qtimageformats \ diff --git a/.ci/Ubuntu20.04/Dockerfile b/.ci/Ubuntu20.04/Dockerfile deleted file mode 100644 index 45481d44b..000000000 --- a/.ci/Ubuntu20.04/Dockerfile +++ /dev/null @@ -1,26 +0,0 @@ -FROM ubuntu:20.04 - -RUN apt-get update && \ - DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ - build-essential \ - ccache \ - clang-format \ - cmake \ - file \ - g++ \ - git \ - liblzma-dev \ - libmariadb-dev-compat \ - libprotobuf-dev \ - libqt5multimedia5-plugins \ - libqt5sql5-mysql \ - libqt5svg5-dev \ - libqt5websockets5-dev \ - protobuf-compiler \ - qt5-default \ - qt5-image-formats-plugins \ - qtmultimedia5-dev \ - qttools5-dev \ - qttools5-dev-tools \ - && apt-get clean \ - && rm -rf /var/lib/apt/lists/* diff --git a/.ci/Ubuntu22.04/Dockerfile b/.ci/Ubuntu22.04/Dockerfile index 260f449cb..ff2e6e43b 100644 --- a/.ci/Ubuntu22.04/Dockerfile +++ b/.ci/Ubuntu22.04/Dockerfile @@ -17,6 +17,7 @@ RUN apt-get update && \ libqt6sql6-mysql \ libqt6svg6-dev \ libqt6websockets6-dev \ + ninja-build \ protobuf-compiler \ qt6-image-formats-plugins \ qt6-l10n-tools \ diff --git a/.ci/Ubuntu24.04/Dockerfile b/.ci/Ubuntu24.04/Dockerfile index d203690e1..809b2e43a 100644 --- a/.ci/Ubuntu24.04/Dockerfile +++ b/.ci/Ubuntu24.04/Dockerfile @@ -15,13 +15,14 @@ RUN apt-get update && \ libprotobuf-dev \ libqt6multimedia6 \ libqt6sql6-mysql \ - qt6-svg-dev \ - qt6-websockets-dev \ + ninja-build \ protobuf-compiler \ qt6-image-formats-plugins \ qt6-l10n-tools \ qt6-multimedia-dev \ + qt6-svg-dev \ qt6-tools-dev \ qt6-tools-dev-tools \ + qt6-websockets-dev \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* diff --git a/.ci/compile.sh b/.ci/compile.sh index 1588c0d5f..6f51bc476 100755 --- a/.ci/compile.sh +++ b/.ci/compile.sh @@ -11,9 +11,8 @@ # --debug or --release sets the build type ie CMAKE_BUILD_TYPE # --ccache [] uses ccache and shows stats, optionally provide size # --dir sets the name of the build dir, default is "build" -# --parallel sets how many cores cmake should build with in parallel -# uses env: BUILDTYPE MAKE_INSTALL MAKE_PACKAGE PACKAGE_TYPE PACKAGE_SUFFIX MAKE_SERVER MAKE_TEST USE_CCACHE CCACHE_SIZE BUILD_DIR PARALLEL_COUNT -# (correspond to args: --debug/--release --install --package --suffix --server --test --ccache --dir --parallel ) +# uses env: BUILDTYPE MAKE_INSTALL MAKE_PACKAGE PACKAGE_TYPE PACKAGE_SUFFIX MAKE_SERVER MAKE_TEST USE_CCACHE CCACHE_SIZE BUILD_DIR CMAKE_GENERATOR +# (correspond to args: --debug/--release --install --package --suffix --server --test --ccache --dir ) # exitcode: 1 for failure, 3 for invalid arguments # Read arguments @@ -76,15 +75,6 @@ while [[ $# != 0 ]]; do BUILD_DIR="$1" shift ;; - '--parallel') - shift - if [[ $# == 0 ]]; then - echo "::error file=$0::--parallel expects an argument" - exit 3 - fi - PARALLEL_COUNT="$1" - shift - ;; *) echo "::error file=$0::unrecognized option: $1" exit 3 @@ -126,16 +116,6 @@ fi # Add cmake --build flags buildflags=(--config "$BUILDTYPE") -if [[ $PARALLEL_COUNT ]]; then - if [[ $(cmake --build /not_a_dir --parallel 2>&1 | head -1) =~ parallel ]]; then - # workaround for bionic having an old cmake - echo "this version of cmake does not support --parallel, using native build tool -j instead" - buildflags+=(-- -j "$PARALLEL_COUNT") - # note, no normal build flags should be added after this - else - buildflags+=(--parallel "$PARALLEL_COUNT") - fi -fi function ccachestatsverbose() { # note, verbose only works on newer ccache, discard the error diff --git a/.ci/docker.sh b/.ci/docker.sh index c6ec1b774..a9fcfcc5b 100644 --- a/.ci/docker.sh +++ b/.ci/docker.sh @@ -148,6 +148,9 @@ function RUN () args+=(--mount "type=bind,source=$CCACHE_DIR,target=/.ccache") args+=(--env "CCACHE_DIR=/.ccache") fi + if [[ -n "$CMAKE_GENERATOR" ]]; then + args+=(--env "CMAKE_GENERATOR=$CMAKE_GENERATOR") + fi docker run "${args[@]}" $RUN_ARGS "$IMAGE_NAME" bash "$BUILD_SCRIPT" $RUN_OPTS "$@" return $? else diff --git a/.ci/release_template.md b/.ci/release_template.md index a7a2d31cb..d2e6716f8 100644 --- a/.ci/release_template.md +++ b/.ci/release_template.md @@ -20,7 +20,6 @@ Available pre-compiled binaries for installation: LinuxUbuntu 24.04 LTS Noble NumbatUbuntu 22.04 LTS Jammy Jellyfish - • Ubuntu 20.04 LTS Focal FossaDebian 12 BookwormDebian 11 BullseyeFedora 42 diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 4744d611a..7d7490eee 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -9,14 +9,24 @@ assignees: '' --- +sure to post your OS and the file name of the setup binary instead. +--> **System Information:** + +
Debug Log: + + + +
+ _______________________________________________________________________________________ @@ -26,7 +36,7 @@ ________________________________________________________________________________ _______________________________________________________________________________________ - + **Steps to reproduce:** - Do A diff --git a/.github/workflows/desktop-build.yml b/.github/workflows/desktop-build.yml index 99eb2de5d..cec2fa44e 100644 --- a/.github/workflows/desktop-build.yml +++ b/.github/workflows/desktop-build.yml @@ -110,11 +110,6 @@ jobs: version: 42 package: RPM - - distro: Ubuntu - version: 20.04 - package: DEB - test: skip # Ubuntu 20.04 has a broken Qt for debug builds - - distro: Ubuntu version: 22.04 package: DEB @@ -130,29 +125,25 @@ jobs: continue-on-error: ${{matrix.allow-failure == 'yes'}} env: NAME: ${{matrix.distro}}${{matrix.version}} - CACHE: /tmp/${{matrix.distro}}${{matrix.version}}-cache # ${{runner.temp}} does not work? + CACHE: ${{github.workspace}}/.cache/${{matrix.distro}}${{matrix.version}} # directory for caching docker image and ccache # Cache size over the entire repo is 10Gi: # https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#usage-limits-and-eviction-policy CCACHE_SIZE: 500M + CMAKE_GENERATOR: 'Ninja' steps: - name: Checkout uses: actions/checkout@v4 - - name: Generate cache timestamp - id: cache_timestamp - shell: bash - run: echo "timestamp=$(date -u '+%Y%m%d%H%M%S')" >>"$GITHUB_OUTPUT" - - - name: Restore cache - uses: actions/cache@v4 + - name: Restore compiler cache (ccache) + id: ccache_restore + uses: actions/cache/restore@v4 env: - timestamp: ${{steps.cache_timestamp.outputs.timestamp}} + BRANCH_NAME: ${{ github.head_ref || github.ref_name }} with: path: ${{env.CACHE}} - key: docker-${{matrix.distro}}${{matrix.version}}-cache-${{env.timestamp}} - restore-keys: | - docker-${{matrix.distro}}${{matrix.version}}-cache- + key: ccache-${{matrix.distro}}${{matrix.version}}-${{env.BRANCH_NAME}} + restore-keys: ccache-${{matrix.distro}}${{matrix.version}}- - name: Build ${{matrix.distro}} ${{matrix.version}} Docker image shell: bash @@ -161,9 +152,11 @@ jobs: - name: Build debug and test if: matrix.test != 'skip' shell: bash + env: + CMAKE_GENERATOR: '${{env.CMAKE_GENERATOR}}' run: | source .ci/docker.sh - RUN --server --debug --test --ccache "$CCACHE_SIZE" --parallel 4 + RUN --server --debug --test --ccache "$CCACHE_SIZE" - name: Build release package id: build @@ -172,13 +165,21 @@ jobs: env: BUILD_DIR: build SUFFIX: '-${{matrix.distro}}${{matrix.version}}' - type: '${{matrix.package}}' + package: '${{matrix.package}}' + CMAKE_GENERATOR: '${{env.CMAKE_GENERATOR}}' run: | source .ci/docker.sh - RUN --server --release --package "$type" --dir "$BUILD_DIR" \ - --ccache "$CCACHE_SIZE" --parallel 4 + RUN --server --release --package "$package" --dir "$BUILD_DIR" \ + --ccache "$CCACHE_SIZE" .ci/name_build.sh + - name: Save compiler cache (ccache) + if: github.ref == 'refs/heads/master' + uses: actions/cache/save@v4 + with: + path: ${{env.CACHE}} + key: ${{ steps.ccache_restore.outputs.cache-primary-key }} + - name: Upload artifact id: upload_artifact if: matrix.package != 'skip' @@ -223,7 +224,6 @@ jobs: os: macos-13 xcode: "14.3.1" type: Release - core_count: 4 make_package: 1 - target: 14 @@ -231,7 +231,6 @@ jobs: os: macos-14 xcode: "15.4" type: Release - core_count: 3 make_package: 1 - target: 15 @@ -239,7 +238,6 @@ jobs: os: macos-15 xcode: "16.2" type: Release - core_count: 3 make_package: 1 - target: 15 @@ -247,15 +245,17 @@ jobs: os: macos-15 xcode: "16.2" type: Debug - core_count: 3 name: macOS ${{matrix.target}}${{ matrix.soc == 'Intel' && ' Intel' || '' }}${{ matrix.type == 'Debug' && ' Debug' || '' }} needs: configure runs-on: ${{matrix.os}} continue-on-error: ${{matrix.allow-failure == 'yes'}} env: + CCACHE_DIR: ${{github.workspace}}/.ccache/${{matrix.os}}-${{matrix.type}} + CCACHE_SIZE: 500M DEVELOPER_DIR: /Applications/Xcode_${{matrix.xcode}}.app/Contents/Developer + CMAKE_GENERATOR: 'Ninja' steps: - name: Checkout @@ -269,23 +269,30 @@ jobs: HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK: 1 run: | brew update - brew install protobuf qt --force-bottle + brew install ccache protobuf qt --force-bottle + + - name: Restore compiler cache (ccache) + id: ccache_restore + uses: actions/cache/restore@v4 + env: + BRANCH_NAME: ${{ github.head_ref || github.ref_name }} + with: + path: ${{env.CCACHE_DIR}} + key: ccache-${{matrix.os}}-${{matrix.type}}-${{env.BRANCH_NAME}} + restore-keys: ccache-${{matrix.os}}-${{matrix.type}}- - name: Build & Sign on Xcode ${{matrix.xcode}} shell: bash id: build env: BUILDTYPE: '${{matrix.type}}' - MAKE_TEST: 1 MAKE_PACKAGE: '${{matrix.make_package}}' PACKAGE_SUFFIX: '-macOS${{matrix.target}}_${{matrix.soc}}' MACOS_CERTIFICATE: ${{ secrets.PROD_MACOS_CERTIFICATE }} MACOS_CERTIFICATE_PWD: ${{ secrets.PROD_MACOS_CERTIFICATE_PWD }} MACOS_CERTIFICATE_NAME: ${{ secrets.PROD_MACOS_CERTIFICATE_NAME }} MACOS_CI_KEYCHAIN_PWD: ${{ secrets.PROD_MACOS_CI_KEYCHAIN_PWD }} - # macOS runner have 3 cores usually - only the macos-13 image has 4: - # https://docs.github.com/en/actions/using-github-hosted-runners/using-github-hosted-runners/about-github-hosted-runners#standard-github-hosted-runners-for-public-repositories - # https://github.com/actions/runner-images?tab=readme-ov-file#available-images + CMAKE_GENERATOR: '${{env.CMAKE_GENERATOR}}' run: | if [[ -n "$MACOS_CERTIFICATE_NAME" ]] then @@ -297,10 +304,17 @@ jobs: security import certificate.p12 -k build.keychain -P "$MACOS_CERTIFICATE_PWD" -T /usr/bin/codesign security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$MACOS_CI_KEYCHAIN_PWD" build.keychain fi - .ci/compile.sh --server --parallel ${{matrix.core_count}} + .ci/compile.sh --server --test --ccache "$CCACHE_SIZE" + + - name: Save compiler cache (ccache) + if: github.ref == 'refs/heads/master' + uses: actions/cache/save@v4 + with: + path: ${{env.CCACHE_DIR}} + key: ${{ steps.ccache_restore.outputs.cache-primary-key }} - name: Sign app bundle - if: matrix.make_package + if: matrix.make_package && (github.ref == 'refs/heads/master' || needs.configure.outputs.tag != null) env: MACOS_CERTIFICATE_NAME: ${{ secrets.PROD_MACOS_CERTIFICATE_NAME }} MACOS_CI_KEYCHAIN_PWD: ${{ secrets.PROD_MACOS_CI_KEYCHAIN_PWD }} @@ -312,7 +326,7 @@ jobs: fi - name: Notarize app bundle - if: matrix.make_package + if: matrix.make_package && (github.ref == 'refs/heads/master' || needs.configure.outputs.tag != null) env: MACOS_NOTARIZATION_APPLE_ID: ${{ secrets.PROD_MACOS_NOTARIZATION_APPLE_ID }} MACOS_NOTARIZATION_TEAM_ID: ${{ secrets.PROD_MACOS_NOTARIZATION_TEAM_ID }} @@ -354,7 +368,7 @@ jobs: - name: Upload to release id: upload_release - if: matrix.package != 'skip' && needs.configure.outputs.tag != null + if: matrix.make_package && needs.configure.outputs.tag != null shell: bash env: GH_TOKEN: ${{github.token}} @@ -419,16 +433,11 @@ jobs: tools: ${{matrix.qt_tools}} modules: ${{matrix.qt_modules}} - # TODO: re-enable when https://github.com/lukka/run-vcpkg/issues/243 is fixed - - if: false - name: Run vcpkg (disabled) - uses: lukka/run-vcpkg@v11 + - name: Setup vcpkg cache + id: vcpkg-cache + uses: TAServers/vcpkg-cache@v3 with: - runVcpkgInstall: true - doNotCache: false - env: - VCPKG_DEFAULT_TRIPLET: 'x64-windows' - VCPKG_DISABLE_METRICS: 1 + token: ${{ secrets.GITHUB_TOKEN }} - name: Build Cockatrice id: build @@ -438,6 +447,8 @@ jobs: CMAKE_GENERATOR: '${{env.CMAKE_GENERATOR}}' CMAKE_GENERATOR_PLATFORM: 'x64' QTDIR: '${{github.workspace}}\Qt\${{matrix.qt_version}}\win64_${{matrix.qt_arch}}' + VCPKG_DISABLE_METRICS: 1 + VCPKG_BINARY_SOURCES: 'clear;files,${{ steps.vcpkg-cache.outputs.path }},readwrite' # No need for --parallel flag, MTT is added in the compile script to let cmake/msbuild manage core count, # project and process parallelism: https://devblogs.microsoft.com/cppblog/improved-parallelism-in-msbuild/ run: .ci/compile.sh --server --release --test --package @@ -461,7 +472,7 @@ jobs: - name: Upload to release id: upload_release - if: matrix.package != 'skip' && needs.configure.outputs.tag != null + if: needs.configure.outputs.tag != null shell: bash env: GH_TOKEN: ${{github.token}} diff --git a/cockatrice/CMakeLists.txt b/cockatrice/CMakeLists.txt index 50c721a6f..c25b319db 100644 --- a/cockatrice/CMakeLists.txt +++ b/cockatrice/CMakeLists.txt @@ -190,6 +190,7 @@ set(cockatrice_SOURCES src/game/cards/card_search_model.cpp src/game/deckview/deck_view.cpp src/game/deckview/deck_view_container.cpp + src/game/filters/deck_filter_string.cpp src/game/filters/filter_builder.cpp src/game/filters/filter_card.cpp src/game/filters/filter_string.cpp diff --git a/cockatrice/cockatrice.qrc b/cockatrice/cockatrice.qrc index 38c40561f..72ca1d83f 100644 --- a/cockatrice/cockatrice.qrc +++ b/cockatrice/cockatrice.qrc @@ -379,5 +379,6 @@ resources/tips/tips_of_the_day.xml resources/help/search.md + resources/help/deck_search.md diff --git a/cockatrice/resources/config/qtlogging.ini b/cockatrice/resources/config/qtlogging.ini index eeae47b7f..2cd3d871a 100644 --- a/cockatrice/resources/config/qtlogging.ini +++ b/cockatrice/resources/config/qtlogging.ini @@ -60,4 +60,6 @@ # pixel_map_generator = false +# deck_filter_string = false # filter_string = false +# syntax_help = false diff --git a/cockatrice/resources/help/deck_search.md b/cockatrice/resources/help/deck_search.md new file mode 100644 index 000000000..cb06a40a6 --- /dev/null +++ b/cockatrice/resources/help/deck_search.md @@ -0,0 +1,26 @@ +## Deck Search Syntax Help +----- +The search bar recognizes a set of special commands.
+In this list of examples below, each entry has an explanation and can be clicked to test the query. Note that all +searches are case insensitive. +
+
Filename:
+
[red deck wins](#red deck wins) (Any deck filename containing the words red, deck, and wins)
+
["red deck wins"](#%22red deck wins%22) (Any deck filename containing the exact phrase "red deck wins")
+ +
Deck Contents (Uses [card search expressions](#cardSearchSyntaxHelp)):
+
[[plains]] (Any deck that contains at least one card with "plains" in its name)
+
[[t:legendary]] (Any deck that contains at least one legendary)
+
[[t:legendary]]>5 (Any card that contains at least 5 legendaries)
+
[[]]:100 (Any deck that contains exactly 100 cards)
+ +
Negate:
+
[soldier -aggro](#soldier -aggro) (Any deck filename that contains "soldier", but not "aggro")
+ +
Branching:
+
[t:aggro OR o:control](#t:aggro OR o:control) (Any deck filename that contains either aggro or control)
+ +
Grouping:
+
red -([[]]:100 or aggro) (Any deck that has red in its filename but is not 100 cards or has aggro in its filename)
+ +
diff --git a/cockatrice/src/client/tabs/tab_game.cpp b/cockatrice/src/client/tabs/tab_game.cpp index 999b5b164..40c25067c 100644 --- a/cockatrice/src/client/tabs/tab_game.cpp +++ b/cockatrice/src/client/tabs/tab_game.cpp @@ -1538,7 +1538,7 @@ void TabGame::createPlayAreaWidget(bool bReplay) scene = new GameScene(phasesToolbar, this); gameView = new GameView(scene); - gamePlayAreaVBox = new QVBoxLayout; + auto gamePlayAreaVBox = new QVBoxLayout; gamePlayAreaVBox->setContentsMargins(0, 0, 0, 0); gamePlayAreaVBox->addWidget(gameView); @@ -1594,12 +1594,12 @@ void TabGame::createReplayDock() connect(replayFastForwardButton, &QToolButton::toggled, this, &TabGame::replayFastForwardButtonToggled); // putting everything together - replayControlLayout = new QHBoxLayout; + auto replayControlLayout = new QHBoxLayout; replayControlLayout->addWidget(timelineWidget, 10); replayControlLayout->addWidget(replayPlayButton); replayControlLayout->addWidget(replayFastForwardButton); - replayControlWidget = new QWidget(); + auto replayControlWidget = new QWidget(); replayControlWidget->setObjectName("replayControlWidget"); replayControlWidget->setLayout(replayControlLayout); @@ -1635,13 +1635,13 @@ void TabGame::createCardInfoDock(bool bReplay) Q_UNUSED(bReplay); cardInfoFrameWidget = new CardInfoFrameWidget(); - cardHInfoLayout = new QHBoxLayout; - cardVInfoLayout = new QVBoxLayout; + auto cardHInfoLayout = new QHBoxLayout; + auto cardVInfoLayout = new QVBoxLayout; cardVInfoLayout->setContentsMargins(0, 0, 0, 0); cardVInfoLayout->addWidget(cardInfoFrameWidget); cardVInfoLayout->addLayout(cardHInfoLayout); - cardBoxLayoutWidget = new QWidget; + auto cardBoxLayoutWidget = new QWidget; cardBoxLayoutWidget->setLayout(cardVInfoLayout); cardInfoDock = new QDockWidget(this); @@ -1679,6 +1679,22 @@ void TabGame::createPlayerListDock(bool bReplay) void TabGame::createMessageDock(bool bReplay) { + auto messageLogLayout = new QVBoxLayout; + messageLogLayout->setContentsMargins(0, 0, 0, 0); + + // clock + if (!bReplay) { + timeElapsedLabel = new QLabel; + timeElapsedLabel->setAlignment(Qt::AlignCenter); + gameTimer = new QTimer(this); + gameTimer->setInterval(1000); + connect(gameTimer, &QTimer::timeout, this, &TabGame::incrementGameTime); + gameTimer->start(); + + messageLogLayout->addWidget(timeElapsedLabel); + } + + // message log messageLog = new MessageLogWidget(tabSupervisor, this); connect(messageLog, &MessageLogWidget::cardNameHovered, cardInfoFrameWidget, qOverload(&CardInfoFrameWidget::setCard)); @@ -1690,14 +1706,12 @@ void TabGame::createMessageDock(bool bReplay) connect(messageLog, &MessageLogWidget::addMentionTag, this, &TabGame::addMentionTag); connect(&SettingsCache::instance(), &SettingsCache::chatMentionCompleterChanged, this, &TabGame::actCompleterChanged); + } - timeElapsedLabel = new QLabel; - timeElapsedLabel->setAlignment(Qt::AlignCenter); - gameTimer = new QTimer(this); - gameTimer->setInterval(1000); - connect(gameTimer, &QTimer::timeout, this, &TabGame::incrementGameTime); - gameTimer->start(); + messageLogLayout->addWidget(messageLog); + // chat entry + if (!bReplay) { sayLabel = new QLabel; sayEdit = new LineEditCompleter; sayEdit->setMaxLength(MAX_TEXT_LENGTH); @@ -1726,20 +1740,15 @@ void TabGame::createMessageDock(bool bReplay) connect(tabSupervisor, &TabSupervisor::adminLockChanged, this, &TabGame::adminLockChanged); connect(sayEdit, &LineEditCompleter::returnPressed, this, &TabGame::actSay); - sayHLayout = new QHBoxLayout; + auto sayHLayout = new QHBoxLayout; sayHLayout->addWidget(sayLabel); sayHLayout->addWidget(sayEdit); + + messageLogLayout->addLayout(sayHLayout); } - messageLogLayout = new QVBoxLayout; - messageLogLayout->setContentsMargins(0, 0, 0, 0); - if (!bReplay) - messageLogLayout->addWidget(timeElapsedLabel); - messageLogLayout->addWidget(messageLog); - if (!bReplay) - messageLogLayout->addLayout(sayHLayout); - - messageLogLayoutWidget = new QWidget; + // dock + auto messageLogLayoutWidget = new QWidget; messageLogLayoutWidget->setLayout(messageLogLayout); messageLayoutDock = new QDockWidget(this); diff --git a/cockatrice/src/client/tabs/tab_game.h b/cockatrice/src/client/tabs/tab_game.h index 096998728..370634742 100644 --- a/cockatrice/src/client/tabs/tab_game.h +++ b/cockatrice/src/client/tabs/tab_game.h @@ -111,10 +111,8 @@ private: GameScene *scene; GameView *gameView; QMap deckViewContainers; - QVBoxLayout *cardVInfoLayout, *messageLogLayout, *gamePlayAreaVBox, *deckViewContainerLayout; - QHBoxLayout *cardHInfoLayout, *sayHLayout, *mainHLayout, *replayControlLayout; - QWidget *cardBoxLayoutWidget, *messageLogLayoutWidget, *gamePlayAreaWidget, *deckViewContainerWidget, - *replayControlWidget; + QVBoxLayout *deckViewContainerLayout; + QWidget *gamePlayAreaWidget, *deckViewContainerWidget; QDockWidget *cardInfoDock, *messageLayoutDock, *playerListDock, *replayDock; QAction *playersSeparator; QMenu *gameMenu, *viewMenu, *cardInfoDockMenu, *messageLayoutDockMenu, *playerListDockMenu, *replayDockMenu; diff --git a/cockatrice/src/client/ui/widgets/deck_editor/deck_editor_deck_dock_widget.cpp b/cockatrice/src/client/ui/widgets/deck_editor/deck_editor_deck_dock_widget.cpp index 3683666a2..0cbeb2b50 100644 --- a/cockatrice/src/client/ui/widgets/deck_editor/deck_editor_deck_dock_widget.cpp +++ b/cockatrice/src/client/ui/widgets/deck_editor/deck_editor_deck_dock_widget.cpp @@ -104,6 +104,20 @@ void DeckEditorDeckDockWidget::createDeckDock() deckTagsDisplayWidget = new DeckPreviewDeckTagsDisplayWidget(this, deckModel->getDeckList()); deckTagsDisplayWidget->setHidden(!SettingsCache::instance().getDeckEditorTagsWidgetVisible()); + activeGroupCriteriaLabel = new QLabel(this); + + activeGroupCriteriaComboBox = new QComboBox(this); + activeGroupCriteriaComboBox->addItem(tr("Main Type"), DeckListModelGroupCriteria::MAIN_TYPE); + activeGroupCriteriaComboBox->addItem(tr("Mana Cost"), DeckListModelGroupCriteria::MANA_COST); + activeGroupCriteriaComboBox->addItem(tr("Colors"), DeckListModelGroupCriteria::COLOR); + connect(activeGroupCriteriaComboBox, QOverload::of(&QComboBox::currentIndexChanged), [this]() { + deckModel->setActiveGroupCriteria( + static_cast(activeGroupCriteriaComboBox->currentData(Qt::UserRole).toInt())); + deckModel->sort(deckView->header()->sortIndicatorSection(), deckView->header()->sortIndicatorOrder()); + deckView->expandAll(); + deckView->expandAll(); + }); + aIncrement = new QAction(QString(), this); aIncrement->setIcon(QPixmap("theme:icons/increment")); connect(aIncrement, &QAction::triggered, this, &DeckEditorDeckDockWidget::actIncrement); @@ -144,6 +158,9 @@ void DeckEditorDeckDockWidget::createDeckDock() upperLayout->addWidget(deckTagsDisplayWidget, 3, 1); + upperLayout->addWidget(activeGroupCriteriaLabel, 4, 0); + upperLayout->addWidget(activeGroupCriteriaComboBox, 4, 1); + hashLabel1 = new QLabel(); hashLabel1->setObjectName("hashLabel1"); auto *hashSizePolicy = new QSizePolicy(); @@ -286,11 +303,7 @@ void DeckEditorDeckDockWidget::updateBannerCardComboBox() }); for (const auto &pair : pairList) { - QVariantMap dataMap; - dataMap["name"] = pair.first; - dataMap["uuid"] = pair.second; - - bannerCardComboBox->addItem(pair.first, dataMap); + bannerCardComboBox->addItem(pair.first, QVariant::fromValue(pair)); } // Try to restore the previous selection by finding the currentText @@ -298,7 +311,7 @@ void DeckEditorDeckDockWidget::updateBannerCardComboBox() if (restoredIndex != -1) { bannerCardComboBox->setCurrentIndex(restoredIndex); if (deckModel->getDeckList()->getBannerCard().second != - bannerCardComboBox->itemData(bannerCardComboBox->currentIndex()).toMap()["uuid"].toString()) { + bannerCardComboBox->currentData().value>().second) { setBannerCard(restoredIndex); } } else { @@ -318,9 +331,8 @@ void DeckEditorDeckDockWidget::updateBannerCardComboBox() void DeckEditorDeckDockWidget::setBannerCard(int /* changedIndex */) { - QVariantMap itemData = bannerCardComboBox->itemData(bannerCardComboBox->currentIndex()).toMap(); - deckModel->getDeckList()->setBannerCard( - QPair(itemData["name"].toString(), itemData["uuid"].toString())); + auto cardAndId = bannerCardComboBox->currentData().value>(); + deckModel->getDeckList()->setBannerCard(cardAndId); emit deckModified(); } @@ -578,6 +590,7 @@ void DeckEditorDeckDockWidget::retranslateUi() showBannerCardCheckBox->setText(tr("Show banner card selection menu")); showTagsWidgetCheckBox->setText(tr("Show tags selection menu")); commentsLabel->setText(tr("&Comments:")); + activeGroupCriteriaLabel->setText(tr("Group by:")); hashLabel1->setText(tr("Hash:")); aIncrement->setText(tr("&Increment number")); diff --git a/cockatrice/src/client/ui/widgets/deck_editor/deck_editor_deck_dock_widget.h b/cockatrice/src/client/ui/widgets/deck_editor/deck_editor_deck_dock_widget.h index c81b4a6fa..27364a662 100644 --- a/cockatrice/src/client/ui/widgets/deck_editor/deck_editor_deck_dock_widget.h +++ b/cockatrice/src/client/ui/widgets/deck_editor/deck_editor_deck_dock_widget.h @@ -70,6 +70,8 @@ private: DeckPreviewDeckTagsDisplayWidget *deckTagsDisplayWidget; QLabel *hashLabel1; LineEditUnfocusable *hashLabel; + QLabel *activeGroupCriteriaLabel; + QComboBox *activeGroupCriteriaComboBox; QAction *aRemoveCard, *aIncrement, *aDecrement, *aSwapCard; diff --git a/cockatrice/src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_deck_tags_display_widget.cpp b/cockatrice/src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_deck_tags_display_widget.cpp index 8c4723c37..d446589d0 100644 --- a/cockatrice/src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_deck_tags_display_widget.cpp +++ b/cockatrice/src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_deck_tags_display_widget.cpp @@ -12,6 +12,7 @@ #include #include #include +#include DeckPreviewDeckTagsDisplayWidget::DeckPreviewDeckTagsDisplayWidget(QWidget *_parent, DeckList *_deckList) : QWidget(_parent), deckList(nullptr) @@ -77,6 +78,21 @@ static QStringList getAllFiles(const QString &filePath) return allFiles; } +bool confirmOverwriteIfExists(QWidget *parent, const QString &filePath) +{ + QFileInfo fileInfo(filePath); + QString newFileName = QDir::toNativeSeparators(fileInfo.path() + "/" + fileInfo.completeBaseName() + ".cod"); + + if (QFile::exists(newFileName)) { + QMessageBox::StandardButton reply = + QMessageBox::question(parent, QObject::tr("Overwrite Existing File?"), + QObject::tr("A .cod version of this deck already exists. Overwrite it?"), + QMessageBox::Yes | QMessageBox::No); + return reply == QMessageBox::Yes; + } + return true; // Safe to proceed +} + void DeckPreviewDeckTagsDisplayWidget::openTagEditDlg() { if (qobject_cast(parentWidget())) { @@ -91,6 +107,10 @@ void DeckPreviewDeckTagsDisplayWidget::openTagEditDlg() // Retrieve saved preference if the prompt is disabled if (!SettingsCache::instance().getVisualDeckStoragePromptForConversion()) { if (SettingsCache::instance().getVisualDeckStorageAlwaysConvert()) { + + if (!confirmOverwriteIfExists(this, deckPreviewWidget->filePath)) + return; + deckPreviewWidget->deckLoader->convertToCockatriceFormat(deckPreviewWidget->filePath); deckPreviewWidget->filePath = deckPreviewWidget->deckLoader->getLastFileName(); deckPreviewWidget->refreshBannerCardText(); @@ -100,6 +120,10 @@ void DeckPreviewDeckTagsDisplayWidget::openTagEditDlg() // Show the dialog to the user DialogConvertDeckToCodFormat conversionDialog(parentWidget()); if (conversionDialog.exec() == QDialog::Accepted) { + + if (!confirmOverwriteIfExists(this, deckPreviewWidget->filePath)) + return; + deckPreviewWidget->deckLoader->convertToCockatriceFormat(deckPreviewWidget->filePath); deckPreviewWidget->filePath = deckPreviewWidget->deckLoader->getLastFileName(); deckPreviewWidget->refreshBannerCardText(); diff --git a/cockatrice/src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_tag_display_widget.cpp b/cockatrice/src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_tag_display_widget.cpp index 4cfd9447d..db466b77a 100644 --- a/cockatrice/src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_tag_display_widget.cpp +++ b/cockatrice/src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_tag_display_widget.cpp @@ -50,10 +50,10 @@ void DeckPreviewTagDisplayWidget::mousePressEvent(QMouseEvent *event) { switch (event->button()) { case Qt::LeftButton: - setState(TagState::Selected); + setState(state != TagState::Selected ? TagState::Selected : TagState::NotSelected); break; case Qt::RightButton: - setState(TagState::Excluded); + setState(state != TagState::Excluded ? TagState::Excluded : TagState::NotSelected); break; case Qt::MiddleButton: setState(TagState::NotSelected); diff --git a/cockatrice/src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_widget.cpp b/cockatrice/src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_widget.cpp index 556968d2b..7fc5c729f 100644 --- a/cockatrice/src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_widget.cpp +++ b/cockatrice/src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_widget.cpp @@ -46,6 +46,8 @@ DeckPreviewWidget::DeckPreviewWidget(QWidget *_parent, &DeckPreviewWidget::updateTagsVisibility); connect(&SettingsCache::instance(), &SettingsCache::visualDeckStorageShowBannerCardComboBoxChanged, this, &DeckPreviewWidget::updateBannerCardComboBoxVisibility); + connect(visualDeckStorageWidget->settings(), &VisualDeckStorageQuickSettingsWidget::deckPreviewTooltipChanged, this, + &DeckPreviewWidget::refreshBannerCardToolTip); layout->addWidget(bannerCardDisplayWidget); } @@ -79,7 +81,6 @@ void DeckPreviewWidget::initializeUi(const bool deckLoadSuccess) bannerCardDisplayWidget->setCard(bannerCard); bannerCardDisplayWidget->setFontSize(24); - refreshBannerCardText(); setFilePath(deckLoader->getLastFileName()); colorIdentityWidget = new ColorIdentityWidget(this, getColorIdentity()); @@ -104,6 +105,8 @@ void DeckPreviewWidget::initializeUi(const bool deckLoadSuccess) layout->addWidget(bannerCardLabel); layout->addWidget(bannerCardComboBox); + refreshBannerCardText(); + retranslateUi(); } @@ -184,10 +187,29 @@ void DeckPreviewWidget::setFilePath(const QString &_filePath) filePath = _filePath; } +/** + * Refreshes the banner card text. + * This also calls `refreshBannerCardToolTip`, since those two often need to be updated together. + */ void DeckPreviewWidget::refreshBannerCardText() { bannerCardDisplayWidget->setOverlayText( deckLoader->getName().isEmpty() ? QFileInfo(deckLoader->getLastFileName()).fileName() : deckLoader->getName()); + + refreshBannerCardToolTip(); +} + +void DeckPreviewWidget::refreshBannerCardToolTip() +{ + auto type = visualDeckStorageWidget->settings()->getDeckPreviewTooltip(); + switch (type) { + case VisualDeckStorageQuickSettingsWidget::TooltipType::None: + bannerCardDisplayWidget->setToolTip(""); + break; + case VisualDeckStorageQuickSettingsWidget::TooltipType::Filepath: + bannerCardDisplayWidget->setToolTip(filePath); + break; + } } void DeckPreviewWidget::updateBannerCardComboBox() @@ -260,11 +282,11 @@ void DeckPreviewWidget::updateBannerCardComboBox() void DeckPreviewWidget::setBannerCard(int /* changedIndex */) { - QVariant itemData = bannerCardComboBox->itemData(bannerCardComboBox->currentIndex()); - deckLoader->setBannerCard(QPair(bannerCardComboBox->currentText(), itemData.toString())); + auto nameAndId = bannerCardComboBox->currentData().value>(); + deckLoader->setBannerCard(nameAndId); deckLoader->saveToFile(filePath, DeckLoader::getFormatFromName(filePath)); - bannerCardDisplayWidget->setCard(CardDatabaseManager::getInstance()->getCardByNameAndProviderId( - bannerCardComboBox->currentText(), itemData.toString())); + bannerCardDisplayWidget->setCard( + CardDatabaseManager::getInstance()->getCardByNameAndProviderId(nameAndId.first, nameAndId.second)); } void DeckPreviewWidget::imageClickedEvent(QMouseEvent *event, DeckPreviewCardPictureWidget *instance) diff --git a/cockatrice/src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_widget.h b/cockatrice/src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_widget.h index 8b594a9db..7c0ff1ad4 100644 --- a/cockatrice/src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_widget.h +++ b/cockatrice/src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_widget.h @@ -49,6 +49,7 @@ signals: public slots: void setFilePath(const QString &filePath); void refreshBannerCardText(); + void refreshBannerCardToolTip(); void updateBannerCardComboBox(); void setBannerCard(int); void imageClickedEvent(QMouseEvent *event, DeckPreviewCardPictureWidget *instance); diff --git a/cockatrice/src/client/ui/widgets/visual_deck_storage/visual_deck_storage_quick_settings_widget.cpp b/cockatrice/src/client/ui/widgets/visual_deck_storage/visual_deck_storage_quick_settings_widget.cpp index 3b9063a9b..cd085a511 100644 --- a/cockatrice/src/client/ui/widgets/visual_deck_storage/visual_deck_storage_quick_settings_widget.cpp +++ b/cockatrice/src/client/ui/widgets/visual_deck_storage/visual_deck_storage_quick_settings_widget.cpp @@ -4,6 +4,7 @@ #include "visual_deck_storage_widget.h" #include +#include #include VisualDeckStorageQuickSettingsWidget::VisualDeckStorageQuickSettingsWidget(QWidget *parent) @@ -80,6 +81,26 @@ VisualDeckStorageQuickSettingsWidget::VisualDeckStorageQuickSettingsWidget(QWidg unusedColorIdentityOpacityLayout->addWidget(unusedColorIdentitiesOpacityLabel); unusedColorIdentityOpacityLayout->addWidget(unusedColorIdentitiesOpacitySpinBox); + // tooltip selector + auto deckPreviewTooltipWidget = new QWidget(this); + + deckPreviewTooltipLabel = new QLabel(deckPreviewTooltipWidget); + deckPreviewTooltipComboBox = new QComboBox(deckPreviewTooltipWidget); + deckPreviewTooltipComboBox->setFocusPolicy(Qt::StrongFocus); + deckPreviewTooltipComboBox->addItem("", TooltipType::None); + deckPreviewTooltipComboBox->addItem("", TooltipType::Filepath); + + deckPreviewTooltipComboBox->setCurrentIndex(SettingsCache::instance().getVisualDeckStorageTooltipType()); + connect(deckPreviewTooltipComboBox, QOverload::of(&QComboBox::currentIndexChanged), this, + [this] { emit deckPreviewTooltipChanged(getDeckPreviewTooltip()); }); + connect(deckPreviewTooltipComboBox, QOverload::of(&QComboBox::currentIndexChanged), &SettingsCache::instance(), + &SettingsCache::setVisualDeckStorageTooltipType); + + auto deckPreviewTooltipLayout = new QHBoxLayout(deckPreviewTooltipWidget); + deckPreviewTooltipLayout->setContentsMargins(11, 0, 11, 0); + deckPreviewTooltipLayout->addWidget(deckPreviewTooltipLabel); + deckPreviewTooltipLayout->addWidget(deckPreviewTooltipComboBox); + // card size slider cardSizeWidget = new CardSizeWidget(this, nullptr, SettingsCache::instance().getVisualDeckStorageCardSize()); connect(cardSizeWidget->getSlider(), &QSlider::valueChanged, this, @@ -95,6 +116,7 @@ VisualDeckStorageQuickSettingsWidget::VisualDeckStorageQuickSettingsWidget(QWidg this->addSettingsWidget(searchFolderNamesCheckBox); this->addSettingsWidget(drawUnusedColorIdentitiesCheckBox); this->addSettingsWidget(unusedColorIdentityOpacityWidget); + this->addSettingsWidget(deckPreviewTooltipWidget); this->addSettingsWidget(cardSizeWidget); connect(&SettingsCache::instance(), &SettingsCache::langChanged, this, @@ -112,6 +134,10 @@ void VisualDeckStorageQuickSettingsWidget::retranslateUi() drawUnusedColorIdentitiesCheckBox->setText(tr("Draw unused Color Identities")); unusedColorIdentitiesOpacityLabel->setText(tr("Unused Color Identities Opacity")); unusedColorIdentitiesOpacitySpinBox->setSuffix("%"); + + deckPreviewTooltipLabel->setText(tr("Deck tooltip:")); + deckPreviewTooltipComboBox->setItemText(0, tr("None")); + deckPreviewTooltipComboBox->setItemText(1, tr("Filepath")); } bool VisualDeckStorageQuickSettingsWidget::getShowFolders() const @@ -149,6 +175,11 @@ int VisualDeckStorageQuickSettingsWidget::getUnusedColorIdentitiesOpacity() cons return unusedColorIdentitiesOpacitySpinBox->value(); } +VisualDeckStorageQuickSettingsWidget::TooltipType VisualDeckStorageQuickSettingsWidget::getDeckPreviewTooltip() const +{ + return deckPreviewTooltipComboBox->currentData().value(); +} + int VisualDeckStorageQuickSettingsWidget::getCardSize() const { return cardSizeWidget->getSlider()->value(); diff --git a/cockatrice/src/client/ui/widgets/visual_deck_storage/visual_deck_storage_quick_settings_widget.h b/cockatrice/src/client/ui/widgets/visual_deck_storage/visual_deck_storage_quick_settings_widget.h index da82b177b..d27a9053b 100644 --- a/cockatrice/src/client/ui/widgets/visual_deck_storage/visual_deck_storage_quick_settings_widget.h +++ b/cockatrice/src/client/ui/widgets/visual_deck_storage/visual_deck_storage_quick_settings_widget.h @@ -7,6 +7,7 @@ class CardSizeWidget; class QLabel; class QSpinBox; class QCheckBox; +class QComboBox; /** * The VDS's quick settings menu. @@ -25,9 +26,21 @@ class VisualDeckStorageQuickSettingsWidget : public SettingsButtonWidget QCheckBox *searchFolderNamesCheckBox; QLabel *unusedColorIdentitiesOpacityLabel; QSpinBox *unusedColorIdentitiesOpacitySpinBox; + QLabel *deckPreviewTooltipLabel; + QComboBox *deckPreviewTooltipComboBox; CardSizeWidget *cardSizeWidget; public: + /** + * The info to display in the deck preview's banner card tooltip. + */ + enum TooltipType + { + None, + Filepath + }; + Q_ENUM(TooltipType) + explicit VisualDeckStorageQuickSettingsWidget(QWidget *parent = nullptr); void retranslateUi(); @@ -39,6 +52,7 @@ public: bool getShowTagsOnDeckPreviews() const; bool getSearchFolderNames() const; int getUnusedColorIdentitiesOpacity() const; + TooltipType getDeckPreviewTooltip() const; int getCardSize() const; signals: @@ -49,6 +63,7 @@ signals: void showTagsOnDeckPreviewsChanged(bool enabled); void searchFolderNamesChanged(bool enabled); void unusedColorIdentitiesOpacityChanged(int opacity); + void deckPreviewTooltipChanged(TooltipType tooltip); void cardSizeChanged(int scale); }; diff --git a/cockatrice/src/client/ui/widgets/visual_deck_storage/visual_deck_storage_search_widget.cpp b/cockatrice/src/client/ui/widgets/visual_deck_storage/visual_deck_storage_search_widget.cpp index 9b7f65224..2998845f1 100644 --- a/cockatrice/src/client/ui/widgets/visual_deck_storage/visual_deck_storage_search_widget.cpp +++ b/cockatrice/src/client/ui/widgets/visual_deck_storage/visual_deck_storage_search_widget.cpp @@ -1,6 +1,11 @@ #include "visual_deck_storage_search_widget.h" +#include "../../../../game/filters/deck_filter_string.h" +#include "../../../../game/filters/syntax_help.h" #include "../../../../settings/cache_settings.h" +#include "../../pixel_map_generator.h" + +#include /** * @brief Constructs a PrintingSelectorCardSearchWidget for searching cards by set name or set code. @@ -17,7 +22,13 @@ VisualDeckStorageSearchWidget::VisualDeckStorageSearchWidget(VisualDeckStorageWi setLayout(layout); searchBar = new QLineEdit(this); - searchBar->setPlaceholderText(tr("Search by filename")); + searchBar->setPlaceholderText(tr("Search by filename (or search expression)")); + searchBar->setClearButtonEnabled(true); + searchBar->addAction(loadColorAdjustedPixmap("theme:icons/search"), QLineEdit::LeadingPosition); + + auto help = searchBar->addAction(QPixmap("theme:icons/info"), QLineEdit::TrailingPosition); + connect(help, &QAction::triggered, this, [this] { createDeckSearchSyntaxHelpWindow(searchBar); }); + layout->addWidget(searchBar); // Add a debounce timer for the search bar to limit frequent updates @@ -52,11 +63,11 @@ static QString getFileSearchName(const QString &filePath, bool includeFolderName { QString deckPath = SettingsCache::instance().getDeckPath(); if (includeFolderName && filePath.startsWith(deckPath)) { - return filePath.mid(deckPath.length()).toLower(); + return filePath.mid(deckPath.length()); } QFileInfo fileInfo(filePath); - QString fileName = fileInfo.fileName().toLower(); + QString fileName = fileInfo.fileName(); return fileName; } @@ -64,14 +75,10 @@ void VisualDeckStorageSearchWidget::filterWidgets(QList wid const QString &searchText, bool includeFolderName) { - if (searchText.isEmpty() || searchText.isNull()) { - for (auto widget : widgets) { - widget->filteredBySearch = false; - } - } + auto filterString = DeckFilterString(searchText); - for (auto file : widgets) { - QString fileSearchName = getFileSearchName(file->filePath, includeFolderName); - file->filteredBySearch = !fileSearchName.contains(searchText.toLower()); + for (auto widget : widgets) { + QString fileSearchName = getFileSearchName(widget->filePath, includeFolderName); + widget->filteredBySearch = !filterString.check(widget, {fileSearchName}); } } diff --git a/cockatrice/src/client/ui/window_main.cpp b/cockatrice/src/client/ui/window_main.cpp index 7dabc96c6..721881c89 100644 --- a/cockatrice/src/client/ui/window_main.cpp +++ b/cockatrice/src/client/ui/window_main.cpp @@ -1099,7 +1099,7 @@ void MainWindow::cardDatabaseLoadingFailed() void MainWindow::cardDatabaseNewSetsFound(int numUnknownSets, QStringList unknownSetsNames) { - QMessageBox msgBox; + QMessageBox msgBox(this); msgBox.setWindowTitle(tr("New sets found")); msgBox.setIcon(QMessageBox::Question); msgBox.setText(tr("%n new set(s) found in the card database\n" diff --git a/cockatrice/src/deck/deck_list_model.cpp b/cockatrice/src/deck/deck_list_model.cpp index 5d8cb60f3..746a1d9e9 100644 --- a/cockatrice/src/deck/deck_list_model.cpp +++ b/cockatrice/src/deck/deck_list_model.cpp @@ -29,6 +29,24 @@ DeckListModel::~DeckListModel() delete root; } +QString DeckListModel::getSortCriteriaForCard(CardInfoPtr info) +{ + if (!info) { + return "unknown"; + } + + switch (activeGroupCriteria) { + case DeckListModelGroupCriteria::MAIN_TYPE: + return info->getMainCardType(); + case DeckListModelGroupCriteria::MANA_COST: + return info->getCmc(); + case DeckListModelGroupCriteria::COLOR: + return info->getColors() == "" ? "Colorless" : info->getColors(); + default: + return "unknown"; + } +} + void DeckListModel::rebuildTree() { beginResetModel(); @@ -49,7 +67,7 @@ void DeckListModel::rebuildTree() } CardInfoPtr info = CardDatabaseManager::getInstance()->getCard(currentCard->getName()); - QString cardType = info ? info->getMainCardType() : "unknown"; + QString cardType = getSortCriteriaForCard(info); auto *cardTypeNode = dynamic_cast(node->findChild(cardType)); @@ -467,6 +485,12 @@ void DeckListModel::sort(int column, Qt::SortOrder order) emit layoutChanged(); } +void DeckListModel::setActiveGroupCriteria(DeckListModelGroupCriteria newCriteria) +{ + activeGroupCriteria = newCriteria; + rebuildTree(); +} + void DeckListModel::cleanList() { setDeckList(new DeckLoader); diff --git a/cockatrice/src/deck/deck_list_model.h b/cockatrice/src/deck/deck_list_model.h index 94b323e18..81ae06508 100644 --- a/cockatrice/src/deck/deck_list_model.h +++ b/cockatrice/src/deck/deck_list_model.h @@ -12,6 +12,13 @@ class CardDatabase; class QPrinter; class QTextCursor; +enum DeckListModelGroupCriteria +{ + MAIN_TYPE, + MANA_COST, + COLOR +}; + class DecklistModelCardNode : public AbstractDecklistCardNode { private: @@ -85,6 +92,7 @@ signals: public: explicit DeckListModel(QObject *parent = nullptr); ~DeckListModel() override; + QString getSortCriteriaForCard(CardInfoPtr info); int rowCount(const QModelIndex &parent) const override; int columnCount(const QModelIndex & /*parent*/ = QModelIndex()) const override; QVariant data(const QModelIndex &index, int role) const override; @@ -113,10 +121,12 @@ public: QList getCardsAsCardInfoPtrs() const; QList getCardsAsCardInfoPtrsForZone(QString zoneName) const; QList *getZones() const; + void setActiveGroupCriteria(DeckListModelGroupCriteria newCriteria); private: DeckLoader *deckList; InnerDecklistNode *root; + DeckListModelGroupCriteria activeGroupCriteria = DeckListModelGroupCriteria::MAIN_TYPE; int lastKnownColumn; Qt::SortOrder lastKnownOrder; InnerDecklistNode *createNodeIfNeeded(const QString &name, InnerDecklistNode *parent); diff --git a/cockatrice/src/game/deckview/deck_view_container.cpp b/cockatrice/src/game/deckview/deck_view_container.cpp index 81ad8d322..4afd07943 100644 --- a/cockatrice/src/game/deckview/deck_view_container.cpp +++ b/cockatrice/src/game/deckview/deck_view_container.cpp @@ -4,6 +4,7 @@ #include "../../client/ui/picture_loader/picture_loader.h" #include "../../deck/deck_loader.h" #include "../../dialogs/dlg_load_deck.h" +#include "../../dialogs/dlg_load_deck_from_clipboard.h" #include "../../dialogs/dlg_load_remote_deck.h" #include "../../server/pending_command.h" #include "../../settings/cache_settings.h" @@ -52,6 +53,7 @@ DeckViewContainer::DeckViewContainer(int _playerId, TabGame *parent) { loadLocalButton = new QPushButton; loadRemoteButton = new QPushButton; + loadFromClipboardButton = new QPushButton; unloadDeckButton = new QPushButton; readyStartButton = new ToggleButton; forceStartGameButton = new QPushButton; @@ -59,6 +61,7 @@ DeckViewContainer::DeckViewContainer(int _playerId, TabGame *parent) connect(loadLocalButton, &QPushButton::clicked, this, &DeckViewContainer::loadLocalDeck); connect(loadRemoteButton, &QPushButton::clicked, this, &DeckViewContainer::loadRemoteDeck); + connect(loadFromClipboardButton, &QPushButton::clicked, this, &DeckViewContainer::loadFromClipboard); connect(readyStartButton, &QPushButton::clicked, this, &DeckViewContainer::readyStart); connect(unloadDeckButton, &QPushButton::clicked, this, &DeckViewContainer::unloadDeck); connect(forceStartGameButton, &QPushButton::clicked, this, &DeckViewContainer::forceStart); @@ -68,6 +71,7 @@ DeckViewContainer::DeckViewContainer(int _playerId, TabGame *parent) auto *buttonHBox = new QHBoxLayout; buttonHBox->addWidget(loadLocalButton); buttonHBox->addWidget(loadRemoteButton); + buttonHBox->addWidget(loadFromClipboardButton); buttonHBox->addWidget(unloadDeckButton); buttonHBox->addWidget(readyStartButton); buttonHBox->addWidget(sideboardLockButton); @@ -118,6 +122,7 @@ void DeckViewContainer::retranslateUi() { loadLocalButton->setText(tr("Load deck...")); loadRemoteButton->setText(tr("Load remote deck...")); + loadFromClipboardButton->setText(tr("Load from clipboard...")); unloadDeckButton->setText(tr("Unload deck")); readyStartButton->setText(tr("Ready to start")); forceStartGameButton->setText(tr("Force start")); @@ -148,6 +153,7 @@ void DeckViewContainer::switchToDeckSelectView() setVisibility(loadLocalButton, true); setVisibility(loadRemoteButton, !parentGame->getIsLocalGame()); + setVisibility(loadFromClipboardButton, true); setVisibility(unloadDeckButton, false); setVisibility(readyStartButton, false); setVisibility(sideboardLockButton, false); @@ -172,6 +178,7 @@ void DeckViewContainer::switchToDeckLoadedView() setVisibility(loadLocalButton, false); setVisibility(loadRemoteButton, false); + setVisibility(loadFromClipboardButton, false); setVisibility(unloadDeckButton, true); setVisibility(readyStartButton, true); setVisibility(sideboardLockButton, true); @@ -198,8 +205,11 @@ void DeckViewContainer::refreshShortcuts() ShortcutsSettings &shortcuts = SettingsCache::instance().shortcuts(); loadLocalButton->setShortcut(shortcuts.getSingleShortcut("DeckViewContainer/loadLocalButton")); loadRemoteButton->setShortcut(shortcuts.getSingleShortcut("DeckViewContainer/loadRemoteButton")); + loadFromClipboardButton->setShortcut(shortcuts.getSingleShortcut("DeckViewContainer/loadFromClipboardButton")); + unloadDeckButton->setShortcut(shortcuts.getSingleShortcut("DeckViewContainer/unloadDeckButton")); readyStartButton->setShortcut(shortcuts.getSingleShortcut("DeckViewContainer/readyStartButton")); sideboardLockButton->setShortcut(shortcuts.getSingleShortcut("DeckViewContainer/sideboardLockButton")); + forceStartGameButton->setShortcut(shortcuts.getSingleShortcut("DeckViewContainer/forceStartGameButton")); } /** @@ -247,19 +257,27 @@ void DeckViewContainer::loadLocalDeck() void DeckViewContainer::loadDeckFromFile(const QString &filePath) { DeckLoader::FileFormat fmt = DeckLoader::getFormatFromName(filePath); - QString deckString; DeckLoader deck; - bool error = !deck.loadFromFile(filePath, fmt, true); - if (!error) { - deckString = deck.writeToString_Native(); - error = deckString.length() > MAX_FILE_LENGTH; - } - if (error) { + bool success = deck.loadFromFile(filePath, fmt, true); + + if (!success) { QMessageBox::critical(this, tr("Error"), tr("The selected file could not be loaded.")); return; } + loadDeckFromDeckLoader(&deck); +} + +void DeckViewContainer::loadDeckFromDeckLoader(const DeckLoader *deck) +{ + QString deckString = deck->writeToString_Native(); + + if (deckString.length() > MAX_FILE_LENGTH) { + QMessageBox::critical(this, tr("Error"), tr("Deck is greater than maximum file size.")); + return; + } + Command_DeckSelect cmd; cmd.set_deck(deckString.toStdString()); PendingCommand *pend = parentGame->prepareGameCommand(cmd); @@ -279,6 +297,18 @@ void DeckViewContainer::loadRemoteDeck() } } +void DeckViewContainer::loadFromClipboard() +{ + auto dlg = DlgLoadDeckFromClipboard(this); + + if (!dlg.exec()) { + return; + } + + DeckLoader *deck = dlg.getDeckList(); + loadDeckFromDeckLoader(deck); +} + void DeckViewContainer::deckSelectFinished(const Response &r) { const Response_DeckDownload &resp = r.GetExtension(Response_DeckDownload::ext); diff --git a/cockatrice/src/game/deckview/deck_view_container.h b/cockatrice/src/game/deckview/deck_view_container.h index c841c5934..d25a43d0d 100644 --- a/cockatrice/src/game/deckview/deck_view_container.h +++ b/cockatrice/src/game/deckview/deck_view_container.h @@ -44,7 +44,8 @@ class DeckViewContainer : public QWidget Q_OBJECT private: QVBoxLayout *deckViewLayout; - QPushButton *loadLocalButton, *loadRemoteButton, *unloadDeckButton, *forceStartGameButton; + QPushButton *loadLocalButton, *loadRemoteButton, *loadFromClipboardButton; + QPushButton *unloadDeckButton, *forceStartGameButton; ToggleButton *readyStartButton, *sideboardLockButton; DeckView *deckView; VisualDeckStorageWidget *visualDeckStorageWidget; @@ -58,6 +59,7 @@ private slots: void switchToDeckLoadedView(); void loadLocalDeck(); void loadRemoteDeck(); + void loadFromClipboard(); void unloadDeck(); void readyStart(); void forceStart(); @@ -81,6 +83,7 @@ public: public slots: void loadDeckFromFile(const QString &filePath); + void loadDeckFromDeckLoader(const DeckLoader *deck); }; #endif // DECK_VIEW_CONTAINER_H diff --git a/cockatrice/src/game/filters/deck_filter_string.cpp b/cockatrice/src/game/filters/deck_filter_string.cpp new file mode 100644 index 000000000..52aa15c71 --- /dev/null +++ b/cockatrice/src/game/filters/deck_filter_string.cpp @@ -0,0 +1,167 @@ +#include "deck_filter_string.h" + +#include "../cards/card_database_manager.h" +#include "filter_string.h" +#include "lib/peglib.h" + +static peg::parser search(R"( +Start <- QueryPartList +~ws <- [ ]+ +QueryPartList <- ComplexQueryPart ( ws ("AND" ws)? ComplexQueryPart)* ws* + +ComplexQueryPart <- SomewhatComplexQueryPart ws "OR" ws ComplexQueryPart / SomewhatComplexQueryPart +SomewhatComplexQueryPart <- [(] QueryPartList [)] / QueryPart + +QueryPart <- NotQuery / DeckContentQuery / GenericQuery + +NotQuery <- ('NOT' ws/'-') SomewhatComplexQueryPart + +DeckContentQuery <- CardSearch NumericExpression? +CardSearch <- '[[' CardFilterString ']]' +CardFilterString <- (!']]'.)* + +GenericQuery <- String + +NonDoubleQuoteUnlessEscaped <- '\\\"'. / !["]. +NonSingleQuoteUnlessEscaped <- "\\\'". / ![']. +UnescapedStringListPart <- !['":<>=! ]. +SingleApostropheString <- (UnescapedStringListPart+ ws*)* ['] (UnescapedStringListPart+ ws*)* + +String <- SingleApostropheString / UnescapedStringListPart+ / ["] ["] / ['] ['] + +NumericExpression <- NumericOperator ws? NumericValue +NumericOperator <- [=:] / <[> +NumericValue <- [0-9]+ +)"); + +static std::once_flag init; + +static void setupParserRules() +{ + // plumbing + auto passthru = [](const peg::SemanticValues &sv) -> DeckFilter { + return !sv.empty() ? std::any_cast(sv[0]) : nullptr; + }; + + search["Start"] = passthru; + search["QueryPartList"] = [](const peg::SemanticValues &sv) -> DeckFilter { + return [=](const DeckPreviewWidget *deck, const ExtraDeckSearchInfo &info) { + auto matchesFilter = [&deck, &info](const std::any &query) { + return std::any_cast(query)(deck, info); + }; + return std::all_of(sv.begin(), sv.end(), matchesFilter); + }; + }; + search["ComplexQueryPart"] = [](const peg::SemanticValues &sv) -> DeckFilter { + return [=](const DeckPreviewWidget *deck, const ExtraDeckSearchInfo &info) { + auto matchesFilter = [&deck, &info](const std::any &query) { + return std::any_cast(query)(deck, info); + }; + return std::any_of(sv.begin(), sv.end(), matchesFilter); + }; + }; + search["SomewhatComplexQueryPart"] = passthru; + search["QueryPart"] = passthru; + search["NotQuery"] = [](const peg::SemanticValues &sv) -> DeckFilter { + const auto dependent = std::any_cast(sv[0]); + return [=](const DeckPreviewWidget *deck, const ExtraDeckSearchInfo &info) -> bool { + return !dependent(deck, info); + }; + }; + + search["String"] = [](const peg::SemanticValues &sv) -> QString { + if (sv.choice() == 0) { + return QString::fromStdString(std::string(sv.sv())); + } + + return QString::fromStdString(std::string(sv.token(0))); + }; + + search["NumericExpression"] = [](const peg::SemanticValues &sv) -> NumberMatcher { + const auto arg = std::any_cast(sv[1]); + const auto op = std::any_cast(sv[0]); + + if (op == ">") + return [=](const int s) { return s > arg; }; + if (op == ">=") + return [=](const int s) { return s >= arg; }; + if (op == "<") + return [=](const int s) { return s < arg; }; + if (op == "<=") + return [=](const int s) { return s <= arg; }; + if (op == "=") + return [=](const int s) { return s == arg; }; + if (op == ":") + return [=](const int s) { return s == arg; }; + if (op == "!=") + return [=](const int s) { return s != arg; }; + return [](int) { return false; }; + }; + + search["NumericValue"] = [](const peg::SemanticValues &sv) -> int { + return QString::fromStdString(std::string(sv.sv())).toInt(); + }; + + search["NumericOperator"] = [](const peg::SemanticValues &sv) -> QString { + return QString::fromStdString(std::string(sv.sv())); + }; + + // actual functionality + search["DeckContentQuery"] = [](const peg::SemanticValues &sv) -> DeckFilter { + auto cardFilter = FilterString(std::any_cast(sv[0])); + auto numberMatcher = sv.size() > 1 ? std::any_cast(sv[1]) : [](int count) { return count > 0; }; + + return [=](const DeckPreviewWidget *deck, const ExtraDeckSearchInfo &) -> bool { + int count = 0; + deck->deckLoader->forEachCard([&](InnerDecklistNode *, const DecklistCardNode *node) { + auto cardInfoPtr = CardDatabaseManager::getInstance()->getCard(node->getName()); + if (!cardInfoPtr.isNull() && cardFilter.check(cardInfoPtr)) { + count += node->getNumber(); + } + }); + return numberMatcher(count); + }; + }; + + search["CardSearch"] = [](const peg::SemanticValues &sv) -> QString { return std::any_cast(sv[0]); }; + + search["CardFilterString"] = [](const peg::SemanticValues &sv) -> QString { + return QString::fromStdString(std::string(sv.sv())); + }; + + search["GenericQuery"] = [](const peg::SemanticValues &sv) -> DeckFilter { + auto name = std::any_cast(sv[0]); + return [=](const DeckPreviewWidget *, const ExtraDeckSearchInfo &info) { + return info.fileSearchName.contains(name, Qt::CaseInsensitive); + }; + }; +} + +DeckFilterString::DeckFilterString() +{ + filter = [](const DeckPreviewWidget *, const ExtraDeckSearchInfo &) { return false; }; + _error = "Not initialized"; +} + +DeckFilterString::DeckFilterString(const QString &expr) +{ + QByteArray ba = expr.simplified().toUtf8(); + + std::call_once(init, setupParserRules); + + _error = QString(); + + if (ba.isEmpty()) { + filter = [](const DeckPreviewWidget *, const ExtraDeckSearchInfo &) { return true; }; + return; + } + + search.set_logger([&](size_t /*ln*/, size_t col, const std::string &msg) { + _error = QString("Error at position %1: %2").arg(col).arg(QString::fromStdString(msg)); + }); + + if (!search.parse(ba.data(), filter)) { + qCInfo(DeckFilterStringLog).nospace() << "DeckFilterString error for " << expr << "; " << qPrintable(_error); + filter = [](const DeckPreviewWidget *, const ExtraDeckSearchInfo &) { return false; }; + } +} \ No newline at end of file diff --git a/cockatrice/src/game/filters/deck_filter_string.h b/cockatrice/src/game/filters/deck_filter_string.h new file mode 100644 index 000000000..dfdb1f1a8 --- /dev/null +++ b/cockatrice/src/game/filters/deck_filter_string.h @@ -0,0 +1,51 @@ +#ifndef DECK_FILTER_STRING_H +#define DECK_FILTER_STRING_H + +#include "../../client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_widget.h" + +#include +#include +#include +#include +#include + +inline Q_LOGGING_CATEGORY(DeckFilterStringLog, "deck_filter_string"); + +/** + * Extra info relevant to filtering that isn't present in the DeckPreviewWidget + */ +struct ExtraDeckSearchInfo +{ + /** + * The filename used for filtering. Varies based on settings. + */ + QString fileSearchName; +}; + +typedef std::function DeckFilter; + +class DeckFilterString +{ +public: + DeckFilterString(); + explicit DeckFilterString(const QString &expr); + bool check(const DeckPreviewWidget *deck, const ExtraDeckSearchInfo &info) const + { + return filter(deck, info); + } + + bool valid() const + { + return _error.isEmpty(); + } + + QString error() + { + return _error; + } + +private: + QString _error; + DeckFilter filter; +}; +#endif // DECK_FILTER_STRING_H diff --git a/cockatrice/src/game/filters/filter_string.cpp b/cockatrice/src/game/filters/filter_string.cpp index 01a7b6be9..e99805541 100644 --- a/cockatrice/src/game/filters/filter_string.cpp +++ b/cockatrice/src/game/filters/filter_string.cpp @@ -7,7 +7,7 @@ #include #include -peg::parser search(R"( +static peg::parser search(R"( Start <- QueryPartList ~ws <- [ ]+ QueryPartList <- ComplexQueryPart ( ws ("AND" ws)? ComplexQueryPart)* ws* @@ -63,7 +63,7 @@ NumericOperator <- [=:] / <[> NumericValue <- [0-9]+ )"); -std::once_flag init; +static std::once_flag init; static void setupParserRules() { diff --git a/cockatrice/src/game/filters/syntax_help.cpp b/cockatrice/src/game/filters/syntax_help.cpp index a4eaf9112..9fb918629 100644 --- a/cockatrice/src/game/filters/syntax_help.cpp +++ b/cockatrice/src/game/filters/syntax_help.cpp @@ -9,11 +9,12 @@ * * @return the QTextBrowser */ -static QTextBrowser *createBrowser() +static QTextBrowser *createBrowser(const QString &helpFile) { - QFile file("theme:help/search.md"); + QFile file(helpFile); if (!file.open(QFile::ReadOnly | QFile::Text)) { + qCWarning(SyntaxHelpLog) << "Could not open syntax help file: " << helpFile; return nullptr; } @@ -54,9 +55,29 @@ static QTextBrowser *createBrowser() */ QTextBrowser *createSearchSyntaxHelpWindow(QLineEdit *lineEdit) { - auto browser = createBrowser(); + auto browser = createBrowser("theme:help/search.md"); QObject::connect(browser, &QTextBrowser::anchorClicked, [lineEdit](const QUrl &link) { lineEdit->setText(link.fragment()); }); QObject::connect(lineEdit, &QObject::destroyed, browser, &QTextBrowser::close); return browser; +} + +/** + * Creates the deck search syntax help window and connects its anchorClicked signal to the given QLineEdit. + * The window will automatically close when the QLineEdit is destroyed. + * + * @return the QTextBrowser + */ +QTextBrowser *createDeckSearchSyntaxHelpWindow(QLineEdit *lineEdit) +{ + auto browser = createBrowser("theme:help/deck_search.md"); + QObject::connect(browser, &QTextBrowser::anchorClicked, [lineEdit](const QUrl &link) { + if (link.fragment() == "cardSearchSyntaxHelp") { + createSearchSyntaxHelpWindow(lineEdit); + } else { + lineEdit->setText(link.fragment()); + } + }); + QObject::connect(lineEdit, &QObject::destroyed, browser, &QTextBrowser::close); + return browser; } \ No newline at end of file diff --git a/cockatrice/src/game/filters/syntax_help.h b/cockatrice/src/game/filters/syntax_help.h index 4016e02bb..911c2fdf0 100644 --- a/cockatrice/src/game/filters/syntax_help.h +++ b/cockatrice/src/game/filters/syntax_help.h @@ -2,8 +2,13 @@ #define SEARCH_SYNTAX_HELP_H #include +#include #include +inline Q_LOGGING_CATEGORY(SyntaxHelpLog, "syntax_help"); + QTextBrowser *createSearchSyntaxHelpWindow(QLineEdit *lineEdit); +QTextBrowser *createDeckSearchSyntaxHelpWindow(QLineEdit *lineEdit); + #endif // SEARCH_SYNTAX_HELP_H diff --git a/cockatrice/src/settings/cache_settings.cpp b/cockatrice/src/settings/cache_settings.cpp index ef369641b..acd8cd581 100644 --- a/cockatrice/src/settings/cache_settings.cpp +++ b/cockatrice/src/settings/cache_settings.cpp @@ -280,6 +280,7 @@ SettingsCache::SettingsCache() settings->value("interface/visualdeckstoragedrawunusedcoloridentities", true).toBool(); visualDeckStorageUnusedColorIdentitiesOpacity = settings->value("interface/visualdeckstorageunusedcoloridentitiesopacity", 15).toInt(); + visualDeckStorageTooltipType = settings->value("interface/visualdeckstoragetooltiptype", 0).toInt(); visualDeckStoragePromptForConversion = settings->value("interface/visualdeckstoragepromptforconversion", true).toBool(); visualDeckStorageAlwaysConvert = settings->value("interface/visualdeckstoragealwaysconvert", false).toBool(); @@ -774,6 +775,12 @@ void SettingsCache::setVisualDeckStorageUnusedColorIdentitiesOpacity(int _visual emit visualDeckStorageUnusedColorIdentitiesOpacityChanged(visualDeckStorageUnusedColorIdentitiesOpacity); } +void SettingsCache::setVisualDeckStorageTooltipType(int value) +{ + visualDeckStorageTooltipType = value; + settings->setValue("interface/visualdeckstoragetooltiptype", visualDeckStorageTooltipType); +} + void SettingsCache::setVisualDeckStoragePromptForConversion(bool _visualDeckStoragePromptForConversion) { visualDeckStoragePromptForConversion = _visualDeckStoragePromptForConversion; diff --git a/cockatrice/src/settings/cache_settings.h b/cockatrice/src/settings/cache_settings.h index 94cb2ed6a..f0861af58 100644 --- a/cockatrice/src/settings/cache_settings.h +++ b/cockatrice/src/settings/cache_settings.h @@ -150,6 +150,7 @@ private: int visualDeckStorageCardSize; bool visualDeckStorageDrawUnusedColorIdentities; int visualDeckStorageUnusedColorIdentitiesOpacity; + int visualDeckStorageTooltipType; bool visualDeckStoragePromptForConversion; bool visualDeckStorageAlwaysConvert; bool visualDeckStorageInGame; @@ -476,6 +477,10 @@ public: { return visualDeckStorageUnusedColorIdentitiesOpacity; } + int getVisualDeckStorageTooltipType() const + { + return visualDeckStorageTooltipType; + } bool getVisualDeckStoragePromptForConversion() const { return visualDeckStoragePromptForConversion; @@ -852,6 +857,7 @@ public slots: void setVisualDeckStorageCardSize(int _visualDeckStorageCardSize); void setVisualDeckStorageDrawUnusedColorIdentities(QT_STATE_CHANGED_T _visualDeckStorageDrawUnusedColorIdentities); void setVisualDeckStorageUnusedColorIdentitiesOpacity(int _visualDeckStorageUnusedColorIdentitiesOpacity); + void setVisualDeckStorageTooltipType(int value); void setVisualDeckStoragePromptForConversion(bool _visualDeckStoragePromptForConversion); void setVisualDeckStorageAlwaysConvert(bool _visualDeckStorageAlwaysConvert); void setVisualDeckStorageInGame(QT_STATE_CHANGED_T value); diff --git a/cockatrice/src/settings/shortcuts_settings.h b/cockatrice/src/settings/shortcuts_settings.h index 595932623..596781cff 100644 --- a/cockatrice/src/settings/shortcuts_settings.h +++ b/cockatrice/src/settings/shortcuts_settings.h @@ -267,6 +267,13 @@ private: {"DeckViewContainer/loadRemoteButton", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Load Remote Deck..."), parseSequenceString("Ctrl+Alt+O"), ShortcutGroup::Game_Lobby)}, + {"DeckViewContainer/loadFromClipboardButton", + ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Load Deck from Clipboard..."), + parseSequenceString("Ctrl+Shift+V"), + ShortcutGroup::Game_Lobby)}, + {"DeckViewContainer/unloadDeckButton", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Unload Deck"), + parseSequenceString("Ctrl+Alt+U"), + ShortcutGroup::Game_Lobby)}, {"DeckViewContainer/readyStartButton", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Set Ready to Start"), parseSequenceString("Ctrl+Shift+S"), ShortcutGroup::Game_Lobby)}, @@ -274,6 +281,9 @@ private: ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Toggle Sideboard Lock"), parseSequenceString("Ctrl+Shift+B"), ShortcutGroup::Game_Lobby)}, + {"DeckViewContainer/forceStartGameButton", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Force Start"), + parseSequenceString(""), + ShortcutGroup::Game_Lobby)}, {"Player/aCCGreen", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Add Green Counter"), parseSequenceString("Ctrl+>"), ShortcutGroup::Card_Counters)}, diff --git a/common/decklist.cpp b/common/decklist.cpp index 6f27d2895..a7da934f4 100644 --- a/common/decklist.cpp +++ b/common/decklist.cpp @@ -441,7 +441,7 @@ bool DeckList::readElement(QXmlStreamReader *xml) return true; } -void DeckList::write(QXmlStreamWriter *xml) +void DeckList::write(QXmlStreamWriter *xml) const { xml->writeStartElement("cockatrice_deck"); xml->writeAttribute("version", "1"); @@ -508,7 +508,7 @@ bool DeckList::loadFromString_Native(const QString &nativeString) return loadFromXml(&xml); } -QString DeckList::writeToString_Native() +QString DeckList::writeToString_Native() const { QString result; QXmlStreamWriter xml(&result); diff --git a/common/decklist.h b/common/decklist.h index 989c3e7fe..a2d58de47 100644 --- a/common/decklist.h +++ b/common/decklist.h @@ -345,10 +345,10 @@ public: } bool readElement(QXmlStreamReader *xml); - void write(QXmlStreamWriter *xml); + void write(QXmlStreamWriter *xml) const; bool loadFromXml(QXmlStreamReader *xml); bool loadFromString_Native(const QString &nativeString); - QString writeToString_Native(); + QString writeToString_Native() const; bool loadFromFile_Native(QIODevice *device); bool saveToFile_Native(QIODevice *device); bool loadFromStream_Plain(QTextStream &stream, bool preserveMetadata); diff --git a/dbconverter/src/mocks.cpp b/dbconverter/src/mocks.cpp index 032cdc95e..acd199759 100644 --- a/dbconverter/src/mocks.cpp +++ b/dbconverter/src/mocks.cpp @@ -247,6 +247,9 @@ void SettingsCache::setVisualDeckStorageUnusedColorIdentitiesOpacity( int /* _visualDeckStorageUnusedColorIdentitiesOpacity */) { } +void SettingsCache::setVisualDeckStorageTooltipType(int /* value */) +{ +} void SettingsCache::setVisualDeckStoragePromptForConversion(bool /* _visualDeckStoragePromptForConversion */) { } diff --git a/tests/carddatabase/mocks.cpp b/tests/carddatabase/mocks.cpp index db7dd9353..8ac925c0b 100644 --- a/tests/carddatabase/mocks.cpp +++ b/tests/carddatabase/mocks.cpp @@ -251,6 +251,9 @@ void SettingsCache::setVisualDeckStorageUnusedColorIdentitiesOpacity( int /* _visualDeckStorageUnusedColorIdentitiesOpacity */) { } +void SettingsCache::setVisualDeckStorageTooltipType(int /* value */) +{ +} void SettingsCache::setVisualDeckStoragePromptForConversion(bool /* _visualDeckStoragePromptForConversion */) { }