diff --git a/.github/workflows/desktop-build.yml b/.github/workflows/desktop-build.yml
index b631f32d3..6ca634389 100644
--- a/.github/workflows/desktop-build.yml
+++ b/.github/workflows/desktop-build.yml
@@ -152,7 +152,7 @@ jobs:
env:
CACHE: ${{ github.workspace }}/.cache/${{ matrix.distro }}${{ matrix.version }} # directory for caching docker image and ccache
CCACHE_EVICTION_AGE: 7d
- CCACHE_SIZE: 550M # space of all repo is 10Gi: https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#usage-limits-and-eviction-policy
+ CCACHE_SIZE: 600M # space of all repo is 10Gi: https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#usage-limits-and-eviction-policy
CMAKE_GENERATOR: 'Ninja'
NAME: ${{ matrix.distro }}${{ matrix.version }}
@@ -342,7 +342,7 @@ jobs:
timeout-minutes: 100
env:
CCACHE_DIR: ${{ github.workspace }}/.cache/
- CCACHE_SIZE: 550M # space of all repo is 10Gi: https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#usage-limits-and-eviction-policy
+ CCACHE_SIZE: 600M # space of all repo is 10Gi: https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#usage-limits-and-eviction-policy
steps:
- name: "Checkout"
diff --git a/README.md b/README.md
index f22df461f..5935bb540 100644
--- a/README.md
+++ b/README.md
@@ -149,15 +149,15 @@ You can then
The following flags (with their non-default values) can be passed to `cmake`:
-| Flag | Description |
-| --- | --- |
-| `-DWITH_SERVER=1` | Build Servatrice server |
-| `-DWITH_CLIENT=0` | Don't build Cockatrice client |
-| `-DWITH_ORACLE=0` | Don't build Oracle card database tool |
-| `-DCMAKE_BUILD_TYPE=Debug` | Compile in debug mode
Enables extra logging output, debug symbols, and much more verbose compiler warnings |
-| `-DWARNING_AS_ERROR=0` | Don't treat compilation warnings as errors in debug mode |
-| `-DUPDATE_TRANSLATIONS=1` | Configure `make` to update the translation .ts files for new strings in the source code
**Note:** `make clean` will remove the .ts files |
-| `-DTEST=1` | Enable regression tests
**Note:** `make test` to run tests, *googletest* will be downloaded if not available |
+| Flag | Description |
+| -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
+| `-DWITH_SERVER=1` | Build Servatrice server |
+| `-DWITH_CLIENT=0` | Don't build Cockatrice client |
+| `-DWITH_ORACLE=0` | Don't build Oracle card database tool |
+| `-DCMAKE_BUILD_TYPE=Debug` | Compile in debug mode
Enables extra logging output, debug symbols, and much more verbose compiler warnings |
+| `-DWARNING_AS_ERROR=0` | Don't treat compilation warnings as errors in debug mode |
+| `-DUPDATE_TRANSLATIONS=1` | Configure `make` to update the translation .ts files for new strings in the source code
**Note:** `make clean` will remove the .ts files |
+| `-DTEST=1` | Enable regression tests
**Note:** `make test` to run tests, *googletest* will be downloaded if not available |
# Run
diff --git a/cmake/FindQtRuntime.cmake b/cmake/FindQtRuntime.cmake
index 0259d12e1..971c9094d 100644
--- a/cmake/FindQtRuntime.cmake
+++ b/cmake/FindQtRuntime.cmake
@@ -28,7 +28,7 @@ if(WITH_CLIENT)
)
endif()
if(WITH_ORACLE)
- set(_ORACLE_NEEDED Concurrent Network Svg Widgets)
+ set(_ORACLE_NEEDED Concurrent Network Svg Widgets Xml)
endif()
if(TEST)
# Union of Qt modules required across all test targets (independent of application targets).
diff --git a/cockatrice/CMakeLists.txt b/cockatrice/CMakeLists.txt
index b6050a1bd..595dd9af6 100644
--- a/cockatrice/CMakeLists.txt
+++ b/cockatrice/CMakeLists.txt
@@ -50,6 +50,7 @@ set(cockatrice_SOURCES
src/interface/widgets/dialogs/dlg_register.cpp
src/interface/widgets/dialogs/dlg_report_user.cpp
src/interface/widgets/dialogs/dlg_select_set_for_cards.cpp
+ src/interface/widgets/dialogs/dlg_share_deck.cpp
src/interface/widgets/dialogs/dlg_settings.cpp
src/interface/widgets/dialogs/dlg_startup_card_check.cpp
src/interface/widgets/dialogs/dlg_tip_of_the_day.cpp
@@ -57,6 +58,8 @@ set(cockatrice_SOURCES
src/interface/widgets/dialogs/dlg_view_log.cpp
src/interface/widgets/dialogs/override_printing_warning.cpp
src/interface/widgets/dialogs/tip_of_the_day.cpp
+ src/interface/widgets/deck_share/deck_share_utils.cpp
+ src/interface/widgets/deck_share/share_bar_widget.cpp
src/filters/deck_filter_string.cpp
src/filters/filter_builder.cpp
src/filters/filter_tree_model.cpp
@@ -163,9 +166,11 @@ set(cockatrice_SOURCES
src/interface/palette_editor/palette_grid_widget.cpp
src/interface/palette_editor/palette_editor_dialog.cpp
src/interface/widgets/cards/additional_info/color_identity_widget.cpp
+ src/interface/widgets/cards/additional_info/deck_color_identity.cpp
src/interface/widgets/cards/additional_info/mana_cost_widget.cpp
src/interface/widgets/cards/additional_info/mana_symbol_widget.cpp
src/interface/widgets/cards/art_crop_attribution.cpp
+ src/interface/widgets/cards/card_art_utils.cpp
src/interface/widgets/cards/card_group_display_widgets/card_group_display_widget.cpp
src/interface/widgets/cards/card_group_display_widgets/flat_card_group_display_widget.cpp
src/interface/widgets/cards/card_group_display_widgets/overlapped_card_group_display_widget.cpp
@@ -381,6 +386,7 @@ set(cockatrice_SOURCES
src/interface/widgets/tabs/tab_card_art_rules.cpp
src/interface/widgets/tabs/tab_deck_editor.cpp
src/interface/widgets/tabs/tab_deck_storage.cpp
+ src/interface/widgets/tabs/tab_developer.cpp
src/interface/widgets/tabs/tab_game.cpp
src/interface/widgets/tabs/tab_home.cpp
src/interface/widgets/tabs/tab_logs.cpp
@@ -401,6 +407,7 @@ set(cockatrice_SOURCES
src/interface/widgets/tabs/api/commander_spellbook/commander_bracket_widget.cpp
src/interface/widgets/tabs/api/commander_spellbook/handle_commander_brackets.cpp
src/interface/widgets/onboarding/banner_shader_config.h
+ src/interface/widgets/onboarding/brand_colors.h
src/interface/widgets/onboarding/first_run_wizard.cpp
src/interface/widgets/onboarding/first_run_wizard.h
src/interface/widgets/onboarding/first_run_wizard_page.cpp
@@ -528,6 +535,7 @@ qt6_add_shaders(
"src/interface/widgets/onboarding/shaders"
FILES
src/interface/widgets/onboarding/shaders/brand_banner.frag
+ src/interface/widgets/onboarding/shaders/brand_plate.frag
)
qt6_add_resources(
diff --git a/cockatrice/cockatrice.qrc b/cockatrice/cockatrice.qrc
index e21bdb0be..14cf15b2f 100644
--- a/cockatrice/cockatrice.qrc
+++ b/cockatrice/cockatrice.qrc
@@ -63,6 +63,8 @@
resources/icons/mana/W.svg
resources/backgrounds/home.png
+ resources/backgrounds/home-dark.png
+ resources/backgrounds/home-light.png
resources/backgrounds/card_triplet.svg
resources/backgrounds/placeholder_printing_selector.svg
@@ -363,6 +365,8 @@
resources/usericons/pawn_single.svg
resources/usericons/pawn_double.svg
+ resources/usericons/pawn_dev_single.svg
+ resources/usericons/pawn_dev_double.svg
resources/usericons/pawn_donator_single.svg
resources/usericons/pawn_donator_double.svg
resources/usericons/pawn_judge_single.svg
diff --git a/cockatrice/resources/backgrounds/home-dark.png b/cockatrice/resources/backgrounds/home-dark.png
new file mode 100644
index 000000000..68f48e2c2
Binary files /dev/null and b/cockatrice/resources/backgrounds/home-dark.png differ
diff --git a/cockatrice/resources/backgrounds/home-light.png b/cockatrice/resources/backgrounds/home-light.png
new file mode 100644
index 000000000..eaaaba932
Binary files /dev/null and b/cockatrice/resources/backgrounds/home-light.png differ
diff --git a/cockatrice/resources/backgrounds/home.png b/cockatrice/resources/backgrounds/home.png
index 68f48e2c2..eaaaba932 100644
Binary files a/cockatrice/resources/backgrounds/home.png and b/cockatrice/resources/backgrounds/home.png differ
diff --git a/cockatrice/resources/usericons/pawn_dev_double.svg b/cockatrice/resources/usericons/pawn_dev_double.svg
new file mode 100644
index 000000000..57ed5c2da
--- /dev/null
+++ b/cockatrice/resources/usericons/pawn_dev_double.svg
@@ -0,0 +1,343 @@
+
+
+
+
diff --git a/cockatrice/resources/usericons/pawn_dev_single.svg b/cockatrice/resources/usericons/pawn_dev_single.svg
new file mode 100644
index 000000000..f7c4e7018
--- /dev/null
+++ b/cockatrice/resources/usericons/pawn_dev_single.svg
@@ -0,0 +1,211 @@
+
+
+
+
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 890a621c8..c1598bd25 100644
--- a/cockatrice/src/client/network/connection_controller/remote_connection_controller.cpp
+++ b/cockatrice/src/client/network/connection_controller/remote_connection_controller.cpp
@@ -1,5 +1,6 @@
#include "remote_connection_controller.h"
+#include "../../../interface/pixel_map_generator.h"
#include "../../settings/cache_settings.h"
#include "../interface/widgets/dialogs/dlg_connect.h"
#include "../interface/widgets/dialogs/dlg_forgot_password_challenge.h"
@@ -180,7 +181,7 @@ void ConnectionController::onServerShutdownEvent(const Event_ServerShutdown &eve
"games will be lost.\nReason for shutdown: %1",
"", event.minutes())
.arg(QString::fromStdString(event.reason())));
- serverShutdownMessageBox.setIconPixmap(QPixmap("theme:cockatrice").scaled(64, 64));
+ serverShutdownMessageBox.setIconPixmap(themePixmap(QStringLiteral("cockatrice")).scaled(64, 64));
serverShutdownMessageBox.setText(tr("Scheduled server shutdown"));
serverShutdownMessageBox.setWindowModality(Qt::ApplicationModal);
serverShutdownMessageBox.setVisible(true);
diff --git a/cockatrice/src/client/sound_engine.cpp b/cockatrice/src/client/sound_engine.cpp
index 18de2264d..96cafa3d3 100644
--- a/cockatrice/src/client/sound_engine.cpp
+++ b/cockatrice/src/client/sound_engine.cpp
@@ -94,7 +94,7 @@ QStringMap &SoundEngine::getAvailableThemes()
QDir dir;
availableThemes.clear();
- // load themes from user profile dir
+ // Load themes from user profile dir
dir.setPath(SettingsCache::instance().getDataPath() + "/sounds");
@@ -104,7 +104,7 @@ QStringMap &SoundEngine::getAvailableThemes()
}
}
- // load themes from cockatrice system dir
+ // Load themes from Cockatrice system dir
dir.setPath(qApp->applicationDirPath() +
#ifdef Q_OS_MAC
"/../Resources/sounds"
diff --git a/cockatrice/src/filters/filter_builder.cpp b/cockatrice/src/filters/filter_builder.cpp
index 785f753e7..f109fbcd3 100644
--- a/cockatrice/src/filters/filter_builder.cpp
+++ b/cockatrice/src/filters/filter_builder.cpp
@@ -1,5 +1,6 @@
#include "filter_builder.h"
+#include "../interface/pixel_map_generator.h"
#include "../interface/widgets/utility/custom_line_edit.h"
#include
@@ -21,7 +22,7 @@ FilterBuilder::FilterBuilder(QWidget *parent) : QWidget(parent)
typeCombo->addItem(CardFilter::typeName(static_cast(i)), QVariant(i));
}
- QPushButton *ok = new QPushButton(QPixmap("theme:icons/increment"), QString());
+ QPushButton *ok = new QPushButton(themePixmap(QStringLiteral("icons/increment")), QString());
ok->setObjectName("ok");
ok->setMaximumSize(20, 20);
diff --git a/cockatrice/src/game/board/counter_state.cpp b/cockatrice/src/game/board/counter_state.cpp
index 6da18b662..0970e4272 100644
--- a/cockatrice/src/game/board/counter_state.cpp
+++ b/cockatrice/src/game/board/counter_state.cpp
@@ -13,12 +13,12 @@ CounterState *CounterState::fromProto(const ServerInfo_Counter &counter, QObject
convertColorToQColor(counter.counter_color()), counter.radius(), counter.count(), parent);
}
-void CounterState::setValue(int newValue)
+void CounterState::setValue(int newValue, bool skipDamageAnimation)
{
if (newValue == value) {
return;
}
int old = value;
value = newValue;
- emit valueChanged(old, newValue);
+ emit valueChanged(old, newValue, skipDamageAnimation);
}
\ No newline at end of file
diff --git a/cockatrice/src/game/board/counter_state.h b/cockatrice/src/game/board/counter_state.h
index 0f2f16b55..4c7b34473 100644
--- a/cockatrice/src/game/board/counter_state.h
+++ b/cockatrice/src/game/board/counter_state.h
@@ -35,10 +35,23 @@ public:
return value;
}
- void setValue(int newValue);
+ /**
+ * @brief Set the counter value.
+ * @param newValue The new value.
+ * @param skipDamageAnimation When true, valueChanged is emitted with skipDamageAnimation=true, letting views
+ * suppress damage-related feedback (e.g. battlefield shimmer, life counter flash) for values set during replay
+ * rewinds.
+ */
+ void setValue(int newValue, bool skipDamageAnimation = false);
signals:
- void valueChanged(int oldValue, int newValue);
+ /**
+ * @brief Emitted whenever the value changes.
+ * @param oldValue The previous value.
+ * @param newValue The new value.
+ * @param skipDamageAnimation True when the change should not trigger damage/life-change feedback in views.
+ */
+ void valueChanged(int oldValue, int newValue, bool skipDamageAnimation);
private:
int id;
diff --git a/cockatrice/src/game/game_event_handler.cpp b/cockatrice/src/game/game_event_handler.cpp
index bc68d4d7c..f146cdbb4 100644
--- a/cockatrice/src/game/game_event_handler.cpp
+++ b/cockatrice/src/game/game_event_handler.cpp
@@ -430,12 +430,13 @@ void GameEventHandler::eventJoin(const Event_Join &event, int /*eventPlayerId*/,
QString playerName = QString::fromStdString(playerInfo.user_info().name());
emit addPlayerToAutoCompleteList(playerName);
- if (game->getPlayerManager()->getPlayers().contains(playerId)) {
+ PlayerManager *playerManager = game->getPlayerManager();
+ if (playerManager->getPlayers().contains(playerId) || playerManager->getSpectators().contains(playerId)) {
return;
}
if (playerInfo.spectator()) {
- game->getPlayerManager()->addSpectator(playerId, playerInfo);
+ playerManager->addSpectator(playerId, playerInfo);
emit logJoinSpectator(playerName);
emit spectatorJoined(playerInfo);
} else {
diff --git a/cockatrice/src/game/player/event_processing_options.h b/cockatrice/src/game/player/event_processing_options.h
index 4c7663789..06238d77e 100644
--- a/cockatrice/src/game/player/event_processing_options.h
+++ b/cockatrice/src/game/player/event_processing_options.h
@@ -13,7 +13,8 @@
enum EventProcessingOption
{
SKIP_REVEAL_WINDOW = 0x0001,
- SKIP_TAP_ANIMATION = 0x0002
+ SKIP_TAP_ANIMATION = 0x0002,
+ SKIP_DAMAGE_ANIMATION = 0x0004
};
// Wrap it in a QFlags typedef
diff --git a/cockatrice/src/game/player/player_event_handler.cpp b/cockatrice/src/game/player/player_event_handler.cpp
index bc48298f7..277b8b1d4 100644
--- a/cockatrice/src/game/player/player_event_handler.cpp
+++ b/cockatrice/src/game/player/player_event_handler.cpp
@@ -262,14 +262,15 @@ void PlayerEventHandler::eventCreateCounter(const Event_CreateCounter &event)
player->addCounter(event.counter_info());
}
-void PlayerEventHandler::eventSetCounter(const Event_SetCounter &event)
+void PlayerEventHandler::eventSetCounter(const Event_SetCounter &event, EventProcessingOptions options)
{
CounterState *ctr = player->getCounters().value(event.counter_id(), nullptr);
if (!ctr) {
return;
}
int oldValue = ctr->getValue();
- ctr->setValue(event.value());
+ const bool skipDamageAnimation = options.testFlag(SKIP_DAMAGE_ANIMATION);
+ ctr->setValue(event.value(), skipDamageAnimation);
emit logSetCounter(player, ctr->getName(), event.value(), oldValue);
}
@@ -625,7 +626,7 @@ void PlayerEventHandler::processGameEvent(GameEvent::GameEventType type,
eventCreateCounter(event.GetExtension(Event_CreateCounter::ext));
break;
case GameEvent::SET_COUNTER:
- eventSetCounter(event.GetExtension(Event_SetCounter::ext));
+ eventSetCounter(event.GetExtension(Event_SetCounter::ext), options);
break;
case GameEvent::DEL_COUNTER:
eventDelCounter(event.GetExtension(Event_DelCounter::ext));
diff --git a/cockatrice/src/game/player/player_event_handler.h b/cockatrice/src/game/player/player_event_handler.h
index 48ad85e88..300cacd08 100644
--- a/cockatrice/src/game/player/player_event_handler.h
+++ b/cockatrice/src/game/player/player_event_handler.h
@@ -153,7 +153,7 @@ public:
void eventCreateCounter(const Event_CreateCounter &event);
/// Set a player-level counter value.
- void eventSetCounter(const Event_SetCounter &event);
+ void eventSetCounter(const Event_SetCounter &event, EventProcessingOptions options);
/// Delete a player-level counter.
void eventDelCounter(const Event_DelCounter &event);
diff --git a/cockatrice/src/game/player/player_logic.cpp b/cockatrice/src/game/player/player_logic.cpp
index 45ba09aac..143df5c57 100644
--- a/cockatrice/src/game/player/player_logic.cpp
+++ b/cockatrice/src/game/player/player_logic.cpp
@@ -175,7 +175,15 @@ void PlayerLogic::processPlayerInfo(const ServerInfo_Player &info)
const ServerInfo_Card &cardInfo = zoneInfo.card_list(j);
auto *card = new CardItem(this);
card->processCardInfo(cardInfo);
- zone->addCard(card, false, cardInfo.x(), cardInfo.y());
+ // Zones without coordinates (hand, piles, stack) preserve the order
+ // they arrive in on the server in the positions of their cards list.
+ // The x coordinate of such cards is always 0, so inserting at it
+ // would reverse the list on reconnect. Append instead.
+ if (zoneInfo.with_coords()) {
+ zone->addCard(card, false, cardInfo.x(), cardInfo.y());
+ } else {
+ zone->addCard(card, false, -1);
+ }
}
}
if (zoneInfo.has_always_reveal_top_card()) {
diff --git a/cockatrice/src/game/player/player_manager.cpp b/cockatrice/src/game/player/player_manager.cpp
index 6772d3ff1..8486efbeb 100644
--- a/cockatrice/src/game/player/player_manager.cpp
+++ b/cockatrice/src/game/player/player_manager.cpp
@@ -75,6 +75,14 @@ PlayerLogic *PlayerManager::getPlayer(int playerId) const
return player;
}
+void PlayerManager::clearSpectators()
+{
+ const QList spectatorIds = spectators.keys();
+ for (int spectatorId : spectatorIds) {
+ removeSpectator(spectatorId);
+ }
+}
+
void PlayerManager::onPlayerConceded(int playerId, bool conceded)
{
// Everything else cares about this
diff --git a/cockatrice/src/game/player/player_manager.h b/cockatrice/src/game/player/player_manager.h
index 2f8b87af8..504e65396 100644
--- a/cockatrice/src/game/player/player_manager.h
+++ b/cockatrice/src/game/player/player_manager.h
@@ -100,6 +100,9 @@ public:
emit spectatorRemoved(spectatorId, spectatorInfo);
}
+ /** @brief Remove all spectators, emitting the removal signal for each. */
+ void clearSpectators();
+
[[nodiscard]] AbstractGame *getGame() const
{
return game;
diff --git a/cockatrice/src/game_graphics/board/abstract_card_item.cpp b/cockatrice/src/game_graphics/board/abstract_card_item.cpp
index 1410d0c80..3969b7d03 100644
--- a/cockatrice/src/game_graphics/board/abstract_card_item.cpp
+++ b/cockatrice/src/game_graphics/board/abstract_card_item.cpp
@@ -1,6 +1,7 @@
#include "abstract_card_item.h"
#include "../../client/settings/cache_settings.h"
+#include "../../interface/card_localization.h"
#include "../../interface/card_picture_loader/card_picture_loader.h"
#include "../game_scene.h"
#include "../z_values.h"
@@ -26,6 +27,8 @@ AbstractCardItem::AbstractCardItem(QGraphicsItem *parent, const CardRef &cardRef
connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::displayCardNamesChanged, this,
[this] { update(); });
+ connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::cardLangChanged, this,
+ [this] { update(); });
refreshCardInfo();
connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::roundCardCornersChanged, this,
@@ -171,7 +174,7 @@ void AbstractCardItem::paintPicture(QPainter *painter, const QSizeF &translatedS
if (SettingsCache::instance().debug().getShowCardId()) {
prefix = "#" + QString::number(id) + " ";
}
- nameStr = prefix + cardRef.name;
+ nameStr = prefix + CardLocalization::displayName(getCardInfo());
}
painter->drawText(QRectF(3 * scaleFactor, 3 * scaleFactor, translatedSize.width() - 6 * scaleFactor,
translatedSize.height() - 6 * scaleFactor),
diff --git a/cockatrice/src/game_graphics/board/abstract_counter.cpp b/cockatrice/src/game_graphics/board/abstract_counter.cpp
index e63117e13..4ba04804f 100644
--- a/cockatrice/src/game_graphics/board/abstract_counter.cpp
+++ b/cockatrice/src/game_graphics/board/abstract_counter.cpp
@@ -29,9 +29,9 @@ AbstractCounter::AbstractCounter(CounterState *state,
{
setAcceptHoverEvents(true);
- connect(state, &CounterState::valueChanged, this, [this](int oldValue, int newValue) {
+ connect(state, &CounterState::valueChanged, this, [this](int oldValue, int newValue, bool skipDamageAnimation) {
value = newValue;
- onValueChanged(oldValue, newValue);
+ onValueChanged(oldValue, newValue, skipDamageAnimation);
update();
});
@@ -230,7 +230,7 @@ void AbstractCounterDialog::changeValue(int diff)
setTextValue(QString::number(curValue));
}
-void AbstractCounter::onValueChanged(int /*oldValue*/, int /*newValue*/)
+void AbstractCounter::onValueChanged(int /*oldValue*/, int /*newValue*/, bool /*skipDamageAnimation*/)
{
// Default: no feedback. Subclasses such as PlayerCounter override this to
// flash the counter on meaningful changes (life gain/loss).
diff --git a/cockatrice/src/game_graphics/board/abstract_counter.h b/cockatrice/src/game_graphics/board/abstract_counter.h
index 9ddcc6d58..67b5b4074 100644
--- a/cockatrice/src/game_graphics/board/abstract_counter.h
+++ b/cockatrice/src/game_graphics/board/abstract_counter.h
@@ -39,8 +39,9 @@ protected:
* @brief Hook for subclasses that need per-value-change feedback (e.g. life-total flash).
*
* Called whenever the counter's value changes, before the item repaints.
+ * @param skipDamageAnimation True when damage-related feedback should be suppressed (replay rewinds).
*/
- virtual void onValueChanged(int oldValue, int newValue);
+ virtual void onValueChanged(int oldValue, int newValue, bool skipDamageAnimation);
void mousePressEvent(QGraphicsSceneMouseEvent *event) override;
void hoverEnterEvent(QGraphicsSceneHoverEvent *event) override;
diff --git a/cockatrice/src/game_graphics/board/card_item.cpp b/cockatrice/src/game_graphics/board/card_item.cpp
index c40c8c214..c2dc455cc 100644
--- a/cockatrice/src/game_graphics/board/card_item.cpp
+++ b/cockatrice/src/game_graphics/board/card_item.cpp
@@ -316,7 +316,7 @@ void CardItem::drawAttachArrow()
for (const auto &item : scene()->selectedItems()) {
CardItem *card = qgraphicsitem_cast(item);
- if (card == nullptr) {
+ if (card == nullptr || card == this) {
continue;
}
if (card->getZone() != state->getZone()) {
diff --git a/cockatrice/src/game_graphics/game_scene.cpp b/cockatrice/src/game_graphics/game_scene.cpp
index 457f1b3f7..17af7618b 100644
--- a/cockatrice/src/game_graphics/game_scene.cpp
+++ b/cockatrice/src/game_graphics/game_scene.cpp
@@ -221,7 +221,12 @@ void GameScene::removePlayer(PlayerLogic *player)
clearArrowsForPlayer(player->getPlayerInfo()->getId());
- for (ZoneViewWidget *zone : zoneViews) {
+ // Closing a view removes it from zoneViews synchronously, so iterate over a
+ // copy: otherwise a player with several open views (e.g. library and hand)
+ // only has the first one closed here and the remaining views are left
+ // pointing at a player that is about to be deleted.
+ const QList zoneViewCopy = zoneViews;
+ for (ZoneViewWidget *zone : zoneViewCopy) {
if (zone->getPlayer() == player) {
zone->close();
}
@@ -664,7 +669,10 @@ CardItem *GameScene::findTopmostCardInZone(const QList &items,
*/
void GameScene::toggleZoneView(PlayerLogic *player, const QString &zoneName, int numberCards, bool isReversed)
{
- for (auto &view : zoneViews) {
+ // Closing a view removes it from zoneViews synchronously, so iterate over a
+ // copy to make sure every already-open matching view is closed.
+ const QList zoneViewCopy = zoneViews;
+ for (auto *view : zoneViewCopy) {
ZoneViewZone *temp = view->getZone();
if (temp->getLogic()->getName() == zoneName && temp->getLogic()->getPlayer() == player &&
qobject_cast(temp->getLogic())->getNumberCards() == numberCards) {
diff --git a/cockatrice/src/game_graphics/hand_counter.cpp b/cockatrice/src/game_graphics/hand_counter.cpp
index 35989ff38..8dcbcfdaa 100644
--- a/cockatrice/src/game_graphics/hand_counter.cpp
+++ b/cockatrice/src/game_graphics/hand_counter.cpp
@@ -1,5 +1,6 @@
#include "hand_counter.h"
+#include "../interface/pixel_map_generator.h"
#include "zones/card_zone.h"
#include
@@ -32,7 +33,8 @@ void HandCounter::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*op
QSize translatedSize = painter->combinedTransform().mapRect(boundingRect()).size().toSize();
QPixmap cachedPixmap;
if (!QPixmapCache::find("handCounter" + QString::number(translatedSize.width()), &cachedPixmap)) {
- cachedPixmap = QPixmap("theme:hand").scaled(translatedSize, Qt::KeepAspectRatio, Qt::SmoothTransformation);
+ cachedPixmap =
+ themePixmap(QStringLiteral("hand")).scaled(translatedSize, Qt::KeepAspectRatio, Qt::SmoothTransformation);
QPixmapCache::insert("handCounter" + QString::number(translatedSize.width()), cachedPixmap);
}
resetPainterTransform(painter);
diff --git a/cockatrice/src/game_graphics/player/player_graphics_item.cpp b/cockatrice/src/game_graphics/player/player_graphics_item.cpp
index 8bf2703e1..122ab83be 100644
--- a/cockatrice/src/game_graphics/player/player_graphics_item.cpp
+++ b/cockatrice/src/game_graphics/player/player_graphics_item.cpp
@@ -3,6 +3,7 @@
#include "../../game/player/player_actions.h"
#include "../../interface/card_picture_loader/card_picture_loader.h"
#include "../../interface/widgets/cards/art_crop_attribution.h"
+#include "../../interface/widgets/cards/card_art_utils.h"
#include "../../interface/widgets/playmat/playmat_utils.h"
#include "../../interface/widgets/tabs/tab_game.h"
#include "../board/abstract_card_item.h"
@@ -251,8 +252,8 @@ void PlayerGraphicsItem::onCounterAdded(CounterState *state)
AbstractCounter *widget;
if (state->getName() == "life") {
widget = playerTarget->addCounter(state);
- connect(state, &CounterState::valueChanged, this, [this](int oldValue, int newValue) {
- if (newValue < oldValue) {
+ connect(state, &CounterState::valueChanged, this, [this](int oldValue, int newValue, bool skipDamageAnimation) {
+ if (newValue < oldValue && !skipDamageAnimation) {
tableZoneGraphicsItem->triggerDamageShimmer();
}
});
@@ -442,7 +443,7 @@ void PlayerGraphicsItem::updatePlaymat()
hasPlaymat = true;
emit playmatChanged(true);
}
- playmatPixmap = fullRes;
+ playmatPixmap = CardArtUtils::rotateSidewaysLayoutArt(fullRes, card);
update();
}
diff --git a/cockatrice/src/game_graphics/player/player_list_widget.cpp b/cockatrice/src/game_graphics/player/player_list_widget.cpp
index 4268e1019..a7ec2e4a9 100644
--- a/cockatrice/src/game_graphics/player/player_list_widget.cpp
+++ b/cockatrice/src/game_graphics/player/player_list_widget.cpp
@@ -53,13 +53,13 @@ PlayerListWidget::PlayerListWidget(TabSupervisor *_tabSupervisor,
QWidget *parent)
: QTreeWidget(parent), tabSupervisor(_tabSupervisor), client(_client), game(_game), gameStarted(false)
{
- readyIcon = QPixmap("theme:icons/ready_start");
- notReadyIcon = QPixmap("theme:icons/not_ready_start");
- concededIcon = QPixmap("theme:icons/conceded");
+ readyIcon = themePixmap(QStringLiteral("icons/ready_start"));
+ notReadyIcon = themePixmap(QStringLiteral("icons/not_ready_start"));
+ concededIcon = themePixmap(QStringLiteral("icons/conceded"));
playerIcon = loadColorAdjustedPixmap("theme:icons/player");
judgeIcon = loadColorAdjustedPixmap("theme:icons/scales");
spectatorIcon = loadColorAdjustedPixmap("theme:icons/spectator");
- lockIcon = QPixmap("theme:icons/lock");
+ lockIcon = themePixmap(QStringLiteral("icons/lock"));
if (tabSupervisor) {
itemDelegate = new PlayerListItemDelegate(this);
@@ -92,6 +92,11 @@ void PlayerListWidget::retranslateUi()
void PlayerListWidget::addPlayer(const ServerInfo_PlayerProperties &player)
{
+ if (players.contains(player.player_id())) {
+ updatePlayerProperties(player);
+ return;
+ }
+
QTreeWidgetItem *newPlayer = new PlayerListTWI;
players.insert(player.player_id(), newPlayer);
updatePlayerProperties(player);
@@ -176,6 +181,17 @@ void PlayerListWidget::removePlayer(int playerId)
delete takeTopLevelItem(indexOfTopLevelItem(player));
}
+void PlayerListWidget::clearSpectators()
+{
+ const QList playerIds = players.keys();
+ for (int playerId : playerIds) {
+ QTreeWidgetItem *player = players.value(playerId, 0);
+ if (player && !player->data(1, Qt::UserRole).toBool()) {
+ removePlayer(playerId);
+ }
+ }
+}
+
void PlayerListWidget::setActivePlayer(int playerId)
{
QMapIterator i(players);
diff --git a/cockatrice/src/game_graphics/player/player_list_widget.h b/cockatrice/src/game_graphics/player/player_list_widget.h
index a53cfa989..f2f0be5fd 100644
--- a/cockatrice/src/game_graphics/player/player_list_widget.h
+++ b/cockatrice/src/game_graphics/player/player_list_widget.h
@@ -66,6 +66,7 @@ public slots:
void addPlayer(const ServerInfo_PlayerProperties &player);
void removePlayer(int playerId);
void updatePlayerProperties(const ServerInfo_PlayerProperties &prop, int playerId = -1);
+ void clearSpectators();
};
#endif
diff --git a/cockatrice/src/game_graphics/player/player_target.cpp b/cockatrice/src/game_graphics/player/player_target.cpp
index 910ee9c17..d6c28370d 100644
--- a/cockatrice/src/game_graphics/player/player_target.cpp
+++ b/cockatrice/src/game_graphics/player/player_target.cpp
@@ -69,7 +69,7 @@ void PlayerCounter::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*
}
}
-void PlayerCounter::onValueChanged(int oldValue, int newValue)
+void PlayerCounter::onValueChanged(int oldValue, int newValue, bool skipDamageAnimation)
{
flashDelta = newValue - oldValue;
if (flashDelta == 0) {
@@ -81,6 +81,11 @@ void PlayerCounter::onValueChanged(int oldValue, int newValue)
return;
}
+ if (skipDamageAnimation) {
+ flashAlpha = 0.0;
+ return;
+ }
+
flashAlpha = 1.0;
flashClock.start();
if (scene()) {
@@ -132,8 +137,18 @@ void PlayerTarget::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*o
QRectF translatedRect = painter->combinedTransform().mapRect(avatarBoundingRect);
QSize translatedSize = translatedRect.size().toSize();
QPixmap cachedPixmap;
+ // The key must cover everything the generated pawn depends on: the rendered
+ // size, the user level, and the pixmap being drawn. fullPixmap.cacheKey() is
+ // 0 for every null pixmap, so the default-pawn branch additionally needs the
+ // pawn's privlevel (lowercased, matching UserLevelPixmapGenerator) and colors
+ // in the key — otherwise two players without a custom avatar (and the same
+ // user level) would share one cached pawn.
const QString cacheKey = "avatar" + QString::number(translatedSize.width()) + "_" +
- QString::number(info->user_level()) + "_" + QString::number(fullPixmap.cacheKey());
+ QString::number(translatedSize.height()) + "_" + QString::number(info->user_level()) +
+ "_" + QString::number(fullPixmap.cacheKey()) + "_" +
+ QString::fromStdString(info->privlevel()).toLower() + "_" +
+ QString::fromStdString(info->pawn_colors().left_side()) + "_" +
+ QString::fromStdString(info->pawn_colors().right_side());
if (!QPixmapCache::find(cacheKey, &cachedPixmap)) {
cachedPixmap = QPixmap(translatedSize.width(), translatedSize.height());
diff --git a/cockatrice/src/game_graphics/player/player_target.h b/cockatrice/src/game_graphics/player/player_target.h
index af0e9c8b7..1d06c6274 100644
--- a/cockatrice/src/game_graphics/player/player_target.h
+++ b/cockatrice/src/game_graphics/player/player_target.h
@@ -21,7 +21,7 @@ class PlayerCounter : public AbstractCounter, public IAnimatedItem
{
Q_OBJECT
protected:
- void onValueChanged(int oldValue, int newValue) override;
+ void onValueChanged(int oldValue, int newValue, bool skipDamageAnimation) override;
private:
static constexpr qreal flashDurationMs = 450.0;
diff --git a/cockatrice/src/game_graphics/zones/view_zone_widget.cpp b/cockatrice/src/game_graphics/zones/view_zone_widget.cpp
index 17118e80d..c13b79fea 100644
--- a/cockatrice/src/game_graphics/zones/view_zone_widget.cpp
+++ b/cockatrice/src/game_graphics/zones/view_zone_widget.cpp
@@ -62,7 +62,7 @@ ZoneViewWidget::ZoneViewWidget(PlayerLogic *_player,
searchEdit.setPlaceholderText(tr("Search by card name (or search expressions)"));
searchEdit.setClearButtonEnabled(true);
searchEdit.addAction(loadColorAdjustedPixmap("theme:icons/search"), QLineEdit::LeadingPosition);
- auto help = searchEdit.addAction(QPixmap("theme:icons/info"), QLineEdit::TrailingPosition);
+ auto help = searchEdit.addAction(themePixmap(QStringLiteral("icons/info")), QLineEdit::TrailingPosition);
connect(help, &QAction::triggered, this, [this] { createSearchSyntaxHelpWindow(&searchEdit); });
@@ -549,7 +549,7 @@ void ZoneViewWidget::initStyleOption(QStyleOption *option) const
{
QStyleOptionTitleBar *titleBar = qstyleoption_cast(option);
if (titleBar) {
- titleBar->icon = QPixmap("theme:cockatrice");
+ titleBar->icon = themePixmap(QStringLiteral("cockatrice"));
}
}
diff --git a/cockatrice/src/interface/card_localization.h b/cockatrice/src/interface/card_localization.h
new file mode 100644
index 000000000..0bfe4a764
--- /dev/null
+++ b/cockatrice/src/interface/card_localization.h
@@ -0,0 +1,59 @@
+#ifndef COCKATRICE_CARD_LOCALIZATION_H
+#define COCKATRICE_CARD_LOCALIZATION_H
+
+#include "../client/settings/cache_settings.h"
+
+#include
+#include
+#include
+
+namespace CardLocalization
+{
+/**
+ * @brief The language code selected for localized card text and images.
+ */
+inline QString displayLang()
+{
+ return SettingsCache::instance().cardsDisplay().getCardLang();
+}
+
+/**
+ * @brief Card name in the configured display language, falling back to English.
+ * @param card The card to display.
+ * @return The localized name, or an empty string for a null card.
+ */
+inline QString displayName(const CardInfoPtr &card)
+{
+ return card.isNull() ? QString() : card->getLocalizedName(displayLang());
+}
+
+/**
+ * @brief Card rules text in the configured display language, falling back to English.
+ * @param card The card to display.
+ * @return The localized text, or an empty string for a null card.
+ */
+inline QString displayText(const CardInfoPtr &card)
+{
+ return card.isNull() ? QString() : card->getLocalizedText(displayLang());
+}
+
+/**
+ * @brief Card name in the configured display language, falling back to English.
+ * @param card The card to display.
+ */
+inline QString displayName(const CardInfo &card)
+{
+ return card.getLocalizedName(displayLang());
+}
+
+/**
+ * @brief Card rules text in the configured display language, falling back to English.
+ * @param card The card to display.
+ */
+inline QString displayText(const CardInfo &card)
+{
+ return card.getLocalizedText(displayLang());
+}
+} // namespace CardLocalization
+
+#endif // COCKATRICE_CARD_LOCALIZATION_H
\ No newline at end of file
diff --git a/cockatrice/src/interface/card_picture_loader/card_picture_loader.cpp b/cockatrice/src/interface/card_picture_loader/card_picture_loader.cpp
index 2f46e7941..b8a54761a 100644
--- a/cockatrice/src/interface/card_picture_loader/card_picture_loader.cpp
+++ b/cockatrice/src/interface/card_picture_loader/card_picture_loader.cpp
@@ -1,13 +1,14 @@
#include "card_picture_loader.h"
#include "../../client/settings/cache_settings.h"
+#include "../pixel_map_generator.h"
#include "card_picture_loader_cache_method.h"
#include "card_picture_loader_local_schemes.h"
#include
#include
#include
-#include
+#include
#include
#include
#include
@@ -19,6 +20,7 @@
#include
#include
#include
+#include
#include
#include
#include
@@ -36,8 +38,10 @@ CardPictureLoader::CardPictureLoader() : QObject(nullptr)
&CardPictureLoader::picsPathChanged);
connect(&SettingsCache::instance().downloads(), &DownloadSettings::picDownloadChanged, this,
&CardPictureLoader::picDownloadChanged);
+ connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::cardLangChanged, this,
+ &CardPictureLoader::cardLangChanged);
- qRegisterMetaType();
+ qRegisterMetaType("ExactCard");
connect(worker, &CardPictureLoaderWorker::imageLoaded, this, &CardPictureLoader::imageLoaded);
statusBar = new CardPictureLoaderStatusBar(nullptr);
@@ -62,7 +66,7 @@ void CardPictureLoader::getCardBackPixmap(QPixmap &pixmap, QSize size)
QString backCacheKey = "_trice_card_back_" + QString::number(size.width()) + "x" + QString::number(size.height());
if (!QPixmapCache::find(backCacheKey, &pixmap)) {
qCDebug(CardPictureLoaderLog) << "PictureLoader: cache miss for" << backCacheKey;
- QPixmap tmpPixmap("theme:cardback");
+ QPixmap tmpPixmap = themePixmap(QStringLiteral("cardback"));
if (tmpPixmap.isNull()) {
qCWarning(CardPictureLoaderLog) << "Failed to load 'theme:cardback'! Using fallback pixmap.";
@@ -83,7 +87,7 @@ void CardPictureLoader::getCardBackLoadingInProgressPixmap(QPixmap &pixmap, QSiz
"_trice_card_back_inprogress_" + QString::number(size.width()) + "x" + QString::number(size.height());
if (!QPixmapCache::find(backCacheKey, &pixmap)) {
qCDebug(CardPictureLoaderCardBackCacheFailLog) << "PictureLoader: cache miss for" << backCacheKey;
- QPixmap tmpPixmap("theme:cardback");
+ QPixmap tmpPixmap = themePixmap(QStringLiteral("cardback"));
if (tmpPixmap.isNull()) {
qCWarning(CardPictureLoaderLog) << "Failed to load 'theme:cardback' for in-progress state! Using fallback.";
@@ -105,7 +109,7 @@ void CardPictureLoader::getCardBackLoadingFailedPixmap(QPixmap &pixmap, QSize si
"_trice_card_back_failed_" + QString::number(size.width()) + "x" + QString::number(size.height());
if (!QPixmapCache::find(backCacheKey, &pixmap)) {
qCDebug(CardPictureLoaderCardBackCacheFailLog) << "PictureLoader: cache miss for" << backCacheKey;
- QPixmap tmpPixmap("theme:cardback");
+ QPixmap tmpPixmap = themePixmap(QStringLiteral("cardback"));
if (tmpPixmap.isNull()) {
qCWarning(CardPictureLoaderLog) << "Failed to load 'theme:cardback' for failed state! Using fallback.";
@@ -138,7 +142,8 @@ void CardPictureLoader::getPixmap(QPixmap &pixmap, const ExactCard &card, QSize
QPixmap bigPixmap;
if (QPixmapCache::find(key, &bigPixmap)) {
if (bigPixmap.isNull()) {
- getCardBackLoadingFailedPixmap(pixmap, size);
+ // Leave the pixmap null so callers fall back to a solid color
+ // instead of showing the card back.
QDateTime failedAtTime = getInstance().failedAt.value(key);
if (!failedAtTime.isValid() ||
failedAtTime.addSecs(RETRY_FAILED_CARDS_SECS) < QDateTime::currentDateTime()) {
@@ -204,7 +209,49 @@ void CardPictureLoader::imageLoaded(const ExactCard &card, const QImage &image)
card.emitPixmapUpdated();
}
-void CardPictureLoader::saveCardImageToLocalStorage(const ExactCard &card, const QPixmap &pixmap)
+void CardPictureLoader::deleteAllLocalOverrides(const ExactCard &card)
+{
+ const QString picsRoot = SettingsCache::instance().paths().getPicsPath();
+ if (picsRoot.isEmpty() || !card) {
+ return;
+ }
+
+ QDir baseDir(picsRoot);
+ if (!baseDir.cd("downloadedPics")) {
+ return;
+ }
+
+ const QString name = card.getInfo().getCorrectedName();
+
+ QString set, collector, uuid;
+ auto printing = card.getPrinting();
+ if (printing.getSet()) {
+ set = printing.getSet()->getCorrectedShortName();
+ collector = printing.getProperty("num");
+ uuid = printing.getUuid();
+ }
+
+ for (const auto &scheme : CardPictureLoaderLocalSchemes::exportSchemes()) {
+ QString rel = CardPictureLoaderLocalSchemes::expandPattern(scheme.pattern, name, set, collector, uuid);
+
+ if (rel.isEmpty()) {
+ continue;
+ }
+
+ rel += ".png";
+ rel = QDir::cleanPath(rel);
+
+ QString fullPath = baseDir.filePath(rel);
+
+ if (QFile::exists(fullPath)) {
+ QFile::remove(fullPath);
+ }
+ }
+}
+
+void CardPictureLoader::saveCardImageToLocalStorage(const ExactCard &card,
+ const QPixmap &pixmap,
+ const bool allowOverwrite)
{
if (pixmap.isNull() || !card) {
return;
@@ -264,8 +311,9 @@ void CardPictureLoader::saveCardImageToLocalStorage(const ExactCard &card, const
QFileInfo outInfo(baseDir.filePath(relativePath));
- // Do not overwrite existing files
- if (outInfo.exists()) {
+ // Automatic cache writes (FILESYSTEM_CACHE) must never clobber an explicit user override.
+ // Only the explicit override paths pass allowOverwrite == true.
+ if (!allowOverwrite && outInfo.exists()) {
return;
}
@@ -286,6 +334,122 @@ void CardPictureLoader::saveCardImageToLocalStorage(const ExactCard &card, const
}
}
+void CardPictureLoader::installPrintingOverrideOnLoad(const ExactCard &originalCard, const ExactCard &overrideCard)
+{
+ // Overriding a card with itself is the reset case, not a real override: every code path below
+ // would re-enter itself through emitPixmapUpdated(). Reject it outright.
+ if (originalCard == overrideCard) {
+ return;
+ }
+
+ CardInfoPtr cardPtr = overrideCard.getCardPtr();
+ if (!cardPtr) {
+ return;
+ }
+
+ // Heap-allocate so the lambda can capture it before the connection is made
+ auto *connectionHandle = new QMetaObject::Connection;
+
+ *connectionHandle =
+ connect(cardPtr.data(), &CardInfo::pixmapUpdated, cardPtr.data(),
+ [originalCard, overrideCard, connectionHandle, this](const PrintingInfo &printing) {
+ // All printings share the same CardInfo, so ignore updates triggered by any
+ // other printing (e.g., the original card re-loading from disk).
+ if (printing != overrideCard.getPrinting()) {
+ return;
+ }
+
+ QPixmap pixmap;
+ if (QPixmapCache::find(overrideCard.getPixmapCacheKey(), &pixmap) && !pixmap.isNull()) {
+ // The override art has resolved — persist it and reflect it immediately.
+ // Retire the connection before emitting so the refresh can't re-enter.
+ saveCardImageToLocalStorage(originalCard, pixmap, /*allowOverwrite=*/true);
+
+ QObject::disconnect(*connectionHandle);
+ delete connectionHandle;
+
+ QPixmapCache::clear();
+ originalCard.emitPixmapUpdated();
+ return;
+ }
+
+ // The art could not be resolved. Keep the connection armed so a late resolution
+ // still lands, and surface a visible refusal instead of a silent no-op. An
+ // override already on disk is left untouched and simply re-displayed.
+ QPixmapCache::clear();
+ if (!hasLocalOverrides(originalCard)) {
+ QPixmap refusedPixmap;
+ getCardBackLoadingFailedPixmap(refusedPixmap, QSize(480, 672));
+ QPixmapCache::insert(originalCard.getPixmapCacheKey(), refusedPixmap);
+ }
+ originalCard.emitPixmapUpdated();
+ });
+
+ // Now enqueue; if the image is already loading (deduplicated in the worker),
+ // the signal will still fire when it completes
+ CardPictureLoader::getInstance().worker->enqueueImageLoad(overrideCard);
+}
+
+void CardPictureLoader::installPrintingOverride(const ExactCard &originalCard, const ExactCard &overrideCard)
+{
+ // Same guard as installPrintingOverrideOnLoad: self-override is the reset case.
+ if (originalCard == overrideCard) {
+ return;
+ }
+
+ QPixmap pixmap;
+ const QString key = overrideCard.getPixmapCacheKey();
+
+ if (QPixmapCache::find(key, &pixmap) && !pixmap.isNull()) {
+ // Already cached — save immediately; the caller refreshes the card.
+ saveCardImageToLocalStorage(originalCard, pixmap, /*allowOverwrite=*/true);
+ return;
+ }
+
+ // Cache miss or previously failed load — enqueue load and wait for the signal.
+ installPrintingOverrideOnLoad(originalCard, overrideCard);
+}
+
+bool CardPictureLoader::hasLocalOverrides(const ExactCard &card)
+{
+ const QString picsRoot = SettingsCache::instance().paths().getPicsPath();
+ if (picsRoot.isEmpty() || !card) {
+ return false;
+ }
+
+ QDir baseDir(picsRoot);
+ if (!baseDir.cd("downloadedPics")) {
+ return false;
+ }
+
+ const QString name = card.getInfo().getCorrectedName();
+
+ QString set, collector, uuid;
+ const PrintingInfo printing = card.getPrinting();
+ if (printing.getSet()) {
+ set = printing.getSet()->getCorrectedShortName();
+ collector = printing.getProperty("num");
+ uuid = printing.getUuid();
+ }
+
+ for (const auto &scheme : CardPictureLoaderLocalSchemes::exportSchemes()) {
+ QString rel = CardPictureLoaderLocalSchemes::expandPattern(scheme.pattern, name, set, collector, uuid);
+
+ if (rel.isEmpty()) {
+ continue;
+ }
+
+ rel += ".png";
+ rel = QDir::cleanPath(rel);
+
+ if (QFile::exists(baseDir.filePath(rel))) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
void CardPictureLoader::clearPixmapCache()
{
QPixmapCache::clear();
@@ -325,31 +489,11 @@ void CardPictureLoader::picsPathChanged()
QPixmapCache::clear();
}
-bool CardPictureLoader::hasCustomArt()
+void CardPictureLoader::cardLangChanged()
{
- auto picsPath = SettingsCache::instance().paths().getPicsPath();
- QDirIterator it(picsPath, QDir::Dirs | QDir::NoDotAndDotDot);
-
- // Check if there is at least one non-directory file in the pics path, other
- // than in the "downloadedPics" subdirectory.
- while (it.hasNext()) {
-#if (QT_VERSION >= QT_VERSION_CHECK(6, 3, 0))
- QFileInfo dir(it.nextFileInfo());
-#else
- // nextFileInfo() is only available in Qt 6.3+, for previous versions, we build
- // the QFileInfo from a QString which requires more system calls.
- QFileInfo dir(it.next());
-#endif
-
- if (it.fileName() == "downloadedPics") {
- continue;
- }
-
- QDirIterator subIt(it.filePath(), QDir::Files, QDirIterator::Subdirectories | QDirIterator::FollowSymlinks);
- if (subIt.hasNext()) {
- return true;
- }
- }
-
- return false;
+ // Localized images are fetched via a different URL, but the in-memory
+ // pixmap cache is keyed by card name/uuid, so drop everything cached
+ // (including failure timestamps) to force a reload in the new language.
+ QPixmapCache::clear();
+ failedAt.clear();
}
diff --git a/cockatrice/src/interface/card_picture_loader/card_picture_loader.h b/cockatrice/src/interface/card_picture_loader/card_picture_loader.h
index 5c3ac84a3..0a4934e6d 100644
--- a/cockatrice/src/interface/card_picture_loader/card_picture_loader.h
+++ b/cockatrice/src/interface/card_picture_loader/card_picture_loader.h
@@ -97,10 +97,17 @@ public:
static void cacheCardPixmaps(const QList &cards);
/**
- * @brief Check if the user has custom card art in the picsPath directory.
- * @return True if any custom art exists.
+ * @brief Check if a local override image already exists for the card.
+ * @param card The card to check.
+ * @return True if the card has at least one locally stored override image.
*/
- static bool hasCustomArt();
+ static bool hasLocalOverrides(const ExactCard &card);
+
+ /**
+ * @brief Removes all locally stored override images for the card.
+ * @param card The card to remove the override images of.
+ */
+ static void deleteAllLocalOverrides(const ExactCard &card);
/**
* @brief Clears the in-memory QPixmap cache for all cards.
@@ -120,7 +127,9 @@ public slots:
* @param image Loaded QImage.
*/
void imageLoaded(const ExactCard &card, const QImage &image);
- void saveCardImageToLocalStorage(const ExactCard &card, const QPixmap &pixmap);
+ void saveCardImageToLocalStorage(const ExactCard &card, const QPixmap &pixmap, bool allowOverwrite = false);
+ void installPrintingOverride(const ExactCard &originalCard, const ExactCard &overrideCard);
+ void installPrintingOverrideOnLoad(const ExactCard &originalCard, const ExactCard &overrideCard);
private slots:
/**
@@ -134,6 +143,12 @@ private slots:
* Clears the QPixmap cache to reload images.
*/
void picsPathChanged();
+
+ /**
+ * @brief Triggered when the card language setting changes.
+ * Clears the in-memory picture caches so images reload in the new language.
+ */
+ void cardLangChanged();
};
#endif
diff --git a/cockatrice/src/interface/card_picture_loader/card_picture_loader_local.cpp b/cockatrice/src/interface/card_picture_loader/card_picture_loader_local.cpp
index 39621839a..c82fca403 100644
--- a/cockatrice/src/interface/card_picture_loader/card_picture_loader_local.cpp
+++ b/cockatrice/src/interface/card_picture_loader/card_picture_loader_local.cpp
@@ -94,6 +94,10 @@ QImage CardPictureLoaderLocal::tryLoadCardImageFromDisk(const QString &setName,
candidatePaths << picsPath + "/downloadedPics/" + setName + "/" + nameVariant;
}
+ // Non-set-folder export schemes (e.g., Name_Set_Collector) write straight into
+ // downloadedPics/; check there as a fallback so local overrides round-trip.
+ candidatePaths << picsPath + "/downloadedPics/" + nameVariant;
+
for (const QString &path : candidatePaths) {
QFileInfo fileInfo(path);
QDir dir = fileInfo.dir();
@@ -105,7 +109,8 @@ QImage CardPictureLoaderLocal::tryLoadCardImageFromDisk(const QString &setName,
QStringList files = dir.entryList(QDir::Files);
for (const QString &file : files) {
- if (!file.startsWith(baseName)) {
+ QFileInfo fi(file);
+ if (fi.completeBaseName() != baseName) {
continue;
}
diff --git a/cockatrice/src/interface/card_picture_loader/card_picture_loader_worker.cpp b/cockatrice/src/interface/card_picture_loader/card_picture_loader_worker.cpp
index d288236d2..34092f361 100644
--- a/cockatrice/src/interface/card_picture_loader/card_picture_loader_worker.cpp
+++ b/cockatrice/src/interface/card_picture_loader/card_picture_loader_worker.cpp
@@ -84,8 +84,8 @@ void CardPictureLoaderWorker::queueRequest(const QUrl &url, CardPictureLoaderWor
SettingsCache::instance().cacheStorage().getCardPictureLoaderCacheMethod()) ==
CardPictureLoaderCacheMethod::CacheMethod::NETWORK_CACHE &&
cache->metaData(url).isValid()) {
- // If we hit a cached url, we get to make the request for free, since it won't contribute towards the
- // rate-limit
+ // A request that will be served from the disk cache never touches the network and therefore
+ // doesn't use up any of the rate limit, so it gets to skip the queue.
makeRequest(url, worker);
return;
}
@@ -107,10 +107,13 @@ QNetworkReply *CardPictureLoaderWorker::makeRequest(const QUrl &url, CardPicture
req.setHeader(QNetworkRequest::UserAgentHeader, QString("Cockatrice %1").arg(VERSION_STRING));
req.setRawHeader("Accept", "image/avif,image/webp,image/apng,image/,/*;q=0.8");
- bool useNetworkCache =
- !picDownload && static_cast(
- SettingsCache::instance().cacheStorage().getCardPictureLoaderCacheMethod()) ==
- CardPictureLoaderCacheMethod::CacheMethod::NETWORK_CACHE;
+ // Cached entries are served straight from the disk cache even when picture downloads are
+ // enabled: re-fetching an already-cached image would burn the rate limit for nothing. Only a
+ // genuine cache miss goes to the network, and only when downloads are enabled.
+ bool useNetworkCache = static_cast(
+ SettingsCache::instance().cacheStorage().getCardPictureLoaderCacheMethod()) ==
+ CardPictureLoaderCacheMethod::CacheMethod::NETWORK_CACHE &&
+ (cache->metaData(url).isValid() || !picDownload);
req.setAttribute(QNetworkRequest::CacheLoadControlAttribute,
useNetworkCache ? QNetworkRequest::AlwaysCache : QNetworkRequest::AlwaysNetwork);
diff --git a/cockatrice/src/interface/card_picture_loader/card_picture_to_load.cpp b/cockatrice/src/interface/card_picture_loader/card_picture_to_load.cpp
index 5f4ff0bbd..7cb502e92 100644
--- a/cockatrice/src/interface/card_picture_loader/card_picture_to_load.cpp
+++ b/cockatrice/src/interface/card_picture_loader/card_picture_to_load.cpp
@@ -94,7 +94,8 @@ void CardPictureToLoad::populateSetUrls()
}
}
- for (const QString &urlTemplate : urlTemplates) {
+ const QStringList orderedTemplates = urlTemplates;
+ for (const QString &urlTemplate : orderedTemplates) {
QString transformedUrl = transformUrl(urlTemplate);
if (!transformedUrl.isEmpty()) {
@@ -282,8 +283,15 @@ QString CardPictureToLoad::transformUrl(const QString &urlTemplate) const
}
// language setting
- transformMap["!sflang!"] = QString(QCoreApplication::translate(
- "PictureLoader", "en", "code for scryfall's language property, not available for all languages"));
+ const QString cardLang = SettingsCache::instance().cardsDisplay().getCardLang();
+ transformMap["!sflang!"] = cardLang;
+
+ // The localized printing's own id is unknown, so Scryfall must resolve it by
+ // its translated name (see populateSetUrls); expose that name for the
+ // `/cards/named` template.
+ if (cardLang != "en") {
+ transformMap["!localizedName!"] = card.getInfo().getLocalizedName(cardLang);
+ }
QString transformedUrl = urlTemplate;
for (const QString &prop : transformMap.keys()) {
diff --git a/cockatrice/src/interface/deck_loader/deck_file_format.h b/cockatrice/src/interface/deck_loader/deck_file_format.h
index 995de32c0..3a25797ec 100644
--- a/cockatrice/src/interface/deck_loader/deck_file_format.h
+++ b/cockatrice/src/interface/deck_loader/deck_file_format.h
@@ -17,7 +17,7 @@ enum Format
PlainText,
/**
- * This is cockatrice's native deck file format, and supports deck metadata such as banner cards and tags.
+ * This is Cockatrice's native deck file format, and supports deck metadata such as banner cards and tags.
* Stored as .cod files.
*/
Cockatrice
diff --git a/cockatrice/src/interface/deck_loader/deck_loader.cpp b/cockatrice/src/interface/deck_loader/deck_loader.cpp
index f03339da8..f29b4eed2 100644
--- a/cockatrice/src/interface/deck_loader/deck_loader.cpp
+++ b/cockatrice/src/interface/deck_loader/deck_loader.cpp
@@ -50,7 +50,7 @@ DeckLoader::loadFromFile(const QString &fileName, DeckFileFormat::Format fmt, bo
result = deckList.loadFromFile_Native(&file);
if (!result) {
qCInfo(DeckLoaderLog) << "Failed to load " << fileName
- << "as cockatrice format; retrying as plain format";
+ << "as Cockatrice format; retrying as plain format";
file.seek(0);
result = deckList.loadFromFile_Plain(&file, CardNameNormalizer());
fmt = DeckFileFormat::PlainText;
diff --git a/cockatrice/src/interface/deck_loader/deck_loader.h b/cockatrice/src/interface/deck_loader/deck_loader.h
index b851c6895..be0df311d 100644
--- a/cockatrice/src/interface/deck_loader/deck_loader.h
+++ b/cockatrice/src/interface/deck_loader/deck_loader.h
@@ -131,7 +131,7 @@ public:
static void printDeckList(QPrinter *printer, const DeckList &deckList);
/**
- * Converts the given deck's file to the cockatrice file format.
+ * Converts the given deck's file to the Cockatrice file format.
* Uses the lastLoadInfo in the LoadedDeck to determine the current name of the file and where to save to.
* @param deck The deck to convert. Should have valid lastLoadInfo. Will update the lastLoadInfo.
* @return Whether the conversion succeeded.
diff --git a/cockatrice/src/interface/palette_editor/palette_editor_dialog.cpp b/cockatrice/src/interface/palette_editor/palette_editor_dialog.cpp
index 9cde72c01..d5a168708 100644
--- a/cockatrice/src/interface/palette_editor/palette_editor_dialog.cpp
+++ b/cockatrice/src/interface/palette_editor/palette_editor_dialog.cpp
@@ -1,6 +1,5 @@
#include "palette_editor_dialog.h"
-#include "../../client/settings/cache_settings.h"
#include "../theme_manager.h"
#include "palette_generator.h"
#include "palette_grid_widget.h"
@@ -11,31 +10,11 @@
#include
#include
#include
-#include
#include
-#include
#include
-#include
#include
#include
-#include
#include
-#include
-
-// Probe whether a directory is truly writable by trying to create and remove a
-// temporary file. QFileInfo::isWritable() on a directory is unreliable (notably
-// on Windows where UAC VirtualStore can make a system dir appear writable).
-static bool isDirReallyWritable(const QString &dirPath)
-{
- const QString probe = QDir(dirPath).absoluteFilePath(".cockatrice_write_test");
- QFile f(probe);
- if (!f.open(QIODevice::WriteOnly)) {
- return false;
- }
- f.close();
- f.remove();
- return true;
-}
PaletteEditorDialog::PaletteEditorDialog(const QString &_themeDirPath, const QString &_themeName, QWidget *parent)
: QDialog(parent), themeDirPath(_themeDirPath), themeName(_themeName)
@@ -46,14 +25,7 @@ PaletteEditorDialog::PaletteEditorDialog(const QString &_themeDirPath, const QSt
// Resolve a writable directory for saving. Built-in (Default / Fusion) and
// other read-only theme directories must be customised in the user-writable
// themes directory; otherwise the write would fail or be lost on upgrade.
- if (!themeDirPath.isEmpty() && isDirReallyWritable(themeDirPath)) {
- saveDir = themeDirPath;
- } else {
- saveDir = QDir(SettingsCache::instance().paths().getThemesPath()).absoluteFilePath(themeName);
- if (!QDir().mkpath(saveDir)) {
- qWarning() << "Failed to create palette save directory:" << saveDir;
- }
- }
+ saveDir = ThemeManager::writableThemeDir(themeName);
// Load both scheme configs upfront so switching is instant
loadSchemes();
@@ -214,7 +186,7 @@ void PaletteEditorDialog::retranslateUi()
resetBtn->setToolTip(tr("Discard unsaved edits and restore the last saved palette"));
saveBtn->setToolTip(tr("Write palette-%1.toml and reload the theme").arg(loadedScheme.toLower()));
- if (saveDir.isEmpty() || !isDirReallyWritable(saveDir)) {
+ if (saveDir.isEmpty() || !ThemeManager::isDirReallyWritable(saveDir)) {
saveBtn->setEnabled(false);
saveBtn->setToolTip(tr("Cannot save: this theme has no writable directory"));
}
@@ -297,7 +269,7 @@ void PaletteEditorDialog::onSave()
if (it.key() == loadedScheme) {
continue;
}
- if (it.value().colors == savedConfig.value(it.key()).colors) {
+ if (it.value() == savedConfig.value(it.key())) {
continue;
}
if (!ThemeManager::commitPalette(saveDir, it.key(), it.value())) {
@@ -308,7 +280,7 @@ void PaletteEditorDialog::onSave()
}
// Commit the active scheme last so the global colour scheme matches.
- if (workingConfig[loadedScheme].colors != savedConfig.value(loadedScheme).colors) {
+ if (workingConfig[loadedScheme] != savedConfig.value(loadedScheme)) {
if (!ThemeManager::commitPalette(saveDir, loadedScheme, workingConfig[loadedScheme])) {
QMessageBox::warning(this, tr("Save failed"),
tr("Could not write %1 to:\n%2").arg(PaletteConfig::fileName(loadedScheme), saveDir));
diff --git a/cockatrice/src/interface/palette_editor/palette_generator.cpp b/cockatrice/src/interface/palette_editor/palette_generator.cpp
index d30dd14f1..822e57250 100644
--- a/cockatrice/src/interface/palette_editor/palette_generator.cpp
+++ b/cockatrice/src/interface/palette_editor/palette_generator.cpp
@@ -150,6 +150,17 @@ PaletteConfig fromAccent(const QColor &accent, int intensity, const QString &sch
cfg.colors[CG::Disabled][CR::HighlightedText] = disText;
cfg.colors[CG::Inactive][CR::HighlightedText] = dark ? Qt::white : Qt::black;
+ // Accent: same primary hue as Highlight, so palettes derived from a
+ // QuickSetup accent always carry a matching Accent role.
+#if QT_VERSION >= QT_VERSION_CHECK(6, 6, 0)
+ set3(CR::Accent, hl, disText, hl);
+#endif
+
+ // Application role colors: Strong tracks the primary accent, while Soft is
+ // the lightened, desaturated companion used for button-gradient highlights.
+ cfg.appColors[AppColor::AccentStrong] = hl;
+ cfg.appColors[AppColor::AccentSoft] = hsl(accent.lightness() + 60, qRound(accent.hslSaturation() * 70 / 100.0));
+
// BrightText
QColor bright;
if (achromatic) {
diff --git a/cockatrice/src/interface/palette_editor/palette_grid_widget.cpp b/cockatrice/src/interface/palette_editor/palette_grid_widget.cpp
index 67294cd98..97d28b731 100644
--- a/cockatrice/src/interface/palette_editor/palette_grid_widget.cpp
+++ b/cockatrice/src/interface/palette_editor/palette_grid_widget.cpp
@@ -1,5 +1,7 @@
#include "palette_grid_widget.h"
+#include "../theme_manager.h"
+
#include
#include
#include
@@ -45,6 +47,11 @@ static const QMap ROLE_DESCRIPTIONS = {
{QPalette::Shadow, QT_TR_NOOP("Very dark shadow colour")},
};
+static const QMap APP_ROLE_DESCRIPTIONS = {
+ {AppColor::AccentStrong, QT_TR_NOOP("Vivid primary accent (e.g. home-tab button gradient start)")},
+ {AppColor::AccentSoft, QT_TR_NOOP("Lightened, desaturated accent (e.g. home-tab button gradient end)")},
+};
+
PaletteGridWidget::PaletteGridWidget(QWidget *parent) : QWidget(parent)
{
scroll = new QScrollArea(this);
@@ -122,6 +129,46 @@ void PaletteGridWidget::buildGrid(QWidget *host)
grid->addWidget(btn, row + 1, col + 1, Qt::AlignHCenter | Qt::AlignVCenter);
}
}
+
+ // Application color section: one ColorButton per role below the role grid.
+ // These are not tied to a color group, so a single button spans the row.
+ QMetaEnum appEnum = QMetaEnum::fromType();
+
+ const int appHeaderRow = roles.size() + 1;
+
+ auto *appHeader = new QLabel(tr("App colors"), host);
+ appHeader->setToolTip(tr("Application-specific colors layered on top of the Qt palette"));
+ QFont appHeaderFont = appHeader->font();
+ appHeaderFont.setBold(true);
+ appHeader->setFont(appHeaderFont);
+ appHeader->setAutoFillBackground(true);
+ appHeader->setContentsMargins(4, 4, 4, 4);
+ grid->addWidget(appHeader, appHeaderRow, 0, 1, 4);
+ headerLabels.append(appHeader);
+
+ for (int i = 0; i < appEnum.keyCount(); ++i) {
+ auto role = static_cast(appEnum.value(i));
+ const int row = appHeaderRow + 1 + i;
+
+ if (i % 2 == 0) {
+ for (int col = 0; col < 4; ++col) {
+ auto *shade = new QWidget(host);
+ shade->setAutoFillBackground(true);
+ grid->addWidget(shade, row, col);
+ rowShadeWidgets.push_back(shade);
+ }
+ }
+
+ auto *label = new QLabel(QString(appEnum.valueToKey(role)), host);
+ label->setToolTip(APP_ROLE_DESCRIPTIONS.value(role, {}));
+ label->setContentsMargins(4, 2, 8, 2);
+ grid->addWidget(label, row, 0);
+
+ auto *btn = new ColorButton(host);
+ connect(btn, &ColorButton::colorChanged, this, [this] { emit paletteChanged(); });
+ appColorButtons[role] = btn;
+ grid->addWidget(btn, row, 1, Qt::AlignHCenter | Qt::AlignVCenter);
+ }
}
void PaletteGridWidget::changeEvent(QEvent *e)
@@ -166,6 +213,16 @@ void PaletteGridWidget::loadPalette(const PaletteConfig &cfg)
colorButtons[group][role]->setColor(color);
}
}
+
+ QMetaEnum appEnum = QMetaEnum::fromType();
+ for (int i = 0; i < appEnum.keyCount(); ++i) {
+ auto role = static_cast(appEnum.value(i));
+ QColor color = cfg.appColors.value(role);
+ if (!color.isValid()) {
+ color = themeManager->appColor(role);
+ }
+ appColorButtons[role]->setColor(color);
+ }
}
PaletteConfig PaletteGridWidget::currentPaletteConfig() const
@@ -176,5 +233,12 @@ PaletteConfig PaletteGridWidget::currentPaletteConfig() const
cfg.colors[group][role] = colorButtons[group][role]->getColor();
}
}
+
+ QMetaEnum appEnum = QMetaEnum::fromType();
+ for (int i = 0; i < appEnum.keyCount(); ++i) {
+ auto role = static_cast(appEnum.value(i));
+ cfg.appColors[role] = appColorButtons[role]->getColor();
+ }
+
return cfg;
}
\ No newline at end of file
diff --git a/cockatrice/src/interface/palette_editor/palette_grid_widget.h b/cockatrice/src/interface/palette_editor/palette_grid_widget.h
index 1a665971a..77cbf1c62 100644
--- a/cockatrice/src/interface/palette_editor/palette_grid_widget.h
+++ b/cockatrice/src/interface/palette_editor/palette_grid_widget.h
@@ -31,6 +31,7 @@ private:
void refreshChromePalettes();
QMap> colorButtons;
+ QMap appColorButtons;
QScrollArea *scroll;
QWidget *gridHost;
QVBoxLayout *layout;
diff --git a/cockatrice/src/interface/pixel_map_generator.cpp b/cockatrice/src/interface/pixel_map_generator.cpp
index 9b8c4bcdc..b70dc576f 100644
--- a/cockatrice/src/interface/pixel_map_generator.cpp
+++ b/cockatrice/src/interface/pixel_map_generator.cpp
@@ -1,5 +1,7 @@
#include "pixel_map_generator.h"
+#include "theme_manager.h"
+
#include
#include
#include
@@ -82,7 +84,13 @@ static QPixmap loadSvg(const QString &svgPath, const QSize &size, bool expandOnl
/**
* Try to load path image from non-SVG formats, otherwise fall back to SVG.
* This is to allow custom themes to support non-SVG format type overrides, since SVG requires custom loading.
- * @param path The path to the file, with no file extension. File formats will be automatically detected.
+ *
+ * The path may already carry the resolved file extension (e.g. via
+ * ThemeManager::assetPath); such paths are loaded directly. Otherwise a
+ * format-agnostic lookup probes png, jpg and finally svg.
+ *
+ * @param path The path to the file, with no file extension unless the caller
+ * already resolved it. File formats will be automatically detected.
* @param size The desired size of the pixmap.
* @param expandOnly If true, then keep the size of the initial pixmap to at least the size (Only relevant if SVG).
*
@@ -90,6 +98,19 @@ static QPixmap loadSvg(const QString &svgPath, const QSize &size, bool expandOnl
*/
static QPixmap tryLoadImage(const QString &path, const QSize &size, bool expandOnly = false)
{
+ if (path.endsWith(QLatin1String(".svg"), Qt::CaseInsensitive)) {
+ return loadSvg(path, size, expandOnly);
+ }
+ if (path.endsWith(QLatin1String(".png"), Qt::CaseInsensitive) ||
+ path.endsWith(QLatin1String(".jpg"), Qt::CaseInsensitive) ||
+ path.endsWith(QLatin1String(".jpeg"), Qt::CaseInsensitive)) {
+ QPixmap pix(path);
+ if (!pix.isNull()) {
+ return pix.scaled(size, Qt::KeepAspectRatio, Qt::SmoothTransformation);
+ }
+ return {};
+ }
+
const auto formats = {"png", "jpg"};
QPixmap returnPixmap;
@@ -111,7 +132,8 @@ QPixmap PhasePixmapGenerator::generatePixmap(int height, QString name)
return pmCache.value(key);
}
- QPixmap pixmap = tryLoadImage("theme:phases/" + name, QSize(height, height));
+ QPixmap pixmap = tryLoadImage(QStringLiteral("theme:") + themeManager->assetPath(QStringLiteral("phases/") + name),
+ QSize(height, height));
pmCache.insert(key, pixmap);
return pixmap;
@@ -339,6 +361,10 @@ static QString getIconType(const bool isBuddy, const UserLevelFlags &userLevelFl
return "pawn_judge";
}
+ if (userLevelFlags.testFlag(ServerInfo_User::IsDeveloper)) {
+ return "pawn_dev";
+ }
+
if (!privLevel.isEmpty() && privLevel.toLower() != "none") {
return QString("pawn_%1").arg(privLevel.toLower());
}
@@ -396,7 +422,8 @@ QPixmap LockPixmapGenerator::generatePixmap(int height)
return pmCache.value(key);
}
- QPixmap pixmap = tryLoadImage("theme:icons/lock", QSize(height, height), true);
+ QPixmap pixmap = tryLoadImage(QStringLiteral("theme:") + themeManager->assetPath(QStringLiteral("icons/lock")),
+ QSize(height, height), true);
pmCache.insert(key, pixmap);
return pixmap;
}
@@ -411,7 +438,8 @@ QPixmap DropdownIconPixmapGenerator::generatePixmap(int height, bool expanded)
}
QString name = expanded ? "dropdown_expanded" : "dropdown_collapsed";
- QPixmap pixmap = tryLoadImage("theme:icons/" + name, QSize(height, height), true);
+ QPixmap pixmap = tryLoadImage(QStringLiteral("theme:") + themeManager->assetPath(QStringLiteral("icons/") + name),
+ QSize(height, height), true);
pmCache.insert(key, pixmap);
return pixmap;
@@ -472,6 +500,13 @@ QHash ManaSymbolPixmapGenerator::scaledCache;
QPixmap loadColorAdjustedPixmap(const QString &name)
{
+ // Prefer an authored scheme-qualified variant when one exists for this asset.
+ const QString variant = themeManager->schemeVariantPath(QStringView(name).mid(QStringLiteral("theme:").size()));
+ if (!variant.isEmpty()) {
+ return QPixmap(QStringLiteral("theme:") + variant);
+ }
+
+ // Legacy fallback: runtime-invert for dark mode when no authored variant.
if (qApp->palette().windowText().color().lightness() > 200) {
QImage img(name);
img.invertPixels();
@@ -482,3 +517,21 @@ QPixmap loadColorAdjustedPixmap(const QString &name)
return QPixmap(name);
}
}
+
+QPixmap themePixmap(QStringView prefix)
+{
+ const QString resolved = themeManager->assetPath(prefix);
+ return QPixmap(QStringLiteral("theme:") + resolved);
+}
+
+void clearPixmapGeneratorCaches()
+{
+ PhasePixmapGenerator::clear();
+ CounterPixmapGenerator::clear();
+ PingPixmapGenerator::clear();
+ CountryPixmapGenerator::clear();
+ UserLevelPixmapGenerator::clear();
+ LockPixmapGenerator::clear();
+ DropdownIconPixmapGenerator::clear();
+ ManaSymbolPixmapGenerator::clear();
+}
diff --git a/cockatrice/src/interface/pixel_map_generator.h b/cockatrice/src/interface/pixel_map_generator.h
index 17720166a..b6e822fd9 100644
--- a/cockatrice/src/interface/pixel_map_generator.h
+++ b/cockatrice/src/interface/pixel_map_generator.h
@@ -156,4 +156,15 @@ public:
QPixmap loadColorAdjustedPixmap(const QString &name);
+// Loads a "theme:" asset (with no file extension in prefix), preferring the
+// scheme-qualified variant (prefix-dark / prefix-light, resolved via
+// ThemeManager::assetPath) and falling back to the plain asset. Callers load
+// the returned path directly. Use for scheme-sensitive pixmaps like
+// backgrounds, the card back, and the app logo.
+QPixmap themePixmap(QStringView prefix);
+
+// Clears every PixmapGenerator's static cache so scheme variants are
+// re-resolved when the active theme or color scheme changes.
+void clearPixmapGeneratorCaches();
+
#endif
diff --git a/cockatrice/src/interface/theme_config.cpp b/cockatrice/src/interface/theme_config.cpp
index 3c43c467d..8293a82cf 100644
--- a/cockatrice/src/interface/theme_config.cpp
+++ b/cockatrice/src/interface/theme_config.cpp
@@ -16,7 +16,7 @@ QString ThemeConfig::toIni() const
out += "[Appearance]\n";
out += QString("ColorScheme = %1\n").arg(colorScheme.isEmpty() ? "System" : colorScheme);
out += "\n[Style]\n";
- out += QString("Name = %1\n").arg(styleName.isEmpty() ? "Default" : styleName);
+ out += QString("Name = %1\n").arg(styleName.isEmpty() ? "System" : styleName);
return out;
}
@@ -96,7 +96,7 @@ bool ThemeConfig::save(const QString &themeDirPath) const
bool PaletteConfig::hasPalette() const
{
- return !colors.isEmpty();
+ return !colors.isEmpty() || !appColors.isEmpty();
}
QString PaletteConfig::toToml() const
@@ -133,6 +133,24 @@ QString PaletteConfig::toToml() const
out += "\n";
}
+ if (!appColors.isEmpty()) {
+ QMetaEnum appEnum = QMetaEnum::fromType();
+
+ out += "[AppColors]\n";
+
+ for (auto it = appColors.cbegin(); it != appColors.cend(); ++it) {
+ const char *roleName = appEnum.valueToKey(it.key());
+
+ if (!roleName) {
+ continue;
+ }
+
+ out += QString("%1 = %2\n").arg(QString(roleName), -20).arg(it.value().name(QColor::HexArgb));
+ }
+
+ out += "\n";
+ }
+
return out;
}
@@ -152,6 +170,7 @@ PaletteConfig PaletteConfig::fromFile(const QString &filePath)
}
QMetaEnum roleEnum = QMetaEnum::fromType();
+ QMetaEnum appEnum = QMetaEnum::fromType();
QString currentSection;
QPalette::ColorGroup currentGroup = QPalette::Active;
@@ -202,6 +221,26 @@ PaletteConfig PaletteConfig::fromFile(const QString &filePath)
}
}
+ QColor color(value);
+
+ if (!color.isValid()) {
+ continue;
+ }
+
+ if (currentSection.compare("AppColors", Qt::CaseInsensitive) == 0) {
+ if (key.startsWith("AppColor::")) {
+ key = key.mid(10);
+ }
+
+ int appRoleInt = appEnum.keyToValue(key.toUtf8().constData());
+
+ if (appRoleInt >= 0) {
+ cfg.appColors[static_cast(appRoleInt)] = color;
+ }
+
+ continue;
+ }
+
if (!currentSection.startsWith("Palette", Qt::CaseInsensitive)) {
continue;
}
@@ -216,11 +255,7 @@ PaletteConfig PaletteConfig::fromFile(const QString &filePath)
continue;
}
- QColor color(value);
-
- if (color.isValid()) {
- cfg.colors[currentGroup][static_cast(roleInt)] = color;
- }
+ cfg.colors[currentGroup][static_cast(roleInt)] = color;
}
return cfg;
diff --git a/cockatrice/src/interface/theme_config.h b/cockatrice/src/interface/theme_config.h
index 07bf55b7a..567aeccda 100644
--- a/cockatrice/src/interface/theme_config.h
+++ b/cockatrice/src/interface/theme_config.h
@@ -3,9 +3,25 @@
#include
#include
+#include
#include
#include
+// Application-specific color roles, layered on top of the fixed QPalette role
+// set. Stored in the same palette-.toml under an [AppColors] section
+// and editable from the palette editor, so theme authors can control colors
+// beyond what Qt's palette can express.
+namespace AppColor
+{
+Q_NAMESPACE
+enum Role
+{
+ AccentStrong,
+ AccentSoft,
+};
+Q_ENUM_NS(Role)
+} // namespace AppColor
+
struct ThemeConfig
{
QString colorScheme;
@@ -21,7 +37,16 @@ struct ThemeConfig
struct PaletteConfig
{
QMap> colors;
+ QMap appColors;
+ bool operator==(const PaletteConfig &rhs) const
+ {
+ return colors == rhs.colors && appColors == rhs.appColors;
+ }
+ bool operator!=(const PaletteConfig &rhs) const
+ {
+ return !(*this == rhs);
+ }
bool hasPalette() const;
QString toToml() const;
diff --git a/cockatrice/src/interface/theme_manager.cpp b/cockatrice/src/interface/theme_manager.cpp
index e6b4b3c7f..d86ed77f9 100644
--- a/cockatrice/src/interface/theme_manager.cpp
+++ b/cockatrice/src/interface/theme_manager.cpp
@@ -1,10 +1,13 @@
#include "theme_manager.h"
#include "../../client/settings/cache_settings.h"
+#include "pixel_map_generator.h"
#include
#include
#include
+#include
+#include
#include
#include
#include
@@ -19,7 +22,7 @@
#include
#include
-#define NONE_THEME_NAME "Default"
+#define SYSTEM_THEME_NAME "System"
#define FUSION_THEME_NAME "Fusion"
#define STYLE_CSS_NAME "style.css"
#define HANDZONE_BG_NAME "handzone"
@@ -93,7 +96,7 @@ struct PaletteColorInfo
static QString usableDefaultStyle(const QString &style)
{
// The Windows 11 native style is broken: when the OS default
- // ("Default" theme selection) would use it, fall back to the Vista style.
+ // ("System" theme selection) would use it, fall back to the Vista style.
// Explicitly choosing "windows11" in a theme is still honored.
return style.compare("windows11", Qt::CaseInsensitive) == 0 ? QStringLiteral("windowsvista") : style;
}
@@ -116,10 +119,16 @@ ThemeManager::ThemeManager(QObject *parent) : QObject(parent)
void ThemeManager::ensureThemeDirectoryExists()
{
- if (SettingsCache::instance().getThemeName().isEmpty() ||
- !getAvailableThemes().contains(SettingsCache::instance().getThemeName())) {
+ auto &settings = SettingsCache::instance();
+
+ // Migrate the old "Default" theme name to "System"
+ if (settings.getThemeName() == "Default") {
+ settings.setThemeName(SYSTEM_THEME_NAME);
+ }
+
+ if (settings.getThemeName().isEmpty() || !getAvailableThemes().contains(settings.getThemeName())) {
qCInfo(ThemeManagerLog) << "Theme name not set, setting default value";
- SettingsCache::instance().setThemeName(NONE_THEME_NAME);
+ settings.setThemeName(FUSION_THEME_NAME);
}
}
@@ -140,11 +149,74 @@ bool ThemeManager::isDarkMode(const QString &themeDirPath) const
}
}
-bool ThemeManager::isBuiltInTheme()
+QString ThemeManager::schemeVariantPath(QStringView prefix) const
{
- const auto themeName = SettingsCache::instance().getThemeName();
+ static const QStringList formats = {QStringLiteral(".png"), QStringLiteral(".jpg"), QStringLiteral(".jpeg"),
+ QStringLiteral(".svg")};
+ const QString scheme = isDarkMode(currentThemePath) ? QStringLiteral("dark") : QStringLiteral("light");
+ const QString variantStem = prefix.toString() + QLatin1Char('-') + scheme;
- return themeName == NONE_THEME_NAME || themeName == FUSION_THEME_NAME;
+ for (const QString &format : formats) {
+ if (QFileInfo::exists(QStringLiteral("theme:") + variantStem + format)) {
+ return variantStem + format;
+ }
+ }
+ return QString();
+}
+
+QString ThemeManager::assetPath(QStringView prefix) const
+{
+ // Probe order mirrors tryLoadImage: a theme may override the default SVG
+ // with a raster of the same stem, so raster wins over SVG within a stem.
+ static const QStringList formats = {QStringLiteral(".png"), QStringLiteral(".jpg"), QStringLiteral(".jpeg"),
+ QStringLiteral(".svg")};
+
+ auto findExisting = [](const QString &stem) {
+ for (const QString &format : formats) {
+ if (QFileInfo::exists(QStringLiteral("theme:") + stem + format)) {
+ return stem + format;
+ }
+ }
+ return QString();
+ };
+
+ // Prefer the scheme-qualified variant when it exists, else the plain
+ // asset as the super fallback. Both return the resolved path including
+ // its file extension so callers can load it directly.
+ const QString variant = schemeVariantPath(prefix);
+ if (!variant.isEmpty()) {
+ return variant;
+ }
+ const QString resolvedPlain = findExisting(prefix.toString());
+ return resolvedPlain.isEmpty() ? prefix.toString() : resolvedPlain;
+}
+
+// Probe whether a directory is truly writable by trying to create and remove a
+// temporary file. QFileInfo::isWritable() on a directory is unreliable (notably
+// on Windows where UAC VirtualStore can make a system dir appear writable).
+bool ThemeManager::isDirReallyWritable(const QString &dirPath)
+{
+ const QString probe = QDir(dirPath).absoluteFilePath(".cockatrice_write_test");
+ QFile f(probe);
+ if (!f.open(QIODevice::WriteOnly)) {
+ return false;
+ }
+ f.close();
+ f.remove();
+ return true;
+}
+
+QString ThemeManager::writableThemeDir(const QString &themeName)
+{
+ // All theme writes go to the user themes directory regardless of whether
+ // the resolved (system) theme directory happens to be writable. Even when a
+ // write would succeed in-place, routing it to the user directory keeps the
+ // install intact and guarantees changes survive upgrades.
+ const QString dirPath = QDir(SettingsCache::instance().paths().getThemesPath()).absoluteFilePath(themeName);
+ if (!QDir().mkpath(dirPath)) {
+ qWarning() << "Failed to create theme save directory:" << dirPath;
+ }
+ return dirPath;
}
// System (read-only) themes location, relative to the application binary.
@@ -169,9 +241,7 @@ QStringMap &ThemeManager::getAvailableThemes()
// load themes from user profile dir
dir.setPath(SettingsCache::instance().paths().getThemesPath());
- // add default value
- availableThemes.insert(NONE_THEME_NAME, dir.absoluteFilePath("Default"));
-
+ availableThemes.insert(SYSTEM_THEME_NAME, dir.absoluteFilePath("System"));
availableThemes.insert(FUSION_THEME_NAME, dir.absoluteFilePath("Fusion"));
for (QString themeName : dir.entryList(QDir::AllDirs | QDir::NoDotAndDotDot, QDir::Name)) {
@@ -180,7 +250,7 @@ QStringMap &ThemeManager::getAvailableThemes()
}
}
- // load themes from cockatrice system dir
+ // Load themes from Cockatrice system dir
dir.setPath(systemThemesBasePath());
for (QString themeName : dir.entryList(QDir::AllDirs | QDir::NoDotAndDotDot, QDir::Name)) {
@@ -195,7 +265,7 @@ QStringMap &ThemeManager::getAvailableThemes()
QBrush ThemeManager::loadBrush(QString fileName, QColor fallbackColor)
{
QBrush brush;
- QPixmap tmp = QPixmap("theme:zones/" + fileName);
+ QPixmap tmp = QPixmap("theme:" + assetPath(QStringLiteral("zones/") + fileName));
if (tmp.isNull()) {
brush.setColor(fallbackColor);
brush.setStyle(Qt::SolidPattern);
@@ -209,7 +279,7 @@ QBrush ThemeManager::loadBrush(QString fileName, QColor fallbackColor)
QBrush ThemeManager::loadExtraBrush(QString fileName, QBrush &fallbackBrush)
{
QBrush brush;
- QPixmap tmp = QPixmap("theme:zones/" + fileName);
+ QPixmap tmp = QPixmap("theme:" + assetPath(QStringLiteral("zones/") + fileName));
if (tmp.isNull()) {
brush = fallbackBrush;
@@ -287,7 +357,7 @@ bool ThemeManager::commitPalette(const QString &themeDirPath, const QString &col
void ThemeManager::setColorScheme(const QString &scheme)
{
- const QString dirPath = getAvailableThemes().value(SettingsCache::instance().getThemeName());
+ const QString dirPath = writableThemeDir(SettingsCache::instance().getThemeName());
ThemeConfig cfg = ThemeConfig::fromThemeDir(dirPath);
cfg.colorScheme = scheme;
@@ -298,7 +368,7 @@ void ThemeManager::setColorScheme(const QString &scheme)
void ThemeManager::setStyleName(const QString &styleName)
{
- const QString dirPath = getAvailableThemes().value(SettingsCache::instance().getThemeName());
+ const QString dirPath = writableThemeDir(SettingsCache::instance().getThemeName());
ThemeConfig cfg = ThemeConfig::fromThemeDir(dirPath);
cfg.styleName = styleName;
@@ -329,7 +399,7 @@ void ThemeManager::applyStyleAndPalette(const QString &themeName,
Q_UNUSED(activeScheme)
#endif
QString styleName = themeCfg.styleName;
- if (styleName.isEmpty() || styleName.compare("Default", Qt::CaseInsensitive) == 0) {
+ if (styleName.isEmpty() || styleName.compare("System", Qt::CaseInsensitive) == 0) {
if (themeName == FUSION_THEME_NAME) {
styleName = "Fusion";
} else {
@@ -372,6 +442,8 @@ void ThemeManager::applyStyleAndPalette(const QString &themeName,
qApp->setPalette(base);
qApp->setStyle(style);
+ currentAppColors = palCfg.appColors;
+
// Force every widget to re-polish and repaint immediately rather than
// waiting for natural expose events, which produces a patchwork of old
// and new colours during a live preview.
@@ -384,6 +456,35 @@ void ThemeManager::applyStyleAndPalette(const QString &themeName,
style->polish(widget);
widget->update();
}
+
+ emit paletteChanged();
+}
+
+QColor ThemeManager::appColor(AppColor::Role role) const
+{
+ const auto it = currentAppColors.constFind(role);
+ if (it != currentAppColors.constEnd()) {
+ return it.value();
+ }
+
+ // QPalette::Accent was introduced in Qt 6.6 and several shipped palettes
+ // set it to a value barely distinguishable from Window, so it is not a
+ // reliable accent source. The selection highlight is the stable accent
+ // (Accent defaults to Highlight when unset), and deriving from it
+ // unconditionally keeps every Qt version rendering identically.
+ const QColor accent = qApp->palette().color(QPalette::Active, QPalette::Highlight);
+
+ if (role == AppColor::AccentSoft) {
+ constexpr int SOFT_SATURATION_PERCENT = 70;
+ constexpr int SOFT_LIGHTNESS_OFFSET = 60;
+
+ // Light end of the gradient: same hue, softened and lightened
+ return QColor::fromHsl(qMax(0, accent.hslHue()),
+ qBound(0, qRound(accent.hslSaturation() * SOFT_SATURATION_PERCENT / 100.0), 255),
+ qBound(0, accent.lightness() + SOFT_LIGHTNESS_OFFSET, 255));
+ }
+
+ return accent;
}
void ThemeManager::themeChangedSlot()
@@ -393,9 +494,19 @@ void ThemeManager::themeChangedSlot()
currentThemePath = dirPath;
QDir dir(dirPath);
- // CSS
- if (!dirPath.isEmpty() && dir.exists(STYLE_CSS_NAME)) {
- qApp->setStyleSheet("file:///" + dir.absoluteFilePath(STYLE_CSS_NAME));
+ // CSS — prefer the scheme-qualified stylesheet (style-dark.css /
+ // style-light.css) when present, else the plain style.css as fallback.
+ if (!dirPath.isEmpty()) {
+ const QString scheme = isDarkMode(dirPath) ? QStringLiteral("dark") : QStringLiteral("light");
+ const QString schemeCss = QFileInfo(QStringLiteral(STYLE_CSS_NAME)).completeBaseName() + QLatin1Char('-') +
+ scheme + QStringLiteral(".css");
+ if (dir.exists(schemeCss)) {
+ qApp->setStyleSheet("file:///" + dir.absoluteFilePath(schemeCss));
+ } else if (dir.exists(STYLE_CSS_NAME)) {
+ qApp->setStyleSheet("file:///" + dir.absoluteFilePath(STYLE_CSS_NAME));
+ } else {
+ qApp->setStyleSheet("");
+ }
} else {
qApp->setStyleSheet("");
}
@@ -410,8 +521,19 @@ void ThemeManager::themeChangedSlot()
// ── Load palette: custom first, then theme default ────────────────────
PaletteConfig palette = PaletteConfig::fromScheme(dirPath, activeScheme);
- if (!palette.hasPalette()) {
- palette = ThemeManager::loadDefaultPaletteConfig(dirPath, themeName, activeScheme);
+ const PaletteConfig themeDefault = ThemeManager::loadDefaultPaletteConfig(dirPath, themeName, activeScheme);
+ if (palette.hasPalette()) {
+ // A custom palette written before [AppColors] existed carries no app
+ // colors; merge the theme's shipped defaults so the identity colors
+ // survive (hasPalette() counts an app-colors-only file as a palette,
+ // so those are kept wholesale and never reach here empty).
+ for (auto it = themeDefault.appColors.cbegin(); it != themeDefault.appColors.cend(); ++it) {
+ if (!palette.appColors.contains(it.key())) {
+ palette.appColors.insert(it.key(), it.value());
+ }
+ }
+ } else {
+ palette = themeDefault;
}
applyStyleAndPalette(themeName, themeCfg, palette, activeScheme);
@@ -446,6 +568,7 @@ void ThemeManager::themeChangedSlot()
}
QPixmapCache::clear();
+ clearPixmapGeneratorCaches();
emit themeChanged();
}
diff --git a/cockatrice/src/interface/theme_manager.h b/cockatrice/src/interface/theme_manager.h
index 79a1b6470..aadb38ee9 100644
--- a/cockatrice/src/interface/theme_manager.h
+++ b/cockatrice/src/interface/theme_manager.h
@@ -50,6 +50,7 @@ private:
QString currentThemePath;
std::array brushes;
QStringMap availableThemes;
+ QMap currentAppColors;
/*
Internal cache for multiple backgrounds
*/
@@ -65,7 +66,16 @@ protected:
const QString &activeScheme);
public:
- bool isBuiltInTheme();
+ // Resolves the directory to write theme changes to for the given theme
+ // name. The resolved theme dir (user or system) is used when writable;
+ // read-only system themes fall back to the user themes directory, creating
+ // it if needed, so customisations never get lost on upgrade.
+ static QString writableThemeDir(const QString &themeName);
+ // Probe whether a directory is truly writable by trying to create and remove
+ // a temporary file. QFileInfo::isWritable() on a directory is unreliable
+ // (notably on Windows where UAC VirtualStore can make a system dir appear
+ // writable).
+ static bool isDirReallyWritable(const QString &dirPath);
// Explicit color scheme of the theme: theme.cfg's ColorScheme setting
// (Dark/Light), falling back to the OS color scheme when it is "System".
bool isDarkMode(const QString &themeDirPath) const;
@@ -87,6 +97,20 @@ public:
// Load/save per-scheme palette colors
static PaletteConfig loadPaletteConfig(const QString &themeDirPath, const QString &colorScheme);
static bool savePaletteConfig(const QString &themeDirPath, const QString &colorScheme, const PaletteConfig &cfg);
+ // Resolve prefix to a scheme-qualified "theme:" path. Existence is probed
+ // internally across the formats themes may ship (.png/.jpg/.svg), so
+ // callers load the returned path directly. Prefers "-"
+ // when a file exists at that stem, otherwise the plain "" as the
+ // super fallback. The resolved scheme covers explicit light/dark as well
+ // as OS-resolved "system". Returns the path with its file extension when a
+ // match is found; unqualified assets keep working unchanged.
+ QString assetPath(QStringView prefix) const;
+ // Like assetPath, but resolves only the scheme-qualified variant
+ // ("-.") and returns an empty string when no
+ // variant exists — it never falls back to the plain "" asset.
+ // Callers that must distinguish "no authored variant" (e.g. to keep a
+ // legacy runtime fallback alive) should use this instead of assetPath.
+ QString schemeVariantPath(QStringView prefix) const;
// Load the theme's shipped default palette, falling back to the system
// theme directory when it is absent from the resolved (user) directory.
static PaletteConfig
@@ -101,12 +125,17 @@ public:
void reloadCurrentTheme();
void previewPalette(const PaletteConfig &cfg, const QString &scheme);
+ // Resolves an application color role: the theme's stored [AppColors] value
+ // when present, otherwise a palette-accent-derived fallback.
+ QColor appColor(AppColor::Role role) const;
+
QBrush &getBgBrush(Role zone);
QBrush getExtraBgBrush(Role zone, int zoneId = 0);
protected slots:
void themeChangedSlot();
signals:
void themeChanged();
+ void paletteChanged();
};
extern ThemeManager *themeManager;
diff --git a/cockatrice/src/interface/widgets/cards/additional_info/color_identity_widget.cpp b/cockatrice/src/interface/widgets/cards/additional_info/color_identity_widget.cpp
index 1ea1bcb10..2199faf30 100644
--- a/cockatrice/src/interface/widgets/cards/additional_info/color_identity_widget.cpp
+++ b/cockatrice/src/interface/widgets/cards/additional_info/color_identity_widget.cpp
@@ -85,7 +85,7 @@ void ColorIdentityWidget::resizeEvent(QResizeEvent *event)
}
lastWidth = totalWidth;
- const int totalHeight = totalWidth / 6; // Set height to 1/4 of the width
+ const int totalHeight = qMax(0, totalWidth / 6); // Set height to 1/4 of the width
setFixedHeight(totalHeight);
const int count = layout->count();
@@ -97,6 +97,10 @@ void ColorIdentityWidget::resizeEvent(QResizeEvent *event)
const int availableWidth = totalWidth - (spacing * (count - 1));
const int iconSize = qMin(availableWidth / count, totalHeight); // Ensure icons fit within the new height
+ if (iconSize <= 0) {
+ lastIconSize = iconSize;
+ return;
+ }
if (iconSize == lastIconSize) {
return;
}
diff --git a/cockatrice/src/interface/widgets/cards/additional_info/deck_color_identity.cpp b/cockatrice/src/interface/widgets/cards/additional_info/deck_color_identity.cpp
new file mode 100644
index 000000000..62b01511e
--- /dev/null
+++ b/cockatrice/src/interface/widgets/cards/additional_info/deck_color_identity.cpp
@@ -0,0 +1,37 @@
+#include "deck_color_identity.h"
+
+#include
+#include
+#include
+#include
+
+QString getDeckColorIdentity(const DeckList &deck, const CardDatabaseQuerier *db)
+{
+ const QStringList cardList = deck.getCardList({DECK_ZONE_MAIN, DECK_ZONE_SIDE});
+ if (cardList.isEmpty()) {
+ return {};
+ }
+
+ QSet colorSet; // A set to collect unique color symbols (e.g., W, U, B, R, G)
+
+ for (const QString &cardName : cardList) {
+ CardInfoPtr currentCard = db->getCardInfo(cardName);
+ if (currentCard) {
+ const QString colors = currentCard->getColors(); // returns something like "WUB"
+ for (const QChar &color : colors) {
+ colorSet.insert(color);
+ }
+ }
+ }
+
+ // Ensure the color identity is in WUBRG order
+ QString colorIdentity;
+ const QString wubrgOrder = "WUBRG";
+ for (const QChar &color : wubrgOrder) {
+ if (colorSet.contains(color)) {
+ colorIdentity.append(color);
+ }
+ }
+
+ return colorIdentity;
+}
diff --git a/cockatrice/src/interface/widgets/cards/additional_info/deck_color_identity.h b/cockatrice/src/interface/widgets/cards/additional_info/deck_color_identity.h
new file mode 100644
index 000000000..04294cb1c
--- /dev/null
+++ b/cockatrice/src/interface/widgets/cards/additional_info/deck_color_identity.h
@@ -0,0 +1,20 @@
+#ifndef COCKATRICE_DECK_COLOR_IDENTITY_H
+#define COCKATRICE_DECK_COLOR_IDENTITY_H
+
+#include
+
+class CardDatabaseQuerier;
+class DeckList;
+
+/**
+ * @brief Computes the color identity of a deck (e.g. "WUBRG") from the color
+ * symbols of all cards in the main deck and sideboard, ordered WUBRG.
+ *
+ * Shared as a free function so the deck storage previews and the deck share
+ * dialog compute identities identically.
+ *
+ * @param db Card database used to look up card color symbols.
+ */
+QString getDeckColorIdentity(const DeckList &deck, const CardDatabaseQuerier *db);
+
+#endif // COCKATRICE_DECK_COLOR_IDENTITY_H
diff --git a/cockatrice/src/interface/widgets/cards/card_art_utils.cpp b/cockatrice/src/interface/widgets/cards/card_art_utils.cpp
new file mode 100644
index 000000000..b26b73593
--- /dev/null
+++ b/cockatrice/src/interface/widgets/cards/card_art_utils.cpp
@@ -0,0 +1,18 @@
+#include "card_art_utils.h"
+
+#include
+#include
+
+namespace CardArtUtils
+{
+QPixmap rotateSidewaysLayoutArt(const QPixmap &art, const ExactCard &card)
+{
+ if (!card.getInfo().getUiAttributes().landscapeOrientation) {
+ return art;
+ }
+
+ QTransform transform;
+ transform.rotate(90);
+ return art.transformed(transform, Qt::SmoothTransformation);
+}
+} // namespace CardArtUtils
\ No newline at end of file
diff --git a/cockatrice/src/interface/widgets/cards/card_art_utils.h b/cockatrice/src/interface/widgets/cards/card_art_utils.h
new file mode 100644
index 000000000..5c331a12c
--- /dev/null
+++ b/cockatrice/src/interface/widgets/cards/card_art_utils.h
@@ -0,0 +1,25 @@
+#ifndef CARD_ART_UTILS_H
+#define CARD_ART_UTILS_H
+
+#include
+
+class ExactCard;
+
+namespace CardArtUtils
+{
+/**
+ * @brief Rotates a card's art upright when its layout shows sideways.
+ *
+ * Sideways-layout cards (planes, sieges/battles, split cards) store their
+ * landscape artwork rotated 90° inside a portrait frame. Art-crop displays,
+ * playmat art, and the card-info picture must show such art upright before
+ * sampling or painting. Portrait cards are returned unchanged.
+ *
+ * @param art The card pixmap to orient.
+ * @param card The card describing the art orientation.
+ * @return @p art rotated 90° clockwise for sideways-layout cards, else @p art.
+ */
+QPixmap rotateSidewaysLayoutArt(const QPixmap &art, const ExactCard &card);
+} // namespace CardArtUtils
+
+#endif // CARD_ART_UTILS_H
\ No newline at end of file
diff --git a/cockatrice/src/interface/widgets/cards/card_info_picture_widget.cpp b/cockatrice/src/interface/widgets/cards/card_info_picture_widget.cpp
index 79ae087d7..2dd21e78a 100644
--- a/cockatrice/src/interface/widgets/cards/card_info_picture_widget.cpp
+++ b/cockatrice/src/interface/widgets/cards/card_info_picture_widget.cpp
@@ -5,6 +5,7 @@
#include "../../../interface/card_picture_loader/card_picture_loader.h"
#include "../../../interface/widgets/tabs/tab_supervisor.h"
#include "../../window_main.h"
+#include "card_art_utils.h"
#include
#include
@@ -73,6 +74,8 @@ CardInfoPictureWidget::CardInfoPictureWidget(QWidget *parent, const bool _hoverT
update();
});
+ connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::cardLangChanged, this,
+ &CardInfoPictureWidget::updatePixmap);
}
/**
@@ -193,12 +196,7 @@ void CardInfoPictureWidget::paintEvent(QPaintEvent *event)
QPixmap transformedPixmap = resizedPixmap; // Default pixmap
if (SettingsCache::instance().cardsDisplay().getAutoRotateSidewaysLayoutCards()) {
- if (exactCard.getInfo().getUiAttributes().landscapeOrientation) {
- // Rotate pixmap 90 degrees to the left
- QTransform transform;
- transform.rotate(90);
- transformedPixmap = resizedPixmap.transformed(transform, Qt::SmoothTransformation);
- }
+ transformedPixmap = CardArtUtils::rotateSidewaysLayoutArt(resizedPixmap, exactCard);
}
// Handle DPI scaling
diff --git a/cockatrice/src/interface/widgets/cards/card_info_text_widget.cpp b/cockatrice/src/interface/widgets/cards/card_info_text_widget.cpp
index c6af5320b..e98c3c02a 100644
--- a/cockatrice/src/interface/widgets/cards/card_info_text_widget.cpp
+++ b/cockatrice/src/interface/widgets/cards/card_info_text_widget.cpp
@@ -1,6 +1,7 @@
#include "card_info_text_widget.h"
#include "../../../game_graphics/board/card_item.h"
+#include "../../card_localization.h"
#include
#include
@@ -10,7 +11,7 @@
#include
#include
-CardInfoTextWidget::CardInfoTextWidget(QWidget *parent) : QFrame(parent), info(nullptr)
+CardInfoTextWidget::CardInfoTextWidget(QWidget *parent) : QFrame(parent)
{
propsLabel = new QLabel;
propsLabel->setOpenExternalLinks(false);
@@ -39,6 +40,12 @@ CardInfoTextWidget::CardInfoTextWidget(QWidget *parent) : QFrame(parent), info(n
grid->setRowStretch(1, 1);
retranslateUi();
+
+ connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::cardLangChanged, this, [this] {
+ if (currentCard) {
+ setCard(currentCard);
+ }
+ });
}
void CardInfoTextWidget::setTexts(const QString &propsText, const QString &textText)
@@ -60,7 +67,7 @@ void CardInfoTextWidget::setCard(const ExactCard &exactCard)
QString text = "";
text += QString("| %1 | | %2 |
")
- .arg(tr("Name:"), card->getName().toHtmlEscaped());
+ .arg(tr("Name:"), CardLocalization::displayName(card).toHtmlEscaped());
if (!exactCard.getPrinting().isEmpty()) {
QString setShort = exactCard.getPrinting().getSet()->getShortName().toHtmlEscaped();
@@ -94,7 +101,8 @@ void CardInfoTextWidget::setCard(const ExactCard &exactCard)
}
text += "
";
- setTexts(text, card->getText());
+ setTexts(text, CardLocalization::displayText(card));
+ currentCard = exactCard;
}
void CardInfoTextWidget::setInvalidCardName(const QString &cardName)
diff --git a/cockatrice/src/interface/widgets/cards/card_info_text_widget.h b/cockatrice/src/interface/widgets/cards/card_info_text_widget.h
index a9c29da37..683be5ab5 100644
--- a/cockatrice/src/interface/widgets/cards/card_info_text_widget.h
+++ b/cockatrice/src/interface/widgets/cards/card_info_text_widget.h
@@ -23,7 +23,7 @@ private:
QLabel *propsLabel;
QScrollArea *propsScroll;
QTextEdit *textLabel;
- CardInfoPtr info;
+ ExactCard currentCard; ///< Last card set, re-rendered when the card language changes.
void setTexts(const QString &propsText, const QString &textText);
public:
diff --git a/cockatrice/src/interface/widgets/cards/deck_preview_card_picture_widget.cpp b/cockatrice/src/interface/widgets/cards/deck_preview_card_picture_widget.cpp
index 1614836fc..707173560 100644
--- a/cockatrice/src/interface/widgets/cards/deck_preview_card_picture_widget.cpp
+++ b/cockatrice/src/interface/widgets/cards/deck_preview_card_picture_widget.cpp
@@ -38,7 +38,10 @@ DeckPreviewCardPictureWidget::DeckPreviewCardPictureWidget(QWidget *parent,
{
singleClickTimer = new QTimer(this);
singleClickTimer->setSingleShot(true);
- connect(singleClickTimer, &QTimer::timeout, this, [this]() { emit imageClicked(lastMouseEvent, this); });
+ connect(singleClickTimer, &QTimer::timeout, this, [this]() {
+ emit imageClicked(lastMouseEvent, this);
+ emit imageSingleClicked();
+ });
connect(&SettingsCache::instance().visualDeckStorage(),
&VisualDeckStorageSettings::visualDeckStorageSelectionAnimationChanged, this,
&CardInfoPictureWidget::setRaiseOnEnterEnabled);
diff --git a/cockatrice/src/interface/widgets/cards/deck_preview_card_picture_widget.h b/cockatrice/src/interface/widgets/cards/deck_preview_card_picture_widget.h
index 154e938aa..303b6bf67 100644
--- a/cockatrice/src/interface/widgets/cards/deck_preview_card_picture_widget.h
+++ b/cockatrice/src/interface/widgets/cards/deck_preview_card_picture_widget.h
@@ -30,6 +30,7 @@ public:
signals:
void imageClicked(QMouseEvent *event, DeckPreviewCardPictureWidget *instance);
+ void imageSingleClicked();
void imageDoubleClicked(QMouseEvent *event, DeckPreviewCardPictureWidget *instance);
private:
diff --git a/cockatrice/src/interface/widgets/deck_analytics/abstract_analytics_panel_widget.cpp b/cockatrice/src/interface/widgets/deck_analytics/abstract_analytics_panel_widget.cpp
index 089abc5c8..483fc71b0 100644
--- a/cockatrice/src/interface/widgets/deck_analytics/abstract_analytics_panel_widget.cpp
+++ b/cockatrice/src/interface/widgets/deck_analytics/abstract_analytics_panel_widget.cpp
@@ -1,5 +1,6 @@
#include "abstract_analytics_panel_widget.h"
+#include "../../pixel_map_generator.h"
#include "deck_list_statistics_analyzer.h"
#include
@@ -20,7 +21,7 @@ AbstractAnalyticsPanelWidget::AbstractAnalyticsPanelWidget(QWidget *parent, Deck
// config button
configureButton = new QPushButton(this);
- configureButton->setIcon(QPixmap("theme:icons/cogwheel"));
+ configureButton->setIcon(themePixmap(QStringLiteral("icons/cogwheel")));
configureButton->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum);
connect(configureButton, &QPushButton::clicked, this, &AbstractAnalyticsPanelWidget::applyConfigFromDialog);
bannerAndSettingsLayout->addWidget(configureButton, 0);
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 9da821813..c8bdacf0d 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
@@ -28,7 +28,7 @@ DeckEditorDatabaseDisplayWidget::DeckEditorDatabaseDisplayWidget(QWidget *parent
searchEdit->setPlaceholderText(tr("Search by card name (or search expressions)"));
searchEdit->setClearButtonEnabled(true);
searchEdit->addAction(loadColorAdjustedPixmap("theme:icons/search"), QLineEdit::LeadingPosition);
- auto help = searchEdit->addAction(QPixmap("theme:icons/info"), QLineEdit::TrailingPosition);
+ auto help = searchEdit->addAction(themePixmap(QStringLiteral("icons/info")), QLineEdit::TrailingPosition);
setFocusProxy(searchEdit);
setFocusPolicy(Qt::ClickFocus);
@@ -59,13 +59,13 @@ DeckEditorDatabaseDisplayWidget::DeckEditorDatabaseDisplayWidget(QWidget *parent
&DeckEditorDatabaseDisplayWidget::onRelatedCardClicked);
aAddCard = new QAction(QString(), this);
- aAddCard->setIcon(QPixmap("theme:icons/arrow_right_green"));
+ aAddCard->setIcon(themePixmap(QStringLiteral("icons/arrow_right_green")));
connect(aAddCard, &QAction::triggered, this, &DeckEditorDatabaseDisplayWidget::actAddCardToMainDeck);
auto *tbAddCard = new QToolButton(this);
tbAddCard->setDefaultAction(aAddCard);
aAddCardToSideboard = new QAction(QString(), this);
- aAddCardToSideboard->setIcon(QPixmap("theme:icons/arrow_right_blue"));
+ aAddCardToSideboard->setIcon(themePixmap(QStringLiteral("icons/arrow_right_blue")));
connect(aAddCardToSideboard, &QAction::triggered, this, &DeckEditorDatabaseDisplayWidget::actAddCardToSideboard);
auto *tbAddCardToSideboard = new QToolButton(this);
tbAddCardToSideboard->setDefaultAction(aAddCardToSideboard);
diff --git a/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.cpp b/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.cpp
index e2175a358..b0b31074c 100644
--- a/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.cpp
+++ b/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.cpp
@@ -2,6 +2,7 @@
#include "../../../client/settings/cache_settings.h"
#include "../../../client/settings/shortcuts_settings.h"
+#include "../../pixel_map_generator.h"
#include "../playmat/playmat_settings_dialog.h"
#include "../settings_page/user_interface_settings_page.h"
#include "../tabs/api/commander_spellbook/commander_bracket_widget.h"
@@ -192,25 +193,25 @@ void DeckEditorDeckDockWidget::createDeckDock()
&DeckEditorDeckDockWidget::applyActiveGroupCriteria);
aIncrement = new QAction(QString(), this);
- aIncrement->setIcon(QPixmap("theme:icons/increment"));
+ aIncrement->setIcon(themePixmap(QStringLiteral("icons/increment")));
connect(aIncrement, &QAction::triggered, this, &DeckEditorDeckDockWidget::actIncrementSelection);
auto *tbIncrement = new QToolButton(this);
tbIncrement->setDefaultAction(aIncrement);
aDecrement = new QAction(QString(), this);
- aDecrement->setIcon(QPixmap("theme:icons/decrement"));
+ aDecrement->setIcon(themePixmap(QStringLiteral("icons/decrement")));
connect(aDecrement, &QAction::triggered, this, &DeckEditorDeckDockWidget::actDecrementSelection);
auto *tbDecrement = new QToolButton(this);
tbDecrement->setDefaultAction(aDecrement);
aRemoveCard = new QAction(QString(), this);
- aRemoveCard->setIcon(QPixmap("theme:icons/remove_row"));
+ aRemoveCard->setIcon(themePixmap(QStringLiteral("icons/remove_row")));
connect(aRemoveCard, &QAction::triggered, this, &DeckEditorDeckDockWidget::actRemoveCard);
auto *tbRemoveCard = new QToolButton(this);
tbRemoveCard->setDefaultAction(aRemoveCard);
aSwapCard = new QAction(QString(), this);
- aSwapCard->setIcon(QPixmap("theme:icons/swap"));
+ aSwapCard->setIcon(themePixmap(QStringLiteral("icons/swap")));
connect(aSwapCard, &QAction::triggered, this, &DeckEditorDeckDockWidget::actSwapSelection);
auto *tbSwapCard = new QToolButton(this);
tbSwapCard->setDefaultAction(aSwapCard);
@@ -344,7 +345,9 @@ ExactCard DeckEditorDeckDockWidget::getCurrentCard()
if (!current.isValid()) {
return {};
}
- const QString cardName = current.siblingAtColumn(DeckListModelColumns::CARD_NAME).data().toString();
+ // The display role holds the localized card name; the edit role always carries the
+ // canonical English name needed to look the card up in the database.
+ const QString cardName = current.siblingAtColumn(DeckListModelColumns::CARD_NAME).data(Qt::EditRole).toString();
const QString cardProviderID = current.siblingAtColumn(DeckListModelColumns::CARD_PROVIDER_ID).data().toString();
const QModelIndex gparent = current.parent().parent();
diff --git a/cockatrice/src/interface/widgets/deck_editor/deck_editor_filter_dock_widget.cpp b/cockatrice/src/interface/widgets/deck_editor/deck_editor_filter_dock_widget.cpp
index 2d4fb60e8..c6b6e4416 100644
--- a/cockatrice/src/interface/widgets/deck_editor/deck_editor_filter_dock_widget.cpp
+++ b/cockatrice/src/interface/widgets/deck_editor/deck_editor_filter_dock_widget.cpp
@@ -4,6 +4,7 @@
#include "../../../client/settings/shortcuts_settings.h"
#include "../../../filters/filter_builder.h"
#include "../../../filters/filter_tree_model.h"
+#include "../../pixel_map_generator.h"
#include
#include
@@ -42,11 +43,11 @@ void DeckEditorFilterDockWidget::createFiltersDock()
connect(filterBuilder, &FilterBuilder::add, filterModel, &FilterTreeModel::addFilter);
aClearFilterOne = new QAction(QString(), this);
- aClearFilterOne->setIcon(QPixmap("theme:icons/decrement"));
+ aClearFilterOne->setIcon(themePixmap(QStringLiteral("icons/decrement")));
connect(aClearFilterOne, &QAction::triggered, this, &DeckEditorFilterDockWidget::actClearFilterOne);
aClearFilterAll = new QAction(QString(), this);
- aClearFilterAll->setIcon(QPixmap("theme:icons/clearsearch"));
+ aClearFilterAll->setIcon(themePixmap(QStringLiteral("icons/clearsearch")));
connect(aClearFilterAll, &QAction::triggered, this, &DeckEditorFilterDockWidget::actClearFilterAll);
auto *filterDelOne = new QToolButton();
diff --git a/cockatrice/src/interface/widgets/deck_editor/deck_list_history_manager_widget.cpp b/cockatrice/src/interface/widgets/deck_editor/deck_list_history_manager_widget.cpp
index cef459752..c93f12b34 100644
--- a/cockatrice/src/interface/widgets/deck_editor/deck_list_history_manager_widget.cpp
+++ b/cockatrice/src/interface/widgets/deck_editor/deck_list_history_manager_widget.cpp
@@ -1,5 +1,6 @@
#include "deck_list_history_manager_widget.h"
+#include "../../pixel_map_generator.h"
#include "deck_state_manager.h"
DeckListHistoryManagerWidget::DeckListHistoryManagerWidget(DeckStateManager *_deckStateManager,
@@ -10,7 +11,7 @@ DeckListHistoryManagerWidget::DeckListHistoryManagerWidget(DeckStateManager *_de
layout = new QHBoxLayout(this);
aUndo = new QAction(QString(), this);
- aUndo->setIcon(QPixmap("theme:icons/arrow_undo"));
+ aUndo->setIcon(themePixmap(QStringLiteral("icons/arrow_undo")));
aUndo->setShortcut(QKeySequence::Undo);
aUndo->setShortcutContext(Qt::ApplicationShortcut);
connect(aUndo, &QAction::triggered, this, &DeckListHistoryManagerWidget::doUndo);
@@ -19,7 +20,7 @@ DeckListHistoryManagerWidget::DeckListHistoryManagerWidget(DeckStateManager *_de
undoButton->setDefaultAction(aUndo);
aRedo = new QAction(QString(), this);
- aRedo->setIcon(QPixmap("theme:icons/arrow_redo"));
+ aRedo->setIcon(themePixmap(QStringLiteral("icons/arrow_redo")));
aRedo->setShortcut(QKeySequence::Redo);
aRedo->setShortcutContext(Qt::ApplicationShortcut);
connect(aRedo, &QAction::triggered, this, &DeckListHistoryManagerWidget::doRedo);
@@ -31,7 +32,7 @@ DeckListHistoryManagerWidget::DeckListHistoryManagerWidget(DeckStateManager *_de
layout->addWidget(redoButton);
historyButton = new SettingsButtonWidget(this);
- historyButton->setButtonIcon(QPixmap("theme:icons/arrow_history"));
+ historyButton->setButtonIcon(themePixmap(QStringLiteral("icons/arrow_history")));
historyLabel = new QLabel(this);
diff --git a/cockatrice/src/interface/widgets/deck_editor/deck_state_manager.cpp b/cockatrice/src/interface/widgets/deck_editor/deck_state_manager.cpp
index e563729a4..cfe989e9f 100644
--- a/cockatrice/src/interface/widgets/deck_editor/deck_state_manager.cpp
+++ b/cockatrice/src/interface/widgets/deck_editor/deck_state_manager.cpp
@@ -1,13 +1,20 @@
#include "deck_state_manager.h"
+#include "../../../client/settings/cache_settings.h"
+
#include
#include
#include
+#include
DeckStateManager::DeckStateManager(QObject *parent)
: QObject(parent), deckList(QSharedPointer(new DeckList)),
deckListModel(new DeckListModel(this, deckList)), historyManager(new DeckListHistoryManager(this))
{
+ deckListModel->setDisplayLanguage(SettingsCache::instance().cardsDisplay().getCardLang());
+ connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::cardLangChanged, deckListModel,
+ [this](const QString &lang) { deckListModel->setDisplayLanguage(lang); });
+
connect(historyManager, &DeckListHistoryManager::undoRedoStateChanged, this, [this] {
setModified(true);
emit historyChanged();
@@ -260,7 +267,10 @@ bool DeckStateManager::swapCardAtIndex(const QModelIndex &idx)
return false;
}
- QString cardName = idx.siblingAtColumn(DeckListModelColumns::CARD_NAME).data().toString();
+ // The display role holds the localized card name; the edit role always carries the
+ // canonical English name needed to look the card up in the database.
+ QString displayCardName = idx.siblingAtColumn(DeckListModelColumns::CARD_NAME).data().toString();
+ QString cardName = idx.siblingAtColumn(DeckListModelColumns::CARD_NAME).data(Qt::EditRole).toString();
QString providerId = idx.siblingAtColumn(DeckListModelColumns::CARD_PROVIDER_ID).data().toString();
QModelIndex gparent = idx.parent().parent();
@@ -277,7 +287,7 @@ bool DeckStateManager::swapCardAtIndex(const QModelIndex &idx)
QString reason = tr("Moved to %1 1 × \"%2\" (%3)") //
.arg(otherZoneName)
- .arg(cardName)
+ .arg(displayCardName)
.arg(providerId);
return modifyDeck(reason, [&idx, &cardName, &providerId, &otherZoneName](auto model) {
@@ -291,9 +301,8 @@ bool DeckStateManager::removeCardAtIndex(const QModelIndex &idx)
return false;
}
- QString cardName = idx.siblingAtColumn(DeckListModelColumns::CARD_NAME).data().toString();
-
- QString reason = tr("Removed \"%1\" (all copies)").arg(cardName);
+ QString reason =
+ tr("Removed \"%1\" (all copies)").arg(idx.siblingAtColumn(DeckListModelColumns::CARD_NAME).data().toString());
return modifyDeck(reason, [&idx](auto model) { return model->removeRow(idx.row(), idx.parent()); });
}
diff --git a/cockatrice/src/interface/widgets/deck_share/deck_share_utils.cpp b/cockatrice/src/interface/widgets/deck_share/deck_share_utils.cpp
new file mode 100644
index 000000000..4343f0aab
--- /dev/null
+++ b/cockatrice/src/interface/widgets/deck_share/deck_share_utils.cpp
@@ -0,0 +1,59 @@
+#include "deck_share_utils.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+namespace DeckShareUtils
+{
+
+QString buildShareLink(const AbstractClient *client, const QString &token)
+{
+ QUrl url;
+ url.setScheme(QStringLiteral("cockatrice"));
+ url.setHost(QStringLiteral("opendeck"));
+
+ QUrlQuery query;
+ query.addQueryItem(QStringLiteral("share"), token);
+ query.addQueryItem(QStringLiteral("hostname"), client->serverName());
+ query.addQueryItem(QStringLiteral("port"), QString::number(client->serverPort()));
+ url.setQuery(query);
+
+ return url.toString(QUrl::FullyEncoded);
+}
+
+QString copyShareLinkToClipboard(const QString &link)
+{
+ QGuiApplication::clipboard()->setText(link);
+ return link;
+}
+
+QString formatShareExpiry(const QDateTime &expiry)
+{
+ return QLocale().toString(expiry.toLocalTime(), QLocale::ShortFormat);
+}
+
+ShareResponse handleShareResponse(const AbstractClient *client, const Response &response)
+{
+ const Response_DeckShareCreate &resp = response.GetExtension(Response_DeckShareCreate::ext);
+ const QString token = QString::fromStdString(resp.token());
+
+ const QString link = buildShareLink(client, token);
+ copyShareLinkToClipboard(link);
+
+#if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0)
+ const QDateTime expiry = QDateTime::fromSecsSinceEpoch(resp.expires_at(), QTimeZone::UTC);
+#else
+ const QDateTime expiry = QDateTime::fromSecsSinceEpoch(resp.expires_at(), Qt::UTC);
+#endif
+
+ return {link, expiry};
+}
+
+} // namespace DeckShareUtils
diff --git a/cockatrice/src/interface/widgets/deck_share/deck_share_utils.h b/cockatrice/src/interface/widgets/deck_share/deck_share_utils.h
new file mode 100644
index 000000000..c9f7afa8b
--- /dev/null
+++ b/cockatrice/src/interface/widgets/deck_share/deck_share_utils.h
@@ -0,0 +1,59 @@
+/**
+ * @file deck_share_utils.h
+ * @ingroup DeckShareWidgets
+ */
+//! \todo Document this file.
+
+#ifndef DECK_SHARE_UTILS_H
+#define DECK_SHARE_UTILS_H
+
+#include
+#include
+
+class AbstractClient;
+class Response;
+
+/**
+ * @brief Shared helpers for creating temporary deck shares.
+ */
+namespace DeckShareUtils
+{
+
+/**
+ * @brief The outcome of a successful share-create response.
+ */
+struct ShareResponse
+{
+ QString link; ///< The share link that was copied to the clipboard.
+ QDateTime expiry; ///< When the share expires (UTC).
+};
+
+/**
+ * @brief Builds the cockatrice:// link for a freshly created deck share.
+ * @param client Used to embed the target server's hostname and port.
+ * @param token The share token from Response_DeckShareCreate.
+ */
+QString buildShareLink(const AbstractClient *client, const QString &token);
+
+/**
+ * @brief Copies the share link to the clipboard.
+ * @return The link that was copied.
+ */
+QString copyShareLinkToClipboard(const QString &link);
+
+/**
+ * @brief Formats the expiration timestamp for a share.
+ */
+QString formatShareExpiry(const QDateTime &expiry);
+
+/**
+ * @brief Handles a successful Response_DeckShareCreate: builds the share link,
+ * copies it to the clipboard, and derives the share expiry.
+ * @param client Used to embed the target server's hostname and port.
+ * @param response The successful response carrying the share token and expiry.
+ */
+ShareResponse handleShareResponse(const AbstractClient *client, const Response &response);
+
+} // namespace DeckShareUtils
+
+#endif // DECK_SHARE_UTILS_H
diff --git a/cockatrice/src/interface/widgets/deck_share/share_bar_widget.cpp b/cockatrice/src/interface/widgets/deck_share/share_bar_widget.cpp
new file mode 100644
index 000000000..5aca343ca
--- /dev/null
+++ b/cockatrice/src/interface/widgets/deck_share/share_bar_widget.cpp
@@ -0,0 +1,77 @@
+#include "share_bar_widget.h"
+
+#include
+#include
+#include
+#include
+
+ShareBarWidget::ShareBarWidget(QWidget *parent) : QWidget(parent)
+{
+ auto *layout = new QHBoxLayout(this);
+ layout->setContentsMargins(12, 10, 12, 10);
+ layout->setSpacing(8);
+
+ hintLabel = new QLabel(this);
+ hintLabel->setWordWrap(true);
+
+ nameEdit = new QLineEdit(this);
+ nameEdit->setMaximumWidth(260);
+
+ countLabel = new QLabel(this);
+
+ cancelButton = new QPushButton(this);
+ connect(cancelButton, &QPushButton::clicked, this, &ShareBarWidget::cancelRequested);
+
+ createButton = new QPushButton(this);
+ createButton->setDefault(true);
+ connect(createButton, &QPushButton::clicked, this, &ShareBarWidget::createRequested);
+
+ layout->addWidget(hintLabel, 1);
+ layout->addWidget(nameEdit);
+ layout->addWidget(countLabel);
+ layout->addStretch();
+ layout->addWidget(cancelButton);
+ layout->addWidget(createButton);
+
+ setLayout(layout);
+
+ retranslateUi();
+}
+
+void ShareBarWidget::retranslateUi()
+{
+ nameEdit->setPlaceholderText(tr("Share name"));
+ cancelButton->setText(tr("Cancel"));
+ createButton->setText(tr("Create share link"));
+}
+
+QString ShareBarWidget::name() const
+{
+ return nameEdit->text().trimmed();
+}
+
+void ShareBarWidget::setName(const QString &value)
+{
+ nameEdit->setText(value);
+}
+
+void ShareBarWidget::setCountText(const QString &text)
+{
+ countLabel->setText(text);
+}
+
+void ShareBarWidget::setHintText(const QString &text, bool visible)
+{
+ hintLabel->setText(text);
+ hintLabel->setVisible(visible);
+}
+
+void ShareBarWidget::setCreateEnabled(bool enabled)
+{
+ createButton->setEnabled(enabled);
+}
+
+void ShareBarWidget::focusName()
+{
+ nameEdit->setFocus();
+}
diff --git a/cockatrice/src/interface/widgets/deck_share/share_bar_widget.h b/cockatrice/src/interface/widgets/deck_share/share_bar_widget.h
new file mode 100644
index 000000000..ede9fb41b
--- /dev/null
+++ b/cockatrice/src/interface/widgets/deck_share/share_bar_widget.h
@@ -0,0 +1,63 @@
+/**
+ * @file share_bar_widget.h
+ * @ingroup DeckShareWidgets
+ */
+//! \todo Document this file.
+
+#ifndef SHARE_BAR_WIDGET_H
+#define SHARE_BAR_WIDGET_H
+
+#include
+
+class QLabel;
+class QLineEdit;
+class QPushButton;
+
+/**
+ * @brief The activated toolbar used to create a temporary deck share.
+ *
+ * A single reusable component shared by the local visual deck storage and the
+ * remote server deck storage tabs, so the share workflow renders identically in
+ * both places. It owns its own widgets, strings, and layout; the owning tab only
+ * sets the count/hint text and reacts to the create/cancel signals.
+ */
+class ShareBarWidget final : public QWidget
+{
+ Q_OBJECT
+
+public:
+ explicit ShareBarWidget(QWidget *parent = nullptr);
+
+ void retranslateUi();
+
+ /** @return The trimmed name entered by the user. */
+ [[nodiscard]] QString name() const;
+
+ /** @brief Resets the name field to the given default. */
+ void setName(const QString &name);
+
+ /** @brief Sets the selected-count summary label text. */
+ void setCountText(const QString &text);
+
+ /** @brief Sets the explainer hint text, showing it when @p visible is true. */
+ void setHintText(const QString &text, bool visible);
+
+ /** @brief Enables or disables the create-share-link button (guards double submission). */
+ void setCreateEnabled(bool enabled);
+
+ /** @brief Moves keyboard focus to the name field. */
+ void focusName();
+
+signals:
+ void createRequested();
+ void cancelRequested();
+
+private:
+ QLabel *hintLabel;
+ QLineEdit *nameEdit;
+ QLabel *countLabel;
+ QPushButton *cancelButton;
+ QPushButton *createButton;
+};
+
+#endif // SHARE_BAR_WIDGET_H
diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_connect.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_connect.cpp
index aa8a916f8..700012d45 100644
--- a/cockatrice/src/interface/widgets/dialogs/dlg_connect.cpp
+++ b/cockatrice/src/interface/widgets/dialogs/dlg_connect.cpp
@@ -1,6 +1,7 @@
#include "dlg_connect.h"
#include "../../../client/settings/cache_settings.h"
+#include "../../pixel_map_generator.h"
#include
#include
@@ -21,7 +22,7 @@ DlgConnect::DlgConnect(QWidget *parent) : QDialog(parent)
previousHosts = new QComboBox(this);
btnDeleteServer = new QPushButton(this);
- btnDeleteServer->setIcon(QPixmap("theme:icons/remove_row"));
+ btnDeleteServer->setIcon(themePixmap(QStringLiteral("icons/remove_row")));
btnDeleteServer->setToolTip(tr("Delete the currently selected saved server"));
btnDeleteServer->setFixedWidth(30);
@@ -29,7 +30,7 @@ DlgConnect::DlgConnect(QWidget *parent) : QDialog(parent)
hps = new HandlePublicServers(this);
btnRefreshServers = new QPushButton(this);
- btnRefreshServers->setIcon(QPixmap("theme:icons/sync"));
+ btnRefreshServers->setIcon(themePixmap(QStringLiteral("icons/sync")));
btnRefreshServers->setToolTip(tr("Refresh the server list with known public servers"));
btnRefreshServers->setFixedWidth(30);
@@ -99,7 +100,7 @@ DlgConnect::DlgConnect(QWidget *parent) : QDialog(parent)
updateDisplayInfo(previousHosts->currentText());
btnForgotPassword = new QPushButton(this);
- btnForgotPassword->setIcon(QPixmap("theme:icons/forgot_password"));
+ btnForgotPassword->setIcon(themePixmap(QStringLiteral("icons/forgot_password")));
btnForgotPassword->setToolTip(tr("Reset Password"));
btnForgotPassword->setFixedWidth(30);
connect(btnForgotPassword, &QPushButton::released, this, &DlgConnect::actForgotPassword);
diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_edit_tokens.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_edit_tokens.cpp
index f249976c2..a56aa8e35 100644
--- a/cockatrice/src/interface/widgets/dialogs/dlg_edit_tokens.cpp
+++ b/cockatrice/src/interface/widgets/dialogs/dlg_edit_tokens.cpp
@@ -1,5 +1,6 @@
#include "dlg_edit_tokens.h"
+#include "../../pixel_map_generator.h"
#include "../interface/widgets/utility/get_text_with_max.h"
#include
@@ -90,10 +91,10 @@ DlgEditTokens::DlgEditTokens(QWidget *parent) : QDialog(parent), currentCard(nul
&DlgEditTokens::tokenSelectionChanged);
QAction *aAddToken = new QAction(tr("Add token"), this);
- aAddToken->setIcon(QPixmap("theme:icons/increment"));
+ aAddToken->setIcon(themePixmap(QStringLiteral("icons/increment")));
connect(aAddToken, &QAction::triggered, this, &DlgEditTokens::actAddToken);
QAction *aRemoveToken = new QAction(tr("Remove token"), this);
- aRemoveToken->setIcon(QPixmap("theme:icons/decrement"));
+ aRemoveToken->setIcon(themePixmap(QStringLiteral("icons/decrement")));
connect(aRemoveToken, &QAction::triggered, this, &DlgEditTokens::actRemoveToken);
auto *databaseToolBar = new QToolBar;
diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_manage_sets.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_manage_sets.cpp
index 7c107eb2f..556c270be 100644
--- a/cockatrice/src/interface/widgets/dialogs/dlg_manage_sets.cpp
+++ b/cockatrice/src/interface/widgets/dialogs/dlg_manage_sets.cpp
@@ -1,6 +1,7 @@
#include "dlg_manage_sets.h"
#include "../../../client/settings/cache_settings.h"
+#include "../../pixel_map_generator.h"
#include "../interface/card_picture_loader/card_picture_loader.h"
#include "../interface/widgets/utility/custom_line_edit.h"
@@ -35,28 +36,28 @@ WndSets::WndSets(QWidget *parent) : QMainWindow(parent)
setsEditToolBar->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum);
aTop = new QAction(QString(), this);
- aTop->setIcon(QPixmap("theme:icons/arrow_top_green"));
+ aTop->setIcon(themePixmap(QStringLiteral("icons/arrow_top_green")));
aTop->setToolTip(tr("Move selected set to the top"));
aTop->setEnabled(false);
connect(aTop, &QAction::triggered, this, &WndSets::actTop);
setsEditToolBar->addAction(aTop);
aUp = new QAction(QString(), this);
- aUp->setIcon(QPixmap("theme:icons/arrow_up_green"));
+ aUp->setIcon(themePixmap(QStringLiteral("icons/arrow_up_green")));
aUp->setToolTip(tr("Move selected set up"));
aUp->setEnabled(false);
connect(aUp, &QAction::triggered, this, &WndSets::actUp);
setsEditToolBar->addAction(aUp);
aDown = new QAction(QString(), this);
- aDown->setIcon(QPixmap("theme:icons/arrow_down_green"));
+ aDown->setIcon(themePixmap(QStringLiteral("icons/arrow_down_green")));
aDown->setToolTip(tr("Move selected set down"));
aDown->setEnabled(false);
connect(aDown, &QAction::triggered, this, &WndSets::actDown);
setsEditToolBar->addAction(aDown);
aBottom = new QAction(QString(), this);
- aBottom->setIcon(QPixmap("theme:icons/arrow_bottom_green"));
+ aBottom->setIcon(themePixmap(QStringLiteral("icons/arrow_bottom_green")));
aBottom->setToolTip(tr("Move selected set to the bottom"));
aBottom->setEnabled(false);
connect(aBottom, &QAction::triggered, this, &WndSets::actBottom);
@@ -66,7 +67,7 @@ WndSets::WndSets(QWidget *parent) : QMainWindow(parent)
searchField = new LineEditUnfocusable;
searchField->setObjectName("searchEdit");
searchField->setPlaceholderText(tr("Search by set name, code, type, or release date"));
- searchField->addAction(QPixmap("theme:icons/search"), LineEditUnfocusable::LeadingPosition);
+ searchField->addAction(themePixmap(QStringLiteral("icons/search")), LineEditUnfocusable::LeadingPosition);
searchField->setClearButtonEnabled(true);
setFocusProxy(searchField);
diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_register.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_register.cpp
index 6ae8c9adb..6d022861c 100644
--- a/cockatrice/src/interface/widgets/dialogs/dlg_register.cpp
+++ b/cockatrice/src/interface/widgets/dialogs/dlg_register.cpp
@@ -1,6 +1,7 @@
#include "dlg_register.h"
#include "../../../client/settings/cache_settings.h"
+#include "../../pixel_map_generator.h"
#include "../server/handle_public_servers.h"
#include "../server/user/user_info_connection.h"
@@ -24,7 +25,7 @@ DlgRegister::DlgRegister(QWidget *parent) : QDialog(parent)
previousHosts = new QComboBox(this);
btnDeleteServer = new QPushButton(this);
- btnDeleteServer->setIcon(QPixmap("theme:icons/remove_row"));
+ btnDeleteServer->setIcon(themePixmap(QStringLiteral("icons/remove_row")));
btnDeleteServer->setToolTip(tr("Delete the currently selected saved server"));
btnDeleteServer->setFixedWidth(30);
@@ -32,7 +33,7 @@ DlgRegister::DlgRegister(QWidget *parent) : QDialog(parent)
hps = new HandlePublicServers(this);
btnRefreshServers = new QPushButton(this);
- btnRefreshServers->setIcon(QPixmap("theme:icons/sync"));
+ btnRefreshServers->setIcon(themePixmap(QStringLiteral("icons/sync")));
btnRefreshServers->setToolTip(tr("Refresh the server list with known public servers"));
btnRefreshServers->setFixedWidth(30);
diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_settings.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_settings.cpp
index 883cfcd03..581920dbc 100644
--- a/cockatrice/src/interface/widgets/dialogs/dlg_settings.cpp
+++ b/cockatrice/src/interface/widgets/dialogs/dlg_settings.cpp
@@ -6,6 +6,7 @@
#include "dlg_settings.h"
#include "../../../client/settings/cache_settings.h"
+#include "../../pixel_map_generator.h"
#include "../main.h"
#include "../settings_page/appearance_settings_page.h"
#include "../settings_page/deck_editor_settings_page.h"
@@ -96,7 +97,7 @@ void DlgSettings::setupUi()
// Search bar
searchEdit = new QLineEdit;
searchEdit->setClearButtonEnabled(true);
- searchEdit->addAction(QPixmap("theme:icons/search"), QLineEdit::LeadingPosition);
+ searchEdit->addAction(themePixmap(QStringLiteral("icons/search")), QLineEdit::LeadingPosition);
searchEdit->installEventFilter(this);
connect(searchEdit, &QLineEdit::textChanged, this, &DlgSettings::onSearchTextChanged);
@@ -132,7 +133,7 @@ void DlgSettings::setupUi()
pagesWidget->addWidget(makeScrollable(userInterfacePage));
pagesWidget->addWidget(makeScrollable(deckEditorPage));
pagesWidget->addWidget(makeScrollable(storagePage));
- pagesWidget->addWidget(messagesPage);
+ pagesWidget->addWidget(makeScrollable(messagesPage));
pagesWidget->addWidget(soundPage);
pagesWidget->addWidget(shortcutsPage);
@@ -415,6 +416,11 @@ void DlgSettings::setTab(int index)
}
}
+AbstractSettingsPage *DlgSettings::page(SettingsPage which) const
+{
+ return pages.value(static_cast(which));
+}
+
void DlgSettings::updateLanguage()
{
qApp->removeTranslator(translator); // NOLINT(cppcoreguidelines-pro-type-static-cast-downcast)
@@ -477,13 +483,13 @@ void DlgSettings::closeEvent(QCloseEvent *event)
case Invalid:
loadErrorMessage = tr("Your card database is invalid.\n\n"
"Cockatrice may not function correctly with an invalid database\n\n"
- "You may need to rerun oracle to update your card database.\n\n"
+ "You may need to rerun Oracle to update your card database.\n\n"
"Would you like to change your database location setting?");
break;
case VersionTooOld:
loadErrorMessage = tr("Your card database version is too old.\n\n"
"This can cause problems loading card information or images\n\n"
- "Usually this can be fixed by rerunning oracle to to update your card database.\n\n"
+ "Usually this can be fixed by rerunning Oracle to to update your card database.\n\n"
"Would you like to change your database location setting?");
break;
case NotLoaded:
diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_settings.h b/cockatrice/src/interface/widgets/dialogs/dlg_settings.h
index b700f7af9..845c0b4d6 100644
--- a/cockatrice/src/interface/widgets/dialogs/dlg_settings.h
+++ b/cockatrice/src/interface/widgets/dialogs/dlg_settings.h
@@ -54,6 +54,7 @@ public:
explicit DlgSettings(QWidget *parent = nullptr);
void setTab(int index);
+ AbstractSettingsPage *page(SettingsPage which) const;
private slots:
void onTabClicked(int index);
diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_share_deck.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_share_deck.cpp
new file mode 100644
index 000000000..897d32062
--- /dev/null
+++ b/cockatrice/src/interface/widgets/dialogs/dlg_share_deck.cpp
@@ -0,0 +1,96 @@
+#include "dlg_share_deck.h"
+
+#include "../../../client/settings/cache_settings.h"
+#include "../cards/additional_info/deck_color_identity.h"
+#include "../deck_share/deck_share_utils.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+DlgShareDeck::DlgShareDeck(AbstractClient *_client, const QSharedPointer &_deck, QWidget *_parent)
+ : QDialog(_parent), client(_client), deck(_deck), shareTimeoutTimer(new QTimer(this))
+{
+ setWindowTitle(tr("Share deck"));
+
+ auto *layout = new QVBoxLayout(this);
+
+ nameEdit = new QLineEdit(this);
+ nameEdit->setText(tr("Shared deck"));
+
+ auto *form = new QFormLayout;
+ form->addRow(tr("Share name:"), nameEdit);
+ layout->addLayout(form);
+
+ auto *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
+ buttonBox->button(QDialogButtonBox::Ok)->setText(tr("Create share link"));
+ buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel"));
+ connect(buttonBox, &QDialogButtonBox::accepted, this, &DlgShareDeck::actShare);
+ connect(buttonBox, &QDialogButtonBox::rejected, this, &DlgShareDeck::reject);
+ this->buttonBox = buttonBox;
+ layout->addWidget(buttonBox);
+
+ shareTimeoutTimer->setSingleShot(true);
+ shareTimeoutTimer->setInterval(
+ static_cast((static_cast(SettingsCache::instance().network().getTimeOut()) + 1) *
+ SettingsCache::instance().network().getKeepAlive() * 1000));
+ connect(shareTimeoutTimer, &QTimer::timeout, this, &DlgShareDeck::onShareTimeout);
+}
+
+void DlgShareDeck::actShare()
+{
+ buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false);
+
+ Command_DeckShareCreate cmd;
+ cmd.set_name(nameEdit->text().trimmed().toStdString());
+ if (cmd.name().empty()) {
+ cmd.set_name(tr("Shared deck").toStdString());
+ }
+
+ DeckShareItem *item = cmd.add_items();
+ item->set_deck_list(deck->writeToString_Native().toStdString());
+ item->set_color_identity(getDeckColorIdentity(*deck, CardDatabaseManager::query()).toStdString());
+
+ PendingCommand *pend = client->prepareSessionCommand(cmd);
+ connect(pend, &PendingCommand::finished, this, &DlgShareDeck::shareFinished);
+ client->sendCommand(pend);
+ shareTimeoutTimer->start();
+}
+
+void DlgShareDeck::shareFinished(const Response &response, const CommandContainer & /*commandContainer*/)
+{
+ shareTimeoutTimer->stop();
+ if (response.response_code() != Response::RespOk) {
+ buttonBox->button(QDialogButtonBox::Ok)->setEnabled(true);
+ QMessageBox::critical(this, tr("Share deck"),
+ tr("Failed to create the share link (server response code %1).")
+ .arg(QString::number(static_cast(response.response_code()))));
+ return;
+ }
+
+ const DeckShareUtils::ShareResponse share = DeckShareUtils::handleShareResponse(client, response);
+
+ QMessageBox::information(this, tr("Share deck"),
+ tr("Share link created and copied to the clipboard:\n\n%1\n\n"
+ "The share expires on %2.")
+ .arg(share.link, DeckShareUtils::formatShareExpiry(share.expiry)));
+ accept();
+}
+
+void DlgShareDeck::onShareTimeout()
+{
+ buttonBox->button(QDialogButtonBox::Ok)->setEnabled(true);
+ QMessageBox::warning(this, tr("Share deck"), tr("The server did not respond in time. Try again."));
+}
diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_share_deck.h b/cockatrice/src/interface/widgets/dialogs/dlg_share_deck.h
new file mode 100644
index 000000000..162fa3677
--- /dev/null
+++ b/cockatrice/src/interface/widgets/dialogs/dlg_share_deck.h
@@ -0,0 +1,46 @@
+/**
+ * @file dlg_share_deck.h
+ * @ingroup Dialogs
+ */
+//! \todo Document this file.
+
+#ifndef DLG_SHARE_DECK_H
+#define DLG_SHARE_DECK_H
+
+#include
+#include
+
+class AbstractClient;
+class CommandContainer;
+class DeckList;
+class QDialogButtonBox;
+class QLineEdit;
+class QTimer;
+class Response;
+
+/**
+ * @brief Slim dialog to create a temporary share for the deck open in the editor.
+ *
+ * Asks for a share name, sends Command_DeckShareCreate for the single inline
+ * deck, and copies the resulting link to the clipboard.
+ */
+class DlgShareDeck : public QDialog
+{
+ Q_OBJECT
+public:
+ DlgShareDeck(AbstractClient *_client, const QSharedPointer &_deck, QWidget *parent = nullptr);
+
+private slots:
+ void actShare();
+ void shareFinished(const Response &response, const CommandContainer &commandContainer);
+ void onShareTimeout();
+
+private:
+ AbstractClient *client;
+ QSharedPointer deck;
+ QLineEdit *nameEdit;
+ QDialogButtonBox *buttonBox;
+ QTimer *shareTimeoutTimer;
+};
+
+#endif // DLG_SHARE_DECK_H
diff --git a/cockatrice/src/interface/widgets/general/home_tab_button_color.h b/cockatrice/src/interface/widgets/general/home_tab_button_color.h
index 1550b57e7..b45bd7a92 100644
--- a/cockatrice/src/interface/widgets/general/home_tab_button_color.h
+++ b/cockatrice/src/interface/widgets/general/home_tab_button_color.h
@@ -11,8 +11,8 @@ namespace HomeTabButtonColor
*/
enum Source
{
- Automatic, ///< Extract color from background, or use theme color if no background
- FromBackground, ///< Always extract color from background
+ FromThemeColors, ///< Use the theme's identity accent colors
+ FromBackground, ///< Extract colour from the background image
};
struct Entry
@@ -23,7 +23,7 @@ struct Entry
inline QList all()
{
- static QList entries = {{Automatic, QT_TR_NOOP("Automatic")},
+ static QList entries = {{FromThemeColors, QT_TR_NOOP("From theme colors")},
{FromBackground, QT_TR_NOOP("Extract from background")}};
return entries;
@@ -33,12 +33,12 @@ inline QList all()
* Safely converts an int into the corresponding Source.
*
* @param value The int value
- * @return The Source. Returns Source::Automatic if the value is not within range
+ * @return The Source. Returns Source::FromThemeColors if the value is not within range
*/
inline Source intToSource(int value)
{
if (value > FromBackground) {
- return Automatic; // default
+ return FromThemeColors; // default
}
return static_cast(value);
diff --git a/cockatrice/src/interface/widgets/general/home_widget.cpp b/cockatrice/src/interface/widgets/general/home_widget.cpp
index 10fcdcb43..648d315f9 100644
--- a/cockatrice/src/interface/widgets/general/home_widget.cpp
+++ b/cockatrice/src/interface/widgets/general/home_widget.cpp
@@ -2,6 +2,7 @@
#include "../../../client/settings/cache_settings.h"
#include "../../../interface/widgets/tabs/tab_supervisor.h"
+#include "../../pixel_map_generator.h"
#include "../../theme_manager.h"
#include "../../window_main.h"
#include "../cards/art_crop_attribution.h"
@@ -10,6 +11,8 @@
#include "home_tab_button_color.h"
#include
+#include
+#include
#include
#include
#include
@@ -20,7 +23,7 @@
#include
HomeWidget::HomeWidget(QWidget *parent, TabSupervisor *_tabSupervisor)
- : QWidget(parent), tabSupervisor(_tabSupervisor), background("theme:backgrounds/home"), overlay("theme:cockatrice")
+ : QWidget(parent), tabSupervisor(_tabSupervisor), background(themePixmap(QStringLiteral("backgrounds/home")))
{
layout = new QGridLayout(this);
@@ -52,16 +55,27 @@ HomeWidget::HomeWidget(QWidget *parent, TabSupervisor *_tabSupervisor)
// Lambda is cleaner to read than overloading this
connect(&SettingsCache::instance().appearance(), &AppearanceSettings::homeTabDisplayCardNameChanged, this,
[this] { repaint(); });
+ connect(&SettingsCache::instance().appearance(), &AppearanceSettings::homeTabBackgroundDimChanged, this,
+ [this] { repaint(); });
connect(&SettingsCache::instance(), &SettingsCache::themeChanged, this,
&HomeWidget::initializeBackgroundFromSource);
connect(&SettingsCache::instance(), &SettingsCache::themeChanged, this,
&HomeWidget::updateButtonsToBackgroundColor);
+ // Scheme flips (light/dark/system with an OS switch) fire on themeManager,
+ // not on SettingsCache::themeChanged, so re-resolve the variant background.
+ connect(themeManager, &ThemeManager::themeChanged, this, &HomeWidget::initializeBackgroundFromSource);
+ connect(themeManager, &ThemeManager::paletteChanged, this, &HomeWidget::updateButtonsToBackgroundColor);
+ connect(themeManager, &ThemeManager::paletteChanged, this, &HomeWidget::updateLogoOverlay);
connect(&SettingsCache::instance().appearance(), &AppearanceSettings::homeTabButtonColorChanged, this,
&HomeWidget::updateButtonsToBackgroundColor);
}
void HomeWidget::initializeBackgroundFromSource()
{
+ // The featured logo is theme/scheme-derived too; reload it alongside the
+ // background so a theme or appearance switch doesn't leave it stale.
+ updateLogoOverlay();
+
if (CardDatabaseManager::getInstance()->getLoadStatus() != LoadStatus::Ok) {
connect(CardDatabaseManager::getInstance(), &CardDatabase::cardDatabaseLoadingFinished, this,
&HomeWidget::initializeBackgroundFromSource);
@@ -74,7 +88,7 @@ void HomeWidget::initializeBackgroundFromSource()
switch (backgroundSourceType) {
case BackgroundSources::Theme:
cardChangeTimer->stop();
- background = QPixmap("theme:backgrounds/home");
+ background = themePixmap(QStringLiteral("backgrounds/home"));
backgroundSourceDeck = DeckList();
backgroundSourceCard->setCard(ExactCard());
updateButtonsToBackgroundColor();
@@ -100,32 +114,24 @@ void HomeWidget::loadBackgroundSourceDeck()
backgroundSourceDeck = deckOpt.has_value() ? deckOpt.value().deckList : DeckList();
}
-static bool isDefaultBackgroundAndTheme()
+static QPair paletteDerivedButtonColors()
{
- QString sourceId = SettingsCache::instance().appearance().getHomeTabBackgroundSource();
- return themeManager->isBuiltInTheme() && BackgroundSources::fromId(sourceId) == BackgroundSources::Theme;
+ return {themeManager->appColor(AppColor::AccentStrong), themeManager->appColor(AppColor::AccentSoft)};
}
QPair HomeWidget::determineButtonColor() const
{
- static QPair defaultColor = {QColor::fromRgb(20, 140, 60), QColor::fromRgb(120, 200, 80)};
-
auto colorSource =
HomeTabButtonColor::intToSource(SettingsCache::instance().appearance().getHomeTabButtonColorSourceIndex());
switch (colorSource) {
- case HomeTabButtonColor::Automatic: {
- if (isDefaultBackgroundAndTheme()) {
- return defaultColor;
- } else {
- return extractDominantColors(background);
- }
- }
+ case HomeTabButtonColor::FromThemeColors:
+ return paletteDerivedButtonColors();
case HomeTabButtonColor::FromBackground:
return extractDominantColors(background);
}
- return defaultColor;
+ return paletteDerivedButtonColors();
}
void HomeWidget::setRandomCard(ExactCard &newCard)
@@ -229,10 +235,10 @@ QGroupBox *HomeWidget::createButtons()
QVBoxLayout *boxLayout = new QVBoxLayout;
boxLayout->setAlignment(Qt::AlignHCenter);
- QLabel *logoLabel = new QLabel;
- logoLabel->setPixmap(overlay.scaledToWidth(200, Qt::SmoothTransformation));
+ logoLabel = new QLabel;
logoLabel->setAlignment(Qt::AlignCenter);
boxLayout->addWidget(logoLabel);
+ updateLogoOverlay();
boxLayout->addSpacing(25);
connectButton = new HomeStyledButton("Connect/Play", gradientColors);
@@ -360,13 +366,15 @@ void HomeWidget::paintEvent(QPaintEvent *event)
painter.drawPixmap(topLeft, toDraw);
}
- // Draw translucent black overlay with rounded corners
- QRectF overlayRect(5, 5, width() - 10, height() - 10);
- QPainterPath roundedRectPath;
- roundedRectPath.addRoundedRect(overlayRect, 20, 20);
+ if (SettingsCache::instance().appearance().getHomeTabBackgroundDim()) {
+ // Draw translucent black overlay with rounded corners
+ QRectF overlayRect(5, 5, width() - 10, height() - 10);
+ QPainterPath roundedRectPath;
+ roundedRectPath.addRoundedRect(overlayRect, 20, 20);
- QColor semiTransparentBlack(0, 0, 0, static_cast(255 * 0.33));
- painter.fillPath(roundedRectPath, semiTransparentBlack);
+ QColor semiTransparentBlack(0, 0, 0, static_cast(255 * 0.33));
+ painter.fillPath(roundedRectPath, semiTransparentBlack);
+ }
// Card name overlay (above the attribution, bottom-right)
QString cardName;
@@ -431,3 +439,56 @@ void HomeWidget::paintEvent(QPaintEvent *event)
QWidget::paintEvent(event);
}
+
+void HomeWidget::updateLogoOverlay()
+{
+ // Emulate cockatrice.svg in Qt rather than rendering the baked-in SVG.
+ // The SVG has no separate plate: the gradient fills the bird's silhouette
+ // paths (light #c9fd62/AccentSoft at the top-left, dark #139740/AccentStrong
+ // toward the bottom-right — the SVG's linearGradient4265-7-8 stops along
+ // its userSpaceOnUse axis), and the white highlight path
+ // (cockatrice-logo-white) sits on top. So we paint that gradient clipped to
+ // the full logo silhouette (the full-color logo's alpha), then overlay the
+ // white mark. Colours stay fully theme-driven and independent of the static
+ // greens baked into the SVG.
+ const QColor strong = themeManager->appColor(AppColor::AccentStrong);
+ const QColor soft = themeManager->appColor(AppColor::AccentSoft);
+
+ const QPixmap silhouette = themePixmap(QStringLiteral("cockatrice")).scaledToWidth(200, Qt::SmoothTransformation);
+ const QPixmap whiteMark =
+ themePixmap(QStringLiteral("cockatrice-logo-white")).scaledToWidth(200, Qt::SmoothTransformation);
+ if (silhouette.isNull() || whiteMark.isNull()) {
+ return;
+ }
+
+ QPixmap composite(silhouette.size());
+ composite.fill(Qt::transparent);
+
+ {
+ QPainter painter(&composite);
+ painter.setRenderHint(QPainter::Antialiasing);
+ painter.setRenderHint(QPainter::SmoothPixmapTransform);
+
+ // Recreate cockatrice.svg's own gradient geometry (linearGradient
+ // 4265-7-8, userSpaceOnUse): light AccentSoft at S=(-8.097,-97.746),
+ // dark AccentStrong at E=(162.455,295.208), on the SVG's 300x300
+ // canvas. Scale those coordinates to this composite's size.
+ const qreal scale = composite.width() / 300.0;
+ QLinearGradient gradient(QPointF(-8.097, -97.746) * scale, QPointF(162.455, 295.208) * scale);
+ gradient.setColorAt(0.0, soft);
+ gradient.setColorAt(1.0, strong);
+ painter.fillRect(composite.rect(), gradient);
+
+ // Clip the gradient to the full logo silhouette exactly as the SVG's
+ // gradient paths are confined to the bird.
+ painter.setCompositionMode(QPainter::CompositionMode_DestinationIn);
+ painter.drawPixmap(0, 0, silhouette);
+
+ painter.setCompositionMode(QPainter::CompositionMode_SourceOver);
+ painter.drawPixmap(0, 0, whiteMark);
+ }
+
+ if (logoLabel) {
+ logoLabel->setPixmap(composite);
+ }
+}
diff --git a/cockatrice/src/interface/widgets/general/home_widget.h b/cockatrice/src/interface/widgets/general/home_widget.h
index 9df0d7b6a..1cadc4a67 100644
--- a/cockatrice/src/interface/widgets/general/home_widget.h
+++ b/cockatrice/src/interface/widgets/general/home_widget.h
@@ -15,6 +15,9 @@
#include
#include
+class QGridLayout;
+class QLabel;
+
class HomeWidget : public QWidget
{
@@ -41,13 +44,14 @@ private:
QPixmap background;
CardInfoPictureArtCropWidget *backgroundSourceCard = nullptr;
DeckList backgroundSourceDeck;
- QPixmap overlay;
+ QLabel *logoLabel = nullptr;
QPair gradientColors;
HomeStyledButton *connectButton;
void setRandomCard(ExactCard &newCard);
void loadBackgroundSourceDeck();
QPair determineButtonColor() const;
+ void updateLogoOverlay();
};
#endif // HOME_WIDGET_H
diff --git a/cockatrice/src/interface/widgets/menus/deck_editor_menu.cpp b/cockatrice/src/interface/widgets/menus/deck_editor_menu.cpp
index f057680be..6e27b4e42 100644
--- a/cockatrice/src/interface/widgets/menus/deck_editor_menu.cpp
+++ b/cockatrice/src/interface/widgets/menus/deck_editor_menu.cpp
@@ -28,6 +28,9 @@ DeckEditorMenu::DeckEditorMenu(AbstractTabDeckEditor *parent) : QMenu(parent), d
aSaveDeckAs = new QAction(QString(), this);
connect(aSaveDeckAs, &QAction::triggered, deckEditor, &AbstractTabDeckEditor::actSaveDeckAs);
+ aShareDeck = new QAction(QString(), this);
+ connect(aShareDeck, &QAction::triggered, deckEditor, &AbstractTabDeckEditor::actShareDeck);
+
aLoadDeckFromClipboard = new QAction(QString(), this);
connect(aLoadDeckFromClipboard, &QAction::triggered, deckEditor, &AbstractTabDeckEditor::actLoadDeckFromClipboard);
@@ -96,6 +99,7 @@ DeckEditorMenu::DeckEditorMenu(AbstractTabDeckEditor *parent) : QMenu(parent), d
addMenu(loadRecentDeckMenu);
addAction(aSaveDeck);
addAction(aSaveDeckAs);
+ addAction(aShareDeck);
addSeparator();
addAction(aLoadDeckFromClipboard);
addMenu(editDeckInClipboardMenu);
@@ -120,6 +124,7 @@ void DeckEditorMenu::setSaveStatus(bool newStatus)
{
aSaveDeck->setEnabled(newStatus);
aSaveDeckAs->setEnabled(newStatus);
+ aShareDeck->setEnabled(newStatus);
aSaveDeckToClipboard->setEnabled(newStatus);
aSaveDeckToClipboardNoSetInfo->setEnabled(newStatus);
aSaveDeckToClipboardRaw->setEnabled(newStatus);
@@ -157,6 +162,7 @@ void DeckEditorMenu::retranslateUi()
aClearRecents->setText(tr("Clear"));
aSaveDeck->setText(tr("&Save deck"));
aSaveDeckAs->setText(tr("Save deck &as..."));
+ aShareDeck->setText(tr("Share deck..."));
aLoadDeckFromClipboard->setText(tr("Load deck from cl&ipboard..."));
diff --git a/cockatrice/src/interface/widgets/menus/deck_editor_menu.h b/cockatrice/src/interface/widgets/menus/deck_editor_menu.h
index eff9257bb..08ce7bba1 100644
--- a/cockatrice/src/interface/widgets/menus/deck_editor_menu.h
+++ b/cockatrice/src/interface/widgets/menus/deck_editor_menu.h
@@ -21,7 +21,8 @@ public:
QAction *aNewDeck, *aLoadDeck, *aClearRecents, *aSaveDeck, *aSaveDeckAs, *aLoadDeckFromClipboard,
*aEditDeckInClipboard, *aEditDeckInClipboardRaw, *aSaveDeckToClipboard, *aSaveDeckToClipboardNoSetInfo,
*aSaveDeckToClipboardRaw, *aSaveDeckToClipboardRawNoSetInfo, *aPrintDeck, *aLoadDeckFromWebsite,
- *aExportDeckDecklist, *aExportDeckDecklistXyz, *aAnalyzeDeckDeckstats, *aAnalyzeDeckTappedout, *aClose;
+ *aExportDeckDecklist, *aExportDeckDecklistXyz, *aAnalyzeDeckDeckstats, *aAnalyzeDeckTappedout, *aShareDeck,
+ *aClose;
QMenu *loadRecentDeckMenu, *analyzeDeckMenu, *editDeckInClipboardMenu, *saveDeckToClipboardMenu;
void setSaveStatus(bool newStatus);
diff --git a/cockatrice/src/interface/widgets/onboarding/banner_shader_config.h b/cockatrice/src/interface/widgets/onboarding/banner_shader_config.h
index 32f3e89c0..6008044ff 100644
--- a/cockatrice/src/interface/widgets/onboarding/banner_shader_config.h
+++ b/cockatrice/src/interface/widgets/onboarding/banner_shader_config.h
@@ -38,6 +38,10 @@ class BannerShaderConfig : public QObject
Q_PROPERTY(QColor colorA READ colorA WRITE setColorA NOTIFY colorAChanged)
Q_PROPERTY(QColor colorB READ colorB WRITE setColorB NOTIFY colorBChanged)
Q_PROPERTY(QColor accent READ accent WRITE setAccent NOTIFY accentChanged)
+ Q_PROPERTY(QColor glowColor READ glowColor WRITE setGlowColor NOTIFY glowColorChanged)
+ Q_PROPERTY(QColor brandStrong READ brandStrong WRITE setBrandStrong NOTIFY brandStrongChanged)
+ Q_PROPERTY(QColor brandSoft READ brandSoft WRITE setBrandSoft NOTIFY brandSoftChanged)
+ Q_PROPERTY(qreal vignetteMin READ vignetteMin WRITE setVignetteMin NOTIFY vignetteMinChanged)
Q_PROPERTY(bool logoVisible READ logoVisible WRITE setLogoVisible NOTIFY logoVisibleChanged)
Q_PROPERTY(qreal logoGlow READ logoGlow WRITE setLogoGlow NOTIFY logoGlowChanged)
@@ -185,6 +189,54 @@ public:
}
}
+ QColor glowColor() const
+ {
+ return m_glowColor;
+ }
+ void setGlowColor(const QColor &c)
+ {
+ if (c != m_glowColor) {
+ m_glowColor = c;
+ emit glowColorChanged();
+ }
+ }
+
+ QColor brandStrong() const
+ {
+ return m_brandStrong;
+ }
+ void setBrandStrong(const QColor &c)
+ {
+ if (c != m_brandStrong) {
+ m_brandStrong = c;
+ emit brandStrongChanged();
+ }
+ }
+
+ QColor brandSoft() const
+ {
+ return m_brandSoft;
+ }
+ void setBrandSoft(const QColor &c)
+ {
+ if (c != m_brandSoft) {
+ m_brandSoft = c;
+ emit brandSoftChanged();
+ }
+ }
+
+ qreal vignetteMin() const
+ {
+ return m_vignetteMin;
+ }
+ void setVignetteMin(qreal v)
+ {
+ if (v != m_vignetteMin) {
+ m_vignetteMin = v;
+ emit vignetteMinChanged();
+ }
+ }
+
bool logoVisible() const
{
return m_logoVisible;
@@ -222,6 +274,10 @@ signals:
void colorAChanged();
void colorBChanged();
void accentChanged();
+ void glowColorChanged();
+ void brandStrongChanged();
+ void brandSoftChanged();
+ void vignetteMinChanged();
void logoVisibleChanged();
void logoGlowChanged();
@@ -239,9 +295,16 @@ private:
bool m_frontIsA = true;
+ // Curated fallback seed values -- BannerHost overwrites these with
+ // palette-derived colours (see shader_banner_widget.cpp) before the first
+ // paint, so they only matter as a safe pre-first-apply default.
QColor m_colorA{0x1A, 0x1A, 0x20};
QColor m_colorB{0x0E, 0x0E, 0x12};
QColor m_accent{0x8B, 0xDD, 0x6B};
+ QColor m_glowColor{Qt::white};
+ QColor m_brandStrong{0x13, 0x97, 0x40};
+ QColor m_brandSoft{0xC9, 0xFD, 0x62};
+ qreal m_vignetteMin = 0.62;
bool m_logoVisible = false;
qreal m_logoGlow = 1.0;
diff --git a/cockatrice/src/interface/widgets/onboarding/brand_colors.h b/cockatrice/src/interface/widgets/onboarding/brand_colors.h
new file mode 100644
index 000000000..bf173270b
--- /dev/null
+++ b/cockatrice/src/interface/widgets/onboarding/brand_colors.h
@@ -0,0 +1,15 @@
+#ifndef BRAND_COLORS_H
+#define BRAND_COLORS_H
+
+#include
+
+/** @brief Cockatrice brand green.
+ *
+ * Single source of truth for the onboarding brand accent: it backs the
+ * banner's shader-accent uniform as the curated fallback when the active
+ * palette resolves no usable Highlight, and it preseads the wizard's
+ * QuickSetupPanel so a freshly generated palette keeps the brand identity
+ * until the user picks their own look. */
+inline const QColor kCockatriceBrandGreen(0x8B, 0xDD, 0x6B);
+
+#endif // BRAND_COLORS_H
\ No newline at end of file
diff --git a/cockatrice/src/interface/widgets/onboarding/first_run_wizard.cpp b/cockatrice/src/interface/widgets/onboarding/first_run_wizard.cpp
index 618ac6f26..4af02fe4f 100644
--- a/cockatrice/src/interface/widgets/onboarding/first_run_wizard.cpp
+++ b/cockatrice/src/interface/widgets/onboarding/first_run_wizard.cpp
@@ -182,6 +182,13 @@ void FirstRunWizard::onCardDatabaseUpdateFinished(bool success)
}
}
+void FirstRunWizard::onCardDatabaseUpdateProgress(const QString &stage, qint64 done, qint64 total)
+{
+ if (cardDatabasePage) {
+ cardDatabasePage->onUpdateProgress(stage, done, total);
+ }
+}
+
void FirstRunWizard::finish()
{
accept();
diff --git a/cockatrice/src/interface/widgets/onboarding/first_run_wizard.h b/cockatrice/src/interface/widgets/onboarding/first_run_wizard.h
index 2c186ef95..21d7b6e06 100644
--- a/cockatrice/src/interface/widgets/onboarding/first_run_wizard.h
+++ b/cockatrice/src/interface/widgets/onboarding/first_run_wizard.h
@@ -37,6 +37,9 @@ public slots:
/** @brief Forwarded from MainWindow once the background card database update process exits. */
void onCardDatabaseUpdateFinished(bool success);
+ /** @brief Forwarded from MainWindow while the background card database update process runs. */
+ void onCardDatabaseUpdateProgress(const QString &stage, qint64 done, qint64 total);
+
protected:
void closeEvent(QCloseEvent *event) override;
void changeEvent(QEvent *event) override;
diff --git a/cockatrice/src/interface/widgets/onboarding/pages/card_database_setup_page.cpp b/cockatrice/src/interface/widgets/onboarding/pages/card_database_setup_page.cpp
index 12116de7a..50e8ff63d 100644
--- a/cockatrice/src/interface/widgets/onboarding/pages/card_database_setup_page.cpp
+++ b/cockatrice/src/interface/widgets/onboarding/pages/card_database_setup_page.cpp
@@ -15,6 +15,7 @@
#include
#include
#include
+#include
#include
#include
@@ -179,6 +180,25 @@ void CardDatabaseSetupPage::onUpdateFinished(bool success)
}
}
+void CardDatabaseSetupPage::onUpdateProgress(const QString &stage, qint64 done, qint64 total)
+{
+ if (state != State::Running) {
+ return;
+ }
+ progressBar->setRange(0, total > 0 ? static_cast(qMin(total, INT_MAX)) : 0);
+ progressBar->setValue(static_cast(qMin(done, INT_MAX)));
+ if (total > 0) {
+ const int percent = static_cast((100.0 * done) / total);
+ if (stage == QLatin1String("download")) {
+ statusLabel->setText(tr("Downloading the card database (%1%)…").arg(percent));
+ } else if (stage == QLatin1String("scan")) {
+ statusLabel->setText(tr("Parsing the card database (%1%)…").arg(percent));
+ } else if (stage == QLatin1String("import")) {
+ statusLabel->setText(tr("Importing cards (%1%)…").arg(percent));
+ }
+ }
+}
+
QString CardDatabaseSetupPage::nextButtonText() const
{
return state == State::NotStarted ? tr("Download") : QString();
diff --git a/cockatrice/src/interface/widgets/onboarding/pages/card_database_setup_page.h b/cockatrice/src/interface/widgets/onboarding/pages/card_database_setup_page.h
index 0461d11d5..870e759ea 100644
--- a/cockatrice/src/interface/widgets/onboarding/pages/card_database_setup_page.h
+++ b/cockatrice/src/interface/widgets/onboarding/pages/card_database_setup_page.h
@@ -30,6 +30,7 @@ public:
void retranslateUi() override;
void onUpdateFinished(bool success);
+ void onUpdateProgress(const QString &stage, qint64 done, qint64 total);
signals:
void updateRequested();
diff --git a/cockatrice/src/interface/widgets/onboarding/pages/theme_setup_page.cpp b/cockatrice/src/interface/widgets/onboarding/pages/theme_setup_page.cpp
index 3293b19ac..797fe4425 100644
--- a/cockatrice/src/interface/widgets/onboarding/pages/theme_setup_page.cpp
+++ b/cockatrice/src/interface/widgets/onboarding/pages/theme_setup_page.cpp
@@ -14,10 +14,31 @@
#include
#include
#include
+#include
#include
#include
#include
+namespace
+{
+/** @brief A theme's shipped identity accent, immune to any user- or auto-
+ * generated palette that may currently be masking appColor(). */
+QColor themeIdentityAccent(const QString &themeDirPath, const QString &themeName)
+{
+ for (const QString &scheme : {QStringLiteral("Light"), QStringLiteral("Dark")}) {
+ const PaletteConfig cfg = ThemeManager::loadDefaultPaletteConfig(themeDirPath, themeName, scheme);
+ if (cfg.appColors.contains(AppColor::AccentStrong)) {
+ return cfg.appColors.value(AppColor::AccentStrong);
+ }
+ const QColor highlight = cfg.colors.value(QPalette::Active).value(QPalette::Highlight);
+ if (highlight.isValid()) {
+ return highlight;
+ }
+ }
+ return {};
+}
+} // namespace
+
ThemeSetupPage::ThemeSetupPage(QWidget *parent) : FirstRunWizardPage(parent)
{
themeCombo = new QComboBox(this);
@@ -30,6 +51,15 @@ ThemeSetupPage::ThemeSetupPage(QWidget *parent) : FirstRunWizardPage(parent)
quickSetupPanel = new QuickSetupPanel(this);
+ // Seed the picker from the current theme's own identity accent rather than
+ // a hardcoded brand green: Plasma seeds violet, Fusion green, etc., and it
+ // is immune to stale generated palettes that may mask appColor(). This was
+ // initially a brand-green workaround from before Fusion became the default.
+ // setAccentColor blocks signals, so this never triggers a generation.
+ lastSeededTheme = SettingsCache::instance().getThemeName();
+ quickSetupPanel->setAccentColor(
+ themeIdentityAccent(themeManager->getAvailableThemes().value(lastSeededTheme), lastSeededTheme));
+
connect(themeCombo, QOverload::of(&QComboBox::currentIndexChanged), this, &ThemeSetupPage::onThemeChanged);
connect(schemeCombo, QOverload::of(&QComboBox::currentIndexChanged), this, &ThemeSetupPage::onSchemeChanged);
connect(quickSetupPanel, &QuickSetupPanel::valueChanged, this, &ThemeSetupPage::onGenerateFromAccent);
@@ -47,7 +77,8 @@ ThemeSetupPage::ThemeSetupPage(QWidget *parent) : FirstRunWizardPage(parent)
// Mirrors AppearanceSettingsPage's identical listener for the combo-sync
// half of this.
connect(themeManager, &ThemeManager::themeChanged, this, [this] {
- const QString newDir = themeManager->getAvailableThemes().value(SettingsCache::instance().getThemeName());
+ const QString newTheme = SettingsCache::instance().getThemeName();
+ const QString newDir = themeManager->getAvailableThemes().value(newTheme);
const ThemeConfig cfg = ThemeConfig::fromThemeDir(newDir);
const QString current = cfg.colorScheme;
@@ -56,6 +87,14 @@ ThemeSetupPage::ThemeSetupPage(QWidget *parent) : FirstRunWizardPage(parent)
schemeCombo->setCurrentIndex(idx >= 0 ? idx : 0);
schemeCombo->blockSignals(false);
+ // Keep the picker's accent in step with the theme's own identity; the
+ // swatch seeded at construction would otherwise stay stale (e.g. green
+ // from a previous theme) when the user toggles themes.
+ if (newTheme != lastSeededTheme) {
+ lastSeededTheme = newTheme;
+ quickSetupPanel->setAccentColor(themeIdentityAccent(newDir, newTheme));
+ }
+
maybeAutoGeneratePalette();
});
@@ -158,8 +197,14 @@ void ThemeSetupPage::maybeAutoGeneratePalette()
const QString dirPath = themeManager->getAvailableThemes().value(SettingsCache::instance().getThemeName());
const QString scheme = resolvedScheme();
+ // The theme dir may resolve to the user profile even for built-in themes
+ // (getAvailableThemes gives the user copy precedence), so consult the
+ // shipped palette too -- both via loadDefaultPaletteConfig's system fallback.
+ // Without it, scheme flips regenerate a fresh palette from the picker accent
+ // and clobber the curated colours the theme explicitly ships.
if (PaletteConfig::fromScheme(dirPath, scheme).hasPalette() ||
- PaletteConfig::fromDefault(dirPath, scheme).hasPalette()) {
+ ThemeManager::loadDefaultPaletteConfig(dirPath, SettingsCache::instance().getThemeName(), scheme)
+ .hasPalette()) {
return; // theme already has something real to show -- leave it alone
}
diff --git a/cockatrice/src/interface/widgets/onboarding/pages/theme_setup_page.h b/cockatrice/src/interface/widgets/onboarding/pages/theme_setup_page.h
index d1f84c1b9..16a3c9a5d 100644
--- a/cockatrice/src/interface/widgets/onboarding/pages/theme_setup_page.h
+++ b/cockatrice/src/interface/widgets/onboarding/pages/theme_setup_page.h
@@ -53,6 +53,9 @@ private:
QComboBox *homeTabBackgroundCombo;
bool paletteDirty = false;
+
+ /// Theme whose identity accent currently seeds the picker.
+ QString lastSeededTheme;
};
#endif // THEME_SETUP_PAGE_H
diff --git a/cockatrice/src/interface/widgets/onboarding/qml/BrandBanner.qml b/cockatrice/src/interface/widgets/onboarding/qml/BrandBanner.qml
index f1a385cad..d94e15280 100644
--- a/cockatrice/src/interface/widgets/onboarding/qml/BrandBanner.qml
+++ b/cockatrice/src/interface/widgets/onboarding/qml/BrandBanner.qml
@@ -16,6 +16,8 @@ Item {
property vector4d uColorA: Qt.vector4d(bannerConfig.colorA.r, bannerConfig.colorA.g, bannerConfig.colorA.b, 1.0)
property vector4d uColorB: Qt.vector4d(bannerConfig.colorB.r, bannerConfig.colorB.g, bannerConfig.colorB.b, 1.0)
property vector4d uAccent: Qt.vector4d(bannerConfig.accent.r, bannerConfig.accent.g, bannerConfig.accent.b, 1.0)
+ property vector4d uGlowColor: Qt.vector4d(bannerConfig.glowColor.r, bannerConfig.glowColor.g, bannerConfig.glowColor.b, 1.0)
+ property real uVignetteMin: bannerConfig.vignetteMin
property real uLogoGlow: bannerConfig.logoGlow
fragmentShader: "qrc:/onboarding/shaders/brand_banner.frag.qsb"
}
@@ -33,30 +35,62 @@ Item {
property vector4d uColorA: Qt.vector4d(bannerConfig.colorA.r, bannerConfig.colorA.g, bannerConfig.colorA.b, 1.0)
property vector4d uColorB: Qt.vector4d(bannerConfig.colorB.r, bannerConfig.colorB.g, bannerConfig.colorB.b, 1.0)
property vector4d uAccent: Qt.vector4d(bannerConfig.accent.r, bannerConfig.accent.g, bannerConfig.accent.b, 1.0)
+ property vector4d uGlowColor: Qt.vector4d(bannerConfig.glowColor.r, bannerConfig.glowColor.g, bannerConfig.glowColor.b, 1.0)
+ property real uVignetteMin: bannerConfig.vignetteMin
property real uLogoGlow: bannerConfig.logoGlow
fragmentShader: "qrc:/onboarding/shaders/brand_banner.frag.qsb"
}
- // The hero logo itself — breathes cleanly over a 0.5–1.0 opacity range
- Image {
- id: logo
- anchors.centerIn: parent
+ // The white logo sits at full opacity on top of the static gradient plate —
+ // no glow, no breathing. The plate matches home_widget's QPainter composite.
+ Item {
+ id: logoHost
visible: bannerConfig.logoVisible
- source: "qrc:/resources/cockatrice-logo-white.svg"
+ anchors.centerIn: parent
width: root.height * 0.6
- height: width * (sourceSize.height > 0 ? sourceSize.height / Math.max(sourceSize.width, 1) : 1)
- fillMode: Image.PreserveAspectFit
- smooth: true
- opacity: 0.5 + 0.5 * bannerConfig.logoGlow
- sourceSize: Qt.size(256, 256)
+ height: width
- Behavior on opacity { NumberAnimation { duration: 300; easing.type: Easing.InOutSine } }
+ // The full-color logo renders beneath the white mark and is consumed as
+ // a texture (layer.enabled) by the plate shader's silhouette mask, so
+ // the gradient is clipped to the bird exactly as the SVG's gradient
+ // paths are. It is never drawn to the screen itself.
+ Image {
+ id: silhouetteMask
+ anchors.fill: parent
+ source: "qrc:/resources/cockatrice.svg"
+ sourceSize: Qt.size(256, 256)
+ fillMode: Image.PreserveAspectFit
+ smooth: true
+ visible: false
+ layer.enabled: true
+ layer.smooth: true
+ }
- transform: Scale {
- origin.x: logo.width / 2
- origin.y: logo.height / 2
- xScale: 0.94 + 0.06 * bannerConfig.logoGlow
- yScale: 0.94 + 0.06 * bannerConfig.logoGlow
+ // The logo's gradient plate, drawn behind the mark: a linear
+ // AccentSoft (light) -> AccentStrong (dark) sheet along the same
+ // top-left -> bottom-right userSpaceOnUse axis the baked-in SVG used,
+ // clipped to the bird silhouette via uSilhouette. The white highlight
+ // path above is theme independent. Sized to the logo itself — no
+ // rounded badge, matching home_widget's QPainter composite. Static.
+ // Small ShaderEffect, Qt 6.4-safe.
+ ShaderEffect {
+ id: brandPlate
+ anchors.fill: parent
+ property vector4d uStrong: Qt.vector4d(bannerConfig.brandStrong.r, bannerConfig.brandStrong.g,
+ bannerConfig.brandStrong.b, 1.0)
+ property vector4d uSoft: Qt.vector4d(bannerConfig.brandSoft.r, bannerConfig.brandSoft.g,
+ bannerConfig.brandSoft.b, 1.0)
+ property var uSilhouette: silhouetteMask
+ fragmentShader: "qrc:/onboarding/shaders/brand_plate.frag.qsb"
+ }
+
+ Image {
+ id: logoImage
+ anchors.fill: parent
+ source: "qrc:/resources/cockatrice-logo-white.svg"
+ sourceSize: Qt.size(256, 256)
+ fillMode: Image.PreserveAspectFit
+ smooth: true
}
}
-}
+}
\ No newline at end of file
diff --git a/cockatrice/src/interface/widgets/onboarding/shader_banner_widget.cpp b/cockatrice/src/interface/widgets/onboarding/shader_banner_widget.cpp
index fd1fb2a98..0b9f65783 100644
--- a/cockatrice/src/interface/widgets/onboarding/shader_banner_widget.cpp
+++ b/cockatrice/src/interface/widgets/onboarding/shader_banner_widget.cpp
@@ -1,7 +1,10 @@
#include "shader_banner_widget.h"
+#include "../../theme_manager.h"
#include "banner_shader_config.h"
+#include "brand_colors.h"
+#include
#include
#include
#include
@@ -11,11 +14,98 @@
namespace
{
-// Near-black base palette -- the background is dark and quiet so the green
-// accent stands out.
-constexpr QRgb kColorA = 0x1A1A20;
-constexpr QRgb kColorB = 0x0E0E12;
-constexpr QRgb kAccent = 0x8BDD6B;
+// Curated near-black stage -- used only when the active palette resolves no
+// usable window colour. Matches the banner's original design (dark and quiet
+// so the accent stands out) and satisfies design-plans §2.1's "identity
+// survives a bare palette".
+constexpr QRgb kFallbackColorA = 0x1A1A20;
+constexpr QRgb kFallbackColorB = 0x0E0E12;
+
+struct SuggestedColors
+{
+ QColor colorA;
+ QColor colorB;
+ QColor accent;
+ QColor glowColor;
+ QColor brandStrong;
+ QColor brandSoft;
+ qreal vignetteMin = 0.62;
+};
+
+SuggestedColors suggestedBannerColors()
+{
+ const QPalette &pal = qApp->palette();
+ const QColor window = pal.color(QPalette::Active, QPalette::Window);
+ // Identity accent: the theme's [AppColors] AccentStrong, which appColor()
+ // resolves to QPalette::Highlight when a theme doesn't pin AccentStrong.
+ // Reading bare Highlight ignored curated accent tokens (Plasma's violet
+ // vs Default's green) whenever a palette didn't set the role itself.
+ const QColor accentStrong = themeManager->appColor(AppColor::AccentStrong);
+ if (!window.isValid() || !accentStrong.isValid()) {
+ return {QColor(kFallbackColorA),
+ QColor(kFallbackColorB),
+ kCockatriceBrandGreen,
+ QColor(Qt::white),
+ kCockatriceBrandGreen,
+ QColor(0xC9, 0xFD, 0x62),
+ 0.62};
+ }
+
+ // The theme's brand pair: AccentStrong is the deep green, AccentSoft the
+ // lime. These two appColors form the logo's "surrounding gradient" (deep
+ // core grading out to the soft, brand-toned glow) on both the banner and
+ // the home screen.
+ const QColor brandStrong = accentStrong;
+ const QColor brandSoft = themeManager->appColor(AppColor::AccentSoft);
+
+ // Dress the stage for the scheme so the banner never fights the
+ // surrounding window in either mode. Dark palettes keep the original
+ // quiet near-black stage (lightness 29 → 16) with the theme's window
+ // hue; light palettes get a pastel "frosted accent" treatment built from
+ // the accent hue instead of a plain near-white copy: a coloured wash that
+ // clearly belongs to the theme.
+ const qreal luma = 0.299 * window.red() + 0.587 * window.green() + 0.114 * window.blue();
+ const bool lightStage = luma > 115.0;
+ if (lightStage) {
+ const int hue = accentStrong.hslHue();
+ // Achromatic accents (grey) get a neutral near-white stage instead.
+ const int stageSat = hue < 0 ? 0 : 64;
+ const int hueSafe = hue < 0 ? 0 : hue;
+ // Depth is what stops a light stage reading as a washed-out near-white
+ // copy of the page behind the banner: deepen the lower pastel band and
+ // raise saturation so the hue is clearly present while staying frosted.
+ auto pastel = [hueSafe, stageSat](int lightness) { return QColor::fromHsl(hueSafe, stageSat, lightness); };
+ auto pastelLower = [hueSafe](int lightness) { return QColor::fromHsl(hueSafe, 76, lightness); };
+ // Brightness-lifted accent for additive glows: the raw accent on a
+ // light stage must be mid-bright to read instead of washing out, so
+ // lift lightness and saturation together.
+ const int accentLightness = qBound(158, accentStrong.lightness() + 82, 198);
+ const int accentSaturation = hue < 0 ? 0 : qMax(accentStrong.hslSaturation(), 180);
+ const QColor liftedAccent =
+ hue < 0 ? accentStrong : QColor::fromHsl(hueSafe, accentSaturation, accentLightness);
+ // The centre glow (and logo tint in QML) uses the deep accent itself:
+ // a coloured halo/fill behind the logo instead of a white or black one.
+ return {pastel(214), pastelLower(186), liftedAccent, accentStrong, brandStrong, brandSoft, 0.80};
+ }
+
+ // Dark stage: force the window hue down to the banner's curated darkness,
+ // scaling saturation away so chromatic palettes tint it without going
+ // muddy. The accent is the bright, brand-driven tone (hue from the accent
+ // itself, never the -- often grey -- window), and it drives both the
+ // embers/fog and the logo glow so the mark tints like the light stage.
+ auto stage = [&window](int lightness) {
+ const int hue = window.hslHue();
+ const int saturation = hue < 0 ? 0 : qBound(0, qRound(window.hslSaturation() * (lightness / 40.0)), 255);
+ return QColor::fromHsl(hue, saturation, lightness);
+ };
+ const int accentHue = accentStrong.hslHue();
+ const int accentHueSafe = accentHue < 0 ? 0 : accentHue;
+ const int accentLightness = qBound(150, accentStrong.lightness() + 70, 185);
+ const int accentSaturation = accentHue < 0 ? 0 : qMax(accentStrong.hslSaturation(), 160);
+ const QColor accent =
+ accentHue < 0 ? accentStrong : QColor::fromHsl(accentHueSafe, accentSaturation, accentLightness);
+ return {stage(29), stage(16), accent, accent, brandStrong, brandSoft, 0.62};
+}
} // namespace
class GradientFallbackWidget : public QWidget
@@ -23,15 +113,27 @@ class GradientFallbackWidget : public QWidget
public:
using QWidget::QWidget;
+ void setColors(const QColor &a, const QColor &b)
+ {
+ if (a != colorA || b != colorB) {
+ colorA = a;
+ colorB = b;
+ }
+ }
+
protected:
void paintEvent(QPaintEvent *) override
{
QPainter painter(this);
QLinearGradient gradient(0, 0, width(), height());
- gradient.setColorAt(0.0, QColor(kColorA));
- gradient.setColorAt(1.0, QColor(kColorB));
+ gradient.setColorAt(0.0, colorA);
+ gradient.setColorAt(1.0, colorB);
painter.fillRect(rect(), gradient);
}
+
+private:
+ QColor colorA{QColor(kFallbackColorA)};
+ QColor colorB{QColor(kFallbackColorB)};
};
BannerHost::BannerHost(QWidget *parent) : QWidget(parent)
@@ -62,6 +164,9 @@ BannerHost::BannerHost(QWidget *parent) : QWidget(parent)
connect(&clock, &QTimer::timeout, this, &BannerHost::tick);
clock.setInterval(16); // ~60fps; the shader itself is cheap, this is just a wall clock
+ connect(themeManager, &ThemeManager::themeChanged, this, &BannerHost::applyThemeColors);
+ applyThemeColors();
+
applyMotifPreset(currentMotif);
updateAspect();
}
@@ -123,9 +228,9 @@ void BannerHost::applyMotifPreset(Motif motif)
const Preset p = presetFor(motif);
- config->setColorA(QColor(kColorA));
- config->setColorB(QColor(kColorB));
- config->setAccent(QColor(kAccent));
+ config->setColorA(bannerColorA);
+ config->setColorB(bannerColorB);
+ config->setAccent(bannerAccent);
config->setLogoVisible(motif == Motif::Welcome);
if (isFirstApply) {
@@ -183,8 +288,34 @@ void BannerHost::hideEvent(QHideEvent *event)
clock.stop();
}
+void BannerHost::applyThemeColors()
+{
+ const SuggestedColors colors = suggestedBannerColors();
+ bannerColorA = colors.colorA;
+ bannerColorB = colors.colorB;
+ bannerAccent = colors.accent;
+
+ if (usingFallback) {
+ fallback->setColors(bannerColorA, bannerColorB);
+ fallback->update();
+ } else if (config) {
+ config->setColorA(bannerColorA);
+ config->setColorB(bannerColorB);
+ config->setAccent(bannerAccent);
+ config->setGlowColor(colors.glowColor);
+ config->setBrandStrong(colors.brandStrong);
+ config->setBrandSoft(colors.brandSoft);
+ config->setVignetteMin(colors.vignetteMin);
+ }
+}
+
void BannerHost::tick()
{
+ // Palette previews (e.g. accent drags in the wizard's QuickSetupPanel)
+ // apply qApp->palette() without firing themeChanged, so re-derive here;
+ // BannerShaderConfig's setters are equality-guarded, so this is a no-op
+ // unless the colours actually changed.
+ applyThemeColors();
if (config) {
qreal t = elapsed.elapsed() / 1000.0;
config->setTime(t);
diff --git a/cockatrice/src/interface/widgets/onboarding/shader_banner_widget.h b/cockatrice/src/interface/widgets/onboarding/shader_banner_widget.h
index 2e230ad7f..ac47b1141 100644
--- a/cockatrice/src/interface/widgets/onboarding/shader_banner_widget.h
+++ b/cockatrice/src/interface/widgets/onboarding/shader_banner_widget.h
@@ -1,6 +1,7 @@
#ifndef SHADER_BANNER_WIDGET_H
#define SHADER_BANNER_WIDGET_H
+#include
#include
#include
#include
@@ -53,6 +54,7 @@ protected:
private slots:
void tick();
void onSceneGraphFailed();
+ void applyThemeColors();
private:
struct Preset
@@ -73,6 +75,12 @@ private:
BannerShaderConfig *config = nullptr;
GradientFallbackWidget *fallback = nullptr;
+ // Palette-derived banner colours -- the theme's window hue forced down to
+ // the banner's curated darkness, plus the theme's Highlight as accent.
+ QColor bannerColorA;
+ QColor bannerColorB;
+ QColor bannerAccent;
+
QTimer clock;
QElapsedTimer elapsed;
Motif currentMotif = Motif::Welcome;
diff --git a/cockatrice/src/interface/widgets/onboarding/shaders/brand_banner.frag b/cockatrice/src/interface/widgets/onboarding/shaders/brand_banner.frag
index 508bd4bc4..2bf6d0abf 100644
--- a/cockatrice/src/interface/widgets/onboarding/shaders/brand_banner.frag
+++ b/cockatrice/src/interface/widgets/onboarding/shaders/brand_banner.frag
@@ -28,6 +28,8 @@ layout(std140, binding = 0) uniform buf
vec4 uColorA;
vec4 uColorB;
vec4 uAccent;
+ vec4 uGlowColor;
+ float uVignetteMin;
float uLogoGlow;
};
@@ -119,7 +121,7 @@ vec3 backgroundField(vec2 uv, float time)
// Accent-coloured fog layer: flowNoise peaks above 0.6 contribute accent
float fog = flowNoise(uv * 0.8 + vec2(100.0, 50.0), time * 0.02);
- col += uAccent.rgb * max(fog - 0.6, 0.0) * 0.10;
+ col += uAccent.rgb * max(fog - 0.6, 0.0) * 0.14;
return col;
}
@@ -136,14 +138,17 @@ vec3 motifWelcome(vec2 uv, vec3 bg, float t)
vec2 center = vec2(asp * 0.5, 0.5);
float cDist = length(ac - center);
- // Centre bloom at logo position; intensity scales with uLogoGlow
+ // Centre bloom at logo position; intensity scales with uLogoGlow. The
+ // QML brandGlow halo now supplies the primary logo surround (the two
+ // brand appColors), so this shader bloom is deliberately kept as a subtle
+ // ambience rather than a competing glow.
float centreLight = bloom(cDist, 0.08 * asp, 0.40 * asp);
- col += centreLight * 0.20 * uLogoGlow;
+ col += uGlowColor.rgb * centreLight * 0.20 * uLogoGlow;
// Flow-noise shimmer gated by Gaussian mask at centre
float shimmer = flowNoise(ac * 0.8 + vec2(55.0, 33.0), t * 0.05) * 0.5 + 0.5;
float shimmerMask = exp(-(cDist * cDist) / (0.18 * asp * 0.18 * asp));
- col += shimmer * shimmerMask * 0.04 * uLogoGlow;
+ col += uGlowColor.rgb * shimmer * shimmerMask * 0.04 * uLogoGlow;
// 48 ember particles: hash-seeded position, speed, size, brightness.
// Embers within a distance threshold of centre are deflected into an
@@ -162,8 +167,8 @@ vec3 motifWelcome(vec2 uv, vec3 bg, float t)
float pX = baseX * asp + sin(t * driftFreq + fi * 1.7) * driftAmp * asp;
float pY = fract(baseY + t * riseSpeed);
- float size = 0.006 + hash21(vec2(fi * 2.9, uSeed * 4.7)) * 0.012;
- float bright = 0.15 + hash21(vec2(fi * 6.1, uSeed * 0.9)) * 0.30;
+ float size = 0.010 + hash21(vec2(fi * 2.9, uSeed * 4.7)) * 0.014;
+ float bright = 0.18 + hash21(vec2(fi * 6.1, uSeed * 0.9)) * 0.32;
// Fade out near top/bottom edges
float edgeFade = smoothstep(0.0, 0.12, pY) * smoothstep(1.0, 0.88, pY);
@@ -232,7 +237,7 @@ vec3 motifCardDatabase(vec2 uv, vec3 bg, float t)
// Semi-transparent dark fill
float fill = smoothstep(0.015, -0.005, d);
- col = mix(col, uColorB.rgb * 0.55, fill * 0.50);
+ col = mix(col, uColorB.rgb * 0.60, fill * 0.62);
// Accent outline
float edge = smoothstep(0.035, 0.0, abs(d));
@@ -309,7 +314,7 @@ vec3 motifAccount(vec2 uv, vec3 bg, float t)
// Node glow via bloom; intensity modulated by pulse
float dist = length(ac - pos);
- col += uAccent.rgb * bloom(dist, 0.018, 0.08) * mix(0.20, 0.45, pulse);
+ col += uAccent.rgb * bloom(dist, 0.018, 0.08) * mix(0.30, 0.55, pulse);
}
// Edges: connect nodes within a radius threshold
@@ -323,19 +328,19 @@ vec3 motifAccount(vec2 uv, vec3 bg, float t)
vec2 ba = nodePos[j] - nodePos[i];
float h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0);
float lineDist = length(pa - ba * h);
- col += uAccent.rgb * smoothstep(0.010, 0.0, lineDist) * strength * 0.10;
+ col += uAccent.rgb * smoothstep(0.010, 0.0, lineDist) * strength * 0.14;
}
}
}
// Central bloom at banner centre
float cDist = length(ac - center);
- col += uAccent.rgb * bloom(cDist, 0.04, 0.25) * 0.12;
+ col += uAccent.rgb * bloom(cDist, 0.04, 0.25) * 0.20;
// Periodic expanding ring from centre
float ripplePhase = t * 0.4;
float rippleDist = abs(cDist - fract(ripplePhase) * asp * 0.7);
- col += uAccent.rgb * smoothstep(0.02, 0.0, rippleDist) * 0.10;
+ col += uAccent.rgb * smoothstep(0.02, 0.0, rippleDist) * 0.14;
return col;
}
@@ -456,6 +461,8 @@ void main()
else if (uMode < 4.5) col = motifPreferences(uv, bg, t);
else col = motifFinish(uv, bg, t);
- col *= mix(0.62, 1.0, vignette(uv));
+ // Corner vignette; uVignetteMin is scheme-driven (0.62 on dark stages,
+ // gentler on light ones so near-white corners don't go muddy grey).
+ col *= mix(uVignetteMin, 1.0, vignette(uv));
fragColor = vec4(col, 1.0) * qt_Opacity;
}
diff --git a/cockatrice/src/interface/widgets/onboarding/shaders/brand_plate.frag b/cockatrice/src/interface/widgets/onboarding/shaders/brand_plate.frag
new file mode 100644
index 000000000..43b798c9f
--- /dev/null
+++ b/cockatrice/src/interface/widgets/onboarding/shaders/brand_plate.frag
@@ -0,0 +1,42 @@
+#version 440
+
+// The logo's gradient plate, drawn by us rather than the baked-in SVG: a
+// linear blend between the two brand appColors (light AccentSoft at the
+// top-left grading to dark AccentStrong at the bottom-right, mirroring
+// cockatrice.svg's linearGradient4265-7-8 userSpaceOnUse axis), clipped to
+// the bird's full silhouette via the full-color logo's alpha (uSilhouette).
+// The white highlight path (cockatrice-logo-white) is overlaid in QML on top,
+// exactly as the SVG stacks its white path over the gradient paths. Fully
+// static: no glow, no breathing — the plate just sits there like the home
+// widget's QPainter composite.
+
+layout(location = 0) in vec2 qt_TexCoord0;
+layout(location = 0) out vec4 fragColor;
+
+layout(std140, binding = 0) uniform buf
+{
+ mat4 qt_Matrix;
+ float qt_Opacity;
+ vec4 uStrong;
+ vec4 uSoft;
+};
+
+// The full-color logo's alpha channel acts as the silhouette mask: the
+// gradient only appears inside the bird, exactly like the SVG's gradient paths.
+layout(binding = 1) uniform sampler2D uSilhouette;
+
+void main()
+{
+ // Recreate cockatrice.svg's own gradient geometry (linearGradient4265-7-8,
+ // userSpaceOnUse): light AccentSoft at the start point S=(-8.097,-97.746),
+ // dark AccentStrong at the end E=(162.455,295.208), on the SVG's 300x300
+ // canvas. Normalized to UV space, V=E-S=(0.5685,1.3098), so
+ // t = dot(uv - S_norm, V)/|V|^2 with S_norm=(-0.0270,-0.3258).
+ float t = clamp(dot(qt_TexCoord0 - vec2(-0.02699, -0.32582), vec2(0.56851, 1.30985)) / 2.03891, 0.0, 1.0);
+ vec3 color = mix(uSoft.rgb, uStrong.rgb, t);
+
+ // Anti-aliased silhouette clip from the full-color logo's alpha.
+ float alpha = texture(uSilhouette, qt_TexCoord0).a;
+
+ fragColor = vec4(color * alpha, alpha) * qt_Opacity;
+}
\ No newline at end of file
diff --git a/cockatrice/src/interface/widgets/playmat/playmat_settings_dialog.cpp b/cockatrice/src/interface/widgets/playmat/playmat_settings_dialog.cpp
index 57706cf93..9459c5ea9 100644
--- a/cockatrice/src/interface/widgets/playmat/playmat_settings_dialog.cpp
+++ b/cockatrice/src/interface/widgets/playmat/playmat_settings_dialog.cpp
@@ -2,6 +2,7 @@
#include "../../card_picture_loader/card_picture_loader.h"
#include "../cards/art_crop_attribution.h"
+#include "../cards/card_art_utils.h"
#include "../utility/completer_utils.h"
#include "card_database_display_model.h"
#include "card_database_model.h"
@@ -276,7 +277,7 @@ void PlaymatSettingsDialog::reloadPreview()
return;
}
- currentPixmap = fullRes;
+ currentPixmap = CardArtUtils::rotateSidewaysLayoutArt(fullRes, card);
preview->setPixmap(currentPixmap);
preview->setParams(currentParams);
preview->setAttribution(buildArtAttribution(card));
diff --git a/cockatrice/src/interface/widgets/printing_selector/printing_selector_card_overlay_widget.cpp b/cockatrice/src/interface/widgets/printing_selector/printing_selector_card_overlay_widget.cpp
index 0b77ca185..e8816847e 100644
--- a/cockatrice/src/interface/widgets/printing_selector/printing_selector_card_overlay_widget.cpp
+++ b/cockatrice/src/interface/widgets/printing_selector/printing_selector_card_overlay_widget.cpp
@@ -1,13 +1,19 @@
#include "printing_selector_card_overlay_widget.h"
#include "../../../client/settings/cache_settings.h"
+#include "../../card_picture_loader/card_picture_loader.h"
#include "../cards/card_info_picture_widget.h"
#include "printing_selector_card_display_widget.h"
+#include
+#include
#include
#include
#include
+#include
#include
+#include
+#include
#include
#include
#include
@@ -15,6 +21,49 @@
#include
#include
+namespace
+{
+/**
+ * @brief Places the preview beside the highlighted action inside the given screen.
+ *
+ * Side-aware: hugs the side of the action that has room, aligned with its row, then
+ * clamps every edge so the preview always lands fully on-screen on first show.
+ */
+QPoint
+previewPositionNear(const QRect &actionRect, const QSize &labelSize, const QRect &screenGeometry, int previewOffset)
+{
+ const bool rightFits = actionRect.right() + previewOffset + labelSize.width() <= screenGeometry.right();
+ const bool leftFits = actionRect.left() - previewOffset - labelSize.width() >= screenGeometry.left();
+
+ int x;
+ if (rightFits) {
+ x = actionRect.right() + previewOffset;
+ } else if (leftFits) {
+ x = actionRect.left() - previewOffset - labelSize.width();
+ } else {
+ x = actionRect.left();
+ }
+ x = qMax(screenGeometry.left(), x);
+ x = qMin(screenGeometry.right() - labelSize.width() + 1, x);
+
+ const bool belowFits = actionRect.bottom() + previewOffset + labelSize.height() <= screenGeometry.bottom();
+ const bool aboveFits = actionRect.top() - previewOffset - labelSize.height() >= screenGeometry.top();
+
+ int y;
+ if (belowFits) {
+ y = actionRect.bottom() + previewOffset;
+ } else if (aboveFits) {
+ y = actionRect.top() - previewOffset - labelSize.height();
+ } else {
+ y = actionRect.top();
+ }
+ y = qMax(screenGeometry.top(), y);
+ y = qMin(screenGeometry.bottom() - labelSize.height() + 1, y);
+
+ return {x, y};
+}
+} // namespace
+
/**
* @brief Constructs a PrintingSelectorCardOverlayWidget for displaying a card overlay.
*
@@ -50,6 +99,31 @@ PrintingSelectorCardOverlayWidget::PrintingSelectorCardOverlayWidget(QWidget *pa
initializePinBadge();
+ // Parent the preview to this overlay so it is destroyed with it (Qt::ToolTip keeps
+ // it a frameless, non-activating top-level window despite the parent).
+ cardOverridePreviewLabel = new QLabel(this, Qt::ToolTip);
+ cardOverridePreviewLabel->setWindowFlag(Qt::FramelessWindowHint);
+ cardOverridePreviewLabel->setAttribute(Qt::WA_ShowWithoutActivating);
+ cardOverridePreviewLabel->setScaledContents(true);
+ cardOverridePreviewLabel->hide();
+
+ // While the preview is visible, keep it honest: when the hovered printing's art
+ // resolves (all alternate printings share the root card's CardInfo), redraw it in place.
+ if (rootCard.getCardPtr()) {
+ connect(rootCard.getCardPtr().data(), &CardInfo::pixmapUpdated, this, [this] {
+ if (cardOverridePreviewLabel->isVisible()) {
+ refreshPreview();
+ }
+ });
+ }
+
+ // Alt-Tab / app-inactive must not strand the floating preview.
+ connect(qApp, &QGuiApplication::applicationStateChanged, this, [this](Qt::ApplicationState state) {
+ if (state != Qt::ApplicationActive) {
+ hidePreview();
+ }
+ });
+
// Update when this overlay emits cardPreferenceChanged or when size/scale changes
connect(this, &PrintingSelectorCardOverlayWidget::cardPreferenceChanged, this,
&PrintingSelectorCardOverlayWidget::updatePinBadgeVisibility);
@@ -185,19 +259,23 @@ void PrintingSelectorCardOverlayWidget::leaveEvent(QEvent *event)
}
/**
- * @brief Creates and shows a custom context menu when the right mouse button is clicked.
+ * @brief Creates and shows the card-overlay context menu.
*
- * The context menu includes an option to show related cards, which displays a submenu with actions
- * for each related card. When an action is triggered, the card information is updated, and the
- * printing selector is shown.
+ * The menu includes the card art preference (Pin/Unpin Printing), the Image Overrides
+ * submenu (Load Custom Image, Clear Custom Image, and one entry per alternate printing with a
+ * live preview), and the Show Related cards submenu.
*
- * @param point The position of the mouse when the right-click occurred.
+ * @param point The local position the menu should pop at.
*/
void PrintingSelectorCardOverlayWidget::customMenu(QPoint point)
{
QMenu menu;
- auto *preferenceMenu = new QMenu(tr("Preference"));
+ hidePreview(); // Clear any preview state left over from a previous menu run.
+
+ // Submenus are owned by the stack-allocated top-level menu (addMenu() does not
+ // transfer ownership).
+ auto *preferenceMenu = new QMenu(tr("Preference"), &menu);
menu.addMenu(preferenceMenu);
const auto &preferredProviderId =
@@ -219,8 +297,66 @@ void PrintingSelectorCardOverlayWidget::customMenu(QPoint point)
});
}
+ menu.addSeparator();
+
+ auto *overrideMenu = new QMenu(tr("Image Overrides"), &menu);
+
+ auto *loadCustomAction = overrideMenu->addAction(tr("Load Custom Image..."));
+ auto *clearOverrideAction = overrideMenu->addAction(tr("Clear Custom Image"));
+
+ // Nothing to clear on a card that has no local override yet.
+ clearOverrideAction->setEnabled(CardPictureLoader::hasLocalOverrides(rootCard));
+
+ overrideMenu->addSeparator();
+
+ const auto &allSets = rootCard.getInfo().getSets();
+
+ for (const auto &set : allSets) {
+ for (const auto &printing : set) {
+ if (printing == rootCard.getPrinting()) {
+ continue;
+ }
+
+ // The submenu is already scoped to this card, so the rows lead with set +
+ // collector; only printings with a distinct display name add their own name.
+ const CardSetPtr cardSet = printing.getSet();
+ if (!cardSet) {
+ continue;
+ }
+
+ QString label = tr("%1 %2").arg(cardSet->getCorrectedShortName(), printing.getProperty("num"));
+
+ auto *action = overrideMenu->addAction(label);
+
+ ExactCard overrideCard(rootCard.getCardPtr(), printing);
+ action->setData(QVariant::fromValue(overrideCard));
+
+ connect(action, &QAction::triggered, this, [this, overrideCard]() {
+ CardPictureLoader::getInstance().installPrintingOverride(rootCard, overrideCard);
+ QPixmapCache::clear();
+ rootCard.emitPixmapUpdated(); // refresh the overlay art in place, like the other paths
+ });
+ }
+ }
+
+ connect(clearOverrideAction, &QAction::triggered, this, [this]() {
+ CardPictureLoader::deleteAllLocalOverrides(rootCard);
+ QPixmapCache::clear();
+ rootCard.emitPixmapUpdated(); // force UI refresh
+ });
+
+ connect(loadCustomAction, &QAction::triggered, this, &PrintingSelectorCardOverlayWidget::loadCustomImage);
+
+ connect(overrideMenu, &QMenu::hovered, this, &PrintingSelectorCardOverlayWidget::showPreviewForAction);
+ connect(overrideMenu, &QMenu::aboutToHide, this, &PrintingSelectorCardOverlayWidget::hidePreview);
+ connect(overrideMenu, &QMenu::triggered, this, &PrintingSelectorCardOverlayWidget::hidePreview);
+
+ menu.addMenu(overrideMenu);
+
+ menu.addSeparator();
+
// filling out the related cards submenu
- auto *relatedMenu = new QMenu(tr("Show Related cards"));
+ auto *relatedMenu = new QMenu(tr("Show Related cards"), &menu);
menu.addMenu(relatedMenu);
auto relatedCards = rootCard.getInfo().getAllRelatedCards();
if (relatedCards.isEmpty()) {
@@ -235,7 +371,11 @@ void PrintingSelectorCardOverlayWidget::customMenu(QPoint point)
});
}
}
+ // The preview anchors itself to this popup's global geometry while it is open, so the
+ // pointer must stay valid for the whole exec() and be dropped before the stack unwinds.
+ previewSourceMenu = overrideMenu;
menu.exec(this->mapToGlobal(point));
+ previewSourceMenu = nullptr;
}
/**
@@ -291,3 +431,136 @@ void PrintingSelectorCardOverlayWidget::initializePinBadge()
pinBadge->setVisible(false);
pinBadge->raise();
}
+
+/**
+ * @brief Asks for an image file and installs it as the card's custom art.
+ *
+ * Unreadable files answer with a visible warning instead of a silent no-op.
+ */
+void PrintingSelectorCardOverlayWidget::loadCustomImage()
+{
+ QString filePath = QFileDialog::getOpenFileName(this, tr("Select Card Image"), QString(),
+ tr("Images (*.png *.jpg *.jpeg *.webp)"));
+
+ if (filePath.isEmpty()) {
+ return;
+ }
+
+ QPixmap pixmap(filePath);
+ if (pixmap.isNull()) {
+ // No silent paths: a file that cannot be read answers visibly instead of a no-op.
+ QMessageBox::warning(this, tr("Load Custom Image"), tr("The selected file could not be read as an image."));
+ return;
+ }
+
+ CardPictureLoader::getInstance().saveCardImageToLocalStorage(rootCard, pixmap, true);
+
+ QPixmapCache::clear();
+ rootCard.emitPixmapUpdated();
+}
+
+/**
+ * @brief Shows the hover preview for a highlighted printing entry in the Image Overrides submenu.
+ *
+ * QMenu::hovered fires on keyboard highlight too, so the preview appears when arrows walk
+ * onto a printing entry, not only under the mouse.
+ *
+ * Non-printing entries (e.g., Load Custom Image, Clear Custom Image) hide the preview.
+ *
+ * @param action The action that was highlighted.
+ */
+void PrintingSelectorCardOverlayWidget::showPreviewForAction(QAction *action)
+{
+ if (!action) {
+ hidePreview();
+ return;
+ }
+
+ const QVariant data = action->data();
+
+ if (!data.canConvert()) {
+ hidePreview();
+ return;
+ }
+
+ const ExactCard previewCard = qvariant_cast(data);
+ if (previewCard.isEmpty()) {
+ hidePreview();
+ return;
+ }
+
+ hoveredOverrideCard = previewCard;
+ hoveredOverrideAction = action;
+ refreshPreview();
+}
+
+/**
+ * @brief Renders the hover preview for the currently highlighted printing.
+ *
+ * The preview shows the loading placeholder while the art is pending and swaps in the real
+ * art when it resolves. The label is positioned against its already-resized geometry so the
+ * first-ever show at the screen's edges stays fully on-screen.
+ */
+void PrintingSelectorCardOverlayWidget::refreshPreview()
+{
+ if (hoveredOverrideCard.isEmpty()) {
+ hidePreview();
+ return;
+ }
+
+ constexpr QSize previewSize(240, 336);
+ constexpr int previewOffset = 20;
+
+ QPixmap pixmap;
+ CardPictureLoader::getPixmap(pixmap, hoveredOverrideCard, previewSize);
+
+ if (pixmap.isNull()) {
+ // Keep the preview honest while loading: show the loading placeholder instead of a void.
+ // Fetch at the logical size and let the label scale it, so the placeholder matches the
+ // real art's footprint rather than doubling on HiDPI displays.
+ CardPictureLoader::getCardBackLoadingInProgressPixmap(pixmap, previewSize);
+ }
+
+ cardOverridePreviewLabel->setPixmap(pixmap);
+ // QPixmap::size() is physical pixels; the label layout must use the device-independent size
+ // so the preview keeps a constant footprint across DPI settings (QScreen geometry is logical).
+ const QSize labelSize = pixmap.deviceIndependentSize().toSize();
+ cardOverridePreviewLabel->resize(labelSize);
+
+ // Anchor the preview to the walked submenu popup rather than QCursor::pos(), which is idle
+ // under keyboard-only operation: a keyboard-highlighted row must preview at the same place as
+ // a hovered one. The mouse path is unchanged in effect — the popup sits under the cursor, so
+ // the preview stays beside the row in both modalities.
+ const QMenu *popup = previewSourceMenu;
+ if (!popup || !popup->isVisible() || !hoveredOverrideAction) {
+ hidePreview();
+ return;
+ }
+
+ const QRect popupGeometry = popup->geometry();
+ const QRect actionRectLocal = popup->actionGeometry(hoveredOverrideAction);
+ const QRect actionRect(popupGeometry.topLeft() + actionRectLocal.topLeft(), actionRectLocal.size());
+
+ QScreen *screen = QGuiApplication::screenAt(popupGeometry.center());
+ if (!screen) {
+ hidePreview();
+ return;
+ }
+ const QRect &screenGeometry = screen->geometry();
+
+ cardOverridePreviewLabel->move(previewPositionNear(actionRect, labelSize, screenGeometry, previewOffset));
+ cardOverridePreviewLabel->show();
+}
+
+/**
+ * @brief Hides the hover preview and forgets the currently highlighted printing.
+ */
+void PrintingSelectorCardOverlayWidget::hidePreview()
+{
+ hoveredOverrideCard = ExactCard();
+ hoveredOverrideAction = nullptr;
+
+ if (cardOverridePreviewLabel) {
+ cardOverridePreviewLabel->hide();
+ }
+}
diff --git a/cockatrice/src/interface/widgets/printing_selector/printing_selector_card_overlay_widget.h b/cockatrice/src/interface/widgets/printing_selector/printing_selector_card_overlay_widget.h
index 228393c9c..fbcfa9230 100644
--- a/cockatrice/src/interface/widgets/printing_selector/printing_selector_card_overlay_widget.h
+++ b/cockatrice/src/interface/widgets/printing_selector/printing_selector_card_overlay_widget.h
@@ -13,6 +13,9 @@
#include
+class QAction;
+class QMenu;
+
class PrintingSelectorCardOverlayWidget : public QWidget
{
Q_OBJECT
@@ -43,11 +46,19 @@ private slots:
private:
void initializePinBadge();
+ void loadCustomImage();
+ void showPreviewForAction(QAction *action);
+ void refreshPreview();
+ void hidePreview();
CardInfoPictureWidget *cardInfoPicture;
AllZonesCardAmountWidget *allZonesCardAmountWidget;
QLabel *pinBadge = nullptr;
AbstractTabDeckEditor *deckEditor;
ExactCard rootCard;
+ QLabel *cardOverridePreviewLabel = nullptr;
+ ExactCard hoveredOverrideCard;
+ QMenu *previewSourceMenu = nullptr;
+ QAction *hoveredOverrideAction = nullptr;
};
#endif // PRINTING_SELECTOR_CARD_OVERLAY_WIDGET_H
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 9c433ab5a..881c3e3ac 100644
--- a/cockatrice/src/interface/widgets/quick_settings/settings_button_widget.cpp
+++ b/cockatrice/src/interface/widgets/quick_settings/settings_button_widget.cpp
@@ -1,5 +1,7 @@
#include "settings_button_widget.h"
+#include "../../pixel_map_generator.h"
+
#include
#include
#include
@@ -8,7 +10,7 @@
SettingsButtonWidget::SettingsButtonWidget(QWidget *parent)
: QWidget(parent), button(new QToolButton(this)), popup(new SettingsPopupWidget(nullptr))
{
- button->setIcon(QPixmap("theme:icons/cogwheel"));
+ button->setIcon(themePixmap(QStringLiteral("icons/cogwheel")));
button->setCheckable(true);
button->setFixedSize(32, 32);
connect(button, &QToolButton::clicked, this, &SettingsButtonWidget::togglePopup);
diff --git a/cockatrice/src/interface/widgets/replay/replay_manager.cpp b/cockatrice/src/interface/widgets/replay/replay_manager.cpp
index a2c1e0ff0..c51b96b6c 100644
--- a/cockatrice/src/interface/widgets/replay/replay_manager.cpp
+++ b/cockatrice/src/interface/widgets/replay/replay_manager.cpp
@@ -142,8 +142,10 @@ void ReplayManager::processNewEvents(PlaybackMode playbackMode)
}
// backwards skip => always skip tap animation
+ // backwards skip => always skip damage animation (battlefield shimmer / life counter flash)
if (playbackMode == BACKWARD_SKIP) {
options |= SKIP_TAP_ANIMATION;
+ options |= SKIP_DAMAGE_ANIMATION;
}
emit eventReplayed(replay->event_list(currentEvent), options);
diff --git a/cockatrice/src/interface/widgets/replay/replay_widget.cpp b/cockatrice/src/interface/widgets/replay/replay_widget.cpp
index 6c85d950e..c92771f8c 100644
--- a/cockatrice/src/interface/widgets/replay/replay_widget.cpp
+++ b/cockatrice/src/interface/widgets/replay/replay_widget.cpp
@@ -2,6 +2,7 @@
#include "../../../client/settings/cache_settings.h"
#include "../../../client/settings/shortcuts_settings.h"
+#include "../../pixel_map_generator.h"
#include "../interface/widgets/tabs/tab_game.h"
#include "replay_manager.h"
#include "replay_quick_settings_widget.h"
@@ -50,15 +51,15 @@ ReplayWidget::ReplayWidget(QWidget *parent, GameReplay *replay)
replayPlayButton = new QToolButton;
replayPlayButton->setIconSize(QSize(32, 32));
QIcon playButtonIcon = QIcon();
- playButtonIcon.addPixmap(QPixmap("theme:replay/start"), QIcon::Normal, QIcon::Off);
- playButtonIcon.addPixmap(QPixmap("theme:replay/pause"), QIcon::Normal, QIcon::On);
+ playButtonIcon.addPixmap(themePixmap(QStringLiteral("replay/start")), QIcon::Normal, QIcon::Off);
+ playButtonIcon.addPixmap(themePixmap(QStringLiteral("replay/pause")), QIcon::Normal, QIcon::On);
replayPlayButton->setIcon(playButtonIcon);
replayPlayButton->setCheckable(true);
connect(replayPlayButton, &QToolButton::toggled, this, &ReplayWidget::replayPlayButtonToggled);
replayFastForwardButton = new QToolButton;
replayFastForwardButton->setIconSize(QSize(32, 32));
- replayFastForwardButton->setIcon(QPixmap("theme:replay/fastforward"));
+ replayFastForwardButton->setIcon(themePixmap(QStringLiteral("replay/fastforward")));
replayFastForwardButton->setCheckable(true);
connect(replayFastForwardButton, &QToolButton::toggled, this, &ReplayWidget::updateTimeScaleFactor);
diff --git a/cockatrice/src/interface/widgets/server/game_selector.cpp b/cockatrice/src/interface/widgets/server/game_selector.cpp
index f41002247..a8bf54e91 100644
--- a/cockatrice/src/interface/widgets/server/game_selector.cpp
+++ b/cockatrice/src/interface/widgets/server/game_selector.cpp
@@ -1,5 +1,6 @@
#include "game_selector.h"
+#include "../../pixel_map_generator.h"
#include "../interface/widgets/dialogs/dlg_create_game.h"
#include "../interface/widgets/dialogs/dlg_filter_games.h"
#include "../interface/widgets/tabs/tab_account.h"
@@ -95,10 +96,10 @@ GameSelector::GameSelector(AbstractClient *_client,
}
filterButton = new QPushButton;
- filterButton->setIcon(QPixmap("theme:icons/search"));
+ filterButton->setIcon(themePixmap(QStringLiteral("icons/search")));
connect(filterButton, &QPushButton::clicked, this, &GameSelector::actSetFilter);
clearFilterButton = new QPushButton;
- clearFilterButton->setIcon(QPixmap("theme:icons/clearsearch"));
+ clearFilterButton->setIcon(themePixmap(QStringLiteral("icons/clearsearch")));
bool filtersSetToDefault = showFilters && gameListProxyModel->areFilterParametersSetToDefaults();
clearFilterButton->setEnabled(!filtersSetToDefault);
connect(clearFilterButton, &QPushButton::clicked, this, &GameSelector::actClearFilter);
diff --git a/cockatrice/src/interface/widgets/server/remote/remote_replay_list_tree_widget.cpp b/cockatrice/src/interface/widgets/server/remote/remote_replay_list_tree_widget.cpp
index 1f034b767..d62bf81aa 100644
--- a/cockatrice/src/interface/widgets/server/remote/remote_replay_list_tree_widget.cpp
+++ b/cockatrice/src/interface/widgets/server/remote/remote_replay_list_tree_widget.cpp
@@ -1,5 +1,7 @@
#include "remote_replay_list_tree_widget.h"
+#include "../../../pixel_map_generator.h"
+
#include
#include
#include
@@ -37,7 +39,7 @@ RemoteReplayList_TreeModel::RemoteReplayList_TreeModel(AbstractClient *_client,
QFileIconProvider fip;
dirIcon = fip.icon(QFileIconProvider::Folder);
fileIcon = fip.icon(QFileIconProvider::File);
- lockIcon = QPixmap("theme:icons/lock");
+ lockIcon = themePixmap(QStringLiteral("icons/lock"));
}
RemoteReplayList_TreeModel::~RemoteReplayList_TreeModel()
diff --git a/cockatrice/src/interface/widgets/server/user/user_card_art_provider.cpp b/cockatrice/src/interface/widgets/server/user/user_card_art_provider.cpp
index 3a1876fa1..2ba745715 100644
--- a/cockatrice/src/interface/widgets/server/user/user_card_art_provider.cpp
+++ b/cockatrice/src/interface/widgets/server/user/user_card_art_provider.cpp
@@ -1,6 +1,7 @@
#include "user_card_art_provider.h"
#include "../../../card_picture_loader/card_picture_loader.h"
+#include "../../cards/card_art_utils.h"
#include
#include
@@ -52,16 +53,25 @@ void UserCardArtProvider::requestCardArt(const QString &userName, const QString
processQueue();
}
-QPixmap UserCardArtProvider::cropCardArt(const QPixmap &fullRes)
+QPixmap UserCardArtProvider::cropCardArt(const QPixmap &fullRes, const ExactCard &card)
{
- const QSize sz = fullRes.size();
+ QPixmap source = fullRes;
+
+ // Sideways-layout cards (plane, siege/battle, split) store their landscape
+ // artwork rotated 90° inside a portrait frame. Rotate it upright first so
+ // the crop below lands on the horizontal art, mirroring the way
+ // CardInfoPictureWidget displays these cards.
+ const bool landscape = card.getInfo().getUiAttributes().landscapeOrientation;
+ source = CardArtUtils::rotateSidewaysLayoutArt(source, card);
+
+ const QSize sz = source.size();
const int marginX = sz.width() * 0.07;
- const int topMargin = sz.height() * 0.11;
- const int bottomMargin = sz.height() * 0.45;
+ const int topMargin = landscape ? sz.height() * 0.05 : sz.height() * 0.11;
+ const int bottomMargin = landscape ? sz.height() * 0.42 : sz.height() * 0.45;
- const QRect foilRect(marginX, topMargin, sz.width() - 2 * marginX, sz.height() - topMargin - bottomMargin);
+ const QRect artRect(marginX, topMargin, sz.width() - 2 * marginX, sz.height() - topMargin - bottomMargin);
- return fullRes.copy(foilRect.intersected(fullRes.rect()));
+ return source.copy(artRect.intersected(source.rect()));
}
void UserCardArtProvider::insertIntoCache(const QString &key, const QPixmap &pixmap)
@@ -111,7 +121,7 @@ void UserCardArtProvider::processQueue()
// Synchronous hit (already loaded/on disk)
if (!fullRes.isNull()) {
- insertIntoCache(key, cropCardArt(fullRes));
+ insertIntoCache(key, cropCardArt(fullRes, card));
pending.remove(key);
emit cardArtUpdated(userName);
@@ -135,7 +145,7 @@ void UserCardArtProvider::processQueue()
CardPictureLoader::getPixmap(fullRes, card, QSize(745, 1040));
if (!fullRes.isNull()) {
- self->insertIntoCache(key, self->cropCardArt(fullRes));
+ self->insertIntoCache(key, self->cropCardArt(fullRes, card));
}
self->pending.remove(key);
diff --git a/cockatrice/src/interface/widgets/server/user/user_card_art_provider.h b/cockatrice/src/interface/widgets/server/user/user_card_art_provider.h
index 2592237c4..e8283a891 100644
--- a/cockatrice/src/interface/widgets/server/user/user_card_art_provider.h
+++ b/cockatrice/src/interface/widgets/server/user/user_card_art_provider.h
@@ -6,6 +6,7 @@
#include
#include
#include
+#include
class UserCardArtProvider : public QObject
{
@@ -16,7 +17,7 @@ public:
void requestCardArt(const QString &userName, const QString &cardName, const QString &providerId);
const QMap &cache() const;
- static QPixmap cropCardArt(const QPixmap &fullRes);
+ static QPixmap cropCardArt(const QPixmap &fullRes, const ExactCard &card);
signals:
void cardArtUpdated(const QString &userName);
diff --git a/cockatrice/src/interface/widgets/server/user/user_card_settings_dialog.cpp b/cockatrice/src/interface/widgets/server/user/user_card_settings_dialog.cpp
index 532112964..d49e3d540 100644
--- a/cockatrice/src/interface/widgets/server/user/user_card_settings_dialog.cpp
+++ b/cockatrice/src/interface/widgets/server/user/user_card_settings_dialog.cpp
@@ -560,7 +560,7 @@ void UserCardArtSettingsDialog::reloadPreview()
return;
}
- currentPixmap = UserCardArtProvider::cropCardArt(fullRes);
+ currentPixmap = UserCardArtProvider::cropCardArt(fullRes, card);
preview->setPixmap(currentPixmap);
preview->setParams(currentParams);
diff --git a/cockatrice/src/interface/widgets/server/user/user_context_menu.cpp b/cockatrice/src/interface/widgets/server/user/user_context_menu.cpp
index 8d5d423f6..0d2267a63 100644
--- a/cockatrice/src/interface/widgets/server/user/user_context_menu.cpp
+++ b/cockatrice/src/interface/widgets/server/user/user_context_menu.cpp
@@ -51,6 +51,8 @@ UserContextMenu::UserContextMenu(TabSupervisor *_tabSupervisor, QWidget *parent,
aDemoteFromMod = new QAction(QString(), this);
aPromoteToJudge = new QAction(QString(), this);
aDemoteFromJudge = new QAction(QString(), this);
+ aPromoteToDeveloper = new QAction(QString(), this);
+ aDemoteFromDeveloper = new QAction(QString(), this);
aGetAdminNotes = new QAction(QString(), this);
aInvestigateUser = new QAction(QString(), this);
@@ -76,6 +78,8 @@ void UserContextMenu::retranslateUi()
aDemoteFromMod->setText(tr("Dem&ote user from moderator"));
aPromoteToJudge->setText(tr("Promote user to &judge"));
aDemoteFromJudge->setText(tr("Demote user from judge"));
+ aPromoteToDeveloper->setText(tr("Promote user to &developer"));
+ aDemoteFromDeveloper->setText(tr("Demote user from de&veloper"));
aGetAdminNotes->setText(tr("View admin notes"));
aInvestigateUser->setText(tr("Investigate user"));
}
@@ -268,7 +272,7 @@ void UserContextMenu::adjustMod_processUserResponse(const Response &resp, const
const Command_AdjustMod &cmd = commandContainer.admin_command(0).GetExtension(Command_AdjustMod::ext);
if (resp.response_code() == Response::RespOk) {
- if (cmd.should_be_mod() || cmd.should_be_judge()) {
+ if (cmd.should_be_mod() || cmd.should_be_judge() || cmd.should_be_developer()) {
QMessageBox::information(static_cast(parent()), tr("Success"),
tr("Successfully promoted user."));
} else {
@@ -276,7 +280,7 @@ void UserContextMenu::adjustMod_processUserResponse(const Response &resp, const
}
} else {
- if (cmd.should_be_mod() || cmd.should_be_judge()) {
+ if (cmd.should_be_mod() || cmd.should_be_judge() || cmd.should_be_developer()) {
QMessageBox::information(static_cast(parent()), tr("Failed"), tr("Failed to promote user."));
} else {
QMessageBox::information(static_cast(parent()), tr("Failed"), tr("Failed to demote user."));
@@ -437,9 +441,18 @@ void UserContextMenu::showContextMenu(const QPoint &pos,
(tabSupervisor->getUserInfo()->user_level() & ServerInfo_User::IsAdmin)) {
menu->addAction(aPromoteToJudge);
}
+
+ if (userLevel.testFlag(ServerInfo_User::IsDeveloper) &&
+ (tabSupervisor->getUserInfo()->user_level() & ServerInfo_User::IsAdmin)) {
+ menu->addAction(aDemoteFromDeveloper);
+
+ } else if (userLevel.testFlag(ServerInfo_User::IsRegistered) &&
+ (tabSupervisor->getUserInfo()->user_level() & ServerInfo_User::IsAdmin)) {
+ menu->addAction(aPromoteToDeveloper);
+ }
}
aDetails->setEnabled(true);
- aChat->setEnabled(anotherUser && online);
+ aChat->setEnabled(anotherUser && online && !userListProxy->isUserIgnored(userName));
aShowGames->setEnabled(online);
aReport->setEnabled(anotherUser);
aAddToBuddyList->setEnabled(anotherUser);
@@ -455,6 +468,10 @@ void UserContextMenu::showContextMenu(const QPoint &pos,
aInvestigateUser->setEnabled(anotherUser);
aPromoteToMod->setEnabled(anotherUser);
aDemoteFromMod->setEnabled(anotherUser);
+ aPromoteToJudge->setEnabled(anotherUser);
+ aDemoteFromJudge->setEnabled(anotherUser);
+ aPromoteToDeveloper->setEnabled(anotherUser);
+ aDemoteFromDeveloper->setEnabled(anotherUser);
QAction *actionClicked = menu->exec(pos);
if (actionClicked == nullptr) {
@@ -489,6 +506,8 @@ void UserContextMenu::showContextMenu(const QPoint &pos,
execAdjustMod(userName, actionClicked == aPromoteToMod);
} else if (actionClicked == aPromoteToJudge || actionClicked == aDemoteFromJudge) {
execAdjustJudge(userName, actionClicked == aPromoteToJudge);
+ } else if (actionClicked == aPromoteToDeveloper || actionClicked == aDemoteFromDeveloper) {
+ execAdjustDeveloper(userName, actionClicked == aPromoteToDeveloper);
} else if (actionClicked == aBanHistory) {
execBanHistory(userName);
} else if (actionClicked == aWarnUser) {
@@ -606,7 +625,15 @@ void UserContextMenu::execAddToIgnore(const QString &userName)
Command_AddToList cmd;
cmd.set_list("ignore");
cmd.set_user_name(userName.toStdString());
- client->sendCommand(client->prepareSessionCommand(cmd));
+ PendingCommand *pend = client->prepareSessionCommand(cmd);
+ connect(pend, &PendingCommand::finished, this,
+ [this, userName](const Response &response, const CommandContainer &, const QVariant &) {
+ if (response.response_code() == Response::RespOk) {
+ QMessageBox::information(static_cast(parent()), tr("Ignore list"),
+ tr("%1 has been added to your ignore list.").arg(userName));
+ }
+ });
+ client->sendCommand(pend);
}
void UserContextMenu::execRemoveFromIgnore(const QString &userName)
@@ -698,4 +725,14 @@ void UserContextMenu::execAdjustJudge(const QString &userName, bool shouldBeJudg
PendingCommand *pend = client->prepareAdminCommand(cmd);
connect(pend, &PendingCommand::finished, this, &UserContextMenu::adjustMod_processUserResponse);
client->sendCommand(pend);
-}
\ No newline at end of file
+}
+
+void UserContextMenu::execAdjustDeveloper(const QString &userName, bool shouldBeDeveloper)
+{
+ Command_AdjustMod cmd;
+ cmd.set_user_name(userName.toStdString());
+ cmd.set_should_be_developer(shouldBeDeveloper);
+ PendingCommand *pend = client->prepareAdminCommand(cmd);
+ connect(pend, &PendingCommand::finished, this, &UserContextMenu::adjustMod_processUserResponse);
+ client->sendCommand(pend);
+}
diff --git a/cockatrice/src/interface/widgets/server/user/user_context_menu.h b/cockatrice/src/interface/widgets/server/user/user_context_menu.h
index f1ce931f8..6abbc057a 100644
--- a/cockatrice/src/interface/widgets/server/user/user_context_menu.h
+++ b/cockatrice/src/interface/widgets/server/user/user_context_menu.h
@@ -45,6 +45,7 @@ private:
QAction *aBan, *aBanHistory;
QAction *aPromoteToMod, *aDemoteFromMod;
QAction *aPromoteToJudge, *aDemoteFromJudge;
+ QAction *aPromoteToDeveloper, *aDemoteFromDeveloper;
QAction *aWarnUser, *aWarnHistory;
QAction *aGetAdminNotes;
std::function()> gameInviteLinkProvider;
@@ -123,6 +124,7 @@ public:
void execInvestigateUser(const QString &userName);
void execAdjustMod(const QString &userName, bool shouldBeMod);
void execAdjustJudge(const QString &userName, bool shouldBeJudge);
+ void execAdjustDeveloper(const QString &userName, bool shouldBeDeveloper);
private:
void execInvite(const QString &userName, const GameInviteOption &option);
diff --git a/cockatrice/src/interface/widgets/server/user/user_info_box.cpp b/cockatrice/src/interface/widgets/server/user/user_info_box.cpp
index 875bdfb05..3d89cecf5 100644
--- a/cockatrice/src/interface/widgets/server/user/user_info_box.cpp
+++ b/cockatrice/src/interface/widgets/server/user/user_info_box.cpp
@@ -122,6 +122,8 @@ void UserInfoBox::updateInfo(const ServerInfo_User &user)
QString userLevelText;
if (userLevel.testFlag(ServerInfo_User::IsAdmin)) {
userLevelText = tr("Administrator");
+ } else if (userLevel.testFlag(ServerInfo_User::IsDeveloper)) {
+ userLevelText = tr("Developer");
} else if (userLevel.testFlag(ServerInfo_User::IsModerator)) {
userLevelText = tr("Moderator");
} else if (userLevel.testFlag(ServerInfo_User::IsRegistered)) {
diff --git a/cockatrice/src/interface/widgets/server/user/user_info_popup.cpp b/cockatrice/src/interface/widgets/server/user/user_info_popup.cpp
index fb610e814..8be76eea0 100644
--- a/cockatrice/src/interface/widgets/server/user/user_info_popup.cpp
+++ b/cockatrice/src/interface/widgets/server/user/user_info_popup.cpp
@@ -245,6 +245,9 @@ void UserInfoHeaderWidget::paintEvent(QPaintEvent *)
if (level.testFlag(ServerInfo_User::IsAdmin)) {
return QColor(245, 158, 11);
}
+ if (level.testFlag(ServerInfo_User::IsDeveloper)) {
+ return QColor(185, 28, 28);
+ }
if (level.testFlag(ServerInfo_User::IsModerator)) {
return QColor(59, 130, 246);
}
@@ -300,6 +303,8 @@ void UserInfoHeaderWidget::paintEvent(QPaintEvent *)
} badge;
if (level.testFlag(ServerInfo_User::IsAdmin)) {
badge = {"ADMIN", QColor(245, 158, 11)};
+ } else if (level.testFlag(ServerInfo_User::IsDeveloper)) {
+ badge = {"DEV", QColor(185, 28, 28)};
} else if (level.testFlag(ServerInfo_User::IsModerator)) {
badge = {"MOD", QColor(59, 130, 246)};
} else if (level.testFlag(ServerInfo_User::IsJudge)) {
diff --git a/cockatrice/src/interface/widgets/server/user/user_list_painter.cpp b/cockatrice/src/interface/widgets/server/user/user_list_painter.cpp
index 5c65b090d..34a3d6ae1 100644
--- a/cockatrice/src/interface/widgets/server/user/user_list_painter.cpp
+++ b/cockatrice/src/interface/widgets/server/user/user_list_painter.cpp
@@ -49,6 +49,8 @@ QColor UserListPainter::getAccentColor(const UserLevelFlags &userLevel, bool onl
if (userLevel.testFlag(ServerInfo_User::IsAdmin)) {
accentColor = QColor(245, 158, 11);
+ } else if (userLevel.testFlag(ServerInfo_User::IsDeveloper)) {
+ accentColor = QColor(185, 28, 28);
} else if (userLevel.testFlag(ServerInfo_User::IsModerator)) {
accentColor = QColor(59, 130, 246);
} else if (userLevel.testFlag(ServerInfo_User::IsJudge)) {
@@ -299,6 +301,8 @@ QList UserListPainter::buildBadges(const UserLevelFlags
if (userLevel.testFlag(ServerInfo_User::IsAdmin)) {
badges << Badge{"ADMIN", QColor(245, 158, 11)};
+ } else if (userLevel.testFlag(ServerInfo_User::IsDeveloper)) {
+ badges << Badge{"DEV", QColor(185, 28, 28)};
} else if (userLevel.testFlag(ServerInfo_User::IsModerator)) {
badges << Badge{"MOD", QColor(59, 130, 246)};
} else if (userLevel.testFlag(ServerInfo_User::IsJudge)) {
@@ -385,9 +389,9 @@ void UserListPainter::paint(QPainter *painter,
const QString userName = QString::fromStdString(userInfo.name());
const QString privLevel = QString::fromStdString(userInfo.privlevel());
const QColor accentColor = getAccentColor(userLevel, online);
- const bool hasRole = userLevel.testFlag(ServerInfo_User::IsAdmin) ||
- userLevel.testFlag(ServerInfo_User::IsModerator) ||
- userLevel.testFlag(ServerInfo_User::IsJudge);
+ const bool hasRole =
+ userLevel.testFlag(ServerInfo_User::IsAdmin) || userLevel.testFlag(ServerInfo_User::IsDeveloper) ||
+ userLevel.testFlag(ServerInfo_User::IsModerator) || userLevel.testFlag(ServerInfo_User::IsJudge);
const QRectF cardRect = QRectF(rect).adjusted(3, 2, -3, -2);
const int cardRight = getCardRight(option, rect);
diff --git a/cockatrice/src/interface/widgets/server/user/user_list_widget.cpp b/cockatrice/src/interface/widgets/server/user/user_list_widget.cpp
index a8c99c979..1bb7c5288 100644
--- a/cockatrice/src/interface/widgets/server/user/user_list_widget.cpp
+++ b/cockatrice/src/interface/widgets/server/user/user_list_widget.cpp
@@ -234,9 +234,11 @@ bool UserListTWI::operator<(const QTreeWidgetItem &other) const
const auto &lhsUserLevelFlags = UserLevelFlags(data(0, Qt::UserRole).toInt());
const auto &rhsUserLevelFlags = UserLevelFlags(other.data(0, Qt::UserRole).toInt());
- // Admins & Mods need no additional comparison checks, just to see if they're an admin or a moderator
+ // Admins, Developers & Mods need no additional comparison checks, just to see if they're an admin, a developer
+ // or a moderator
static const QList userLevelWithNoOtherPrefOrder = {
- ServerInfo_User_UserLevelFlag_IsAdmin, ServerInfo_User_UserLevelFlag_IsModerator};
+ ServerInfo_User_UserLevelFlag_IsAdmin, ServerInfo_User_UserLevelFlag_IsDeveloper,
+ ServerInfo_User_UserLevelFlag_IsModerator};
for (const auto &userLevelEntry : userLevelWithNoOtherPrefOrder) {
if (lhsUserLevelFlags.testFlag(userLevelEntry) &&
lhsUserLevelFlags.testFlag(userLevelEntry) == rhsUserLevelFlags.testFlag(userLevelEntry)) {
diff --git a/cockatrice/src/interface/widgets/settings_page/appearance_settings_page.cpp b/cockatrice/src/interface/widgets/settings_page/appearance_settings_page.cpp
index c8494f095..f161f0f19 100644
--- a/cockatrice/src/interface/widgets/settings_page/appearance_settings_page.cpp
+++ b/cockatrice/src/interface/widgets/settings_page/appearance_settings_page.cpp
@@ -59,8 +59,8 @@ AppearanceSettingsPage::AppearanceSettingsPage()
connect(&schemeCombo, &QComboBox::currentIndexChanged, this,
[this] { themeManager->setColorScheme(schemeCombo.currentData().toString()); });
- // Qt widget style; "Default" lets the application decide
- styleCombo.addItem(tr("Default"), QStringLiteral("Default"));
+ // Qt widget style; "System" lets the application decide
+ styleCombo.addItem(tr("System"), QStringLiteral("System"));
for (const QString &key : QStyleFactory::keys()) {
styleCombo.addItem(key, key);
}
@@ -132,6 +132,10 @@ AppearanceSettingsPage::AppearanceSettingsPage()
connect(&homeTabDisplayCardNameCheckBox, &QCheckBox::QT_STATE_CHANGED, &settings.appearance(),
&AppearanceSettings::setHomeTabDisplayCardName);
+ homeTabBackgroundDimCheckBox.setChecked(settings.appearance().getHomeTabBackgroundDim());
+ connect(&homeTabBackgroundDimCheckBox, &QCheckBox::QT_STATE_CHANGED, &settings.appearance(),
+ &AppearanceSettings::setHomeTabBackgroundDim);
+
for (const auto &entry : HomeTabButtonColor::all()) {
homeTabButtonColorSourceBox.addItem(QObject::tr(entry.trKey));
}
@@ -150,6 +154,7 @@ AppearanceSettingsPage::AppearanceSettingsPage()
homeTabGrid->addWidget(&homeTabDisplayCardNameCheckBox, 2, 0, 1, 2);
homeTabGrid->addWidget(&homeTabButtonColorSourceLabel, 3, 0);
homeTabGrid->addWidget(&homeTabButtonColorSourceBox, 3, 1);
+ homeTabGrid->addWidget(&homeTabBackgroundDimCheckBox, 4, 0, 1, 2);
homeTabGroupBox = new QGroupBox;
homeTabGroupBox->setLayout(homeTabGrid);
@@ -508,9 +513,12 @@ void AppearanceSettingsPage::retranslateUi()
homeTabBackgroundShuffleFrequencyLabel.setText(tr("Home tab background shuffle frequency:"));
homeTabBackgroundShuffleFrequencySpinBox.setSpecialValueText(tr("Disabled"));
homeTabDisplayCardNameCheckBox.setText(tr("Display card name of background in bottom right"));
+ homeTabBackgroundDimCheckBox.setText(tr("Dim the home tab background"));
+ homeTabBackgroundDimCheckBox.setToolTip(
+ tr("Draw a translucent overlay over the home tab background so buttons and text stand out"));
homeTabButtonColorSourceLabel.setText(tr("Home tab button color:"));
homeTabButtonColorSourceBox.setToolTip(
- tr("Automatic: extract from background if present, otherwise use theme default"));
+ tr("Use the theme's identity accent colors, or extract colors from the background image"));
playmatGroupBox->setTitle(tr("Playmat settings"));
playmatVisibilityLabel.setText(tr("Playmat visibility:"));
diff --git a/cockatrice/src/interface/widgets/settings_page/appearance_settings_page.h b/cockatrice/src/interface/widgets/settings_page/appearance_settings_page.h
index 8db71ff8f..bec4cd72f 100644
--- a/cockatrice/src/interface/widgets/settings_page/appearance_settings_page.h
+++ b/cockatrice/src/interface/widgets/settings_page/appearance_settings_page.h
@@ -41,6 +41,7 @@ private:
QLabel homeTabBackgroundShuffleFrequencyLabel;
QSpinBox homeTabBackgroundShuffleFrequencySpinBox;
QCheckBox homeTabDisplayCardNameCheckBox;
+ QCheckBox homeTabBackgroundDimCheckBox;
QLabel homeTabButtonColorSourceLabel;
QComboBox homeTabButtonColorSourceBox;
diff --git a/cockatrice/src/interface/widgets/settings_page/deck_editor_settings_page.cpp b/cockatrice/src/interface/widgets/settings_page/deck_editor_settings_page.cpp
index f425afe60..f3eac05b8 100644
--- a/cockatrice/src/interface/widgets/settings_page/deck_editor_settings_page.cpp
+++ b/cockatrice/src/interface/widgets/settings_page/deck_editor_settings_page.cpp
@@ -1,6 +1,7 @@
#include "deck_editor_settings_page.h"
#include "../../../client/settings/cache_settings.h"
+#include "../../pixel_map_generator.h"
#include "update/card_spoiler/spoiler_background_updater.h"
#include
@@ -53,15 +54,15 @@ DeckEditorSettingsPage::DeckEditorSettingsPage()
urlList->addItems(SettingsCache::instance().downloads().getAllURLs());
aAdd = new QAction(this);
- aAdd->setIcon(QPixmap("theme:icons/increment"));
+ aAdd->setIcon(themePixmap(QStringLiteral("icons/increment")));
connect(aAdd, &QAction::triggered, this, &DeckEditorSettingsPage::actAddURL);
aEdit = new QAction(this);
- aEdit->setIcon(QPixmap("theme:icons/pencil"));
+ aEdit->setIcon(themePixmap(QStringLiteral("icons/pencil")));
connect(aEdit, &QAction::triggered, this, &DeckEditorSettingsPage::actEditURL);
aRemove = new QAction(this);
- aRemove->setIcon(QPixmap("theme:icons/decrement"));
+ aRemove->setIcon(themePixmap(QStringLiteral("icons/decrement")));
connect(aRemove, &QAction::triggered, this, &DeckEditorSettingsPage::actRemoveURL);
auto *urlToolBar = new QToolBar;
diff --git a/cockatrice/src/interface/widgets/settings_page/general_settings_page.cpp b/cockatrice/src/interface/widgets/settings_page/general_settings_page.cpp
index 62b06fb60..36436f8a3 100644
--- a/cockatrice/src/interface/widgets/settings_page/general_settings_page.cpp
+++ b/cockatrice/src/interface/widgets/settings_page/general_settings_page.cpp
@@ -1,15 +1,21 @@
#include "general_settings_page.h"
#include "../../../client/settings/cache_settings.h"
+#include "../interface/card_picture_loader/card_picture_loader.h"
#include "../main.h"
#include "../server/user/user_info_connection.h"
#include "update/client/release_channel.h"
#include
+#include
#include
#include