From 6b5f341e1045305cc88aef23d653ec076d27bf54 Mon Sep 17 00:00:00 2001
From: tooomm
Date: Sat, 9 May 2026 00:04:16 +0200
Subject: [PATCH 001/121] Fix toc link (#6854)
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 472891b15..9a27601cd 100644
--- a/README.md
+++ b/README.md
@@ -8,7 +8,7 @@
Related |
Community |
Contribute |
- Build |
+ Build |
Run
From 6cace2a8e6bdab132faef1bcc7aa2b570921627a Mon Sep 17 00:00:00 2001
From: SlightlyCircuitous
<71394296+SlightlyCircuitous@users.noreply.github.com>
Date: Fri, 8 May 2026 18:05:10 -0400
Subject: [PATCH 002/121] Bump minimum required cmake to 3.10 in gtest (#6851)
* Bump minimum_required to 3.5 and GoogleTest to 1.12
GoogleTest 1.12 is the oldest version that sets cmake_minimum_required() to 3.5 in the CMakeLists files it provides
* code style changes
* Use 1.17.0
* Set minimum to 3.10 to make top-level CMakeLists
* New hash
Co-authored-by: tooomm
* Update cmake/gtest-CMakeLists.txt.in
Co-authored-by: tooomm
---------
Co-authored-by: tooomm
Co-authored-by: ebbit1q
---
cmake/gtest-CMakeLists.txt.in | 15 ++++++++-------
1 file changed, 8 insertions(+), 7 deletions(-)
diff --git a/cmake/gtest-CMakeLists.txt.in b/cmake/gtest-CMakeLists.txt.in
index 2d71a55e5..2062d7f8c 100644
--- a/cmake/gtest-CMakeLists.txt.in
+++ b/cmake/gtest-CMakeLists.txt.in
@@ -1,15 +1,16 @@
-cmake_minimum_required(VERSION 3.2)
+cmake_minimum_required(VERSION 3.10)
project(gtest-download LANGUAGES NONE)
include(ExternalProject)
-ExternalProject_Add(googletest
- URL https://github.com/google/googletest/archive/release-1.11.0.zip
- URL_HASH SHA1=9ffb7b5923f4a8fcdabf2f42c6540cce299f44c0
+externalproject_add(
+ googletest
+ URL https://github.com/google/googletest/archive/refs/tags/v1.17.0.zip
+ URL_HASH SHA1=f638fa0e724760e2ba07ff8cfba32cd644e1ce28
SOURCE_DIR "${CMAKE_BINARY_DIR}/gtest-src"
BINARY_DIR "${CMAKE_BINARY_DIR}/gtest-build"
CONFIGURE_COMMAND ""
- BUILD_COMMAND ""
- INSTALL_COMMAND ""
- TEST_COMMAND ""
+ BUILD_COMMAND ""
+ INSTALL_COMMAND ""
+ TEST_COMMAND ""
)
From f7eeaeddcb11ecfbd0a17ea1c99d17bbdd41af62 Mon Sep 17 00:00:00 2001
From: SlightlyCircuitous
<71394296+SlightlyCircuitous@users.noreply.github.com>
Date: Fri, 8 May 2026 18:28:03 -0400
Subject: [PATCH 003/121] Drop builds for Debian 11 and Ubuntu 22.04 (#6643)
* Delete .ci/Debian11 directory
* Delete .ci/Servatrice_Debian11 directory
* Delete .ci/Ubuntu22.04 directory
* Update desktop-build.yml
* Update release_template.md
* Add servatrice debian 12
* Update desktop-build.yml
---
.ci/Debian11/Dockerfile | 26 -------------------
.../Dockerfile | 10 +++----
.ci/Ubuntu22.04/Dockerfile | 26 -------------------
.ci/release_template.md | 2 --
.github/workflows/desktop-build.yml | 12 ++-------
5 files changed, 7 insertions(+), 69 deletions(-)
delete mode 100644 .ci/Debian11/Dockerfile
rename .ci/{Servatrice_Debian11 => Servatrice_Debian12}/Dockerfile (75%)
delete mode 100644 .ci/Ubuntu22.04/Dockerfile
diff --git a/.ci/Debian11/Dockerfile b/.ci/Debian11/Dockerfile
deleted file mode 100644
index b994863bf..000000000
--- a/.ci/Debian11/Dockerfile
+++ /dev/null
@@ -1,26 +0,0 @@
-FROM debian:11
-
-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 \
- ninja-build \
- protobuf-compiler \
- qt5-image-formats-plugins \
- qtmultimedia5-dev \
- qttools5-dev \
- qttools5-dev-tools \
- && apt-get clean \
- && rm -rf /var/lib/apt/lists/*
diff --git a/.ci/Servatrice_Debian11/Dockerfile b/.ci/Servatrice_Debian12/Dockerfile
similarity index 75%
rename from .ci/Servatrice_Debian11/Dockerfile
rename to .ci/Servatrice_Debian12/Dockerfile
index fadc9e0e7..21f6a036e 100644
--- a/.ci/Servatrice_Debian11/Dockerfile
+++ b/.ci/Servatrice_Debian12/Dockerfile
@@ -1,4 +1,4 @@
-FROM debian:11
+FROM debian:12
RUN apt-get update && \
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
@@ -11,11 +11,11 @@ RUN apt-get update && \
git \
libmariadb-dev-compat \
libprotobuf-dev \
- libqt5sql5-mysql \
- libqt5websockets5-dev \
+ libqt6sql6-mysql \
ninja-build \
protobuf-compiler \
- qttools5-dev \
- qttools5-dev-tools \
+ qt6-tools-dev \
+ qt6-tools-dev-tools \
+ qt6-websockets-dev \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
diff --git a/.ci/Ubuntu22.04/Dockerfile b/.ci/Ubuntu22.04/Dockerfile
deleted file mode 100644
index 93c8fdea9..000000000
--- a/.ci/Ubuntu22.04/Dockerfile
+++ /dev/null
@@ -1,26 +0,0 @@
-FROM ubuntu:22.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 \
- ninja-build \
- protobuf-compiler \
- qt5-image-formats-plugins \
- qtmultimedia5-dev \
- qttools5-dev \
- qttools5-dev-tools \
- && apt-get clean \
- && rm -rf /var/lib/apt/lists/*
diff --git a/.ci/release_template.md b/.ci/release_template.md
index e6dc2e4be..ac78a193a 100644
--- a/.ci/release_template.md
+++ b/.ci/release_template.md
@@ -19,10 +19,8 @@ Available pre-compiled binaries for installation:
Linux
• Ubuntu 26.04 LTS Resolute Racoon
• Ubuntu 24.04 LTS Noble Numbat
- • Ubuntu 22.04 LTS Jammy Jellyfish
• Debian 13 Trixie
• Debian 12 Bookworm
- • Debian 11 Bullseye
• Fedora 44
• Fedora 43
diff --git a/.github/workflows/desktop-build.yml b/.github/workflows/desktop-build.yml
index 9eb81c007..62108b34a 100644
--- a/.github/workflows/desktop-build.yml
+++ b/.github/workflows/desktop-build.yml
@@ -107,12 +107,8 @@ jobs:
package: skip # We are packaged in Arch already
allow-failure: yes
- - distro: Debian
- version: 11
- package: DEB
-
- distro: Servatrice_Debian
- version: 11
+ version: 12
package: DEB
test: skip
server_only: yes
@@ -135,14 +131,10 @@ jobs:
version: 44
package: RPM
- - distro: Ubuntu
- version: 22.04
- package: DEB
- test: skip # Running tests on all distros is superfluous
-
- distro: Ubuntu
version: 24.04
package: DEB
+ test: skip # Running tests on all distros is superfluous
- distro: Ubuntu
version: 26.04
From 2c51054e7795b9a7996262aa3a17e8692703a68d Mon Sep 17 00:00:00 2001
From: ebbit1q
Date: Sat, 9 May 2026 02:40:39 +0200
Subject: [PATCH 004/121] update version number after update (#6853)
---
CMakeLists.txt | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/CMakeLists.txt b/CMakeLists.txt
index d2c10de72..14137ac55 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -74,7 +74,7 @@ endif()
# A project name is needed for CPack
# Version can be overriden by git tags, see cmake/getversion.cmake
-project("Cockatrice" VERSION 3.0.0)
+project("Cockatrice" VERSION 3.0.1)
# Set release name if not provided via env/cmake var
if(NOT DEFINED GIT_TAG_RELEASENAME)
From 985936a91706315204564867f8946bfff682cc2c Mon Sep 17 00:00:00 2001
From: RickyRister <42636155+RickyRister@users.noreply.github.com>
Date: Sat, 9 May 2026 02:45:33 -0700
Subject: [PATCH 005/121] [GameModel] Refactor game filters options into struct
(#6856)
---
cockatrice/CMakeLists.txt | 1 +
.../widgets/dialogs/dlg_filter_games.cpp | 118 ++++-------
.../widgets/dialogs/dlg_filter_games.h | 35 +---
.../widgets/server/game_filter_configs.cpp | 7 +
.../widgets/server/game_filter_configs.h | 42 ++++
.../widgets/server/game_selector.cpp | 8 +-
.../game_selector_quick_filter_toolbar.cpp | 101 +++------
.../game_selector_quick_filter_toolbar.h | 18 +-
.../interface/widgets/server/games_model.cpp | 143 +++++--------
.../interface/widgets/server/games_model.h | 191 +-----------------
10 files changed, 189 insertions(+), 475 deletions(-)
create mode 100644 cockatrice/src/interface/widgets/server/game_filter_configs.cpp
create mode 100644 cockatrice/src/interface/widgets/server/game_filter_configs.h
diff --git a/cockatrice/CMakeLists.txt b/cockatrice/CMakeLists.txt
index 6fd009dd9..5884db9e3 100644
--- a/cockatrice/CMakeLists.txt
+++ b/cockatrice/CMakeLists.txt
@@ -216,6 +216,7 @@ set(cockatrice_SOURCES
src/interface/widgets/replay/replay_manager.cpp
src/interface/widgets/replay/replay_timeline_widget.cpp
src/interface/widgets/server/chat_view/chat_view.cpp
+ src/interface/widgets/server/game_filter_configs.cpp
src/interface/widgets/server/game_selector.cpp
src/interface/widgets/server/game_selector_quick_filter_toolbar.cpp
src/interface/widgets/server/games_model.cpp
diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_filter_games.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_filter_games.cpp
index 1bf98822c..043b7a1cd 100644
--- a/cockatrice/src/interface/widgets/dialogs/dlg_filter_games.cpp
+++ b/cockatrice/src/interface/widgets/dialogs/dlg_filter_games.cpp
@@ -22,41 +22,43 @@ DlgFilterGames::DlgFilterGames(const QMap &_allGameTypes,
{QTime(1, 0), tr("1 hour")},
{QTime(2, 0), tr("2 hours")}})
{
+ const GameFilterConfigs &filters = gamesProxyModel->getFilters();
+
hideBuddiesOnlyGames = new QCheckBox(tr("Hide 'buddies only' games"));
- hideBuddiesOnlyGames->setChecked(gamesProxyModel->getHideBuddiesOnlyGames());
+ hideBuddiesOnlyGames->setChecked(filters.hideBuddiesOnlyGames);
hideFullGames = new QCheckBox(tr("Hide full games"));
- hideFullGames->setChecked(gamesProxyModel->getHideFullGames());
+ hideFullGames->setChecked(filters.hideFullGames);
hideGamesThatStarted = new QCheckBox(tr("Hide games that have started"));
- hideGamesThatStarted->setChecked(gamesProxyModel->getHideGamesThatStarted());
+ hideGamesThatStarted->setChecked(filters.hideGamesThatStarted);
hidePasswordProtectedGames = new QCheckBox(tr("Hide password protected games"));
- hidePasswordProtectedGames->setChecked(gamesProxyModel->getHidePasswordProtectedGames());
+ hidePasswordProtectedGames->setChecked(filters.hidePasswordProtectedGames);
hideIgnoredUserGames = new QCheckBox(tr("Hide 'ignored user' games"));
- hideIgnoredUserGames->setChecked(gamesProxyModel->getHideIgnoredUserGames());
+ hideIgnoredUserGames->setChecked(filters.hideIgnoredUserGames);
hideNotBuddyCreatedGames = new QCheckBox(tr("Hide games not created by buddies"));
- hideNotBuddyCreatedGames->setChecked(gamesProxyModel->getHideNotBuddyCreatedGames());
+ hideNotBuddyCreatedGames->setChecked(filters.hideNotBuddyCreatedGames);
hideOpenDecklistGames = new QCheckBox(tr("Hide games with forced open decklists"));
- hideOpenDecklistGames->setChecked(gamesProxyModel->getHideOpenDecklistGames());
+ hideOpenDecklistGames->setChecked(filters.hideOpenDecklistGames);
maxGameAgeComboBox = new QComboBox();
maxGameAgeComboBox->setEditable(false);
maxGameAgeComboBox->addItems(gameAgeMap.values());
- QTime gameAge = gamesProxyModel->getMaxGameAge();
+ QTime gameAge = filters.maxGameAge;
maxGameAgeComboBox->setCurrentIndex(gameAgeMap.keys().indexOf(gameAge)); // index is -1 if unknown
auto *maxGameAgeLabel = new QLabel(tr("&Newer than:"));
maxGameAgeLabel->setBuddy(maxGameAgeComboBox);
gameNameFilterEdit = new QLineEdit;
- gameNameFilterEdit->setText(gamesProxyModel->getGameNameFilter());
+ gameNameFilterEdit->setText(filters.gameNameFilter);
auto *gameNameFilterLabel = new QLabel(tr("Game &description:"));
gameNameFilterLabel->setBuddy(gameNameFilterEdit);
creatorNameFilterEdit = new QLineEdit;
- creatorNameFilterEdit->setText(gamesProxyModel->getCreatorNameFilters().join(", "));
+ creatorNameFilterEdit->setText(filters.creatorNameFilters.join(", "));
auto *creatorNameFilterLabel = new QLabel(tr("&Creator name:"));
creatorNameFilterLabel->setBuddy(creatorNameFilterEdit);
@@ -76,7 +78,7 @@ DlgFilterGames::DlgFilterGames(const QMap &_allGameTypes,
gameTypesIterator.next();
auto *temp = new QCheckBox(gameTypesIterator.value());
- temp->setChecked(gamesProxyModel->getGameTypeFilter().contains(gameTypesIterator.key()));
+ temp->setChecked(filters.gameTypeFilter.contains(gameTypesIterator.key()));
gameTypeFilterCheckBoxes.insert(gameTypesIterator.key(), temp);
gameTypeFilterLayout->addWidget(temp);
@@ -92,14 +94,14 @@ DlgFilterGames::DlgFilterGames(const QMap &_allGameTypes,
maxPlayersFilterMinSpinBox = new QSpinBox;
maxPlayersFilterMinSpinBox->setMinimum(0);
maxPlayersFilterMinSpinBox->setMaximum(99);
- maxPlayersFilterMinSpinBox->setValue(gamesProxyModel->getMaxPlayersFilterMin());
+ maxPlayersFilterMinSpinBox->setValue(filters.maxPlayersFilterMin);
maxPlayersFilterMinLabel->setBuddy(maxPlayersFilterMinSpinBox);
auto *maxPlayersFilterMaxLabel = new QLabel(tr("at &most:"));
maxPlayersFilterMaxSpinBox = new QSpinBox;
maxPlayersFilterMaxSpinBox->setMinimum(0);
maxPlayersFilterMaxSpinBox->setMaximum(99);
- maxPlayersFilterMaxSpinBox->setValue(gamesProxyModel->getMaxPlayersFilterMax());
+ maxPlayersFilterMaxSpinBox->setValue(filters.maxPlayersFilterMax);
maxPlayersFilterMaxLabel->setBuddy(maxPlayersFilterMaxSpinBox);
auto *maxPlayersFilterLayout = new QGridLayout;
@@ -124,17 +126,17 @@ DlgFilterGames::DlgFilterGames(const QMap &_allGameTypes,
restrictionsGroupBox->setLayout(restrictionsLayout);
showOnlyIfSpectatorsCanWatch = new QCheckBox(tr("Show games only if &spectators can watch"));
- showOnlyIfSpectatorsCanWatch->setChecked(gamesProxyModel->getShowOnlyIfSpectatorsCanWatch());
+ showOnlyIfSpectatorsCanWatch->setChecked(filters.showOnlyIfSpectatorsCanWatch);
connect(showOnlyIfSpectatorsCanWatch, &QCheckBox::toggled, this,
&DlgFilterGames::toggleSpectatorCheckboxEnabledness);
showSpectatorPasswordProtected = new QCheckBox(tr("Show spectator password p&rotected games"));
- showSpectatorPasswordProtected->setChecked(gamesProxyModel->getShowSpectatorPasswordProtected());
+ showSpectatorPasswordProtected->setChecked(filters.showSpectatorPasswordProtected);
showOnlyIfSpectatorsCanChat = new QCheckBox(tr("Show only if spectators can ch&at"));
- showOnlyIfSpectatorsCanChat->setChecked(gamesProxyModel->getShowOnlyIfSpectatorsCanChat());
+ showOnlyIfSpectatorsCanChat->setChecked(filters.showOnlyIfSpectatorsCanChat);
showOnlyIfSpectatorsCanSeeHands = new QCheckBox(tr("Show only if spectators can see &hands"));
- showOnlyIfSpectatorsCanSeeHands->setChecked(gamesProxyModel->getShowOnlyIfSpectatorsCanSeeHands());
- toggleSpectatorCheckboxEnabledness(getShowOnlyIfSpectatorsCanWatch());
+ showOnlyIfSpectatorsCanSeeHands->setChecked(filters.showOnlyIfSpectatorsCanSeeHands);
+ toggleSpectatorCheckboxEnabledness(filters.showOnlyIfSpectatorsCanWatch);
auto *spectatorsLayout = new QGridLayout;
spectatorsLayout->addWidget(showOnlyIfSpectatorsCanWatch, 0, 0);
@@ -180,6 +182,27 @@ DlgFilterGames::DlgFilterGames(const QMap &_allGameTypes,
setFixedHeight(sizeHint().height());
}
+GameFilterConfigs DlgFilterGames::getFilters() const
+{
+ return {hideBuddiesOnlyGames->isChecked(),
+ hideIgnoredUserGames->isChecked(),
+ hideFullGames->isChecked(),
+ hideGamesThatStarted->isChecked(),
+ hidePasswordProtectedGames->isChecked(),
+ hideNotBuddyCreatedGames->isChecked(),
+ hideOpenDecklistGames->isChecked(),
+ gameNameFilterEdit->text(),
+ getCreatorNameFilters(),
+ getGameTypeFilter(),
+ maxPlayersFilterMinSpinBox->value(),
+ maxPlayersFilterMaxSpinBox->value(),
+ getMaxGameAge(),
+ showOnlyIfSpectatorsCanWatch->isChecked(),
+ getShowSpectatorPasswordProtected(),
+ getShowOnlyIfSpectatorsCanChat(),
+ getShowOnlyIfSpectatorsCanSeeHands()};
+}
+
void DlgFilterGames::actOk()
{
accept();
@@ -192,46 +215,6 @@ void DlgFilterGames::toggleSpectatorCheckboxEnabledness(bool spectatorsEnabled)
showOnlyIfSpectatorsCanSeeHands->setDisabled(!spectatorsEnabled);
}
-bool DlgFilterGames::getHideFullGames() const
-{
- return hideFullGames->isChecked();
-}
-
-bool DlgFilterGames::getHideGamesThatStarted() const
-{
- return hideGamesThatStarted->isChecked();
-}
-
-bool DlgFilterGames::getHideBuddiesOnlyGames() const
-{
- return hideBuddiesOnlyGames->isChecked();
-}
-
-bool DlgFilterGames::getHidePasswordProtectedGames() const
-{
- return hidePasswordProtectedGames->isChecked();
-}
-
-bool DlgFilterGames::getHideIgnoredUserGames() const
-{
- return hideIgnoredUserGames->isChecked();
-}
-
-bool DlgFilterGames::getHideNotBuddyCreatedGames() const
-{
- return hideNotBuddyCreatedGames->isChecked();
-}
-
-bool DlgFilterGames::getHideOpenDecklistGames() const
-{
- return hideOpenDecklistGames->isChecked();
-}
-
-QString DlgFilterGames::getGameNameFilter() const
-{
- return gameNameFilterEdit->text();
-}
-
QStringList DlgFilterGames::getCreatorNameFilters() const
{
return creatorNameFilterEdit->text().split(",", Qt::SkipEmptyParts);
@@ -249,30 +232,15 @@ QSet DlgFilterGames::getGameTypeFilter() const
return result;
}
-int DlgFilterGames::getMaxPlayersFilterMin() const
-{
- return maxPlayersFilterMinSpinBox->value();
-}
-
-int DlgFilterGames::getMaxPlayersFilterMax() const
-{
- return maxPlayersFilterMaxSpinBox->value();
-}
-
QTime DlgFilterGames::getMaxGameAge() const
{
int index = maxGameAgeComboBox->currentIndex();
- if (index < 0 || index >= gameAgeMap.size()) { // index is out of bounds
- return gamesProxyModel->getMaxGameAge(); // leave the setting unchanged
+ if (index < 0 || index >= gameAgeMap.size()) { // index is out of bounds
+ return gamesProxyModel->getFilters().maxGameAge; // leave the setting unchanged
}
return gameAgeMap.keys().at(index);
}
-bool DlgFilterGames::getShowOnlyIfSpectatorsCanWatch() const
-{
- return showOnlyIfSpectatorsCanWatch->isChecked();
-}
-
bool DlgFilterGames::getShowSpectatorPasswordProtected() const
{
return showSpectatorPasswordProtected->isEnabled() && showSpectatorPasswordProtected->isChecked();
diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_filter_games.h b/cockatrice/src/interface/widgets/dialogs/dlg_filter_games.h
index abfdab28a..c4cc2dd60 100644
--- a/cockatrice/src/interface/widgets/dialogs/dlg_filter_games.h
+++ b/cockatrice/src/interface/widgets/dialogs/dlg_filter_games.h
@@ -48,6 +48,14 @@ private:
const QMap &allGameTypes;
const GamesProxyModel *gamesProxyModel;
+ const QMap gameAgeMap;
+
+ [[nodiscard]] QStringList getCreatorNameFilters() const;
+ [[nodiscard]] QSet getGameTypeFilter() const;
+ [[nodiscard]] QTime getMaxGameAge() const;
+ [[nodiscard]] bool getShowSpectatorPasswordProtected() const;
+ [[nodiscard]] bool getShowOnlyIfSpectatorsCanChat() const;
+ [[nodiscard]] bool getShowOnlyIfSpectatorsCanSeeHands() const;
private slots:
void actOk();
@@ -58,32 +66,7 @@ public:
const GamesProxyModel *_gamesProxyModel,
QWidget *parent = nullptr);
- [[nodiscard]] bool getHideFullGames() const;
- [[nodiscard]] bool getHideGamesThatStarted() const;
- [[nodiscard]] bool getHidePasswordProtectedGames() const;
- void setShowPasswordProtectedGames(bool _passwordProtectedGamesHidden);
- [[nodiscard]] bool getHideBuddiesOnlyGames() const;
- void setHideBuddiesOnlyGames(bool _hideBuddiesOnlyGames);
- [[nodiscard]] bool getHideOpenDecklistGames() const;
- void setHideOpenDecklistGames(bool _hideOpenDecklistGames);
- [[nodiscard]] bool getHideIgnoredUserGames() const;
- void setHideIgnoredUserGames(bool _hideIgnoredUserGames);
- [[nodiscard]] bool getHideNotBuddyCreatedGames() const;
- [[nodiscard]] QString getGameNameFilter() const;
- void setGameNameFilter(const QString &_gameNameFilter);
- [[nodiscard]] QStringList getCreatorNameFilters() const;
- void setCreatorNameFilter(const QString &_creatorNameFilter);
- [[nodiscard]] QSet getGameTypeFilter() const;
- void setGameTypeFilter(const QSet &_gameTypeFilter);
- [[nodiscard]] int getMaxPlayersFilterMin() const;
- [[nodiscard]] int getMaxPlayersFilterMax() const;
- void setMaxPlayersFilter(int _maxPlayersFilterMin, int _maxPlayersFilterMax);
- [[nodiscard]] QTime getMaxGameAge() const;
- const QMap gameAgeMap;
- [[nodiscard]] bool getShowOnlyIfSpectatorsCanWatch() const;
- [[nodiscard]] bool getShowSpectatorPasswordProtected() const;
- [[nodiscard]] bool getShowOnlyIfSpectatorsCanChat() const;
- [[nodiscard]] bool getShowOnlyIfSpectatorsCanSeeHands() const;
+ [[nodiscard]] GameFilterConfigs getFilters() const;
};
#endif
diff --git a/cockatrice/src/interface/widgets/server/game_filter_configs.cpp b/cockatrice/src/interface/widgets/server/game_filter_configs.cpp
new file mode 100644
index 000000000..5a6282b75
--- /dev/null
+++ b/cockatrice/src/interface/widgets/server/game_filter_configs.cpp
@@ -0,0 +1,7 @@
+#include "game_filter_configs.h"
+
+bool GameFilterConfigs::isDefault() const
+{
+ static const GameFilterConfigs DEFAULT = {};
+ return *this == DEFAULT;
+}
\ No newline at end of file
diff --git a/cockatrice/src/interface/widgets/server/game_filter_configs.h b/cockatrice/src/interface/widgets/server/game_filter_configs.h
new file mode 100644
index 000000000..0ece7e00c
--- /dev/null
+++ b/cockatrice/src/interface/widgets/server/game_filter_configs.h
@@ -0,0 +1,42 @@
+#ifndef COCKATRICE_GAME_FILTER_CONFIGS_H
+#define COCKATRICE_GAME_FILTER_CONFIGS_H
+
+#include
+
+/**
+ * @brief The possible game filter configs.
+ */
+struct GameFilterConfigs
+{
+ static constexpr int DEFAULT_MAX_PLAYERS_MIN = 1;
+ static constexpr int DEFAULT_MAX_PLAYERS_MAX = 99;
+
+ bool hideBuddiesOnlyGames = false;
+ bool hideIgnoredUserGames = false;
+ bool hideFullGames = false;
+ bool hideGamesThatStarted = false;
+ bool hidePasswordProtectedGames = false;
+ bool hideNotBuddyCreatedGames = false;
+ bool hideOpenDecklistGames = false;
+ QString gameNameFilter = "";
+ QStringList creatorNameFilters = {};
+ QSet gameTypeFilter = {};
+ int maxPlayersFilterMin = DEFAULT_MAX_PLAYERS_MIN;
+ int maxPlayersFilterMax = DEFAULT_MAX_PLAYERS_MAX;
+ QTime maxGameAge = {};
+ bool showOnlyIfSpectatorsCanWatch = false;
+ bool showSpectatorPasswordProtected = false;
+ bool showOnlyIfSpectatorsCanChat = false;
+ bool showOnlyIfSpectatorsCanSeeHands = false;
+
+ bool operator==(const GameFilterConfigs &) const = default;
+
+ /**
+ * @brief Checks if this config has exactly the default values.
+ *
+ * @return Whether this config has the default values
+ */
+ bool isDefault() const;
+};
+
+#endif // COCKATRICE_GAME_FILTER_CONFIGS_H
diff --git a/cockatrice/src/interface/widgets/server/game_selector.cpp b/cockatrice/src/interface/widgets/server/game_selector.cpp
index 0ff2a5542..ea1a1cc91 100644
--- a/cockatrice/src/interface/widgets/server/game_selector.cpp
+++ b/cockatrice/src/interface/widgets/server/game_selector.cpp
@@ -180,13 +180,7 @@ void GameSelector::actSetFilter()
if (!dlg.exec())
return;
- gameListProxyModel->setGameFilters(
- dlg.getHideBuddiesOnlyGames(), dlg.getHideIgnoredUserGames(), dlg.getHideFullGames(),
- dlg.getHideGamesThatStarted(), dlg.getHidePasswordProtectedGames(), dlg.getHideNotBuddyCreatedGames(),
- dlg.getHideOpenDecklistGames(), dlg.getGameNameFilter(), dlg.getCreatorNameFilters(), dlg.getGameTypeFilter(),
- dlg.getMaxPlayersFilterMin(), dlg.getMaxPlayersFilterMax(), dlg.getMaxGameAge(),
- dlg.getShowOnlyIfSpectatorsCanWatch(), dlg.getShowSpectatorPasswordProtected(),
- dlg.getShowOnlyIfSpectatorsCanChat(), dlg.getShowOnlyIfSpectatorsCanSeeHands());
+ gameListProxyModel->setGameFilters(dlg.getFilters());
gameListProxyModel->saveFilterParameters(gameTypeMap);
updateTitle();
diff --git a/cockatrice/src/interface/widgets/server/game_selector_quick_filter_toolbar.cpp b/cockatrice/src/interface/widgets/server/game_selector_quick_filter_toolbar.cpp
index f7eacd636..b2c9fb84e 100644
--- a/cockatrice/src/interface/widgets/server/game_selector_quick_filter_toolbar.cpp
+++ b/cockatrice/src/interface/widgets/server/game_selector_quick_filter_toolbar.cpp
@@ -18,46 +18,42 @@ GameSelectorQuickFilterToolBar::GameSelectorQuickFilterToolBar(QWidget *parent,
mainLayout->setContentsMargins(0, 0, 0, 0);
mainLayout->setSpacing(5);
+ const GameFilterConfigs &filters = model->getFilters();
+
searchBar = new QLineEdit(this);
- searchBar->setText(model->getGameNameFilter());
+ searchBar->setText(filters.gameNameFilter);
connect(searchBar, &QLineEdit::textChanged, this, [this](const QString &text) {
- applyFilters([&](auto &, auto &, auto &, auto &, auto &, auto &, auto &, QString &gameNameFilter, auto &,
- auto &, auto &, auto &, auto &, auto &, auto &, auto &, auto &) { gameNameFilter = text; });
+ applyFilters([&](GameFilterConfigs &configs) { configs.gameNameFilter = text; });
});
hideGamesNotCreatedByBuddiesCheckBox = new QCheckBox(this);
- hideGamesNotCreatedByBuddiesCheckBox->setChecked(model->getHideNotBuddyCreatedGames());
+ hideGamesNotCreatedByBuddiesCheckBox->setChecked(filters.hideNotBuddyCreatedGames);
connect(hideGamesNotCreatedByBuddiesCheckBox, &QCheckBox::toggled, this, [this](bool checked) {
- applyFilters([&](auto &, auto &, auto &, auto &, auto &, bool &hideNotBuddyCreatedGames, auto &, auto &,
- QStringList &creatorNameFilters, auto &, auto &, auto &, auto &, auto &, auto &, auto &,
- auto &) {
- hideNotBuddyCreatedGames = checked;
+ applyFilters([&](GameFilterConfigs &configs) {
+ configs.hideNotBuddyCreatedGames = checked;
if (checked) {
QStringList buddyNames;
for (auto buddy : tabSupervisor->getUserListManager()->getBuddyList().values()) {
buddyNames << QString::fromStdString(buddy.name());
}
- creatorNameFilters = buddyNames;
+ configs.creatorNameFilters = buddyNames;
} else {
- creatorNameFilters.clear();
+ configs.creatorNameFilters.clear();
}
});
});
hideFullGamesCheckBox = new QCheckBox(this);
- hideFullGamesCheckBox->setChecked(model->getHideFullGames());
+ hideFullGamesCheckBox->setChecked(filters.hideFullGames);
connect(hideFullGamesCheckBox, &QCheckBox::toggled, this, [this](bool checked) {
- applyFilters([&](auto &, auto &, bool &hideFullGames, auto &, auto &, auto &, auto &, auto &, auto &, auto &,
- auto &, auto &, auto &, auto &, auto &, auto &, auto &) { hideFullGames = checked; });
+ applyFilters([&](GameFilterConfigs &configs) { configs.hideFullGames = checked; });
});
hideStartedGamesCheckBox = new QCheckBox(this);
- hideStartedGamesCheckBox->setChecked(model->getHideGamesThatStarted());
+ hideStartedGamesCheckBox->setChecked(filters.hideGamesThatStarted);
connect(hideStartedGamesCheckBox, &QCheckBox::toggled, this, [this](bool checked) {
- applyFilters([&](auto &, auto &, auto &, bool &hideGamesThatStarted, auto &, auto &, auto &, auto &, auto &,
- auto &, auto &, auto &, auto &, auto &, auto &, auto &,
- auto &) { hideGamesThatStarted = checked; });
+ applyFilters([&](GameFilterConfigs &configs) { configs.hideGamesThatStarted = checked; });
});
filterToFormatComboBox = new QComboBox(this);
@@ -71,7 +67,7 @@ GameSelectorQuickFilterToolBar::GameSelectorQuickFilterToolBar(QWidget *parent,
filterToFormatComboBox->addItem(i.value(), i.key()); // text = name, data = type ID
}
- QSet currentTypes = model->getGameTypeFilter();
+ QSet currentTypes = filters.gameTypeFilter;
if (currentTypes.size() == 1) {
int typeId = *currentTypes.begin();
int index = filterToFormatComboBox->findData(typeId);
@@ -83,13 +79,12 @@ GameSelectorQuickFilterToolBar::GameSelectorQuickFilterToolBar(QWidget *parent,
// Update proxy model on selection change
connect(filterToFormatComboBox, QOverload::of(&QComboBox::currentIndexChanged), this, [this](int index) {
- applyFilters([&](auto &, auto &, auto &, auto &, auto &, auto &, auto &, auto &, auto &,
- QSet &gameTypeFilter, auto &, auto &, auto &, auto &, auto &, auto &, auto &) {
+ applyFilters([&](GameFilterConfigs &configs) {
QVariant data = filterToFormatComboBox->itemData(index);
if (!data.isValid()) {
- gameTypeFilter.clear();
+ configs.gameTypeFilter.clear();
} else {
- gameTypeFilter = {data.toInt()};
+ configs.gameTypeFilter = {data.toInt()};
}
});
});
@@ -127,13 +122,15 @@ void GameSelectorQuickFilterToolBar::syncFromModel()
QSignalBlocker b4(hideFullGamesCheckBox);
QSignalBlocker b5(hideStartedGamesCheckBox);
- searchBar->setText(model->getGameNameFilter());
+ const GameFilterConfigs filters = model->getFilters();
- hideGamesNotCreatedByBuddiesCheckBox->setChecked(model->getHideNotBuddyCreatedGames());
- hideFullGamesCheckBox->setChecked(model->getHideFullGames());
- hideStartedGamesCheckBox->setChecked(model->getHideGamesThatStarted());
+ searchBar->setText(filters.gameNameFilter);
- QSet types = model->getGameTypeFilter();
+ hideGamesNotCreatedByBuddiesCheckBox->setChecked(filters.hideNotBuddyCreatedGames);
+ hideFullGamesCheckBox->setChecked(filters.hideFullGames);
+ hideStartedGamesCheckBox->setChecked(filters.hideGamesThatStarted);
+
+ QSet types = filters.gameTypeFilter;
if (types.size() == 1) {
int idx = filterToFormatComboBox->findData(*types.begin());
filterToFormatComboBox->setCurrentIndex(idx >= 0 ? idx : 0);
@@ -142,55 +139,13 @@ void GameSelectorQuickFilterToolBar::syncFromModel()
}
}
-void GameSelectorQuickFilterToolBar::applyFilters(std::function &,
- int &,
- int &,
- QTime &,
- bool &,
- bool &,
- bool &,
- bool &)> mutator)
+void GameSelectorQuickFilterToolBar::applyFilters(std::function mutator)
{
- bool hideBuddiesOnlyGames = model->getHideBuddiesOnlyGames();
- bool hideIgnoredUserGames = model->getHideIgnoredUserGames();
- bool hideFullGames = model->getHideFullGames();
- bool hideGamesThatStarted = model->getHideGamesThatStarted();
- bool hidePasswordProtectedGames = model->getHidePasswordProtectedGames();
- bool hideNotBuddyCreatedGames = model->getHideNotBuddyCreatedGames();
- bool hideOpenDecklistGames = model->getHideOpenDecklistGames();
+ GameFilterConfigs configs = model->getFilters();
- QString gameNameFilter = model->getGameNameFilter();
- QStringList creatorNameFilters = model->getCreatorNameFilters();
- QSet gameTypeFilter = model->getGameTypeFilter();
+ mutator(configs);
- int minPlayers = model->getMaxPlayersFilterMin();
- int maxPlayers = model->getMaxPlayersFilterMax();
- QTime maxGameAge = model->getMaxGameAge();
-
- bool showOnlyIfSpectatorsCanWatch = model->getShowOnlyIfSpectatorsCanWatch();
- bool showSpectatorPasswordProtected = model->getShowSpectatorPasswordProtected();
- bool showOnlyIfSpectatorsCanChat = model->getShowOnlyIfSpectatorsCanChat();
- bool showOnlyIfSpectatorsCanSeeHands = model->getShowOnlyIfSpectatorsCanSeeHands();
-
- mutator(hideBuddiesOnlyGames, hideIgnoredUserGames, hideFullGames, hideGamesThatStarted, hidePasswordProtectedGames,
- hideNotBuddyCreatedGames, hideOpenDecklistGames, gameNameFilter, creatorNameFilters, gameTypeFilter,
- minPlayers, maxPlayers, maxGameAge, showOnlyIfSpectatorsCanWatch, showSpectatorPasswordProtected,
- showOnlyIfSpectatorsCanChat, showOnlyIfSpectatorsCanSeeHands);
-
- model->setGameFilters(hideBuddiesOnlyGames, hideIgnoredUserGames, hideFullGames, hideGamesThatStarted,
- hidePasswordProtectedGames, hideNotBuddyCreatedGames, hideOpenDecklistGames, gameNameFilter,
- creatorNameFilters, gameTypeFilter, minPlayers, maxPlayers, maxGameAge,
- showOnlyIfSpectatorsCanWatch, showSpectatorPasswordProtected, showOnlyIfSpectatorsCanChat,
- showOnlyIfSpectatorsCanSeeHands);
+ model->setGameFilters(configs);
}
void GameSelectorQuickFilterToolBar::retranslateUi()
diff --git a/cockatrice/src/interface/widgets/server/game_selector_quick_filter_toolbar.h b/cockatrice/src/interface/widgets/server/game_selector_quick_filter_toolbar.h
index c658418f9..9b7b6e61f 100644
--- a/cockatrice/src/interface/widgets/server/game_selector_quick_filter_toolbar.h
+++ b/cockatrice/src/interface/widgets/server/game_selector_quick_filter_toolbar.h
@@ -19,23 +19,7 @@ public:
GamesProxyModel *model,
const QMap &allGameTypes);
void syncFromModel();
- void applyFilters(std::function &,
- int &,
- int &,
- QTime &,
- bool &,
- bool &,
- bool &,
- bool &)> mutator);
+ void applyFilters(std::function mutator);
void retranslateUi();
private:
diff --git a/cockatrice/src/interface/widgets/server/games_model.cpp b/cockatrice/src/interface/widgets/server/games_model.cpp
index 1f05308b8..e894ac4fd 100644
--- a/cockatrice/src/interface/widgets/server/games_model.cpp
+++ b/cockatrice/src/interface/widgets/server/games_model.cpp
@@ -23,10 +23,6 @@ enum GameListColumn
SPECTATORS
};
-const int DEFAULT_MAX_PLAYERS_MIN = 1;
-const int DEFAULT_MAX_PLAYERS_MAX = 99;
-constexpr auto DEFAULT_MAX_GAME_AGE = QTime();
-
const QString GamesModel::getGameCreatedString(const int secs)
{
static const QTime zeroTime{0, 0};
@@ -283,44 +279,12 @@ GamesProxyModel::GamesProxyModel(QObject *parent, const UserListProxy *_userList
setDynamicSortFilter(true);
}
-void GamesProxyModel::setGameFilters(bool _hideBuddiesOnlyGames,
- bool _hideIgnoredUserGames,
- bool _hideFullGames,
- bool _hideGamesThatStarted,
- bool _hidePasswordProtectedGames,
- bool _hideNotBuddyCreatedGames,
- bool _hideOpenDecklistGames,
- const QString &_gameNameFilter,
- const QStringList &_creatorNameFilters,
- const QSet &_gameTypeFilter,
- int _maxPlayersFilterMin,
- int _maxPlayersFilterMax,
- const QTime &_maxGameAge,
- bool _showOnlyIfSpectatorsCanWatch,
- bool _showSpectatorPasswordProtected,
- bool _showOnlyIfSpectatorsCanChat,
- bool _showOnlyIfSpectatorsCanSeeHands)
+void GamesProxyModel::setGameFilters(const GameFilterConfigs &_filters)
{
#if (QT_VERSION >= QT_VERSION_CHECK(6, 9, 0))
beginFilterChange();
#endif
- hideBuddiesOnlyGames = _hideBuddiesOnlyGames;
- hideIgnoredUserGames = _hideIgnoredUserGames;
- hideFullGames = _hideFullGames;
- hideGamesThatStarted = _hideGamesThatStarted;
- hidePasswordProtectedGames = _hidePasswordProtectedGames;
- hideNotBuddyCreatedGames = _hideNotBuddyCreatedGames;
- hideOpenDecklistGames = _hideOpenDecklistGames;
- gameNameFilter = _gameNameFilter;
- creatorNameFilters = _creatorNameFilters;
- gameTypeFilter = _gameTypeFilter;
- maxPlayersFilterMin = _maxPlayersFilterMin;
- maxPlayersFilterMax = _maxPlayersFilterMax;
- maxGameAge = _maxGameAge;
- showOnlyIfSpectatorsCanWatch = _showOnlyIfSpectatorsCanWatch;
- showSpectatorPasswordProtected = _showSpectatorPasswordProtected;
- showOnlyIfSpectatorsCanChat = _showOnlyIfSpectatorsCanChat;
- showOnlyIfSpectatorsCanSeeHands = _showOnlyIfSpectatorsCanSeeHands;
+ filters = _filters;
#if (QT_VERSION >= QT_VERSION_CHECK(6, 10, 0))
endFilterChange(QSortFilterProxyModel::Direction::Rows);
#else
@@ -346,18 +310,12 @@ int GamesProxyModel::getNumFilteredGames() const
void GamesProxyModel::resetFilterParameters()
{
- setGameFilters(false, false, false, false, false, false, false, QString(), QStringList(), {},
- DEFAULT_MAX_PLAYERS_MIN, DEFAULT_MAX_PLAYERS_MAX, DEFAULT_MAX_GAME_AGE, false, false, false, false);
+ setGameFilters({});
}
bool GamesProxyModel::areFilterParametersSetToDefaults() const
{
- return !hideFullGames && !hideGamesThatStarted && !hidePasswordProtectedGames && !hideBuddiesOnlyGames &&
- !hideOpenDecklistGames && !hideIgnoredUserGames && !hideNotBuddyCreatedGames && gameNameFilter.isEmpty() &&
- creatorNameFilters.isEmpty() && gameTypeFilter.isEmpty() && maxPlayersFilterMin == DEFAULT_MAX_PLAYERS_MIN &&
- maxPlayersFilterMax == DEFAULT_MAX_PLAYERS_MAX && maxGameAge == DEFAULT_MAX_GAME_AGE &&
- !showOnlyIfSpectatorsCanWatch && !showSpectatorPasswordProtected && !showOnlyIfSpectatorsCanChat &&
- !showOnlyIfSpectatorsCanSeeHands;
+ return filters.isDefault();
}
void GamesProxyModel::loadFilterParameters(const QMap &allGameTypes)
@@ -373,44 +331,44 @@ void GamesProxyModel::loadFilterParameters(const QMap &allGameType
}
}
- setGameFilters(gameFilters.isHideBuddiesOnlyGames(), gameFilters.isHideIgnoredUserGames(),
- gameFilters.isHideFullGames(), gameFilters.isHideGamesThatStarted(),
- gameFilters.isHidePasswordProtectedGames(), gameFilters.isHideNotBuddyCreatedGames(),
- gameFilters.isHideOpenDecklistGames(), gameFilters.getGameNameFilter(),
- gameFilters.getCreatorNameFilters(), newGameTypeFilter, gameFilters.getMinPlayers(),
- gameFilters.getMaxPlayers(), gameFilters.getMaxGameAge(),
- gameFilters.isShowOnlyIfSpectatorsCanWatch(), gameFilters.isShowSpectatorPasswordProtected(),
- gameFilters.isShowOnlyIfSpectatorsCanChat(), gameFilters.isShowOnlyIfSpectatorsCanSeeHands());
+ setGameFilters({gameFilters.isHideBuddiesOnlyGames(), gameFilters.isHideIgnoredUserGames(),
+ gameFilters.isHideFullGames(), gameFilters.isHideGamesThatStarted(),
+ gameFilters.isHidePasswordProtectedGames(), gameFilters.isHideNotBuddyCreatedGames(),
+ gameFilters.isHideOpenDecklistGames(), gameFilters.getGameNameFilter(),
+ gameFilters.getCreatorNameFilters(), newGameTypeFilter, gameFilters.getMinPlayers(),
+ gameFilters.getMaxPlayers(), gameFilters.getMaxGameAge(),
+ gameFilters.isShowOnlyIfSpectatorsCanWatch(), gameFilters.isShowSpectatorPasswordProtected(),
+ gameFilters.isShowOnlyIfSpectatorsCanChat(), gameFilters.isShowOnlyIfSpectatorsCanSeeHands()});
}
void GamesProxyModel::saveFilterParameters(const QMap &allGameTypes)
{
GameFiltersSettings &gameFilters = SettingsCache::instance().gameFilters();
- gameFilters.setHideBuddiesOnlyGames(hideBuddiesOnlyGames);
- gameFilters.setHideFullGames(hideFullGames);
- gameFilters.setHideGamesThatStarted(hideGamesThatStarted);
- gameFilters.setHidePasswordProtectedGames(hidePasswordProtectedGames);
- gameFilters.setHideIgnoredUserGames(hideIgnoredUserGames);
- gameFilters.setHideNotBuddyCreatedGames(hideNotBuddyCreatedGames);
- gameFilters.setHideOpenDecklistGames(hideOpenDecklistGames);
- gameFilters.setGameNameFilter(gameNameFilter);
- gameFilters.setCreatorNameFilters(creatorNameFilters);
+ gameFilters.setHideBuddiesOnlyGames(filters.hideBuddiesOnlyGames);
+ gameFilters.setHideFullGames(filters.hideFullGames);
+ gameFilters.setHideGamesThatStarted(filters.hideGamesThatStarted);
+ gameFilters.setHidePasswordProtectedGames(filters.hidePasswordProtectedGames);
+ gameFilters.setHideIgnoredUserGames(filters.hideIgnoredUserGames);
+ gameFilters.setHideNotBuddyCreatedGames(filters.hideNotBuddyCreatedGames);
+ gameFilters.setHideOpenDecklistGames(filters.hideOpenDecklistGames);
+ gameFilters.setGameNameFilter(filters.gameNameFilter);
+ gameFilters.setCreatorNameFilters(filters.creatorNameFilters);
QMapIterator gameTypeIterator(allGameTypes);
while (gameTypeIterator.hasNext()) {
gameTypeIterator.next();
- bool enabled = gameTypeFilter.contains(gameTypeIterator.key());
+ bool enabled = filters.gameTypeFilter.contains(gameTypeIterator.key());
gameFilters.setGameTypeEnabled(gameTypeIterator.value(), enabled);
}
- gameFilters.setMinPlayers(maxPlayersFilterMin);
- gameFilters.setMaxPlayers(maxPlayersFilterMax);
- gameFilters.setMaxGameAge(maxGameAge);
+ gameFilters.setMinPlayers(filters.maxPlayersFilterMin);
+ gameFilters.setMaxPlayers(filters.maxPlayersFilterMax);
+ gameFilters.setMaxGameAge(filters.maxGameAge);
- gameFilters.setShowOnlyIfSpectatorsCanWatch(showOnlyIfSpectatorsCanWatch);
- gameFilters.setShowSpectatorPasswordProtected(showSpectatorPasswordProtected);
- gameFilters.setShowOnlyIfSpectatorsCanChat(showOnlyIfSpectatorsCanChat);
- gameFilters.setShowOnlyIfSpectatorsCanSeeHands(showOnlyIfSpectatorsCanSeeHands);
+ gameFilters.setShowOnlyIfSpectatorsCanWatch(filters.showOnlyIfSpectatorsCanWatch);
+ gameFilters.setShowSpectatorPasswordProtected(filters.showSpectatorPasswordProtected);
+ gameFilters.setShowOnlyIfSpectatorsCanChat(filters.showOnlyIfSpectatorsCanChat);
+ gameFilters.setShowOnlyIfSpectatorsCanSeeHands(filters.showOnlyIfSpectatorsCanSeeHands);
}
bool GamesProxyModel::filterAcceptsRow(int sourceRow, const QModelIndex & /*sourceParent*/) const
@@ -431,33 +389,35 @@ bool GamesProxyModel::filterAcceptsRow(int sourceRow) const
const ServerInfo_Game &game = model->getGame(sourceRow);
- if (hideBuddiesOnlyGames && game.only_buddies()) {
+ if (filters.hideBuddiesOnlyGames && game.only_buddies()) {
return false;
}
- if (hideOpenDecklistGames && game.share_decklists_on_load()) {
+ if (filters.hideOpenDecklistGames && game.share_decklists_on_load()) {
return false;
}
- if (hideIgnoredUserGames && userListProxy->isUserIgnored(QString::fromStdString(game.creator_info().name()))) {
+ if (filters.hideIgnoredUserGames &&
+ userListProxy->isUserIgnored(QString::fromStdString(game.creator_info().name()))) {
return false;
}
- if (hideNotBuddyCreatedGames && !userListProxy->isUserBuddy(QString::fromStdString(game.creator_info().name()))) {
+ if (filters.hideNotBuddyCreatedGames &&
+ !userListProxy->isUserBuddy(QString::fromStdString(game.creator_info().name()))) {
return false;
}
- if (hideFullGames && game.player_count() == game.max_players())
+ if (filters.hideFullGames && game.player_count() == game.max_players())
return false;
- if (hideGamesThatStarted && game.started())
+ if (filters.hideGamesThatStarted && game.started())
return false;
if (!userListProxy->isOwnUserRegistered())
if (game.only_registered())
return false;
- if (hidePasswordProtectedGames && game.with_password())
+ if (filters.hidePasswordProtectedGames && game.with_password())
return false;
- if (!gameNameFilter.isEmpty())
- if (!QString::fromStdString(game.description()).contains(gameNameFilter, Qt::CaseInsensitive))
+ if (!filters.gameNameFilter.isEmpty())
+ if (!QString::fromStdString(game.description()).contains(filters.gameNameFilter, Qt::CaseInsensitive))
return false;
- if (!creatorNameFilters.isEmpty()) {
+ if (!filters.creatorNameFilters.isEmpty()) {
bool found = false;
- for (const auto &createNameFilter : creatorNameFilters) {
+ for (const auto &createNameFilter : filters.creatorNameFilters) {
if (QString::fromStdString(game.creator_info().name()).contains(createNameFilter, Qt::CaseInsensitive)) {
found = true;
}
@@ -470,33 +430,34 @@ bool GamesProxyModel::filterAcceptsRow(int sourceRow) const
QSet gameTypes;
for (int i = 0; i < game.game_types_size(); ++i)
gameTypes.insert(game.game_types(i));
- if (!gameTypeFilter.isEmpty() && gameTypes.intersect(gameTypeFilter).isEmpty())
+ if (!filters.gameTypeFilter.isEmpty() && gameTypes.intersect(filters.gameTypeFilter).isEmpty())
return false;
- if (game.max_players() < maxPlayersFilterMin)
+ if (static_cast(game.max_players()) < filters.maxPlayersFilterMin)
return false;
- if (game.max_players() > maxPlayersFilterMax)
+ if (static_cast(game.max_players()) > filters.maxPlayersFilterMax)
return false;
- if (maxGameAge.isValid()) {
+ if (filters.maxGameAge.isValid()) {
QDateTime now = QDateTime::currentDateTimeUtc();
qint64 signed_start_time = game.start_time(); // cast to 64 bit value to allow signed value
QDateTime total = now.addSecs(-signed_start_time); // a 32 bit value would wrap at 2038-1-19
// games shouldn't have negative ages but we'll not filter them
// because qtime wraps after a day we consider all games older than a day to be too old
- if (total.isValid() && total.date() >= epochDate && (total.date() > epochDate || total.time() > maxGameAge)) {
+ if (total.isValid() && total.date() >= epochDate &&
+ (total.date() > epochDate || total.time() > filters.maxGameAge)) {
return false;
}
}
- if (showOnlyIfSpectatorsCanWatch) {
+ if (filters.showOnlyIfSpectatorsCanWatch) {
if (!game.spectators_allowed())
return false;
- if (!showSpectatorPasswordProtected && game.spectators_need_password())
+ if (!filters.showSpectatorPasswordProtected && game.spectators_need_password())
return false;
- if (showOnlyIfSpectatorsCanChat && !game.spectators_can_chat())
+ if (filters.showOnlyIfSpectatorsCanChat && !game.spectators_can_chat())
return false;
- if (showOnlyIfSpectatorsCanSeeHands && !game.spectators_omniscient())
+ if (filters.showOnlyIfSpectatorsCanSeeHands && !game.spectators_omniscient())
return false;
}
return true;
diff --git a/cockatrice/src/interface/widgets/server/games_model.h b/cockatrice/src/interface/widgets/server/games_model.h
index c6884093d..b5f86c291 100644
--- a/cockatrice/src/interface/widgets/server/games_model.h
+++ b/cockatrice/src/interface/widgets/server/games_model.h
@@ -1,6 +1,7 @@
#ifndef GAMESMODEL_H
#define GAMESMODEL_H
+#include "game_filter_configs.h"
#include "game_type_map.h"
#include
@@ -121,22 +122,7 @@ private:
// - loadFilterParameters()
// - saveFilterParameters()
// - filterAcceptsRow()
- bool hideBuddiesOnlyGames;
- bool hideIgnoredUserGames;
- bool hideFullGames;
- bool hideGamesThatStarted;
- bool hidePasswordProtectedGames;
- bool hideNotBuddyCreatedGames;
- bool hideOpenDecklistGames;
- QString gameNameFilter;
- QStringList creatorNameFilters;
- QSet gameTypeFilter;
- quint32 maxPlayersFilterMin, maxPlayersFilterMax;
- QTime maxGameAge;
- bool showOnlyIfSpectatorsCanWatch;
- bool showSpectatorPasswordProtected;
- bool showOnlyIfSpectatorsCanChat;
- bool showOnlyIfSpectatorsCanSeeHands;
+ GameFilterConfigs filters;
signals:
void filtersChanged();
@@ -150,182 +136,15 @@ public:
explicit GamesProxyModel(QObject *parent = nullptr, const UserListProxy *_userListProxy = nullptr);
// Getters for filter parameters
- [[nodiscard]] bool getHideBuddiesOnlyGames() const
+ [[nodiscard]] const GameFilterConfigs &getFilters() const
{
- return hideBuddiesOnlyGames;
- }
- [[nodiscard]] bool getHideIgnoredUserGames() const
- {
- return hideIgnoredUserGames;
- }
- [[nodiscard]] bool getHideFullGames() const
- {
- return hideFullGames;
- }
- [[nodiscard]] bool getHideGamesThatStarted() const
- {
- return hideGamesThatStarted;
- }
- [[nodiscard]] bool getHidePasswordProtectedGames() const
- {
- return hidePasswordProtectedGames;
- }
- [[nodiscard]] bool getHideNotBuddyCreatedGames() const
- {
- return hideNotBuddyCreatedGames;
- }
- [[nodiscard]] bool getHideOpenDecklistGames() const
- {
- return hideOpenDecklistGames;
- }
- [[nodiscard]] QString getGameNameFilter() const
- {
- return gameNameFilter;
- }
- [[nodiscard]] QStringList getCreatorNameFilters() const
- {
- return creatorNameFilters;
- }
- [[nodiscard]] QSet getGameTypeFilter() const
- {
- return gameTypeFilter;
- }
- [[nodiscard]] int getMaxPlayersFilterMin() const
- {
- return maxPlayersFilterMin;
- }
- [[nodiscard]] int getMaxPlayersFilterMax() const
- {
- return maxPlayersFilterMax;
- }
- [[nodiscard]] const QTime &getMaxGameAge() const
- {
- return maxGameAge;
- }
- [[nodiscard]] bool getShowOnlyIfSpectatorsCanWatch() const
- {
- return showOnlyIfSpectatorsCanWatch;
- }
- [[nodiscard]] bool getShowSpectatorPasswordProtected() const
- {
- return showSpectatorPasswordProtected;
- }
- [[nodiscard]] bool getShowOnlyIfSpectatorsCanChat() const
- {
- return showOnlyIfSpectatorsCanChat;
- }
- [[nodiscard]] bool getShowOnlyIfSpectatorsCanSeeHands() const
- {
- return showOnlyIfSpectatorsCanSeeHands;
+ return filters;
}
/**
* @brief Sets all game filters at once.
*/
- void setGameFilters(bool _hideBuddiesOnlyGames,
- bool _hideIgnoredUserGames,
- bool _hideFullGames,
- bool _hideGamesThatStarted,
- bool _hidePasswordProtectedGames,
- bool _hideNotBuddyCreatedGames,
- bool _hideOpenDecklistGames,
- const QString &_gameNameFilter,
- const QStringList &_creatorNameFilter,
- const QSet &_gameTypeFilter,
- int _maxPlayersFilterMin,
- int _maxPlayersFilterMax,
- const QTime &_maxGameAge,
- bool _showOnlyIfSpectatorsCanWatch,
- bool _showSpectatorPasswordProtected,
- bool _showOnlyIfSpectatorsCanChat,
- bool _showOnlyIfSpectatorsCanSeeHands);
-
- // Individual setters
- void setHideBuddiesOnlyGames(bool value)
- {
- hideBuddiesOnlyGames = value;
- refresh();
- }
- void setHideIgnoredUserGames(bool value)
- {
- hideIgnoredUserGames = value;
- refresh();
- }
- void setHideFullGames(bool value)
- {
- hideFullGames = value;
- refresh();
- }
- void setHideGamesThatStarted(bool value)
- {
- hideGamesThatStarted = value;
- refresh();
- }
- void setHidePasswordProtectedGames(bool value)
- {
- hidePasswordProtectedGames = value;
- refresh();
- }
- void setHideNotBuddyCreatedGames(bool value)
- {
- hideNotBuddyCreatedGames = value;
- refresh();
- }
- void setHideOpenDecklistGames(bool value)
- {
- hideOpenDecklistGames = value;
- refresh();
- }
- void setGameNameFilter(const QString &value)
- {
- gameNameFilter = value;
- refresh();
- }
- void setCreatorNameFilters(const QStringList &values)
- {
- creatorNameFilters = values;
- refresh();
- }
- void setGameTypeFilter(const QSet &value)
- {
- gameTypeFilter = value;
- refresh();
- }
- void setMaxPlayersFilterMin(int value)
- {
- maxPlayersFilterMin = value;
- refresh();
- }
- void setMaxPlayersFilterMax(int value)
- {
- maxPlayersFilterMax = value;
- refresh();
- }
- void setMaxGameAge(const QTime &value)
- {
- maxGameAge = value;
- refresh();
- }
- void setShowOnlyIfSpectatorsCanWatch(bool value)
- {
- showOnlyIfSpectatorsCanWatch = value;
- refresh();
- }
- void setShowSpectatorPasswordProtected(bool value)
- {
- showSpectatorPasswordProtected = value;
- refresh();
- }
- void setShowOnlyIfSpectatorsCanChat(bool value)
- {
- showOnlyIfSpectatorsCanChat = value;
- refresh();
- }
- void setShowOnlyIfSpectatorsCanSeeHands(bool value)
- {
- showOnlyIfSpectatorsCanSeeHands = value;
- refresh();
- }
+ void setGameFilters(const GameFilterConfigs &_filters);
/**
* @brief Returns the number of games filtered out by the current filter.
From caf2bb9deda25bc2e281b100a24259ebcce21dd0 Mon Sep 17 00:00:00 2001
From: BruebachL <44814898+BruebachL@users.noreply.github.com>
Date: Sat, 9 May 2026 12:50:53 +0200
Subject: [PATCH 006/121] Pull client networking out of window_main and into
remote_connection_controller (#6796)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* Pull client networking out of window_main and into remote_connection_controller
Took 2 minutes
* Things.
Took 13 minutes
---------
Co-authored-by: Lukas Brübach
---
cockatrice/CMakeLists.txt | 1 +
.../remote_connection_controller.cpp | 581 ++++++++++++++++++
.../remote_connection_controller.h | 98 +++
cockatrice/src/interface/window_main.cpp | 489 +--------------
cockatrice/src/interface/window_main.h | 27 +-
5 files changed, 703 insertions(+), 493 deletions(-)
create mode 100644 cockatrice/src/client/network/connection_controller/remote_connection_controller.cpp
create mode 100644 cockatrice/src/client/network/connection_controller/remote_connection_controller.h
diff --git a/cockatrice/CMakeLists.txt b/cockatrice/CMakeLists.txt
index 5884db9e3..1d07371d8 100644
--- a/cockatrice/CMakeLists.txt
+++ b/cockatrice/CMakeLists.txt
@@ -7,6 +7,7 @@ project(Cockatrice VERSION "${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}.${
set(cockatrice_SOURCES
${VERSION_STRING_CPP}
# sort by alphabetical order, so that there is no debate about where to add new sources to the list
+ src/client/network/connection_controller/remote_connection_controller.cpp
src/client/network/update/client/update_downloader.cpp
src/client/network/interfaces/deck_stats_interface.cpp
src/client/network/interfaces/tapped_out_interface.cpp
diff --git a/cockatrice/src/client/network/connection_controller/remote_connection_controller.cpp b/cockatrice/src/client/network/connection_controller/remote_connection_controller.cpp
new file mode 100644
index 000000000..f517acdc4
--- /dev/null
+++ b/cockatrice/src/client/network/connection_controller/remote_connection_controller.cpp
@@ -0,0 +1,581 @@
+#include "remote_connection_controller.h"
+
+#include "../../settings/cache_settings.h"
+#include "../interface/widgets/dialogs/dlg_connect.h"
+#include "../interface/widgets/dialogs/dlg_forgot_password_challenge.h"
+#include "../interface/widgets/dialogs/dlg_forgot_password_request.h"
+#include "../interface/widgets/dialogs/dlg_forgot_password_reset.h"
+#include "../interface/widgets/dialogs/dlg_register.h"
+#include "../interface/widgets/utility/get_text_with_max.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+ConnectionController::ConnectionController(QWidget *dialogParent, QObject *parent)
+ : QObject(parent), dialogParent(dialogParent)
+{
+ remoteClient = new RemoteClient(nullptr, &SettingsCache::instance());
+
+ clientThread = new QThread(this);
+ remoteClient->moveToThread(clientThread);
+ clientThread->start();
+
+ wireClientSignals();
+}
+
+ConnectionController::~ConnectionController()
+{
+ remoteClient->deleteLater();
+ clientThread->wait();
+}
+
+void ConnectionController::wireClientSignals()
+{
+ connect(remoteClient, &RemoteClient::connectionClosedEventReceived, this,
+ &ConnectionController::onConnectionClosedEvent);
+
+ connect(remoteClient, &RemoteClient::serverShutdownEventReceived, this,
+ &ConnectionController::onServerShutdownEvent);
+
+ connect(remoteClient, &RemoteClient::statusChanged, this, &ConnectionController::onStatusChanged);
+
+ connect(remoteClient, &RemoteClient::userInfoChanged, this, &ConnectionController::onUserInfoReceived,
+ Qt::BlockingQueuedConnection);
+
+ connect(remoteClient, &RemoteClient::loginError, this,
+ [this](Response::ResponseCode r, QString rs, quint32 et, QList mf) {
+ onLoginError(static_cast(r), rs, et, mf);
+ });
+
+ connect(remoteClient, &RemoteClient::registerError, this,
+ [this](Response::ResponseCode r, QString rs, quint32 et) { onRegisterError(static_cast(r), rs, et); });
+
+ connect(remoteClient, &RemoteClient::activateError, this, &ConnectionController::onActivateError);
+ connect(remoteClient, &RemoteClient::socketError, this, &ConnectionController::onSocketError);
+ connect(remoteClient, &RemoteClient::serverTimeout, this, &ConnectionController::onServerTimeout);
+
+ connect(remoteClient, &RemoteClient::protocolVersionMismatch, this,
+ &ConnectionController::onProtocolVersionMismatch);
+
+ connect(remoteClient, &RemoteClient::registerAccepted, this, &ConnectionController::onRegisterAccepted);
+
+ connect(remoteClient, &RemoteClient::registerAcceptedNeedsActivate, this,
+ &ConnectionController::onRegisterAcceptedNeedsActivate);
+
+ connect(remoteClient, &RemoteClient::activateAccepted, this, &ConnectionController::onActivateAccepted);
+
+ connect(remoteClient, &RemoteClient::notifyUserAboutUpdate, this, &ConnectionController::onNotifyUserAboutUpdate);
+
+ connect(remoteClient, &RemoteClient::sigForgotPasswordSuccess, this,
+ &ConnectionController::onForgotPasswordSuccess);
+
+ connect(remoteClient, &RemoteClient::sigForgotPasswordError, this, &ConnectionController::onForgotPasswordError);
+
+ connect(remoteClient, &RemoteClient::sigPromptForForgotPasswordReset, this,
+ &ConnectionController::onPromptForgotPasswordReset);
+
+ connect(remoteClient, &RemoteClient::sigPromptForForgotPasswordChallenge, this,
+ &ConnectionController::onPromptForgotPasswordChallenge);
+}
+
+void ConnectionController::connectToServer()
+{
+ dlgConnect = new DlgConnect(dialogParent);
+ connect(dlgConnect, &DlgConnect::sigStartForgotPasswordRequest, this, &ConnectionController::forgotPasswordRequest);
+
+ if (dlgConnect->exec()) {
+ remoteClient->connectToServer(dlgConnect->getHost(), static_cast(dlgConnect->getPort()),
+ dlgConnect->getPlayerName(), dlgConnect->getPassword());
+ }
+}
+
+void ConnectionController::connectToServerDirect(const QString &host,
+ unsigned int port,
+ const QString &playerName,
+ const QString &password)
+{
+ remoteClient->connectToServer(host, port, playerName, password);
+}
+
+void ConnectionController::disconnectFromServer()
+{
+ remoteClient->disconnectFromServer();
+}
+
+void ConnectionController::registerToServer()
+{
+ DlgRegister dlg(dialogParent);
+ if (dlg.exec()) {
+ remoteClient->registerToServer(dlg.getHost(), static_cast(dlg.getPort()), dlg.getPlayerName(),
+ dlg.getPassword(), dlg.getEmail(), dlg.getCountry(), dlg.getRealName());
+ }
+}
+
+void ConnectionController::forgotPasswordRequest()
+{
+ DlgForgotPasswordRequest dlg(dialogParent);
+ if (dlg.exec()) {
+ remoteClient->requestForgotPasswordToServer(dlg.getHost(), static_cast(dlg.getPort()),
+ dlg.getPlayerName());
+ }
+}
+
+void ConnectionController::onConnectionClosedEvent(const Event_ConnectionClosed &event)
+{
+ remoteClient->disconnectFromServer();
+
+ QString reasonStr;
+ switch (event.reason()) {
+ case Event_ConnectionClosed::USER_LIMIT_REACHED: {
+ reasonStr = tr("The server has reached its maximum user capacity, please check back later.");
+ break;
+ }
+ case Event_ConnectionClosed::TOO_MANY_CONNECTIONS: {
+ reasonStr = tr("There are too many concurrent connections from your address.");
+ break;
+ }
+ case Event_ConnectionClosed::BANNED: {
+ reasonStr = tr("Banned by moderator");
+ if (event.has_end_time())
+ reasonStr.append(
+ "\n" + tr("Expected end time: %1").arg(QDateTime::fromSecsSinceEpoch(event.end_time()).toString()));
+ else
+ reasonStr.append("\n" + tr("This ban lasts indefinitely."));
+ if (event.has_reason_str())
+ reasonStr.append("\n\n" + QString::fromStdString(event.reason_str()));
+ break;
+ }
+ case Event_ConnectionClosed::SERVER_SHUTDOWN: {
+ reasonStr = tr("Scheduled server shutdown.");
+ break;
+ }
+ case Event_ConnectionClosed::USERNAMEINVALID: {
+ reasonStr = tr("Invalid username.");
+ break;
+ }
+ case Event_ConnectionClosed::LOGGEDINELSEWERE: {
+ reasonStr = tr("You have been logged out due to logging in at another location.");
+ break;
+ }
+ default:
+ reasonStr = QString::fromStdString(event.reason_str());
+ }
+
+ QMessageBox::critical(dialogParent, tr("Connection closed"),
+ tr("The server has terminated your connection.\nReason: %1").arg(reasonStr));
+}
+
+void ConnectionController::onServerShutdownEvent(const Event_ServerShutdown &event)
+{
+ serverShutdownMessageBox.setInformativeText(tr("The server is going to be restarted in %n minute(s).\nAll running "
+ "games will be lost.\nReason for shutdown: %1",
+ "", event.minutes())
+ .arg(QString::fromStdString(event.reason())));
+ serverShutdownMessageBox.setIconPixmap(QPixmap("theme:cockatrice").scaled(64, 64));
+ serverShutdownMessageBox.setText(tr("Scheduled server shutdown"));
+ serverShutdownMessageBox.setWindowModality(Qt::ApplicationModal);
+ serverShutdownMessageBox.setVisible(true);
+}
+
+void ConnectionController::onStatusChanged(ClientStatus status)
+{
+ // Update the window title first, then let MainWindow handle its own UI
+ // state via the forwarded signal
+ updateWindowTitle();
+ emit statusChanged(status);
+
+ // TabSupervisor::stop() needs calling on disconnect; start() is driven by
+ // onUserInfoReceived → tabSupervisorStartRequested.
+ if (status == StatusDisconnected) {
+ emit tabSupervisorStopRequested();
+ }
+}
+
+void ConnectionController::onUserInfoReceived(const ServerInfo_User &info)
+{
+ emit tabSupervisorStartRequested(info);
+}
+
+void ConnectionController::onLoginError(int r,
+ QString reasonStr,
+ quint32 endTime,
+ const QList &missingFeatures)
+{
+ switch (static_cast(r)) {
+ case Response::RespClientUpdateRequired: {
+ QString formatted = "Missing Features: ";
+ for (int i = 0; i < missingFeatures.size(); ++i) {
+ formatted.append(QString("\n %1").arg(QChar(0x2022)) + " " + missingFeatures.value(i));
+ }
+
+ QMessageBox msgBox(dialogParent);
+ msgBox.setIcon(QMessageBox::Critical);
+ msgBox.setWindowTitle(tr("Failed Login"));
+ msgBox.setText(tr("Your client seems to be missing features this server requires for connection.") +
+ "\n\n" + tr("To update your client, go to 'Help -> Check for Client Updates'."));
+ msgBox.setDetailedText(formatted);
+ msgBox.exec();
+ break;
+ }
+
+ case Response::RespWrongPassword: {
+ QMessageBox::critical(dialogParent, tr("Error"),
+ tr("Incorrect username or password. "
+ "Please check your authentication information and try again."));
+ break;
+ }
+
+ case Response::RespWouldOverwriteOldSession: {
+ QMessageBox::critical(dialogParent, tr("Error"),
+ tr("There is already an active session using this user name.\n"
+ "Please close that session first and re-login."));
+ break;
+ }
+
+ case Response::RespUserIsBanned: {
+ QString bannedStr =
+ endTime ? tr("You are banned until %1.").arg(QDateTime::fromSecsSinceEpoch(endTime).toString())
+ : tr("You are banned indefinitely.");
+ if (!reasonStr.isEmpty())
+ bannedStr.append("\n\n" + reasonStr);
+ QMessageBox::critical(dialogParent, tr("Error"), bannedStr);
+ break;
+ }
+
+ case Response::RespUsernameInvalid: {
+ QMessageBox::critical(dialogParent, tr("Error"), extractInvalidUsernameMessage(reasonStr));
+ break;
+ }
+
+ case Response::RespRegistrationRequired: {
+ if (QMessageBox::question(dialogParent, tr("Error"),
+ tr("This server requires user registration. Do you want to register now?"),
+ QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) {
+ registerToServer();
+ }
+ return; // don't re-prompt connect
+ }
+
+ case Response::RespClientIdRequired: {
+ QMessageBox::critical(dialogParent, tr("Error"),
+ tr("This server requires client IDs. Your client is either failing to generate an "
+ "ID or you are running a modified client.\n"
+ "Please close and reopen your client to try again."));
+ break;
+ }
+
+ case Response::RespContextError: {
+ QMessageBox::critical(dialogParent, tr("Error"),
+ tr("An internal error has occurred, please close and reopen Cockatrice before "
+ "trying again.\nIf the error persists, ensure you are running the latest "
+ "version of the software and if needed contact the software developers."));
+ break;
+ }
+
+ case Response::RespAccountNotActivated: {
+ bool ok = false;
+ QString token =
+ getTextWithMax(dialogParent, tr("Account activation"),
+ tr("Your account has not been activated yet.\n"
+ "You need to provide the activation token received in the activation email."),
+ QLineEdit::Normal, QString(), &ok);
+
+ if (ok && !token.isEmpty()) {
+ remoteClient->activateToServer(token);
+ return;
+ }
+ remoteClient->disconnectFromServer();
+ return;
+ }
+
+ case Response::RespServerFull: {
+ QMessageBox::critical(dialogParent, tr("Server Full"),
+ tr("The server has reached its maximum user capacity, please check back later."));
+ break;
+ }
+
+ default: {
+ QMessageBox::critical(dialogParent, tr("Error"),
+ tr("Unknown login error: %1").arg(r) +
+ tr("\nThis usually means that your client version is out of date, and the server "
+ "sent a reply your client doesn't understand."));
+ break;
+ }
+ }
+
+ // Re-open the connect dialog after any handled error
+ connectToServer();
+}
+
+void ConnectionController::onRegisterError(int r, QString reasonStr, quint32 endTime)
+{
+ switch (static_cast(r)) {
+ case Response::RespRegistrationDisabled: {
+ QMessageBox::critical(dialogParent, tr("Registration denied"),
+ tr("Registration is currently disabled on this server"));
+ break;
+ }
+ case Response::RespUserAlreadyExists: {
+ QMessageBox::critical(dialogParent, tr("Registration denied"),
+ tr("There is already an existing account with the same user name."));
+ break;
+ }
+ case Response::RespEmailRequiredToRegister: {
+ QMessageBox::critical(dialogParent, tr("Registration denied"),
+ tr("It's mandatory to specify a valid email address when registering."));
+ break;
+ }
+ case Response::RespEmailBlackListed: {
+ if (reasonStr.isEmpty()) {
+ reasonStr =
+ "The email address provider used during registration has been blocked from use on this server.";
+ }
+ QMessageBox::critical(dialogParent, tr("Registration denied"), reasonStr);
+ break;
+ }
+ case Response::RespTooManyRequests: {
+ QMessageBox::critical(dialogParent, tr("Registration denied"),
+ tr("It appears you are attempting to register a new account on this server yet you "
+ "already have an account registered with the email provided. This server "
+ "restricts the number of accounts a user can register per address. Please "
+ "contact the server operator for further assistance or to obtain your "
+ "credential information."));
+ break;
+ }
+ case Response::RespPasswordTooShort: {
+ QMessageBox::critical(dialogParent, tr("Registration denied"), tr("Password too short."));
+ break;
+ }
+ case Response::RespUserIsBanned: {
+ QString bannedStr =
+ endTime ? tr("You are banned until %1.").arg(QDateTime::fromSecsSinceEpoch(endTime).toString())
+ : tr("You are banned indefinitely.");
+ if (!reasonStr.isEmpty())
+ bannedStr.append("\n\n" + reasonStr);
+ QMessageBox::critical(dialogParent, tr("Error"), bannedStr);
+ break;
+ }
+ case Response::RespUsernameInvalid: {
+ QMessageBox::critical(dialogParent, tr("Error"), extractInvalidUsernameMessage(reasonStr));
+ break;
+ }
+ case Response::RespRegistrationFailed: {
+ QMessageBox::critical(dialogParent, tr("Error"),
+ tr("Registration failed for a technical problem on the server."));
+ break;
+ }
+ case Response::RespNotConnected: {
+ QMessageBox::critical(dialogParent, tr("Error"), tr("The connection to the server has been lost."));
+ break;
+ }
+ default: {
+ QMessageBox::critical(dialogParent, tr("Error"),
+ tr("Unknown registration error: %1").arg(r) +
+ tr("\nThis usually means that your client version is out of date, and the server "
+ "sent a reply your client doesn't understand."));
+ break;
+ }
+ }
+
+ registerToServer();
+}
+
+void ConnectionController::onActivateError()
+{
+ QMessageBox::critical(dialogParent, tr("Error"), tr("Account activation failed"));
+ remoteClient->disconnectFromServer();
+ connectToServer();
+}
+
+void ConnectionController::onSocketError(const QString &errorStr)
+{
+ QMessageBox::critical(dialogParent, tr("Error"), tr("Socket error: %1").arg(errorStr));
+ connectToServer();
+}
+
+void ConnectionController::onServerTimeout()
+{
+ QMessageBox::critical(dialogParent, tr("Error"), tr("Server timeout"));
+ connectToServer();
+}
+
+void ConnectionController::onProtocolVersionMismatch(int localVersion, int remoteVersion)
+{
+ if (localVersion > remoteVersion) {
+ QMessageBox::critical(dialogParent, tr("Error"),
+ tr("You are trying to connect to an obsolete server. Please downgrade your Cockatrice "
+ "version or connect to a suitable server.\n"
+ "Local version is %1, remote version is %2.")
+ .arg(localVersion)
+ .arg(remoteVersion));
+ } else {
+ QMessageBox::critical(dialogParent, tr("Error"),
+ tr("Your Cockatrice client is obsolete. Please update your Cockatrice version.\n"
+ "Local version is %1, remote version is %2.")
+ .arg(localVersion)
+ .arg(remoteVersion));
+ }
+}
+
+void ConnectionController::onRegisterAccepted()
+{
+ QMessageBox::information(dialogParent, tr("Success"), tr("Registration accepted.\nWill now login."));
+}
+
+void ConnectionController::onRegisterAcceptedNeedsActivate()
+{
+ // Server will send activation email; nothing to display here.
+}
+
+void ConnectionController::onActivateAccepted()
+{
+ QMessageBox::information(dialogParent, tr("Success"), tr("Account activation accepted.\nWill now login."));
+}
+
+void ConnectionController::onNotifyUserAboutUpdate()
+{
+ QMessageBox::information(
+ dialogParent, tr("Information"),
+ tr("This server supports additional features that your client doesn't have.\n"
+ "This is most likely not a problem, but this message might mean there is a new version of "
+ "Cockatrice available or this server is running a custom or pre-release version.\n\n"
+ "To update your client, go to Help -> Check for Updates."));
+}
+
+void ConnectionController::onForgotPasswordSuccess()
+{
+ QMessageBox::information(
+ dialogParent, tr("Reset Password"),
+ tr("Your password has been reset successfully, you can now log in using the new credentials."));
+ SettingsCache::instance().servers().setFPHostName("");
+ SettingsCache::instance().servers().setFPPort("");
+ SettingsCache::instance().servers().setFPPlayerName("");
+}
+
+void ConnectionController::onForgotPasswordError()
+{
+ QMessageBox::warning(
+ dialogParent, tr("Reset Password"),
+ tr("Failed to reset user account password, please contact the server operator to reset your password."));
+ SettingsCache::instance().servers().setFPHostName("");
+ SettingsCache::instance().servers().setFPPort("");
+ SettingsCache::instance().servers().setFPPlayerName("");
+}
+
+void ConnectionController::onPromptForgotPasswordReset()
+{
+ QMessageBox::information(dialogParent, tr("Reset Password"),
+ tr("Activation request received, please check your email for an activation token."));
+ DlgForgotPasswordReset dlg(dialogParent);
+ if (dlg.exec()) {
+ remoteClient->submitForgotPasswordResetToServer(dlg.getHost(), static_cast(dlg.getPort()),
+ dlg.getPlayerName(), dlg.getToken(), dlg.getPassword());
+ }
+}
+
+void ConnectionController::onPromptForgotPasswordChallenge()
+{
+ DlgForgotPasswordChallenge dlg(dialogParent);
+ if (dlg.exec()) {
+ remoteClient->submitForgotPasswordChallengeToServer(dlg.getHost(), static_cast(dlg.getPort()),
+ dlg.getPlayerName(), dlg.getEmail());
+ }
+}
+
+void ConnectionController::updateWindowTitle()
+{
+ const QString appName = QStringLiteral("Cockatrice");
+ QString title;
+
+ switch (remoteClient->getStatus()) {
+ case StatusConnecting: {
+ title = appName + " - " + tr("Connecting to %1...").arg(remoteClient->peerName());
+ break;
+ }
+ case StatusRegistering: {
+ title = appName + " - " +
+ tr("Registering to %1 as %2...").arg(remoteClient->peerName()).arg(remoteClient->getUserName());
+ break;
+ }
+ case StatusDisconnected: {
+ title = appName + " - " + tr("Disconnected");
+ break;
+ }
+ case StatusLoggingIn: {
+ title = appName + " - " + tr("Connected, logging in at %1").arg(remoteClient->peerName());
+ break;
+ }
+ case StatusLoggedIn: {
+ title = remoteClient->getUserName() + "@" + remoteClient->peerName();
+ break;
+ }
+ case StatusRequestingForgotPassword:
+ case StatusSubmitForgotPasswordChallenge:
+ case StatusSubmitForgotPasswordReset:
+ title = appName + " - " +
+ tr("Requesting forgotten password to %1 as %2...")
+ .arg(remoteClient->peerName())
+ .arg(remoteClient->getUserName());
+ break;
+ default:
+ title = appName;
+ }
+
+ emit windowTitleChanged(title);
+}
+
+// static
+QString ConnectionController::extractInvalidUsernameMessage(QString &in)
+{
+ QString out = tr("Invalid username.") + "
";
+ QStringList rules = in.split(QChar('|'));
+
+ if (rules.size() == 7 || rules.size() == 9) {
+ out += tr("Your username must respect these rules:") + "";
+
+ out += "- " + tr("is %1 - %2 characters long").arg(rules.at(0)).arg(rules.at(1)) + "
";
+ out += "- " + tr("can %1 contain lowercase characters").arg((rules.at(2).toInt() > 0) ? "" : tr("NOT")) +
+ "
";
+ out += "- " + tr("can %1 contain uppercase characters").arg((rules.at(3).toInt() > 0) ? "" : tr("NOT")) +
+ "
";
+ out +=
+ "- " + tr("can %1 contain numeric characters").arg((rules.at(4).toInt() > 0) ? "" : tr("NOT")) + "
";
+
+ if (rules.at(6).size() > 0)
+ out += "- " + tr("can contain the following punctuation: %1").arg(rules.at(6).toHtmlEscaped()) + "
";
+
+ out += "- " +
+ tr("first character can %1 be a punctuation mark").arg((rules.at(5).toInt() > 0) ? "" : tr("NOT")) +
+ "
";
+
+ if (rules.size() == 9) {
+ if (rules.at(7).size() > 0) {
+ QString words = rules.at(7).toHtmlEscaped();
+ if (words.startsWith("\n")) {
+ out += tr("no unacceptable language as specified by these server rules:",
+ "note that the following lines will not be translated");
+ for (QString &line : words.split("\n", Qt::SkipEmptyParts)) {
+ out += "- " + line + "
";
+ }
+ } else {
+ out += "- " + tr("can not contain any of the following words: %1").arg(words) + "
";
+ }
+ }
+
+ if (rules.at(8).size() > 0)
+ out += "- " +
+ tr("can not match any of the following expressions: %1").arg(rules.at(8).toHtmlEscaped()) +
+ "
";
+ }
+
+ out += "
";
+ } else {
+ out += tr("You may only use A-Z, a-z, 0-9, _, ., and - in your username.");
+ }
+
+ return out;
+}
\ No newline at end of file
diff --git a/cockatrice/src/client/network/connection_controller/remote_connection_controller.h b/cockatrice/src/client/network/connection_controller/remote_connection_controller.h
new file mode 100644
index 000000000..7486bc81a
--- /dev/null
+++ b/cockatrice/src/client/network/connection_controller/remote_connection_controller.h
@@ -0,0 +1,98 @@
+#ifndef COCKATRICE_REMOTE_CONNECTION_CONTROLLER_H
+#define COCKATRICE_REMOTE_CONNECTION_CONTROLLER_H
+
+#include "abstract_client.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+class RemoteClient;
+class ServerInfo_User;
+class DlgConnect;
+
+/**
+ * Owns the RemoteClient and its worker thread.
+ * Encapsulates all connection, authentication, and registration logic so that
+ * MainWindow only needs to react to high-level signals.
+ */
+class ConnectionController : public QObject
+{
+ Q_OBJECT
+
+public:
+ explicit ConnectionController(QWidget *dialogParent, QObject *parent = nullptr);
+ ~ConnectionController() override;
+
+ RemoteClient *client() const
+ {
+ return remoteClient;
+ }
+
+ void registerToServer();
+ void forgotPasswordRequest();
+ void connectToServer();
+ void
+ connectToServerDirect(const QString &host, unsigned int port, const QString &playerName, const QString &password);
+ void disconnectFromServer();
+
+ void refreshWindowTitle()
+ {
+ updateWindowTitle();
+ }
+
+signals:
+ void windowTitleChanged(const QString &title);
+
+ void tabSupervisorStartRequested(const ServerInfo_User &info);
+ void tabSupervisorStopRequested();
+
+ // Passes the raw ClientStatus through so MainWindow can drive its own
+ // action enable/disable logic
+ void statusChanged(ClientStatus status);
+
+private slots:
+ // Slots wired directly to RemoteClient signals
+ void onStatusChanged(ClientStatus status);
+ void onUserInfoReceived(const ServerInfo_User &info);
+ void onLoginError(int r, QString reasonStr, quint32 endTime, const QList &missingFeatures);
+ void onRegisterAccepted();
+ void onRegisterAcceptedNeedsActivate();
+ void onRegisterError(int r, QString reasonStr, quint32 endTime);
+ void onActivateAccepted();
+ void onActivateError();
+ void onProtocolVersionMismatch(int localVersion, int remoteVersion);
+ void onNotifyUserAboutUpdate();
+ void onConnectionClosedEvent(const Event_ConnectionClosed &event);
+ void onServerShutdownEvent(const Event_ServerShutdown &event);
+ void onSocketError(const QString &errorStr);
+ void onServerTimeout();
+
+ // Forgot-password flow
+ void onForgotPasswordSuccess();
+ void onForgotPasswordError();
+ void onPromptForgotPasswordReset();
+ void onPromptForgotPasswordChallenge();
+
+private:
+ void wireClientSignals();
+ void updateWindowTitle();
+
+ /** Parse the server's pipe-delimited username-rule string into HTML. */
+ static QString extractInvalidUsernameMessage(QString &in);
+
+ RemoteClient *remoteClient{nullptr};
+ QThread *clientThread{nullptr};
+ QWidget *dialogParent{nullptr}; // used as parent for QMessageBox / dialog calls
+
+ // Persistent so it can be updated in-place by onServerShutdownEvent
+ QMessageBox serverShutdownMessageBox;
+
+ // Kept as a member so the forgot-password signal can be wired to it
+ DlgConnect *dlgConnect{nullptr};
+};
+
+#endif // COCKATRICE_REMOTE_CONNECTION_CONTROLLER_H
diff --git a/cockatrice/src/interface/window_main.cpp b/cockatrice/src/interface/window_main.cpp
index 50375eea6..bd7a5904e 100644
--- a/cockatrice/src/interface/window_main.cpp
+++ b/cockatrice/src/interface/window_main.cpp
@@ -22,14 +22,9 @@
#include "../client/network/update/client/client_update_checker.h"
#include "../client/network/update/client/release_channel.h"
#include "../client/settings/cache_settings.h"
-#include "../interface/widgets/dialogs/dlg_connect.h"
#include "../interface/widgets/dialogs/dlg_edit_tokens.h"
-#include "../interface/widgets/dialogs/dlg_forgot_password_challenge.h"
-#include "../interface/widgets/dialogs/dlg_forgot_password_request.h"
-#include "../interface/widgets/dialogs/dlg_forgot_password_reset.h"
#include "../interface/widgets/dialogs/dlg_local_game_options.h"
#include "../interface/widgets/dialogs/dlg_manage_sets.h"
-#include "../interface/widgets/dialogs/dlg_register.h"
#include "../interface/widgets/dialogs/dlg_settings.h"
#include "../interface/widgets/dialogs/dlg_startup_card_check.h"
#include "../interface/widgets/dialogs/dlg_tip_of_the_day.h"
@@ -40,6 +35,8 @@
#include "../main.h"
#include "logger.h"
#include "version_string.h"
+#include "widgets/dialogs/dlg_connect.h"
+#include "widgets/server/handle_public_servers.h"
#include "widgets/utility/get_text_with_max.h"
#include
@@ -67,8 +64,6 @@
#include
#include
#include
-#include
-#include
#include
#include
@@ -100,59 +95,9 @@ void MainWindow::updateTabMenu(const QList &newMenuList)
menuBar()->insertMenu(helpMenu->menuAction(), tabMenu);
}
-void MainWindow::processConnectionClosedEvent(const Event_ConnectionClosed &event)
-{
- client->disconnectFromServer();
- QString reasonStr;
- switch (event.reason()) {
- case Event_ConnectionClosed::USER_LIMIT_REACHED:
- reasonStr = tr("The server has reached its maximum user capacity, please check back later.");
- break;
- case Event_ConnectionClosed::TOO_MANY_CONNECTIONS:
- reasonStr = tr("There are too many concurrent connections from your address.");
- break;
- case Event_ConnectionClosed::BANNED: {
- reasonStr = tr("Banned by moderator");
- if (event.has_end_time())
- reasonStr.append(
- "\n" + tr("Expected end time: %1").arg(QDateTime::fromSecsSinceEpoch(event.end_time()).toString()));
- else
- reasonStr.append("\n" + tr("This ban lasts indefinitely."));
- if (event.has_reason_str())
- reasonStr.append("\n\n" + QString::fromStdString(event.reason_str()));
- break;
- }
- case Event_ConnectionClosed::SERVER_SHUTDOWN:
- reasonStr = tr("Scheduled server shutdown.");
- break;
- case Event_ConnectionClosed::USERNAMEINVALID:
- reasonStr = tr("Invalid username.");
- break;
- case Event_ConnectionClosed::LOGGEDINELSEWERE:
- reasonStr = tr("You have been logged out due to logging in at another location.");
- break;
- default:
- reasonStr = QString::fromStdString(event.reason_str());
- }
- QMessageBox::critical(this, tr("Connection closed"),
- tr("The server has terminated your connection.\nReason: %1").arg(reasonStr));
-}
-
-void MainWindow::processServerShutdownEvent(const Event_ServerShutdown &event)
-{
- serverShutdownMessageBox.setInformativeText(tr("The server is going to be restarted in %n minute(s).\nAll running "
- "games will be lost.\nReason for shutdown: %1",
- "", event.minutes())
- .arg(QString::fromStdString(event.reason())));
- serverShutdownMessageBox.setIconPixmap(QPixmap("theme:cockatrice").scaled(64, 64));
- serverShutdownMessageBox.setText(tr("Scheduled server shutdown"));
- serverShutdownMessageBox.setWindowModality(Qt::ApplicationModal);
- serverShutdownMessageBox.setVisible(true);
-}
-
void MainWindow::statusChanged(ClientStatus _status)
{
- setClientStatusTitle();
+ connectionController->refreshWindowTitle();
switch (_status) {
case StatusDisconnected:
tabSupervisor->stop();
@@ -177,51 +122,16 @@ void MainWindow::statusChanged(ClientStatus _status)
}
}
-void MainWindow::userInfoReceived(const ServerInfo_User &info)
-{
- tabSupervisor->start(info);
-}
-
-void MainWindow::registerAccepted()
-{
- QMessageBox::information(this, tr("Success"), tr("Registration accepted.\nWill now login."));
-}
-
-void MainWindow::registerAcceptedNeedsActivate()
-{
- // nothing
-}
-
-void MainWindow::activateAccepted()
-{
- QMessageBox::information(this, tr("Success"), tr("Account activation accepted.\nWill now login."));
-}
-
// Actions
void MainWindow::actConnect()
{
- dlgConnect = new DlgConnect(this);
- connect(dlgConnect, &DlgConnect::sigStartForgotPasswordRequest, this, &MainWindow::actForgotPasswordRequest);
-
- if (dlgConnect->exec()) {
- client->connectToServer(dlgConnect->getHost(), static_cast(dlgConnect->getPort()),
- dlgConnect->getPlayerName(), dlgConnect->getPassword());
- }
-}
-
-void MainWindow::actRegister()
-{
- DlgRegister dlg(this);
- if (dlg.exec()) {
- client->registerToServer(dlg.getHost(), static_cast(dlg.getPort()), dlg.getPlayerName(),
- dlg.getPassword(), dlg.getEmail(), dlg.getCountry(), dlg.getRealName());
- }
+ connectionController->connectToServer();
}
void MainWindow::actDisconnect()
{
- client->disconnectFromServer();
+ connectionController->disconnectFromServer();
}
void MainWindow::actSinglePlayer()
@@ -373,292 +283,9 @@ void MainWindow::actOpenSettingsFolder()
QDesktopServices::openUrl(QUrl::fromLocalFile(dir));
}
-void MainWindow::serverTimeout()
-{
- QMessageBox::critical(this, tr("Error"), tr("Server timeout"));
- actConnect();
-}
-
-void MainWindow::loginError(Response::ResponseCode r,
- QString reasonStr,
- quint32 endTime,
- QList missingFeatures)
-{
- switch (r) {
- case Response::RespClientUpdateRequired: {
- QString formattedMissingFeatures;
- formattedMissingFeatures = "Missing Features: ";
- for (int i = 0; i < missingFeatures.size(); ++i)
- formattedMissingFeatures.append(QString("\n %1").arg(QChar(0x2022)) + " " +
- missingFeatures.value(i));
-
- QMessageBox msgBox;
- msgBox.setIcon(QMessageBox::Critical);
- msgBox.setWindowTitle(tr("Failed Login"));
- msgBox.setText(tr("Your client seems to be missing features this server requires for connection.") +
- "\n\n" + tr("To update your client, go to 'Help -> Check for Client Updates'."));
- msgBox.setDetailedText(formattedMissingFeatures);
- msgBox.exec();
- break;
- }
- case Response::RespWrongPassword:
- QMessageBox::critical(
- this, tr("Error"),
- tr("Incorrect username or password. Please check your authentication information and try again."));
- break;
- case Response::RespWouldOverwriteOldSession:
- QMessageBox::critical(this, tr("Error"),
- tr("There is already an active session using this user name.\nPlease close that "
- "session first and re-login."));
- break;
- case Response::RespUserIsBanned: {
- QString bannedStr;
- if (endTime)
- bannedStr = tr("You are banned until %1.").arg(QDateTime::fromSecsSinceEpoch(endTime).toString());
- else
- bannedStr = tr("You are banned indefinitely.");
- if (!reasonStr.isEmpty())
- bannedStr.append("\n\n" + reasonStr);
-
- QMessageBox::critical(this, tr("Error"), bannedStr);
- break;
- }
- case Response::RespUsernameInvalid: {
- QMessageBox::critical(this, tr("Error"), extractInvalidUsernameMessage(reasonStr));
- break;
- }
- case Response::RespRegistrationRequired:
- if (QMessageBox::question(this, tr("Error"),
- tr("This server requires user registration. Do you want to register now?"),
- QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) {
- actRegister();
- }
- break;
- case Response::RespClientIdRequired:
- QMessageBox::critical(
- this, tr("Error"),
- tr("This server requires client IDs. Your client is either failing to generate an ID or you are "
- "running a modified client.\nPlease close and reopen your client to try again."));
- break;
- case Response::RespContextError:
- QMessageBox::critical(this, tr("Error"),
- tr("An internal error has occurred, please close and reopen Cockatrice before trying "
- "again.\nIf the error persists, ensure you are running the latest version of the "
- "software and if needed contact the software developers."));
- break;
- case Response::RespAccountNotActivated: {
- bool ok = false;
- QString token = getTextWithMax(this, tr("Account activation"),
- tr("Your account has not been activated yet.\nYou need to provide "
- "the activation token received in the activation email."),
- QLineEdit::Normal, QString(), &ok);
- if (ok && !token.isEmpty()) {
- client->activateToServer(token);
- return;
- }
- client->disconnectFromServer();
- break;
- }
- case Response::RespServerFull: {
- QMessageBox::critical(this, tr("Server Full"),
- tr("The server has reached its maximum user capacity, please check back later."));
- break;
- }
- default:
- QMessageBox::critical(this, tr("Error"),
- tr("Unknown login error: %1").arg(static_cast(r)) +
- tr("\nThis usually means that your client version is out of date, and the server "
- "sent a reply your client doesn't understand."));
- break;
- }
- actConnect();
-}
-
-QString MainWindow::extractInvalidUsernameMessage(QString &in)
-{
- QString out = tr("Invalid username.") + "
";
- QStringList rules = in.split(QChar('|'));
- if (rules.size() == 7 || rules.size() == 9) {
- out += tr("Your username must respect these rules:") + "";
-
- out += "- " + tr("is %1 - %2 characters long").arg(rules.at(0)).arg(rules.at(1)) + "
";
- out += "- " + tr("can %1 contain lowercase characters").arg((rules.at(2).toInt() > 0) ? "" : tr("NOT")) +
- "
";
- out += "- " + tr("can %1 contain uppercase characters").arg((rules.at(3).toInt() > 0) ? "" : tr("NOT")) +
- "
";
- out +=
- "- " + tr("can %1 contain numeric characters").arg((rules.at(4).toInt() > 0) ? "" : tr("NOT")) + "
";
-
- if (rules.at(6).size() > 0)
- out += "- " + tr("can contain the following punctuation: %1").arg(rules.at(6).toHtmlEscaped()) + "
";
-
- out += "- " +
- tr("first character can %1 be a punctuation mark").arg((rules.at(5).toInt() > 0) ? "" : tr("NOT")) +
- "
";
-
- if (rules.size() == 9) {
- if (rules.at(7).size() > 0) {
- QString words = rules.at(7).toHtmlEscaped();
- if (words.startsWith("\n")) {
- out += tr("no unacceptable language as specified by these server rules:",
- "note that the following lines will not be translated");
- for (QString &line : words.split("\n", Qt::SkipEmptyParts)) {
- out += "- " + line + "
";
- }
- } else {
- out += "- " + tr("can not contain any of the following words: %1").arg(words) + "
";
- }
- }
-
- if (rules.at(8).size() > 0)
- out += "- " +
- tr("can not match any of the following expressions: %1").arg(rules.at(8).toHtmlEscaped()) +
- "
";
- }
-
- out += "
";
- } else {
- out += tr("You may only use A-Z, a-z, 0-9, _, ., and - in your username.");
- }
-
- return out;
-}
-
-void MainWindow::registerError(Response::ResponseCode r, QString reasonStr, quint32 endTime)
-{
- switch (r) {
- case Response::RespRegistrationDisabled:
- QMessageBox::critical(this, tr("Registration denied"),
- tr("Registration is currently disabled on this server"));
- break;
- case Response::RespUserAlreadyExists:
- QMessageBox::critical(this, tr("Registration denied"),
- tr("There is already an existing account with the same user name."));
- break;
- case Response::RespEmailRequiredToRegister:
- QMessageBox::critical(this, tr("Registration denied"),
- tr("It's mandatory to specify a valid email address when registering."));
- break;
- case Response::RespEmailBlackListed:
- if (reasonStr.isEmpty()) {
- reasonStr =
- "The email address provider used during registration has been blocked from use on this server.";
- }
- QMessageBox::critical(this, tr("Registration denied"), reasonStr);
- break;
- case Response::RespTooManyRequests:
- QMessageBox::critical(
- this, tr("Registration denied"),
- tr("It appears you are attempting to register a new account on this server yet you already have an "
- "account registered with the email provided. This server restricts the number of accounts a user "
- "can register per address. Please contact the server operator for further assistance or to obtain "
- "your credential information."));
- break;
- case Response::RespPasswordTooShort:
- QMessageBox::critical(this, tr("Registration denied"), tr("Password too short."));
- break;
- case Response::RespUserIsBanned: {
- QString bannedStr;
- if (endTime)
- bannedStr = tr("You are banned until %1.").arg(QDateTime::fromSecsSinceEpoch(endTime).toString());
- else
- bannedStr = tr("You are banned indefinitely.");
- if (!reasonStr.isEmpty())
- bannedStr.append("\n\n" + reasonStr);
-
- QMessageBox::critical(this, tr("Error"), bannedStr);
- break;
- }
- case Response::RespUsernameInvalid: {
- QMessageBox::critical(this, tr("Error"), extractInvalidUsernameMessage(reasonStr));
- break;
- }
- case Response::RespRegistrationFailed:
- QMessageBox::critical(this, tr("Error"), tr("Registration failed for a technical problem on the server."));
- break;
- case Response::RespNotConnected:
- QMessageBox::critical(this, tr("Error"), tr("The connection to the server has been lost."));
- break;
- default:
- QMessageBox::critical(this, tr("Error"),
- tr("Unknown registration error: %1").arg(static_cast(r)) +
- tr("\nThis usually means that your client version is out of date, and the server "
- "sent a reply your client doesn't understand."));
- }
- actRegister();
-}
-
-void MainWindow::activateError()
-{
- QMessageBox::critical(this, tr("Error"), tr("Account activation failed"));
- client->disconnectFromServer();
- actConnect();
-}
-
-void MainWindow::socketError(const QString &errorStr)
-{
- QMessageBox::critical(this, tr("Error"), tr("Socket error: %1").arg(errorStr));
- actConnect();
-}
-
-void MainWindow::protocolVersionMismatch(int localVersion, int remoteVersion)
-{
- if (localVersion > remoteVersion)
- QMessageBox::critical(this, tr("Error"),
- tr("You are trying to connect to an obsolete server. Please downgrade your Cockatrice "
- "version or connect to a suitable server.\nLocal version is %1, remote version is %2.")
- .arg(localVersion)
- .arg(remoteVersion));
- else
- QMessageBox::critical(this, tr("Error"),
- tr("Your Cockatrice client is obsolete. Please update your Cockatrice version.\nLocal "
- "version is %1, remote version is %2.")
- .arg(localVersion)
- .arg(remoteVersion));
-}
-
-void MainWindow::setClientStatusTitle()
-{
- switch (client->getStatus()) {
- case StatusConnecting:
- setWindowTitle(appName + " - " + tr("Connecting to %1...").arg(client->peerName()));
- break;
- case StatusRegistering:
- setWindowTitle(appName + " - " +
- tr("Registering to %1 as %2...").arg(client->peerName()).arg(client->getUserName()));
- break;
- case StatusDisconnected:
- setWindowTitle(appName + " - " + tr("Disconnected"));
- break;
- case StatusLoggingIn:
- setWindowTitle(appName + " - " + tr("Connected, logging in at %1").arg(client->peerName()));
- break;
- case StatusLoggedIn:
- setWindowTitle(client->getUserName() + "@" + client->peerName());
- break;
- case StatusRequestingForgotPassword:
- setWindowTitle(
- appName + " - " +
- tr("Requesting forgotten password to %1 as %2...").arg(client->peerName()).arg(client->getUserName()));
- break;
- case StatusSubmitForgotPasswordChallenge:
- setWindowTitle(
- appName + " - " +
- tr("Requesting forgotten password to %1 as %2...").arg(client->peerName()).arg(client->getUserName()));
- break;
- case StatusSubmitForgotPasswordReset:
- setWindowTitle(
- appName + " - " +
- tr("Requesting forgotten password to %1 as %2...").arg(client->peerName()).arg(client->getUserName()));
- break;
- default:
- setWindowTitle(appName);
- }
-}
-
void MainWindow::retranslateUi()
{
- setClientStatusTitle();
+ connectionController->refreshWindowTitle();
aConnect->setText(tr("&Connect..."));
aDisconnect->setText(tr("&Disconnect"));
@@ -717,9 +344,9 @@ void MainWindow::createActions()
aFullScreen->setCheckable(true);
connect(aFullScreen, &QAction::toggled, this, &MainWindow::actFullScreen);
aRegister = new QAction(this);
- connect(aRegister, &QAction::triggered, this, &MainWindow::actRegister);
+ connect(aRegister, &QAction::triggered, connectionController, &ConnectionController::registerToServer);
aForgotPassword = new QAction(this);
- connect(aForgotPassword, &QAction::triggered, this, &MainWindow::actForgotPasswordRequest);
+ connect(aForgotPassword, &QAction::triggered, connectionController, &ConnectionController::forgotPasswordRequest);
aSettings = new QAction(this);
connect(aSettings, &QAction::triggered, this, &MainWindow::actSettings);
aExit = new QAction(this);
@@ -844,38 +471,22 @@ MainWindow::MainWindow(QWidget *parent)
&MainWindow::pixmapCacheSizeChanged);
pixmapCacheSizeChanged(SettingsCache::instance().getPixmapCacheSize());
- client = new RemoteClient(nullptr, &SettingsCache::instance());
- connect(client, &RemoteClient::connectionClosedEventReceived, this, &MainWindow::processConnectionClosedEvent);
- connect(client, &RemoteClient::serverShutdownEventReceived, this, &MainWindow::processServerShutdownEvent);
- connect(client, &RemoteClient::loginError, this, &MainWindow::loginError);
- connect(client, &RemoteClient::socketError, this, &MainWindow::socketError);
- connect(client, &RemoteClient::serverTimeout, this, &MainWindow::serverTimeout);
- connect(client, &RemoteClient::statusChanged, this, &MainWindow::statusChanged);
- connect(client, &RemoteClient::protocolVersionMismatch, this, &MainWindow::protocolVersionMismatch);
- connect(client, &RemoteClient::userInfoChanged, this, &MainWindow::userInfoReceived, Qt::BlockingQueuedConnection);
- connect(client, &RemoteClient::notifyUserAboutUpdate, this, &MainWindow::notifyUserAboutUpdate);
- connect(client, &RemoteClient::registerAccepted, this, &MainWindow::registerAccepted);
- connect(client, &RemoteClient::registerAcceptedNeedsActivate, this, &MainWindow::registerAcceptedNeedsActivate);
- connect(client, &RemoteClient::registerError, this, &MainWindow::registerError);
- connect(client, &RemoteClient::activateAccepted, this, &MainWindow::activateAccepted);
- connect(client, &RemoteClient::activateError, this, &MainWindow::activateError);
- connect(client, &RemoteClient::sigForgotPasswordSuccess, this, &MainWindow::forgotPasswordSuccess);
- connect(client, &RemoteClient::sigForgotPasswordError, this, &MainWindow::forgotPasswordError);
- connect(client, &RemoteClient::sigPromptForForgotPasswordReset, this, &MainWindow::promptForgotPasswordReset);
- connect(client, &RemoteClient::sigPromptForForgotPasswordChallenge, this,
- &MainWindow::promptForgotPasswordChallenge);
-
- clientThread = new QThread(this);
- client->moveToThread(clientThread);
- clientThread->start();
+ connectionController = new ConnectionController(this, this);
createActions();
createMenus();
- tabSupervisor = new TabSupervisor(client, tabsMenu, this);
+ connect(connectionController, &ConnectionController::windowTitleChanged, this, &MainWindow::setWindowTitle);
+ connect(connectionController, &ConnectionController::statusChanged, this, &MainWindow::statusChanged);
+
+ tabSupervisor = new TabSupervisor(connectionController->client(), tabsMenu, this);
connect(tabSupervisor, &TabSupervisor::setMenu, this, &MainWindow::updateTabMenu);
connect(tabSupervisor, &TabSupervisor::localGameEnded, this, &MainWindow::localGameEnded);
connect(tabSupervisor, &TabSupervisor::showWindowIfHidden, this, &MainWindow::showWindowIfHidden);
+ connect(connectionController, &ConnectionController::tabSupervisorStartRequested, tabSupervisor,
+ &TabSupervisor::start);
+ connect(connectionController, &ConnectionController::tabSupervisorStopRequested, tabSupervisor,
+ &TabSupervisor::stop);
tabSupervisor->initStartupTabs();
setCentralWidget(tabSupervisor);
@@ -1043,9 +654,6 @@ MainWindow::~MainWindow()
cardUpdateProcess->waitForFinished(1000);
cardUpdateProcess = nullptr;
}
-
- client->deleteLater();
- clientThread->wait();
}
void MainWindow::createTrayIcon()
@@ -1079,14 +687,6 @@ void MainWindow::actShow()
});
}
-void MainWindow::promptForgotPasswordChallenge()
-{
- DlgForgotPasswordChallenge dlg(this);
- if (dlg.exec())
- client->submitForgotPasswordChallengeToServer(dlg.getHost(), static_cast(dlg.getPort()),
- dlg.getPlayerName(), dlg.getEmail());
-}
-
void MainWindow::closeEvent(QCloseEvent *event)
{
// workaround Qt bug where closeEvent gets called twice
@@ -1116,13 +716,14 @@ void MainWindow::changeEvent(QEvent *event)
bHasActivated = true;
if (!connectTo.isEmpty()) {
qCInfo(WindowMainStartupAutoconnectLog) << "Command line connect to " << connectTo;
- client->connectToServer(connectTo.host(), connectTo.port(), connectTo.userName(), connectTo.password());
+ connectionController->connectToServerDirect(connectTo.host(), connectTo.port(), connectTo.userName(),
+ connectTo.password());
} else if (SettingsCache::instance().servers().getAutoConnect() &&
!SettingsCache::instance().debug().getLocalGameOnStartup()) {
qCInfo(WindowMainStartupAutoconnectLog) << "Attempting auto-connect...";
DlgConnect dlg(this);
- client->connectToServer(dlg.getHost(), static_cast(dlg.getPort()), dlg.getPlayerName(),
- dlg.getPassword());
+ connectionController->connectToServerDirect(dlg.getHost(), static_cast(dlg.getPort()),
+ dlg.getPlayerName(), dlg.getPassword());
}
}
}
@@ -1395,15 +996,6 @@ void MainWindow::refreshShortcuts()
aStatusBar->setShortcuts(shortcuts.getShortcut("MainWindow/aStatusBar"));
}
-void MainWindow::notifyUserAboutUpdate()
-{
- QMessageBox::information(
- this, tr("Information"),
- tr("This server supports additional features that your client doesn't have.\nThis is most likely not a "
- "problem, but this message might mean there is a new version of Cockatrice available or this server is "
- "running a custom or pre-release version.\n\nTo update your client, go to Help -> Check for Updates."));
-}
-
void MainWindow::actOpenCustomFolder()
{
QString dir = SettingsCache::instance().getCustomPicsPath();
@@ -1504,42 +1096,3 @@ void MainWindow::actEditTokens()
dlg.exec();
CardDatabaseManager::getInstance()->saveCustomTokensToFile();
}
-
-void MainWindow::actForgotPasswordRequest()
-{
- DlgForgotPasswordRequest dlg(this);
- if (dlg.exec())
- client->requestForgotPasswordToServer(dlg.getHost(), static_cast(dlg.getPort()),
- dlg.getPlayerName());
-}
-
-void MainWindow::forgotPasswordSuccess()
-{
- QMessageBox::information(
- this, tr("Reset Password"),
- tr("Your password has been reset successfully, you can now log in using the new credentials."));
- SettingsCache::instance().servers().setFPHostName("");
- SettingsCache::instance().servers().setFPPort("");
- SettingsCache::instance().servers().setFPPlayerName("");
-}
-
-void MainWindow::forgotPasswordError()
-{
- QMessageBox::warning(
- this, tr("Reset Password"),
- tr("Failed to reset user account password, please contact the server operator to reset your password."));
- SettingsCache::instance().servers().setFPHostName("");
- SettingsCache::instance().servers().setFPPort("");
- SettingsCache::instance().servers().setFPPlayerName("");
-}
-
-void MainWindow::promptForgotPasswordReset()
-{
- QMessageBox::information(this, tr("Reset Password"),
- tr("Activation request received, please check your email for an activation token."));
- DlgForgotPasswordReset dlg(this);
- if (dlg.exec()) {
- client->submitForgotPasswordResetToServer(dlg.getHost(), static_cast(dlg.getPort()),
- dlg.getPlayerName(), dlg.getToken(), dlg.getPassword());
- }
-}
diff --git a/cockatrice/src/interface/window_main.h b/cockatrice/src/interface/window_main.h
index ed6de5b0d..528e6f211 100644
--- a/cockatrice/src/interface/window_main.h
+++ b/cockatrice/src/interface/window_main.h
@@ -25,6 +25,7 @@
#ifndef WINDOW_H
#define WINDOW_H
+#include "connection_controller/remote_connection_controller.h"
#include "widgets/dialogs/dlg_local_game_options.h"
#include
@@ -68,38 +69,19 @@ public slots:
private slots:
void updateTabMenu(const QList &newMenuList);
void statusChanged(ClientStatus _status);
- void processConnectionClosedEvent(const Event_ConnectionClosed &event);
- void processServerShutdownEvent(const Event_ServerShutdown &event);
- void serverTimeout();
- void loginError(Response::ResponseCode r, QString reasonStr, quint32 endTime, QList missingFeatures);
- void registerError(Response::ResponseCode r, QString reasonStr, quint32 endTime);
- void activateError();
- void socketError(const QString &errorStr);
- void protocolVersionMismatch(int localVersion, int remoteVersion);
- void userInfoReceived(const ServerInfo_User &userInfo);
- void registerAccepted();
- void registerAcceptedNeedsActivate();
- void activateAccepted();
void localGameEnded();
void pixmapCacheSizeChanged(int newSizeInMBs);
- void notifyUserAboutUpdate();
void actDisconnect();
void actSinglePlayer();
void actWatchReplay();
void actFullScreen(bool checked);
- void actRegister();
void actSettings();
- void actForgotPasswordRequest();
void actAbout();
void actTips();
void actUpdate();
void actViewLog();
void actOpenSettingsFolder();
- void forgotPasswordSuccess();
- void forgotPasswordError();
- void promptForgotPasswordReset();
void actShow();
- void promptForgotPasswordChallenge();
void showWindowIfHidden();
void cardUpdateError(QProcess::ProcessError err);
@@ -125,7 +107,6 @@ private slots:
private:
static const QString appName;
static const QStringList fileNameFilters;
- void setClientStatusTitle();
void retranslateUi();
void createActions();
void createMenus();
@@ -152,14 +133,11 @@ private:
TabSupervisor *tabSupervisor;
WndSets *wndSets;
- RemoteClient *client;
- QThread *clientThread;
+ ConnectionController *connectionController;
LocalServer *localServer;
bool bHasActivated, askedForDbUpdater;
- QMessageBox serverShutdownMessageBox;
QProcess *cardUpdateProcess;
DlgViewLog *logviewDialog;
- DlgConnect *dlgConnect;
GameReplay *replay;
DlgTipOfTheDay *tip;
QUrl connectTo;
@@ -180,7 +158,6 @@ public:
protected:
void closeEvent(QCloseEvent *event) override;
void changeEvent(QEvent *event) override;
- QString extractInvalidUsernameMessage(QString &in);
};
#endif
From 8845a756271e7aa0058c0769b698b33013c676c9 Mon Sep 17 00:00:00 2001
From: RickyRister <42636155+RickyRister@users.noreply.github.com>
Date: Sat, 9 May 2026 04:08:30 -0700
Subject: [PATCH 007/121] [GameSelector] Fix bug with hideNotBuddyCreatedGames
checkbox (#6858)
---
.../server/game_selector_quick_filter_toolbar.cpp | 14 +-------------
1 file changed, 1 insertion(+), 13 deletions(-)
diff --git a/cockatrice/src/interface/widgets/server/game_selector_quick_filter_toolbar.cpp b/cockatrice/src/interface/widgets/server/game_selector_quick_filter_toolbar.cpp
index b2c9fb84e..0e8a0d4c7 100644
--- a/cockatrice/src/interface/widgets/server/game_selector_quick_filter_toolbar.cpp
+++ b/cockatrice/src/interface/widgets/server/game_selector_quick_filter_toolbar.cpp
@@ -29,19 +29,7 @@ GameSelectorQuickFilterToolBar::GameSelectorQuickFilterToolBar(QWidget *parent,
hideGamesNotCreatedByBuddiesCheckBox = new QCheckBox(this);
hideGamesNotCreatedByBuddiesCheckBox->setChecked(filters.hideNotBuddyCreatedGames);
connect(hideGamesNotCreatedByBuddiesCheckBox, &QCheckBox::toggled, this, [this](bool checked) {
- applyFilters([&](GameFilterConfigs &configs) {
- configs.hideNotBuddyCreatedGames = checked;
-
- if (checked) {
- QStringList buddyNames;
- for (auto buddy : tabSupervisor->getUserListManager()->getBuddyList().values()) {
- buddyNames << QString::fromStdString(buddy.name());
- }
- configs.creatorNameFilters = buddyNames;
- } else {
- configs.creatorNameFilters.clear();
- }
- });
+ applyFilters([&](GameFilterConfigs &configs) { configs.hideNotBuddyCreatedGames = checked; });
});
hideFullGamesCheckBox = new QCheckBox(this);
From f223ff387e4f58c855f90333753d3d32f8c0b8e3 Mon Sep 17 00:00:00 2001
From: tooomm
Date: Sat, 9 May 2026 14:49:58 +0200
Subject: [PATCH 008/121] bump vcpkg to 2026.04.27 release (#6855)
* bump vcpkg to 2026.04.27
* remove hint
---
.github/dependabot.yml | 2 +-
vcpkg | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index 06fdf7ab9..404cac62e 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -10,7 +10,7 @@ updates:
# Look for `.gitmodules` in the `root` directory
directory: "/"
ignore:
- # Ignore updates for vcpkg (Bump to latest tag not working (no SemVer used) & macOS Intel triplet broken with newer releases)
+ # Ignore updates for vcpkg (Bump to latest tag not working (no SemVer used)
- dependency-name: "vcpkg"
# Check for updates once a month
schedule:
diff --git a/vcpkg b/vcpkg
index d29e8309d..56bb24116 160000
--- a/vcpkg
+++ b/vcpkg
@@ -1 +1 @@
-Subproject commit d29e8309d1cf7a7cb6998a935b2ac5b1958a7432
+Subproject commit 56bb2411609227288b70117ead2c47585ba07713
From 48e21aad383a43b7a38f49f20e7d4575f09e1cd6 Mon Sep 17 00:00:00 2001
From: RickyRister <42636155+RickyRister@users.noreply.github.com>
Date: Sat, 9 May 2026 12:55:11 -0700
Subject: [PATCH 009/121] [Game] Refactor: move parsePT to a static method
(#6860)
---
cockatrice/src/game/board/card_item.cpp | 31 ++++++++++++++++
cockatrice/src/game/board/card_item.h | 20 ++++++++++
cockatrice/src/game/player/player_actions.cpp | 37 ++-----------------
cockatrice/src/game/player/player_actions.h | 2 -
4 files changed, 54 insertions(+), 36 deletions(-)
diff --git a/cockatrice/src/game/board/card_item.cpp b/cockatrice/src/game/board/card_item.cpp
index 62de4a02e..072c1a231 100644
--- a/cockatrice/src/game/board/card_item.cpp
+++ b/cockatrice/src/game/board/card_item.cpp
@@ -396,6 +396,37 @@ void CardItem::playCard(bool faceDown)
}
}
+QVariantList CardItem::parsePT(const QString &pt)
+{
+ QVariantList ptList = QVariantList();
+ if (!pt.isEmpty()) {
+ int sep = pt.indexOf('/');
+ if (sep == 0) {
+ ptList.append(QVariant(pt.mid(1))); // cut off starting '/' and take full string
+ } else {
+ int start = 0;
+ for (;;) {
+ QString item = pt.mid(start, sep - start);
+ if (item.isEmpty()) {
+ ptList.append(QVariant(QString()));
+ } else if (item[0] == '+') {
+ ptList.append(QVariant(item.mid(1).toInt())); // add as int
+ } else if (item[0] == '-') {
+ ptList.append(QVariant(item.toInt())); // add as int
+ } else {
+ ptList.append(QVariant(item)); // add as qstring
+ }
+ if (sep == -1) {
+ break;
+ }
+ start = sep + 1;
+ sep = pt.indexOf('/', start);
+ }
+ }
+ }
+ return ptList;
+}
+
/**
* @brief returns true if the zone is a unwritable reveal zone view (eg a card reveal window). Will return false if zone
* is nullptr.
diff --git a/cockatrice/src/game/board/card_item.h b/cockatrice/src/game/board/card_item.h
index da2097a2c..9799b5067 100644
--- a/cockatrice/src/game/board/card_item.h
+++ b/cockatrice/src/game/board/card_item.h
@@ -146,6 +146,26 @@ public:
void drawAttachArrow();
void playCard(bool faceDown);
+ /**
+ * @brief Parses a string representing a p/t in order to extract the values from it.
+ *
+ * If the string contains '/', the string will be split at the '/' and each side will be parsed separately,
+ * which means the result list will have two elements.
+ *
+ * If '/' is not found, then the entire string is parsed together, which means the result list will
+ * have a single element.
+ *
+ * If either side of the split is empty, there will also only be a single element in the result list.
+ *
+ * This function will attempt to parse each substring as an int first, handling plus and minus prefixes.
+ * If successful, it will put the parsed value into the QVariant as an int.
+ * If failed, it will just put the substring into the QVariant as a QString.
+ *
+ * @param pt The p/t string
+ * @return A QVariantList that can contain one or two elements, where each QVariant can be either int or QString
+ */
+ static QVariantList parsePT(const QString &pt);
+
protected:
void mouseMoveEvent(QGraphicsSceneMouseEvent *event) override;
void mouseReleaseEvent(QGraphicsSceneMouseEvent *event) override;
diff --git a/cockatrice/src/game/player/player_actions.cpp b/cockatrice/src/game/player/player_actions.cpp
index 92c4b16bf..7d163147d 100644
--- a/cockatrice/src/game/player/player_actions.cpp
+++ b/cockatrice/src/game/player/player_actions.cpp
@@ -1272,7 +1272,7 @@ void PlayerActions::actIncPT(int deltaP, int deltaT)
for (const auto &item : player->getGameScene()->selectedItems()) {
auto *card = static_cast(item);
QString pt = card->getPT();
- const auto ptList = parsePT(pt);
+ const auto ptList = CardItem::parsePT(pt);
QString newpt;
if (ptList.isEmpty()) {
newpt = QString::number(deltaP) + (deltaT ? "/" + QString::number(deltaT) : "");
@@ -1332,37 +1332,6 @@ void PlayerActions::actResetPT()
}
}
-QVariantList PlayerActions::parsePT(const QString &pt)
-{
- QVariantList ptList = QVariantList();
- if (!pt.isEmpty()) {
- int sep = pt.indexOf('/');
- if (sep == 0) {
- ptList.append(QVariant(pt.mid(1))); // cut off starting '/' and take full string
- } else {
- int start = 0;
- for (;;) {
- QString item = pt.mid(start, sep - start);
- if (item.isEmpty()) {
- ptList.append(QVariant(QString()));
- } else if (item[0] == '+') {
- ptList.append(QVariant(item.mid(1).toInt())); // add as int
- } else if (item[0] == '-') {
- ptList.append(QVariant(item.toInt())); // add as int
- } else {
- ptList.append(QVariant(item)); // add as qstring
- }
- if (sep == -1) {
- break;
- }
- start = sep + 1;
- sep = pt.indexOf('/', start);
- }
- }
- }
- return ptList;
-}
-
void PlayerActions::actSetPT()
{
QString oldPT;
@@ -1384,7 +1353,7 @@ void PlayerActions::actSetPT()
return;
}
- const auto ptList = parsePT(pt);
+ const auto ptList = CardItem::parsePT(pt);
bool empty = ptList.isEmpty();
QList commandList;
@@ -1393,7 +1362,7 @@ void PlayerActions::actSetPT()
auto *cmd = new Command_SetCardAttr;
QString newpt = QString();
if (!empty) {
- const auto oldpt = parsePT(card->getPT());
+ const auto oldpt = CardItem::parsePT(card->getPT());
int ptIter = 0;
for (const auto &_item : ptList) {
#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
diff --git a/cockatrice/src/game/player/player_actions.h b/cockatrice/src/game/player/player_actions.h
index 08e016f2a..f619b618c 100644
--- a/cockatrice/src/game/player/player_actions.h
+++ b/cockatrice/src/game/player/player_actions.h
@@ -197,8 +197,6 @@ private:
void cmdSetTopCard(Command_MoveCard &cmd);
void cmdSetBottomCard(Command_MoveCard &cmd);
-
- QVariantList parsePT(const QString &pt);
};
#endif // COCKATRICE_PLAYER_ACTIONS_H
From cdb171f20120a5403eb53e0c583231d72a64611b Mon Sep 17 00:00:00 2001
From: RickyRister <42636155+RickyRister@users.noreply.github.com>
Date: Sat, 9 May 2026 13:10:56 -0700
Subject: [PATCH 010/121] [Game] Refactor: move setCardAttrHelper to
PlayerEventHandler (#6772)
---
cockatrice/src/game/player/player.cpp | 14 -----
cockatrice/src/game/player/player.h | 1 -
cockatrice/src/game/player/player_actions.cpp | 55 ----------------
cockatrice/src/game/player/player_actions.h | 14 -----
.../src/game/player/player_event_handler.cpp | 62 +++++++++++++++++--
.../src/game/player/player_event_handler.h | 8 +++
6 files changed, 66 insertions(+), 88 deletions(-)
diff --git a/cockatrice/src/game/player/player.cpp b/cockatrice/src/game/player/player.cpp
index ac4149f0e..74a6eb21b 100644
--- a/cockatrice/src/game/player/player.cpp
+++ b/cockatrice/src/game/player/player.cpp
@@ -43,20 +43,6 @@ Player::Player(const ServerInfo_User &info, int _id, bool _local, bool _judge, A
connect(this, &Player::activeChanged, graphicsItem, &PlayerGraphicsItem::onPlayerActiveChanged);
connect(this, &Player::openDeckEditor, game->getTab(), &TabGame::openDeckEditor);
-
- forwardActionSignalsToEventHandler();
-}
-
-// Event Handler is the controller i.e. everything hooks up to this to know about player state
-// Player should forward (private) signals to the event handler
-
-void Player::forwardActionSignalsToEventHandler()
-{
- connect(playerActions, &PlayerActions::logSetTapped, playerEventHandler, &PlayerEventHandler::logSetTapped);
- connect(playerActions, &PlayerActions::logSetDoesntUntap, playerEventHandler,
- &PlayerEventHandler::logSetDoesntUntap);
- connect(playerActions, &PlayerActions::logSetAnnotation, playerEventHandler, &PlayerEventHandler::logSetAnnotation);
- connect(playerActions, &PlayerActions::logSetPT, playerEventHandler, &PlayerEventHandler::logSetPT);
}
void Player::initializeZones()
diff --git a/cockatrice/src/game/player/player.h b/cockatrice/src/game/player/player.h
index e9c008821..9b8456aaf 100644
--- a/cockatrice/src/game/player/player.h
+++ b/cockatrice/src/game/player/player.h
@@ -82,7 +82,6 @@ public slots:
public:
Player(const ServerInfo_User &info, int _id, bool _local, bool _judge, AbstractGame *_parent);
- void forwardActionSignalsToEventHandler();
~Player() override;
void initializeZones();
diff --git a/cockatrice/src/game/player/player_actions.cpp b/cockatrice/src/game/player/player_actions.cpp
index 7d163147d..f9508c036 100644
--- a/cockatrice/src/game/player/player_actions.cpp
+++ b/cockatrice/src/game/player/player_actions.cpp
@@ -1157,61 +1157,6 @@ void PlayerActions::actSayMessage()
sendGameCommand(cmd);
}
-void PlayerActions::setCardAttrHelper(const GameEventContext &context,
- CardItem *card,
- CardAttribute attribute,
- const QString &avalue,
- bool allCards,
- EventProcessingOptions options)
-{
- if (card == nullptr) {
- return;
- }
-
- bool moveCardContext = context.HasExtension(Context_MoveCard::ext);
- switch (attribute) {
- case AttrTapped: {
- bool tapped = avalue == "1";
- if (!(!tapped && card->getDoesntUntap() && allCards)) {
- if (!allCards) {
- emit logSetTapped(player, card, tapped);
- }
- bool canAnimate = !options.testFlag(SKIP_TAP_ANIMATION) && !moveCardContext;
- card->setTapped(tapped, canAnimate);
- }
- break;
- }
- case AttrAttacking: {
- card->setAttacking(avalue == "1");
- break;
- }
- case AttrFaceDown: {
- card->setFaceDown(avalue == "1");
- break;
- }
- case AttrColor: {
- card->setColor(avalue);
- break;
- }
- case AttrAnnotation: {
- emit logSetAnnotation(player, card, avalue);
- card->setAnnotation(avalue);
- break;
- }
- case AttrDoesntUntap: {
- bool value = (avalue == "1");
- emit logSetDoesntUntap(player, card, value);
- card->setDoesntUntap(value);
- break;
- }
- case AttrPT: {
- emit logSetPT(player, card, avalue);
- card->setPT(avalue);
- break;
- }
- }
-}
-
void PlayerActions::actMoveCardXCardsFromTop()
{
int deckSize = player->getDeckZone()->getCards().size() + 1; // add the card to move to the deck
diff --git a/cockatrice/src/game/player/player_actions.h b/cockatrice/src/game/player/player_actions.h
index f619b618c..12c954dab 100644
--- a/cockatrice/src/game/player/player_actions.h
+++ b/cockatrice/src/game/player/player_actions.h
@@ -16,7 +16,6 @@
#include
#include
#include
-#include
namespace google
{
@@ -35,12 +34,6 @@ class PlayerActions : public QObject
{
Q_OBJECT
-signals:
- void logSetTapped(Player *player, CardItem *card, bool tapped);
- void logSetAnnotation(Player *player, CardItem *card, QString newAnnotation);
- void logSetDoesntUntap(Player *player, CardItem *card, bool doesntUntap);
- void logSetPT(Player *player, CardItem *card, QString newPT);
-
public:
enum CardsToReveal
{
@@ -55,13 +48,6 @@ public:
PendingCommand *prepareGameCommand(const ::google::protobuf::Message &cmd);
PendingCommand *prepareGameCommand(const QList &cmdList);
- void setCardAttrHelper(const GameEventContext &context,
- CardItem *card,
- CardAttribute attribute,
- const QString &avalue,
- bool allCards,
- EventProcessingOptions options);
-
void moveOneCardUntil(CardItem *card);
void stopMoveTopCardsUntil();
diff --git a/cockatrice/src/game/player/player_event_handler.cpp b/cockatrice/src/game/player/player_event_handler.cpp
index 6fb1ff19a..dc1e2393c 100644
--- a/cockatrice/src/game/player/player_event_handler.cpp
+++ b/cockatrice/src/game/player/player_event_handler.cpp
@@ -149,8 +149,8 @@ void PlayerEventHandler::eventSetCardAttr(const Event_SetCardAttr &event,
if (!event.has_card_id()) {
const CardList &cards = zone->getCards();
for (int i = 0; i < cards.size(); ++i) {
- player->getPlayerActions()->setCardAttrHelper(context, cards.at(i), event.attribute(),
- QString::fromStdString(event.attr_value()), true, options);
+ setCardAttrHelper(context, cards.at(i), event.attribute(), QString::fromStdString(event.attr_value()), true,
+ options);
}
if (event.attribute() == AttrTapped) {
emit logSetTapped(player, nullptr, event.attr_value() == "1");
@@ -161,8 +161,62 @@ void PlayerEventHandler::eventSetCardAttr(const Event_SetCardAttr &event,
qWarning() << "PlayerEventHandler::eventSetCardAttr: card id=" << event.card_id() << "not found";
return;
}
- player->getPlayerActions()->setCardAttrHelper(context, card, event.attribute(),
- QString::fromStdString(event.attr_value()), false, options);
+ setCardAttrHelper(context, card, event.attribute(), QString::fromStdString(event.attr_value()), false, options);
+ }
+}
+
+void PlayerEventHandler::setCardAttrHelper(const GameEventContext &context,
+ CardItem *card,
+ CardAttribute attribute,
+ const QString &avalue,
+ bool allCards,
+ EventProcessingOptions options)
+{
+ if (card == nullptr) {
+ return;
+ }
+
+ bool moveCardContext = context.HasExtension(Context_MoveCard::ext);
+ switch (attribute) {
+ case AttrTapped: {
+ bool tapped = avalue == "1";
+ if (!(!tapped && card->getDoesntUntap() && allCards)) {
+ if (!allCards) {
+ emit logSetTapped(player, card, tapped);
+ }
+ bool canAnimate = !options.testFlag(SKIP_TAP_ANIMATION) && !moveCardContext;
+ card->setTapped(tapped, canAnimate);
+ }
+ break;
+ }
+ case AttrAttacking: {
+ card->setAttacking(avalue == "1");
+ break;
+ }
+ case AttrFaceDown: {
+ card->setFaceDown(avalue == "1");
+ break;
+ }
+ case AttrColor: {
+ card->setColor(avalue);
+ break;
+ }
+ case AttrAnnotation: {
+ emit logSetAnnotation(player, card, avalue);
+ card->setAnnotation(avalue);
+ break;
+ }
+ case AttrDoesntUntap: {
+ bool value = (avalue == "1");
+ emit logSetDoesntUntap(player, card, value);
+ card->setDoesntUntap(value);
+ break;
+ }
+ case AttrPT: {
+ emit logSetPT(player, card, avalue);
+ card->setPT(avalue);
+ break;
+ }
}
}
diff --git a/cockatrice/src/game/player/player_event_handler.h b/cockatrice/src/game/player/player_event_handler.h
index 5d1ddd4b4..9f67d24ab 100644
--- a/cockatrice/src/game/player/player_event_handler.h
+++ b/cockatrice/src/game/player/player_event_handler.h
@@ -9,6 +9,7 @@
#include "event_processing_options.h"
#include
+#include
#include
#include
@@ -110,6 +111,13 @@ public:
private:
Player *player;
+
+ void setCardAttrHelper(const GameEventContext &context,
+ CardItem *card,
+ CardAttribute attribute,
+ const QString &avalue,
+ bool allCards,
+ EventProcessingOptions options);
};
#endif // COCKATRICE_PLAYER_EVENT_HANDLER_H
From 7814204fe21aa22449606a8040b7e812421d84f4 Mon Sep 17 00:00:00 2001
From: RickyRister <42636155+RickyRister@users.noreply.github.com>
Date: Sat, 9 May 2026 13:20:33 -0700
Subject: [PATCH 011/121] [Game] Refactor: explicitly pass params from card
counter actions (#6780)
---
cockatrice/src/game/player/menu/card_menu.cpp | 12 +-
cockatrice/src/game/player/player_actions.cpp | 137 +++++++++---------
cockatrice/src/game/player/player_actions.h | 6 +-
3 files changed, 78 insertions(+), 77 deletions(-)
diff --git a/cockatrice/src/game/player/menu/card_menu.cpp b/cockatrice/src/game/player/menu/card_menu.cpp
index 66ca5e46b..34ed254fb 100644
--- a/cockatrice/src/game/player/menu/card_menu.cpp
+++ b/cockatrice/src/game/player/menu/card_menu.cpp
@@ -75,17 +75,17 @@ CardMenu::CardMenu(Player *_player, const CardItem *_card, bool _shortcutsActive
for (int i = 0; i < 6; ++i) {
auto *tempAddCounter = new QAction(this);
- tempAddCounter->setData(9 + i * 1000);
auto *tempRemoveCounter = new QAction(this);
- tempRemoveCounter->setData(10 + i * 1000);
auto *tempSetCounter = new QAction(this);
- tempSetCounter->setData(11 + i * 1000);
aAddCounter.append(tempAddCounter);
aRemoveCounter.append(tempRemoveCounter);
aSetCounter.append(tempSetCounter);
- connect(tempAddCounter, &QAction::triggered, playerActions, &PlayerActions::actCardCounterTrigger);
- connect(tempRemoveCounter, &QAction::triggered, playerActions, &PlayerActions::actCardCounterTrigger);
- connect(tempSetCounter, &QAction::triggered, playerActions, &PlayerActions::actCardCounterTrigger);
+ connect(tempAddCounter, &QAction::triggered, playerActions,
+ [playerActions, i] { playerActions->actAddCardCounter(i); });
+ connect(tempRemoveCounter, &QAction::triggered, playerActions,
+ [playerActions, i] { playerActions->actRemoveCardCounter(i); });
+ connect(tempSetCounter, &QAction::triggered, playerActions,
+ [playerActions, i] { playerActions->actSetCardCounter(i); });
}
setShortcutsActive();
diff --git a/cockatrice/src/game/player/player_actions.cpp b/cockatrice/src/game/player/player_actions.cpp
index f9508c036..8d00e5ba3 100644
--- a/cockatrice/src/game/player/player_actions.cpp
+++ b/cockatrice/src/game/player/player_actions.cpp
@@ -1462,80 +1462,77 @@ void PlayerActions::actUnattach()
sendGameCommand(prepareGameCommand(commandList));
}
-void PlayerActions::actCardCounterTrigger()
+void PlayerActions::actAddCardCounter(int counterId)
+{
+ offsetCardCounter(counterId, 1);
+}
+
+void PlayerActions::actRemoveCardCounter(int counterId)
+{
+ offsetCardCounter(counterId, -1);
+}
+
+void PlayerActions::offsetCardCounter(int counterId, int offset)
{
- auto *action = static_cast(sender());
- int counterId = action->data().toInt() / 1000;
QList commandList;
- switch (action->data().toInt() % 1000) {
- case 9: { // increment counter
- for (const auto &item : player->getGameScene()->selectedItems()) {
- auto *card = static_cast(item);
- if (card->getCounters().value(counterId, 0) < MAX_COUNTERS_ON_CARD) {
- auto *cmd = new Command_SetCardCounter;
- cmd->set_zone(card->getZone()->getName().toStdString());
- cmd->set_card_id(card->getId());
- cmd->set_counter_id(counterId);
- cmd->set_counter_value(card->getCounters().value(counterId, 0) + 1);
- commandList.append(cmd);
- }
- }
- break;
+ for (const auto &item : player->getGameScene()->selectedItems()) {
+ auto *card = static_cast(item);
+
+ int oldValue = card->getCounters().value(counterId, 0);
+ int newValue = oldValue + offset;
+
+ if (newValue >= 0 && newValue <= MAX_COUNTERS_ON_CARD) {
+ auto *cmd = new Command_SetCardCounter;
+ cmd->set_zone(card->getZone()->getName().toStdString());
+ cmd->set_card_id(card->getId());
+ cmd->set_counter_id(counterId);
+ cmd->set_counter_value(newValue);
+ commandList.append(cmd);
}
- case 10: { // decrement counter
- for (const auto &item : player->getGameScene()->selectedItems()) {
- auto *card = static_cast(item);
- if (card->getCounters().value(counterId, 0)) {
- auto *cmd = new Command_SetCardCounter;
- cmd->set_zone(card->getZone()->getName().toStdString());
- cmd->set_card_id(card->getId());
- cmd->set_counter_id(counterId);
- cmd->set_counter_value(card->getCounters().value(counterId, 0) - 1);
- commandList.append(cmd);
- }
- }
- break;
- }
- case 11: { // set counter with dialog
- player->setDialogSemaphore(true);
-
- // If a single card is selected, we show the old value in the dialog. Otherwise, we show "x"
- QList sel = player->getGameScene()->selectedItems();
- QString oldValueForDlg = "x";
- if (sel.size() == 1) {
- auto *card = dynamic_cast(sel.first());
- oldValueForDlg = QString::number(card->getCounters().value(counterId, 0));
- }
-
- auto &cardCounterSettings = SettingsCache::instance().cardCounters();
- QString counterName = cardCounterSettings.displayName(counterId);
-
- AbstractCounterDialog dialog(counterName, oldValueForDlg, player->getGame()->getTab());
- int ok = dialog.exec();
-
- player->setDialogSemaphore(false);
- if (player->clearCardsToDelete() || !ok) {
- return;
- }
-
- for (const auto &item : sel) {
- auto *card = dynamic_cast(item);
-
- int oldValue = card->getCounters().value(counterId, 0);
- Expression exp(oldValue);
- int number = static_cast(exp.parse(dialog.textValue()));
-
- auto *cmd = new Command_SetCardCounter;
- cmd->set_zone(card->getZone()->getName().toStdString());
- cmd->set_card_id(card->getId());
- cmd->set_counter_id(counterId);
- cmd->set_counter_value(number);
- commandList.append(cmd);
- }
- break;
- }
- default:;
}
+
+ sendGameCommand(prepareGameCommand(commandList));
+}
+
+void PlayerActions::actSetCardCounter(int counterId)
+{
+ player->setDialogSemaphore(true);
+
+ // If a single card is selected, we show the old value in the dialog. Otherwise, we show "x"
+ QList sel = player->getGameScene()->selectedItems();
+ QString oldValueForDlg = "x";
+ if (sel.size() == 1) {
+ auto *card = dynamic_cast(sel.first());
+ oldValueForDlg = QString::number(card->getCounters().value(counterId, 0));
+ }
+
+ auto &cardCounterSettings = SettingsCache::instance().cardCounters();
+ QString counterName = cardCounterSettings.displayName(counterId);
+
+ AbstractCounterDialog dialog(counterName, oldValueForDlg, player->getGame()->getTab());
+ int ok = dialog.exec();
+
+ player->setDialogSemaphore(false);
+ if (player->clearCardsToDelete() || !ok) {
+ return;
+ }
+
+ QList commandList;
+ for (const auto &item : sel) {
+ auto *card = dynamic_cast(item);
+
+ int oldValue = card->getCounters().value(counterId, 0);
+ Expression exp(oldValue);
+ int number = static_cast(exp.parse(dialog.textValue()));
+
+ auto *cmd = new Command_SetCardCounter;
+ cmd->set_zone(card->getZone()->getName().toStdString());
+ cmd->set_card_id(card->getId());
+ cmd->set_counter_id(counterId);
+ cmd->set_counter_value(number);
+ commandList.append(cmd);
+ }
+
sendGameCommand(prepareGameCommand(commandList));
}
diff --git a/cockatrice/src/game/player/player_actions.h b/cockatrice/src/game/player/player_actions.h
index 12c954dab..181b1b135 100644
--- a/cockatrice/src/game/player/player_actions.h
+++ b/cockatrice/src/game/player/player_actions.h
@@ -128,7 +128,9 @@ public slots:
void actCreateAllRelatedCards();
void actMoveCardXCardsFromTop();
- void actCardCounterTrigger();
+ void actRemoveCardCounter(int counterId);
+ void actAddCardCounter(int counterId);
+ void actSetCardCounter(int counterId);
void actAttach();
void actUnattach();
void actDrawArrow();
@@ -183,6 +185,8 @@ private:
void cmdSetTopCard(Command_MoveCard &cmd);
void cmdSetBottomCard(Command_MoveCard &cmd);
+
+ void offsetCardCounter(int counterId, int offset);
};
#endif // COCKATRICE_PLAYER_ACTIONS_H
From 9c53dad4b8893ed3877d9ac0c52a0c9ad1f44979 Mon Sep 17 00:00:00 2001
From: RickyRister <42636155+RickyRister@users.noreply.github.com>
Date: Sat, 9 May 2026 17:03:48 -0700
Subject: [PATCH 012/121] [Game] Refactor: add selectedCards method to
GameScene (#6859)
---
cockatrice/src/game/game_scene.cpp | 12 ++++
cockatrice/src/game/game_scene.h | 3 +
cockatrice/src/game/player/player.cpp | 15 +----
cockatrice/src/game/player/player_actions.cpp | 65 ++++++-------------
4 files changed, 37 insertions(+), 58 deletions(-)
diff --git a/cockatrice/src/game/game_scene.cpp b/cockatrice/src/game/game_scene.cpp
index 034ff6947..323adda38 100644
--- a/cockatrice/src/game/game_scene.cpp
+++ b/cockatrice/src/game/game_scene.cpp
@@ -58,6 +58,18 @@ void GameScene::retranslateUi()
view->retranslateUi();
}
+QList GameScene::selectedCards() const
+{
+ QList selectedCards;
+ for (auto item : selectedItems()) {
+ if (auto card = qgraphicsitem_cast(item)) {
+ selectedCards.append(card);
+ }
+ }
+
+ return selectedCards;
+}
+
/**
* @brief Adds a player to the scene and stores their graphics item.
* @param player Player to add.
diff --git a/cockatrice/src/game/game_scene.h b/cockatrice/src/game/game_scene.h
index f08e83aa4..2ce92b775 100644
--- a/cockatrice/src/game/game_scene.h
+++ b/cockatrice/src/game/game_scene.h
@@ -70,6 +70,9 @@ public:
/** Updates UI text for all zone views. */
void retranslateUi();
+ /** Gets all selected CardItems */
+ QList selectedCards() const;
+
/**
* @brief Adds a player to the scene and stores their graphics item.
* @param player Player to add.
diff --git a/cockatrice/src/game/player/player.cpp b/cockatrice/src/game/player/player.cpp
index 74a6eb21b..e95e2b2ef 100644
--- a/cockatrice/src/game/player/player.cpp
+++ b/cockatrice/src/game/player/player.cpp
@@ -309,19 +309,10 @@ void Player::clearCounters()
void Player::incrementAllCardCounters()
{
- QList cardsToUpdate;
-
- auto selectedItems = getGameScene()->selectedItems();
- if (!selectedItems.isEmpty()) {
- // If cards are selected, only update those
- for (const auto &item : selectedItems) {
- auto *card = static_cast(item);
- cardsToUpdate.append(card);
- }
- } else {
+ auto cardsToUpdate = getGameScene()->selectedCards();
+ if (cardsToUpdate.isEmpty()) {
// If no cards selected, update all cards on table
- const CardList &tableCards = getTableZone()->getCards();
- cardsToUpdate = tableCards;
+ cardsToUpdate = static_cast>(getTableZone()->getCards());
}
QList commandList;
diff --git a/cockatrice/src/game/player/player_actions.cpp b/cockatrice/src/game/player/player_actions.cpp
index 8d00e5ba3..bce8af74b 100644
--- a/cockatrice/src/game/player/player_actions.cpp
+++ b/cockatrice/src/game/player/player_actions.cpp
@@ -1173,16 +1173,11 @@ void PlayerActions::actMoveCardXCardsFromTop()
defaultNumberTopCardsToPlaceBelow = number;
- QList sel = player->getGameScene()->selectedItems();
- if (sel.isEmpty()) {
+ QList cardList = player->getGameScene()->selectedCards();
+ if (cardList.isEmpty()) {
return;
}
- QList cardList;
- while (!sel.isEmpty()) {
- cardList.append(qgraphicsitem_cast(sel.takeFirst()));
- }
-
QList commandList;
ListOfCardsToMove idList;
for (const auto &i : cardList) {
@@ -1214,8 +1209,7 @@ void PlayerActions::actIncPT(int deltaP, int deltaT)
int playerid = player->getPlayerInfo()->getId();
QList commandList;
- for (const auto &item : player->getGameScene()->selectedItems()) {
- auto *card = static_cast(item);
+ for (auto card : player->getGameScene()->selectedCards()) {
QString pt = card->getPT();
const auto ptList = CardItem::parsePT(pt);
QString newpt;
@@ -1247,8 +1241,7 @@ void PlayerActions::actResetPT()
{
int playerid = player->getPlayerInfo()->getId();
QList commandList;
- for (const auto &item : player->getGameScene()->selectedItems()) {
- auto *card = static_cast(item);
+ for (auto card : player->getGameScene()->selectedCards()) {
QString ptString;
if (!card->getFaceDown()) { // leave the pt empty if the card is face down
ExactCard ec = card->getCard();
@@ -1282,9 +1275,8 @@ void PlayerActions::actSetPT()
QString oldPT;
int playerid = player->getPlayerInfo()->getId();
- auto sel = player->getGameScene()->selectedItems();
- for (const auto &item : sel) {
- auto *card = static_cast(item);
+ auto cards = player->getGameScene()->selectedCards();
+ for (auto card : cards) {
if (!card->getPT().isEmpty()) {
oldPT = card->getPT();
}
@@ -1302,8 +1294,7 @@ void PlayerActions::actSetPT()
bool empty = ptList.isEmpty();
QList commandList;
- for (const auto &item : sel) {
- auto *card = static_cast(item);
+ for (auto card : cards) {
auto *cmd = new Command_SetCardAttr;
QString newpt = QString();
if (!empty) {
@@ -1400,9 +1391,8 @@ void AnnotationDialog::keyPressEvent(QKeyEvent *event)
void PlayerActions::actSetAnnotation()
{
QString oldAnnotation;
- auto sel = player->getGameScene()->selectedItems();
- for (const auto &item : sel) {
- auto *card = static_cast(item);
+ auto cards = player->getGameScene()->selectedCards();
+ for (auto card : cards) {
if (!card->getAnnotation().isEmpty()) {
oldAnnotation = card->getAnnotation();
}
@@ -1422,8 +1412,7 @@ void PlayerActions::actSetAnnotation()
QString annotation = dialog->textValue().left(MAX_NAME_LENGTH);
QList commandList;
- for (const auto &item : sel) {
- auto *card = static_cast(item);
+ for (auto card : cards) {
auto *cmd = new Command_SetCardAttr;
cmd->set_zone(card->getZone()->getName().toStdString());
cmd->set_card_id(card->getId());
@@ -1447,9 +1436,7 @@ void PlayerActions::actAttach()
void PlayerActions::actUnattach()
{
QList commandList;
- for (QGraphicsItem *item : player->getGameScene()->selectedItems()) {
- auto *card = static_cast(item);
-
+ for (auto card : player->getGameScene()->selectedCards()) {
if (!card->getAttachedTo()) {
continue;
}
@@ -1475,9 +1462,7 @@ void PlayerActions::actRemoveCardCounter(int counterId)
void PlayerActions::offsetCardCounter(int counterId, int offset)
{
QList commandList;
- for (const auto &item : player->getGameScene()->selectedItems()) {
- auto *card = static_cast(item);
-
+ for (auto card : player->getGameScene()->selectedCards()) {
int oldValue = card->getCounters().value(counterId, 0);
int newValue = oldValue + offset;
@@ -1499,10 +1484,10 @@ void PlayerActions::actSetCardCounter(int counterId)
player->setDialogSemaphore(true);
// If a single card is selected, we show the old value in the dialog. Otherwise, we show "x"
- QList sel = player->getGameScene()->selectedItems();
+ QList sel = player->getGameScene()->selectedCards();
QString oldValueForDlg = "x";
if (sel.size() == 1) {
- auto *card = dynamic_cast(sel.first());
+ auto *card = sel.first();
oldValueForDlg = QString::number(card->getCounters().value(counterId, 0));
}
@@ -1518,9 +1503,7 @@ void PlayerActions::actSetCardCounter(int counterId)
}
QList commandList;
- for (const auto &item : sel) {
- auto *card = dynamic_cast(item);
-
+ for (auto card : sel) {
int oldValue = card->getCounters().value(counterId, 0);
Expression exp(oldValue);
int number = static_cast(exp.parse(dialog.textValue()));
@@ -1550,11 +1533,7 @@ static bool isUnwritableRevealZone(CardZoneLogic *zone)
void PlayerActions::playSelectedCards(const bool faceDown)
{
- QList selectedCards;
- for (const auto &item : player->getGameScene()->selectedItems()) {
- auto *card = static_cast(item);
- selectedCards.append(card);
- }
+ QList selectedCards = player->getGameScene()->selectedCards();
// CardIds will get shuffled downwards when cards leave the deck.
// We need to iterate through the cards in reverse order so cardIds don't get changed out from under us as we play
@@ -1581,7 +1560,7 @@ void PlayerActions::actPlayFacedown()
void PlayerActions::actHide()
{
- for (const auto &item : player->getGameScene()->selectedItems()) {
+ for (const auto &item : player->getGameScene()->selectedCards()) {
auto *card = static_cast(item);
if (card && isUnwritableRevealZone(card->getZone())) {
card->getZone()->removeCard(card);
@@ -1598,9 +1577,7 @@ void PlayerActions::actReveal(QAction *action)
cmd.set_player_id(otherPlayerId);
}
- QList sel = player->getGameScene()->selectedItems();
- while (!sel.isEmpty()) {
- const auto *card = qgraphicsitem_cast(sel.takeFirst());
+ for (auto card : player->getGameScene()->selectedCards()) {
if (!cmd.has_zone_name()) {
cmd.set_zone_name(card->getZone()->getName().toStdString());
}
@@ -1685,11 +1662,7 @@ void PlayerActions::actRevealRandomGraveyardCard(int revealToPlayerId)
void PlayerActions::cardMenuAction()
{
auto *a = dynamic_cast(sender());
- QList sel = player->getGameScene()->selectedItems();
- QList cardList;
- while (!sel.isEmpty()) {
- cardList.append(qgraphicsitem_cast(sel.takeFirst()));
- }
+ QList cardList = player->getGameScene()->selectedCards();
QList commandList;
if (a->data().toInt() <= (int)cmClone) {
From dbaf5f2e059528e63489e14a102896761780ad97 Mon Sep 17 00:00:00 2001
From: BruebachL <44814898+BruebachL@users.noreply.github.com>
Date: Sun, 10 May 2026 11:35:17 +0200
Subject: [PATCH 013/121] [GameSelector] Don't conditionally initialize
gamesListProxyModel (#6870)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Took 44 seconds
Co-authored-by: Lukas Brübach
---
cockatrice/src/interface/widgets/server/game_selector.cpp | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/cockatrice/src/interface/widgets/server/game_selector.cpp b/cockatrice/src/interface/widgets/server/game_selector.cpp
index ea1a1cc91..dd29f4fc6 100644
--- a/cockatrice/src/interface/widgets/server/game_selector.cpp
+++ b/cockatrice/src/interface/widgets/server/game_selector.cpp
@@ -37,8 +37,9 @@ GameSelector::GameSelector(AbstractClient *_client,
connect(gameListView, &QTreeView::customContextMenuRequested, this, &GameSelector::customContextMenu);
gameListModel = new GamesModel(_rooms, _gameTypes, this);
+ gameListProxyModel = new GamesProxyModel(this, tabSupervisor->getUserListManager());
+
if (showFilters) {
- gameListProxyModel = new GamesProxyModel(this, tabSupervisor->getUserListManager());
gameListProxyModel->setSourceModel(gameListModel);
gameListProxyModel->setSortCaseSensitivity(Qt::CaseInsensitive);
gameListView->setModel(gameListProxyModel);
From 5735a44a9a7f40f38807b2979745546b8b14f8fd Mon Sep 17 00:00:00 2001
From: tooomm
Date: Sun, 10 May 2026 14:53:59 +0200
Subject: [PATCH 014/121] Update Dockerfile to 26.04 (#6861)
---
Dockerfile | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/Dockerfile b/Dockerfile
index d185b746a..7c5c773c9 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,5 +1,5 @@
# -------- Build Stage --------
-FROM ubuntu:24.04 AS build
+FROM ubuntu:26.04 AS build
ARG DEBIAN_FRONTEND=noninteractive
@@ -26,7 +26,7 @@ RUN mkdir build && cd build && \
# -------- Runtime Stage (clean) --------
-FROM ubuntu:24.04
+FROM ubuntu:26.04
RUN apt-get update && apt-get install -y --no-install-recommends \
libprotobuf32t64 \
From 1a62f82aeed2086210c194442988b4e6e021f552 Mon Sep 17 00:00:00 2001
From: DawnFire42
Date: Sun, 10 May 2026 22:10:14 -0400
Subject: [PATCH 015/121] Refactor vertical card stacking with clip containers
for variable zone sizes (#6774)
* Refactor vertical card stacking with opt-in overflow for variable zone sizes
Introduce a shared vertical stacking layout system in SelectZone that replaces the old divideCardSpaceInZone() free function with structured layout computation (StackLayoutParams, ZoneLayout, computeZoneLayout).
By default, cards are guaranteed to fit within zone bounds (no overflow). Zones can opt-in to bottom overflow via allowBottomOverflow flag, with sqrt-scaled compression for smooth visual transitions. A clip container mechanism is available for future zones that need visual clipping.
Key changes:
- SelectZone: new layout engine with allowBottomOverflow opt-in; clip container infrastructure for future zones needing visual clipping
- StackZone: uses new layout (no overflow); adds setHeight() for dynamic resizing capabilities
- HandZone: vertical layout delegates to SelectZone's shared stacking
- AbstractCardItem: preserves hover z-value during layout passes; invalidates scene rect on hover exit for proper sibling repainting
- CardZone::onCardAdded made virtual for clip container reparenting
- Zone widths updated to CardDimensions::WIDTH_F * 1.5
* Changed anonymous namespace for static and braced functions
* CI tests re-run
---
.../src/game/board/abstract_card_item.cpp | 16 +-
.../src/game/board/abstract_card_item.h | 6 +-
cockatrice/src/game/game_scene.cpp | 21 +-
cockatrice/src/game/game_scene.h | 6 +
cockatrice/src/game/zones/card_zone.cpp | 1 +
cockatrice/src/game/zones/card_zone.h | 7 +-
cockatrice/src/game/zones/hand_zone.cpp | 58 +++--
cockatrice/src/game/zones/hand_zone.h | 5 +-
cockatrice/src/game/zones/select_zone.cpp | 209 ++++++++++++++++--
cockatrice/src/game/zones/select_zone.h | 109 ++++++++-
cockatrice/src/game/zones/stack_zone.cpp | 71 +++---
cockatrice/src/game/zones/stack_zone.h | 4 +-
12 files changed, 402 insertions(+), 111 deletions(-)
diff --git a/cockatrice/src/game/board/abstract_card_item.cpp b/cockatrice/src/game/board/abstract_card_item.cpp
index 9ec6ada9a..a519fa4b6 100644
--- a/cockatrice/src/game/board/abstract_card_item.cpp
+++ b/cockatrice/src/game/board/abstract_card_item.cpp
@@ -85,7 +85,11 @@ const CardInfo &AbstractCardItem::getCardInfo() const
void AbstractCardItem::setRealZValue(qreal _zValue)
{
realZValue = _zValue;
- setZValue(_zValue);
+ // During hover, zValue is overridden to HOVERED_CARD. Layout operations
+ // like reorganizeCards() call setRealZValue() on all cards including the
+ // hovered one — skip setZValue() here to avoid clobbering the override.
+ if (!isHovered)
+ setZValue(_zValue);
}
QSizeF AbstractCardItem::getTranslatedSize(QPainter *painter) const
@@ -213,8 +217,16 @@ void AbstractCardItem::setHovered(bool _hovered)
if (isHovered == _hovered)
return;
- if (_hovered)
+ if (_hovered) {
processHoverEvent();
+ } else {
+ // Mark the hovered card's current scene footprint dirty so overlapped
+ // sibling zones (e.g. StackZone) repaint after the card moves away.
+ if (scene()) {
+ scene()->update(sceneBoundingRect());
+ }
+ }
+
isHovered = _hovered;
setZValue(_hovered ? ZValues::HOVERED_CARD : realZValue);
setScale(_hovered && SettingsCache::instance().getScaleCards() ? 1.1 : 1);
diff --git a/cockatrice/src/game/board/abstract_card_item.h b/cockatrice/src/game/board/abstract_card_item.h
index 7d2c29cae..517b5cf28 100644
--- a/cockatrice/src/game/board/abstract_card_item.h
+++ b/cockatrice/src/game/board/abstract_card_item.h
@@ -1,7 +1,7 @@
/**
* @file abstract_card_item.h
* @ingroup GameGraphicsCards
- * @brief TODO: Document this.
+ * @brief Base class for graphical card items, providing shared rendering, identity, and interaction logic.
*/
#ifndef ABSTRACTCARDITEM_H
@@ -96,6 +96,10 @@ public:
}
void setRealZValue(qreal _zValue);
void setHovered(bool _hovered);
+ bool getIsHovered() const
+ {
+ return isHovered;
+ }
QString getColor() const
{
return color;
diff --git a/cockatrice/src/game/game_scene.cpp b/cockatrice/src/game/game_scene.cpp
index 323adda38..aa6f1e6f1 100644
--- a/cockatrice/src/game/game_scene.cpp
+++ b/cockatrice/src/game/game_scene.cpp
@@ -5,6 +5,7 @@
#include "phases_toolbar.h"
#include "player/player.h"
#include "player/player_graphics_item.h"
+#include "zones/select_zone.h"
#include "zones/view_zone.h"
#include "zones/view_zone_widget.h"
@@ -356,12 +357,26 @@ void GameScene::updateHover(const QPointF &scenePos)
void GameScene::updateHoveredCard(CardItem *newCard)
{
if (hoveredCard && (newCard != hoveredCard))
- hoveredCard->setHovered(false);
+ endCardHover(hoveredCard);
if (newCard && (newCard != hoveredCard))
- newCard->setHovered(true);
+ beginCardHover(newCard);
hoveredCard = newCard;
}
+void GameScene::beginCardHover(CardItem *card)
+{
+ card->setHovered(true);
+ if (auto *zone = SelectZone::findOwningSelectZone(card))
+ zone->escapeClipForHover(card);
+}
+
+void GameScene::endCardHover(CardItem *card)
+{
+ if (auto *zone = SelectZone::findOwningSelectZone(card))
+ zone->restoreClipAfterHover(card);
+ card->setHovered(false);
+}
+
CardZone *GameScene::findTopmostZone(const QList &items)
{
for (QGraphicsItem *item : items)
@@ -496,6 +511,8 @@ bool GameScene::event(QEvent *event)
{
if (event->type() == QEvent::GraphicsSceneMouseMove)
updateHover(static_cast(event)->scenePos());
+ else if (event->type() == QEvent::Leave)
+ updateHoveredCard(nullptr);
return QGraphicsScene::event(event);
}
diff --git a/cockatrice/src/game/game_scene.h b/cockatrice/src/game/game_scene.h
index 2ce92b775..6d491297c 100644
--- a/cockatrice/src/game/game_scene.h
+++ b/cockatrice/src/game/game_scene.h
@@ -56,6 +56,12 @@ private:
*/
void updateHover(const QPointF &scenePos);
+ /// Activates hover state and escapes the card from its clip container so hover scaling is visible beyond zone
+ /// bounds.
+ void beginCardHover(CardItem *card);
+ /// Deactivates hover state and restores the card to its clip container.
+ void endCardHover(CardItem *card);
+
public:
/**
* @brief Constructs the GameScene.
diff --git a/cockatrice/src/game/zones/card_zone.cpp b/cockatrice/src/game/zones/card_zone.cpp
index 0c189cd2b..cd7ae6a56 100644
--- a/cockatrice/src/game/zones/card_zone.cpp
+++ b/cockatrice/src/game/zones/card_zone.cpp
@@ -19,6 +19,7 @@ CardZone::CardZone(CardZoneLogic *_logic, QGraphicsItem *parent)
void CardZone::onCardAdded(CardItem *addedCard)
{
addedCard->setParentItem(this);
+ addedCard->setVisible(true);
addedCard->update();
}
diff --git a/cockatrice/src/game/zones/card_zone.h b/cockatrice/src/game/zones/card_zone.h
index 6fe8157e4..5df48f7fb 100644
--- a/cockatrice/src/game/zones/card_zone.h
+++ b/cockatrice/src/game/zones/card_zone.h
@@ -1,7 +1,7 @@
/**
* @file card_zone.h
* @ingroup GameGraphicsZones
- * @brief TODO: Document this.
+ * @brief Base graphics item for zones that contain cards.
*/
#ifndef CARDZONE_H
@@ -40,7 +40,10 @@ protected:
}
public slots:
bool showContextMenu(const QPoint &screenPos);
- void onCardAdded(CardItem *addedCard);
+ /// @brief Called when a card is added to this zone. Default: reparents card to this item.
+ /// Virtual so subclasses (e.g. SelectZone) can override parenting behavior — the Qt signal
+ /// connection in CardZone's constructor dispatches through the vtable.
+ virtual void onCardAdded(CardItem *addedCard);
public:
enum
diff --git a/cockatrice/src/game/zones/hand_zone.cpp b/cockatrice/src/game/zones/hand_zone.cpp
index 7badfcca4..506a26a80 100644
--- a/cockatrice/src/game/zones/hand_zone.cpp
+++ b/cockatrice/src/game/zones/hand_zone.cpp
@@ -27,6 +27,10 @@ void HandZone::handleDropEvent(const QList &dragItems,
CardZoneLogic *startZone,
const QPoint &dropPoint)
{
+ if (startZone == nullptr || startZone->getPlayer() == nullptr || dragItems.isEmpty()) {
+ return;
+ }
+
QPoint point = dropPoint + scenePos().toPoint();
int x = -1;
if (SettingsCache::instance().getHorizontalHand()) {
@@ -34,9 +38,7 @@ void HandZone::handleDropEvent(const QList &dragItems,
if (point.x() < static_cast(getLogic()->getCards().at(x))->scenePos().x())
break;
} else {
- for (x = 0; x < getLogic()->getCards().size(); x++)
- if (point.y() < static_cast(getLogic()->getCards().at(x))->scenePos().y())
- break;
+ x = calcDropIndexFromY(dropPoint.y());
}
Command_MoveCard cmd;
@@ -58,7 +60,7 @@ QRectF HandZone::boundingRect() const
if (SettingsCache::instance().getHorizontalHand())
return QRectF(0, 0, width, CardDimensions::HEIGHT_F + 10);
else
- return QRectF(0, 0, 100, zoneHeight);
+ return QRectF(0, 0, CardDimensions::WIDTH_F * 1.5, zoneHeight);
}
void HandZone::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*option*/, QWidget * /*widget*/)
@@ -78,35 +80,31 @@ void HandZone::reorganizeCards()
qreal totalWidth =
leftJustified ? boundingRect().width() - (1 * xPadding) - 5 : boundingRect().width() - 2 * xPadding;
- for (int i = 0; i < cardCount; i++) {
- CardItem *c = getLogic()->getCards().at(i);
- // If the total width of the cards is smaller than the available width,
- // the cards do not need to overlap and are displayed in the center of the area.
- if (cardWidth * cardCount > totalWidth)
- c->setPos(xPadding + ((qreal)i) * (totalWidth - cardWidth) / (cardCount - 1), 5);
- else {
- qreal xPosition =
- leftJustified ? xPadding + ((qreal)i) * cardWidth
- : xPadding + ((qreal)i) * cardWidth + (totalWidth - cardCount * cardWidth) / 2;
- c->setPos(xPosition, 5);
+ if (cardCount == 1) {
+ CardItem *c = getLogic()->getCards().at(0);
+ qreal xPosition = leftJustified ? xPadding : xPadding + (totalWidth - cardWidth) / 2;
+ c->setPos(xPosition, 5);
+ c->setRealZValue(0);
+ } else {
+ for (int i = 0; i < cardCount; i++) {
+ CardItem *c = getLogic()->getCards().at(i);
+ // If the total width of the cards is smaller than the available width,
+ // the cards do not need to overlap and are displayed in the center of the area.
+ if (cardWidth * cardCount > totalWidth)
+ c->setPos(xPadding + ((qreal)i) * (totalWidth - cardWidth) / (cardCount - 1), 5);
+ else {
+ qreal xPosition = leftJustified ? xPadding + ((qreal)i) * cardWidth
+ : xPadding + ((qreal)i) * cardWidth +
+ (totalWidth - cardCount * cardWidth) / 2;
+ c->setPos(xPosition, 5);
+ }
+ c->setRealZValue(i);
}
- c->setRealZValue(i);
}
} else {
- qreal totalWidth = boundingRect().width();
- qreal cardWidth = getLogic()->getCards().at(0)->boundingRect().width();
- qreal xspace = 5;
- qreal x1 = xspace;
- qreal x2 = totalWidth - xspace - cardWidth;
-
- for (int i = 0; i < cardCount; i++) {
- CardItem *card = getLogic()->getCards().at(i);
- qreal x = (i % 2) ? x2 : x1;
- qreal y = divideCardSpaceInZone(i, cardCount, boundingRect().height(),
- getLogic()->getCards().at(0)->boundingRect().height());
- card->setPos(x, y);
- card->setRealZValue(i);
- }
+ // No clip container: hand cards should always be visible to the player.
+ const auto params = buildStackParams();
+ layoutCardsVertically(params);
}
}
update();
diff --git a/cockatrice/src/game/zones/hand_zone.h b/cockatrice/src/game/zones/hand_zone.h
index 25f4148bd..586302308 100644
--- a/cockatrice/src/game/zones/hand_zone.h
+++ b/cockatrice/src/game/zones/hand_zone.h
@@ -1,7 +1,7 @@
/**
* @file hand_zone.h
* @ingroup GameGraphicsZones
- * @brief TODO: Document this.
+ * @brief Graphical zone for the player's hand, supporting horizontal and vertical layouts.
*/
#ifndef HANDZONE_H
@@ -14,7 +14,8 @@ class HandZone : public SelectZone
{
Q_OBJECT
private:
- qreal width, zoneHeight;
+ qreal width = 0.0;
+ qreal zoneHeight;
private slots:
void updateBg();
public slots:
diff --git a/cockatrice/src/game/zones/select_zone.cpp b/cockatrice/src/game/zones/select_zone.cpp
index 9bf5f9faf..fd733e01f 100644
--- a/cockatrice/src/game/zones/select_zone.cpp
+++ b/cockatrice/src/game/zones/select_zone.cpp
@@ -4,38 +4,207 @@
#include "../board/card_item.h"
#include "../game_scene.h"
-#include
+#include
#include
+#include
-qreal divideCardSpaceInZone(qreal index, int cardCount, qreal totalHeight, qreal cardHeight, bool reverse)
+static qreal stackingOffset(qreal cardHeight)
{
- qreal cardMinOverlap = cardHeight * SettingsCache::instance().getStackCardOverlapPercent() / 100;
- qreal desiredHeight = cardHeight * cardCount - cardMinOverlap * (cardCount - 1);
- qreal y;
- if (desiredHeight > totalHeight) {
- if (reverse) {
- y = index / ((totalHeight - cardHeight) / (cardCount - 1));
+ const qreal overlapPercent = SettingsCache::instance().getStackCardOverlapPercent();
+ return cardHeight * (100.0 - overlapPercent) / 100.0;
+}
+
+SelectZone::ZoneLayout SelectZone::computeZoneLayout(const StackLayoutParams ¶ms)
+{
+ if (params.cardCount <= 0) {
+ return {0.0, 0.0};
+ }
+ qreal effectiveOffset = params.desiredOffset;
+ if (params.cardCount > 1) {
+ qreal fitOffset;
+ if (params.totalHeight < params.cardHeight && params.minOffset > 0.0) {
+ // Zone is shorter than a card (e.g. minimized). Compress offsets so
+ // every card has at least minOffset pixels of its top visible.
+ fitOffset = (params.totalHeight - params.minOffset) / (params.cardCount - 1);
+ effectiveOffset = qMax(0.0, qMin(params.desiredOffset, fitOffset));
} else {
- y = index * (totalHeight - cardHeight) / (cardCount - 1);
- }
- } else {
- qreal start = (totalHeight - desiredHeight) / 2;
- if (reverse) {
- if (index <= start) {
- return 0;
+ qreal reservedForBottomCard;
+ if (params.allowBottomOverflow) {
+ // Allow the bottom card to partially overflow in tight zones, scaling the
+ // overflow allowance by sqrt(cardCount-1) so offsets decrease smoothly
+ // as cards are added rather than dropping by 1/(n-1) each time.
+ // The 0.75 ratio was tuned experimentally to balance card visibility vs. overflow.
+ constexpr qreal bottomCardZoneRatio = 0.75;
+ const qreal adjustedRatio = bottomCardZoneRatio / qSqrt(static_cast(params.cardCount - 1));
+ reservedForBottomCard = qMin(params.cardHeight, params.totalHeight * adjustedRatio);
+ } else {
+ // No overflow: reserve full card height for the bottom card
+ reservedForBottomCard = params.cardHeight;
}
- y = (index - start) / (cardHeight - cardMinOverlap);
- } else {
- y = index * (cardHeight - cardMinOverlap) + start;
+ fitOffset = (params.totalHeight - reservedForBottomCard) / (params.cardCount - 1);
+ effectiveOffset = qMax(params.minOffset, qMin(params.desiredOffset, fitOffset));
}
}
- return y;
+ qreal stackHeight = (params.cardCount - 1) * effectiveOffset + params.cardHeight;
+ qreal start = (stackHeight <= params.totalHeight) ? (params.totalHeight - stackHeight) / 2.0 : 0.0;
+ return {effectiveOffset, start};
+}
+
+SelectZone *SelectZone::findOwningSelectZone(const QGraphicsItem *card)
+{
+ QGraphicsItem *parent = card ? card->parentItem() : nullptr;
+ if (!parent) {
+ return nullptr;
+ }
+ // Card may be direct child of zone (escaped for hover) or child of clip container.
+ if (auto *zone = dynamic_cast(parent)) {
+ return zone;
+ }
+ if (auto *zone = dynamic_cast(parent->parentItem())) {
+ return zone;
+ }
+ return nullptr;
+}
+
+SelectZone::StackLayoutParams SelectZone::buildStackParams(qreal minOffset) const
+{
+ const auto &cards = getLogic()->getCards();
+ if (cards.isEmpty())
+ return {0, boundingRect().height(), 0.0, 0.0, minOffset};
+ const auto cardCount = static_cast(cards.size());
+ const qreal cardHeight = cards.at(0)->boundingRect().height();
+ const qreal offset = stackingOffset(cardHeight);
+ return {cardCount, boundingRect().height(), cardHeight, offset, minOffset};
+}
+
+int SelectZone::calcDropIndexFromY(qreal dropY, qreal minOffset) const
+{
+ const auto &cards = getLogic()->getCards();
+ if (cards.isEmpty()) {
+ return 0;
+ }
+ const auto params = buildStackParams(minOffset);
+ auto [effectiveOffset, start] = computeZoneLayout(params);
+ if (effectiveOffset <= 0.0) {
+ return 0;
+ }
+ return qBound(0, qRound((dropY - start) / effectiveOffset), params.cardCount - 1);
+}
+
+void SelectZone::restoreStaleEscapedCards()
+{
+ if (!cardClipContainer)
+ return;
+ for (auto *card : getLogic()->getCards()) {
+ // A card parented to the zone (instead of the clip container) should
+ // only occur while it is actively hovered. If hover cleanup was
+ // missed, reparent it back so clipping resumes.
+ if (card && card->parentItem() == this && !card->getIsHovered()) {
+ card->setParentItem(cardClipContainer);
+ }
+ }
+}
+
+void SelectZone::layoutCardsVertically(const StackLayoutParams ¶ms)
+{
+ const auto &cards = getLogic()->getCards();
+ if (cards.isEmpty() || params.cardCount <= 0)
+ return;
+ if (params.cardCount > cards.size())
+ return;
+
+ constexpr qreal xspace = 5;
+ const qreal cardWidth = cards.at(0)->boundingRect().width();
+ const qreal totalWidth = boundingRect().width();
+ const qreal x1 = xspace;
+ const qreal x2 = totalWidth - xspace - cardWidth;
+ const qreal xCentered = (totalWidth - cardWidth) / 2.0;
+
+ auto [effectiveOffset, start] = computeZoneLayout(params);
+ for (int i = 0; i < params.cardCount; i++) {
+ CardItem *card = cards.at(i);
+ qreal y = start + i * effectiveOffset;
+ // Center single card; alternate left/right for multiple cards
+ qreal x = (params.cardCount == 1) ? xCentered : ((i % 2) ? x2 : x1);
+ card->setPos(x, y);
+ card->setRealZValue(i);
+ }
}
SelectZone::SelectZone(CardZoneLogic *_logic, QGraphicsItem *parent) : CardZone(_logic, parent)
{
}
+SelectZone::~SelectZone()
+{
+ if (cardClipContainer) {
+ // Reparent any hover-escaped cards back to the clip container so Qt's
+ // parent-child tree is consistent for destruction. setParentItem() does
+ // not invalidate getLogic()->getCards() (it modifies the graphics tree,
+ // not the zone's logical card list).
+ for (auto *card : getLogic()->getCards()) {
+ if (card && card->parentItem() == this) {
+ card->setParentItem(cardClipContainer);
+ }
+ }
+ }
+}
+
+void SelectZone::onCardAdded(CardItem *addedCard)
+{
+ if (cardClipContainer && addedCard) {
+ addedCard->setParentItem(cardClipContainer);
+ addedCard->setVisible(true);
+ addedCard->update();
+ } else {
+ CardZone::onCardAdded(addedCard);
+ }
+}
+
+void SelectZone::setupClipContainer(std::optional zValue)
+{
+ if (cardClipContainer)
+ return;
+
+ setFlag(QGraphicsItem::ItemClipsChildrenToShape, false);
+
+ cardClipContainer = new QGraphicsRectItem(this); // Owned by Qt parent-child tree; deleted with this zone.
+ cardClipContainer->setFlag(QGraphicsItem::ItemClipsChildrenToShape, true);
+ cardClipContainer->setPen(Qt::NoPen);
+ cardClipContainer->setBrush(Qt::NoBrush);
+ cardClipContainer->setRect(boundingRect());
+ if (zValue.has_value()) {
+ cardClipContainer->setZValue(*zValue);
+ }
+}
+
+void SelectZone::escapeClipForHover(QGraphicsItem *card)
+{
+ // Reparent from clip container to zone so the hover-scaled card is visible
+ // beyond clip bounds. Coordinates are identical because the clip container
+ // is at (0,0) with no transform relative to this zone.
+ if (cardClipContainer && card && card->parentItem() == cardClipContainer) {
+ card->setParentItem(this);
+ cardClipContainer->update();
+ }
+}
+
+void SelectZone::restoreClipAfterHover(QGraphicsItem *card)
+{
+ // Restore card to clip container. If card's parent is not this zone,
+ // a zone transition already reparented it via onCardAdded — skip.
+ if (cardClipContainer && card && card->parentItem() == this) {
+ card->setParentItem(cardClipContainer);
+ }
+}
+
+void SelectZone::updateClipRect()
+{
+ if (cardClipContainer) {
+ cardClipContainer->setRect(boundingRect());
+ }
+}
+
void SelectZone::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
{
if (event->buttons().testFlag(Qt::LeftButton)) {
@@ -56,7 +225,7 @@ void SelectZone::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
continue;
}
- bool inRect = selectionRect.intersects(card->mapRectToParent(card->boundingRect()));
+ bool inRect = selectionRect.intersects(card->mapRectToItem(this, card->boundingRect()));
if (inRect && !cardsInSelectionRect.contains(card)) {
// selection has just expanded to cover the card
cardsInSelectionRect.insert(card);
diff --git a/cockatrice/src/game/zones/select_zone.h b/cockatrice/src/game/zones/select_zone.h
index d6fd3e10e..1da5c1988 100644
--- a/cockatrice/src/game/zones/select_zone.h
+++ b/cockatrice/src/game/zones/select_zone.h
@@ -1,7 +1,7 @@
/**
* @file select_zone.h
* @ingroup GameGraphicsZones
- * @brief TODO: Document this.
+ * @brief Base class for zones where cards are laid out and individually interactable.
*/
#ifndef SELECTZONE_H
@@ -10,6 +10,9 @@
#include "card_zone.h"
#include
+#include
+
+class QGraphicsRectItem;
/**
* A CardZone where the cards are laid out, with each card directly interactable by clicking.
@@ -17,19 +20,113 @@
class SelectZone : public CardZone
{
Q_OBJECT
+public:
+ /// Finds the SelectZone that owns a card, regardless of whether the card is parented
+ /// to the zone directly or to its clip container. Returns nullptr if not in a SelectZone.
+ static SelectZone *findOwningSelectZone(const QGraphicsItem *card);
+
+ SelectZone(CardZoneLogic *logic, QGraphicsItem *parent = nullptr);
+ ~SelectZone() override;
+ void onCardAdded(CardItem *addedCard) override;
+
+ /// @brief Temporarily reparents a card from the clip container to this zone so hover scaling is visible beyond clip
+ /// bounds. Safe no-op if no clip container exists. Coordinates are preserved (clip container is at (0,0) with no
+ /// transform).
+ void escapeClipForHover(QGraphicsItem *card);
+ /// @brief Restores a hover-escaped card back to the clip container. Guards against zone transitions that already
+ /// reparented the card.
+ void restoreClipAfterHover(QGraphicsItem *card);
+
private:
QPointF selectionOrigin;
QSet cardsInSelectionRect;
+ /// Invisible clipping parent for cards; owned by Qt parent-child tree (parented to this zone).
+ /// Created by setupClipContainer(); null when no clip container is active.
+ QGraphicsRectItem *cardClipContainer = nullptr;
protected:
+ // -- Layout computation --
+
+ /// Parameters describing a vertical card stack's geometry.
+ struct StackLayoutParams
+ {
+ int cardCount; ///< Number of cards in the stack
+ qreal totalHeight; ///< Available height for the stack (zone height)
+ qreal cardHeight; ///< Height of a single card
+ qreal desiredOffset; ///< Preferred vertical offset between card tops
+ qreal minOffset = 0.0; ///< Minimum offset to preserve (0 allows full compression)
+ /// When false (default), reserves full cardHeight for the bottom card, ensuring
+ /// all cards remain within zone bounds. When true, allows the bottom card to
+ /// partially overflow using sqrt-scaled allowance. Use with setupClipContainer()
+ /// for zones too short to fit a full card.
+ bool allowBottomOverflow = false;
+ };
+
+ /// Result of computing a vertical stack layout.
+ struct ZoneLayout
+ {
+ qreal effectiveOffset; ///< Actual offset between card tops (may be compressed)
+ qreal start; ///< Y coordinate of the first card's top edge
+ };
+
+ /// Minimum visible pixels of each card's top edge when stacking compresses offsets in tight zones.
+ static constexpr qreal MIN_CARD_VISIBLE = 10.0;
+
+ /**
+ * @brief Computes layout for a vertical card stack (effective offset and start position).
+ *
+ * Three regimes:
+ * 1. Minimized zone (totalHeight < card height with minOffset > 0): offsets compress
+ * so each card retains at least minOffset visible pixels of its top edge.
+ * 2. Normal zone with allowBottomOverflow=false (default): the bottom card is
+ * guaranteed to fit within the zone boundary. Offsets compress as needed.
+ * 3. Normal zone with allowBottomOverflow=true: the bottom card may partially
+ * overflow. The overflow allowance is scaled by sqrt(cardCount-1) so that
+ * adding one card shifts existing cards smoothly.
+ *
+ * When the stack fits with room to spare, it is centered vertically.
+ */
+ static ZoneLayout computeZoneLayout(const StackLayoutParams ¶ms);
+
+ /// Builds StackLayoutParams from the current card list and zone geometry.
+ StackLayoutParams buildStackParams(qreal minOffset = 0.0) const;
+
+ /// Computes the card index at a given y-coordinate within the zone's vertical layout.
+ /// Returns 0 if the zone has no cards or the offset is zero.
+ int calcDropIndexFromY(qreal dropY, qreal minOffset = 0.0) const;
+
+ /**
+ * @brief Positions cards vertically with alternating left/right x-offsets.
+ *
+ * Cards alternate between left and right margins (5px padding from zone edges):
+ * even-indexed cards at left, odd-indexed at right.
+ * Cards are assigned ascending z-values.
+ *
+ * @param params Stack layout geometry parameters (use allowBottomOverflow to control overflow)
+ */
+ void layoutCardsVertically(const StackLayoutParams ¶ms);
+
+ // -- Clip container --
+ // The clip container mechanism is available for future zones that need visual clipping
+ // (e.g., zones too short to fit a full card). To enable: call setupClipContainer() in the
+ // zone's constructor, and set allowBottomOverflow=true in layout params.
+
+ /// Restores any cards that were hover-escaped but whose hover state was not properly cleaned up.
+ /// Call at the start of reorganizeCards() in zones that use a clip container.
+ void restoreStaleEscapedCards();
+
+ /// Creates a clip container child item that clips card overflow to zone bounds.
+ /// Cards entering this zone are reparented to this container by the onCardAdded override.
+ /// Disables zone-level child clipping; clipping is delegated to the container.
+ /// @param zValue Optional z-value for the clip container (e.g. ZValues::CARD_BASE)
+ void setupClipContainer(std::optional zValue = std::nullopt);
+
+ /// Updates the clip container rect to match this zone's current boundingRect().
+ void updateClipRect();
+
void mouseMoveEvent(QGraphicsSceneMouseEvent *event) override;
void mousePressEvent(QGraphicsSceneMouseEvent *event) override;
void mouseReleaseEvent(QGraphicsSceneMouseEvent *event) override;
-
-public:
- SelectZone(CardZoneLogic *logic, QGraphicsItem *parent = nullptr);
};
-qreal divideCardSpaceInZone(qreal index, int cardCount, qreal totalHeight, qreal cardHeight, bool reverse = false);
-
#endif
diff --git a/cockatrice/src/game/zones/stack_zone.cpp b/cockatrice/src/game/zones/stack_zone.cpp
index a9d649a4a..c0da0bfd2 100644
--- a/cockatrice/src/game/zones/stack_zone.cpp
+++ b/cockatrice/src/game/zones/stack_zone.cpp
@@ -1,10 +1,9 @@
#include "stack_zone.h"
-#include "../../client/settings/cache_settings.h"
#include "../../interface/theme_manager.h"
-#include "../board/arrow_item.h"
#include "../board/card_drag_item.h"
#include "../board/card_item.h"
+#include "../card_dimensions.h"
#include "../player/player.h"
#include "../player/player_actions.h"
#include "logic/stack_zone_logic.h"
@@ -27,7 +26,7 @@ void StackZone::updateBg()
QRectF StackZone::boundingRect() const
{
- return {0, 0, 100, zoneHeight};
+ return {0, 0, CardDimensions::WIDTH_F * 1.5, zoneHeight};
}
void StackZone::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*option*/, QWidget * /*widget*/)
@@ -40,7 +39,15 @@ void StackZone::handleDropEvent(const QList &dragItems,
CardZoneLogic *startZone,
const QPoint &dropPoint)
{
- if (startZone == nullptr || startZone->getPlayer() == nullptr) {
+ if (startZone == nullptr || startZone->getPlayer() == nullptr || dragItems.isEmpty()) {
+ return;
+ }
+
+ int index = calcDropIndexFromY(dropPoint.y(), MIN_CARD_VISIBLE);
+
+ // Same-zone no-op: don't move a card onto itself
+ const auto &cards = getLogic()->getCards();
+ if (!cards.isEmpty() && startZone == getLogic() && cards.at(index)->getId() == dragItems.at(0)->getId()) {
return;
}
@@ -49,36 +56,12 @@ void StackZone::handleDropEvent(const QList &dragItems,
cmd.set_start_zone(startZone->getName().toStdString());
cmd.set_target_player_id(getLogic()->getPlayer()->getPlayerInfo()->getId());
cmd.set_target_zone(getLogic()->getName().toStdString());
-
- int index = 0;
-
- if (!getLogic()->getCards().isEmpty()) {
- const auto cardCount = static_cast(getLogic()->getCards().size());
- const auto &card = getLogic()->getCards().at(0);
-
- index = qRound(divideCardSpaceInZone(dropPoint.y(), cardCount, boundingRect().height(),
- card->boundingRect().height(), true));
-
- // divideCardSpaceInZone is not guaranteed to return a valid index
- // currently, so clamp it to avoid crashes.
- index = qBound(0, index, cardCount - 1);
-
- if (startZone == getLogic()) {
- const auto &dragItem = dragItems.at(0);
- const auto &card = getLogic()->getCards().at(index);
-
- if (card->getId() == dragItem->getId()) {
- return;
- }
- }
- }
-
cmd.set_x(index);
cmd.set_y(0);
- for (CardDragItem *item : dragItems) {
+ for (const CardDragItem *item : dragItems) {
if (item) {
- auto cardToMove = cmd.mutable_cards_to_move()->add_card();
+ auto *cardToMove = cmd.mutable_cards_to_move()->add_card();
cardToMove->set_card_id(item->getId());
if (item->isForceFaceDown()) {
cardToMove->set_face_down(true);
@@ -89,24 +72,22 @@ void StackZone::handleDropEvent(const QList &dragItems,
getLogic()->getPlayer()->getPlayerActions()->sendGameCommand(cmd);
}
+void StackZone::setHeight(qreal newHeight)
+{
+ if (qFuzzyCompare(1.0 + zoneHeight, 1.0 + newHeight)) {
+ return;
+ }
+ prepareGeometryChange();
+ zoneHeight = newHeight;
+ reorganizeCards();
+ update();
+}
+
void StackZone::reorganizeCards()
{
if (!getLogic()->getCards().isEmpty()) {
- const auto cardCount = static_cast(getLogic()->getCards().size());
- qreal totalWidth = boundingRect().width();
- qreal cardWidth = getLogic()->getCards().at(0)->boundingRect().width();
- qreal xspace = 5;
- qreal x1 = xspace;
- qreal x2 = totalWidth - xspace - cardWidth;
-
- for (int i = 0; i < cardCount; i++) {
- CardItem *card = getLogic()->getCards().at(i);
- qreal x = (i % 2) ? x2 : x1;
- qreal y = divideCardSpaceInZone(i, cardCount, boundingRect().height(),
- getLogic()->getCards().at(0)->boundingRect().height());
- card->setPos(x, y);
- card->setRealZValue(i);
- }
+ const auto params = buildStackParams(MIN_CARD_VISIBLE);
+ layoutCardsVertically(params);
}
update();
}
diff --git a/cockatrice/src/game/zones/stack_zone.h b/cockatrice/src/game/zones/stack_zone.h
index 7c98f5128..8f5bed09d 100644
--- a/cockatrice/src/game/zones/stack_zone.h
+++ b/cockatrice/src/game/zones/stack_zone.h
@@ -1,7 +1,7 @@
/**
* @file stack_zone.h
* @ingroup GameGraphicsZones
- * @brief TODO: Document this.
+ * @brief Graphical zone for the stack, displaying cards in a vertical pile.
*/
#ifndef STACKZONE_H
@@ -20,6 +20,8 @@ private slots:
public:
StackZone(StackZoneLogic *_logic, int _zoneHeight, QGraphicsItem *parent);
+ /// @brief Resizes the stack zone height, e.g. when sharing vertical space with the command zone.
+ void setHeight(qreal newHeight);
void
handleDropEvent(const QList &dragItems, CardZoneLogic *startZone, const QPoint &dropPoint) override;
QRectF boundingRect() const override;
From b66743c83c7f5b5dcbb9a239ece3ec611aa499f9 Mon Sep 17 00:00:00 2001
From: RickyRister <42636155+RickyRister@users.noreply.github.com>
Date: Mon, 11 May 2026 20:23:12 -0700
Subject: [PATCH 016/121] [DeckEditor] Refactor searchEdit highlighting after
add card (#6874)
* [DeckEditor] Refactor searchEdit highlighting after add card
* make searchEdit private
---
.../deck_editor/deck_editor_card_database_dock_widget.cpp | 4 ----
.../deck_editor/deck_editor_card_database_dock_widget.h | 1 -
.../deck_editor/deck_editor_database_display_widget.cpp | 7 +++++++
.../deck_editor/deck_editor_database_display_widget.h | 4 +++-
.../interface/widgets/tabs/abstract_tab_deck_editor.cpp | 2 --
5 files changed, 10 insertions(+), 8 deletions(-)
diff --git a/cockatrice/src/interface/widgets/deck_editor/deck_editor_card_database_dock_widget.cpp b/cockatrice/src/interface/widgets/deck_editor/deck_editor_card_database_dock_widget.cpp
index bacebe385..f2a2ab4ea 100644
--- a/cockatrice/src/interface/widgets/deck_editor/deck_editor_card_database_dock_widget.cpp
+++ b/cockatrice/src/interface/widgets/deck_editor/deck_editor_card_database_dock_widget.cpp
@@ -58,7 +58,3 @@ void DeckEditorCardDatabaseDockWidget::clearAllDatabaseFilters()
{
databaseDisplayWidget->clearAllDatabaseFilters();
}
-void DeckEditorCardDatabaseDockWidget::highlightAllSearchEdit()
-{
- databaseDisplayWidget->searchEdit->setSelection(0, databaseDisplayWidget->searchEdit->text().length());
-}
diff --git a/cockatrice/src/interface/widgets/deck_editor/deck_editor_card_database_dock_widget.h b/cockatrice/src/interface/widgets/deck_editor/deck_editor_card_database_dock_widget.h
index 6ad442075..bff9ee36f 100644
--- a/cockatrice/src/interface/widgets/deck_editor/deck_editor_card_database_dock_widget.h
+++ b/cockatrice/src/interface/widgets/deck_editor/deck_editor_card_database_dock_widget.h
@@ -23,7 +23,6 @@ public:
public slots:
void retranslateUi();
void clearAllDatabaseFilters();
- void highlightAllSearchEdit();
private:
void createDatabaseDisplayDock(AbstractTabDeckEditor *deckEditor);
diff --git a/cockatrice/src/interface/widgets/deck_editor/deck_editor_database_display_widget.cpp b/cockatrice/src/interface/widgets/deck_editor/deck_editor_database_display_widget.cpp
index c625ff1d9..20eef180a 100644
--- a/cockatrice/src/interface/widgets/deck_editor/deck_editor_database_display_widget.cpp
+++ b/cockatrice/src/interface/widgets/deck_editor/deck_editor_database_display_widget.cpp
@@ -147,11 +147,13 @@ void DeckEditorDatabaseDisplayWidget::updateCard(const QModelIndex ¤t, con
void DeckEditorDatabaseDisplayWidget::actAddCardToMainDeck()
{
+ highlightAllSearchEdit();
emit addCardToMainDeck(currentCard());
}
void DeckEditorDatabaseDisplayWidget::actAddCardToSideboard()
{
+ highlightAllSearchEdit();
emit addCardToSideboard(currentCard());
}
@@ -240,4 +242,9 @@ void DeckEditorDatabaseDisplayWidget::retranslateUi()
{
aAddCard->setText(tr("Add card to &maindeck"));
aAddCardToSideboard->setText(tr("Add card to &sideboard"));
+}
+
+void DeckEditorDatabaseDisplayWidget::highlightAllSearchEdit()
+{
+ searchEdit->setSelection(0, searchEdit->text().length());
}
\ No newline at end of file
diff --git a/cockatrice/src/interface/widgets/deck_editor/deck_editor_database_display_widget.h b/cockatrice/src/interface/widgets/deck_editor/deck_editor_database_display_widget.h
index 16ae6e255..0f62998ef 100644
--- a/cockatrice/src/interface/widgets/deck_editor/deck_editor_database_display_widget.h
+++ b/cockatrice/src/interface/widgets/deck_editor/deck_editor_database_display_widget.h
@@ -25,7 +25,6 @@ class DeckEditorDatabaseDisplayWidget : public QWidget
public:
explicit DeckEditorDatabaseDisplayWidget(QWidget *parent, AbstractTabDeckEditor *deckEditor);
AbstractTabDeckEditor *deckEditor;
- SearchLineEdit *searchEdit;
CardDatabaseModel *databaseModel;
CardDatabaseDisplayModel *databaseDisplayModel;
@@ -58,10 +57,13 @@ private:
KeySignals searchKeySignals;
QTreeView *databaseView;
QHBoxLayout *searchLayout;
+ SearchLineEdit *searchEdit;
QAction *aAddCard, *aAddCardToSideboard;
QVBoxLayout *centralFrame;
QWidget *centralWidget;
+ void highlightAllSearchEdit();
+
private slots:
void retranslateUi();
void saveDbHeaderState();
diff --git a/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.cpp b/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.cpp
index afc834e10..a739ac83b 100644
--- a/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.cpp
+++ b/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.cpp
@@ -137,8 +137,6 @@ void AbstractTabDeckEditor::onDeckModified()
void AbstractTabDeckEditor::addCardHelper(const ExactCard &card, const QString &zoneName)
{
deckStateManager->addCard(card, zoneName);
-
- cardDatabaseDockWidget->highlightAllSearchEdit();
}
/**
From d18f3bce47c13952622a73d400df75148c6b045f Mon Sep 17 00:00:00 2001
From: RickyRister <42636155+RickyRister@users.noreply.github.com>
Date: Mon, 11 May 2026 20:23:29 -0700
Subject: [PATCH 017/121] [VisualDatabaseDisplayWidget] Refactor: Make all
fields private (#6875)
---
.../tabs/tab_visual_database_display.h | 4 ++--
...database_display_filter_toolbar_widget.cpp | 2 +-
.../visual_database_display_widget.cpp | 5 +++++
.../visual_database_display_widget.h | 22 ++++++++++++++-----
4 files changed, 24 insertions(+), 9 deletions(-)
diff --git a/cockatrice/src/interface/widgets/tabs/tab_visual_database_display.h b/cockatrice/src/interface/widgets/tabs/tab_visual_database_display.h
index 66f38fb3d..4fe4d3e8f 100644
--- a/cockatrice/src/interface/widgets/tabs/tab_visual_database_display.h
+++ b/cockatrice/src/interface/widgets/tabs/tab_visual_database_display.h
@@ -23,8 +23,8 @@ public:
void retranslateUi() override;
[[nodiscard]] QString getTabText() const override
{
- return visualDatabaseDisplayWidget->displayModeButton->isChecked() ? tr("Database Display")
- : tr("Visual Database Display");
+ return visualDatabaseDisplayWidget->isVisualDisplayMode() ? tr("Visual Database Display")
+ : tr("Database Display");
}
};
diff --git a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_filter_toolbar_widget.cpp b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_filter_toolbar_widget.cpp
index 3cc1bf23b..790ac771a 100644
--- a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_filter_toolbar_widget.cpp
+++ b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_filter_toolbar_widget.cpp
@@ -101,7 +101,7 @@ void VisualDatabaseDisplayFilterToolbarWidget::initialize()
filterLayout->setAlignment(Qt::AlignLeft);
// create settings widgets
- auto filterModel = visualDatabaseDisplay->filterModel;
+ auto filterModel = visualDatabaseDisplay->getFilterModel();
saveLoadWidget = new VisualDatabaseDisplayFilterSaveLoadWidget(this, filterModel);
nameFilterWidget =
diff --git a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_widget.cpp b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_widget.cpp
index b9f8dab51..25b3c33f8 100644
--- a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_widget.cpp
+++ b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_widget.cpp
@@ -237,6 +237,11 @@ void VisualDatabaseDisplayWidget::updateSearch(const QString &search) const
QItemSelectionModel::SelectCurrent | QItemSelectionModel::Rows);
}
+bool VisualDatabaseDisplayWidget::isVisualDisplayMode() const
+{
+ return !displayModeButton->isChecked();
+}
+
void VisualDatabaseDisplayWidget::onSearchModelChanged()
{
if (flowWidget->isVisible()) {
diff --git a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_widget.h b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_widget.h
index 3aa8d7f8e..5c112b4e4 100644
--- a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_widget.h
+++ b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_widget.h
@@ -62,12 +62,15 @@ public:
return databaseView;
}
- QWidget *searchContainer;
- QHBoxLayout *searchLayout;
- SearchLineEdit *searchEdit;
- QPushButton *displayModeButton;
- FilterTreeModel *filterModel;
- VisualDatabaseDisplayColorFilterWidget *colorFilterWidget;
+ FilterTreeModel *getFilterModel()
+ {
+ return filterModel;
+ }
+
+ /**
+ * @return False if the widget is in database display mode and true if it's in visual display mode
+ */
+ bool isVisualDisplayMode() const;
public slots:
void onSearchModelChanged();
@@ -88,6 +91,13 @@ protected slots:
void onDisplayModeChanged(bool checked);
private:
+ QWidget *searchContainer;
+ QHBoxLayout *searchLayout;
+ SearchLineEdit *searchEdit;
+ QPushButton *displayModeButton;
+ FilterTreeModel *filterModel;
+ VisualDatabaseDisplayColorFilterWidget *colorFilterWidget;
+
QLabel *databaseLoadIndicator;
QToolButton *clearFilterWidget;
From fe12f4cbb9c4281fdb9bc561fb8989f21dc8f29e Mon Sep 17 00:00:00 2001
From: RickyRister <42636155+RickyRister@users.noreply.github.com>
Date: Mon, 11 May 2026 20:23:44 -0700
Subject: [PATCH 018/121] [VisualDeckEditor] Highlight searchEdit after add
card (#6876)
---
.../visual_database_display_widget.cpp | 10 ++++++++++
.../visual_database_display_widget.h | 2 ++
2 files changed, 12 insertions(+)
diff --git a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_widget.cpp b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_widget.cpp
index 25b3c33f8..9c4432042 100644
--- a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_widget.cpp
+++ b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_widget.cpp
@@ -99,6 +99,11 @@ VisualDatabaseDisplayWidget::VisualDatabaseDisplayWidget(QWidget *parent,
&DeckEditorDatabaseDisplayWidget::copyDatabaseCellContents);
connect(help, &QAction::triggered, this, [this] { createSearchSyntaxHelpWindow(searchEdit); });
+ connect(databaseDisplayWidget, &DeckEditorDatabaseDisplayWidget::addCardToMainDeck, this,
+ &VisualDatabaseDisplayWidget::highlightAllSearchEdit);
+ connect(databaseDisplayWidget, &DeckEditorDatabaseDisplayWidget::addCardToSideboard, this,
+ &VisualDatabaseDisplayWidget::highlightAllSearchEdit);
+
databaseView = databaseDisplayWidget->getDatabaseView();
databaseView->setFocusProxy(searchEdit);
databaseView->setItemDelegate(nullptr);
@@ -181,6 +186,11 @@ void VisualDatabaseDisplayWidget::retranslateUi()
clearFilterWidget->setToolTip(tr("Clear all filters"));
}
+void VisualDatabaseDisplayWidget::highlightAllSearchEdit()
+{
+ searchEdit->setSelection(0, searchEdit->text().length());
+}
+
void VisualDatabaseDisplayWidget::resizeEvent(QResizeEvent *event)
{
QWidget::resizeEvent(event);
diff --git a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_widget.h b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_widget.h
index 5c112b4e4..190ea1f6d 100644
--- a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_widget.h
+++ b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_widget.h
@@ -122,6 +122,8 @@ private:
int currentPage = 0; // Current page index
int cardsPerPage = 100; // Number of cards per page
+ void highlightAllSearchEdit();
+
protected:
void resizeEvent(QResizeEvent *event) override;
};
From 7507103bb2d47314e52e10bd0572aa014b3375a6 Mon Sep 17 00:00:00 2001
From: tooomm
Date: Wed, 13 May 2026 14:54:47 +0200
Subject: [PATCH 019/121] CI: Update trigger for doc builds (#6871)
* Update trigger
* use `published` trigger
* narrow deploy step filter
* re-add deployments on manual runs
---
.github/workflows/documentation-build.yml | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)
diff --git a/.github/workflows/documentation-build.yml b/.github/workflows/documentation-build.yml
index 7306c726a..b0093d6b1 100644
--- a/.github/workflows/documentation-build.yml
+++ b/.github/workflows/documentation-build.yml
@@ -1,9 +1,9 @@
name: Generate Docs
on:
- push:
- tags:
- - '*' # Only re-generate docs when a new tagged version is pushed
+ release:
+ types:
+ - published # publishing of stable releases and pre-releases
pull_request:
paths:
- 'doc/doxygen/**'
@@ -53,11 +53,11 @@ jobs:
run: doxygen Doxyfile
- name: Deploy to cockatrice.github.io
- if: github.event_name != 'pull_request'
+ if: github.event_name == 'release' || github.event_name == 'workflow_dispatch'
uses: peaceiris/actions-gh-pages@v4
with:
deploy_key: ${{ secrets.DOCS_DEPLOY_KEY }}
external_repository: Cockatrice/cockatrice.github.io
publish_branch: master
publish_dir: ./docs/html
- destination_dir: docs # Docs will live under https://cockatrice.github.io/docs/
+ destination_dir: docs # Docs will live under https://cockatrice.github.io/docs/
From 67f6ab66f03c663a5725c1477997c67c642d894c Mon Sep 17 00:00:00 2001
From: RickyRister <42636155+RickyRister@users.noreply.github.com>
Date: Wed, 13 May 2026 11:32:05 -0700
Subject: [PATCH 020/121] [DeckLoader] Don't modify text deck files on load
(#6888)
* [DeckLoader] Don't modify text deck files on load
* formatter
---
.../src/interface/deck_loader/deck_loader.cpp | 19 ++++++++-----------
1 file changed, 8 insertions(+), 11 deletions(-)
diff --git a/cockatrice/src/interface/deck_loader/deck_loader.cpp b/cockatrice/src/interface/deck_loader/deck_loader.cpp
index d71dca24a..e616c5eb5 100644
--- a/cockatrice/src/interface/deck_loader/deck_loader.cpp
+++ b/cockatrice/src/interface/deck_loader/deck_loader.cpp
@@ -181,6 +181,11 @@ bool DeckLoader::saveToNewFile(LoadedDeck &deck, const QString &fileName, DeckFi
*/
bool DeckLoader::updateLastLoadedTimestamp(LoadedDeck &deck)
{
+ // text format doesn't support lastLoadedTimestamp, so there's no point in proceeding
+ if (deck.lastLoadInfo.fileFormat != DeckFileFormat::Cockatrice) {
+ return false;
+ }
+
QString fileName = deck.lastLoadInfo.fileName;
QFileInfo fileInfo(fileName);
@@ -201,15 +206,8 @@ bool DeckLoader::updateLastLoadedTimestamp(LoadedDeck &deck)
bool result = false;
// Perform file modifications
- switch (deck.lastLoadInfo.fileFormat) {
- case DeckFileFormat::PlainText:
- result = deck.deckList.saveToFile_Plain(&file);
- break;
- case DeckFileFormat::Cockatrice:
- deck.deckList.setLastLoadedTimestamp(QDateTime::currentDateTime().toString());
- result = deck.deckList.saveToFile_Native(&file);
- break;
- }
+ deck.deckList.setLastLoadedTimestamp(QDateTime::currentDateTime().toString());
+ result = deck.deckList.saveToFile_Native(&file);
file.close(); // Close the file to ensure changes are flushed
@@ -429,8 +427,7 @@ void DeckLoader::saveToStream_DeckZoneCards(QTextStream &out,
}
if (addSetNameAndNumber) {
if (!card->getCardSetShortName().isNull() && !card->getCardSetShortName().isEmpty()) {
- out << " "
- << "(" << card->getCardSetShortName() << ")";
+ out << " " << "(" << card->getCardSetShortName() << ")";
}
if (!card->getCardCollectorNumber().isNull()) {
out << " " << card->getCardCollectorNumber();
From 762e742be0b7987f2c908ffe38ba9301253523cd Mon Sep 17 00:00:00 2001
From: RickyRister <42636155+RickyRister@users.noreply.github.com>
Date: Wed, 13 May 2026 15:03:41 -0700
Subject: [PATCH 021/121] [Game] Implement action to automatically take damage
from creatures (#6869)
* [Game] Implement action to automatically take damage from creatures
* cleanup
---
.../src/client/settings/shortcuts_settings.h | 3 +++
cockatrice/src/game/player/menu/card_menu.cpp | 9 +++++++
cockatrice/src/game/player/menu/card_menu.h | 1 +
cockatrice/src/game/player/player.cpp | 10 +++++++
cockatrice/src/game/player/player.h | 5 ++++
cockatrice/src/game/player/player_actions.cpp | 27 +++++++++++++++++++
cockatrice/src/game/player/player_actions.h | 3 +++
7 files changed, 58 insertions(+)
diff --git a/cockatrice/src/client/settings/shortcuts_settings.h b/cockatrice/src/client/settings/shortcuts_settings.h
index d9ea494dc..fc159db48 100644
--- a/cockatrice/src/client/settings/shortcuts_settings.h
+++ b/cockatrice/src/client/settings/shortcuts_settings.h
@@ -537,6 +537,9 @@ private:
{"Player/aSetAnnotation", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Set Annotation..."),
parseSequenceString("Alt+N"),
ShortcutGroup::Playing_Area)},
+ {"Player/aReduceLifeByPower", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Reduce Life by Power"),
+ parseSequenceString("Ctrl+Shift+L"),
+ ShortcutGroup::Playing_Area)},
{"Player/aSelectAll", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Select All Cards in Zone"),
parseSequenceString("Ctrl+A"),
ShortcutGroup::Playing_Area)},
diff --git a/cockatrice/src/game/player/menu/card_menu.cpp b/cockatrice/src/game/player/menu/card_menu.cpp
index 34ed254fb..54403e237 100644
--- a/cockatrice/src/game/player/menu/card_menu.cpp
+++ b/cockatrice/src/game/player/menu/card_menu.cpp
@@ -62,6 +62,9 @@ CardMenu::CardMenu(Player *_player, const CardItem *_card, bool _shortcutsActive
aSelectColumn = new QAction(this);
connect(aSelectColumn, &QAction::triggered, playerActions, &PlayerActions::actSelectColumn);
+ aReduceLifeByPower = new QAction(this);
+ connect(aReduceLifeByPower, &QAction::triggered, playerActions, &PlayerActions::actReduceLifeByPower);
+
aPlay = new QAction(this);
connect(aPlay, &QAction::triggered, playerActions, &PlayerActions::actPlay);
aHide = new QAction(this);
@@ -153,6 +156,8 @@ void CardMenu::createTableMenu(bool canModifyCard)
addSeparator();
addAction(aClone);
addSeparator();
+ addAction(aReduceLifeByPower);
+ addSeparator();
addAction(aSelectAll);
addAction(aSelectRow);
addRelatedCardView();
@@ -179,6 +184,8 @@ void CardMenu::createTableMenu(bool canModifyCard)
addMenu(new PtMenu(player));
addAction(aSetAnnotation);
addSeparator();
+ addAction(aReduceLifeByPower);
+ addSeparator();
addAction(aSelectAll);
addAction(aSelectRow);
@@ -463,6 +470,7 @@ void CardMenu::retranslateUi()
aUnattach->setText(tr("Unattac&h"));
aDrawArrow->setText(tr("&Draw arrow..."));
aSetAnnotation->setText(tr("&Set annotation..."));
+ aReduceLifeByPower->setText(tr("Reduce life by power"));
mCardCounters->setTitle(tr("Ca&rd counters"));
@@ -497,6 +505,7 @@ void CardMenu::setShortcutsActive()
aUnattach->setShortcuts(shortcuts.getShortcut("Player/aUnattach"));
aDrawArrow->setShortcuts(shortcuts.getShortcut("Player/aDrawArrow"));
aSetAnnotation->setShortcuts(shortcuts.getShortcut("Player/aSetAnnotation"));
+ aReduceLifeByPower->setShortcuts(shortcuts.getShortcut("Player/aReduceLifeByPower"));
aSelectAll->setShortcuts(shortcuts.getShortcut("Player/aSelectAll"));
aSelectRow->setShortcuts(shortcuts.getShortcut("Player/aSelectRow"));
diff --git a/cockatrice/src/game/player/menu/card_menu.h b/cockatrice/src/game/player/menu/card_menu.h
index b7f2f8241..3cff238de 100644
--- a/cockatrice/src/game/player/menu/card_menu.h
+++ b/cockatrice/src/game/player/menu/card_menu.h
@@ -36,6 +36,7 @@ public:
QAction *aFlip, *aPeek;
QAction *aAttach, *aUnattach;
QAction *aSetAnnotation;
+ QAction *aReduceLifeByPower;
QList aAddCounter, aSetCounter, aRemoveCounter;
diff --git a/cockatrice/src/game/player/player.cpp b/cockatrice/src/game/player/player.cpp
index e95e2b2ef..8f5067eae 100644
--- a/cockatrice/src/game/player/player.cpp
+++ b/cockatrice/src/game/player/player.cpp
@@ -343,6 +343,16 @@ void Player::incrementAllCardCounters()
}
}
+AbstractCounter *Player::getLifeCounter() const
+{
+ for (auto counter : counters.values()) {
+ if (counter->getName() == "life") {
+ return counter;
+ }
+ }
+ return nullptr;
+}
+
ArrowItem *Player::addArrow(const ServerInfo_Arrow &arrow)
{
const QMap &playerList = game->getPlayerManager()->getPlayers();
diff --git a/cockatrice/src/game/player/player.h b/cockatrice/src/game/player/player.h
index 9b8456aaf..1e7bc928d 100644
--- a/cockatrice/src/game/player/player.h
+++ b/cockatrice/src/game/player/player.h
@@ -199,6 +199,11 @@ public:
return counters;
}
+ /**
+ * Gets the counter that represents the life total.
+ */
+ AbstractCounter *getLifeCounter() const;
+
ArrowItem *addArrow(const ServerInfo_Arrow &arrow);
ArrowItem *addArrow(int arrowId, CardItem *startCard, ArrowTarget *targetItem, const QColor &color);
void delArrow(int arrowId);
diff --git a/cockatrice/src/game/player/player_actions.cpp b/cockatrice/src/game/player/player_actions.cpp
index bce8af74b..24da37c82 100644
--- a/cockatrice/src/game/player/player_actions.cpp
+++ b/cockatrice/src/game/player/player_actions.cpp
@@ -19,6 +19,7 @@
#include
#include
#include
+#include
#include
#include
#include
@@ -1378,6 +1379,32 @@ void PlayerActions::actFlowT()
actIncPT(-1, 1);
}
+void PlayerActions::actReduceLifeByPower()
+{
+ // find life counter
+ auto lifeCounter = player->getLifeCounter();
+ if (!lifeCounter) {
+ return;
+ }
+
+ // calculate total power
+ auto cards = player->getGameScene()->selectedCards();
+ int total = 0;
+ for (auto card : cards) {
+ QVariantList parsed = CardItem::parsePT(card->getPT());
+ if (!parsed.isEmpty()) {
+ int power = parsed.first().toInt(); // toInt will default to 0 if it's not an int
+ total += qMax(power, 0);
+ }
+ }
+
+ // send cmd
+ Command_IncCounter cmd;
+ cmd.set_counter_id(lifeCounter->getId());
+ cmd.set_delta(-total);
+ sendGameCommand(prepareGameCommand(cmd));
+}
+
void AnnotationDialog::keyPressEvent(QKeyEvent *event)
{
if (event->key() == Qt::Key_Return && event->modifiers() & Qt::ControlModifier) {
diff --git a/cockatrice/src/game/player/player_actions.h b/cockatrice/src/game/player/player_actions.h
index 181b1b135..ba62f1c59 100644
--- a/cockatrice/src/game/player/player_actions.h
+++ b/cockatrice/src/game/player/player_actions.h
@@ -145,6 +145,9 @@ public slots:
void actDecPT();
void actFlowP();
void actFlowT();
+
+ void actReduceLifeByPower();
+
void actSetAnnotation();
void actReveal(QAction *action);
void actRevealHand(int revealToPlayerId);
From 7153f7d4c11d92cc8690e1365037896e28da8d3d Mon Sep 17 00:00:00 2001
From: BruebachL <44814898+BruebachL@users.noreply.github.com>
Date: Thu, 14 May 2026 19:07:15 +0200
Subject: [PATCH 022/121] [VDD] Fix minimum size by adding a compact mode to
quickSettingsButtons (#6890)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* [VDD] Fix minimum size by adding a compact mode to quickSettingsButtons
Took 17 minutes
Took 5 seconds
* Fix and use FlowWidget/FlowLayout
Took 35 minutes
Took 4 seconds
* Set spacings.
Took 12 minutes
* Make VDE tools flow
Took 1 hour 23 minutes
Took 5 seconds
* Squeeze and flow even more.
Took 11 minutes
* Make pushbutton compact.
Took 54 minutes
Took 7 seconds
---------
Co-authored-by: Lukas Brübach
---
cockatrice/CMakeLists.txt | 2 +
.../src/interface/layouts/flow_layout.cpp | 696 +++++++-----------
.../src/interface/layouts/flow_layout.h | 58 +-
.../draw_probability_widget.cpp | 20 +-
.../draw_probability_widget.h | 5 +-
.../deck_analytics/deck_analytics_widget.cpp | 14 +-
.../deck_analytics/deck_analytics_widget.h | 4 +-
.../general/layout_containers/flow_widget.cpp | 144 ++--
.../general/layout_containers/flow_widget.h | 14 +-
.../quick_settings/settings_button_widget.cpp | 26 +-
.../quick_settings/settings_button_widget.h | 7 +
.../widgets/utility/compact_push_button.cpp | 73 ++
.../widgets/utility/compact_push_button.h | 31 +
...database_display_filter_toolbar_widget.cpp | 48 +-
...l_database_display_filter_toolbar_widget.h | 9 +-
.../visual_database_display_widget.cpp | 12 +-
.../visual_database_display_widget.h | 3 +-
.../visual_deck_display_options_widget.cpp | 30 +-
.../visual_deck_display_options_widget.h | 5 +-
.../visual_deck_editor_widget.cpp | 70 +-
.../visual_deck_editor_widget.h | 12 +-
21 files changed, 666 insertions(+), 617 deletions(-)
create mode 100644 cockatrice/src/interface/widgets/utility/compact_push_button.cpp
create mode 100644 cockatrice/src/interface/widgets/utility/compact_push_button.h
diff --git a/cockatrice/CMakeLists.txt b/cockatrice/CMakeLists.txt
index 1d07371d8..1017f1247 100644
--- a/cockatrice/CMakeLists.txt
+++ b/cockatrice/CMakeLists.txt
@@ -327,6 +327,8 @@ set(cockatrice_SOURCES
src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_bracket_navigation_widget.h
src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_budget_navigation_widget.cpp
src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_budget_navigation_widget.h
+ src/interface/widgets/utility/compact_push_button.cpp
+ src/interface/widgets/utility/compact_push_button.h
)
add_subdirectory(sounds)
diff --git a/cockatrice/src/interface/layouts/flow_layout.cpp b/cockatrice/src/interface/layouts/flow_layout.cpp
index f7ebbfb79..36edd2c21 100644
--- a/cockatrice/src/interface/layouts/flow_layout.cpp
+++ b/cockatrice/src/interface/layouts/flow_layout.cpp
@@ -1,7 +1,16 @@
/**
* @file flow_layout.cpp
- * @brief Implementation of the FlowLayout class, a custom layout for dynamically organizing widgets
- * in rows within the constraints of available width or parent scroll areas.
+ * @brief Implementation of FlowLayout — a QLayout that wraps child widgets into rows
+ * (Qt::Horizontal flow) or columns (Qt::Vertical flow).
+ *
+ * Design contract (following Qt layout conventions):
+ * - setGeometry() places children inside the given rect. Nothing else.
+ * - sizeHint() reports the unconstrained preferred size (all items in one line).
+ * - minimumSize() reports the minimum size (largest single item).
+ * - heightForWidth() reports the height needed for a given width (horizontal flow only).
+ *
+ * The layout never calls setFixedSize() or adjustSize() on its parent widget;
+ * that is the responsibility of the parent widget / scroll area.
*/
#include "flow_layout.h"
@@ -12,27 +21,18 @@
#include
#include
#include
+#include
-/**
- * @brief Constructs a FlowLayout instance with the specified parent widget, margin, and spacing values.
- * @param parent The parent widget for this layout.
- * @param margin The layout margin.
- * @param hSpacing The horizontal spacing between items.
- * @param vSpacing The vertical spacing between items.
- */
FlowLayout::FlowLayout(QWidget *parent,
- const Qt::Orientation _flowDirection,
+ const Qt::Orientation flowDirection,
const int margin,
const int hSpacing,
const int vSpacing)
- : QLayout(parent), flowDirection(_flowDirection), horizontalMargin(hSpacing), verticalMargin(vSpacing)
+ : QLayout(parent), flowDirection(flowDirection), horizontalMargin(hSpacing), verticalMargin(vSpacing)
{
setContentsMargins(margin, margin, margin, margin);
}
-/**
- * @brief Destructor for FlowLayout, which cleans up all items in the layout.
- */
FlowLayout::~FlowLayout()
{
QLayoutItem *item;
@@ -42,499 +42,349 @@ FlowLayout::~FlowLayout()
}
/**
- * @brief Indicates the layout's support for expansion in both horizontal and vertical directions.
- * @return The orientations (Qt::Horizontal | Qt::Vertical) this layout can expand to fill.
+ * @brief Reports which axis the layout can expand along.
+ *
+ * A horizontally-flowing layout expands horizontally (and wraps vertically,
+ * but that is governed by heightForWidth, not by this flag).
+ * A vertically-flowing layout expands vertically.
*/
Qt::Orientations FlowLayout::expandingDirections() const
{
- return Qt::Horizontal | Qt::Vertical;
+ return (flowDirection == Qt::Horizontal) ? Qt::Horizontal : Qt::Vertical;
}
/**
- * @brief Indicates that this layout's height depends on its width.
- * @return True, as the layout adjusts its height to fit the specified width.
+ * @brief Height-for-width is only meaningful for horizontal (wrapping) flow.
*/
bool FlowLayout::hasHeightForWidth() const
{
- return true;
+ return flowDirection == Qt::Horizontal;
}
/**
- * @brief Calculates the required height to display all items within the specified width.
- * @param width The available width for arranging items.
- * @return The total height needed to fit all items in rows constrained by the specified width.
+ * @brief Returns the height required to display all items within @p width.
+ *
+ * Only valid for horizontal flow; returns -1 otherwise so Qt ignores it.
+ * Spacing is counted once between adjacent items, never before the first
+ * or after the last.
*/
int FlowLayout::heightForWidth(const int width) const
{
- if (flowDirection == Qt::Vertical) {
- int height = 0;
- int rowWidth = 0;
- int rowHeight = 0;
+ if (flowDirection != Qt::Horizontal)
+ return -1;
- for (const QLayoutItem *item : items) {
- if (item == nullptr || item->isEmpty()) {
- continue;
- }
+ int totalHeight = 0;
+ int rowUsedWidth = 0;
+ int rowHeight = 0;
- int itemWidth = item->sizeHint().width() + horizontalSpacing();
- if (rowWidth + itemWidth > width) {
- height += rowHeight + verticalSpacing();
- rowWidth = itemWidth;
- rowHeight = item->sizeHint().height();
- } else {
- rowWidth += itemWidth;
- rowHeight = qMax(rowHeight, item->sizeHint().height());
- }
- }
- height += rowHeight; // Add height of the last row
- return height;
- } else {
- int width = 0;
- int colWidth = 0;
- int colHeight = 0;
-
- for (const QLayoutItem *item : items) {
- if (item == nullptr || item->isEmpty()) {
- continue;
- }
-
- int itemHeight = item->sizeHint().height();
- if (colHeight + itemHeight > width) {
- width += colWidth;
- colHeight = itemHeight;
- colWidth = item->sizeHint().width();
- } else {
- colHeight += itemHeight;
- colWidth = qMax(colWidth, item->sizeHint().width());
- }
- }
- width += colWidth; // Add width of the last column
- return width;
- }
-}
-
-/**
- * @brief Arranges layout items in rows within the specified rectangle bounds.
- * @param rect The area within which to position layout items.
- */
-void FlowLayout::setGeometry(const QRect &rect)
-{
- QLayout::setGeometry(rect); // Sets the geometry of the layout based on the given rectangle.
-
- if (flowDirection == Qt::Horizontal) {
- // If we have a parent scroll area, we're clamped to that, else we use our own rectangle.
- const int availableWidth = getParentScrollAreaWidth() == 0 ? rect.width() : getParentScrollAreaWidth();
-
- const int totalHeight = layoutAllRows(rect.x(), rect.y(), availableWidth);
-
- if (QWidget *parentWidgetPtr = parentWidget()) {
- parentWidgetPtr->setFixedSize(availableWidth, totalHeight);
- }
- } else {
- const int availableHeight = qMax(rect.height(), getParentScrollAreaHeight());
-
- const int totalWidth = layoutAllColumns(rect.x(), rect.y(), availableHeight);
-
- if (QWidget *parentWidgetPtr = parentWidget()) {
- parentWidgetPtr->setFixedSize(totalWidth, availableHeight);
- }
- }
-}
-
-/**
- * @brief Lays out items into rows according to the available width, starting from a given origin.
- * Each row is arranged within `availableWidth`, wrapping to a new row as necessary.
- * @param originX The x-coordinate for the layout start position.
- * @param originY The y-coordinate for the layout start position.
- * @param availableWidth The width within which each row is constrained.
- * @return The total height after arranging all rows.
- */
-int FlowLayout::layoutAllRows(const int originX, const int originY, const int availableWidth)
-{
- QVector rowItems; // Holds items for the current row
- int currentXPosition = originX; // Tracks the x-coordinate while placing items
- int currentYPosition = originY; // Tracks the y-coordinate, moving down after each row
-
- int rowHeight = 0; // Tracks the maximum height of items in the current row
-
- for (QLayoutItem *item : items) {
- if (item == nullptr || item->isEmpty()) {
+ for (const QLayoutItem *item : items) {
+ if (!item || item->isEmpty()) {
continue;
}
- QSize itemSize = item->sizeHint(); // The suggested size for the current item
- int itemWidth = itemSize.width() + horizontalSpacing(); // Item width plus spacing
+ const QSize itemSize = item->sizeHint();
+ // Spacing is only inserted between items, not before the first.
+ const int spaceX = (rowUsedWidth > 0) ? horizontalSpacing() : 0;
- // Check if the current item fits in the remaining width of the current row
- if (currentXPosition + itemWidth > availableWidth) {
- // If not, layout the current row and start a new row
- layoutSingleRow(rowItems, originX, currentYPosition);
- rowItems.clear(); // Reset the list for the new row
- currentXPosition = originX; // Reset x-position to the row's start
- currentYPosition += rowHeight + verticalSpacing(); // Move y-position down to the next row
- rowHeight = 0; // Reset row height for the new row
+ if (rowUsedWidth > 0 && rowUsedWidth + spaceX + itemSize.width() > width) {
+ // This item overflows the current row — commit the row and start a new one.
+ totalHeight += rowHeight + verticalSpacing();
+ rowUsedWidth = itemSize.width();
+ rowHeight = itemSize.height();
+ } else {
+ rowUsedWidth += spaceX + itemSize.width();
+ rowHeight = qMax(rowHeight, itemSize.height());
+ }
+ }
+
+ return totalHeight + rowHeight; // Include the final (possibly only) row.
+}
+
+/**
+ * @brief Places all children within @p rect.
+ *
+ * This is the only method that may move/resize children. It does NOT resize
+ * the parent widget; that would break Qt's layout protocol.
+ */
+void FlowLayout::setGeometry(const QRect &rect)
+{
+ QLayout::setGeometry(rect);
+
+ if (flowDirection == Qt::Horizontal) {
+ layoutAllRows(rect.x(), rect.y(), rect.width());
+ } else {
+ layoutAllColumns(rect.x(), rect.y(), rect.height());
+ }
+}
+
+QSize FlowLayout::sizeHint() const
+{
+ return (flowDirection == Qt::Horizontal) ? calculateSizeHintHorizontal() : calculateSizeHintVertical();
+}
+
+QSize FlowLayout::minimumSize() const
+{
+ return (flowDirection == Qt::Horizontal) ? calculateMinimumSizeHorizontal() : calculateMinimumSizeVertical();
+}
+
+// ─── Row layout (horizontal flow) ────────────────────────────────────────────
+
+/**
+ * @brief Places all items into wrapping rows within @p availableWidth.
+ * @return The y-coordinate of the bottom edge of the last row.
+ */
+int FlowLayout::layoutAllRows(const int originX, const int originY, const int availableWidth)
+{
+ QVector rowItems;
+ int rowUsedWidth = 0; // Width consumed by items already in the current row.
+ int currentY = originY;
+ int rowHeight = 0;
+
+ for (QLayoutItem *item : items) {
+ if (!item || item->isEmpty()) {
+ continue;
+ }
+
+ const QSize itemSize = item->sizeHint();
+ // No leading space for the first item in a row.
+ const int spaceX = rowItems.isEmpty() ? 0 : horizontalSpacing();
+
+ if (!rowItems.isEmpty() && rowUsedWidth + spaceX + itemSize.width() > availableWidth) {
+ // Current item does not fit — flush the current row, begin a new one.
+ layoutSingleRow(rowItems, originX, currentY, availableWidth);
+ rowItems.clear();
+ currentY += rowHeight + verticalSpacing();
+ rowUsedWidth = 0;
+ rowHeight = 0;
}
// Add the item to the current row
rowItems.append(item);
- rowHeight = qMax(rowHeight, itemSize.height()); // Update the row's height to the tallest item
- currentXPosition += itemWidth + horizontalSpacing(); // Move x-position for the next item
+ // `rowItems.size() > 1` is equivalent to "this is not the first item in the row"
+ // because we just appended above.
+ rowUsedWidth += (rowItems.size() > 1 ? horizontalSpacing() : 0) + itemSize.width();
+ rowHeight = qMax(rowHeight, itemSize.height());
}
- // Layout the final row if there are any remaining items
- layoutSingleRow(rowItems, originX, currentYPosition);
-
- // Return the total height used, including the last row's height
- return currentYPosition + rowHeight;
+ layoutSingleRow(rowItems, originX, currentY, availableWidth); // Flush the final row.
+ return currentY + rowHeight;
}
/**
- * @brief Arranges a single row of items within specified x and y starting positions.
- * @param rowItems A list of items to be arranged in the row.
- * @param x The starting x-coordinate for the row.
- * @param y The starting y-coordinate for the row.
+ * @brief Sets the geometry for every item in @p rowItems, starting at (@p x, @p y).
+ *
+ * Items whose horizontal size policy includes the Expand or MinimumExpanding flag
+ * share the leftover row width proportionally (like QHBoxLayout stretch), so that
+ * e.g. a QLineEdit can fill remaining space while fixed-size buttons stay compact.
+ *
+ * Items without an expanding policy are placed at their sizeHint, clamped to maximumSize.
*/
-void FlowLayout::layoutSingleRow(const QVector &rowItems, int x, const int y)
+void FlowLayout::layoutSingleRow(const QVector &rowItems, int x, const int y, const int availableWidth)
{
+ if (rowItems.isEmpty())
+ return;
+
+ // ── Pass 1: measure fixed width and count expanding items ────────────────
+ int fixedWidth = 0;
+ int expandingCount = 0;
+ int spacingTotal = (rowItems.size() - 1) * horizontalSpacing();
+
for (QLayoutItem *item : rowItems) {
- if (item == nullptr || item->isEmpty()) {
+ if (!item || item->isEmpty()) {
continue;
}
- // Get the maximum allowed size for the item
- QSize itemMaxSize = item->widget()->maximumSize();
- // Constrain the item's width and height to its size hint or maximum size
- const int itemWidth = qMin(item->sizeHint().width(), itemMaxSize.width());
- const int itemHeight = qMin(item->sizeHint().height(), itemMaxSize.height());
- // Set the item's geometry based on the computed size and position
+ QWidget *widget = item->widget();
+ const QSizePolicy::Policy hPolicy = widget ? widget->sizePolicy().horizontalPolicy() : QSizePolicy::Fixed;
+
+ if (hPolicy & QSizePolicy::ExpandFlag) {
+ ++expandingCount;
+ } else {
+ const int maxW = widget ? widget->maximumWidth() : QWIDGETSIZE_MAX;
+ fixedWidth += qMin(item->sizeHint().width(), maxW);
+ }
+ }
+
+ // Extra pixels to share among expanding items (never negative).
+ const int extra = qMax(0, availableWidth - spacingTotal - fixedWidth);
+ const int expandingShare = (expandingCount > 0) ? extra / expandingCount : 0;
+
+ // ── Pass 2: place items ──────────────────────────────────────────────────
+ for (QLayoutItem *item : rowItems) {
+ if (!item || item->isEmpty())
+ continue;
+
+ QWidget *widget = item->widget();
+ if (!widget)
+ continue;
+
+ const QSizePolicy::Policy hPolicy = widget->sizePolicy().horizontalPolicy();
+ const QSize maxSize = widget->maximumSize();
+ const bool expands = hPolicy & QSizePolicy::ExpandFlag;
+
+ const int itemWidth =
+ expands ? qMin(expandingShare, maxSize.width()) : qMin(item->sizeHint().width(), maxSize.width());
+ const int itemHeight = qMin(item->sizeHint().height(), maxSize.height());
+
item->setGeometry(QRect(QPoint(x, y), QSize(itemWidth, itemHeight)));
- // Move the x-position to the right, leaving space for horizontal spacing
x += itemWidth + horizontalSpacing();
}
}
+// ─── Column layout (vertical flow) ───────────────────────────────────────────
+
/**
- * @brief Lays out items into columns according to the available height, starting from a given origin.
- * Each column is arranged within `availableHeight`, wrapping to a new column as necessary.
- * @param originX The x-coordinate for the layout start position.
- * @param originY The y-coordinate for the layout start position.
- * @param availableHeight The height within which each column is constrained.
- * @return The total width after arranging all columns.
+ * @brief Places all items into wrapping columns within @p availableHeight.
+ * @return The x-coordinate of the right edge of the last column.
*/
int FlowLayout::layoutAllColumns(const int originX, const int originY, const int availableHeight)
{
- QVector colItems; // Holds items for the current column
- int currentXPosition = originX; // Tracks the x-coordinate while placing items
- int currentYPosition = originY; // Tracks the y-coordinate, resetting for each new column
-
- int colWidth = 0; // Tracks the maximum width of items in the current column
+ QVector colItems;
+ int colUsedHeight = 0; // Height consumed by items already in the current column.
+ int currentX = originX;
+ int colWidth = 0;
for (QLayoutItem *item : items) {
- if (item == nullptr || item->isEmpty()) {
+ if (!item || item->isEmpty()) {
continue;
}
- QSize itemSize = item->sizeHint(); // The suggested size for the current item
+ const QSize itemSize = item->sizeHint();
+ // No leading space for the first item in a column.
+ const int spaceY = colItems.isEmpty() ? 0 : verticalSpacing();
- // Check if the current item fits in the remaining height of the current column
- if (currentYPosition + itemSize.height() > availableHeight) {
- // If not, layout the current column and start a new column
- layoutSingleColumn(colItems, currentXPosition, originY);
- colItems.clear(); // Reset the list for the new column
- currentYPosition = originY; // Reset y-position to the column's start
- currentXPosition += colWidth; // Move x-position to the next column
- colWidth = 0; // Reset column width for the new column
+ if (!colItems.isEmpty() && colUsedHeight + spaceY + itemSize.height() > availableHeight) {
+ // Current item does not fit — flush the current column, begin a new one.
+ layoutSingleColumn(colItems, currentX, originY);
+ colItems.clear();
+ currentX += colWidth + horizontalSpacing();
+ colUsedHeight = 0;
+ colWidth = 0;
}
- // Add the item to the current column
colItems.append(item);
- colWidth = qMax(colWidth, itemSize.width()); // Update the column's width to the widest item
- currentYPosition += itemSize.height(); // Move y-position for the next item
+ colUsedHeight += (colItems.size() > 1 ? verticalSpacing() : 0) + itemSize.height();
+ colWidth = qMax(colWidth, itemSize.width());
}
- // Layout the final column if there are any remaining items
- layoutSingleColumn(colItems, currentXPosition, originY);
-
- // Return the total width used, including the last column's width
- return currentXPosition + colWidth;
+ layoutSingleColumn(colItems, currentX, originY); // Flush the final column.
+ return currentX + colWidth;
}
/**
- * @brief Arranges a single column of items within specified x and y starting positions.
- * @param colItems A list of items to be arranged in the column.
- * @param x The starting x-coordinate for the column.
- * @param y The starting y-coordinate for the column.
+ * @brief Sets the geometry for every item in @p colItems, starting at (@p x, @p y).
+ *
+ * Each item is placed at its sizeHint, clamped to its maximumSize.
*/
void FlowLayout::layoutSingleColumn(const QVector &colItems, const int x, int y)
{
for (QLayoutItem *item : colItems) {
- if (item == nullptr) {
- qCDebug(FlowLayoutLog) << "Item is null.";
+ if (!item || item->isEmpty()) {
+ qCDebug(FlowLayoutLog) << "Skipping null or empty item in column.";
continue;
}
- if (item->isEmpty()) {
- qCDebug(FlowLayoutLog) << "Skipping empty item.";
- continue;
- }
-
- // Debugging: Print the item's widget class name and size hint
QWidget *widget = item->widget();
- if (widget) {
- qCDebug(FlowLayoutLog) << "Widget class:" << widget->metaObject()->className();
- qCDebug(FlowLayoutLog) << "Widget size hint:" << widget->sizeHint();
- qCDebug(FlowLayoutLog) << "Widget maximum size:" << widget->maximumSize();
- qCDebug(FlowLayoutLog) << "Widget minimum size:" << widget->minimumSize();
-
- // Debugging: Print child widgets
- const QObjectList &children = widget->children();
- qCDebug(FlowLayoutLog) << "Child widgets:";
- for (QObject *child : children) {
- if (QWidget *childWidget = qobject_cast(child)) {
- qCDebug(FlowLayoutLog) << " - Child widget class:" << childWidget->metaObject()->className();
- qCDebug(FlowLayoutLog) << " Size hint:" << childWidget->sizeHint();
- qCDebug(FlowLayoutLog) << " Maximum size:" << childWidget->maximumSize();
- }
- }
- } else {
- qCDebug(FlowLayoutLog) << "Item does not have a widget.";
+ if (!widget) {
+ qCDebug(FlowLayoutLog) << "Item has no widget; skipping.";
+ continue;
}
- // Get the maximum allowed size for the item
- QSize itemMaxSize = widget->maximumSize();
- // Constrain the item's width and height to its size hint or maximum size
- const int itemWidth = qMin(item->sizeHint().width(), itemMaxSize.width());
- const int itemHeight = qMin(item->sizeHint().height(), itemMaxSize.height());
- // Debugging: Print the computed geometry
- qCDebug(FlowLayoutLog) << "Computed geometry: x=" << x << ", y=" << y << ", width=" << itemWidth
- << ", height=" << itemHeight;
+ qCDebug(FlowLayoutLog) << "Widget:" << widget->metaObject()->className() << "sizeHint:" << widget->sizeHint()
+ << "maximumSize:" << widget->maximumSize() << "minimumSize:" << widget->minimumSize();
+
+ const QSize maxSize = widget->maximumSize();
+ const int itemWidth = qMin(item->sizeHint().width(), maxSize.width());
+ const int itemHeight = qMin(item->sizeHint().height(), maxSize.height());
+
+ qCDebug(FlowLayoutLog) << "Placing at x=" << x << "y=" << y << "w=" << itemWidth << "h=" << itemHeight;
// Set the item's geometry based on the computed size and position
item->setGeometry(QRect(QPoint(x, y), QSize(itemWidth, itemHeight)));
-
- // Move the y-position down by the item's height to place the next item below
- y += itemHeight;
+ y += itemHeight + verticalSpacing();
}
}
/**
- * @brief Calculates the preferred size of the layout based on the flow direction.
- * @return A QSize representing the ideal dimensions of the layout.
- */
-QSize FlowLayout::sizeHint() const
-{
- if (flowDirection == Qt::Horizontal) {
- return calculateSizeHintHorizontal();
- } else {
- return calculateSizeHintVertical();
- }
-}
-
-/**
- * @brief Calculates the minimum size required by the layout based on the flow direction.
- * @return A QSize representing the minimum required dimensions.
- */
-QSize FlowLayout::minimumSize() const
-{
- if (flowDirection == Qt::Horizontal) {
- return calculateMinimumSizeHorizontal();
- } else {
- return calculateMinimumSizeVertical();
- }
-}
-
-/**
- * @brief Calculates the size hint for horizontal flow direction.
- * @return A QSize representing the preferred dimensions.
+ * @brief Preferred size for horizontal flow: all items in a single row (unconstrained).
+ *
+ * The actual displayed height is determined by heightForWidth() once Qt knows the
+ * real available width.
*/
QSize FlowLayout::calculateSizeHintHorizontal() const
{
- int maxWidth = 0; // Tracks the maximum width needed
- int totalHeight = 0; // Tracks the total height across all rows
- int rowHeight = 0; // Tracks the height of the current row
- int currentWidth = 0; // Tracks the current row's width
-
- const int availableWidth = getParentScrollAreaWidth() == 0 ? parentWidget()->width() : getParentScrollAreaWidth();
-
- qCDebug(FlowLayoutLog) << "Calculating horizontal size hint. Available width:" << availableWidth;
+ int totalWidth = 0;
+ int maxHeight = 0;
for (const QLayoutItem *item : items) {
if (!item || item->isEmpty()) {
- qCDebug(FlowLayoutLog) << "Skipping empty item.";
continue;
}
-
- QSize itemSize = item->sizeHint();
- int itemWidth = itemSize.width() + horizontalSpacing();
- qCDebug(FlowLayoutLog) << "Processing item. Size:" << itemSize << "Width with spacing:" << itemWidth;
-
- if (currentWidth + itemWidth > availableWidth) {
- qCDebug(FlowLayoutLog) << "Row overflow. Current width:" << currentWidth << "Row height:" << rowHeight;
- maxWidth = qMax(maxWidth, currentWidth);
- totalHeight += rowHeight + verticalSpacing();
- qCDebug(FlowLayoutLog) << "Updated total height:" << totalHeight << "Max width so far:" << maxWidth;
-
- currentWidth = 0;
- rowHeight = 0;
+ const QSize s = item->sizeHint();
+ if (totalWidth > 0) {
+ totalWidth += horizontalSpacing();
}
-
- currentWidth += itemWidth;
- rowHeight = qMax(rowHeight, itemSize.height());
- qCDebug(FlowLayoutLog) << "Updated current width:" << currentWidth << "Updated row height:" << rowHeight;
+ totalWidth += s.width();
+ maxHeight = qMax(maxHeight, s.height());
}
-
- // Account for the final row
- maxWidth = qMax(maxWidth, currentWidth);
- totalHeight += rowHeight;
- qCDebug(FlowLayoutLog) << "Final total height:" << totalHeight << "Final max width:" << maxWidth;
-
- return QSize(maxWidth, totalHeight);
+ return QSize(totalWidth, maxHeight);
}
/**
- * @brief Calculates the minimum size for horizontal flow direction.
- * @return A QSize representing the minimum required dimensions.
+ * @brief Minimum size for horizontal flow: the largest single item.
+ *
+ * This guarantees we can always display at least one item per row.
*/
QSize FlowLayout::calculateMinimumSizeHorizontal() const
{
- int maxWidth = 0; // Tracks the maximum width of a row
- int totalHeight = 0; // Tracks the total height across all rows
- int rowHeight = 0; // Tracks the height of the current row
- int currentWidth = 0; // Tracks the current row's width
+ QSize size(0, 0);
+ for (const QLayoutItem *item : items) {
+ if (!item || item->isEmpty()) {
+ qCDebug(FlowLayoutLog) << "Skipping empty item.";
+ continue;
+ }
+ size = size.expandedTo(item->minimumSize());
+ }
+ return size;
+}
- const int availableWidth = getParentScrollAreaWidth() == 0 ? parentWidget()->width() : getParentScrollAreaWidth();
-
- qCDebug(FlowLayoutLog) << "Calculating horizontal minimum size. Available width:" << availableWidth;
+/**
+ * @brief Preferred size for vertical flow: all items in a single column (unconstrained).
+ */
+QSize FlowLayout::calculateSizeHintVertical() const
+{
+ int maxWidth = 0;
+ int totalHeight = 0;
for (const QLayoutItem *item : items) {
if (!item || item->isEmpty()) {
qCDebug(FlowLayoutLog) << "Skipping empty item.";
continue;
}
-
- QSize itemMinSize = item->minimumSize();
- int itemWidth = itemMinSize.width() + horizontalSpacing();
- qCDebug(FlowLayoutLog) << "Processing item. Minimum size:" << itemMinSize << "Width with spacing:" << itemWidth;
-
- if (currentWidth + itemWidth > availableWidth) {
- qCDebug(FlowLayoutLog) << "Row overflow. Current width:" << currentWidth << "Row height:" << rowHeight;
- maxWidth = qMax(maxWidth, currentWidth);
- totalHeight += rowHeight + verticalSpacing();
- qCDebug(FlowLayoutLog) << "Updated total height:" << totalHeight << "Max width so far:" << maxWidth;
-
- currentWidth = 0;
- rowHeight = 0;
+ const QSize s = item->sizeHint();
+ if (totalHeight > 0) {
+ totalHeight += verticalSpacing();
}
-
- currentWidth += itemWidth;
- rowHeight = qMax(rowHeight, itemMinSize.height());
- qCDebug(FlowLayoutLog) << "Updated current width:" << currentWidth << "Updated row height:" << rowHeight;
+ totalHeight += s.height();
+ maxWidth = qMax(maxWidth, s.width());
}
-
- // Account for the final row
- maxWidth = qMax(maxWidth, currentWidth);
- totalHeight += rowHeight;
- qCDebug(FlowLayoutLog) << "Final total height:" << totalHeight << "Final max width:" << maxWidth;
-
return QSize(maxWidth, totalHeight);
}
/**
- * @brief Calculates the size hint for vertical flow direction.
- * @return A QSize representing the preferred dimensions.
- */
-QSize FlowLayout::calculateSizeHintVertical() const
-{
- int totalWidth = 0;
- int maxHeight = 0;
- int colWidth = 0;
- int currentHeight = 0;
-
- const int availableHeight = qMax(parentWidget()->height(), getParentScrollAreaHeight());
-
- qCDebug(FlowLayoutLog) << "Calculating vertical size hint. Available height:" << availableHeight;
-
- for (const QLayoutItem *item : items) {
- if (!item || item->isEmpty()) {
- qCDebug(FlowLayoutLog) << "Skipping empty item.";
- continue;
- }
-
- QSize itemSize = item->sizeHint();
- qCDebug(FlowLayoutLog) << "Processing item. Size:" << itemSize;
-
- if (currentHeight + itemSize.height() > availableHeight) {
- qCDebug(FlowLayoutLog) << "Column overflow. Current height:" << currentHeight
- << "Column width:" << colWidth;
- totalWidth += colWidth + horizontalSpacing();
- maxHeight = qMax(maxHeight, currentHeight);
- qCDebug(FlowLayoutLog) << "Updated total width:" << totalWidth << "Max height so far:" << maxHeight;
-
- currentHeight = 0;
- colWidth = 0;
- }
-
- currentHeight += itemSize.height() + verticalSpacing();
- colWidth = qMax(colWidth, itemSize.width());
- qCDebug(FlowLayoutLog) << "Updated current height:" << currentHeight << "Updated column width:" << colWidth;
- }
-
- // Account for the final column
- totalWidth += colWidth;
- maxHeight = qMax(maxHeight, currentHeight);
- qCDebug(FlowLayoutLog) << "Final total width:" << totalWidth << "Final max height:" << maxHeight;
-
- return QSize(totalWidth, maxHeight);
-}
-
-/**
- * @brief Calculates the minimum size for vertical flow direction.
- * @return A QSize representing the minimum required dimensions.
+ * @brief Minimum size for vertical flow: the largest single item.
*/
QSize FlowLayout::calculateMinimumSizeVertical() const
{
- int totalWidth = 0; // Tracks the total width across all columns
- int maxHeight = 0; // Tracks the maximum height of a column
- int colWidth = 0; // Tracks the width of the current column
- int currentHeight = 0; // Tracks the current column's height
-
- const int availableHeight = qMax(parentWidget()->height(), getParentScrollAreaHeight());
-
- qCDebug(FlowLayoutLog) << "Calculating vertical minimum size. Available height:" << availableHeight;
-
+ QSize size(0, 0);
for (const QLayoutItem *item : items) {
if (!item || item->isEmpty()) {
qCDebug(FlowLayoutLog) << "Skipping empty item.";
continue;
}
-
- QSize itemMinSize = item->minimumSize();
- int itemHeight = itemMinSize.height() + verticalSpacing();
- qCDebug(FlowLayoutLog) << "Processing item. Minimum size:" << itemMinSize
- << "Height with spacing:" << itemHeight;
-
- if (currentHeight + itemHeight > availableHeight) {
- qCDebug(FlowLayoutLog) << "Column overflow. Current height:" << currentHeight
- << "Column width:" << colWidth;
- totalWidth += colWidth + horizontalSpacing();
- maxHeight = qMax(maxHeight, currentHeight);
- qCDebug(FlowLayoutLog) << "Updated total width:" << totalWidth << "Max height so far:" << maxHeight;
-
- currentHeight = 0;
- colWidth = 0;
- }
-
- currentHeight += itemHeight;
- colWidth = qMax(colWidth, itemMinSize.width());
- qCDebug(FlowLayoutLog) << "Updated current height:" << currentHeight << "Updated column width:" << colWidth;
+ size = size.expandedTo(item->minimumSize());
}
-
- // Account for the final column
- totalWidth += colWidth;
- maxHeight = qMax(maxHeight, currentHeight);
- qCDebug(FlowLayoutLog) << "Final total width:" << totalWidth << "Final max height:" << maxHeight;
-
- return QSize(totalWidth, maxHeight);
+ return size;
}
/**
@@ -543,7 +393,7 @@ QSize FlowLayout::calculateMinimumSizeVertical() const
*/
void FlowLayout::addItem(QLayoutItem *item)
{
- if (item != nullptr) {
+ if (item) {
items.append(item);
}
}
@@ -551,11 +401,8 @@ void FlowLayout::addItem(QLayoutItem *item)
void FlowLayout::insertWidgetAtIndex(QWidget *toInsert, int index)
{
addChildWidget(toInsert);
-
- // We don't want to fail on an index that violates the bounds, so we just clamp it.
- int boundedIndex = qBound(0, index, qMax(0, static_cast(items.size())));
- items.insert(boundedIndex, new QWidgetItem(toInsert));
-
+ const int bounded = qBound(0, index, static_cast(items.size()));
+ items.insert(bounded, new QWidgetItem(toInsert));
invalidate();
}
@@ -613,52 +460,13 @@ int FlowLayout::verticalSpacing() const
*/
int FlowLayout::smartSpacing(const QStyle::PixelMetric pm) const
{
- QObject *parent = this->parent();
-
- if (!parent) {
+ QObject *p = parent();
+ if (!p) {
return -1;
}
-
- if (parent->isWidgetType()) {
- const auto *pw = dynamic_cast(parent);
+ if (p->isWidgetType()) {
+ const auto *pw = static_cast(p);
return pw->style()->pixelMetric(pm, nullptr, pw);
}
-
- return dynamic_cast(parent)->spacing();
-}
-
-/**
- * @brief Gets the width of the parent scroll area, if any.
- * @return The width of the scroll area's viewport, or 0 if not found.
- */
-int FlowLayout::getParentScrollAreaWidth() const
-{
- QWidget *parent = parentWidget();
-
- while (parent) {
- if (const auto *scrollArea = qobject_cast(parent)) {
- return scrollArea->viewport()->width();
- }
- parent = parent->parentWidget();
- }
-
- return 0;
-}
-
-/**
- * @brief Gets the height of the parent scroll area, if any.
- * @return The height of the scroll area's viewport, or 0 if not found.
- */
-int FlowLayout::getParentScrollAreaHeight() const
-{
- QWidget *parent = parentWidget();
-
- while (parent) {
- if (const auto *scrollArea = qobject_cast(parent)) {
- return scrollArea->viewport()->height();
- }
- parent = parent->parentWidget();
- }
-
- return 0;
-}
+ return static_cast(p)->spacing();
+}
\ No newline at end of file
diff --git a/cockatrice/src/interface/layouts/flow_layout.h b/cockatrice/src/interface/layouts/flow_layout.h
index cf109d260..cc206afae 100644
--- a/cockatrice/src/interface/layouts/flow_layout.h
+++ b/cockatrice/src/interface/layouts/flow_layout.h
@@ -1,7 +1,8 @@
/**
* @file flow_layout.h
* @ingroup UI
- * @brief TODO: Document this.
+ * @brief A QLayout subclass that arranges child widgets in wrapping rows (horizontal flow)
+ * or wrapping columns (vertical flow).
*/
#ifndef FLOW_LAYOUT_H
@@ -10,8 +11,8 @@
#include
#include
#include
+#include
#include
-#include
inline Q_LOGGING_CATEGORY(FlowLayoutLog, "flow_layout", QtInfoMsg);
@@ -19,42 +20,55 @@ class FlowLayout : public QLayout
{
public:
explicit FlowLayout(QWidget *parent = nullptr);
- FlowLayout(QWidget *parent, Qt::Orientation _flowDirection, int margin = 0, int hSpacing = 0, int vSpacing = 0);
+ FlowLayout(QWidget *parent, Qt::Orientation flowDirection, int margin = 0, int hSpacing = 0, int vSpacing = 0);
~FlowLayout() override;
+
void insertWidgetAtIndex(QWidget *toInsert, int index);
- [[nodiscard]] QSize calculateMinimumSizeHorizontal() const;
- [[nodiscard]] QSize calculateSizeHintVertical() const;
- [[nodiscard]] QSize calculateMinimumSizeVertical() const;
+ // QLayout interface
void addItem(QLayoutItem *item) override;
[[nodiscard]] int count() const override;
[[nodiscard]] QLayoutItem *itemAt(int index) const override;
QLayoutItem *takeAt(int index) override;
- [[nodiscard]] int horizontalSpacing() const;
+ void setGeometry(const QRect &rect) override;
+ // Size negotiation
[[nodiscard]] Qt::Orientations expandingDirections() const override;
[[nodiscard]] bool hasHeightForWidth() const override;
[[nodiscard]] int heightForWidth(int width) const override;
- [[nodiscard]] int verticalSpacing() const;
- [[nodiscard]] int doLayout(const QRect &rect, bool testOnly) const;
- [[nodiscard]] int smartSpacing(QStyle::PixelMetric pm) const;
- [[nodiscard]] int getParentScrollAreaWidth() const;
- [[nodiscard]] int getParentScrollAreaHeight() const;
-
- void setGeometry(const QRect &rect) override;
- virtual int layoutAllRows(int originX, int originY, int availableWidth);
- virtual void layoutSingleRow(const QVector &rowItems, int x, int y);
- int layoutAllColumns(int originX, int originY, int availableHeight);
- void layoutSingleColumn(const QVector &colItems, int x, int y);
[[nodiscard]] QSize sizeHint() const override;
[[nodiscard]] QSize minimumSize() const override;
- [[nodiscard]] QSize calculateSizeHintHorizontal() const;
+
+ // Spacing helpers
+ void setHorizontalMargin(int margin)
+ {
+ horizontalMargin = margin;
+ }
+ [[nodiscard]] int horizontalSpacing() const;
+ void setVerticalMargin(int margin)
+ {
+ verticalMargin = margin;
+ }
+ [[nodiscard]] int verticalSpacing() const;
+ [[nodiscard]] int smartSpacing(QStyle::PixelMetric pm) const;
+
+ // Layout passes (virtual so subclasses can override placement logic)
+ virtual int layoutAllRows(int originX, int originY, int availableWidth);
+ virtual void layoutSingleRow(const QVector &rowItems, int x, int y, int availableWidth);
+ int layoutAllColumns(int originX, int originY, int availableHeight);
+ void layoutSingleColumn(const QVector &colItems, int x, int y);
protected:
- QList items; // List to store layout items
+ // Size-hint helpers split by direction
+ [[nodiscard]] QSize calculateSizeHintHorizontal() const;
+ [[nodiscard]] QSize calculateMinimumSizeHorizontal() const;
+ [[nodiscard]] QSize calculateSizeHintVertical() const;
+ [[nodiscard]] QSize calculateMinimumSizeVertical() const;
+
+ QList items;
Qt::Orientation flowDirection;
- int horizontalMargin;
- int verticalMargin;
+ int horizontalMargin; ///< Horizontal spacing between items (-1 = use style default)
+ int verticalMargin; ///< Vertical spacing between items (-1 = use style default)
};
#endif // FLOW_LAYOUT_H
\ No newline at end of file
diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_widget.cpp b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_widget.cpp
index 4931aeaa4..0107294c7 100644
--- a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_widget.cpp
+++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_widget.cpp
@@ -20,12 +20,11 @@
DrawProbabilityWidget::DrawProbabilityWidget(QWidget *parent, DeckListStatisticsAnalyzer *analyzer)
: AbstractAnalyticsPanelWidget(parent, analyzer)
{
- controls = new QWidget(this);
- controlLayout = new QHBoxLayout(controls);
- controlLayout->setContentsMargins(11, 0, 11, 0);
+ controls = new FlowWidget(this, Qt::Horizontal, Qt::ScrollBarAlwaysOff, Qt::ScrollBarAlwaysOff);
+ controls->setSpacing(4, 4);
labelPrefix = new QLabel(this);
- controlLayout->addWidget(labelPrefix);
+ controls->addWidget(labelPrefix);
criteriaCombo = new QComboBox(this);
// Give these things item-data so we can translate the actual user-facing strings
@@ -33,33 +32,32 @@ DrawProbabilityWidget::DrawProbabilityWidget(QWidget *parent, DeckListStatistics
criteriaCombo->addItem(QString(), "type");
criteriaCombo->addItem(QString(), "subtype");
criteriaCombo->addItem(QString(), "cmc");
- controlLayout->addWidget(criteriaCombo);
+ controls->addWidget(criteriaCombo);
exactnessCombo = new QComboBox(this);
exactnessCombo->addItem(QString(), true); // At least
exactnessCombo->addItem(QString(), false); // Exactly
- controlLayout->addWidget(exactnessCombo);
+ controls->addWidget(exactnessCombo);
quantitySpin = new QSpinBox(this);
quantitySpin->setRange(1, 60);
- controlLayout->addWidget(quantitySpin);
+ controls->addWidget(quantitySpin);
labelMiddle = new QLabel(this);
- controlLayout->addWidget(labelMiddle);
+ controls->addWidget(labelMiddle);
drawnSpin = new QSpinBox(this);
drawnSpin->setRange(1, 60);
drawnSpin->setValue(7);
- controlLayout->addWidget(drawnSpin);
+ controls->addWidget(drawnSpin);
labelSuffix = new QLabel(this);
- controlLayout->addWidget(labelSuffix);
+ controls->addWidget(labelSuffix);
labelPrefix->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed);
labelMiddle->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed);
labelSuffix->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed);
- controlLayout->addStretch(1);
layout->addWidget(controls);
// Table
diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_widget.h b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_widget.h
index 80015999f..9f7b971b1 100644
--- a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_widget.h
+++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_widget.h
@@ -1,6 +1,8 @@
#ifndef COCKATRICE_DRAW_PROBABILITY_WIDGET_H
#define COCKATRICE_DRAW_PROBABILITY_WIDGET_H
+#include "../../../../layouts/flow_layout.h"
+#include "../../../general/layout_containers/flow_widget.h"
#include "../../abstract_analytics_panel_widget.h"
#include "../../deck_list_statistics_analyzer.h"
#include "draw_probability_config.h"
@@ -31,8 +33,7 @@ private slots:
private:
DrawProbabilityConfig config;
- QWidget *controls;
- QHBoxLayout *controlLayout;
+ FlowWidget *controls;
QLabel *labelPrefix;
QLabel *labelMiddle;
QLabel *labelSuffix;
diff --git a/cockatrice/src/interface/widgets/deck_analytics/deck_analytics_widget.cpp b/cockatrice/src/interface/widgets/deck_analytics/deck_analytics_widget.cpp
index 147675e21..76552ea2f 100644
--- a/cockatrice/src/interface/widgets/deck_analytics/deck_analytics_widget.cpp
+++ b/cockatrice/src/interface/widgets/deck_analytics/deck_analytics_widget.cpp
@@ -23,8 +23,8 @@ DeckAnalyticsWidget::DeckAnalyticsWidget(QWidget *parent, DeckListStatisticsAnal
layout = new QVBoxLayout(this);
// Controls
- controlContainer = new QWidget(this);
- controlLayout = new QHBoxLayout(controlContainer);
+ controlContainer = new FlowWidget(this, Qt::Horizontal, Qt::ScrollBarAlwaysOff, Qt::ScrollBarAlwaysOff);
+ controlContainer->setSpacing(4, 4);
addButton = new QPushButton(this);
removeButton = new QPushButton(this);
saveButton = new QPushButton(this);
@@ -32,11 +32,11 @@ DeckAnalyticsWidget::DeckAnalyticsWidget(QWidget *parent, DeckListStatisticsAnal
includeSideboardCheckBox = new QCheckBox(this);
includeSideboardCheckBox->setChecked(false);
- controlLayout->addWidget(addButton);
- controlLayout->addWidget(removeButton);
- controlLayout->addWidget(saveButton);
- controlLayout->addWidget(loadButton);
- controlLayout->addWidget(includeSideboardCheckBox);
+ controlContainer->addWidget(addButton);
+ controlContainer->addWidget(removeButton);
+ controlContainer->addWidget(saveButton);
+ controlContainer->addWidget(loadButton);
+ controlContainer->addWidget(includeSideboardCheckBox);
layout->addWidget(controlContainer);
diff --git a/cockatrice/src/interface/widgets/deck_analytics/deck_analytics_widget.h b/cockatrice/src/interface/widgets/deck_analytics/deck_analytics_widget.h
index 09618c3f8..3c73deca2 100644
--- a/cockatrice/src/interface/widgets/deck_analytics/deck_analytics_widget.h
+++ b/cockatrice/src/interface/widgets/deck_analytics/deck_analytics_widget.h
@@ -7,6 +7,7 @@
#ifndef DECK_ANALYTICS_WIDGET_H
#define DECK_ANALYTICS_WIDGET_H
+#include "../general/layout_containers/flow_widget.h"
#include "abstract_analytics_panel_widget.h"
#include "deck_list_statistics_analyzer.h"
#include "resizable_panel.h"
@@ -51,8 +52,7 @@ private:
void addPanelInstance(const QString &typeId, AbstractAnalyticsPanelWidget *panel, const QJsonObject &cfg = {});
QVBoxLayout *layout;
- QWidget *controlContainer;
- QHBoxLayout *controlLayout;
+ FlowWidget *controlContainer;
QPushButton *addButton;
QPushButton *removeButton;
diff --git a/cockatrice/src/interface/widgets/general/layout_containers/flow_widget.cpp b/cockatrice/src/interface/widgets/general/layout_containers/flow_widget.cpp
index 75ab56b34..59c657724 100644
--- a/cockatrice/src/interface/widgets/general/layout_containers/flow_widget.cpp
+++ b/cockatrice/src/interface/widgets/general/layout_containers/flow_widget.cpp
@@ -1,42 +1,52 @@
/**
* @file flow_widget.cpp
- * @brief Implementation of the FlowWidget class for organizing widgets in a flow layout within a scrollable area.
+ * @brief Implementation of FlowWidget — a QWidget hosting a FlowLayout inside an
+ * optional QScrollArea.
*/
#include "flow_widget.h"
#include
#include
+#include
+#include
#include
-#include
-#include
/**
- * @brief Constructs a FlowWidget with a scrollable layout.
+ * @brief Constructs a FlowWidget.
*
- * @param parent The parent widget of this FlowWidget.
- * @param horizontalPolicy The horizontal scroll bar policy for the scroll area.
- * @param verticalPolicy The vertical scroll bar policy for the scroll area.
+ * When both scroll policies are Qt::ScrollBarAlwaysOff the scroll area is
+ * omitted entirely and the container is placed directly in the main layout.
+ *
+ * @param parent Parent widget.
+ * @param _flowDirection Qt::Horizontal for row-wrapping, Qt::Vertical for column-wrapping.
+ * @param horizontalPolicy Horizontal scroll-bar policy.
+ * @param verticalPolicy Vertical scroll-bar policy.
*/
FlowWidget::FlowWidget(QWidget *parent,
const Qt::Orientation _flowDirection,
const Qt::ScrollBarPolicy horizontalPolicy,
const Qt::ScrollBarPolicy verticalPolicy)
- : QWidget(parent), flowDirection(_flowDirection)
+ : QWidget(parent), scrollArea(nullptr), flowDirection(_flowDirection)
+
{
- // Main Widget and Layout
- if (_flowDirection == Qt::Horizontal) {
- setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Minimum);
- setMinimumWidth(0);
+ // Top-level size policy
+ // Horizontal flow: expand horizontally, let height be determined by wrapping.
+ // Vertical flow: expand vertically, let width be determined by wrapping.
+ if (flowDirection == Qt::Horizontal) {
+ setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
} else {
- setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Expanding);
- setMinimumHeight(0);
+ setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding);
}
+
mainLayout = new QHBoxLayout(this);
+ mainLayout->setContentsMargins(0, 0, 0, 0);
setLayout(mainLayout);
- if (horizontalPolicy != Qt::ScrollBarAlwaysOff || verticalPolicy != Qt::ScrollBarAlwaysOff) {
- // Scroll Area, which should expand as much as possible, since it should be the only direct child widget.
+ const bool useScrollArea = (horizontalPolicy != Qt::ScrollBarAlwaysOff || verticalPolicy != Qt::ScrollBarAlwaysOff);
+
+ // Scroll area (optional)
+ if (useScrollArea) {
scrollArea = new QScrollArea(this);
scrollArea->setWidgetResizable(true);
scrollArea->setMinimumSize(0, 0);
@@ -48,39 +58,28 @@ FlowWidget::FlowWidget(QWidget *parent,
scrollArea = nullptr;
}
- // Flow Layout inside the scroll area
- if (horizontalPolicy == Qt::ScrollBarAlwaysOff && verticalPolicy == Qt::ScrollBarAlwaysOff) {
- container = new QWidget(this);
- } else {
- container = new QWidget(scrollArea);
- }
+ // Container widget (holds the FlowLayout)
+ container = new QWidget(useScrollArea ? static_cast(scrollArea) : this);
+
+ // The container should be willing to grow in both axes; its actual size is
+ // governed by the FlowLayout's sizeHint / heightForWidth, not by a fixed policy.
+ container->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred);
+ container->setMinimumSize(0, 0);
flowLayout = new FlowLayout(container, flowDirection);
-
container->setLayout(flowLayout);
- // The container should expand as much as possible, trusting the scrollArea to constrain it.
- if (_flowDirection == Qt::Horizontal) {
- container->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum);
- container->setMinimumWidth(0);
- } else {
- container->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum);
- container->setMinimumHeight(0);
- }
- // Use the FlowLayout container directly if we disable the ScrollArea
- if (horizontalPolicy == Qt::ScrollBarAlwaysOff && verticalPolicy == Qt::ScrollBarAlwaysOff) {
- mainLayout->addWidget(container);
- } else {
+ if (useScrollArea) {
scrollArea->setWidget(container);
mainLayout->addWidget(scrollArea);
+ } else {
+ mainLayout->addWidget(container);
}
}
/**
* @brief Adds a widget to the flow layout within the FlowWidget.
*
- * Adjusts the widget's size policy based on the scroll bar policies.
- *
* @param widget_to_add The widget to add to the flow layout.
*/
void FlowWidget::addWidget(QWidget *widget_to_add) const
@@ -100,77 +99,74 @@ void FlowWidget::removeWidget(QWidget *widgetToRemove) const
}
/**
- * @brief Clears all widgets from the flow layout.
+ * @brief Removes all widgets from the flow layout and deletes them.
*
- * Deletes each widget and layout item, and recreates the flow layout if it was removed.
+ * If the layout pointer has somehow been lost it is recreated before returning.
*/
void FlowWidget::clearLayout()
{
- if (flowLayout != nullptr) {
+ if (flowLayout) {
QLayoutItem *item;
- while ((item = flowLayout->takeAt(0)) != nullptr) {
- item->widget()->deleteLater(); // Delete the widget
- delete item; // Delete the layout item
+ while ((item = flowLayout->takeAt(0))) {
+ if (item->widget())
+ item->widget()->deleteLater();
+ delete item;
}
} else {
+ // Defensive fallback: recreate the layout if it was deleted externally.
flowLayout = new FlowLayout(container, flowDirection);
container->setLayout(flowLayout);
}
}
/**
- * @brief Handles resize events for the FlowWidget.
+ * @brief Marks the flow layout as dirty so Qt recomputes item positions.
*
- * Triggers layout recalculation and adjusts the scroll area content size.
- *
- * @param event The resize event containing the new size information.
+ * We do NOT call adjustSize() or activate() here:
+ * - adjustSize() would freeze geometry by calling setFixedSize internally.
+ * - activate() called inside a resize event can cause synchronous re-entrancy.
+ * Qt automatically calls setGeometry on the layout after a resize, so simply
+ * invalidating is sufficient.
*/
void FlowWidget::resizeEvent(QResizeEvent *event)
{
QWidget::resizeEvent(event);
+ qCDebug(FlowWidgetSizeLog) << "resizeEvent:" << event->size();
- qCDebug(FlowWidgetSizeLog) << event->size();
-
- // Trigger the layout to recalculate
- if (flowLayout != nullptr) {
- flowLayout->invalidate(); // Marks the layout as dirty and requires recalculation
- flowLayout->activate(); // Recalculate the layout based on the new size
- }
-
- // Ensure the scroll area and its content adjust correctly
- if (scrollArea != nullptr && scrollArea->widget() != nullptr) {
- qCDebug(FlowWidgetSizeLog) << "Got a scrollarea: " << scrollArea->widget()->size();
- scrollArea->widget()->adjustSize();
- } else {
- container->adjustSize();
+ if (flowLayout) {
+ flowLayout->invalidate();
}
}
+void FlowWidget::setSpacing(int hSpacing, int vSpacing)
+{
+ flowLayout->setHorizontalMargin(hSpacing);
+ flowLayout->setVerticalMargin(vSpacing);
+ flowLayout->invalidate();
+}
+
/**
- * @brief Sets the minimum size for all widgets inside the FlowWidget to the maximum sizeHint of all of them.
+ * @brief Sets every child widget's minimum size to the largest sizeHint in the layout.
+ *
+ * Useful for toolbars or button bars where all items should be the same size.
*/
void FlowWidget::setMinimumSizeToMaxSizeHint()
{
- QSize maxSize(0, 0); // Initialize to a zero size
+ QSize maxSize(0, 0);
// Iterate over all widgets in the flow layout to find the maximum sizeHint
for (int i = 0; i < flowLayout->count(); ++i) {
- if (QLayoutItem *item = flowLayout->itemAt(i)) {
- if (QWidget *widget = item->widget()) {
- // Update the max size based on the sizeHint of each widget
- QSize widgetSizeHint = widget->sizeHint();
- maxSize.setWidth(qMax(maxSize.width(), widgetSizeHint.width()));
- maxSize.setHeight(qMax(maxSize.height(), widgetSizeHint.height()));
- }
+ QLayoutItem *item = flowLayout->itemAt(i);
+ if (item && item->widget()) {
+ maxSize = maxSize.expandedTo(item->widget()->sizeHint());
}
}
// Set the minimum size for all widgets to the max sizeHint
for (int i = 0; i < flowLayout->count(); ++i) {
- if (QLayoutItem *item = flowLayout->itemAt(i)) {
- if (QWidget *widget = item->widget()) {
- widget->setMinimumSize(maxSize);
- }
+ QLayoutItem *item = flowLayout->itemAt(i);
+ if (item && item->widget()) {
+ item->widget()->setMinimumSize(maxSize);
}
}
}
diff --git a/cockatrice/src/interface/widgets/general/layout_containers/flow_widget.h b/cockatrice/src/interface/widgets/general/layout_containers/flow_widget.h
index d9fa49937..1932ce70a 100644
--- a/cockatrice/src/interface/widgets/general/layout_containers/flow_widget.h
+++ b/cockatrice/src/interface/widgets/general/layout_containers/flow_widget.h
@@ -1,22 +1,23 @@
/**
* @file flow_widget.h
* @ingroup UI
- * @brief TODO: Document this.
+ * @brief A QWidget that wraps a FlowLayout inside an optional QScrollArea.
*/
#ifndef FLOW_WIDGET_H
#define FLOW_WIDGET_H
+
#include "../../../layouts/flow_layout.h"
#include
#include
+#include
#include
-#include
inline Q_LOGGING_CATEGORY(FlowWidgetLog, "flow_widget", QtInfoMsg);
inline Q_LOGGING_CATEGORY(FlowWidgetSizeLog, "flow_widget.size", QtInfoMsg);
-class FlowWidget final : public QWidget
+class FlowWidget : public QWidget
{
Q_OBJECT
@@ -25,17 +26,20 @@ public:
Qt::Orientation orientation,
Qt::ScrollBarPolicy horizontalPolicy,
Qt::ScrollBarPolicy verticalPolicy);
+
void addWidget(QWidget *widget_to_add) const;
void insertWidgetAtIndex(QWidget *toInsert, int index);
void removeWidget(QWidget *widgetToRemove) const;
void clearLayout();
+
[[nodiscard]] int count() const;
[[nodiscard]] QLayoutItem *itemAt(int index) const;
- QScrollArea *scrollArea;
+ QScrollArea *scrollArea; ///< Null when both scroll policies are AlwaysOff.
public slots:
void setMinimumSizeToMaxSizeHint();
+ void setSpacing(int hSpacing, int vSpacing);
protected:
void resizeEvent(QResizeEvent *event) override;
@@ -47,4 +51,4 @@ private:
QWidget *container;
};
-#endif // FLOW_WIDGET_H
+#endif // FLOW_WIDGET_H
\ No newline at end of file
diff --git a/cockatrice/src/interface/widgets/quick_settings/settings_button_widget.cpp b/cockatrice/src/interface/widgets/quick_settings/settings_button_widget.cpp
index 81812104a..badc437ee 100644
--- a/cockatrice/src/interface/widgets/quick_settings/settings_button_widget.cpp
+++ b/cockatrice/src/interface/widgets/quick_settings/settings_button_widget.cpp
@@ -35,19 +35,33 @@ void SettingsButtonWidget::setButtonIcon(QPixmap iconMap)
button->setIcon(iconMap);
}
-void SettingsButtonWidget::setButtonText(const QString &buttonText)
+void SettingsButtonWidget::setButtonText(const QString &text)
{
- // 🔓 unlock size constraints
+ buttonText = text;
+
button->setMinimumSize(QSize(0, 0));
button->setMaximumSize(QSize(QWIDGETSIZE_MAX, QWIDGETSIZE_MAX));
-
button->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
- button->setText(buttonText);
-
+ button->setText(text);
button->setFixedHeight(32);
button->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed);
- button->setMinimumWidth(button->sizeHint().width());
+ button->setMinimumWidth(32); // icon-only fallback minimum
+}
+
+void SettingsButtonWidget::setCompact(bool _compact)
+{
+ compact = _compact;
+ if (compact) {
+ button->setToolButtonStyle(Qt::ToolButtonIconOnly);
+ button->setFixedWidth(32);
+ } else {
+ button->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
+ button->setText(buttonText);
+ button->setFixedWidth(QWIDGETSIZE_MAX); // release fixed width
+ button->setMinimumWidth(32);
+ button->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed);
+ }
}
void SettingsButtonWidget::togglePopup()
diff --git a/cockatrice/src/interface/widgets/quick_settings/settings_button_widget.h b/cockatrice/src/interface/widgets/quick_settings/settings_button_widget.h
index 36f01ac38..43de8183f 100644
--- a/cockatrice/src/interface/widgets/quick_settings/settings_button_widget.h
+++ b/cockatrice/src/interface/widgets/quick_settings/settings_button_widget.h
@@ -23,6 +23,11 @@ public:
void removeSettingsWidget(QWidget *toRemove) const;
void setButtonIcon(QPixmap iconMap);
void setButtonText(const QString &buttonText);
+ void setCompact(bool compact);
+ bool isCompact() const
+ {
+ return compact;
+ };
protected:
void mousePressEvent(QMouseEvent *event) override;
@@ -34,6 +39,8 @@ private slots:
private:
QHBoxLayout *layout;
QToolButton *button;
+ QString buttonText;
+ bool compact;
public:
SettingsPopupWidget *popup;
diff --git a/cockatrice/src/interface/widgets/utility/compact_push_button.cpp b/cockatrice/src/interface/widgets/utility/compact_push_button.cpp
new file mode 100644
index 000000000..cf98bfede
--- /dev/null
+++ b/cockatrice/src/interface/widgets/utility/compact_push_button.cpp
@@ -0,0 +1,73 @@
+#include "compact_push_button.h"
+
+CompactPushButton::CompactPushButton(QWidget *parent) : QPushButton(parent)
+{
+ setCheckable(true);
+ setFixedHeight(32);
+
+ // default sizing
+ setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed);
+
+ connect(this, &QPushButton::clicked, this, [] {
+ // your popup logic here
+ });
+}
+
+void CompactPushButton::setButtonText(const QString &text)
+{
+ fullText = text;
+
+ if (!compact) {
+ setText(fullText);
+ }
+
+ updateGeometryState();
+}
+
+void CompactPushButton::setButtonIcon(const QIcon &icon)
+{
+ setIcon(icon);
+}
+
+void CompactPushButton::setCompact(bool enabled)
+{
+ compact = enabled;
+
+ if (compact) {
+ setText(QString()); // icon only
+ } else {
+ setText(fullText);
+ }
+
+ updateGeometryState();
+}
+
+void CompactPushButton::updateGeometryState()
+{
+ const int buttonHeight = 32;
+
+ setMinimumHeight(buttonHeight);
+ setMaximumHeight(buttonHeight);
+
+ if (compact) {
+ setMinimumWidth(buttonHeight);
+ setMaximumWidth(buttonHeight);
+ } else {
+ setMinimumWidth(0);
+ setMaximumWidth(QWIDGETSIZE_MAX);
+ }
+
+ updateGeometry();
+}
+
+int CompactPushButton::expandedWidth() const
+{
+ QFontMetrics fm(font());
+
+ return fm.horizontalAdvance(fullText) + 48; // icon + padding
+}
+
+int CompactPushButton::compactWidth() const
+{
+ return 32;
+}
\ No newline at end of file
diff --git a/cockatrice/src/interface/widgets/utility/compact_push_button.h b/cockatrice/src/interface/widgets/utility/compact_push_button.h
new file mode 100644
index 000000000..b0017beee
--- /dev/null
+++ b/cockatrice/src/interface/widgets/utility/compact_push_button.h
@@ -0,0 +1,31 @@
+#ifndef COCKATRICE_COMPACT_PUSH_BUTTON_H
+#define COCKATRICE_COMPACT_PUSH_BUTTON_H
+
+#include
+
+class CompactPushButton : public QPushButton
+{
+ Q_OBJECT
+
+public:
+ explicit CompactPushButton(QWidget *parent = nullptr);
+
+ void setButtonText(const QString &text);
+
+ void setButtonIcon(const QIcon &icon);
+
+ void setCompact(bool enabled);
+
+ int expandedWidth() const;
+
+ int compactWidth() const;
+
+private:
+ void updateGeometryState();
+
+private:
+ QString fullText;
+ bool compact = false;
+};
+
+#endif // COCKATRICE_COMPACT_PUSH_BUTTON_H
diff --git a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_filter_toolbar_widget.cpp b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_filter_toolbar_widget.cpp
index 790ac771a..885925694 100644
--- a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_filter_toolbar_widget.cpp
+++ b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_filter_toolbar_widget.cpp
@@ -5,16 +5,9 @@
#include
VisualDatabaseDisplayFilterToolbarWidget::VisualDatabaseDisplayFilterToolbarWidget(VisualDatabaseDisplayWidget *_parent)
- : QWidget(_parent), visualDatabaseDisplay(_parent)
+ : FlowWidget(_parent, Qt::Horizontal, Qt::ScrollBarAlwaysOff, Qt::ScrollBarAlwaysOff),
+ visualDatabaseDisplay(_parent)
{
- filterContainerLayout = new QHBoxLayout(this);
- filterContainerLayout->setContentsMargins(11, 0, 11, 0);
- filterContainerLayout->setSpacing(2);
- setLayout(filterContainerLayout);
- filterContainerLayout->setAlignment(Qt::AlignLeft);
-
- setMaximumHeight(80);
-
connect(this, &VisualDatabaseDisplayFilterToolbarWidget::searchModelChanged, visualDatabaseDisplay,
&VisualDatabaseDisplayWidget::onSearchModelChanged);
@@ -131,10 +124,18 @@ void VisualDatabaseDisplayFilterToolbarWidget::initialize()
filterLayout->addWidget(quickFilterFormatLegalityWidget);
// put everything into main layout
- filterContainerLayout->addWidget(sortGroupBox);
- filterContainerLayout->addWidget(filterGroupBox);
- filterContainerLayout->addStretch();
- filterContainerLayout->addWidget(quickFilterSaveLoadWidget);
+ addWidget(sortGroupBox);
+ addWidget(filterGroupBox);
+ auto *spacer = new QWidget(this);
+ spacer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
+ spacer->setAttribute(Qt::WA_TransparentForMouseEvents);
+ addWidget(spacer);
+ addWidget(quickFilterSaveLoadWidget);
+ addWidget(quickFilterSaveLoadWidget);
+
+ // Force a layout pass so sizeHint() is accurate
+ layout()->activate();
+ fullWidthHint = sizeHint().width();
}
void VisualDatabaseDisplayFilterToolbarWidget::retranslateUi()
@@ -155,4 +156,25 @@ void VisualDatabaseDisplayFilterToolbarWidget::retranslateUi()
quickFilterSubTypeWidget->setButtonText(tr("Sub Type"));
quickFilterSetWidget->setButtonText(tr("Sets"));
quickFilterFormatLegalityWidget->setButtonText(tr("Formats"));
+}
+
+void VisualDatabaseDisplayFilterToolbarWidget::resizeEvent(QResizeEvent *event)
+{
+ QWidget::resizeEvent(event);
+ updateCompactMode(event->size().width());
+}
+
+void VisualDatabaseDisplayFilterToolbarWidget::updateCompactMode(int availableWidth)
+{
+ const bool compact = availableWidth < fullWidthHint;
+
+ const QList filterButtons = {
+ quickFilterSaveLoadWidget, quickFilterNameWidget, quickFilterMainTypeWidget,
+ quickFilterSubTypeWidget, quickFilterSetWidget, quickFilterFormatLegalityWidget,
+ };
+
+ for (auto *btn : filterButtons) {
+ if (btn->isCompact() != compact) // only act on transitions
+ btn->setCompact(compact);
+ }
}
\ No newline at end of file
diff --git a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_filter_toolbar_widget.h b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_filter_toolbar_widget.h
index 5cca5187a..5b55f4ba6 100644
--- a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_filter_toolbar_widget.h
+++ b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_filter_toolbar_widget.h
@@ -10,7 +10,7 @@
class VisualDatabaseDisplayWidget;
-class VisualDatabaseDisplayFilterToolbarWidget : public QWidget
+class VisualDatabaseDisplayFilterToolbarWidget : public FlowWidget
{
Q_OBJECT
@@ -32,7 +32,6 @@ private:
QGroupBox *filterGroupBox;
QLabel *filterByLabel;
- QHBoxLayout *filterContainerLayout;
SettingsButtonWidget *quickFilterSaveLoadWidget;
VisualDatabaseDisplayFilterSaveLoadWidget *saveLoadWidget;
SettingsButtonWidget *quickFilterNameWidget;
@@ -45,6 +44,12 @@ private:
VisualDatabaseDisplaySetFilterWidget *setFilterWidget;
SettingsButtonWidget *quickFilterFormatLegalityWidget;
VisualDatabaseDisplayFormatLegalityFilterWidget *formatLegalityWidget;
+
+ int fullWidthHint = 0;
+ void updateCompactMode(int availableWidth);
+
+protected:
+ void resizeEvent(QResizeEvent *event) override;
};
#endif // COCKATRICE_VISUAL_DATABASE_DISPLAY_FILTER_TOOLBAR_WIDGET_H
diff --git a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_widget.cpp b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_widget.cpp
index 9c4432042..f9a783a3c 100644
--- a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_widget.cpp
+++ b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_widget.cpp
@@ -51,9 +51,7 @@ VisualDatabaseDisplayWidget::VisualDatabaseDisplayWidget(QWidget *parent,
connect(cardSizeWidget, &CardSizeWidget::cardSizeSettingUpdated, &SettingsCache::instance(),
&SettingsCache::setVisualDatabaseDisplayCardSize);
- searchContainer = new QWidget(this);
- searchLayout = new QHBoxLayout(searchContainer);
- searchContainer->setLayout(searchLayout);
+ searchContainer = new FlowWidget(this, Qt::Horizontal, Qt::ScrollBarAlwaysOff, Qt::ScrollBarAlwaysOff);
searchEdit = new SearchLineEdit();
searchEdit->setObjectName("searchEdit");
@@ -152,10 +150,10 @@ void VisualDatabaseDisplayWidget::initialize()
filterContainer->initialize();
filterContainer->setVisible(true);
- searchLayout->addWidget(colorFilterWidget);
- searchLayout->addWidget(clearFilterWidget);
- searchLayout->addWidget(searchEdit);
- searchLayout->addWidget(displayModeButton);
+ searchContainer->addWidget(colorFilterWidget);
+ searchContainer->addWidget(clearFilterWidget);
+ searchContainer->addWidget(searchEdit);
+ searchContainer->addWidget(displayModeButton);
mainLayout->addWidget(searchContainer);
diff --git a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_widget.h b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_widget.h
index 190ea1f6d..1db953b26 100644
--- a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_widget.h
+++ b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_widget.h
@@ -91,8 +91,7 @@ protected slots:
void onDisplayModeChanged(bool checked);
private:
- QWidget *searchContainer;
- QHBoxLayout *searchLayout;
+ FlowWidget *searchContainer;
SearchLineEdit *searchEdit;
QPushButton *displayModeButton;
FilterTreeModel *filterModel;
diff --git a/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_display_options_widget.cpp b/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_display_options_widget.cpp
index 79a98fda6..f44c9c3ef 100644
--- a/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_display_options_widget.cpp
+++ b/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_display_options_widget.cpp
@@ -72,7 +72,7 @@ VisualDeckDisplayOptionsWidget::VisualDeckDisplayOptionsWidget(QWidget *parent)
sortCriteriaButton->addSettingsWidget(sortLabel);
sortCriteriaButton->addSettingsWidget(sortByListWidget);
- displayTypeButton = new QPushButton(this);
+ displayTypeButton = new CompactPushButton(this);
connect(displayTypeButton, &QPushButton::clicked, this, &VisualDeckDisplayOptionsWidget::updateDisplayType);
groupAndSortLayout->addWidget(groupByLabel);
@@ -91,7 +91,8 @@ void VisualDeckDisplayOptionsWidget::retranslateUi()
sortByLabel->setText(tr("Sort by:"));
sortLabel->setText(tr("Click and drag to change the sort order within the groups"));
sortCriteriaButton->setToolTip(tr("Configure how cards are sorted within their groups"));
- displayTypeButton->setText(tr("Toggle Layout: Overlap"));
+ displayTypeButton->setButtonText(tr("Toggle Layout: Overlap"));
+ displayTypeButton->setButtonIcon(QPixmap("theme:icons/scales"));
displayTypeButton->setToolTip(
tr("Change how cards are displayed within zones (i.e. overlapped or fully visible.)"));
}
@@ -115,11 +116,32 @@ void VisualDeckDisplayOptionsWidget::updateDisplayType()
// Update UI and emit signal
switch (currentDisplayType) {
case DisplayType::Flat:
- displayTypeButton->setText(tr("Toggle Layout: Flat"));
+ displayTypeButton->setButtonText(tr("Toggle Layout: Flat"));
+ displayTypeButton->setButtonIcon(QPixmap("theme:icons/scroll"));
break;
case DisplayType::Overlap:
- displayTypeButton->setText(tr("Toggle Layout: Overlap"));
+ displayTypeButton->setButtonText(tr("Toggle Layout: Overlap"));
+ displayTypeButton->setButtonIcon(QPixmap("theme:icons/scales"));
break;
}
emit displayTypeChanged(currentDisplayType);
}
+
+void VisualDeckDisplayOptionsWidget::updateCompactMode(bool mode)
+{
+ displayTypeButton->setCompact(mode);
+}
+
+int VisualDeckDisplayOptionsWidget::expandedWidth() const
+{
+ return groupByLabel->sizeHint().width() + groupByComboBox->sizeHint().width() + sortByLabel->sizeHint().width() +
+ sortCriteriaButton->sizeHint().width() + displayTypeButton->expandedWidth() +
+ (groupAndSortLayout->spacing() * 4);
+}
+
+int VisualDeckDisplayOptionsWidget::compactWidth() const
+{
+ return groupByLabel->sizeHint().width() + groupByComboBox->sizeHint().width() + sortByLabel->sizeHint().width() +
+ sortCriteriaButton->sizeHint().width() + displayTypeButton->compactWidth() +
+ (groupAndSortLayout->spacing() * 4);
+}
diff --git a/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_display_options_widget.h b/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_display_options_widget.h
index 7a447753f..9930ff50b 100644
--- a/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_display_options_widget.h
+++ b/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_display_options_widget.h
@@ -53,6 +53,9 @@ public slots:
* Called when the application language changes.
*/
void retranslateUi();
+ void updateCompactMode(bool mode);
+ int expandedWidth() const;
+ int compactWidth() const;
public:
/**
@@ -108,7 +111,7 @@ private:
DisplayType currentDisplayType = DisplayType::Overlap;
/// Button used to toggle the display layout.
- QPushButton *displayTypeButton;
+ CompactPushButton *displayTypeButton;
/// Label for the group-by selector.
QLabel *groupByLabel;
diff --git a/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_editor_widget.cpp b/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_editor_widget.cpp
index e957eb304..19a99d3e9 100644
--- a/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_editor_widget.cpp
+++ b/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_editor_widget.cpp
@@ -9,6 +9,7 @@
#include "../general/layout_containers/flow_widget.h"
#include "../tabs/visual_deck_editor/tab_deck_editor_visual.h"
#include "../tabs/visual_deck_editor/tab_deck_editor_visual_tab_widget.h"
+#include "../utility/compact_push_button.h"
#include "visual_deck_display_options_widget.h"
#include
@@ -69,7 +70,13 @@ VisualDeckEditorWidget::VisualDeckEditorWidget(QWidget *parent,
void VisualDeckEditorWidget::initializeSearchBarAndCompleter()
{
- searchBar = new QLineEdit(this);
+ searchContainer = new QWidget(this);
+ searchContainer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
+ searchLayout = new QHBoxLayout(searchContainer);
+ searchContainer->setLayout(searchLayout);
+
+ searchBar = new QLineEdit(searchContainer);
+ searchContainer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
connect(searchBar, &QLineEdit::returnPressed, this, [=, this]() {
if (!searchBar->hasFocus())
return;
@@ -80,6 +87,8 @@ void VisualDeckEditorWidget::initializeSearchBarAndCompleter()
}
});
+ searchLayout->addWidget(searchBar);
+
setFocusProxy(searchBar);
setFocusPolicy(Qt::ClickFocus);
@@ -133,13 +142,16 @@ void VisualDeckEditorWidget::initializeSearchBarAndCompleter()
});
// Search button functionality
- searchPushButton = new QPushButton(this);
+ searchPushButton = new CompactPushButton(searchContainer);
+ searchPushButton->setButtonIcon(QPixmap("theme:icons/search"));
connect(searchPushButton, &QPushButton::clicked, this, [=, this]() {
ExactCard card = CardDatabaseManager::query()->getCard({searchBar->text()});
if (card) {
emit cardAdditionRequested(card);
}
});
+
+ searchLayout->addWidget(searchPushButton);
}
void VisualDeckEditorWidget::initializeDisplayOptionsWidget()
@@ -156,18 +168,14 @@ void VisualDeckEditorWidget::initializeDisplayOptionsWidget()
void VisualDeckEditorWidget::initializeDisplayOptionsAndSearchWidget()
{
initializeSearchBarAndCompleter();
-
initializeDisplayOptionsWidget();
- displayOptionsAndSearch = new QWidget(this);
- displayOptionsAndSearchLayout = new QHBoxLayout(displayOptionsAndSearch);
- displayOptionsAndSearchLayout->setContentsMargins(0, 0, 0, 0);
- displayOptionsAndSearchLayout->setAlignment(Qt::AlignLeft);
- displayOptionsAndSearch->setLayout(displayOptionsAndSearchLayout);
+ displayOptionsAndSearch = new FlowWidget(this, Qt::Horizontal, Qt::ScrollBarAlwaysOff, Qt::ScrollBarAlwaysOff);
- displayOptionsAndSearchLayout->addWidget(displayOptionsWidget);
- displayOptionsAndSearchLayout->addWidget(searchBar);
- displayOptionsAndSearchLayout->addWidget(searchPushButton);
+ // We split into two sub-widgets here so that the searchBar and button wrap together. At this point, we've done
+ // pretty much all we can and have reached our minimum size.
+ displayOptionsAndSearch->addWidget(displayOptionsWidget);
+ displayOptionsAndSearch->addWidget(searchContainer); // Expanding — fills remainder of its row
}
void VisualDeckEditorWidget::initializeScrollAreaAndZoneContainer()
@@ -205,7 +213,7 @@ void VisualDeckEditorWidget::connectDeckListModel()
void VisualDeckEditorWidget::retranslateUi()
{
searchBar->setPlaceholderText(tr("Type a card name here for suggestions from the database..."));
- searchPushButton->setText(tr("Quick search and add card"));
+ searchPushButton->setButtonText(tr("Quick search and add card"));
searchPushButton->setToolTip(tr("Search for closest match in the database (with auto-suggestions) and add "
"preferred printing to the deck on pressing enter"));
@@ -214,6 +222,44 @@ void VisualDeckEditorWidget::retranslateUi()
}
}
+void VisualDeckEditorWidget::resizeEvent(QResizeEvent *event)
+{
+ QWidget::resizeEvent(event);
+ updateCompactMode();
+}
+
+void VisualDeckEditorWidget::updateCompactMode()
+{
+ const int spacing = displayOptionsAndSearch->layout()->spacing();
+
+ const int available = displayOptionsAndSearch->width();
+
+ const int searchExpanded =
+ searchBar->sizeHint().width() + searchPushButton->expandedWidth() + searchLayout->spacing();
+
+ const int fullWidth = displayOptionsWidget->expandedWidth() + spacing + searchExpanded;
+
+ const int displayCompactWidth = displayOptionsWidget->compactWidth() + spacing + searchExpanded;
+
+ // everything expanded
+ if (available >= fullWidth) {
+ displayOptionsWidget->updateCompactMode(false);
+ searchPushButton->setCompact(false);
+ return;
+ }
+
+ // only display compact
+ if (available >= displayCompactWidth) {
+ displayOptionsWidget->updateCompactMode(true);
+ searchPushButton->setCompact(false);
+ return;
+ }
+
+ // both compact
+ displayOptionsWidget->updateCompactMode(true);
+ searchPushButton->setCompact(true);
+}
+
void VisualDeckEditorWidget::updatePlaceholderVisibility()
{
if (placeholderWidget) {
diff --git a/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_editor_widget.h b/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_editor_widget.h
index 13065d623..3b90ee4e2 100644
--- a/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_editor_widget.h
+++ b/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_editor_widget.h
@@ -11,6 +11,7 @@
#include "../cards/card_size_widget.h"
#include "../general/layout_containers/overlap_control_widget.h"
#include "../quick_settings/settings_button_widget.h"
+#include "../utility/compact_push_button.h"
#include "visual_deck_editor_placeholder_widget.h"
#include
@@ -39,6 +40,7 @@ class VisualDeckEditorWidget : public QWidget
public:
explicit VisualDeckEditorWidget(QWidget *parent, DeckListModel *deckListModel, QItemSelectionModel *selectionModel);
void retranslateUi();
+ void updateCompactMode();
void clearAllDisplayWidgets();
void setDeckList(const DeckList &_deckListModel);
@@ -82,19 +84,23 @@ protected slots:
void onHover(const ExactCard &hoveredCard);
void onCardClick(QMouseEvent *event, CardInfoPictureWithTextOverlayWidget *instance, QString zoneName);
void decklistModelReset();
+ void resizeEvent(QResizeEvent *event) override;
private:
+ int expandedWidthAll = -1;
+ int expandedWidthDisplayCompact = -1;
DeckListModel *deckListModel;
QItemSelectionModel *selectionModel;
QVBoxLayout *mainLayout;
CardDatabaseModel *cardDatabaseModel;
CardDatabaseDisplayModel *cardDatabaseDisplayModel;
CardCompleterProxyModel *proxyModel;
+ QWidget *searchContainer;
+ QHBoxLayout *searchLayout;
QCompleter *completer;
- QWidget *displayOptionsAndSearch;
- QHBoxLayout *displayOptionsAndSearchLayout;
+ FlowWidget *displayOptionsAndSearch;
VisualDeckDisplayOptionsWidget *displayOptionsWidget;
- QPushButton *searchPushButton;
+ CompactPushButton *searchPushButton;
QScrollArea *scrollArea;
QWidget *zoneContainer;
QVBoxLayout *zoneContainerLayout;
From aadee342381ad4a7aea84ea1217e872956cd95ac Mon Sep 17 00:00:00 2001
From: DawnFire42
Date: Sat, 16 May 2026 13:19:53 -0400
Subject: [PATCH 023/121] style: Add braces to all control flow statements
(#6887)
* style: Add braces to all control flow statements
Standardize code style by adding explicit braces to all single-statement
control flow blocks (if, else, for, while) across the entire codebase.
Also documents the InsertBraces clang-format option (requires v15+) for
future automated enforcement.
* InsertBraces-check-enabled
---
.clang-format | 2 +
.../remote_connection_controller.cpp | 20 +-
.../interfaces/tapped_out_interface.cpp | 8 +-
.../network/update/client/release_channel.cpp | 6 +-
.../update/client/update_downloader.cpp | 6 +-
.../src/client/settings/cache_settings.cpp | 51 ++-
.../client/settings/card_counter_settings.cpp | 3 +-
cockatrice/src/client/sound_engine.cpp | 6 +-
cockatrice/src/filters/deck_filter_string.cpp | 21 +-
cockatrice/src/filters/filter_builder.cpp | 12 +-
cockatrice/src/filters/filter_tree_model.cpp | 96 +++--
cockatrice/src/game/abstract_game.cpp | 11 +-
.../game/board/abstract_card_drag_item.cpp | 3 +-
.../src/game/board/abstract_card_item.cpp | 37 +-
.../src/game/board/abstract_counter.cpp | 22 +-
cockatrice/src/game/board/arrow_item.cpp | 41 +-
cockatrice/src/game/board/arrow_target.cpp | 6 +-
cockatrice/src/game/board/card_drag_item.cpp | 31 +-
cockatrice/src/game/board/card_item.cpp | 69 ++--
cockatrice/src/game/deckview/deck_view.cpp | 82 ++--
.../src/game/deckview/deck_view_container.cpp | 9 +-
.../src/game/dialogs/dlg_create_token.cpp | 6 +-
cockatrice/src/game/game_event_handler.cpp | 48 ++-
cockatrice/src/game/game_meta_info.h | 6 +-
cockatrice/src/game/game_scene.cpp | 74 ++--
cockatrice/src/game/game_view.cpp | 12 +-
cockatrice/src/game/phases_toolbar.cpp | 30 +-
.../src/game/player/menu/grave_menu.cpp | 3 +-
cockatrice/src/game/player/menu/hand_menu.cpp | 12 +-
.../src/game/player/menu/library_menu.cpp | 9 +-
cockatrice/src/game/player/player.cpp | 3 +-
cockatrice/src/game/player/player_actions.cpp | 3 +-
.../src/game/player/player_list_widget.cpp | 43 +-
cockatrice/src/game/player/player_manager.cpp | 12 +-
cockatrice/src/game/player/player_target.cpp | 6 +-
cockatrice/src/game/zones/card_zone.cpp | 14 +-
cockatrice/src/game/zones/hand_zone.cpp | 18 +-
.../src/game/zones/logic/card_zone_logic.cpp | 27 +-
.../src/game/zones/logic/pile_zone_logic.cpp | 3 +-
.../src/game/zones/logic/table_zone_logic.cpp | 3 +-
cockatrice/src/game/zones/pile_zone.cpp | 18 +-
cockatrice/src/game/zones/select_zone.cpp | 30 +-
cockatrice/src/game/zones/table_zone.cpp | 56 ++-
cockatrice/src/game/zones/view_zone.cpp | 7 +-
.../src/game/zones/view_zone_widget.cpp | 21 +-
.../board/abstract_graphics_item.cpp | 12 +-
.../card_picture_loader.cpp | 3 +-
.../card_picture_to_load.cpp | 3 +-
cockatrice/src/interface/key_signals.cpp | 34 +-
cockatrice/src/interface/logger.cpp | 3 +-
.../src/interface/pixel_map_generator.cpp | 32 +-
cockatrice/src/interface/theme_manager.cpp | 6 +-
.../card_group_display_widget.cpp | 3 +-
.../cards/card_info_display_widget.cpp | 6 +-
.../widgets/cards/card_info_text_widget.cpp | 3 +-
.../analytics_panel_widget_factory.cpp | 3 +-
.../mana_base/mana_base_config_dialog.cpp | 3 +-
.../mana_curve/mana_curve_config_dialog.cpp | 6 +-
.../mana_curve/mana_curve_total_widget.cpp | 6 +-
.../mana_curve/mana_curve_widget.cpp | 9 +-
.../mana_devotion_config_dialog.cpp | 3 +-
.../mana_distribution_config.cpp | 3 +-
.../mana_distribution_config_dialog.cpp | 6 +-
.../deck_list_statistics_analyzer.cpp | 6 +-
.../deck_editor_database_display_widget.cpp | 3 +-
.../deck_editor_deck_dock_widget.cpp | 9 +-
.../deck_editor_filter_dock_widget.cpp | 6 +-
.../deck_editor/deck_list_style_proxy.cpp | 3 +-
.../deck_editor/deck_state_manager.cpp | 9 +-
.../interface/widgets/dialogs/dlg_connect.cpp | 8 +-
.../widgets/dialogs/dlg_create_game.cpp | 7 +-
.../dialogs/dlg_default_tags_editor.cpp | 6 +-
.../widgets/dialogs/dlg_edit_tokens.cpp | 12 +-
.../widgets/dialogs/dlg_edit_user.cpp | 3 +-
.../widgets/dialogs/dlg_filter_games.cpp | 6 +-
.../widgets/dialogs/dlg_manage_sets.cpp | 21 +-
.../widgets/dialogs/dlg_register.cpp | 3 +-
.../dialogs/dlg_select_set_for_cards.cpp | 6 +-
.../widgets/dialogs/dlg_settings.cpp | 45 ++-
.../interface/widgets/dialogs/dlg_update.cpp | 3 +-
.../widgets/dialogs/dlg_view_log.cpp | 3 +-
.../widgets/dialogs/tip_of_the_day.cpp | 3 +-
.../widgets/general/background_sources.h | 9 +-
.../display/charts/bars/bar_chart_widget.cpp | 9 +-
.../general/display/charts/bars/color_bar.cpp | 30 +-
.../charts/bars/segmented_bar_widget.cpp | 6 +-
.../general/display/charts/pies/color_pie.cpp | 3 +-
.../interface/widgets/general/home_widget.cpp | 3 +-
.../printing_selector_card_overlay_widget.cpp | 3 +-
.../widgets/replay/replay_manager.cpp | 9 +-
.../widgets/replay/replay_timeline_widget.cpp | 21 +-
.../widgets/server/chat_view/chat_view.cpp | 57 ++-
.../widgets/server/game_selector.cpp | 27 +-
.../game_selector_quick_filter_toolbar.cpp | 3 +-
.../interface/widgets/server/games_model.cpp | 81 ++--
.../widgets/server/handle_public_servers.cpp | 6 +-
.../remote/remote_decklist_tree_widget.cpp | 60 ++-
.../remote/remote_decklist_tree_widget.h | 3 +-
.../remote/remote_replay_list_tree_widget.cpp | 72 ++--
.../widgets/server/user/user_context_menu.cpp | 15 +-
.../widgets/server/user/user_info_box.cpp | 30 +-
.../server/user/user_info_connection.cpp | 6 +-
.../widgets/server/user/user_list_widget.cpp | 33 +-
.../widgets/tabs/abstract_tab_deck_editor.cpp | 62 ++-
.../api_response/archidekt_formats.h | 72 ++--
...api_response_deck_entry_display_widget.cpp | 18 +-
.../tabs/api/archidekt/tab_archidekt.cpp | 18 +-
.../widgets/tabs/tab_deck_editor.cpp | 7 +-
.../widgets/tabs/tab_deck_storage.cpp | 57 ++-
.../src/interface/widgets/tabs/tab_game.cpp | 76 ++--
.../src/interface/widgets/tabs/tab_logs.cpp | 12 +-
.../interface/widgets/tabs/tab_message.cpp | 18 +-
.../interface/widgets/tabs/tab_replays.cpp | 21 +-
.../src/interface/widgets/tabs/tab_room.cpp | 27 +-
.../src/interface/widgets/tabs/tab_server.cpp | 42 +-
.../interface/widgets/tabs/tab_supervisor.cpp | 112 +++--
.../tab_deck_editor_visual.cpp | 3 +-
.../widgets/utility/custom_line_edit.cpp | 21 +-
.../widgets/utility/line_edit_completer.cpp | 9 +-
...tabase_display_filter_save_load_widget.cpp | 12 +-
...al_database_display_name_filter_widget.cpp | 9 +-
.../visual_database_display_widget.cpp | 3 +-
.../visual_deck_editor_sample_hand_widget.cpp | 6 +-
.../visual_deck_editor_widget.cpp | 3 +-
.../visual_deck_storage_tag_filter_widget.cpp | 3 +-
cockatrice/src/interface/window_main.cpp | 27 +-
cockatrice/src/main.cpp | 6 +-
.../card/database/card_database.cpp | 15 +-
.../card/database/card_database_loader.cpp | 3 +-
.../card/database/card_database_querier.cpp | 15 +-
.../card/database/parser/cockatrice_xml_4.cpp | 3 +-
.../card/format/format_legality_rules.h | 15 +-
.../libcockatrice/card/game_specific_terms.h | 30 +-
.../libcockatrice/card/set/card_set.cpp | 25 +-
.../libcockatrice/deck_list/deck_list.cpp | 12 +-
.../deck_list/deck_list_history_manager.cpp | 6 +-
.../deck_list/deck_list_node_tree.cpp | 3 +-
.../deck_list/sideboard_plan.cpp | 14 +-
.../tree/abstract_deck_list_card_node.cpp | 3 +-
.../deck_list/tree/inner_deck_list_node.cpp | 9 +-
.../client/abstract/abstract_client.cpp | 3 +-
.../network/client/remote/remote_client.cpp | 45 ++-
.../remote/game/server_abstract_player.cpp | 6 +-
.../server/remote/game/server_arrow.cpp | 3 +-
.../server/remote/game/server_card.cpp | 29 +-
.../server/remote/game/server_cardzone.cpp | 97 +++--
.../server/remote/game/server_game.cpp | 76 ++--
.../network/server/remote/server.cpp | 92 +++--
.../remote/server_abstractuserinterface.cpp | 18 +-
.../server/remote/server_protocolhandler.cpp | 158 +++++---
.../remote/server_response_containers.cpp | 21 +-
.../network/server/remote/server_room.cpp | 56 ++-
.../remote/serverinfo_user_container.cpp | 8 +-
.../protocol/debug_pb_message.cpp | 3 +-
.../libcockatrice/protocol/featureset.cpp | 6 +-
.../protocol/get_pb_extension.cpp | 6 +-
.../libcockatrice/rng/rng_abstract.cpp | 11 +-
.../libcockatrice/rng/rng_sfmt.cpp | 3 +-
.../settings/servers_settings.cpp | 12 +-
.../libcockatrice/utility/color.h | 6 +-
.../libcockatrice/utility/expression.cpp | 3 +-
.../libcockatrice/utility/levenshtein.cpp | 6 +-
.../libcockatrice/utility/qt_utils.h | 9 +-
oracle/src/oracleimporter.cpp | 21 +-
servatrice/src/isl_interface.cpp | 25 +-
servatrice/src/main.cpp | 19 +-
servatrice/src/servatrice.cpp | 86 ++--
.../src/servatrice_database_interface.cpp | 338 ++++++++++------
servatrice/src/server_logger.cpp | 24 +-
servatrice/src/serversocketinterface.cpp | 381 ++++++++++++------
servatrice/src/settingscache.cpp | 6 +-
servatrice/src/signalhandler.cpp | 5 +-
servatrice/src/smtpclient.cpp | 9 +-
173 files changed, 2725 insertions(+), 1461 deletions(-)
diff --git a/.clang-format b/.clang-format
index 1ef3ca6db..4be096155 100644
--- a/.clang-format
+++ b/.clang-format
@@ -26,6 +26,8 @@ PointerAlignment: Right
SortIncludes: true
IncludeBlocks: Regroup
StatementAttributeLikeMacros: [emit]
+# requires clang-format 15
+InsertBraces: true
# requires clang-format 16
# RemoveSemicolon: true
---
diff --git a/cockatrice/src/client/network/connection_controller/remote_connection_controller.cpp b/cockatrice/src/client/network/connection_controller/remote_connection_controller.cpp
index f517acdc4..75cedcafc 100644
--- a/cockatrice/src/client/network/connection_controller/remote_connection_controller.cpp
+++ b/cockatrice/src/client/network/connection_controller/remote_connection_controller.cpp
@@ -140,13 +140,15 @@ void ConnectionController::onConnectionClosedEvent(const Event_ConnectionClosed
}
case Event_ConnectionClosed::BANNED: {
reasonStr = tr("Banned by moderator");
- if (event.has_end_time())
+ if (event.has_end_time()) {
reasonStr.append(
"\n" + tr("Expected end time: %1").arg(QDateTime::fromSecsSinceEpoch(event.end_time()).toString()));
- else
+ } else {
reasonStr.append("\n" + tr("This ban lasts indefinitely."));
- if (event.has_reason_str())
+ }
+ if (event.has_reason_str()) {
reasonStr.append("\n\n" + QString::fromStdString(event.reason_str()));
+ }
break;
}
case Event_ConnectionClosed::SERVER_SHUTDOWN: {
@@ -240,8 +242,9 @@ void ConnectionController::onLoginError(int r,
QString bannedStr =
endTime ? tr("You are banned until %1.").arg(QDateTime::fromSecsSinceEpoch(endTime).toString())
: tr("You are banned indefinitely.");
- if (!reasonStr.isEmpty())
+ if (!reasonStr.isEmpty()) {
bannedStr.append("\n\n" + reasonStr);
+ }
QMessageBox::critical(dialogParent, tr("Error"), bannedStr);
break;
}
@@ -354,8 +357,9 @@ void ConnectionController::onRegisterError(int r, QString reasonStr, quint32 end
QString bannedStr =
endTime ? tr("You are banned until %1.").arg(QDateTime::fromSecsSinceEpoch(endTime).toString())
: tr("You are banned indefinitely.");
- if (!reasonStr.isEmpty())
+ if (!reasonStr.isEmpty()) {
bannedStr.append("\n\n" + reasonStr);
+ }
QMessageBox::critical(dialogParent, tr("Error"), bannedStr);
break;
}
@@ -545,8 +549,9 @@ QString ConnectionController::extractInvalidUsernameMessage(QString &in)
out +=
"" + tr("can %1 contain numeric characters").arg((rules.at(4).toInt() > 0) ? "" : tr("NOT")) + "";
- if (rules.at(6).size() > 0)
+ if (rules.at(6).size() > 0) {
out += "" + tr("can contain the following punctuation: %1").arg(rules.at(6).toHtmlEscaped()) + "";
+ }
out += "" +
tr("first character can %1 be a punctuation mark").arg((rules.at(5).toInt() > 0) ? "" : tr("NOT")) +
@@ -566,10 +571,11 @@ QString ConnectionController::extractInvalidUsernameMessage(QString &in)
}
}
- if (rules.at(8).size() > 0)
+ if (rules.at(8).size() > 0) {
out += "" +
tr("can not match any of the following expressions: %1").arg(rules.at(8).toHtmlEscaped()) +
"";
+ }
}
out += "";
diff --git a/cockatrice/src/client/network/interfaces/tapped_out_interface.cpp b/cockatrice/src/client/network/interfaces/tapped_out_interface.cpp
index af377d176..cd39ea251 100644
--- a/cockatrice/src/client/network/interfaces/tapped_out_interface.cpp
+++ b/cockatrice/src/client/network/interfaces/tapped_out_interface.cpp
@@ -99,14 +99,16 @@ void TappedOutInterface::copyDeckSplitMainAndSide(const DeckList &source, DeckLi
{
auto copyMainOrSide = [this, &mainboard, &sideboard](const auto node, const auto card) {
CardInfoPtr dbCard = cardDatabase.query()->getCardInfo(card->getName());
- if (!dbCard || dbCard->getIsToken())
+ if (!dbCard || dbCard->getIsToken()) {
return;
+ }
DecklistCardNode *addedCard;
- if (node->getName() == DECK_ZONE_SIDE)
+ if (node->getName() == DECK_ZONE_SIDE) {
addedCard = sideboard.addCard(card->getName(), node->getName(), -1);
- else
+ } else {
addedCard = mainboard.addCard(card->getName(), node->getName(), -1);
+ }
addedCard->setNumber(card->getNumber());
};
diff --git a/cockatrice/src/client/network/update/client/release_channel.cpp b/cockatrice/src/client/network/update/client/release_channel.cpp
index 9edfe7caf..260167bc8 100644
--- a/cockatrice/src/client/network/update/client/release_channel.cpp
+++ b/cockatrice/src/client/network/update/client/release_channel.cpp
@@ -129,8 +129,9 @@ void StableReleaseChannel::releaseListFinished()
return;
}
- if (!lastRelease)
+ if (!lastRelease) {
lastRelease = new Release;
+ }
lastRelease->setName(resultMap["name"].toString());
lastRelease->setDescriptionUrl(resultMap["html_url"].toString());
@@ -246,8 +247,9 @@ void BetaReleaseChannel::releaseListFinished()
return;
}
- if (lastRelease == nullptr)
+ if (lastRelease == nullptr) {
lastRelease = new Release;
+ }
lastRelease->setCommitHash(resultMap["target_commitish"].toString());
lastRelease->setPublishDate(resultMap["published_at"].toDate());
diff --git a/cockatrice/src/client/network/update/client/update_downloader.cpp b/cockatrice/src/client/network/update/client/update_downloader.cpp
index c0f3e945c..a71bcf8f8 100644
--- a/cockatrice/src/client/network/update/client/update_downloader.cpp
+++ b/cockatrice/src/client/network/update/client/update_downloader.cpp
@@ -10,8 +10,9 @@ UpdateDownloader::UpdateDownloader(QObject *parent) : QObject(parent), response(
void UpdateDownloader::beginDownload(QUrl downloadUrl)
{
// Save the original URL because we need it for the filename
- if (originalUrl.isEmpty())
+ if (originalUrl.isEmpty()) {
originalUrl = downloadUrl;
+ }
response = netMan->get(QNetworkRequest(downloadUrl));
connect(response, &QNetworkReply::finished, this, &UpdateDownloader::fileFinished);
@@ -21,8 +22,9 @@ void UpdateDownloader::beginDownload(QUrl downloadUrl)
void UpdateDownloader::downloadError(QNetworkReply::NetworkError)
{
- if (response == nullptr)
+ if (response == nullptr) {
return;
+ }
emit error(response->errorString().toUtf8());
}
diff --git a/cockatrice/src/client/settings/cache_settings.cpp b/cockatrice/src/client/settings/cache_settings.cpp
index 9a46c6426..779522b72 100644
--- a/cockatrice/src/client/settings/cache_settings.cpp
+++ b/cockatrice/src/client/settings/cache_settings.cpp
@@ -24,10 +24,11 @@ SettingsCache &SettingsCache::instance()
QString SettingsCache::getDataPath()
{
- if (isPortableBuild)
+ if (isPortableBuild) {
return qApp->applicationDirPath() + "/data";
- else
+ } else {
return QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation);
+ }
}
QString SettingsCache::getSettingsPath()
@@ -37,10 +38,11 @@ QString SettingsCache::getSettingsPath()
QString SettingsCache::getCachePath() const
{
- if (isPortableBuild)
+ if (isPortableBuild) {
return qApp->applicationDirPath() + "/cache";
- else
+ } else {
return QStandardPaths::writableLocation(QStandardPaths::CacheLocation);
+ }
}
QString SettingsCache::getNetworkCachePath() const
@@ -50,14 +52,17 @@ QString SettingsCache::getNetworkCachePath() const
void SettingsCache::translateLegacySettings()
{
- if (isPortableBuild)
+ if (isPortableBuild) {
return;
+ }
// Layouts
QFile layoutFile(getSettingsPath() + "layouts/deckLayout.ini");
- if (layoutFile.exists())
- if (layoutFile.copy(getSettingsPath() + "layouts.ini"))
+ if (layoutFile.exists()) {
+ if (layoutFile.copy(getSettingsPath() + "layouts.ini")) {
layoutFile.remove();
+ }
+ }
QStringList usedKeys;
QSettings legacySetting;
@@ -116,10 +121,11 @@ void SettingsCache::translateLegacySettings()
gameFilters().setHideIgnoredUserGames(legacySetting.value("hide_ignored_user_games").toBool());
gameFilters().setMinPlayers(legacySetting.value("min_players").toInt());
- if (legacySetting.value("max_players").toInt() > 1)
+ if (legacySetting.value("max_players").toInt() > 1) {
gameFilters().setMaxPlayers(legacySetting.value("max_players").toInt());
- else
+ } else {
gameFilters().setMaxPlayers(99); // This prevents a bug where no games will show if max was not set before
+ }
QStringList allFilters = legacySetting.allKeys();
for (int i = 0; i < allFilters.size(); ++i) {
@@ -135,8 +141,9 @@ void SettingsCache::translateLegacySettings()
QStringList allLegacyKeys = legacySetting.allKeys();
for (int i = 0; i < allLegacyKeys.size(); ++i) {
- if (usedKeys.contains(allLegacyKeys.at(i)))
+ if (usedKeys.contains(allLegacyKeys.at(i))) {
continue;
+ }
settings->setValue(allLegacyKeys.at(i), legacySetting.value(allLegacyKeys.at(i)));
}
}
@@ -147,8 +154,9 @@ QString SettingsCache::getSafeConfigPath(QString configEntry, QString defaultPat
// if the config settings is empty or refers to a not-existing folder,
// ensure that the defaut path exists and return it
if (tmp.isEmpty() || !QDir(tmp).exists()) {
- if (!QDir().mkpath(defaultPath))
+ if (!QDir().mkpath(defaultPath)) {
qCInfo(SettingsCacheLog) << "[SettingsCache] Could not create folder:" << defaultPath;
+ }
tmp = defaultPath;
}
return tmp;
@@ -159,8 +167,9 @@ QString SettingsCache::getSafeConfigFilePath(QString configEntry, QString defaul
QString tmp = settings->value(configEntry).toString();
// if the config settings is empty or refers to a not-existing file,
// return the default Path
- if (!QFile::exists(tmp) || tmp.isEmpty())
+ if (!QFile::exists(tmp) || tmp.isEmpty()) {
tmp = std::move(defaultPath);
+ }
return tmp;
}
@@ -168,8 +177,9 @@ SettingsCache::SettingsCache()
{
// first, figure out if we are running in portable mode
isPortableBuild = QFile::exists(qApp->applicationDirPath() + "/portable.dat");
- if (isPortableBuild)
+ if (isPortableBuild) {
qCInfo(SettingsCacheLog) << "Portable mode enabled";
+ }
// define a dummy context that will be used where needed
QString dummy = QT_TRANSLATE_NOOP("i18n", "English");
@@ -189,8 +199,9 @@ SettingsCache::SettingsCache()
cardCounterSettings = new CardCounterSettings(settingsPath, this);
- if (!QFile(settingsPath + "global.ini").exists())
+ if (!QFile(settingsPath + "global.ini").exists()) {
translateLegacySettings();
+ }
// updates - don't reorder them or their index in the settings won't match
// append channels one by one, or msvc will add them in the wrong order.
@@ -257,11 +268,13 @@ SettingsCache::SettingsCache()
settings->setValue("personal/pixmapCacheSize", pixmapCacheSize);
settings->setValue("personal/picturedownloadhq", false);
settings->setValue("revert/pixmapCacheSize", true);
- } else
+ } else {
pixmapCacheSize = settings->value("personal/pixmapCacheSize", PIXMAPCACHE_SIZE_DEFAULT).toInt();
+ }
// sanity check
- if (pixmapCacheSize < PIXMAPCACHE_SIZE_MIN || pixmapCacheSize > PIXMAPCACHE_SIZE_MAX)
+ if (pixmapCacheSize < PIXMAPCACHE_SIZE_MIN || pixmapCacheSize > PIXMAPCACHE_SIZE_MAX) {
pixmapCacheSize = PIXMAPCACHE_SIZE_DEFAULT;
+ }
networkCacheSize = settings->value("personal/networkCacheSize", NETWORK_CACHE_SIZE_DEFAULT).toInt();
redirectCacheTtl = settings->value("personal/redirectCacheTtl", NETWORK_REDIRECT_CACHE_TTL_DEFAULT).toInt();
@@ -770,8 +783,9 @@ void SettingsCache::setPrintingSelectorCardSize(int _printingSelectorCardSize)
void SettingsCache::setIncludeRebalancedCards(bool _includeRebalancedCards)
{
- if (includeRebalancedCards == _includeRebalancedCards)
+ if (includeRebalancedCards == _includeRebalancedCards) {
return;
+ }
includeRebalancedCards = _includeRebalancedCards;
settings->setValue("cards/includerebalancedcards", includeRebalancedCards);
@@ -1310,8 +1324,9 @@ void SettingsCache::setMaxFontSize(int _max)
void SettingsCache::setRoundCardCorners(bool _roundCardCorners)
{
- if (_roundCardCorners == roundCardCorners)
+ if (_roundCardCorners == roundCardCorners) {
return;
+ }
roundCardCorners = _roundCardCorners;
settings->setValue("cards/roundcardcorners", _roundCardCorners);
diff --git a/cockatrice/src/client/settings/card_counter_settings.cpp b/cockatrice/src/client/settings/card_counter_settings.cpp
index 399365c99..662ae0c7d 100644
--- a/cockatrice/src/client/settings/card_counter_settings.cpp
+++ b/cockatrice/src/client/settings/card_counter_settings.cpp
@@ -15,8 +15,9 @@ void CardCounterSettings::setColor(int counterId, const QColor &color)
QString key = QString("cards/counters/%1/color").arg(counterId);
- if (settings.value(key).value() == color)
+ if (settings.value(key).value