Merge branch 'master' into tooomm-qt5

This commit is contained in:
tooomm 2026-09-12 18:34:48 +02:00 • committed by GitHub
commit 6d7dadf657
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
149 changed files with 2962 additions and 423 deletions

View file

@ -152,7 +152,7 @@ jobs:
env: env:
CACHE: ${{ github.workspace }}/.cache/${{ matrix.distro }}${{ matrix.version }} # directory for caching docker image and ccache CACHE: ${{ github.workspace }}/.cache/${{ matrix.distro }}${{ matrix.version }} # directory for caching docker image and ccache
CCACHE_EVICTION_AGE: 7d 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' CMAKE_GENERATOR: 'Ninja'
NAME: ${{ matrix.distro }}${{ matrix.version }} NAME: ${{ matrix.distro }}${{ matrix.version }}
@ -342,7 +342,7 @@ jobs:
timeout-minutes: 100 timeout-minutes: 100
env: env:
CCACHE_DIR: ${{ github.workspace }}/.cache/ 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: steps:
- name: "Checkout" - name: "Checkout"

View file

@ -149,15 +149,15 @@ You can then
<br> <br>
The following flags (with their non-default values) can be passed to `cmake`: The following flags (with their non-default values) can be passed to `cmake`:
| Flag | Description | | Flag | Description |
| --- | --- | | -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| `-DWITH_SERVER=1` | Build <kbd>Servatrice</kbd> server | | `-DWITH_SERVER=1` | Build <kbd>Servatrice</kbd> server |
| `-DWITH_CLIENT=0` | Don't build <kbd>Cockatrice</kbd> client | | `-DWITH_CLIENT=0` | Don't build <kbd>Cockatrice</kbd> client |
| `-DWITH_ORACLE=0` | Don't build <kbd>Oracle</kbd> card database tool | | `-DWITH_ORACLE=0` | Don't build <kbd>Oracle</kbd> card database tool |
| `-DCMAKE_BUILD_TYPE=Debug` | Compile in debug mode<br> Enables extra logging output, debug symbols, and much more verbose compiler warnings | | `-DCMAKE_BUILD_TYPE=Debug` | Compile in debug mode<br> 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 | | `-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<br> **Note:** `make clean` will remove the .ts files | | `-DUPDATE_TRANSLATIONS=1` | Configure `make` to update the translation .ts files for new strings in the source code<br> **Note:** `make clean` will remove the .ts files |
| `-DTEST=1` | Enable regression tests<br> **Note:** `make test` to run tests, *googletest* will be downloaded if not available | | `-DTEST=1` | Enable regression tests<br> **Note:** `make test` to run tests, *googletest* will be downloaded if not available |
# Run # Run

View file

@ -37,7 +37,7 @@ set(QT_COMPONENTS_COCKATRICE
QuickWidgets QuickWidgets
) )
set(QT_COMPONENTS_ORACLE Concurrent Network Svg Widgets) set(QT_COMPONENTS_ORACLE Concurrent Network Svg Widgets Xml)
set(QT_COMPONENTS_SERVATRICE Network Sql WebSockets) set(QT_COMPONENTS_SERVATRICE Network Sql WebSockets)

View file

@ -166,6 +166,7 @@ set(cockatrice_SOURCES
src/interface/widgets/cards/additional_info/mana_cost_widget.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/additional_info/mana_symbol_widget.cpp
src/interface/widgets/cards/art_crop_attribution.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/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/flat_card_group_display_widget.cpp
src/interface/widgets/cards/card_group_display_widgets/overlapped_card_group_display_widget.cpp src/interface/widgets/cards/card_group_display_widgets/overlapped_card_group_display_widget.cpp
@ -381,6 +382,7 @@ set(cockatrice_SOURCES
src/interface/widgets/tabs/tab_card_art_rules.cpp src/interface/widgets/tabs/tab_card_art_rules.cpp
src/interface/widgets/tabs/tab_deck_editor.cpp src/interface/widgets/tabs/tab_deck_editor.cpp
src/interface/widgets/tabs/tab_deck_storage.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_game.cpp
src/interface/widgets/tabs/tab_home.cpp src/interface/widgets/tabs/tab_home.cpp
src/interface/widgets/tabs/tab_logs.cpp src/interface/widgets/tabs/tab_logs.cpp

View file

@ -1,5 +1,6 @@
#include "remote_connection_controller.h" #include "remote_connection_controller.h"
#include "../../../interface/pixel_map_generator.h"
#include "../../settings/cache_settings.h" #include "../../settings/cache_settings.h"
#include "../interface/widgets/dialogs/dlg_connect.h" #include "../interface/widgets/dialogs/dlg_connect.h"
#include "../interface/widgets/dialogs/dlg_forgot_password_challenge.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", "games will be lost.\nReason for shutdown: %1",
"", event.minutes()) "", event.minutes())
.arg(QString::fromStdString(event.reason()))); .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.setText(tr("Scheduled server shutdown"));
serverShutdownMessageBox.setWindowModality(Qt::ApplicationModal); serverShutdownMessageBox.setWindowModality(Qt::ApplicationModal);
serverShutdownMessageBox.setVisible(true); serverShutdownMessageBox.setVisible(true);

View file

@ -94,7 +94,7 @@ QStringMap &SoundEngine::getAvailableThemes()
QDir dir; QDir dir;
availableThemes.clear(); availableThemes.clear();
// load themes from user profile dir // Load themes from user profile dir
dir.setPath(SettingsCache::instance().getDataPath() + "/sounds"); 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() + dir.setPath(qApp->applicationDirPath() +
#ifdef Q_OS_MAC #ifdef Q_OS_MAC
"/../Resources/sounds" "/../Resources/sounds"

View file

@ -1,5 +1,6 @@
#include "filter_builder.h" #include "filter_builder.h"
#include "../interface/pixel_map_generator.h"
#include "../interface/widgets/utility/custom_line_edit.h" #include "../interface/widgets/utility/custom_line_edit.h"
#include <QComboBox> #include <QComboBox>
@ -21,7 +22,7 @@ FilterBuilder::FilterBuilder(QWidget *parent) : QWidget(parent)
typeCombo->addItem(CardFilter::typeName(static_cast<CardFilter::Type>(i)), QVariant(i)); typeCombo->addItem(CardFilter::typeName(static_cast<CardFilter::Type>(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->setObjectName("ok");
ok->setMaximumSize(20, 20); ok->setMaximumSize(20, 20);

View file

@ -13,12 +13,12 @@ CounterState *CounterState::fromProto(const ServerInfo_Counter &counter, QObject
convertColorToQColor(counter.counter_color()), counter.radius(), counter.count(), parent); convertColorToQColor(counter.counter_color()), counter.radius(), counter.count(), parent);
} }
void CounterState::setValue(int newValue) void CounterState::setValue(int newValue, bool skipDamageAnimation)
{ {
if (newValue == value) { if (newValue == value) {
return; return;
} }
int old = value; int old = value;
value = newValue; value = newValue;
emit valueChanged(old, newValue); emit valueChanged(old, newValue, skipDamageAnimation);
} }

View file

@ -35,10 +35,23 @@ public:
return value; 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: 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: private:
int id; int id;

View file

@ -430,12 +430,13 @@ void GameEventHandler::eventJoin(const Event_Join &event, int /*eventPlayerId*/,
QString playerName = QString::fromStdString(playerInfo.user_info().name()); QString playerName = QString::fromStdString(playerInfo.user_info().name());
emit addPlayerToAutoCompleteList(playerName); emit addPlayerToAutoCompleteList(playerName);
if (game->getPlayerManager()->getPlayers().contains(playerId)) { PlayerManager *playerManager = game->getPlayerManager();
if (playerManager->getPlayers().contains(playerId) || playerManager->getSpectators().contains(playerId)) {
return; return;
} }
if (playerInfo.spectator()) { if (playerInfo.spectator()) {
game->getPlayerManager()->addSpectator(playerId, playerInfo); playerManager->addSpectator(playerId, playerInfo);
emit logJoinSpectator(playerName); emit logJoinSpectator(playerName);
emit spectatorJoined(playerInfo); emit spectatorJoined(playerInfo);
} else { } else {

View file

@ -13,7 +13,8 @@
enum EventProcessingOption enum EventProcessingOption
{ {
SKIP_REVEAL_WINDOW = 0x0001, SKIP_REVEAL_WINDOW = 0x0001,
SKIP_TAP_ANIMATION = 0x0002 SKIP_TAP_ANIMATION = 0x0002,
SKIP_DAMAGE_ANIMATION = 0x0004
}; };
// Wrap it in a QFlags typedef // Wrap it in a QFlags typedef

View file

@ -262,14 +262,15 @@ void PlayerEventHandler::eventCreateCounter(const Event_CreateCounter &event)
player->addCounter(event.counter_info()); 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); CounterState *ctr = player->getCounters().value(event.counter_id(), nullptr);
if (!ctr) { if (!ctr) {
return; return;
} }
int oldValue = ctr->getValue(); 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); emit logSetCounter(player, ctr->getName(), event.value(), oldValue);
} }
@ -625,7 +626,7 @@ void PlayerEventHandler::processGameEvent(GameEvent::GameEventType type,
eventCreateCounter(event.GetExtension(Event_CreateCounter::ext)); eventCreateCounter(event.GetExtension(Event_CreateCounter::ext));
break; break;
case GameEvent::SET_COUNTER: case GameEvent::SET_COUNTER:
eventSetCounter(event.GetExtension(Event_SetCounter::ext)); eventSetCounter(event.GetExtension(Event_SetCounter::ext), options);
break; break;
case GameEvent::DEL_COUNTER: case GameEvent::DEL_COUNTER:
eventDelCounter(event.GetExtension(Event_DelCounter::ext)); eventDelCounter(event.GetExtension(Event_DelCounter::ext));

View file

@ -153,7 +153,7 @@ public:
void eventCreateCounter(const Event_CreateCounter &event); void eventCreateCounter(const Event_CreateCounter &event);
/// Set a player-level counter value. /// 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. /// Delete a player-level counter.
void eventDelCounter(const Event_DelCounter &event); void eventDelCounter(const Event_DelCounter &event);

View file

@ -175,7 +175,15 @@ void PlayerLogic::processPlayerInfo(const ServerInfo_Player &info)
const ServerInfo_Card &cardInfo = zoneInfo.card_list(j); const ServerInfo_Card &cardInfo = zoneInfo.card_list(j);
auto *card = new CardItem(this); auto *card = new CardItem(this);
card->processCardInfo(cardInfo); 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()) { if (zoneInfo.has_always_reveal_top_card()) {

View file

@ -75,6 +75,14 @@ PlayerLogic *PlayerManager::getPlayer(int playerId) const
return player; return player;
} }
void PlayerManager::clearSpectators()
{
const QList<int> spectatorIds = spectators.keys();
for (int spectatorId : spectatorIds) {
removeSpectator(spectatorId);
}
}
void PlayerManager::onPlayerConceded(int playerId, bool conceded) void PlayerManager::onPlayerConceded(int playerId, bool conceded)
{ {
// Everything else cares about this // Everything else cares about this

View file

@ -100,6 +100,9 @@ public:
emit spectatorRemoved(spectatorId, spectatorInfo); emit spectatorRemoved(spectatorId, spectatorInfo);
} }
/** @brief Remove all spectators, emitting the removal signal for each. */
void clearSpectators();
[[nodiscard]] AbstractGame *getGame() const [[nodiscard]] AbstractGame *getGame() const
{ {
return game; return game;

View file

@ -29,9 +29,9 @@ AbstractCounter::AbstractCounter(CounterState *state,
{ {
setAcceptHoverEvents(true); 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; value = newValue;
onValueChanged(oldValue, newValue); onValueChanged(oldValue, newValue, skipDamageAnimation);
update(); update();
}); });
@ -230,7 +230,7 @@ void AbstractCounterDialog::changeValue(int diff)
setTextValue(QString::number(curValue)); 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 // Default: no feedback. Subclasses such as PlayerCounter override this to
// flash the counter on meaningful changes (life gain/loss). // flash the counter on meaningful changes (life gain/loss).

View file

@ -38,8 +38,9 @@ protected:
* @brief Hook for subclasses that need per-value-change feedback (e.g. life-total flash). * @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. * 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 mousePressEvent(QGraphicsSceneMouseEvent *event) override;
void hoverEnterEvent(QGraphicsSceneHoverEvent *event) override; void hoverEnterEvent(QGraphicsSceneHoverEvent *event) override;

View file

@ -316,7 +316,7 @@ void CardItem::drawAttachArrow()
for (const auto &item : scene()->selectedItems()) { for (const auto &item : scene()->selectedItems()) {
CardItem *card = qgraphicsitem_cast<CardItem *>(item); CardItem *card = qgraphicsitem_cast<CardItem *>(item);
if (card == nullptr) { if (card == nullptr || card == this) {
continue; continue;
} }
if (card->getZone() != state->getZone()) { if (card->getZone() != state->getZone()) {

View file

@ -221,7 +221,12 @@ void GameScene::removePlayer(PlayerLogic *player)
clearArrowsForPlayer(player->getPlayerInfo()->getId()); 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<ZoneViewWidget *> zoneViewCopy = zoneViews;
for (ZoneViewWidget *zone : zoneViewCopy) {
if (zone->getPlayer() == player) { if (zone->getPlayer() == player) {
zone->close(); zone->close();
} }
@ -664,7 +669,10 @@ CardItem *GameScene::findTopmostCardInZone(const QList<QGraphicsItem *> &items,
*/ */
void GameScene::toggleZoneView(PlayerLogic *player, const QString &zoneName, int numberCards, bool isReversed) 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<ZoneViewWidget *> zoneViewCopy = zoneViews;
for (auto *view : zoneViewCopy) {
ZoneViewZone *temp = view->getZone(); ZoneViewZone *temp = view->getZone();
if (temp->getLogic()->getName() == zoneName && temp->getLogic()->getPlayer() == player && if (temp->getLogic()->getName() == zoneName && temp->getLogic()->getPlayer() == player &&
qobject_cast<ZoneViewZoneLogic *>(temp->getLogic())->getNumberCards() == numberCards) { qobject_cast<ZoneViewZoneLogic *>(temp->getLogic())->getNumberCards() == numberCards) {

View file

@ -1,5 +1,6 @@
#include "hand_counter.h" #include "hand_counter.h"
#include "../interface/pixel_map_generator.h"
#include "zones/card_zone.h" #include "zones/card_zone.h"
#include <QGraphicsSceneMouseEvent> #include <QGraphicsSceneMouseEvent>
@ -32,7 +33,8 @@ void HandCounter::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*op
QSize translatedSize = painter->combinedTransform().mapRect(boundingRect()).size().toSize(); QSize translatedSize = painter->combinedTransform().mapRect(boundingRect()).size().toSize();
QPixmap cachedPixmap; QPixmap cachedPixmap;
if (!QPixmapCache::find("handCounter" + QString::number(translatedSize.width()), &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); QPixmapCache::insert("handCounter" + QString::number(translatedSize.width()), cachedPixmap);
} }
resetPainterTransform(painter); resetPainterTransform(painter);

View file

@ -3,6 +3,7 @@
#include "../../game/player/player_actions.h" #include "../../game/player/player_actions.h"
#include "../../interface/card_picture_loader/card_picture_loader.h" #include "../../interface/card_picture_loader/card_picture_loader.h"
#include "../../interface/widgets/cards/art_crop_attribution.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/playmat/playmat_utils.h"
#include "../../interface/widgets/tabs/tab_game.h" #include "../../interface/widgets/tabs/tab_game.h"
#include "../board/abstract_card_item.h" #include "../board/abstract_card_item.h"
@ -251,8 +252,8 @@ void PlayerGraphicsItem::onCounterAdded(CounterState *state)
AbstractCounter *widget; AbstractCounter *widget;
if (state->getName() == "life") { if (state->getName() == "life") {
widget = playerTarget->addCounter(state); widget = playerTarget->addCounter(state);
connect(state, &CounterState::valueChanged, this, [this](int oldValue, int newValue) { connect(state, &CounterState::valueChanged, this, [this](int oldValue, int newValue, bool skipDamageAnimation) {
if (newValue < oldValue) { if (newValue < oldValue && !skipDamageAnimation) {
tableZoneGraphicsItem->triggerDamageShimmer(); tableZoneGraphicsItem->triggerDamageShimmer();
} }
}); });
@ -442,7 +443,7 @@ void PlayerGraphicsItem::updatePlaymat()
hasPlaymat = true; hasPlaymat = true;
emit playmatChanged(true); emit playmatChanged(true);
} }
playmatPixmap = fullRes; playmatPixmap = CardArtUtils::rotateSidewaysLayoutArt(fullRes, card);
update(); update();
} }

View file

@ -53,13 +53,13 @@ PlayerListWidget::PlayerListWidget(TabSupervisor *_tabSupervisor,
QWidget *parent) QWidget *parent)
: QTreeWidget(parent), tabSupervisor(_tabSupervisor), client(_client), game(_game), gameStarted(false) : QTreeWidget(parent), tabSupervisor(_tabSupervisor), client(_client), game(_game), gameStarted(false)
{ {
readyIcon = QPixmap("theme:icons/ready_start"); readyIcon = themePixmap(QStringLiteral("icons/ready_start"));
notReadyIcon = QPixmap("theme:icons/not_ready_start"); notReadyIcon = themePixmap(QStringLiteral("icons/not_ready_start"));
concededIcon = QPixmap("theme:icons/conceded"); concededIcon = themePixmap(QStringLiteral("icons/conceded"));
playerIcon = loadColorAdjustedPixmap("theme:icons/player"); playerIcon = loadColorAdjustedPixmap("theme:icons/player");
judgeIcon = loadColorAdjustedPixmap("theme:icons/scales"); judgeIcon = loadColorAdjustedPixmap("theme:icons/scales");
spectatorIcon = loadColorAdjustedPixmap("theme:icons/spectator"); spectatorIcon = loadColorAdjustedPixmap("theme:icons/spectator");
lockIcon = QPixmap("theme:icons/lock"); lockIcon = themePixmap(QStringLiteral("icons/lock"));
if (tabSupervisor) { if (tabSupervisor) {
itemDelegate = new PlayerListItemDelegate(this); itemDelegate = new PlayerListItemDelegate(this);
@ -92,6 +92,11 @@ void PlayerListWidget::retranslateUi()
void PlayerListWidget::addPlayer(const ServerInfo_PlayerProperties &player) void PlayerListWidget::addPlayer(const ServerInfo_PlayerProperties &player)
{ {
if (players.contains(player.player_id())) {
updatePlayerProperties(player);
return;
}
QTreeWidgetItem *newPlayer = new PlayerListTWI; QTreeWidgetItem *newPlayer = new PlayerListTWI;
players.insert(player.player_id(), newPlayer); players.insert(player.player_id(), newPlayer);
updatePlayerProperties(player); updatePlayerProperties(player);
@ -176,6 +181,17 @@ void PlayerListWidget::removePlayer(int playerId)
delete takeTopLevelItem(indexOfTopLevelItem(player)); delete takeTopLevelItem(indexOfTopLevelItem(player));
} }
void PlayerListWidget::clearSpectators()
{
const QList<int> 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) void PlayerListWidget::setActivePlayer(int playerId)
{ {
QMapIterator<int, QTreeWidgetItem *> i(players); QMapIterator<int, QTreeWidgetItem *> i(players);

View file

@ -66,6 +66,7 @@ public slots:
void addPlayer(const ServerInfo_PlayerProperties &player); void addPlayer(const ServerInfo_PlayerProperties &player);
void removePlayer(int playerId); void removePlayer(int playerId);
void updatePlayerProperties(const ServerInfo_PlayerProperties &prop, int playerId = -1); void updatePlayerProperties(const ServerInfo_PlayerProperties &prop, int playerId = -1);
void clearSpectators();
}; };
#endif #endif

View file

@ -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; flashDelta = newValue - oldValue;
if (flashDelta == 0) { if (flashDelta == 0) {
@ -81,6 +81,11 @@ void PlayerCounter::onValueChanged(int oldValue, int newValue)
return; return;
} }
if (skipDamageAnimation) {
flashAlpha = 0.0;
return;
}
flashAlpha = 1.0; flashAlpha = 1.0;
flashClock.start(); flashClock.start();
if (scene()) { if (scene()) {
@ -132,8 +137,18 @@ void PlayerTarget::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*o
QRectF translatedRect = painter->combinedTransform().mapRect(avatarBoundingRect); QRectF translatedRect = painter->combinedTransform().mapRect(avatarBoundingRect);
QSize translatedSize = translatedRect.size().toSize(); QSize translatedSize = translatedRect.size().toSize();
QPixmap cachedPixmap; 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()) + "_" + 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)) { if (!QPixmapCache::find(cacheKey, &cachedPixmap)) {
cachedPixmap = QPixmap(translatedSize.width(), translatedSize.height()); cachedPixmap = QPixmap(translatedSize.width(), translatedSize.height());

View file

@ -21,7 +21,7 @@ class PlayerCounter : public AbstractCounter, public IAnimatedItem
{ {
Q_OBJECT Q_OBJECT
protected: protected:
void onValueChanged(int oldValue, int newValue) override; void onValueChanged(int oldValue, int newValue, bool skipDamageAnimation) override;
private: private:
static constexpr qreal flashDurationMs = 450.0; static constexpr qreal flashDurationMs = 450.0;

View file

@ -62,7 +62,7 @@ ZoneViewWidget::ZoneViewWidget(PlayerLogic *_player,
searchEdit.setPlaceholderText(tr("Search by card name (or search expressions)")); searchEdit.setPlaceholderText(tr("Search by card name (or search expressions)"));
searchEdit.setClearButtonEnabled(true); searchEdit.setClearButtonEnabled(true);
searchEdit.addAction(loadColorAdjustedPixmap("theme:icons/search"), QLineEdit::LeadingPosition); 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); }); connect(help, &QAction::triggered, this, [this] { createSearchSyntaxHelpWindow(&searchEdit); });
@ -549,7 +549,7 @@ void ZoneViewWidget::initStyleOption(QStyleOption *option) const
{ {
QStyleOptionTitleBar *titleBar = qstyleoption_cast<QStyleOptionTitleBar *>(option); QStyleOptionTitleBar *titleBar = qstyleoption_cast<QStyleOptionTitleBar *>(option);
if (titleBar) { if (titleBar) {
titleBar->icon = QPixmap("theme:cockatrice"); titleBar->icon = themePixmap(QStringLiteral("cockatrice"));
} }
} }

View file

@ -1,6 +1,7 @@
#include "card_picture_loader.h" #include "card_picture_loader.h"
#include "../../client/settings/cache_settings.h" #include "../../client/settings/cache_settings.h"
#include "../pixel_map_generator.h"
#include "card_picture_loader_cache_method.h" #include "card_picture_loader_cache_method.h"
#include "card_picture_loader_local_schemes.h" #include "card_picture_loader_local_schemes.h"
@ -62,7 +63,7 @@ void CardPictureLoader::getCardBackPixmap(QPixmap &pixmap, QSize size)
QString backCacheKey = "_trice_card_back_" + QString::number(size.width()) + "x" + QString::number(size.height()); QString backCacheKey = "_trice_card_back_" + QString::number(size.width()) + "x" + QString::number(size.height());
if (!QPixmapCache::find(backCacheKey, &pixmap)) { if (!QPixmapCache::find(backCacheKey, &pixmap)) {
qCDebug(CardPictureLoaderLog) << "PictureLoader: cache miss for" << backCacheKey; qCDebug(CardPictureLoaderLog) << "PictureLoader: cache miss for" << backCacheKey;
QPixmap tmpPixmap("theme:cardback"); QPixmap tmpPixmap = themePixmap(QStringLiteral("cardback"));
if (tmpPixmap.isNull()) { if (tmpPixmap.isNull()) {
qCWarning(CardPictureLoaderLog) << "Failed to load 'theme:cardback'! Using fallback pixmap."; qCWarning(CardPictureLoaderLog) << "Failed to load 'theme:cardback'! Using fallback pixmap.";
@ -83,7 +84,7 @@ void CardPictureLoader::getCardBackLoadingInProgressPixmap(QPixmap &pixmap, QSiz
"_trice_card_back_inprogress_" + QString::number(size.width()) + "x" + QString::number(size.height()); "_trice_card_back_inprogress_" + QString::number(size.width()) + "x" + QString::number(size.height());
if (!QPixmapCache::find(backCacheKey, &pixmap)) { if (!QPixmapCache::find(backCacheKey, &pixmap)) {
qCDebug(CardPictureLoaderCardBackCacheFailLog) << "PictureLoader: cache miss for" << backCacheKey; qCDebug(CardPictureLoaderCardBackCacheFailLog) << "PictureLoader: cache miss for" << backCacheKey;
QPixmap tmpPixmap("theme:cardback"); QPixmap tmpPixmap = themePixmap(QStringLiteral("cardback"));
if (tmpPixmap.isNull()) { if (tmpPixmap.isNull()) {
qCWarning(CardPictureLoaderLog) << "Failed to load 'theme:cardback' for in-progress state! Using fallback."; qCWarning(CardPictureLoaderLog) << "Failed to load 'theme:cardback' for in-progress state! Using fallback.";
@ -105,7 +106,7 @@ void CardPictureLoader::getCardBackLoadingFailedPixmap(QPixmap &pixmap, QSize si
"_trice_card_back_failed_" + QString::number(size.width()) + "x" + QString::number(size.height()); "_trice_card_back_failed_" + QString::number(size.width()) + "x" + QString::number(size.height());
if (!QPixmapCache::find(backCacheKey, &pixmap)) { if (!QPixmapCache::find(backCacheKey, &pixmap)) {
qCDebug(CardPictureLoaderCardBackCacheFailLog) << "PictureLoader: cache miss for" << backCacheKey; qCDebug(CardPictureLoaderCardBackCacheFailLog) << "PictureLoader: cache miss for" << backCacheKey;
QPixmap tmpPixmap("theme:cardback"); QPixmap tmpPixmap = themePixmap(QStringLiteral("cardback"));
if (tmpPixmap.isNull()) { if (tmpPixmap.isNull()) {
qCWarning(CardPictureLoaderLog) << "Failed to load 'theme:cardback' for failed state! Using fallback."; qCWarning(CardPictureLoaderLog) << "Failed to load 'theme:cardback' for failed state! Using fallback.";
@ -138,7 +139,8 @@ void CardPictureLoader::getPixmap(QPixmap &pixmap, const ExactCard &card, QSize
QPixmap bigPixmap; QPixmap bigPixmap;
if (QPixmapCache::find(key, &bigPixmap)) { if (QPixmapCache::find(key, &bigPixmap)) {
if (bigPixmap.isNull()) { 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); QDateTime failedAtTime = getInstance().failedAt.value(key);
if (!failedAtTime.isValid() || if (!failedAtTime.isValid() ||
failedAtTime.addSecs(RETRY_FAILED_CARDS_SECS) < QDateTime::currentDateTime()) { failedAtTime.addSecs(RETRY_FAILED_CARDS_SECS) < QDateTime::currentDateTime()) {

View file

@ -17,7 +17,7 @@ enum Format
PlainText, 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. * Stored as .cod files.
*/ */
Cockatrice Cockatrice

View file

@ -50,7 +50,7 @@ DeckLoader::loadFromFile(const QString &fileName, DeckFileFormat::Format fmt, bo
result = deckList.loadFromFile_Native(&file); result = deckList.loadFromFile_Native(&file);
if (!result) { if (!result) {
qCInfo(DeckLoaderLog) << "Failed to load " << fileName qCInfo(DeckLoaderLog) << "Failed to load " << fileName
<< "as cockatrice format; retrying as plain format"; << "as Cockatrice format; retrying as plain format";
file.seek(0); file.seek(0);
result = deckList.loadFromFile_Plain(&file, CardNameNormalizer()); result = deckList.loadFromFile_Plain(&file, CardNameNormalizer());
fmt = DeckFileFormat::PlainText; fmt = DeckFileFormat::PlainText;

View file

@ -131,7 +131,7 @@ public:
static void printDeckList(QPrinter *printer, const DeckList &deckList); 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. * 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. * @param deck The deck to convert. Should have valid lastLoadInfo. Will update the lastLoadInfo.
* @return Whether the conversion succeeded. * @return Whether the conversion succeeded.

View file

@ -1,5 +1,7 @@
#include "pixel_map_generator.h" #include "pixel_map_generator.h"
#include "theme_manager.h"
#include <QApplication> #include <QApplication>
#include <QDomDocument> #include <QDomDocument>
#include <QFile> #include <QFile>
@ -14,6 +16,7 @@
#define DEFAULT_COLOR_MODERATOR_LEFT "#ffffff"; #define DEFAULT_COLOR_MODERATOR_LEFT "#ffffff";
#define DEFAULT_COLOR_MODERATOR_RIGHT "#000000"; #define DEFAULT_COLOR_MODERATOR_RIGHT "#000000";
#define DEFAULT_COLOR_ADMIN "#ff2701"; #define DEFAULT_COLOR_ADMIN "#ff2701";
#define DEFAULT_COLOR_DEVELOPER "#B8B8B8"
/** /**
* Clamps an svg render size so that rendering does not exceed a multiple of the requested size. * Clamps an svg render size so that rendering does not exceed a multiple of the requested size.
@ -82,7 +85,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. * 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. * 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 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). * @param expandOnly If true, then keep the size of the initial pixmap to at least the size (Only relevant if SVG).
* *
@ -90,6 +99,19 @@ static QPixmap loadSvg(const QString &svgPath, const QSize &size, bool expandOnl
*/ */
static QPixmap tryLoadImage(const QString &path, const QSize &size, bool expandOnly = false) 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"}; const auto formats = {"png", "jpg"};
QPixmap returnPixmap; QPixmap returnPixmap;
@ -111,7 +133,8 @@ QPixmap PhasePixmapGenerator::generatePixmap(int height, QString name)
return pmCache.value(key); 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); pmCache.insert(key, pixmap);
return pixmap; return pixmap;
@ -359,6 +382,8 @@ QIcon UserLevelPixmapGenerator::generateIconDefault(int height,
if (userLevel.testFlag(ServerInfo_User::IsAdmin)) { if (userLevel.testFlag(ServerInfo_User::IsAdmin)) {
colorLeft = DEFAULT_COLOR_ADMIN; colorLeft = DEFAULT_COLOR_ADMIN;
} else if (userLevel.testFlag(ServerInfo_User::IsDeveloper)) {
colorLeft = DEFAULT_COLOR_DEVELOPER;
} else if (userLevel.testFlag(ServerInfo_User::IsModerator)) { } else if (userLevel.testFlag(ServerInfo_User::IsModerator)) {
colorLeft = DEFAULT_COLOR_MODERATOR_LEFT; colorLeft = DEFAULT_COLOR_MODERATOR_LEFT;
colorRight = DEFAULT_COLOR_MODERATOR_RIGHT; colorRight = DEFAULT_COLOR_MODERATOR_RIGHT;
@ -396,7 +421,8 @@ QPixmap LockPixmapGenerator::generatePixmap(int height)
return pmCache.value(key); 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); pmCache.insert(key, pixmap);
return pixmap; return pixmap;
} }
@ -411,7 +437,8 @@ QPixmap DropdownIconPixmapGenerator::generatePixmap(int height, bool expanded)
} }
QString name = expanded ? "dropdown_expanded" : "dropdown_collapsed"; 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); pmCache.insert(key, pixmap);
return pixmap; return pixmap;
@ -472,6 +499,13 @@ QHash<QString, QPixmap> ManaSymbolPixmapGenerator::scaledCache;
QPixmap loadColorAdjustedPixmap(const QString &name) 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) { if (qApp->palette().windowText().color().lightness() > 200) {
QImage img(name); QImage img(name);
img.invertPixels(); img.invertPixels();
@ -482,3 +516,21 @@ QPixmap loadColorAdjustedPixmap(const QString &name)
return QPixmap(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();
}

View file

@ -156,4 +156,15 @@ public:
QPixmap loadColorAdjustedPixmap(const QString &name); 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 #endif

View file

@ -1,10 +1,12 @@
#include "theme_manager.h" #include "theme_manager.h"
#include "../../client/settings/cache_settings.h" #include "../../client/settings/cache_settings.h"
#include "pixel_map_generator.h"
#include <QApplication> #include <QApplication>
#include <QColor> #include <QColor>
#include <QDebug> #include <QDebug>
#include <QFileInfo>
#include <QLibraryInfo> #include <QLibraryInfo>
#include <QMap> #include <QMap>
#include <QMetaEnum> #include <QMetaEnum>
@ -140,6 +142,48 @@ bool ThemeManager::isDarkMode(const QString &themeDirPath) const
} }
} }
QString ThemeManager::schemeVariantPath(QStringView prefix) const
{
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;
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;
}
bool ThemeManager::isBuiltInTheme() bool ThemeManager::isBuiltInTheme()
{ {
const auto themeName = SettingsCache::instance().getThemeName(); const auto themeName = SettingsCache::instance().getThemeName();
@ -180,7 +224,7 @@ QStringMap &ThemeManager::getAvailableThemes()
} }
} }
// load themes from cockatrice system dir // Load themes from Cockatrice system dir
dir.setPath(systemThemesBasePath()); dir.setPath(systemThemesBasePath());
for (QString themeName : dir.entryList(QDir::AllDirs | QDir::NoDotAndDotDot, QDir::Name)) { for (QString themeName : dir.entryList(QDir::AllDirs | QDir::NoDotAndDotDot, QDir::Name)) {
@ -195,7 +239,7 @@ QStringMap &ThemeManager::getAvailableThemes()
QBrush ThemeManager::loadBrush(QString fileName, QColor fallbackColor) QBrush ThemeManager::loadBrush(QString fileName, QColor fallbackColor)
{ {
QBrush brush; QBrush brush;
QPixmap tmp = QPixmap("theme:zones/" + fileName); QPixmap tmp = QPixmap("theme:" + assetPath(QStringLiteral("zones/") + fileName));
if (tmp.isNull()) { if (tmp.isNull()) {
brush.setColor(fallbackColor); brush.setColor(fallbackColor);
brush.setStyle(Qt::SolidPattern); brush.setStyle(Qt::SolidPattern);
@ -209,7 +253,7 @@ QBrush ThemeManager::loadBrush(QString fileName, QColor fallbackColor)
QBrush ThemeManager::loadExtraBrush(QString fileName, QBrush &fallbackBrush) QBrush ThemeManager::loadExtraBrush(QString fileName, QBrush &fallbackBrush)
{ {
QBrush brush; QBrush brush;
QPixmap tmp = QPixmap("theme:zones/" + fileName); QPixmap tmp = QPixmap("theme:" + assetPath(QStringLiteral("zones/") + fileName));
if (tmp.isNull()) { if (tmp.isNull()) {
brush = fallbackBrush; brush = fallbackBrush;
@ -393,9 +437,19 @@ void ThemeManager::themeChangedSlot()
currentThemePath = dirPath; currentThemePath = dirPath;
QDir dir(dirPath); QDir dir(dirPath);
// CSS // CSS — prefer the scheme-qualified stylesheet (style-dark.css /
if (!dirPath.isEmpty() && dir.exists(STYLE_CSS_NAME)) { // style-light.css) when present, else the plain style.css as fallback.
qApp->setStyleSheet("file:///" + dir.absoluteFilePath(STYLE_CSS_NAME)); 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 { } else {
qApp->setStyleSheet(""); qApp->setStyleSheet("");
} }
@ -446,6 +500,7 @@ void ThemeManager::themeChangedSlot()
} }
QPixmapCache::clear(); QPixmapCache::clear();
clearPixmapGeneratorCaches();
emit themeChanged(); emit themeChanged();
} }

View file

@ -87,6 +87,20 @@ public:
// Load/save per-scheme palette colors // Load/save per-scheme palette colors
static PaletteConfig loadPaletteConfig(const QString &themeDirPath, const QString &colorScheme); static PaletteConfig loadPaletteConfig(const QString &themeDirPath, const QString &colorScheme);
static bool savePaletteConfig(const QString &themeDirPath, const QString &colorScheme, const PaletteConfig &cfg); 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 "<prefix>-<dark|light>"
// when a file exists at that stem, otherwise the plain "<prefix>" 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
// ("<prefix>-<dark|light>.<ext>") and returns an empty string when no
// variant exists — it never falls back to the plain "<prefix>" 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 // Load the theme's shipped default palette, falling back to the system
// theme directory when it is absent from the resolved (user) directory. // theme directory when it is absent from the resolved (user) directory.
static PaletteConfig static PaletteConfig

View file

@ -0,0 +1,18 @@
#include "card_art_utils.h"
#include <QTransform>
#include <libcockatrice/card/printing/exact_card.h>
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

View file

@ -0,0 +1,25 @@
#ifndef CARD_ART_UTILS_H
#define CARD_ART_UTILS_H
#include <QPixmap>
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

View file

@ -5,6 +5,7 @@
#include "../../../interface/card_picture_loader/card_picture_loader.h" #include "../../../interface/card_picture_loader/card_picture_loader.h"
#include "../../../interface/widgets/tabs/tab_supervisor.h" #include "../../../interface/widgets/tabs/tab_supervisor.h"
#include "../../window_main.h" #include "../../window_main.h"
#include "card_art_utils.h"
#include <QMenu> #include <QMenu>
#include <QMouseEvent> #include <QMouseEvent>
@ -193,12 +194,7 @@ void CardInfoPictureWidget::paintEvent(QPaintEvent *event)
QPixmap transformedPixmap = resizedPixmap; // Default pixmap QPixmap transformedPixmap = resizedPixmap; // Default pixmap
if (SettingsCache::instance().cardsDisplay().getAutoRotateSidewaysLayoutCards()) { if (SettingsCache::instance().cardsDisplay().getAutoRotateSidewaysLayoutCards()) {
if (exactCard.getInfo().getUiAttributes().landscapeOrientation) { transformedPixmap = CardArtUtils::rotateSidewaysLayoutArt(resizedPixmap, exactCard);
// Rotate pixmap 90 degrees to the left
QTransform transform;
transform.rotate(90);
transformedPixmap = resizedPixmap.transformed(transform, Qt::SmoothTransformation);
}
} }
// Handle DPI scaling // Handle DPI scaling

View file

@ -1,5 +1,6 @@
#include "abstract_analytics_panel_widget.h" #include "abstract_analytics_panel_widget.h"
#include "../../pixel_map_generator.h"
#include "deck_list_statistics_analyzer.h" #include "deck_list_statistics_analyzer.h"
#include <QPushButton> #include <QPushButton>
@ -20,7 +21,7 @@ AbstractAnalyticsPanelWidget::AbstractAnalyticsPanelWidget(QWidget *parent, Deck
// config button // config button
configureButton = new QPushButton(this); configureButton = new QPushButton(this);
configureButton->setIcon(QPixmap("theme:icons/cogwheel")); configureButton->setIcon(themePixmap(QStringLiteral("icons/cogwheel")));
configureButton->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); configureButton->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum);
connect(configureButton, &QPushButton::clicked, this, &AbstractAnalyticsPanelWidget::applyConfigFromDialog); connect(configureButton, &QPushButton::clicked, this, &AbstractAnalyticsPanelWidget::applyConfigFromDialog);
bannerAndSettingsLayout->addWidget(configureButton, 0); bannerAndSettingsLayout->addWidget(configureButton, 0);

View file

@ -28,7 +28,7 @@ DeckEditorDatabaseDisplayWidget::DeckEditorDatabaseDisplayWidget(QWidget *parent
searchEdit->setPlaceholderText(tr("Search by card name (or search expressions)")); searchEdit->setPlaceholderText(tr("Search by card name (or search expressions)"));
searchEdit->setClearButtonEnabled(true); searchEdit->setClearButtonEnabled(true);
searchEdit->addAction(loadColorAdjustedPixmap("theme:icons/search"), QLineEdit::LeadingPosition); 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); setFocusProxy(searchEdit);
setFocusPolicy(Qt::ClickFocus); setFocusPolicy(Qt::ClickFocus);
@ -59,13 +59,13 @@ DeckEditorDatabaseDisplayWidget::DeckEditorDatabaseDisplayWidget(QWidget *parent
&DeckEditorDatabaseDisplayWidget::onRelatedCardClicked); &DeckEditorDatabaseDisplayWidget::onRelatedCardClicked);
aAddCard = new QAction(QString(), this); 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); connect(aAddCard, &QAction::triggered, this, &DeckEditorDatabaseDisplayWidget::actAddCardToMainDeck);
auto *tbAddCard = new QToolButton(this); auto *tbAddCard = new QToolButton(this);
tbAddCard->setDefaultAction(aAddCard); tbAddCard->setDefaultAction(aAddCard);
aAddCardToSideboard = new QAction(QString(), this); 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); connect(aAddCardToSideboard, &QAction::triggered, this, &DeckEditorDatabaseDisplayWidget::actAddCardToSideboard);
auto *tbAddCardToSideboard = new QToolButton(this); auto *tbAddCardToSideboard = new QToolButton(this);
tbAddCardToSideboard->setDefaultAction(aAddCardToSideboard); tbAddCardToSideboard->setDefaultAction(aAddCardToSideboard);

View file

@ -2,6 +2,7 @@
#include "../../../client/settings/cache_settings.h" #include "../../../client/settings/cache_settings.h"
#include "../../../client/settings/shortcuts_settings.h" #include "../../../client/settings/shortcuts_settings.h"
#include "../../pixel_map_generator.h"
#include "../playmat/playmat_settings_dialog.h" #include "../playmat/playmat_settings_dialog.h"
#include "../settings_page/user_interface_settings_page.h" #include "../settings_page/user_interface_settings_page.h"
#include "../tabs/api/commander_spellbook/commander_bracket_widget.h" #include "../tabs/api/commander_spellbook/commander_bracket_widget.h"
@ -192,25 +193,25 @@ void DeckEditorDeckDockWidget::createDeckDock()
&DeckEditorDeckDockWidget::applyActiveGroupCriteria); &DeckEditorDeckDockWidget::applyActiveGroupCriteria);
aIncrement = new QAction(QString(), this); aIncrement = new QAction(QString(), this);
aIncrement->setIcon(QPixmap("theme:icons/increment")); aIncrement->setIcon(themePixmap(QStringLiteral("icons/increment")));
connect(aIncrement, &QAction::triggered, this, &DeckEditorDeckDockWidget::actIncrementSelection); connect(aIncrement, &QAction::triggered, this, &DeckEditorDeckDockWidget::actIncrementSelection);
auto *tbIncrement = new QToolButton(this); auto *tbIncrement = new QToolButton(this);
tbIncrement->setDefaultAction(aIncrement); tbIncrement->setDefaultAction(aIncrement);
aDecrement = new QAction(QString(), this); aDecrement = new QAction(QString(), this);
aDecrement->setIcon(QPixmap("theme:icons/decrement")); aDecrement->setIcon(themePixmap(QStringLiteral("icons/decrement")));
connect(aDecrement, &QAction::triggered, this, &DeckEditorDeckDockWidget::actDecrementSelection); connect(aDecrement, &QAction::triggered, this, &DeckEditorDeckDockWidget::actDecrementSelection);
auto *tbDecrement = new QToolButton(this); auto *tbDecrement = new QToolButton(this);
tbDecrement->setDefaultAction(aDecrement); tbDecrement->setDefaultAction(aDecrement);
aRemoveCard = new QAction(QString(), this); 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); connect(aRemoveCard, &QAction::triggered, this, &DeckEditorDeckDockWidget::actRemoveCard);
auto *tbRemoveCard = new QToolButton(this); auto *tbRemoveCard = new QToolButton(this);
tbRemoveCard->setDefaultAction(aRemoveCard); tbRemoveCard->setDefaultAction(aRemoveCard);
aSwapCard = new QAction(QString(), this); aSwapCard = new QAction(QString(), this);
aSwapCard->setIcon(QPixmap("theme:icons/swap")); aSwapCard->setIcon(themePixmap(QStringLiteral("icons/swap")));
connect(aSwapCard, &QAction::triggered, this, &DeckEditorDeckDockWidget::actSwapSelection); connect(aSwapCard, &QAction::triggered, this, &DeckEditorDeckDockWidget::actSwapSelection);
auto *tbSwapCard = new QToolButton(this); auto *tbSwapCard = new QToolButton(this);
tbSwapCard->setDefaultAction(aSwapCard); tbSwapCard->setDefaultAction(aSwapCard);

View file

@ -4,6 +4,7 @@
#include "../../../client/settings/shortcuts_settings.h" #include "../../../client/settings/shortcuts_settings.h"
#include "../../../filters/filter_builder.h" #include "../../../filters/filter_builder.h"
#include "../../../filters/filter_tree_model.h" #include "../../../filters/filter_tree_model.h"
#include "../../pixel_map_generator.h"
#include <QGridLayout> #include <QGridLayout>
#include <QMenu> #include <QMenu>
@ -42,11 +43,11 @@ void DeckEditorFilterDockWidget::createFiltersDock()
connect(filterBuilder, &FilterBuilder::add, filterModel, &FilterTreeModel::addFilter); connect(filterBuilder, &FilterBuilder::add, filterModel, &FilterTreeModel::addFilter);
aClearFilterOne = new QAction(QString(), this); aClearFilterOne = new QAction(QString(), this);
aClearFilterOne->setIcon(QPixmap("theme:icons/decrement")); aClearFilterOne->setIcon(themePixmap(QStringLiteral("icons/decrement")));
connect(aClearFilterOne, &QAction::triggered, this, &DeckEditorFilterDockWidget::actClearFilterOne); connect(aClearFilterOne, &QAction::triggered, this, &DeckEditorFilterDockWidget::actClearFilterOne);
aClearFilterAll = new QAction(QString(), this); aClearFilterAll = new QAction(QString(), this);
aClearFilterAll->setIcon(QPixmap("theme:icons/clearsearch")); aClearFilterAll->setIcon(themePixmap(QStringLiteral("icons/clearsearch")));
connect(aClearFilterAll, &QAction::triggered, this, &DeckEditorFilterDockWidget::actClearFilterAll); connect(aClearFilterAll, &QAction::triggered, this, &DeckEditorFilterDockWidget::actClearFilterAll);
auto *filterDelOne = new QToolButton(); auto *filterDelOne = new QToolButton();

View file

@ -1,5 +1,6 @@
#include "deck_list_history_manager_widget.h" #include "deck_list_history_manager_widget.h"
#include "../../pixel_map_generator.h"
#include "deck_state_manager.h" #include "deck_state_manager.h"
DeckListHistoryManagerWidget::DeckListHistoryManagerWidget(DeckStateManager *_deckStateManager, DeckListHistoryManagerWidget::DeckListHistoryManagerWidget(DeckStateManager *_deckStateManager,
@ -10,7 +11,7 @@ DeckListHistoryManagerWidget::DeckListHistoryManagerWidget(DeckStateManager *_de
layout = new QHBoxLayout(this); layout = new QHBoxLayout(this);
aUndo = new QAction(QString(), 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->setShortcut(QKeySequence::Undo);
aUndo->setShortcutContext(Qt::ApplicationShortcut); aUndo->setShortcutContext(Qt::ApplicationShortcut);
connect(aUndo, &QAction::triggered, this, &DeckListHistoryManagerWidget::doUndo); connect(aUndo, &QAction::triggered, this, &DeckListHistoryManagerWidget::doUndo);
@ -19,7 +20,7 @@ DeckListHistoryManagerWidget::DeckListHistoryManagerWidget(DeckStateManager *_de
undoButton->setDefaultAction(aUndo); undoButton->setDefaultAction(aUndo);
aRedo = new QAction(QString(), this); aRedo = new QAction(QString(), this);
aRedo->setIcon(QPixmap("theme:icons/arrow_redo")); aRedo->setIcon(themePixmap(QStringLiteral("icons/arrow_redo")));
aRedo->setShortcut(QKeySequence::Redo); aRedo->setShortcut(QKeySequence::Redo);
aRedo->setShortcutContext(Qt::ApplicationShortcut); aRedo->setShortcutContext(Qt::ApplicationShortcut);
connect(aRedo, &QAction::triggered, this, &DeckListHistoryManagerWidget::doRedo); connect(aRedo, &QAction::triggered, this, &DeckListHistoryManagerWidget::doRedo);
@ -31,7 +32,7 @@ DeckListHistoryManagerWidget::DeckListHistoryManagerWidget(DeckStateManager *_de
layout->addWidget(redoButton); layout->addWidget(redoButton);
historyButton = new SettingsButtonWidget(this); historyButton = new SettingsButtonWidget(this);
historyButton->setButtonIcon(QPixmap("theme:icons/arrow_history")); historyButton->setButtonIcon(themePixmap(QStringLiteral("icons/arrow_history")));
historyLabel = new QLabel(this); historyLabel = new QLabel(this);

View file

@ -1,6 +1,7 @@
#include "dlg_connect.h" #include "dlg_connect.h"
#include "../../../client/settings/cache_settings.h" #include "../../../client/settings/cache_settings.h"
#include "../../pixel_map_generator.h"
#include <QCheckBox> #include <QCheckBox>
#include <QComboBox> #include <QComboBox>
@ -21,7 +22,7 @@ DlgConnect::DlgConnect(QWidget *parent) : QDialog(parent)
previousHosts = new QComboBox(this); previousHosts = new QComboBox(this);
btnDeleteServer = new QPushButton(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->setToolTip(tr("Delete the currently selected saved server"));
btnDeleteServer->setFixedWidth(30); btnDeleteServer->setFixedWidth(30);
@ -29,7 +30,7 @@ DlgConnect::DlgConnect(QWidget *parent) : QDialog(parent)
hps = new HandlePublicServers(this); hps = new HandlePublicServers(this);
btnRefreshServers = new QPushButton(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->setToolTip(tr("Refresh the server list with known public servers"));
btnRefreshServers->setFixedWidth(30); btnRefreshServers->setFixedWidth(30);
@ -99,7 +100,7 @@ DlgConnect::DlgConnect(QWidget *parent) : QDialog(parent)
updateDisplayInfo(previousHosts->currentText()); updateDisplayInfo(previousHosts->currentText());
btnForgotPassword = new QPushButton(this); btnForgotPassword = new QPushButton(this);
btnForgotPassword->setIcon(QPixmap("theme:icons/forgot_password")); btnForgotPassword->setIcon(themePixmap(QStringLiteral("icons/forgot_password")));
btnForgotPassword->setToolTip(tr("Reset Password")); btnForgotPassword->setToolTip(tr("Reset Password"));
btnForgotPassword->setFixedWidth(30); btnForgotPassword->setFixedWidth(30);
connect(btnForgotPassword, &QPushButton::released, this, &DlgConnect::actForgotPassword); connect(btnForgotPassword, &QPushButton::released, this, &DlgConnect::actForgotPassword);

View file

@ -1,5 +1,6 @@
#include "dlg_edit_tokens.h" #include "dlg_edit_tokens.h"
#include "../../pixel_map_generator.h"
#include "../interface/widgets/utility/get_text_with_max.h" #include "../interface/widgets/utility/get_text_with_max.h"
#include <QAction> #include <QAction>
@ -90,10 +91,10 @@ DlgEditTokens::DlgEditTokens(QWidget *parent) : QDialog(parent), currentCard(nul
&DlgEditTokens::tokenSelectionChanged); &DlgEditTokens::tokenSelectionChanged);
QAction *aAddToken = new QAction(tr("Add token"), this); 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); connect(aAddToken, &QAction::triggered, this, &DlgEditTokens::actAddToken);
QAction *aRemoveToken = new QAction(tr("Remove token"), this); 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); connect(aRemoveToken, &QAction::triggered, this, &DlgEditTokens::actRemoveToken);
auto *databaseToolBar = new QToolBar; auto *databaseToolBar = new QToolBar;

View file

@ -1,6 +1,7 @@
#include "dlg_manage_sets.h" #include "dlg_manage_sets.h"
#include "../../../client/settings/cache_settings.h" #include "../../../client/settings/cache_settings.h"
#include "../../pixel_map_generator.h"
#include "../interface/card_picture_loader/card_picture_loader.h" #include "../interface/card_picture_loader/card_picture_loader.h"
#include "../interface/widgets/utility/custom_line_edit.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); setsEditToolBar->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum);
aTop = new QAction(QString(), this); 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->setToolTip(tr("Move selected set to the top"));
aTop->setEnabled(false); aTop->setEnabled(false);
connect(aTop, &QAction::triggered, this, &WndSets::actTop); connect(aTop, &QAction::triggered, this, &WndSets::actTop);
setsEditToolBar->addAction(aTop); setsEditToolBar->addAction(aTop);
aUp = new QAction(QString(), this); 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->setToolTip(tr("Move selected set up"));
aUp->setEnabled(false); aUp->setEnabled(false);
connect(aUp, &QAction::triggered, this, &WndSets::actUp); connect(aUp, &QAction::triggered, this, &WndSets::actUp);
setsEditToolBar->addAction(aUp); setsEditToolBar->addAction(aUp);
aDown = new QAction(QString(), this); 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->setToolTip(tr("Move selected set down"));
aDown->setEnabled(false); aDown->setEnabled(false);
connect(aDown, &QAction::triggered, this, &WndSets::actDown); connect(aDown, &QAction::triggered, this, &WndSets::actDown);
setsEditToolBar->addAction(aDown); setsEditToolBar->addAction(aDown);
aBottom = new QAction(QString(), this); 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->setToolTip(tr("Move selected set to the bottom"));
aBottom->setEnabled(false); aBottom->setEnabled(false);
connect(aBottom, &QAction::triggered, this, &WndSets::actBottom); connect(aBottom, &QAction::triggered, this, &WndSets::actBottom);
@ -66,7 +67,7 @@ WndSets::WndSets(QWidget *parent) : QMainWindow(parent)
searchField = new LineEditUnfocusable; searchField = new LineEditUnfocusable;
searchField->setObjectName("searchEdit"); searchField->setObjectName("searchEdit");
searchField->setPlaceholderText(tr("Search by set name, code, type, or release date")); 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); searchField->setClearButtonEnabled(true);
setFocusProxy(searchField); setFocusProxy(searchField);

View file

@ -1,6 +1,7 @@
#include "dlg_register.h" #include "dlg_register.h"
#include "../../../client/settings/cache_settings.h" #include "../../../client/settings/cache_settings.h"
#include "../../pixel_map_generator.h"
#include "../server/handle_public_servers.h" #include "../server/handle_public_servers.h"
#include "../server/user/user_info_connection.h" #include "../server/user/user_info_connection.h"
@ -24,7 +25,7 @@ DlgRegister::DlgRegister(QWidget *parent) : QDialog(parent)
previousHosts = new QComboBox(this); previousHosts = new QComboBox(this);
btnDeleteServer = new QPushButton(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->setToolTip(tr("Delete the currently selected saved server"));
btnDeleteServer->setFixedWidth(30); btnDeleteServer->setFixedWidth(30);
@ -32,7 +33,7 @@ DlgRegister::DlgRegister(QWidget *parent) : QDialog(parent)
hps = new HandlePublicServers(this); hps = new HandlePublicServers(this);
btnRefreshServers = new QPushButton(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->setToolTip(tr("Refresh the server list with known public servers"));
btnRefreshServers->setFixedWidth(30); btnRefreshServers->setFixedWidth(30);

View file

@ -6,6 +6,7 @@
#include "dlg_settings.h" #include "dlg_settings.h"
#include "../../../client/settings/cache_settings.h" #include "../../../client/settings/cache_settings.h"
#include "../../pixel_map_generator.h"
#include "../main.h" #include "../main.h"
#include "../settings_page/appearance_settings_page.h" #include "../settings_page/appearance_settings_page.h"
#include "../settings_page/deck_editor_settings_page.h" #include "../settings_page/deck_editor_settings_page.h"
@ -96,7 +97,7 @@ void DlgSettings::setupUi()
// Search bar // Search bar
searchEdit = new QLineEdit; searchEdit = new QLineEdit;
searchEdit->setClearButtonEnabled(true); searchEdit->setClearButtonEnabled(true);
searchEdit->addAction(QPixmap("theme:icons/search"), QLineEdit::LeadingPosition); searchEdit->addAction(themePixmap(QStringLiteral("icons/search")), QLineEdit::LeadingPosition);
searchEdit->installEventFilter(this); searchEdit->installEventFilter(this);
connect(searchEdit, &QLineEdit::textChanged, this, &DlgSettings::onSearchTextChanged); connect(searchEdit, &QLineEdit::textChanged, this, &DlgSettings::onSearchTextChanged);
@ -132,7 +133,7 @@ void DlgSettings::setupUi()
pagesWidget->addWidget(makeScrollable(userInterfacePage)); pagesWidget->addWidget(makeScrollable(userInterfacePage));
pagesWidget->addWidget(makeScrollable(deckEditorPage)); pagesWidget->addWidget(makeScrollable(deckEditorPage));
pagesWidget->addWidget(makeScrollable(storagePage)); pagesWidget->addWidget(makeScrollable(storagePage));
pagesWidget->addWidget(messagesPage); pagesWidget->addWidget(makeScrollable(messagesPage));
pagesWidget->addWidget(soundPage); pagesWidget->addWidget(soundPage);
pagesWidget->addWidget(shortcutsPage); pagesWidget->addWidget(shortcutsPage);
@ -477,13 +478,13 @@ void DlgSettings::closeEvent(QCloseEvent *event)
case Invalid: case Invalid:
loadErrorMessage = tr("Your card database is invalid.\n\n" loadErrorMessage = tr("Your card database is invalid.\n\n"
"Cockatrice may not function correctly with an invalid database\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?"); "Would you like to change your database location setting?");
break; break;
case VersionTooOld: case VersionTooOld:
loadErrorMessage = tr("Your card database version is too old.\n\n" loadErrorMessage = tr("Your card database version is too old.\n\n"
"This can cause problems loading card information or images\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?"); "Would you like to change your database location setting?");
break; break;
case NotLoaded: case NotLoaded:

View file

@ -2,6 +2,7 @@
#include "../../../client/settings/cache_settings.h" #include "../../../client/settings/cache_settings.h"
#include "../../../interface/widgets/tabs/tab_supervisor.h" #include "../../../interface/widgets/tabs/tab_supervisor.h"
#include "../../pixel_map_generator.h"
#include "../../theme_manager.h" #include "../../theme_manager.h"
#include "../../window_main.h" #include "../../window_main.h"
#include "../cards/art_crop_attribution.h" #include "../cards/art_crop_attribution.h"
@ -20,7 +21,8 @@
#include <libcockatrice/settings/paths_settings.h> #include <libcockatrice/settings/paths_settings.h>
HomeWidget::HomeWidget(QWidget *parent, TabSupervisor *_tabSupervisor) 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"))),
overlay(themePixmap(QStringLiteral("cockatrice")))
{ {
layout = new QGridLayout(this); layout = new QGridLayout(this);
@ -56,6 +58,9 @@ HomeWidget::HomeWidget(QWidget *parent, TabSupervisor *_tabSupervisor)
&HomeWidget::initializeBackgroundFromSource); &HomeWidget::initializeBackgroundFromSource);
connect(&SettingsCache::instance(), &SettingsCache::themeChanged, this, connect(&SettingsCache::instance(), &SettingsCache::themeChanged, this,
&HomeWidget::updateButtonsToBackgroundColor); &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(&SettingsCache::instance().appearance(), &AppearanceSettings::homeTabButtonColorChanged, this, connect(&SettingsCache::instance().appearance(), &AppearanceSettings::homeTabButtonColorChanged, this,
&HomeWidget::updateButtonsToBackgroundColor); &HomeWidget::updateButtonsToBackgroundColor);
} }
@ -74,7 +79,7 @@ void HomeWidget::initializeBackgroundFromSource()
switch (backgroundSourceType) { switch (backgroundSourceType) {
case BackgroundSources::Theme: case BackgroundSources::Theme:
cardChangeTimer->stop(); cardChangeTimer->stop();
background = QPixmap("theme:backgrounds/home"); background = themePixmap(QStringLiteral("backgrounds/home"));
backgroundSourceDeck = DeckList(); backgroundSourceDeck = DeckList();
backgroundSourceCard->setCard(ExactCard()); backgroundSourceCard->setCard(ExactCard());
updateButtonsToBackgroundColor(); updateButtonsToBackgroundColor();

View file

@ -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() void FirstRunWizard::finish()
{ {
accept(); accept();

View file

@ -37,6 +37,9 @@ public slots:
/** @brief Forwarded from MainWindow once the background card database update process exits. */ /** @brief Forwarded from MainWindow once the background card database update process exits. */
void onCardDatabaseUpdateFinished(bool success); 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: protected:
void closeEvent(QCloseEvent *event) override; void closeEvent(QCloseEvent *event) override;
void changeEvent(QEvent *event) override; void changeEvent(QEvent *event) override;

View file

@ -15,6 +15,7 @@
#include <QTimer> #include <QTimer>
#include <QUrl> #include <QUrl>
#include <QVBoxLayout> #include <QVBoxLayout>
#include <climits>
#include <libcockatrice/card/database/card_database_manager.h> #include <libcockatrice/card/database/card_database_manager.h>
#include <libcockatrice/settings/updates_settings.h> #include <libcockatrice/settings/updates_settings.h>
@ -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<int>(qMin<qint64>(total, INT_MAX)) : 0);
progressBar->setValue(static_cast<int>(qMin<qint64>(done, INT_MAX)));
if (total > 0) {
const int percent = static_cast<int>((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 QString CardDatabaseSetupPage::nextButtonText() const
{ {
return state == State::NotStarted ? tr("Download") : QString(); return state == State::NotStarted ? tr("Download") : QString();

View file

@ -30,6 +30,7 @@ public:
void retranslateUi() override; void retranslateUi() override;
void onUpdateFinished(bool success); void onUpdateFinished(bool success);
void onUpdateProgress(const QString &stage, qint64 done, qint64 total);
signals: signals:
void updateRequested(); void updateRequested();

View file

@ -2,6 +2,7 @@
#include "../../card_picture_loader/card_picture_loader.h" #include "../../card_picture_loader/card_picture_loader.h"
#include "../cards/art_crop_attribution.h" #include "../cards/art_crop_attribution.h"
#include "../cards/card_art_utils.h"
#include "../utility/completer_utils.h" #include "../utility/completer_utils.h"
#include "card_database_display_model.h" #include "card_database_display_model.h"
#include "card_database_model.h" #include "card_database_model.h"
@ -276,7 +277,7 @@ void PlaymatSettingsDialog::reloadPreview()
return; return;
} }
currentPixmap = fullRes; currentPixmap = CardArtUtils::rotateSidewaysLayoutArt(fullRes, card);
preview->setPixmap(currentPixmap); preview->setPixmap(currentPixmap);
preview->setParams(currentParams); preview->setParams(currentParams);
preview->setAttribution(buildArtAttribution(card)); preview->setAttribution(buildArtAttribution(card));

View file

@ -1,5 +1,7 @@
#include "settings_button_widget.h" #include "settings_button_widget.h"
#include "../../pixel_map_generator.h"
#include <QApplication> #include <QApplication>
#include <QHBoxLayout> #include <QHBoxLayout>
#include <QMouseEvent> #include <QMouseEvent>
@ -8,7 +10,7 @@
SettingsButtonWidget::SettingsButtonWidget(QWidget *parent) SettingsButtonWidget::SettingsButtonWidget(QWidget *parent)
: QWidget(parent), button(new QToolButton(this)), popup(new SettingsPopupWidget(nullptr)) : 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->setCheckable(true);
button->setFixedSize(32, 32); button->setFixedSize(32, 32);
connect(button, &QToolButton::clicked, this, &SettingsButtonWidget::togglePopup); connect(button, &QToolButton::clicked, this, &SettingsButtonWidget::togglePopup);

View file

@ -142,8 +142,10 @@ void ReplayManager::processNewEvents(PlaybackMode playbackMode)
} }
// backwards skip => always skip tap animation // backwards skip => always skip tap animation
// backwards skip => always skip damage animation (battlefield shimmer / life counter flash)
if (playbackMode == BACKWARD_SKIP) { if (playbackMode == BACKWARD_SKIP) {
options |= SKIP_TAP_ANIMATION; options |= SKIP_TAP_ANIMATION;
options |= SKIP_DAMAGE_ANIMATION;
} }
emit eventReplayed(replay->event_list(currentEvent), options); emit eventReplayed(replay->event_list(currentEvent), options);

View file

@ -2,6 +2,7 @@
#include "../../../client/settings/cache_settings.h" #include "../../../client/settings/cache_settings.h"
#include "../../../client/settings/shortcuts_settings.h" #include "../../../client/settings/shortcuts_settings.h"
#include "../../pixel_map_generator.h"
#include "../interface/widgets/tabs/tab_game.h" #include "../interface/widgets/tabs/tab_game.h"
#include "replay_manager.h" #include "replay_manager.h"
#include "replay_quick_settings_widget.h" #include "replay_quick_settings_widget.h"
@ -50,15 +51,15 @@ ReplayWidget::ReplayWidget(QWidget *parent, GameReplay *replay)
replayPlayButton = new QToolButton; replayPlayButton = new QToolButton;
replayPlayButton->setIconSize(QSize(32, 32)); replayPlayButton->setIconSize(QSize(32, 32));
QIcon playButtonIcon = QIcon(); QIcon playButtonIcon = QIcon();
playButtonIcon.addPixmap(QPixmap("theme:replay/start"), QIcon::Normal, QIcon::Off); playButtonIcon.addPixmap(themePixmap(QStringLiteral("replay/start")), QIcon::Normal, QIcon::Off);
playButtonIcon.addPixmap(QPixmap("theme:replay/pause"), QIcon::Normal, QIcon::On); playButtonIcon.addPixmap(themePixmap(QStringLiteral("replay/pause")), QIcon::Normal, QIcon::On);
replayPlayButton->setIcon(playButtonIcon); replayPlayButton->setIcon(playButtonIcon);
replayPlayButton->setCheckable(true); replayPlayButton->setCheckable(true);
connect(replayPlayButton, &QToolButton::toggled, this, &ReplayWidget::replayPlayButtonToggled); connect(replayPlayButton, &QToolButton::toggled, this, &ReplayWidget::replayPlayButtonToggled);
replayFastForwardButton = new QToolButton; replayFastForwardButton = new QToolButton;
replayFastForwardButton->setIconSize(QSize(32, 32)); replayFastForwardButton->setIconSize(QSize(32, 32));
replayFastForwardButton->setIcon(QPixmap("theme:replay/fastforward")); replayFastForwardButton->setIcon(themePixmap(QStringLiteral("replay/fastforward")));
replayFastForwardButton->setCheckable(true); replayFastForwardButton->setCheckable(true);
connect(replayFastForwardButton, &QToolButton::toggled, this, &ReplayWidget::updateTimeScaleFactor); connect(replayFastForwardButton, &QToolButton::toggled, this, &ReplayWidget::updateTimeScaleFactor);

View file

@ -1,5 +1,6 @@
#include "game_selector.h" #include "game_selector.h"
#include "../../pixel_map_generator.h"
#include "../interface/widgets/dialogs/dlg_create_game.h" #include "../interface/widgets/dialogs/dlg_create_game.h"
#include "../interface/widgets/dialogs/dlg_filter_games.h" #include "../interface/widgets/dialogs/dlg_filter_games.h"
#include "../interface/widgets/tabs/tab_account.h" #include "../interface/widgets/tabs/tab_account.h"
@ -95,10 +96,10 @@ GameSelector::GameSelector(AbstractClient *_client,
} }
filterButton = new QPushButton; filterButton = new QPushButton;
filterButton->setIcon(QPixmap("theme:icons/search")); filterButton->setIcon(themePixmap(QStringLiteral("icons/search")));
connect(filterButton, &QPushButton::clicked, this, &GameSelector::actSetFilter); connect(filterButton, &QPushButton::clicked, this, &GameSelector::actSetFilter);
clearFilterButton = new QPushButton; clearFilterButton = new QPushButton;
clearFilterButton->setIcon(QPixmap("theme:icons/clearsearch")); clearFilterButton->setIcon(themePixmap(QStringLiteral("icons/clearsearch")));
bool filtersSetToDefault = showFilters && gameListProxyModel->areFilterParametersSetToDefaults(); bool filtersSetToDefault = showFilters && gameListProxyModel->areFilterParametersSetToDefaults();
clearFilterButton->setEnabled(!filtersSetToDefault); clearFilterButton->setEnabled(!filtersSetToDefault);
connect(clearFilterButton, &QPushButton::clicked, this, &GameSelector::actClearFilter); connect(clearFilterButton, &QPushButton::clicked, this, &GameSelector::actClearFilter);

View file

@ -1,5 +1,7 @@
#include "remote_replay_list_tree_widget.h" #include "remote_replay_list_tree_widget.h"
#include "../../../pixel_map_generator.h"
#include <QFileIconProvider> #include <QFileIconProvider>
#include <QHeaderView> #include <QHeaderView>
#include <QSortFilterProxyModel> #include <QSortFilterProxyModel>
@ -37,7 +39,7 @@ RemoteReplayList_TreeModel::RemoteReplayList_TreeModel(AbstractClient *_client,
QFileIconProvider fip; QFileIconProvider fip;
dirIcon = fip.icon(QFileIconProvider::Folder); dirIcon = fip.icon(QFileIconProvider::Folder);
fileIcon = fip.icon(QFileIconProvider::File); fileIcon = fip.icon(QFileIconProvider::File);
lockIcon = QPixmap("theme:icons/lock"); lockIcon = themePixmap(QStringLiteral("icons/lock"));
} }
RemoteReplayList_TreeModel::~RemoteReplayList_TreeModel() RemoteReplayList_TreeModel::~RemoteReplayList_TreeModel()

View file

@ -1,6 +1,7 @@
#include "user_card_art_provider.h" #include "user_card_art_provider.h"
#include "../../../card_picture_loader/card_picture_loader.h" #include "../../../card_picture_loader/card_picture_loader.h"
#include "../../cards/card_art_utils.h"
#include <QPointer> #include <QPointer>
#include <libcockatrice/card/database/card_database_manager.h> #include <libcockatrice/card/database/card_database_manager.h>
@ -52,16 +53,25 @@ void UserCardArtProvider::requestCardArt(const QString &userName, const QString
processQueue(); 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 marginX = sz.width() * 0.07;
const int topMargin = sz.height() * 0.11; const int topMargin = landscape ? sz.height() * 0.05 : sz.height() * 0.11;
const int bottomMargin = sz.height() * 0.45; 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) void UserCardArtProvider::insertIntoCache(const QString &key, const QPixmap &pixmap)
@ -111,7 +121,7 @@ void UserCardArtProvider::processQueue()
// Synchronous hit (already loaded/on disk) // Synchronous hit (already loaded/on disk)
if (!fullRes.isNull()) { if (!fullRes.isNull()) {
insertIntoCache(key, cropCardArt(fullRes)); insertIntoCache(key, cropCardArt(fullRes, card));
pending.remove(key); pending.remove(key);
emit cardArtUpdated(userName); emit cardArtUpdated(userName);
@ -135,7 +145,7 @@ void UserCardArtProvider::processQueue()
CardPictureLoader::getPixmap(fullRes, card, QSize(745, 1040)); CardPictureLoader::getPixmap(fullRes, card, QSize(745, 1040));
if (!fullRes.isNull()) { if (!fullRes.isNull()) {
self->insertIntoCache(key, self->cropCardArt(fullRes)); self->insertIntoCache(key, self->cropCardArt(fullRes, card));
} }
self->pending.remove(key); self->pending.remove(key);

View file

@ -6,6 +6,7 @@
#include <QPixmap> #include <QPixmap>
#include <QQueue> #include <QQueue>
#include <QSet> #include <QSet>
#include <libcockatrice/card/printing/exact_card.h>
class UserCardArtProvider : public QObject class UserCardArtProvider : public QObject
{ {
@ -16,7 +17,7 @@ public:
void requestCardArt(const QString &userName, const QString &cardName, const QString &providerId); void requestCardArt(const QString &userName, const QString &cardName, const QString &providerId);
const QMap<QString, QPixmap> &cache() const; const QMap<QString, QPixmap> &cache() const;
static QPixmap cropCardArt(const QPixmap &fullRes); static QPixmap cropCardArt(const QPixmap &fullRes, const ExactCard &card);
signals: signals:
void cardArtUpdated(const QString &userName); void cardArtUpdated(const QString &userName);

View file

@ -560,7 +560,7 @@ void UserCardArtSettingsDialog::reloadPreview()
return; return;
} }
currentPixmap = UserCardArtProvider::cropCardArt(fullRes); currentPixmap = UserCardArtProvider::cropCardArt(fullRes, card);
preview->setPixmap(currentPixmap); preview->setPixmap(currentPixmap);
preview->setParams(currentParams); preview->setParams(currentParams);

View file

@ -51,6 +51,8 @@ UserContextMenu::UserContextMenu(TabSupervisor *_tabSupervisor, QWidget *parent,
aDemoteFromMod = new QAction(QString(), this); aDemoteFromMod = new QAction(QString(), this);
aPromoteToJudge = new QAction(QString(), this); aPromoteToJudge = new QAction(QString(), this);
aDemoteFromJudge = new QAction(QString(), this); aDemoteFromJudge = new QAction(QString(), this);
aPromoteToDeveloper = new QAction(QString(), this);
aDemoteFromDeveloper = new QAction(QString(), this);
aGetAdminNotes = new QAction(QString(), this); aGetAdminNotes = new QAction(QString(), this);
aInvestigateUser = new QAction(QString(), this); aInvestigateUser = new QAction(QString(), this);
@ -76,6 +78,8 @@ void UserContextMenu::retranslateUi()
aDemoteFromMod->setText(tr("Dem&ote user from moderator")); aDemoteFromMod->setText(tr("Dem&ote user from moderator"));
aPromoteToJudge->setText(tr("Promote user to &judge")); aPromoteToJudge->setText(tr("Promote user to &judge"));
aDemoteFromJudge->setText(tr("Demote user from 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")); aGetAdminNotes->setText(tr("View admin notes"));
aInvestigateUser->setText(tr("Investigate user")); 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); const Command_AdjustMod &cmd = commandContainer.admin_command(0).GetExtension(Command_AdjustMod::ext);
if (resp.response_code() == Response::RespOk) { 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<QWidget *>(parent()), tr("Success"), QMessageBox::information(static_cast<QWidget *>(parent()), tr("Success"),
tr("Successfully promoted user.")); tr("Successfully promoted user."));
} else { } else {
@ -276,7 +280,7 @@ void UserContextMenu::adjustMod_processUserResponse(const Response &resp, const
} }
} else { } 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<QWidget *>(parent()), tr("Failed"), tr("Failed to promote user.")); QMessageBox::information(static_cast<QWidget *>(parent()), tr("Failed"), tr("Failed to promote user."));
} else { } else {
QMessageBox::information(static_cast<QWidget *>(parent()), tr("Failed"), tr("Failed to demote user.")); QMessageBox::information(static_cast<QWidget *>(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)) { (tabSupervisor->getUserInfo()->user_level() & ServerInfo_User::IsAdmin)) {
menu->addAction(aPromoteToJudge); 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); aDetails->setEnabled(true);
aChat->setEnabled(anotherUser && online); aChat->setEnabled(anotherUser && online && !userListProxy->isUserIgnored(userName));
aShowGames->setEnabled(online); aShowGames->setEnabled(online);
aReport->setEnabled(anotherUser); aReport->setEnabled(anotherUser);
aAddToBuddyList->setEnabled(anotherUser); aAddToBuddyList->setEnabled(anotherUser);
@ -455,6 +468,10 @@ void UserContextMenu::showContextMenu(const QPoint &pos,
aInvestigateUser->setEnabled(anotherUser); aInvestigateUser->setEnabled(anotherUser);
aPromoteToMod->setEnabled(anotherUser); aPromoteToMod->setEnabled(anotherUser);
aDemoteFromMod->setEnabled(anotherUser); aDemoteFromMod->setEnabled(anotherUser);
aPromoteToJudge->setEnabled(anotherUser);
aDemoteFromJudge->setEnabled(anotherUser);
aPromoteToDeveloper->setEnabled(anotherUser);
aDemoteFromDeveloper->setEnabled(anotherUser);
QAction *actionClicked = menu->exec(pos); QAction *actionClicked = menu->exec(pos);
if (actionClicked == nullptr) { if (actionClicked == nullptr) {
@ -489,6 +506,8 @@ void UserContextMenu::showContextMenu(const QPoint &pos,
execAdjustMod(userName, actionClicked == aPromoteToMod); execAdjustMod(userName, actionClicked == aPromoteToMod);
} else if (actionClicked == aPromoteToJudge || actionClicked == aDemoteFromJudge) { } else if (actionClicked == aPromoteToJudge || actionClicked == aDemoteFromJudge) {
execAdjustJudge(userName, actionClicked == aPromoteToJudge); execAdjustJudge(userName, actionClicked == aPromoteToJudge);
} else if (actionClicked == aPromoteToDeveloper || actionClicked == aDemoteFromDeveloper) {
execAdjustDeveloper(userName, actionClicked == aPromoteToDeveloper);
} else if (actionClicked == aBanHistory) { } else if (actionClicked == aBanHistory) {
execBanHistory(userName); execBanHistory(userName);
} else if (actionClicked == aWarnUser) { } else if (actionClicked == aWarnUser) {
@ -606,7 +625,15 @@ void UserContextMenu::execAddToIgnore(const QString &userName)
Command_AddToList cmd; Command_AddToList cmd;
cmd.set_list("ignore"); cmd.set_list("ignore");
cmd.set_user_name(userName.toStdString()); 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<QWidget *>(parent()), tr("Ignore list"),
tr("%1 has been added to your ignore list.").arg(userName));
}
});
client->sendCommand(pend);
} }
void UserContextMenu::execRemoveFromIgnore(const QString &userName) void UserContextMenu::execRemoveFromIgnore(const QString &userName)
@ -698,4 +725,14 @@ void UserContextMenu::execAdjustJudge(const QString &userName, bool shouldBeJudg
PendingCommand *pend = client->prepareAdminCommand(cmd); PendingCommand *pend = client->prepareAdminCommand(cmd);
connect(pend, &PendingCommand::finished, this, &UserContextMenu::adjustMod_processUserResponse); connect(pend, &PendingCommand::finished, this, &UserContextMenu::adjustMod_processUserResponse);
client->sendCommand(pend); client->sendCommand(pend);
} }
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);
}

View file

@ -45,6 +45,7 @@ private:
QAction *aBan, *aBanHistory; QAction *aBan, *aBanHistory;
QAction *aPromoteToMod, *aDemoteFromMod; QAction *aPromoteToMod, *aDemoteFromMod;
QAction *aPromoteToJudge, *aDemoteFromJudge; QAction *aPromoteToJudge, *aDemoteFromJudge;
QAction *aPromoteToDeveloper, *aDemoteFromDeveloper;
QAction *aWarnUser, *aWarnHistory; QAction *aWarnUser, *aWarnHistory;
QAction *aGetAdminNotes; QAction *aGetAdminNotes;
std::function<QList<GameInviteOption>()> gameInviteLinkProvider; std::function<QList<GameInviteOption>()> gameInviteLinkProvider;
@ -123,6 +124,7 @@ public:
void execInvestigateUser(const QString &userName); void execInvestigateUser(const QString &userName);
void execAdjustMod(const QString &userName, bool shouldBeMod); void execAdjustMod(const QString &userName, bool shouldBeMod);
void execAdjustJudge(const QString &userName, bool shouldBeJudge); void execAdjustJudge(const QString &userName, bool shouldBeJudge);
void execAdjustDeveloper(const QString &userName, bool shouldBeDeveloper);
private: private:
void execInvite(const QString &userName, const GameInviteOption &option); void execInvite(const QString &userName, const GameInviteOption &option);

View file

@ -122,6 +122,8 @@ void UserInfoBox::updateInfo(const ServerInfo_User &user)
QString userLevelText; QString userLevelText;
if (userLevel.testFlag(ServerInfo_User::IsAdmin)) { if (userLevel.testFlag(ServerInfo_User::IsAdmin)) {
userLevelText = tr("Administrator"); userLevelText = tr("Administrator");
} else if (userLevel.testFlag(ServerInfo_User::IsDeveloper)) {
userLevelText = tr("Developer");
} else if (userLevel.testFlag(ServerInfo_User::IsModerator)) { } else if (userLevel.testFlag(ServerInfo_User::IsModerator)) {
userLevelText = tr("Moderator"); userLevelText = tr("Moderator");
} else if (userLevel.testFlag(ServerInfo_User::IsRegistered)) { } else if (userLevel.testFlag(ServerInfo_User::IsRegistered)) {

View file

@ -245,6 +245,9 @@ void UserInfoHeaderWidget::paintEvent(QPaintEvent *)
if (level.testFlag(ServerInfo_User::IsAdmin)) { if (level.testFlag(ServerInfo_User::IsAdmin)) {
return QColor(245, 158, 11); return QColor(245, 158, 11);
} }
if (level.testFlag(ServerInfo_User::IsDeveloper)) {
return QColor(185, 28, 28);
}
if (level.testFlag(ServerInfo_User::IsModerator)) { if (level.testFlag(ServerInfo_User::IsModerator)) {
return QColor(59, 130, 246); return QColor(59, 130, 246);
} }
@ -300,6 +303,8 @@ void UserInfoHeaderWidget::paintEvent(QPaintEvent *)
} badge; } badge;
if (level.testFlag(ServerInfo_User::IsAdmin)) { if (level.testFlag(ServerInfo_User::IsAdmin)) {
badge = {"ADMIN", QColor(245, 158, 11)}; 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)) { } else if (level.testFlag(ServerInfo_User::IsModerator)) {
badge = {"MOD", QColor(59, 130, 246)}; badge = {"MOD", QColor(59, 130, 246)};
} else if (level.testFlag(ServerInfo_User::IsJudge)) { } else if (level.testFlag(ServerInfo_User::IsJudge)) {

View file

@ -49,6 +49,8 @@ QColor UserListPainter::getAccentColor(const UserLevelFlags &userLevel, bool onl
if (userLevel.testFlag(ServerInfo_User::IsAdmin)) { if (userLevel.testFlag(ServerInfo_User::IsAdmin)) {
accentColor = QColor(245, 158, 11); accentColor = QColor(245, 158, 11);
} else if (userLevel.testFlag(ServerInfo_User::IsDeveloper)) {
accentColor = QColor(185, 28, 28);
} else if (userLevel.testFlag(ServerInfo_User::IsModerator)) { } else if (userLevel.testFlag(ServerInfo_User::IsModerator)) {
accentColor = QColor(59, 130, 246); accentColor = QColor(59, 130, 246);
} else if (userLevel.testFlag(ServerInfo_User::IsJudge)) { } else if (userLevel.testFlag(ServerInfo_User::IsJudge)) {
@ -299,6 +301,8 @@ QList<UserListPainter::Badge> UserListPainter::buildBadges(const UserLevelFlags
if (userLevel.testFlag(ServerInfo_User::IsAdmin)) { if (userLevel.testFlag(ServerInfo_User::IsAdmin)) {
badges << Badge{"ADMIN", QColor(245, 158, 11)}; 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)) { } else if (userLevel.testFlag(ServerInfo_User::IsModerator)) {
badges << Badge{"MOD", QColor(59, 130, 246)}; badges << Badge{"MOD", QColor(59, 130, 246)};
} else if (userLevel.testFlag(ServerInfo_User::IsJudge)) { } 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 userName = QString::fromStdString(userInfo.name());
const QString privLevel = QString::fromStdString(userInfo.privlevel()); const QString privLevel = QString::fromStdString(userInfo.privlevel());
const QColor accentColor = getAccentColor(userLevel, online); const QColor accentColor = getAccentColor(userLevel, online);
const bool hasRole = userLevel.testFlag(ServerInfo_User::IsAdmin) || const bool hasRole =
userLevel.testFlag(ServerInfo_User::IsModerator) || userLevel.testFlag(ServerInfo_User::IsAdmin) || userLevel.testFlag(ServerInfo_User::IsDeveloper) ||
userLevel.testFlag(ServerInfo_User::IsJudge); userLevel.testFlag(ServerInfo_User::IsModerator) || userLevel.testFlag(ServerInfo_User::IsJudge);
const QRectF cardRect = QRectF(rect).adjusted(3, 2, -3, -2); const QRectF cardRect = QRectF(rect).adjusted(3, 2, -3, -2);
const int cardRight = getCardRight(option, rect); const int cardRight = getCardRight(option, rect);

View file

@ -234,9 +234,11 @@ bool UserListTWI::operator<(const QTreeWidgetItem &other) const
const auto &lhsUserLevelFlags = UserLevelFlags(data(0, Qt::UserRole).toInt()); const auto &lhsUserLevelFlags = UserLevelFlags(data(0, Qt::UserRole).toInt());
const auto &rhsUserLevelFlags = UserLevelFlags(other.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<ServerInfo_User_UserLevelFlag> userLevelWithNoOtherPrefOrder = { static const QList<ServerInfo_User_UserLevelFlag> 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) { for (const auto &userLevelEntry : userLevelWithNoOtherPrefOrder) {
if (lhsUserLevelFlags.testFlag(userLevelEntry) && if (lhsUserLevelFlags.testFlag(userLevelEntry) &&
lhsUserLevelFlags.testFlag(userLevelEntry) == rhsUserLevelFlags.testFlag(userLevelEntry)) { lhsUserLevelFlags.testFlag(userLevelEntry) == rhsUserLevelFlags.testFlag(userLevelEntry)) {

View file

@ -1,6 +1,7 @@
#include "deck_editor_settings_page.h" #include "deck_editor_settings_page.h"
#include "../../../client/settings/cache_settings.h" #include "../../../client/settings/cache_settings.h"
#include "../../pixel_map_generator.h"
#include "update/card_spoiler/spoiler_background_updater.h" #include "update/card_spoiler/spoiler_background_updater.h"
#include <QFileDialog> #include <QFileDialog>
@ -53,15 +54,15 @@ DeckEditorSettingsPage::DeckEditorSettingsPage()
urlList->addItems(SettingsCache::instance().downloads().getAllURLs()); urlList->addItems(SettingsCache::instance().downloads().getAllURLs());
aAdd = new QAction(this); aAdd = new QAction(this);
aAdd->setIcon(QPixmap("theme:icons/increment")); aAdd->setIcon(themePixmap(QStringLiteral("icons/increment")));
connect(aAdd, &QAction::triggered, this, &DeckEditorSettingsPage::actAddURL); connect(aAdd, &QAction::triggered, this, &DeckEditorSettingsPage::actAddURL);
aEdit = new QAction(this); aEdit = new QAction(this);
aEdit->setIcon(QPixmap("theme:icons/pencil")); aEdit->setIcon(themePixmap(QStringLiteral("icons/pencil")));
connect(aEdit, &QAction::triggered, this, &DeckEditorSettingsPage::actEditURL); connect(aEdit, &QAction::triggered, this, &DeckEditorSettingsPage::actEditURL);
aRemove = new QAction(this); aRemove = new QAction(this);
aRemove->setIcon(QPixmap("theme:icons/decrement")); aRemove->setIcon(themePixmap(QStringLiteral("icons/decrement")));
connect(aRemove, &QAction::triggered, this, &DeckEditorSettingsPage::actRemoveURL); connect(aRemove, &QAction::triggered, this, &DeckEditorSettingsPage::actRemoveURL);
auto *urlToolBar = new QToolBar; auto *urlToolBar = new QToolBar;

View file

@ -1,6 +1,7 @@
#include "messages_settings_page.h" #include "messages_settings_page.h"
#include "../../../client/settings/cache_settings.h" #include "../../../client/settings/cache_settings.h"
#include "../../pixel_map_generator.h"
#include "../interface/widgets/utility/get_text_with_max.h" #include "../interface/widgets/utility/get_text_with_max.h"
#include <QGridLayout> #include <QGridLayout>
@ -59,6 +60,10 @@ MessagesSettingsPage::MessagesSettingsPage()
connect(&roomHistory, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().chat(), connect(&roomHistory, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().chat(),
&ChatSettings::setRoomHistory); &ChatSettings::setRoomHistory);
ignoreAllPrivateMessagesCheckBox.setChecked(SettingsCache::instance().chat().getIgnoreAllPrivateMessages());
connect(&ignoreAllPrivateMessagesCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().chat(),
&ChatSettings::setIgnoreAllPrivateMessages);
customAlertString = new QLineEdit(); customAlertString = new QLineEdit();
customAlertString->setText(SettingsCache::instance().chat().getHighlightWords()); customAlertString->setText(SettingsCache::instance().chat().getHighlightWords());
connect(customAlertString, &QLineEdit::textChanged, &SettingsCache::instance().chat(), connect(customAlertString, &QLineEdit::textChanged, &SettingsCache::instance().chat(),
@ -76,6 +81,7 @@ MessagesSettingsPage::MessagesSettingsPage()
chatGrid->addWidget(&messagePopups, 5, 0); chatGrid->addWidget(&messagePopups, 5, 0);
chatGrid->addWidget(&mentionPopups, 6, 0); chatGrid->addWidget(&mentionPopups, 6, 0);
chatGrid->addWidget(&roomHistory, 7, 0); chatGrid->addWidget(&roomHistory, 7, 0);
chatGrid->addWidget(&ignoreAllPrivateMessagesCheckBox, 8, 0);
chatGroupBox = new QGroupBox; chatGroupBox = new QGroupBox;
chatGroupBox->setLayout(chatGrid); chatGroupBox->setLayout(chatGrid);
@ -102,15 +108,15 @@ MessagesSettingsPage::MessagesSettingsPage()
} }
aAdd = new QAction(this); aAdd = new QAction(this);
aAdd->setIcon(QPixmap("theme:icons/increment")); aAdd->setIcon(themePixmap(QStringLiteral("icons/increment")));
connect(aAdd, &QAction::triggered, this, &MessagesSettingsPage::actAdd); connect(aAdd, &QAction::triggered, this, &MessagesSettingsPage::actAdd);
aEdit = new QAction(this); aEdit = new QAction(this);
aEdit->setIcon(QPixmap("theme:icons/pencil")); aEdit->setIcon(themePixmap(QStringLiteral("icons/pencil")));
connect(aEdit, &QAction::triggered, this, &MessagesSettingsPage::actEdit); connect(aEdit, &QAction::triggered, this, &MessagesSettingsPage::actEdit);
aRemove = new QAction(this); aRemove = new QAction(this);
aRemove->setIcon(QPixmap("theme:icons/decrement")); aRemove->setIcon(themePixmap(QStringLiteral("icons/decrement")));
connect(aRemove, &QAction::triggered, this, &MessagesSettingsPage::actRemove); connect(aRemove, &QAction::triggered, this, &MessagesSettingsPage::actRemove);
auto *messageToolBar = new QToolBar; auto *messageToolBar = new QToolBar;
@ -246,6 +252,7 @@ void MessagesSettingsPage::retranslateUi()
messagePopups.setText(tr("Enable desktop notifications for private messages")); messagePopups.setText(tr("Enable desktop notifications for private messages"));
mentionPopups.setText(tr("Enable desktop notification for mentions")); mentionPopups.setText(tr("Enable desktop notification for mentions"));
roomHistory.setText(tr("Enable room message history on join")); roomHistory.setText(tr("Enable room message history on join"));
ignoreAllPrivateMessagesCheckBox.setText(tr("Ignore all private messages"));
hexLabel.setText(tr("(Color is hexadecimal)")); hexLabel.setText(tr("(Color is hexadecimal)"));
hexHighlightLabel.setText(tr("(Color is hexadecimal)")); hexHighlightLabel.setText(tr("(Color is hexadecimal)"));
customAlertStringLabel.setText(tr("Separate words with a space, alphanumeric characters only")); customAlertStringLabel.setText(tr("Separate words with a space, alphanumeric characters only"));

View file

@ -40,6 +40,7 @@ private:
QCheckBox messagePopups; QCheckBox messagePopups;
QCheckBox mentionPopups; QCheckBox mentionPopups;
QCheckBox roomHistory; QCheckBox roomHistory;
QCheckBox ignoreAllPrivateMessagesCheckBox;
QGroupBox *chatGroupBox; QGroupBox *chatGroupBox;
QGroupBox *highlightGroupBox; QGroupBox *highlightGroupBox;
QGroupBox *messageGroupBox; QGroupBox *messageGroupBox;

View file

@ -3,6 +3,7 @@
#include "../../../client/settings/cache_settings.h" #include "../../../client/settings/cache_settings.h"
#include "../../../client/settings/shortcut_treeview.h" #include "../../../client/settings/shortcut_treeview.h"
#include "../../../client/settings/shortcuts_settings.h" #include "../../../client/settings/shortcuts_settings.h"
#include "../../pixel_map_generator.h"
#include "../interface/widgets/utility/custom_line_edit.h" #include "../interface/widgets/utility/custom_line_edit.h"
#include "../interface/widgets/utility/sequence_edit.h" #include "../interface/widgets/utility/sequence_edit.h"
@ -47,8 +48,8 @@ ShortcutSettingsPage::ShortcutSettingsPage()
btnResetAll = new QPushButton(this); btnResetAll = new QPushButton(this);
btnClearAll = new QPushButton(this); btnClearAll = new QPushButton(this);
btnResetAll->setIcon(QPixmap("theme:icons/update")); btnResetAll->setIcon(themePixmap(QStringLiteral("icons/update")));
btnClearAll->setIcon(QPixmap("theme:icons/clearsearch")); btnClearAll->setIcon(themePixmap(QStringLiteral("icons/clearsearch")));
// layout // layout
auto *_editLayout = new QGridLayout; auto *_editLayout = new QGridLayout;

View file

@ -1,6 +1,7 @@
#include "tab_archidekt.h" #include "tab_archidekt.h"
#include "../../../../../client/settings/cache_settings.h" #include "../../../../../client/settings/cache_settings.h"
#include "../../../../pixel_map_generator.h"
#include "../../../cards/additional_info/mana_symbol_widget.h" #include "../../../cards/additional_info/mana_symbol_widget.h"
#include "../../../utility/completer_utils.h" #include "../../../utility/completer_utils.h"
#include "../../tab_supervisor.h" #include "../../tab_supervisor.h"
@ -213,7 +214,7 @@ void TabArchidekt::setupFilterWidgets()
// Format filter (collapsible) // Format filter (collapsible)
formatButton = new SettingsButtonWidget(secondaryToolbar); formatButton = new SettingsButtonWidget(secondaryToolbar);
formatButton->setButtonText(tr("Formats")); formatButton->setButtonText(tr("Formats"));
formatButton->setButtonIcon(QPixmap("theme:icons/scale_balanced")); formatButton->setButtonIcon(themePixmap(QStringLiteral("icons/scale_balanced")));
QWidget *formatContainer = new QWidget(secondaryToolbar); QWidget *formatContainer = new QWidget(secondaryToolbar);
QGridLayout *formatLayout = new QGridLayout(formatContainer); QGridLayout *formatLayout = new QGridLayout(formatContainer);

View file

@ -1,6 +1,7 @@
#include "commander_bracket_widget.h" #include "commander_bracket_widget.h"
#include "../../../../../client/settings/cache_settings.h" #include "../../../../../client/settings/cache_settings.h"
#include "../../../../pixel_map_generator.h"
#include "commander_bracket_service.h" #include "commander_bracket_service.h"
#include <QComboBox> #include <QComboBox>
@ -30,7 +31,7 @@ CommanderBracketWidget::CommanderBracketWidget(QWidget *parent) : QWidget(parent
bracketInfoButton->setEnabled(false); bracketInfoButton->setEnabled(false);
bracketRefreshButton = new QToolButton(this); bracketRefreshButton = new QToolButton(this);
bracketRefreshButton->setIcon(QPixmap("theme:icons/reload")); bracketRefreshButton->setIcon(themePixmap(QStringLiteral("icons/reload")));
bracketRefreshButton->setAutoRaise(true); bracketRefreshButton->setAutoRaise(true);
connect(bracketRefreshButton, &QToolButton::clicked, this, &CommanderBracketWidget::requestBracketEstimate); connect(bracketRefreshButton, &QToolButton::clicked, this, &CommanderBracketWidget::requestBracketEstimate);

View file

@ -137,6 +137,11 @@ void TabAccount::retranslateUi()
buddyList->retranslateUi(); buddyList->retranslateUi();
ignoreList->retranslateUi(); ignoreList->retranslateUi();
userInfoBox->retranslateUi(); userInfoBox->retranslateUi();
buddyList->setToolTip(tr("Buddies are marked with a star in chat, a sound plays when they join or leave the "
"server, and they can be invited to buddy-only games."));
ignoreList->setToolTip(tr("Ignored users' chat messages are hidden from you, and they cannot send you private "
"messages or join your games."));
} }
void TabAccount::processListUsersResponse(const Response &response) void TabAccount::processListUsersResponse(const Response &response)

View file

@ -2,6 +2,7 @@
#include "../../../client/settings/cache_settings.h" #include "../../../client/settings/cache_settings.h"
#include "../../deck_loader/deck_loader.h" #include "../../deck_loader/deck_loader.h"
#include "../../pixel_map_generator.h"
#include "../interface/widgets/server/remote/remote_decklist_tree_widget.h" #include "../interface/widgets/server/remote/remote_decklist_tree_widget.h"
#include "../interface/widgets/utility/get_text_with_max.h" #include "../interface/widgets/utility/get_text_with_max.h"
@ -105,19 +106,19 @@ TabDeckStorage::TabDeckStorage(TabSupervisor *_tabSupervisor,
// Left side actions // Left side actions
aOpenLocalDeck = new QAction(this); aOpenLocalDeck = new QAction(this);
aOpenLocalDeck->setIcon(QPixmap("theme:icons/pencil")); aOpenLocalDeck->setIcon(themePixmap(QStringLiteral("icons/pencil")));
connect(aOpenLocalDeck, &QAction::triggered, this, &TabDeckStorage::actOpenLocalDeck); connect(aOpenLocalDeck, &QAction::triggered, this, &TabDeckStorage::actOpenLocalDeck);
aRenameLocal = new QAction(this); aRenameLocal = new QAction(this);
aRenameLocal->setIcon(QPixmap("theme:icons/rename")); aRenameLocal->setIcon(themePixmap(QStringLiteral("icons/rename")));
connect(aRenameLocal, &QAction::triggered, this, &TabDeckStorage::actRenameLocal); connect(aRenameLocal, &QAction::triggered, this, &TabDeckStorage::actRenameLocal);
aUpload = new QAction(this); aUpload = new QAction(this);
aUpload->setIcon(QPixmap("theme:icons/arrow_right_green")); aUpload->setIcon(themePixmap(QStringLiteral("icons/arrow_right_green")));
connect(aUpload, &QAction::triggered, this, &TabDeckStorage::actUpload); connect(aUpload, &QAction::triggered, this, &TabDeckStorage::actUpload);
aNewLocalFolder = new QAction(this); aNewLocalFolder = new QAction(this);
aNewLocalFolder->setIcon(qApp->style()->standardIcon(QStyle::SP_FileDialogNewFolder)); aNewLocalFolder->setIcon(qApp->style()->standardIcon(QStyle::SP_FileDialogNewFolder));
connect(aNewLocalFolder, &QAction::triggered, this, &TabDeckStorage::actNewLocalFolder); connect(aNewLocalFolder, &QAction::triggered, this, &TabDeckStorage::actNewLocalFolder);
aDeleteLocalDeck = new QAction(this); aDeleteLocalDeck = new QAction(this);
aDeleteLocalDeck->setIcon(QPixmap("theme:icons/remove_row")); aDeleteLocalDeck->setIcon(themePixmap(QStringLiteral("icons/remove_row")));
connect(aDeleteLocalDeck, &QAction::triggered, this, &TabDeckStorage::actDeleteLocalDeck); connect(aDeleteLocalDeck, &QAction::triggered, this, &TabDeckStorage::actDeleteLocalDeck);
aOpenDecksFolder = new QAction(this); aOpenDecksFolder = new QAction(this);
@ -126,16 +127,16 @@ TabDeckStorage::TabDeckStorage(TabSupervisor *_tabSupervisor,
// Right side actions // Right side actions
aOpenRemoteDeck = new QAction(this); aOpenRemoteDeck = new QAction(this);
aOpenRemoteDeck->setIcon(QPixmap("theme:icons/pencil")); aOpenRemoteDeck->setIcon(themePixmap(QStringLiteral("icons/pencil")));
connect(aOpenRemoteDeck, &QAction::triggered, this, &TabDeckStorage::actOpenRemoteDeck); connect(aOpenRemoteDeck, &QAction::triggered, this, &TabDeckStorage::actOpenRemoteDeck);
aDownload = new QAction(this); aDownload = new QAction(this);
aDownload->setIcon(QPixmap("theme:icons/arrow_left_green")); aDownload->setIcon(themePixmap(QStringLiteral("icons/arrow_left_green")));
connect(aDownload, &QAction::triggered, this, &TabDeckStorage::actDownload); connect(aDownload, &QAction::triggered, this, &TabDeckStorage::actDownload);
aNewFolder = new QAction(this); aNewFolder = new QAction(this);
aNewFolder->setIcon(qApp->style()->standardIcon(QStyle::SP_FileDialogNewFolder)); aNewFolder->setIcon(qApp->style()->standardIcon(QStyle::SP_FileDialogNewFolder));
connect(aNewFolder, &QAction::triggered, this, &TabDeckStorage::actNewFolder); connect(aNewFolder, &QAction::triggered, this, &TabDeckStorage::actNewFolder);
aDeleteRemoteDeck = new QAction(this); aDeleteRemoteDeck = new QAction(this);
aDeleteRemoteDeck->setIcon(QPixmap("theme:icons/remove_row")); aDeleteRemoteDeck->setIcon(themePixmap(QStringLiteral("icons/remove_row")));
connect(aDeleteRemoteDeck, &QAction::triggered, this, &TabDeckStorage::actDeleteRemoteDeck); connect(aDeleteRemoteDeck, &QAction::triggered, this, &TabDeckStorage::actDeleteRemoteDeck);
// Add actions to toolbars // Add actions to toolbars

View file

@ -0,0 +1,261 @@
/**
* @file tab_developer.cpp
* @ingroup ServerTabs
*/
//! \todo Document this file.
#include "tab_developer.h"
#include <QCheckBox>
#include <QDateTime>
#include <QHBoxLayout>
#include <QHeaderView>
#include <QLabel>
#include <QPushButton>
#include <QSpinBox>
#include <QTableWidget>
#include <QTimer>
#include <QVBoxLayout>
#include <algorithm>
#include <libcockatrice/network/client/abstract/abstract_client.h>
#include <libcockatrice/protocol/pb/command_get_server_stats.pb.h>
#include <libcockatrice/protocol/pb/response_get_server_stats.pb.h>
#include <libcockatrice/protocol/pending_command.h>
static constexpr int DEFAULT_AUTO_REFRESH_INTERVAL_SECS = 30;
TabDeveloper::TabDeveloper(TabSupervisor *_tabSupervisor, AbstractClient *_client)
: Tab(_tabSupervisor), client(_client)
{
statsTable = new QTableWidget(0, 2);
statsTable->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
statsTable->setEditTriggers(QAbstractItemView::NoEditTriggers);
statsTable->setSelectionBehavior(QAbstractItemView::SelectRows);
statsTable->setSelectionMode(QAbstractItemView::SingleSelection);
statsTable->verticalHeader()->setVisible(false);
statsTable->horizontalHeader()->setSectionResizeMode(0, QHeaderView::Interactive);
statsTable->horizontalHeader()->setSectionResizeMode(1, QHeaderView::Interactive);
statsTable->horizontalHeader()->setStretchLastSection(true);
commandTable = new QTableWidget(0, 4);
commandTable->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
commandTable->setEditTriggers(QAbstractItemView::NoEditTriggers);
commandTable->setSelectionBehavior(QAbstractItemView::SelectRows);
commandTable->setSelectionMode(QAbstractItemView::SingleSelection);
commandTable->verticalHeader()->setVisible(false);
commandTable->horizontalHeader()->setSectionResizeMode(0, QHeaderView::Interactive);
commandTable->horizontalHeader()->setSectionResizeMode(1, QHeaderView::Interactive);
commandTable->horizontalHeader()->setSectionResizeMode(2, QHeaderView::Interactive);
commandTable->horizontalHeader()->setSectionResizeMode(3, QHeaderView::Interactive);
statusLabel = new QLabel;
autoRefreshCheckBox = new QCheckBox;
autoRefreshCheckBox->setChecked(false);
refreshIntervalSpinBox = new QSpinBox;
refreshIntervalSpinBox->setRange(5, 3600);
refreshIntervalSpinBox->setValue(DEFAULT_AUTO_REFRESH_INTERVAL_SECS);
refreshIntervalSpinBox->setEnabled(false);
autoRefreshTimer = new QTimer(this);
connect(autoRefreshTimer, &QTimer::timeout, this, &TabDeveloper::refreshClicked);
connect(autoRefreshCheckBox, &QCheckBox::toggled, this, &TabDeveloper::autoRefreshToggled);
connect(refreshIntervalSpinBox, QOverload<int>::of(&QSpinBox::valueChanged), this,
&TabDeveloper::refreshIntervalChanged);
refreshButton = new QPushButton;
refreshButton->setAutoDefault(true);
connect(refreshButton, &QPushButton::clicked, this, &TabDeveloper::refreshClicked);
auto *buttonLayout = new QHBoxLayout;
buttonLayout->addWidget(statusLabel, 1, Qt::AlignLeft);
buttonLayout->addWidget(autoRefreshCheckBox, 0, Qt::AlignRight);
buttonLayout->addWidget(refreshIntervalSpinBox, 0, Qt::AlignRight);
buttonLayout->addWidget(refreshButton, 0, Qt::AlignRight);
auto *tableLayout = new QHBoxLayout;
tableLayout->addWidget(statsTable, 1);
tableLayout->addWidget(commandTable, 2);
auto *mainLayout = new QVBoxLayout;
mainLayout->addLayout(tableLayout, 1);
mainLayout->addLayout(buttonLayout);
auto *central = new QWidget;
central->setLayout(mainLayout);
setCentralWidget(central);
retranslateUi();
}
void TabDeveloper::retranslateUi()
{
autoRefreshCheckBox->setText(tr("Auto-refresh"));
autoRefreshCheckBox->setToolTip(tr("Automatically request fresh server statistics at a fixed interval."));
refreshIntervalSpinBox->setSuffix(tr(" s"));
refreshIntervalSpinBox->setToolTip(tr("Seconds between automatic refreshes."));
refreshButton->setText(tr("Refresh server stats"));
statsTable->setHorizontalHeaderLabels(QString(tr("Statistic;Value")).split(";"));
commandTable->setHorizontalHeaderLabels(QString(tr("Command;Count;Total ms;Avg ms")).split(";"));
if (statsTable->rowCount() == 0) {
statusLabel->clear();
}
}
QString TabDeveloper::formatBytes(quint64 bytes)
{
const quint64 kib = 1024;
const quint64 mib = 1024 * kib;
const quint64 gib = 1024 * mib;
if (bytes >= gib) {
return tr("%1 GiB").arg(QString::number(bytes / static_cast<double>(gib), 'f', 2));
}
if (bytes >= mib) {
return tr("%1 MiB").arg(QString::number(bytes / static_cast<double>(mib), 'f', 2));
}
if (bytes >= kib) {
return tr("%1 KiB").arg(QString::number(bytes / static_cast<double>(kib), 'f', 2));
}
return tr("%1 bytes").arg(bytes);
}
QString TabDeveloper::formatDurationMs(qint64 ms)
{
if (ms >= 1000) {
return tr("%1 s").arg(QString::number(ms / 1000.0, 'f', 2));
}
return tr("%1 ms").arg(ms);
}
void TabDeveloper::appendStatRow(const QString &name, const QString &value)
{
const int row = statsTable->rowCount();
statsTable->insertRow(row);
statsTable->setItem(row, 0, new QTableWidgetItem(name));
statsTable->setItem(row, 1, new QTableWidgetItem(value));
}
void TabDeveloper::appendSeparatorRow(const QString &sectionTitle)
{
const int row = statsTable->rowCount();
statsTable->insertRow(row);
auto *labelItem = new QTableWidgetItem(sectionTitle);
auto font = labelItem->font();
font.setBold(true);
labelItem->setFont(font);
labelItem->setFlags(labelItem->flags() & ~Qt::ItemIsSelectable);
statsTable->setItem(row, 0, labelItem);
statsTable->setItem(row, 1, new QTableWidgetItem(QString()));
}
void TabDeveloper::refreshClicked()
{
if (requestPending) {
return;
}
requestPending = true;
Command_GetServerStats cmd;
PendingCommand *pend = client->prepareDeveloperCommand(cmd);
connect(pend, &PendingCommand::finished, this, &TabDeveloper::serverStatsResponse);
client->sendCommand(pend);
}
void TabDeveloper::autoRefreshToggled(bool checked)
{
refreshIntervalSpinBox->setEnabled(checked);
if (checked) {
refreshIntervalChanged();
refreshClicked();
} else {
autoRefreshTimer->stop();
}
}
void TabDeveloper::refreshIntervalChanged()
{
if (autoRefreshCheckBox->isChecked()) {
autoRefreshTimer->start(refreshIntervalSpinBox->value() * 1000);
}
}
void TabDeveloper::serverStatsResponse(const Response &resp)
{
requestPending = false;
if (resp.response_code() != Response::RespOk) {
statusLabel->setText(tr("No server statistics available yet."));
return;
}
const Response_GetServerStats &response = resp.GetExtension(Response_GetServerStats::ext);
statsTable->setRowCount(0);
// Overview section
appendStatRow(tr("Registered users online"), QString::number(response.users_count()));
appendStatRow(tr("Moderators online"), QString::number(response.mods_count()));
appendStatRow(tr("Games running"), QString::number(response.games_count()));
appendStatRow(tr("Traffic sent (last tick)"), formatBytes(response.tx_bytes()));
appendStatRow(tr("Traffic received (last tick)"), formatBytes(response.rx_bytes()));
const qint64 uptime = static_cast<qint64>(response.uptime_secs());
const int days = static_cast<int>(uptime / 86400);
const int hours = static_cast<int>((uptime % 86400) / 3600);
const int minutes = static_cast<int>((uptime % 3600) / 60);
appendStatRow(tr("Server uptime"), days > 0 ? tr("%1d %2h %3m").arg(days).arg(hours).arg(minutes)
: tr("%1h %2m").arg(hours).arg(minutes));
const QDateTime snapshotTime = QDateTime::fromSecsSinceEpoch(static_cast<qint64>(response.timest()));
appendStatRow(tr("Snapshot taken"), snapshotTime.toLocalTime().toString("yyyy-MM-dd HH:mm"));
// Live metrics section
appendSeparatorRow(tr("Live Metrics"));
appendStatRow(tr("Cards in live games"), QString::number(response.cards_in_games()));
appendStatRow(tr("Total commands processed"), QString::number(response.total_commands()));
if (response.total_commands() > 0) {
const double avgMs = static_cast<double>(response.total_command_time_ms()) / response.total_commands();
appendStatRow(tr("Avg command time"), QString::number(avgMs, 'f', 2) + " ms");
}
appendStatRow(tr("Active command types"), QString::number(response.active_command_types()));
appendStatRow(tr("Event loop stalls"), QString::number(response.eventloop_stalls_total()));
appendStatRow(tr("Last stall overshoot"), formatDurationMs(response.eventloop_last_stall_ms()));
appendStatRow(tr("Worst stall overshoot"), formatDurationMs(response.eventloop_max_stall_ms()));
if (response.game_start_count() > 0) {
appendStatRow(tr("Game starts"), QString::number(response.game_start_count()));
const double avgStartMs = static_cast<double>(response.game_start_total_ms()) / response.game_start_count();
appendStatRow(tr("Avg game start time"), QString::number(avgStartMs, 'f', 1) + " ms");
}
// Per-command breakdown table
QList<CommandStats> sortedStats(response.command_stats().begin(), response.command_stats().end());
std::sort(sortedStats.begin(), sortedStats.end(),
[](const auto &a, const auto &b) { return a.total_ms() > b.total_ms(); });
commandTable->setRowCount(0);
for (const auto &cs : sortedStats) {
const int row = commandTable->rowCount();
commandTable->insertRow(row);
commandTable->setItem(row, 0, new QTableWidgetItem(QString::fromStdString(cs.command_name())));
auto *countItem = new QTableWidgetItem(QString::number(cs.count()));
countItem->setTextAlignment(Qt::AlignRight | Qt::AlignVCenter);
commandTable->setItem(row, 1, countItem);
auto *totalItem = new QTableWidgetItem(QString::number(cs.total_ms()));
totalItem->setTextAlignment(Qt::AlignRight | Qt::AlignVCenter);
commandTable->setItem(row, 2, totalItem);
const double avg = cs.count() > 0 ? static_cast<double>(cs.total_ms()) / cs.count() : 0.0;
auto *avgItem = new QTableWidgetItem(QString::number(avg, 'f', 2));
avgItem->setTextAlignment(Qt::AlignRight | Qt::AlignVCenter);
commandTable->setItem(row, 3, avgItem);
}
commandTable->resizeColumnsToContents();
statsTable->resizeColumnsToContents();
commandTable->resizeColumnsToContents();
statusLabel->setText(tr("Updated %1").arg(QDateTime::currentDateTime().toString("yyyy-MM-dd HH:mm")));
}

View file

@ -0,0 +1,55 @@
/**
* @file tab_developer.h
* @ingroup ServerTabs
*/
//! \todo Document this file.
#ifndef TAB_DEVELOPER_H
#define TAB_DEVELOPER_H
#include "tab.h"
class AbstractClient;
class QCheckBox;
class QLabel;
class QPushButton;
class QSpinBox;
class QTableWidget;
class QTimer;
class Response;
class TabDeveloper : public Tab
{
Q_OBJECT
private:
AbstractClient *client;
QTableWidget *statsTable;
QTableWidget *commandTable;
QPushButton *refreshButton;
QLabel *statusLabel;
QCheckBox *autoRefreshCheckBox;
QSpinBox *refreshIntervalSpinBox;
QTimer *autoRefreshTimer;
bool requestPending = false;
void appendStatRow(const QString &name, const QString &value);
void appendSeparatorRow(const QString &sectionTitle);
static QString formatBytes(quint64 bytes);
static QString formatDurationMs(qint64 ms);
private slots:
void refreshClicked();
void serverStatsResponse(const Response &resp);
void autoRefreshToggled(bool checked);
void refreshIntervalChanged();
public:
explicit TabDeveloper(TabSupervisor *_tabSupervisor, AbstractClient *_client);
void retranslateUi() override;
[[nodiscard]] QString getTabText() const override
{
return tr("Developer");
}
};
#endif

View file

@ -266,6 +266,10 @@ void TabGame::resetChatAndPhase()
// reset phase markers // reset phase markers
game->getGameState()->setCurrentPhase(-1); game->getGameState()->setCurrentPhase(-1);
// reset spectator state so the replay can rebuild it from the start
game->getPlayerManager()->clearSpectators();
playerListWidget->clearSpectators();
} }
void TabGame::emitUserEvent() void TabGame::emitUserEvent()

View file

@ -19,7 +19,8 @@
#include <libcockatrice/protocol/pending_command.h> #include <libcockatrice/protocol/pending_command.h>
#include <libcockatrice/utility/string_limits.h> #include <libcockatrice/utility/string_limits.h>
TabLog::TabLog(TabSupervisor *_tabSupervisor, AbstractClient *_client) : Tab(_tabSupervisor), client(_client) TabLog::TabLog(TabSupervisor *_tabSupervisor, AbstractClient *_client, bool _canUseDeveloperCommands)
: Tab(_tabSupervisor), client(_client), canUseDeveloperCommands(_canUseDeveloperCommands)
{ {
roomTable = new QTableWidget(); roomTable = new QTableWidget();
roomTable->setColumnCount(6); roomTable->setColumnCount(6);
@ -80,7 +81,9 @@ void TabLog::getClicked()
if (!mainRoom->isChecked() && !gameRoom->isChecked() && !privateChat->isChecked()) { if (!mainRoom->isChecked() && !gameRoom->isChecked() && !privateChat->isChecked()) {
mainRoom->setChecked(true); mainRoom->setChecked(true);
gameRoom->setChecked(true); gameRoom->setChecked(true);
privateChat->setChecked(true); if (!canUseDeveloperCommands) {
privateChat->setChecked(true);
}
} }
if (maximumResults->value() == 0) { if (maximumResults->value() == 0) {
@ -117,7 +120,15 @@ void TabLog::getClicked()
}; };
cmd.set_date_range(dateRange); cmd.set_date_range(dateRange);
cmd.set_maximum_results(maximumResults->value()); cmd.set_maximum_results(maximumResults->value());
PendingCommand *pend = client->prepareModeratorCommand(cmd);
PendingCommand *pend;
if (canUseDeveloperCommands) {
// Developers query logs through the developer command family.
pend = client->prepareDeveloperCommand(cmd);
} else {
pend = client->prepareModeratorCommand(cmd);
}
connect(pend, &PendingCommand::finished, this, &TabLog::viewLogHistory_processResponse); connect(pend, &PendingCommand::finished, this, &TabLog::viewLogHistory_processResponse);
client->sendCommand(pend); client->sendCommand(pend);
} }
@ -171,6 +182,14 @@ void TabLog::createDock()
mainRoom = new QCheckBox(tr("Main Room")); mainRoom = new QCheckBox(tr("Main Room"));
gameRoom = new QCheckBox(tr("Game Room")); gameRoom = new QCheckBox(tr("Game Room"));
privateChat = new QCheckBox(tr("Private Chat")); privateChat = new QCheckBox(tr("Private Chat"));
if (canUseDeveloperCommands) {
// Developers cannot query private conversations.
privateChat->setVisible(false);
// The developer family ignores the IP filter server-side, so showing
// the field would silently unfilter the result by it. Hide it.
labelFindIPAddress->setVisible(false);
findIPAddress->setVisible(false);
}
pastDays = new QRadioButton(tr("Past X Days: ")); pastDays = new QRadioButton(tr("Past X Days: "));
today = new QRadioButton(tr("Today")); today = new QRadioButton(tr("Today"));

View file

@ -33,6 +33,7 @@ class TabLog : public Tab
Q_OBJECT Q_OBJECT
private: private:
AbstractClient *client; AbstractClient *client;
bool canUseDeveloperCommands;
QLabel *labelFindUserName, *labelFindIPAddress, *labelFindGameName, *labelFindGameID, *labelMessage, *labelMaximum, QLabel *labelFindUserName, *labelFindIPAddress, *labelFindGameName, *labelFindGameID, *labelMessage, *labelMaximum,
*labelDescription; *labelDescription;
LineEditUnfocusable *findUsername, *findIPAddress, *findGameName, *findGameID, *findMessage; LineEditUnfocusable *findUsername, *findIPAddress, *findGameName, *findGameID, *findMessage;
@ -58,7 +59,7 @@ private slots:
void restartLayout(); void restartLayout();
public: public:
TabLog(TabSupervisor *_tabSupervisor, AbstractClient *_client); TabLog(TabSupervisor *_tabSupervisor, AbstractClient *_client, bool _canUseDeveloperCommands = false);
~TabLog() override; ~TabLog() override;
void retranslateUi() override; void retranslateUi() override;
[[nodiscard]] QString getTabText() const override [[nodiscard]] QString getTabText() const override

View file

@ -98,6 +98,12 @@ void TabMessage::closeEvent(QCloseEvent *event)
void TabMessage::sendPrivateMessage(const QString &text) void TabMessage::sendPrivateMessage(const QString &text)
{ {
if (tabSupervisor->getUserListManager()->isUserIgnored(getUserName())) {
chatView->appendMessage(tr("You have ignored %1; your messages are not delivered.")
.arg(QString::fromStdString(otherUserInfo->name())));
return;
}
Command_Message cmd; Command_Message cmd;
cmd.set_user_name(otherUserInfo->name()); cmd.set_user_name(otherUserInfo->name());
cmd.set_message(text.toStdString()); cmd.set_message(text.toStdString());

View file

@ -381,6 +381,9 @@ void TabModeration::moderatorLoginsResponse(const Response &response)
if (login.user_level() & ServerInfo_User::IsAdmin) { if (login.user_level() & ServerInfo_User::IsAdmin) {
levels << tr("Admin"); levels << tr("Admin");
} }
if (login.user_level() & ServerInfo_User::IsDeveloper) {
levels << tr("Developer");
}
if (login.user_level() & ServerInfo_User::IsModerator) { if (login.user_level() & ServerInfo_User::IsModerator) {
levels << tr("Moderator"); levels << tr("Moderator");
} }

View file

@ -1,6 +1,7 @@
#include "tab_replays.h" #include "tab_replays.h"
#include "../../../client/settings/cache_settings.h" #include "../../../client/settings/cache_settings.h"
#include "../../pixel_map_generator.h"
#include "../interface/widgets/server/remote/remote_replay_list_tree_widget.h" #include "../interface/widgets/server/remote/remote_replay_list_tree_widget.h"
#include "tab_game.h" #include "tab_game.h"
@ -102,17 +103,17 @@ QGroupBox *TabReplays::createLeftLayout()
// Left side actions // Left side actions
aOpenLocalReplay = new QAction(this); aOpenLocalReplay = new QAction(this);
aOpenLocalReplay->setIcon(QPixmap("theme:icons/view")); aOpenLocalReplay->setIcon(themePixmap(QStringLiteral("icons/view")));
connect(aOpenLocalReplay, &QAction::triggered, this, &TabReplays::actOpenLocalReplay); connect(aOpenLocalReplay, &QAction::triggered, this, &TabReplays::actOpenLocalReplay);
connect(localDirView, &QTreeView::doubleClicked, this, &TabReplays::actOpenLocalReplay); connect(localDirView, &QTreeView::doubleClicked, this, &TabReplays::actOpenLocalReplay);
aRenameLocal = new QAction(this); aRenameLocal = new QAction(this);
aRenameLocal->setIcon(QPixmap("theme:icons/rename")); aRenameLocal->setIcon(themePixmap(QStringLiteral("icons/rename")));
connect(aRenameLocal, &QAction::triggered, this, &TabReplays::actRenameLocal); connect(aRenameLocal, &QAction::triggered, this, &TabReplays::actRenameLocal);
aNewLocalFolder = new QAction(this); aNewLocalFolder = new QAction(this);
aNewLocalFolder->setIcon(qApp->style()->standardIcon(QStyle::SP_FileDialogNewFolder)); aNewLocalFolder->setIcon(qApp->style()->standardIcon(QStyle::SP_FileDialogNewFolder));
connect(aNewLocalFolder, &QAction::triggered, this, &TabReplays::actNewLocalFolder); connect(aNewLocalFolder, &QAction::triggered, this, &TabReplays::actNewLocalFolder);
aDeleteLocalReplay = new QAction(this); aDeleteLocalReplay = new QAction(this);
aDeleteLocalReplay->setIcon(QPixmap("theme:icons/remove_row")); aDeleteLocalReplay->setIcon(themePixmap(QStringLiteral("icons/remove_row")));
connect(aDeleteLocalReplay, &QAction::triggered, this, &TabReplays::actDeleteLocalReplay); connect(aDeleteLocalReplay, &QAction::triggered, this, &TabReplays::actDeleteLocalReplay);
aOpenReplaysFolder = new QAction(this); aOpenReplaysFolder = new QAction(this);
@ -164,24 +165,24 @@ QGroupBox *TabReplays::createRightLayout()
// Right side actions // Right side actions
aOpenRemoteReplay = new QAction(this); aOpenRemoteReplay = new QAction(this);
aOpenRemoteReplay->setIcon(QPixmap("theme:icons/view")); aOpenRemoteReplay->setIcon(themePixmap(QStringLiteral("icons/view")));
connect(aOpenRemoteReplay, &QAction::triggered, this, &TabReplays::actOpenRemoteReplay); connect(aOpenRemoteReplay, &QAction::triggered, this, &TabReplays::actOpenRemoteReplay);
connect(serverDirView, &QTreeView::doubleClicked, this, &TabReplays::actOpenRemoteReplay); connect(serverDirView, &QTreeView::doubleClicked, this, &TabReplays::actOpenRemoteReplay);
aDownload = new QAction(this); aDownload = new QAction(this);
aDownload->setIcon(QPixmap("theme:icons/arrow_left_green")); aDownload->setIcon(themePixmap(QStringLiteral("icons/arrow_left_green")));
connect(aDownload, &QAction::triggered, this, &TabReplays::actDownload); connect(aDownload, &QAction::triggered, this, &TabReplays::actDownload);
aKeep = new QAction(this); aKeep = new QAction(this);
aKeep->setIcon(QPixmap("theme:icons/lock")); aKeep->setIcon(themePixmap(QStringLiteral("icons/lock")));
connect(aKeep, &QAction::triggered, this, &TabReplays::actKeepRemoteReplay); connect(aKeep, &QAction::triggered, this, &TabReplays::actKeepRemoteReplay);
aDeleteRemoteReplay = new QAction(this); aDeleteRemoteReplay = new QAction(this);
aDeleteRemoteReplay->setIcon(QPixmap("theme:icons/remove_row")); aDeleteRemoteReplay->setIcon(themePixmap(QStringLiteral("icons/remove_row")));
connect(aDeleteRemoteReplay, &QAction::triggered, this, &TabReplays::actDeleteRemoteReplay); connect(aDeleteRemoteReplay, &QAction::triggered, this, &TabReplays::actDeleteRemoteReplay);
aGetReplayCode = new QAction(this); aGetReplayCode = new QAction(this);
aGetReplayCode->setIcon(QPixmap("theme:icons/share")); aGetReplayCode->setIcon(themePixmap(QStringLiteral("icons/share")));
connect(aGetReplayCode, &QAction::triggered, this, &TabReplays::actGetReplayCode); connect(aGetReplayCode, &QAction::triggered, this, &TabReplays::actGetReplayCode);
aSubmitReplayCode = new QAction(this); aSubmitReplayCode = new QAction(this);
aSubmitReplayCode->setIcon(QPixmap("theme:icons/search")); aSubmitReplayCode->setIcon(themePixmap(QStringLiteral("icons/search")));
connect(aSubmitReplayCode, &QAction::triggered, this, &TabReplays::actSubmitReplayCode); connect(aSubmitReplayCode, &QAction::triggered, this, &TabReplays::actSubmitReplayCode);
// Add actions to toolbars // Add actions to toolbars

View file

@ -2,6 +2,7 @@
#include "../../../client/settings/cache_settings.h" #include "../../../client/settings/cache_settings.h"
#include "../../../client/settings/shortcuts_settings.h" #include "../../../client/settings/shortcuts_settings.h"
#include "../../pixel_map_generator.h"
#include "../interface/widgets/dialogs/dlg_settings.h" #include "../interface/widgets/dialogs/dlg_settings.h"
#include "../interface/widgets/server/chat_view/chat_view.h" #include "../interface/widgets/server/chat_view/chat_view.h"
#include "../interface/widgets/server/game_link.h" #include "../interface/widgets/server/game_link.h"
@ -98,7 +99,7 @@ TabRoom::TabRoom(TabSupervisor *_tabSupervisor,
connect(aOpenChatSettings, &QAction::triggered, this, &TabRoom::actOpenChatSettings); connect(aOpenChatSettings, &QAction::triggered, this, &TabRoom::actOpenChatSettings);
auto *chatSettingsButton = new QToolButton; auto *chatSettingsButton = new QToolButton;
chatSettingsButton->setIcon(QPixmap("theme:icons/settings")); chatSettingsButton->setIcon(themePixmap(QStringLiteral("icons/settings")));
chatSettingsButton->setMenu(chatSettingsMenu); chatSettingsButton->setMenu(chatSettingsMenu);
chatSettingsButton->setPopupMode(QToolButton::InstantPopup); chatSettingsButton->setPopupMode(QToolButton::InstantPopup);

View file

@ -15,6 +15,7 @@
#include "tab_card_art_rules.h" #include "tab_card_art_rules.h"
#include "tab_deck_editor.h" #include "tab_deck_editor.h"
#include "tab_deck_storage.h" #include "tab_deck_storage.h"
#include "tab_developer.h"
#include "tab_game.h" #include "tab_game.h"
#include "tab_home.h" #include "tab_home.h"
#include "tab_logs.h" #include "tab_logs.h"
@ -119,7 +120,7 @@ TabSupervisor::TabSupervisor(AbstractClient *_client, QMenu *tabsMenu, QWidget *
: QTabWidget(parent), userInfo(nullptr), client(_client), tabsMenu(tabsMenu), tabHome(nullptr), : QTabWidget(parent), userInfo(nullptr), client(_client), tabsMenu(tabsMenu), tabHome(nullptr),
tabVisualDeckStorage(nullptr), tabServer(nullptr), tabAccount(nullptr), tabDeckStorage(nullptr), tabVisualDeckStorage(nullptr), tabServer(nullptr), tabAccount(nullptr), tabDeckStorage(nullptr),
tabReplays(nullptr), tabAdmin(nullptr), tabCardArtRules(nullptr), tabLog(nullptr), tabReport(nullptr), tabReplays(nullptr), tabAdmin(nullptr), tabCardArtRules(nullptr), tabLog(nullptr), tabReport(nullptr),
tabModeration(nullptr), isLocalGame(false) tabModeration(nullptr), tabDeveloper(nullptr), isLocalGame(false)
{ {
setElideMode(Qt::ElideRight); setElideMode(Qt::ElideRight);
setMovable(true); setMovable(true);
@ -205,6 +206,10 @@ TabSupervisor::TabSupervisor(AbstractClient *_client, QMenu *tabsMenu, QWidget *
aTabModeration->setCheckable(true); aTabModeration->setCheckable(true);
connect(aTabModeration, &QAction::triggered, this, &TabSupervisor::actTabModeration); connect(aTabModeration, &QAction::triggered, this, &TabSupervisor::actTabModeration);
aTabDeveloper = new QAction(this);
aTabDeveloper->setCheckable(true);
connect(aTabDeveloper, &QAction::triggered, this, &TabSupervisor::actTabDeveloper);
connect(&SettingsCache::instance().shortcuts(), &ShortcutsSettings::shortCutChanged, this, connect(&SettingsCache::instance().shortcuts(), &ShortcutsSettings::shortCutChanged, this,
&TabSupervisor::refreshShortcuts); &TabSupervisor::refreshShortcuts);
refreshShortcuts(); refreshShortcuts();
@ -247,6 +252,7 @@ void TabSupervisor::retranslateUi()
aTabReport->setText(tr("Report Queue")); aTabReport->setText(tr("Report Queue"));
aTabModeration->setText(tr("Moderation")); aTabModeration->setText(tr("Moderation"));
aTabCardArtRules->setText(tr("Card Art Rules")); aTabCardArtRules->setText(tr("Card Art Rules"));
aTabDeveloper->setText(tr("Developer"));
// tabs // tabs
QList<Tab *> tabs; QList<Tab *> tabs;
@ -259,6 +265,7 @@ void TabSupervisor::retranslateUi()
tabs.append(tabReport); tabs.append(tabReport);
tabs.append(tabModeration); tabs.append(tabModeration);
tabs.append(tabCardArtRules); tabs.append(tabCardArtRules);
tabs.append(tabDeveloper);
QMapIterator<int, TabRoom *> roomIterator(roomTabs); QMapIterator<int, TabRoom *> roomIterator(roomTabs);
while (roomIterator.hasNext()) { while (roomIterator.hasNext()) {
tabs.append(roomIterator.next().value()); tabs.append(roomIterator.next().value());
@ -528,6 +535,19 @@ void TabSupervisor::start(const ServerInfo_User &_userInfo)
} }
} }
if (userInfo->user_level() & ServerInfo_User::IsDeveloper) {
tabsMenu->addSeparator();
tabsMenu->addAction(aTabDeveloper);
// Developers without moderation rights get log access through their
// own role. Moderators already have the Logs entry from above.
if (!(userInfo->user_level() & ServerInfo_User::IsModerator)) {
tabsMenu->addAction(aTabLog);
if (SettingsCache::instance().tabs().getTabLogOpen()) {
openTabLog();
}
}
}
retranslateUi(); retranslateUi();
} }
@ -540,6 +560,7 @@ void TabSupervisor::startLocal(const QList<AbstractClient *> &_clients)
tabLog = nullptr; tabLog = nullptr;
tabReport = nullptr; tabReport = nullptr;
tabModeration = nullptr; tabModeration = nullptr;
tabDeveloper = nullptr;
isLocalGame = true; isLocalGame = true;
userInfo = new ServerInfo_User; userInfo = new ServerInfo_User;
localClients = _clients; localClients = _clients;
@ -590,6 +611,9 @@ void TabSupervisor::stop()
if (tabCardArtRules) { if (tabCardArtRules) {
tabCardArtRules->close(); tabCardArtRules->close();
} }
if (tabDeveloper) {
tabDeveloper->close();
}
} }
QList<Tab *> tabsToDelete; QList<Tab *> tabsToDelete;
@ -819,7 +843,13 @@ void TabSupervisor::actTabLog(bool checked)
void TabSupervisor::openTabLog() void TabSupervisor::openTabLog()
{ {
tabLog = new TabLog(this, client); // Developers query logs through the developer command family, so tell the
// tab which family to use. The moderator family is strictly stronger, so a
// moderator who also holds the developer bit keeps the moderator path — the
// developer bit only selects the (narrowed) developer family on its own.
const bool useDeveloperCommands = (userInfo->user_level() & ServerInfo_User::IsDeveloper) &&
!(userInfo->user_level() & ServerInfo_User::IsModerator);
tabLog = new TabLog(this, client, useDeveloperCommands);
myAddTab(tabLog, aTabLog); myAddTab(tabLog, aTabLog);
connect(tabLog, &QObject::destroyed, this, [this] { connect(tabLog, &QObject::destroyed, this, [this] {
tabLog = nullptr; tabLog = nullptr;
@ -881,6 +911,27 @@ void TabSupervisor::openTabModeration(const QString &userName)
aTabModeration->setChecked(true); aTabModeration->setChecked(true);
} }
void TabSupervisor::actTabDeveloper(bool checked)
{
if (checked && !tabDeveloper) {
openTabDeveloper();
setCurrentWidget(tabDeveloper);
} else if (!checked && tabDeveloper) {
tabDeveloper->closeRequest();
}
}
void TabSupervisor::openTabDeveloper()
{
tabDeveloper = new TabDeveloper(this, client);
myAddTab(tabDeveloper, aTabDeveloper);
connect(tabDeveloper, &QObject::destroyed, this, [this] {
tabDeveloper = nullptr;
aTabDeveloper->setChecked(false);
});
aTabDeveloper->setChecked(true);
}
void TabSupervisor::updatePingTime(int value, int max) void TabSupervisor::updatePingTime(int value, int max)
{ {
if (!tabServer) { if (!tabServer) {
@ -1063,6 +1114,13 @@ TabMessage *TabSupervisor::addMessageTab(const QString &receiverName, bool focus
return tab; return tab;
} }
if (focus && userListManager->isUserIgnored(receiverName)) {
QMessageBox::information(
this, tr("Ignored user"),
tr("You have ignored %1. Remove them from your ignore list to open a private chat.").arg(receiverName));
return nullptr;
}
tab = new TabMessage(this, client, *userInfo, otherUser, userOnline); tab = new TabMessage(this, client, *userInfo, otherUser, userOnline);
connect(tab, &TabMessage::talkClosing, this, &TabSupervisor::talkLeft); connect(tab, &TabMessage::talkClosing, this, &TabSupervisor::talkLeft);
connect(tab, &TabMessage::maximizeClient, this, &TabSupervisor::maximizeMainWindow); connect(tab, &TabMessage::maximizeClient, this, &TabSupervisor::maximizeMainWindow);
@ -1242,7 +1300,7 @@ void TabSupervisor::tabUserEvent(bool globalEvent)
auto *tab = static_cast<Tab *>(sender()); auto *tab = static_cast<Tab *>(sender());
if (tab != currentWidget()) { if (tab != currentWidget()) {
tab->setContentsChanged(true); tab->setContentsChanged(true);
setTabIcon(indexOf(tab), QPixmap("theme:icons/tab_changed")); setTabIcon(indexOf(tab), themePixmap(QStringLiteral("icons/tab_changed")));
} }
if (globalEvent && SettingsCache::instance().userInterface().getNotificationsEnabled()) { if (globalEvent && SettingsCache::instance().userInterface().getNotificationsEnabled()) {
QApplication::alert(this); QApplication::alert(this);
@ -1277,7 +1335,21 @@ void TabSupervisor::processGameEventContainer(const GameEventContainer &cont)
void TabSupervisor::processUserMessageEvent(const Event_UserMessage &event) void TabSupervisor::processUserMessageEvent(const Event_UserMessage &event)
{ {
// "Ignore all private messages" silences every PM, including messages to
// already-open tabs — unlike the unregistered/non-buddy filters below,
// which only apply when creating a new tab. Messages from moderators/admins
// are exempt to ensure warnings still reach users.
QString senderName = QString::fromStdString(event.sender_name()); QString senderName = QString::fromStdString(event.sender_name());
if (SettingsCache::instance().chat().getIgnoreAllPrivateMessages()) {
const ServerInfo_User *onlineUserInfo = userListManager->getOnlineUser(senderName);
if (!onlineUserInfo) {
return;
}
const UserLevelFlags userLevel(onlineUserInfo->user_level());
if (!userLevel.testFlag(ServerInfo_User::IsModerator) && !userLevel.testFlag(ServerInfo_User::IsAdmin)) {
return;
}
}
TabMessage *tab = messageTabs.value(senderName); TabMessage *tab = messageTabs.value(senderName);
if (!tab) { if (!tab) {
tab = messageTabs.value(QString::fromStdString(event.receiver_name())); tab = messageTabs.value(QString::fromStdString(event.receiver_name()));

View file

@ -45,6 +45,7 @@ class TabReport;
class TabModeration; class TabModeration;
class TabAccount; class TabAccount;
class TabDeckEditor; class TabDeckEditor;
class TabDeveloper;
class TabLog; class TabLog;
class RoomEvent; class RoomEvent;
class GameEventContainer; class GameEventContainer;
@ -108,6 +109,7 @@ private:
TabLog *tabLog; TabLog *tabLog;
TabReport *tabReport; TabReport *tabReport;
TabModeration *tabModeration; TabModeration *tabModeration;
TabDeveloper *tabDeveloper;
QMap<int, TabRoom *> roomTabs; QMap<int, TabRoom *> roomTabs;
QMap<int, TabGame *> gameTabs; QMap<int, TabGame *> gameTabs;
QList<TabGame *> replayTabs; QList<TabGame *> replayTabs;
@ -117,7 +119,7 @@ private:
QAction *aTabHome, *aTabDeckEditor, *aTabVisualDeckEditor, *aTabEdhRec, *aTabArchidekt, *aTabVisualDeckStorage, QAction *aTabHome, *aTabDeckEditor, *aTabVisualDeckEditor, *aTabEdhRec, *aTabArchidekt, *aTabVisualDeckStorage,
*aTabVisualDatabaseDisplay, *aTabServer, *aTabAccount, *aTabDeckStorage, *aTabReplays, *aTabAdmin, *aTabVisualDatabaseDisplay, *aTabServer, *aTabAccount, *aTabDeckStorage, *aTabReplays, *aTabAdmin,
*aTabCardArtRules, *aTabLog, *aTabReport, *aTabModeration; *aTabCardArtRules, *aTabLog, *aTabReport, *aTabModeration, *aTabDeveloper;
int myAddTab(Tab *tab, QAction *manager = nullptr); int myAddTab(Tab *tab, QAction *manager = nullptr);
void addCloseButtonToTab(Tab *tab, int tabIndex, QAction *manager); void addCloseButtonToTab(Tab *tab, int tabIndex, QAction *manager);
@ -207,6 +209,7 @@ private slots:
void actTabLog(bool checked); void actTabLog(bool checked);
void actTabReport(bool checked); void actTabReport(bool checked);
void actTabModeration(bool checked); void actTabModeration(bool checked);
void actTabDeveloper(bool checked);
void openTabVisualDeckStorage(); void openTabVisualDeckStorage();
void openTabHome(); void openTabHome();
@ -218,6 +221,7 @@ private slots:
void openTabCardArtRules(); void openTabCardArtRules();
void openTabLog(); void openTabLog();
void openTabReport(); void openTabReport();
void openTabDeveloper();
void updateCurrent(int index); void updateCurrent(int index);
void updatePingTime(int value, int max); void updatePingTime(int value, int max);

View file

@ -2,6 +2,7 @@
#include "../../../client/settings/cache_settings.h" #include "../../../client/settings/cache_settings.h"
#include "../../../client/settings/shortcuts_settings.h" #include "../../../client/settings/shortcuts_settings.h"
#include "../../pixel_map_generator.h"
#include <QHBoxLayout> #include <QHBoxLayout>
#include <QKeyEvent> #include <QKeyEvent>
@ -14,8 +15,8 @@ SequenceEdit::SequenceEdit(const QString &_shortcutName, QWidget *parent) : QWid
defaultButton = new QPushButton("", this); defaultButton = new QPushButton("", this);
lineEdit->setMinimumWidth(70); lineEdit->setMinimumWidth(70);
clearButton->setIcon(QPixmap("theme:icons/clearsearch")); clearButton->setIcon(themePixmap(QStringLiteral("icons/clearsearch")));
defaultButton->setIcon(QPixmap("theme:icons/update")); defaultButton->setIcon(themePixmap(QStringLiteral("icons/update")));
auto *layout = new QHBoxLayout(this); auto *layout = new QHBoxLayout(this);
layout->setContentsMargins(0, 0, 0, 0); layout->setContentsMargins(0, 0, 0, 0);

View file

@ -1,5 +1,6 @@
#include "visual_database_display_filter_toolbar_widget.h" #include "visual_database_display_filter_toolbar_widget.h"
#include "../../pixel_map_generator.h"
#include "../deck_editor/card_database_view.h" #include "../deck_editor/card_database_view.h"
#include "visual_database_display_widget.h" #include "visual_database_display_widget.h"
@ -60,22 +61,22 @@ VisualDatabaseDisplayFilterToolbarWidget::VisualDatabaseDisplayFilterToolbarWidg
}); });
quickFilterSaveLoadWidget = new SettingsButtonWidget(this); quickFilterSaveLoadWidget = new SettingsButtonWidget(this);
quickFilterSaveLoadWidget->setButtonIcon(QPixmap("theme:icons/floppy_disk")); quickFilterSaveLoadWidget->setButtonIcon(themePixmap(QStringLiteral("icons/floppy_disk")));
quickFilterNameWidget = new SettingsButtonWidget(this); quickFilterNameWidget = new SettingsButtonWidget(this);
quickFilterNameWidget->setButtonIcon(QPixmap("theme:icons/pen_to_square")); quickFilterNameWidget->setButtonIcon(themePixmap(QStringLiteral("icons/pen_to_square")));
quickFilterMainTypeWidget = new SettingsButtonWidget(this); quickFilterMainTypeWidget = new SettingsButtonWidget(this);
quickFilterMainTypeWidget->setButtonIcon(QPixmap("theme:icons/circle_half_stroke")); quickFilterMainTypeWidget->setButtonIcon(themePixmap(QStringLiteral("icons/circle_half_stroke")));
quickFilterSubTypeWidget = new SettingsButtonWidget(this); quickFilterSubTypeWidget = new SettingsButtonWidget(this);
quickFilterSubTypeWidget->setButtonIcon(QPixmap("theme:icons/dragon")); quickFilterSubTypeWidget->setButtonIcon(themePixmap(QStringLiteral("icons/dragon")));
quickFilterSetWidget = new SettingsButtonWidget(this); quickFilterSetWidget = new SettingsButtonWidget(this);
quickFilterSetWidget->setButtonIcon(QPixmap("theme:icons/scroll")); quickFilterSetWidget->setButtonIcon(themePixmap(QStringLiteral("icons/scroll")));
quickFilterFormatLegalityWidget = new SettingsButtonWidget(this); quickFilterFormatLegalityWidget = new SettingsButtonWidget(this);
quickFilterFormatLegalityWidget->setButtonIcon(QPixmap("theme:icons/scale_balanced")); quickFilterFormatLegalityWidget->setButtonIcon(themePixmap(QStringLiteral("icons/scale_balanced")));
retranslateUi(); retranslateUi();
} }

View file

@ -66,7 +66,7 @@ VisualDatabaseDisplayWidget::VisualDatabaseDisplayWidget(QWidget *parent,
searchEdit->setPlaceholderText(tr("Search by card name (or search expressions)")); searchEdit->setPlaceholderText(tr("Search by card name (or search expressions)"));
searchEdit->setClearButtonEnabled(true); searchEdit->setClearButtonEnabled(true);
searchEdit->addAction(loadColorAdjustedPixmap("theme:icons/search"), QLineEdit::LeadingPosition); 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); }); connect(help, &QAction::triggered, this, [this] { createSearchSyntaxHelpWindow(searchEdit); });
setFocusProxy(searchEdit); setFocusProxy(searchEdit);
@ -121,7 +121,7 @@ VisualDatabaseDisplayWidget::VisualDatabaseDisplayWidget(QWidget *parent,
clearFilterWidget = new QToolButton(); clearFilterWidget = new QToolButton();
clearFilterWidget->setFixedSize(32, 32); clearFilterWidget->setFixedSize(32, 32);
clearFilterWidget->setIcon(QPixmap("theme:icons/delete")); clearFilterWidget->setIcon(themePixmap(QStringLiteral("icons/delete")));
connect(clearFilterWidget, &QToolButton::clicked, this, [this] { connect(clearFilterWidget, &QToolButton::clicked, this, [this] {
filterModel->blockSignals(true); filterModel->blockSignals(true);
filterModel->filterTree()->blockSignals(true); filterModel->filterTree()->blockSignals(true);

View file

@ -1,5 +1,6 @@
#include "visual_deck_display_options_widget.h" #include "visual_deck_display_options_widget.h"
#include "../../pixel_map_generator.h"
#include "../tabs/visual_deck_editor/tab_deck_editor_visual.h" #include "../tabs/visual_deck_editor/tab_deck_editor_visual.h"
#include <libcockatrice/utility/qt_utils.h> #include <libcockatrice/utility/qt_utils.h>
@ -47,7 +48,7 @@ VisualDeckDisplayOptionsWidget::VisualDeckDisplayOptionsWidget(QWidget *parent)
sortByLabel = new QLabel(this); sortByLabel = new QLabel(this);
sortCriteriaButton = new SettingsButtonWidget(this); sortCriteriaButton = new SettingsButtonWidget(this);
sortCriteriaButton->setButtonIcon(QPixmap("theme:icons/sort_arrow_down")); sortCriteriaButton->setButtonIcon(themePixmap(QStringLiteral("icons/sort_arrow_down")));
sortLabel = new QLabel(sortCriteriaButton); sortLabel = new QLabel(sortCriteriaButton);
sortLabel->setWordWrap(true); sortLabel->setWordWrap(true);
@ -92,7 +93,7 @@ void VisualDeckDisplayOptionsWidget::retranslateUi()
sortLabel->setText(tr("Click and drag to change the sort order within the groups")); sortLabel->setText(tr("Click and drag to change the sort order within the groups"));
sortCriteriaButton->setToolTip(tr("Configure how cards are sorted within their groups")); sortCriteriaButton->setToolTip(tr("Configure how cards are sorted within their groups"));
displayTypeButton->setButtonText(tr("Toggle Layout: Overlap")); displayTypeButton->setButtonText(tr("Toggle Layout: Overlap"));
displayTypeButton->setButtonIcon(QPixmap("theme:icons/scales")); displayTypeButton->setButtonIcon(themePixmap(QStringLiteral("icons/scales")));
displayTypeButton->setToolTip( displayTypeButton->setToolTip(
tr("Change how cards are displayed within zones (i.e. overlapped or fully visible.)")); tr("Change how cards are displayed within zones (i.e. overlapped or fully visible.)"));
} }
@ -117,11 +118,11 @@ void VisualDeckDisplayOptionsWidget::updateDisplayType()
switch (currentDisplayType) { switch (currentDisplayType) {
case DisplayType::Flat: case DisplayType::Flat:
displayTypeButton->setButtonText(tr("Toggle Layout: Flat")); displayTypeButton->setButtonText(tr("Toggle Layout: Flat"));
displayTypeButton->setButtonIcon(QPixmap("theme:icons/scroll")); displayTypeButton->setButtonIcon(themePixmap(QStringLiteral("icons/scroll")));
break; break;
case DisplayType::Overlap: case DisplayType::Overlap:
displayTypeButton->setButtonText(tr("Toggle Layout: Overlap")); displayTypeButton->setButtonText(tr("Toggle Layout: Overlap"));
displayTypeButton->setButtonIcon(QPixmap("theme:icons/scales")); displayTypeButton->setButtonIcon(themePixmap(QStringLiteral("icons/scales")));
break; break;
} }
emit displayTypeChanged(currentDisplayType); emit displayTypeChanged(currentDisplayType);

View file

@ -4,6 +4,7 @@
#include "../../../main.h" #include "../../../main.h"
#include "../../deck_loader/deck_loader.h" #include "../../deck_loader/deck_loader.h"
#include "../../layouts/overlap_layout.h" #include "../../layouts/overlap_layout.h"
#include "../../pixel_map_generator.h"
#include "../cards/card_info_picture_with_text_overlay_widget.h" #include "../cards/card_info_picture_with_text_overlay_widget.h"
#include "../cards/deck_card_zone_display_widget.h" #include "../cards/deck_card_zone_display_widget.h"
#include "../general/layout_containers/flow_widget.h" #include "../general/layout_containers/flow_widget.h"
@ -131,7 +132,7 @@ void VisualDeckEditorWidget::initializeSearchBarAndCompleter()
// Search button functionality // Search button functionality
searchPushButton = new CompactPushButton(searchContainer); searchPushButton = new CompactPushButton(searchContainer);
searchPushButton->setButtonIcon(QPixmap("theme:icons/search")); searchPushButton->setButtonIcon(themePixmap(QStringLiteral("icons/search")));
connect(searchPushButton, &QPushButton::clicked, this, [=, this]() { connect(searchPushButton, &QPushButton::clicked, this, [=, this]() {
ExactCard card = CardDatabaseManager::query()->getCard({searchBar->text()}); ExactCard card = CardDatabaseManager::query()->getCard({searchBar->text()});
if (card) { if (card) {

View file

@ -25,7 +25,7 @@ VisualDeckStorageSearchWidget::VisualDeckStorageSearchWidget(QWidget *parent) :
searchBar->setClearButtonEnabled(true); searchBar->setClearButtonEnabled(true);
searchBar->addAction(loadColorAdjustedPixmap("theme:icons/search"), QLineEdit::LeadingPosition); searchBar->addAction(loadColorAdjustedPixmap("theme:icons/search"), QLineEdit::LeadingPosition);
auto help = searchBar->addAction(QPixmap("theme:icons/info"), QLineEdit::TrailingPosition); auto help = searchBar->addAction(themePixmap(QStringLiteral("icons/info")), QLineEdit::TrailingPosition);
connect(help, &QAction::triggered, this, [this] { createDeckSearchSyntaxHelpWindow(searchBar); }); connect(help, &QAction::triggered, this, [this] { createDeckSearchSyntaxHelpWindow(searchBar); });
layout->addWidget(searchBar); layout->addWidget(searchBar);

View file

@ -1,6 +1,7 @@
#include "visual_deck_storage_widget.h" #include "visual_deck_storage_widget.h"
#include "../../../client/settings/cache_settings.h" #include "../../../client/settings/cache_settings.h"
#include "../../pixel_map_generator.h"
#include "../quick_settings/settings_button_widget.h" #include "../quick_settings/settings_button_widget.h"
#include "deck_preview/deck_preview_color_identity_filter_widget.h" #include "deck_preview/deck_preview_color_identity_filter_widget.h"
#include "deck_preview/deck_preview_widget.h" #include "deck_preview/deck_preview_widget.h"
@ -43,7 +44,7 @@ VisualDeckStorageWidget::VisualDeckStorageWidget(QWidget *parent) : QWidget(pare
searchWidget = new VisualDeckStorageSearchWidget(this); searchWidget = new VisualDeckStorageSearchWidget(this);
refreshButton = new QToolButton(this); refreshButton = new QToolButton(this);
refreshButton->setIcon(QPixmap("theme:icons/reload")); refreshButton->setIcon(themePixmap(QStringLiteral("icons/reload")));
refreshButton->setFixedSize(32, 32); refreshButton->setFixedSize(32, 32);
connect(refreshButton, &QPushButton::clicked, this, &VisualDeckStorageWidget::refreshIfPossible); connect(refreshButton, &QPushButton::clicked, this, &VisualDeckStorageWidget::refreshIfPossible);

View file

@ -44,6 +44,7 @@
#include "intents/intent_open_server_room_by_name.h" #include "intents/intent_open_server_room_by_name.h"
#include "intents/url_parser.h" #include "intents/url_parser.h"
#include "logger.h" #include "logger.h"
#include "pixel_map_generator.h"
#include "version_string.h" #include "version_string.h"
#include "widgets/dialogs/dlg_connect.h" #include "widgets/dialogs/dlg_connect.h"
#include "widgets/server/handle_public_servers.h" #include "widgets/server/handle_public_servers.h"
@ -91,8 +92,8 @@
#include <libcockatrice/settings/updates_settings.h> #include <libcockatrice/settings/updates_settings.h>
#define GITHUB_PAGES_URL "https://cockatrice.github.io" #define GITHUB_PAGES_URL "https://cockatrice.github.io"
#define GITHUB_CONTRIBUTORS_URL "https://github.com/Cockatrice/Cockatrice/graphs/contributors?type=c" #define GITHUB_CONTRIBUTORS_URL "https://github.com/Cockatrice/Cockatrice/graphs/contributors"
#define GITHUB_CONTRIBUTE_URL "https://github.com/Cockatrice/Cockatrice#cockatrice" #define GITHUB_CONTRIBUTE_URL "https://github.com/Cockatrice/Cockatrice#"
#define GITHUB_TRANSIFEX_TRANSLATORS_URL "https://github.com/Cockatrice/Cockatrice/wiki/Translator-Hall-of-Fame" #define GITHUB_TRANSIFEX_TRANSLATORS_URL "https://github.com/Cockatrice/Cockatrice/wiki/Translator-Hall-of-Fame"
#define GITHUB_TRANSLATOR_FAQ_URL "https://github.com/Cockatrice/Cockatrice/wiki/Translation-FAQ" #define GITHUB_TRANSLATOR_FAQ_URL "https://github.com/Cockatrice/Cockatrice/wiki/Translation-FAQ"
#define GITHUB_ISSUES_URL "https://github.com/Cockatrice/Cockatrice/issues" #define GITHUB_ISSUES_URL "https://github.com/Cockatrice/Cockatrice/issues"
@ -271,7 +272,8 @@ void MainWindow::actAbout()
GITHUB_TROUBLESHOOTING_URL + "'>" + tr("Troubleshooting") + "</a><br>" + "<a href='" + GITHUB_FAQ_URL + GITHUB_TROUBLESHOOTING_URL + "'>" + tr("Troubleshooting") + "</a><br>" + "<a href='" + GITHUB_FAQ_URL +
"'>" + tr("F.A.Q.") + "</a><br>"), "'>" + tr("F.A.Q.") + "</a><br>"),
QMessageBox::Ok, this); QMessageBox::Ok, this);
mb.setIconPixmap(QPixmap("theme:cockatrice").scaled(64, 64, Qt::KeepAspectRatio, Qt::SmoothTransformation)); mb.setIconPixmap(
themePixmap(QStringLiteral("cockatrice")).scaled(64, 64, Qt::KeepAspectRatio, Qt::SmoothTransformation));
mb.setTextInteractionFlags(Qt::TextBrowserInteraction); mb.setTextInteractionFlags(Qt::TextBrowserInteraction);
mb.exec(); mb.exec();
} }
@ -323,7 +325,7 @@ void MainWindow::retranslateUi()
aRegister->setText(tr("&Register to server...")); aRegister->setText(tr("&Register to server..."));
aForgotPassword->setText(tr("&Restore password...")); aForgotPassword->setText(tr("&Restore password..."));
aSettings->setText(tr("&Settings...")); aSettings->setText(tr("&Settings..."));
aSettings->setIcon(QPixmap("theme:icons/settings")); aSettings->setIcon(themePixmap(QStringLiteral("icons/settings")));
aExit->setText(tr("&Exit")); aExit->setText(tr("&Exit"));
#if defined(__APPLE__) /* For OSX */ #if defined(__APPLE__) /* For OSX */
@ -682,6 +684,7 @@ void MainWindow::runFirstRunWizard()
connect(wizard, &FirstRunWizard::cardDatabaseUpdateRequested, this, &MainWindow::actCheckCardUpdatesBackground); connect(wizard, &FirstRunWizard::cardDatabaseUpdateRequested, this, &MainWindow::actCheckCardUpdatesBackground);
connect(wizard, &FirstRunWizard::manualCardDatabaseSetupRequested, this, &MainWindow::actCheckCardUpdates); connect(wizard, &FirstRunWizard::manualCardDatabaseSetupRequested, this, &MainWindow::actCheckCardUpdates);
connect(this, &MainWindow::cardDatabaseUpdateFinished, wizard, &FirstRunWizard::onCardDatabaseUpdateFinished); connect(this, &MainWindow::cardDatabaseUpdateFinished, wizard, &FirstRunWizard::onCardDatabaseUpdateFinished);
connect(this, &MainWindow::cardDatabaseUpdateProgress, wizard, &FirstRunWizard::onCardDatabaseUpdateProgress);
connect(wizard, &FirstRunWizard::registerRequested, connectionController, &ConnectionController::registerToServer); connect(wizard, &FirstRunWizard::registerRequested, connectionController, &ConnectionController::registerToServer);
connect(wizard, &FirstRunWizard::connectRequested, connectionController, &ConnectionController::connectToServer); connect(wizard, &FirstRunWizard::connectRequested, connectionController, &ConnectionController::connectToServer);
@ -816,7 +819,7 @@ void MainWindow::createTrayIcon()
trayIcon = new QSystemTrayIcon(this); trayIcon = new QSystemTrayIcon(this);
trayIcon->setContextMenu(trayIconMenu); trayIcon->setContextMenu(trayIconMenu);
trayIcon->setIcon(QPixmap("theme:cockatrice")); trayIcon->setIcon(themePixmap(QStringLiteral("cockatrice")));
trayIcon->show(); trayIcon->show();
} }
@ -843,6 +846,17 @@ void MainWindow::closeEvent(QCloseEvent *event)
} }
bClosingDown = true; bClosingDown = true;
if (cardUpdateProcess && cardUpdateProcess->state() != QProcess::NotRunning) {
if (QMessageBox::question(this, tr("Are you sure?"),
tr("A card database update is still running. Quitting now will cancel it.\n"
"Are you sure you want to quit?"),
QMessageBox::Yes | QMessageBox::No, QMessageBox::No) == QMessageBox::No) {
event->ignore();
bClosingDown = false;
return;
}
}
if (!tabSupervisor->close()) { if (!tabSupervisor->close()) {
event->ignore(); event->ignore();
bClosingDown = false; bClosingDown = false;
@ -1036,7 +1050,7 @@ void MainWindow::createCardUpdateProcess(bool background)
if (dir.exists(binaryName)) { if (dir.exists(binaryName)) {
updaterCmd = dir.absoluteFilePath(binaryName); updaterCmd = dir.absoluteFilePath(binaryName);
} else { // try and find the directory oracle is stored in the build directory } else { // try and find the directory Oracle is stored in the build directory
QDir findLocalDir(dir); QDir findLocalDir(dir);
findLocalDir.cdUp(); findLocalDir.cdUp();
findLocalDir.cd(getCardUpdaterBinaryName()); findLocalDir.cd(getCardUpdaterBinaryName());
@ -1057,11 +1071,45 @@ void MainWindow::createCardUpdateProcess(bool background)
if (!background) { if (!background) {
cardUpdateProcess->start(updaterCmd, QStringList()); cardUpdateProcess->start(updaterCmd, QStringList());
} else { } else {
cardUpdateOutputBuffer.clear();
connect(cardUpdateProcess, &QProcess::readyReadStandardOutput, this, &MainWindow::cardUpdateProgressOutput);
cardUpdateProcess->start(updaterCmd, QStringList("-b")); cardUpdateProcess->start(updaterCmd, QStringList("-b"));
statusBar()->showMessage(tr("Card database update running.")); statusBar()->showMessage(tr("Card database update running."));
} }
} }
void MainWindow::cardUpdateProgressOutput()
{
if (!cardUpdateProcess) {
return;
}
cardUpdateOutputBuffer.append(cardUpdateProcess->readAllStandardOutput());
while (true) {
const int newline = cardUpdateOutputBuffer.indexOf('\n');
if (newline < 0) {
break;
}
const QByteArray line = cardUpdateOutputBuffer.left(newline).trimmed();
cardUpdateOutputBuffer.remove(0, newline + 1);
// Protocol emitted by `oracle -b`: "PROGRESS <stage> <done> <total>"
if (!line.startsWith("PROGRESS ")) {
continue;
}
const QList<QByteArray> parts = line.split(' ');
if (parts.size() != 4) {
continue;
}
bool doneOk = false;
bool totalOk = false;
const qint64 done = parts.at(2).toLongLong(&doneOk);
const qint64 total = parts.at(3).toLongLong(&totalOk);
if (!doneOk || !totalOk || done < 0 || total < 0) {
continue;
}
emit cardDatabaseUpdateProgress(QString::fromLatin1(parts.at(1)), done, total);
}
}
void MainWindow::exitCardDatabaseUpdate() void MainWindow::exitCardDatabaseUpdate()
{ {
if (!cardUpdateProcess) { if (!cardUpdateProcess) {
@ -1109,6 +1157,8 @@ void MainWindow::cardUpdateError(QProcess::ProcessError err)
void MainWindow::cardUpdateFinished(int exitCode, QProcess::ExitStatus exitStatus) void MainWindow::cardUpdateFinished(int exitCode, QProcess::ExitStatus exitStatus)
{ {
cardUpdateProgressOutput(); // drain any progress lines not yet parsed
const bool success = (exitStatus == QProcess::NormalExit) && (exitCode == 0); const bool success = (exitStatus == QProcess::NormalExit) && (exitCode == 0);
if (exitStatus == QProcess::NormalExit) { if (exitStatus == QProcess::NormalExit) {
SettingsCache::instance().updates().setLastCardUpdateCheck(QDateTime::currentDateTime().date()); SettingsCache::instance().updates().setLastCardUpdateCheck(QDateTime::currentDateTime().date());

View file

@ -68,6 +68,11 @@ signals:
/** @brief Emitted after the background card-database update subprocess exits. */ /** @brief Emitted after the background card-database update subprocess exits. */
void cardDatabaseUpdateFinished(bool success); void cardDatabaseUpdateFinished(bool success);
/** @brief Emitted while the background card-database update subprocess runs.
* @p stage is one of "download", "scan" or "import"; @p done/@p total
* are byte counts for the first two stages and set indices for "import". */
void cardDatabaseUpdateProgress(const QString &stage, qint64 done, qint64 total);
public slots: public slots:
void actCheckCardUpdates(); void actCheckCardUpdates();
void actCheckCardUpdatesBackground(); void actCheckCardUpdatesBackground();
@ -96,6 +101,7 @@ private slots:
void cardUpdateError(QProcess::ProcessError err); void cardUpdateError(QProcess::ProcessError err);
void cardUpdateFinished(int exitCode, QProcess::ExitStatus exitStatus); void cardUpdateFinished(int exitCode, QProcess::ExitStatus exitStatus);
void cardUpdateProgressOutput();
void refreshShortcuts(); void refreshShortcuts();
void cardDatabaseLoadingFailed(); void cardDatabaseLoadingFailed();
void cardDatabaseNewSetsFound(int numUnknownSets, QStringList unknownSetsNames); void cardDatabaseNewSetsFound(int numUnknownSets, QStringList unknownSetsNames);
@ -159,6 +165,7 @@ private:
LatencyStatusWidget *latencyStatus = nullptr; ///< status bar widget with live round-trip stats and history graph LatencyStatusWidget *latencyStatus = nullptr; ///< status bar widget with live round-trip stats and history graph
bool bHasActivated, askedForDbUpdater; bool bHasActivated, askedForDbUpdater;
QProcess *cardUpdateProcess; QProcess *cardUpdateProcess;
QByteArray cardUpdateOutputBuffer;
DlgViewLog *logviewDialog; DlgViewLog *logviewDialog;
GameReplay *replay; GameReplay *replay;
DlgTipOfTheDay *tip; DlgTipOfTheDay *tip;

View file

@ -231,7 +231,7 @@ int main(int argc, char *argv[])
// These values are only used by the settings loader/saver // These values are only used by the settings loader/saver
// Wrong or outdated values are kept to not break things // Wrong or outdated values are kept to not break things
QCoreApplication::setOrganizationName("Cockatrice"); QCoreApplication::setOrganizationName("Cockatrice");
QCoreApplication::setOrganizationDomain("cockatrice.de"); QCoreApplication::setOrganizationDomain("cockatrice.github.io");
QCoreApplication::setApplicationName("Cockatrice"); QCoreApplication::setApplicationName("Cockatrice");
QCoreApplication::setApplicationVersion(VERSION_STRING); QCoreApplication::setApplicationVersion(VERSION_STRING);
@ -250,7 +250,7 @@ int main(int argc, char *argv[])
// Command-line parser // Command-line parser
QCommandLineParser parser; QCommandLineParser parser;
parser.setApplicationDescription("Cockatrice"); parser.setApplicationDescription("Cockatrice Client");
parser.addHelpOption(); parser.addHelpOption();
parser.addVersionOption(); parser.addVersionOption();
@ -349,9 +349,9 @@ int main(int argc, char *argv[])
} }
qCInfo(MainLog) << "MainWindow constructor finished"; qCInfo(MainLog) << "MainWindow constructor finished";
ui.setWindowIcon(QPixmap("theme:cockatrice")); ui.setWindowIcon(themePixmap(QStringLiteral("cockatrice")));
// set name of the app desktop file; used by wayland to load the window icon // Set name of the app desktop file; used by wayland to load the window icon
QGuiApplication::setDesktopFileName("cockatrice"); QGuiApplication::setDesktopFileName("Cockatrice");
SettingsCache::instance().network().setClientID(generateClientID()); SettingsCache::instance().network().setClientID(generateClientID());

View file

@ -20,6 +20,7 @@ public:
[[nodiscard]] virtual bool getShowMessagePopup() const = 0; [[nodiscard]] virtual bool getShowMessagePopup() const = 0;
[[nodiscard]] virtual bool getShowMentionPopup() const = 0; [[nodiscard]] virtual bool getShowMentionPopup() const = 0;
[[nodiscard]] virtual bool getRoomHistory() const = 0; [[nodiscard]] virtual bool getRoomHistory() const = 0;
[[nodiscard]] virtual bool getIgnoreAllPrivateMessages() const = 0;
[[nodiscard]] virtual QString getHighlightWords() const = 0; [[nodiscard]] virtual QString getHighlightWords() const = 0;
}; };

View file

@ -253,3 +253,24 @@ PendingCommand *AbstractClient::prepareAdminCommand(const ::google::protobuf::Me
c->GetReflection()->MutableMessage(c, cmd.GetDescriptor()->FindExtensionByName("ext"))->CopyFrom(cmd); c->GetReflection()->MutableMessage(c, cmd.GetDescriptor()->FindExtensionByName("ext"))->CopyFrom(cmd);
return new PendingCommand(cont); return new PendingCommand(cont);
} }
PendingCommand *AbstractClient::prepareDeveloperCommand(const ::google::protobuf::Message &cmd)
{
CommandContainer cont;
DeveloperCommand *c = cont.add_developer_command();
// A developer command message may also be usable through other command
// families, so select the extension scoped to DeveloperCommand rather than
// guessing by name.
const ::google::protobuf::Descriptor *cmdDescriptor = cmd.GetDescriptor();
const ::google::protobuf::Descriptor *developerDescriptor = DeveloperCommand::descriptor();
const ::google::protobuf::FieldDescriptor *developerExtension = nullptr;
for (int i = 0; i < cmdDescriptor->extension_count(); ++i) {
if (cmdDescriptor->extension(i)->containing_type() == developerDescriptor) {
developerExtension = cmdDescriptor->extension(i);
break;
}
}
Q_ASSERT(developerExtension != nullptr);
c->GetReflection()->MutableMessage(c, developerExtension)->CopyFrom(cmd);
return new PendingCommand(cont);
}

View file

@ -173,6 +173,7 @@ public:
static PendingCommand *prepareRoomCommand(const ::google::protobuf::Message &cmd, int roomId); static PendingCommand *prepareRoomCommand(const ::google::protobuf::Message &cmd, int roomId);
static PendingCommand *prepareModeratorCommand(const ::google::protobuf::Message &cmd); static PendingCommand *prepareModeratorCommand(const ::google::protobuf::Message &cmd);
static PendingCommand *prepareAdminCommand(const ::google::protobuf::Message &cmd); static PendingCommand *prepareAdminCommand(const ::google::protobuf::Message &cmd);
static PendingCommand *prepareDeveloperCommand(const ::google::protobuf::Message &cmd);
QMap<QString, bool> clientFeatures; QMap<QString, bool> clientFeatures;
}; };

Some files were not shown because too many files have changed in this diff Show more