Merge branch 'master' into tooomm-ci_attestations

This commit is contained in:
tooomm 2025-06-07 11:51:59 +02:00 committed by GitHub
commit e56df7b2df
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
45 changed files with 649 additions and 173 deletions

View file

@ -7,6 +7,7 @@ RUN pacman --sync --refresh --sysupgrade --needed --noconfirm \
git \
gtest \
mariadb-libs \
ninja \
protobuf \
qt6-base \
qt6-imageformats \

View file

@ -16,6 +16,7 @@ RUN apt-get update && \
libqt5sql5-mysql \
libqt5svg5-dev \
libqt5websockets5-dev \
ninja-build \
protobuf-compiler \
qt5-image-formats-plugins \
qtmultimedia5-dev \

View file

@ -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/*

View file

@ -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 \

View file

@ -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 \

View file

@ -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/*

View file

@ -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 \

View file

@ -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/*

View file

@ -11,9 +11,8 @@
# --debug or --release sets the build type ie CMAKE_BUILD_TYPE
# --ccache [<size>] uses ccache and shows stats, optionally provide size
# --dir <dir> sets the name of the build dir, default is "build"
# --parallel <core count> 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 <package type> --suffix <suffix> --server --test --ccache <ccache_size> --dir <dir> --parallel <core_count>)
# 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 <package type> --suffix <suffix> --server --test --ccache <ccache_size> --dir <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

View file

@ -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

View file

@ -20,7 +20,6 @@ Available pre-compiled binaries for installation:
<b>Linux</b>
<kbd>Ubuntu 24.04 LTS</kbd> <sub><i>Noble Numbat</i></sub>
<kbd>Ubuntu 22.04 LTS</kbd> <sub><i>Jammy Jellyfish</i></sub>
<kbd>Ubuntu 20.04 LTS</kbd> <sub><i>Focal Fossa</i></sub>
<kbd>Debian 12</kbd> <sub><i>Bookworm</i></sub>
<kbd>Debian 11</kbd> <sub><i>Bullseye</i></sub>
<kbd>Fedora 42</kbd>

View file

@ -9,14 +9,24 @@ assignees: ''
---
<!-- READ THIS BEFORE POSTING
Go to "Help → View Debug Log" in Cockatrice and copy all information at the
top (above the separation line) below "System Information" in this ticket!
In Cockatrice, go to "Help" → "View Debug Log" and copy all information displayed at the
top (above the separation line "----"), to below "System Information" section in this ticket!
If you can't start Cockatrice to access these details, make
sure to post your OS and the file name of the setup binary instead. -->
sure to post your OS and the file name of the setup binary instead.
-->
**System Information:**
<!-- Read the hint above on where to find the important information to provide here! -->
<details><summary>Debug Log:</summary>
<!--
In Cockatrice, go to "Help" → "View Debug Log", click the "Copy to clickboard" button and paste the output here.
-->
</details>
_______________________________________________________________________________________
<!-- Explain your issue in detail here! Please attach screenshots if possible. -->
@ -26,7 +36,7 @@ ________________________________________________________________________________
_______________________________________________________________________________________
<!-- Describe the sequence of actions needed to experience the bug -->
<!-- Describe the sequence of actions needed to experience the bug. -->
**Steps to reproduce:**
- Do A

View file

@ -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}}

View file

@ -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

View file

@ -379,5 +379,6 @@
<file>resources/tips/tips_of_the_day.xml</file>
<file>resources/help/search.md</file>
<file>resources/help/deck_search.md</file>
</qresource>
</RCC>

View file

@ -60,4 +60,6 @@
# pixel_map_generator = false
# deck_filter_string = false
# filter_string = false
# syntax_help = false

View file

@ -0,0 +1,26 @@
## Deck Search Syntax Help
-----
The search bar recognizes a set of special commands.<br>
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.
<dl>
<dt>Filename:</dt>
<dd>[red deck wins](#red deck wins) <small>(Any deck filename containing the words red, deck, and wins)</small></dd>
<dd>["red deck wins"](#%22red deck wins%22) <small>(Any deck filename containing the exact phrase "red deck wins")</small></dd>
<dt>Deck Contents (Uses [card search expressions](#cardSearchSyntaxHelp)):</dt>
<dd><a href="#[[plains]]">[[plains]]</a> <small>(Any deck that contains at least one card with "plains" in its name)</small></dd>
<dd><a href="#[[t:legendary]]">[[t:legendary]]</a> <small>(Any deck that contains at least one legendary)</small></dd>
<dd><a href="#[[t:legendary]]>5">[[t:legendary]]>5</a> <small>(Any card that contains at least 5 legendaries)</small></dd>
<dd><a href="#[[]]:100">[[]]:100</a> <small>(Any deck that contains exactly 100 cards)</small></dd>
<dt>Negate:</dt>
<dd>[soldier -aggro](#soldier -aggro) <small>(Any deck filename that contains "soldier", but not "aggro")</small></dd>
<dt>Branching:</dt>
<dd>[t:aggro OR o:control](#t:aggro OR o:control) <small>(Any deck filename that contains either aggro or control)</small></dd>
<dt>Grouping:</dt>
<dd><a href="#red -([[]]:100 or aggro)">red -([[]]:100 or aggro)</a> <small>(Any deck that has red in its filename but is not 100 cards or has aggro in its filename)</small></dd>
</dl>

View file

@ -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<const QString &>(&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);

View file

@ -111,10 +111,8 @@ private:
GameScene *scene;
GameView *gameView;
QMap<int, DeckViewContainer *> 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;

View file

@ -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<int>::of(&QComboBox::currentIndexChanged), [this]() {
deckModel->setActiveGroupCriteria(
static_cast<DeckListModelGroupCriteria>(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<QPair<QString, QString>>().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<QString, QString>(itemData["name"].toString(), itemData["uuid"].toString()));
auto cardAndId = bannerCardComboBox->currentData().value<QPair<QString, QString>>();
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"));

View file

@ -70,6 +70,8 @@ private:
DeckPreviewDeckTagsDisplayWidget *deckTagsDisplayWidget;
QLabel *hashLabel1;
LineEditUnfocusable *hashLabel;
QLabel *activeGroupCriteriaLabel;
QComboBox *activeGroupCriteriaComboBox;
QAction *aRemoveCard, *aIncrement, *aDecrement, *aSwapCard;

View file

@ -12,6 +12,7 @@
#include <QDirIterator>
#include <QHBoxLayout>
#include <QLabel>
#include <QMessageBox>
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<DeckPreviewWidget *>(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();

View file

@ -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);

View file

@ -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<QString, QString>(bannerCardComboBox->currentText(), itemData.toString()));
auto nameAndId = bannerCardComboBox->currentData().value<QPair<QString, QString>>();
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)

View file

@ -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);

View file

@ -4,6 +4,7 @@
#include "visual_deck_storage_widget.h"
#include <QCheckBox>
#include <QComboBox>
#include <QSpinBox>
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<int>::of(&QComboBox::currentIndexChanged), this,
[this] { emit deckPreviewTooltipChanged(getDeckPreviewTooltip()); });
connect(deckPreviewTooltipComboBox, QOverload<int>::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<TooltipType>();
}
int VisualDeckStorageQuickSettingsWidget::getCardSize() const
{
return cardSizeWidget->getSlider()->value();

View file

@ -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);
};

View file

@ -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 <QAction>
/**
* @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<DeckPreviewWidget *> 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});
}
}

View file

@ -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"

View file

@ -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<InnerDecklistNode *>(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);

View file

@ -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<CardInfoPtr> getCardsAsCardInfoPtrs() const;
QList<CardInfoPtr> getCardsAsCardInfoPtrsForZone(QString zoneName) const;
QList<QString> *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);

View file

@ -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);

View file

@ -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

View file

@ -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+ / ["] <NonDoubleQuoteUnlessEscaped*> ["] / ['] <NonSingleQuoteUnlessEscaped*> [']
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<DeckFilter>(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<DeckFilter>(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<DeckFilter>(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<DeckFilter>(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<int>(sv[1]);
const auto op = std::any_cast<QString>(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<QString>(sv[0]));
auto numberMatcher = sv.size() > 1 ? std::any_cast<NumberMatcher>(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<QString>(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<QString>(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; };
}
}

View file

@ -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 <QLoggingCategory>
#include <QMap>
#include <QString>
#include <functional>
#include <utility>
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<bool(const DeckPreviewWidget *, const ExtraDeckSearchInfo &)> 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

View file

@ -7,7 +7,7 @@
#include <QString>
#include <functional>
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()
{

View file

@ -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;
}

View file

@ -2,8 +2,13 @@
#define SEARCH_SYNTAX_HELP_H
#include <QLineEdit>
#include <QLoggingCategory>
#include <QTextBrowser>
inline Q_LOGGING_CATEGORY(SyntaxHelpLog, "syntax_help");
QTextBrowser *createSearchSyntaxHelpWindow(QLineEdit *lineEdit);
QTextBrowser *createDeckSearchSyntaxHelpWindow(QLineEdit *lineEdit);
#endif // SEARCH_SYNTAX_HELP_H

View file

@ -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;

View file

@ -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);

View file

@ -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)},

View file

@ -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);

View file

@ -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);

View file

@ -247,6 +247,9 @@ void SettingsCache::setVisualDeckStorageUnusedColorIdentitiesOpacity(
int /* _visualDeckStorageUnusedColorIdentitiesOpacity */)
{
}
void SettingsCache::setVisualDeckStorageTooltipType(int /* value */)
{
}
void SettingsCache::setVisualDeckStoragePromptForConversion(bool /* _visualDeckStoragePromptForConversion */)
{
}

View file

@ -251,6 +251,9 @@ void SettingsCache::setVisualDeckStorageUnusedColorIdentitiesOpacity(
int /* _visualDeckStorageUnusedColorIdentitiesOpacity */)
{
}
void SettingsCache::setVisualDeckStorageTooltipType(int /* value */)
{
}
void SettingsCache::setVisualDeckStoragePromptForConversion(bool /* _visualDeckStoragePromptForConversion */)
{
}