From 486b8e84074b677c46b81e43c2998886a0659647 Mon Sep 17 00:00:00 2001 From: tooomm Date: Mon, 3 Aug 2026 03:17:20 +0200 Subject: [PATCH 01/21] Capitalize label (#7066) --- .../settings_page/user_interface_settings_page.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cockatrice/src/interface/widgets/settings_page/user_interface_settings_page.cpp b/cockatrice/src/interface/widgets/settings_page/user_interface_settings_page.cpp index fa27ec198..8203dee58 100644 --- a/cockatrice/src/interface/widgets/settings_page/user_interface_settings_page.cpp +++ b/cockatrice/src/interface/widgets/settings_page/user_interface_settings_page.cpp @@ -241,15 +241,15 @@ void UserInterfaceSettingsPage::retranslateUi() visualDeckStoragePromptForConversionLabel.setText( tr("When adding a tag in the visual deck storage to a .txt deck:")); visualDeckStoragePromptForConversionSelector.setItemText(visualDeckStoragePromptForConversionIndexNone, - tr("do nothing")); + tr("Do nothing")); visualDeckStoragePromptForConversionSelector.setItemText(visualDeckStoragePromptForConversionIndexPrompt, - tr("ask to convert to .cod")); + tr("Ask to convert to .cod")); visualDeckStoragePromptForConversionSelector.setItemText(visualDeckStoragePromptForConversionIndexAlways, - tr("always convert to .cod")); + tr("Always convert to .cod")); defaultDeckEditorTypeLabel.setText(tr("Default deck editor type")); defaultDeckEditorTypeSelector.setItemText(TabSupervisor::ClassicDeckEditor, tr("Classic Deck Editor")); defaultDeckEditorTypeSelector.setItemText(TabSupervisor::VisualDeckEditor, tr("Visual Deck Editor")); replayGroupBox->setTitle(tr("Replay settings")); rewindBufferingMsLabel.setText(tr("Buffer time for backwards skip via shortcut:")); rewindBufferingMsBox.setSuffix(" ms"); -} \ No newline at end of file +} From a4557454a7c6b47f3f13cd3af76455b85535d600 Mon Sep 17 00:00:00 2001 From: RickyRister <42636155+RickyRister@users.noreply.github.com> Date: Sun, 2 Aug 2026 18:51:10 -0700 Subject: [PATCH 02/21] [Game] Remove tally requiring two cards selected (#7058) --- cockatrice/src/game_graphics/game_view.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cockatrice/src/game_graphics/game_view.cpp b/cockatrice/src/game_graphics/game_view.cpp index bda5ea76d..ed190552e 100644 --- a/cockatrice/src/game_graphics/game_view.cpp +++ b/cockatrice/src/game_graphics/game_view.cpp @@ -256,7 +256,7 @@ void GameView::updateTotalSelectionCount(const QSize &viewSize) GameScene *gameScene = static_cast(scene()); QList entries = Tally::compute(gameScene->selectedCards(), tallyType); - if (entries.isEmpty() || count <= 1) { + if (entries.isEmpty()) { tallyContainer->hide(); cachedTallyRows.clear(); return; From ca1c06368711cbdcec050dc445de89ef920d624c Mon Sep 17 00:00:00 2001 From: RickyRister <42636155+RickyRister@users.noreply.github.com> Date: Sun, 2 Aug 2026 19:20:47 -0700 Subject: [PATCH 03/21] [Game] Implement total power tally (#7057) * [Game] Implement total power tally * PR comments --- cockatrice/CMakeLists.txt | 1 + .../game_graphics/player/menu/tally_menu.cpp | 3 ++ .../game_graphics/player/menu/tally_menu.h | 1 + .../src/game_graphics/tally/stats_tally.cpp | 36 +++++++++++++++++++ .../src/game_graphics/tally/stats_tally.h | 21 +++++++++++ cockatrice/src/game_graphics/tally/tally.cpp | 3 ++ cockatrice/src/game_graphics/tally/tally.h | 3 +- 7 files changed, 67 insertions(+), 1 deletion(-) create mode 100644 cockatrice/src/game_graphics/tally/stats_tally.cpp create mode 100644 cockatrice/src/game_graphics/tally/stats_tally.h diff --git a/cockatrice/CMakeLists.txt b/cockatrice/CMakeLists.txt index 7d0e22fd8..2d370d03c 100644 --- a/cockatrice/CMakeLists.txt +++ b/cockatrice/CMakeLists.txt @@ -99,6 +99,7 @@ set(cockatrice_SOURCES src/game_graphics/player/menu/sideboard_menu.cpp src/game_graphics/player/menu/tally_menu.cpp src/game_graphics/player/menu/utility_menu.cpp + src/game_graphics/tally/stats_tally.cpp src/game_graphics/tally/subtype_tally.cpp src/game_graphics/tally/tally.cpp src/game/player/player_actions.cpp diff --git a/cockatrice/src/game_graphics/player/menu/tally_menu.cpp b/cockatrice/src/game_graphics/player/menu/tally_menu.cpp index b8616c75a..2bf02904e 100644 --- a/cockatrice/src/game_graphics/player/menu/tally_menu.cpp +++ b/cockatrice/src/game_graphics/player/menu/tally_menu.cpp @@ -11,10 +11,12 @@ TallyMenu::TallyMenu() aTallyNone = createTallyAction(TallyType::None); aTallySubtypes = createTallyAction(TallyType::Subtypes); + aTallyTotalPower = createTallyAction(TallyType::TotalPower); addAction(aTallyNone); addSeparator(); addAction(aTallySubtypes); + addAction(aTallyTotalPower); retranslateUi(); } @@ -51,4 +53,5 @@ void TallyMenu::retranslateUi() aTallyNone->setText(tr("None")); aTallySubtypes->setText(tr("Subtypes")); + aTallyTotalPower->setText(tr("Total Power")); } diff --git a/cockatrice/src/game_graphics/player/menu/tally_menu.h b/cockatrice/src/game_graphics/player/menu/tally_menu.h index 28c056f44..acd1daf67 100644 --- a/cockatrice/src/game_graphics/player/menu/tally_menu.h +++ b/cockatrice/src/game_graphics/player/menu/tally_menu.h @@ -23,6 +23,7 @@ private: QAction *aTallyNone = nullptr; QAction *aTallySubtypes = nullptr; + QAction *aTallyTotalPower = nullptr; QAction *createTallyAction(TallyType tallyType); }; diff --git a/cockatrice/src/game_graphics/tally/stats_tally.cpp b/cockatrice/src/game_graphics/tally/stats_tally.cpp new file mode 100644 index 000000000..e7a6621fa --- /dev/null +++ b/cockatrice/src/game_graphics/tally/stats_tally.cpp @@ -0,0 +1,36 @@ +#include "stats_tally.h" + +#include "../board/card_item.h" + +#include +#include +#include + +static int sumPowers(const QList &cards) +{ + // calculate total power; + int total = 0; + for (auto card : cards) { + QVariantList parsed = CardItem::parsePT(card->getPT()); + if (!parsed.isEmpty()) { + int power = parsed.first().toInt(); // toInt will default to 0 if it's not an int + total += qMax(power, 0); + } + } + return total; +} + +QList StatsTally::computeTotalPower(const QList &cards) +{ + // don't bother if none of the cards have pt + bool hasPT = + std::any_of(cards.cbegin(), cards.cend(), [](const CardItem *card) { return !card->getPT().isEmpty(); }); + if (!hasPT) { + return {}; + } + + int total = sumPowers(cards); + + QString name = QCoreApplication::translate("StatsTally", "Total Power"); + return {TallyRow{name, QString::number(total)}}; +} diff --git a/cockatrice/src/game_graphics/tally/stats_tally.h b/cockatrice/src/game_graphics/tally/stats_tally.h new file mode 100644 index 000000000..4c3d93b56 --- /dev/null +++ b/cockatrice/src/game_graphics/tally/stats_tally.h @@ -0,0 +1,21 @@ +#ifndef COCKATRICE_STATS_TALLY_H +#define COCKATRICE_STATS_TALLY_H +#include "tally.h" + +/** + * @brief Extracts and tallies stats from selected cards. + */ +namespace StatsTally +{ + +/** + * @brief Sums the power of all selected cards + * + * @param cards The list of selected card items to analyze. + * @return A single row containing the total, or an empty list if none of the cards have pt + */ +QList computeTotalPower(const QList &cards); + +} // namespace StatsTally + +#endif // COCKATRICE_STATS_TALLY_H diff --git a/cockatrice/src/game_graphics/tally/tally.cpp b/cockatrice/src/game_graphics/tally/tally.cpp index f9389d0d6..aa2cae024 100644 --- a/cockatrice/src/game_graphics/tally/tally.cpp +++ b/cockatrice/src/game_graphics/tally/tally.cpp @@ -1,5 +1,6 @@ #include "tally.h" +#include "stats_tally.h" #include "subtype_tally.h" TallyType Tally::intToType(int value) @@ -18,6 +19,8 @@ QList Tally::compute(const QList &cards, const TallyType t return {}; case TallyType::Subtypes: return SubtypeTally::countSubtypes(cards); + case TallyType::TotalPower: + return StatsTally::computeTotalPower(cards); } return {}; } diff --git a/cockatrice/src/game_graphics/tally/tally.h b/cockatrice/src/game_graphics/tally/tally.h index d0fd77127..97406cddb 100644 --- a/cockatrice/src/game_graphics/tally/tally.h +++ b/cockatrice/src/game_graphics/tally/tally.h @@ -20,7 +20,8 @@ enum class TallyType { None, Subtypes, - MaxValue = Subtypes // sentinel value + TotalPower, + MaxValue = TotalPower // sentinel value }; namespace Tally From b44dcf5951649f460a097ca4fde7abb6669084bb Mon Sep 17 00:00:00 2001 From: RickyRister <42636155+RickyRister@users.noreply.github.com> Date: Sun, 2 Aug 2026 19:22:33 -0700 Subject: [PATCH 04/21] [Replay] Refactor: extract replay playback logic into single class (#7060) * [Replay] Refactor: consolidate replay logic into single class * fixes --- cockatrice/CMakeLists.txt | 1 + cockatrice/src/game/abstract_game.cpp | 2 +- cockatrice/src/game/abstract_game.h | 2 +- cockatrice/src/game/replay.cpp | 2 +- cockatrice/src/game/replay.h | 2 +- .../widgets/replay/replay_manager.cpp | 178 ++++++++++++++++++ .../interface/widgets/replay/replay_manager.h | 80 ++++++++ .../widgets/replay/replay_timeline_widget.cpp | 143 ++------------ .../widgets/replay/replay_timeline_widget.h | 48 +---- .../widgets/replay/replay_widget.cpp | 75 ++------ .../interface/widgets/replay/replay_widget.h | 20 +- .../src/interface/widgets/tabs/tab_game.cpp | 4 +- 12 files changed, 320 insertions(+), 237 deletions(-) create mode 100644 cockatrice/src/interface/widgets/replay/replay_manager.cpp create mode 100644 cockatrice/src/interface/widgets/replay/replay_manager.h diff --git a/cockatrice/CMakeLists.txt b/cockatrice/CMakeLists.txt index 2d370d03c..cc58c5b43 100644 --- a/cockatrice/CMakeLists.txt +++ b/cockatrice/CMakeLists.txt @@ -230,6 +230,7 @@ set(cockatrice_SOURCES src/interface/widgets/printing_selector/set_name_and_collectors_number_display_widget.cpp src/interface/widgets/quick_settings/settings_button_widget.cpp src/interface/widgets/quick_settings/settings_popup_widget.cpp + src/interface/widgets/replay/replay_manager.cpp src/interface/widgets/replay/replay_quick_settings_widget.cpp src/interface/widgets/replay/replay_timeline_widget.cpp src/interface/widgets/replay/replay_widget.cpp diff --git a/cockatrice/src/game/abstract_game.cpp b/cockatrice/src/game/abstract_game.cpp index c20003ece..6aa2ab28f 100644 --- a/cockatrice/src/game/abstract_game.cpp +++ b/cockatrice/src/game/abstract_game.cpp @@ -31,7 +31,7 @@ AbstractClient *AbstractGame::getClientForPlayer(int playerId) const } } -void AbstractGame::loadReplay(GameReplay *replay) +void AbstractGame::loadReplay(const GameReplay *replay) { gameMetaInfo->setFromProto(replay->game_info()); gameMetaInfo->setSpectatorsOmniscient(true); diff --git a/cockatrice/src/game/abstract_game.h b/cockatrice/src/game/abstract_game.h index 5115ed5ca..fcf764492 100644 --- a/cockatrice/src/game/abstract_game.h +++ b/cockatrice/src/game/abstract_game.h @@ -53,7 +53,7 @@ public: AbstractClient *getClientForPlayer(int playerId) const; - void loadReplay(GameReplay *replay); + void loadReplay(const GameReplay *replay); CardItem *getCard(int playerId, const QString &zoneName, int cardId) const; diff --git a/cockatrice/src/game/replay.cpp b/cockatrice/src/game/replay.cpp index 69f9d8b20..dcf3e9b9b 100644 --- a/cockatrice/src/game/replay.cpp +++ b/cockatrice/src/game/replay.cpp @@ -2,7 +2,7 @@ #include "../interface/widgets/tabs/tab_game.h" -Replay::Replay(QObject *_parent, GameReplay *_replay, bool isLocalGame) : AbstractGame(_parent) +Replay::Replay(QObject *_parent, const GameReplay *_replay, bool isLocalGame) : AbstractGame(_parent) { gameState = new GameState(this, 0, -1, isLocalGame, {}, false, false, -1, false); connect(gameMetaInfo, &GameMetaInfo::startedChanged, gameState, &GameState::onStartedChanged); diff --git a/cockatrice/src/game/replay.h b/cockatrice/src/game/replay.h index ecb3a10d0..1c269b273 100644 --- a/cockatrice/src/game/replay.h +++ b/cockatrice/src/game/replay.h @@ -15,7 +15,7 @@ class Replay : public AbstractGame Q_OBJECT public: - explicit Replay(QObject *_parent, GameReplay *_replay, bool isLocalGame); + explicit Replay(QObject *_parent, const GameReplay *_replay, bool isLocalGame); }; #endif // COCKATRICE_REPLAY_H diff --git a/cockatrice/src/interface/widgets/replay/replay_manager.cpp b/cockatrice/src/interface/widgets/replay/replay_manager.cpp new file mode 100644 index 000000000..1037d36a8 --- /dev/null +++ b/cockatrice/src/interface/widgets/replay/replay_manager.cpp @@ -0,0 +1,178 @@ +#include "replay_manager.h" + +#include "../../../client/settings/cache_settings.h" + +#include +#include + +static constexpr int TIMER_INTERVAL_MS = 200; + +static QList createReplayTimeline(const GameReplay *replay) +{ + // Create list: event number -> time [ms] + unsigned int lastEventTimestamp = 0; + const int eventCount = replay->event_list_size(); + + QList replayTimeline; + for (int i = 0; i < eventCount; ++i) { + int nextSecondIndex = i + 1; + while (nextSecondIndex < eventCount && + replay->event_list(nextSecondIndex).seconds_elapsed() == lastEventTimestamp) { + ++nextSecondIndex; + } + + // Distribute simultaneous events evenly across 1 second. + const int numberEventsThisSecond = nextSecondIndex - i; + for (int k = 0; k < numberEventsThisSecond; ++k) { + int eventMs = replay->event_list(i + k).seconds_elapsed() * 1000; + int distributionMs = static_cast(static_cast(k) / numberEventsThisSecond * 1000); + replayTimeline.append(eventMs + distributionMs); + } + + if (nextSecondIndex < eventCount) { + lastEventTimestamp = replay->event_list(nextSecondIndex).seconds_elapsed(); + } + i += numberEventsThisSecond - 1; + } + + return replayTimeline; +} + +ReplayManager::ReplayManager(QObject *parent, GameReplay *replay) + : QObject(parent), replay(replay), replayTimeline(createReplayTimeline(replay)) +{ + maxTime = replayTimeline.isEmpty() ? 0 : replayTimeline.last(); + + replayTimer = new QTimer(this); + replayTimer->setInterval(TIMER_INTERVAL_MS); + connect(replayTimer, &QTimer::timeout, this, &ReplayManager::replayTimerTimeout); + + rewindBufferingTimer = new QTimer(this); + rewindBufferingTimer->setSingleShot(true); + connect(rewindBufferingTimer, &QTimer::timeout, this, &ReplayManager::processRewind); +} + +ReplayManager::~ReplayManager() +{ + delete replay; +} + +void ReplayManager::skipToTime(int newTime, bool doRewindBuffering) +{ + // check boundary conditions + if (newTime < 0) { + newTime = 0; + } + if (newTime > maxTime) { + newTime = maxTime; + } + + newTime -= newTime % TIMER_INTERVAL_MS; // Time should always be a multiple of the interval + + const bool isBackwardsSkip = newTime < currentProcessedTime; + currentVisualTime = newTime; + + if (isBackwardsSkip) { + handleBackwardsSkip(doRewindBuffering); + } else { + processNewEvents(FORWARD_SKIP); + } + + timeChanged(currentVisualTime); +} + +/** + * @brief Handles a backwards skip in the replay timeline. + * + * @param doRewindBuffering When true, if multiple backward skips are made in quick succession, only a single rewind + * is processed at the end. When false, the backwards skip will always cause an immediate rewind. + */ +void ReplayManager::handleBackwardsSkip(bool doRewindBuffering) +{ + if (doRewindBuffering) { + // We use a one-shot timer to implement the rewind buffering. + // The rewind only happens once the timer runs out. + // If another backwards skip happens, the timer will just get reset instead of rewinding. + rewindBufferingTimer->stop(); + rewindBufferingTimer->start(SettingsCache::instance().interface().getRewindBufferingMs()); + } else { + // otherwise, process the rewind immediately + processRewind(); + } +} + +void ReplayManager::processRewind() +{ + // stop any queued-up rewinds + rewindBufferingTimer->stop(); + + // process the rewind + currentEvent = 0; + emit rewound(); + processNewEvents(BACKWARD_SKIP); +} + +void ReplayManager::replayTimerTimeout() +{ + currentVisualTime += TIMER_INTERVAL_MS; + + processNewEvents(NORMAL_PLAYBACK); + + timeChanged(currentVisualTime); +} + +/** @brief Processes all unprocessed events up to the current time. */ +void ReplayManager::processNewEvents(PlaybackMode playbackMode) +{ + currentProcessedTime = currentVisualTime; + + while (currentEvent < replayTimeline.size() && replayTimeline[currentEvent] < currentProcessedTime) { + EventProcessingOptions options; + + // backwards skip => always skip reveal windows + // forwards skip => skip reveal windows that don't happen within a big skip of the target + if (playbackMode == BACKWARD_SKIP || currentProcessedTime - replayTimeline[currentEvent] > BIG_SKIP_MS) { + options |= SKIP_REVEAL_WINDOW; + } + + // backwards skip => always skip tap animation + if (playbackMode == BACKWARD_SKIP) { + options |= SKIP_TAP_ANIMATION; + } + + emit eventReplayed(replay->event_list(currentEvent), options); + ++currentEvent; + } + if (currentEvent == replayTimeline.size()) { + emit replayFinished(); + replayTimer->stop(); + } +} + +void ReplayManager::setTimeScaleFactor(qreal _timeScaleFactor) +{ + timeScaleFactor = _timeScaleFactor; + int interval = std::max(1, qRound(TIMER_INTERVAL_MS / timeScaleFactor)); + replayTimer->setInterval(interval); +} + +void ReplayManager::startReplay() +{ + replayTimer->start(); +} + +void ReplayManager::stopReplay() +{ + replayTimer->stop(); +} + +void ReplayManager::setTime(int time) +{ + // don't buffer rewinds from clicks, since clicks usually don't happen fast enough to require buffering + skipToTime(time, false); +} + +void ReplayManager::skipByAmount(int amount) +{ + skipToTime(currentVisualTime + amount, amount < 0); +} \ No newline at end of file diff --git a/cockatrice/src/interface/widgets/replay/replay_manager.h b/cockatrice/src/interface/widgets/replay/replay_manager.h new file mode 100644 index 000000000..16d3591ba --- /dev/null +++ b/cockatrice/src/interface/widgets/replay/replay_manager.h @@ -0,0 +1,80 @@ +#ifndef COCKATRICE_REPLAY_MANAGER_H +#define COCKATRICE_REPLAY_MANAGER_H + +#include "../../../game/player/event_processing_options.h" + +#include +#include + +class GameReplay; +class QTimer; + +/** + * @brief This class handles all logic to do with playing back replays + */ +class ReplayManager : public QObject +{ + Q_OBJECT + + enum PlaybackMode + { + NORMAL_PLAYBACK, + FORWARD_SKIP, + BACKWARD_SKIP + }; + + GameReplay *replay; + QList replayTimeline; ///< timestamp of each event, with the indexes corresponding + int maxTime; + + QTimer *replayTimer; + QTimer *rewindBufferingTimer; + + qreal timeScaleFactor = 1.0; + + int currentVisualTime = 0; ///< time currently displayed by the timeline + int currentProcessedTime = 0; ///< time that events are currently processed up to. Could differ from visual time due + ///< to rewind buffering + int currentEvent = 0; ///< current event's index + + void skipToTime(int newTime, bool doRewindBuffering); + void handleBackwardsSkip(bool doRewindBuffering); + void processRewind(); + void processNewEvents(PlaybackMode playbackMode); + +private slots: + void replayTimerTimeout(); + +public: + static constexpr int SMALL_SKIP_MS = 1000; + static constexpr int BIG_SKIP_MS = 10000; + + /** + * @param parent The parent QObject + * @param replay Cannot be null. Takes ownership of the object. + */ + explicit ReplayManager(QObject *parent, GameReplay *replay); + + ~ReplayManager() override; + + const QList &getReplayTimeline() const + { + return replayTimeline; + } + + void setTimeScaleFactor(qreal _timeScaleFactor); + +public slots: + void startReplay(); + void stopReplay(); + void setTime(int time); + void skipByAmount(int amount); // use a negative amount to skip backwards + +signals: + void timeChanged(int time); + void eventReplayed(const GameEventContainer &cont, EventProcessingOptions options); + void replayFinished(); + void rewound(); +}; + +#endif // COCKATRICE_REPLAY_MANAGER_H diff --git a/cockatrice/src/interface/widgets/replay/replay_timeline_widget.cpp b/cockatrice/src/interface/widgets/replay/replay_timeline_widget.cpp index 9c699d300..b5c7bf301 100644 --- a/cockatrice/src/interface/widgets/replay/replay_timeline_widget.cpp +++ b/cockatrice/src/interface/widgets/replay/replay_timeline_widget.cpp @@ -4,26 +4,19 @@ #include #include -#include -#include -ReplayTimelineWidget::ReplayTimelineWidget(QWidget *parent) - : QWidget(parent), maxBinValue(1), maxTime(1), timeScaleFactor(1.0), currentVisualTime(0), currentProcessedTime(0), - currentEvent(0) +static constexpr int BIN_LENGTH = 5000; +static constexpr int MIN_RESOLUTION_MS = 1000; + +ReplayTimelineWidget::ReplayTimelineWidget(QWidget *parent) : QWidget(parent) { - replayTimer = new QTimer(this); - replayTimer->setInterval(TIMER_INTERVAL_MS); - connect(replayTimer, &QTimer::timeout, this, &ReplayTimelineWidget::replayTimerTimeout); - - rewindBufferingTimer = new QTimer(this); - rewindBufferingTimer->setSingleShot(true); - connect(rewindBufferingTimer, &QTimer::timeout, this, &ReplayTimelineWidget::processRewind); } -void ReplayTimelineWidget::setTimeline(const QList &_replayTimeline) +void ReplayTimelineWidget::setTimeline(const QList &replayTimeline) { - replayTimeline = _replayTimeline; histogram.clear(); + currentTime = 0; + int binEndTime = BIN_LENGTH - 1; int binValue = 0; for (int i : replayTimeline) { @@ -66,7 +59,7 @@ void ReplayTimelineWidget::paintEvent(QPaintEvent * /* event */) painter.fillPath(path, Qt::black); const QColor barColor = QColor::fromHsv(120, 255, 255, 100); - quint64 w = (quint64)(width() - 1) * (quint64)currentVisualTime / maxTime; + quint64 w = (quint64)(width() - 1) * (quint64)currentTime / maxTime; painter.fillRect(0, 0, static_cast(w), height() - 1, barColor); } @@ -77,63 +70,24 @@ void ReplayTimelineWidget::mousePressEvent(QMouseEvent *event) #else int newTime = static_cast((qint64)maxTime * (qint64)event->x() / width()); #endif - // don't buffer rewinds from clicks, since clicks usually don't happen fast enough to require buffering - skipToTime(newTime, false); + emit timeClicked(newTime); } -void ReplayTimelineWidget::skipToTime(int newTime, bool doRewindBuffering) +void ReplayTimelineWidget::setCurrentTime(int time) { - // check boundary conditions - if (newTime < 0) { - newTime = 0; - } - if (newTime > maxTime) { - newTime = maxTime; + int newTime = qBound(0, time, maxTime); + + if (currentTime == newTime) { + return; } - newTime -= newTime % TIMER_INTERVAL_MS; // Time should always be a multiple of the interval + bool doUpdate = currentTime / MIN_RESOLUTION_MS != newTime / MIN_RESOLUTION_MS; - const bool isBackwardsSkip = newTime < currentProcessedTime; - currentVisualTime = newTime; + currentTime = newTime; - if (isBackwardsSkip) { - handleBackwardsSkip(doRewindBuffering); - } else { - processNewEvents(FORWARD_SKIP); + if (doUpdate) { + update(); } - - update(); -} - -/** - * @brief Handles a backwards skip in the replay timeline. - * - * @param doRewindBuffering When true, if multiple backward skips are made in quick succession, only a single rewind - * is processed at the end. When false, the backwards skip will always cause an immediate rewind. - */ -void ReplayTimelineWidget::handleBackwardsSkip(bool doRewindBuffering) -{ - if (doRewindBuffering) { - // We use a one-shot timer to implement the rewind buffering. - // The rewind only happens once the timer runs out. - // If another backwards skip happens, the timer will just get reset instead of rewinding. - rewindBufferingTimer->stop(); - rewindBufferingTimer->start(SettingsCache::instance().interface().getRewindBufferingMs()); - } else { - // otherwise, process the rewind immediately - processRewind(); - } -} - -void ReplayTimelineWidget::processRewind() -{ - // stop any queued-up rewinds - rewindBufferingTimer->stop(); - - // process the rewind - currentEvent = 0; - emit rewound(); - processNewEvents(BACKWARD_SKIP); } QSize ReplayTimelineWidget::sizeHint() const @@ -145,64 +99,3 @@ QSize ReplayTimelineWidget::minimumSizeHint() const { return {400, 50}; } - -void ReplayTimelineWidget::replayTimerTimeout() -{ - currentVisualTime += TIMER_INTERVAL_MS; - - processNewEvents(NORMAL_PLAYBACK); - - if (!(currentVisualTime % 1000)) { - update(); - } -} - -/** @brief Processes all unprocessed events up to the current time. */ -void ReplayTimelineWidget::processNewEvents(PlaybackMode playbackMode) -{ - currentProcessedTime = currentVisualTime; - - while ((currentEvent < replayTimeline.size()) && (replayTimeline[currentEvent] < currentProcessedTime)) { - EventProcessingOptions options; - - // backwards skip => always skip reveal windows - // forwards skip => skip reveal windows that don't happen within a big skip of the target - if (playbackMode == BACKWARD_SKIP || currentProcessedTime - replayTimeline[currentEvent] > BIG_SKIP_MS) { - options |= SKIP_REVEAL_WINDOW; - } - - // backwards skip => always skip tap animation - if (playbackMode == BACKWARD_SKIP) { - options |= SKIP_TAP_ANIMATION; - } - - emit processNextEvent(options); - ++currentEvent; - } - if (currentEvent == replayTimeline.size()) { - emit replayFinished(); - replayTimer->stop(); - } -} - -void ReplayTimelineWidget::setTimeScaleFactor(qreal _timeScaleFactor) -{ - timeScaleFactor = _timeScaleFactor; - int interval = std::max(1, qRound(TIMER_INTERVAL_MS / timeScaleFactor)); - replayTimer->setInterval(interval); -} - -void ReplayTimelineWidget::startReplay() -{ - replayTimer->start(); -} - -void ReplayTimelineWidget::stopReplay() -{ - replayTimer->stop(); -} - -void ReplayTimelineWidget::skipByAmount(int amount) -{ - skipToTime(currentVisualTime + amount, amount < 0); -} \ No newline at end of file diff --git a/cockatrice/src/interface/widgets/replay/replay_timeline_widget.h b/cockatrice/src/interface/widgets/replay/replay_timeline_widget.h index 6cdb8bcb2..47d19a741 100644 --- a/cockatrice/src/interface/widgets/replay/replay_timeline_widget.h +++ b/cockatrice/src/interface/widgets/replay/replay_timeline_widget.h @@ -18,57 +18,25 @@ class QTimer; class ReplayTimelineWidget : public QWidget { Q_OBJECT + signals: - void processNextEvent(EventProcessingOptions options); - void replayFinished(); - void rewound(); + void timeClicked(int newTime); private: - enum PlaybackMode - { - NORMAL_PLAYBACK, - FORWARD_SKIP, - BACKWARD_SKIP - }; - - static constexpr int TIMER_INTERVAL_MS = 200; - static constexpr int BIN_LENGTH = 5000; - - QTimer *replayTimer; - QTimer *rewindBufferingTimer; - QList replayTimeline; QList histogram; - int maxBinValue, maxTime; - qreal timeScaleFactor; - int currentVisualTime; // time currently displayed by the timeline - int currentProcessedTime; // time that events are currently processed up to. Could differ from visual time due to - // rewind buffering - int currentEvent; + int maxBinValue = 1; + int maxTime = 1; - void skipToTime(int newTime, bool doRewindBuffering); - void handleBackwardsSkip(bool doRewindBuffering); - void processRewind(); - void processNewEvents(PlaybackMode playbackMode); -private slots: - void replayTimerTimeout(); + int currentTime = 0; public: - static constexpr int SMALL_SKIP_MS = 1000; - static constexpr int BIG_SKIP_MS = 10000; - explicit ReplayTimelineWidget(QWidget *parent = nullptr); - void setTimeline(const QList &_replayTimeline); + void setTimeline(const QList &replayTimeline); [[nodiscard]] QSize sizeHint() const override; [[nodiscard]] QSize minimumSizeHint() const override; - void setTimeScaleFactor(qreal _timeScaleFactor); - [[nodiscard]] int getCurrentEvent() const - { - return currentEvent; - } + public slots: - void startReplay(); - void stopReplay(); - void skipByAmount(int amount); // use a negative amount to skip backwards + void setCurrentTime(int time); protected: void paintEvent(QPaintEvent *event) override; diff --git a/cockatrice/src/interface/widgets/replay/replay_widget.cpp b/cockatrice/src/interface/widgets/replay/replay_widget.cpp index 4dc7b1380..f5768a7aa 100644 --- a/cockatrice/src/interface/widgets/replay/replay_widget.cpp +++ b/cockatrice/src/interface/widgets/replay/replay_widget.cpp @@ -1,70 +1,50 @@ #include "replay_widget.h" +#include "../../../client/settings/cache_settings.h" #include "../../../client/settings/shortcuts_settings.h" #include "../interface/widgets/tabs/tab_game.h" +#include "replay_manager.h" #include "replay_quick_settings_widget.h" #include #include -ReplayWidget::ReplayWidget(TabGame *parent, GameReplay *_replay) - : QWidget(parent), game(parent), replay(_replay), replayPlayButton(nullptr), replayFastForwardButton(nullptr), - aReplaySkipForward(nullptr), aReplaySkipBackward(nullptr), aReplaySkipForwardBig(nullptr), - aReplaySkipBackwardBig(nullptr) +ReplayWidget::ReplayWidget(QWidget *parent, GameReplay *replay) + : QWidget(parent), replayPlayButton(nullptr), replayFastForwardButton(nullptr), aReplaySkipForward(nullptr), + aReplaySkipBackward(nullptr), aReplaySkipForwardBig(nullptr), aReplaySkipBackwardBig(nullptr) { - if (replay) { - game->getGame()->loadReplay(replay); - - // Create list: event number -> time [ms] - // Distribute simultaneous events evenly across 1 second. - unsigned int lastEventTimestamp = 0; - const int eventCount = replay->event_list_size(); - for (int i = 0; i < eventCount; ++i) { - int j = i + 1; - while ((j < eventCount) && (replay->event_list(j).seconds_elapsed() == lastEventTimestamp)) { - ++j; - } - - const int numberEventsThisSecond = j - i; - for (int k = 0; k < numberEventsThisSecond; ++k) { - replayTimeline.append(replay->event_list(i + k).seconds_elapsed() * 1000 + - (int)((qreal)k / (qreal)numberEventsThisSecond * 1000)); - } - - if (j < eventCount) { - lastEventTimestamp = replay->event_list(j).seconds_elapsed(); - } - i += numberEventsThisSecond - 1; - } - } + // replay manager + replayManager = new ReplayManager(this, replay); + connect(replayManager, &ReplayManager::eventReplayed, this, &ReplayWidget::eventReplayed); + connect(replayManager, &ReplayManager::replayFinished, this, &ReplayWidget::replayFinished); + connect(replayManager, &ReplayManager::rewound, this, &ReplayWidget::rewound); // timeline widget timelineWidget = new ReplayTimelineWidget; - timelineWidget->setTimeline(replayTimeline); - connect(timelineWidget, &ReplayTimelineWidget::processNextEvent, this, &ReplayWidget::replayNextEvent); - connect(timelineWidget, &ReplayTimelineWidget::replayFinished, this, &ReplayWidget::replayFinished); - connect(timelineWidget, &ReplayTimelineWidget::rewound, this, &ReplayWidget::replayRewind); + timelineWidget->setTimeline(replayManager->getReplayTimeline()); + connect(replayManager, &ReplayManager::timeChanged, timelineWidget, &ReplayTimelineWidget::setCurrentTime); + connect(timelineWidget, &ReplayTimelineWidget::timeClicked, replayManager, &ReplayManager::setTime); // timeline skip shortcuts aReplaySkipForward = new QAction(timelineWidget); timelineWidget->addAction(aReplaySkipForward); connect(aReplaySkipForward, &QAction::triggered, this, - [this] { timelineWidget->skipByAmount(ReplayTimelineWidget::SMALL_SKIP_MS); }); + [this] { replayManager->skipByAmount(ReplayManager::SMALL_SKIP_MS); }); aReplaySkipBackward = new QAction(timelineWidget); timelineWidget->addAction(aReplaySkipBackward); connect(aReplaySkipBackward, &QAction::triggered, this, - [this] { timelineWidget->skipByAmount(-ReplayTimelineWidget::SMALL_SKIP_MS); }); + [this] { replayManager->skipByAmount(-ReplayManager::SMALL_SKIP_MS); }); aReplaySkipForwardBig = new QAction(timelineWidget); timelineWidget->addAction(aReplaySkipForwardBig); connect(aReplaySkipForwardBig, &QAction::triggered, this, - [this] { timelineWidget->skipByAmount(ReplayTimelineWidget::BIG_SKIP_MS); }); + [this] { replayManager->skipByAmount(ReplayManager::BIG_SKIP_MS); }); aReplaySkipBackwardBig = new QAction(timelineWidget); timelineWidget->addAction(aReplaySkipBackwardBig); connect(aReplaySkipBackwardBig, &QAction::triggered, this, - [this] { timelineWidget->skipByAmount(-ReplayTimelineWidget::BIG_SKIP_MS); }); + [this] { replayManager->skipByAmount(-ReplayManager::BIG_SKIP_MS); }); // buttons replayPlayButton = new QToolButton; @@ -97,18 +77,11 @@ ReplayWidget::ReplayWidget(TabGame *parent, GameReplay *_replay) setObjectName("replayControlWidget"); setLayout(replayControlLayout); - connect(this, &ReplayWidget::requestChatAndPhaseReset, game, &TabGame::resetChatAndPhase); - connect(&SettingsCache::instance().shortcuts(), &ShortcutsSettings::shortCutChanged, this, &ReplayWidget::refreshShortcuts); refreshShortcuts(); } -void ReplayWidget::replayNextEvent(EventProcessingOptions options) -{ - emit eventReplayed(replay->event_list(timelineWidget->getCurrentEvent()), options); -} - void ReplayWidget::replayFinished() { replayPlayButton->setChecked(false); @@ -117,24 +90,16 @@ void ReplayWidget::replayFinished() void ReplayWidget::replayPlayButtonToggled(bool checked) { if (checked) { // start replay - timelineWidget->startReplay(); + replayManager->startReplay(); } else { // pause replay - timelineWidget->stopReplay(); + replayManager->stopReplay(); } } void ReplayWidget::updateTimeScaleFactor(bool isFastForward) { qreal factor = isFastForward ? SettingsCache::instance().interface().getFastForwardSpeed() : 1.0; - timelineWidget->setTimeScaleFactor(factor); -} - -/** - * @brief Handles everything that needs to be reset when doing a replay rewind. - */ -void ReplayWidget::replayRewind() -{ - emit requestChatAndPhaseReset(); + replayManager->setTimeScaleFactor(factor); } void ReplayWidget::refreshShortcuts() diff --git a/cockatrice/src/interface/widgets/replay/replay_widget.h b/cockatrice/src/interface/widgets/replay/replay_widget.h index eca356e9e..6d2b7a043 100644 --- a/cockatrice/src/interface/widgets/replay/replay_widget.h +++ b/cockatrice/src/interface/widgets/replay/replay_widget.h @@ -14,11 +14,12 @@ #include #include +class ReplayManager; class ReplayQuickSettingsWidget; class TabGame; /** - * @brief The top-level that is put in the replay dock widget. + * @brief The top-level widget that is put in the replay dock widget. * Contains the replay timeline as well as the buttons. */ class ReplayWidget : public QWidget @@ -26,29 +27,28 @@ class ReplayWidget : public QWidget Q_OBJECT public: - ReplayWidget(TabGame *parent, GameReplay *replay); - TabGame *game; - GameReplay *replay; + /** + * @param parent The parent widget + * @param replay Cannot be null. Takes ownership of the replay. + */ + ReplayWidget(QWidget *parent, GameReplay *replay); signals: - void requestChatAndPhaseReset(); + void rewound(); void eventReplayed(const GameEventContainer &cont, EventProcessingOptions options); private: - // Replay related members - int currentReplayStep = 0; - QList replayTimeline; + ReplayManager *replayManager; + ReplayTimelineWidget *timelineWidget; QToolButton *replayPlayButton, *replayFastForwardButton; ReplayQuickSettingsWidget *settingsWidget; QAction *aReplaySkipForward, *aReplaySkipBackward, *aReplaySkipForwardBig, *aReplaySkipBackwardBig; private slots: - void replayNextEvent(EventProcessingOptions options); void replayFinished(); void replayPlayButtonToggled(bool checked); void updateTimeScaleFactor(bool checked); - void replayRewind(); void refreshShortcuts(); }; diff --git a/cockatrice/src/interface/widgets/tabs/tab_game.cpp b/cockatrice/src/interface/widgets/tabs/tab_game.cpp index c9ebf06c8..d3f3a1735 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_game.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_game.cpp @@ -265,9 +265,6 @@ void TabGame::emitUserEvent() TabGame::~TabGame() { - if (replayWidget) { - delete replayWidget->replay; - } for (auto &player : game->getPlayerManager()->getPlayers()) { player->clear(); } @@ -1183,6 +1180,7 @@ void TabGame::createReplayDock(GameReplay *replay) replayDock->setWidget(replayWidget); replayDock->setFloating(false); + connect(replayWidget, &ReplayWidget::rewound, this, &TabGame::resetChatAndPhase); connect(replayWidget, &ReplayWidget::eventReplayed, game->getGameEventHandler(), [this](const auto &event, auto options) { game->getGameEventHandler()->processGameEventContainer(event, nullptr, options); From 1ed9823b56a382c2dfc621baedca48b443a85367 Mon Sep 17 00:00:00 2001 From: RickyRister <42636155+RickyRister@users.noreply.github.com> Date: Sun, 2 Aug 2026 20:02:37 -0700 Subject: [PATCH 05/21] [CardInfo] use QString QHash instead of QVariantHash for properties (#7063) * [CardInfo] use QString QHash instead of QVariantHash for properties * bump CACHE_VERSION * cleanups --- .../libcockatrice/card/card_info.cpp | 17 +++++----- .../libcockatrice/card/card_info.h | 28 ++++++++-------- .../card/card_info_comparator.cpp | 2 +- .../card/database/card_database_cache.cpp | 9 +++-- .../card/database/parser/cockatrice_xml_3.cpp | 4 +-- .../card/database/parser/cockatrice_xml_4.cpp | 8 ++--- .../card/database/parser/cockatrice_xml_4.h | 6 ++-- .../card/printing/printing_info.cpp | 10 +++--- .../card/printing/printing_info.h | 18 +++++----- oracle/src/oracleimporter.cpp | 33 +++++++++---------- oracle/src/oracleimporter.h | 8 ++--- 11 files changed, 70 insertions(+), 73 deletions(-) diff --git a/libcockatrice_card/libcockatrice/card/card_info.cpp b/libcockatrice_card/libcockatrice/card/card_info.cpp index d37408a58..786c6cd9f 100644 --- a/libcockatrice_card/libcockatrice/card/card_info.cpp +++ b/libcockatrice_card/libcockatrice/card/card_info.cpp @@ -12,7 +12,6 @@ #include #include #include -#include #include #include @@ -24,7 +23,7 @@ using CardInfoPtr = QSharedPointer; namespace { -QByteArray serializeProperties(const QVariantHash &props) +QByteArray serializeProperties(const QHash &props) { QByteArray blob; QDataStream out(&blob, QIODevice::WriteOnly); @@ -48,7 +47,7 @@ void CardInfo::ensurePropertiesLoaded() const propertiesLoaded = true; } -const QVariantHash &CardInfo::getPropertiesHash() const +const QHash &CardInfo::getPropertiesHash() const { ensurePropertiesLoaded(); return propertiesCache; @@ -57,7 +56,7 @@ const QVariantHash &CardInfo::getPropertiesHash() const void CardInfo::setProperty(const QString &_name, const QString &_value) { ensurePropertiesLoaded(); - if (propertiesCache.value(_name).toString() == _value) { + if (propertiesCache.value(_name) == _value) { return; } propertiesCache.insert(_name, _value); @@ -65,7 +64,7 @@ void CardInfo::setProperty(const QString &_name, const QString &_value) emit cardInfoChanged(smartThis); } -void CardInfo::setProperties(const QVariantHash &_props) +void CardInfo::setProperties(const QHash &_props) { ensurePropertiesLoaded(); propertiesCache = _props; @@ -76,7 +75,7 @@ void CardInfo::setProperties(const QVariantHash &_props) CardInfo::CardInfo(const QString &_name, const QString &_text, bool _isToken, - QVariantHash _properties, + QHash _properties, const QList &_relatedCards, const QList &_reverseRelatedCards, SetToPrintingsMap _sets, @@ -122,7 +121,7 @@ CardInfoPtr CardInfo::newInstance(const QString &_name) CardInfoPtr CardInfo::newInstance(const QString &_name, const QString &_text, bool _isToken, - QVariantHash _properties, + QHash _properties, const QList &_relatedCards, const QList &_reverseRelatedCards, SetToPrintingsMap _sets, @@ -210,10 +209,10 @@ void CardInfo::addToSet(const CardSetPtr &_set, const PrintingInfo &_info) refreshCachedSets(); } -void CardInfo::combineLegalities(const QVariantHash &props) +void CardInfo::combineLegalities(const QHash &props) { ensurePropertiesLoaded(); - QHashIterator it(props); + QHashIterator it(props); while (it.hasNext()) { it.next(); if (it.key().startsWith("format-")) { diff --git a/libcockatrice_card/libcockatrice/card/card_info.h b/libcockatrice_card/libcockatrice/card/card_info.h index ad99864c8..e625a0748 100644 --- a/libcockatrice_card/libcockatrice/card/card_info.h +++ b/libcockatrice_card/libcockatrice/card/card_info.h @@ -76,12 +76,12 @@ private: QString text; ///< Text description or rules text of the card. bool isToken; ///< Whether this card is a token or not. // Properties are stored as a pre-serialized blob (cheap to load) and the - // QVariantHash is materialized on first query, so database load avoids - // constructing thousands of QVariants per card. - mutable QByteArray propertiesBlob; ///< Serialized properties (load form). - mutable QVariantHash propertiesCache; ///< Materialized properties (query form). - mutable bool propertiesLoaded = false; ///< Whether propertiesCache is valid. - mutable QMutex propertiesMutex; ///< Guards lazy materialization. + // QHash is materialized on first query, so database load avoids + // constructing thousands of QStrings per card. + mutable QByteArray propertiesBlob; ///< Serialized properties (load form). + mutable QHash propertiesCache; ///< Materialized properties (query form). + mutable bool propertiesLoaded = false; ///< Whether propertiesCache is valid. + mutable QMutex propertiesMutex; ///< Guards lazy materialization. /** * @brief Materializes propertiesCache from propertiesBlob if not already done. @@ -114,7 +114,7 @@ public: explicit CardInfo(const QString &_name, const QString &_text, bool _isToken, - QVariantHash _properties, + QHash _properties, const QList &_relatedCards, const QList &_reverseRelatedCards, SetToPrintingsMap _sets, @@ -127,7 +127,7 @@ public: * Used by the binary cache reader to skip recomputing @p _simpleName and * @p _altNames (which otherwise require a Unicode normalization and a full * printing scan). Properties are supplied as a pre-serialized @p _propertiesBlob - * so the QVariantHash is not built at load time (it is materialized on first + * so the QHash is not built at load time (it is materialized on first * query). * * @param _name The card name. @@ -194,7 +194,7 @@ public: static CardInfoPtr newInstance(const QString &_name, const QString &_text, bool _isToken, - QVariantHash _properties, + QHash _properties, const QList &_relatedCards, const QList &_reverseRelatedCards, SetToPrintingsMap _sets, @@ -288,12 +288,12 @@ public: { return getPropertiesHash().keys(); } - [[nodiscard]] const QVariantHash &getPropertiesHash() const; + [[nodiscard]] const QHash &getPropertiesHash() const; /** * @brief Stores the pre-serialized properties blob and invalidates the * materialized cache. Used by the binary cache reader so the - * QVariantHash is not built at load time. + * QHash is not built at load time. * @param _blob The serialized properties (as written by the cache writer). */ void setPropertiesBlob(QByteArray _blob) const @@ -305,10 +305,10 @@ public: } [[nodiscard]] QString getProperty(const QString &propertyName) const { - return getPropertiesHash().value(propertyName).toString(); + return getPropertiesHash().value(propertyName); } void setProperty(const QString &_name, const QString &_value); - void setProperties(const QVariantHash &_props); + void setProperties(const QHash &_props); [[nodiscard]] bool hasProperty(const QString &propertyName) const { return getPropertiesHash().contains(propertyName); @@ -415,7 +415,7 @@ public: * * @param props Key-value mapping of format legalities. */ - void combineLegalities(const QVariantHash &props); + void combineLegalities(const QHash &props); /** * @brief Refreshes all cached fields that are calculated from the contained sets and printings. diff --git a/libcockatrice_card/libcockatrice/card/card_info_comparator.cpp b/libcockatrice_card/libcockatrice/card/card_info_comparator.cpp index 821cc8675..f4a74194c 100644 --- a/libcockatrice_card/libcockatrice/card/card_info_comparator.cpp +++ b/libcockatrice_card/libcockatrice/card/card_info_comparator.cpp @@ -66,7 +66,7 @@ QVariant CardInfoComparator::getProperty(const CardInfoPtr &card, const QString return card->getIsToken(); } - // Otherwise, check if it's a custom property in the QVariantHash + // Otherwise, check if it's a custom property in the properties hash if (card->hasProperty(property)) { return card->getProperty(property); } diff --git a/libcockatrice_card/libcockatrice/card/database/card_database_cache.cpp b/libcockatrice_card/libcockatrice/card/database/card_database_cache.cpp index 0b68a389a..dfa684dbe 100644 --- a/libcockatrice_card/libcockatrice/card/database/card_database_cache.cpp +++ b/libcockatrice_card/libcockatrice/card/database/card_database_cache.cpp @@ -13,12 +13,11 @@ #include #include #include -#include namespace { constexpr quint32 CACHE_MAGIC = 0x43445243; // "CDRC" -constexpr quint32 CACHE_VERSION = 1; +constexpr quint32 CACHE_VERSION = 2; // ---- Primitives ----------------------------------------------------------- @@ -34,11 +33,11 @@ QString readString(QDataStream &in) return s; } -// Stores a QVariantHash as a single pre-serialized blob. The reader keeps the -// blob as-is and materializes the QVariantHash lazily on first query, which is +// Stores a QHash as a single pre-serialized blob. The reader keeps the +// blob as-is and materializes the QHash lazily on first query, which is // what removes the allocation storm from database load (see card_info.cpp / // printing_info.cpp). -void writeHashBlob(QDataStream &out, const QVariantHash &h) +void writeHashBlob(QDataStream &out, const QHash &h) { QByteArray blob; QDataStream blobOut(&blob, QIODevice::WriteOnly); diff --git a/libcockatrice_card/libcockatrice/card/database/parser/cockatrice_xml_3.cpp b/libcockatrice_card/libcockatrice/card/database/parser/cockatrice_xml_3.cpp index 2dd5e91e9..e403a641d 100644 --- a/libcockatrice_card/libcockatrice/card/database/parser/cockatrice_xml_3.cpp +++ b/libcockatrice_card/libcockatrice/card/database/parser/cockatrice_xml_3.cpp @@ -172,7 +172,7 @@ void CockatriceXml3Parser::loadCardsFromXml(QXmlStreamReader &xml) if (xmlName == "card") { QString name = QString(""); QString text = QString(""); - QVariantHash properties = QVariantHash(); + QHash properties; QString colors = QString(""); QList relatedCards, reverseRelatedCards; auto _sets = SetToPrintingsMap(); @@ -229,7 +229,7 @@ void CockatriceXml3Parser::loadCardsFromXml(QXmlStreamReader &xml) // behaviour. Without this check, disabling a set has no effect on v3 databases. if (set->getEnabled()) { PrintingInfo setInfo(set); - QVariantHash printingProps; + QHash printingProps; if (attrs.hasAttribute("muId")) { printingProps.insert("muid", attrs.value("muId").toString()); } diff --git a/libcockatrice_card/libcockatrice/card/database/parser/cockatrice_xml_4.cpp b/libcockatrice_card/libcockatrice/card/database/parser/cockatrice_xml_4.cpp index 129bae9bc..df92618da 100644 --- a/libcockatrice_card/libcockatrice/card/database/parser/cockatrice_xml_4.cpp +++ b/libcockatrice_card/libcockatrice/card/database/parser/cockatrice_xml_4.cpp @@ -243,9 +243,9 @@ void CockatriceXml4Parser::loadSetsFromXml(QXmlStreamReader &xml) } } -QVariantHash CockatriceXml4Parser::loadCardPropertiesFromXml(QXmlStreamReader &xml) +QHash CockatriceXml4Parser::loadCardPropertiesFromXml(QXmlStreamReader &xml) { - QVariantHash properties = QVariantHash(); + QHash properties; while (!xml.atEnd()) { if (xml.readNext() == QXmlStreamReader::EndElement) { break; @@ -272,7 +272,7 @@ void CockatriceXml4Parser::loadCardsFromXml(QXmlStreamReader &xml) if (xmlName == "card") { QString name = QString(""); QString text = QString(""); - QVariantHash properties = QVariantHash(); + QHash properties; QList relatedCards, reverseRelatedCards; auto _sets = SetToPrintingsMap(); int tableRow = 0; @@ -315,7 +315,7 @@ void CockatriceXml4Parser::loadCardsFromXml(QXmlStreamReader &xml) auto set = internalAddSet(setName); if (set->getEnabled()) { PrintingInfo printingInfo(set); - QVariantHash printingProps; + QHash printingProps; for (QXmlStreamAttribute attr : attrs) { QString attrName = attr.name().toString(); if (attrName == "picURL") { diff --git a/libcockatrice_card/libcockatrice/card/database/parser/cockatrice_xml_4.h b/libcockatrice_card/libcockatrice/card/database/parser/cockatrice_xml_4.h index 92c967a0c..55f7c5a3b 100644 --- a/libcockatrice_card/libcockatrice/card/database/parser/cockatrice_xml_4.h +++ b/libcockatrice_card/libcockatrice/card/database/parser/cockatrice_xml_4.h @@ -19,7 +19,7 @@ inline Q_LOGGING_CATEGORY(CockatriceXml4Log, "cockatrice_xml.xml_4_parser"); * making the parser more extensible and schema-compliant. * * @note Differences from v3: - * - Card properties are stored in blocks as a QVariantHash. + * - Card properties are stored in blocks as a QHash. * - Sets can include a element. * - Supports user preferences via ICardPreferenceProvider (e.g., skipping rebalanced cards). * - Related cards support persistent relations and multiple attach types (e.g., transform). @@ -70,9 +70,9 @@ private: /** * @brief Loads a generic block from a element. * @param xml The open QXmlStreamReader positioned at a element. - * @return A QVariantHash mapping property names to values. + * @return A QHash mapping property names to values. */ - QVariantHash loadCardPropertiesFromXml(QXmlStreamReader &xml); + QHash loadCardPropertiesFromXml(QXmlStreamReader &xml); /** * @brief Load all elements from the XML stream. diff --git a/libcockatrice_card/libcockatrice/card/printing/printing_info.cpp b/libcockatrice_card/libcockatrice/card/printing/printing_info.cpp index 5df3680a0..ee5b1329b 100644 --- a/libcockatrice_card/libcockatrice/card/printing/printing_info.cpp +++ b/libcockatrice_card/libcockatrice/card/printing/printing_info.cpp @@ -27,14 +27,14 @@ void PrintingInfo::ensurePropertiesLoaded() const void PrintingInfo::setProperty(const QString &_name, const QString &_value) { ensurePropertiesLoaded(); - if (propertiesCache.value(_name).toString() == _value) { + if (propertiesCache.value(_name) == _value) { return; } propertiesCache.insert(_name, _value); setProperties(propertiesCache); } -void PrintingInfo::setProperties(const QVariantHash &_props) +void PrintingInfo::setProperties(const QHash &_props) { ensurePropertiesLoaded(); propertiesCache = _props; @@ -48,10 +48,10 @@ void PrintingInfo::setProperties(const QVariantHash &_props) */ QString PrintingInfo::getUuid() const { - return getPropertiesHash().value("uuid").toString(); + return getPropertiesHash().value("uuid"); } QString PrintingInfo::getFlavorName() const { - return getPropertiesHash().value("flavorName").toString(); -} \ No newline at end of file + return getPropertiesHash().value("flavorName"); +} diff --git a/libcockatrice_card/libcockatrice/card/printing/printing_info.h b/libcockatrice_card/libcockatrice/card/printing/printing_info.h index adec2010c..f7ca90aad 100644 --- a/libcockatrice_card/libcockatrice/card/printing/printing_info.h +++ b/libcockatrice_card/libcockatrice/card/printing/printing_info.h @@ -70,11 +70,11 @@ private: CardSetPtr set; ///< The set this variation belongs to. // Properties are stored as a pre-serialized blob (cheap to load) and the - // QVariantHash is materialized on first query. This avoids constructing - // thousands of QVariants per card at database-load time. - mutable QByteArray propertiesBlob; ///< Serialized properties (load form). - mutable QVariantHash propertiesCache; ///< Materialized properties (query form). - mutable bool propertiesLoaded = false; ///< Whether propertiesCache is valid. + // QHash is materialized on first query. This avoids constructing + // thousands of QStrings per card at database-load time. + mutable QByteArray propertiesBlob; ///< Serialized properties (load form). + mutable QHash propertiesCache; ///< Materialized properties (query form). + mutable bool propertiesLoaded = false; ///< Whether propertiesCache is valid. mutable QSharedPointer propertiesMutex = QSharedPointer::create(); ///< Guards lazy materialization. @@ -101,7 +101,7 @@ public: return getPropertiesHash().keys(); } - [[nodiscard]] const QVariantHash &getPropertiesHash() const + [[nodiscard]] const QHash &getPropertiesHash() const { ensurePropertiesLoaded(); return propertiesCache; @@ -115,7 +115,7 @@ public: */ [[nodiscard]] QString getProperty(const QString &propertyName) const { - return getPropertiesHash().value(propertyName).toString(); + return getPropertiesHash().value(propertyName); } /** @@ -127,12 +127,12 @@ public: * @param _value The string value to assign. */ void setProperty(const QString &_name, const QString &_value); - void setProperties(const QVariantHash &_props); + void setProperties(const QHash &_props); /** * @brief Stores the pre-serialized properties blob and marks the materialized * cache as invalid. Used by the binary cache reader to avoid building a - * QVariantHash at load time. + * QHash at load time. * @param _blob The serialized properties (as written by the cache writer). */ void setPropertiesBlob(QByteArray _blob) const diff --git a/oracle/src/oracleimporter.cpp b/oracle/src/oracleimporter.cpp index 4eeb6ecc4..3d7b7b555 100644 --- a/oracle/src/oracleimporter.cpp +++ b/oracle/src/oracleimporter.cpp @@ -18,7 +18,7 @@ static const QList kSingletonCounts = {{1, "legal"}, {0, "banned"} SplitCardPart::SplitCardPart(const QString &_name, const QString &_text, - const QVariantHash &_properties, + const QHash &_properties, const PrintingInfo &_printingInfo) : name(_name), text(_text), properties(_properties), printingInfo(_printingInfo) { @@ -135,7 +135,7 @@ static void sortAndReduceColors(QString &colors) CardInfoPtr OracleImporter::addCard(QString name, const QString &text, bool isToken, - QVariantHash properties, + QHash properties, const QList &relatedCards, const PrintingInfo &printingInfo) { @@ -152,7 +152,7 @@ CardInfoPtr OracleImporter::addCard(QString name, } // Remove {} around mana costs, except if it's split cost - QString manacost = properties.value("manacost").toString(); + QString manacost = properties.value("manacost"); if (!manacost.isEmpty()) { QStringList symbols = manacost.split("}"); QString formattedCardCost; @@ -169,12 +169,12 @@ CardInfoPtr OracleImporter::addCard(QString name, } // fix colors - QString allColors = properties.value("colors").toString(); + QString allColors = properties.value("colors"); if (allColors.size() > 1) { sortAndReduceColors(allColors); properties.insert("colors", allColors); } - QString allColorIdent = properties.value("coloridentity").toString(); + QString allColorIdent = properties.value("coloridentity"); if (allColorIdent.size() > 1) { sortAndReduceColors(allColorIdent); properties.insert("coloridentity", allColorIdent); @@ -182,16 +182,15 @@ CardInfoPtr OracleImporter::addCard(QString name, // DETECT CARD POSITIONING INFO - bool landscapeOrientation = properties.value("maintype").toString() == "Battle" || - properties.value("layout").toString() == "split" || - properties.value("layout").toString() == "planar"; + bool landscapeOrientation = properties.value("maintype") == "Battle" || properties.value("layout") == "split" || + properties.value("layout") == "planar"; // cards that enter the field tapped bool cipt = parseCipt(name, text) || landscapeOrientation; // table row int tableRow = 1; - QString mainCardType = properties.value("maintype").toString(); + QString mainCardType = properties.value("maintype"); if (mainCardType == "Land") { tableRow = 0; } else if (mainCardType == "Sorcery" || mainCardType == "Instant") { @@ -201,11 +200,11 @@ CardInfoPtr OracleImporter::addCard(QString name, } // card side - QString side = properties.value("side").toString() == "b" ? "back" : "front"; + QString side = properties.value("side") == "b" ? "back" : "front"; properties.insert("side", side); // upsideDown (flip cards) - QString layout = properties.value("layout").toString(); + QString layout = properties.value("layout"); bool upsideDown = layout == "flip" && side == "back"; // insert the card and its properties @@ -279,7 +278,7 @@ int OracleImporter::importCardsFromSet(const CardSetPtr ¤tSet, const QList } // card properties - QVariantHash properties; + QHash properties; for (auto i = cardProperties.cbegin(), end = cardProperties.cend(); i != end; ++i) { QString mtgjsonProperty = i.key(); QString xmlPropertyName = i.value(); @@ -291,7 +290,7 @@ int OracleImporter::importCardsFromSet(const CardSetPtr ¤tSet, const QList // per-set properties PrintingInfo printingInfo = PrintingInfo(currentSet); - QVariantHash printingProps; + QHash printingProps; for (auto i = setInfoProperties.cbegin(), end = setInfoProperties.cend(); i != end; ++i) { QString mtgjsonProperty = i.key(); QString xmlPropertyName = i.value(); @@ -431,7 +430,7 @@ int OracleImporter::importCardsFromSet(const CardSetPtr ¤tSet, const QList QList, QString>> partsAndNames = splitCards.values(); for (auto [splitCardParts, name] : partsAndNames) { QString text; - QVariantHash properties; + QHash properties; PrintingInfo printingInfo; for (const SplitCardPart &tmp : splitCardParts) { @@ -444,11 +443,11 @@ int OracleImporter::importCardsFromSet(const CardSetPtr ¤tSet, const QList properties = tmp.getProperties(); printingInfo = tmp.getPrintingInfo(); } else { - const QVariantHash &tmpProps = tmp.getProperties(); + const QHash &tmpProps = tmp.getProperties(); for (auto i = tmpProps.cbegin(), end = tmpProps.cend(); i != end; ++i) { QString prop = i.key(); - QString originalPropertyValue = properties.value(prop).toString(); - QString thisCardPropertyValue = i.value().toString(); + QString originalPropertyValue = properties.value(prop); + QString thisCardPropertyValue = i.value(); if (!thisCardPropertyValue.isEmpty() && originalPropertyValue != thisCardPropertyValue) { if (originalPropertyValue.isEmpty()) { // don't create //es if one field is empty properties.insert(prop, thisCardPropertyValue); diff --git a/oracle/src/oracleimporter.h b/oracle/src/oracleimporter.h index 5bf352594..99644f9ce 100644 --- a/oracle/src/oracleimporter.h +++ b/oracle/src/oracleimporter.h @@ -95,7 +95,7 @@ class SplitCardPart public: SplitCardPart(const QString &_name, const QString &_text, - const QVariantHash &_properties, + const QHash &_properties, const PrintingInfo &_printingInfo); inline const QString &getName() const { @@ -105,7 +105,7 @@ public: { return text; } - inline const QVariantHash &getProperties() const + inline const QHash &getProperties() const { return properties; } @@ -117,7 +117,7 @@ public: private: QString name; QString text; - QVariantHash properties; + QHash properties; PrintingInfo printingInfo; }; @@ -142,7 +142,7 @@ private: CardInfoPtr addCard(QString name, const QString &text, bool isToken, - QVariantHash properties, + QHash properties, const QList &relatedCards, const PrintingInfo &printingInfo); signals: From c7ab75c332f444c322001fef51743362a318564c Mon Sep 17 00:00:00 2001 From: BruebachL <44814898+BruebachL@users.noreply.github.com> Date: Wed, 5 Aug 2026 12:27:46 +0200 Subject: [PATCH 06/21] [UpdateDialog] Close dialog when declining an update (#2517) (#7081) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Lukas Brübach --- cockatrice/src/interface/widgets/dialogs/dlg_update.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_update.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_update.cpp index ee2149309..fb18f7216 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_update.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_update.cpp @@ -156,6 +156,8 @@ void DlgUpdate::finishedUpdateCheck(bool needToUpdate, bool isCompatible, Releas if (reply == QMessageBox::Yes) { downloadUpdate(release->getName()); + } else { + closeDialog(); } } else { QMessageBox::information( From 10b99a74153898131f2214a7ee5a6637ed401f30 Mon Sep 17 00:00:00 2001 From: BruebachL <44814898+BruebachL@users.noreply.github.com> Date: Wed, 5 Aug 2026 20:09:22 +0200 Subject: [PATCH 07/21] [Dialogs] Focus login after selecting server, center update message box (#7080) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [ConnectDialog] Focus login field after selecting a server (#3346) Took 12 minutes * [UpdateDialog] Center update message boxes over the main window (#3538) --------- Co-authored-by: Lukas Brübach --- cockatrice/src/interface/widgets/dialogs/dlg_connect.cpp | 1 + cockatrice/src/interface/widgets/dialogs/dlg_update.cpp | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_connect.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_connect.cpp index f1e7a8ba1..aa8a916f8 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_connect.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_connect.cpp @@ -271,6 +271,7 @@ void DlgConnect::updateDisplayInfo(const QString &saveName) hostEdit->setText(_data.at(1)); portEdit->setText(_data.at(2)); playernameEdit->setText(_data.at(3)); + playernameEdit->setFocus(); savePasswordCheckBox->setChecked(savePasswordStatus); if (savePasswordStatus) { diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_update.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_update.cpp index fb18f7216..7cf58d3e0 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_update.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_update.cpp @@ -134,7 +134,7 @@ void DlgUpdate::finishedUpdateCheck(bool needToUpdate, bool isCompatible, Releas // If there's no need to update, tell them that. However we still allow them to run the // downloader themselves if there's a compatible build QMessageBox::information( - this, tr("No Update Available"), + window(), tr("No Update Available"), tr("Cockatrice is up to date!") + "

" + tr("You are already running the latest version available in the chosen release channel.") + "
" + "" + tr("Current version") + QString(": %1
").arg(VERSION_STRING) + "" + @@ -147,7 +147,7 @@ void DlgUpdate::finishedUpdateCheck(bool needToUpdate, bool isCompatible, Releas if (isCompatible) { int reply; reply = QMessageBox::question( - this, tr("Update Available"), + window(), tr("Update Available"), tr("A new version of Cockatrice is available!") + "

" + "" + tr("New version") + QString(": %1
").arg(release->getName()) + "" + tr("Released") + QString(": %1 (").arg(publishDate, release->getDescriptionUrl()) + tr("Changelog") + @@ -161,7 +161,7 @@ void DlgUpdate::finishedUpdateCheck(bool needToUpdate, bool isCompatible, Releas } } else { QMessageBox::information( - this, tr("Update Available"), + window(), tr("Update Available"), tr("A new version of Cockatrice is available!") + "

" + "" + tr("New version") + QString(": %1
").arg(release->getName()) + "" + tr("Released") + QString(": %1 (
").arg(publishDate, release->getDescriptionUrl()) + tr("Changelog") + From 2bc921321bc582d70a60e3dd61a09d620d2c49b5 Mon Sep 17 00:00:00 2001 From: tooomm Date: Wed, 5 Aug 2026 22:00:06 +0200 Subject: [PATCH 08/21] Update vcpkg submodule to `2026.07.29` release (+ add baseline) (#7067) --- vcpkg | 2 +- vcpkg.json | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/vcpkg b/vcpkg index 56bb24116..9e593bb18 160000 --- a/vcpkg +++ b/vcpkg @@ -1 +1 @@ -Subproject commit 56bb2411609227288b70117ead2c47585ba07713 +Subproject commit 9e593bb18ea69cc5095e012465dcd675a822ed0d diff --git a/vcpkg.json b/vcpkg.json index ac7b75d07..f12227024 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -1,5 +1,6 @@ { "$schema": "https://raw.githubusercontent.com/microsoft/vcpkg-tool/main/docs/vcpkg.schema.json", + "builtin-baseline": "9e593bb18ea69cc5095e012465dcd675a822ed0d", "dependencies": [ "gtest", "liblzma", From 32619f4d557ddb584b2fd52a8215af10978e8ef7 Mon Sep 17 00:00:00 2001 From: tooomm Date: Wed, 5 Aug 2026 23:14:29 +0200 Subject: [PATCH 09/21] Add `--x-abi-tools-use-exact-version` to vcpkg invocation (#7068) --- .ci/compile.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/.ci/compile.sh b/.ci/compile.sh index ee846897b..8a16d3243 100755 --- a/.ci/compile.sh +++ b/.ci/compile.sh @@ -159,6 +159,7 @@ if [[ $PACKAGE_TYPE ]]; then fi if [[ $USE_VCPKG ]]; then flags+=("-DUSE_VCPKG=1") + flags+=("-DVCPKG_INSTALL_OPTIONS=--x-abi-tools-use-exact-versions") fi # Add cmake --build flags From 0d14fb77a077dd834ad6b0276dd41676c99d48df Mon Sep 17 00:00:00 2001 From: RickyRister <42636155+RickyRister@users.noreply.github.com> Date: Fri, 7 Aug 2026 11:50:58 -0700 Subject: [PATCH 10/21] [CardInfo] Refactor: remove properties setters (#7070) --- .../libcockatrice/card/card_info.cpp | 8 ------ .../libcockatrice/card/card_info.h | 14 ---------- .../card/database/card_database_cache.cpp | 9 +++---- .../card/database/parser/cockatrice_xml_3.cpp | 3 +-- .../card/database/parser/cockatrice_xml_4.cpp | 3 +-- .../card/printing/printing_info.cpp | 17 +++++------- .../card/printing/printing_info.h | 26 +++++++------------ oracle/src/oracleimporter.cpp | 3 +-- 8 files changed, 22 insertions(+), 61 deletions(-) diff --git a/libcockatrice_card/libcockatrice/card/card_info.cpp b/libcockatrice_card/libcockatrice/card/card_info.cpp index 786c6cd9f..f03503550 100644 --- a/libcockatrice_card/libcockatrice/card/card_info.cpp +++ b/libcockatrice_card/libcockatrice/card/card_info.cpp @@ -64,14 +64,6 @@ void CardInfo::setProperty(const QString &_name, const QString &_value) emit cardInfoChanged(smartThis); } -void CardInfo::setProperties(const QHash &_props) -{ - ensurePropertiesLoaded(); - propertiesCache = _props; - propertiesBlob = serializeProperties(propertiesCache); - emit cardInfoChanged(smartThis); -} - CardInfo::CardInfo(const QString &_name, const QString &_text, bool _isToken, diff --git a/libcockatrice_card/libcockatrice/card/card_info.h b/libcockatrice_card/libcockatrice/card/card_info.h index e625a0748..a5c208893 100644 --- a/libcockatrice_card/libcockatrice/card/card_info.h +++ b/libcockatrice_card/libcockatrice/card/card_info.h @@ -290,25 +290,11 @@ public: } [[nodiscard]] const QHash &getPropertiesHash() const; - /** - * @brief Stores the pre-serialized properties blob and invalidates the - * materialized cache. Used by the binary cache reader so the - * QHash is not built at load time. - * @param _blob The serialized properties (as written by the cache writer). - */ - void setPropertiesBlob(QByteArray _blob) const - { - QMutexLocker lock(&propertiesMutex); - propertiesBlob = std::move(_blob); - propertiesLoaded = false; - propertiesCache.clear(); - } [[nodiscard]] QString getProperty(const QString &propertyName) const { return getPropertiesHash().value(propertyName); } void setProperty(const QString &_name, const QString &_value); - void setProperties(const QHash &_props); [[nodiscard]] bool hasProperty(const QString &propertyName) const { return getPropertiesHash().contains(propertyName); diff --git a/libcockatrice_card/libcockatrice/card/database/card_database_cache.cpp b/libcockatrice_card/libcockatrice/card/database/card_database_cache.cpp index dfa684dbe..3985889b7 100644 --- a/libcockatrice_card/libcockatrice/card/database/card_database_cache.cpp +++ b/libcockatrice_card/libcockatrice/card/database/card_database_cache.cpp @@ -117,12 +117,9 @@ PrintingInfo readPrinting(QDataStream &in, const SetNameMap &sets) { QString setName = readString(in); QByteArray propsBlob = readHashBlob(in); - PrintingInfo p; - if (auto set = sets.value(setName)) { - p = PrintingInfo(set); - } - p.setPropertiesBlob(propsBlob); - return p; + auto set = sets.value(setName); + + return PrintingInfo(set, propsBlob); } // ---- CardSet --------------------------------------------------------------- diff --git a/libcockatrice_card/libcockatrice/card/database/parser/cockatrice_xml_3.cpp b/libcockatrice_card/libcockatrice/card/database/parser/cockatrice_xml_3.cpp index e403a641d..f3aac7809 100644 --- a/libcockatrice_card/libcockatrice/card/database/parser/cockatrice_xml_3.cpp +++ b/libcockatrice_card/libcockatrice/card/database/parser/cockatrice_xml_3.cpp @@ -228,7 +228,6 @@ void CockatriceXml3Parser::loadCardsFromXml(QXmlStreamReader &xml) // Only load printings from sets the user has enabled, matching the v4 loader's // behaviour. Without this check, disabling a set has no effect on v3 databases. if (set->getEnabled()) { - PrintingInfo setInfo(set); QHash printingProps; if (attrs.hasAttribute("muId")) { printingProps.insert("muid", attrs.value("muId").toString()); @@ -249,7 +248,7 @@ void CockatriceXml3Parser::loadCardsFromXml(QXmlStreamReader &xml) if (attrs.hasAttribute("rarity")) { printingProps.insert("rarity", attrs.value("rarity").toString()); } - setInfo.setProperties(printingProps); + PrintingInfo setInfo(set, printingProps); _sets[setName].append(setInfo); } // related cards diff --git a/libcockatrice_card/libcockatrice/card/database/parser/cockatrice_xml_4.cpp b/libcockatrice_card/libcockatrice/card/database/parser/cockatrice_xml_4.cpp index df92618da..8649bbfaf 100644 --- a/libcockatrice_card/libcockatrice/card/database/parser/cockatrice_xml_4.cpp +++ b/libcockatrice_card/libcockatrice/card/database/parser/cockatrice_xml_4.cpp @@ -314,7 +314,6 @@ void CockatriceXml4Parser::loadCardsFromXml(QXmlStreamReader &xml) QString setName = xml.readElementText(QXmlStreamReader::IncludeChildElements); auto set = internalAddSet(setName); if (set->getEnabled()) { - PrintingInfo printingInfo(set); QHash printingProps; for (QXmlStreamAttribute attr : attrs) { QString attrName = attr.name().toString(); @@ -323,7 +322,7 @@ void CockatriceXml4Parser::loadCardsFromXml(QXmlStreamReader &xml) } printingProps.insert(attrName, attr.value().toString()); } - printingInfo.setProperties(printingProps); + PrintingInfo printingInfo(set, printingProps); // This is very much a hack and not the right place to // put this check, as it requires a reload of Cockatrice diff --git a/libcockatrice_card/libcockatrice/card/printing/printing_info.cpp b/libcockatrice_card/libcockatrice/card/printing/printing_info.cpp index ee5b1329b..49086f8b7 100644 --- a/libcockatrice_card/libcockatrice/card/printing/printing_info.cpp +++ b/libcockatrice_card/libcockatrice/card/printing/printing_info.cpp @@ -5,7 +5,12 @@ #include #include -PrintingInfo::PrintingInfo(const CardSetPtr &_set) : set(_set) +PrintingInfo::PrintingInfo(const CardSetPtr &_set, const QHash &_properties) + : set(_set), propertiesCache(_properties), propertiesLoaded(true) +{ +} + +PrintingInfo::PrintingInfo(const CardSetPtr &_set, const QByteArray &_blob) : set(_set), propertiesBlob(_blob) { } @@ -31,16 +36,6 @@ void PrintingInfo::setProperty(const QString &_name, const QString &_value) return; } propertiesCache.insert(_name, _value); - setProperties(propertiesCache); -} - -void PrintingInfo::setProperties(const QHash &_props) -{ - ensurePropertiesLoaded(); - propertiesCache = _props; - QDataStream out(&propertiesBlob, QIODevice::WriteOnly); - out.setVersion(QDataStream::Qt_6_4); - out << propertiesCache; } /** diff --git a/libcockatrice_card/libcockatrice/card/printing/printing_info.h b/libcockatrice_card/libcockatrice/card/printing/printing_info.h index f7ca90aad..70093b686 100644 --- a/libcockatrice_card/libcockatrice/card/printing/printing_info.h +++ b/libcockatrice_card/libcockatrice/card/printing/printing_info.h @@ -32,8 +32,17 @@ public: * @brief Constructs a PrintingInfo associated with a specific set. * * @param _set The set this printing belongs to (defaults to null). + * @param _properties The printing properties (defaults to empty) */ - explicit PrintingInfo(const CardSetPtr &_set = nullptr); + explicit PrintingInfo(const CardSetPtr &_set = nullptr, const QHash &_properties = {}); + + /** + * @brief Constructs a PrintingInfo associated with a specific set. + * + * @param _set The set this printing belongs to (defaults to null). + * @param _blob The serialized properties (as written by the cache writer). + */ + explicit PrintingInfo(const CardSetPtr &_set, const QByteArray &_blob); /** * @brief Destroys the PrintingInfo. @@ -127,21 +136,6 @@ public: * @param _value The string value to assign. */ void setProperty(const QString &_name, const QString &_value); - void setProperties(const QHash &_props); - - /** - * @brief Stores the pre-serialized properties blob and marks the materialized - * cache as invalid. Used by the binary cache reader to avoid building a - * QHash at load time. - * @param _blob The serialized properties (as written by the cache writer). - */ - void setPropertiesBlob(QByteArray _blob) const - { - QMutexLocker lock(propertiesMutex.data()); - propertiesBlob = std::move(_blob); - propertiesLoaded = false; - propertiesCache.clear(); - } /** * @brief Returns the providerID for this printing. diff --git a/oracle/src/oracleimporter.cpp b/oracle/src/oracleimporter.cpp index 3d7b7b555..a9008c4da 100644 --- a/oracle/src/oracleimporter.cpp +++ b/oracle/src/oracleimporter.cpp @@ -289,7 +289,6 @@ int OracleImporter::importCardsFromSet(const CardSetPtr ¤tSet, const QList } // per-set properties - PrintingInfo printingInfo = PrintingInfo(currentSet); QHash printingProps; for (auto i = setInfoProperties.cbegin(), end = setInfoProperties.cend(); i != end; ++i) { QString mtgjsonProperty = i.key(); @@ -317,7 +316,7 @@ int OracleImporter::importCardsFromSet(const CardSetPtr ¤tSet, const QList } } - printingInfo.setProperties(printingProps); + PrintingInfo printingInfo(currentSet, printingProps); QString numComponent; const QString numProperty = printingInfo.getProperty("num"); From 27fb5e51dedfe7e11aa8eb57754e8ee5332daa85 Mon Sep 17 00:00:00 2001 From: BruebachL <44814898+BruebachL@users.noreply.github.com> Date: Sat, 8 Aug 2026 17:32:39 +0200 Subject: [PATCH 11/21] [Security] Redact sensitive user data from client-visible responses (#7077) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The getUserInfo command for a user that is not currently online returned the full database record, including the account id, the email address and the stored client id, to any logged-in requester. Mirror the redaction already applied to online users via copyUserInfo(): the id and email are only ever exposed to the account owner, and the client id only to moderators. The buddy/ignore add-to-list event likewise returned the target user's email address and client id to the requester. The list entry only needs the public profile fields, so strip the email and client id from it as well. Co-authored-by: Lukas Brübach --- .../network/server/remote/server_protocolhandler.cpp | 9 +++++++++ servatrice/src/serversocketinterface.cpp | 4 ++++ 2 files changed, 13 insertions(+) diff --git a/libcockatrice_network/libcockatrice/network/server/remote/server_protocolhandler.cpp b/libcockatrice_network/libcockatrice/network/server/remote/server_protocolhandler.cpp index c441da781..c3686ddfa 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/server_protocolhandler.cpp +++ b/libcockatrice_network/libcockatrice/network/server/remote/server_protocolhandler.cpp @@ -685,6 +685,15 @@ Response::ResponseCode Server_ProtocolHandler::cmdGetUserInfo(const Command_GetU ServerInfo_User_Container *infoSource = server->findUser(userName); if (!infoSource) { re->mutable_user_info()->CopyFrom(databaseInterface->getUserData(userName, true)); + // The user is not currently online. Mirror the redaction that + // copyUserInfo() applies to online users: the id and email address + // are only ever visible to the account owner, and the client id + // only to moderators. + re->mutable_user_info()->clear_id(); + re->mutable_user_info()->clear_email(); + if (!(userInfo->user_level() & ServerInfo_User::IsModerator)) { + re->mutable_user_info()->clear_clientid(); + } } else { re->mutable_user_info()->CopyFrom( infoSource->copyUserInfo(true, false, userInfo->user_level() & ServerInfo_User::IsModerator)); diff --git a/servatrice/src/serversocketinterface.cpp b/servatrice/src/serversocketinterface.cpp index 6ceebfca9..842ddb4c8 100644 --- a/servatrice/src/serversocketinterface.cpp +++ b/servatrice/src/serversocketinterface.cpp @@ -325,6 +325,10 @@ Response::ResponseCode AbstractServerSocketInterface::cmdAddToList(const Command Event_AddToList event; event.set_list_name(cmd.list()); event.mutable_user_info()->CopyFrom(databaseInterface->getUserData(user)); + // The buddy/ignore list entry is only used to display the user's basic + // profile: never leak the target's email address or client id. + event.mutable_user_info()->clear_email(); + event.mutable_user_info()->clear_clientid(); rc.enqueuePreResponseItem(ServerMessage::SESSION_EVENT, prepareSessionEvent(event)); return Response::RespOk; From 59d90db3c73a03de2ac59ea83567c9540667b91f Mon Sep 17 00:00:00 2001 From: BruebachL <44814898+BruebachL@users.noreply.github.com> Date: Sat, 8 Aug 2026 19:02:19 +0200 Subject: [PATCH 12/21] Define Cockatrice as an editor/handler for .cod files and cockatrice:// protocol on all platforms (#6775) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [Application] Add single instance guard and mime types. Took 2 hours 39 minutes Took 18 minutes Took 5 minutes Took 12 seconds Took 11 seconds * Rework Took 30 minutes Took 50 seconds * Only enforce single instance if launched with arguments. Took 5 minutes * Prototype intents Took 53 minutes Took 6 seconds * Connect/disconnect and join game/room intents. Took 3 hours 14 minutes Took 2 seconds Took 15 seconds * Fix include. Took 1 minute Took 23 seconds Took 2 seconds * Mac handling. Took 10 minutes Took 12 seconds Took 3 minutes * Lint. Took 3 minutes * Rebase. Took 3 minutes Took 17 seconds * Implement UrlSchemeEventFilter Took 10 minutes Took 7 seconds * Qt Moc Took 3 minutes * Modern PList. Took 21 minutes Took 1 minute * Debug output. Took 6 minutes Took 19 minutes * Watch file:// prefix. Took 15 minutes Took 7 seconds * Better handler. Took 6 minutes * Don't store reference in member Took 5 minutes * Move impl to cpp, fix lifetime issues. Took 11 minutes Took 2 minutes * Better single-instance handoff, url intent harded copy game link context-menu Polish for installers Took 35 minutes Took 8 seconds --------- Co-authored-by: Lukas Brübach --- cmake/Info.plist | 90 ++++++++++++- cmake/NSIS.template.in | 26 ++++ cockatrice/CMakeLists.txt | 43 ++++++ cockatrice/cockatrice-cod.xml | 7 + cockatrice/cockatrice.desktop | 4 +- .../src/client/url_scheme_event_filter.h | 69 ++++++++++ .../contexts/context_connect_to_server.h | 14 ++ .../intents/contexts/context_join_game.h | 11 ++ .../intents/contexts/context_join_room.h | 14 ++ cockatrice/src/interface/intents/intent.cpp | 48 +++++++ cockatrice/src/interface/intents/intent.h | 37 +++++ .../intents/intent_connect_to_server.cpp | 46 +++++++ .../intents/intent_connect_to_server.h | 29 ++++ .../intents/intent_disconnect_from_server.cpp | 29 ++++ .../intents/intent_disconnect_from_server.h | 26 ++++ .../intents/intent_join_server_game.cpp | 76 +++++++++++ .../intents/intent_join_server_game.h | 37 +++++ .../intents/intent_join_server_room.cpp | 74 ++++++++++ .../intents/intent_join_server_room.h | 28 ++++ .../src/interface/intents/intent_login.cpp | 33 +++++ .../src/interface/intents/intent_login.h | 23 ++++ .../intents/intent_open_local_deck.cpp | 34 +++++ .../intents/intent_open_local_deck.h | 27 ++++ .../intents/intent_wait_for_database_load.cpp | 19 +++ .../intents/intent_wait_for_database_load.h | 16 +++ .../src/interface/intents/url_parser.cpp | 89 +++++++++++++ cockatrice/src/interface/intents/url_parser.h | 20 +++ .../widgets/server/game_selector.cpp | 42 ++++++ .../interface/widgets/server/game_selector.h | 1 + .../src/interface/widgets/tabs/tab_room.cpp | 1 + .../src/interface/widgets/tabs/tab_room.h | 5 + .../src/interface/widgets/tabs/tab_server.cpp | 12 +- .../src/interface/widgets/tabs/tab_server.h | 8 +- .../interface/widgets/tabs/tab_supervisor.cpp | 4 + .../interface/widgets/tabs/tab_supervisor.h | 6 +- cockatrice/src/interface/window_main.h | 5 + cockatrice/src/main.cpp | 95 ++++++++++++- cockatrice/src/single_instance_manager.cpp | 126 ++++++++++++++++++ cockatrice/src/single_instance_manager.h | 32 +++++ .../network/client/abstract/abstract_client.h | 14 ++ .../network/client/remote/remote_client.h | 16 +++ .../settings/servers_settings.cpp | 45 +++++++ .../libcockatrice/settings/servers_settings.h | 4 + 43 files changed, 1372 insertions(+), 13 deletions(-) create mode 100644 cockatrice/cockatrice-cod.xml create mode 100644 cockatrice/src/client/url_scheme_event_filter.h create mode 100644 cockatrice/src/interface/intents/contexts/context_connect_to_server.h create mode 100644 cockatrice/src/interface/intents/contexts/context_join_game.h create mode 100644 cockatrice/src/interface/intents/contexts/context_join_room.h create mode 100644 cockatrice/src/interface/intents/intent.cpp create mode 100644 cockatrice/src/interface/intents/intent.h create mode 100644 cockatrice/src/interface/intents/intent_connect_to_server.cpp create mode 100644 cockatrice/src/interface/intents/intent_connect_to_server.h create mode 100644 cockatrice/src/interface/intents/intent_disconnect_from_server.cpp create mode 100644 cockatrice/src/interface/intents/intent_disconnect_from_server.h create mode 100644 cockatrice/src/interface/intents/intent_join_server_game.cpp create mode 100644 cockatrice/src/interface/intents/intent_join_server_game.h create mode 100644 cockatrice/src/interface/intents/intent_join_server_room.cpp create mode 100644 cockatrice/src/interface/intents/intent_join_server_room.h create mode 100644 cockatrice/src/interface/intents/intent_login.cpp create mode 100644 cockatrice/src/interface/intents/intent_login.h create mode 100644 cockatrice/src/interface/intents/intent_open_local_deck.cpp create mode 100644 cockatrice/src/interface/intents/intent_open_local_deck.h create mode 100644 cockatrice/src/interface/intents/intent_wait_for_database_load.cpp create mode 100644 cockatrice/src/interface/intents/intent_wait_for_database_load.h create mode 100644 cockatrice/src/interface/intents/url_parser.cpp create mode 100644 cockatrice/src/interface/intents/url_parser.h create mode 100644 cockatrice/src/single_instance_manager.cpp create mode 100644 cockatrice/src/single_instance_manager.h diff --git a/cmake/Info.plist b/cmake/Info.plist index 614d82509..7f01befcb 100644 --- a/cmake/Info.plist +++ b/cmake/Info.plist @@ -1,38 +1,118 @@ - + + + + + + CFBundleDevelopmentRegion English + CFBundleExecutable ${MACOSX_BUNDLE_EXECUTABLE_NAME} + CFBundleGetInfoString ${MACOSX_BUNDLE_INFO_STRING} + CFBundleIconFile ${MACOSX_BUNDLE_ICON_FILE} + CFBundleIdentifier ${MACOSX_BUNDLE_GUI_IDENTIFIER} + CFBundleInfoDictionaryVersion 6.0 + CFBundleLongVersionString ${MACOSX_BUNDLE_LONG_VERSION_STRING} + CFBundleName ${MACOSX_BUNDLE_BUNDLE_NAME} + CFBundlePackageType APPL + CFBundleShortVersionString ${MACOSX_BUNDLE_SHORT_VERSION_STRING} + CFBundleSignature ???? + CFBundleVersion ${MACOSX_BUNDLE_BUNDLE_VERSION} - CSResourcesFileMapped - - LSRequiresCarbon - + NSHumanReadableCopyright ${MACOSX_BUNDLE_COPYRIGHT} + NSHighResolutionCapable + + + + + + UTExportedTypeDeclarations + + + UTTypeIdentifier + org.cockatrice.deck + + UTTypeDescription + Cockatrice Deck + + UTTypeConformsTo + + public.data + + + UTTypeTagSpecification + + public.filename-extension + + cod + + + + + + CFBundleDocumentTypes + + + CFBundleTypeName + Cockatrice Deck + + CFBundleTypeRole + Editor + + LSHandlerRank + Default + + LSItemContentTypes + + org.cockatrice.deck + + + + + + + + + CFBundleURLTypes + + + CFBundleURLName + Cockatrice URL Scheme + + CFBundleURLSchemes + + cockatrice + + + + diff --git a/cmake/NSIS.template.in b/cmake/NSIS.template.in index 5af116470..84b2c38af 100644 --- a/cmake/NSIS.template.in +++ b/cmake/NSIS.template.in @@ -294,6 +294,20 @@ Section "Application" SecApplication SetShellVarContext all SetOutPath "$INSTDIR" +${If} $PortableMode = 0 + + ; --- Register .cod file type --- + WriteRegStr HKCR ".cod" "" "Cockatrice" + WriteRegStr HKCR "Cockatrice" "" "Cockatrice Deck File" + WriteRegStr HKCR "Cockatrice\shell\open\command" "" '"$INSTDIR\cockatrice.exe" "%1"' + + ; --- Register custom URI protocol --- + WriteRegStr HKCR "cockatrice" "" "URL: Cockatrice Protocol" + WriteRegStr HKCR "cockatrice" "URL Protocol" "" + WriteRegStr HKCR "cockatrice\shell\open\command" "" '"$INSTDIR\cockatrice.exe" "%1"' + +${EndIf} + ${If} $PortableMode = 1 ${AndIf} ${FileExists} "$INSTDIR\portable.dat" ; upgrade portable mode @@ -402,6 +416,18 @@ Section "un.Application" UnSecApplication RMDir "$SMPROGRAMS\Cockatrice" DeleteRegKey HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Cockatrice" + + ; Only remove the file/protocol associations if we registered them (i.e. the + ; install was not portable) and .cod is still owned by Cockatrice, so we don't + ; clobber a .cod association installed by another application. + ${If} Not ${FileExists} "$INSTDIR\portable.dat" + ReadRegStr $0 HKCR ".cod" "" + ${If} $0 == "Cockatrice" + DeleteRegKey HKCR ".cod" + DeleteRegKey HKCR "Cockatrice" + DeleteRegKey HKCR "cockatrice" + ${EndIf} + ${EndIf} SectionEnd ; unselected because it is /o diff --git a/cockatrice/CMakeLists.txt b/cockatrice/CMakeLists.txt index cc58c5b43..574c9bc34 100644 --- a/cockatrice/CMakeLists.txt +++ b/cockatrice/CMakeLists.txt @@ -135,6 +135,12 @@ set(cockatrice_SOURCES src/interface/card_picture_loader/card_picture_loader_worker.cpp src/interface/card_picture_loader/card_picture_loader_worker_work.cpp src/interface/card_picture_loader/card_picture_to_load.cpp + src/interface/intents/intent.cpp + src/interface/intents/intent.h + src/interface/intents/intent_open_local_deck.cpp + src/interface/intents/intent_open_local_deck.h + src/interface/intents/intent_wait_for_database_load.cpp + src/interface/intents/intent_wait_for_database_load.h src/interface/layouts/flow_layout.cpp src/interface/layouts/overlap_layout.cpp src/interface/widgets/utility/line_edit_completer.cpp @@ -293,6 +299,7 @@ set(cockatrice_SOURCES src/interface/widgets/visual_deck_storage/visual_deck_storage_widget.cpp src/interface/window_main.cpp src/main.cpp + src/single_instance_manager.cpp src/interface/widgets/tabs/abstract_tab_deck_editor.cpp src/interface/widgets/tabs/api/archidekt/tab_archidekt.cpp src/interface/widgets/tabs/api/archidekt/api_response/archidekt_deck_listing_api_response.cpp @@ -360,6 +367,20 @@ set(cockatrice_SOURCES src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_budget_navigation_widget.h src/interface/widgets/utility/compact_push_button.cpp src/interface/widgets/utility/compact_push_button.h + src/single_instance_manager.h + src/client/url_scheme_event_filter.h + src/interface/intents/intent_connect_to_server.cpp + src/interface/intents/intent_connect_to_server.h + src/interface/intents/intent_disconnect_from_server.cpp + src/interface/intents/intent_disconnect_from_server.h + src/interface/intents/intent_join_server_game.cpp + src/interface/intents/intent_join_server_game.h + src/interface/intents/intent_join_server_room.cpp + src/interface/intents/intent_join_server_room.h + src/interface/intents/intent_login.cpp + src/interface/intents/intent_login.h + src/interface/intents/url_parser.cpp + src/interface/intents/url_parser.h src/interface/widgets/server/user/user_info_popup.cpp src/interface/widgets/server/user/user_info_popup.h ) @@ -419,6 +440,11 @@ set(DESKTOPDIR CACHE STRING "desktop file destination" ) +set(MIMEDIR + share/mime/packages + CACHE STRING "mime file destination" +) + set(COCKATRICE_MAC_QM_INSTALL_DIR "cockatrice.app/Contents/Resources/translations") set(COCKATRICE_UNIX_QM_INSTALL_DIR "share/cockatrice/translations") set(COCKATRICE_WIN32_QM_INSTALL_DIR "translations") @@ -503,6 +529,23 @@ if(UNIX) install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/resources/cockatrice.png DESTINATION ${ICONDIR}/hicolor/48x48/apps) install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/resources/cockatrice.svg DESTINATION ${ICONDIR}/hicolor/scalable/apps) install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/cockatrice.desktop DESTINATION ${DESKTOPDIR}) + install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/cockatrice-cod.xml DESTINATION ${MIMEDIR}) + + # Refresh the freedesktop databases so the file associations and scheme + # handler register without requiring the user to run them manually. The + # tools may be missing on minimal systems; that is fine, packaging systems + # usually refresh these databases through their own triggers. + find_program(UPDATE_MIME_DATABASE update-mime-database) + if(UPDATE_MIME_DATABASE) + install(CODE "execute_process(COMMAND \"${UPDATE_MIME_DATABASE}\" \"${CMAKE_INSTALL_PREFIX}/share/mime\")") + endif() + + find_program(UPDATE_DESKTOP_DATABASE update-desktop-database) + if(UPDATE_DESKTOP_DATABASE) + install( + CODE "execute_process(COMMAND \"${UPDATE_DESKTOP_DATABASE}\" \"${CMAKE_INSTALL_PREFIX}/share/applications\")" + ) + endif() endif() elseif(WIN32) install(TARGETS cockatrice RUNTIME DESTINATION ./) diff --git a/cockatrice/cockatrice-cod.xml b/cockatrice/cockatrice-cod.xml new file mode 100644 index 000000000..1a0199433 --- /dev/null +++ b/cockatrice/cockatrice-cod.xml @@ -0,0 +1,7 @@ + + + + Cockatrice Deck File + + + diff --git a/cockatrice/cockatrice.desktop b/cockatrice/cockatrice.desktop index 092d84ef5..4b15fa9c1 100644 --- a/cockatrice/cockatrice.desktop +++ b/cockatrice/cockatrice.desktop @@ -3,6 +3,8 @@ Version=1.0 Type=Application Name=Cockatrice -Exec=cockatrice +Exec=cockatrice %U Icon=cockatrice Categories=Game;CardGame; +MimeType=application/x-cockatrice; +X-Scheme-Handler/cockatrice=true diff --git a/cockatrice/src/client/url_scheme_event_filter.h b/cockatrice/src/client/url_scheme_event_filter.h new file mode 100644 index 000000000..9e96502ca --- /dev/null +++ b/cockatrice/src/client/url_scheme_event_filter.h @@ -0,0 +1,69 @@ +#ifndef COCKATRICE_URL_SCHEME_EVENT_FILTER_H +#define COCKATRICE_URL_SCHEME_EVENT_FILTER_H + +#include +#include +#include +#include +#include + +/** + * @brief Event filter that catches QFileOpenEvent URLs matching a scheme and + * re-emits them as urlReceived(). + * + * On macOS, when the application is registered as a URL scheme handler, the + * OS delivers incoming URLs via QFileOpenEvent on the QApplication object. + * Install this filter on QApplication to intercept them: + * + * @code + * UrlSchemeEventFilter filter(QStringList{QStringLiteral("cockatrice")}); + * QObject::connect(&filter, &UrlSchemeEventFilter::urlReceived, + * &mainWindow, &MainWindow::handleUrl); + * app.installEventFilter(&filter); + * @endcode + * + * Note: the strings are compared against QUrl::scheme(), so they must be + * written without the "://" suffix (e.g. "cockatrice", not "cockatrice://"). + */ +class UrlSchemeEventFilter : public QObject +{ + Q_OBJECT + +public: + explicit UrlSchemeEventFilter(const QStringList &schemes, QObject *parent = nullptr) + : QObject(parent), prefixes(schemes) + { + } + +signals: + void urlReceived(const QString &url); + +public: + bool eventFilter(QObject *watched, QEvent *event) override + { + if (event->type() == QEvent::FileOpen) { + auto *fileEvent = static_cast(event); + + const QUrl url = fileEvent->url(); + + for (const auto &prefix : prefixes) { + if (url.scheme() == prefix) { + emit urlReceived(url.toString()); + return true; + } + } + + if (url.isLocalFile()) { + emit urlReceived(url.toLocalFile()); + return true; + } + } + + return QObject::eventFilter(watched, event); + } + +private: + QStringList prefixes; +}; + +#endif // COCKATRICE_URL_SCHEME_EVENT_FILTER_H diff --git a/cockatrice/src/interface/intents/contexts/context_connect_to_server.h b/cockatrice/src/interface/intents/contexts/context_connect_to_server.h new file mode 100644 index 000000000..c7c40b261 --- /dev/null +++ b/cockatrice/src/interface/intents/contexts/context_connect_to_server.h @@ -0,0 +1,14 @@ +#ifndef COCKATRICE_CONTEXT_CONNECT_TO_SERVER_H +#define COCKATRICE_CONTEXT_CONNECT_TO_SERVER_H + +#include + +struct ContextConnectToServer +{ + QString hostname; + QString port; + QString username; + QString password; +}; + +#endif // COCKATRICE_CONTEXT_CONNECT_TO_SERVER_H diff --git a/cockatrice/src/interface/intents/contexts/context_join_game.h b/cockatrice/src/interface/intents/contexts/context_join_game.h new file mode 100644 index 000000000..102e2a520 --- /dev/null +++ b/cockatrice/src/interface/intents/contexts/context_join_game.h @@ -0,0 +1,11 @@ +#ifndef COCKATRICE_CONTEXT_JOIN_GAME_H +#define COCKATRICE_CONTEXT_JOIN_GAME_H +#include "context_join_room.h" + +struct ContextJoinGame +{ + ContextJoinRoom roomContext; + int gameId; +}; + +#endif // COCKATRICE_CONTEXT_JOIN_GAME_H diff --git a/cockatrice/src/interface/intents/contexts/context_join_room.h b/cockatrice/src/interface/intents/contexts/context_join_room.h new file mode 100644 index 000000000..23ae05e81 --- /dev/null +++ b/cockatrice/src/interface/intents/contexts/context_join_room.h @@ -0,0 +1,14 @@ +#ifndef COCKATRICE_CONTEXT_JOIN_ROOM_H +#define COCKATRICE_CONTEXT_JOIN_ROOM_H + +#include "context_connect_to_server.h" + +#include + +struct ContextJoinRoom +{ + ContextConnectToServer serverContext; + int roomId; +}; + +#endif // COCKATRICE_CONTEXT_JOIN_ROOM_H diff --git a/cockatrice/src/interface/intents/intent.cpp b/cockatrice/src/interface/intents/intent.cpp new file mode 100644 index 000000000..c02a89f35 --- /dev/null +++ b/cockatrice/src/interface/intents/intent.cpp @@ -0,0 +1,48 @@ +#include "intent.h" + +Intent::Intent(QObject *parent) : QObject(parent) +{ + // An intent is done as soon as it reports success or failure. Deleting it + // also tears down its dependency chain and disconnects any signal wiring. + connect(this, &Intent::finished, this, &QObject::deleteLater); + connect(this, &Intent::failed, this, &QObject::deleteLater); +} + +Intent::~Intent() = default; + +void Intent::execute() +{ + if (checkPrecondition()) { + onPreconditionSatisfied(); + } else { + onPreconditionNotSatisfied(); + } +} + +void Intent::runDependency(Intent *dependency) +{ + dependency->setParent(this); + connect(dependency, &Intent::finished, this, [this]() { + // Re-check after dependency finishes + this->execute(); + }); + connect(dependency, &Intent::failed, this, &Intent::failed); + + dependency->execute(); +} + +void Intent::emitFinished() +{ + if (!completed) { + completed = true; + emit finished(); + } +} + +void Intent::emitFailed(const QString &reason) +{ + if (!completed) { + completed = true; + emit failed(reason); + } +} diff --git a/cockatrice/src/interface/intents/intent.h b/cockatrice/src/interface/intents/intent.h new file mode 100644 index 000000000..125900ecd --- /dev/null +++ b/cockatrice/src/interface/intents/intent.h @@ -0,0 +1,37 @@ +#ifndef COCKATRICE_INTENT_H +#define COCKATRICE_INTENT_H + +#include + +class Intent : public QObject +{ + Q_OBJECT + +public: + explicit Intent(QObject *parent = nullptr); + ~Intent() override; + + void execute(); + +signals: + void finished(); + void failed(QString reason); + +protected: + // --- Subclasses must implement these --- + virtual bool checkPrecondition() const = 0; + virtual void onPreconditionSatisfied() = 0; + virtual void onPreconditionNotSatisfied() = 0; + + // Helper to chain another intent + void runDependency(Intent *dependency); + + // Emit the outcome exactly once; ignore late signals after the intent is done. + void emitFinished(); + void emitFailed(const QString &reason); + +private: + bool completed = false; +}; + +#endif // COCKATRICE_INTENT_H diff --git a/cockatrice/src/interface/intents/intent_connect_to_server.cpp b/cockatrice/src/interface/intents/intent_connect_to_server.cpp new file mode 100644 index 000000000..1cccc5a23 --- /dev/null +++ b/cockatrice/src/interface/intents/intent_connect_to_server.cpp @@ -0,0 +1,46 @@ +#include "intent_connect_to_server.h" + +#include "intent_disconnect_from_server.h" + +#include + +IntentConnectToServer::IntentConnectToServer(RemoteClient *_remoteClient, ContextConnectToServer *_context) + : Intent(), remoteClient(_remoteClient), context(_context) +{ +} + +bool IntentConnectToServer::checkPrecondition() const +{ + return remoteClient->getStatus() == ClientStatus::StatusDisconnected; +} + +void IntentConnectToServer::onPreconditionSatisfied() +{ + remoteClient->connectToServer(context->hostname, context->port.toUInt(), context->username, context->password); + connect(remoteClient, &RemoteClient::statusChanged, this, &IntentConnectToServer::onStatusChanged); + connect(remoteClient, &RemoteClient::socketError, this, &IntentConnectToServer::onSocketError); + connect( + remoteClient, &RemoteClient::loginError, this, + [this](Response::ResponseCode, const QString &reason, quint32, const QList &) { emitFailed(reason); }); + + QTimer::singleShot(15000, this, [this]() { + emitFailed(tr("Timed out while connecting to %1:%2").arg(context->hostname, context->port)); + }); +} + +void IntentConnectToServer::onPreconditionNotSatisfied() +{ + runDependency(new IntentDisconnectFromServer(remoteClient)); +} + +void IntentConnectToServer::onStatusChanged(ClientStatus status) +{ + if (status == ClientStatus::StatusLoggedIn) { + emitFinished(); + } +} + +void IntentConnectToServer::onSocketError(const QString &errorString) +{ + emitFailed(tr("Failed to connect to %1:%2: %3").arg(context->hostname, context->port, errorString)); +} diff --git a/cockatrice/src/interface/intents/intent_connect_to_server.h b/cockatrice/src/interface/intents/intent_connect_to_server.h new file mode 100644 index 000000000..eab4d1a21 --- /dev/null +++ b/cockatrice/src/interface/intents/intent_connect_to_server.h @@ -0,0 +1,29 @@ +#ifndef COCKATRICE_INTENT_CONNECT_TO_SERVER_H +#define COCKATRICE_INTENT_CONNECT_TO_SERVER_H + +#include "contexts/context_connect_to_server.h" +#include "intent.h" +#include "remote_client.h" + +class IntentConnectToServer : public Intent +{ + Q_OBJECT + +public: + IntentConnectToServer(RemoteClient *_remoteClient, ContextConnectToServer *_context); + +protected: + bool checkPrecondition() const override; + void onPreconditionSatisfied() override; + void onPreconditionNotSatisfied() override; + +private: + RemoteClient *remoteClient; + ContextConnectToServer *context; + +private slots: + void onStatusChanged(ClientStatus status); + void onSocketError(const QString &errorString); +}; + +#endif // COCKATRICE_INTENT_CONNECT_TO_SERVER_H diff --git a/cockatrice/src/interface/intents/intent_disconnect_from_server.cpp b/cockatrice/src/interface/intents/intent_disconnect_from_server.cpp new file mode 100644 index 000000000..cb39d7bab --- /dev/null +++ b/cockatrice/src/interface/intents/intent_disconnect_from_server.cpp @@ -0,0 +1,29 @@ +#include "intent_disconnect_from_server.h" + +IntentDisconnectFromServer::IntentDisconnectFromServer(RemoteClient *_remoteClient) + : Intent(), remoteClient(_remoteClient) +{ +} + +bool IntentDisconnectFromServer::checkPrecondition() const +{ + return remoteClient->getStatus() == ClientStatus::StatusDisconnected; +} + +void IntentDisconnectFromServer::onPreconditionSatisfied() +{ + emitFinished(); +} + +void IntentDisconnectFromServer::onPreconditionNotSatisfied() +{ + connect(remoteClient, &RemoteClient::statusChanged, this, &IntentDisconnectFromServer::onStatusChanged); + remoteClient->disconnectFromServer(); +} + +void IntentDisconnectFromServer::onStatusChanged(ClientStatus status) +{ + if (status == ClientStatus::StatusDisconnected) { + emitFinished(); + } +} diff --git a/cockatrice/src/interface/intents/intent_disconnect_from_server.h b/cockatrice/src/interface/intents/intent_disconnect_from_server.h new file mode 100644 index 000000000..6e1dfd0c1 --- /dev/null +++ b/cockatrice/src/interface/intents/intent_disconnect_from_server.h @@ -0,0 +1,26 @@ +#ifndef COCKATRICE_INTENT_DISCONNECT_FROM_SERVER_H +#define COCKATRICE_INTENT_DISCONNECT_FROM_SERVER_H + +#include "intent.h" +#include "remote_client.h" + +class IntentDisconnectFromServer : public Intent +{ + Q_OBJECT + +public: + IntentDisconnectFromServer(RemoteClient *_remoteClient); + +protected: + bool checkPrecondition() const override; + void onPreconditionSatisfied() override; + void onPreconditionNotSatisfied() override; + +private: + RemoteClient *remoteClient; + +private slots: + void onStatusChanged(ClientStatus status); +}; + +#endif // COCKATRICE_INTENT_DISCONNECT_FROM_SERVER_H diff --git a/cockatrice/src/interface/intents/intent_join_server_game.cpp b/cockatrice/src/interface/intents/intent_join_server_game.cpp new file mode 100644 index 000000000..fb9c4d5ce --- /dev/null +++ b/cockatrice/src/interface/intents/intent_join_server_game.cpp @@ -0,0 +1,76 @@ +#include "intent_join_server_game.h" + +#include "../widgets/server/game_selector.h" +#include "../widgets/tabs/tab_room.h" +#include "../widgets/tabs/tab_supervisor.h" +#include "intent_join_server_room.h" + +#include + +IntentJoinServerGame::IntentJoinServerGame(TabSupervisor *_tabSupervisor, + RemoteClient *_remoteClient, + std::unique_ptr _context) + : Intent(), tabSupervisor(_tabSupervisor), remoteClient(_remoteClient), context(_context.release()) +{ +} + +bool IntentJoinServerGame::checkPrecondition() const +{ + if (remoteClient->getStatus() != ClientStatus::StatusLoggedIn) { + return false; + } + // peerPort() reflects the actual TCP peer, which may differ from the + // configured server port (e.g. when connecting through a proxy), so only + // the hostname is compared here. + if (remoteClient->peerName() != context->roomContext.serverContext.hostname) { + return false; + } + if (QString::number(remoteClient->peerPort()) != context->roomContext.serverContext.port) { + return false; + } + + if (!tabSupervisor->getRoomTabs().contains(context->roomContext.roomId)) { + return false; + } + + return true; +} + +void IntentJoinServerGame::onPreconditionSatisfied() +{ + TabRoom *room = tabSupervisor->getRoomTabs().value(context->roomContext.roomId); + if (!tryJoinGame(room)) { + waitForGame(room); + } +} + +void IntentJoinServerGame::onPreconditionNotSatisfied() +{ + runDependency(new IntentJoinServerRoom(tabSupervisor, remoteClient, &context->roomContext)); +} + +bool IntentJoinServerGame::tryJoinGame(TabRoom *room) +{ + if (!room) { + return false; + } + + if (room->getGameSelector()->joinGameById(context->gameId)) { + emitFinished(); + return true; + } + + return false; +} + +void IntentJoinServerGame::waitForGame(TabRoom *room) +{ + connect(room, &TabRoom::gameListUpdated, this, [this]() { + TabRoom *updatedRoom = tabSupervisor->getRoomTabs().value(context->roomContext.roomId); + if (updatedRoom) { + tryJoinGame(updatedRoom); + } + }); + + QTimer::singleShot(15000, this, [this]() { emitFailed(tr("Game %1 not found in the room").arg(context->gameId)); }); +} diff --git a/cockatrice/src/interface/intents/intent_join_server_game.h b/cockatrice/src/interface/intents/intent_join_server_game.h new file mode 100644 index 000000000..5e196df38 --- /dev/null +++ b/cockatrice/src/interface/intents/intent_join_server_game.h @@ -0,0 +1,37 @@ +#ifndef COCKATRICE_INTENT_JOIN_SERVER_GAME_H +#define COCKATRICE_INTENT_JOIN_SERVER_GAME_H + +#include "contexts/context_join_game.h" +#include "intent.h" +#include "remote_client.h" + +#include +#include + +class TabRoom; +class TabSupervisor; + +class IntentJoinServerGame : public Intent +{ + Q_OBJECT + +public: + IntentJoinServerGame(TabSupervisor *_tabSupervisor, + RemoteClient *_remoteClient, + std::unique_ptr _context); + +protected: + bool checkPrecondition() const override; + void onPreconditionSatisfied() override; + void onPreconditionNotSatisfied() override; + +private: + bool tryJoinGame(TabRoom *room); + void waitForGame(TabRoom *room); + + TabSupervisor *tabSupervisor; + RemoteClient *remoteClient; + QScopedPointer context; +}; + +#endif // COCKATRICE_INTENT_JOIN_SERVER_GAME_H diff --git a/cockatrice/src/interface/intents/intent_join_server_room.cpp b/cockatrice/src/interface/intents/intent_join_server_room.cpp new file mode 100644 index 000000000..d25bc8d17 --- /dev/null +++ b/cockatrice/src/interface/intents/intent_join_server_room.cpp @@ -0,0 +1,74 @@ +#include "intent_join_server_room.h" + +#include "../widgets/tabs/tab_room.h" +#include "../widgets/tabs/tab_server.h" +#include "../widgets/tabs/tab_supervisor.h" +#include "intent_connect_to_server.h" + +#include +#include + +IntentJoinServerRoom::IntentJoinServerRoom(TabSupervisor *_tabSupervisor, + RemoteClient *_remoteClient, + ContextJoinRoom *_context) + : Intent(), tabSupervisor(_tabSupervisor), remoteClient(_remoteClient), context(_context) +{ +} + +bool IntentJoinServerRoom::checkPrecondition() const +{ + if (remoteClient->getStatus() != ClientStatus::StatusLoggedIn) { + return false; + } + // peerPort() reflects the actual TCP peer, which may differ from the + // configured server port (e.g. when connecting through a proxy), so only + // the hostname is compared here. + if (remoteClient->peerName() != context->serverContext.hostname) { + return false; + } + if (QString::number(remoteClient->peerPort()) != context->serverContext.port) { + return false; + } + + return true; +} + +void IntentJoinServerRoom::onPreconditionSatisfied() +{ + if (tabSupervisor->getRoomTabs().contains(context->roomId)) { + tabSupervisor->setCurrentWidget(tabSupervisor->getRoomTabs().value(context->roomId)); + emitFinished(); + return; + } + + TabServer *tabServer = tabSupervisor->getTabServer(); + if (!tabServer) { + tabSupervisor->openTabServer(); + tabServer = tabSupervisor->getTabServer(); + } + if (!tabServer) { + emitFailed(tr("No server tab available")); + return; + } + + const int roomId = context->roomId; + tabServer->joinRoom(roomId, true); + connect(tabServer, &TabServer::roomJoined, this, [this, roomId](const ServerInfo_Room &info, bool) { + if (info.room_id() == roomId) { + emitFinished(); + } + }); + connect(tabServer, &TabServer::roomJoinFailed, this, [this, roomId](int failedRoomId) { + if (failedRoomId == roomId) { + emitFailed(tr("Failed to join the server room %1").arg(roomId)); + } + }); + + QTimer::singleShot(15000, this, + [this, roomId]() { emitFailed(tr("Timed out while joining the server room %1").arg(roomId)); }); +} + +void IntentJoinServerRoom::onPreconditionNotSatisfied() +{ + runDependency(new IntentConnectToServer(remoteClient, &context->serverContext)); +} diff --git a/cockatrice/src/interface/intents/intent_join_server_room.h b/cockatrice/src/interface/intents/intent_join_server_room.h new file mode 100644 index 000000000..4a5599896 --- /dev/null +++ b/cockatrice/src/interface/intents/intent_join_server_room.h @@ -0,0 +1,28 @@ +#ifndef COCKATRICE_INTENT_JOIN_SERVER_ROOM_H +#define COCKATRICE_INTENT_JOIN_SERVER_ROOM_H + +#include "contexts/context_join_room.h" +#include "intent.h" +#include "remote_client.h" + +class TabSupervisor; + +class IntentJoinServerRoom : public Intent +{ + Q_OBJECT + +public: + IntentJoinServerRoom(TabSupervisor *_tabSupervisor, RemoteClient *_remoteClient, ContextJoinRoom *_context); + +protected: + bool checkPrecondition() const override; + void onPreconditionSatisfied() override; + void onPreconditionNotSatisfied() override; + +private: + TabSupervisor *tabSupervisor; + RemoteClient *remoteClient; + ContextJoinRoom *context; +}; + +#endif // COCKATRICE_INTENT_JOIN_SERVER_ROOM_H diff --git a/cockatrice/src/interface/intents/intent_login.cpp b/cockatrice/src/interface/intents/intent_login.cpp new file mode 100644 index 000000000..ff871fd03 --- /dev/null +++ b/cockatrice/src/interface/intents/intent_login.cpp @@ -0,0 +1,33 @@ +#include "intent_login.h" + +#include "../../client/settings/cache_settings.h" +#include "libcockatrice/settings/servers_settings.h" + +IntentGetLoginCredentials::IntentGetLoginCredentials(ContextConnectToServer *_context) : Intent(), context(_context) +{ +} + +bool IntentGetLoginCredentials::checkPrecondition() const +{ + ServersSettings &servers = SettingsCache::instance().servers(); + return servers.hasLoginData(context->hostname, context->port); +} + +void IntentGetLoginCredentials::onPreconditionSatisfied() +{ + ServersSettings &servers = SettingsCache::instance().servers(); + const int index = servers.findServerIndex(context->hostname, context->port); + + if (index >= 0) { + context->username = servers.getValue(QString("username%1").arg(index), "server", "server_details").toString(); + context->password = servers.getValue(QString("password%1").arg(index), "server", "server_details").toString(); + emitFinished(); + } else { + emitFailed(tr("No saved credentials for this server")); + } +} + +void IntentGetLoginCredentials::onPreconditionNotSatisfied() +{ + emitFailed(tr("No saved credentials for this server")); +} diff --git a/cockatrice/src/interface/intents/intent_login.h b/cockatrice/src/interface/intents/intent_login.h new file mode 100644 index 000000000..c7fec92b7 --- /dev/null +++ b/cockatrice/src/interface/intents/intent_login.h @@ -0,0 +1,23 @@ +#ifndef COCKATRICE_INTENT_LOGIN_H +#define COCKATRICE_INTENT_LOGIN_H + +#include "contexts/context_connect_to_server.h" +#include "intent.h" + +class IntentGetLoginCredentials : public Intent +{ + Q_OBJECT + +public: + IntentGetLoginCredentials(ContextConnectToServer *_context); + +protected: + bool checkPrecondition() const override; + void onPreconditionSatisfied() override; + void onPreconditionNotSatisfied() override; + +private: + ContextConnectToServer *context; +}; + +#endif // COCKATRICE_INTENT_LOGIN_H diff --git a/cockatrice/src/interface/intents/intent_open_local_deck.cpp b/cockatrice/src/interface/intents/intent_open_local_deck.cpp new file mode 100644 index 000000000..2457bec72 --- /dev/null +++ b/cockatrice/src/interface/intents/intent_open_local_deck.cpp @@ -0,0 +1,34 @@ +#include "intent_open_local_deck.h" + +#include "../deck_loader/deck_file_format.h" +#include "../deck_loader/deck_loader.h" +#include "../widgets/tabs/tab_supervisor.h" +#include "intent_wait_for_database_load.h" + +#include + +IntentOpenLocalDeck::IntentOpenLocalDeck(TabSupervisor *_tabSupervisor, const QString &_file) + : Intent(), tabSupervisor(_tabSupervisor), file(_file) +{ +} + +bool IntentOpenLocalDeck::checkPrecondition() const +{ + return CardDatabaseManager::getInstance()->getLoadStatus() == LoadStatus::Ok; +} + +void IntentOpenLocalDeck::onPreconditionSatisfied() +{ + std::optional deckOpt = DeckLoader::loadFromFile(file, DeckFileFormat::getFormatFromName(file), true); + if (deckOpt) { + tabSupervisor->openDeckInNewTab(deckOpt.value()); + emitFinished(); + } else { + emitFailed(tr("Unable to load deck file %1").arg(file)); + } +} + +void IntentOpenLocalDeck::onPreconditionNotSatisfied() +{ + runDependency(new IntentWaitForDatabaseLoad); +} diff --git a/cockatrice/src/interface/intents/intent_open_local_deck.h b/cockatrice/src/interface/intents/intent_open_local_deck.h new file mode 100644 index 000000000..97f875e39 --- /dev/null +++ b/cockatrice/src/interface/intents/intent_open_local_deck.h @@ -0,0 +1,27 @@ +#ifndef COCKATRICE_INTENT_OPEN_LOCAL_DECK_H +#define COCKATRICE_INTENT_OPEN_LOCAL_DECK_H + +#include "intent.h" + +#include + +class TabSupervisor; + +class IntentOpenLocalDeck : public Intent +{ + Q_OBJECT + +public: + IntentOpenLocalDeck(TabSupervisor *_tabSupervisor, const QString &_file); + +protected: + bool checkPrecondition() const override; + void onPreconditionSatisfied() override; + void onPreconditionNotSatisfied() override; + +private: + TabSupervisor *tabSupervisor; + QString file; +}; + +#endif // COCKATRICE_INTENT_OPEN_LOCAL_DECK_H diff --git a/cockatrice/src/interface/intents/intent_wait_for_database_load.cpp b/cockatrice/src/interface/intents/intent_wait_for_database_load.cpp new file mode 100644 index 000000000..c36378818 --- /dev/null +++ b/cockatrice/src/interface/intents/intent_wait_for_database_load.cpp @@ -0,0 +1,19 @@ +#include "intent_wait_for_database_load.h" + +#include + +bool IntentWaitForDatabaseLoad::checkPrecondition() const +{ + return CardDatabaseManager::getInstance()->getLoadStatus() == LoadStatus::Ok; +} + +void IntentWaitForDatabaseLoad::onPreconditionSatisfied() +{ + emitFinished(); +} + +void IntentWaitForDatabaseLoad::onPreconditionNotSatisfied() +{ + connect(CardDatabaseManager::getInstance(), &CardDatabase::cardDatabaseLoadingFinished, this, + [this]() { emitFinished(); }); +} diff --git a/cockatrice/src/interface/intents/intent_wait_for_database_load.h b/cockatrice/src/interface/intents/intent_wait_for_database_load.h new file mode 100644 index 000000000..72f4a1ffc --- /dev/null +++ b/cockatrice/src/interface/intents/intent_wait_for_database_load.h @@ -0,0 +1,16 @@ +#ifndef COCKATRICE_INTENT_WAIT_FOR_DATABASE_LOAD_H +#define COCKATRICE_INTENT_WAIT_FOR_DATABASE_LOAD_H + +#include "intent.h" + +class IntentWaitForDatabaseLoad : public Intent +{ + Q_OBJECT + +protected: + bool checkPrecondition() const override; + void onPreconditionSatisfied() override; + void onPreconditionNotSatisfied() override; +}; + +#endif // COCKATRICE_INTENT_WAIT_FOR_DATABASE_LOAD_H diff --git a/cockatrice/src/interface/intents/url_parser.cpp b/cockatrice/src/interface/intents/url_parser.cpp new file mode 100644 index 000000000..8b5309603 --- /dev/null +++ b/cockatrice/src/interface/intents/url_parser.cpp @@ -0,0 +1,89 @@ +#include "url_parser.h" + +#include "../window_main.h" +#include "contexts/context_join_game.h" +#include "intent_join_server_game.h" +#include "intent_login.h" + +#include +#include +#include +#include +#include + +IntentUrlParser::IntentUrlParser(QObject *parent, MainWindow *_mainWindow) : QObject(parent), mainWindow(_mainWindow) +{ +} + +void IntentUrlParser::handle(const QString &urlStr) +{ + QUrl url(urlStr); + + if (url.scheme() != "cockatrice") { + return; + } + + const QString action = url.host(); + QUrlQuery query(url); + + if (action == "joingame") { + handleJoinGame(query); + } else if (action == "opendeck") { + // handleOpenDeck(query); + } else { + qWarning() << "Unknown intent:" << action; + } +} + +void IntentUrlParser::handleJoinGame(const QUrlQuery &query) +{ + auto showError = [this](const QString &message) { QMessageBox::warning(mainWindow, tr("Open game"), message); }; + + auto ctx = std::make_unique(); + + ctx->roomContext.serverContext.hostname = query.queryItemValue("hostname"); + ctx->roomContext.serverContext.port = query.queryItemValue("port"); + + if (ctx->roomContext.serverContext.hostname.isEmpty()) { + showError(tr("Missing or empty hostname in the game link")); + return; + } + + bool ok = false; + ctx->roomContext.serverContext.port.toUShort(&ok); + if (!ok) { + showError(tr("Invalid or missing port in the game link")); + return; + } + + ctx->roomContext.roomId = query.queryItemValue("roomid").toInt(&ok); + + if (!ok) { + showError(tr("Invalid or missing room id in the game link")); + return; + } + + ok = false; + ctx->gameId = query.queryItemValue("gameid").toInt(&ok); + + if (!ok) { + showError(tr("Invalid or missing game id in the game link")); + return; + } + + // The join game intent owns the context and the credential lookup; once the + // chain finishes (or fails) it deletes the whole tree. + ContextConnectToServer *serverContext = &ctx->roomContext.serverContext; + auto joinGameIntent = + new IntentJoinServerGame(mainWindow->getTabSupervisor(), mainWindow->getRemoteClient(), std::move(ctx)); + joinGameIntent->setParent(this); + + auto getLoginCredentialsIntent = new IntentGetLoginCredentials(serverContext); + getLoginCredentialsIntent->setParent(joinGameIntent); + + connect(getLoginCredentialsIntent, &Intent::finished, joinGameIntent, &Intent::execute); + connect(getLoginCredentialsIntent, &Intent::failed, joinGameIntent, &Intent::failed); + connect(joinGameIntent, &Intent::failed, this, [showError](const QString &reason) { showError(reason); }); + + getLoginCredentialsIntent->execute(); +} diff --git a/cockatrice/src/interface/intents/url_parser.h b/cockatrice/src/interface/intents/url_parser.h new file mode 100644 index 000000000..bac0e3d25 --- /dev/null +++ b/cockatrice/src/interface/intents/url_parser.h @@ -0,0 +1,20 @@ +#ifndef COCKATRICE_URL_PARSER_H +#define COCKATRICE_URL_PARSER_H +#include +#include + +class MainWindow; +class IntentUrlParser : public QObject +{ + Q_OBJECT + +public: + IntentUrlParser(QObject *parent, MainWindow *mainWindow); + void handle(const QString &urlStr); + void handleJoinGame(const QUrlQuery &query); + +private: + MainWindow *mainWindow; +}; + +#endif // COCKATRICE_URL_PARSER_H diff --git a/cockatrice/src/interface/widgets/server/game_selector.cpp b/cockatrice/src/interface/widgets/server/game_selector.cpp index e9fa3c3cf..11b36ca92 100644 --- a/cockatrice/src/interface/widgets/server/game_selector.cpp +++ b/cockatrice/src/interface/widgets/server/game_selector.cpp @@ -10,12 +10,16 @@ #include "games_model.h" #include "user/user_list_manager.h" +#include #include +#include #include #include #include #include #include +#include +#include #include #include #include @@ -315,6 +319,21 @@ void GameSelector::customContextMenu(const QPoint &point) dlg.exec(); }); + QAction copyLink(tr("Copy Game Link")); + connect(©Link, &QAction::triggered, this, [=, this]() { + const ServerInfo_Game &gameInfo = gameListModel->getGame(index.data(Qt::UserRole).toInt()); + QUrl url; + url.setScheme("cockatrice"); + url.setHost("joingame"); + QUrlQuery query; + query.addQueryItem("hostname", client->serverName()); + query.addQueryItem("port", QString::number(client->serverPort())); + query.addQueryItem("roomid", QString::number(gameInfo.room_id())); + query.addQueryItem("gameid", QString::number(gameInfo.game_id())); + url.setQuery(query); + QGuiApplication::clipboard()->setText(url.toString(QUrl::FullyEncoded)); + }); + QMenu menu; menu.addAction(&joinGame); @@ -332,6 +351,11 @@ void GameSelector::customContextMenu(const QPoint &point) menu.addAction(&spectateGame); menu.addAction(&getGameInfo); + + if (!client->serverName().isEmpty()) { + menu.addAction(©Link); + } + menu.exec(gameListView->mapToGlobal(point)); } @@ -379,6 +403,24 @@ void GameSelector::joinGame(const bool asSpectator, const bool asJudge) disableButtons(); } +bool GameSelector::joinGameById(int gameId) +{ + auto *model = gameListView->model(); + + for (int row = 0; row < model->rowCount(); ++row) { + QModelIndex idx = model->index(row, 0); + const ServerInfo_Game &game = gameListModel->getGame(idx.data(Qt::UserRole).toInt()); + if (game.game_id() == gameId) { + gameListView->setCurrentIndex(idx); + joinGame(); + return true; + } + } + + qWarning() << "Game" << gameId << "not found"; + return false; +} + void GameSelector::disableButtons() { if (createButton) { diff --git a/cockatrice/src/interface/widgets/server/game_selector.h b/cockatrice/src/interface/widgets/server/game_selector.h index fa91e5f96..da34d5322 100644 --- a/cockatrice/src/interface/widgets/server/game_selector.h +++ b/cockatrice/src/interface/widgets/server/game_selector.h @@ -202,6 +202,7 @@ public: * @param info The ServerInfo_Game object containing information about the game to update. */ void processGameInfo(const ServerInfo_Game &info); + bool joinGameById(int gameId); }; #endif diff --git a/cockatrice/src/interface/widgets/tabs/tab_room.cpp b/cockatrice/src/interface/widgets/tabs/tab_room.cpp index 5cf400099..899f38ec2 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_room.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_room.cpp @@ -280,6 +280,7 @@ void TabRoom::processListGamesEvent(const Event_ListGames &event) for (int i = 0; i < gameListSize; ++i) { gameSelector->processGameInfo(event.game_list(i)); } + emit gameListUpdated(); } void TabRoom::processJoinRoomEvent(const Event_JoinRoom &event) diff --git a/cockatrice/src/interface/widgets/tabs/tab_room.h b/cockatrice/src/interface/widgets/tabs/tab_room.h index d669b6107..2881c25f4 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_room.h +++ b/cockatrice/src/interface/widgets/tabs/tab_room.h @@ -78,6 +78,7 @@ signals: void openMessageDialog(const QString &userName, bool focus); void maximizeClient(); void notIdle(); + void gameListUpdated(); private slots: void sendMessage(); void sayFinished(const Response &response); @@ -127,6 +128,10 @@ public: { return ownUser; } + [[nodiscard]] GameSelector *getGameSelector() const + { + return gameSelector; + } PendingCommand *prepareRoomCommand(const ::google::protobuf::Message &cmd); void sendRoomCommand(PendingCommand *pend); diff --git a/cockatrice/src/interface/widgets/tabs/tab_server.cpp b/cockatrice/src/interface/widgets/tabs/tab_server.cpp index 2fce5c1fa..13a77e957 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_server.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_server.cpp @@ -191,7 +191,10 @@ void TabServer::joinRoom(int id, bool setCurrent) PendingCommand *pend = client->prepareSessionCommand(cmd); pend->setExtraData(setCurrent); - connect(pend, &PendingCommand::finished, this, &TabServer::joinRoomFinished); + connect(pend, &PendingCommand::finished, this, + [this, id](const Response &r, const CommandContainer &c, const QVariant &v) { + joinRoomFinished(r, c, v, id); + }); client->sendCommand(pend); @@ -205,7 +208,8 @@ void TabServer::joinRoom(int id, bool setCurrent) void TabServer::joinRoomFinished(const Response &r, const CommandContainer & /*commandContainer*/, - const QVariant &extraData) + const QVariant &extraData, + int roomId) { switch (r.response_code()) { case Response::RespOk: @@ -213,21 +217,25 @@ void TabServer::joinRoomFinished(const Response &r, case Response::RespNameNotFound: QMessageBox::critical(this, tr("Error"), tr("Failed to join the server room: it doesn't exist on the server.")); + emit roomJoinFailed(roomId); return; case Response::RespContextError: QMessageBox::critical( this, tr("Error"), tr("The server thinks you are in the server room but your client is unable to display it. " "Try restarting your client.")); + emit roomJoinFailed(roomId); return; case Response::RespUserLevelTooLow: QMessageBox::critical(this, tr("Error"), tr("You do not have the required permission to join this server room.")); + emit roomJoinFailed(roomId); return; default: QMessageBox::critical( this, tr("Error"), tr("Failed to join the server room due to an unknown error: %1.").arg(r.response_code())); + emit roomJoinFailed(roomId); return; } diff --git a/cockatrice/src/interface/widgets/tabs/tab_server.h b/cockatrice/src/interface/widgets/tabs/tab_server.h index 137823592..c10b7945b 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_server.h +++ b/cockatrice/src/interface/widgets/tabs/tab_server.h @@ -49,10 +49,13 @@ class TabServer : public Tab Q_OBJECT signals: void roomJoined(const ServerInfo_Room &info, bool setCurrent); + void roomJoinFailed(int roomId); private slots: void processServerMessageEvent(const Event_ServerMessage &event); - void joinRoom(int id, bool setCurrent); - void joinRoomFinished(const Response &resp, const CommandContainer &commandContainer, const QVariant &extraData); + void joinRoomFinished(const Response &resp, + const CommandContainer &commandContainer, + const QVariant &extraData, + int roomId); private: AbstractClient *client; @@ -62,6 +65,7 @@ private: public: TabServer(TabSupervisor *_tabSupervisor, AbstractClient *_client); + void joinRoom(int id, bool setCurrent); void retranslateUi() override; [[nodiscard]] QString getTabText() const override { diff --git a/cockatrice/src/interface/widgets/tabs/tab_supervisor.cpp b/cockatrice/src/interface/widgets/tabs/tab_supervisor.cpp index c9478ee0b..3f30ba8be 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_supervisor.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_supervisor.cpp @@ -587,6 +587,10 @@ void TabSupervisor::actTabServer(bool checked) void TabSupervisor::openTabServer() { + if (tabServer) { + return; + } + tabServer = new TabServer(this, client); connect(tabServer, &TabServer::roomJoined, this, &TabSupervisor::addRoomTab); myAddTab(tabServer, aTabServer); diff --git a/cockatrice/src/interface/widgets/tabs/tab_supervisor.h b/cockatrice/src/interface/widgets/tabs/tab_supervisor.h index 3eac144b7..e6c009fda 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_supervisor.h +++ b/cockatrice/src/interface/widgets/tabs/tab_supervisor.h @@ -152,6 +152,10 @@ public: { return userListManager; } + [[nodiscard]] TabServer *getTabServer() const + { + return tabServer; + } [[nodiscard]] const QMap &getRoomTabs() const { return roomTabs; @@ -183,6 +187,7 @@ public slots: void maximizeMainWindow(); void actTabVisualDeckStorage(bool checked); void actTabReplays(bool checked); + void openTabServer(); private slots: void refreshShortcuts(); @@ -195,7 +200,6 @@ private slots: void openTabVisualDeckStorage(); void openTabHome(); - void openTabServer(); void openTabAccount(); void openTabDeckStorage(); void openTabReplays(); diff --git a/cockatrice/src/interface/window_main.h b/cockatrice/src/interface/window_main.h index 5f631ddc3..610f11965 100644 --- a/cockatrice/src/interface/window_main.h +++ b/cockatrice/src/interface/window_main.h @@ -150,6 +150,11 @@ public: } ~MainWindow() override; + RemoteClient *getRemoteClient() const + { + return connectionController->client(); + } + TabSupervisor *getTabSupervisor() const { return tabSupervisor; diff --git a/cockatrice/src/main.cpp b/cockatrice/src/main.cpp index dbfd2b6b7..0524112e4 100644 --- a/cockatrice/src/main.cpp +++ b/cockatrice/src/main.cpp @@ -23,12 +23,17 @@ #include "client/network/update/card_spoiler/spoiler_background_updater.h" #include "client/settings/cache_settings.h" #include "client/sound_engine.h" +#include "client/url_scheme_event_filter.h" #include "database/interface/settings_card_preference_provider.h" +#include "interface/intents/intent_open_local_deck.h" +#include "interface/intents/url_parser.h" #include "interface/logger.h" #include "interface/pixel_map_generator.h" #include "interface/theme_manager.h" #include "interface/widgets/dialogs/dlg_settings.h" +#include "interface/widgets/tabs/tab_supervisor.h" #include "interface/window_main.h" +#include "single_instance_manager.h" #include "version_string.h" #include @@ -37,6 +42,7 @@ #include #include #include +#include #include #include #include @@ -177,6 +183,7 @@ int main(int argc, char *argv[]) SetUnhandledExceptionFilter(CockatriceUnhandledExceptionFilter); #endif + // Logging setup #ifdef Q_OS_APPLE // /cockatrice/cockatrice.app/Contents/MacOS/cockatrice const QByteArray configPath = "../../../qtlogging.ini"; @@ -194,15 +201,29 @@ int main(int argc, char *argv[]) // Set the QT_LOGGING_CONF environment variable qputenv("QT_LOGGING_CONF", configPath); } + qSetMessagePattern( "\033[0m[%{time yyyy-MM-dd h:mm:ss.zzz} " "%{if-debug}\033[36mD%{endif}%{if-info}\033[32mI%{endif}%{if-warning}\033[33mW%{endif}%{if-critical}\033[31mC%{" "endif}%{if-fatal}\033[1;31mF%{endif}\033[0m] [%{function}] - %{message} [%{file}:%{line}]"); QApplication app(argc, argv); +#ifdef Q_OS_MAC + UrlSchemeEventFilter cockatriceFilter(QStringList{QStringLiteral("cockatrice")}); + + QStringList pendingMacUrls; + + const auto cocoaBufferConn = + QObject::connect(&cockatriceFilter, &UrlSchemeEventFilter::urlReceived, + [&pendingMacUrls](const QString &url) { pendingMacUrls.append(url); }); + + app.installEventFilter(&cockatriceFilter); +#endif + QObject::connect(&app, &QApplication::lastWindowClosed, &app, &QApplication::quit); qInstallMessageHandler(CockatriceLogger); + #ifdef Q_OS_WIN app.addLibraryPath(app.applicationDirPath() + "/plugins"); #endif @@ -218,6 +239,7 @@ int main(int argc, char *argv[]) qApp->setAttribute(Qt::AA_DontShowIconsInMenus, true); #endif + // Translations #ifdef Q_OS_MAC translationPath = qApp->applicationDirPath() + "/../Resources/translations"; #elif defined(Q_OS_WIN) @@ -226,6 +248,7 @@ int main(int argc, char *argv[]) translationPath = qApp->applicationDirPath() + "/../share/cockatrice/translations"; #endif + // Command-line parser QCommandLineParser parser; parser.setApplicationDescription("Cockatrice"); parser.addHelpOption(); @@ -241,6 +264,35 @@ int main(int argc, char *argv[]) Logger::getInstance().logToFile(true); } + // --- Handle files or URLs passed at startup --- + // Only positional arguments are treated as files/URLs, so options like + // --connect are never handed off to another instance. + const QStringList startupFiles = parser.positionalArguments(); + const bool hasActivationFiles = !startupFiles.isEmpty(); + + SingleInstanceManager instance; + + if (hasActivationFiles) { + // Activation launch: hand off to the primary instance if one is + // running, otherwise become the primary ourselves. Do this before + // constructing the main window so a hand-off exits cheaply. + if (!instance.tryRun(startupFiles)) { + // Sent successfully → exit + return 0; + } + // No primary instance → become server + qInfo() << "No existing instance found, becoming primary instance"; + } else { + // Plain launch: if another instance is running, run independently + // instead of handing off and exiting. + if (!instance.tryRun(QStringList())) { + // Another instance is already running → just run independently + qInfo() << "Another instance exists, running independently"; + } else { + qInfo() << "No existing instance found, starting server"; + } + } + rng = new RNG_SFMT; themeManager = new ThemeManager; soundEngine = new SoundEngine; @@ -272,6 +324,26 @@ int main(int argc, char *argv[]) CardDatabaseManager::getInstance()->loadCardDatabases(); MainWindow ui; + + auto handleActivation = [&ui](const QString &file) { + if (file.startsWith("cockatrice://")) { + auto urlParser = new IntentUrlParser(&ui, &ui); + urlParser->handle(file); + } else if (QFileInfo(file).exists()) { + auto openDeckIntent = new IntentOpenLocalDeck(ui.getTabSupervisor(), file); + QObject::connect(openDeckIntent, &Intent::failed, &ui, [&ui](const QString &reason) { + QMessageBox::warning(&ui, QObject::tr("Open deck"), reason); + }); + openDeckIntent->execute(); + } + }; + +#ifdef Q_OS_MAC + QObject::disconnect(cocoaBufferConn); + + QObject::connect(&cockatriceFilter, &UrlSchemeEventFilter::urlReceived, + [&handleActivation](const QString &url) { handleActivation(url); }); +#endif if (parser.isSet("connect")) { ui.setConnectTo(parser.value("connect")); } @@ -297,7 +369,26 @@ int main(int argc, char *argv[]) #if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0)) app.setAttribute(Qt::AA_UseHighDpiPixmaps); #endif - app.exec(); + +#ifdef Q_OS_MAC + for (const QString &url : pendingMacUrls) { + handleActivation(url); + } + pendingMacUrls.clear(); +#endif + + for (const QString &file : startupFiles) { + handleActivation(file); + } + + // Connect to future file/URL events from other instances + QObject::connect(&instance, &SingleInstanceManager::filesReceived, [&handleActivation](const QStringList &files) { + for (const QString &file : files) { + handleActivation(file); + } + }); + + int ret = app.exec(); qCInfo(MainLog) << "Event loop finished, terminating..."; delete rng; @@ -305,5 +396,5 @@ int main(int argc, char *argv[]) CountryPixmapGenerator::clear(); UserLevelPixmapGenerator::clear(); - return 0; + return ret; } diff --git a/cockatrice/src/single_instance_manager.cpp b/cockatrice/src/single_instance_manager.cpp new file mode 100644 index 000000000..aca23160c --- /dev/null +++ b/cockatrice/src/single_instance_manager.cpp @@ -0,0 +1,126 @@ +#include "single_instance_manager.h" + +#include + +SingleInstanceManager::SingleInstanceManager(QObject *parent) : QObject(parent) +{ +} + +bool SingleInstanceManager::tryRun(const QStringList &filesToSend) +{ + // Scope the socket name to the current user. On Linux the default abstract + // namespace is system-wide, so a plain name would let one user's instance + // hijack another user's session. + QString userName = qEnvironmentVariable("USER"); + if (userName.isEmpty()) { + userName = qEnvironmentVariable("USERNAME"); + } + if (userName.isEmpty()) { + userName = QDir::home().dirName(); + } + serverName = QStringLiteral("CockatriceSingleInstance-%1").arg(userName); + + // Hand off to an already-running primary instance if one exists. + if (forwardToPrimary(filesToSend)) { + return false; + } + + // No primary instance is currently reachable, so become the primary. + server = new QLocalServer(this); + connect(server, &QLocalServer::newConnection, this, &SingleInstanceManager::handleNewConnection); + + if (server->listen(serverName)) { + return true; + } + + // Another instance may have started while we were probing; hand off to it + // instead of stealing its socket. + if (forwardToPrimary(filesToSend)) { + return false; + } + + // The socket is stale (left over by a crashed instance): remove it and + // retry. If that still fails, another instance just took the name. + QLocalServer::removeServer(serverName); + if (server->listen(serverName)) { + return true; + } + + forwardToPrimary(filesToSend); + return false; +} + +bool SingleInstanceManager::forwardToPrimary(const QStringList &filesToSend) +{ + QLocalSocket socket; + socket.connectToServer(serverName); + if (!socket.waitForConnected(200)) { + return false; + } + + // Serialize payload with length prefix + QByteArray payload; + QDataStream out(&payload, QIODevice::WriteOnly); + out << filesToSend; + + QByteArray message; + QDataStream msgStream(&message, QIODevice::WriteOnly); + msgStream << quint32(payload.size()); + message.append(payload); + + socket.write(message); + socket.flush(); + socket.waitForBytesWritten(1000); + + return true; +} + +void SingleInstanceManager::handleNewConnection() +{ + QLocalSocket *socket = server->nextPendingConnection(); + + // Per-connection state. QSharedPointer keeps the buffers alive for as long + // as the connection handler is attached to the socket. + auto buffer = QSharedPointer::create(); + auto expectedSize = QSharedPointer::create(0); + + connect(socket, &QLocalSocket::readyRead, this, [this, socket, buffer, expectedSize]() { + buffer->append(socket->readAll()); + + QDataStream stream(buffer.data(), QIODevice::ReadOnly); + + while (true) { + // Step 1: read size + if (*expectedSize == 0) { + if (buffer->size() < static_cast(sizeof(quint32))) { + return; + } + + stream >> *expectedSize; + } + + // Step 2: wait for full payload + if (buffer->size() < static_cast(sizeof(quint32) + *expectedSize)) { + return; + } + + // Step 3: extract payload + QByteArray payload = buffer->mid(sizeof(quint32), *expectedSize); + + QDataStream payloadStream(&payload, QIODevice::ReadOnly); + QStringList files; + payloadStream >> files; + + emit filesReceived(files); + + // Reset buffer (single message use-case) + buffer->clear(); + *expectedSize = 0; + + socket->disconnectFromServer(); + return; + } + }); + + connect(socket, &QLocalSocket::disconnected, socket, &QLocalSocket::deleteLater); +} diff --git a/cockatrice/src/single_instance_manager.h b/cockatrice/src/single_instance_manager.h new file mode 100644 index 000000000..55bff0e80 --- /dev/null +++ b/cockatrice/src/single_instance_manager.h @@ -0,0 +1,32 @@ +#ifndef COCKATRICE_SINGLE_INSTANCE_MANAGER_H +#define COCKATRICE_SINGLE_INSTANCE_MANAGER_H + +#include +#include +#include +#include + +class SingleInstanceManager : public QObject +{ + Q_OBJECT +public: + explicit SingleInstanceManager(QObject *parent = nullptr); + + // Returns true if this process became the primary instance, false if + // another instance is already running (and received our files). + bool tryRun(const QStringList &initialFiles); + +signals: + void filesReceived(const QStringList &files); + +private slots: + void handleNewConnection(); + +private: + bool forwardToPrimary(const QStringList &filesToSend); + + QString serverName; + QLocalServer *server = nullptr; +}; + +#endif // COCKATRICE_SINGLE_INSTANCE_MANAGER_H diff --git a/libcockatrice_network/libcockatrice/network/client/abstract/abstract_client.h b/libcockatrice_network/libcockatrice/network/client/abstract/abstract_client.h index 2eb7e3356..982aa6bf3 100644 --- a/libcockatrice_network/libcockatrice/network/client/abstract/abstract_client.h +++ b/libcockatrice_network/libcockatrice/network/client/abstract/abstract_client.h @@ -122,6 +122,20 @@ public: return userName; } + /** + * @brief Returns the server address configured for the current connection. + * + * May be empty for clients that have no server counterpart (e.g. local test clients). + */ + virtual QString serverName() const + { + return {}; + } + virtual quint16 serverPort() const + { + return 0; + } + static PendingCommand *prepareSessionCommand(const ::google::protobuf::Message &cmd); static PendingCommand *prepareRoomCommand(const ::google::protobuf::Message &cmd, int roomId); static PendingCommand *prepareModeratorCommand(const ::google::protobuf::Message &cmd); diff --git a/libcockatrice_network/libcockatrice/network/client/remote/remote_client.h b/libcockatrice_network/libcockatrice/network/client/remote/remote_client.h index 289fdc5d0..862dac06e 100644 --- a/libcockatrice_network/libcockatrice/network/client/remote/remote_client.h +++ b/libcockatrice_network/libcockatrice/network/client/remote/remote_client.h @@ -131,6 +131,22 @@ public: return socket->peerName(); } } + quint16 peerPort() const + { + if (usingWebSocket) { + return websocket->peerPort(); + } else { + return socket->peerPort(); + } + } + QString serverName() const override + { + return lastHostname; + } + quint16 serverPort() const override + { + return static_cast(lastPort); + } void connectToServer(const QString &hostname, unsigned int port, const QString &_userName, const QString &_password); void registerToServer(const QString &hostname, diff --git a/libcockatrice_settings/libcockatrice/settings/servers_settings.cpp b/libcockatrice_settings/libcockatrice/settings/servers_settings.cpp index d9b98e036..5c271328b 100644 --- a/libcockatrice_settings/libcockatrice/settings/servers_settings.cpp +++ b/libcockatrice_settings/libcockatrice/settings/servers_settings.cpp @@ -293,3 +293,48 @@ bool ServersSettings::updateExistingServer(QString saveName, } return false; } + +int ServersSettings::findServerIndex(const QString &host, const QString &port) const +{ + int size = getValue("totalServers", "server", "server_details").toInt(); + + for (int i = 0; i <= size; ++i) { + QString storedHost = getValue(QString("server%1").arg(i), "server", "server_details").toString(); + QString storedPort = getValue(QString("port%1").arg(i), "server", "server_details").toString(); + + if (storedHost == host && storedPort == port) { + return i; + } + } + + return -1; +} + +bool ServersSettings::hasUsername(const QString &host, const QString &port) const +{ + int index = findServerIndex(host, port); + if (index < 0) { + return false; + } + + QString user = getValue(QString("username%1").arg(index), "server", "server_details").toString(); + return !user.isEmpty(); +} + +bool ServersSettings::hasCredentials(const QString &host, const QString &port) const +{ + int index = findServerIndex(host, port); + if (index < 0) { + return false; + } + + bool save = getValue(QString("savePassword%1").arg(index), "server", "server_details").toBool(); + QString password = getValue(QString("password%1").arg(index), "server", "server_details").toString(); + + return save && !password.isEmpty(); +} + +bool ServersSettings::hasLoginData(const QString &host, const QString &port) const +{ + return hasUsername(host, port) && hasCredentials(host, port); +} diff --git a/libcockatrice_settings/libcockatrice/settings/servers_settings.h b/libcockatrice_settings/libcockatrice/settings/servers_settings.h index 40fa996fb..f9803a158 100644 --- a/libcockatrice_settings/libcockatrice/settings/servers_settings.h +++ b/libcockatrice_settings/libcockatrice/settings/servers_settings.h @@ -61,6 +61,10 @@ public: QString password, bool savePassword, QString site = QString()); + int findServerIndex(const QString &host, const QString &port) const; + bool hasUsername(const QString &host, const QString &port) const; + bool hasCredentials(const QString &host, const QString &port) const; + bool hasLoginData(const QString &host, const QString &port) const; bool updateExistingServerWithoutLoss(QString saveName, QString serv = QString(), From c4316c40e906b11bdaeca8f93d8e0ce12136e327 Mon Sep 17 00:00:00 2001 From: BruebachL <44814898+BruebachL@users.noreply.github.com> Date: Sat, 8 Aug 2026 22:04:50 +0200 Subject: [PATCH 13/21] [CI] Remove Qt5 (#7071) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [CI] Remove Qt5 Took 10 minutes Took 9 minutes * Revert CI failure and fix up comments Took 4 minutes --------- Co-authored-by: Lukas Brübach --- .github/CONTRIBUTING.md | 2 +- CMakeLists.txt | 21 ++-- README.md | 1 - cmake/FindQtRuntime.cmake | 81 +++++----------- cmake/NSIS.template.in | 26 ----- cockatrice/CMakeLists.txt | 97 +++++-------------- cockatrice/src/client/sound_engine.cpp | 14 +-- cockatrice/src/game/player/player_actions.cpp | 4 - .../player/player_list_widget.cpp | 4 - .../src/game_graphics/tally/subtype_tally.cpp | 2 +- .../cards/card_info_picture_widget.cpp | 4 - .../widgets/cards/card_info_picture_widget.h | 6 +- .../deck_analytics/resizable_panel.cpp | 29 ------ .../dialogs/dlg_select_set_for_cards.cpp | 8 -- .../dialogs/dlg_select_set_for_cards.h | 4 - .../display/charts/bars/bar_chart_widget.cpp | 4 - .../general/display/charts/bars/color_bar.cpp | 13 --- .../general/display/charts/bars/color_bar.h | 9 +- .../charts/bars/segmented_bar_widget.cpp | 4 - .../general/display/charts/pies/color_pie.cpp | 17 ---- .../general/display/charts/pies/color_pie.h | 4 - .../all_zones_card_amount_widget.cpp | 4 - .../all_zones_card_amount_widget.h | 4 - .../printing_selector_card_overlay_widget.cpp | 4 - .../printing_selector_card_overlay_widget.h | 4 - .../quick_settings/settings_button_widget.cpp | 4 - .../widgets/replay/replay_timeline_widget.cpp | 4 - .../widgets/server/chat_view/chat_view.cpp | 15 +-- .../widgets/server/chat_view/chat_view.h | 4 - .../widgets/server/user/user_info_popup.cpp | 8 -- .../widgets/server/user/user_info_popup.h | 4 - .../widgets/server/user/user_list_widget.cpp | 4 - ...api_response_deck_entry_display_widget.cpp | 4 - ...t_api_response_deck_entry_display_widget.h | 6 +- ...i_response_card_details_display_widget.cpp | 4 - ...api_response_card_details_display_widget.h | 6 +- .../interface/widgets/tabs/tab_supervisor.cpp | 8 +- .../interface/widgets/tabs/tab_supervisor.h | 4 - .../deck_preview/deck_preview_widget.cpp | 4 - .../deck_preview/deck_preview_widget.h | 6 +- cockatrice/src/main.cpp | 8 -- libcockatrice_card/CMakeLists.txt | 6 +- .../card/card_info_comparator.cpp | 9 -- libcockatrice_deck_list/CMakeLists.txt | 6 +- libcockatrice_filters/CMakeLists.txt | 6 +- libcockatrice_interfaces/CMakeLists.txt | 6 +- .../models/database/CMakeLists.txt | 6 +- .../models/deck_list/CMakeLists.txt | 6 +- .../network/client/abstract/CMakeLists.txt | 6 +- .../network/client/local/CMakeLists.txt | 6 +- .../network/client/remote/CMakeLists.txt | 6 +- .../network/server/local/CMakeLists.txt | 6 +- .../network/server/remote/CMakeLists.txt | 6 +- libcockatrice_rng/CMakeLists.txt | 6 +- libcockatrice_settings/CMakeLists.txt | 6 +- oracle/CMakeLists.txt | 53 +++------- oracle/src/main.cpp | 4 - oracle/src/qt-json/json.cpp | 40 ++------ servatrice/CMakeLists.txt | 6 +- servatrice/src/smtp/qxtsmtp.cpp | 5 - 60 files changed, 103 insertions(+), 555 deletions(-) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 56ad64283..589cae1d8 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -334,7 +334,7 @@ the tr() call, also you can add an extra string as a hint for translators: QString message = tr("Everyone draws %n cards", "english hint for translators", amount); ``` See [Qt's wiki on translations]( -https://doc.qt.io/qt-5/i18n-source-translation.html#handling-plurals) +https://doc.qt.io/qt-6/i18n-source-translation.html#handling-plurals) If you're about to propose a change that adds or modifies any translatable string in the code, you don't need to take care of adding the new strings to diff --git a/CMakeLists.txt b/CMakeLists.txt index c10e1db68..27fecc979 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -64,10 +64,11 @@ if(WIN32 OR USE_VCPKG) else() set(QTDIR "" - CACHE PATH "Path to Qt (e.g. C:/Qt/5.7/msvc2015_64)" + CACHE PATH "Path to Qt (e.g. C:/Qt/6.4.2/msvc2019_64)" ) message( - WARNING "QTDIR variable is missing. Please set this variable to specify path to Qt (e.g. C:/Qt/5.7/msvc2015_64)" + WARNING + "QTDIR variable is missing. Please set this variable to specify path to Qt (e.g. C:/Qt/6.4.2/msvc2019_64)" ) endif() endif() @@ -174,7 +175,7 @@ elseif(CMAKE_COMPILER_IS_GNUCXX) -Wno-error=delete-non-virtual-dtor -Wno-error=sign-compare -Wno-error=missing-declarations - -Wno-error=sfinae-incomplete # GCC 16+: Qt MOC + protobuf forward decls trigger this + -Wno-error=sfinae-incomplete # GCC 16+: Qt MOC + protobuf forward decls trigger this ) foreach(FLAG ${ADDITIONAL_DEBUG_FLAGS}) @@ -280,11 +281,7 @@ if(UNIX) if(CPACK_GENERATOR STREQUAL "RPM") set(CPACK_RPM_PACKAGE_LICENSE "GPLv2") set(CPACK_RPM_MAIN_COMPONENT "cockatrice") - if(Qt6_FOUND) - set(CPACK_RPM_PACKAGE_REQUIRES "protobuf, qt6-qttools, qt6-qtsvg, qt6-qtmultimedia, qt6-qtimageformats") - elseif(Qt5_FOUND) - set(CPACK_RPM_PACKAGE_REQUIRES "protobuf, qt5-qttools, qt5-qtsvg, qt5-qtmultimedia") - endif() + set(CPACK_RPM_PACKAGE_REQUIRES "protobuf, qt6-qttools, qt6-qtsvg, qt6-qtmultimedia, qt6-qtimageformats") set(CPACK_RPM_PACKAGE_GROUP "Amusements/Games") set(CPACK_RPM_PACKAGE_URL "http://github.com/Cockatrice/Cockatrice") # stop directories from making package conflicts @@ -302,12 +299,8 @@ if(UNIX) set(CPACK_DEBIAN_PACKAGE_SHLIBDEPS ON) set(CPACK_DEBIAN_PACKAGE_SECTION "games") set(CPACK_DEBIAN_PACKAGE_HOMEPAGE "http://github.com/Cockatrice/Cockatrice") - if(Qt6_FOUND) - set(CPACK_DEBIAN_PACKAGE_DEPENDS "libqt6multimedia6, libqt6svg6, qt6-qpa-plugins, qt6-image-formats-plugins") - set(CPACK_DEBIAN_PACKAGE_RECOMMENDS "libqt6sql6-mysql") # for connecting servatrice to a mysql db - elseif(Qt5_FOUND) - set(CPACK_DEBIAN_PACKAGE_DEPENDS "libqt5multimedia5-plugins, libqt5svg5") - endif() + set(CPACK_DEBIAN_PACKAGE_DEPENDS "libqt6multimedia6, libqt6svg6, qt6-qpa-plugins, qt6-image-formats-plugins") + set(CPACK_DEBIAN_PACKAGE_RECOMMENDS "libqt6sql6-mysql") # for connecting servatrice to a mysql db endif() endif() elseif(WIN32) diff --git a/README.md b/README.md index 9a27601cd..f22df461f 100644 --- a/README.md +++ b/README.md @@ -158,7 +158,6 @@ The following flags (with their non-default values) can be passed to `cmake`: | `-DWARNING_AS_ERROR=0` | Don't treat compilation warnings as errors in debug mode | | `-DUPDATE_TRANSLATIONS=1` | Configure `make` to update the translation .ts files for new strings in the source code
**Note:** `make clean` will remove the .ts files | | `-DTEST=1` | Enable regression tests
**Note:** `make test` to run tests, *googletest* will be downloaded if not available | -| `-DFORCE_USE_QT5=1` | Skip looking for Qt6 before trying to find Qt5 | # Run diff --git a/cmake/FindQtRuntime.cmake b/cmake/FindQtRuntime.cmake index 485affe52..8a3050813 100644 --- a/cmake/FindQtRuntime.cmake +++ b/cmake/FindQtRuntime.cmake @@ -1,8 +1,7 @@ # Find a compatible Qt version -# Inputs: WITH_SERVER, WITH_CLIENT, WITH_ORACLE, FORCE_USE_QT5 +# Inputs: WITH_SERVER, WITH_CLIENT, WITH_ORACLE # Optional Input: QT6_DIR -- Hint as to where Qt6 lives on the system -# Optional Input: QT5_DIR -- Hint as to where Qt5 lives on the system -# Output: COCKATRICE_QT_VERSION_NAME -- Example values: Qt5, Qt6 +# Output: COCKATRICE_QT_VERSION_NAME -- Example values: Qt6 # Output: SERVATRICE_QT_MODULES # Output: COCKATRICE_QT_MODULES # Output: ORACLE_QT_MODULES @@ -39,69 +38,37 @@ set(REQUIRED_QT_COMPONENTS ${REQUIRED_QT_COMPONENTS} ${_SERVATRICE_NEEDED} ${_CO ) list(REMOVE_DUPLICATES REQUIRED_QT_COMPONENTS) -if(NOT FORCE_USE_QT5) - # Linguist is now a component in Qt6 instead of an external package - find_package( - Qt6 6.4.2 - COMPONENTS ${REQUIRED_QT_COMPONENTS} Linguist - QUIET HINTS ${Qt6_DIR} - ) +# Linguist is now a component in Qt6 instead of an external package +find_package( + Qt6 6.4.2 + COMPONENTS ${REQUIRED_QT_COMPONENTS} Linguist + QUIET HINTS ${Qt6_DIR} +) +if(NOT Qt6_FOUND) + message(FATAL_ERROR "No suitable version of Qt was found") endif() -if(Qt6_FOUND) - set(COCKATRICE_QT_VERSION_NAME Qt6) +set(COCKATRICE_QT_VERSION_NAME Qt6) - list(FIND Qt6LinguistTools_TARGETS Qt6::lrelease QT6_LRELEASE_INDEX) - if(QT6_LRELEASE_INDEX EQUAL -1) - message(WARNING "Qt6 lrelease not found.") - endif() - - list(FIND Qt6LinguistTools_TARGETS Qt6::lupdate QT6_LUPDATE_INDEX) - if(QT6_LUPDATE_INDEX EQUAL -1) - message(WARNING "Qt6 lupdate not found.") - endif() -else() - find_package( - Qt5 5.15.2 - COMPONENTS ${REQUIRED_QT_COMPONENTS} - QUIET HINTS ${Qt5_DIR} - ) - if(Qt5_FOUND) - set(COCKATRICE_QT_VERSION_NAME Qt5) - else() - message(FATAL_ERROR "No suitable version of Qt was found") - endif() - - # Qt5 Linguist is in a separate package - find_package(Qt5LinguistTools QUIET) - if(Qt5LinguistTools_FOUND) - if(NOT Qt5_LRELEASE_EXECUTABLE) - message(WARNING "Qt5 lrelease not found.") - endif() - if(NOT Qt5_LUPDATE_EXECUTABLE) - message(WARNING "Qt5 lupdate not found.") - endif() - else() - message(WARNING "Linguist Tools not found, cannot handle translations") - endif() +list(FIND Qt6LinguistTools_TARGETS Qt6::lrelease QT6_LRELEASE_INDEX) +if(QT6_LRELEASE_INDEX EQUAL -1) + message(WARNING "Qt6 lrelease not found.") endif() -if(Qt5_POSITION_INDEPENDENT_CODE OR Qt6_FOUND) - set(CMAKE_POSITION_INDEPENDENT_CODE ON) +list(FIND Qt6LinguistTools_TARGETS Qt6::lupdate QT6_LUPDATE_INDEX) +if(QT6_LUPDATE_INDEX EQUAL -1) + message(WARNING "Qt6 lupdate not found.") endif() +set(CMAKE_POSITION_INDEPENDENT_CODE ON) + # Establish Qt Plugins directory & Library directories get_target_property(QT_LIBRARY_DIR ${COCKATRICE_QT_VERSION_NAME}::Core LOCATION) get_filename_component(QT_LIBRARY_DIR ${QT_LIBRARY_DIR} DIRECTORY) -if(Qt6_FOUND) - get_filename_component(QT_PLUGINS_DIR "${Qt6Core_DIR}/../../../${QT6_INSTALL_PLUGINS}" ABSOLUTE) - get_filename_component(QT_LIBRARY_DIR "${QT_LIBRARY_DIR}/../../.." ABSOLUTE) - if(UNIX AND APPLE) - # Mac needs a bit more help finding all necessary components - list(APPEND QT_LIBRARY_DIR "/usr/local/lib") - endif() -elseif(Qt5_FOUND) - get_filename_component(QT_PLUGINS_DIR "${Qt5Core_DIR}/../../../plugins" ABSOLUTE) - get_filename_component(QT_LIBRARY_DIR "${QT_LIBRARY_DIR}/.." ABSOLUTE) +get_filename_component(QT_PLUGINS_DIR "${Qt6Core_DIR}/../../../${QT6_INSTALL_PLUGINS}" ABSOLUTE) +get_filename_component(QT_LIBRARY_DIR "${QT_LIBRARY_DIR}/../../.." ABSOLUTE) +if(UNIX AND APPLE) + # Mac needs a bit more help finding all necessary components + list(APPEND QT_LIBRARY_DIR "/usr/local/lib") endif() message(DEBUG "QT_PLUGINS_DIR = ${QT_PLUGINS_DIR}") message(DEBUG "QT_LIBRARY_DIR = ${QT_LIBRARY_DIR}") diff --git a/cmake/NSIS.template.in b/cmake/NSIS.template.in index 84b2c38af..5af116470 100644 --- a/cmake/NSIS.template.in +++ b/cmake/NSIS.template.in @@ -294,20 +294,6 @@ Section "Application" SecApplication SetShellVarContext all SetOutPath "$INSTDIR" -${If} $PortableMode = 0 - - ; --- Register .cod file type --- - WriteRegStr HKCR ".cod" "" "Cockatrice" - WriteRegStr HKCR "Cockatrice" "" "Cockatrice Deck File" - WriteRegStr HKCR "Cockatrice\shell\open\command" "" '"$INSTDIR\cockatrice.exe" "%1"' - - ; --- Register custom URI protocol --- - WriteRegStr HKCR "cockatrice" "" "URL: Cockatrice Protocol" - WriteRegStr HKCR "cockatrice" "URL Protocol" "" - WriteRegStr HKCR "cockatrice\shell\open\command" "" '"$INSTDIR\cockatrice.exe" "%1"' - -${EndIf} - ${If} $PortableMode = 1 ${AndIf} ${FileExists} "$INSTDIR\portable.dat" ; upgrade portable mode @@ -416,18 +402,6 @@ Section "un.Application" UnSecApplication RMDir "$SMPROGRAMS\Cockatrice" DeleteRegKey HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Cockatrice" - - ; Only remove the file/protocol associations if we registered them (i.e. the - ; install was not portable) and .cod is still owned by Cockatrice, so we don't - ; clobber a .cod association installed by another application. - ${If} Not ${FileExists} "$INSTDIR\portable.dat" - ReadRegStr $0 HKCR ".cod" "" - ${If} $0 == "Cockatrice" - DeleteRegKey HKCR ".cod" - DeleteRegKey HKCR "Cockatrice" - DeleteRegKey HKCR "cockatrice" - ${EndIf} - ${EndIf} SectionEnd ; unselected because it is /o diff --git a/cockatrice/CMakeLists.txt b/cockatrice/CMakeLists.txt index 574c9bc34..eac9bc83d 100644 --- a/cockatrice/CMakeLists.txt +++ b/cockatrice/CMakeLists.txt @@ -424,11 +424,7 @@ if(APPLE) set(cockatrice_SOURCES ${cockatrice_SOURCES} ${CMAKE_CURRENT_SOURCE_DIR}/resources/appicon.icns) endif(APPLE) -if(Qt6_FOUND) - qt6_add_resources(cockatrice_RESOURCES_RCC ${cockatrice_RESOURCES}) -elseif(Qt5_FOUND) - qt5_add_resources(cockatrice_RESOURCES_RCC ${cockatrice_RESOURCES}) -endif() +qt6_add_resources(cockatrice_RESOURCES_RCC ${cockatrice_RESOURCES}) # Declare path variables set(ICONDIR @@ -449,67 +445,28 @@ set(COCKATRICE_MAC_QM_INSTALL_DIR "cockatrice.app/Contents/Resources/translation set(COCKATRICE_UNIX_QM_INSTALL_DIR "share/cockatrice/translations") set(COCKATRICE_WIN32_QM_INSTALL_DIR "translations") -if(Qt6_FOUND) - qt6_add_executable( - cockatrice - WIN32 - MACOSX_BUNDLE - ${cockatrice_SOURCES} - ${cockatrice_RESOURCES_RCC} - ${cockatrice_MOC_SRCS} - MANUAL_FINALIZATION - ) -elseif(Qt5_FOUND) - # Qt5 Translations need to be linked at executable creation time - if(Qt5LinguistTools_FOUND) - if(UPDATE_TRANSLATIONS) - qt5_create_translation(cockatrice_QM ${translate_SRCS} ${cockatrice_TS}) - else() - qt5_add_translation(cockatrice_QM ${cockatrice_TS}) - endif() - endif() - add_executable( - cockatrice WIN32 MACOSX_BUNDLE ${cockatrice_MOC_SRCS} ${cockatrice_QM} ${cockatrice_RESOURCES_RCC} - ${cockatrice_SOURCES} - ) - if(UNIX) - if(APPLE) - install(FILES ${cockatrice_QM} DESTINATION ${COCKATRICE_MAC_QM_INSTALL_DIR}) - else() - install(FILES ${cockatrice_QM} DESTINATION ${COCKATRICE_UNIX_QM_INSTALL_DIR}) - endif() - elseif(WIN32) - install(FILES ${cockatrice_QM} DESTINATION ${COCKATRICE_WIN32_QM_INSTALL_DIR}) - endif() -endif() +qt6_add_executable( + cockatrice + WIN32 + MACOSX_BUNDLE + ${cockatrice_SOURCES} + ${cockatrice_RESOURCES_RCC} + ${cockatrice_MOC_SRCS} + MANUAL_FINALIZATION +) -if(Qt5_FOUND) - target_link_libraries( - cockatrice - libcockatrice_card - libcockatrice_deck_list - libcockatrice_filters - libcockatrice_utility - libcockatrice_network - libcockatrice_models - libcockatrice_rng - libcockatrice_settings - ${COCKATRICE_QT_MODULES} - ) -else() - target_link_libraries( - cockatrice - PUBLIC libcockatrice_card - libcockatrice_deck_list - libcockatrice_filters - libcockatrice_utility - libcockatrice_network - libcockatrice_models - libcockatrice_rng - libcockatrice_settings - ${COCKATRICE_QT_MODULES} - ) -endif() +target_link_libraries( + cockatrice + PUBLIC libcockatrice_card + libcockatrice_deck_list + libcockatrice_filters + libcockatrice_utility + libcockatrice_network + libcockatrice_models + libcockatrice_rng + libcockatrice_settings + ${COCKATRICE_QT_MODULES} +) if(UNIX) if(APPLE) @@ -556,7 +513,7 @@ if(APPLE) set(plugin_dest_dir cockatrice.app/Contents/Plugins) set(qtconf_dest_dir cockatrice.app/Contents/Resources) - # Qt plugins: audio (Qt5), iconengines, imageformats, multimedia (Qt6), platforms, printsupport (Qt5), styles, tls (Qt6) + # Qt plugins: audio, iconengines, imageformats, multimedia, platforms, printsupport, styles, tls install( DIRECTORY "${QT_PLUGINS_DIR}/" DESTINATION ${plugin_dest_dir} @@ -623,7 +580,7 @@ if(WIN32) PATTERN "*.ini" ) - # Qt plugins: audio (Qt5), iconengines, imageformats, multimedia (Qt6) platforms, printsupport (Qt5), styles, tls (Qt6) + # Qt plugins: audio, iconengines, imageformats, multimedia, platforms, printsupport, styles, tls install( DIRECTORY "${QT_PLUGINS_DIR}/" DESTINATION ${plugin_dest_dir} @@ -679,7 +636,7 @@ Data = Resources\") endif() endif() -if(Qt6_FOUND AND Qt6LinguistTools_FOUND) +if(Qt6LinguistTools_FOUND) #Qt6 Translations happen after the executable is built up if(UPDATE_TRANSLATIONS) qt6_add_translations( @@ -706,6 +663,4 @@ if(Qt6_FOUND AND Qt6LinguistTools_FOUND) endif() endif() -if(Qt6_FOUND) - qt6_finalize_target(cockatrice) -endif() +qt6_finalize_target(cockatrice) diff --git a/cockatrice/src/client/sound_engine.cpp b/cockatrice/src/client/sound_engine.cpp index 8d09341be..18de2264d 100644 --- a/cockatrice/src/client/sound_engine.cpp +++ b/cockatrice/src/client/sound_engine.cpp @@ -2,14 +2,11 @@ #include "settings/cache_settings.h" -#include -#include - -#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)) #include #include +#include +#include #include -#endif #define DEFAULT_THEME_NAME "Default" #define TEST_SOUND_FILENAME "player_join" @@ -44,10 +41,8 @@ void SoundEngine::soundEnabledChanged() qCInfo(SoundEngineLog) << "SoundEngine: enabling sound with" << audioData.size() << "sounds"; if (!player) { player = new QMediaPlayer; -#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)) audioOutput = new QAudioOutput(player); player->setAudioOutput(audioOutput); -#endif } } else { qCInfo(SoundEngineLog) << "SoundEngine: disabling sound"; @@ -75,13 +70,8 @@ void SoundEngine::playSound(const QString &fileName) player->stop(); int volumeSliderValue = SettingsCache::instance().sound().getMasterVolume(); -#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)) player->audioOutput()->setVolume(qreal(volumeSliderValue) / 100); player->setSource(QUrl::fromLocalFile(audioData[fileName])); -#else - player->setVolume(volumeSliderValue); - player->setMedia(QUrl::fromLocalFile(audioData[fileName])); -#endif player->play(); } diff --git a/cockatrice/src/game/player/player_actions.cpp b/cockatrice/src/game/player/player_actions.cpp index 504c1de89..12abb994f 100644 --- a/cockatrice/src/game/player/player_actions.cpp +++ b/cockatrice/src/game/player/player_actions.cpp @@ -1354,11 +1354,7 @@ void PlayerActions::actSetPT(QList selectedCards, const QString &pt) const auto oldpt = CardItem::parsePT(card->getPT()); int ptIter = 0; for (const auto &_item : ptList) { -#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)) if (_item.typeId() == QMetaType::Type::Int) { -#else - if (_item.type() == QVariant::Int) { -#endif int oldItem = ptIter < oldpt.size() ? oldpt.at(ptIter).toInt() : 0; newpt += '/' + QString::number(oldItem + _item.toInt()); } else { diff --git a/cockatrice/src/game_graphics/player/player_list_widget.cpp b/cockatrice/src/game_graphics/player/player_list_widget.cpp index 6b1cf6cc6..4268e1019 100644 --- a/cockatrice/src/game_graphics/player/player_list_widget.cpp +++ b/cockatrice/src/game_graphics/player/player_list_widget.cpp @@ -25,11 +25,7 @@ bool PlayerListItemDelegate::editorEvent(QEvent *event, if ((event->type() == QEvent::MouseButtonPress) && index.isValid()) { auto *const mouseEvent = static_cast(event); if (mouseEvent->button() == Qt::RightButton) { -#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)) static_cast(parent())->showContextMenu(mouseEvent->globalPosition().toPoint(), index); -#else - static_cast(parent())->showContextMenu(mouseEvent->globalPos(), index); -#endif return true; } } diff --git a/cockatrice/src/game_graphics/tally/subtype_tally.cpp b/cockatrice/src/game_graphics/tally/subtype_tally.cpp index 804443b15..2241eb3b2 100644 --- a/cockatrice/src/game_graphics/tally/subtype_tally.cpp +++ b/cockatrice/src/game_graphics/tally/subtype_tally.cpp @@ -67,7 +67,7 @@ QList countSubtypes(const QList &cards) // convert entries into TallyRows QList rows; - rows.reserve(entries.size()); // for backwards compatibility with Qt5 + rows.reserve(entries.size()); std::transform(entries.begin(), entries.end(), std::back_inserter(rows), [](const SubtypeEntry &e) { return TallyRow{e.name, QString::number(e.count)}; }); diff --git a/cockatrice/src/interface/widgets/cards/card_info_picture_widget.cpp b/cockatrice/src/interface/widgets/cards/card_info_picture_widget.cpp index af778889b..79ae087d7 100644 --- a/cockatrice/src/interface/widgets/cards/card_info_picture_widget.cpp +++ b/cockatrice/src/interface/widgets/cards/card_info_picture_widget.cpp @@ -250,11 +250,7 @@ QSize CardInfoPictureWidget::sizeHint() const * @brief Starts the hover timer to show the enlarged pixmap on hover. * @param event The enter event. */ -#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) void CardInfoPictureWidget::enterEvent(QEnterEvent *event) -#else -void CardInfoPictureWidget::enterEvent(QEvent *event) -#endif { QWidget::enterEvent(event); // Call the base class implementation diff --git a/cockatrice/src/interface/widgets/cards/card_info_picture_widget.h b/cockatrice/src/interface/widgets/cards/card_info_picture_widget.h index 1f065eed9..4fe84ed0b 100644 --- a/cockatrice/src/interface/widgets/cards/card_info_picture_widget.h +++ b/cockatrice/src/interface/widgets/cards/card_info_picture_widget.h @@ -48,11 +48,7 @@ signals: protected: void resizeEvent(QResizeEvent *event) override; void paintEvent(QPaintEvent *) override; -#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) - void enterEvent(QEnterEvent *event) override; // Qt6 signature -#else - void enterEvent(QEvent *event) override; // Qt5 signature -#endif + void enterEvent(QEnterEvent *event) override; void leaveEvent(QEvent *event) override; void moveEvent(QMoveEvent *event) override; void mouseMoveEvent(QMouseEvent *event) override; diff --git a/cockatrice/src/interface/widgets/deck_analytics/resizable_panel.cpp b/cockatrice/src/interface/widgets/deck_analytics/resizable_panel.cpp index f7bb5ac35..250fe1ad3 100644 --- a/cockatrice/src/interface/widgets/deck_analytics/resizable_panel.cpp +++ b/cockatrice/src/interface/widgets/deck_analytics/resizable_panel.cpp @@ -147,11 +147,7 @@ bool ResizablePanel::eventFilter(QObject *obj, QEvent *event) if (event->type() == QEvent::MouseButtonPress) { auto *mouseEvent = static_cast(event); if (mouseEvent->button() == Qt::LeftButton) { -#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) dragStartPos = mouseEvent->globalPosition().toPoint(); -#else - dragStartPos = mouseEvent->globalPos(); -#endif isDraggingPanel = false; dragButton->setCursor(Qt::ClosedHandCursor); } @@ -159,11 +155,7 @@ bool ResizablePanel::eventFilter(QObject *obj, QEvent *event) } else if (event->type() == QEvent::MouseMove) { auto *mouseEvent = static_cast(event); if (mouseEvent->buttons() & Qt::LeftButton) { -#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) QPoint currentPos = mouseEvent->globalPosition().toPoint(); -#else - QPoint currentPos = mouseEvent->globalPos(); -#endif int distance = (currentPos - dragStartPos).manhattanLength(); if (distance >= 5 && !isDraggingPanel) { isDraggingPanel = true; @@ -182,22 +174,14 @@ bool ResizablePanel::eventFilter(QObject *obj, QEvent *event) if (obj == resizeHandle) { if (event->type() == QEvent::MouseButtonPress) { auto *mouseEvent = static_cast(event); -#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) resizeStartY = mouseEvent->globalPosition().y(); -#else - resizeStartY = mouseEvent->globalPos().y(); -#endif isResizing = true; resizeStartHeight = currentHeight; resizeHandle->grabMouse(); return true; } else if (event->type() == QEvent::MouseMove && isResizing) { auto *mouseEvent = static_cast(event); -#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) int deltaY = mouseEvent->globalPosition().y() - resizeStartY; -#else - int deltaY = mouseEvent->globalPos().y() - resizeStartY; -#endif int newHeight = resizeStartHeight + deltaY; int minAllowed = getMinimumAllowedHeight(); @@ -221,11 +205,7 @@ void ResizablePanel::dragEnterEvent(QDragEnterEvent *event) { if (event->mimeData()->hasFormat("application/x-resizablepanel")) { event->acceptProposedAction(); -#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) showDropIndicator(event->position().y()); -#else - showDropIndicator(event->pos().y()); -#endif } } @@ -233,13 +213,8 @@ void ResizablePanel::dragMoveEvent(QDragMoveEvent *event) { if (event->mimeData()->hasFormat("application/x-resizablepanel")) { event->acceptProposedAction(); -#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) showDropIndicator(event->position().y()); lastDragPos = mapToGlobal(event->position().toPoint()); -#else - showDropIndicator(event->pos().y()); - lastDragPos = mapToGlobal(event->pos()); -#endif if (!autoScrollTimer->isActive()) { autoScrollTimer->start(); @@ -265,11 +240,7 @@ void ResizablePanel::dropEvent(QDropEvent *event) ResizablePanel *draggedPanel = reinterpret_cast(ptr); if (draggedPanel && draggedPanel != this) { -#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) bool insertBefore = (event->position().y() < height() / 2); -#else - bool insertBefore = (event->pos().y() < height() / 2); -#endif emit dropRequested(draggedPanel, this, insertBefore); event->acceptProposedAction(); } diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_select_set_for_cards.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_select_set_for_cards.cpp index bc846bf3d..8eed083e1 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_select_set_for_cards.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_select_set_for_cards.cpp @@ -321,11 +321,7 @@ void DlgSelectSetForCards::dropEvent(QDropEvent *event) { QByteArray itemData = event->mimeData()->data("application/x-setentrywidget"); QString draggedSetName = QString::fromUtf8(itemData); -#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) QPoint adjustedPos = event->position().toPoint() + QPoint(0, scrollArea->verticalScrollBar()->value()); -#else - QPoint adjustedPos = event->pos() + QPoint(0, scrollArea->verticalScrollBar()->value()); -#endif int dropIndex = -1; for (int i = 0; i < listLayout->count(); ++i) { QWidget *widget = listLayout->itemAt(i)->widget(); @@ -491,11 +487,7 @@ void SetEntryWidget::mousePressEvent(QMouseEvent *event) } } -#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) void SetEntryWidget::enterEvent(QEnterEvent *event) -#else -void SetEntryWidget::enterEvent(QEvent *event) -#endif { QWidget::enterEvent(event); // Call the base class handler // Highlight the widget by changing the background color only for the widget itself diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_select_set_for_cards.h b/cockatrice/src/interface/widgets/dialogs/dlg_select_set_for_cards.h index 92f285aa0..4528552d0 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_select_set_for_cards.h +++ b/cockatrice/src/interface/widgets/dialogs/dlg_select_set_for_cards.h @@ -87,11 +87,7 @@ public: public slots: void mousePressEvent(QMouseEvent *event) override; -#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)) void enterEvent(QEnterEvent *event) override; -#else - void enterEvent(QEvent *event) override; -#endif void leaveEvent(QEvent *event) override; void dragMoveEvent(QDragMoveEvent *event) override; diff --git a/cockatrice/src/interface/widgets/general/display/charts/bars/bar_chart_widget.cpp b/cockatrice/src/interface/widgets/general/display/charts/bars/bar_chart_widget.cpp index d9e108e6a..04315d0ce 100644 --- a/cockatrice/src/interface/widgets/general/display/charts/bars/bar_chart_widget.cpp +++ b/cockatrice/src/interface/widgets/general/display/charts/bars/bar_chart_widget.cpp @@ -207,11 +207,7 @@ void BarChartWidget::mouseMoveEvent(QMouseEvent *e) if (hoveredSegment >= 0) { const auto &s = segments[hoveredSegment]; QString text = QString("%1: %2 cards\n\n%3").arg(s.category).arg(s.value).arg(s.cards.join("\n")); -#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) QToolTip::showText(e->globalPosition().toPoint(), text, this); -#else - QToolTip::showText(e->globalPos(), text, this); -#endif } else { QToolTip::hideText(); } diff --git a/cockatrice/src/interface/widgets/general/display/charts/bars/color_bar.cpp b/cockatrice/src/interface/widgets/general/display/charts/bars/color_bar.cpp index 12ab5bb3b..1087c621e 100644 --- a/cockatrice/src/interface/widgets/general/display/charts/bars/color_bar.cpp +++ b/cockatrice/src/interface/widgets/general/display/charts/bars/color_bar.cpp @@ -83,19 +83,11 @@ void ColorBar::paintEvent(QPaintEvent *) } } -#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) void ColorBar::enterEvent(QEnterEvent *event) { Q_UNUSED(event); isHovered = true; } -#else -void ColorBar::enterEvent(QEvent *event) -{ - Q_UNUSED(event); - isHovered = true; -} -#endif void ColorBar::leaveEvent(QEvent *) { @@ -108,13 +100,8 @@ void ColorBar::mouseMoveEvent(QMouseEvent *event) return; } -#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) int x = int(event->position().x()); QPoint gp = event->globalPosition().toPoint(); -#else - int x = event->pos().x(); - QPoint gp = event->globalPos(); -#endif QString text = tooltipForPosition(x); if (!text.isEmpty()) { diff --git a/cockatrice/src/interface/widgets/general/display/charts/bars/color_bar.h b/cockatrice/src/interface/widgets/general/display/charts/bars/color_bar.h index 100f95310..1d435cec1 100644 --- a/cockatrice/src/interface/widgets/general/display/charts/bars/color_bar.h +++ b/cockatrice/src/interface/widgets/general/display/charts/bars/color_bar.h @@ -76,17 +76,10 @@ protected: */ void paintEvent(QPaintEvent *event) override; -#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) /** - * @brief Handles mouse hover entering (Qt6 version). + * @brief Handles mouse hover entering. */ void enterEvent(QEnterEvent *event) override; -#else - /** - * @brief Handles mouse hover entering (Qt5 version). - */ - void enterEvent(QEvent *event) override; -#endif /** * @brief Handles mouse hover leaving. diff --git a/cockatrice/src/interface/widgets/general/display/charts/bars/segmented_bar_widget.cpp b/cockatrice/src/interface/widgets/general/display/charts/bars/segmented_bar_widget.cpp index 9bad32bda..03c69373a 100644 --- a/cockatrice/src/interface/widgets/general/display/charts/bars/segmented_bar_widget.cpp +++ b/cockatrice/src/interface/widgets/general/display/charts/bars/segmented_bar_widget.cpp @@ -134,9 +134,5 @@ void SegmentedBarWidget::mouseMoveEvent(QMouseEvent *e) const Segment &s = segments[idx]; QString text = QString("%1: %2 cards\n%3").arg(s.category).arg(s.value).arg(s.cards.join(", ")); -#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) QToolTip::showText(e->globalPosition().toPoint(), text, this); -#else - QToolTip::showText(e->globalPos(), text, this); -#endif } diff --git a/cockatrice/src/interface/widgets/general/display/charts/pies/color_pie.cpp b/cockatrice/src/interface/widgets/general/display/charts/pies/color_pie.cpp index b129fbe18..bd4641981 100644 --- a/cockatrice/src/interface/widgets/general/display/charts/pies/color_pie.cpp +++ b/cockatrice/src/interface/widgets/general/display/charts/pies/color_pie.cpp @@ -82,11 +82,7 @@ void ColorPie::paintEvent(QPaintEvent *) QString label = QString("%1%").arg(int(ratio * 100 + 0.5)); QFontMetrics fm(p.font()); -#if QT_VERSION >= QT_VERSION_CHECK(5, 11, 0) int labelWidth = fm.horizontalAdvance(label); -#else - int labelWidth = fm.width(label); -#endif QRectF textRect(labelPos.x() - labelWidth / 2.0, labelPos.y() - fm.height() / 2.0, labelWidth, fm.height()); p.setPen(Qt::black); @@ -96,19 +92,11 @@ void ColorPie::paintEvent(QPaintEvent *) } } -#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) void ColorPie::enterEvent(QEnterEvent *event) { Q_UNUSED(event); isHovered = true; } -#else -void ColorPie::enterEvent(QEvent *event) -{ - Q_UNUSED(event); - isHovered = true; -} -#endif void ColorPie::leaveEvent(QEvent *) { @@ -121,13 +109,8 @@ void ColorPie::mouseMoveEvent(QMouseEvent *event) return; } -#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) QPoint p = event->position().toPoint(); QPoint gp = event->globalPosition().toPoint(); -#else - QPoint p = event->pos(); - QPoint gp = event->globalPos(); -#endif QString text = tooltipForPoint(p); if (!text.isEmpty()) { diff --git a/cockatrice/src/interface/widgets/general/display/charts/pies/color_pie.h b/cockatrice/src/interface/widgets/general/display/charts/pies/color_pie.h index 7d71ea3b9..60d8e18f2 100644 --- a/cockatrice/src/interface/widgets/general/display/charts/pies/color_pie.h +++ b/cockatrice/src/interface/widgets/general/display/charts/pies/color_pie.h @@ -22,11 +22,7 @@ public: protected: void paintEvent(QPaintEvent *) override; -#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) void enterEvent(QEnterEvent *event) override; -#else - void enterEvent(QEvent *event) override; -#endif void leaveEvent(QEvent *) override; void mouseMoveEvent(QMouseEvent *event) override; diff --git a/cockatrice/src/interface/widgets/printing_selector/all_zones_card_amount_widget.cpp b/cockatrice/src/interface/widgets/printing_selector/all_zones_card_amount_widget.cpp index 05e269174..579106540 100644 --- a/cockatrice/src/interface/widgets/printing_selector/all_zones_card_amount_widget.cpp +++ b/cockatrice/src/interface/widgets/printing_selector/all_zones_card_amount_widget.cpp @@ -141,11 +141,7 @@ bool AllZonesCardAmountWidget::isNonZero() * * @param event The event information for the mouse entry. */ -#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)) void AllZonesCardAmountWidget::enterEvent(QEnterEvent *event) -#else -void AllZonesCardAmountWidget::enterEvent(QEvent *event) -#endif { QWidget::enterEvent(event); update(); diff --git a/cockatrice/src/interface/widgets/printing_selector/all_zones_card_amount_widget.h b/cockatrice/src/interface/widgets/printing_selector/all_zones_card_amount_widget.h index de4a984be..325dd1c1b 100644 --- a/cockatrice/src/interface/widgets/printing_selector/all_zones_card_amount_widget.h +++ b/cockatrice/src/interface/widgets/printing_selector/all_zones_card_amount_widget.h @@ -26,11 +26,7 @@ public: int getTokensboardAmount(); bool isNonZero(); -#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)) void enterEvent(QEnterEvent *event) override; -#else - void enterEvent(QEvent *event) override; -#endif public slots: void adjustFontSize(int scalePercentage); diff --git a/cockatrice/src/interface/widgets/printing_selector/printing_selector_card_overlay_widget.cpp b/cockatrice/src/interface/widgets/printing_selector/printing_selector_card_overlay_widget.cpp index 2d8cf278c..2b9201e6c 100644 --- a/cockatrice/src/interface/widgets/printing_selector/printing_selector_card_overlay_widget.cpp +++ b/cockatrice/src/interface/widgets/printing_selector/printing_selector_card_overlay_widget.cpp @@ -106,11 +106,7 @@ void PrintingSelectorCardOverlayWidget::resizeEvent(QResizeEvent *event) * * @param event The event triggered when the mouse enters the widget. */ -#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) void PrintingSelectorCardOverlayWidget::enterEvent(QEnterEvent *event) -#else -void PrintingSelectorCardOverlayWidget::enterEvent(QEvent *event) -#endif { QWidget::enterEvent(event); deckEditor->updateCard(rootCard); diff --git a/cockatrice/src/interface/widgets/printing_selector/printing_selector_card_overlay_widget.h b/cockatrice/src/interface/widgets/printing_selector/printing_selector_card_overlay_widget.h index 52a43d220..228393c9c 100644 --- a/cockatrice/src/interface/widgets/printing_selector/printing_selector_card_overlay_widget.h +++ b/cockatrice/src/interface/widgets/printing_selector/printing_selector_card_overlay_widget.h @@ -26,11 +26,7 @@ public: protected: void resizeEvent(QResizeEvent *event) override; -#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) void enterEvent(QEnterEvent *event) override; -#else - void enterEvent(QEvent *event) override; -#endif void leaveEvent(QEvent *event) override; void mousePressEvent(QMouseEvent *event) override; void customMenu(QPoint point); diff --git a/cockatrice/src/interface/widgets/quick_settings/settings_button_widget.cpp b/cockatrice/src/interface/widgets/quick_settings/settings_button_widget.cpp index c69fa3f14..9c433ab5a 100644 --- a/cockatrice/src/interface/widgets/quick_settings/settings_button_widget.cpp +++ b/cockatrice/src/interface/widgets/quick_settings/settings_button_widget.cpp @@ -110,11 +110,7 @@ void SettingsButtonWidget::onPopupClosed() const void SettingsButtonWidget::mousePressEvent(QMouseEvent *event) { -#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)) if (popup->isVisible() && !popup->geometry().contains(event->globalPosition().toPoint())) { -#else - if (popup->isVisible() && !popup->geometry().contains(event->globalPos())) { -#endif popup->close(); } QWidget::mousePressEvent(event); diff --git a/cockatrice/src/interface/widgets/replay/replay_timeline_widget.cpp b/cockatrice/src/interface/widgets/replay/replay_timeline_widget.cpp index b5c7bf301..fe4ac330b 100644 --- a/cockatrice/src/interface/widgets/replay/replay_timeline_widget.cpp +++ b/cockatrice/src/interface/widgets/replay/replay_timeline_widget.cpp @@ -65,11 +65,7 @@ void ReplayTimelineWidget::paintEvent(QPaintEvent * /* event */) void ReplayTimelineWidget::mousePressEvent(QMouseEvent *event) { -#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)) int newTime = static_cast((qint64)maxTime * (qint64)event->position().x() / width()); -#else - int newTime = static_cast((qint64)maxTime * (qint64)event->x() / width()); -#endif emit timeClicked(newTime); } diff --git a/cockatrice/src/interface/widgets/server/chat_view/chat_view.cpp b/cockatrice/src/interface/widgets/server/chat_view/chat_view.cpp index 43bd0aa0a..d286af038 100644 --- a/cockatrice/src/interface/widgets/server/chat_view/chat_view.cpp +++ b/cockatrice/src/interface/widgets/server/chat_view/chat_view.cpp @@ -580,11 +580,7 @@ void ChatView::redactMessages(const QString &userName, int amount) } } -#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)) void ChatView::enterEvent(QEnterEvent * /*event*/) -#else -void ChatView::enterEvent(QEvent * /*event*/) -#endif { setMouseTracking(true); } @@ -641,12 +637,9 @@ void ChatView::mousePressEvent(QMouseEvent *event) { switch (hoveredItemType) { case HoveredCard: { - if ((event->button() == Qt::MiddleButton) || (event->button() == Qt::LeftButton)) -#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)) + if ((event->button() == Qt::MiddleButton) || (event->button() == Qt::LeftButton)) { emit showCardInfoPopup(event->globalPosition().toPoint(), {hoveredContent}); -#else - emit showCardInfoPopup(event->globalPos(), {hoveredContent}); -#endif + } break; } case HoveredUser: { @@ -656,11 +649,7 @@ void ChatView::mousePressEvent(QMouseEvent *event) switch (event->button()) { case Qt::RightButton: { UserLevelFlags userLevel(hoveredContent.left(delimiterIndex).toInt()); -#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)) userContextMenu->showContextMenu(event->globalPosition().toPoint(), userName, userLevel, this); -#else - userContextMenu->showContextMenu(event->globalPos(), userName, userLevel, this); -#endif break; } case Qt::LeftButton: { diff --git a/cockatrice/src/interface/widgets/server/chat_view/chat_view.h b/cockatrice/src/interface/widgets/server/chat_view/chat_view.h index c5ae2b81a..506605d24 100644 --- a/cockatrice/src/interface/widgets/server/chat_view/chat_view.h +++ b/cockatrice/src/interface/widgets/server/chat_view/chat_view.h @@ -103,11 +103,7 @@ public: void redactMessages(const QString &userName, int amount); protected: -#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)) void enterEvent(QEnterEvent *event) override; -#else - void enterEvent(QEvent *event) override; -#endif void leaveEvent(QEvent *event) override; void mouseMoveEvent(QMouseEvent *event) override; void mousePressEvent(QMouseEvent *event) override; diff --git a/cockatrice/src/interface/widgets/server/user/user_info_popup.cpp b/cockatrice/src/interface/widgets/server/user/user_info_popup.cpp index 2b4dcb8ed..edb95f2df 100644 --- a/cockatrice/src/interface/widgets/server/user/user_info_popup.cpp +++ b/cockatrice/src/interface/widgets/server/user/user_info_popup.cpp @@ -636,19 +636,11 @@ void UserInfoPopup::refreshGames() // ── Mouse events ────────────────────────────────────────────────────────────── -#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) void UserInfoPopup::enterEvent(QEnterEvent *e) { QFrame::enterEvent(e); emit mouseEnteredPopup(); } -#else -void UserInfoPopup::enterEvent(QEvent *e) -{ - QFrame::enterEvent(e); - emit mouseEnteredPopup(); -} -#endif void UserInfoPopup::leaveEvent(QEvent *e) { QFrame::leaveEvent(e); diff --git a/cockatrice/src/interface/widgets/server/user/user_info_popup.h b/cockatrice/src/interface/widgets/server/user/user_info_popup.h index 0e03147c4..69517093f 100644 --- a/cockatrice/src/interface/widgets/server/user/user_info_popup.h +++ b/cockatrice/src/interface/widgets/server/user/user_info_popup.h @@ -143,11 +143,7 @@ signals: void demoteFromJudgeRequested(const QString &userName); protected: -#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) void enterEvent(QEnterEvent *e) override; -#else - void enterEvent(QEvent *e) override; -#endif void leaveEvent(QEvent *e) override; private slots: diff --git a/cockatrice/src/interface/widgets/server/user/user_list_widget.cpp b/cockatrice/src/interface/widgets/server/user/user_list_widget.cpp index aaefe7cbf..9c29c62bc 100644 --- a/cockatrice/src/interface/widgets/server/user/user_list_widget.cpp +++ b/cockatrice/src/interface/widgets/server/user/user_list_widget.cpp @@ -340,11 +340,7 @@ bool UserListItemDelegate::editorEvent(QEvent *event, if ((event->type() == QEvent::MouseButtonPress) && index.isValid()) { QMouseEvent *const mouseEvent = static_cast(event); if (mouseEvent->button() == Qt::RightButton) { -#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)) static_cast(parent())->showContextMenu(mouseEvent->globalPosition().toPoint(), index); -#else - static_cast(parent())->showContextMenu(mouseEvent->globalPos(), index); -#endif return true; } } diff --git a/cockatrice/src/interface/widgets/tabs/api/archidekt/display/archidekt_api_response_deck_entry_display_widget.cpp b/cockatrice/src/interface/widgets/tabs/api/archidekt/display/archidekt_api_response_deck_entry_display_widget.cpp index 70156df79..c35c2d0b8 100644 --- a/cockatrice/src/interface/widgets/tabs/api/archidekt/display/archidekt_api_response_deck_entry_display_widget.cpp +++ b/cockatrice/src/interface/widgets/tabs/api/archidekt/display/archidekt_api_response_deck_entry_display_widget.cpp @@ -137,11 +137,7 @@ void ArchidektApiResponseDeckEntryDisplayWidget::mousePressEvent(QMouseEvent *ev actRequestNavigationToDeck(); } -#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) void ArchidektApiResponseDeckEntryDisplayWidget::enterEvent(QEnterEvent *event) -#else -void ArchidektApiResponseDeckEntryDisplayWidget::enterEvent(QEvent *event) -#endif { QWidget::enterEvent(event); backgroundPlateWidget->setFocused(true); diff --git a/cockatrice/src/interface/widgets/tabs/api/archidekt/display/archidekt_api_response_deck_entry_display_widget.h b/cockatrice/src/interface/widgets/tabs/api/archidekt/display/archidekt_api_response_deck_entry_display_widget.h index 365a99c9c..575a81b5f 100644 --- a/cockatrice/src/interface/widgets/tabs/api/archidekt/display/archidekt_api_response_deck_entry_display_widget.h +++ b/cockatrice/src/interface/widgets/tabs/api/archidekt/display/archidekt_api_response_deck_entry_display_widget.h @@ -98,11 +98,7 @@ public slots: protected: void mousePressEvent(QMouseEvent *event) override; -#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) - void enterEvent(QEnterEvent *event) override; ///< Qt6 hover enter -#else - void enterEvent(QEvent *event) override; ///< Qt5 hover enter -#endif + void enterEvent(QEnterEvent *event) override; ///< Hover enter void leaveEvent(QEvent *event) override; private: diff --git a/cockatrice/src/interface/widgets/tabs/api/edhrec/display/cards/edhrec_api_response_card_details_display_widget.cpp b/cockatrice/src/interface/widgets/tabs/api/edhrec/display/cards/edhrec_api_response_card_details_display_widget.cpp index 9d45f254b..9d294f10c 100644 --- a/cockatrice/src/interface/widgets/tabs/api/edhrec/display/cards/edhrec_api_response_card_details_display_widget.cpp +++ b/cockatrice/src/interface/widgets/tabs/api/edhrec/display/cards/edhrec_api_response_card_details_display_widget.cpp @@ -65,11 +65,7 @@ void EdhrecApiResponseCardDetailsDisplayWidget::mousePressEvent(QMouseEvent *eve } } -#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) void EdhrecApiResponseCardDetailsDisplayWidget::enterEvent(QEnterEvent *event) -#else -void EdhrecApiResponseCardDetailsDisplayWidget::enterEvent(QEvent *event) -#endif { QWidget::enterEvent(event); backgroundPlateWidget->setFocused(true); diff --git a/cockatrice/src/interface/widgets/tabs/api/edhrec/display/cards/edhrec_api_response_card_details_display_widget.h b/cockatrice/src/interface/widgets/tabs/api/edhrec/display/cards/edhrec_api_response_card_details_display_widget.h index 5daf32412..b09cc4e04 100644 --- a/cockatrice/src/interface/widgets/tabs/api/edhrec/display/cards/edhrec_api_response_card_details_display_widget.h +++ b/cockatrice/src/interface/widgets/tabs/api/edhrec/display/cards/edhrec_api_response_card_details_display_widget.h @@ -39,11 +39,7 @@ private: protected slots: void mousePressEvent(QMouseEvent *event) override; -#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) - void enterEvent(QEnterEvent *event) override; ///< Qt6 hover enter -#else - void enterEvent(QEvent *event) override; ///< Qt5 hover enter -#endif + void enterEvent(QEnterEvent *event) override; ///< Hover enter void leaveEvent(QEvent *event) override; }; diff --git a/cockatrice/src/interface/widgets/tabs/tab_supervisor.cpp b/cockatrice/src/interface/widgets/tabs/tab_supervisor.cpp index 3f30ba8be..3d03a1863 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_supervisor.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_supervisor.cpp @@ -50,7 +50,7 @@ QRect MacOSTabFixStyle::subElementRect(SubElement element, const QStyleOption *o } // Skip over QProxyStyle handling subElementRect, - // This fixes an issue with Qt 5.10 on OSX where the labels for tabs with a button and an icon + // This fixes an issue on OSX where the labels for tabs with a button and an icon // get cut-off too early return QCommonStyle::subElementRect(element, option, widget); } @@ -70,11 +70,7 @@ QSize CloseButton::sizeHint() const return {width, height}; } -#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)) void CloseButton::enterEvent(QEnterEvent *event) -#else -void CloseButton::enterEvent(QEvent *event) -#endif { update(); QAbstractButton::enterEvent(event); @@ -123,7 +119,7 @@ TabSupervisor::TabSupervisor(AbstractClient *_client, QMenu *tabsMenu, QWidget * setIconSize(QSize(15, 15)); #if defined(Q_OS_MAC) - // This is necessary to fix an issue on macOS with qt5.10, + // This is necessary to fix an issue on macOS, // where tabs with icons and buttons get drawn incorrectly tabBar()->setStyle(new MacOSTabFixStyle); #endif diff --git a/cockatrice/src/interface/widgets/tabs/tab_supervisor.h b/cockatrice/src/interface/widgets/tabs/tab_supervisor.h index e6c009fda..d3c147138 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_supervisor.h +++ b/cockatrice/src/interface/widgets/tabs/tab_supervisor.h @@ -71,11 +71,7 @@ public: } protected: -#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)) void enterEvent(QEnterEvent *event) override; -#else - void enterEvent(QEvent *event) override; -#endif void leaveEvent(QEvent *event) override; void paintEvent(QPaintEvent *event) override; }; diff --git a/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_widget.cpp b/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_widget.cpp index 6f888dd26..ffe954308 100644 --- a/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_widget.cpp +++ b/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_widget.cpp @@ -75,11 +75,7 @@ void DeckPreviewWidget::resizeEvent(QResizeEvent *event) } } -#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) void DeckPreviewWidget::enterEvent(QEnterEvent *event) -#else -void DeckPreviewWidget::enterEvent(QEvent *event) -#endif { QWidget::enterEvent(event); diff --git a/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_widget.h b/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_widget.h index 0ed64e9e2..de66c194b 100644 --- a/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_widget.h +++ b/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_widget.h @@ -72,11 +72,7 @@ public slots: void resizeEvent(QResizeEvent *event) override; protected: -#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) - void enterEvent(QEnterEvent *event) override; // Qt6 signature -#else - void enterEvent(QEvent *event) override; // Qt5 signature -#endif + void enterEvent(QEnterEvent *event) override; private: void updateLastModifiedTime(); diff --git a/cockatrice/src/main.cpp b/cockatrice/src/main.cpp index 0524112e4..13f724a8b 100644 --- a/cockatrice/src/main.cpp +++ b/cockatrice/src/main.cpp @@ -137,11 +137,7 @@ void installNewTranslator() QString lang = SettingsCache::instance().personal().getLang(); QString qtNameHint = "qt_" + lang; -#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)) QString qtTranslationPath = QLibraryInfo::path(QLibraryInfo::TranslationsPath); -#else - QString qtTranslationPath = QLibraryInfo::location(QLibraryInfo::TranslationsPath); -#endif bool qtTranslationLoaded = qtTranslator->load(qtNameHint, qtTranslationPath); if (!qtTranslationLoaded) { @@ -366,10 +362,6 @@ int main(int argc, char *argv[]) qApp->setAttribute(Qt::AA_DontShowShortcutsInContextMenus, !SettingsCache::instance().cardsDisplay().getShowShortcuts()); -#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0)) - app.setAttribute(Qt::AA_UseHighDpiPixmaps); -#endif - #ifdef Q_OS_MAC for (const QString &url : pendingMacUrls) { handleActivation(url); diff --git a/libcockatrice_card/CMakeLists.txt b/libcockatrice_card/CMakeLists.txt index 8cf976f92..7d3d47eea 100644 --- a/libcockatrice_card/CMakeLists.txt +++ b/libcockatrice_card/CMakeLists.txt @@ -19,11 +19,7 @@ set(HEADERS libcockatrice/card/relation/card_relation.h ) -if(Qt6_FOUND) - qt6_wrap_cpp(MOC_SOURCES ${HEADERS}) -elseif(Qt5_FOUND) - qt5_wrap_cpp(MOC_SOURCES ${HEADERS}) -endif() +qt6_wrap_cpp(MOC_SOURCES ${HEADERS}) add_library( libcockatrice_card STATIC diff --git a/libcockatrice_card/libcockatrice/card/card_info_comparator.cpp b/libcockatrice_card/libcockatrice/card/card_info_comparator.cpp index f4a74194c..cc0314e21 100644 --- a/libcockatrice_card/libcockatrice/card/card_info_comparator.cpp +++ b/libcockatrice_card/libcockatrice/card/card_info_comparator.cpp @@ -25,22 +25,13 @@ bool CardInfoComparator::operator()(const CardInfoPtr &a, const CardInfoPtr &b) bool CardInfoComparator::compareVariants(const QVariant &a, const QVariant &b) const { - // Determine the type of QVariant based on Qt version -#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) if (a.typeId() != b.typeId()) { -#else - if (a.type() != b.type()) { -#endif // If they are not the same type, compare as strings return a.toString() < b.toString(); } // Perform type-specific comparison -#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) switch (static_cast(a.typeId())) { -#else - switch (static_cast(a.type())) { -#endif case static_cast(QMetaType::Int): return a.toInt() < b.toInt(); case static_cast(QMetaType::Double): diff --git a/libcockatrice_deck_list/CMakeLists.txt b/libcockatrice_deck_list/CMakeLists.txt index 5ccdb5f66..33494b9a9 100644 --- a/libcockatrice_deck_list/CMakeLists.txt +++ b/libcockatrice_deck_list/CMakeLists.txt @@ -14,11 +14,7 @@ set(HEADERS libcockatrice/deck_list/sideboard_plan.h ) -if(Qt6_FOUND) - qt6_wrap_cpp(MOC_SOURCES ${HEADERS}) -elseif(Qt5_FOUND) - qt5_wrap_cpp(MOC_SOURCES ${HEADERS}) -endif() +qt6_wrap_cpp(MOC_SOURCES ${HEADERS}) add_library( libcockatrice_deck_list STATIC diff --git a/libcockatrice_filters/CMakeLists.txt b/libcockatrice_filters/CMakeLists.txt index 74566ca05..905533df8 100644 --- a/libcockatrice_filters/CMakeLists.txt +++ b/libcockatrice_filters/CMakeLists.txt @@ -6,11 +6,7 @@ set(HEADERS libcockatrice/filters/filter_card.h libcockatrice/filters/filter_str libcockatrice/filters/filter_tree.h ) -if(Qt6_FOUND) - qt6_wrap_cpp(MOC_SOURCES ${HEADERS}) -elseif(Qt5_FOUND) - qt5_wrap_cpp(MOC_SOURCES ${HEADERS}) -endif() +qt6_wrap_cpp(MOC_SOURCES ${HEADERS}) add_library( libcockatrice_filters STATIC ${MOC_SOURCES} libcockatrice/filters/filter_card.cpp diff --git a/libcockatrice_interfaces/CMakeLists.txt b/libcockatrice_interfaces/CMakeLists.txt index 7f39a2e15..c0afe09d4 100644 --- a/libcockatrice_interfaces/CMakeLists.txt +++ b/libcockatrice_interfaces/CMakeLists.txt @@ -23,11 +23,7 @@ set(HEADERS libcockatrice/interfaces/noop_card_set_priority_controller.h ) -if(Qt6_FOUND) - qt6_wrap_cpp(MOC_SOURCES ${HEADERS}) -elseif(Qt5_FOUND) - qt5_wrap_cpp(MOC_SOURCES ${HEADERS}) -endif() +qt6_wrap_cpp(MOC_SOURCES ${HEADERS}) add_library(libcockatrice_interfaces STATIC ${MOC_SOURCES}) diff --git a/libcockatrice_models/libcockatrice/models/database/CMakeLists.txt b/libcockatrice_models/libcockatrice/models/database/CMakeLists.txt index 950d6d79f..840f14b08 100644 --- a/libcockatrice_models/libcockatrice/models/database/CMakeLists.txt +++ b/libcockatrice_models/libcockatrice/models/database/CMakeLists.txt @@ -12,11 +12,7 @@ set(HEADERS token/token_edit_model.h ) -if(Qt6_FOUND) - qt6_wrap_cpp(MOC_SOURCES ${HEADERS}) -elseif(Qt5_FOUND) - qt5_wrap_cpp(MOC_SOURCES ${HEADERS}) -endif() +qt6_wrap_cpp(MOC_SOURCES ${HEADERS}) add_library( libcockatrice_models_database STATIC diff --git a/libcockatrice_models/libcockatrice/models/deck_list/CMakeLists.txt b/libcockatrice_models/libcockatrice/models/deck_list/CMakeLists.txt index 851636a35..d4aee3686 100644 --- a/libcockatrice_models/libcockatrice/models/deck_list/CMakeLists.txt +++ b/libcockatrice_models/libcockatrice/models/deck_list/CMakeLists.txt @@ -4,11 +4,7 @@ set(CMAKE_AUTORCC ON) set(HEADERS deck_list_model.h deck_list_sort_filter_proxy_model.h) -if(Qt6_FOUND) - qt6_wrap_cpp(MOC_SOURCES ${HEADERS}) -elseif(Qt5_FOUND) - qt5_wrap_cpp(MOC_SOURCES ${HEADERS}) -endif() +qt6_wrap_cpp(MOC_SOURCES ${HEADERS}) add_library( libcockatrice_models_deck_list STATIC ${MOC_SOURCES} deck_list_model.cpp deck_list_sort_filter_proxy_model.cpp diff --git a/libcockatrice_network/libcockatrice/network/client/abstract/CMakeLists.txt b/libcockatrice_network/libcockatrice/network/client/abstract/CMakeLists.txt index 2753246de..c4a8e4648 100644 --- a/libcockatrice_network/libcockatrice/network/client/abstract/CMakeLists.txt +++ b/libcockatrice_network/libcockatrice/network/client/abstract/CMakeLists.txt @@ -6,11 +6,7 @@ set(HEADERS abstract_client.h) set(SOURCES abstract_client.cpp) -if(Qt6_FOUND) - qt6_wrap_cpp(MOC_SOURCES ${HEADERS}) -elseif(Qt5_FOUND) - qt5_wrap_cpp(MOC_SOURCES ${HEADERS}) -endif() +qt6_wrap_cpp(MOC_SOURCES ${HEADERS}) add_library(libcockatrice_network_client_abstract STATIC ${MOC_SOURCES} ${SOURCES}) diff --git a/libcockatrice_network/libcockatrice/network/client/local/CMakeLists.txt b/libcockatrice_network/libcockatrice/network/client/local/CMakeLists.txt index d12a324dc..2ac12e1fe 100644 --- a/libcockatrice_network/libcockatrice/network/client/local/CMakeLists.txt +++ b/libcockatrice_network/libcockatrice/network/client/local/CMakeLists.txt @@ -6,11 +6,7 @@ set(HEADERS local_client.h) set(SOURCES local_client.cpp) -if(Qt6_FOUND) - qt6_wrap_cpp(MOC_SOURCES ${HEADERS}) -elseif(Qt5_FOUND) - qt5_wrap_cpp(MOC_SOURCES ${HEADERS}) -endif() +qt6_wrap_cpp(MOC_SOURCES ${HEADERS}) add_library(libcockatrice_network_client_local STATIC ${MOC_SOURCES} ${SOURCES}) diff --git a/libcockatrice_network/libcockatrice/network/client/remote/CMakeLists.txt b/libcockatrice_network/libcockatrice/network/client/remote/CMakeLists.txt index 0548700e4..cb68d0c37 100644 --- a/libcockatrice_network/libcockatrice/network/client/remote/CMakeLists.txt +++ b/libcockatrice_network/libcockatrice/network/client/remote/CMakeLists.txt @@ -6,11 +6,7 @@ set(HEADERS remote_client.h) set(SOURCES remote_client.cpp) -if(Qt6_FOUND) - qt6_wrap_cpp(MOC_SOURCES ${HEADERS}) -elseif(Qt5_FOUND) - qt5_wrap_cpp(MOC_SOURCES ${HEADERS}) -endif() +qt6_wrap_cpp(MOC_SOURCES ${HEADERS}) add_library(libcockatrice_network_client_remote STATIC ${MOC_SOURCES} ${SOURCES}) diff --git a/libcockatrice_network/libcockatrice/network/server/local/CMakeLists.txt b/libcockatrice_network/libcockatrice/network/server/local/CMakeLists.txt index 80fb379a4..494ae1294 100644 --- a/libcockatrice_network/libcockatrice/network/server/local/CMakeLists.txt +++ b/libcockatrice_network/libcockatrice/network/server/local/CMakeLists.txt @@ -6,11 +6,7 @@ set(HEADERS local_server.h local_server_interface.h) set(SOURCES local_server.cpp local_server_interface.cpp) -if(Qt6_FOUND) - qt6_wrap_cpp(MOC_SOURCES ${HEADERS}) -elseif(Qt5_FOUND) - qt5_wrap_cpp(MOC_SOURCES ${HEADERS}) -endif() +qt6_wrap_cpp(MOC_SOURCES ${HEADERS}) add_library(libcockatrice_network_server_local STATIC ${MOC_SOURCES} ${SOURCES}) diff --git a/libcockatrice_network/libcockatrice/network/server/remote/CMakeLists.txt b/libcockatrice_network/libcockatrice/network/server/remote/CMakeLists.txt index e883baa0d..9fb63c221 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/CMakeLists.txt +++ b/libcockatrice_network/libcockatrice/network/server/remote/CMakeLists.txt @@ -23,11 +23,7 @@ set(HEADERS serverinfo_user_container.h ) -if(Qt6_FOUND) - qt6_wrap_cpp(MOC_SOURCES ${HEADERS}) -elseif(Qt5_FOUND) - qt5_wrap_cpp(MOC_SOURCES ${HEADERS}) -endif() +qt6_wrap_cpp(MOC_SOURCES ${HEADERS}) add_library( libcockatrice_network_server_remote STATIC diff --git a/libcockatrice_rng/CMakeLists.txt b/libcockatrice_rng/CMakeLists.txt index 6ff2a4537..b988c9d2f 100644 --- a/libcockatrice_rng/CMakeLists.txt +++ b/libcockatrice_rng/CMakeLists.txt @@ -4,11 +4,7 @@ set(CMAKE_AUTORCC ON) set(HEADERS libcockatrice/rng/rng_abstract.h libcockatrice/rng/rng_sfmt.h libcockatrice/rng/sfmt/SFMT.h) -if(Qt6_FOUND) - qt6_wrap_cpp(MOC_SOURCES ${HEADERS}) -elseif(Qt5_FOUND) - qt5_wrap_cpp(MOC_SOURCES ${HEADERS}) -endif() +qt6_wrap_cpp(MOC_SOURCES ${HEADERS}) add_library( libcockatrice_rng STATIC ${MOC_SOURCES} libcockatrice/rng/rng_abstract.cpp libcockatrice/rng/rng_sfmt.cpp diff --git a/libcockatrice_settings/CMakeLists.txt b/libcockatrice_settings/CMakeLists.txt index 8f78130bf..f8e9c2bce 100644 --- a/libcockatrice_settings/CMakeLists.txt +++ b/libcockatrice_settings/CMakeLists.txt @@ -27,11 +27,7 @@ set(HEADERS libcockatrice/settings/visual_deck_storage_settings.h ) -if(Qt6_FOUND) - qt6_wrap_cpp(MOC_SOURCES ${HEADERS}) -elseif(Qt5_FOUND) - qt5_wrap_cpp(MOC_SOURCES ${HEADERS}) -endif() +qt6_wrap_cpp(MOC_SOURCES ${HEADERS}) add_library( libcockatrice_settings STATIC diff --git a/oracle/CMakeLists.txt b/oracle/CMakeLists.txt index a51982625..0736db7f5 100644 --- a/oracle/CMakeLists.txt +++ b/oracle/CMakeLists.txt @@ -64,11 +64,7 @@ set(oracle_RESOURCES oracle.qrc) # ------------------------ # Qt resources # ------------------------ -if(Qt6_FOUND) - qt6_add_resources(oracle_RESOURCES_RCC ${oracle_RESOURCES}) -elseif(Qt5_FOUND) - qt5_add_resources(oracle_RESOURCES_RCC ${oracle_RESOURCES}) -endif() +qt6_add_resources(oracle_RESOURCES_RCC ${oracle_RESOURCES}) # ------------------------ # Include directories @@ -106,37 +102,16 @@ set(ORACLE_MAC_QM_INSTALL_DIR "oracle.app/Contents/Resources/translations") set(ORACLE_UNIX_QM_INSTALL_DIR "share/oracle/translations") set(ORACLE_WIN32_QM_INSTALL_DIR "translations") -if(Qt6_FOUND) - # Qt6 Translations are linked after the executable is created in manual mode - qt6_add_executable( - oracle - WIN32 - MACOSX_BUNDLE - ${oracle_SOURCES} - ${oracle_RESOURCES_RCC} - ${oracle_MOC_SRCS} - MANUAL_FINALIZATION - ) -elseif(Qt5_FOUND) - # Qt5 Translations need to be linked at executable creation time - if(Qt5LinguistTools_FOUND) - if(UPDATE_TRANSLATIONS) - qt5_create_translation(oracle_QM ${translate_SRCS} ${oracle_TS}) - else() - qt5_add_translation(oracle_QM ${oracle_TS}) - endif() - endif() - add_executable(oracle WIN32 MACOSX_BUNDLE ${oracle_MOC_SRCS} ${oracle_QM} ${oracle_RESOURCES_RCC} ${oracle_SOURCES}) - if(UNIX) - if(APPLE) - install(FILES ${oracle_QM} DESTINATION ${ORACLE_MAC_QM_INSTALL_DIR}) - else() - install(FILES ${oracle_QM} DESTINATION ${ORACLE_UNIX_QM_INSTALL_DIR}) - endif() - elseif(WIN32) - install(FILES ${oracle_QM} DESTINATION ${ORACLE_WIN32_QM_INSTALL_DIR}) - endif() -endif() +# Qt6 Translations are linked after the executable is created in manual mode +qt6_add_executable( + oracle + WIN32 + MACOSX_BUNDLE + ${oracle_SOURCES} + ${oracle_RESOURCES_RCC} + ${oracle_MOC_SRCS} + MANUAL_FINALIZATION +) # ------------------------ # Link libraries @@ -285,7 +260,7 @@ endif() # ------------------------ # Qt translations # ------------------------ -if(Qt6_FOUND AND Qt6LinguistTools_FOUND) +if(Qt6LinguistTools_FOUND) #Qt6 Translations happen after the executable is built up if(UPDATE_TRANSLATIONS) qt6_add_translations( @@ -312,6 +287,4 @@ if(Qt6_FOUND AND Qt6LinguistTools_FOUND) endif() endif() -if(Qt6_FOUND) - qt6_finalize_target(oracle) -endif() +qt6_finalize_target(oracle) diff --git a/oracle/src/main.cpp b/oracle/src/main.cpp index f8f069ab0..bb88153b1 100644 --- a/oracle/src/main.cpp +++ b/oracle/src/main.cpp @@ -25,11 +25,7 @@ void installNewTranslator() QString lang = SettingsCache::instance().personal().getLang(); QString qtNameHint = "qt_" + lang; -#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)) QString qtTranslationPath = QLibraryInfo::path(QLibraryInfo::TranslationsPath); -#else - QString qtTranslationPath = QLibraryInfo::location(QLibraryInfo::TranslationsPath); -#endif bool qtTranslationLoaded = qtTranslator->load(qtNameHint, qtTranslationPath); if (!qtTranslationLoaded) { diff --git a/oracle/src/qt-json/json.cpp b/oracle/src/qt-json/json.cpp index 2fffd0f70..ff739b49d 100644 --- a/oracle/src/qt-json/json.cpp +++ b/oracle/src/qt-json/json.cpp @@ -109,12 +109,8 @@ QByteArray Json::serialize(const QVariant &data, bool &success) { str = "null"; } -#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)) else if ((data.typeId() == QMetaType::Type::QVariantList) || (data.typeId() == QMetaType::Type::QStringList)) // variant is a list? -#else - else if ((data.type() == QVariant::List) || (data.type() == QVariant::StringList)) // variant is a list? -#endif { QList values; const QVariantList list = data.toList(); @@ -129,11 +125,7 @@ QByteArray Json::serialize(const QVariant &data, bool &success) str = "[ " + join(values, ", ") + " ]"; } -#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)) - else if ((data.typeId() == QMetaType::Type::QVariantHash)) // variant is a list? -#else - else if (data.type() == QVariant::Hash) // variant is a hash? -#endif + else if ((data.typeId() == QMetaType::Type::QVariantHash)) // variant is a hash? { const QVariantHash vhash = data.toHash(); QHashIterator it(vhash); @@ -155,11 +147,7 @@ QByteArray Json::serialize(const QVariant &data, bool &success) str += join(pairs, ", "); str += " }"; } -#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)) - else if ((data.typeId() == QMetaType::Type::QVariantMap)) // variant is a list? -#else - else if (data.type() == QVariant::Map) // variant is a map? -#endif + else if ((data.typeId() == QMetaType::Type::QVariantMap)) // variant is a map? { const QVariantMap vmap = data.toMap(); QMapIterator it(vmap); @@ -177,39 +165,23 @@ QByteArray Json::serialize(const QVariant &data, bool &success) str += join(pairs, ", "); str += " }"; } -#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)) else if ((data.typeId() == QMetaType::Type::QString) || - (data.typeId() == QMetaType::Type::QByteArray)) // variant is a list? -#else - else if ((data.type() == QVariant::String) || (data.type() == QVariant::ByteArray)) // a string or a byte array? -#endif + (data.typeId() == QMetaType::Type::QByteArray)) // a string or a byte array? { str = sanitizeString(data.toString()).toUtf8(); } -#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)) - else if (data.typeId() == QMetaType::Type::Double) -#else - else if (data.type() == QVariant::Double) // double? -#endif + else if (data.typeId() == QMetaType::Type::Double) // double? { str = QByteArray::number(data.toDouble(), 'g', 20); if (!str.contains(".") && !str.contains("e")) { str += ".0"; } } -#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)) - else if (data.typeId() == QMetaType::Type::Bool) -#else - else if (data.type() == QVariant::Bool) // boolean value? -#endif + else if (data.typeId() == QMetaType::Type::Bool) // boolean value? { str = data.toBool() ? "true" : "false"; } -#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)) - else if (data.typeId() == QMetaType::Type::ULongLong) -#else - else if (data.type() == QVariant::ULongLong) // large unsigned number? -#endif + else if (data.typeId() == QMetaType::Type::ULongLong) // large unsigned number? { str = QByteArray::number(data.value()); } else if (data.canConvert()) // any signed number? diff --git a/servatrice/CMakeLists.txt b/servatrice/CMakeLists.txt index 6e4191beb..aba63800c 100644 --- a/servatrice/CMakeLists.txt +++ b/servatrice/CMakeLists.txt @@ -43,11 +43,7 @@ if(APPLE) set(servatrice_SOURCES ${servatrice_SOURCES} ${CMAKE_CURRENT_SOURCE_DIR}/resources/appicon.icns) endif(APPLE) -if(Qt6_FOUND) - qt6_add_resources(servatrice_RESOURCES_RCC ${servatrice_RESOURCES}) -elseif(Qt5_FOUND) - qt5_add_resources(servatrice_RESOURCES_RCC ${servatrice_RESOURCES}) -endif() +qt6_add_resources(servatrice_RESOURCES_RCC ${servatrice_RESOURCES}) set(QT_DONT_USE_QTGUI TRUE) diff --git a/servatrice/src/smtp/qxtsmtp.cpp b/servatrice/src/smtp/qxtsmtp.cpp index 6326b101d..c81e0955c 100644 --- a/servatrice/src/smtp/qxtsmtp.cpp +++ b/servatrice/src/smtp/qxtsmtp.cpp @@ -54,13 +54,8 @@ QxtSmtp::QxtSmtp(QObject *parent) : QObject(parent) // QObject::connect(socket(), SIGNAL(encrypted()), &qxt_d(), SLOT(ehlo())); QObject::connect(socket(), SIGNAL(connected()), this, SIGNAL(connected())); QObject::connect(socket(), SIGNAL(disconnected()), this, SIGNAL(disconnected())); -#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)) QObject::connect(socket(), SIGNAL(errorOccurred(QAbstractSocket::SocketError)), &qxt_d(), SLOT(socketError(QAbstractSocket::SocketError))); -#else - QObject::connect(socket(), SIGNAL(error(QAbstractSocket::SocketError)), &qxt_d(), - SLOT(socketError(QAbstractSocket::SocketError))); -#endif QObject::connect(this, SIGNAL(authenticated()), &qxt_d(), SLOT(sendNext())); QObject::connect(socket(), SIGNAL(readyRead()), &qxt_d(), SLOT(socketRead())); } From ba203440af514f84a927189f2880b5b2ad43026f Mon Sep 17 00:00:00 2001 From: BruebachL <44814898+BruebachL@users.noreply.github.com> Date: Sat, 8 Aug 2026 22:18:11 +0200 Subject: [PATCH 14/21] [Chat] Don't tag cards that have no match in the database (#7078) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [Chat] Don't tag cards that have no match in the database (#2217) Took 6 minutes Took 4 minutes * Highlight not found but attempted tag Took 6 minutes * Change color to not be server message color Took 3 minutes --------- Co-authored-by: Lukas Brübach --- .../widgets/server/chat_view/chat_view.cpp | 24 ++++++++++++++----- .../widgets/server/chat_view/chat_view.h | 1 + 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/cockatrice/src/interface/widgets/server/chat_view/chat_view.cpp b/cockatrice/src/interface/widgets/server/chat_view/chat_view.cpp index d286af038..e97d25e64 100644 --- a/cockatrice/src/interface/widgets/server/chat_view/chat_view.cpp +++ b/cockatrice/src/interface/widgets/server/chat_view/chat_view.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include @@ -62,12 +63,14 @@ void ChatView::adjustColorsToPalette() serverMessageColor = QColor(0xFF, 0x73, 0x83); otherUserColor = otherUserColor.lighter(150); linkColor = QColor(71, 158, 252); + unresolvedCardTagColor = QColor(0xFF, 0xA5, 0x00); } else { document()->setDefaultStyleSheet(R"( a { text-decoration: none; color: blue; } .blue { color: blue } )"); linkColor = palette().link().color(); + unresolvedCardTagColor = QColor(0xA0, 0x52, 0x2D); } QTimer::singleShot(0, this, &ChatView::refreshBlockColors); @@ -173,13 +176,22 @@ void ChatView::appendHtmlServerMessage(const QString &html, bool optionalIsBold, void ChatView::appendCardTag(QTextCursor &cursor, const QString &cardName) { QTextCharFormat oldFormat = cursor.charFormat(); - QTextCharFormat anchorFormat = oldFormat; - anchorFormat.setForeground(linkColor); - anchorFormat.setAnchor(true); - anchorFormat.setAnchorHref("card://" + cardName); - anchorFormat.setFontItalic(true); + QTextCharFormat cardFormat = oldFormat; + cardFormat.setFontItalic(true); - cursor.setCharFormat(anchorFormat); + if (!CardDatabaseManager::query()->lookupCardByName(cardName)) { + cardFormat.setForeground(unresolvedCardTagColor); + cursor.setCharFormat(cardFormat); + cursor.insertText(cardName); + cursor.setCharFormat(oldFormat); + return; + } + + cardFormat.setForeground(linkColor); + cardFormat.setAnchor(true); + cardFormat.setAnchorHref("card://" + cardName); + + cursor.setCharFormat(cardFormat); cursor.insertText(cardName); cursor.setCharFormat(oldFormat); } diff --git a/cockatrice/src/interface/widgets/server/chat_view/chat_view.h b/cockatrice/src/interface/widgets/server/chat_view/chat_view.h index 506605d24..8d5894613 100644 --- a/cockatrice/src/interface/widgets/server/chat_view/chat_view.h +++ b/cockatrice/src/interface/widgets/server/chat_view/chat_view.h @@ -81,6 +81,7 @@ private: QColor otherUserColor = QColor(0, 65, 255); // dark blue QColor serverMessageColor = QColor(0x85, 0x15, 0x15); QColor linkColor; + QColor unresolvedCardTagColor; private slots: void openLink(const QUrl &link); From bf6b2a90bc1d5b065d69a7fe410114236d371ff8 Mon Sep 17 00:00:00 2001 From: BruebachL <44814898+BruebachL@users.noreply.github.com> Date: Sat, 8 Aug 2026 22:19:21 +0200 Subject: [PATCH 15/21] [Settings][Dialog] Implement search for settings by text, description, tooltip, etc. (#7065) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [Settings] Implement search Took 38 minutes Took 8 seconds Took 9 minutes Took 5 seconds Took 44 seconds Took 25 seconds * Comments Took 23 minutes Took 15 seconds * Comments Took 1 hour 14 minutes * Minor fixes to search Took 13 minutes --------- Co-authored-by: Lukas Brübach --- cockatrice/CMakeLists.txt | 3 + .../dialogs/dlg_load_deck_from_clipboard.cpp | 1 - .../widgets/dialogs/dlg_settings.cpp | 496 ++++++++++++++---- .../interface/widgets/dialogs/dlg_settings.h | 80 ++- .../settings_page/abstract_settings_page.cpp | 196 +++++++ .../settings_page/abstract_settings_page.h | 8 + .../settings_page/messages_settings_page.cpp | 1 + .../settings_search_delegate.cpp | 125 +++++ .../settings_page/settings_search_delegate.h | 38 ++ .../settings_page/settings_search_model.cpp | 145 +++++ .../settings_page/settings_search_model.h | 76 +++ .../settings_page/shortcut_settings_page.cpp | 1 + .../src/interface/widgets/tabs/tab_room.cpp | 2 +- 13 files changed, 1069 insertions(+), 103 deletions(-) create mode 100644 cockatrice/src/interface/widgets/settings_page/abstract_settings_page.cpp create mode 100644 cockatrice/src/interface/widgets/settings_page/settings_search_delegate.cpp create mode 100644 cockatrice/src/interface/widgets/settings_page/settings_search_delegate.h create mode 100644 cockatrice/src/interface/widgets/settings_page/settings_search_model.cpp create mode 100644 cockatrice/src/interface/widgets/settings_page/settings_search_model.h diff --git a/cockatrice/CMakeLists.txt b/cockatrice/CMakeLists.txt index eac9bc83d..af24dfc26 100644 --- a/cockatrice/CMakeLists.txt +++ b/cockatrice/CMakeLists.txt @@ -257,10 +257,13 @@ set(cockatrice_SOURCES src/interface/widgets/server/user/user_list_manager.cpp src/interface/widgets/server/user/user_list_painter.cpp src/interface/widgets/server/user/user_list_widget.cpp + src/interface/widgets/settings_page/abstract_settings_page.cpp src/interface/widgets/settings_page/appearance_settings_page.cpp src/interface/widgets/settings_page/deck_editor_settings_page.cpp src/interface/widgets/settings_page/general_settings_page.cpp src/interface/widgets/settings_page/messages_settings_page.cpp + src/interface/widgets/settings_page/settings_search_delegate.cpp + src/interface/widgets/settings_page/settings_search_model.cpp src/interface/widgets/settings_page/shortcut_settings_page.cpp src/interface/widgets/settings_page/sound_settings_page.cpp src/interface/widgets/settings_page/storage_settings_page.cpp diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_load_deck_from_clipboard.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_load_deck_from_clipboard.cpp index 267b80a2c..a3242c797 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_load_deck_from_clipboard.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_load_deck_from_clipboard.cpp @@ -4,7 +4,6 @@ #include "../../../client/settings/shortcuts_settings.h" #include "../../deck_loader/card_node_function.h" #include "../../deck_loader/deck_loader.h" -#include "dlg_settings.h" #include #include diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_settings.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_settings.cpp index 279e10c28..883cfcd03 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_settings.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_settings.cpp @@ -1,3 +1,8 @@ +/** + * @file dlg_settings.cpp + * @brief Implementation of the main settings dialog + * @ingroup Dialogs + */ #include "dlg_settings.h" #include "../../../client/settings/cache_settings.h" @@ -6,6 +11,8 @@ #include "../settings_page/deck_editor_settings_page.h" #include "../settings_page/general_settings_page.h" #include "../settings_page/messages_settings_page.h" +#include "../settings_page/settings_search_delegate.h" +#include "../settings_page/settings_search_model.h" #include "../settings_page/shortcut_settings_page.h" #include "../settings_page/sound_settings_page.h" #include "../settings_page/storage_settings_page.h" @@ -13,20 +20,36 @@ #include "libcockatrice/card/database/card_database_loader.h" #include "libcockatrice/card/database/card_database_manager.h" +#include #include -#include #include +#include +#include #include -#include +#include +#include +#include +#include #include +#include +#include #include #include #include +#include +#include +#include #include +#include #include #include #include +/** + * @brief Wraps a widget in a scroll area for long settings pages + * @param widget The widget to wrap + * @return The scroll area containing the widget + */ static QScrollArea *makeScrollable(QWidget *widget) { widget->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Maximum); @@ -40,112 +63,355 @@ static QScrollArea *makeScrollable(QWidget *widget) return scrollArea; } -DlgSettings::DlgSettings(QWidget *parent) : QDialog(parent) +/** + * @brief Returns the theme icon resources for each settings page, indexed by SettingsPage order + */ +static QStringList pageIconResources() +{ + return {QStringLiteral("theme:config/general"), QStringLiteral("theme:config/appearance"), + QStringLiteral("theme:config/interface"), QStringLiteral("theme:config/deckeditor"), + QStringLiteral("theme:config/storage"), QStringLiteral("theme:config/messages"), + QStringLiteral("theme:config/sound"), QStringLiteral("theme:config/shorcuts")}; +} + +DlgSettings::DlgSettings(QWidget *parent) : QDialog(parent), currentTabIndex(0), searchActive(false) { auto rec = QGuiApplication::primaryScreen()->availableGeometry(); - this->setMinimumSize(qMin(700, rec.width()), qMin(700, rec.height())); + setMinimumSize(qMin(750, rec.width()), qMin(700, rec.height())); connect(&SettingsCache::instance().personal(), &PersonalSettings::langChanged, this, &DlgSettings::updateLanguage); - contentsWidget = new QListWidget; - contentsWidget->setViewMode(QListView::IconMode); - contentsWidget->setIconSize(QSize(58, 50)); - contentsWidget->setMovement(QListView::Static); - contentsWidget->setMinimumHeight(85); - contentsWidget->setMaximumHeight(85); - contentsWidget->setSpacing(5); - - pagesWidget = new QStackedWidget; - pagesWidget->addWidget(makeScrollable(new GeneralSettingsPage)); - pagesWidget->addWidget(makeScrollable(new AppearanceSettingsPage)); - pagesWidget->addWidget(makeScrollable(new UserInterfaceSettingsPage)); - pagesWidget->addWidget(new DeckEditorSettingsPage); - pagesWidget->addWidget(makeScrollable(new StorageSettingsPage)); - pagesWidget->addWidget(new MessagesSettingsPage); - pagesWidget->addWidget(new SoundSettingsPage); - pagesWidget->addWidget(new ShortcutSettingsPage); - - createIcons(); - contentsWidget->setCurrentRow(0); - - auto *vboxLayout = new QVBoxLayout; - vboxLayout->addWidget(contentsWidget); - vboxLayout->addWidget(pagesWidget); - - auto *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok); - connect(buttonBox, &QDialogButtonBox::accepted, this, &DlgSettings::close); - - auto *mainLayout = new QVBoxLayout; - mainLayout->addLayout(vboxLayout); - mainLayout->addSpacing(2); - mainLayout->addWidget(buttonBox); - setLayout(mainLayout); + setupUi(); connect(&SettingsCache::instance().personal(), &PersonalSettings::langChanged, this, &DlgSettings::retranslateUi); retranslateUi(); + searchEdit->setFocus(); + adjustSize(); } -void DlgSettings::createIcons() +void DlgSettings::setupUi() { - generalButton = new QListWidgetItem(contentsWidget); - generalButton->setTextAlignment(Qt::AlignHCenter); - generalButton->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); - generalButton->setIcon(QPixmap("theme:config/general")); + // Search bar + searchEdit = new QLineEdit; + searchEdit->setClearButtonEnabled(true); + searchEdit->addAction(QPixmap("theme:icons/search"), QLineEdit::LeadingPosition); + searchEdit->installEventFilter(this); + connect(searchEdit, &QLineEdit::textChanged, this, &DlgSettings::onSearchTextChanged); - appearanceButton = new QListWidgetItem(contentsWidget); - appearanceButton->setTextAlignment(Qt::AlignHCenter); - appearanceButton->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); - appearanceButton->setIcon(QPixmap("theme:config/appearance")); + auto *searchLayout = new QHBoxLayout; + searchLayout->addWidget(searchEdit); - userInterfaceButton = new QListWidgetItem(contentsWidget); - userInterfaceButton->setTextAlignment(Qt::AlignHCenter); - userInterfaceButton->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); - userInterfaceButton->setIcon(QPixmap("theme:config/interface")); + // Tab bar (built in setupTabBar) + setupTabBar(); - deckEditorButton = new QListWidgetItem(contentsWidget); - deckEditorButton->setTextAlignment(Qt::AlignHCenter); - deckEditorButton->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); - deckEditorButton->setIcon(QPixmap("theme:config/deckeditor")); + // Pages stacked widget + pagesWidget = new QStackedWidget; - storageButton = new QListWidgetItem(contentsWidget); - storageButton->setTextAlignment(Qt::AlignHCenter); - storageButton->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); - storageButton->setIcon(QPixmap("theme:config/storage")); + auto *generalPage = new GeneralSettingsPage; + auto *appearancePage = new AppearanceSettingsPage; + auto *userInterfacePage = new UserInterfaceSettingsPage; + auto *deckEditorPage = new DeckEditorSettingsPage; + auto *storagePage = new StorageSettingsPage; + auto *messagesPage = new MessagesSettingsPage; + auto *soundPage = new SoundSettingsPage; + auto *shortcutsPage = new ShortcutSettingsPage; - messagesButton = new QListWidgetItem(contentsWidget); - messagesButton->setTextAlignment(Qt::AlignHCenter); - messagesButton->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); - messagesButton->setIcon(QPixmap("theme:config/messages")); + pages.append(generalPage); + pages.append(appearancePage); + pages.append(userInterfacePage); + pages.append(deckEditorPage); + pages.append(storagePage); + pages.append(messagesPage); + pages.append(soundPage); + pages.append(shortcutsPage); - soundButton = new QListWidgetItem(contentsWidget); - soundButton->setTextAlignment(Qt::AlignHCenter); - soundButton->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); - soundButton->setIcon(QPixmap("theme:config/sound")); + pagesWidget->addWidget(makeScrollable(generalPage)); + pagesWidget->addWidget(makeScrollable(appearancePage)); + pagesWidget->addWidget(makeScrollable(userInterfacePage)); + pagesWidget->addWidget(makeScrollable(deckEditorPage)); + pagesWidget->addWidget(makeScrollable(storagePage)); + pagesWidget->addWidget(messagesPage); + pagesWidget->addWidget(soundPage); + pagesWidget->addWidget(shortcutsPage); - shortcutsButton = new QListWidgetItem(contentsWidget); - shortcutsButton->setTextAlignment(Qt::AlignHCenter); - shortcutsButton->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); - shortcutsButton->setIcon(QPixmap("theme:config/shorcuts")); + Q_ASSERT(pages.size() == NumPages); - connect(contentsWidget, &QListWidget::currentItemChanged, this, &DlgSettings::changePage); + // Search results view (hidden by default) + searchResultsView = new QListView; + searchResultsView->setUniformItemSizes(false); + searchResultsView->setSelectionMode(QAbstractItemView::SingleSelection); + searchResultsView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + searchResultsView->setVisible(false); + searchResultsView->setStyleSheet( + "QListView::item:selected { background: palette(highlight); color: palette(highlighted-text); }"); + + searchModel = new SettingsSearchModel(this); + searchDelegate = new SettingsSearchDelegate(this); + searchResultsView->setModel(searchModel); + searchResultsView->setItemDelegate(searchDelegate); + connect(searchResultsView, &QListView::clicked, this, &DlgSettings::onSearchResultClicked); + + connect(&SettingsCache::instance(), &SettingsCache::themeChanged, this, [this] { + const QStringList icons = pageIconResources(); + for (int i = 0; i < tabButtons.size() && i < icons.size(); ++i) { + tabButtons[i]->setIcon(QPixmap(icons[i])); + } + searchDelegate->setPageIcons(icons); + searchResultsView->viewport()->update(); + }); + + // Build search index after pages are created + buildSearchIndex(); + + // Pages container (stacked widget + search results overlay) + pagesContainer = new QWidget; + auto *containerLayout = new QStackedLayout; + containerLayout->setStackingMode(QStackedLayout::StackAll); + containerLayout->addWidget(pagesWidget); + containerLayout->addWidget(searchResultsView); + pagesContainer->setLayout(containerLayout); + + // Bottom buttons + auto *buttonBox = new QHBoxLayout; + buttonBox->addStretch(); + okButton = new QPushButton; + okButton->setDefault(true); + connect(okButton, &QPushButton::clicked, this, &DlgSettings::close); + buttonBox->addWidget(okButton); + + // Main layout + auto *mainLayout = new QVBoxLayout; + mainLayout->addLayout(searchLayout); + mainLayout->addWidget(tabBarWidget); + auto *separator = new QFrame; + separator->setFrameShape(QFrame::HLine); + separator->setFrameShadow(QFrame::Sunken); + mainLayout->addWidget(separator); + mainLayout->addWidget(pagesContainer); + mainLayout->addSpacing(4); + mainLayout->addLayout(buttonBox); + setLayout(mainLayout); + + // Keyboard shortcuts + auto *searchShortcut = new QShortcut(QKeySequence(Qt::CTRL | Qt::Key_F), this); + connect(searchShortcut, &QShortcut::activated, searchEdit, qOverload<>(&QLineEdit::setFocus)); + + auto *nextTabShortcut = new QShortcut(QKeySequence(Qt::CTRL | Qt::Key_Tab), this); + connect(nextTabShortcut, &QShortcut::activated, this, [this] { + int next = (currentTabIndex + 1) % tabButtons.size(); + setActiveTab(next); + }); + + auto *prevTabShortcut = new QShortcut(QKeySequence(Qt::CTRL | Qt::SHIFT | Qt::Key_Tab), this); + connect(prevTabShortcut, &QShortcut::activated, this, [this] { + int prev = (currentTabIndex - 1 + tabButtons.size()) % tabButtons.size(); + setActiveTab(prev); + }); + + // Initialize to first tab + setActiveTab(0); } -void DlgSettings::changePage(QListWidgetItem *current, QListWidgetItem *previous) +void DlgSettings::setupTabBar() { - if (!current) { - current = previous; + tabBarWidget = new QWidget; + auto *tabLayout = new QHBoxLayout; + tabLayout->setContentsMargins(0, 0, 0, 0); + tabLayout->setSpacing(2); + + const QStringList iconResources = pageIconResources(); + + for (int i = 0; i < iconResources.size(); ++i) { + auto *tabButton = new QToolButton; + tabButton->setCheckable(true); + tabButton->setIcon(QPixmap(iconResources[i])); + tabButton->setIconSize(QSize(48, 48)); + tabButton->setToolButtonStyle(Qt::ToolButtonTextUnderIcon); + tabButton->setAutoExclusive(true); + tabButton->setMinimumHeight(85); + tabButton->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); + + connect(tabButton, &QToolButton::clicked, this, [this, idx = i] { onTabClicked(idx); }); + + tabButtons.append(tabButton); + tabLayout->addWidget(tabButton); } - pagesWidget->setCurrentIndex(contentsWidget->row(current)); + tabBarWidget->setLayout(tabLayout); +} + +void DlgSettings::buildSearchIndex() +{ + QList allEntries; + + const QStringList pageNames = translatedPageNames(); + searchDelegate->setPageNames(pageNames); + searchDelegate->setPageIcons(pageIconResources()); + + for (int i = 0; i < pages.size(); ++i) { + QList pageEntries = pages[i]->getSearchEntries(); + for (auto &entry : pageEntries) { + if (entry.pageIndex == -1) { + entry.pageIndex = i; + } + } + allEntries.append(pageEntries); + } + + searchModel->setSourceEntries(allEntries); +} + +void DlgSettings::onTabClicked(int index) +{ + if (searchActive) { + switchToTabMode(); + } + setActiveTab(index); +} + +void DlgSettings::setActiveTab(int index) +{ + if (index < 0 || index >= tabButtons.size()) { + return; + } + + currentTabIndex = index; + pagesWidget->setCurrentIndex(index); + + for (int i = 0; i < tabButtons.size(); ++i) { + tabButtons[i]->setChecked(i == index); + } + + // Style active tab with a thick accent border + subtle background tint + for (int i = 0; i < tabButtons.size(); ++i) { + if (i == index) { + tabButtons[i]->setStyleSheet("QToolButton { border: none; border-bottom: 3px solid palette(highlight); " + "border-top-left-radius: 4px; border-top-right-radius: 4px; " + "background: palette(window); padding-bottom: 1px; }"); + } else { + tabButtons[i]->setStyleSheet("QToolButton { border: none; border-bottom: 1px solid transparent; " + "border-top-left-radius: 4px; border-top-right-radius: 4px; " + "background: transparent; }"); + } + } +} + +void DlgSettings::flashWidget(QWidget *widget) +{ + auto *overlay = new QWidget(widget); + overlay->setGeometry(widget->rect()); + overlay->setAttribute(Qt::WA_TransparentForMouseEvents, true); + + QPalette pal = overlay->palette(); + QColor flashColor = pal.color(QPalette::Highlight); + flashColor.setAlpha(100); + pal.setBrush(QPalette::Window, flashColor); + overlay->setPalette(pal); + overlay->setAutoFillBackground(true); + + auto *effect = new QGraphicsOpacityEffect(overlay); + effect->setOpacity(0.0); + overlay->setGraphicsEffect(effect); + overlay->show(); + overlay->raise(); + + auto *flashIn = new QPropertyAnimation(effect, "opacity"); + flashIn->setDuration(120); + flashIn->setStartValue(0.0); + flashIn->setEndValue(0.6); + flashIn->setEasingCurve(QEasingCurve::OutCubic); + + auto *fadeOut = new QPropertyAnimation(effect, "opacity"); + fadeOut->setDuration(900); + fadeOut->setStartValue(0.6); + fadeOut->setEndValue(0.0); + fadeOut->setEasingCurve(QEasingCurve::InCubic); + + auto *group = new QSequentialAnimationGroup(overlay); + group->addAnimation(flashIn); + group->addAnimation(fadeOut); + + connect(group, &QSequentialAnimationGroup::finished, overlay, &QWidget::deleteLater); + + group->start(QAbstractAnimation::DeleteWhenStopped); +} + +void DlgSettings::onSearchTextChanged(const QString &text) +{ + searchModel->setFilterString(text); + + if (searchModel->isFilterActive() && !text.trimmed().isEmpty()) { + if (!searchActive) { + switchToSearchMode(); + } + if (searchModel->rowCount(QModelIndex()) > 0) { + searchResultsView->setCurrentIndex(searchModel->index(0)); + } + } else if (searchActive) { + switchToTabMode(); + } +} + +void DlgSettings::switchToSearchMode() +{ + searchActive = true; + tabBarWidget->setVisible(false); + pagesWidget->setVisible(false); + searchResultsView->setVisible(true); + if (searchModel->rowCount(QModelIndex()) > 0) { + searchResultsView->setCurrentIndex(searchModel->index(0)); + } +} + +void DlgSettings::switchToTabMode() +{ + searchActive = false; + tabBarWidget->setVisible(true); + pagesWidget->setVisible(true); + searchResultsView->setVisible(false); + searchEdit->blockSignals(true); + searchEdit->clear(); + searchEdit->blockSignals(false); + setActiveTab(currentTabIndex); +} + +void DlgSettings::onSearchResultClicked(const QModelIndex &index) +{ + navigateToSearchResult(index); +} + +void DlgSettings::navigateToSearchResult(const QModelIndex &index) +{ + SettingsSearchEntry entry = searchModel->entryForIndex(index); + if (entry.pageIndex < 0 || entry.pageIndex >= pages.size()) { + return; + } + + // Switch to the page + switchToTabMode(); + setActiveTab(entry.pageIndex); + + // Scroll to the widget, focus it, and flash to highlight it + if (entry.widget) { + QWidget *widget = entry.widget; + while (widget) { + if (auto *scrollArea = qobject_cast(widget)) { + scrollArea->ensureWidgetVisible(entry.widget); + break; + } + widget = widget->parentWidget(); + } + entry.widget->setFocus(); + flashWidget(entry.widget); + } } void DlgSettings::setTab(int index) { - if (index <= contentsWidget->count() - 1 && index >= 0) { - changePage(contentsWidget->item(index), contentsWidget->currentItem()); - contentsWidget->setCurrentRow(index); + if (index >= 0 && index < tabButtons.size()) { + setActiveTab(index); } } @@ -155,6 +421,49 @@ void DlgSettings::updateLanguage() installNewTranslator(); } +bool DlgSettings::eventFilter(QObject *watched, QEvent *event) +{ + if (watched == searchEdit && event->type() == QEvent::KeyPress) { + auto *keyEvent = static_cast(event); + if (keyEvent->key() == Qt::Key_Escape) { + if (searchActive) { + switchToTabMode(); + return true; + } + } else if (keyEvent->key() == Qt::Key_Return || keyEvent->key() == Qt::Key_Enter) { + if (searchActive) { + if (searchResultsView->currentIndex().isValid()) { + navigateToSearchResult(searchResultsView->currentIndex()); + } + return true; + } + } else if (keyEvent->key() == Qt::Key_Down) { + if (searchActive) { + int nextRow = searchResultsView->currentIndex().row() + 1; + if (nextRow >= searchModel->rowCount()) { + nextRow = 0; + } + searchResultsView->setCurrentIndex(searchModel->index(nextRow)); + searchResultsView->scrollTo(searchModel->index(nextRow)); + return true; + } + } else if (keyEvent->key() == Qt::Key_Up) { + if (searchActive) { + int prevRow = searchResultsView->currentIndex().row() - 1; + if (prevRow < 0) { + prevRow = searchModel->rowCount() - 1; + } + if (prevRow >= 0) { + searchResultsView->setCurrentIndex(searchModel->index(prevRow)); + searchResultsView->scrollTo(searchModel->index(prevRow)); + } + return true; + } + } + } + return QDialog::eventFilter(watched, event); +} + void DlgSettings::closeEvent(QCloseEvent *event) { bool showLoadError = true; @@ -209,7 +518,6 @@ void DlgSettings::closeEvent(QCloseEvent *event) if (!QDir(SettingsCache::instance().paths().getDeckPath()).exists() || SettingsCache::instance().paths().getDeckPath().isEmpty()) { - //! \todo Prompt to create the deck directory. if (QMessageBox::critical( this, tr("Error"), tr("The path to your deck directory is invalid. Would you like to go back and set the correct path?"), @@ -221,7 +529,6 @@ void DlgSettings::closeEvent(QCloseEvent *event) if (!QDir(SettingsCache::instance().paths().getPicsPath()).exists() || SettingsCache::instance().paths().getPicsPath().isEmpty()) { - //! \todo Prompt to create the pictures directory. if (QMessageBox::critical(this, tr("Error"), tr("The path to your card pictures directory is invalid. Would you like to go back " "and set the correct path?"), @@ -236,15 +543,26 @@ void DlgSettings::closeEvent(QCloseEvent *event) void DlgSettings::retranslateUi() { setWindowTitle(tr("Settings")); + retranslateTabNames(); - generalButton->setText(tr("General")); - appearanceButton->setText(tr("Appearance")); - userInterfaceButton->setText(tr("User Interface")); - storageButton->setText(tr("Storage")); - deckEditorButton->setText(tr("Card Sources")); - messagesButton->setText(tr("Chat")); - soundButton->setText(tr("Sound")); - shortcutsButton->setText(tr("Shortcuts")); + searchEdit->setPlaceholderText(tr("Search settings...")); + okButton->setText(tr("OK")); - contentsWidget->reset(); + // Rebuild search index for translated text + buildSearchIndex(); +} + +QStringList DlgSettings::translatedPageNames() +{ + return {tr("General"), tr("Appearance"), tr("User Interface"), tr("Card Sources"), + tr("Storage"), tr("Chat"), tr("Sound"), tr("Shortcuts")}; +} + +void DlgSettings::retranslateTabNames() +{ + const QStringList tabLabels = translatedPageNames(); + + for (int i = 0; i < tabButtons.size() && i < tabLabels.size(); ++i) { + tabButtons[i]->setText(tabLabels[i]); + } } diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_settings.h b/cockatrice/src/interface/widgets/dialogs/dlg_settings.h index 3ffee6388..b700f7af9 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_settings.h +++ b/cockatrice/src/interface/widgets/dialogs/dlg_settings.h @@ -1,43 +1,99 @@ /** * @file dlg_settings.h + * @brief Main settings dialog for the Cockatrice client * @ingroup Dialogs */ -//! \todo Document this file. - #ifndef DLG_SETTINGS_H #define DLG_SETTINGS_H -#include #include #include +class QPushButton; + inline Q_LOGGING_CATEGORY(DlgSettingsLog, "dlg_settings"); -class QListWidget; class QStackedWidget; -class QListWidgetItem; +class QToolButton; +class QListView; +class QLineEdit; +class AbstractSettingsPage; +class SettingsSearchModel; +class SettingsSearchDelegate; + +/** + * @brief Main application settings dialog with tabbed navigation and search + * + * Provides a modern settings interface organized into tabbed pages. Users can + * either navigate by clicking tabs or search for specific settings using the + * built-in search bar. Search results are filtered and ranked by relevance. + */ class DlgSettings : public QDialog { Q_OBJECT public: + /** + * @brief Page order in the tab bar, matching the order pages are added in setupUi() + * + * Use these values instead of raw indices so reordering pages never silently + * breaks external callers like tab_room.cpp. + */ + enum SettingsPage + { + GeneralPage = 0, + AppearancePage, + UserInterfacePage, + DeckEditorPage, + StoragePage, + MessagesPage, + SoundPage, + ShortcutsPage, + NumPages + }; + explicit DlgSettings(QWidget *parent = nullptr); void setTab(int index); private slots: - void changePage(QListWidgetItem *current, QListWidgetItem *previous); + void onTabClicked(int index); + void onSearchTextChanged(const QString &text); + void onSearchResultClicked(const QModelIndex &index); void updateLanguage(); private: - QListWidget *contentsWidget; - QStackedWidget *pagesWidget; - QListWidgetItem *generalButton, *appearanceButton, *userInterfaceButton, *deckEditorButton, *storageButton, - *messagesButton, *soundButton, *shortcutsButton; - void createIcons(); + // UI elements + QLineEdit *searchEdit; ///< Search bar for filtering settings + QWidget *tabBarWidget; ///< Container widget for the tab buttons + QList tabButtons; ///< Navigation tab buttons + QStackedWidget *pagesWidget; ///< Stacked widget containing settings pages + QListView *searchResultsView; ///< Search results list view + QWidget *pagesContainer; ///< Container stacking pages and search results + QPushButton *okButton; ///< Button to close the dialog + + // Data + QList pages; ///< All settings page instances + SettingsSearchModel *searchModel; ///< Model for search results + SettingsSearchDelegate *searchDelegate; ///< Delegate for search result rendering + int currentTabIndex; ///< Currently active tab index + bool searchActive; ///< Whether search mode is active + + void setupUi(); + void setupTabBar(); + void buildSearchIndex(); + void switchToTabMode(); + void switchToSearchMode(); + void navigateToSearchResult(const QModelIndex &index); + void setActiveTab(int index); + static void flashWidget(QWidget *widget); + static QStringList translatedPageNames(); + void retranslateUi(); + void retranslateTabNames(); protected: void closeEvent(QCloseEvent *event) override; + bool eventFilter(QObject *watched, QEvent *event) override; }; -#endif +#endif // DLG_SETTINGS_H diff --git a/cockatrice/src/interface/widgets/settings_page/abstract_settings_page.cpp b/cockatrice/src/interface/widgets/settings_page/abstract_settings_page.cpp new file mode 100644 index 000000000..9a70e94c5 --- /dev/null +++ b/cockatrice/src/interface/widgets/settings_page/abstract_settings_page.cpp @@ -0,0 +1,196 @@ +#include "abstract_settings_page.h" + +#include "settings_search_model.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +/** + * @brief Recursively collects all widgets within a layout + * @param layout The layout to walk + * @param widgets Output list of (widget, containing layout) pairs + */ +static void collectWidgets(QLayout *layout, QList> &widgets) +{ + for (int i = 0; i < layout->count(); ++i) { + QLayoutItem *item = layout->itemAt(i); + if (!item) { + continue; + } + if (QWidget *widget = item->widget()) { + widgets.append({widget, layout}); + } else if (QLayout *subLayout = item->layout()) { + collectWidgets(subLayout, widgets); + } + } +} + +/** + * @brief Rejects QLabels that are not setting names + * + * HTML link labels, path values, and excessively long labels are filtered out. + */ +static bool isValidSettingLabel(const QLabel *label) +{ + const QString &text = label->text(); + if (Qt::mightBeRichText(text)) { + return false; + } + if (text.contains(QLatin1Char('/')) || text.contains(QLatin1Char('\\'))) { + return false; + } + if (text.size() > 60) { + return false; + } + return true; +} + +/** + * @brief Finds the control associated with a setting label + * + * Uses the explicit buddy if set, otherwise the widget in the cell (or slot) + * immediately following the label within the same layout. Returns nullptr when + * no obvious control is found. + */ +static QWidget *controlForLabel(QLabel *label, QLayout *containingLayout) +{ + if (QWidget *buddy = label->buddy()) { + return buddy; + } + + if (auto *grid = qobject_cast(containingLayout)) { + int index = grid->indexOf(label); + if (index != -1) { + int row = 0; + int column = 0; + int rowSpan = 1; + int columnSpan = 1; + grid->getItemPosition(index, &row, &column, &rowSpan, &columnSpan); + if (QLayoutItem *next = grid->itemAtPosition(row, column + columnSpan)) { + if (QWidget *nextWidget = next->widget()) { + return nextWidget; + } + } + } + } else { + int index = containingLayout->indexOf(label); + if (index != -1) { + for (int i = index + 1; i < containingLayout->count(); ++i) { + if (QLayoutItem *next = containingLayout->itemAt(i)) { + if (QWidget *nextWidget = next->widget()) { + return nextWidget; + } + } + } + } + } + + return nullptr; +} + +/** + * @brief Builds the extended search text for an entry + * + * Combines the group title, label, and any extra searchable text derived from + * the associated control (placeholder, prefix/suffix, tooltip). Combo values + * and numeric tooltips are excluded since they change at runtime and would + * make the search index stale. + */ +static QString buildFullSearchText(const QString &groupTitle, const QString &cleanLabel, QWidget *control) +{ + QStringList parts = {groupTitle, cleanLabel}; + if (control) { + if (auto *lineEdit = qobject_cast(control)) { + parts.append(lineEdit->placeholderText()); + } else if (auto *spinBox = qobject_cast(control)) { + parts.append(spinBox->prefix()); + parts.append(spinBox->suffix()); + } + if (!control->toolTip().isEmpty()) { + bool isNumeric = false; + control->toolTip().toInt(&isNumeric); + if (!isNumeric) { + parts.append(control->toolTip()); + } + } + } + parts.removeAll(QString()); + return parts.join(QLatin1Char(' ')); +} + +QList AbstractSettingsPage::getSearchEntries() +{ + return autoDetectSearchEntries(this, -1); +} + +QList AbstractSettingsPage::autoDetectSearchEntries(QWidget *page, int pageIndex) +{ + QList entries; + + const auto children = page->children(); + for (QObject *child : children) { + auto *groupBox = qobject_cast(child); + if (!groupBox) { + continue; + } + + QString groupTitle = groupBox->title(); + if (groupTitle.isEmpty()) { + continue; + } + + QLayout *groupLayout = groupBox->layout(); + if (!groupLayout) { + continue; + } + + QList> widgets; + collectWidgets(groupLayout, widgets); + + for (const auto &pair : widgets) { + QWidget *widget = pair.first; + QString label; + + auto *checkBox = qobject_cast(widget); + if (checkBox) { + label = checkBox->text(); + } else { + auto *labelWidget = qobject_cast(widget); + if (!labelWidget || labelWidget->text().isEmpty() || !isValidSettingLabel(labelWidget)) { + continue; + } + label = labelWidget->text(); + } + + if (label.isEmpty()) { + continue; + } + + // Strip accelerator markers (&) for search + QString cleanLabel = label; + cleanLabel.remove(QLatin1Char('&')); + + QWidget *control = widget; + if (auto *labelWidget = qobject_cast(widget)) { + control = controlForLabel(labelWidget, pair.second); + if (!control) { + continue; + } + } + + entries.append(SettingsSearchEntry{.pageIndex = pageIndex, + .groupTitle = groupTitle, + .widgetLabel = cleanLabel, + .fullSearchText = buildFullSearchText(groupTitle, cleanLabel, control), + .widget = control}); + } + } + + return entries; +} diff --git a/cockatrice/src/interface/widgets/settings_page/abstract_settings_page.h b/cockatrice/src/interface/widgets/settings_page/abstract_settings_page.h index 4cbf2d71a..3140f94f3 100644 --- a/cockatrice/src/interface/widgets/settings_page/abstract_settings_page.h +++ b/cockatrice/src/interface/widgets/settings_page/abstract_settings_page.h @@ -1,16 +1,24 @@ #ifndef COCKATRICE_ABSTRACT_SETTINGS_PAGE_H #define COCKATRICE_ABSTRACT_SETTINGS_PAGE_H +#include #include #define WIKI_CUSTOM_PIC_URL "https://github.com/Cockatrice/Cockatrice/wiki/Custom-Picture-Download-URLs" #define WIKI_CUSTOM_SHORTCUTS "https://github.com/Cockatrice/Cockatrice/wiki/Custom-Keyboard-Shortcuts" #define WIKI_TRANSLATION_FAQ "https://github.com/Cockatrice/Cockatrice/wiki/Translation-FAQ" +struct SettingsSearchEntry; + class AbstractSettingsPage : public QWidget { + Q_OBJECT public: virtual void retranslateUi() = 0; + virtual QList getSearchEntries(); + +protected: + static QList autoDetectSearchEntries(QWidget *page, int pageIndex); }; #endif // COCKATRICE_ABSTRACT_SETTINGS_PAGE_H diff --git a/cockatrice/src/interface/widgets/settings_page/messages_settings_page.cpp b/cockatrice/src/interface/widgets/settings_page/messages_settings_page.cpp index e9878434b..e4f24ab73 100644 --- a/cockatrice/src/interface/widgets/settings_page/messages_settings_page.cpp +++ b/cockatrice/src/interface/widgets/settings_page/messages_settings_page.cpp @@ -90,6 +90,7 @@ MessagesSettingsPage::MessagesSettingsPage() highlightNotice->addWidget(&hexHighlightLabel, 1, 2); highlightNotice->addWidget(customAlertString, 0, 0); highlightNotice->addWidget(&customAlertStringLabel, 1, 0); + customAlertStringLabel.setBuddy(customAlertString); highlightGroupBox = new QGroupBox; highlightGroupBox->setLayout(highlightNotice); diff --git a/cockatrice/src/interface/widgets/settings_page/settings_search_delegate.cpp b/cockatrice/src/interface/widgets/settings_page/settings_search_delegate.cpp new file mode 100644 index 000000000..2fdff3bce --- /dev/null +++ b/cockatrice/src/interface/widgets/settings_page/settings_search_delegate.cpp @@ -0,0 +1,125 @@ +/** + * @file settings_search_delegate.cpp + * @brief Implementation of the custom settings search result delegate + * @ingroup Dialogs + */ +#include "settings_search_delegate.h" + +#include "settings_search_model.h" + +#include + +SettingsSearchDelegate::SettingsSearchDelegate(QObject *parent) : QStyledItemDelegate(parent) +{ +} + +void SettingsSearchDelegate::setPageNames(const QStringList &names) +{ + pageNames = names; +} + +void SettingsSearchDelegate::setPageIcons(const QStringList &iconResources) +{ + pageIcons.clear(); + for (const QString &resource : iconResources) { + pageIcons.append(QPixmap(resource)); + } +} + +void SettingsSearchDelegate::paint(QPainter *painter, + const QStyleOptionViewItem &option, + const QModelIndex &index) const +{ + painter->save(); + + SettingsSearchEntry entry = index.data(SettingsSearchModel::EntryRole).value(); + + bool isSelected = option.state & QStyle::State_Selected; + bool isHovered = option.state & QStyle::State_MouseOver; + + // Background + QColor bgColor = isSelected ? option.palette.color(QPalette::Highlight) + : isHovered ? option.palette.color(QPalette::Midlight) + : option.palette.color(QPalette::Base); + painter->fillRect(option.rect, bgColor); + + if (isSelected) { + // Accent bar on the left to make the selection unmistakable + painter->fillRect(QRect(option.rect.left(), option.rect.top(), 4, option.rect.height()), + option.palette.color(QPalette::Highlight).darker(150)); + } + + int leftMargin = 12; + int topMargin = 8; + int rightMargin = 12; + int bottomMargin = 4; + + QRect contentRect = option.rect.adjusted(leftMargin, topMargin, -rightMargin, -bottomMargin); + int yPos = contentRect.top(); + + // Icon of the related settings page + const int iconSize = 24; + QPixmap pageIcon = + (entry.pageIndex >= 0 && entry.pageIndex < pageIcons.size()) ? pageIcons.at(entry.pageIndex) : QPixmap(); + int iconOffset = pageIcon.isNull() ? 0 : iconSize + 8; + if (!pageIcon.isNull()) { + QRect iconRect(contentRect.left(), contentRect.top() + (contentRect.height() - iconSize) / 2, iconSize, + iconSize); + painter->drawPixmap(iconRect, pageIcon); + } + + QRect textRect = contentRect.adjusted(iconOffset, 0, 0, 0); + + // Breadcrumb: "Page > Group" + QFont breadcrumbFont = option.font; + breadcrumbFont.setPointSize(breadcrumbFont.pointSize() - 1); + breadcrumbFont.setBold(true); + + QColor breadcrumbColor = + isSelected ? option.palette.color(QPalette::HighlightedText) : option.palette.color(QPalette::Text); + if (!isSelected) { + breadcrumbColor.setAlpha(180); + } + + QString pageName; + if (entry.pageIndex >= 0 && entry.pageIndex < pageNames.size()) { + pageName = pageNames[entry.pageIndex]; + } else { + pageName = QString::number(entry.pageIndex); + } + + QString breadcrumbText = QStringLiteral("%1 > %2").arg(pageName, entry.groupTitle); + painter->setFont(breadcrumbFont); + painter->setPen(breadcrumbColor); + painter->drawText(QRect(textRect.left(), yPos, textRect.width(), 20), Qt::AlignLeft | Qt::AlignVCenter, + breadcrumbText); + yPos += 20; + + // Setting label + QFont labelFont = option.font; + labelFont.setPointSize(labelFont.pointSize() + 1); + labelFont.setBold(isSelected); + + QColor labelColor = + isSelected ? option.palette.color(QPalette::HighlightedText) : option.palette.color(QPalette::Text); + + painter->setFont(labelFont); + painter->setPen(labelColor); + painter->drawText(QRect(textRect.left(), yPos, textRect.width(), 24), Qt::AlignLeft | Qt::AlignVCenter, + entry.widgetLabel); + yPos += 24; + + // Bottom separator + QPen separatorPen(option.palette.color(QPalette::Mid), 1); + painter->setPen(separatorPen); + painter->drawLine(option.rect.left() + leftMargin, option.rect.bottom(), option.rect.right() - rightMargin, + option.rect.bottom()); + + painter->restore(); +} + +QSize SettingsSearchDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const +{ + Q_UNUSED(index); + return QSize(option.rect.width(), 56); +} diff --git a/cockatrice/src/interface/widgets/settings_page/settings_search_delegate.h b/cockatrice/src/interface/widgets/settings_page/settings_search_delegate.h new file mode 100644 index 000000000..aa296049a --- /dev/null +++ b/cockatrice/src/interface/widgets/settings_page/settings_search_delegate.h @@ -0,0 +1,38 @@ +/** + * @file settings_search_delegate.h + * @brief Custom delegate for rendering settings search results + * @ingroup Dialogs + */ +#ifndef COCKATRICE_SETTINGS_SEARCH_DELEGATE_H +#define COCKATRICE_SETTINGS_SEARCH_DELEGATE_H + +#include +#include + +/** + * @brief Custom paint delegate for settings search result items + * + * Renders each search result with a breadcrumb line ("Page > Group"), + * the setting label, and a subtle separator. Supports selected/hovered states. + */ +class SettingsSearchDelegate : public QStyledItemDelegate +{ + Q_OBJECT +public: + explicit SettingsSearchDelegate(QObject *parent = nullptr); + + void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override; + QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override; + + /** @brief Sets the translated page names for breadcrumb display */ + void setPageNames(const QStringList &names); + + /** @brief Sets the icons shown in front of results, indexed by page position */ + void setPageIcons(const QStringList &iconResources); + +private: + QStringList pageNames; ///< Translated page names indexed by page position + QList pageIcons; ///< Icons of the related settings pages +}; + +#endif // COCKATRICE_SETTINGS_SEARCH_DELEGATE_H diff --git a/cockatrice/src/interface/widgets/settings_page/settings_search_model.cpp b/cockatrice/src/interface/widgets/settings_page/settings_search_model.cpp new file mode 100644 index 000000000..71f859d4b --- /dev/null +++ b/cockatrice/src/interface/widgets/settings_page/settings_search_model.cpp @@ -0,0 +1,145 @@ +/** + * @file settings_search_model.cpp + * @brief Implementation of the settings search list model + * @ingroup Dialogs + */ +#include "settings_search_model.h" + +#include +#include + +SettingsSearchModel::SettingsSearchModel(QObject *parent) : QAbstractListModel(parent) +{ +} + +void SettingsSearchModel::setSourceEntries(const QList &entries) +{ + beginResetModel(); + sourceEntries = entries; + endResetModel(); + rebuildFilter(); +} + +void SettingsSearchModel::setFilterString(const QString &text) +{ + filterActive = !text.trimmed().isEmpty(); + if (filterActive) { + filterQuery = text.trimmed(); + filterRegex = + QRegularExpression(QRegularExpression::escape(filterQuery), QRegularExpression::CaseInsensitiveOption); + } + rebuildFilter(); +} + +bool SettingsSearchModel::isFilterActive() const +{ + return filterActive; +} + +/** + * @brief Calculates a relevance score for a single entry against the query + * + * Scoring priorities (highest to lowest): + * 1. Label starts with query -> 100 + * 2. Label contains query -> 80 + * 3. Group title starts with query -> 60 + * 4. Group title contains query -> 40 + * 5. Full text regex match -> 20 + * 6. No match -> 0 (excluded from results) + */ +static int relevanceScore(const SettingsSearchEntry &entry, const QString &query, const QRegularExpression ®ex) +{ + QString lowerQuery = query.toLower(); + + // Label matches are most relevant + QString label = entry.widgetLabel.toLower(); + if (label.startsWith(lowerQuery)) { + return 100; + } + if (label.contains(lowerQuery)) { + return 80; + } + + // Group title matches are next + QString group = entry.groupTitle.toLower(); + if (group.startsWith(lowerQuery)) { + return 60; + } + if (group.contains(lowerQuery)) { + return 40; + } + + // Full text match is least relevant + if (entry.fullSearchText.contains(regex)) { + return 20; + } + + return 0; +} + +void SettingsSearchModel::rebuildFilter() +{ + beginResetModel(); + filteredIndices.clear(); + + if (!filterActive) { + for (int i = 0; i < sourceEntries.size(); ++i) { + filteredIndices.append(i); + } + } else { + QList> scored; // + for (int i = 0; i < sourceEntries.size(); ++i) { + const SettingsSearchEntry &entry = sourceEntries[i]; + // Skip conditional settings that are currently disabled or hidden + if (entry.widget && (!entry.widget->isEnabled() || entry.widget->isHidden())) { + continue; + } + int score = relevanceScore(entry, filterQuery, filterRegex); + if (score > 0) { + scored.append({-score, i}); // negative for descending sort + } + } + std::sort(scored.begin(), scored.end()); + for (const auto &pair : scored) { + filteredIndices.append(pair.second); + } + } + endResetModel(); +} + +int SettingsSearchModel::rowCount(const QModelIndex &parent) const +{ + if (parent.isValid()) { + return 0; + } + return filteredIndices.size(); +} + +QVariant SettingsSearchModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid() || index.row() >= filteredIndices.size()) { + return {}; + } + + const SettingsSearchEntry &entry = sourceEntries[filteredIndices[index.row()]]; + + switch (role) { + case EntryRole: + return QVariant::fromValue(entry); + case Qt::DisplayRole: + return entry.widgetLabel; + case Qt::ToolTipRole: + return QStringLiteral("%1 > %2 > %3") + .arg(QString::number(entry.pageIndex), entry.groupTitle, entry.widgetLabel); + default: + return {}; + } +} + +SettingsSearchEntry SettingsSearchModel::entryForIndex(const QModelIndex &index) const +{ + if (!index.isValid() || index.row() >= filteredIndices.size()) { + return {}; + } + return sourceEntries[filteredIndices[index.row()]]; +} diff --git a/cockatrice/src/interface/widgets/settings_page/settings_search_model.h b/cockatrice/src/interface/widgets/settings_page/settings_search_model.h new file mode 100644 index 000000000..0b590f180 --- /dev/null +++ b/cockatrice/src/interface/widgets/settings_page/settings_search_model.h @@ -0,0 +1,76 @@ +/** + * @file settings_search_model.h + * @brief Data model for the settings search feature + * @ingroup Dialogs + */ +#ifndef COCKATRICE_SETTINGS_SEARCH_MODEL_H +#define COCKATRICE_SETTINGS_SEARCH_MODEL_H + +#include +#include +#include +#include + +/** + * @brief Represents a single searchable setting entry + * + * Each settings page provides a list of these entries via getSearchEntries(). + * The model uses them for filtering, relevance scoring, and display. + */ +struct SettingsSearchEntry +{ + int pageIndex; ///< Index of the settings page this entry belongs to + QString groupTitle; ///< Title of the group/section within the page + QString widgetLabel; ///< Display label for the setting widget + QString fullSearchText; ///< Extended search text (label, control text, tooltip) for full-text matching + QWidget *widget; ///< Pointer to the setting widget for focus/scrolling +}; + +/** + * @brief List model providing filtered, ranked search results + * + * Manages a list of SettingsSearchEntry items. When a filter string is set, + * entries are scored by relevance and sorted so the best matches appear first. + * Supports custom roles for accessing entry fields from views and delegates. + */ +class SettingsSearchModel : public QAbstractListModel +{ + Q_OBJECT +public: + /** + * @brief Custom data role for accessing the full entry + */ + enum Roles + { + EntryRole = Qt::UserRole + 1, ///< Full SettingsSearchEntry object + }; + + explicit SettingsSearchModel(QObject *parent = nullptr); + + /** @brief Replaces the source entries and rebuilds the filter */ + void setSourceEntries(const QList &entries); + /** @brief Sets the filter string and recalculates the results */ + void setFilterString(const QString &text); + /** @brief Whether a non-empty filter is currently active */ + bool isFilterActive() const; + + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + + /** @brief Returns the full entry for a given model index */ + SettingsSearchEntry entryForIndex(const QModelIndex &index) const; + +private: + QList sourceEntries; ///< Complete unfiltered entry list + QList filteredIndices; ///< Indices into sourceEntries matching the filter + QRegularExpression filterRegex; ///< Compiled regex for the current filter + QString filterQuery; ///< Current filter query string + bool filterActive = false; ///< Whether filtering is active + + /** @brief Recalculates the filtered index list and ranking */ + void rebuildFilter(); +}; + +Q_DECLARE_METATYPE(SettingsSearchEntry) + +#endif // COCKATRICE_SETTINGS_SEARCH_MODEL_H diff --git a/cockatrice/src/interface/widgets/settings_page/shortcut_settings_page.cpp b/cockatrice/src/interface/widgets/settings_page/shortcut_settings_page.cpp index e3b9994ea..1f1867f7c 100644 --- a/cockatrice/src/interface/widgets/settings_page/shortcut_settings_page.cpp +++ b/cockatrice/src/interface/widgets/settings_page/shortcut_settings_page.cpp @@ -123,6 +123,7 @@ void ShortcutSettingsPage::retranslateUi() currentActionGroupLabel->setText(tr("Section:")); currentActionLabel->setText(tr("Action:")); currentShortcutLabel->setText(tr("Shortcut:")); + editShortcutGroupBox->setTitle(tr("Shortcut editor")); editTextBox->retranslateUi(); faqLabel->setText(QString("
%2").arg(WIKI_CUSTOM_SHORTCUTS).arg(tr("How to set custom shortcuts"))); btnResetAll->setText(tr("Restore all default shortcuts")); diff --git a/cockatrice/src/interface/widgets/tabs/tab_room.cpp b/cockatrice/src/interface/widgets/tabs/tab_room.cpp index 899f38ec2..5705c184e 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_room.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_room.cpp @@ -242,7 +242,7 @@ void TabRoom::actClearChat() void TabRoom::actOpenChatSettings() { DlgSettings settings(this); - settings.setTab(4); + settings.setTab(DlgSettings::MessagesPage); settings.exec(); } From adf574e038bf798f847a09f6ab8da46579ecdabd Mon Sep 17 00:00:00 2001 From: BruebachL <44814898+BruebachL@users.noreply.github.com> Date: Sat, 8 Aug 2026 22:27:41 +0200 Subject: [PATCH 16/21] [Settings] Shuffle some settings around (#7084) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [Settings] Shuffle some settings around Took 21 minutes Took 1 hour 25 minutes * [Settings] Camel case everything * Revert debug schema change * Add new classes * Fix card counters writing to global * Fix CI tests * Fix Windows CI * interface() is a protected keyword for MSVC Took 5 minutes Took 5 seconds * [Settings] Keep menu settings on the appearance settings page Leave the 'Menu settings' group box on the appearance settings page for now; relocating it to the user interface settings page will be done in a separate PR. Took 6 minutes --------- Co-authored-by: Lukas Brübach --- .../spoiler_background_updater.cpp | 4 +- .../src/client/settings/cache_settings.cpp | 49 +- .../src/client/settings/cache_settings.h | 11 +- .../client/settings/card_counter_settings.cpp | 2 +- cockatrice/src/game/player/player_actions.cpp | 22 +- cockatrice/src/game/zones/view_zone_logic.cpp | 4 +- .../board/abstract_card_item.cpp | 4 +- .../src/game_graphics/board/arrow_item.cpp | 2 +- .../src/game_graphics/board/card_item.cpp | 10 +- .../dialogs/dlg_create_token.cpp | 2 +- cockatrice/src/game_graphics/game_scene.cpp | 4 +- cockatrice/src/game_graphics/game_view.cpp | 12 +- .../game_graphics/player/menu/tally_menu.cpp | 6 +- .../player/player_graphics_item.cpp | 12 +- .../src/game_graphics/zones/hand_zone.cpp | 10 +- .../src/game_graphics/zones/table_zone.cpp | 6 +- .../game_graphics/zones/view_zone_widget.cpp | 24 +- .../card_picture_loader.cpp | 4 +- .../card_picture_loader_worker.cpp | 4 +- .../card_picture_loader_worker_work.cpp | 8 +- .../deck_editor_deck_dock_widget.cpp | 26 +- .../interface/widgets/general/home_widget.cpp | 22 +- .../interface/widgets/menus/tearoff_menu.h | 8 +- .../widgets/replay/replay_manager.cpp | 2 +- .../replay/replay_quick_settings_widget.cpp | 4 +- .../widgets/replay/replay_widget.cpp | 2 +- .../widgets/server/game_selector.cpp | 7 +- .../widgets/server/user/user_list_widget.cpp | 14 +- .../appearance_settings_page.cpp | 66 +- .../settings_page/appearance_settings_page.h | 4 +- .../deck_editor_settings_page.cpp | 12 +- .../user_interface_settings_page.cpp | 80 +-- .../widgets/tabs/abstract_tab_deck_editor.cpp | 4 +- .../tabs/api/archidekt/tab_archidekt.cpp | 10 +- .../tabs/api/edhrec/tab_edhrec_main.cpp | 9 +- .../src/interface/widgets/tabs/tab_game.cpp | 2 +- .../interface/widgets/tabs/tab_supervisor.cpp | 8 +- .../visual_database_display_widget.cpp | 10 +- .../visual_deck_editor_sample_hand_widget.cpp | 8 +- .../visual_deck_editor_widget.cpp | 8 +- ...ual_deck_storage_quick_settings_widget.cpp | 7 +- cockatrice/src/interface/window_main.cpp | 22 +- cockatrice/src/main.cpp | 7 +- libcockatrice_interfaces/CMakeLists.txt | 1 + ...nterface_cards_display_settings_provider.h | 10 +- .../interface_deck_editor_settings_provider.h | 15 + .../interface_interface_settings_provider.h | 6 +- .../interface_personal_settings_provider.h | 12 - ...ce_visual_deck_storage_settings_provider.h | 7 - libcockatrice_settings/CMakeLists.txt | 6 + .../settings/appearance_settings.cpp | 71 +++ .../settings/appearance_settings.h | 45 ++ .../settings/cache_storage_settings.cpp | 2 +- .../settings/card_database_settings.cpp | 10 +- .../settings/cards_display_settings.cpp | 125 ++-- .../settings/cards_display_settings.h | 33 +- .../libcockatrice/settings/chat_settings.cpp | 48 +- .../settings/deck_editor_settings.cpp | 48 ++ .../settings/deck_editor_settings.h | 35 ++ .../settings/download_settings.cpp | 22 + .../settings/download_settings.h | 8 + .../settings/game_filters_settings.cpp | 68 +-- .../libcockatrice/settings/game_settings.cpp | 64 +- .../settings/interface_settings.cpp | 115 ++-- .../settings/interface_settings.h | 19 +- .../settings/network_settings.cpp | 56 ++ .../libcockatrice/settings/network_settings.h | 36 ++ .../libcockatrice/settings/paths_settings.cpp | 20 +- .../settings/personal_settings.cpp | 127 ---- .../settings/personal_settings.h | 31 - .../settings/recents_settings.cpp | 8 +- .../settings/servers_settings.cpp | 32 +- .../settings/settings_migration.cpp | 567 ++++++++++++------ .../libcockatrice/settings/sound_settings.cpp | 4 +- .../settings/updates_settings.cpp | 12 +- .../settings/visual_deck_storage_settings.cpp | 144 ++--- .../settings/visual_deck_storage_settings.h | 20 - tests/settings/settings_defaults_test.cpp | 258 ++++++-- tests/settings/settings_migration_test.cpp | 395 ++++++++++-- 79 files changed, 1899 insertions(+), 1123 deletions(-) create mode 100644 libcockatrice_interfaces/libcockatrice/interfaces/interface_deck_editor_settings_provider.h create mode 100644 libcockatrice_settings/libcockatrice/settings/appearance_settings.cpp create mode 100644 libcockatrice_settings/libcockatrice/settings/appearance_settings.h create mode 100644 libcockatrice_settings/libcockatrice/settings/deck_editor_settings.cpp create mode 100644 libcockatrice_settings/libcockatrice/settings/deck_editor_settings.h create mode 100644 libcockatrice_settings/libcockatrice/settings/network_settings.cpp create mode 100644 libcockatrice_settings/libcockatrice/settings/network_settings.h diff --git a/cockatrice/src/client/network/update/card_spoiler/spoiler_background_updater.cpp b/cockatrice/src/client/network/update/card_spoiler/spoiler_background_updater.cpp index 480ff701d..dae633717 100644 --- a/cockatrice/src/client/network/update/card_spoiler/spoiler_background_updater.cpp +++ b/cockatrice/src/client/network/update/card_spoiler/spoiler_background_updater.cpp @@ -14,8 +14,8 @@ #include #include #include +#include #include -#include #include #define SPOILERS_STATUS_URL "https://raw.githubusercontent.com/Cockatrice/Magic-Spoiler/files/SpoilerSeasonEnabled" @@ -23,7 +23,7 @@ SpoilerBackgroundUpdater::SpoilerBackgroundUpdater(QObject *apParent) : QObject(apParent), cardUpdateProcess(nullptr) { - isSpoilerDownloadEnabled = SettingsCache::instance().personal().getDownloadSpoilersStatus(); + isSpoilerDownloadEnabled = SettingsCache::instance().downloads().getDownloadSpoilersStatus(); if (isSpoilerDownloadEnabled) { // Start the process of checking if we're in spoiler season // File exists means we're in spoiler season diff --git a/cockatrice/src/client/settings/cache_settings.cpp b/cockatrice/src/client/settings/cache_settings.cpp index 4f36bbb3b..1052629a5 100644 --- a/cockatrice/src/client/settings/cache_settings.cpp +++ b/cockatrice/src/client/settings/cache_settings.cpp @@ -11,18 +11,21 @@ #include #include #include +#include #include #include #include #include #include #include +#include #include #include #include #include #include #include +#include #include #include #include @@ -135,8 +138,11 @@ SettingsCache::SettingsCache() personalSettings = new PersonalSettings(settingsPath, this); cardsDisplaySettings = new CardsDisplaySettings(settingsPath, this); interfaceSettings = new InterfaceSettings(settingsPath, this); + deckEditorSettings = new DeckEditorSettings(settingsPath, this); pathsSettings = new PathsSettings(settingsPath, this); visualDeckStorageSettings = new VisualDeckStorageSettings(settingsPath, this); + appearanceSettings = new AppearanceSettings(settingsPath, this); + networkSettings = new NetworkSettings(settingsPath, this); // Forward ICardDatabasePathProvider signal from PathsSettings connect(pathsSettings, &PathsSettings::cardDatabasePathChanged, this, @@ -147,7 +153,7 @@ SettingsCache::SettingsCache() releaseChannels << new StableReleaseChannel(); releaseChannels << new BetaReleaseChannel(); - themeName = personalSettings->getThemeName(); + themeName = appearanceSettings->getThemeName(); loadPaths(); } @@ -155,7 +161,7 @@ SettingsCache::SettingsCache() void SettingsCache::setThemeName(const QString &_themeName) { themeName = _themeName; - personalSettings->setThemeName(themeName); + appearanceSettings->setThemeName(themeName); emit themeChanged(); } @@ -216,15 +222,15 @@ void SettingsCache::loadPaths() // customPicsPath derived from picsPath QString picsPath = pathsIni.value("paths/pics").toString(); if (picsPath.endsWith("/")) { - computePath("custompics", picsPath + "CUSTOM/"); + computePath("customPics", picsPath + "CUSTOM/"); } else { - computePath("custompics", picsPath + "/CUSTOM/"); + computePath("customPics", picsPath + "/CUSTOM/"); } - computePath("customsets", dataPath + "/customsets/"); - computeFilePath("carddatabase", dataPath + "/cards.xml"); - computeFilePath("tokendatabase", dataPath + "/tokens.xml"); - computeFilePath("spoilerdatabase", dataPath + "/spoiler.xml"); + computePath("customSets", dataPath + "/customsets/"); + computeFilePath("cardDatabase", dataPath + "/cards.xml"); + computeFilePath("tokenDatabase", dataPath + "/tokens.xml"); + computeFilePath("spoilerDatabase", dataPath + "/spoiler.xml"); } void SettingsCache::resetPaths() @@ -272,12 +278,12 @@ QString SettingsCache::getTokenDatabasePath() const // INetworkSettingsProvider - delegate to sub-objects int SettingsCache::getKeepAlive() const { - return personalSettings->getKeepAlive(); + return networkSettings->getKeepAlive(); } int SettingsCache::getTimeOut() const { - return personalSettings->getTimeOut(); + return networkSettings->getTimeOut(); } bool SettingsCache::getNotifyAboutUpdates() const @@ -287,17 +293,17 @@ bool SettingsCache::getNotifyAboutUpdates() const void SettingsCache::setKnownMissingFeatures(const QString &_knownMissingFeatures) { - interfaceSettings->setKnownMissingFeatures(_knownMissingFeatures); + networkSettings->setKnownMissingFeatures(_knownMissingFeatures); } QString SettingsCache::getKnownMissingFeatures() { - return interfaceSettings->getKnownMissingFeatures(); + return networkSettings->getKnownMissingFeatures(); } QString SettingsCache::getClientID() { - return personalSettings->getClientID(); + return networkSettings->getClientID(); } // Release channels @@ -412,7 +418,7 @@ CardsDisplaySettings &SettingsCache::cardsDisplay() const return *cardsDisplaySettings; } -InterfaceSettings &SettingsCache::interface() const +InterfaceSettings &SettingsCache::userInterface() const { return *interfaceSettings; } @@ -422,7 +428,22 @@ PathsSettings &SettingsCache::paths() const return *pathsSettings; } +DeckEditorSettings &SettingsCache::deckEditor() const +{ + return *deckEditorSettings; +} + VisualDeckStorageSettings &SettingsCache::visualDeckStorage() const { return *visualDeckStorageSettings; } + +AppearanceSettings &SettingsCache::appearance() const +{ + return *appearanceSettings; +} + +NetworkSettings &SettingsCache::network() const +{ + return *networkSettings; +} diff --git a/cockatrice/src/client/settings/cache_settings.h b/cockatrice/src/client/settings/cache_settings.h index f9cce4cfe..f2886d167 100644 --- a/cockatrice/src/client/settings/cache_settings.h +++ b/cockatrice/src/client/settings/cache_settings.h @@ -29,6 +29,7 @@ class CardOverrideSettings; class CardsDisplaySettings; class ChatSettings; class DebugSettings; +class DeckEditorSettings; class DownloadSettings; class GameFiltersSettings; class GameSettings; @@ -44,6 +45,8 @@ class SoundSettings; class TabsSettings; class UpdatesSettings; class VisualDeckStorageSettings; +class AppearanceSettings; +class NetworkSettings; class QSettings; class SettingsCache : public ICardDatabasePathProvider, public INetworkSettingsProvider @@ -75,8 +78,11 @@ private: PersonalSettings *personalSettings; CardsDisplaySettings *cardsDisplaySettings; InterfaceSettings *interfaceSettings; + DeckEditorSettings *deckEditorSettings; PathsSettings *pathsSettings; VisualDeckStorageSettings *visualDeckStorageSettings; + AppearanceSettings *appearanceSettings; + NetworkSettings *networkSettings; QString themeName; @@ -138,9 +144,12 @@ public: [[nodiscard]] UpdatesSettings &updates() const; [[nodiscard]] PersonalSettings &personal() const; [[nodiscard]] CardsDisplaySettings &cardsDisplay() const; - [[nodiscard]] InterfaceSettings &interface() const; + [[nodiscard]] InterfaceSettings &userInterface() const; + [[nodiscard]] DeckEditorSettings &deckEditor() const; [[nodiscard]] PathsSettings &paths() const; [[nodiscard]] VisualDeckStorageSettings &visualDeckStorage() const; + [[nodiscard]] AppearanceSettings &appearance() const; + [[nodiscard]] NetworkSettings &network() const; [[nodiscard]] bool getIsPortableBuild() const { diff --git a/cockatrice/src/client/settings/card_counter_settings.cpp b/cockatrice/src/client/settings/card_counter_settings.cpp index 662ae0c7d..d4030c174 100644 --- a/cockatrice/src/client/settings/card_counter_settings.cpp +++ b/cockatrice/src/client/settings/card_counter_settings.cpp @@ -5,7 +5,7 @@ #include CardCounterSettings::CardCounterSettings(const QString &settingsPath, QObject *parent) - : SettingsManager(settingsPath + "global.ini", "cards", "counters", parent) + : SettingsManager(settingsPath + "card_counters.ini", "cards", "counters", parent) { } diff --git a/cockatrice/src/game/player/player_actions.cpp b/cockatrice/src/game/player/player_actions.cpp index 12abb994f..67c3295d6 100644 --- a/cockatrice/src/game/player/player_actions.cpp +++ b/cockatrice/src/game/player/player_actions.cpp @@ -69,7 +69,7 @@ void PlayerActions::playCard(CardItem *card, bool faceDown) const CardInfo &info = exactCard.getInfo(); int tableRow = info.getUiAttributes().tableRow; - bool playToStack = SettingsCache::instance().interface().getPlayToStack(); + bool playToStack = SettingsCache::instance().userInterface().getPlayToStack(); QString currentZone = card->getZone()->getName(); if (!faceDown && currentZone == ZoneNames::STACK && tableRow == 3) { cmd.set_target_zone(ZoneNames::GRAVE); @@ -312,7 +312,7 @@ void PlayerActions::actDrawCard() void PlayerActions::actRequestMulliganDialog() { - int startSize = SettingsCache::instance().interface().getStartingHandSize(); + int startSize = SettingsCache::instance().userInterface().getStartingHandSize(); int handSize = player->getHandZone()->getCards().size(); int deckSize = player->getDeckZone()->getCards().size() + handSize; @@ -328,7 +328,7 @@ void PlayerActions::actMulligan(int number) } doMulligan(number); - SettingsCache::instance().interface().setStartingHandSize(number); + SettingsCache::instance().userInterface().setStartingHandSize(number); } void PlayerActions::actMulliganSameSize() @@ -932,13 +932,13 @@ void PlayerActions::setLastTokenInfo(CardInfoPtr cardInfo) return; } - lastTokenInfo = {.name = cardInfo->getName(), - .color = cardInfo->getColors().isEmpty() ? QString() : cardInfo->getColors().left(1).toLower(), - .pt = cardInfo->getPowTough(), - .annotation = SettingsCache::instance().interface().getAnnotateTokens() ? cardInfo->getText() : "", - .destroy = true, - .providerId = - SettingsCache::instance().cardOverrides().getCardPreferenceOverride(cardInfo->getName())}; + lastTokenInfo = { + .name = cardInfo->getName(), + .color = cardInfo->getColors().isEmpty() ? QString() : cardInfo->getColors().left(1).toLower(), + .pt = cardInfo->getPowTough(), + .annotation = SettingsCache::instance().userInterface().getAnnotateTokens() ? cardInfo->getText() : "", + .destroy = true, + .providerId = SettingsCache::instance().cardOverrides().getCardPreferenceOverride(cardInfo->getName())}; lastTokenTableRow = TableZone::tableRowToGridY(cardInfo->getUiAttributes().tableRow); @@ -1171,7 +1171,7 @@ void PlayerActions::createCard(const CardItem *sourceCard, } cmd.set_pt(cardInfo->getPowTough().toStdString()); - if (SettingsCache::instance().interface().getAnnotateTokens()) { + if (SettingsCache::instance().userInterface().getAnnotateTokens()) { cmd.set_annotation(cardInfo->getText().toStdString()); } else { cmd.set_annotation(""); diff --git a/cockatrice/src/game/zones/view_zone_logic.cpp b/cockatrice/src/game/zones/view_zone_logic.cpp index 60fe39bb2..2ef04284a 100644 --- a/cockatrice/src/game/zones/view_zone_logic.cpp +++ b/cockatrice/src/game/zones/view_zone_logic.cpp @@ -58,7 +58,7 @@ bool ZoneViewZoneLogic::prepareAddCard(int x) // autoclose check is done both here and in removeCard - if (cards.isEmpty() && !doInsert && SettingsCache::instance().interface().getCloseEmptyCardView()) { + if (cards.isEmpty() && !doInsert && SettingsCache::instance().userInterface().getCloseEmptyCardView()) { emit closeView(); } @@ -145,7 +145,7 @@ void ZoneViewZoneLogic::removeCard(int position, bool toNewZone) // card gets dragged within the view. // Another autoclose check is done in prepareAddCard so that the view autocloses if the last card was moved to an // unrevealed portion of the same zone. - if (cards.isEmpty() && SettingsCache::instance().interface().getCloseEmptyCardView() && toNewZone) { + if (cards.isEmpty() && SettingsCache::instance().userInterface().getCloseEmptyCardView() && toNewZone) { emit closeView(); return; } diff --git a/cockatrice/src/game_graphics/board/abstract_card_item.cpp b/cockatrice/src/game_graphics/board/abstract_card_item.cpp index a9e0167d4..e0029ee2d 100644 --- a/cockatrice/src/game_graphics/board/abstract_card_item.cpp +++ b/cockatrice/src/game_graphics/board/abstract_card_item.cpp @@ -12,9 +12,9 @@ #include #include #include +#include #include #include -#include AbstractCardItem::AbstractCardItem(QGraphicsItem *parent, const CardRef &cardRef, PlayerLogic *_owner, int _id) : ArrowTarget(_owner, parent), id(_id), cardRef(cardRef), tapped(false), facedown(false), tapAngle(0), @@ -107,7 +107,7 @@ QSizeF AbstractCardItem::getTranslatedSize(QPainter *painter) const void AbstractCardItem::transformPainter(QPainter *painter, const QSizeF &translatedSize, int angle) { - const int MAX_FONT_SIZE = SettingsCache::instance().personal().getMaxFontSize(); + const int MAX_FONT_SIZE = SettingsCache::instance().appearance().getMaxFontSize(); const int fontSize = std::max(9, MAX_FONT_SIZE); QRectF totalBoundingRect = painter->combinedTransform().mapRect(boundingRect()); diff --git a/cockatrice/src/game_graphics/board/arrow_item.cpp b/cockatrice/src/game_graphics/board/arrow_item.cpp index c40827361..ce8967bb5 100644 --- a/cockatrice/src/game_graphics/board/arrow_item.cpp +++ b/cockatrice/src/game_graphics/board/arrow_item.cpp @@ -262,7 +262,7 @@ void ArrowDragItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) if (startZone->getName() == ZoneNames::HAND) { startCard->playCard(false); CardInfoPtr ci = startCard->getCard().getCardPtr(); - bool playToStack = SettingsCache::instance().interface().getPlayToStack(); + bool playToStack = SettingsCache::instance().userInterface().getPlayToStack(); if (ci && ((!playToStack && ci->getUiAttributes().tableRow == 3) || (playToStack && ci->getUiAttributes().tableRow != 0 && startCard->getZone()->getName() != ZoneNames::STACK))) { diff --git a/cockatrice/src/game_graphics/board/card_item.cpp b/cockatrice/src/game_graphics/board/card_item.cpp index 63e298886..c40c8c214 100644 --- a/cockatrice/src/game_graphics/board/card_item.cpp +++ b/cockatrice/src/game_graphics/board/card_item.cpp @@ -281,7 +281,7 @@ void CardItem::drawArrow(const QColor &arrowColor) auto *game = owner->getGame(); PlayerLogic *arrowOwner = game->getPlayerManager()->getActiveLocalPlayer(game->getGameState()->getActivePlayer()); int phase = 0; // 0 means to not set the phase - if (SettingsCache::instance().interface().getDoNotDeleteArrowsInSubPhases()) { + if (SettingsCache::instance().userInterface().getDoNotDeleteArrowsInSubPhases()) { int currentPhase = game->getGameState()->getCurrentPhase(); phase = Phases::getLastSubphase(currentPhase) + 1; } @@ -400,7 +400,7 @@ void CardItem::playCard(bool faceDown) if (tz) { emit tz->toggleTapped(); } else { - if (SettingsCache::instance().interface().getClickPlaysAllSelected()) { + if (SettingsCache::instance().userInterface().getClickPlaysAllSelected()) { if (faceDown) { emit playSelectedFaceDown(this); } else { @@ -464,7 +464,7 @@ static bool isUnwritableRevealZone(CardZoneLogic *zone) void CardItem::handleClickedToPlay(bool shiftHeld) { if (isUnwritableRevealZone(state->getZone())) { - if (SettingsCache::instance().interface().getClickPlaysAllSelected()) { + if (SettingsCache::instance().userInterface().getClickPlaysAllSelected()) { emit hideSelected(this); } else { state->getZone()->removeCard(this); @@ -481,7 +481,7 @@ void CardItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) return; } if ((event->modifiers() != Qt::AltModifier) && (event->button() == Qt::LeftButton) && - (!SettingsCache::instance().interface().getDoubleClickToPlay())) { + (!SettingsCache::instance().userInterface().getDoubleClickToPlay())) { handleClickedToPlay(event->modifiers().testFlag(Qt::ShiftModifier)); } if (owner != nullptr) { @@ -493,7 +493,7 @@ void CardItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) void CardItem::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) { if ((event->modifiers() != Qt::AltModifier) && (event->buttons() == Qt::LeftButton) && - (SettingsCache::instance().interface().getDoubleClickToPlay())) { + (SettingsCache::instance().userInterface().getDoubleClickToPlay())) { handleClickedToPlay(event->modifiers().testFlag(Qt::ShiftModifier)); } event->accept(); diff --git a/cockatrice/src/game_graphics/dialogs/dlg_create_token.cpp b/cockatrice/src/game_graphics/dialogs/dlg_create_token.cpp index e53069025..b311d2ebd 100644 --- a/cockatrice/src/game_graphics/dialogs/dlg_create_token.cpp +++ b/cockatrice/src/game_graphics/dialogs/dlg_create_token.cpp @@ -189,7 +189,7 @@ void DlgCreateToken::tokenSelectionChanged(const QModelIndex ¤t, const QMo const QChar cardColor = cardInfo->getColorChar(); colorEdit->setCurrentIndex(colorEdit->findData(cardColor, Qt::UserRole, Qt::MatchFixedString)); ptEdit->setText(cardInfo->getPowTough()); - if (SettingsCache::instance().interface().getAnnotateTokens()) { + if (SettingsCache::instance().userInterface().getAnnotateTokens()) { annotationEdit->setText(cardInfo->getText()); } } else { diff --git a/cockatrice/src/game_graphics/game_scene.cpp b/cockatrice/src/game_graphics/game_scene.cpp index 58e6888c6..db2088104 100644 --- a/cockatrice/src/game_graphics/game_scene.cpp +++ b/cockatrice/src/game_graphics/game_scene.cpp @@ -37,7 +37,7 @@ GameScene::GameScene(PhasesToolbar *_phasesToolbar, QObject *parent) { animationTimer = new QBasicTimer; addItem(phasesToolbar); - connect(&SettingsCache::instance().interface(), &InterfaceSettings::minPlayersForMultiColumnLayoutChanged, this, + connect(&SettingsCache::instance().userInterface(), &InterfaceSettings::minPlayersForMultiColumnLayoutChanged, this, &GameScene::rearrange); rearrange(); @@ -336,7 +336,7 @@ QList GameScene::rotatePlayers(const QList &active int GameScene::determineColumnCount(int playerCount) { - return playerCount < SettingsCache::instance().interface().getMinPlayersForMultiColumnLayout() ? 1 : 2; + return playerCount < SettingsCache::instance().userInterface().getMinPlayersForMultiColumnLayout() ? 1 : 2; } /** diff --git a/cockatrice/src/game_graphics/game_view.cpp b/cockatrice/src/game_graphics/game_view.cpp index ed190552e..b768c8317 100644 --- a/cockatrice/src/game_graphics/game_view.cpp +++ b/cockatrice/src/game_graphics/game_view.cpp @@ -47,11 +47,11 @@ GameView::GameView(GameScene *scene, QWidget *parent) : QGraphicsView(scene, par connect(scene, &GameScene::sigResizeRubberBand, this, &GameView::resizeRubberBand); connect(scene, &GameScene::sigStopRubberBand, this, &GameView::stopRubberBand); connect(scene, &QGraphicsScene::selectionChanged, this, [this]() { updateTotalSelectionCount(); }); - connect(&SettingsCache::instance().interface(), &InterfaceSettings::tallyTypeChanged, this, + connect(&SettingsCache::instance().userInterface(), &InterfaceSettings::tallyTypeChanged, this, [this] { updateTotalSelectionCount(); }); - setFocusDisabled(SettingsCache::instance().interface().getKeepGameChatFocus()); - connect(&SettingsCache::instance().interface(), &InterfaceSettings::keepGameChatFocusChanged, this, + setFocusDisabled(SettingsCache::instance().userInterface().getKeepGameChatFocus()); + connect(&SettingsCache::instance().userInterface(), &InterfaceSettings::keepGameChatFocusChanged, this, &GameView::setFocusDisabled); aCloseMostRecentZoneView = new QAction(this); @@ -130,7 +130,7 @@ void GameView::resizeRubberBand(const QPointF &cursorPoint, int selectedCount) QRect rect = QRect(mapFromScene(selectionOrigin), cursor).normalized(); rubberBand->setGeometry(rect); - if (!SettingsCache::instance().interface().getShowDragSelectionCount()) { + if (!SettingsCache::instance().userInterface().getShowDragSelectionCount()) { dragCountLabel->hide(); return; } @@ -239,7 +239,7 @@ void GameView::updateTotalSelectionCount(const QSize &viewSize) int count = scene()->selectedItems().count(); - if (!SettingsCache::instance().interface().getShowTotalSelectionCount() || count <= 1) { + if (!SettingsCache::instance().userInterface().getShowTotalSelectionCount() || count <= 1) { totalCountLabel->hide(); } else { totalCountLabel->setText(QString::number(count)); @@ -251,7 +251,7 @@ void GameView::updateTotalSelectionCount(const QSize &viewSize) totalCountLabel->show(); } - TallyType tallyType = Tally::intToType(SettingsCache::instance().interface().getTallyType()); + TallyType tallyType = Tally::intToType(SettingsCache::instance().userInterface().getTallyType()); GameScene *gameScene = static_cast(scene()); QList entries = Tally::compute(gameScene->selectedCards(), tallyType); diff --git a/cockatrice/src/game_graphics/player/menu/tally_menu.cpp b/cockatrice/src/game_graphics/player/menu/tally_menu.cpp index 2bf02904e..7eb3945b3 100644 --- a/cockatrice/src/game_graphics/player/menu/tally_menu.cpp +++ b/cockatrice/src/game_graphics/player/menu/tally_menu.cpp @@ -23,14 +23,14 @@ TallyMenu::TallyMenu() QAction *TallyMenu::createTallyAction(TallyType tallyType) { - TallyType currentType = Tally::intToType(SettingsCache::instance().interface().getTallyType()); + TallyType currentType = Tally::intToType(SettingsCache::instance().userInterface().getTallyType()); QAction *action = new QAction(this); action->setCheckable(true); action->setChecked(tallyType == currentType); - connect(action, &QAction::triggered, &SettingsCache::instance().interface(), - [tallyType] { SettingsCache::instance().interface().setTallyType(static_cast(tallyType)); }); + connect(action, &QAction::triggered, &SettingsCache::instance().userInterface(), + [tallyType] { SettingsCache::instance().userInterface().setTallyType(static_cast(tallyType)); }); actionGroup->addAction(action); diff --git a/cockatrice/src/game_graphics/player/player_graphics_item.cpp b/cockatrice/src/game_graphics/player/player_graphics_item.cpp index 20f6128f4..d443853ce 100644 --- a/cockatrice/src/game_graphics/player/player_graphics_item.cpp +++ b/cockatrice/src/game_graphics/player/player_graphics_item.cpp @@ -17,9 +17,9 @@ PlayerGraphicsItem::PlayerGraphicsItem(PlayerLogic *_player) : player(_player) { - connect(&SettingsCache::instance().interface(), &InterfaceSettings::horizontalHandChanged, this, + connect(&SettingsCache::instance().userInterface(), &InterfaceSettings::horizontalHandChanged, this, &PlayerGraphicsItem::rearrangeZones); - connect(&SettingsCache::instance().interface(), &InterfaceSettings::handJustificationChanged, this, + connect(&SettingsCache::instance().userInterface(), &InterfaceSettings::handJustificationChanged, this, &PlayerGraphicsItem::rearrangeZones); connect(player, &PlayerLogic::rearrangeCounters, this, &PlayerGraphicsItem::rearrangeCounters); connect(player, &PlayerLogic::activeChanged, this, &PlayerGraphicsItem::onPlayerActiveChanged); @@ -149,7 +149,7 @@ qreal PlayerGraphicsItem::getMinimumWidth() const { qreal result = tableZoneGraphicsItem->getMinimumWidth() + CardDimensions::HEIGHT_F + 15 + counterAreaWidth + stackZoneGraphicsItem->boundingRect().width(); - if (!SettingsCache::instance().interface().getHorizontalHand()) { + if (!SettingsCache::instance().userInterface().getHorizontalHand()) { result += handZoneGraphicsItem->boundingRect().width(); } return result; @@ -166,7 +166,7 @@ void PlayerGraphicsItem::processSceneSizeChange(int newPlayerWidth) // Extend table (and hand, if horizontal) to accommodate the new player width. qreal tableWidth = newPlayerWidth - CardDimensions::HEIGHT_F - 15 - counterAreaWidth - stackZoneGraphicsItem->boundingRect().width(); - if (!SettingsCache::instance().interface().getHorizontalHand()) { + if (!SettingsCache::instance().userInterface().getHorizontalHand()) { tableWidth -= handZoneGraphicsItem->boundingRect().width(); } @@ -234,7 +234,7 @@ void PlayerGraphicsItem::rearrangeCounters() void PlayerGraphicsItem::rearrangeZones() { auto base = QPointF(CardDimensions::HEIGHT_F + counterAreaWidth + 15, 0); - if (SettingsCache::instance().interface().getHorizontalHand()) { + if (SettingsCache::instance().userInterface().getHorizontalHand()) { if (mirrored) { if (player->getHandZone()->contentsKnown()) { handVisible = true; @@ -285,7 +285,7 @@ void PlayerGraphicsItem::updateBoundingRect() { prepareGeometryChange(); qreal width = CardDimensions::HEIGHT_F + 15 + counterAreaWidth + stackZoneGraphicsItem->boundingRect().width(); - if (SettingsCache::instance().interface().getHorizontalHand()) { + if (SettingsCache::instance().userInterface().getHorizontalHand()) { qreal handHeight = handVisible ? handZoneGraphicsItem->boundingRect().height() : 0; bRect = QRectF(0, 0, width + tableZoneGraphicsItem->boundingRect().width(), tableZoneGraphicsItem->boundingRect().height() + handHeight); diff --git a/cockatrice/src/game_graphics/zones/hand_zone.cpp b/cockatrice/src/game_graphics/zones/hand_zone.cpp index 8d0a28fc6..b52a4955a 100644 --- a/cockatrice/src/game_graphics/zones/hand_zone.cpp +++ b/cockatrice/src/game_graphics/zones/hand_zone.cpp @@ -34,7 +34,7 @@ void HandZone::handleDropEvent(const QList &dragItems, QPoint point = dropPoint + scenePos().toPoint(); int x = -1; - if (SettingsCache::instance().interface().getHorizontalHand()) { + if (SettingsCache::instance().userInterface().getHorizontalHand()) { for (x = 0; x < getLogic()->getCards().size(); x++) { if (point.x() < static_cast(getLogic()->getCards().at(x))->scenePos().x()) { break; @@ -61,7 +61,7 @@ void HandZone::handleDropEvent(const QList &dragItems, QRectF HandZone::boundingRect() const { - if (SettingsCache::instance().interface().getHorizontalHand()) { + if (SettingsCache::instance().userInterface().getHorizontalHand()) { return QRectF(0, 0, width, CardDimensions::HEIGHT_F + 10); } else { return QRectF(0, 0, CardDimensions::WIDTH_F * 1.5, zoneHeight); @@ -78,8 +78,8 @@ void HandZone::reorganizeCards() { if (!getLogic()->getCards().isEmpty()) { const int cardCount = getLogic()->getCards().size(); - if (SettingsCache::instance().interface().getHorizontalHand()) { - bool leftJustified = SettingsCache::instance().interface().getLeftJustified(); + if (SettingsCache::instance().userInterface().getHorizontalHand()) { + bool leftJustified = SettingsCache::instance().userInterface().getLeftJustified(); qreal cardWidth = getLogic()->getCards().at(0)->boundingRect().width(); const int xPadding = leftJustified ? cardWidth * 1.4 : 5; qreal totalWidth = @@ -127,7 +127,7 @@ void HandZone::sortHand(const QList &options) void HandZone::setWidth(qreal _width) { - if (SettingsCache::instance().interface().getHorizontalHand()) { + if (SettingsCache::instance().userInterface().getHorizontalHand()) { prepareGeometryChange(); width = _width; reorganizeCards(); diff --git a/cockatrice/src/game_graphics/zones/table_zone.cpp b/cockatrice/src/game_graphics/zones/table_zone.cpp index 21138854e..4ef01853f 100644 --- a/cockatrice/src/game_graphics/zones/table_zone.cpp +++ b/cockatrice/src/game_graphics/zones/table_zone.cpp @@ -29,7 +29,7 @@ TableZone::TableZone(TableZoneLogic *_logic, bool _mirrored, QGraphicsItem *pare connect(_logic, &TableZoneLogic::contentSizeChanged, this, &TableZone::resizeToContents); connect(_logic, &TableZoneLogic::toggleTapped, this, &TableZone::toggleTapped); connect(themeManager, &ThemeManager::themeChanged, this, &TableZone::updateBg); - connect(&SettingsCache::instance().interface(), &InterfaceSettings::invertVerticalCoordinateChanged, this, + connect(&SettingsCache::instance().userInterface(), &InterfaceSettings::invertVerticalCoordinateChanged, this, &TableZone::reorganizeCards); updateBg(); @@ -60,8 +60,8 @@ void TableZone::setMirrored(bool isMirrored) bool TableZone::isInverted() const { - return ((mirrored && !SettingsCache::instance().interface().getInvertVerticalCoordinate()) || - (!mirrored && SettingsCache::instance().interface().getInvertVerticalCoordinate())); + return ((mirrored && !SettingsCache::instance().userInterface().getInvertVerticalCoordinate()) || + (!mirrored && SettingsCache::instance().userInterface().getInvertVerticalCoordinate())); } void TableZone::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*option*/, QWidget * /*widget*/) diff --git a/cockatrice/src/game_graphics/zones/view_zone_widget.cpp b/cockatrice/src/game_graphics/zones/view_zone_widget.cpp index 90f7dbca5..17118e80d 100644 --- a/cockatrice/src/game_graphics/zones/view_zone_widget.cpp +++ b/cockatrice/src/game_graphics/zones/view_zone_widget.cpp @@ -66,7 +66,7 @@ ZoneViewWidget::ZoneViewWidget(PlayerLogic *_player, connect(help, &QAction::triggered, this, [this] { createSearchSyntaxHelpWindow(&searchEdit); }); - if (SettingsCache::instance().interface().getFocusCardViewSearchBar()) { + if (SettingsCache::instance().userInterface().getFocusCardViewSearchBar()) { this->setActive(true); searchEdit.setFocus(); } @@ -77,9 +77,9 @@ ZoneViewWidget::ZoneViewWidget(PlayerLogic *_player, vbox->addItem(searchEditProxy); // hide search bar if chat autofocus setting is enabled, since typing into it will no longer work anyway - searchEditProxy->setVisible(!SettingsCache::instance().interface().getKeepGameChatFocus()); - connect(&SettingsCache::instance().interface(), &InterfaceSettings::keepGameChatFocusChanged, searchEditProxy, - [searchEditProxy](bool keepFocus) { searchEditProxy->setVisible(!keepFocus); }); + searchEditProxy->setVisible(!SettingsCache::instance().userInterface().getKeepGameChatFocus()); + connect(&SettingsCache::instance().userInterface(), &InterfaceSettings::keepGameChatFocusChanged, + searchEditProxy, [searchEditProxy](bool keepFocus) { searchEditProxy->setVisible(!keepFocus); }); // top row QGraphicsLinearLayout *hTopRow = new QGraphicsLinearLayout(Qt::Horizontal); @@ -159,9 +159,9 @@ ZoneViewWidget::ZoneViewWidget(PlayerLogic *_player, connect(&sortBySelector, static_cast(&QComboBox::currentIndexChanged), this, &ZoneViewWidget::processSortBy); connect(&pileViewCheckBox, &QCheckBox::QT_STATE_CHANGED, this, &ZoneViewWidget::processSetPileView); - groupBySelector.setCurrentIndex(SettingsCache::instance().interface().getZoneViewGroupByIndex()); - sortBySelector.setCurrentIndex(SettingsCache::instance().interface().getZoneViewSortByIndex()); - pileViewCheckBox.setChecked(SettingsCache::instance().interface().getZoneViewPileView()); + groupBySelector.setCurrentIndex(SettingsCache::instance().userInterface().getZoneViewGroupByIndex()); + sortBySelector.setCurrentIndex(SettingsCache::instance().userInterface().getZoneViewSortByIndex()); + pileViewCheckBox.setChecked(SettingsCache::instance().userInterface().getZoneViewPileView()); if (CardList::NoSort == static_cast(groupBySelector.currentData().toInt())) { pileViewCheckBox.setEnabled(false); @@ -191,7 +191,7 @@ ZoneViewWidget::ZoneViewWidget(PlayerLogic *_player, void ZoneViewWidget::processGroupBy(int index) { auto option = static_cast(groupBySelector.itemData(index).toInt()); - SettingsCache::instance().interface().setZoneViewGroupByIndex(index); + SettingsCache::instance().userInterface().setZoneViewGroupByIndex(index); zone->setGroupBy(option); // disable pile view checkbox if we're not grouping by anything @@ -215,13 +215,13 @@ void ZoneViewWidget::processSortBy(int index) return; } - SettingsCache::instance().interface().setZoneViewSortByIndex(index); + SettingsCache::instance().userInterface().setZoneViewSortByIndex(index); zone->setSortBy(option); } void ZoneViewWidget::processSetPileView(QT_STATE_CHANGED_T value) { - SettingsCache::instance().interface().setZoneViewPileView(value); + SettingsCache::instance().userInterface().setZoneViewPileView(value); zone->setPileView(value); } @@ -478,7 +478,7 @@ static qreal rowsToHeight(int rows) **/ static qreal calcMaxInitialHeight() { - return rowsToHeight(SettingsCache::instance().interface().getCardViewInitialRowsMax()); + return rowsToHeight(SettingsCache::instance().userInterface().getCardViewInitialRowsMax()); } /** @@ -560,7 +560,7 @@ void ZoneViewWidget::initStyleOption(QStyleOption *option) const void ZoneViewWidget::expandWindow() { qreal maxInitialHeight = calcMaxInitialHeight(); - qreal maxExpandedHeight = rowsToHeight(SettingsCache::instance().interface().getCardViewExpandedRowsMax()); + qreal maxExpandedHeight = rowsToHeight(SettingsCache::instance().userInterface().getCardViewExpandedRowsMax()); qreal height = rect().height() - extraHeight - 10; qreal maxHeight = maximumHeight() - extraHeight - 10; diff --git a/cockatrice/src/interface/card_picture_loader/card_picture_loader.cpp b/cockatrice/src/interface/card_picture_loader/card_picture_loader.cpp index bf5c84276..f9391f7ce 100644 --- a/cockatrice/src/interface/card_picture_loader/card_picture_loader.cpp +++ b/cockatrice/src/interface/card_picture_loader/card_picture_loader.cpp @@ -19,8 +19,8 @@ #include #include #include +#include #include -#include #include // never cache more than 300 cards at once for a single deck @@ -31,7 +31,7 @@ CardPictureLoader::CardPictureLoader() : QObject(nullptr) worker = new CardPictureLoaderWorker; connect(&SettingsCache::instance().paths(), &PathsSettings::picsPathChanged, this, &CardPictureLoader::picsPathChanged); - connect(&SettingsCache::instance().personal(), &PersonalSettings::picDownloadChanged, this, + connect(&SettingsCache::instance().downloads(), &DownloadSettings::picDownloadChanged, this, &CardPictureLoader::picDownloadChanged); qRegisterMetaType(); diff --git a/cockatrice/src/interface/card_picture_loader/card_picture_loader_worker.cpp b/cockatrice/src/interface/card_picture_loader/card_picture_loader_worker.cpp index 3724c184d..8b121d91c 100644 --- a/cockatrice/src/interface/card_picture_loader/card_picture_loader_worker.cpp +++ b/cockatrice/src/interface/card_picture_loader/card_picture_loader_worker.cpp @@ -11,15 +11,15 @@ #include #include #include +#include #include -#include #include #include static constexpr int MAX_REQUESTS_PER_SEC = 10; CardPictureLoaderWorker::CardPictureLoaderWorker() - : QObject(nullptr), picDownload(SettingsCache::instance().personal().getPicDownload()), + : QObject(nullptr), picDownload(SettingsCache::instance().downloads().getPicDownload()), requestQuota(MAX_REQUESTS_PER_SEC) { networkManager = new QNetworkAccessManager(this); diff --git a/cockatrice/src/interface/card_picture_loader/card_picture_loader_worker_work.cpp b/cockatrice/src/interface/card_picture_loader/card_picture_loader_worker_work.cpp index ebaf11fff..bfd46a462 100644 --- a/cockatrice/src/interface/card_picture_loader/card_picture_loader_worker_work.cpp +++ b/cockatrice/src/interface/card_picture_loader/card_picture_loader_worker_work.cpp @@ -10,7 +10,7 @@ #include #include #include -#include +#include // Card back returned by gatherer when card is not found static const QStringList MD5_BLACKLIST = { @@ -20,7 +20,7 @@ static const QStringList MD5_BLACKLIST = { CardPictureLoaderWorkerWork::CardPictureLoaderWorkerWork(const CardPictureLoaderWorker *worker, const ExactCard &toLoad) : QObject(nullptr), cardToDownload(CardPictureToLoad(toLoad)), - picDownload(SettingsCache::instance().personal().getPicDownload()) + picDownload(SettingsCache::instance().downloads().getPicDownload()) { // Hook up signals to the orchestrator connect(this, &CardPictureLoaderWorkerWork::requestImageDownload, worker, &CardPictureLoaderWorker::queueRequest); @@ -32,7 +32,7 @@ CardPictureLoaderWorkerWork::CardPictureLoaderWorkerWork(const CardPictureLoader &CardPictureLoaderWorker::imageRequestSucceeded); // Hook up signals to settings - connect(&SettingsCache::instance().personal(), SIGNAL(picDownloadChanged()), this, SLOT(picDownloadChanged())); + connect(&SettingsCache::instance().downloads(), SIGNAL(picDownloadChanged()), this, SLOT(picDownloadChanged())); startNextPicDownload(); } @@ -211,5 +211,5 @@ void CardPictureLoaderWorkerWork::concludeImageLoad(const QImage &image) void CardPictureLoaderWorkerWork::picDownloadChanged() { - picDownload = SettingsCache::instance().personal().getPicDownload(); + picDownload = SettingsCache::instance().downloads().getPicDownload(); } diff --git a/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.cpp b/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.cpp index 9c52f535f..e33c09426 100644 --- a/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.cpp +++ b/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.cpp @@ -12,7 +12,7 @@ #include #include #include -#include +#include #include #include @@ -111,20 +111,18 @@ void DeckEditorDeckDockWidget::createDeckDock() showBannerCardCheckBox = new QCheckBox(); showBannerCardCheckBox->setObjectName("showBannerCardCheckBox"); - showBannerCardCheckBox->setChecked( - SettingsCache::instance().cardsDisplay().getDeckEditorBannerCardComboBoxVisible()); - connect(showBannerCardCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().cardsDisplay(), - &CardsDisplaySettings::setDeckEditorBannerCardComboBoxVisible); - connect(&SettingsCache::instance().cardsDisplay(), - &CardsDisplaySettings::deckEditorBannerCardComboBoxVisibleChanged, this, + showBannerCardCheckBox->setChecked(SettingsCache::instance().deckEditor().getBannerCardComboBoxVisible()); + connect(showBannerCardCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().deckEditor(), + &DeckEditorSettings::setBannerCardComboBoxVisible); + connect(&SettingsCache::instance().deckEditor(), &DeckEditorSettings::bannerCardComboBoxVisibleChanged, this, &DeckEditorDeckDockWidget::updateShowBannerCardComboBox); showTagsWidgetCheckBox = new QCheckBox(); showTagsWidgetCheckBox->setObjectName("showTagsWidgetCheckBox"); - showTagsWidgetCheckBox->setChecked(SettingsCache::instance().cardsDisplay().getDeckEditorTagsWidgetVisible()); - connect(showTagsWidgetCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().cardsDisplay(), - &CardsDisplaySettings::setDeckEditorTagsWidgetVisible); - connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::deckEditorTagsWidgetVisibleChanged, this, + showTagsWidgetCheckBox->setChecked(SettingsCache::instance().deckEditor().getTagsWidgetVisible()); + connect(showTagsWidgetCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().deckEditor(), + &DeckEditorSettings::setTagsWidgetVisible); + connect(&SettingsCache::instance().deckEditor(), &DeckEditorSettings::tagsWidgetVisibleChanged, this, &DeckEditorDeckDockWidget::updateShowTagsWidget); quickSettingsWidget->addSettingsWidget(showBannerCardCheckBox); @@ -156,7 +154,7 @@ void DeckEditorDeckDockWidget::createDeckDock() bannerCardLabel = new QLabel(); bannerCardLabel->setObjectName("bannerCardLabel"); bannerCardLabel->setText(tr("Banner Card")); - bannerCardLabel->setHidden(!SettingsCache::instance().cardsDisplay().getDeckEditorBannerCardComboBoxVisible()); + bannerCardLabel->setHidden(!SettingsCache::instance().deckEditor().getBannerCardComboBoxVisible()); bannerCardComboBox = new QComboBox(this); connect(getModel(), &DeckListModel::cardNodesChanged, this, [this]() { // Delay the update to avoid race conditions @@ -167,10 +165,10 @@ void DeckEditorDeckDockWidget::createDeckDock() connect(bannerCardComboBox, QOverload::of(&QComboBox::currentIndexChanged), this, &DeckEditorDeckDockWidget::writeBannerCard); - bannerCardComboBox->setHidden(!SettingsCache::instance().cardsDisplay().getDeckEditorBannerCardComboBoxVisible()); + bannerCardComboBox->setHidden(!SettingsCache::instance().deckEditor().getBannerCardComboBoxVisible()); deckTagsDisplayWidget = new DeckPreviewDeckTagsDisplayWidget(this, {}); - deckTagsDisplayWidget->setHidden(!SettingsCache::instance().cardsDisplay().getDeckEditorTagsWidgetVisible()); + deckTagsDisplayWidget->setHidden(!SettingsCache::instance().deckEditor().getTagsWidgetVisible()); connect(deckTagsDisplayWidget, &DeckPreviewDeckTagsDisplayWidget::tagsChanged, deckStateManager, &DeckStateManager::setTags); diff --git a/cockatrice/src/interface/widgets/general/home_widget.cpp b/cockatrice/src/interface/widgets/general/home_widget.cpp index e873f5f3e..8589e3517 100644 --- a/cockatrice/src/interface/widgets/general/home_widget.cpp +++ b/cockatrice/src/interface/widgets/general/home_widget.cpp @@ -14,8 +14,8 @@ #include #include #include +#include #include -#include HomeWidget::HomeWidget(QWidget *parent, TabSupervisor *_tabSupervisor) : QWidget(parent), tabSupervisor(_tabSupervisor), background("theme:backgrounds/home"), overlay("theme:cockatrice") @@ -43,12 +43,12 @@ HomeWidget::HomeWidget(QWidget *parent, TabSupervisor *_tabSupervisor) updateConnectButton(tabSupervisor->getClient()->getStatus()); connect(tabSupervisor->getClient(), &RemoteClient::statusChanged, this, &HomeWidget::updateConnectButton); - connect(&SettingsCache::instance().personal(), &PersonalSettings::homeTabBackgroundSourceChanged, this, + connect(&SettingsCache::instance().appearance(), &AppearanceSettings::homeTabBackgroundSourceChanged, this, &HomeWidget::initializeBackgroundFromSource); - connect(&SettingsCache::instance().personal(), &PersonalSettings::homeTabBackgroundShuffleFrequencyChanged, this, - &HomeWidget::onBackgroundShuffleFrequencyChanged); + connect(&SettingsCache::instance().appearance(), &AppearanceSettings::homeTabBackgroundShuffleFrequencyChanged, + this, &HomeWidget::onBackgroundShuffleFrequencyChanged); // Lambda is cleaner to read than overloading this - connect(&SettingsCache::instance().personal(), &PersonalSettings::homeTabDisplayCardNameChanged, this, + connect(&SettingsCache::instance().appearance(), &AppearanceSettings::homeTabDisplayCardNameChanged, this, [this] { repaint(); }); connect(&SettingsCache::instance(), &SettingsCache::themeChanged, this, &HomeWidget::initializeBackgroundFromSource); @@ -65,7 +65,7 @@ void HomeWidget::initializeBackgroundFromSource() } auto backgroundSourceType = - BackgroundSources::fromId(SettingsCache::instance().personal().getHomeTabBackgroundSource()); + BackgroundSources::fromId(SettingsCache::instance().appearance().getHomeTabBackgroundSource()); switch (backgroundSourceType) { case BackgroundSources::Theme: @@ -113,7 +113,7 @@ void HomeWidget::setRandomCard(ExactCard &newCard) void HomeWidget::updateRandomCard() { auto backgroundSourceType = - BackgroundSources::fromId(SettingsCache::instance().personal().getHomeTabBackgroundSource()); + BackgroundSources::fromId(SettingsCache::instance().appearance().getHomeTabBackgroundSource()); ExactCard newCard; @@ -156,8 +156,8 @@ void HomeWidget::updateRandomCard() void HomeWidget::onBackgroundShuffleFrequencyChanged() { cardChangeTimer->stop(); - if (SettingsCache::instance().personal().getHomeTabBackgroundShuffleFrequency() > 0) { - cardChangeTimer->start(SettingsCache::instance().personal().getHomeTabBackgroundShuffleFrequency() * 1000); + if (SettingsCache::instance().appearance().getHomeTabBackgroundShuffleFrequency() > 0) { + cardChangeTimer->start(SettingsCache::instance().appearance().getHomeTabBackgroundShuffleFrequency() * 1000); } } @@ -265,7 +265,7 @@ void HomeWidget::updateConnectButton(const ClientStatus status) QPair HomeWidget::extractDominantColors(const QPixmap &pixmap) { - if (themeManager->isBuiltInTheme() && SettingsCache::instance().personal().getHomeTabBackgroundSource() == + if (themeManager->isBuiltInTheme() && SettingsCache::instance().appearance().getHomeTabBackgroundSource() == BackgroundSources::toId(BackgroundSources::Theme)) { return QPair(QColor::fromRgb(20, 140, 60), QColor::fromRgb(120, 200, 80)); } @@ -352,7 +352,7 @@ void HomeWidget::paintEvent(QPaintEvent *event) } } - if (!cardName.isEmpty() && SettingsCache::instance().personal().getHomeTabDisplayCardName()) { + if (!cardName.isEmpty() && SettingsCache::instance().appearance().getHomeTabDisplayCardName()) { QFont font = painter.font(); font.setPointSize(14); font.setBold(true); diff --git a/cockatrice/src/interface/widgets/menus/tearoff_menu.h b/cockatrice/src/interface/widgets/menus/tearoff_menu.h index 26dcd1f6c..9a9c4ff01 100644 --- a/cockatrice/src/interface/widgets/menus/tearoff_menu.h +++ b/cockatrice/src/interface/widgets/menus/tearoff_menu.h @@ -16,16 +16,16 @@ class TearOffMenu : public QMenu public: explicit TearOffMenu(const QString &title, QWidget *parent = nullptr) : QMenu(title, parent) { - connect(&SettingsCache::instance().interface(), &InterfaceSettings::useTearOffMenusChanged, this, + connect(&SettingsCache::instance().userInterface(), &InterfaceSettings::useTearOffMenusChanged, this, [this](const bool state) { setTearOffEnabled(state); }); - setTearOffEnabled(SettingsCache::instance().interface().getUseTearOffMenus()); + setTearOffEnabled(SettingsCache::instance().userInterface().getUseTearOffMenus()); } explicit TearOffMenu(QWidget *parent = nullptr) : QMenu(parent) { - connect(&SettingsCache::instance().interface(), &InterfaceSettings::useTearOffMenusChanged, this, + connect(&SettingsCache::instance().userInterface(), &InterfaceSettings::useTearOffMenusChanged, this, [this](const bool state) { setTearOffEnabled(state); }); - setTearOffEnabled(SettingsCache::instance().interface().getUseTearOffMenus()); + setTearOffEnabled(SettingsCache::instance().userInterface().getUseTearOffMenus()); } TearOffMenu *addTearOffMenu(const QString &title) diff --git a/cockatrice/src/interface/widgets/replay/replay_manager.cpp b/cockatrice/src/interface/widgets/replay/replay_manager.cpp index 1037d36a8..c6e7ff1bb 100644 --- a/cockatrice/src/interface/widgets/replay/replay_manager.cpp +++ b/cockatrice/src/interface/widgets/replay/replay_manager.cpp @@ -94,7 +94,7 @@ void ReplayManager::handleBackwardsSkip(bool doRewindBuffering) // The rewind only happens once the timer runs out. // If another backwards skip happens, the timer will just get reset instead of rewinding. rewindBufferingTimer->stop(); - rewindBufferingTimer->start(SettingsCache::instance().interface().getRewindBufferingMs()); + rewindBufferingTimer->start(SettingsCache::instance().userInterface().getRewindBufferingMs()); } else { // otherwise, process the rewind immediately processRewind(); diff --git a/cockatrice/src/interface/widgets/replay/replay_quick_settings_widget.cpp b/cockatrice/src/interface/widgets/replay/replay_quick_settings_widget.cpp index 446427e26..08113d2cd 100644 --- a/cockatrice/src/interface/widgets/replay/replay_quick_settings_widget.cpp +++ b/cockatrice/src/interface/widgets/replay/replay_quick_settings_widget.cpp @@ -14,7 +14,7 @@ ReplayQuickSettingsWidget::ReplayQuickSettingsWidget(QWidget *parent) : Settings fastForwardSpeedBox.setMinimum(1); fastForwardSpeedBox.setMaximum(99.9); fastForwardSpeedBox.setDecimals(1); - fastForwardSpeedBox.setValue(SettingsCache::instance().interface().getFastForwardSpeed()); + fastForwardSpeedBox.setValue(SettingsCache::instance().userInterface().getFastForwardSpeed()); connect(&fastForwardSpeedBox, qOverload(&QDoubleSpinBox::valueChanged), this, &ReplayQuickSettingsWidget::actUpdateFastForwardSpeed); @@ -40,6 +40,6 @@ void ReplayQuickSettingsWidget::retranslateUi() void ReplayQuickSettingsWidget::actUpdateFastForwardSpeed(qreal value) { - SettingsCache::instance().interface().setFastForwardSpeed(value); + SettingsCache::instance().userInterface().setFastForwardSpeed(value); emit fastForwardSpeedChanged(value); } diff --git a/cockatrice/src/interface/widgets/replay/replay_widget.cpp b/cockatrice/src/interface/widgets/replay/replay_widget.cpp index f5768a7aa..fc0110ff1 100644 --- a/cockatrice/src/interface/widgets/replay/replay_widget.cpp +++ b/cockatrice/src/interface/widgets/replay/replay_widget.cpp @@ -98,7 +98,7 @@ void ReplayWidget::replayPlayButtonToggled(bool checked) void ReplayWidget::updateTimeScaleFactor(bool isFastForward) { - qreal factor = isFastForward ? SettingsCache::instance().interface().getFastForwardSpeed() : 1.0; + qreal factor = isFastForward ? SettingsCache::instance().userInterface().getFastForwardSpeed() : 1.0; replayManager->setTimeScaleFactor(factor); } diff --git a/cockatrice/src/interface/widgets/server/game_selector.cpp b/cockatrice/src/interface/widgets/server/game_selector.cpp index 11b36ca92..6580f0262 100644 --- a/cockatrice/src/interface/widgets/server/game_selector.cpp +++ b/cockatrice/src/interface/widgets/server/game_selector.cpp @@ -26,6 +26,7 @@ #include #include #include +#include GameSelector::GameSelector(AbstractClient *_client, TabSupervisor *_tabSupervisor, @@ -83,12 +84,12 @@ GameSelector::GameSelector(AbstractClient *_client, if (showFilters && restoresettings) { quickFilterToolBar = new GameSelectorQuickFilterToolBar(this, tabSupervisor, gameListProxyModel, gameTypeMap); quickFilterToolBar->setVisible(showFilters && restoresettings && - SettingsCache::instance().cardsDisplay().getShowGameSelectorFilterToolbar()); + SettingsCache::instance().userInterface().getShowGameSelectorFilterToolbar()); - connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::showGameSelectorFilterToolbarChanged, + connect(&SettingsCache::instance().userInterface(), &InterfaceSettings::showGameSelectorFilterToolbarChanged, this, [this] { quickFilterToolBar->setVisible( - SettingsCache::instance().cardsDisplay().getShowGameSelectorFilterToolbar()); + SettingsCache::instance().userInterface().getShowGameSelectorFilterToolbar()); }); } else { quickFilterToolBar = nullptr; diff --git a/cockatrice/src/interface/widgets/server/user/user_list_widget.cpp b/cockatrice/src/interface/widgets/server/user/user_list_widget.cpp index 9c29c62bc..64cbb7b7d 100644 --- a/cockatrice/src/interface/widgets/server/user/user_list_widget.cpp +++ b/cockatrice/src/interface/widgets/server/user/user_list_widget.cpp @@ -30,7 +30,7 @@ #include #include #include -#include +#include #include BanDialog::BanDialog(const ServerInfo_User &info, QWidget *parent) : QDialog(parent) @@ -349,7 +349,7 @@ bool UserListItemDelegate::editorEvent(QEvent *event, QSize UserListItemDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const { - if (!SettingsCache::instance().interface().getStyleUserList()) { + if (!SettingsCache::instance().appearance().getStyleUserList()) { return QStyledItemDelegate::sizeHint(option, index); } return UserListPainter::sizeHint(); @@ -357,7 +357,7 @@ QSize UserListItemDelegate::sizeHint(const QStyleOptionViewItem &option, const Q void UserListItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const { - if (!SettingsCache::instance().interface().getStyleUserList()) { + if (!SettingsCache::instance().appearance().getStyleUserList()) { QStyledItemDelegate::paint(painter, option, index); return; } @@ -521,7 +521,7 @@ UserListWidget::UserListWidget(TabSupervisor *_tabSupervisor, // Pin on item click connect(userTree, &QTreeWidget::itemClicked, this, [this](QTreeWidgetItem *item, int) { - if (!SettingsCache::instance().interface().getStyleUserList()) { + if (!SettingsCache::instance().appearance().getStyleUserList()) { return; } const QString name = static_cast(item)->getUserInfo().name().c_str(); @@ -553,7 +553,7 @@ UserListWidget::UserListWidget(TabSupervisor *_tabSupervisor, connect(cardArtProvider, &UserCardArtProvider::cardArtUpdated, this, [this](const QString &) { userTree->viewport()->update(); }); - connect(&SettingsCache::instance().interface(), &InterfaceSettings::styleUserListChanged, this, + connect(&SettingsCache::instance().appearance(), &AppearanceSettings::styleUserListChanged, this, &UserListWidget::applyDisplayMode); applyDisplayMode(); @@ -659,7 +659,7 @@ void UserListWidget::hideEvent(QHideEvent *e) void UserListWidget::applyDisplayMode() { - const bool styled = SettingsCache::instance().interface().getStyleUserList(); + const bool styled = SettingsCache::instance().appearance().getStyleUserList(); if (styled) { userTree->header()->setSectionResizeMode(0, QHeaderView::Stretch); @@ -718,7 +718,7 @@ bool UserListWidget::eventFilter(QObject *obj, QEvent *event) { if (obj == userTree->viewport()) { if (event->type() == QEvent::MouseMove) { - if (!SettingsCache::instance().interface().getStyleUserList()) { + if (!SettingsCache::instance().appearance().getStyleUserList()) { return QGroupBox::eventFilter(obj, event); } auto *me = static_cast(event); diff --git a/cockatrice/src/interface/widgets/settings_page/appearance_settings_page.cpp b/cockatrice/src/interface/widgets/settings_page/appearance_settings_page.cpp index 0441c0d03..c470e54fe 100644 --- a/cockatrice/src/interface/widgets/settings_page/appearance_settings_page.cpp +++ b/cockatrice/src/interface/widgets/settings_page/appearance_settings_page.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #include #include @@ -104,7 +105,7 @@ AppearanceSettingsPage::AppearanceSettingsPage() homeTabBackgroundSourceBox.addItem(QObject::tr(entry.trKey), QVariant::fromValue(entry.type)); } - QString homeTabBackgroundSource = SettingsCache::instance().personal().getHomeTabBackgroundSource(); + QString homeTabBackgroundSource = settings.appearance().getHomeTabBackgroundSource(); int homeTabBackgroundSourceId = homeTabBackgroundSourceBox.findData(BackgroundSources::fromId(homeTabBackgroundSource)); if (homeTabBackgroundSourceId != -1) { @@ -113,20 +114,19 @@ AppearanceSettingsPage::AppearanceSettingsPage() connect(&homeTabBackgroundSourceBox, QOverload::of(&QComboBox::currentIndexChanged), this, [this]() { auto type = homeTabBackgroundSourceBox.currentData().value(); - SettingsCache::instance().personal().setHomeTabBackgroundSource(BackgroundSources::toId(type)); + SettingsCache::instance().appearance().setHomeTabBackgroundSource(BackgroundSources::toId(type)); updateHomeTabSettingsVisibility(); }); homeTabBackgroundShuffleFrequencySpinBox.setRange(0, 3600); homeTabBackgroundShuffleFrequencySpinBox.setSuffix(tr(" seconds")); - homeTabBackgroundShuffleFrequencySpinBox.setValue( - SettingsCache::instance().personal().getHomeTabBackgroundShuffleFrequency()); - connect(&homeTabBackgroundShuffleFrequencySpinBox, qOverload(&QSpinBox::valueChanged), &settings.personal(), - &PersonalSettings::setHomeTabBackgroundShuffleFrequency); + homeTabBackgroundShuffleFrequencySpinBox.setValue(settings.appearance().getHomeTabBackgroundShuffleFrequency()); + connect(&homeTabBackgroundShuffleFrequencySpinBox, qOverload(&QSpinBox::valueChanged), &settings.appearance(), + &AppearanceSettings::setHomeTabBackgroundShuffleFrequency); - homeTabDisplayCardNameCheckBox.setChecked(settings.personal().getHomeTabDisplayCardName()); - connect(&homeTabDisplayCardNameCheckBox, &QCheckBox::QT_STATE_CHANGED, &settings.personal(), - &PersonalSettings::setHomeTabDisplayCardName); + homeTabDisplayCardNameCheckBox.setChecked(settings.appearance().getHomeTabDisplayCardName()); + connect(&homeTabDisplayCardNameCheckBox, &QCheckBox::QT_STATE_CHANGED, &settings.appearance(), + &AppearanceSettings::setHomeTabDisplayCardName); updateHomeTabSettingsVisibility(); @@ -140,9 +140,9 @@ AppearanceSettingsPage::AppearanceSettingsPage() homeTabGroupBox = new QGroupBox; homeTabGroupBox->setLayout(homeTabGrid); - styleUserListCheckBox.setChecked(settings.interface().getStyleUserList()); - connect(&styleUserListCheckBox, &QCheckBox::QT_STATE_CHANGED, &settings.interface(), - &InterfaceSettings::setStyleUserList); + styleUserListCheckBox.setChecked(settings.appearance().getStyleUserList()); + connect(&styleUserListCheckBox, &QCheckBox::QT_STATE_CHANGED, &settings.appearance(), + &AppearanceSettings::setStyleUserList); auto stylingTabGrid = new QGridLayout; stylingTabGrid->addWidget(&styleUserListCheckBox, 0, 0, 1, 2); @@ -151,12 +151,12 @@ AppearanceSettingsPage::AppearanceSettingsPage() stylingGroupBox->setLayout(stylingTabGrid); // Menu settings - showShortcutsCheckBox.setChecked(settings.cardsDisplay().getShowShortcuts()); + showShortcutsCheckBox.setChecked(settings.userInterface().getShowShortcuts()); connect(&showShortcutsCheckBox, &QCheckBox::QT_STATE_CHANGED, this, &AppearanceSettingsPage::showShortcutsChanged); - showGameSelectorFilterToolbarCheckBox.setChecked(settings.cardsDisplay().getShowGameSelectorFilterToolbar()); - connect(&showGameSelectorFilterToolbarCheckBox, &QCheckBox::QT_STATE_CHANGED, &settings.cardsDisplay(), - &CardsDisplaySettings::setShowGameSelectorFilterToolbar); + showGameSelectorFilterToolbarCheckBox.setChecked(settings.userInterface().getShowGameSelectorFilterToolbar()); + connect(&showGameSelectorFilterToolbarCheckBox, &QCheckBox::QT_STATE_CHANGED, &settings.userInterface(), + &InterfaceSettings::setShowGameSelectorFilterToolbar); auto *menuGrid = new QGridLayout; menuGrid->addWidget(&showShortcutsCheckBox, 0, 0); @@ -199,9 +199,9 @@ AppearanceSettingsPage::AppearanceSettingsPage() connect(&roundCardCornersCheckBox, &QAbstractButton::toggled, &settings.cardsDisplay(), &CardsDisplaySettings::setRoundCardCorners); - connect(&maxFontSizeForCardsEdit, qOverload(&QSpinBox::valueChanged), &settings.personal(), - &PersonalSettings::setMaxFontSize); - maxFontSizeForCardsEdit.setValue(settings.personal().getMaxFontSize()); + connect(&maxFontSizeForCardsEdit, qOverload(&QSpinBox::valueChanged), &settings.appearance(), + &AppearanceSettings::setMaxFontSize); + maxFontSizeForCardsEdit.setValue(settings.appearance().getMaxFontSize()); maxFontSizeForCardsLabel.setBuddy(&maxFontSizeForCardsEdit); maxFontSizeForCardsEdit.setMinimum(9); maxFontSizeForCardsEdit.setMaximum(100); @@ -224,12 +224,12 @@ AppearanceSettingsPage::AppearanceSettingsPage() &CardsDisplaySettings::setStackCardOverlapPercent); cardViewInitialRowsMaxBox.setRange(1, 999); - cardViewInitialRowsMaxBox.setValue(SettingsCache::instance().interface().getCardViewInitialRowsMax()); + cardViewInitialRowsMaxBox.setValue(SettingsCache::instance().userInterface().getCardViewInitialRowsMax()); connect(&cardViewInitialRowsMaxBox, qOverload(&QSpinBox::valueChanged), this, &AppearanceSettingsPage::cardViewInitialRowsMaxChanged); cardViewExpandedRowsMaxBox.setRange(1, 999); - cardViewExpandedRowsMaxBox.setValue(SettingsCache::instance().interface().getCardViewExpandedRowsMax()); + cardViewExpandedRowsMaxBox.setValue(SettingsCache::instance().userInterface().getCardViewExpandedRowsMax()); connect(&cardViewExpandedRowsMaxBox, qOverload(&QSpinBox::valueChanged), this, &AppearanceSettingsPage::cardViewExpandedRowsMaxChanged); @@ -291,12 +291,12 @@ AppearanceSettingsPage::AppearanceSettingsPage() cardCountersGroupBox->setLayout(cardCountersLayout); // Hand layout - horizontalHandCheckBox.setChecked(settings.interface().getHorizontalHand()); - connect(&horizontalHandCheckBox, &QCheckBox::QT_STATE_CHANGED, &settings.interface(), + horizontalHandCheckBox.setChecked(settings.userInterface().getHorizontalHand()); + connect(&horizontalHandCheckBox, &QCheckBox::QT_STATE_CHANGED, &settings.userInterface(), &InterfaceSettings::setHorizontalHand); - leftJustifiedHandCheckBox.setChecked(settings.interface().getLeftJustified()); - connect(&leftJustifiedHandCheckBox, &QCheckBox::QT_STATE_CHANGED, &settings.interface(), + leftJustifiedHandCheckBox.setChecked(settings.userInterface().getLeftJustified()); + connect(&leftJustifiedHandCheckBox, &QCheckBox::QT_STATE_CHANGED, &settings.userInterface(), &InterfaceSettings::setLeftJustified); auto *handGrid = new QGridLayout; @@ -307,13 +307,13 @@ AppearanceSettingsPage::AppearanceSettingsPage() handGroupBox->setLayout(handGrid); // table grid layout - invertVerticalCoordinateCheckBox.setChecked(settings.interface().getInvertVerticalCoordinate()); - connect(&invertVerticalCoordinateCheckBox, &QCheckBox::QT_STATE_CHANGED, &settings.interface(), + invertVerticalCoordinateCheckBox.setChecked(settings.userInterface().getInvertVerticalCoordinate()); + connect(&invertVerticalCoordinateCheckBox, &QCheckBox::QT_STATE_CHANGED, &settings.userInterface(), &InterfaceSettings::setInvertVerticalCoordinate); minPlayersForMultiColumnLayoutEdit.setMinimum(2); - minPlayersForMultiColumnLayoutEdit.setValue(settings.interface().getMinPlayersForMultiColumnLayout()); - connect(&minPlayersForMultiColumnLayoutEdit, qOverload(&QSpinBox::valueChanged), &settings.interface(), + minPlayersForMultiColumnLayoutEdit.setValue(settings.userInterface().getMinPlayersForMultiColumnLayout()); + connect(&minPlayersForMultiColumnLayoutEdit, qOverload(&QSpinBox::valueChanged), &settings.userInterface(), &InterfaceSettings::setMinPlayersForMultiColumnLayout); minPlayersForMultiColumnLayoutLabel.setBuddy(&minPlayersForMultiColumnLayoutEdit); @@ -375,7 +375,7 @@ void AppearanceSettingsPage::editPalette() void AppearanceSettingsPage::updateHomeTabSettingsVisibility() { - bool visible = SettingsCache::instance().personal().getHomeTabBackgroundSource() != + bool visible = SettingsCache::instance().appearance().getHomeTabBackgroundSource() != BackgroundSources::toId(BackgroundSources::Theme); homeTabBackgroundShuffleFrequencyLabel.setVisible(visible); @@ -385,7 +385,7 @@ void AppearanceSettingsPage::updateHomeTabSettingsVisibility() void AppearanceSettingsPage::showShortcutsChanged(QT_STATE_CHANGED_T value) { - SettingsCache::instance().cardsDisplay().setShowShortcuts(value); + SettingsCache::instance().userInterface().setShowShortcuts(value); qApp->setAttribute(Qt::AA_DontShowShortcutsInContextMenus, value == 0); // 0 = unchecked } @@ -412,7 +412,7 @@ void AppearanceSettingsPage::overrideAllCardArtWithPersonalPreferenceToggled(QT_ */ void AppearanceSettingsPage::cardViewInitialRowsMaxChanged(int value) { - SettingsCache::instance().interface().setCardViewInitialRowsMax(value); + SettingsCache::instance().userInterface().setCardViewInitialRowsMax(value); if (cardViewExpandedRowsMaxBox.value() < value) { cardViewExpandedRowsMaxBox.setValue(value); } @@ -425,7 +425,7 @@ void AppearanceSettingsPage::cardViewInitialRowsMaxChanged(int value) */ void AppearanceSettingsPage::cardViewExpandedRowsMaxChanged(int value) { - SettingsCache::instance().interface().setCardViewExpandedRowsMax(value); + SettingsCache::instance().userInterface().setCardViewExpandedRowsMax(value); if (cardViewInitialRowsMaxBox.value() > value) { cardViewInitialRowsMaxBox.setValue(value); } diff --git a/cockatrice/src/interface/widgets/settings_page/appearance_settings_page.h b/cockatrice/src/interface/widgets/settings_page/appearance_settings_page.h index 09b73ad04..0b6b6832c 100644 --- a/cockatrice/src/interface/widgets/settings_page/appearance_settings_page.h +++ b/cockatrice/src/interface/widgets/settings_page/appearance_settings_page.h @@ -40,10 +40,10 @@ private: QSpinBox homeTabBackgroundShuffleFrequencySpinBox; QCheckBox homeTabDisplayCardNameCheckBox; QCheckBox styleUserListCheckBox; - QLabel minPlayersForMultiColumnLayoutLabel; - QLabel maxFontSizeForCardsLabel; QCheckBox showShortcutsCheckBox; QCheckBox showGameSelectorFilterToolbarCheckBox; + QLabel minPlayersForMultiColumnLayoutLabel; + QLabel maxFontSizeForCardsLabel; QCheckBox overrideAllCardArtWithPersonalPreferenceCheckBox; QCheckBox bumpSetsWithCardsInDeckToTopCheckBox; QCheckBox displayCardNamesCheckBox; diff --git a/cockatrice/src/interface/widgets/settings_page/deck_editor_settings_page.cpp b/cockatrice/src/interface/widgets/settings_page/deck_editor_settings_page.cpp index bb0a1096c..f425afe60 100644 --- a/cockatrice/src/interface/widgets/settings_page/deck_editor_settings_page.cpp +++ b/cockatrice/src/interface/widgets/settings_page/deck_editor_settings_page.cpp @@ -17,9 +17,9 @@ DeckEditorSettingsPage::DeckEditorSettingsPage() { - picDownloadCheckBox.setChecked(SettingsCache::instance().personal().getPicDownload()); - connect(&picDownloadCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().personal(), - &PersonalSettings::setPicDownload); + picDownloadCheckBox.setChecked(SettingsCache::instance().downloads().getPicDownload()); + connect(&picDownloadCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().downloads(), + &DownloadSettings::setPicDownload); urlLinkLabel.setTextInteractionFlags(Qt::LinksAccessibleByMouse); urlLinkLabel.setOpenExternalLinks(true); @@ -29,7 +29,7 @@ DeckEditorSettingsPage::DeckEditorSettingsPage() auto *lpGeneralGrid = new QGridLayout; auto *lpSpoilerGrid = new QGridLayout; - mcDownloadSpoilersCheckBox.setChecked(SettingsCache::instance().personal().getDownloadSpoilersStatus()); + mcDownloadSpoilersCheckBox.setChecked(SettingsCache::instance().downloads().getDownloadSpoilersStatus()); mpSpoilerSavePathLineEdit = new QLineEdit(SettingsCache::instance().getSpoilerCardDatabasePath()); mpSpoilerSavePathLineEdit->setReadOnly(true); @@ -91,8 +91,8 @@ DeckEditorSettingsPage::DeckEditorSettingsPage() lpSpoilerGrid->addWidget(&infoOnSpoilersLabel, 3, 0, 1, 3, Qt::AlignTop); // On a change to the checkbox, hide/un-hide the other fields - connect(&mcDownloadSpoilersCheckBox, &QCheckBox::toggled, &SettingsCache::instance().personal(), - &PersonalSettings::setDownloadSpoilerStatus); + connect(&mcDownloadSpoilersCheckBox, &QCheckBox::toggled, &SettingsCache::instance().downloads(), + &DownloadSettings::setDownloadSpoilerStatus); connect(&mcDownloadSpoilersCheckBox, &QCheckBox::toggled, this, &DeckEditorSettingsPage::setSpoilersEnabled); mpGeneralGroupBox = new QGroupBox; diff --git a/cockatrice/src/interface/widgets/settings_page/user_interface_settings_page.cpp b/cockatrice/src/interface/widgets/settings_page/user_interface_settings_page.cpp index 8203dee58..cfd855d33 100644 --- a/cockatrice/src/interface/widgets/settings_page/user_interface_settings_page.cpp +++ b/cockatrice/src/interface/widgets/settings_page/user_interface_settings_page.cpp @@ -5,6 +5,7 @@ #include #include +#include #include #include #include @@ -19,70 +20,70 @@ enum visualDeckStoragePromptForConversionIndex UserInterfaceSettingsPage::UserInterfaceSettingsPage() { // general settings and notification settings - notificationsEnabledCheckBox.setChecked(SettingsCache::instance().interface().getNotificationsEnabled()); - connect(¬ificationsEnabledCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().interface(), + notificationsEnabledCheckBox.setChecked(SettingsCache::instance().userInterface().getNotificationsEnabled()); + connect(¬ificationsEnabledCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().userInterface(), &InterfaceSettings::setNotificationsEnabled); connect(¬ificationsEnabledCheckBox, &QCheckBox::QT_STATE_CHANGED, this, &UserInterfaceSettingsPage::setNotificationEnabled); specNotificationsEnabledCheckBox.setChecked( - SettingsCache::instance().interface().getSpectatorNotificationsEnabled()); - specNotificationsEnabledCheckBox.setEnabled(SettingsCache::instance().interface().getNotificationsEnabled()); - connect(&specNotificationsEnabledCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().interface(), + SettingsCache::instance().userInterface().getSpectatorNotificationsEnabled()); + specNotificationsEnabledCheckBox.setEnabled(SettingsCache::instance().userInterface().getNotificationsEnabled()); + connect(&specNotificationsEnabledCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().userInterface(), &InterfaceSettings::setSpectatorNotificationsEnabled); buddyConnectNotificationsEnabledCheckBox.setChecked( - SettingsCache::instance().interface().getBuddyConnectNotificationsEnabled()); + SettingsCache::instance().userInterface().getBuddyConnectNotificationsEnabled()); buddyConnectNotificationsEnabledCheckBox.setEnabled( - SettingsCache::instance().interface().getNotificationsEnabled()); + SettingsCache::instance().userInterface().getNotificationsEnabled()); connect(&buddyConnectNotificationsEnabledCheckBox, &QCheckBox::QT_STATE_CHANGED, - &SettingsCache::instance().interface(), &InterfaceSettings::setBuddyConnectNotificationsEnabled); + &SettingsCache::instance().userInterface(), &InterfaceSettings::setBuddyConnectNotificationsEnabled); - doubleClickToPlayCheckBox.setChecked(SettingsCache::instance().interface().getDoubleClickToPlay()); - connect(&doubleClickToPlayCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().interface(), + doubleClickToPlayCheckBox.setChecked(SettingsCache::instance().userInterface().getDoubleClickToPlay()); + connect(&doubleClickToPlayCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().userInterface(), &InterfaceSettings::setDoubleClickToPlay); - clickPlaysAllSelectedCheckBox.setChecked(SettingsCache::instance().interface().getClickPlaysAllSelected()); - connect(&clickPlaysAllSelectedCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().interface(), + clickPlaysAllSelectedCheckBox.setChecked(SettingsCache::instance().userInterface().getClickPlaysAllSelected()); + connect(&clickPlaysAllSelectedCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().userInterface(), &InterfaceSettings::setClickPlaysAllSelected); - playToStackCheckBox.setChecked(SettingsCache::instance().interface().getPlayToStack()); - connect(&playToStackCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().interface(), + playToStackCheckBox.setChecked(SettingsCache::instance().userInterface().getPlayToStack()); + connect(&playToStackCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().userInterface(), &InterfaceSettings::setPlayToStack); doNotDeleteArrowsInSubPhasesCheckBox.setChecked( - SettingsCache::instance().interface().getDoNotDeleteArrowsInSubPhases()); - connect(&doNotDeleteArrowsInSubPhasesCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().interface(), - &InterfaceSettings::setDoNotDeleteArrowsInSubPhases); + SettingsCache::instance().userInterface().getDoNotDeleteArrowsInSubPhases()); + connect(&doNotDeleteArrowsInSubPhasesCheckBox, &QCheckBox::QT_STATE_CHANGED, + &SettingsCache::instance().userInterface(), &InterfaceSettings::setDoNotDeleteArrowsInSubPhases); - closeEmptyCardViewCheckBox.setChecked(SettingsCache::instance().interface().getCloseEmptyCardView()); - connect(&closeEmptyCardViewCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().interface(), + closeEmptyCardViewCheckBox.setChecked(SettingsCache::instance().userInterface().getCloseEmptyCardView()); + connect(&closeEmptyCardViewCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().userInterface(), &InterfaceSettings::setCloseEmptyCardView); - focusCardViewSearchBarCheckBox.setChecked(SettingsCache::instance().interface().getFocusCardViewSearchBar()); - connect(&focusCardViewSearchBarCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().interface(), + focusCardViewSearchBarCheckBox.setChecked(SettingsCache::instance().userInterface().getFocusCardViewSearchBar()); + connect(&focusCardViewSearchBarCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().userInterface(), &InterfaceSettings::setFocusCardViewSearchBar); - annotateTokensCheckBox.setChecked(SettingsCache::instance().interface().getAnnotateTokens()); - connect(&annotateTokensCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().interface(), + annotateTokensCheckBox.setChecked(SettingsCache::instance().userInterface().getAnnotateTokens()); + connect(&annotateTokensCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().userInterface(), &InterfaceSettings::setAnnotateTokens); - showDragSelectionCountCheckBox.setChecked(SettingsCache::instance().interface().getShowDragSelectionCount()); - connect(&showDragSelectionCountCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().interface(), + showDragSelectionCountCheckBox.setChecked(SettingsCache::instance().userInterface().getShowDragSelectionCount()); + connect(&showDragSelectionCountCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().userInterface(), &InterfaceSettings::setShowDragSelectionCount); - showTotalSelectionCountCheckBox.setChecked(SettingsCache::instance().interface().getShowTotalSelectionCount()); - connect(&showTotalSelectionCountCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().interface(), + showTotalSelectionCountCheckBox.setChecked(SettingsCache::instance().userInterface().getShowTotalSelectionCount()); + connect(&showTotalSelectionCountCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().userInterface(), &InterfaceSettings::setShowTotalSelectionCount); - useTearOffMenusCheckBox.setChecked(SettingsCache::instance().interface().getUseTearOffMenus()); - connect(&useTearOffMenusCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().interface(), + useTearOffMenusCheckBox.setChecked(SettingsCache::instance().userInterface().getUseTearOffMenus()); + connect(&useTearOffMenusCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().userInterface(), [](const QT_STATE_CHANGED_T state) { - SettingsCache::instance().interface().setUseTearOffMenus(state == Qt::Checked); + SettingsCache::instance().userInterface().setUseTearOffMenus(state == Qt::Checked); }); - keepGameChatFocusCheckBox.setChecked(SettingsCache::instance().interface().getKeepGameChatFocus()); - connect(&keepGameChatFocusCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().interface(), + keepGameChatFocusCheckBox.setChecked(SettingsCache::instance().userInterface().getKeepGameChatFocus()); + connect(&keepGameChatFocusCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().userInterface(), &InterfaceSettings::setKeepGameChatFocus); auto *generalGrid = new QGridLayout; @@ -121,9 +122,9 @@ UserInterfaceSettingsPage::UserInterfaceSettingsPage() animationGroupBox->setLayout(animationGrid); // deck editor settings - openDeckInNewTabCheckBox.setChecked(SettingsCache::instance().interface().getOpenDeckInNewTab()); - connect(&openDeckInNewTabCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().interface(), - &InterfaceSettings::setOpenDeckInNewTab); + openDeckInNewTabCheckBox.setChecked(SettingsCache::instance().deckEditor().getOpenDeckInNewTab()); + connect(&openDeckInNewTabCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().deckEditor(), + &DeckEditorSettings::setOpenDeckInNewTab); visualDeckStorageInGameCheckBox.setChecked( SettingsCache::instance().visualDeckStorage().getVisualDeckStorageInGame()); @@ -156,10 +157,9 @@ UserInterfaceSettingsPage::UserInterfaceSettingsPage() defaultDeckEditorTypeSelector.addItem(""); // these will be set in retranslateUI defaultDeckEditorTypeSelector.addItem(""); - defaultDeckEditorTypeSelector.setCurrentIndex( - SettingsCache::instance().visualDeckStorage().getDefaultDeckEditorType()); + defaultDeckEditorTypeSelector.setCurrentIndex(SettingsCache::instance().deckEditor().getDefaultDeckEditorType()); connect(&defaultDeckEditorTypeSelector, QOverload::of(&QComboBox::currentIndexChanged), - &SettingsCache::instance().visualDeckStorage(), &VisualDeckStorageSettings::setDefaultDeckEditorType); + &SettingsCache::instance().deckEditor(), &DeckEditorSettings::setDefaultDeckEditorType); auto *deckEditorGrid = new QGridLayout; deckEditorGrid->addWidget(&openDeckInNewTabCheckBox, 0, 0); @@ -175,8 +175,8 @@ UserInterfaceSettingsPage::UserInterfaceSettingsPage() // replay settings rewindBufferingMsBox.setRange(0, 9999); - rewindBufferingMsBox.setValue(SettingsCache::instance().interface().getRewindBufferingMs()); - connect(&rewindBufferingMsBox, qOverload(&QSpinBox::valueChanged), &SettingsCache::instance().interface(), + rewindBufferingMsBox.setValue(SettingsCache::instance().userInterface().getRewindBufferingMs()); + connect(&rewindBufferingMsBox, qOverload(&QSpinBox::valueChanged), &SettingsCache::instance().userInterface(), &InterfaceSettings::setRewindBufferingMs); auto *replayGrid = new QGridLayout; diff --git a/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.cpp b/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.cpp index 565cc3341..f80649eba 100644 --- a/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.cpp +++ b/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.cpp @@ -44,7 +44,7 @@ #include #include #include -#include +#include #include #include #include @@ -203,7 +203,7 @@ void AbstractTabDeckEditor::cleanDeckAndResetModified() */ AbstractTabDeckEditor::DeckOpenLocation AbstractTabDeckEditor::confirmOpen(const bool openInSameTabIfBlank) { - if (SettingsCache::instance().interface().getOpenDeckInNewTab()) { + if (SettingsCache::instance().deckEditor().getOpenDeckInNewTab()) { if (openInSameTabIfBlank && deckStateManager->isBlankNewDeck()) { return SAME_TAB; } else { diff --git a/cockatrice/src/interface/widgets/tabs/api/archidekt/tab_archidekt.cpp b/cockatrice/src/interface/widgets/tabs/api/archidekt/tab_archidekt.cpp index b9b946a25..374d35cdf 100644 --- a/cockatrice/src/interface/widgets/tabs/api/archidekt/tab_archidekt.cpp +++ b/cockatrice/src/interface/widgets/tabs/api/archidekt/tab_archidekt.cpp @@ -27,7 +27,7 @@ #include #include #include -#include +#include #include TabArchidekt::TabArchidekt(TabSupervisor *_tabSupervisor) @@ -132,8 +132,8 @@ void TabArchidekt::initializeUi() // Settings settingsButton = new SettingsButtonWidget(primaryToolbar); - cardSizeSlider = new CardSizeWidget(primaryToolbar, nullptr, - SettingsCache::instance().visualDeckStorage().getArchidektPreviewSize()); + cardSizeSlider = + new CardSizeWidget(primaryToolbar, nullptr, SettingsCache::instance().cardsDisplay().getArchidektPreviewSize()); settingsButton->addSettingsWidget(cardSizeSlider); // Assemble primary toolbar @@ -339,8 +339,8 @@ void TabArchidekt::connectSignals() doSearch(); }); - connect(cardSizeSlider, &CardSizeWidget::cardSizeSettingUpdated, &SettingsCache::instance().visualDeckStorage(), - &VisualDeckStorageSettings::setArchidektPreviewCardSize); + connect(cardSizeSlider, &CardSizeWidget::cardSizeSettingUpdated, &SettingsCache::instance().cardsDisplay(), + &CardsDisplaySettings::setArchidektPreviewCardSize); // Search button triggers immediate search connect(searchButton, &QPushButton::clicked, this, &TabArchidekt::doSearchImmediate); diff --git a/cockatrice/src/interface/widgets/tabs/api/edhrec/tab_edhrec_main.cpp b/cockatrice/src/interface/widgets/tabs/api/edhrec/tab_edhrec_main.cpp index b833f3369..42a689898 100644 --- a/cockatrice/src/interface/widgets/tabs/api/edhrec/tab_edhrec_main.cpp +++ b/cockatrice/src/interface/widgets/tabs/api/edhrec/tab_edhrec_main.cpp @@ -25,7 +25,7 @@ #include #include #include -#include +#include #include static bool canBeCommander(const CardInfoPtr &cardInfo) @@ -95,10 +95,9 @@ TabEdhRecMain::TabEdhRecMain(TabSupervisor *_tabSupervisor) : Tab(_tabSupervisor settingsButton = new SettingsButtonWidget(this); - cardSizeSlider = - new CardSizeWidget(this, nullptr, SettingsCache::instance().visualDeckStorage().getEDHRecCardSize()); - connect(cardSizeSlider, &CardSizeWidget::cardSizeSettingUpdated, &SettingsCache::instance().visualDeckStorage(), - &VisualDeckStorageSettings::setEDHRecCardSize); + cardSizeSlider = new CardSizeWidget(this, nullptr, SettingsCache::instance().cardsDisplay().getEDHRecCardSize()); + connect(cardSizeSlider, &CardSizeWidget::cardSizeSettingUpdated, &SettingsCache::instance().cardsDisplay(), + &CardsDisplaySettings::setEDHRecCardSize); settingsButton->addSettingsWidget(cardSizeSlider); diff --git a/cockatrice/src/interface/widgets/tabs/tab_game.cpp b/cockatrice/src/interface/widgets/tabs/tab_game.cpp index d3f3a1735..7ffcd8a9b 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_game.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_game.cpp @@ -258,7 +258,7 @@ void TabGame::resetChatAndPhase() void TabGame::emitUserEvent() { bool globalEvent = !game->getPlayerManager()->isSpectator() || - SettingsCache::instance().interface().getSpectatorNotificationsEnabled(); + SettingsCache::instance().userInterface().getSpectatorNotificationsEnabled(); emit userEvent(globalEvent); updatePlayerListDockTitle(); } diff --git a/cockatrice/src/interface/widgets/tabs/tab_supervisor.cpp b/cockatrice/src/interface/widgets/tabs/tab_supervisor.cpp index 3d03a1863..07774770e 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_supervisor.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_supervisor.cpp @@ -39,9 +39,9 @@ #include #include #include +#include #include #include -#include QRect MacOSTabFixStyle::subElementRect(SubElement element, const QStyleOption *option, const QWidget *widget) const { @@ -910,7 +910,7 @@ void TabSupervisor::talkLeft(TabMessage *tab) */ void TabSupervisor::openDeckInNewTab(const LoadedDeck &deckToOpen) { - int type = SettingsCache::instance().visualDeckStorage().getDefaultDeckEditorType(); + int type = SettingsCache::instance().deckEditor().getDefaultDeckEditorType(); switch (type) { case ClassicDeckEditor: addDeckEditorTab(deckToOpen); @@ -1009,7 +1009,7 @@ void TabSupervisor::tabUserEvent(bool globalEvent) tab->setContentsChanged(true); setTabIcon(indexOf(tab), QPixmap("theme:icons/tab_changed")); } - if (globalEvent && SettingsCache::instance().interface().getNotificationsEnabled()) { + if (globalEvent && SettingsCache::instance().userInterface().getNotificationsEnabled()) { QApplication::alert(this); } } @@ -1104,7 +1104,7 @@ void TabSupervisor::processUserJoined(const ServerInfo_User &userInfoJoined) } } - if (SettingsCache::instance().interface().getBuddyConnectNotificationsEnabled()) { + if (SettingsCache::instance().userInterface().getBuddyConnectNotificationsEnabled()) { QApplication::alert(this); this->actShowPopup(tr("Your buddy %1 has signed on!").arg(userName)); } diff --git a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_widget.cpp b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_widget.cpp index 21c44246e..dc98e6940 100644 --- a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_widget.cpp +++ b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_widget.cpp @@ -20,7 +20,7 @@ #include #include #include -#include +#include #include VisualDatabaseDisplayWidget::VisualDatabaseDisplayWidget(QWidget *parent, @@ -52,10 +52,10 @@ VisualDatabaseDisplayWidget::VisualDatabaseDisplayWidget(QWidget *parent, mainLayout->setContentsMargins(0, 0, 0, 0); flowWidget = new FlowWidget(this, Qt::Horizontal, Qt::ScrollBarAlwaysOff, Qt::ScrollBarPolicy::ScrollBarAsNeeded); - cardSizeWidget = new CardSizeWidget( - this, flowWidget, SettingsCache::instance().visualDeckStorage().getVisualDatabaseDisplayCardSize()); - connect(cardSizeWidget, &CardSizeWidget::cardSizeSettingUpdated, &SettingsCache::instance().visualDeckStorage(), - &VisualDeckStorageSettings::setVisualDatabaseDisplayCardSize); + cardSizeWidget = new CardSizeWidget(this, flowWidget, + SettingsCache::instance().cardsDisplay().getVisualDatabaseDisplayCardSize()); + connect(cardSizeWidget, &CardSizeWidget::cardSizeSettingUpdated, &SettingsCache::instance().cardsDisplay(), + &CardsDisplaySettings::setVisualDatabaseDisplayCardSize); searchContainer = new FlowWidget(this, Qt::Horizontal, Qt::ScrollBarAlwaysOff, Qt::ScrollBarAlwaysOff); diff --git a/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_editor_sample_hand_widget.cpp b/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_editor_sample_hand_widget.cpp index 20ad3b65c..a4268563a 100644 --- a/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_editor_sample_hand_widget.cpp +++ b/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_editor_sample_hand_widget.cpp @@ -8,7 +8,7 @@ #include #include -#include +#include #include VisualDeckEditorSampleHandWidget::VisualDeckEditorSampleHandWidget(QWidget *parent, @@ -34,10 +34,10 @@ VisualDeckEditorSampleHandWidget::VisualDeckEditorSampleHandWidget(QWidget *pare resetAndHandSizeLayout->addWidget(resetButton); handSizeSpinBox = new QSpinBox(this); - handSizeSpinBox->setValue(SettingsCache::instance().visualDeckStorage().getVisualDeckEditorSampleHandSize()); + handSizeSpinBox->setValue(SettingsCache::instance().cardsDisplay().getSampleHandSize()); handSizeSpinBox->setMinimum(1); - connect(handSizeSpinBox, qOverload(&QSpinBox::valueChanged), &SettingsCache::instance().visualDeckStorage(), - &VisualDeckStorageSettings::setVisualDeckEditorSampleHandSize); + connect(handSizeSpinBox, qOverload(&QSpinBox::valueChanged), &SettingsCache::instance().cardsDisplay(), + &CardsDisplaySettings::setSampleHandSize); connect(handSizeSpinBox, qOverload(&QSpinBox::valueChanged), this, &VisualDeckEditorSampleHandWidget::updateDisplay); resetAndHandSizeLayout->addWidget(handSizeSpinBox); diff --git a/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_editor_widget.cpp b/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_editor_widget.cpp index befd57804..e5bcb2fd3 100644 --- a/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_editor_widget.cpp +++ b/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_editor_widget.cpp @@ -25,7 +25,7 @@ #include #include #include -#include +#include #include VisualDeckEditorWidget::VisualDeckEditorWidget(QWidget *parent, @@ -45,9 +45,9 @@ VisualDeckEditorWidget::VisualDeckEditorWidget(QWidget *parent, initializeScrollAreaAndZoneContainer(); cardSizeWidget = - new CardSizeWidget(this, nullptr, SettingsCache::instance().visualDeckStorage().getVisualDeckEditorCardSize()); - connect(cardSizeWidget, &CardSizeWidget::cardSizeSettingUpdated, &SettingsCache::instance().visualDeckStorage(), - &VisualDeckStorageSettings::setVisualDeckEditorCardSize); + new CardSizeWidget(this, nullptr, SettingsCache::instance().cardsDisplay().getVisualDeckEditorCardSize()); + connect(cardSizeWidget, &CardSizeWidget::cardSizeSettingUpdated, &SettingsCache::instance().cardsDisplay(), + &CardsDisplaySettings::setVisualDeckEditorCardSize); mainLayout->addWidget(displayOptionsAndSearch); mainLayout->addWidget(scrollArea); diff --git a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_quick_settings_widget.cpp b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_quick_settings_widget.cpp index b89c62f9c..cce3ff6ce 100644 --- a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_quick_settings_widget.cpp +++ b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_quick_settings_widget.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include @@ -115,11 +116,11 @@ VisualDeckStorageQuickSettingsWidget::VisualDeckStorageQuickSettingsWidget(QWidg // card size slider cardSizeWidget = - new CardSizeWidget(this, nullptr, SettingsCache::instance().visualDeckStorage().getVisualDeckStorageCardSize()); + new CardSizeWidget(this, nullptr, SettingsCache::instance().cardsDisplay().getVisualDeckStorageCardSize()); connect(cardSizeWidget->getSlider(), &QSlider::valueChanged, this, &VisualDeckStorageQuickSettingsWidget::cardSizeChanged); - connect(cardSizeWidget, &CardSizeWidget::cardSizeSettingUpdated, &SettingsCache::instance().visualDeckStorage(), - &VisualDeckStorageSettings::setVisualDeckStorageCardSize); + connect(cardSizeWidget, &CardSizeWidget::cardSizeSettingUpdated, &SettingsCache::instance().cardsDisplay(), + &CardsDisplaySettings::setVisualDeckStorageCardSize); // putting everything together this->addSettingsWidget(showFoldersCheckBox); diff --git a/cockatrice/src/interface/window_main.cpp b/cockatrice/src/interface/window_main.cpp index 21d847e63..e21737d67 100644 --- a/cockatrice/src/interface/window_main.cpp +++ b/cockatrice/src/interface/window_main.cpp @@ -70,7 +70,9 @@ #include #include #include +#include #include +#include #include #include #include @@ -391,9 +393,9 @@ void MainWindow::createActions() connect(aCheckCardUpdatesBackground, &QAction::triggered, this, &MainWindow::actCheckCardUpdatesBackground); aStatusBar = new QAction(this); aStatusBar->setCheckable(true); - aStatusBar->setChecked(SettingsCache::instance().personal().getShowStatusBar()); - connect(aStatusBar, &QAction::triggered, &SettingsCache::instance().personal(), - &PersonalSettings::setShowStatusBar); + aStatusBar->setChecked(SettingsCache::instance().userInterface().getShowStatusBar()); + connect(aStatusBar, &QAction::triggered, &SettingsCache::instance().userInterface(), + &InterfaceSettings::setShowStatusBar); aViewLog = new QAction(this); connect(aViewLog, &QAction::triggered, this, &MainWindow::actViewLog); aOpenSettingsFolder = new QAction(this); @@ -518,9 +520,9 @@ MainWindow::MainWindow(QWidget *parent) } // status bar - connect(&SettingsCache::instance().personal(), &PersonalSettings::showStatusBarChanged, this, + connect(&SettingsCache::instance().userInterface(), &InterfaceSettings::showStatusBarChanged, this, [this](bool show) { statusBar()->setVisible(show); }); - statusBar()->setVisible(SettingsCache::instance().personal().getShowStatusBar()); + statusBar()->setVisible(SettingsCache::instance().userInterface().getShowStatusBar()); connect(&SettingsCache::instance().shortcuts(), &ShortcutsSettings::shortCutChanged, this, &MainWindow::refreshShortcuts); @@ -557,23 +559,23 @@ void MainWindow::startupConfigCheck() actCheckClientUpdates(); } - if (SettingsCache::instance().personal().getClientVersion() == CLIENT_INFO_NOT_SET) { + if (SettingsCache::instance().network().getClientVersion() == CLIENT_INFO_NOT_SET) { // no config found, 99% new clean install qCInfo(WindowMainStartupVersionLog) << "Startup: old client version empty, assuming first start after clean install"; alertForcedOracleRun(VERSION_STRING, false); SettingsCache::instance().downloads().resetToDefaultURLs(); // populate the download urls - SettingsCache::instance().personal().setClientVersion(VERSION_STRING); + SettingsCache::instance().network().setClientVersion(VERSION_STRING); if (QString(VERSION_STRING).contains("custom", Qt::CaseInsensitive)) { SettingsCache::instance().updates().setCheckUpdatesOnStartup(false); } else if (QString(VERSION_STRING).contains("beta", Qt::CaseInsensitive)) { SettingsCache::instance().updates().setUpdateReleaseChannelIndex(1); } - } else if (SettingsCache::instance().personal().getClientVersion() != VERSION_STRING) { + } else if (SettingsCache::instance().network().getClientVersion() != VERSION_STRING) { // config found, from another (presumably older) version qCInfo(WindowMainStartupVersionLog) - << "Startup: old client version" << SettingsCache::instance().personal().getClientVersion() + << "Startup: old client version" << SettingsCache::instance().network().getClientVersion() << "differs, assuming first start after update"; if (SettingsCache::instance().updates().getNotifyAboutNewVersion()) { alertForcedOracleRun(VERSION_STRING, true); @@ -598,7 +600,7 @@ void MainWindow::startupConfigCheck() } } - SettingsCache::instance().personal().setClientVersion(VERSION_STRING); + SettingsCache::instance().network().setClientVersion(VERSION_STRING); } else { // previous config from this version found qCInfo(WindowMainStartupVersionLog) << "Startup: found config with current version"; diff --git a/cockatrice/src/main.cpp b/cockatrice/src/main.cpp index 13f724a8b..814da9808 100644 --- a/cockatrice/src/main.cpp +++ b/cockatrice/src/main.cpp @@ -47,8 +47,11 @@ #include #include #include +#include #include #include +#include +#include #include QTranslator *translator, *qtTranslator; @@ -349,7 +352,7 @@ int main(int argc, char *argv[]) // set name of the app desktop file; used by wayland to load the window icon QGuiApplication::setDesktopFileName("cockatrice"); - SettingsCache::instance().personal().setClientID(generateClientID()); + SettingsCache::instance().network().setClientID(generateClientID()); // If spoiler mode is enabled, we will download the spoilers // then reload the DB. otherwise just reload the DB @@ -360,7 +363,7 @@ int main(int argc, char *argv[]) // force shortcuts to be shown/hidden in right-click menus, regardless of system defaults qApp->setAttribute(Qt::AA_DontShowShortcutsInContextMenus, - !SettingsCache::instance().cardsDisplay().getShowShortcuts()); + !SettingsCache::instance().userInterface().getShowShortcuts()); #ifdef Q_OS_MAC for (const QString &url : pendingMacUrls) { diff --git a/libcockatrice_interfaces/CMakeLists.txt b/libcockatrice_interfaces/CMakeLists.txt index c0afe09d4..f606f6207 100644 --- a/libcockatrice_interfaces/CMakeLists.txt +++ b/libcockatrice_interfaces/CMakeLists.txt @@ -9,6 +9,7 @@ set(HEADERS libcockatrice/interfaces/interface_card_set_priority_controller.h libcockatrice/interfaces/interface_cards_display_settings_provider.h libcockatrice/interfaces/interface_chat_settings_provider.h + libcockatrice/interfaces/interface_deck_editor_settings_provider.h libcockatrice/interfaces/interface_game_settings_provider.h libcockatrice/interfaces/interface_interface_settings_provider.h libcockatrice/interfaces/interface_network_settings_provider.h diff --git a/libcockatrice_interfaces/libcockatrice/interfaces/interface_cards_display_settings_provider.h b/libcockatrice_interfaces/libcockatrice/interfaces/interface_cards_display_settings_provider.h index 2ef9dc87d..3ee3d2aef 100644 --- a/libcockatrice_interfaces/libcockatrice/interfaces/interface_cards_display_settings_provider.h +++ b/libcockatrice_interfaces/libcockatrice/interfaces/interface_cards_display_settings_provider.h @@ -14,15 +14,17 @@ public: [[nodiscard]] virtual int getPrintingSelectorCardSize() const = 0; [[nodiscard]] virtual bool getIncludeRebalancedCards() const = 0; [[nodiscard]] virtual bool getPrintingSelectorNavigationButtonsVisible() const = 0; - [[nodiscard]] virtual bool getDeckEditorBannerCardComboBoxVisible() const = 0; - [[nodiscard]] virtual bool getDeckEditorTagsWidgetVisible() const = 0; [[nodiscard]] virtual bool getTapAnimation() const = 0; [[nodiscard]] virtual bool getAutoRotateSidewaysLayoutCards() const = 0; [[nodiscard]] virtual bool getScaleCards() const = 0; [[nodiscard]] virtual int getStackCardOverlapPercent() const = 0; [[nodiscard]] virtual int getCardInfoViewMode() const = 0; - [[nodiscard]] virtual bool getShowShortcuts() const = 0; - [[nodiscard]] virtual bool getShowGameSelectorFilterToolbar() const = 0; + [[nodiscard]] virtual int getVisualDeckStorageCardSize() const = 0; + [[nodiscard]] virtual int getVisualDatabaseDisplayCardSize() const = 0; + [[nodiscard]] virtual int getVisualDeckEditorCardSize() const = 0; + [[nodiscard]] virtual int getEDHRecCardSize() const = 0; + [[nodiscard]] virtual int getArchidektPreviewSize() const = 0; + [[nodiscard]] virtual int getSampleHandSize() const = 0; }; #endif // COCKATRICE_INTERFACE_CARDS_DISPLAY_SETTINGS_PROVIDER_H diff --git a/libcockatrice_interfaces/libcockatrice/interfaces/interface_deck_editor_settings_provider.h b/libcockatrice_interfaces/libcockatrice/interfaces/interface_deck_editor_settings_provider.h new file mode 100644 index 000000000..0ef39d044 --- /dev/null +++ b/libcockatrice_interfaces/libcockatrice/interfaces/interface_deck_editor_settings_provider.h @@ -0,0 +1,15 @@ +#ifndef COCKATRICE_INTERFACE_DECK_EDITOR_SETTINGS_PROVIDER_H +#define COCKATRICE_INTERFACE_DECK_EDITOR_SETTINGS_PROVIDER_H + +class IDeckEditorSettingsProvider +{ +public: + virtual ~IDeckEditorSettingsProvider() = default; + + [[nodiscard]] virtual bool getOpenDeckInNewTab() const = 0; + [[nodiscard]] virtual bool getBannerCardComboBoxVisible() const = 0; + [[nodiscard]] virtual bool getTagsWidgetVisible() const = 0; + [[nodiscard]] virtual int getDefaultDeckEditorType() const = 0; +}; + +#endif // COCKATRICE_INTERFACE_DECK_EDITOR_SETTINGS_PROVIDER_H diff --git a/libcockatrice_interfaces/libcockatrice/interfaces/interface_interface_settings_provider.h b/libcockatrice_interfaces/libcockatrice/interfaces/interface_interface_settings_provider.h index e4c8677c8..07e60b28f 100644 --- a/libcockatrice_interfaces/libcockatrice/interfaces/interface_interface_settings_provider.h +++ b/libcockatrice_interfaces/libcockatrice/interfaces/interface_interface_settings_provider.h @@ -29,15 +29,15 @@ public: [[nodiscard]] virtual bool getHorizontalHand() const = 0; [[nodiscard]] virtual bool getInvertVerticalCoordinate() const = 0; [[nodiscard]] virtual int getMinPlayersForMultiColumnLayout() const = 0; - [[nodiscard]] virtual bool getOpenDeckInNewTab() const = 0; [[nodiscard]] virtual int getRewindBufferingMs() const = 0; [[nodiscard]] virtual qreal getFastForwardSpeed() const = 0; - [[nodiscard]] virtual bool getStyleUserList() const = 0; [[nodiscard]] virtual bool getLeftJustified() const = 0; [[nodiscard]] virtual int getZoneViewGroupByIndex() const = 0; [[nodiscard]] virtual int getZoneViewSortByIndex() const = 0; [[nodiscard]] virtual bool getZoneViewPileView() const = 0; - [[nodiscard]] virtual QString getKnownMissingFeatures() = 0; + [[nodiscard]] virtual bool getShowStatusBar() const = 0; + [[nodiscard]] virtual bool getShowShortcuts() const = 0; + [[nodiscard]] virtual bool getShowGameSelectorFilterToolbar() const = 0; }; #endif // COCKATRICE_INTERFACE_INTERFACE_SETTINGS_PROVIDER_H diff --git a/libcockatrice_interfaces/libcockatrice/interfaces/interface_personal_settings_provider.h b/libcockatrice_interfaces/libcockatrice/interfaces/interface_personal_settings_provider.h index 76c8a8367..f3d3b1220 100644 --- a/libcockatrice_interfaces/libcockatrice/interfaces/interface_personal_settings_provider.h +++ b/libcockatrice_interfaces/libcockatrice/interfaces/interface_personal_settings_provider.h @@ -11,20 +11,8 @@ public: virtual ~IPersonalSettingsProvider() = default; [[nodiscard]] virtual QString getLang() const = 0; - [[nodiscard]] virtual QString getClientID() = 0; - [[nodiscard]] virtual QString getClientVersion() = 0; - [[nodiscard]] virtual int getKeepAlive() const = 0; - [[nodiscard]] virtual int getTimeOut() const = 0; - [[nodiscard]] virtual bool getPicDownload() const = 0; - [[nodiscard]] virtual bool getShowStatusBar() const = 0; - [[nodiscard]] virtual int getMaxFontSize() const = 0; - [[nodiscard]] virtual QString getHighlightWords() const = 0; - [[nodiscard]] virtual QString getHomeTabBackgroundSource() const = 0; - [[nodiscard]] virtual int getHomeTabBackgroundShuffleFrequency() const = 0; - [[nodiscard]] virtual bool getHomeTabDisplayCardName() const = 0; [[nodiscard]] virtual bool getShowTipsOnStartup() const = 0; [[nodiscard]] virtual QList getSeenTips() const = 0; - [[nodiscard]] virtual bool getDownloadSpoilersStatus() const = 0; }; #endif // COCKATRICE_INTERFACE_PERSONAL_SETTINGS_PROVIDER_H diff --git a/libcockatrice_interfaces/libcockatrice/interfaces/interface_visual_deck_storage_settings_provider.h b/libcockatrice_interfaces/libcockatrice/interfaces/interface_visual_deck_storage_settings_provider.h index e5e7fe0d3..114b053c6 100644 --- a/libcockatrice_interfaces/libcockatrice/interfaces/interface_visual_deck_storage_settings_provider.h +++ b/libcockatrice_interfaces/libcockatrice/interfaces/interface_visual_deck_storage_settings_provider.h @@ -16,7 +16,6 @@ public: [[nodiscard]] virtual bool getVisualDeckStorageShowColorIdentity() const = 0; [[nodiscard]] virtual bool getVisualDeckStorageShowBannerCardComboBox() const = 0; [[nodiscard]] virtual bool getVisualDeckStorageShowTagsOnDeckPreviews() const = 0; - [[nodiscard]] virtual int getVisualDeckStorageCardSize() const = 0; [[nodiscard]] virtual bool getVisualDeckStorageDrawUnusedColorIdentities() const = 0; [[nodiscard]] virtual int getVisualDeckStorageUnusedColorIdentitiesOpacity() const = 0; [[nodiscard]] virtual int getVisualDeckStorageTooltipType() const = 0; @@ -24,14 +23,8 @@ public: [[nodiscard]] virtual bool getVisualDeckStorageAlwaysConvert() const = 0; [[nodiscard]] virtual bool getVisualDeckStorageInGame() const = 0; [[nodiscard]] virtual bool getVisualDeckStorageSelectionAnimation() const = 0; - [[nodiscard]] virtual int getVisualDeckEditorCardSize() const = 0; - [[nodiscard]] virtual int getVisualDeckEditorSampleHandSize() const = 0; - [[nodiscard]] virtual int getVisualDatabaseDisplayCardSize() const = 0; [[nodiscard]] virtual bool getVisualDatabaseDisplayFilterToMostRecentSetsEnabled() const = 0; [[nodiscard]] virtual int getVisualDatabaseDisplayFilterToMostRecentSetsAmount() const = 0; - [[nodiscard]] virtual int getEDHRecCardSize() const = 0; - [[nodiscard]] virtual int getArchidektPreviewSize() const = 0; - [[nodiscard]] virtual int getDefaultDeckEditorType() const = 0; }; #endif // COCKATRICE_INTERFACE_VISUAL_DECK_STORAGE_SETTINGS_PROVIDER_H diff --git a/libcockatrice_settings/CMakeLists.txt b/libcockatrice_settings/CMakeLists.txt index f8e9c2bce..9e2654a9a 100644 --- a/libcockatrice_settings/CMakeLists.txt +++ b/libcockatrice_settings/CMakeLists.txt @@ -3,18 +3,21 @@ set(CMAKE_AUTOUIC ON) set(CMAKE_AUTORCC ON) set(HEADERS + libcockatrice/settings/appearance_settings.h libcockatrice/settings/cache_storage_settings.h libcockatrice/settings/card_database_settings.h libcockatrice/settings/card_override_settings.h libcockatrice/settings/cards_display_settings.h libcockatrice/settings/chat_settings.h libcockatrice/settings/debug_settings.h + libcockatrice/settings/deck_editor_settings.h libcockatrice/settings/download_settings.h libcockatrice/settings/game_filters_settings.h libcockatrice/settings/game_settings.h libcockatrice/settings/interface_settings.h libcockatrice/settings/layouts_settings.h libcockatrice/settings/message_settings.h + libcockatrice/settings/network_settings.h libcockatrice/settings/paths_settings.h libcockatrice/settings/personal_settings.h libcockatrice/settings/recents_settings.h @@ -32,18 +35,21 @@ qt6_wrap_cpp(MOC_SOURCES ${HEADERS}) add_library( libcockatrice_settings STATIC ${MOC_SOURCES} + libcockatrice/settings/appearance_settings.cpp libcockatrice/settings/cache_storage_settings.cpp libcockatrice/settings/card_database_settings.cpp libcockatrice/settings/card_override_settings.cpp libcockatrice/settings/cards_display_settings.cpp libcockatrice/settings/chat_settings.cpp libcockatrice/settings/debug_settings.cpp + libcockatrice/settings/deck_editor_settings.cpp libcockatrice/settings/download_settings.cpp libcockatrice/settings/game_filters_settings.cpp libcockatrice/settings/game_settings.cpp libcockatrice/settings/interface_settings.cpp libcockatrice/settings/layouts_settings.cpp libcockatrice/settings/message_settings.cpp + libcockatrice/settings/network_settings.cpp libcockatrice/settings/paths_settings.cpp libcockatrice/settings/personal_settings.cpp libcockatrice/settings/recents_settings.cpp diff --git a/libcockatrice_settings/libcockatrice/settings/appearance_settings.cpp b/libcockatrice_settings/libcockatrice/settings/appearance_settings.cpp new file mode 100644 index 000000000..45a02299e --- /dev/null +++ b/libcockatrice_settings/libcockatrice/settings/appearance_settings.cpp @@ -0,0 +1,71 @@ +#include "appearance_settings.h" + +AppearanceSettings::AppearanceSettings(const QString &settingPath, QObject *parent) + : SettingsManager(settingPath + "appearance.ini", "appearance", QString(), parent) +{ +} + +QString AppearanceSettings::getThemeName() const +{ + return getValue("themeName", QString(), QString()).toString(); +} + +void AppearanceSettings::setThemeName(const QString &_themeName) +{ + setValue(_themeName, "themeName"); + emit themeNameChanged(); +} + +bool AppearanceSettings::getStyleUserList() const +{ + return getValue("styleUserList", QString(), QString(), true).toBool(); +} + +void AppearanceSettings::setStyleUserList(bool _styleUserList) +{ + setValue(_styleUserList, "styleUserList"); + emit styleUserListChanged(); +} + +int AppearanceSettings::getMaxFontSize() const +{ + return getValue("maxFontSize", QString(), QString(), 12).toInt(); +} + +void AppearanceSettings::setMaxFontSize(int _max) +{ + setValue(_max, "maxFontSize"); +} + +QString AppearanceSettings::getHomeTabBackgroundSource() const +{ + return getValue("homeTabBackgroundSource", QString(), QString(), "themed").toString(); +} + +void AppearanceSettings::setHomeTabBackgroundSource(const QString &_backgroundSource) +{ + setValue(_backgroundSource, "homeTabBackgroundSource"); + emit homeTabBackgroundSourceChanged(); +} + +int AppearanceSettings::getHomeTabBackgroundShuffleFrequency() const +{ + return getValue("homeTabBackgroundShuffleFrequency", QString(), QString(), 0).toInt(); +} + +void AppearanceSettings::setHomeTabBackgroundShuffleFrequency(int _frequency) +{ + setValue(_frequency, "homeTabBackgroundShuffleFrequency"); + emit homeTabBackgroundShuffleFrequencyChanged(); +} + +bool AppearanceSettings::getHomeTabDisplayCardName() const +{ + return getValue("homeTabDisplayCardName", QString(), QString(), true).toBool(); +} + +void AppearanceSettings::setHomeTabDisplayCardName(bool _displayCardName) +{ + setValue(_displayCardName, "homeTabDisplayCardName"); + emit homeTabDisplayCardNameChanged(); +} diff --git a/libcockatrice_settings/libcockatrice/settings/appearance_settings.h b/libcockatrice_settings/libcockatrice/settings/appearance_settings.h new file mode 100644 index 000000000..d9b326bee --- /dev/null +++ b/libcockatrice_settings/libcockatrice/settings/appearance_settings.h @@ -0,0 +1,45 @@ +/** + * @file appearance_settings.h + * @ingroup CoreSettings + */ +//! \todo Document this file. + +#ifndef APPEARANCE_SETTINGS_H +#define APPEARANCE_SETTINGS_H + +#include "settings_manager.h" + +class AppearanceSettings : public SettingsManager +{ + Q_OBJECT + friend class SettingsCache; + +public: + [[nodiscard]] QString getThemeName() const; + void setThemeName(const QString &_themeName); + [[nodiscard]] bool getStyleUserList() const; + void setStyleUserList(bool _styleUserList); + [[nodiscard]] int getMaxFontSize() const; + void setMaxFontSize(int _max); + [[nodiscard]] QString getHomeTabBackgroundSource() const; + void setHomeTabBackgroundSource(const QString &_backgroundSource); + [[nodiscard]] int getHomeTabBackgroundShuffleFrequency() const; + void setHomeTabBackgroundShuffleFrequency(int _frequency); + [[nodiscard]] bool getHomeTabDisplayCardName() const; + void setHomeTabDisplayCardName(bool _displayCardName); + +signals: + void themeNameChanged(); + void styleUserListChanged(); + void homeTabBackgroundSourceChanged(); + void homeTabBackgroundShuffleFrequencyChanged(); + void homeTabDisplayCardNameChanged(); + +public: + explicit AppearanceSettings(const QString &settingPath, QObject *parent = nullptr); + +private: + AppearanceSettings(const AppearanceSettings & /*other*/); +}; + +#endif // APPEARANCE_SETTINGS_H diff --git a/libcockatrice_settings/libcockatrice/settings/cache_storage_settings.cpp b/libcockatrice_settings/libcockatrice/settings/cache_storage_settings.cpp index c1fd87ceb..c800cc024 100644 --- a/libcockatrice_settings/libcockatrice/settings/cache_storage_settings.cpp +++ b/libcockatrice_settings/libcockatrice/settings/cache_storage_settings.cpp @@ -1,7 +1,7 @@ #include "cache_storage_settings.h" CacheStorageSettings::CacheStorageSettings(const QString &settingPath, QObject *parent) - : SettingsManager(settingPath + "cache_storage.ini", "personal", QString(), parent) + : SettingsManager(settingPath + "cache_storage.ini", "cache_storage", QString(), parent) { } diff --git a/libcockatrice_settings/libcockatrice/settings/card_database_settings.cpp b/libcockatrice_settings/libcockatrice/settings/card_database_settings.cpp index 219c79c34..b55c97909 100644 --- a/libcockatrice_settings/libcockatrice/settings/card_database_settings.cpp +++ b/libcockatrice_settings/libcockatrice/settings/card_database_settings.cpp @@ -9,7 +9,7 @@ CardDatabaseSettings::CardDatabaseSettings(const QString &settingPath, QObject * void CardDatabaseSettings::setSortKey(QString shortName, unsigned int sortKey) { - setValue(sortKey, "sortkey", "sets", shortName); + setValue(sortKey, "sortKey", "sets", shortName); QMutexLocker lock(&setOptionsMutex); ensureSetOptionsLoaded(); setOptionsCache[shortName].sortKey = sortKey; @@ -25,7 +25,7 @@ void CardDatabaseSettings::setEnabled(QString shortName, bool enabled) void CardDatabaseSettings::setIsKnown(QString shortName, bool isknown) { - setValue(isknown, "isknown", "sets", shortName); + setValue(isknown, "isKnown", "sets", shortName); QMutexLocker lock(&setOptionsMutex); ensureSetOptionsLoaded(); setOptionsCache[shortName].isKnown = isknown; @@ -42,9 +42,9 @@ void CardDatabaseSettings::ensureSetOptionsLoaded() const for (const QString &group : groups) { settings.beginGroup(group); SetOptions &o = setOptionsCache[group]; - o.sortKey = settings.value("sortkey", 0).toUInt(); + o.sortKey = settings.value("sortKey", 0).toUInt(); o.enabled = settings.value("enabled", true).toBool(); - o.isKnown = settings.value("isknown", true).toBool(); + o.isKnown = settings.value("isKnown", true).toBool(); settings.endGroup(); } setOptionsLoaded = true; @@ -84,7 +84,7 @@ void CardDatabaseSettings::saveSets(const QVector + +class DeckEditorSettings : public SettingsManager, public IDeckEditorSettingsProvider +{ + Q_OBJECT + friend class SettingsCache; + +public: + [[nodiscard]] bool getOpenDeckInNewTab() const override; + [[nodiscard]] bool getBannerCardComboBoxVisible() const override; + [[nodiscard]] bool getTagsWidgetVisible() const override; + [[nodiscard]] int getDefaultDeckEditorType() const override; + + void setOpenDeckInNewTab(bool _openDeckInNewTab); + void setBannerCardComboBoxVisible(bool _bannerCardComboBoxVisible); + void setTagsWidgetVisible(bool _tagsWidgetVisible); + void setDefaultDeckEditorType(int _defaultDeckEditorType); + +signals: + void bannerCardComboBoxVisibleChanged(bool visible); + void tagsWidgetVisibleChanged(bool visible); + +public: + explicit DeckEditorSettings(const QString &settingPath, QObject *parent = nullptr); + +private: + DeckEditorSettings(const DeckEditorSettings & /*other*/); +}; + +#endif // DECK_EDITOR_SETTINGS_H diff --git a/libcockatrice_settings/libcockatrice/settings/download_settings.cpp b/libcockatrice_settings/libcockatrice/settings/download_settings.cpp index 66525a598..919199126 100644 --- a/libcockatrice_settings/libcockatrice/settings/download_settings.cpp +++ b/libcockatrice_settings/libcockatrice/settings/download_settings.cpp @@ -27,3 +27,25 @@ void DownloadSettings::resetToDefaultURLs() { setValue(QVariant::fromValue(DEFAULT_DOWNLOAD_URLS), "urls"); } + +bool DownloadSettings::getPicDownload() const +{ + return getValue("pictureDownload", QString(), QString(), true).toBool(); +} + +void DownloadSettings::setPicDownload(bool _picDownload) +{ + setValue(_picDownload, "pictureDownload"); + emit picDownloadChanged(); +} + +bool DownloadSettings::getDownloadSpoilersStatus() const +{ + return getValue("downloadSpoilers", QString(), QString(), false).toBool(); +} + +void DownloadSettings::setDownloadSpoilerStatus(bool _spoilerStatus) +{ + setValue(_spoilerStatus, "downloadSpoilers"); + emit downloadSpoilerStatusChanged(); +} diff --git a/libcockatrice_settings/libcockatrice/settings/download_settings.h b/libcockatrice_settings/libcockatrice/settings/download_settings.h index 60e59220b..a3a6f4ca9 100644 --- a/libcockatrice_settings/libcockatrice/settings/download_settings.h +++ b/libcockatrice_settings/libcockatrice/settings/download_settings.h @@ -22,6 +22,14 @@ public: QStringList getAllURLs() const; void setDownloadUrls(const QStringList &downloadURLs); void resetToDefaultURLs(); + [[nodiscard]] bool getPicDownload() const; + void setPicDownload(bool _picDownload); + [[nodiscard]] bool getDownloadSpoilersStatus() const; + void setDownloadSpoilerStatus(bool _spoilerStatus); + +signals: + void picDownloadChanged(); + void downloadSpoilerStatusChanged(); }; #endif // COCKATRICE_DOWNLOADSETTINGS_H diff --git a/libcockatrice_settings/libcockatrice/settings/game_filters_settings.cpp b/libcockatrice_settings/libcockatrice/settings/game_filters_settings.cpp index 4f5bf52ee..ad972b433 100644 --- a/libcockatrice_settings/libcockatrice/settings/game_filters_settings.cpp +++ b/libcockatrice_settings/libcockatrice/settings/game_filters_settings.cpp @@ -19,137 +19,137 @@ static QString hashGameType(const QString &gameType) void GameFiltersSettings::setHideBuddiesOnlyGames(bool hide) { - setValue(hide, "hide_buddies_only_games"); + setValue(hide, "hideBuddiesOnlyGames"); } bool GameFiltersSettings::isHideBuddiesOnlyGames() const { - QVariant previous = getValue("hide_buddies_only_games"); + QVariant previous = getValue("hideBuddiesOnlyGames"); return previous == QVariant() ? false : previous.toBool(); } void GameFiltersSettings::setHideFullGames(bool hide) { - setValue(hide, "hide_full_games"); + setValue(hide, "hideFullGames"); } bool GameFiltersSettings::isHideFullGames() const { - QVariant previous = getValue("hide_full_games"); + QVariant previous = getValue("hideFullGames"); return previous == QVariant() ? false : previous.toBool(); } void GameFiltersSettings::setHideGamesThatStarted(bool hide) { - setValue(hide, "hide_games_that_started"); + setValue(hide, "hideGamesThatStarted"); } bool GameFiltersSettings::isHideGamesThatStarted() const { - QVariant previous = getValue("hide_games_that_started"); + QVariant previous = getValue("hideGamesThatStarted"); return previous == QVariant() ? false : previous.toBool(); } void GameFiltersSettings::setHidePasswordProtectedGames(bool hide) { - setValue(hide, "hide_password_protected_games"); + setValue(hide, "hidePasswordProtectedGames"); } bool GameFiltersSettings::isHidePasswordProtectedGames() const { - QVariant previous = getValue("hide_password_protected_games"); + QVariant previous = getValue("hidePasswordProtectedGames"); return previous == QVariant() ? false : previous.toBool(); } void GameFiltersSettings::setHideIgnoredUserGames(bool hide) { - setValue(hide, "hide_ignored_user_games"); + setValue(hide, "hideIgnoredUserGames"); } bool GameFiltersSettings::isHideIgnoredUserGames() const { - QVariant previous = getValue("hide_ignored_user_games"); + QVariant previous = getValue("hideIgnoredUserGames"); return previous == QVariant() ? true : previous.toBool(); } void GameFiltersSettings::setHideNotBuddyCreatedGames(bool hide) { - setValue(hide, "hide_not_buddy_created_games"); + setValue(hide, "hideNotBuddyCreatedGames"); } bool GameFiltersSettings::isHideNotBuddyCreatedGames() const { - QVariant previous = getValue("hide_not_buddy_created_games"); + QVariant previous = getValue("hideNotBuddyCreatedGames"); return previous == QVariant() ? false : previous.toBool(); } void GameFiltersSettings::setHideOpenDecklistGames(bool hide) { - setValue(hide, "hide_open_decklist_games"); + setValue(hide, "hideOpenDecklistGames"); } bool GameFiltersSettings::isHideOpenDecklistGames() const { - QVariant previous = getValue("hide_open_decklist_games"); + QVariant previous = getValue("hideOpenDecklistGames"); return previous == QVariant() ? false : previous.toBool(); } void GameFiltersSettings::setGameNameFilter(QString gameName) { - setValue(gameName, "game_name_filter"); + setValue(gameName, "gameNameFilter"); } QString GameFiltersSettings::getGameNameFilter() const { - return getValue("game_name_filter").toString(); + return getValue("gameNameFilter").toString(); } void GameFiltersSettings::setCreatorNameFilters(QStringList creatorName) { - setValue(creatorName, "creator_name_filter"); + setValue(creatorName, "creatorNameFilter"); } QStringList GameFiltersSettings::getCreatorNameFilters() const { - return getValue("creator_name_filter").toStringList(); + return getValue("creatorNameFilter").toStringList(); } void GameFiltersSettings::setMinPlayers(int min) { - setValue(min, "min_players"); + setValue(min, "minPlayers"); } int GameFiltersSettings::getMinPlayers() const { - QVariant previous = getValue("min_players"); + QVariant previous = getValue("minPlayers"); return previous == QVariant() ? 1 : previous.toInt(); } void GameFiltersSettings::setMaxPlayers(int max) { - setValue(max, "max_players"); + setValue(max, "maxPlayers"); } int GameFiltersSettings::getMaxPlayers() const { - QVariant previous = getValue("max_players"); + QVariant previous = getValue("maxPlayers"); return previous == QVariant() ? 99 : previous.toInt(); } void GameFiltersSettings::setMaxGameAge(const QTime &maxGameAge) { - setValue(maxGameAge, "max_game_age_time"); + setValue(maxGameAge, "maxGameAgeTime"); } QTime GameFiltersSettings::getMaxGameAge() const { - QVariant previous = getValue("max_game_age_time"); + QVariant previous = getValue("maxGameAgeTime"); return previous.toTime(); } void GameFiltersSettings::setGameTypeEnabled(QString gametype, bool enabled) { - setValue(enabled, "game_type/" + hashGameType(gametype)); + setValue(enabled, "gameType/" + hashGameType(gametype)); } void GameFiltersSettings::setGameHashedTypeEnabled(QString gametypeHASHED, bool enabled) @@ -159,50 +159,50 @@ void GameFiltersSettings::setGameHashedTypeEnabled(QString gametypeHASHED, bool bool GameFiltersSettings::isGameTypeEnabled(QString gametype) const { - QVariant previous = getValue("game_type/" + hashGameType(gametype)); + QVariant previous = getValue("gameType/" + hashGameType(gametype)); return previous == QVariant() ? false : previous.toBool(); } void GameFiltersSettings::setShowOnlyIfSpectatorsCanWatch(bool show) { - setValue(show, "show_only_if_spectators_can_watch"); + setValue(show, "showOnlyIfSpectatorsCanWatch"); } bool GameFiltersSettings::isShowOnlyIfSpectatorsCanWatch() const { - QVariant previous = getValue("show_only_if_spectators_can_watch"); + QVariant previous = getValue("showOnlyIfSpectatorsCanWatch"); return previous == QVariant() ? false : previous.toBool(); } void GameFiltersSettings::setShowSpectatorPasswordProtected(bool show) { - setValue(show, "show_spectator_password_protected"); + setValue(show, "showSpectatorPasswordProtected"); } bool GameFiltersSettings::isShowSpectatorPasswordProtected() const { - QVariant previous = getValue("show_spectator_password_protected"); + QVariant previous = getValue("showSpectatorPasswordProtected"); return previous == QVariant() ? false : previous.toBool(); } void GameFiltersSettings::setShowOnlyIfSpectatorsCanChat(bool show) { - setValue(show, "show_only_if_spectators_can_chat"); + setValue(show, "showOnlyIfSpectatorsCanChat"); } bool GameFiltersSettings::isShowOnlyIfSpectatorsCanChat() const { - QVariant previous = getValue("show_only_if_spectators_can_chat"); + QVariant previous = getValue("showOnlyIfSpectatorsCanChat"); return previous == QVariant() ? false : previous.toBool(); } void GameFiltersSettings::setShowOnlyIfSpectatorsCanSeeHands(bool show) { - setValue(show, "show_only_if_spectators_can_see_hands"); + setValue(show, "showOnlyIfSpectatorsCanSeeHands"); } bool GameFiltersSettings::isShowOnlyIfSpectatorsCanSeeHands() const { - QVariant previous = getValue("show_only_if_spectators_can_see_hands"); + QVariant previous = getValue("showOnlyIfSpectatorsCanSeeHands"); return previous == QVariant() ? false : previous.toBool(); } \ No newline at end of file diff --git a/libcockatrice_settings/libcockatrice/settings/game_settings.cpp b/libcockatrice_settings/libcockatrice/settings/game_settings.cpp index c8d19f801..dfd67c98a 100644 --- a/libcockatrice_settings/libcockatrice/settings/game_settings.cpp +++ b/libcockatrice_settings/libcockatrice/settings/game_settings.cpp @@ -7,160 +7,160 @@ GameSettings::GameSettings(const QString &settingPath, QObject *parent) QString GameSettings::getGameDescription() const { - return getValue("gamedescription", "game").toString(); + return getValue("gameDescription", "game").toString(); } int GameSettings::getMaxPlayers() const { - return getValue("maxplayers", "game", QString(), 2).toInt(); + return getValue("maxPlayers", "game", QString(), 2).toInt(); } QString GameSettings::getGameTypes() const { - return getValue("gametypes", "game").toString(); + return getValue("gameTypes", "game").toString(); } bool GameSettings::getOnlyBuddies() const { - return getValue("onlybuddies", "game").toBool(); + return getValue("onlyBuddies", "game").toBool(); } bool GameSettings::getOnlyRegistered() const { - return getValue("onlyregistered", "game").toBool(); + return getValue("onlyRegistered", "game").toBool(); } bool GameSettings::getSpectatorsAllowed() const { - return getValue("spectatorsallowed", "game").toBool(); + return getValue("spectatorsAllowed", "game").toBool(); } bool GameSettings::getSpectatorsNeedPassword() const { - return getValue("spectatorsneedpassword", "game").toBool(); + return getValue("spectatorsNeedPassword", "game").toBool(); } bool GameSettings::getSpectatorsCanTalk() const { - return getValue("spectatorscantalk", "game").toBool(); + return getValue("spectatorsCanTalk", "game").toBool(); } bool GameSettings::getSpectatorsCanSeeEverything() const { - return getValue("spectatorscanseeeverything", "game").toBool(); + return getValue("spectatorsCanSeeEverything", "game").toBool(); } bool GameSettings::getCreateGameAsSpectator() const { - return getValue("creategameasspectator", "game").toBool(); + return getValue("createGameAsSpectator", "game").toBool(); } int GameSettings::getDefaultStartingLifeTotal() const { - return getValue("defaultstartinglifetotal", "game", QString(), 20).toInt(); + return getValue("defaultStartingLifeTotal", "game", QString(), 20).toInt(); } bool GameSettings::getShareDecklistsOnLoad() const { - return getValue("sharedecklistsonload", "game").toBool(); + return getValue("shareDecklistsOnLoad", "game").toBool(); } bool GameSettings::getRememberGameSettings() const { - return getValue("remembergamesettings", "game", QString(), true).toBool(); + return getValue("rememberGameSettings", "game", QString(), true).toBool(); } bool GameSettings::getLocalGameRememberSettings() const { - return getValue("remembersettings", "localgameoptions").toBool(); + return getValue("rememberSettings", "localgameoptions").toBool(); } int GameSettings::getLocalGameMaxPlayers() const { - return getValue("maxplayers", "localgameoptions", QString(), 1).toInt(); + return getValue("maxPlayers", "localgameoptions", QString(), 1).toInt(); } int GameSettings::getLocalGameStartingLifeTotal() const { - return getValue("startinglifetotal", "localgameoptions", QString(), 20).toInt(); + return getValue("startingLifeTotal", "localgameoptions", QString(), 20).toInt(); } void GameSettings::setGameDescription(const QString &_gameDescription) { - setValue(_gameDescription, "gamedescription", "game"); + setValue(_gameDescription, "gameDescription", "game"); } void GameSettings::setMaxPlayers(int _maxPlayers) { - setValue(_maxPlayers, "maxplayers", "game"); + setValue(_maxPlayers, "maxPlayers", "game"); } void GameSettings::setGameTypes(const QString &_gameTypes) { - setValue(_gameTypes, "gametypes", "game"); + setValue(_gameTypes, "gameTypes", "game"); } void GameSettings::setOnlyBuddies(bool _onlyBuddies) { - setValue(_onlyBuddies, "onlybuddies", "game"); + setValue(_onlyBuddies, "onlyBuddies", "game"); } void GameSettings::setOnlyRegistered(bool _onlyRegistered) { - setValue(_onlyRegistered, "onlyregistered", "game"); + setValue(_onlyRegistered, "onlyRegistered", "game"); } void GameSettings::setSpectatorsAllowed(bool _spectatorsAllowed) { - setValue(_spectatorsAllowed, "spectatorsallowed", "game"); + setValue(_spectatorsAllowed, "spectatorsAllowed", "game"); } void GameSettings::setSpectatorsNeedPassword(bool _spectatorsNeedPassword) { - setValue(_spectatorsNeedPassword, "spectatorsneedpassword", "game"); + setValue(_spectatorsNeedPassword, "spectatorsNeedPassword", "game"); } void GameSettings::setSpectatorsCanTalk(bool _spectatorsCanTalk) { - setValue(_spectatorsCanTalk, "spectatorscantalk", "game"); + setValue(_spectatorsCanTalk, "spectatorsCanTalk", "game"); } void GameSettings::setSpectatorsCanSeeEverything(bool _spectatorsCanSeeEverything) { - setValue(_spectatorsCanSeeEverything, "spectatorscanseeeverything", "game"); + setValue(_spectatorsCanSeeEverything, "spectatorsCanSeeEverything", "game"); } void GameSettings::setCreateGameAsSpectator(bool _createGameAsSpectator) { - setValue(_createGameAsSpectator, "creategameasspectator", "game"); + setValue(_createGameAsSpectator, "createGameAsSpectator", "game"); } void GameSettings::setDefaultStartingLifeTotal(int _defaultStartingLifeTotal) { - setValue(_defaultStartingLifeTotal, "defaultstartinglifetotal", "game"); + setValue(_defaultStartingLifeTotal, "defaultStartingLifeTotal", "game"); } void GameSettings::setShareDecklistsOnLoad(bool _shareDecklistsOnLoad) { - setValue(_shareDecklistsOnLoad, "sharedecklistsonload", "game"); + setValue(_shareDecklistsOnLoad, "shareDecklistsOnLoad", "game"); } void GameSettings::setRememberGameSettings(bool _rememberGameSettings) { - setValue(_rememberGameSettings, "remembergamesettings", "game"); + setValue(_rememberGameSettings, "rememberGameSettings", "game"); } void GameSettings::setLocalGameRememberSettings(bool value) { - setValue(value, "remembersettings", "localgameoptions"); + setValue(value, "rememberSettings", "localgameoptions"); } void GameSettings::setLocalGameMaxPlayers(int value) { - setValue(value, "maxplayers", "localgameoptions"); + setValue(value, "maxPlayers", "localgameoptions"); } void GameSettings::setLocalGameStartingLifeTotal(int value) { - setValue(value, "startinglifetotal", "localgameoptions"); + setValue(value, "startingLifeTotal", "localgameoptions"); } diff --git a/libcockatrice_settings/libcockatrice/settings/interface_settings.cpp b/libcockatrice_settings/libcockatrice/settings/interface_settings.cpp index b0d1d523d..0fa56ee33 100644 --- a/libcockatrice_settings/libcockatrice/settings/interface_settings.cpp +++ b/libcockatrice_settings/libcockatrice/settings/interface_settings.cpp @@ -7,7 +7,7 @@ InterfaceSettings::InterfaceSettings(const QString &settingPath, QObject *parent bool InterfaceSettings::getUseTearOffMenus() const { - return getValue("usetearoffmenus", QString(), QString(), true).toBool(); + return getValue("useTearOffMenus", QString(), QString(), true).toBool(); } int InterfaceSettings::getCardViewInitialRowsMax() const @@ -37,22 +37,22 @@ bool InterfaceSettings::getKeepGameChatFocus() const bool InterfaceSettings::getNotificationsEnabled() const { - return getValue("notificationsenabled", QString(), QString(), true).toBool(); + return getValue("enabled", "interface", "notifications", true).toBool(); } bool InterfaceSettings::getSpectatorNotificationsEnabled() const { - return getValue("specnotificationsenabled", QString(), QString(), false).toBool(); + return getValue("spectatorsEnabled", "interface", "notifications", false).toBool(); } bool InterfaceSettings::getBuddyConnectNotificationsEnabled() const { - return getValue("buddyconnectnotificationsenabled", QString(), QString(), true).toBool(); + return getValue("buddyConnectEnabled", "interface", "notifications", true).toBool(); } bool InterfaceSettings::getDoubleClickToPlay() const { - return getValue("doubleclicktoplay", QString(), QString(), true).toBool(); + return getValue("doubleClickToPlay", QString(), QString(), true).toBool(); } bool InterfaceSettings::getClickPlaysAllSelected() const @@ -62,7 +62,7 @@ bool InterfaceSettings::getClickPlaysAllSelected() const bool InterfaceSettings::getPlayToStack() const { - return getValue("playtostack", QString(), QString(), true).toBool(); + return getValue("playToStack", QString(), QString(), true).toBool(); } bool InterfaceSettings::getDoNotDeleteArrowsInSubPhases() const @@ -72,22 +72,22 @@ bool InterfaceSettings::getDoNotDeleteArrowsInSubPhases() const int InterfaceSettings::getStartingHandSize() const { - return getValue("startinghandsize", QString(), QString(), 7).toInt(); + return getValue("startingHandSize", QString(), QString(), 7).toInt(); } bool InterfaceSettings::getAnnotateTokens() const { - return getValue("annotatetokens", QString(), QString(), false).toBool(); + return getValue("annotateTokens", QString(), QString(), false).toBool(); } bool InterfaceSettings::getShowDragSelectionCount() const { - return getValue("showlassoselectioncount", QString(), QString(), true).toBool(); + return getValue("showLassoSelectionCount", QString(), QString(), true).toBool(); } bool InterfaceSettings::getShowTotalSelectionCount() const { - return getValue("showpersistentselectioncount", QString(), QString(), true).toBool(); + return getValue("showPersistentSelectionCount", QString(), QString(), true).toBool(); } int InterfaceSettings::getTallyType() const @@ -102,17 +102,12 @@ bool InterfaceSettings::getHorizontalHand() const bool InterfaceSettings::getInvertVerticalCoordinate() const { - return getValue("invert_vertical", "table", QString(), false).toBool(); + return getValue("invertVertical", "table", QString(), false).toBool(); } int InterfaceSettings::getMinPlayersForMultiColumnLayout() const { - return getValue("min_players_multicolumn", QString(), QString(), 4).toInt(); -} - -bool InterfaceSettings::getOpenDeckInNewTab() const -{ - return getValue("openDeckInNewTab", "editor", QString(), false).toBool(); + return getValue("minPlayersMulticolumn", QString(), QString(), 4).toInt(); } int InterfaceSettings::getRewindBufferingMs() const @@ -125,39 +120,44 @@ qreal InterfaceSettings::getFastForwardSpeed() const return getValue("fastForwardSpeed", "replay", QString(), 10).toReal(); } -bool InterfaceSettings::getStyleUserList() const -{ - return getValue("styleUserList", "appearance", QString(), true).toBool(); -} - bool InterfaceSettings::getLeftJustified() const { - return getValue("leftjustified", QString(), QString(), false).toBool(); + return getValue("leftJustified", QString(), QString(), false).toBool(); } int InterfaceSettings::getZoneViewGroupByIndex() const { - return getValue("groupby", "zoneview", QString(), 1).toInt(); + return getValue("groupBy", "zoneview", QString(), 1).toInt(); } int InterfaceSettings::getZoneViewSortByIndex() const { - return getValue("sortby", "zoneview", QString(), 1).toInt(); + return getValue("sortBy", "zoneview", QString(), 1).toInt(); } bool InterfaceSettings::getZoneViewPileView() const { - return getValue("pileview", "zoneview", QString(), true).toBool(); + return getValue("pileView", "zoneview", QString(), true).toBool(); } -QString InterfaceSettings::getKnownMissingFeatures() +bool InterfaceSettings::getShowStatusBar() const { - return getValue("knownmissingfeatures", QString(), QString(), "").toString(); + return getValue("showStatusBar", QString(), QString(), false).toBool(); +} + +bool InterfaceSettings::getShowShortcuts() const +{ + return getValue("showShortcuts", QString(), QString(), true).toBool(); +} + +bool InterfaceSettings::getShowGameSelectorFilterToolbar() const +{ + return getValue("showGameSelectorFilterToolbar", QString(), QString(), true).toBool(); } void InterfaceSettings::setUseTearOffMenus(bool _useTearOffMenus) { - setValue(_useTearOffMenus, "usetearoffmenus"); + setValue(_useTearOffMenus, "useTearOffMenus"); emit useTearOffMenusChanged(_useTearOffMenus); } @@ -189,22 +189,22 @@ void InterfaceSettings::setKeepGameChatFocus(bool value) void InterfaceSettings::setNotificationsEnabled(bool _notificationsEnabled) { - setValue(_notificationsEnabled, "notificationsenabled"); + setValue(_notificationsEnabled, "enabled", "interface", "notifications"); } void InterfaceSettings::setSpectatorNotificationsEnabled(bool _spectatorNotificationsEnabled) { - setValue(_spectatorNotificationsEnabled, "specnotificationsenabled"); + setValue(_spectatorNotificationsEnabled, "spectatorsEnabled", "interface", "notifications"); } void InterfaceSettings::setBuddyConnectNotificationsEnabled(bool _buddyConnectNotificationsEnabled) { - setValue(_buddyConnectNotificationsEnabled, "buddyconnectnotificationsenabled"); + setValue(_buddyConnectNotificationsEnabled, "buddyConnectEnabled", "interface", "notifications"); } void InterfaceSettings::setDoubleClickToPlay(bool _doubleClickToPlay) { - setValue(_doubleClickToPlay, "doubleclicktoplay"); + setValue(_doubleClickToPlay, "doubleClickToPlay"); } void InterfaceSettings::setClickPlaysAllSelected(bool _clickPlaysAllSelected) @@ -214,7 +214,7 @@ void InterfaceSettings::setClickPlaysAllSelected(bool _clickPlaysAllSelected) void InterfaceSettings::setPlayToStack(bool _playToStack) { - setValue(_playToStack, "playtostack"); + setValue(_playToStack, "playToStack"); } void InterfaceSettings::setDoNotDeleteArrowsInSubPhases(bool _doNotDeleteArrowsInSubPhases) @@ -224,22 +224,22 @@ void InterfaceSettings::setDoNotDeleteArrowsInSubPhases(bool _doNotDeleteArrowsI void InterfaceSettings::setStartingHandSize(int _startingHandSize) { - setValue(_startingHandSize, "startinghandsize"); + setValue(_startingHandSize, "startingHandSize"); } void InterfaceSettings::setAnnotateTokens(bool _annotateTokens) { - setValue(_annotateTokens, "annotatetokens"); + setValue(_annotateTokens, "annotateTokens"); } void InterfaceSettings::setShowDragSelectionCount(bool _showDragSelectionCount) { - setValue(_showDragSelectionCount, "showlassoselectioncount"); + setValue(_showDragSelectionCount, "showLassoSelectionCount"); } void InterfaceSettings::setShowTotalSelectionCount(bool _showTotalSelectionCount) { - setValue(_showTotalSelectionCount, "showpersistentselectioncount"); + setValue(_showTotalSelectionCount, "showPersistentSelectionCount"); } void InterfaceSettings::setTallyType(int value) @@ -259,21 +259,16 @@ void InterfaceSettings::setHorizontalHand(bool _horizontalHand) void InterfaceSettings::setInvertVerticalCoordinate(bool _invertVerticalCoordinate) { - setValue(_invertVerticalCoordinate, "invert_vertical", "table"); + setValue(_invertVerticalCoordinate, "invertVertical", "table"); emit invertVerticalCoordinateChanged(); } void InterfaceSettings::setMinPlayersForMultiColumnLayout(int _minPlayersForMultiColumnLayout) { - setValue(_minPlayersForMultiColumnLayout, "min_players_multicolumn"); + setValue(_minPlayersForMultiColumnLayout, "minPlayersMulticolumn"); emit minPlayersForMultiColumnLayoutChanged(); } -void InterfaceSettings::setOpenDeckInNewTab(bool _openDeckInNewTab) -{ - setValue(_openDeckInNewTab, "openDeckInNewTab", "editor"); -} - void InterfaceSettings::setRewindBufferingMs(int _rewindBufferingMs) { setValue(_rewindBufferingMs, "rewindBufferingMs", "replay"); @@ -284,34 +279,40 @@ void InterfaceSettings::setFastForwardSpeed(qreal _value) setValue(_value, "fastForwardSpeed", "replay"); } -void InterfaceSettings::setStyleUserList(bool _styleUserList) -{ - setValue(_styleUserList, "styleUserList", "appearance"); - emit styleUserListChanged(); -} - void InterfaceSettings::setLeftJustified(bool _leftJustified) { - setValue(_leftJustified, "leftjustified"); + setValue(_leftJustified, "leftJustified"); emit handJustificationChanged(); } void InterfaceSettings::setZoneViewGroupByIndex(int _zoneViewGroupByIndex) { - setValue(_zoneViewGroupByIndex, "groupby", "zoneview"); + setValue(_zoneViewGroupByIndex, "groupBy", "zoneview"); } void InterfaceSettings::setZoneViewSortByIndex(int _zoneViewSortByIndex) { - setValue(_zoneViewSortByIndex, "sortby", "zoneview"); + setValue(_zoneViewSortByIndex, "sortBy", "zoneview"); } void InterfaceSettings::setZoneViewPileView(bool _zoneViewPileView) { - setValue(_zoneViewPileView, "pileview", "zoneview"); + setValue(_zoneViewPileView, "pileView", "zoneview"); } -void InterfaceSettings::setKnownMissingFeatures(const QString &_knownMissingFeatures) +void InterfaceSettings::setShowStatusBar(bool _showStatusBar) { - setValue(_knownMissingFeatures, "knownmissingfeatures"); + setValue(_showStatusBar, "showStatusBar"); + emit showStatusBarChanged(_showStatusBar); +} + +void InterfaceSettings::setShowShortcuts(bool _showShortcuts) +{ + setValue(_showShortcuts, "showShortcuts"); +} + +void InterfaceSettings::setShowGameSelectorFilterToolbar(bool _showGameSelectorFilterToolbar) +{ + setValue(_showGameSelectorFilterToolbar, "showGameSelectorFilterToolbar"); + emit showGameSelectorFilterToolbarChanged(_showGameSelectorFilterToolbar); } diff --git a/libcockatrice_settings/libcockatrice/settings/interface_settings.h b/libcockatrice_settings/libcockatrice/settings/interface_settings.h index 9d3fec496..7ef367cb9 100644 --- a/libcockatrice_settings/libcockatrice/settings/interface_settings.h +++ b/libcockatrice_settings/libcockatrice/settings/interface_settings.h @@ -32,15 +32,15 @@ public: [[nodiscard]] bool getHorizontalHand() const override; [[nodiscard]] bool getInvertVerticalCoordinate() const override; [[nodiscard]] int getMinPlayersForMultiColumnLayout() const override; - [[nodiscard]] bool getOpenDeckInNewTab() const override; [[nodiscard]] int getRewindBufferingMs() const override; [[nodiscard]] qreal getFastForwardSpeed() const override; - [[nodiscard]] bool getStyleUserList() const override; [[nodiscard]] bool getLeftJustified() const override; [[nodiscard]] int getZoneViewGroupByIndex() const override; [[nodiscard]] int getZoneViewSortByIndex() const override; [[nodiscard]] bool getZoneViewPileView() const override; - [[nodiscard]] QString getKnownMissingFeatures() override; + [[nodiscard]] bool getShowStatusBar() const override; + [[nodiscard]] bool getShowShortcuts() const override; + [[nodiscard]] bool getShowGameSelectorFilterToolbar() const override; void setUseTearOffMenus(bool _useTearOffMenus); void setCardViewInitialRowsMax(int _cardViewInitialRowsMax); @@ -63,15 +63,15 @@ public: void setHorizontalHand(bool _horizontalHand); void setInvertVerticalCoordinate(bool _invertVerticalCoordinate); void setMinPlayersForMultiColumnLayout(int _minPlayersForMultiColumnLayout); - void setOpenDeckInNewTab(bool _openDeckInNewTab); void setRewindBufferingMs(int _rewindBufferingMs); void setFastForwardSpeed(qreal _value); - void setStyleUserList(bool _styleUserList); void setLeftJustified(bool _leftJustified); void setZoneViewGroupByIndex(int _zoneViewGroupByIndex); void setZoneViewSortByIndex(int _zoneViewSortByIndex); void setZoneViewPileView(bool _zoneViewPileView); - void setKnownMissingFeatures(const QString &_knownMissingFeatures); + void setShowStatusBar(bool _showStatusBar); + void setShowShortcuts(bool _showShortcuts); + void setShowGameSelectorFilterToolbar(bool _showGameSelectorFilterToolbar); signals: void useTearOffMenusChanged(bool state); @@ -79,12 +79,15 @@ signals: void horizontalHandChanged(); void invertVerticalCoordinateChanged(); void minPlayersForMultiColumnLayoutChanged(); - void styleUserListChanged(); void handJustificationChanged(); void tallyTypeChanged(int type); + void showStatusBarChanged(bool state); + void showGameSelectorFilterToolbarChanged(bool state); + +public: + explicit InterfaceSettings(const QString &settingPath, QObject *parent = nullptr); private: - explicit InterfaceSettings(const QString &settingPath, QObject *parent = nullptr); InterfaceSettings(const InterfaceSettings & /*other*/); }; diff --git a/libcockatrice_settings/libcockatrice/settings/network_settings.cpp b/libcockatrice_settings/libcockatrice/settings/network_settings.cpp new file mode 100644 index 000000000..2d34d47ad --- /dev/null +++ b/libcockatrice_settings/libcockatrice/settings/network_settings.cpp @@ -0,0 +1,56 @@ +#include "network_settings.h" + +NetworkSettings::NetworkSettings(const QString &settingPath, QObject *parent) + : SettingsManager(settingPath + "network.ini", "network", QString(), parent) +{ +} + +QString NetworkSettings::getClientID() const +{ + return getValue("clientId", QString(), QString(), "notset").toString(); +} + +void NetworkSettings::setClientID(const QString &_clientID) +{ + setValue(_clientID, "clientId"); +} + +QString NetworkSettings::getClientVersion() const +{ + return getValue("clientVersion", QString(), QString(), "notset").toString(); +} + +void NetworkSettings::setClientVersion(const QString &_clientVersion) +{ + setValue(_clientVersion, "clientVersion"); +} + +int NetworkSettings::getKeepAlive() const +{ + return getValue("keepAlive", QString(), QString(), 3).toInt(); +} + +void NetworkSettings::setKeepAlive(int _keepAlive) +{ + setValue(_keepAlive, "keepAlive"); +} + +int NetworkSettings::getTimeOut() const +{ + return getValue("timeout", QString(), QString(), 5).toInt(); +} + +void NetworkSettings::setTimeOut(int _timeOut) +{ + setValue(_timeOut, "timeout"); +} + +QString NetworkSettings::getKnownMissingFeatures() const +{ + return getValue("knownMissingFeatures", QString(), QString(), "").toString(); +} + +void NetworkSettings::setKnownMissingFeatures(const QString &_knownMissingFeatures) +{ + setValue(_knownMissingFeatures, "knownMissingFeatures"); +} diff --git a/libcockatrice_settings/libcockatrice/settings/network_settings.h b/libcockatrice_settings/libcockatrice/settings/network_settings.h new file mode 100644 index 000000000..446bb51d3 --- /dev/null +++ b/libcockatrice_settings/libcockatrice/settings/network_settings.h @@ -0,0 +1,36 @@ +/** + * @file network_settings.h + * @ingroup NetworkSettings + */ +//! \todo Document this file. + +#ifndef NETWORK_SETTINGS_H +#define NETWORK_SETTINGS_H + +#include "settings_manager.h" + +class NetworkSettings : public SettingsManager +{ + Q_OBJECT + friend class SettingsCache; + +public: + [[nodiscard]] QString getClientID() const; + void setClientID(const QString &_clientID); + [[nodiscard]] QString getClientVersion() const; + void setClientVersion(const QString &_clientVersion); + [[nodiscard]] int getKeepAlive() const; + void setKeepAlive(int _keepAlive); + [[nodiscard]] int getTimeOut() const; + void setTimeOut(int _timeOut); + [[nodiscard]] QString getKnownMissingFeatures() const; + void setKnownMissingFeatures(const QString &_knownMissingFeatures); + +public: + explicit NetworkSettings(const QString &settingPath, QObject *parent = nullptr); + +private: + NetworkSettings(const NetworkSettings & /*other*/); +}; + +#endif // NETWORK_SETTINGS_H diff --git a/libcockatrice_settings/libcockatrice/settings/paths_settings.cpp b/libcockatrice_settings/libcockatrice/settings/paths_settings.cpp index bf155414d..67032e38f 100644 --- a/libcockatrice_settings/libcockatrice/settings/paths_settings.cpp +++ b/libcockatrice_settings/libcockatrice/settings/paths_settings.cpp @@ -30,7 +30,7 @@ QString PathsSettings::getPicsPath() const QString PathsSettings::getCustomPicsPath() const { - return getValue("custompics").toString(); + return getValue("customPics").toString(); } QString PathsSettings::getThemesPath() const @@ -40,22 +40,22 @@ QString PathsSettings::getThemesPath() const QString PathsSettings::getCardDatabasePath() const { - return getValue("carddatabase").toString(); + return getValue("cardDatabase").toString(); } QString PathsSettings::getCustomCardDatabasePath() const { - return getValue("customsets").toString(); + return getValue("customSets").toString(); } QString PathsSettings::getTokenDatabasePath() const { - return getValue("tokendatabase").toString(); + return getValue("tokenDatabase").toString(); } QString PathsSettings::getSpoilerCardDatabasePath() const { - return getValue("spoilerdatabase").toString(); + return getValue("spoilerDatabase").toString(); } QString PathsSettings::getRedirectCachePath() const @@ -86,7 +86,7 @@ void PathsSettings::setPicsPath(const QString &_picsPath) void PathsSettings::setCustomPicsPath(const QString &_customPicsPath) { - setValue(_customPicsPath, "custompics"); + setValue(_customPicsPath, "customPics"); } void PathsSettings::setThemesPath(const QString &_themesPath) @@ -97,24 +97,24 @@ void PathsSettings::setThemesPath(const QString &_themesPath) void PathsSettings::setCardDatabasePath(const QString &_cardDatabasePath) { - setValue(_cardDatabasePath, "carddatabase"); + setValue(_cardDatabasePath, "cardDatabase"); emit cardDatabasePathChanged(); } void PathsSettings::setCustomCardDatabasePath(const QString &_customCardDatabasePath) { - setValue(_customCardDatabasePath, "customsets"); + setValue(_customCardDatabasePath, "customSets"); emit cardDatabasePathChanged(); } void PathsSettings::setTokenDatabasePath(const QString &_tokenDatabasePath) { - setValue(_tokenDatabasePath, "tokendatabase"); + setValue(_tokenDatabasePath, "tokenDatabase"); emit cardDatabasePathChanged(); } void PathsSettings::setSpoilerDatabasePath(const QString &_spoilerDatabasePath) { - setValue(_spoilerDatabasePath, "spoilerdatabase"); + setValue(_spoilerDatabasePath, "spoilerDatabase"); emit cardDatabasePathChanged(); } diff --git a/libcockatrice_settings/libcockatrice/settings/personal_settings.cpp b/libcockatrice_settings/libcockatrice/settings/personal_settings.cpp index aec8d4df6..d1cb74d09 100644 --- a/libcockatrice_settings/libcockatrice/settings/personal_settings.cpp +++ b/libcockatrice_settings/libcockatrice/settings/personal_settings.cpp @@ -10,61 +10,6 @@ QString PersonalSettings::getLang() const return getValue("lang", QString(), QString(), QString()).toString(); } -QString PersonalSettings::getClientID() -{ - return getValue("clientid", QString(), QString(), "notset").toString(); -} - -QString PersonalSettings::getClientVersion() -{ - return getValue("clientversion", QString(), QString(), "notset").toString(); -} - -int PersonalSettings::getKeepAlive() const -{ - return getValue("keepalive", QString(), QString(), 3).toInt(); -} - -int PersonalSettings::getTimeOut() const -{ - return getValue("timeout", QString(), QString(), 5).toInt(); -} - -bool PersonalSettings::getPicDownload() const -{ - return getValue("picturedownload", QString(), QString(), true).toBool(); -} - -bool PersonalSettings::getShowStatusBar() const -{ - return getValue("showStatusBar", QString(), QString(), false).toBool(); -} - -int PersonalSettings::getMaxFontSize() const -{ - return getValue("maxfontsize", "game", QString(), 12).toInt(); -} - -QString PersonalSettings::getHighlightWords() const -{ - return getValue("highlightWords", QString(), QString(), "").toString(); -} - -QString PersonalSettings::getHomeTabBackgroundSource() const -{ - return getValue("background", "home", QString(), "themed").toString(); -} - -int PersonalSettings::getHomeTabBackgroundShuffleFrequency() const -{ - return getValue("shuffleTimer", "home/background", QString(), 0).toInt(); -} - -bool PersonalSettings::getHomeTabDisplayCardName() const -{ - return getValue("displayCardName", "home/background", QString(), true).toBool(); -} - bool PersonalSettings::getShowTipsOnStartup() const { return getValue("showTips", "tipOfDay", QString(), true).toBool(); @@ -80,78 +25,12 @@ QList PersonalSettings::getSeenTips() const return tips; } -bool PersonalSettings::getDownloadSpoilersStatus() const -{ - return getValue("downloadspoilers", QString(), QString(), false).toBool(); -} - void PersonalSettings::setLang(const QString &_lang) { setValue(_lang, "lang"); emit langChanged(); } -void PersonalSettings::setClientID(const QString &_clientID) -{ - setValue(_clientID, "clientid"); -} - -void PersonalSettings::setClientVersion(const QString &_clientVersion) -{ - setValue(_clientVersion, "clientversion"); -} - -void PersonalSettings::setPicDownload(bool _picDownload) -{ - setValue(_picDownload, "picturedownload"); - emit picDownloadChanged(); -} - -void PersonalSettings::setShowStatusBar(bool value) -{ - setValue(value, "showStatusBar"); - emit showStatusBarChanged(value); -} - -void PersonalSettings::setMaxFontSize(int _max) -{ - setValue(_max, "maxfontsize", "game"); -} - -QString PersonalSettings::getThemeName() const -{ - return getValue("themeName", QString(), QString()).toString(); -} - -void PersonalSettings::setThemeName(const QString &_themeName) -{ - setValue(_themeName, "themeName"); - emit themeNameChanged(); -} - -void PersonalSettings::setHighlightWords(const QString &_highlightWords) -{ - setValue(_highlightWords, "highlightWords"); -} - -void PersonalSettings::setHomeTabBackgroundSource(const QString &_backgroundSource) -{ - setValue(_backgroundSource, "background", "home"); - emit homeTabBackgroundSourceChanged(); -} - -void PersonalSettings::setHomeTabBackgroundShuffleFrequency(int _frequency) -{ - setValue(_frequency, "shuffleTimer", "home/background"); - emit homeTabBackgroundShuffleFrequencyChanged(); -} - -void PersonalSettings::setHomeTabDisplayCardName(bool _displayCardName) -{ - setValue(_displayCardName, "displayCardName", "home/background"); - emit homeTabDisplayCardNameChanged(); -} - void PersonalSettings::setShowTipsOnStartup(bool _showTipsOnStartup) { setValue(_showTipsOnStartup, "showTips", "tipOfDay"); @@ -165,9 +44,3 @@ void PersonalSettings::setSeenTips(const QList &_seenTips) } setValue(QVariant::fromValue(storedTipList), "seenTips", "tipOfDay"); } - -void PersonalSettings::setDownloadSpoilerStatus(bool _spoilerStatus) -{ - setValue(_spoilerStatus, "downloadspoilers"); - emit downloadSpoilerStatusChanged(); -} diff --git a/libcockatrice_settings/libcockatrice/settings/personal_settings.h b/libcockatrice_settings/libcockatrice/settings/personal_settings.h index 2c79eec8f..04eab2a8a 100644 --- a/libcockatrice_settings/libcockatrice/settings/personal_settings.h +++ b/libcockatrice_settings/libcockatrice/settings/personal_settings.h @@ -14,46 +14,15 @@ class PersonalSettings : public SettingsManager, public IPersonalSettingsProvide public: [[nodiscard]] QString getLang() const override; - [[nodiscard]] QString getClientID() override; - [[nodiscard]] QString getClientVersion() override; - [[nodiscard]] int getKeepAlive() const override; - [[nodiscard]] int getTimeOut() const override; - [[nodiscard]] bool getPicDownload() const override; - [[nodiscard]] bool getShowStatusBar() const override; - [[nodiscard]] int getMaxFontSize() const override; - [[nodiscard]] QString getHighlightWords() const override; - [[nodiscard]] QString getHomeTabBackgroundSource() const override; - [[nodiscard]] int getHomeTabBackgroundShuffleFrequency() const override; - [[nodiscard]] QString getThemeName() const; - void setThemeName(const QString &_themeName); - [[nodiscard]] bool getHomeTabDisplayCardName() const override; [[nodiscard]] bool getShowTipsOnStartup() const override; [[nodiscard]] QList getSeenTips() const override; - [[nodiscard]] bool getDownloadSpoilersStatus() const override; void setLang(const QString &_lang); - void setClientID(const QString &_clientID); - void setClientVersion(const QString &_clientVersion); - void setPicDownload(bool _picDownload); - void setShowStatusBar(bool value); - void setMaxFontSize(int _max); - void setHighlightWords(const QString &_highlightWords); - void setHomeTabBackgroundSource(const QString &_backgroundSource); - void setHomeTabBackgroundShuffleFrequency(int _frequency); - void setHomeTabDisplayCardName(bool _displayCardName); void setShowTipsOnStartup(bool _showTipsOnStartup); void setSeenTips(const QList &_seenTips); - void setDownloadSpoilerStatus(bool _spoilerStatus); signals: void langChanged(); - void themeNameChanged(); - void picDownloadChanged(); - void showStatusBarChanged(bool state); - void homeTabBackgroundSourceChanged(); - void homeTabBackgroundShuffleFrequencyChanged(); - void homeTabDisplayCardNameChanged(); - void downloadSpoilerStatusChanged(); public: explicit PersonalSettings(const QString &settingPath, QObject *parent = nullptr); diff --git a/libcockatrice_settings/libcockatrice/settings/recents_settings.cpp b/libcockatrice_settings/libcockatrice/settings/recents_settings.cpp index 76bc4069e..5f16af179 100644 --- a/libcockatrice_settings/libcockatrice/settings/recents_settings.cpp +++ b/libcockatrice_settings/libcockatrice/settings/recents_settings.cpp @@ -9,16 +9,16 @@ RecentsSettings::RecentsSettings(const QString &settingPath, QObject *parent) QStringList RecentsSettings::getRecentlyOpenedDeckPaths() const { - return getValue("deckpaths").toStringList(); + return getValue("deckPaths").toStringList(); } void RecentsSettings::clearRecentlyOpenedDeckPaths() { - deleteValue("deckpaths"); + deleteValue("deckPaths"); emit recentlyOpenedDeckPathsChanged(); } void RecentsSettings::updateRecentlyOpenedDeckPaths(const QString &deckPath) { - auto deckPaths = getValue("deckpaths").toStringList(); + auto deckPaths = getValue("deckPaths").toStringList(); deckPaths.removeAll(deckPath); deckPaths.prepend(deckPath); @@ -27,7 +27,7 @@ void RecentsSettings::updateRecentlyOpenedDeckPaths(const QString &deckPath) deckPaths.removeLast(); } - setValue(deckPaths, "deckpaths"); + setValue(deckPaths, "deckPaths"); emit recentlyOpenedDeckPathsChanged(); } diff --git a/libcockatrice_settings/libcockatrice/settings/servers_settings.cpp b/libcockatrice_settings/libcockatrice/settings/servers_settings.cpp index 5c271328b..811b0c842 100644 --- a/libcockatrice_settings/libcockatrice/settings/servers_settings.cpp +++ b/libcockatrice_settings/libcockatrice/settings/servers_settings.cpp @@ -10,28 +10,28 @@ ServersSettings::ServersSettings(const QString &settingPath, QObject *parent) void ServersSettings::setPreviousHostLogin(int previous) { - setValue(previous, "previoushostlogin"); + setValue(previous, "previousHostLogin"); } int ServersSettings::getPreviousHostLogin() const { - QVariant previous = getValue("previoushostlogin"); + QVariant previous = getValue("previousHostLogin"); return previous == QVariant() ? 1 : previous.toInt(); } void ServersSettings::setPreviousHostList(QStringList list) { - setValue(list, "previoushosts"); + setValue(list, "previousHosts"); } QStringList ServersSettings::getPreviousHostList() const { - return getValue("previoushosts").toStringList(); + return getValue("previousHosts").toStringList(); } void ServersSettings::setPrevioushostName(const QString &name) { - setValue(name, "previoushostName"); + setValue(name, "previousHostName"); } QString ServersSettings::getSaveName(QString defaultname) @@ -50,7 +50,7 @@ QString ServersSettings::getSite(QString defaultSite) QString ServersSettings::getPrevioushostName() const { - QVariant value = getValue("previoushostName"); + QVariant value = getValue("previousHostName"); return value == QVariant() ? "Rooster Ranges" : value.toString(); } @@ -110,56 +110,56 @@ bool ServersSettings::getSavePassword() const void ServersSettings::setAutoConnect(int autoconnect) { - setValue(autoconnect, "auto_connect"); + setValue(autoconnect, "autoConnect"); } int ServersSettings::getAutoConnect() const { - QVariant autoconnect = getValue("auto_connect"); + QVariant autoconnect = getValue("autoConnect"); return autoconnect == QVariant() ? 0 : autoconnect.toInt(); } void ServersSettings::setFPHostName(QString hostname) { - setValue(hostname, "fphostname"); + setValue(hostname, "fpHostName"); } QString ServersSettings::getFPHostname(QString defaultHost) const { - QVariant hostname = getValue("fphostname"); + QVariant hostname = getValue("fpHostName"); return hostname == QVariant() ? std::move(defaultHost) : hostname.toString(); } void ServersSettings::setFPPort(QString port) { - setValue(port, "fpport"); + setValue(port, "fpPort"); } QString ServersSettings::getFPPort(QString defaultPort) const { - QVariant port = getValue("fpport"); + QVariant port = getValue("fpPort"); return port == QVariant() ? std::move(defaultPort) : port.toString(); } void ServersSettings::setFPPlayerName(QString playerName) { - setValue(playerName, "fpplayername"); + setValue(playerName, "fpPlayerName"); } QString ServersSettings::getFPPlayerName(QString defaultName) const { - QVariant name = getValue("fpplayername"); + QVariant name = getValue("fpPlayerName"); return name == QVariant() ? std::move(defaultName) : name.toString(); } void ServersSettings::setClearDebugLogStatus(bool abIsChecked) { - setValue(abIsChecked, "save_debug_log"); + setValue(abIsChecked, "saveDebugLog"); } bool ServersSettings::getClearDebugLogStatus(bool abDefaultValue) const { - QVariant cbFlushLog = getValue("save_debug_log"); + QVariant cbFlushLog = getValue("saveDebugLog"); return cbFlushLog == QVariant() ? abDefaultValue : cbFlushLog.toBool(); } diff --git a/libcockatrice_settings/libcockatrice/settings/settings_migration.cpp b/libcockatrice_settings/libcockatrice/settings/settings_migration.cpp index 078e5ac16..37dc9a0a0 100644 --- a/libcockatrice_settings/libcockatrice/settings/settings_migration.cpp +++ b/libcockatrice_settings/libcockatrice/settings/settings_migration.cpp @@ -35,66 +35,100 @@ static void migrateSoundSettings(const QString &settingsPath, QSettings &globalI QSettings soundIni(settingsPath + "sound.ini", QSettings::IniFormat); soundIni.setValue("sound/enabled", globalIni.value("sound/enabled", false)); soundIni.setValue("sound/theme", globalIni.value("sound/theme")); - soundIni.setValue("sound/mastervolume", globalIni.value("sound/mastervolume", 100)); + soundIni.setValue("sound/masterVolume", globalIni.value("sound/mastervolume", 100)); } static void migrateGameSettings(const QString &settingsPath, QSettings &globalIni) { - bool hasGameKeys = false; - globalIni.beginGroup("game"); - if (!globalIni.childKeys().isEmpty()) { - hasGameKeys = true; - } - QStringList gameKeys = globalIni.childKeys(); - globalIni.endGroup(); + const QMap gameKeyMap = { + {"game/maxplayers", "game/maxPlayers"}, + {"game/gamedescription", "game/gameDescription"}, + {"game/gametypes", "game/gameTypes"}, + {"game/onlybuddies", "game/onlyBuddies"}, + {"game/onlyregistered", "game/onlyRegistered"}, + {"game/spectatorsallowed", "game/spectatorsAllowed"}, + {"game/spectatorsneedpassword", "game/spectatorsNeedPassword"}, + {"game/spectatorscantalk", "game/spectatorsCanTalk"}, + {"game/spectatorscanseeeverything", "game/spectatorsCanSeeEverything"}, + {"game/creategameasspectator", "game/createGameAsSpectator"}, + {"game/defaultstartinglifetotal", "game/defaultStartingLifeTotal"}, + {"game/sharedecklistsonload", "game/shareDecklistsOnLoad"}, + {"game/remembergamesettings", "game/rememberGameSettings"}, + {"localgameoptions/maxplayers", "localgameoptions/maxPlayers"}, + {"localgameoptions/startinglifetotal", "localgameoptions/startingLifeTotal"}, + {"localgameoptions/remembersettings", "localgameoptions/rememberSettings"}, + }; - globalIni.beginGroup("localgameoptions"); - if (!globalIni.childKeys().isEmpty()) { - hasGameKeys = true; + bool hasAny = false; + for (auto it = gameKeyMap.constBegin(); it != gameKeyMap.constEnd(); ++it) { + if (globalIni.contains(it.key())) { + hasAny = true; + break; + } } - QStringList localGameKeys = globalIni.childKeys(); - globalIni.endGroup(); - - if (!hasGameKeys) { + if (!hasAny) { return; } QSettings gameIni(settingsPath + "game.ini", QSettings::IniFormat); - for (const auto &key : gameKeys) { - if (key == "maxfontsize") { - continue; + for (auto it = gameKeyMap.constBegin(); it != gameKeyMap.constEnd(); ++it) { + if (globalIni.contains(it.key())) { + gameIni.setValue(it.value(), globalIni.value(it.key())); } - gameIni.setValue("game/" + key, globalIni.value("game/" + key)); - } - for (const auto &key : localGameKeys) { - gameIni.setValue("localgameoptions/" + key, globalIni.value("localgameoptions/" + key)); } } static void migrateChatSettings(const QString &settingsPath, QSettings &globalIni) { - globalIni.beginGroup("chat"); - QStringList chatKeys = globalIni.childKeys(); - globalIni.endGroup(); - - if (chatKeys.isEmpty()) { + const QMap chatKeyMap = { + {"chat/mention", "chat/mention"}, + {"chat/mentioncompleter", "chat/mentionCompleter"}, + {"chat/mentioncolor", "chat/mentionColor"}, + {"chat/highlightcolor", "chat/highlightColor"}, + {"chat/mentionforeground", "chat/mentionForeground"}, + {"chat/highlightforeground", "chat/highlightForeground"}, + {"chat/ignore_unregistered", "chat/ignoreUnregistered"}, + {"chat/ignore_unregistered_messages", "chat/ignoreUnregisteredMessages"}, + {"chat/ignore_nonbuddy_messages", "chat/ignoreNonBuddyMessages"}, + {"chat/showmessagepopups", "chat/showMessagePopups"}, + {"chat/showmentionpopups", "chat/showMentionPopups"}, + {"chat/roomhistory", "chat/roomHistory"}, + {"chat/highlightwords", "chat/highlightWords"}, + // Legacy highlight words lived under [personal], but the chat settings + // class reads them from [chat] + {"personal/highlightWords", "chat/highlightWords"}, + }; + bool hasAny = false; + for (auto it = chatKeyMap.constBegin(); it != chatKeyMap.constEnd(); ++it) { + if (globalIni.contains(it.key())) { + hasAny = true; + break; + } + } + if (!hasAny) { return; } QSettings chatIni(settingsPath + "chat.ini", QSettings::IniFormat); - for (const auto &key : chatKeys) { - chatIni.setValue("chat/" + key, globalIni.value("chat/" + key)); + for (auto it = chatKeyMap.constBegin(); it != chatKeyMap.constEnd(); ++it) { + if (globalIni.contains(it.key())) { + chatIni.setValue(it.value(), globalIni.value(it.key())); + } } } static void migrateCacheStorageSettings(const QString &settingsPath, QSettings &globalIni) { - const QStringList cacheKeys = {"personal/pixmapCacheSize", "personal/networkCacheSize", "personal/redirectCacheTtl", - "personal/cardPictureLoaderCacheMethod", - "personal/localCardImageStorageNamingScheme"}; + const QMap cacheStorageKeyMap = { + {"personal/pixmapCacheSize", "cache_storage/pixmapCacheSize"}, + {"personal/networkCacheSize", "cache_storage/networkCacheSize"}, + {"personal/redirectCacheTtl", "cache_storage/redirectCacheTtl"}, + {"personal/cardPictureLoaderCacheMethod", "cache_storage/cardPictureLoaderCacheMethod"}, + {"personal/localCardImageStorageNamingScheme", "cache_storage/localCardImageStorageNamingScheme"}, + }; bool hasAny = false; - for (const auto &key : cacheKeys) { - if (globalIni.contains(key)) { + for (auto it = cacheStorageKeyMap.constBegin(); it != cacheStorageKeyMap.constEnd(); ++it) { + if (globalIni.contains(it.key())) { hasAny = true; break; } @@ -104,9 +138,9 @@ static void migrateCacheStorageSettings(const QString &settingsPath, QSettings & } QSettings cacheStorageIni(settingsPath + "cache_storage.ini", QSettings::IniFormat); - for (const auto &key : cacheKeys) { - if (globalIni.contains(key)) { - cacheStorageIni.setValue(key, globalIni.value(key)); + for (auto it = cacheStorageKeyMap.constBegin(); it != cacheStorageKeyMap.constEnd(); ++it) { + if (globalIni.contains(it.key())) { + cacheStorageIni.setValue(it.value(), globalIni.value(it.key())); } } } @@ -120,9 +154,9 @@ static void migrateUpdatesSettings(const QString &settingsPath, QSettings &globa {"personal/cardUpdateCheckInterval", "updates/cardUpdateCheckInterval"}, {"personal/lastCardUpdateCheck", "updates/lastCardUpdateCheck"}, {"personal/alwaysEnableNewSets", "updates/alwaysEnableNewSets"}, - {"personal/updatenotification", "updates/updatenotification"}, - {"personal/newversionnotification", "updates/newversionnotification"}, - {"personal/updatereleasechannel", "updates/updatereleasechannel"}, + {"personal/updatenotification", "updates/updateNotification"}, + {"personal/newversionnotification", "updates/newVersionNotification"}, + {"personal/updatereleasechannel", "updates/updateReleaseChannel"}, }; bool hasAny = false; for (auto it = updateKeyMap.constBegin(); it != updateKeyMap.constEnd(); ++it) { @@ -145,19 +179,7 @@ static void migrateUpdatesSettings(const QString &settingsPath, QSettings &globa static void migratePersonalSettings(const QString &settingsPath, QSettings &globalIni) { - const QStringList personalRootKeys = {"personal/lang", "personal/highlightWords"}; - const QMap personalKeyMap = { - {"theme/name", "personal/themeName"}, - {"personal/clientid", "personal/clientid"}, - {"personal/clientversion", "personal/clientversion"}, - {"personal/keepalive", "personal/keepalive"}, - {"personal/timeout", "personal/timeout"}, - {"personal/picturedownload", "personal/picturedownload"}, - {"personal/showStatusBar", "personal/showStatusBar"}, - {"game/maxfontsize", "game/maxfontsize"}, - {"personal/downloadspoilers", "personal/downloadspoilers"}, - }; - const QStringList homeKeys = {"home/background", "home/background/shuffleTimer", "home/background/displayCardName"}; + const QStringList personalRootKeys = {"personal/lang"}; const QStringList tipKeys = {"tipOfDay/showTips", "tipOfDay/seenTips"}; bool hasAny = false; @@ -166,16 +188,6 @@ static void migratePersonalSettings(const QString &settingsPath, QSettings &glob hasAny = true; } } - for (auto it = personalKeyMap.constBegin(); it != personalKeyMap.constEnd(); ++it) { - if (globalIni.contains(it.key())) { - hasAny = true; - } - } - for (const auto &key : homeKeys) { - if (globalIni.contains(key)) { - hasAny = true; - } - } for (const auto &key : tipKeys) { if (globalIni.contains(key)) { hasAny = true; @@ -191,16 +203,6 @@ static void migratePersonalSettings(const QString &settingsPath, QSettings &glob personalIni.setValue(key, globalIni.value(key)); } } - for (auto it = personalKeyMap.constBegin(); it != personalKeyMap.constEnd(); ++it) { - if (globalIni.contains(it.key())) { - personalIni.setValue(it.value(), globalIni.value(it.key())); - } - } - for (const auto &key : homeKeys) { - if (globalIni.contains(key)) { - personalIni.setValue(key, globalIni.value(key)); - } - } for (const auto &key : tipKeys) { if (globalIni.contains(key)) { personalIni.setValue(key, globalIni.value(key)); @@ -210,39 +212,33 @@ static void migratePersonalSettings(const QString &settingsPath, QSettings &glob static void migrateCardsDisplaySettings(const QString &settingsPath, QSettings &globalIni) { - const QStringList cardsRootKeys = { - "cards/displaycardnames", - "cards/roundcardcorners", - "cards/overrideallcardartwithpersonalpreference", - "cards/bumpsetswithcardsindecktotop", - "cards/printingselectorsortorder", - "cards/printingselectorcardsize", - "cards/includerebalancedcards", - "cards/printingselectornavigationbuttonsvisible", - "cards/tapanimation", - "cards/autorotatesidewayslayoutcards", - "cards/scaleCards", - "cards/verticalCardOverlapPercent", - "cards/cardinfoviewmode", + const QMap cardsKeyMap = { + {"cards/displaycardnames", "cards/displayCardNames"}, + {"cards/roundcardcorners", "cards/roundCardCorners"}, + {"cards/overrideallcardartwithpersonalpreference", "cards/overrideAllCardArtWithPersonalPreference"}, + {"cards/bumpsetswithcardsindecktotop", "cards/bumpSetsWithCardsInDeckToTop"}, + {"cards/includerebalancedcards", "cards/includerebalancedcards"}, + {"cards/tapanimation", "cards/tapAnimation"}, + {"cards/autorotatesidewayslayoutcards", "cards/autoRotateSidewaysLayoutCards"}, + {"cards/scaleCards", "cards/scaleCards"}, + {"cards/verticalCardOverlapPercent", "cards/verticalCardOverlapPercent"}, + {"cards/cardinfoviewmode", "cards/cardInfoViewMode"}, + {"cards/printingselectorsortorder", "cards/printingSelector/sortOrder"}, + {"cards/printingselectornavigationbuttonsvisible", "cards/printingSelector/navigationButtonsVisible"}, + {"cards/printingselectorcardsize", "cards/cardSize/printingSelector"}, + {"interface/visualdeckstoragecardsize", "cards/cardSize/visualDeckStorage"}, + {"interface/visualdatabasedisplaycardsize", "cards/cardSize/visualDatabaseDisplay"}, + {"interface/visualdeckeditorcardsize", "cards/cardSize/visualDeckEditor"}, + {"interface/edhreccardsize", "cards/cardSize/edhrec"}, + {"interface/archidektpreviewsize", "cards/cardSize/archidektPreview"}, + {"interface/visualdeckeditorsamplehandsize", "cards/cardSize/sampleHandSize"}, }; - const QStringList cardsInterfaceKeys = {"interface/deckeditorbannercardcomboboxvisible", - "interface/deckeditortagswidgetvisible"}; - const QStringList menuKeys = {"menu/showshortcuts", "menu/showgameselectorfiltertoolbar"}; bool hasAny = false; - for (const auto &key : cardsRootKeys) { - if (globalIni.contains(key)) { - hasAny = true; - } - } - for (const auto &key : cardsInterfaceKeys) { - if (globalIni.contains(key)) { - hasAny = true; - } - } - for (const auto &key : menuKeys) { - if (globalIni.contains(key)) { + for (auto it = cardsKeyMap.constBegin(); it != cardsKeyMap.constEnd(); ++it) { + if (globalIni.contains(it.key())) { hasAny = true; + break; } } if (!hasAny) { @@ -250,60 +246,71 @@ static void migrateCardsDisplaySettings(const QString &settingsPath, QSettings & } QSettings cardsIni(settingsPath + "cards_display.ini", QSettings::IniFormat); - for (const auto &key : cardsRootKeys) { - if (globalIni.contains(key)) { - cardsIni.setValue(key, globalIni.value(key)); + for (auto it = cardsKeyMap.constBegin(); it != cardsKeyMap.constEnd(); ++it) { + if (globalIni.contains(it.key())) { + cardsIni.setValue(it.value(), globalIni.value(it.key())); } } - for (const auto &key : cardsInterfaceKeys) { - if (globalIni.contains(key)) { - cardsIni.setValue(key, globalIni.value(key)); +} + +static void migrateCardCounterSettings(const QString &settingsPath, QSettings &globalIni) +{ + QStringList counterKeys; + const QStringList allKeys = globalIni.allKeys(); + for (const auto &key : allKeys) { + if (key.startsWith("cards/counters/")) { + counterKeys.append(key); } } - for (const auto &key : menuKeys) { - if (globalIni.contains(key)) { - cardsIni.setValue(key, globalIni.value(key)); - } + if (counterKeys.isEmpty()) { + return; + } + + QSettings countersIni(settingsPath + "card_counters.ini", QSettings::IniFormat); + for (const auto &key : counterKeys) { + countersIni.setValue(key, globalIni.value(key)); } } static void migrateInterfaceSettings(const QString &settingsPath, QSettings &globalIni) { - const QStringList interfaceRootKeys = { - "interface/usetearoffmenus", - "interface/cardViewInitialRowsMax", - "interface/cardViewExpandedRowsMax", - "interface/closeEmptyCardView", - "interface/focusCardViewSearchBar", - "interface/keepGameChatFocus", - "interface/notificationsenabled", - "interface/specnotificationsenabled", - "interface/buddyconnectnotificationsenabled", - "interface/doubleclicktoplay", - "interface/clickPlaysAllSelected", - "interface/playtostack", - "interface/doNotDeleteArrowsInSubPhases", - "interface/startinghandsize", - "interface/annotatetokens", - "interface/showlassoselectioncount", - "interface/showpersistentselectioncount", - "interface/showsubtypeselectiontally", - "interface/leftjustified", - "interface/min_players_multicolumn", - "interface/knownmissingfeatures", + const QMap interfaceKeyMap = { + {"interface/usetearoffmenus", "interface/useTearOffMenus"}, + {"interface/cardViewInitialRowsMax", "interface/cardViewInitialRowsMax"}, + {"interface/cardViewExpandedRowsMax", "interface/cardViewExpandedRowsMax"}, + {"interface/closeEmptyCardView", "interface/closeEmptyCardView"}, + {"interface/focusCardViewSearchBar", "interface/focusCardViewSearchBar"}, + {"interface/keepGameChatFocus", "interface/keepGameChatFocus"}, + {"interface/doubleclicktoplay", "interface/doubleClickToPlay"}, + {"interface/clickPlaysAllSelected", "interface/clickPlaysAllSelected"}, + {"interface/playtostack", "interface/playToStack"}, + {"interface/doNotDeleteArrowsInSubPhases", "interface/doNotDeleteArrowsInSubPhases"}, + {"interface/startinghandsize", "interface/startingHandSize"}, + {"interface/annotatetokens", "interface/annotateTokens"}, + {"interface/showlassoselectioncount", "interface/showLassoSelectionCount"}, + {"interface/showpersistentselectioncount", "interface/showPersistentSelectionCount"}, + {"interface/tallyType", "interface/tallyType"}, + {"interface/leftjustified", "interface/leftJustified"}, + {"interface/min_players_multicolumn", "interface/minPlayersMulticolumn"}, + {"hand/horizontal", "hand/horizontal"}, + {"table/invert_vertical", "table/invertVertical"}, + {"replay/rewindBufferingMs", "replay/rewindBufferingMs"}, + {"replay/fastForwardSpeed", "replay/fastForwardSpeed"}, + {"zoneview/groupby", "zoneview/groupBy"}, + {"zoneview/sortby", "zoneview/sortBy"}, + {"zoneview/pileview", "zoneview/pileView"}, + {"personal/showStatusBar", "interface/showStatusBar"}, + {"menu/showshortcuts", "interface/showShortcuts"}, + {"menu/showgameselectorfiltertoolbar", "interface/showGameSelectorFilterToolbar"}, + {"interface/notificationsenabled", "interface/notifications/enabled"}, + {"interface/specnotificationsenabled", "interface/notifications/spectatorsEnabled"}, + {"interface/buddyconnectnotificationsenabled", "interface/notifications/buddyConnectEnabled"}, }; - const QStringList interfaceSubKeys = { - "hand/horizontal", "table/invert_vertical", "editor/openDeckInNewTab", "replay/rewindBufferingMs", - "appearance/styleUserList", "zoneview/groupby", "zoneview/sortby", "zoneview/pileview"}; bool hasAny = false; - for (const auto &key : interfaceRootKeys) { - if (globalIni.contains(key)) { - hasAny = true; - } - } - for (const auto &key : interfaceSubKeys) { - if (globalIni.contains(key)) { + for (auto it = interfaceKeyMap.constBegin(); it != interfaceKeyMap.constEnd(); ++it) { + if (globalIni.contains(it.key())) { hasAny = true; + break; } } if (!hasAny) { @@ -311,63 +318,159 @@ static void migrateInterfaceSettings(const QString &settingsPath, QSettings &glo } QSettings interfaceIni(settingsPath + "interface.ini", QSettings::IniFormat); - for (const auto &key : interfaceRootKeys) { - if (globalIni.contains(key)) { - interfaceIni.setValue(key, globalIni.value(key)); + for (auto it = interfaceKeyMap.constBegin(); it != interfaceKeyMap.constEnd(); ++it) { + if (globalIni.contains(it.key())) { + interfaceIni.setValue(it.value(), globalIni.value(it.key())); } } - for (const auto &key : interfaceSubKeys) { - if (globalIni.contains(key)) { - interfaceIni.setValue(key, globalIni.value(key)); +} + +static void migrateDownloadSettings(const QString &settingsPath, QSettings &globalIni) +{ + const QMap downloadKeyMap = { + {"personal/picturedownload", "downloads/pictureDownload"}, + {"personal/downloadspoilers", "downloads/downloadSpoilers"}, + }; + bool hasAny = false; + for (auto it = downloadKeyMap.constBegin(); it != downloadKeyMap.constEnd(); ++it) { + if (globalIni.contains(it.key())) { + hasAny = true; + break; + } + } + if (!hasAny) { + return; + } + + QSettings downloadsIni(settingsPath + "downloads.ini", QSettings::IniFormat); + for (auto it = downloadKeyMap.constBegin(); it != downloadKeyMap.constEnd(); ++it) { + if (globalIni.contains(it.key())) { + downloadsIni.setValue(it.value(), globalIni.value(it.key())); + } + } +} + +static void migrateAppearanceSettings(const QString &settingsPath, QSettings &globalIni) +{ + const QMap appearanceKeyMap = { + {"theme/name", "appearance/themeName"}, + {"game/maxfontsize", "appearance/maxFontSize"}, + {"home/background", "appearance/homeTabBackgroundSource"}, + {"home/background/shuffleTimer", "appearance/homeTabBackgroundShuffleFrequency"}, + {"home/background/displayCardName", "appearance/homeTabDisplayCardName"}, + {"appearance/styleUserList", "appearance/styleUserList"}, + }; + bool hasAny = false; + for (auto it = appearanceKeyMap.constBegin(); it != appearanceKeyMap.constEnd(); ++it) { + if (globalIni.contains(it.key())) { + hasAny = true; + break; + } + } + if (!hasAny) { + return; + } + + QSettings appearanceIni(settingsPath + "appearance.ini", QSettings::IniFormat); + for (auto it = appearanceKeyMap.constBegin(); it != appearanceKeyMap.constEnd(); ++it) { + if (globalIni.contains(it.key())) { + appearanceIni.setValue(it.value(), globalIni.value(it.key())); + } + } +} + +static void migrateNetworkSettings(const QString &settingsPath, QSettings &globalIni) +{ + const QMap networkKeyMap = { + {"personal/clientid", "network/clientId"}, + {"personal/clientversion", "network/clientVersion"}, + {"personal/keepalive", "network/keepAlive"}, + {"personal/timeout", "network/timeout"}, + {"interface/knownmissingfeatures", "network/knownMissingFeatures"}, + }; + bool hasAny = false; + for (auto it = networkKeyMap.constBegin(); it != networkKeyMap.constEnd(); ++it) { + if (globalIni.contains(it.key())) { + hasAny = true; + break; + } + } + if (!hasAny) { + return; + } + + QSettings networkIni(settingsPath + "network.ini", QSettings::IniFormat); + for (auto it = networkKeyMap.constBegin(); it != networkKeyMap.constEnd(); ++it) { + if (globalIni.contains(it.key())) { + networkIni.setValue(it.value(), globalIni.value(it.key())); } } } static void migratePathsSettings(const QString &settingsPath, QSettings &globalIni) { - globalIni.beginGroup("paths"); - QStringList pathsKeys = globalIni.childKeys(); - globalIni.endGroup(); - if (pathsKeys.isEmpty()) { + const QMap pathsKeyMap = { + {"paths/decks", "paths/decks"}, + {"paths/filters", "paths/filters"}, + {"paths/replays", "paths/replays"}, + {"paths/pics", "paths/pics"}, + {"paths/custompics", "paths/customPics"}, + {"paths/themes", "paths/themes"}, + {"paths/carddatabase", "paths/cardDatabase"}, + {"paths/customsets", "paths/customSets"}, + {"paths/tokendatabase", "paths/tokenDatabase"}, + {"paths/spoilerdatabase", "paths/spoilerDatabase"}, + {"paths/redirects", "paths/redirects"}, + }; + bool hasAny = false; + for (auto it = pathsKeyMap.constBegin(); it != pathsKeyMap.constEnd(); ++it) { + if (globalIni.contains(it.key())) { + hasAny = true; + break; + } + } + if (!hasAny) { return; } QSettings pathsIni(settingsPath + "paths.ini", QSettings::IniFormat); - for (const auto &key : pathsKeys) { - pathsIni.setValue("paths/" + key, globalIni.value("paths/" + key)); + for (auto it = pathsKeyMap.constBegin(); it != pathsKeyMap.constEnd(); ++it) { + if (globalIni.contains(it.key())) { + pathsIni.setValue(it.value(), globalIni.value(it.key())); + } } } static void migrateVisualDeckStorageSettings(const QString &settingsPath, QSettings &globalIni) { - const QStringList vdsKeys = {"interface/visualdeckstoragecardsize", - "interface/visualdeckstoragesortingorder", - "interface/visualdeckstorageshowfolders", - "interface/visualdeckstorageshowtagfilter", - "interface/visualdeckstoragedefaulttagslist", - "interface/visualdeckstoragesearchfoldernames", - "interface/visualdeckstorageshowcoloridentity", - "interface/visualdeckstorageshowbannercardcombobox", - "interface/visualdeckstorageshowtagsondeckpreviews", - "interface/visualdeckstoragedrawunusedcoloridentities", - "interface/visualdeckstorageunusedcoloridentitiesopacity", - "interface/visualdeckstoragetooltiptype", - "interface/visualdeckstoragepromptforconversion", - "interface/visualdeckstoragealwaysconvert", - "interface/visualdeckstorageingame", - "interface/visualdeckstorageselectionanimation", - "interface/defaultDeckEditorType", - "interface/visualdatabasedisplayfiltertomostrecentsetsenabled", - "interface/visualdatabasedisplayfiltertomostrecentsetsamount", - "interface/visualdeckeditorsamplehandsize", - "interface/visualdeckeditorcardsize", - "interface/visualdatabasedisplaycardsize", - "interface/edhreccardsize", - "interface/archidektpreviewsize"}; + const QMap vdsKeyMap = { + {"interface/visualdeckstoragesortingorder", "interface/visualDeckStorage/sortingOrder"}, + {"interface/visualdeckstorageshowfolders", "interface/visualDeckStorage/showFolders"}, + {"interface/visualdeckstorageshowtagfilter", "interface/visualDeckStorage/showTagFilter"}, + {"interface/visualdeckstoragedefaulttagslist", "interface/visualDeckStorage/defaultTagsList"}, + {"interface/visualdeckstoragesearchfoldernames", "interface/visualDeckStorage/searchFolderNames"}, + {"interface/visualdeckstorageshowcoloridentity", "interface/visualDeckStorage/showColorIdentity"}, + {"interface/visualdeckstorageshowbannercardcombobox", "interface/visualDeckStorage/showBannerCardComboBox"}, + {"interface/visualdeckstorageshowtagsondeckpreviews", "interface/visualDeckStorage/showTagsOnDeckPreviews"}, + {"interface/visualdeckstoragedrawunusedcoloridentities", + "interface/visualDeckStorage/drawUnusedColorIdentities"}, + {"interface/visualdeckstorageunusedcoloridentitiesopacity", + "interface/visualDeckStorage/unusedColorIdentitiesOpacity"}, + {"interface/visualdeckstoragetooltiptype", "interface/visualDeckStorage/tooltipType"}, + {"interface/visualdeckstoragepromptforconversion", "interface/visualDeckStorage/promptForConversion"}, + {"interface/visualdeckstoragealwaysconvert", "interface/visualDeckStorage/alwaysConvert"}, + {"interface/visualdeckstorageingame", "interface/visualDeckStorage/inGame"}, + {"interface/visualdeckstorageselectionanimation", "interface/visualDeckStorage/selectionAnimation"}, + {"interface/visualdatabasedisplayfiltertomostrecentsetsenabled", + "interface/visualDatabaseDisplay/filterToMostRecentSetsEnabled"}, + {"interface/visualdatabasedisplayfiltertomostrecentsetsamount", + "interface/visualDatabaseDisplay/filterToMostRecentSetsAmount"}, + }; bool hasAny = false; - for (const auto &key : vdsKeys) { - if (globalIni.contains(key)) { + for (auto it = vdsKeyMap.constBegin(); it != vdsKeyMap.constEnd(); ++it) { + if (globalIni.contains(it.key())) { hasAny = true; + break; } } if (!hasAny) { @@ -375,9 +478,36 @@ static void migrateVisualDeckStorageSettings(const QString &settingsPath, QSetti } QSettings vdsIni(settingsPath + "visual_deck_storage.ini", QSettings::IniFormat); - for (const auto &key : vdsKeys) { - if (globalIni.contains(key)) { - vdsIni.setValue(key, globalIni.value(key)); + for (auto it = vdsKeyMap.constBegin(); it != vdsKeyMap.constEnd(); ++it) { + if (globalIni.contains(it.key())) { + vdsIni.setValue(it.value(), globalIni.value(it.key())); + } + } +} + +static void migrateDeckEditorSettings(const QString &settingsPath, QSettings &globalIni) +{ + const QMap deckEditorKeyMap = { + {"editor/openDeckInNewTab", "deckeditor/openDeckInNewTab"}, + {"interface/deckeditorbannercardcomboboxvisible", "deckeditor/bannerCardComboBoxVisible"}, + {"interface/deckeditortagswidgetvisible", "deckeditor/tagsWidgetVisible"}, + {"interface/defaultDeckEditorType", "deckeditor/defaultDeckEditorType"}, + }; + bool hasAny = false; + for (auto it = deckEditorKeyMap.constBegin(); it != deckEditorKeyMap.constEnd(); ++it) { + if (globalIni.contains(it.key())) { + hasAny = true; + break; + } + } + if (!hasAny) { + return; + } + + QSettings deckEditorIni(settingsPath + "deck_editor.ini", QSettings::IniFormat); + for (auto it = deckEditorKeyMap.constBegin(); it != deckEditorKeyMap.constEnd(); ++it) { + if (globalIni.contains(it.key())) { + deckEditorIni.setValue(it.value(), globalIni.value(it.key())); } } } @@ -395,9 +525,9 @@ static void migrateLegacySets(const QString &settingsPath) QSettings cardDbIni(settingsPath + "cardDatabase.ini", QSettings::IniFormat); for (const auto &shortName : groups) { legacySetting.beginGroup(shortName); - cardDbIni.setValue("sets/" + shortName + "/sortkey", legacySetting.value("sortkey")); + cardDbIni.setValue("sets/" + shortName + "/sortKey", legacySetting.value("sortkey")); cardDbIni.setValue("sets/" + shortName + "/enabled", legacySetting.value("enabled")); - cardDbIni.setValue("sets/" + shortName + "/isknown", legacySetting.value("isknown")); + cardDbIni.setValue("sets/" + shortName + "/isKnown", legacySetting.value("isknown")); legacySetting.endGroup(); } legacySetting.endGroup(); @@ -413,13 +543,30 @@ static void migrateLegacyServers(const QString &settingsPath) return; } + const QMap serverKeyMap = { + {"previoushostlogin", "previousHostLogin"}, + {"previoushosts", "previousHosts"}, + {"previoushostName", "previousHostName"}, + {"auto_connect", "autoConnect"}, + {"fphostname", "fpHostName"}, + {"fpport", "fpPort"}, + {"fpplayername", "fpPlayerName"}, + {"save_debug_log", "saveDebugLog"}, + }; + QSettings serversIni(settingsPath + "servers.ini", QSettings::IniFormat); - serversIni.setValue("server/previoushostlogin", legacySetting.value("previoushostlogin")); - serversIni.setValue("server/previoushosts", legacySetting.value("previoushosts")); - serversIni.setValue("server/auto_connect", legacySetting.value("auto_connect")); - serversIni.setValue("server/fphostname", legacySetting.value("fphostname")); - serversIni.setValue("server/fpport", legacySetting.value("fpport")); - serversIni.setValue("server/fpplayername", legacySetting.value("fpplayername")); + for (auto it = serverKeyMap.constBegin(); it != serverKeyMap.constEnd(); ++it) { + if (legacySetting.contains(it.key())) { + serversIni.setValue("server/" + it.value(), legacySetting.value(it.key())); + } + } + + legacySetting.beginGroup("server_details"); + const QStringList detailsKeys = legacySetting.allKeys(); + for (const auto &key : detailsKeys) { + serversIni.setValue("server/server_details/" + key, legacySetting.value(key)); + } + legacySetting.endGroup(); legacySetting.endGroup(); } @@ -454,9 +601,36 @@ static void migrateLegacyGameFilters(const QString &settingsPath) return; } + const QMap filterKeyMap = { + {"hide_buddies_only_games", "hideBuddiesOnlyGames"}, + {"hide_full_games", "hideFullGames"}, + {"hide_games_that_started", "hideGamesThatStarted"}, + {"hide_password_protected_games", "hidePasswordProtectedGames"}, + {"hide_ignored_user_games", "hideIgnoredUserGames"}, + {"hide_not_buddy_created_games", "hideNotBuddyCreatedGames"}, + {"hide_open_decklist_games", "hideOpenDecklistGames"}, + {"game_name_filter", "gameNameFilter"}, + {"creator_name_filter", "creatorNameFilter"}, + {"min_players", "minPlayers"}, + {"max_players", "maxPlayers"}, + {"max_game_age_time", "maxGameAgeTime"}, + {"show_only_if_spectators_can_watch", "showOnlyIfSpectatorsCanWatch"}, + {"show_spectator_password_protected", "showSpectatorPasswordProtected"}, + {"show_only_if_spectators_can_chat", "showOnlyIfSpectatorsCanChat"}, + {"show_only_if_spectators_can_see_hands", "showOnlyIfSpectatorsCanSeeHands"}, + }; + QSettings filtersIni(settingsPath + "gamefilters.ini", QSettings::IniFormat); + for (auto it = filterKeyMap.constBegin(); it != filterKeyMap.constEnd(); ++it) { + if (legacySetting.contains(it.key())) { + filtersIni.setValue("filter_games/" + it.value(), legacySetting.value(it.key())); + } + } + const QString gameTypePrefix = "game_type/"; for (const auto &key : keys) { - filtersIni.setValue("filter_games/" + key, legacySetting.value(key)); + if (key.startsWith(gameTypePrefix)) { + filtersIni.setValue("filter_games/gameType/" + key.mid(gameTypePrefix.size()), legacySetting.value(key)); + } } legacySetting.endGroup(); } @@ -503,10 +677,15 @@ bool SettingsMigration::migrateSettingsFromGlobalIni(const QString &settingsPath migrateCacheStorageSettings(settingsPath, globalIni); migrateUpdatesSettings(settingsPath, globalIni); migratePersonalSettings(settingsPath, globalIni); + migrateDownloadSettings(settingsPath, globalIni); migrateCardsDisplaySettings(settingsPath, globalIni); + migrateCardCounterSettings(settingsPath, globalIni); migrateInterfaceSettings(settingsPath, globalIni); + migrateAppearanceSettings(settingsPath, globalIni); + migrateNetworkSettings(settingsPath, globalIni); migratePathsSettings(settingsPath, globalIni); migrateVisualDeckStorageSettings(settingsPath, globalIni); + migrateDeckEditorSettings(settingsPath, globalIni); QFile::remove(settingsPath + "global.ini.old"); QFile::rename(settingsPath + "global.ini", settingsPath + "global.ini.old"); diff --git a/libcockatrice_settings/libcockatrice/settings/sound_settings.cpp b/libcockatrice_settings/libcockatrice/settings/sound_settings.cpp index 7fd61d263..82b724a07 100644 --- a/libcockatrice_settings/libcockatrice/settings/sound_settings.cpp +++ b/libcockatrice_settings/libcockatrice/settings/sound_settings.cpp @@ -17,7 +17,7 @@ QString SoundSettings::getSoundThemeName() const int SoundSettings::getMasterVolume() const { - return getValue("mastervolume", QString(), QString(), 100).toInt(); + return getValue("masterVolume", QString(), QString(), 100).toInt(); } void SoundSettings::setSoundEnabled(bool _soundEnabled) @@ -34,6 +34,6 @@ void SoundSettings::setSoundThemeName(const QString &_soundThemeName) void SoundSettings::setMasterVolume(int _masterVolume) { - setValue(_masterVolume, "mastervolume"); + setValue(_masterVolume, "masterVolume"); emit masterVolumeChanged(_masterVolume); } diff --git a/libcockatrice_settings/libcockatrice/settings/updates_settings.cpp b/libcockatrice_settings/libcockatrice/settings/updates_settings.cpp index 3d6d77e53..166bc0aa8 100644 --- a/libcockatrice_settings/libcockatrice/settings/updates_settings.cpp +++ b/libcockatrice_settings/libcockatrice/settings/updates_settings.cpp @@ -45,17 +45,17 @@ bool UpdatesSettings::getAlwaysEnableNewSets() const bool UpdatesSettings::getNotifyAboutUpdates() const { - return getValue("updatenotification", QString(), QString(), true).toBool(); + return getValue("updateNotification", QString(), QString(), true).toBool(); } bool UpdatesSettings::getNotifyAboutNewVersion() const { - return getValue("newversionnotification", QString(), QString(), true).toBool(); + return getValue("newVersionNotification", QString(), QString(), true).toBool(); } int UpdatesSettings::getUpdateReleaseChannelIndex() const { - return getValue("updatereleasechannel", QString(), QString(), 0).toInt(); + return getValue("updateReleaseChannel", QString(), QString(), 0).toInt(); } void UpdatesSettings::setCheckUpdatesOnStartup(bool value) @@ -90,15 +90,15 @@ void UpdatesSettings::setAlwaysEnableNewSets(bool value) void UpdatesSettings::setNotifyAboutUpdates(bool _notifyaboutupdate) { - setValue(_notifyaboutupdate, "updatenotification"); + setValue(_notifyaboutupdate, "updateNotification"); } void UpdatesSettings::setNotifyAboutNewVersion(bool _notifyaboutnewversion) { - setValue(_notifyaboutnewversion, "newversionnotification"); + setValue(_notifyaboutnewversion, "newVersionNotification"); } void UpdatesSettings::setUpdateReleaseChannelIndex(int value) { - setValue(value, "updatereleasechannel"); + setValue(value, "updateReleaseChannel"); } diff --git a/libcockatrice_settings/libcockatrice/settings/visual_deck_storage_settings.cpp b/libcockatrice_settings/libcockatrice/settings/visual_deck_storage_settings.cpp index c0ccc37aa..1b21af58e 100644 --- a/libcockatrice_settings/libcockatrice/settings/visual_deck_storage_settings.cpp +++ b/libcockatrice_settings/libcockatrice/settings/visual_deck_storage_settings.cpp @@ -91,258 +91,182 @@ VisualDeckStorageSettings::VisualDeckStorageSettings(const QString &settingPath, int VisualDeckStorageSettings::getVisualDeckStorageSortingOrder() const { - return getValue("visualdeckstoragesortingorder", QString(), QString(), 0).toInt(); + return getValue("sortingOrder", "interface", "visualDeckStorage", 0).toInt(); } bool VisualDeckStorageSettings::getVisualDeckStorageShowFolders() const { - return getValue("visualdeckstorageshowfolders", QString(), QString(), true).toBool(); + return getValue("showFolders", "interface", "visualDeckStorage", true).toBool(); } bool VisualDeckStorageSettings::getVisualDeckStorageShowTagFilter() const { - return getValue("visualdeckstorageshowtagfilter", QString(), QString(), true).toBool(); + return getValue("showTagFilter", "interface", "visualDeckStorage", true).toBool(); } QStringList VisualDeckStorageSettings::getVisualDeckStorageDefaultTagsList() const { - return getValue("visualdeckstoragedefaulttagslist", QString(), QString(), QVariant::fromValue(defaultTags)) + return getValue("defaultTagsList", "interface", "visualDeckStorage", QVariant::fromValue(defaultTags)) .toStringList(); } bool VisualDeckStorageSettings::getVisualDeckStorageSearchFolderNames() const { - return getValue("visualdeckstoragesearchfoldernames", QString(), QString(), true).toBool(); + return getValue("searchFolderNames", "interface", "visualDeckStorage", true).toBool(); } bool VisualDeckStorageSettings::getVisualDeckStorageShowColorIdentity() const { - return getValue("visualdeckstorageshowcoloridentity", QString(), QString(), true).toBool(); + return getValue("showColorIdentity", "interface", "visualDeckStorage", true).toBool(); } bool VisualDeckStorageSettings::getVisualDeckStorageShowBannerCardComboBox() const { - return getValue("visualdeckstorageshowbannercardcombobox", QString(), QString(), true).toBool(); + return getValue("showBannerCardComboBox", "interface", "visualDeckStorage", true).toBool(); } bool VisualDeckStorageSettings::getVisualDeckStorageShowTagsOnDeckPreviews() const { - return getValue("visualdeckstorageshowtagsondeckpreviews", QString(), QString(), true).toBool(); -} - -int VisualDeckStorageSettings::getVisualDeckStorageCardSize() const -{ - return getValue("visualdeckstoragecardsize", QString(), QString(), 100).toInt(); + return getValue("showTagsOnDeckPreviews", "interface", "visualDeckStorage", true).toBool(); } bool VisualDeckStorageSettings::getVisualDeckStorageDrawUnusedColorIdentities() const { - return getValue("visualdeckstoragedrawunusedcoloridentities", QString(), QString(), true).toBool(); + return getValue("drawUnusedColorIdentities", "interface", "visualDeckStorage", true).toBool(); } int VisualDeckStorageSettings::getVisualDeckStorageUnusedColorIdentitiesOpacity() const { - return getValue("visualdeckstorageunusedcoloridentitiesopacity", QString(), QString(), 15).toInt(); + return getValue("unusedColorIdentitiesOpacity", "interface", "visualDeckStorage", 15).toInt(); } int VisualDeckStorageSettings::getVisualDeckStorageTooltipType() const { - return getValue("visualdeckstoragetooltiptype", QString(), QString(), 0).toInt(); + return getValue("tooltipType", "interface", "visualDeckStorage", 0).toInt(); } bool VisualDeckStorageSettings::getVisualDeckStoragePromptForConversion() const { - return getValue("visualdeckstoragepromptforconversion", QString(), QString(), true).toBool(); + return getValue("promptForConversion", "interface", "visualDeckStorage", true).toBool(); } bool VisualDeckStorageSettings::getVisualDeckStorageAlwaysConvert() const { - return getValue("visualdeckstoragealwaysconvert", QString(), QString(), false).toBool(); + return getValue("alwaysConvert", "interface", "visualDeckStorage", false).toBool(); } bool VisualDeckStorageSettings::getVisualDeckStorageInGame() const { - return getValue("visualdeckstorageingame", QString(), QString(), true).toBool(); + return getValue("inGame", "interface", "visualDeckStorage", true).toBool(); } bool VisualDeckStorageSettings::getVisualDeckStorageSelectionAnimation() const { - return getValue("visualdeckstorageselectionanimation", QString(), QString(), true).toBool(); -} - -int VisualDeckStorageSettings::getVisualDeckEditorCardSize() const -{ - return getValue("visualdeckeditorcardsize", QString(), QString(), 100).toInt(); -} - -int VisualDeckStorageSettings::getVisualDeckEditorSampleHandSize() const -{ - return getValue("visualdeckeditorsamplehandsize", QString(), QString(), 7).toInt(); -} - -int VisualDeckStorageSettings::getVisualDatabaseDisplayCardSize() const -{ - return getValue("visualdatabasedisplaycardsize", QString(), QString(), 100).toInt(); + return getValue("selectionAnimation", "interface", "visualDeckStorage", true).toBool(); } bool VisualDeckStorageSettings::getVisualDatabaseDisplayFilterToMostRecentSetsEnabled() const { - return getValue("visualdatabasedisplayfiltertomostrecentsetsenabled", QString(), QString(), false).toBool(); + return getValue("filterToMostRecentSetsEnabled", "interface", "visualDatabaseDisplay", false).toBool(); } int VisualDeckStorageSettings::getVisualDatabaseDisplayFilterToMostRecentSetsAmount() const { - return getValue("visualdatabasedisplayfiltertomostrecentsetsamount", QString(), QString(), 10).toInt(); -} - -int VisualDeckStorageSettings::getEDHRecCardSize() const -{ - return getValue("edhreccardsize", QString(), QString(), 100).toInt(); -} - -int VisualDeckStorageSettings::getArchidektPreviewSize() const -{ - return getValue("archidektpreviewsize", QString(), QString(), 100).toInt(); -} - -int VisualDeckStorageSettings::getDefaultDeckEditorType() const -{ - return getValue("defaultDeckEditorType", QString(), QString(), 1).toInt(); + return getValue("filterToMostRecentSetsAmount", "interface", "visualDatabaseDisplay", 10).toInt(); } void VisualDeckStorageSettings::setVisualDeckStorageSortingOrder(int _sortingOrder) { - setValue(_sortingOrder, "visualdeckstoragesortingorder"); + setValue(_sortingOrder, "sortingOrder", "interface", "visualDeckStorage"); } void VisualDeckStorageSettings::setVisualDeckStorageShowFolders(bool value) { - setValue(value, "visualdeckstorageshowfolders"); + setValue(value, "showFolders", "interface", "visualDeckStorage"); } void VisualDeckStorageSettings::setVisualDeckStorageShowTagFilter(bool _showTags) { - setValue(_showTags, "visualdeckstorageshowtagfilter"); + setValue(_showTags, "showTagFilter", "interface", "visualDeckStorage"); emit visualDeckStorageShowTagFilterChanged(_showTags); } void VisualDeckStorageSettings::setVisualDeckStorageDefaultTagsList(QStringList _defaultTagsList) { - setValue(QVariant::fromValue(_defaultTagsList), "visualdeckstoragedefaulttagslist"); + setValue(QVariant::fromValue(_defaultTagsList), "defaultTagsList", "interface", "visualDeckStorage"); emit visualDeckStorageDefaultTagsListChanged(); } void VisualDeckStorageSettings::setVisualDeckStorageSearchFolderNames(bool value) { - setValue(value, "visualdeckstoragesearchfoldernames"); + setValue(value, "searchFolderNames", "interface", "visualDeckStorage"); } void VisualDeckStorageSettings::setVisualDeckStorageShowColorIdentity(bool value) { - setValue(value, "visualdeckstorageshowcoloridentity"); + setValue(value, "showColorIdentity", "interface", "visualDeckStorage"); emit visualDeckStorageShowColorIdentityChanged(value); } void VisualDeckStorageSettings::setVisualDeckStorageShowBannerCardComboBox(bool _showBannerCardComboBox) { - setValue(_showBannerCardComboBox, "visualdeckstorageshowbannercardcombobox"); + setValue(_showBannerCardComboBox, "showBannerCardComboBox", "interface", "visualDeckStorage"); emit visualDeckStorageShowBannerCardComboBoxChanged(_showBannerCardComboBox); } void VisualDeckStorageSettings::setVisualDeckStorageShowTagsOnDeckPreviews(bool _showTags) { - setValue(_showTags, "visualdeckstorageshowtagsondeckpreviews"); + setValue(_showTags, "showTagsOnDeckPreviews", "interface", "visualDeckStorage"); emit visualDeckStorageShowTagsOnDeckPreviewsChanged(_showTags); } -void VisualDeckStorageSettings::setVisualDeckStorageCardSize(int _cardSize) -{ - setValue(_cardSize, "visualdeckstoragecardsize"); - emit visualDeckStorageCardSizeChanged(); -} - void VisualDeckStorageSettings::setVisualDeckStorageDrawUnusedColorIdentities(bool _draw) { - setValue(_draw, "visualdeckstoragedrawunusedcoloridentities"); + setValue(_draw, "drawUnusedColorIdentities", "interface", "visualDeckStorage"); emit visualDeckStorageDrawUnusedColorIdentitiesChanged(_draw); } void VisualDeckStorageSettings::setVisualDeckStorageUnusedColorIdentitiesOpacity(int _opacity) { - setValue(_opacity, "visualdeckstorageunusedcoloridentitiesopacity"); + setValue(_opacity, "unusedColorIdentitiesOpacity", "interface", "visualDeckStorage"); emit visualDeckStorageUnusedColorIdentitiesOpacityChanged(_opacity); } void VisualDeckStorageSettings::setVisualDeckStorageTooltipType(int value) { - setValue(value, "visualdeckstoragetooltiptype"); + setValue(value, "tooltipType", "interface", "visualDeckStorage"); } void VisualDeckStorageSettings::setVisualDeckStoragePromptForConversion(bool _prompt) { - setValue(_prompt, "visualdeckstoragepromptforconversion"); + setValue(_prompt, "promptForConversion", "interface", "visualDeckStorage"); } void VisualDeckStorageSettings::setVisualDeckStorageAlwaysConvert(bool _always) { - setValue(_always, "visualdeckstoragealwaysconvert"); + setValue(_always, "alwaysConvert", "interface", "visualDeckStorage"); } void VisualDeckStorageSettings::setVisualDeckStorageInGame(bool enabled) { - setValue(enabled, "visualdeckstorageingame"); + setValue(enabled, "inGame", "interface", "visualDeckStorage"); emit visualDeckStorageInGameChanged(enabled); } void VisualDeckStorageSettings::setVisualDeckStorageSelectionAnimation(bool enabled) { - setValue(enabled, "visualdeckstorageselectionanimation"); + setValue(enabled, "selectionAnimation", "interface", "visualDeckStorage"); emit visualDeckStorageSelectionAnimationChanged(enabled); } -void VisualDeckStorageSettings::setVisualDeckEditorCardSize(int _cardSize) -{ - setValue(_cardSize, "visualdeckeditorcardsize"); - emit visualDeckEditorCardSizeChanged(); -} - -void VisualDeckStorageSettings::setVisualDeckEditorSampleHandSize(int _amount) -{ - setValue(_amount, "visualdeckeditorsamplehandsize"); - emit visualDeckEditorSampleHandSizeAmountChanged(_amount); -} - -void VisualDeckStorageSettings::setVisualDatabaseDisplayCardSize(int _cardSize) -{ - setValue(_cardSize, "visualdatabasedisplaycardsize"); - emit visualDatabaseDisplayCardSizeChanged(); -} - void VisualDeckStorageSettings::setVisualDatabaseDisplayFilterToMostRecentSetsEnabled(bool _enabled) { - setValue(_enabled, "visualdatabasedisplayfiltertomostrecentsetsenabled"); + setValue(_enabled, "filterToMostRecentSetsEnabled", "interface", "visualDatabaseDisplay"); emit visualDatabaseDisplayFilterToMostRecentSetsEnabledChanged(_enabled); } void VisualDeckStorageSettings::setVisualDatabaseDisplayFilterToMostRecentSetsAmount(int _amount) { - setValue(_amount, "visualdatabasedisplayfiltertomostrecentsetsamount"); + setValue(_amount, "filterToMostRecentSetsAmount", "interface", "visualDatabaseDisplay"); emit visualDatabaseDisplayFilterToMostRecentSetsAmountChanged(_amount); } - -void VisualDeckStorageSettings::setEDHRecCardSize(int _edhrecCardSize) -{ - setValue(_edhrecCardSize, "edhreccardsize"); - emit edhRecCardSizeChanged(); -} - -void VisualDeckStorageSettings::setArchidektPreviewCardSize(int _archidektPreviewCardSize) -{ - setValue(_archidektPreviewCardSize, "archidektpreviewsize"); - emit archidektPreviewSizeChanged(); -} - -void VisualDeckStorageSettings::setDefaultDeckEditorType(int value) -{ - setValue(value, "defaultDeckEditorType"); -} diff --git a/libcockatrice_settings/libcockatrice/settings/visual_deck_storage_settings.h b/libcockatrice_settings/libcockatrice/settings/visual_deck_storage_settings.h index 06337ce79..fd2a76663 100644 --- a/libcockatrice_settings/libcockatrice/settings/visual_deck_storage_settings.h +++ b/libcockatrice_settings/libcockatrice/settings/visual_deck_storage_settings.h @@ -20,7 +20,6 @@ public: [[nodiscard]] bool getVisualDeckStorageShowColorIdentity() const override; [[nodiscard]] bool getVisualDeckStorageShowBannerCardComboBox() const override; [[nodiscard]] bool getVisualDeckStorageShowTagsOnDeckPreviews() const override; - [[nodiscard]] int getVisualDeckStorageCardSize() const override; [[nodiscard]] bool getVisualDeckStorageDrawUnusedColorIdentities() const override; [[nodiscard]] int getVisualDeckStorageUnusedColorIdentitiesOpacity() const override; [[nodiscard]] int getVisualDeckStorageTooltipType() const override; @@ -28,14 +27,8 @@ public: [[nodiscard]] bool getVisualDeckStorageAlwaysConvert() const override; [[nodiscard]] bool getVisualDeckStorageInGame() const override; [[nodiscard]] bool getVisualDeckStorageSelectionAnimation() const override; - [[nodiscard]] int getVisualDeckEditorCardSize() const override; - [[nodiscard]] int getVisualDeckEditorSampleHandSize() const override; - [[nodiscard]] int getVisualDatabaseDisplayCardSize() const override; [[nodiscard]] bool getVisualDatabaseDisplayFilterToMostRecentSetsEnabled() const override; [[nodiscard]] int getVisualDatabaseDisplayFilterToMostRecentSetsAmount() const override; - [[nodiscard]] int getEDHRecCardSize() const override; - [[nodiscard]] int getArchidektPreviewSize() const override; - [[nodiscard]] int getDefaultDeckEditorType() const override; void setVisualDeckStorageSortingOrder(int _sortingOrder); void setVisualDeckStorageShowFolders(bool value); @@ -45,7 +38,6 @@ public: void setVisualDeckStorageShowColorIdentity(bool value); void setVisualDeckStorageShowBannerCardComboBox(bool _showBannerCardComboBox); void setVisualDeckStorageShowTagsOnDeckPreviews(bool _showTags); - void setVisualDeckStorageCardSize(int _cardSize); void setVisualDeckStorageDrawUnusedColorIdentities(bool _draw); void setVisualDeckStorageUnusedColorIdentitiesOpacity(int _opacity); void setVisualDeckStorageTooltipType(int value); @@ -53,14 +45,8 @@ public: void setVisualDeckStorageAlwaysConvert(bool _always); void setVisualDeckStorageInGame(bool enabled); void setVisualDeckStorageSelectionAnimation(bool enabled); - void setVisualDeckEditorCardSize(int _cardSize); - void setVisualDeckEditorSampleHandSize(int _amount); - void setVisualDatabaseDisplayCardSize(int _cardSize); void setVisualDatabaseDisplayFilterToMostRecentSetsEnabled(bool _enabled); void setVisualDatabaseDisplayFilterToMostRecentSetsAmount(int _amount); - void setEDHRecCardSize(int _edhrecCardSize); - void setArchidektPreviewCardSize(int _archidektPreviewCardSize); - void setDefaultDeckEditorType(int value); signals: void visualDeckStorageShowTagFilterChanged(bool _visible); @@ -68,18 +54,12 @@ signals: void visualDeckStorageShowColorIdentityChanged(bool _visible); void visualDeckStorageShowBannerCardComboBoxChanged(bool _visible); void visualDeckStorageShowTagsOnDeckPreviewsChanged(bool _visible); - void visualDeckStorageCardSizeChanged(); void visualDeckStorageDrawUnusedColorIdentitiesChanged(bool _visible); void visualDeckStorageUnusedColorIdentitiesOpacityChanged(bool value); void visualDeckStorageInGameChanged(bool enabled); void visualDeckStorageSelectionAnimationChanged(bool enabled); void visualDatabaseDisplayFilterToMostRecentSetsEnabledChanged(bool enabled); void visualDatabaseDisplayFilterToMostRecentSetsAmountChanged(int amount); - void visualDeckEditorSampleHandSizeAmountChanged(int amount); - void visualDeckEditorCardSizeChanged(); - void visualDatabaseDisplayCardSizeChanged(); - void edhRecCardSizeChanged(); - void archidektPreviewSizeChanged(); public: explicit VisualDeckStorageSettings(const QString &settingPath, QObject *parent = nullptr); diff --git a/tests/settings/settings_defaults_test.cpp b/tests/settings/settings_defaults_test.cpp index 0cd68d9d4..dfdad4780 100644 --- a/tests/settings/settings_defaults_test.cpp +++ b/tests/settings/settings_defaults_test.cpp @@ -1,9 +1,15 @@ #include "gtest/gtest.h" #include #include +#include #include +#include #include +#include +#include #include +#include +#include #include #include #include @@ -258,66 +264,218 @@ TEST_F(SettingsDefaultsTest, Personal_Lang_Default) ASSERT_EQ(s.getLang(), QString("")); } -TEST_F(SettingsDefaultsTest, Personal_ClientID_Default) -{ - PersonalSettings s(settingsPath, nullptr); - ASSERT_EQ(s.getClientID(), QString("notset")); -} - -TEST_F(SettingsDefaultsTest, Personal_KeepAlive_Default) -{ - PersonalSettings s(settingsPath, nullptr); - ASSERT_EQ(s.getKeepAlive(), 3); -} - -TEST_F(SettingsDefaultsTest, Personal_TimeOut_Default) -{ - PersonalSettings s(settingsPath, nullptr); - ASSERT_EQ(s.getTimeOut(), 5); -} - -TEST_F(SettingsDefaultsTest, Personal_PicDownload_Default) -{ - PersonalSettings s(settingsPath, nullptr); - ASSERT_EQ(s.getPicDownload(), true); -} - -TEST_F(SettingsDefaultsTest, Personal_ShowStatusBar_Default) -{ - PersonalSettings s(settingsPath, nullptr); - ASSERT_EQ(s.getShowStatusBar(), false); -} - -TEST_F(SettingsDefaultsTest, Personal_MaxFontSize_Default) -{ - PersonalSettings s(settingsPath, nullptr); - ASSERT_EQ(s.getMaxFontSize(), 12); -} - -TEST_F(SettingsDefaultsTest, Personal_HomeTabBackgroundSource_Default) -{ - PersonalSettings s(settingsPath, nullptr); - ASSERT_EQ(s.getHomeTabBackgroundSource(), QString("themed")); -} - -TEST_F(SettingsDefaultsTest, Personal_HomeTabDisplayCardName_Default) -{ - PersonalSettings s(settingsPath, nullptr); - ASSERT_EQ(s.getHomeTabDisplayCardName(), true); -} - TEST_F(SettingsDefaultsTest, Personal_ShowTipsOnStartup_Default) { PersonalSettings s(settingsPath, nullptr); ASSERT_EQ(s.getShowTipsOnStartup(), true); } -TEST_F(SettingsDefaultsTest, Personal_DownloadSpoilersStatus_Default) +// --- DownloadSettings --- + +TEST_F(SettingsDefaultsTest, Download_PicDownload_Default) { - PersonalSettings s(settingsPath, nullptr); + DownloadSettings s(settingsPath, nullptr); + ASSERT_EQ(s.getPicDownload(), true); +} + +TEST_F(SettingsDefaultsTest, Download_DownloadSpoilersStatus_Default) +{ + DownloadSettings s(settingsPath, nullptr); ASSERT_EQ(s.getDownloadSpoilersStatus(), false); } +// --- AppearanceSettings --- + +TEST_F(SettingsDefaultsTest, Appearance_ThemeName_Default) +{ + AppearanceSettings s(settingsPath, nullptr); + ASSERT_EQ(s.getThemeName(), QString("")); +} + +TEST_F(SettingsDefaultsTest, Appearance_ThemeName_SetAndGet) +{ + AppearanceSettings s(settingsPath, nullptr); + s.setThemeName("my_theme"); + ASSERT_EQ(s.getThemeName(), QString("my_theme")); +} + +TEST_F(SettingsDefaultsTest, Appearance_StyleUserList_Default) +{ + AppearanceSettings s(settingsPath, nullptr); + ASSERT_EQ(s.getStyleUserList(), true); +} + +TEST_F(SettingsDefaultsTest, Appearance_MaxFontSize_Default) +{ + AppearanceSettings s(settingsPath, nullptr); + ASSERT_EQ(s.getMaxFontSize(), 12); +} + +TEST_F(SettingsDefaultsTest, Appearance_MaxFontSize_SetAndGet) +{ + AppearanceSettings s(settingsPath, nullptr); + s.setMaxFontSize(14); + ASSERT_EQ(s.getMaxFontSize(), 14); +} + +TEST_F(SettingsDefaultsTest, Appearance_HomeTabBackgroundSource_Default) +{ + AppearanceSettings s(settingsPath, nullptr); + ASSERT_EQ(s.getHomeTabBackgroundSource(), QString("themed")); +} + +TEST_F(SettingsDefaultsTest, Appearance_HomeTabBackgroundShuffleFrequency_Default) +{ + AppearanceSettings s(settingsPath, nullptr); + ASSERT_EQ(s.getHomeTabBackgroundShuffleFrequency(), 0); +} + +TEST_F(SettingsDefaultsTest, Appearance_HomeTabDisplayCardName_Default) +{ + AppearanceSettings s(settingsPath, nullptr); + ASSERT_EQ(s.getHomeTabDisplayCardName(), true); +} + +// --- InterfaceSettings --- + +TEST_F(SettingsDefaultsTest, Interface_ShowStatusBar_Default) +{ + InterfaceSettings s(settingsPath, nullptr); + ASSERT_EQ(s.getShowStatusBar(), false); +} + +TEST_F(SettingsDefaultsTest, Interface_ShowShortcuts_Default) +{ + InterfaceSettings s(settingsPath, nullptr); + ASSERT_EQ(s.getShowShortcuts(), true); +} + +TEST_F(SettingsDefaultsTest, Interface_ShowGameSelectorFilterToolbar_Default) +{ + InterfaceSettings s(settingsPath, nullptr); + ASSERT_EQ(s.getShowGameSelectorFilterToolbar(), true); +} + +TEST_F(SettingsDefaultsTest, Interface_NotificationsEnabled_Default) +{ + InterfaceSettings s(settingsPath, nullptr); + ASSERT_EQ(s.getNotificationsEnabled(), true); +} + +TEST_F(SettingsDefaultsTest, Interface_SpectatorNotificationsEnabled_Default) +{ + InterfaceSettings s(settingsPath, nullptr); + ASSERT_EQ(s.getSpectatorNotificationsEnabled(), false); +} + +TEST_F(SettingsDefaultsTest, Interface_BuddyConnectNotificationsEnabled_Default) +{ + InterfaceSettings s(settingsPath, nullptr); + ASSERT_EQ(s.getBuddyConnectNotificationsEnabled(), true); +} + +// --- DeckEditorSettings --- + +TEST_F(SettingsDefaultsTest, DeckEditor_OpenDeckInNewTab_Default) +{ + DeckEditorSettings s(settingsPath, nullptr); + ASSERT_EQ(s.getOpenDeckInNewTab(), false); +} + +TEST_F(SettingsDefaultsTest, DeckEditor_BannerCardComboBoxVisible_Default) +{ + DeckEditorSettings s(settingsPath, nullptr); + ASSERT_EQ(s.getBannerCardComboBoxVisible(), true); +} + +TEST_F(SettingsDefaultsTest, DeckEditor_TagsWidgetVisible_Default) +{ + DeckEditorSettings s(settingsPath, nullptr); + ASSERT_EQ(s.getTagsWidgetVisible(), true); +} + +TEST_F(SettingsDefaultsTest, DeckEditor_DefaultDeckEditorType_Default) +{ + DeckEditorSettings s(settingsPath, nullptr); + ASSERT_EQ(s.getDefaultDeckEditorType(), 1); +} + +// --- NetworkSettings --- + +TEST_F(SettingsDefaultsTest, Network_ClientID_Default) +{ + NetworkSettings s(settingsPath, nullptr); + ASSERT_EQ(s.getClientID(), QString("notset")); +} + +TEST_F(SettingsDefaultsTest, Network_ClientVersion_Default) +{ + NetworkSettings s(settingsPath, nullptr); + ASSERT_EQ(s.getClientVersion(), QString("notset")); +} + +TEST_F(SettingsDefaultsTest, Network_KeepAlive_Default) +{ + NetworkSettings s(settingsPath, nullptr); + ASSERT_EQ(s.getKeepAlive(), 3); +} + +TEST_F(SettingsDefaultsTest, Network_TimeOut_Default) +{ + NetworkSettings s(settingsPath, nullptr); + ASSERT_EQ(s.getTimeOut(), 5); +} + +TEST_F(SettingsDefaultsTest, Network_KnownMissingFeatures_Default) +{ + NetworkSettings s(settingsPath, nullptr); + ASSERT_EQ(s.getKnownMissingFeatures(), QString("")); +} + +// --- CardsDisplaySettings --- + +TEST_F(SettingsDefaultsTest, CardsDisplay_PrintingSelectorCardSize_Default) +{ + CardsDisplaySettings s(settingsPath, nullptr); + ASSERT_EQ(s.getPrintingSelectorCardSize(), 100); +} + +TEST_F(SettingsDefaultsTest, CardsDisplay_VisualDeckStorageCardSize_Default) +{ + CardsDisplaySettings s(settingsPath, nullptr); + ASSERT_EQ(s.getVisualDeckStorageCardSize(), 100); +} + +TEST_F(SettingsDefaultsTest, CardsDisplay_VisualDatabaseDisplayCardSize_Default) +{ + CardsDisplaySettings s(settingsPath, nullptr); + ASSERT_EQ(s.getVisualDatabaseDisplayCardSize(), 100); +} + +TEST_F(SettingsDefaultsTest, CardsDisplay_VisualDeckEditorCardSize_Default) +{ + CardsDisplaySettings s(settingsPath, nullptr); + ASSERT_EQ(s.getVisualDeckEditorCardSize(), 100); +} + +TEST_F(SettingsDefaultsTest, CardsDisplay_EDHRecCardSize_Default) +{ + CardsDisplaySettings s(settingsPath, nullptr); + ASSERT_EQ(s.getEDHRecCardSize(), 100); +} + +TEST_F(SettingsDefaultsTest, CardsDisplay_ArchidektPreviewSize_Default) +{ + CardsDisplaySettings s(settingsPath, nullptr); + ASSERT_EQ(s.getArchidektPreviewSize(), 100); +} + +TEST_F(SettingsDefaultsTest, CardsDisplay_SampleHandSize_Default) +{ + CardsDisplaySettings s(settingsPath, nullptr); + ASSERT_EQ(s.getSampleHandSize(), 7); +} + // --- VisualDeckStorageSettings --- TEST_F(SettingsDefaultsTest, VisualDeckStorage_SortingOrder_Default) diff --git a/tests/settings/settings_migration_test.cpp b/tests/settings/settings_migration_test.cpp index 00e5c65c1..8a3d756a0 100644 --- a/tests/settings/settings_migration_test.cpp +++ b/tests/settings/settings_migration_test.cpp @@ -1,5 +1,7 @@ #include "gtest/gtest.h" +#include #include +#include #include #include #include @@ -10,18 +12,6 @@ namespace { -static bool nativeSettingsAvailable() -{ - QSettings probe; - probe.setValue("_migration_native_probe", "ok"); - probe.sync(); - QSettings read; - bool ok = read.value("_migration_native_probe").toString() == "ok"; - QSettings().clear(); - QSettings().sync(); - return ok; -} - class SettingsMigrationTest : public ::testing::Test { protected: @@ -31,6 +21,17 @@ protected: void SetUp() override { settingsPath = tempDir.path() + "/"; + + // Isolate the settings used by the legacy migration tests inside the temporary + // directory so the tests never read or write the real user config (registry on + // Windows, plist on macOS, .conf on Linux), which would otherwise be shared + // across CI jobs and flaky. Default-format QSettings is forced to IniFormat and + // its UserScope path is redirected here; setPath wins over the XDG_CONFIG_HOME + // default on Unix, so every platform resolves to /config/... + const QString configDir = tempDir.path() + "/config"; + QDir().mkpath(configDir); + qputenv("XDG_CONFIG_HOME", configDir.toUtf8()); + QSettings::setPath(QSettings::IniFormat, QSettings::UserScope, configDir); } bool fileExists(const QString &name) const @@ -43,6 +44,26 @@ protected: QSettings ini(settingsPath + fileName, QSettings::IniFormat); return ini.value(key); } + + // Checks for the exact legacy key in the raw INI content. On Windows, INI keys are + // case-insensitive, so reading back a lower-case legacy key would match its migrated + // camelCase counterpart and hide regressions. Checking the file bytes directly keeps + // the comparison case-sensitive on every platform. + bool iniFileHasKeyCaseSensitive(const QString &fileName, const QString &key) const + { + QFile f(settingsPath + fileName); + if (!f.open(QIODevice::ReadOnly)) { + return false; + } + const QString valueName = key.section('/', -1); + const QStringList lines = QString::fromUtf8(f.readAll()).split('\n'); + for (const auto &line : lines) { + if (line.startsWith(valueName + '=')) { + return true; + } + } + return false; + } }; TEST_F(SettingsMigrationTest, NoGlobalIniDoesNothing) @@ -95,6 +116,8 @@ TEST_F(SettingsMigrationTest, MigratesAllSettingsGroups) g.setValue("maxplayers", 4); g.setValue("gamedescription", "test game"); g.setValue("remembergamesettings", false); + g.setValue("gametypes", "commander"); + g.setValue("onlybuddies", true); g.endGroup(); // localgameoptions @@ -108,44 +131,119 @@ TEST_F(SettingsMigrationTest, MigratesAllSettingsGroups) g.setValue("mention", false); g.setValue("mentioncolor", "FF0000"); g.setValue("showmessagepopups", false); + g.setValue("mentioncompleter", false); + g.setValue("roomhistory", false); + g.setValue("highlightcolor", "00FF00"); g.endGroup(); + // legacy highlight words (under [personal]) + g.setValue("personal/highlightWords", "alpha beta"); + // cache storage (under [personal] group) g.setValue("personal/pixmapCacheSize", 1024); g.setValue("personal/networkCacheSize", 2048); + g.setValue("personal/redirectCacheTtl", 5); + g.setValue("personal/cardPictureLoaderCacheMethod", 1); + g.setValue("personal/localCardImageStorageNamingScheme", 2); // updates (under [personal] group) g.setValue("personal/startupUpdateCheck", false); + g.setValue("personal/startupCardUpdateCheckPromptForUpdate", false); + g.setValue("personal/startupCardUpdateCheckAlwaysUpdate", true); g.setValue("personal/cardUpdateCheckInterval", 14); + g.setValue("personal/lastCardUpdateCheck", QDate(2024, 1, 1)); + g.setValue("personal/alwaysEnableNewSets", true); + g.setValue("personal/updatenotification", false); + g.setValue("personal/newversionnotification", false); // personal g.setValue("personal/lang", "de"); - g.setValue("personal/keepalive", 10); - g.setValue("personal/timeout", 30); - g.setValue("personal/clientid", "test-client-id"); + + // downloads (previously under [personal]) g.setValue("personal/picturedownload", true); + g.setValue("personal/downloadspoilers", true); + + // interface (previously under [personal]) g.setValue("personal/showStatusBar", true); + // theme + g.setValue("theme/name", "custom_theme"); + g.setValue("game/maxfontsize", 14); + + // appearance + g.setValue("appearance/styleUserList", false); + g.setValue("home/background/displayCardName", false); + g.setValue("menu/showshortcuts", false); + g.setValue("menu/showgameselectorfiltertoolbar", false); + + // deck editor + g.setValue("editor/openDeckInNewTab", false); + g.setValue("interface/deckeditortagswidgetvisible", false); + g.setValue("interface/defaultDeckEditorType", 0); + g.setValue("interface/visualdeckeditorsamplehandsize", 5); + // personal home g.setValue("home/background", "custom_bg"); g.setValue("home/background/shuffleTimer", 30); // personal tipOfDay g.setValue("tipOfDay/showTips", false); + g.setValue("tipOfDay/seenTips", QStringList{"1", "2", "3"}); + + // network + g.setValue("personal/keepalive", 10); + g.setValue("personal/timeout", 30); + g.setValue("personal/clientid", "test-client-id"); + g.setValue("personal/clientversion", "test-client-version"); + g.setValue("interface/knownmissingfeatures", "feature1,feature2"); // cards g.setValue("cards/displaycardnames", false); + g.setValue("cards/roundcardcorners", false); + g.setValue("cards/overrideallcardartwithpersonalpreference", true); + g.setValue("cards/bumpsetswithcardsindecktotop", false); + g.setValue("cards/includerebalancedcards", false); + g.setValue("cards/autorotatesidewayslayoutcards", false); g.setValue("cards/tapanimation", true); g.setValue("cards/scaleCards", false); + g.setValue("cards/verticalCardOverlapPercent", 42); + g.setValue("cards/cardinfoviewmode", 1); + g.setValue("cards/printingselectorcardsize", 90); + g.setValue("cards/printingselectorsortorder", 3); + g.setValue("cards/printingselectornavigationbuttonsvisible", false); + // card counters (migrate into card_counters.ini) + g.setValue("cards/counters/0/color", QColor(Qt::red)); // interface g.setValue("interface/usetearoffmenus", true); + g.setValue("interface/cardViewInitialRowsMax", 8); + g.setValue("interface/cardViewExpandedRowsMax", 12); + g.setValue("interface/closeEmptyCardView", false); + g.setValue("interface/focusCardViewSearchBar", false); + g.setValue("interface/keepGameChatFocus", true); g.setValue("interface/notificationsenabled", false); + g.setValue("interface/specnotificationsenabled", true); + g.setValue("interface/buddyconnectnotificationsenabled", false); + g.setValue("interface/doubleclicktoplay", false); + g.setValue("interface/clickPlaysAllSelected", false); + g.setValue("interface/playtostack", false); + g.setValue("interface/doNotDeleteArrowsInSubPhases", false); g.setValue("interface/startinghandsize", 5); - - // hand/table + g.setValue("interface/annotatetokens", true); + g.setValue("interface/showlassoselectioncount", false); + g.setValue("interface/showpersistentselectioncount", false); + g.setValue("interface/tallyType", 2); + g.setValue("interface/leftjustified", true); + g.setValue("interface/min_players_multicolumn", 6); + g.setValue("interface/deckeditorbannercardcomboboxvisible", false); + // hand/table/replay/zoneview g.setValue("hand/horizontal", true); g.setValue("table/invert_vertical", true); + g.setValue("replay/rewindBufferingMs", 6000); + g.setValue("replay/fastForwardSpeed", 5); + g.setValue("zoneview/groupby", 2); + g.setValue("zoneview/sortby", 1); + g.setValue("zoneview/pileview", false); // paths g.beginGroup("paths"); @@ -155,8 +253,29 @@ TEST_F(SettingsMigrationTest, MigratesAllSettingsGroups) // visual deck storage (under [interface] group) g.setValue("interface/visualdeckstoragecardsize", 150); + g.setValue("interface/visualdeckstoragesortingorder", 2); g.setValue("interface/visualdeckstorageshowfolders", false); g.setValue("interface/visualdeckstorageshowtagfilter", false); + g.setValue("interface/visualdeckstoragedefaulttagslist", QStringList{"Alpha", "Beta"}); + g.setValue("interface/visualdeckstoragesearchfoldernames", false); + g.setValue("interface/visualdeckstorageshowcoloridentity", false); + g.setValue("interface/visualdeckstorageshowbannercardcombobox", false); + g.setValue("interface/visualdeckstorageshowtagsondeckpreviews", false); + g.setValue("interface/visualdeckstoragedrawunusedcoloridentities", false); + g.setValue("interface/visualdeckstorageunusedcoloridentitiesopacity", 35); + g.setValue("interface/visualdeckstoragetooltiptype", 1); + g.setValue("interface/visualdeckstoragepromptforconversion", false); + g.setValue("interface/visualdeckstoragealwaysconvert", true); + g.setValue("interface/visualdeckstorageingame", false); + g.setValue("interface/visualdeckstorageselectionanimation", false); + g.setValue("interface/visualdatabasedisplayfiltertomostrecentsetsenabled", true); + g.setValue("interface/visualdatabasedisplayfiltertomostrecentsetsamount", 25); + + // card sizes (migrate into cards_display.ini) + g.setValue("interface/visualdatabasedisplaycardsize", 80); + g.setValue("interface/visualdeckeditorcardsize", 70); + g.setValue("interface/edhreccardsize", 60); + g.setValue("interface/archidektpreviewsize", 50); g.sync(); } @@ -176,54 +295,189 @@ TEST_F(SettingsMigrationTest, MigratesAllSettingsGroups) ASSERT_TRUE(fileExists("sound.ini")); ASSERT_EQ(readFromIni("sound.ini", "sound/enabled"), QVariant(true)); ASSERT_EQ(readFromIni("sound.ini", "sound/theme"), QVariant("custom_theme")); - ASSERT_EQ(readFromIni("sound.ini", "sound/mastervolume"), QVariant(75)); + ASSERT_EQ(readFromIni("sound.ini", "sound/masterVolume"), QVariant(75)); ASSERT_TRUE(fileExists("game.ini")); - ASSERT_EQ(readFromIni("game.ini", "game/maxplayers"), QVariant(4)); - ASSERT_EQ(readFromIni("game.ini", "game/gamedescription"), QVariant("test game")); - ASSERT_EQ(readFromIni("game.ini", "game/remembergamesettings"), QVariant(false)); - ASSERT_EQ(readFromIni("game.ini", "localgameoptions/maxplayers"), QVariant(2)); - ASSERT_EQ(readFromIni("game.ini", "localgameoptions/startinglifetotal"), QVariant(40)); + ASSERT_EQ(readFromIni("game.ini", "game/maxPlayers"), QVariant(4)); + ASSERT_EQ(readFromIni("game.ini", "game/gameDescription"), QVariant("test game")); + ASSERT_EQ(readFromIni("game.ini", "game/rememberGameSettings"), QVariant(false)); + ASSERT_EQ(readFromIni("game.ini", "game/gameTypes"), QVariant("commander")); + ASSERT_EQ(readFromIni("game.ini", "game/onlyBuddies"), QVariant(true)); + ASSERT_EQ(readFromIni("game.ini", "localgameoptions/maxPlayers"), QVariant(2)); + ASSERT_EQ(readFromIni("game.ini", "localgameoptions/startingLifeTotal"), QVariant(40)); ASSERT_TRUE(fileExists("chat.ini")); ASSERT_EQ(readFromIni("chat.ini", "chat/mention"), QVariant(false)); - ASSERT_EQ(readFromIni("chat.ini", "chat/mentioncolor"), QVariant("FF0000")); - ASSERT_EQ(readFromIni("chat.ini", "chat/showmessagepopups"), QVariant(false)); + ASSERT_EQ(readFromIni("chat.ini", "chat/mentionColor"), QVariant("FF0000")); + ASSERT_EQ(readFromIni("chat.ini", "chat/showMessagePopups"), QVariant(false)); + ASSERT_EQ(readFromIni("chat.ini", "chat/mentionCompleter"), QVariant(false)); + ASSERT_EQ(readFromIni("chat.ini", "chat/roomHistory"), QVariant(false)); + ASSERT_EQ(readFromIni("chat.ini", "chat/highlightColor"), QVariant("00FF00")); + ASSERT_EQ(readFromIni("chat.ini", "chat/highlightWords"), QVariant("alpha beta")); ASSERT_TRUE(fileExists("cache_storage.ini")); - ASSERT_EQ(readFromIni("cache_storage.ini", "personal/pixmapCacheSize"), QVariant(1024)); - ASSERT_EQ(readFromIni("cache_storage.ini", "personal/networkCacheSize"), QVariant(2048)); + ASSERT_EQ(readFromIni("cache_storage.ini", "cache_storage/pixmapCacheSize"), QVariant(1024)); + ASSERT_EQ(readFromIni("cache_storage.ini", "cache_storage/networkCacheSize"), QVariant(2048)); + ASSERT_EQ(readFromIni("cache_storage.ini", "cache_storage/redirectCacheTtl"), QVariant(5)); + ASSERT_EQ(readFromIni("cache_storage.ini", "cache_storage/cardPictureLoaderCacheMethod"), QVariant(1)); + ASSERT_EQ(readFromIni("cache_storage.ini", "cache_storage/localCardImageStorageNamingScheme"), QVariant(2)); ASSERT_TRUE(fileExists("updates.ini")); ASSERT_EQ(readFromIni("updates.ini", "updates/startupUpdateCheck"), QVariant(false)); + ASSERT_EQ(readFromIni("updates.ini", "updates/startupCardUpdateCheckPromptForUpdate"), QVariant(false)); + ASSERT_EQ(readFromIni("updates.ini", "updates/startupCardUpdateCheckAlwaysUpdate"), QVariant(true)); ASSERT_EQ(readFromIni("updates.ini", "updates/cardUpdateCheckInterval"), QVariant(14)); + ASSERT_EQ(readFromIni("updates.ini", "updates/lastCardUpdateCheck"), QVariant(QDate(2024, 1, 1))); + ASSERT_EQ(readFromIni("updates.ini", "updates/alwaysEnableNewSets"), QVariant(true)); + ASSERT_EQ(readFromIni("updates.ini", "updates/updateNotification"), QVariant(false)); + ASSERT_EQ(readFromIni("updates.ini", "updates/newVersionNotification"), QVariant(false)); ASSERT_TRUE(fileExists("personal.ini")); ASSERT_EQ(readFromIni("personal.ini", "personal/lang"), QVariant("de")); - ASSERT_EQ(readFromIni("personal.ini", "personal/keepalive"), QVariant(10)); - ASSERT_EQ(readFromIni("personal.ini", "personal/clientid"), QVariant("test-client-id")); - ASSERT_EQ(readFromIni("personal.ini", "personal/showStatusBar"), QVariant(true)); - ASSERT_EQ(readFromIni("personal.ini", "home/background"), QVariant("custom_bg")); ASSERT_EQ(readFromIni("personal.ini", "tipOfDay/showTips"), QVariant(false)); + ASSERT_EQ(readFromIni("personal.ini", "tipOfDay/seenTips"), QVariant(QStringList{"1", "2", "3"})); + + ASSERT_TRUE(fileExists("downloads.ini")); + ASSERT_EQ(readFromIni("downloads.ini", "downloads/pictureDownload"), QVariant(true)); + ASSERT_EQ(readFromIni("downloads.ini", "downloads/downloadSpoilers"), QVariant(true)); + + ASSERT_TRUE(fileExists("appearance.ini")); + ASSERT_EQ(readFromIni("appearance.ini", "appearance/themeName"), QVariant("custom_theme")); + ASSERT_EQ(readFromIni("appearance.ini", "appearance/maxFontSize"), QVariant(14)); + ASSERT_EQ(readFromIni("appearance.ini", "appearance/styleUserList"), QVariant(false)); + ASSERT_EQ(readFromIni("appearance.ini", "appearance/homeTabBackgroundSource"), QVariant("custom_bg")); + ASSERT_EQ(readFromIni("appearance.ini", "appearance/homeTabBackgroundShuffleFrequency"), QVariant(30)); + ASSERT_EQ(readFromIni("appearance.ini", "appearance/homeTabDisplayCardName"), QVariant(false)); + + ASSERT_TRUE(fileExists("network.ini")); + ASSERT_EQ(readFromIni("network.ini", "network/keepAlive"), QVariant(10)); + ASSERT_EQ(readFromIni("network.ini", "network/timeout"), QVariant(30)); + ASSERT_EQ(readFromIni("network.ini", "network/clientId"), QVariant("test-client-id")); + ASSERT_EQ(readFromIni("network.ini", "network/clientVersion"), QVariant("test-client-version")); + ASSERT_EQ(readFromIni("network.ini", "network/knownMissingFeatures"), QVariant("feature1,feature2")); ASSERT_TRUE(fileExists("cards_display.ini")); - ASSERT_EQ(readFromIni("cards_display.ini", "cards/displaycardnames"), QVariant(false)); - ASSERT_EQ(readFromIni("cards_display.ini", "cards/tapanimation"), QVariant(true)); + ASSERT_EQ(readFromIni("cards_display.ini", "cards/displayCardNames"), QVariant(false)); + ASSERT_EQ(readFromIni("cards_display.ini", "cards/roundCardCorners"), QVariant(false)); + ASSERT_EQ(readFromIni("cards_display.ini", "cards/overrideAllCardArtWithPersonalPreference"), QVariant(true)); + ASSERT_EQ(readFromIni("cards_display.ini", "cards/bumpSetsWithCardsInDeckToTop"), QVariant(false)); + ASSERT_EQ(readFromIni("cards_display.ini", "cards/includerebalancedcards"), QVariant(false)); + ASSERT_EQ(readFromIni("cards_display.ini", "cards/autoRotateSidewaysLayoutCards"), QVariant(false)); + ASSERT_EQ(readFromIni("cards_display.ini", "cards/tapAnimation"), QVariant(true)); + ASSERT_EQ(readFromIni("cards_display.ini", "cards/scaleCards"), QVariant(false)); + ASSERT_EQ(readFromIni("cards_display.ini", "cards/verticalCardOverlapPercent"), QVariant(42)); + ASSERT_EQ(readFromIni("cards_display.ini", "cards/cardInfoViewMode"), QVariant(1)); + ASSERT_EQ(readFromIni("cards_display.ini", "cards/cardSize/printingSelector"), QVariant(90)); + ASSERT_EQ(readFromIni("cards_display.ini", "cards/printingSelector/sortOrder"), QVariant(3)); + ASSERT_EQ(readFromIni("cards_display.ini", "cards/printingSelector/navigationButtonsVisible"), QVariant(false)); + ASSERT_EQ(readFromIni("cards_display.ini", "cards/cardSize/visualDeckStorage"), QVariant(150)); + ASSERT_EQ(readFromIni("cards_display.ini", "cards/cardSize/visualDatabaseDisplay"), QVariant(80)); + ASSERT_EQ(readFromIni("cards_display.ini", "cards/cardSize/visualDeckEditor"), QVariant(70)); + ASSERT_EQ(readFromIni("cards_display.ini", "cards/cardSize/edhrec"), QVariant(60)); + ASSERT_EQ(readFromIni("cards_display.ini", "cards/cardSize/archidektPreview"), QVariant(50)); + ASSERT_EQ(readFromIni("cards_display.ini", "cards/cardSize/sampleHandSize"), QVariant(5)); + + ASSERT_TRUE(fileExists("card_counters.ini")); + ASSERT_EQ(readFromIni("card_counters.ini", "cards/counters/0/color").toString(), QColor(Qt::red).name()); + ASSERT_FALSE(readFromIni("global.ini", "cards/counters/0/color").isValid()); ASSERT_TRUE(fileExists("interface.ini")); - ASSERT_EQ(readFromIni("interface.ini", "interface/usetearoffmenus"), QVariant(true)); - ASSERT_EQ(readFromIni("interface.ini", "interface/notificationsenabled"), QVariant(false)); - ASSERT_EQ(readFromIni("interface.ini", "interface/startinghandsize"), QVariant(5)); + ASSERT_EQ(readFromIni("interface.ini", "interface/useTearOffMenus"), QVariant(true)); + ASSERT_EQ(readFromIni("interface.ini", "interface/cardViewInitialRowsMax"), QVariant(8)); + ASSERT_EQ(readFromIni("interface.ini", "interface/cardViewExpandedRowsMax"), QVariant(12)); + ASSERT_EQ(readFromIni("interface.ini", "interface/closeEmptyCardView"), QVariant(false)); + ASSERT_EQ(readFromIni("interface.ini", "interface/focusCardViewSearchBar"), QVariant(false)); + ASSERT_EQ(readFromIni("interface.ini", "interface/keepGameChatFocus"), QVariant(true)); + ASSERT_EQ(readFromIni("interface.ini", "interface/notifications/enabled"), QVariant(false)); + ASSERT_EQ(readFromIni("interface.ini", "interface/notifications/spectatorsEnabled"), QVariant(true)); + ASSERT_EQ(readFromIni("interface.ini", "interface/notifications/buddyConnectEnabled"), QVariant(false)); + ASSERT_EQ(readFromIni("interface.ini", "interface/doubleClickToPlay"), QVariant(false)); + ASSERT_EQ(readFromIni("interface.ini", "interface/clickPlaysAllSelected"), QVariant(false)); + ASSERT_EQ(readFromIni("interface.ini", "interface/playToStack"), QVariant(false)); + ASSERT_EQ(readFromIni("interface.ini", "interface/doNotDeleteArrowsInSubPhases"), QVariant(false)); + ASSERT_EQ(readFromIni("interface.ini", "interface/startingHandSize"), QVariant(5)); + ASSERT_EQ(readFromIni("interface.ini", "interface/annotateTokens"), QVariant(true)); + ASSERT_EQ(readFromIni("interface.ini", "interface/showLassoSelectionCount"), QVariant(false)); + ASSERT_EQ(readFromIni("interface.ini", "interface/showPersistentSelectionCount"), QVariant(false)); + ASSERT_EQ(readFromIni("interface.ini", "interface/tallyType"), QVariant(2)); + ASSERT_EQ(readFromIni("interface.ini", "interface/leftJustified"), QVariant(true)); + ASSERT_EQ(readFromIni("interface.ini", "interface/minPlayersMulticolumn"), QVariant(6)); + ASSERT_EQ(readFromIni("interface.ini", "interface/showStatusBar"), QVariant(true)); + ASSERT_EQ(readFromIni("interface.ini", "interface/showShortcuts"), QVariant(false)); + ASSERT_EQ(readFromIni("interface.ini", "interface/showGameSelectorFilterToolbar"), QVariant(false)); ASSERT_EQ(readFromIni("interface.ini", "hand/horizontal"), QVariant(true)); - ASSERT_EQ(readFromIni("interface.ini", "table/invert_vertical"), QVariant(true)); + ASSERT_EQ(readFromIni("interface.ini", "table/invertVertical"), QVariant(true)); + ASSERT_EQ(readFromIni("interface.ini", "replay/rewindBufferingMs"), QVariant(6000)); + ASSERT_EQ(readFromIni("interface.ini", "replay/fastForwardSpeed"), QVariant(5)); + ASSERT_EQ(readFromIni("interface.ini", "zoneview/groupBy"), QVariant(2)); + ASSERT_EQ(readFromIni("interface.ini", "zoneview/sortBy"), QVariant(1)); + ASSERT_EQ(readFromIni("interface.ini", "zoneview/pileView"), QVariant(false)); + + ASSERT_TRUE(fileExists("deck_editor.ini")); + ASSERT_EQ(readFromIni("deck_editor.ini", "deckeditor/openDeckInNewTab"), QVariant(false)); + ASSERT_EQ(readFromIni("deck_editor.ini", "deckeditor/bannerCardComboBoxVisible"), QVariant(false)); + ASSERT_EQ(readFromIni("deck_editor.ini", "deckeditor/tagsWidgetVisible"), QVariant(false)); + ASSERT_EQ(readFromIni("deck_editor.ini", "deckeditor/defaultDeckEditorType"), QVariant(0)); ASSERT_TRUE(fileExists("paths.ini")); ASSERT_EQ(readFromIni("paths.ini", "paths/decks"), QVariant("/custom/decks")); ASSERT_EQ(readFromIni("paths.ini", "paths/pics"), QVariant("/custom/pics")); ASSERT_TRUE(fileExists("visual_deck_storage.ini")); - ASSERT_EQ(readFromIni("visual_deck_storage.ini", "interface/visualdeckstoragecardsize"), QVariant(150)); - ASSERT_EQ(readFromIni("visual_deck_storage.ini", "interface/visualdeckstorageshowfolders"), QVariant(false)); + ASSERT_EQ(readFromIni("visual_deck_storage.ini", "interface/visualDeckStorage/sortingOrder"), QVariant(2)); + ASSERT_EQ(readFromIni("visual_deck_storage.ini", "interface/visualDeckStorage/showFolders"), QVariant(false)); + ASSERT_EQ(readFromIni("visual_deck_storage.ini", "interface/visualDeckStorage/showTagFilter"), QVariant(false)); + ASSERT_EQ(readFromIni("visual_deck_storage.ini", "interface/visualDeckStorage/defaultTagsList"), + QVariant(QStringList{"Alpha", "Beta"})); + ASSERT_EQ(readFromIni("visual_deck_storage.ini", "interface/visualDeckStorage/searchFolderNames"), QVariant(false)); + ASSERT_EQ(readFromIni("visual_deck_storage.ini", "interface/visualDeckStorage/showColorIdentity"), QVariant(false)); + ASSERT_EQ(readFromIni("visual_deck_storage.ini", "interface/visualDeckStorage/showBannerCardComboBox"), + QVariant(false)); + ASSERT_EQ(readFromIni("visual_deck_storage.ini", "interface/visualDeckStorage/showTagsOnDeckPreviews"), + QVariant(false)); + ASSERT_EQ(readFromIni("visual_deck_storage.ini", "interface/visualDeckStorage/drawUnusedColorIdentities"), + QVariant(false)); + ASSERT_EQ(readFromIni("visual_deck_storage.ini", "interface/visualDeckStorage/unusedColorIdentitiesOpacity"), + QVariant(35)); + ASSERT_EQ(readFromIni("visual_deck_storage.ini", "interface/visualDeckStorage/tooltipType"), QVariant(1)); + ASSERT_EQ(readFromIni("visual_deck_storage.ini", "interface/visualDeckStorage/promptForConversion"), + QVariant(false)); + ASSERT_EQ(readFromIni("visual_deck_storage.ini", "interface/visualDeckStorage/alwaysConvert"), QVariant(true)); + ASSERT_EQ(readFromIni("visual_deck_storage.ini", "interface/visualDeckStorage/inGame"), QVariant(false)); + ASSERT_EQ(readFromIni("visual_deck_storage.ini", "interface/visualDeckStorage/selectionAnimation"), + QVariant(false)); + ASSERT_EQ(readFromIni("visual_deck_storage.ini", "interface/visualDatabaseDisplay/filterToMostRecentSetsEnabled"), + QVariant(true)); + ASSERT_EQ(readFromIni("visual_deck_storage.ini", "interface/visualDatabaseDisplay/filterToMostRecentSetsAmount"), + QVariant(25)); + + // No legacy flat keys should remain in the per-file INIs + ASSERT_FALSE(iniFileHasKeyCaseSensitive("visual_deck_storage.ini", "interface/visualdeckstorageshowfolders")); + ASSERT_FALSE(iniFileHasKeyCaseSensitive("visual_deck_storage.ini", "interface/visualdeckstoragecardsize")); + ASSERT_FALSE(iniFileHasKeyCaseSensitive("deck_editor.ini", "deckeditor/sampleHandSize")); + ASSERT_FALSE(iniFileHasKeyCaseSensitive("deck_editor.ini", "deckeditor/cardSize")); + ASSERT_FALSE(iniFileHasKeyCaseSensitive("interface.ini", "interface/notificationsenabled")); + ASSERT_FALSE(iniFileHasKeyCaseSensitive("cards_display.ini", "cards/printingselectorsortorder")); + ASSERT_FALSE(iniFileHasKeyCaseSensitive("cards_display.ini", "cards/visualDeckStorage/cardSize")); + + // No legacy non-camelCase keys should remain in the per-file INIs + ASSERT_FALSE(iniFileHasKeyCaseSensitive("game.ini", "game/gamedescription")); + ASSERT_FALSE(iniFileHasKeyCaseSensitive("game.ini", "localgameoptions/maxplayers")); + ASSERT_FALSE(iniFileHasKeyCaseSensitive("chat.ini", "chat/roomhistory")); + ASSERT_FALSE(iniFileHasKeyCaseSensitive("chat.ini", "chat/highlightwords")); + ASSERT_FALSE(iniFileHasKeyCaseSensitive("sound.ini", "sound/mastervolume")); + ASSERT_FALSE(iniFileHasKeyCaseSensitive("downloads.ini", "downloads/picturedownload")); + ASSERT_FALSE(iniFileHasKeyCaseSensitive("network.ini", "network/keepalive")); + ASSERT_FALSE(iniFileHasKeyCaseSensitive("network.ini", "network/knownmissingfeatures")); + ASSERT_FALSE(iniFileHasKeyCaseSensitive("updates.ini", "updates/updatenotification")); + ASSERT_FALSE(iniFileHasKeyCaseSensitive("cards_display.ini", "cards/displaycardnames")); + ASSERT_FALSE(iniFileHasKeyCaseSensitive("cards_display.ini", "cards/cardinfoviewmode")); + ASSERT_FALSE(iniFileHasKeyCaseSensitive("interface.ini", "interface/usetearoffmenus")); + ASSERT_FALSE(iniFileHasKeyCaseSensitive("interface.ini", "interface/doubleclicktoplay")); + ASSERT_FALSE(iniFileHasKeyCaseSensitive("interface.ini", "interface/min_players_multicolumn")); + ASSERT_FALSE(iniFileHasKeyCaseSensitive("interface.ini", "table/invert_vertical")); + ASSERT_FALSE(iniFileHasKeyCaseSensitive("interface.ini", "zoneview/groupby")); + ASSERT_FALSE(iniFileHasKeyCaseSensitive("interface.ini", "zoneview/pileview")); // Verify sentinel was written ASSERT_EQ(readFromIni("global.ini", "migration/perfile_complete"), QVariant(true)); @@ -303,7 +557,7 @@ TEST_F(SettingsMigrationTest, KeyMapTranslationIsCorrect) ASSERT_TRUE(fileExists("updates.ini")); // The key should be translated from "personal/cardUpdateCheckInterval" to "updates/cardUpdateCheckInterval" ASSERT_EQ(readFromIni("updates.ini", "updates/cardUpdateCheckInterval"), QVariant(30)); - ASSERT_EQ(readFromIni("updates.ini", "updates/updatereleasechannel"), QVariant(1)); + ASSERT_EQ(readFromIni("updates.ini", "updates/updateReleaseChannel"), QVariant(1)); } TEST_F(SettingsMigrationTest, CardsKeysKeepGroupPrefix) @@ -319,18 +573,15 @@ TEST_F(SettingsMigrationTest, CardsKeysKeepGroupPrefix) ASSERT_TRUE(fileExists("global.ini.old")); ASSERT_TRUE(fileExists("cards_display.ini")); + ASSERT_TRUE(fileExists("deck_editor.ini")); // "cards/displaycardnames" should be stored with its group prefix - ASSERT_EQ(readFromIni("cards_display.ini", "cards/displaycardnames"), QVariant(false)); - // "interface/..." keys should keep their full path - ASSERT_EQ(readFromIni("cards_display.ini", "interface/deckeditorbannercardcomboboxvisible"), QVariant(true)); + ASSERT_EQ(readFromIni("cards_display.ini", "cards/displayCardNames"), QVariant(false)); + // deck editor keys belong to the deck editor settings now + ASSERT_EQ(readFromIni("deck_editor.ini", "deckeditor/bannerCardComboBoxVisible"), QVariant(true)); } TEST_F(SettingsMigrationTest, LegacyMigrationIsIdempotent) { - if (!nativeSettingsAvailable()) { - GTEST_SKIP() << "NativeFormat QSettings not available in this environment"; - } - { QSettings nativeSettings; nativeSettings.setValue("server/previoushostlogin", "test_user"); @@ -343,17 +594,62 @@ TEST_F(SettingsMigrationTest, LegacyMigrationIsIdempotent) // Change the migrated value { QSettings serversIni(settingsPath + "servers.ini", QSettings::IniFormat); - serversIni.setValue("server/previoushostlogin", "modified_user"); + serversIni.setValue("server/previousHostLogin", "modified_user"); serversIni.sync(); } // Second migration should NOT overwrite the change ASSERT_FALSE(SettingsMigration::migrateLegacySettings(settingsPath)); - ASSERT_EQ(readFromIni("servers.ini", "server/previoushostlogin"), QVariant("modified_user")); + ASSERT_EQ(readFromIni("servers.ini", "server/previousHostLogin"), QVariant("modified_user")); +} + +TEST_F(SettingsMigrationTest, LegacyMigrationCamelCasesKeys) +{ + { + QSettings nativeSettings; + nativeSettings.setValue("sets/AAA/sortkey", 2); + nativeSettings.setValue("sets/AAA/enabled", false); + nativeSettings.setValue("sets/AAA/isknown", false); + nativeSettings.setValue("server/previoushostlogin", "legacy_user"); + nativeSettings.setValue("server/auto_connect", 1); + nativeSettings.setValue("server/fpport", "5080"); + nativeSettings.setValue("messages/count", 1); + nativeSettings.setValue("messages/msg0", "hello"); + nativeSettings.setValue("filter_games/hide_full_games", true); + nativeSettings.setValue("filter_games/min_players", 3); + nativeSettings.setValue("filter_games/max_players", 5); + nativeSettings.setValue("filter_games/game_type/deadbeef", true); + nativeSettings.sync(); + } + + ASSERT_TRUE(SettingsMigration::migrateLegacySettings(settingsPath)); + + ASSERT_TRUE(fileExists("cardDatabase.ini")); + ASSERT_EQ(readFromIni("cardDatabase.ini", "sets/AAA/sortKey"), QVariant(2)); + ASSERT_EQ(readFromIni("cardDatabase.ini", "sets/AAA/enabled"), QVariant(false)); + ASSERT_EQ(readFromIni("cardDatabase.ini", "sets/AAA/isKnown"), QVariant(false)); + + ASSERT_TRUE(fileExists("servers.ini")); + ASSERT_EQ(readFromIni("servers.ini", "server/previousHostLogin"), QVariant("legacy_user")); + ASSERT_EQ(readFromIni("servers.ini", "server/autoConnect"), QVariant(1)); + ASSERT_EQ(readFromIni("servers.ini", "server/fpPort"), QVariant("5080")); + + ASSERT_TRUE(fileExists("messages.ini")); + ASSERT_EQ(readFromIni("messages.ini", "messages/count"), QVariant(1)); + ASSERT_EQ(readFromIni("messages.ini", "messages/msg0"), QVariant("hello")); + + ASSERT_TRUE(fileExists("gamefilters.ini")); + ASSERT_EQ(readFromIni("gamefilters.ini", "filter_games/hideFullGames"), QVariant(true)); + ASSERT_EQ(readFromIni("gamefilters.ini", "filter_games/minPlayers"), QVariant(3)); + ASSERT_EQ(readFromIni("gamefilters.ini", "filter_games/maxPlayers"), QVariant(5)); + ASSERT_EQ(readFromIni("gamefilters.ini", "filter_games/gameType/deadbeef"), QVariant(true)); } TEST_F(SettingsMigrationTest, LegacyMigrationEmptyNativeFormatWritesSentinel) { + QSettings().clear(); + QSettings().sync(); + ASSERT_TRUE(SettingsMigration::migrateLegacySettings(settingsPath)); ASSERT_TRUE(fileExists("personal.ini")); ASSERT_EQ(readFromIni("personal.ini", "migration/legacy_complete"), QVariant(true)); @@ -409,5 +705,6 @@ int main(int argc, char **argv) QCoreApplication app(argc, argv); app.setOrganizationName("CockatriceTest"); app.setApplicationName("SettingsMigrationTest"); + QSettings::setDefaultFormat(QSettings::IniFormat); return RUN_ALL_TESTS(); } From e9eab328a4161503f663e2ca44432e549379be5c Mon Sep 17 00:00:00 2001 From: BruebachL <44814898+BruebachL@users.noreply.github.com> Date: Sat, 8 Aug 2026 22:55:27 +0200 Subject: [PATCH 17/21] [PictureLoader] Schedule exponential backoff on 429 - Too many request handler and call failed on fail (#7053) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Took 16 minutes Co-authored-by: Lukas Brübach --- .../card_picture_loader.cpp | 20 ++- .../card_picture_loader/card_picture_loader.h | 3 + .../card_picture_loader_worker.cpp | 35 +++- .../card_picture_loader_worker.h | 15 +- .../card_picture_loader_worker_work.cpp | 110 ++++++++++++- .../card_picture_loader_worker_work.h | 18 +++ .../card_picture_to_load.cpp | 46 ++++-- .../card_picture_to_load.h | 13 ++ .../settings/download_settings.cpp | 1 + libcockatrice_utility/CMakeLists.txt | 3 +- .../utility/server_rate_limiter.cpp | 85 ++++++++++ .../utility/server_rate_limiter.h | 73 +++++++++ tests/CMakeLists.txt | 6 + tests/server_rate_limiter_test.cpp | 151 ++++++++++++++++++ 14 files changed, 558 insertions(+), 21 deletions(-) create mode 100644 libcockatrice_utility/libcockatrice/utility/server_rate_limiter.cpp create mode 100644 libcockatrice_utility/libcockatrice/utility/server_rate_limiter.h create mode 100644 tests/server_rate_limiter_test.cpp diff --git a/cockatrice/src/interface/card_picture_loader/card_picture_loader.cpp b/cockatrice/src/interface/card_picture_loader/card_picture_loader.cpp index f9391f7ce..2f46e7941 100644 --- a/cockatrice/src/interface/card_picture_loader/card_picture_loader.cpp +++ b/cockatrice/src/interface/card_picture_loader/card_picture_loader.cpp @@ -26,6 +26,9 @@ // never cache more than 300 cards at once for a single deck #define CACHED_CARD_PER_DECK_MAX 300 +// wait at least this long before retrying a card whose picture failed to load +static constexpr int RETRY_FAILED_CARDS_SECS = 300; + CardPictureLoader::CardPictureLoader() : QObject(nullptr) { worker = new CardPictureLoaderWorker; @@ -135,7 +138,14 @@ void CardPictureLoader::getPixmap(QPixmap &pixmap, const ExactCard &card, QSize QPixmap bigPixmap; if (QPixmapCache::find(key, &bigPixmap)) { if (bigPixmap.isNull()) { - qCDebug(CardPictureLoaderLog) << "Cached pixmap for key" << key << "is NULL!"; + getCardBackLoadingFailedPixmap(pixmap, size); + QDateTime failedAtTime = getInstance().failedAt.value(key); + if (!failedAtTime.isValid() || + failedAtTime.addSecs(RETRY_FAILED_CARDS_SECS) < QDateTime::currentDateTime()) { + getInstance().failedAt.remove(key); + QPixmapCache::remove(key); + getInstance().worker->enqueueImageLoad(card); + } return; } @@ -159,8 +169,10 @@ void CardPictureLoader::imageLoaded(const ExactCard &card, const QImage &image) QPixmap finalPixmap; if (image.isNull()) { + getInstance().failedAt.insert(card.getPixmapCacheKey(), QDateTime::currentDateTime()); qCDebug(CardPictureLoaderLog) << "Caching NULL pixmap for" << card.getName(); } else { + getInstance().failedAt.remove(card.getPixmapCacheKey()); if (card.getInfo().getUiAttributes().upsideDownArt) { #if (QT_VERSION >= QT_VERSION_CHECK(6, 9, 0)) QImage mirrorImage = image.flipped(Qt::Horizontal | Qt::Vertical); @@ -184,8 +196,10 @@ void CardPictureLoader::imageLoaded(const ExactCard &card, const QImage &image) // imageLoaded should only be reached if the exactCard isn't already in cache. // (plus there's a deduplication mechanism in CardPictureLoaderWorker) // It should be safe to connect the CardInfo here without worrying about redundant connections. - connect(card.getCardPtr().data(), &QObject::destroyed, this, - [cacheKey = card.getPixmapCacheKey()] { QPixmapCache::remove(cacheKey); }); + connect(card.getCardPtr().data(), &QObject::destroyed, this, [cacheKey = card.getPixmapCacheKey()] { + QPixmapCache::remove(cacheKey); + getInstance().failedAt.remove(cacheKey); + }); card.emitPixmapUpdated(); } diff --git a/cockatrice/src/interface/card_picture_loader/card_picture_loader.h b/cockatrice/src/interface/card_picture_loader/card_picture_loader.h index 0c114ae92..5c3ac84a3 100644 --- a/cockatrice/src/interface/card_picture_loader/card_picture_loader.h +++ b/cockatrice/src/interface/card_picture_loader/card_picture_loader.h @@ -4,6 +4,8 @@ #include "card_picture_loader_status_bar.h" #include "card_picture_loader_worker.h" +#include +#include #include inline Q_LOGGING_CATEGORY(CardPictureLoaderLog, "card_picture_loader"); @@ -56,6 +58,7 @@ private: CardPictureLoaderWorker *worker; ///< Worker thread for async image loading CardPictureLoaderStatusBar *statusBar; ///< Status bar widget showing load progress + QHash failedAt; ///< Timestamp of the last failed load attempt per pixmap cache key public: /** diff --git a/cockatrice/src/interface/card_picture_loader/card_picture_loader_worker.cpp b/cockatrice/src/interface/card_picture_loader/card_picture_loader_worker.cpp index 8b121d91c..d288236d2 100644 --- a/cockatrice/src/interface/card_picture_loader/card_picture_loader_worker.cpp +++ b/cockatrice/src/interface/card_picture_loader/card_picture_loader_worker.cpp @@ -17,6 +17,8 @@ #include static constexpr int MAX_REQUESTS_PER_SEC = 10; +static constexpr int MIN_HOST_QUOTA = 1; ///< Floor for the per-host request allowance +static constexpr qint64 QUOTA_RECOVER_MS = 60000; ///< Idle time before a reduced quota starts recovering CardPictureLoaderWorker::CardPictureLoaderWorker() : QObject(nullptr), picDownload(SettingsCache::instance().downloads().getPicDownload()), @@ -124,6 +126,19 @@ QNetworkReply *CardPictureLoaderWorker::makeRequest(const QUrl &url, CardPicture void CardPictureLoaderWorker::resetRequestQuota() { requestQuota = MAX_REQUESTS_PER_SEC; + + QDateTime now = QDateTime::currentDateTime(); + for (auto it = hostRequestQuota.begin(); it != hostRequestQuota.end(); ++it) { + if (!hostLast429.contains(it.key()) || now.msecsTo(hostLast429.value(it.key())) < -QUOTA_RECOVER_MS) { + it.value() = qMin(MAX_REQUESTS_PER_SEC, it.value() + 1); + } + } + + for (const auto &request : requestLoadQueue) { + const QString host = request.first.host(); + hostQuotaRemaining.insert(host, hostRequestQuota.value(host, MAX_REQUESTS_PER_SEC)); + } + processQueuedRequests(); } @@ -136,14 +151,26 @@ void CardPictureLoaderWorker::processQueuedRequests() bool CardPictureLoaderWorker::processSingleRequest() { - if (!requestLoadQueue.isEmpty()) { - auto request = requestLoadQueue.takeFirst(); - makeRequest(request.first, request.second); - return true; + for (int i = 0; i < requestLoadQueue.size(); ++i) { + const auto &request = requestLoadQueue.at(i); + QString host = request.first.host(); + int allowance = hostQuotaRemaining.value(host, MAX_REQUESTS_PER_SEC); + if (allowance > 0) { + hostQuotaRemaining.insert(host, allowance - 1); + makeRequest(request.first, request.second); + requestLoadQueue.removeAt(i); + return true; + } } return false; } +void CardPictureLoaderWorker::onHostRateLimited(const QString &host) +{ + hostRequestQuota.insert(host, qMax(MIN_HOST_QUOTA, hostRequestQuota.value(host, MAX_REQUESTS_PER_SEC) / 2)); + hostLast429.insert(host, QDateTime::currentDateTime()); +} + void CardPictureLoaderWorker::enqueueImageLoad(const ExactCard &card) { // Send call through a connection to ensure the handling is run on the pictureLoader thread diff --git a/cockatrice/src/interface/card_picture_loader/card_picture_loader_worker.h b/cockatrice/src/interface/card_picture_loader/card_picture_loader_worker.h index f927abde6..d1c519b7a 100644 --- a/cockatrice/src/interface/card_picture_loader/card_picture_loader_worker.h +++ b/cockatrice/src/interface/card_picture_loader/card_picture_loader_worker.h @@ -5,6 +5,8 @@ #include "card_picture_loader_worker_work.h" #include "card_picture_to_load.h" +#include +#include #include #include #include @@ -66,6 +68,12 @@ public: */ void queueRequest(const QUrl &url, CardPictureLoaderWorkerWork *worker); + /** + * @brief Handles a server returning HTTP 429 by reducing that host's request quota. + * @param host The host that returned 429 + */ + void onHostRateLimited(const QString &host); + /** @brief Clears the network cache and redirect cache. */ void clearNetworkCache(); @@ -110,8 +118,11 @@ private: bool picDownload; ///< Whether downloading images from network is enabled QQueue> requestLoadQueue; ///< Queue of pending network requests - int requestQuota; ///< Remaining requests allowed per second - QTimer requestTimer; ///< Timer to reset the request quota + int requestQuota; ///< Remaining requests allowed per second + QTimer requestTimer; ///< Timer to reset the request quota + QHash hostRequestQuota; ///< Sustained per-host request allowance + QHash hostQuotaRemaining; ///< Per-host allowance left in the current second + QHash hostLast429; ///< When each host was last rate limited CardPictureLoaderLocal *localLoader; ///< Loader for local images QSet currentlyLoading; ///< Deduplication: contains pixmapCacheKey currently being loaded diff --git a/cockatrice/src/interface/card_picture_loader/card_picture_loader_worker_work.cpp b/cockatrice/src/interface/card_picture_loader/card_picture_loader_worker_work.cpp index bfd46a462..66c56337c 100644 --- a/cockatrice/src/interface/card_picture_loader/card_picture_loader_worker_work.cpp +++ b/cockatrice/src/interface/card_picture_loader/card_picture_loader_worker_work.cpp @@ -8,8 +8,12 @@ #include #include #include +#include #include #include +#include + +ServerRateLimiter CardPictureLoaderWorkerWork::s_rateLimiter; #include // Card back returned by gatherer when card is not found @@ -30,6 +34,7 @@ CardPictureLoaderWorkerWork::CardPictureLoaderWorkerWork(const CardPictureLoader connect(this, &CardPictureLoaderWorkerWork::imageLoaded, worker, &CardPictureLoaderWorker::handleImageLoaded); connect(this, &CardPictureLoaderWorkerWork::requestSucceeded, worker, &CardPictureLoaderWorker::imageRequestSucceeded); + connect(this, &CardPictureLoaderWorkerWork::rateLimited, worker, &CardPictureLoaderWorker::onHostRateLimited); // Hook up signals to settings connect(&SettingsCache::instance().downloads(), SIGNAL(picDownloadChanged()), this, SLOT(picDownloadChanged())); @@ -39,10 +44,38 @@ CardPictureLoaderWorkerWork::CardPictureLoaderWorkerWork(const CardPictureLoader void CardPictureLoaderWorkerWork::startNextPicDownload() { + QDateTime now = QDateTime::currentDateTime(); + while (!cardToDownload.getCurrentUrl().isEmpty() && + s_rateLimiter.isRateLimited(QUrl(cardToDownload.getCurrentUrl()).host(), now)) { + QString host = QUrl(cardToDownload.getCurrentUrl()).host(); + if (s_rateLimiter.rounds(host) == 1) { + // First 429 round for this server: wait out the backoff and give it + // one more chance instead of immediately falling through to a worse + // source. A second 429 makes us fall through instead. + qCDebug(CardPictureLoaderWorkerWorkLog).nospace() + << "PictureLoader: [card: " << cardToDownload.getCard().getInfo().getCorrectedName() + << " set: " << cardToDownload.getSetName() << "]: Waiting out backoff for " << host << " to retry " + << cardToDownload.getCurrentUrl(); + scheduleDeferredRetry(); + return; + } + + // The server has already 429'd us at least twice, so further retries are + // unlikely to succeed: move on to the other configured sources. + qCDebug(CardPictureLoaderWorkerWorkLog).nospace() + << "PictureLoader: [card: " << cardToDownload.getCard().getInfo().getCorrectedName() + << " set: " << cardToDownload.getSetName() << "]: Skipping rate-limited URL " + << cardToDownload.getCurrentUrl() << " (server " << host << " still rate limiting)"; + if (!cardToDownload.nextUrl() && !cardToDownload.nextSet()) { + scheduleDeferredRetry(); + return; + } + } + QString picUrl = cardToDownload.getCurrentUrl(); if (picUrl.isEmpty()) { - picDownloadFailed(); + scheduleDeferredRetry(); } else { QUrl url(picUrl); qCDebug(CardPictureLoaderWorkerWorkLog).nospace() @@ -108,7 +141,41 @@ static bool imageIsBlackListed(const QByteArray &picData) void CardPictureLoaderWorkerWork::handleFailedReply(const QNetworkReply *reply) { if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 429) { - qCWarning(CardPictureLoaderWorkerWorkLog) << "Too many requests."; + QString host = reply->url().host(); + QDateTime now = QDateTime::currentDateTime(); + + qint64 retryAfterMs = 0; + const QByteArray retryAfterHeader = reply->rawHeader("Retry-After"); + if (!retryAfterHeader.isEmpty()) { + bool ok = false; + int seconds = retryAfterHeader.toInt(&ok); + if (ok && seconds > 0) { + retryAfterMs = static_cast(seconds) * 1000; + } else { + QDateTime retryAfterDate = + QDateTime::fromString(QString::fromLatin1(retryAfterHeader), Qt::RFC2822Date); + if (retryAfterDate.isValid()) { + retryAfterMs = qMax(0, now.msecsTo(retryAfterDate)); + } + } + } + + QDateTime backoffUntil = s_rateLimiter.on429(host, now, retryAfterMs); + emit rateLimited(host); + + if (s_rateLimiter.rounds(host) == 1) { + qCWarning(CardPictureLoaderWorkerWorkLog).nospace() + << "PictureLoader: [card: " << cardToDownload.getCard().getName() + << " set: " << cardToDownload.getSetName() << "]: Too many requests from " << host + << ", backing off until " << backoffUntil.toString(Qt::ISODate) << ", retrying the same url"; + scheduleDeferredRetry(); + } else { + qCWarning(CardPictureLoaderWorkerWorkLog).nospace() + << "PictureLoader: [card: " << cardToDownload.getCard().getName() + << " set: " << cardToDownload.getSetName() << "]: Too many requests from " << host + << ", retry already attempted, falling through to other sources"; + picDownloadFailed(); + } } else { bool isFromCache = reply->attribute(QNetworkRequest::SourceIsFromCacheAttribute).toBool(); @@ -149,6 +216,9 @@ void CardPictureLoaderWorkerWork::handleSuccessfulReply(QNetworkReply *reply) return; } + // A non-redirect successful response means the server is not rate limiting us anymore. + s_rateLimiter.onSuccess(reply->url().host()); + // peek is used to keep the data in the buffer for use by QImageReader const QByteArray &picData = reply->peek(reply->size()); @@ -203,6 +273,42 @@ QImage CardPictureLoaderWorkerWork::tryLoadImageFromReply(QNetworkReply *reply) return imgReader.read(); } +void CardPictureLoaderWorkerWork::scheduleDeferredRetry() +{ + QDateTime now = QDateTime::currentDateTime(); + + // Prefer waiting on the current URL's server so we retry the same source. + QString currentHost = QUrl(cardToDownload.getCurrentUrl()).host(); + QDateTime backoffUntil = s_rateLimiter.deadline(currentHost); + if (!s_rateLimiter.isRateLimited(currentHost, now)) { + backoffUntil = s_rateLimiter.earliestDeadline(now); + } + + if (!backoffUntil.isValid()) { + qCWarning(CardPictureLoaderWorkerWorkLog).nospace() + << "PictureLoader: [card: " << cardToDownload.getCard().getInfo().getCorrectedName() + << " set: " << cardToDownload.getSetName() << "]: All URLs exhausted, no servers in backoff: BAILING OUT"; + concludeImageLoad(QImage()); + return; + } + + qint64 waitMs = qMax(0, now.msecsTo(backoffUntil)); + // Add some jitter to desynchronize concurrent retries and avoid a thundering herd. + waitMs += QRandomGenerator::global()->bounded(5000); + + qCDebug(CardPictureLoaderWorkerWorkLog).nospace() + << "PictureLoader: [card: " << cardToDownload.getCard().getInfo().getCorrectedName() + << " set: " << cardToDownload.getSetName() << "]: All URLs exhausted, scheduling deferred retry in " << waitMs + << "ms"; + + QTimer::singleShot(waitMs, this, [this] { + s_rateLimiter.clearExpired(QDateTime::currentDateTime()); + + cardToDownload.resetIndices(); + startNextPicDownload(); + }); +} + void CardPictureLoaderWorkerWork::concludeImageLoad(const QImage &image) { emit imageLoaded(cardToDownload.getCard(), image); diff --git a/cockatrice/src/interface/card_picture_loader/card_picture_loader_worker_work.h b/cockatrice/src/interface/card_picture_loader/card_picture_loader_worker_work.h index cdffc1dff..1e56a4373 100644 --- a/cockatrice/src/interface/card_picture_loader/card_picture_loader_worker_work.h +++ b/cockatrice/src/interface/card_picture_loader/card_picture_loader_worker_work.h @@ -4,12 +4,15 @@ #include "card_picture_loader_worker.h" #include "card_picture_to_load.h" +#include #include #include #include #include +#include #include #include +#include inline Q_LOGGING_CATEGORY(CardPictureLoaderWorkerWorkLog, "card_picture_loader.worker"); @@ -50,6 +53,8 @@ public slots: private: bool picDownload; ///< Whether network downloading is enabled + static ServerRateLimiter s_rateLimiter; ///< Shared per-server 429 backoff state + /** @brief Starts downloading the next URL for this card. */ void startNextPicDownload(); @@ -77,6 +82,16 @@ private: */ void concludeImageLoad(const QImage &image); + /** + * @brief Schedules a deferred retry after the relevant server backoff expires. + * + * Waits on the current URL's server when it is the reason we are blocked, + * otherwise on the earliest active backoff. If no servers are in backoff, + * concludes with failure. Otherwise resets the CardPictureToLoad indices and + * retries after the backoff period. + */ + void scheduleDeferredRetry(); + private slots: /** @brief Updates the picDownload setting when it changes. */ void picDownloadChanged(); @@ -100,6 +115,9 @@ signals: /** @brief Emitted when a URL has been redirected. */ void urlRedirected(const QUrl &originalUrl, const QUrl &redirectUrl); + /** @brief Emitted when a server returned HTTP 429. */ + void rateLimited(const QString &host); + /** @brief Emitted when a cached URL is invalid and must be removed. */ void cachedUrlInvalidated(const QUrl &url); }; diff --git a/cockatrice/src/interface/card_picture_loader/card_picture_to_load.cpp b/cockatrice/src/interface/card_picture_loader/card_picture_to_load.cpp index 33e4aabdb..5f4ff0bbd 100644 --- a/cockatrice/src/interface/card_picture_loader/card_picture_to_load.cpp +++ b/cockatrice/src/interface/card_picture_loader/card_picture_to_load.cpp @@ -17,8 +17,9 @@ CardPictureToLoad::CardPictureToLoad(const ExactCard &_card) { if (card) { sortedSets = extractSetsSorted(card); - // The first time called, nextSet will also populate the Urls for the first set. - nextSet(); + currentSetIndex = 0; + currentSet = sortedSets.first(); + populateSetUrls(); } } @@ -101,15 +102,19 @@ void CardPictureToLoad::populateSetUrls() } } - /* Call nextUrl to make sure currentUrl is up-to-date - but we don't need the result here. */ - (void)nextUrl(); + currentUrlIndex = 0; + if (!currentSetUrls.isEmpty()) { + currentUrl = currentSetUrls.first(); + } else { + currentUrl = QString(); + } } bool CardPictureToLoad::nextSet() { - if (!sortedSets.isEmpty()) { - currentSet = sortedSets.takeFirst(); + currentSetIndex++; + if (currentSetIndex < sortedSets.size()) { + currentSet = sortedSets.at(currentSetIndex); populateSetUrls(); return true; } @@ -119,8 +124,9 @@ bool CardPictureToLoad::nextSet() bool CardPictureToLoad::nextUrl() { - if (!currentSetUrls.isEmpty()) { - currentUrl = currentSetUrls.takeFirst(); + currentUrlIndex++; + if (currentUrlIndex < currentSetUrls.size()) { + currentUrl = currentSetUrls.at(currentUrlIndex); return true; } currentUrl = QString(); @@ -136,6 +142,28 @@ QString CardPictureToLoad::getSetName() const } } +QString CardPictureToLoad::peekNextUrl() const +{ + int nextIndex = currentUrlIndex + 1; + if (nextIndex < currentSetUrls.size()) { + return currentSetUrls.at(nextIndex); + } + return QString(); +} + +void CardPictureToLoad::resetIndices() +{ + currentSetIndex = 0; + if (!sortedSets.isEmpty()) { + currentSet = sortedSets.first(); + populateSetUrls(); + } else { + currentSet = {}; + currentSetUrls.clear(); + currentUrl = QString(); + } +} + static int parse(const QString &urlTemplate, const QString &propType, const QString &cardName, diff --git a/cockatrice/src/interface/card_picture_loader/card_picture_to_load.h b/cockatrice/src/interface/card_picture_loader/card_picture_to_load.h index b57e57644..9e0e7449c 100644 --- a/cockatrice/src/interface/card_picture_loader/card_picture_to_load.h +++ b/cockatrice/src/interface/card_picture_loader/card_picture_to_load.h @@ -25,6 +25,8 @@ private: QList currentSetUrls; ///< URLs for the current set being attempted QString currentUrl; ///< Currently active URL to download CardSetPtr currentSet; ///< Currently active set + int currentSetIndex = 0; ///< Current position in sortedSets + int currentUrlIndex = 0; ///< Current position in currentSetUrls public: /** @@ -56,6 +58,9 @@ public: /** @return The short name of the current set, or empty string if no set. */ [[nodiscard]] QString getSetName() const; + /** @return The next URL in the current set's list without advancing, or empty if at end. */ + [[nodiscard]] QString peekNextUrl() const; + /** * @brief Transforms a URL template into a concrete URL for this card/set. * @param urlTemplate The URL template to transform @@ -88,6 +93,14 @@ public: */ void populateSetUrls(); + /** + * @brief Resets iteration indices to the beginning. + * + * Restarts URL/set iteration from the first set and first URL. + * Used for deferred retry after server backoff expires. + */ + void resetIndices(); + /** * @brief Extract all sets from the card and sort them by priority. * @param card The card to extract sets from diff --git a/libcockatrice_settings/libcockatrice/settings/download_settings.cpp b/libcockatrice_settings/libcockatrice/settings/download_settings.cpp index 919199126..cfa1c054e 100644 --- a/libcockatrice_settings/libcockatrice/settings/download_settings.cpp +++ b/libcockatrice_settings/libcockatrice/settings/download_settings.cpp @@ -3,6 +3,7 @@ #include "settings_manager.h" const QStringList DownloadSettings::DEFAULT_DOWNLOAD_URLS = { + "https://cards.scryfall.io/large/!prop:side!/!set:uuid_substr_0_1!/!set:uuid_substr_1_1!/!set:uuid!.jpg", "https://api.scryfall.com/cards/!set:uuid!?format=image&face=!prop:side!", "https://api.scryfall.com/cards/multiverse/!set:muid!?format=image", "https://gatherer.wizards.com/Handlers/Image.ashx?multiverseid=!set:muid!&type=card", diff --git a/libcockatrice_utility/CMakeLists.txt b/libcockatrice_utility/CMakeLists.txt index 2d34cad31..79f5a11e4 100644 --- a/libcockatrice_utility/CMakeLists.txt +++ b/libcockatrice_utility/CMakeLists.txt @@ -6,7 +6,7 @@ set(CMAKE_AUTOUIC ON) set(CMAKE_AUTORCC ON) set(UTILITY_SOURCES libcockatrice/utility/expression.cpp libcockatrice/utility/levenshtein.cpp - libcockatrice/utility/passwordhasher.cpp + libcockatrice/utility/passwordhasher.cpp libcockatrice/utility/server_rate_limiter.cpp ) set(UTILITY_HEADERS @@ -21,6 +21,7 @@ set(UTILITY_HEADERS libcockatrice/utility/clamped_arithmetic.h libcockatrice/utility/zone_names.h libcockatrice/utility/days_years_between.h + libcockatrice/utility/server_rate_limiter.h ) add_library(libcockatrice_utility STATIC ${UTILITY_SOURCES} ${UTILITY_HEADERS}) diff --git a/libcockatrice_utility/libcockatrice/utility/server_rate_limiter.cpp b/libcockatrice_utility/libcockatrice/utility/server_rate_limiter.cpp new file mode 100644 index 000000000..2a1e3b283 --- /dev/null +++ b/libcockatrice_utility/libcockatrice/utility/server_rate_limiter.cpp @@ -0,0 +1,85 @@ +#include "server_rate_limiter.h" + +bool ServerRateLimiter::isRateLimited(const QString &host, const QDateTime &now) const +{ + auto it = backoffUntil.constFind(host); + return it != backoffUntil.constEnd() && now < it.value(); +} + +int ServerRateLimiter::rounds(const QString &host) const +{ + return retryRounds.value(host, 0); +} + +QDateTime ServerRateLimiter::deadline(const QString &host) const +{ + return backoffUntil.value(host); +} + +QDateTime ServerRateLimiter::earliestDeadline(const QDateTime &now) const +{ + QDateTime earliest; + for (auto it = backoffUntil.cbegin(); it != backoffUntil.cend(); ++it) { + if (now < it.value() && (!earliest.isValid() || it.value() < earliest)) { + earliest = it.value(); + } + } + return earliest; +} + +QDateTime ServerRateLimiter::on429(const QString &host, const QDateTime &now, qint64 retryAfterMs) +{ + QDateTime existing = backoffUntil.value(host); + int round = retryRounds.value(host, 0); + + if (!existing.isValid() || now >= existing) { + if (last429.value(host).isValid() && now >= last429.value(host).addMSecs(RESET_GRACE_MS)) { + round = 0; + } + round = qMin(round + 1, MAX_RETRIES); + retryRounds.insert(host, round); + } + last429.insert(host, now); + + qint64 delay; + if (round >= MAX_RETRIES) { + delay = EXHAUSTED_BACKOFF_MS; + } else { + delay = qMax(MIN_429_BACKOFF_MS, retryAfterMs); + delay = qMin(delay, MAX_BACKOFF_MS); + } + + QDateTime deadline = now.addMSecs(delay); + if (existing.isValid() && existing > deadline) { + deadline = existing; // never shorten an active backoff + } + backoffUntil.insert(host, deadline); + + return deadline; +} + +void ServerRateLimiter::onSuccess(const QString &host) +{ + backoffUntil.remove(host); + retryRounds.remove(host); + last429.remove(host); +} + +void ServerRateLimiter::clearExpired(const QDateTime &now) +{ + auto it = backoffUntil.begin(); + while (it != backoffUntil.end()) { + if (now < it.value()) { + ++it; + continue; + } + + QString host = it.key(); + bool budgetStillFresh = last429.value(host).isValid() && now < last429.value(host).addMSecs(RESET_GRACE_MS); + it = backoffUntil.erase(it); + if (!budgetStillFresh) { + retryRounds.remove(host); + last429.remove(host); + } + } +} diff --git a/libcockatrice_utility/libcockatrice/utility/server_rate_limiter.h b/libcockatrice_utility/libcockatrice/utility/server_rate_limiter.h new file mode 100644 index 000000000..e071b9189 --- /dev/null +++ b/libcockatrice_utility/libcockatrice/utility/server_rate_limiter.h @@ -0,0 +1,73 @@ +#ifndef SERVER_RATE_LIMITER_H +#define SERVER_RATE_LIMITER_H + +#include +#include +#include + +/** + * @class ServerRateLimiter + * @ingroup Utility + * @brief Tracks per-server backoff state triggered by HTTP 429 responses. + * + * Keeps a monotonic backoff deadline per host and a retry-round budget that + * escalates once per backoff window (a burst of concurrent 429s counts as a + * single round). The budget is refreshed after a long period without 429s so a + * server is not permanently blacklisted after a past overload. + */ +class ServerRateLimiter +{ +public: + static constexpr int MAX_RETRIES = 5; ///< Max 429 rounds per host before the budget is exhausted + static constexpr int MIN_429_BACKOFF_MS = 30000; ///< Minimum wait after a 429 (scryfall documented cool-down) + static constexpr int MAX_BACKOFF_MS = 60000; ///< Cap for a single backoff period + static constexpr int EXHAUSTED_BACKOFF_MS = 60000; ///< Cool-down once the retry budget is exhausted + static constexpr qint64 RESET_GRACE_MS = 300000; ///< Idle time after which the retry budget refreshes + + /** + * @brief Checks whether a host is currently in backoff. + * @param host The host to check + * @param now The current time + * @return True if requests to the host should be paused + */ + [[nodiscard]] bool isRateLimited(const QString &host, const QDateTime &now) const; + + /** @return The number of consecutive 429 rounds recorded for the host. */ + [[nodiscard]] int rounds(const QString &host) const; + + /** @return The current backoff deadline for the host, or an invalid QDateTime. */ + [[nodiscard]] QDateTime deadline(const QString &host) const; + + /** @return The earliest active backoff deadline across all hosts, or an invalid QDateTime. */ + [[nodiscard]] QDateTime earliestDeadline(const QDateTime &now) const; + + /** + * @brief Registers a 429 response for the given host. + * @param host The host that returned 429 + * @param now The time the response was received + * @param retryAfterMs A Retry-After hint in milliseconds, or 0 if absent + * @return The new backoff deadline for the host + * + * The round counter only escalates once the previous backoff window has + * fully passed, so concurrent 429s from a burst do not consume the whole + * budget. The deadline is extended monotonically and never shortened. + */ + QDateTime on429(const QString &host, const QDateTime &now, qint64 retryAfterMs = 0); + + /** @brief Clears all penalty state for a host after a successful request. */ + void onSuccess(const QString &host); + + /** + * @brief Removes expired backoffs, refreshing the round budget of hosts + * that have been idle past the reset grace period. + * @param now The current time + */ + void clearExpired(const QDateTime &now); + +private: + QMap backoffUntil; ///< When each host may be contacted again + QMap retryRounds; ///< Consecutive 429 rounds per host + QMap last429; ///< When each host was last rate limited +}; + +#endif // SERVER_RATE_LIMITER_H diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 857e0b041..1c3f4c2c6 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -9,6 +9,7 @@ add_test(NAME test_age_formatting COMMAND test_age_formatting) add_test(NAME password_hash_test COMMAND password_hash_test) add_test(NAME server_card_counter_test COMMAND server_card_counter_test) add_test(NAME server_counter_test COMMAND server_counter_test) +add_test(NAME server_rate_limiter_test COMMAND server_rate_limiter_test) add_test(NAME deck_hash_performance_test COMMAND deck_hash_performance_test) set_tests_properties(deck_hash_performance_test PROPERTIES TIMEOUT 5) @@ -23,6 +24,7 @@ add_executable(password_hash_test password_hash_test.cpp) add_executable(deck_hash_performance_test deck_hash_performance_test.cpp) add_executable(server_card_counter_test server_card_counter_test.cpp) add_executable(server_counter_test server_counter_test.cpp) +add_executable(server_rate_limiter_test server_rate_limiter_test.cpp) find_package(GTest) @@ -57,6 +59,7 @@ if(NOT GTEST_FOUND) add_dependencies(deck_hash_performance_test gtest) add_dependencies(server_card_counter_test gtest) add_dependencies(server_counter_test gtest) + add_dependencies(server_rate_limiter_test gtest) endif() include_directories(${GTEST_INCLUDE_DIRS}) @@ -81,6 +84,9 @@ target_link_libraries( target_link_libraries( server_counter_test libcockatrice_network Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES} ) +target_link_libraries( + server_rate_limiter_test libcockatrice_utility Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES} +) add_subdirectory(card_zone_algorithms) add_subdirectory(carddatabase) diff --git a/tests/server_rate_limiter_test.cpp b/tests/server_rate_limiter_test.cpp new file mode 100644 index 000000000..bc15ca421 --- /dev/null +++ b/tests/server_rate_limiter_test.cpp @@ -0,0 +1,151 @@ +#include "gtest/gtest.h" +#include +#include +#include + +namespace +{ + +const QString HOST = "api.scryfall.com"; + +QDateTime timeAt(qint64 secsFromEpoch) +{ + return QDateTime::fromMSecsSinceEpoch(secsFromEpoch * 1000, QTimeZone::UTC); +} + +TEST(ServerRateLimiterTest, First429SetsThirtySecondBackoff) +{ + ServerRateLimiter limiter; + QDateTime now = timeAt(1000); + + QDateTime deadline = limiter.on429(HOST, now); + + EXPECT_TRUE(limiter.isRateLimited(HOST, now)); + EXPECT_EQ(now.addSecs(30), deadline); + EXPECT_EQ(1, limiter.rounds(HOST)); + EXPECT_FALSE(limiter.isRateLimited(HOST, deadline)); +} + +TEST(ServerRateLimiterTest, RetryAfterHeaderIsHonoredWhenLarger) +{ + ServerRateLimiter limiter; + QDateTime now = timeAt(1000); + + QDateTime deadline = limiter.on429(HOST, now, 45 * 1000); + + EXPECT_EQ(now.addSecs(45), deadline); +} + +TEST(ServerRateLimiterTest, RetryAfterSmallerThanFloorIsIgnored) +{ + ServerRateLimiter limiter; + QDateTime now = timeAt(1000); + + QDateTime deadline = limiter.on429(HOST, now, 5 * 1000); + + EXPECT_EQ(now.addSecs(30), deadline); +} + +TEST(ServerRateLimiterTest, Concurrent429sDoNotEscalateOrShorten) +{ + ServerRateLimiter limiter; + QDateTime now = timeAt(1000); + + QDateTime first = limiter.on429(HOST, now); + limiter.on429(HOST, now.addMSecs(100)); + limiter.on429(HOST, now.addMSecs(200)); + + // The burst counts as a single round, but each 429 slides the deadline forward. + EXPECT_EQ(1, limiter.rounds(HOST)); + EXPECT_EQ(now.addMSecs(30200), limiter.deadline(HOST)); + EXPECT_TRUE(limiter.deadline(HOST) >= first); +} + +TEST(ServerRateLimiterTest, EscalatesAfterWindowPasses) +{ + ServerRateLimiter limiter; + QDateTime now = timeAt(1000); + + limiter.on429(HOST, now); + limiter.on429(HOST, now.addSecs(31)); + + EXPECT_EQ(2, limiter.rounds(HOST)); +} + +TEST(ServerRateLimiterTest, BudgetExhaustionAppliesCooldown) +{ + ServerRateLimiter limiter; + QDateTime now = timeAt(1000); + + QDateTime deadline; + for (int i = 0; i < ServerRateLimiter::MAX_RETRIES; ++i) { + deadline = limiter.on429(HOST, now.addSecs(31 * i)); + } + + EXPECT_EQ(ServerRateLimiter::MAX_RETRIES, limiter.rounds(HOST)); + EXPECT_EQ(now.addSecs(31 * (ServerRateLimiter::MAX_RETRIES - 1) + 60), deadline); + + // A 429 inside the exhausted cooldown does not escalate, but keeps sliding the cooldown forward. + limiter.on429(HOST, now.addSecs(31 * (ServerRateLimiter::MAX_RETRIES - 1) + 30)); + EXPECT_EQ(ServerRateLimiter::MAX_RETRIES, limiter.rounds(HOST)); + EXPECT_EQ(now.addSecs(31 * (ServerRateLimiter::MAX_RETRIES - 1) + 30 + 60), limiter.deadline(HOST)); +} + +TEST(ServerRateLimiterTest, SuccessClearsPenalty) +{ + ServerRateLimiter limiter; + QDateTime now = timeAt(1000); + + limiter.on429(HOST, now); + limiter.onSuccess(HOST); + + EXPECT_EQ(0, limiter.rounds(HOST)); + EXPECT_FALSE(limiter.isRateLimited(HOST, now)); +} + +TEST(ServerRateLimiterTest, ClearExpiredRefreshesStaleBudget) +{ + ServerRateLimiter limiter; + QDateTime now = timeAt(1000); + + limiter.on429(HOST, now); + limiter.clearExpired(now.addSecs(400)); + + EXPECT_EQ(0, limiter.rounds(HOST)); + EXPECT_FALSE(limiter.isRateLimited(HOST, now.addSecs(400))); +} + +TEST(ServerRateLimiterTest, ClearExpiredKeepsRecentBudget) +{ + ServerRateLimiter limiter; + QDateTime now = timeAt(1000); + + for (int i = 0; i < ServerRateLimiter::MAX_RETRIES; ++i) { + limiter.on429(HOST, now.addSecs(31 * i)); + } + limiter.clearExpired(now.addSecs(200)); + + EXPECT_EQ(ServerRateLimiter::MAX_RETRIES, limiter.rounds(HOST)); + EXPECT_FALSE(limiter.isRateLimited(HOST, now.addSecs(200))); +} + +TEST(ServerRateLimiterTest, EarliestDeadlineAcrossHosts) +{ + ServerRateLimiter limiter; + QDateTime now = timeAt(1000); + + limiter.on429("api.scryfall.com", now, 40 * 1000); + limiter.on429("gatherer.wizards.com", now.addSecs(5)); + + EXPECT_EQ(now.addSecs(35), limiter.earliestDeadline(now)); + EXPECT_EQ(now.addSecs(40), limiter.deadline("api.scryfall.com")); + EXPECT_EQ(now.addSecs(40), limiter.earliestDeadline(now.addSecs(36))); +} + +} // namespace + +int main(int argc, char **argv) +{ + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} From 827e44a4d32096daeeaae1c7ac69df10d720a0f7 Mon Sep 17 00:00:00 2001 From: BruebachL <44814898+BruebachL@users.noreply.github.com> Date: Sun, 9 Aug 2026 00:46:17 +0200 Subject: [PATCH 18/21] [DeckEditor] Use CommanderSpellbook.com to estimate bracket if format is 'commander' (#6415) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [DeckEditor] Use CommanderSpellbook.com to estimate bracket if format is 'commander' Took 2 minutes Took 16 minutes * Convert json data holder to structs, rename variables, extract widget - Extract bracket estimation UI from DeckEditorDeckDockWidget into a new CommanderBracketWidget - Move CommanderSpellbook integration settings from CardsDisplaySettings to DeckEditorSettings (matching the settings refactor on master) - Rename CommanderSpellbook integration variables to drop the redundant 'DeckEditor' prefix Took 4 minutes # Commit time for manual adjustment: # Took 6 minutes # Commit time for manual adjustment: # Took 8 minutes --------- Co-authored-by: Lukas Brübach --- cockatrice/CMakeLists.txt | 10 + .../src/client/settings/cache_settings.cpp | 8 + .../src/client/settings/cache_settings.h | 6 + .../deck_editor_deck_dock_widget.cpp | 29 +- .../deck_editor_deck_dock_widget.h | 3 + .../widgets/deck_editor/deck_state_manager.h | 8 + .../user_interface_settings_page.cpp | 93 ++++++ .../user_interface_settings_page.h | 8 + .../api_response/card_in_deck_request.cpp | 17 ++ .../api_response/card_in_deck_request.h | 14 + .../commander_spellbook_card_result.cpp | 26 ++ .../commander_spellbook_card_result.h | 29 ++ .../commander_spellbook_deck_request.cpp | 95 +++++++ .../commander_spellbook_deck_request.h | 19 ++ ...nder_spellbook_estimate_bracket_result.cpp | 93 ++++++ ...mander_spellbook_estimate_bracket_result.h | 32 +++ .../commander_spellbook_variant_result.cpp | 35 +++ .../commander_spellbook_variant_result.h | 39 +++ .../commander_bracket_service.cpp | 53 ++++ .../commander_bracket_service.h | 45 +++ .../commander_bracket_widget.cpp | 269 ++++++++++++++++++ .../commander_bracket_widget.h | 46 +++ .../commander_spellbook_api_accessor.cpp | 75 +++++ .../commander_spellbook_api_accessor.h | 37 +++ .../commander_spellbook_bracket_explainer.cpp | 120 ++++++++ .../commander_spellbook_bracket_explainer.h | 43 +++ .../handle_commander_brackets.cpp | 62 ++++ .../handle_commander_brackets.h | 31 ++ cockatrice/src/interface/window_main.cpp | 14 + cockatrice/src/interface/window_main.h | 1 + libcockatrice_settings/CMakeLists.txt | 2 + .../settings/commander_bracket_settings.cpp | 194 +++++++++++++ .../settings/commander_bracket_settings.h | 56 ++++ .../settings/deck_editor_settings.cpp | 24 ++ .../settings/deck_editor_settings.h | 14 + 35 files changed, 1642 insertions(+), 8 deletions(-) create mode 100644 cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/card_in_deck_request.cpp create mode 100644 cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/card_in_deck_request.h create mode 100644 cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_card_result.cpp create mode 100644 cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_card_result.h create mode 100644 cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_deck_request.cpp create mode 100644 cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_deck_request.h create mode 100644 cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_estimate_bracket_result.cpp create mode 100644 cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_estimate_bracket_result.h create mode 100644 cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_variant_result.cpp create mode 100644 cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_variant_result.h create mode 100644 cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_bracket_service.cpp create mode 100644 cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_bracket_service.h create mode 100644 cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_bracket_widget.cpp create mode 100644 cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_bracket_widget.h create mode 100644 cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_spellbook_api_accessor.cpp create mode 100644 cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_spellbook_api_accessor.h create mode 100644 cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_spellbook_bracket_explainer.cpp create mode 100644 cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_spellbook_bracket_explainer.h create mode 100644 cockatrice/src/interface/widgets/tabs/api/commander_spellbook/handle_commander_brackets.cpp create mode 100644 cockatrice/src/interface/widgets/tabs/api/commander_spellbook/handle_commander_brackets.h create mode 100644 libcockatrice_settings/libcockatrice/settings/commander_bracket_settings.cpp create mode 100644 libcockatrice_settings/libcockatrice/settings/commander_bracket_settings.h diff --git a/cockatrice/CMakeLists.txt b/cockatrice/CMakeLists.txt index af24dfc26..ed8e49f2d 100644 --- a/cockatrice/CMakeLists.txt +++ b/cockatrice/CMakeLists.txt @@ -318,6 +318,13 @@ set(cockatrice_SOURCES src/interface/widgets/tabs/api/archidekt/display/archidekt_api_response_deck_entry_display_widget.cpp src/interface/widgets/tabs/api/archidekt/display/archidekt_api_response_deck_listings_display_widget.cpp src/interface/widgets/tabs/api/archidekt/display/archidekt_deck_preview_image_display_widget.cpp + src/interface/widgets/tabs/api/commander_spellbook/api_response/card_in_deck_request.cpp + src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_deck_request.cpp + src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_card_result.cpp + src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_variant_result.cpp + src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_estimate_bracket_result.cpp + src/interface/widgets/tabs/api/commander_spellbook/commander_spellbook_bracket_explainer.cpp + src/interface/widgets/tabs/api/commander_spellbook/commander_spellbook_api_accessor.cpp src/interface/widgets/tabs/api/edhrec/api_response/archidekt_links/edhrec_api_response_archidekt_links.cpp src/interface/widgets/tabs/api/edhrec/api_response/average_deck/edhrec_average_deck_api_response.cpp src/interface/widgets/tabs/api/edhrec/api_response/average_deck/edhrec_deck_api_response.cpp @@ -364,6 +371,9 @@ set(cockatrice_SOURCES src/interface/widgets/tabs/visual_deck_storage/tab_deck_storage_visual.cpp src/interface/key_signals.cpp src/interface/logger.cpp + src/interface/widgets/tabs/api/commander_spellbook/commander_bracket_service.cpp + src/interface/widgets/tabs/api/commander_spellbook/commander_bracket_widget.cpp + src/interface/widgets/tabs/api/commander_spellbook/handle_commander_brackets.cpp src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_bracket_navigation_widget.cpp src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_bracket_navigation_widget.h src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_budget_navigation_widget.cpp diff --git a/cockatrice/src/client/settings/cache_settings.cpp b/cockatrice/src/client/settings/cache_settings.cpp index 1052629a5..eaa5d96ac 100644 --- a/cockatrice/src/client/settings/cache_settings.cpp +++ b/cockatrice/src/client/settings/cache_settings.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include #include #include @@ -143,6 +144,7 @@ SettingsCache::SettingsCache() visualDeckStorageSettings = new VisualDeckStorageSettings(settingsPath, this); appearanceSettings = new AppearanceSettings(settingsPath, this); networkSettings = new NetworkSettings(settingsPath, this); + commanderBracketSettings = new CommanderBracketSettings(settingsPath, this); // Forward ICardDatabasePathProvider signal from PathsSettings connect(pathsSettings, &PathsSettings::cardDatabasePathChanged, this, @@ -155,6 +157,12 @@ SettingsCache::SettingsCache() themeName = appearanceSettings->getThemeName(); + auto definitions = commanderBracketSettings->loadDefinitions(); + if (definitions.isEmpty()) { + definitions = CommanderBracketSettings::defaultDefinitions(); + } + commanderBracketSettings->reloadDefinitions(definitions); + loadPaths(); } diff --git a/cockatrice/src/client/settings/cache_settings.h b/cockatrice/src/client/settings/cache_settings.h index f2886d167..23cdb4dbf 100644 --- a/cockatrice/src/client/settings/cache_settings.h +++ b/cockatrice/src/client/settings/cache_settings.h @@ -28,6 +28,7 @@ class CardDatabaseSettings; class CardOverrideSettings; class CardsDisplaySettings; class ChatSettings; +class CommanderBracketSettings; class DebugSettings; class DeckEditorSettings; class DownloadSettings; @@ -83,6 +84,7 @@ private: VisualDeckStorageSettings *visualDeckStorageSettings; AppearanceSettings *appearanceSettings; NetworkSettings *networkSettings; + CommanderBracketSettings *commanderBracketSettings; QString themeName; @@ -150,6 +152,10 @@ public: [[nodiscard]] VisualDeckStorageSettings &visualDeckStorage() const; [[nodiscard]] AppearanceSettings &appearance() const; [[nodiscard]] NetworkSettings &network() const; + [[nodiscard]] CommanderBracketSettings &commanderBrackets() const + { + return *commanderBracketSettings; + } [[nodiscard]] bool getIsPortableBuild() const { diff --git a/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.cpp b/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.cpp index e33c09426..fc53b296f 100644 --- a/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.cpp +++ b/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.cpp @@ -2,6 +2,8 @@ #include "../../../client/settings/cache_settings.h" #include "../../../client/settings/shortcuts_settings.h" +#include "../settings_page/user_interface_settings_page.h" +#include "../tabs/api/commander_spellbook/commander_bracket_widget.h" #include "deck_list_style_proxy.h" #include "deck_state_manager.h" @@ -134,6 +136,8 @@ void DeckEditorDeckDockWidget::createDeckDock() formatComboBox->addItem(tr("Loading Database...")); formatComboBox->setEnabled(false); // Disable until loaded + commanderBracketWidget = new CommanderBracketWidget(this); + commentsLabel = new QLabel(); commentsLabel->setObjectName("commentsLabel"); commentsEdit = new QTextEdit; @@ -219,13 +223,15 @@ void DeckEditorDeckDockWidget::createDeckDock() upperLayout->addWidget(formatLabel, 2, 0); upperLayout->addWidget(formatComboBox, 2, 1); - upperLayout->addWidget(bannerCardLabel, 3, 0); - upperLayout->addWidget(bannerCardComboBox, 3, 1); + upperLayout->addWidget(commanderBracketWidget, 3, 0, 1, 2); - upperLayout->addWidget(deckTagsDisplayWidget, 4, 1); + upperLayout->addWidget(bannerCardLabel, 4, 0); + upperLayout->addWidget(bannerCardComboBox, 4, 1); - upperLayout->addWidget(activeGroupCriteriaLabel, 5, 0); - upperLayout->addWidget(activeGroupCriteriaComboBox, 5, 1); + upperLayout->addWidget(deckTagsDisplayWidget, 5, 1); + + upperLayout->addWidget(activeGroupCriteriaLabel, 6, 0); + upperLayout->addWidget(activeGroupCriteriaComboBox, 6, 1); hashLabel1 = new QLabel(); hashLabel1->setObjectName("hashLabel1"); @@ -303,15 +309,19 @@ void DeckEditorDeckDockWidget::initializeFormats() // Ensure no selection is visible initially formatComboBox->setCurrentIndex(-1); } - connect(formatComboBox, QOverload::of(&QComboBox::currentIndexChanged), this, [this](int index) { + QString formatKey; if (index >= 0) { - QString formatKey = formatComboBox->itemData(index).toString(); + formatKey = formatComboBox->itemData(index).toString(); deckStateManager->setFormat(formatKey); } else { deckStateManager->setFormat(""); // clear format if deselected } + + commanderBracketWidget->setDeck(deckStateManager->getDeckListShared()); }); + + commanderBracketWidget->setDeck(deckStateManager->getDeckListShared()); } ExactCard DeckEditorDeckDockWidget::getCurrentCard() @@ -493,6 +503,8 @@ void DeckEditorDeckDockWidget::syncDisplayWidgetsToModel() formatComboBox->setCurrentIndex(formatComboBox->findData(deckStateManager->getMetadata().gameFormat)); formatComboBox->blockSignals(false); + commanderBracketWidget->setDeck(deckStateManager->getDeckListShared()); + deckTagsDisplayWidget->blockSignals(true); deckTagsDisplayWidget->setTags(deckStateManager->getMetadata().tags); deckTagsDisplayWidget->blockSignals(false); @@ -746,6 +758,7 @@ void DeckEditorDeckDockWidget::retranslateUi() commentsLabel->setText(tr("&Comments:")); activeGroupCriteriaLabel->setText(tr("Group by:")); formatLabel->setText(tr("Format:")); + commanderBracketWidget->retranslateUi(); hashLabel1->setText(tr("Hash:")); @@ -753,4 +766,4 @@ void DeckEditorDeckDockWidget::retranslateUi() aDecrement->setText(tr("&Decrement number")); aRemoveCard->setText(tr("&Remove row")); aSwapCard->setText(tr("Swap card to/from sideboard")); -} \ No newline at end of file +} diff --git a/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.h b/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.h index 8dddf5882..540199f0d 100644 --- a/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.h +++ b/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.h @@ -21,6 +21,7 @@ #include #include +class CommanderBracketWidget; class DeckListModel; class AbstractTabDeckEditor; class DeckEditorDeckDockWidget : public QDockWidget @@ -89,6 +90,8 @@ private: QAction *aRemoveCard, *aIncrement, *aDecrement, *aSwapCard; + CommanderBracketWidget *commanderBracketWidget; + DeckListModel *getModel() const; [[nodiscard]] QModelIndexList getSelectedCardNodeSourceIndices() const; void offsetCountAtIndex(const QModelIndex &idx, bool isIncrement); diff --git a/cockatrice/src/interface/widgets/deck_editor/deck_state_manager.h b/cockatrice/src/interface/widgets/deck_editor/deck_state_manager.h index 10312d0a0..6fce6be57 100644 --- a/cockatrice/src/interface/widgets/deck_editor/deck_state_manager.h +++ b/cockatrice/src/interface/widgets/deck_editor/deck_state_manager.h @@ -57,6 +57,14 @@ public: */ const DeckList &getDeckList() const; + /** + * @brief Gets the underlying DeckList. + */ + QSharedPointer getDeckListShared() const + { + return deckList; + } + /** * @brief Creates a LoadedDeck containing the contents of the current deck and the current LoadInfo. * diff --git a/cockatrice/src/interface/widgets/settings_page/user_interface_settings_page.cpp b/cockatrice/src/interface/widgets/settings_page/user_interface_settings_page.cpp index cfd855d33..634df0b15 100644 --- a/cockatrice/src/interface/widgets/settings_page/user_interface_settings_page.cpp +++ b/cockatrice/src/interface/widgets/settings_page/user_interface_settings_page.cpp @@ -2,6 +2,7 @@ #include "../../../client/settings/cache_settings.h" #include "../interface/widgets/tabs/tab_supervisor.h" +#include "../tabs/api/commander_spellbook/commander_spellbook_bracket_explainer.h" #include #include @@ -161,6 +162,57 @@ UserInterfaceSettingsPage::UserInterfaceSettingsPage() connect(&defaultDeckEditorTypeSelector, QOverload::of(&QComboBox::currentIndexChanged), &SettingsCache::instance().deckEditor(), &DeckEditorSettings::setDefaultDeckEditorType); + commanderSpellbookIntegrationUseOfficialBracketNamesExplainer.setText("?"); + commanderSpellbookIntegrationUseOfficialBracketNamesExplainer.setAutoRaise(true); + commanderSpellbookIntegrationUseOfficialBracketNamesExplainer.setEnabled(false); + + // Add items with userData = internal enum + commanderSpellbookIntegrationEnabledSelector.addItem(tr("Disabled"), + commanderSpellbookIntegrationEnabledIndexDisabled); + commanderSpellbookIntegrationEnabledSelector.addItem(tr("Enabled"), + commanderSpellbookIntegrationEnabledIndexEnabled); + commanderSpellbookIntegrationEnabledSelector.addItem(tr("Automatic"), + commanderSpellbookIntegrationEnabledIndexAutomatic); + + int storedMode = SettingsCache::instance().deckEditor().getCommanderSpellbookIntegrationEnabled(); + for (int i = 0; i < commanderSpellbookIntegrationEnabledSelector.count(); ++i) { + if (commanderSpellbookIntegrationEnabledSelector.itemData(i).toInt() == storedMode) { + commanderSpellbookIntegrationEnabledSelector.setCurrentIndex(i); + break; + } + } + + connect(&commanderSpellbookIntegrationEnabledSelector, QOverload::of(&QComboBox::currentIndexChanged), this, + [this](int index) { + int mode = commanderSpellbookIntegrationEnabledSelector.itemData(index).toInt(); + SettingsCache::instance().deckEditor().setCommanderSpellbookIntegrationEnabled(mode); + updateCommanderSpellbookUiState(); + }); + + commanderSpellbookIntegrationBracketNamingSelector.addItem( + tr("CommanderSpellbook bracket names")); // index 0 = false + commanderSpellbookIntegrationBracketNamingSelector.addItem( + tr("Official Commander bracket names (approximate)")); // index 1 = true + + commanderSpellbookIntegrationBracketNamingSelector.setCurrentIndex( + SettingsCache::instance().deckEditor().getCommanderSpellbookIntegrationUseOfficialBracketNames() ? 1 : 0); + + connect(&commanderSpellbookIntegrationBracketNamingSelector, QOverload::of(&QComboBox::currentIndexChanged), + &SettingsCache::instance(), [](int index) { + SettingsCache::instance().deckEditor().setCommanderSpellbookIntegrationUseOfficialBracketNames(index == + 1); + }); + + updateCommanderSpellbookUiState(); + + auto *labelLayout = new QHBoxLayout; + labelLayout->setContentsMargins(0, 0, 0, 0); + labelLayout->addWidget(&commanderSpellbookIntegrationUseOfficialBracketNamesLabel); + labelLayout->addWidget(&commanderSpellbookIntegrationUseOfficialBracketNamesExplainer); + + auto *labelWidget = new QWidget; + labelWidget->setLayout(labelLayout); + auto *deckEditorGrid = new QGridLayout; deckEditorGrid->addWidget(&openDeckInNewTabCheckBox, 0, 0); deckEditorGrid->addWidget(&visualDeckStorageInGameCheckBox, 1, 0); @@ -169,6 +221,10 @@ UserInterfaceSettingsPage::UserInterfaceSettingsPage() deckEditorGrid->addWidget(&visualDeckStoragePromptForConversionSelector, 3, 1); deckEditorGrid->addWidget(&defaultDeckEditorTypeLabel, 4, 0); deckEditorGrid->addWidget(&defaultDeckEditorTypeSelector, 4, 1); + deckEditorGrid->addWidget(&commanderSpellbookIntegrationEnabledLabel, 5, 0); + deckEditorGrid->addWidget(&commanderSpellbookIntegrationEnabledSelector, 5, 1); + deckEditorGrid->addWidget(labelWidget, 6, 0); + deckEditorGrid->addWidget(&commanderSpellbookIntegrationBracketNamingSelector, 6, 1); deckEditorGroupBox = new QGroupBox; deckEditorGroupBox->setLayout(deckEditorGrid); @@ -212,6 +268,27 @@ void UserInterfaceSettingsPage::setNotificationEnabled(QT_STATE_CHANGED_T i) } } +void UserInterfaceSettingsPage::updateCommanderSpellbookUiState() +{ + const int mode = SettingsCache::instance().deckEditor().getCommanderSpellbookIntegrationEnabled(); + + const bool enabled = mode != commanderSpellbookIntegrationEnabledIndexDisabled && + mode != commanderSpellbookIntegrationEnabledIndexUnprompted; + + commanderSpellbookIntegrationBracketNamingSelector.setEnabled(enabled); + commanderSpellbookIntegrationUseOfficialBracketNamesExplainer.setEnabled(enabled); + commanderSpellbookIntegrationUseOfficialBracketNamesLabel.setVisible(enabled); + commanderSpellbookIntegrationUseOfficialBracketNamesExplainer.setVisible(enabled); + commanderSpellbookIntegrationBracketNamingSelector.setVisible(enabled); + + if (enabled) { + // Sync selector with the current stored bool + const bool useOfficial = + SettingsCache::instance().deckEditor().getCommanderSpellbookIntegrationUseOfficialBracketNames(); + commanderSpellbookIntegrationBracketNamingSelector.setCurrentIndex(useOfficial ? 1 : 0); + } +} + void UserInterfaceSettingsPage::retranslateUi() { generalGroupBox->setTitle(tr("General interface settings")); @@ -249,6 +326,22 @@ void UserInterfaceSettingsPage::retranslateUi() defaultDeckEditorTypeLabel.setText(tr("Default deck editor type")); defaultDeckEditorTypeSelector.setItemText(TabSupervisor::ClassicDeckEditor, tr("Classic Deck Editor")); defaultDeckEditorTypeSelector.setItemText(TabSupervisor::VisualDeckEditor, tr("Visual Deck Editor")); + + commanderSpellbookIntegrationEnabledLabel.setText( + tr("CommanderSpellbook integration to estimate commander bracket")); + commanderSpellbookIntegrationEnabledSelector.setItemText(commanderSpellbookIntegrationEnabledIndexDisabled, + tr("Disabled")); + commanderSpellbookIntegrationEnabledSelector.setItemText(commanderSpellbookIntegrationEnabledIndexEnabled, + tr("Enabled")); + commanderSpellbookIntegrationEnabledSelector.setItemText(commanderSpellbookIntegrationEnabledIndexAutomatic, + tr("Automatic")); + commanderSpellbookIntegrationUseOfficialBracketNamesLabel.setText(tr("Bracket naming")); + commanderSpellbookIntegrationBracketNamingSelector.setItemText( + 0, CommanderBracketNames::CommanderSpellbookBracketNames); + commanderSpellbookIntegrationBracketNamingSelector.setItemText( + 1, CommanderBracketNames::OfficialCommanderBracketNames); + + commanderSpellbookIntegrationUseOfficialBracketNamesExplainer.setToolTip(CommanderBracketNames::Explainer); replayGroupBox->setTitle(tr("Replay settings")); rewindBufferingMsLabel.setText(tr("Buffer time for backwards skip via shortcut:")); rewindBufferingMsBox.setSuffix(" ms"); diff --git a/cockatrice/src/interface/widgets/settings_page/user_interface_settings_page.h b/cockatrice/src/interface/widgets/settings_page/user_interface_settings_page.h index e10ed2a06..9e6fada69 100644 --- a/cockatrice/src/interface/widgets/settings_page/user_interface_settings_page.h +++ b/cockatrice/src/interface/widgets/settings_page/user_interface_settings_page.h @@ -8,6 +8,8 @@ #include #include #include +#include +#include #include class UserInterfaceSettingsPage : public AbstractSettingsPage @@ -15,6 +17,7 @@ class UserInterfaceSettingsPage : public AbstractSettingsPage Q_OBJECT private slots: void setNotificationEnabled(QT_STATE_CHANGED_T); + void updateCommanderSpellbookUiState(); private: QCheckBox notificationsEnabledCheckBox; @@ -39,6 +42,11 @@ private: QCheckBox visualDeckStorageSelectionAnimationCheckBox; QLabel defaultDeckEditorTypeLabel; QComboBox defaultDeckEditorTypeSelector; + QLabel commanderSpellbookIntegrationEnabledLabel; + QComboBox commanderSpellbookIntegrationEnabledSelector; + QLabel commanderSpellbookIntegrationUseOfficialBracketNamesLabel; + QToolButton commanderSpellbookIntegrationUseOfficialBracketNamesExplainer; + QComboBox commanderSpellbookIntegrationBracketNamingSelector; QLabel rewindBufferingMsLabel; QSpinBox rewindBufferingMsBox; QGroupBox *generalGroupBox; diff --git a/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/card_in_deck_request.cpp b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/card_in_deck_request.cpp new file mode 100644 index 000000000..3c95a4553 --- /dev/null +++ b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/card_in_deck_request.cpp @@ -0,0 +1,17 @@ +#include "card_in_deck_request.h" + +CardInDeckRequest CardInDeckRequest::fromJson(const QJsonObject &json) +{ + CardInDeckRequest request; + request.card = json.value("card").toString(); + request.quantity = json.value("quantity").toInt(); + return request; +} + +QJsonObject CardInDeckRequest::toJson() const +{ + QJsonObject json; + json.insert("card", card); + json.insert("quantity", quantity); + return json; +} diff --git a/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/card_in_deck_request.h b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/card_in_deck_request.h new file mode 100644 index 000000000..b7eb96211 --- /dev/null +++ b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/card_in_deck_request.h @@ -0,0 +1,14 @@ +#ifndef COCKATRICE_CARD_IN_DECK_REQUEST_H +#define COCKATRICE_CARD_IN_DECK_REQUEST_H +#include + +struct CardInDeckRequest +{ + static CardInDeckRequest fromJson(const QJsonObject &json); + QJsonObject toJson() const; + + QString card; + int quantity; +}; + +#endif // COCKATRICE_CARD_IN_DECK_REQUEST_H diff --git a/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_card_result.cpp b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_card_result.cpp new file mode 100644 index 000000000..14558d234 --- /dev/null +++ b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_card_result.cpp @@ -0,0 +1,26 @@ +#include "commander_spellbook_card_result.h" + +CommanderSpellbookCardResult CommanderSpellbookCardResult::fromJson(const QJsonObject &json) +{ + CommanderSpellbookCardResult result; + + result.id = json.value("id").toString(); + result.name = json.value("name").toString(); + result.oracleId = json.value("oracleId").toString(); + result.spoiler = json.value("spoiler").toBool(); + result.typeLine = json.value("typeLine").toString(); + + result.imageUriFrontPng = json.value("imageUriFrontPng").toString(); + result.imageUriFrontLarge = json.value("imageUriFrontLarge").toString(); + result.imageUriFrontNormal = json.value("imageUriFrontNormal").toString(); + result.imageUriFrontSmall = json.value("imageUriFrontSmall").toString(); + result.imageUriFrontArtCrop = json.value("imageUriFrontArtCrop").toString(); + + result.imageUriBackPng = json.value("imageUriBackPng").toString(); + result.imageUriBackLarge = json.value("imageUriBackLarge").toString(); + result.imageUriBackNormal = json.value("imageUriBackNormal").toString(); + result.imageUriBackSmall = json.value("imageUriBackSmall").toString(); + result.imageUriBackArtCrop = json.value("imageUriBackArtCrop").toString(); + + return result; +} diff --git a/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_card_result.h b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_card_result.h new file mode 100644 index 000000000..eaa474097 --- /dev/null +++ b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_card_result.h @@ -0,0 +1,29 @@ +#ifndef COCKATRICE_COMMANDER_SPELLBOOK_CARD_RESULT_H +#define COCKATRICE_COMMANDER_SPELLBOOK_CARD_RESULT_H +#include +#include + +struct CommanderSpellbookCardResult +{ + static CommanderSpellbookCardResult fromJson(const QJsonObject &json); + + QString id; + QString name; + QString oracleId; + bool spoiler = false; + QString typeLine; + + QString imageUriFrontPng; + QString imageUriFrontLarge; + QString imageUriFrontNormal; + QString imageUriFrontSmall; + QString imageUriFrontArtCrop; + + QString imageUriBackPng; + QString imageUriBackLarge; + QString imageUriBackNormal; + QString imageUriBackSmall; + QString imageUriBackArtCrop; +}; + +#endif // COCKATRICE_COMMANDER_SPELLBOOK_CARD_RESULT_H diff --git a/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_deck_request.cpp b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_deck_request.cpp new file mode 100644 index 000000000..166469353 --- /dev/null +++ b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_deck_request.cpp @@ -0,0 +1,95 @@ +#include "commander_spellbook_deck_request.h" + +#include +#include + +CommanderSpellbookDeckRequest CommanderSpellbookDeckRequest::fromJson(const QJsonObject &json) +{ + CommanderSpellbookDeckRequest request; + + // Main deck + const QJsonArray mainArray = json.value("main").toArray(); + for (const QJsonValue &value : mainArray) { + if (!value.isObject()) { + continue; + } + + request.mainDeck.append(CardInDeckRequest::fromJson(value.toObject())); + + // Max size allowed by commanderspellbook + if (request.mainDeck.size() >= 600) { + break; + } + } + + // Commanders + const QJsonArray commanderArray = json.value("commanders").toArray(); + for (const QJsonValue &value : commanderArray) { + if (!value.isObject()) { + continue; + } + + request.commanderDeck.append(CardInDeckRequest::fromJson(value.toObject())); + + // Max size allowed by commanderspellbook + if (request.commanderDeck.size() >= 12) { + break; + } + } + + return request; +} + +QJsonObject CommanderSpellbookDeckRequest::toJson() const +{ + QJsonObject json; + + QJsonArray mainArray; + for (const CardInDeckRequest &card : mainDeck) { + mainArray.append(card.toJson()); + } + + QJsonArray commanderArray; + for (const CardInDeckRequest &card : commanderDeck) { + commanderArray.append(card.toJson()); + } + + json.insert("main", mainArray); + json.insert("commanders", commanderArray); + + return json; +} + +CommanderSpellbookDeckRequest CommanderSpellbookDeckRequest::fromDeckList(const DeckList &deck) +{ + CommanderSpellbookDeckRequest request; + + // --- Mainboard --- + const auto mainCards = deck.getCardNodes({DECK_ZONE_MAIN}); + for (const DecklistCardNode *node : mainCards) { + if (!node) { + continue; + } + + QJsonObject json; + json.insert("card", node->getName()); + json.insert("quantity", node->getNumber()); + request.mainDeck.append(CardInDeckRequest::fromJson(json)); + + // Max size allowed by commanderspellbook + if (request.mainDeck.size() >= 600) { + break; + } + } + + // --- Commander (bannerCard) --- + const auto &metadata = deck.getMetadata(); + if (!metadata.bannerCard.name.isEmpty()) { + QJsonObject json; + json.insert("card", metadata.bannerCard.name); + json.insert("quantity", 1); + request.commanderDeck.append(CardInDeckRequest::fromJson(json)); + } + + return request; +} diff --git a/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_deck_request.h b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_deck_request.h new file mode 100644 index 000000000..46d8e053b --- /dev/null +++ b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_deck_request.h @@ -0,0 +1,19 @@ +#ifndef COCKATRICE_COMMANDER_SPELLBOOK_DECK_REQUEST_H +#define COCKATRICE_COMMANDER_SPELLBOOK_DECK_REQUEST_H +#include "card_in_deck_request.h" +#include "libcockatrice/deck_list/deck_list.h" + +#include +#include + +struct CommanderSpellbookDeckRequest +{ + static CommanderSpellbookDeckRequest fromJson(const QJsonObject &json); + static CommanderSpellbookDeckRequest fromDeckList(const DeckList &deck); + QJsonObject toJson() const; + + QList mainDeck; // maxItems: 600 + QList commanderDeck; // maxItems: 12 +}; + +#endif // COCKATRICE_COMMANDER_SPELLBOOK_DECK_REQUEST_H diff --git a/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_estimate_bracket_result.cpp b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_estimate_bracket_result.cpp new file mode 100644 index 000000000..c3640ce79 --- /dev/null +++ b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_estimate_bracket_result.cpp @@ -0,0 +1,93 @@ +#include "commander_spellbook_estimate_bracket_result.h" + +EstimateBracketResult EstimateBracketResult::fromJson(const QJsonObject &json) +{ + EstimateBracketResult result; + + result.bracketTag = json.value("bracketTag").toString(); + + // + // Cards + // + for (const auto &value : json.value("cards").toArray()) { + if (!value.isObject()) { + continue; + } + + const QJsonObject obj = value.toObject(); + + CommanderSpellbookCardResult card = CommanderSpellbookCardResult::fromJson(obj.value("card").toObject()); + + if (obj.value("gameChanger").toBool()) { + result.gameChangerCards.append(card); + } + + if (obj.value("massLandDenial").toBool()) { + result.massLandDenialCards.append(card); + } + + if (obj.value("extraTurn").toBool()) { + result.extraTurnCards.append(card); + } + } + + // + // Templates + // + for (const auto &value : json.value("templates").toArray()) { + if (!value.isObject()) { + continue; + } + + const QJsonObject obj = value.toObject(); + + CommanderSpellbookVariantResult variant = CommanderSpellbookVariantResult::fromJson(obj); + + if (obj.value("massLandDenial").toBool()) { + result.massLandDenialTemplates.append(variant); + } + + if (obj.value("extraTurn").toBool()) { + result.extraTurnTemplates.append(variant); + } + } + + // + // Combos + // + for (const auto &value : json.value("combos").toArray()) { + if (!value.isObject()) { + continue; + } + + const QJsonObject obj = value.toObject(); + + CommanderSpellbookVariantResult combo = CommanderSpellbookVariantResult::fromJson(obj); + + if (obj.value("massLandDenial").toBool()) { + result.massLandDenialCombos.append(combo); + } + + if (obj.value("extraTurn").toBool()) { + result.extraTurnCombos.append(combo); + } + + if (obj.value("lock").toBool()) { + result.lockCombos.append(combo); + } + + if (obj.value("skipTurns").toBool()) { + result.skipTurnsCombos.append(combo); + } + + if (obj.value("definitelyTwoCard").toBool()) { + result.definitelyTwoCardCombos.append(combo); + } + + if (obj.value("arguablyTwoCard").toBool()) { + result.arguablyTwoCardCombos.append(combo); + } + } + + return result; +} diff --git a/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_estimate_bracket_result.h b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_estimate_bracket_result.h new file mode 100644 index 000000000..bb589a901 --- /dev/null +++ b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_estimate_bracket_result.h @@ -0,0 +1,32 @@ +#ifndef COCKATRICE_COMMANDER_SPELLBOOK_ESTIMATE_BRACKET_RESULT_H +#define COCKATRICE_COMMANDER_SPELLBOOK_ESTIMATE_BRACKET_RESULT_H + +#include "commander_spellbook_card_result.h" +#include "commander_spellbook_variant_result.h" + +#include +#include + +struct EstimateBracketResult +{ + static EstimateBracketResult fromJson(const QJsonObject &json); + + QString bracketTag; + + QList gameChangerCards; + QList massLandDenialCards; + QList extraTurnCards; + + QList massLandDenialTemplates; + QList extraTurnTemplates; + + QList massLandDenialCombos; + QList extraTurnCombos; + QList lockCombos; + QList skipTurnsCombos; + + QList definitelyTwoCardCombos; + QList arguablyTwoCardCombos; +}; + +#endif // COCKATRICE_COMMANDER_SPELLBOOK_ESTIMATE_BRACKET_RESULT_H diff --git a/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_variant_result.cpp b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_variant_result.cpp new file mode 100644 index 000000000..bbc23e15e --- /dev/null +++ b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_variant_result.cpp @@ -0,0 +1,35 @@ +#include "commander_spellbook_variant_result.h" + +CommanderSpellbookVariantResult CommanderSpellbookVariantResult::fromJson(const QJsonObject &json) +{ + CommanderSpellbookVariantResult result; + + result.id = json.value("id").toString(); + result.status = json.value("status").toString(); + + result.uses = json.value("uses").toArray(); + result.cardRequires = json.value("requires").toArray(); + result.produces = json.value("produces").toArray(); + result.of = json.value("of").toArray(); + result.includes = json.value("includes").toArray(); + + result.manaNeeded = json.value("manaNeeded").toArray(); + result.manaValueNeeded = json.value("manaValueNeeded").toArray(); + + result.easyPrerequisites = json.value("easyPrerequisites").toArray(); + result.notablePrerequisites = json.value("notablePrerequisites").toArray(); + + result.description = json.value("description").toString(); + result.notes = json.value("notes").toString(); + result.popularity = json.value("popularity").toDouble(); + + result.spoiler = json.value("spoiler").toBool(); + result.bracketTag = json.value("bracketTag").toString(); + + result.legalities = json.value("legalities").toObject(); + result.prices = json.value("prices").toObject(); + + result.variantCount = json.value("variantCount").toInt(); + + return result; +} diff --git a/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_variant_result.h b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_variant_result.h new file mode 100644 index 000000000..d908f1299 --- /dev/null +++ b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_variant_result.h @@ -0,0 +1,39 @@ +#ifndef COCKATRICE_COMMANDER_SPELLBOOK_VARIANT_RESULT_H +#define COCKATRICE_COMMANDER_SPELLBOOK_VARIANT_RESULT_H + +#include +#include + +struct CommanderSpellbookVariantResult +{ + static CommanderSpellbookVariantResult fromJson(const QJsonObject &json); + + QString id; + QString status; + + QJsonArray uses; + QJsonArray cardRequires; + QJsonArray produces; + QJsonArray of; + QJsonArray includes; + + QJsonArray manaNeeded; + QJsonArray manaValueNeeded; + + QJsonArray easyPrerequisites; + QJsonArray notablePrerequisites; + + QString description; + QString notes; + double popularity = 0.0; + + bool spoiler = false; + QString bracketTag; + + QJsonObject legalities; + QJsonObject prices; + + int variantCount = 0; +}; + +#endif // COCKATRICE_COMMANDER_SPELLBOOK_VARIANT_RESULT_H diff --git a/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_bracket_service.cpp b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_bracket_service.cpp new file mode 100644 index 000000000..b85129a3f --- /dev/null +++ b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_bracket_service.cpp @@ -0,0 +1,53 @@ +#include "commander_bracket_service.h" + +#include "../../../../../client/settings/cache_settings.h" + +#include + +CommanderBracketService &CommanderBracketService::instance() +{ + static CommanderBracketService service; + return service; +} + +CommanderBracketService::CommanderBracketService(QObject *parent) : QObject(parent) +{ + connect(&CommanderSpellbookApiAccessor::instance(), &CommanderSpellbookApiAccessor::estimateBracketFinished, this, + &CommanderBracketService::onEstimateBracketFinished); + + connect(&CommanderSpellbookApiAccessor::instance(), &CommanderSpellbookApiAccessor::estimateBracketError, this, + &CommanderBracketService::onEstimateBracketError); +} + +quint64 CommanderBracketService::estimateBracket(const DeckList &deck, QObject *requester) +{ + return CommanderSpellbookApiAccessor::instance().estimateBracket(deck, requester); +} + +void CommanderBracketService::onEstimateBracketFinished(CommanderSpellbookApiAccessor::RequestId id, + QObject *requester, + const EstimateBracketResult &result) +{ + CommanderBracketEstimate estimate; + + estimate.bracketTag = result.bracketTag; + + estimate.rawResult = result; + + auto &brackets = SettingsCache::instance().commanderBrackets(); + + estimate.officialName = brackets.officialName(result.bracketTag); + + estimate.displayName = brackets.displayName(result.bracketTag); + + estimate.explanation = brackets.explanation(result.bracketTag); + + emit estimateFinished(id, requester, estimate); +} + +void CommanderBracketService::onEstimateBracketError(CommanderSpellbookApiAccessor::RequestId id, + QObject *requester, + const QString &error) +{ + emit estimateError(id, requester, error); +} diff --git a/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_bracket_service.h b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_bracket_service.h new file mode 100644 index 000000000..e0c2a9d55 --- /dev/null +++ b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_bracket_service.h @@ -0,0 +1,45 @@ +#ifndef COCKATRICE_COMMANDER_BRACKET_SERVICE_H +#define COCKATRICE_COMMANDER_BRACKET_SERVICE_H + +#include "commander_spellbook_api_accessor.h" +#include "libcockatrice/deck_list/deck_list.h" + +#include + +struct CommanderBracketEstimate +{ + QString bracketTag; + + QString officialName; + QString displayName; + QString explanation; + + EstimateBracketResult rawResult; +}; + +class CommanderBracketService : public QObject +{ + Q_OBJECT + +public: + static CommanderBracketService &instance(); + + quint64 estimateBracket(const DeckList &deck, QObject *requester); + +signals: + void estimateFinished(quint64 requestId, QObject *requester, const CommanderBracketEstimate &estimate); + + void estimateError(quint64 requestId, QObject *requester, const QString &error); + +private slots: + void onEstimateBracketFinished(CommanderSpellbookApiAccessor::RequestId id, + QObject *requester, + const EstimateBracketResult &result); + + void onEstimateBracketError(CommanderSpellbookApiAccessor::RequestId id, QObject *requester, const QString &error); + +private: + explicit CommanderBracketService(QObject *parent = nullptr); +}; + +#endif diff --git a/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_bracket_widget.cpp b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_bracket_widget.cpp new file mode 100644 index 000000000..4f50e38a6 --- /dev/null +++ b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_bracket_widget.cpp @@ -0,0 +1,269 @@ +#include "commander_bracket_widget.h" + +#include "../../../../../client/settings/cache_settings.h" +#include "commander_bracket_service.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +CommanderBracketWidget::CommanderBracketWidget(QWidget *parent) : QWidget(parent) +{ + bracketLabel = new QLabel(tr("Bracket:"), this); + + bracketValueLabel = new QLabel(this); + bracketValueLabel->setText("-"); + bracketValueLabel->setObjectName("bracketValueLabel"); + + bracketInfoButton = new QToolButton(this); + bracketInfoButton->setText("?"); + bracketInfoButton->setAutoRaise(true); + bracketInfoButton->setEnabled(false); + + bracketRefreshButton = new QToolButton(this); + bracketRefreshButton->setIcon(QPixmap("theme:icons/reload")); + bracketRefreshButton->setAutoRaise(true); + + connect(bracketRefreshButton, &QToolButton::clicked, this, &CommanderBracketWidget::requestBracketEstimate); + + auto *layout = new QGridLayout(this); + layout->setContentsMargins(0, 0, 0, 0); + layout->addWidget(bracketLabel, 0, 0); + + auto *bracketRow = new QHBoxLayout; + bracketRow->addWidget(bracketValueLabel); + bracketRow->addWidget(bracketInfoButton); + bracketRow->addWidget(bracketRefreshButton); + bracketRow->addStretch(); + + layout->addLayout(bracketRow, 0, 1); + + connect(&CommanderBracketService::instance(), &CommanderBracketService::estimateFinished, this, + &CommanderBracketWidget::onEstimateBracketFinished); + connect(&CommanderBracketService::instance(), &CommanderBracketService::estimateError, this, + &CommanderBracketWidget::onEstimateBracketError); + + connect(&SettingsCache::instance().deckEditor(), &DeckEditorSettings::commanderSpellbookIntegrationEnabledChanged, + this, &CommanderBracketWidget::maybeAutoEstimateBracket); + connect(&SettingsCache::instance().deckEditor(), + &DeckEditorSettings::commanderSpellbookIntegrationUseOfficialBracketNamesChanged, this, + &CommanderBracketWidget::maybeAutoEstimateBracket); + + setVisible(false); +} + +void CommanderBracketWidget::setDeck(const QSharedPointer &_deck) +{ + deck = _deck; + requestId = 0; // invalidate any in-flight estimate for the previous deck + + // Reset the displayed bracket + bracketValueLabel->setText("-"); + bracketInfoButton->setToolTip({}); + bracketInfoButton->setEnabled(false); + bracketRefreshButton->setEnabled(true); + + maybeAutoEstimateBracket(); +} + +bool CommanderBracketWidget::promptCommanderSpellbookIntegration() +{ + QDialog dialog(this); + dialog.setWindowTitle(tr("CommanderSpellbook integration")); + + auto *mainLayout = new QVBoxLayout(&dialog); + + // Main text + auto *label = new QLabel(tr("CommanderSpellbook can analyze your deck and estimate its Commander bracket.\n\n" + "This sends your deck list to an external service.\n\n" + "CommanderSpellbook uses its own bracket naming system based on their own algorithm. " + "These names can be mapped to the official Commander brackets, but the mapping " + "is only an approximation.")); + label->setWordWrap(true); + mainLayout->addWidget(label); + + // Naming selector + auto *formLayout = new QFormLayout; + auto *namingCombo = new QComboBox(&dialog); + namingCombo->addItem(tr("CommanderSpellbook bracket names")); + namingCombo->addItem(tr("Official Commander bracket names (approximate)")); + namingCombo->setCurrentIndex( + SettingsCache::instance().deckEditor().getCommanderSpellbookIntegrationUseOfficialBracketNames() ? 1 : 0); + + // Create label + explainer button + auto *labelWidget = new QWidget(&dialog); + auto *labelLayout = new QHBoxLayout(labelWidget); + labelLayout->setContentsMargins(0, 0, 0, 0); + + auto *namingLabel = new QLabel(tr("Bracket naming:"), labelWidget); + auto *explainerButton = new QToolButton(labelWidget); + explainerButton->setText("?"); + explainerButton->setAutoRaise(true); + explainerButton->setEnabled(false); + explainerButton->setToolTip(CommanderBracketNames::Explainer); + + labelLayout->addWidget(namingLabel); + labelLayout->addWidget(explainerButton); + labelLayout->addStretch(); // push the button next to label, combo stays aligned + + // Add row with the custom label widget + formLayout->addRow(labelWidget, namingCombo); + mainLayout->addLayout(formLayout); + + // Buttons + auto *buttonBox = new QDialogButtonBox(&dialog); + auto *enableBtn = buttonBox->addButton(tr("Enable"), QDialogButtonBox::AcceptRole); + auto *automaticBtn = buttonBox->addButton(tr("Automatic"), QDialogButtonBox::ApplyRole); + auto *disableBtn = buttonBox->addButton(tr("Disable"), QDialogButtonBox::RejectRole); + mainLayout->addWidget(buttonBox); + + // Track which button was clicked + QAbstractButton *clickedButton = nullptr; + QObject::connect(buttonBox, &QDialogButtonBox::clicked, &dialog, [&](QAbstractButton *btn) { + clickedButton = btn; + dialog.accept(); + }); + + dialog.exec(); + + // Persist naming choice (if not disabled) + if (clickedButton != disableBtn) { + bool useOfficial = namingCombo->currentIndex() == 1; + SettingsCache::instance().deckEditor().setCommanderSpellbookIntegrationUseOfficialBracketNames(useOfficial); + } + + // Persist integration mode + if (clickedButton == disableBtn) { + SettingsCache::instance().deckEditor().setCommanderSpellbookIntegrationEnabled( + commanderSpellbookIntegrationEnabledIndexDisabled); + return false; + } + if (clickedButton == enableBtn) { + SettingsCache::instance().deckEditor().setCommanderSpellbookIntegrationEnabled( + commanderSpellbookIntegrationEnabledIndexEnabled); + return true; + } + if (clickedButton == automaticBtn) { + SettingsCache::instance().deckEditor().setCommanderSpellbookIntegrationEnabled( + commanderSpellbookIntegrationEnabledIndexAutomatic); + return true; + } + + return false; +} + +void CommanderBracketWidget::updateBracketVisibility(bool visible) +{ + setVisible(visible); +} + +void CommanderBracketWidget::requestBracketEstimate() +{ + bracketRefreshButton->setEnabled(false); + bracketInfoButton->setEnabled(false); + bracketValueLabel->setText(tr("Calculating…")); + + requestId = CommanderBracketService::instance().estimateBracket(*deck, this); +} + +void CommanderBracketWidget::onEstimateBracketFinished(quint64 id, + QObject *requester, + const CommanderBracketEstimate &result) +{ + if (requester != this || id != requestId) { + return; + } + + BracketExplainer explainer; + lastBracketExplanation = explainer.explain(result.rawResult); + + // Display bracket + bracketValueLabel->setText( + SettingsCache::instance().deckEditor().getCommanderSpellbookIntegrationUseOfficialBracketNames() + ? result.officialName + : result.displayName); + bracketRefreshButton->setEnabled(true); + + // Build tooltip + QString tooltip; + for (const auto §ion : lastBracketExplanation.sections) { + tooltip += "" + section.title + "
"; + for (const auto &line : section.bulletPoints) { + tooltip += "• " + line + "
"; + } + tooltip += "
"; + } + + bracketInfoButton->setToolTip(tooltip); + bracketInfoButton->setEnabled(!tooltip.isEmpty()); +} + +void CommanderBracketWidget::onEstimateBracketError(quint64 id, QObject *requester, const QString & /*error*/) +{ + if (requester != this || id != requestId) { + return; + } + + bracketValueLabel->setText("-"); + bracketRefreshButton->setEnabled(true); + bracketInfoButton->setToolTip({}); + bracketInfoButton->setEnabled(false); +} + +void CommanderBracketWidget::maybeAutoEstimateBracket() +{ + const QString formatKey = deck->getGameFormat(); + + const bool isCommander = (formatKey.compare("commander", Qt::CaseInsensitive) == 0); + + int mode = SettingsCache::instance().deckEditor().getCommanderSpellbookIntegrationEnabled(); + + if (!isCommander || mode == commanderSpellbookIntegrationEnabledIndexDisabled) { + updateBracketVisibility(false); + return; + } + + if (mode == commanderSpellbookIntegrationEnabledIndexUnprompted) { + if (prompting) { + return; + } + prompting = true; + const bool accepted = promptCommanderSpellbookIntegration(); + prompting = false; + if (!accepted) { + updateBracketVisibility(false); + return; + } + } + + updateBracketVisibility(true); + + mode = SettingsCache::instance().deckEditor().getCommanderSpellbookIntegrationEnabled(); + if (mode != commanderSpellbookIntegrationEnabledIndexAutomatic) { + return; + } + + // Avoid firing if we already have a result or a request in flight + if (!bracketRefreshButton->isEnabled()) { + return; + } + + // Defer to avoid races during init / model rebuild + QTimer::singleShot(0, this, &CommanderBracketWidget::requestBracketEstimate); +} + +void CommanderBracketWidget::retranslateUi() +{ + bracketLabel->setText(tr("Bracket:")); + bracketInfoButton->setToolTip(tr("Why this bracket?")); + bracketRefreshButton->setToolTip(tr("Recalculate bracket")); +} diff --git a/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_bracket_widget.h b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_bracket_widget.h new file mode 100644 index 000000000..a45610fcf --- /dev/null +++ b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_bracket_widget.h @@ -0,0 +1,46 @@ +#ifndef COCKATRICE_COMMANDER_BRACKET_WIDGET_H +#define COCKATRICE_COMMANDER_BRACKET_WIDGET_H + +#include "commander_spellbook_bracket_explainer.h" + +#include +#include +#include + +class QLabel; +class QToolButton; +struct CommanderBracketEstimate; + +class CommanderBracketWidget : public QWidget +{ + Q_OBJECT + +public: + explicit CommanderBracketWidget(QWidget *parent = nullptr); + + void setDeck(const QSharedPointer &_deck); + void retranslateUi(); + +private slots: + void requestBracketEstimate(); + void onEstimateBracketFinished(quint64 id, QObject *requester, const CommanderBracketEstimate &result); + void onEstimateBracketError(quint64 id, QObject *requester, const QString &error); + void maybeAutoEstimateBracket(); + +private: + bool promptCommanderSpellbookIntegration(); + void updateBracketVisibility(bool visible); + + QSharedPointer deck; + bool prompting = false; + quint64 requestId = 0; + + QLabel *bracketLabel; + QLabel *bracketValueLabel; + QToolButton *bracketInfoButton; + QToolButton *bracketRefreshButton; + + BracketExplanation lastBracketExplanation; +}; + +#endif // COCKATRICE_COMMANDER_BRACKET_WIDGET_H diff --git a/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_spellbook_api_accessor.cpp b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_spellbook_api_accessor.cpp new file mode 100644 index 000000000..590692d98 --- /dev/null +++ b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_spellbook_api_accessor.cpp @@ -0,0 +1,75 @@ +#include "commander_spellbook_api_accessor.h" + +#include "api_response/commander_spellbook_deck_request.h" + +#include +#include +#include +#include +#include + +static const QUrl ESTIMATE_BRACKET_URL(QStringLiteral("https://backend.commanderspellbook.com/estimate-bracket")); + +CommanderSpellbookApiAccessor &CommanderSpellbookApiAccessor::instance() +{ + static CommanderSpellbookApiAccessor instance; + return instance; +} + +CommanderSpellbookApiAccessor::CommanderSpellbookApiAccessor(QObject *parent) : QObject(parent) +{ +} + +CommanderSpellbookApiAccessor::RequestId CommanderSpellbookApiAccessor::estimateBracket(const DeckList &deck, + QObject *requester) +{ + CommanderSpellbookDeckRequest deckRequest = CommanderSpellbookDeckRequest::fromDeckList(deck); + + QJsonDocument doc(deckRequest.toJson()); + QByteArray body = doc.toJson(QJsonDocument::Compact); + + QNetworkRequest req(ESTIMATE_BRACKET_URL); + req.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + req.setHeader(QNetworkRequest::UserAgentHeader, QString("Cockatrice %1").arg(VERSION_STRING)); + + QNetworkReply *reply = network.post(req, body); + + const RequestId id = nextRequestId++; + + reply->setProperty("requestId", QVariant::fromValue(id)); + reply->setProperty("requester", QVariant::fromValue(requester)); + + connect(reply, &QNetworkReply::finished, this, [this, reply]() { onEstimateReplyFinished(reply); }); + + return id; +} + +void CommanderSpellbookApiAccessor::onEstimateReplyFinished(QNetworkReply *reply) +{ + reply->deleteLater(); + + const RequestId id = reply->property("requestId").toULongLong(); + QObject *requester = reply->property("requester").value(); + + if (!requester) { + // Requester died — silently drop + return; + } + + if (reply->error() != QNetworkReply::NoError) { + emit estimateBracketError(id, requester, reply->errorString()); + return; + } + + QJsonParseError err; + QJsonDocument doc = QJsonDocument::fromJson(reply->readAll(), &err); + + if (err.error != QJsonParseError::NoError || !doc.isObject()) { + emit estimateBracketError(id, requester, QStringLiteral("Invalid JSON response")); + return; + } + + EstimateBracketResult result = EstimateBracketResult::fromJson(doc.object()); + + emit estimateBracketFinished(id, requester, result); +} diff --git a/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_spellbook_api_accessor.h b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_spellbook_api_accessor.h new file mode 100644 index 000000000..682a48524 --- /dev/null +++ b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_spellbook_api_accessor.h @@ -0,0 +1,37 @@ +#ifndef COCKATRICE_COMMANDER_SPELLBOOK_API_ACCESSOR_H +#define COCKATRICE_COMMANDER_SPELLBOOK_API_ACCESSOR_H + +#include "api_response/commander_spellbook_estimate_bracket_result.h" + +#include +#include +#include +#include + +class CommanderSpellbookApiAccessor final : public QObject +{ + Q_OBJECT + +public: + static CommanderSpellbookApiAccessor &instance(); + + using RequestId = quint64; + + RequestId estimateBracket(const DeckList &deck, QObject *requester); + +signals: + void estimateBracketFinished(RequestId id, QObject *requester, const EstimateBracketResult &result); + + void estimateBracketError(RequestId id, QObject *requester, const QString &errorMessage); + +private: + explicit CommanderSpellbookApiAccessor(QObject *parent = nullptr); + Q_DISABLE_COPY_MOVE(CommanderSpellbookApiAccessor) + + void onEstimateReplyFinished(QNetworkReply *reply); + + QNetworkAccessManager network; + RequestId nextRequestId = 1; +}; + +#endif // COCKATRICE_COMMANDER_SPELLBOOK_API_ACCESSOR_H diff --git a/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_spellbook_bracket_explainer.cpp b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_spellbook_bracket_explainer.cpp new file mode 100644 index 000000000..fc308ed60 --- /dev/null +++ b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_spellbook_bracket_explainer.cpp @@ -0,0 +1,120 @@ +#include "commander_spellbook_bracket_explainer.h" + +static QString cardList(const QList &cards, int max = 5) +{ + QStringList names; + for (int i = 0; i < cards.size() && i < max; ++i) { + names << cards[i].name; + } + + if (cards.size() > max) { + names << QString("and %1 more").arg(cards.size() - max); + } + + return names.join(", "); +} + +static QString comboCount(const QList &variants) +{ + return QString::number(variants.size()); +} + +BracketExplanation BracketExplainer::explain(const EstimateBracketResult &r) +{ + BracketExplanation out; + out.bracket = r.bracketTag; + + if (!r.gameChangerCards.isEmpty()) { + BracketExplanationSection s; + s.title = "Game-changing cards"; + + s.bulletPoints << QString("Your deck contains %1 game-changing cards, such as %2.") + .arg(r.gameChangerCards.size()) + .arg(cardList(r.gameChangerCards)); + + out.sections << s; + } + + if (!r.extraTurnCards.isEmpty() || !r.extraTurnTemplates.isEmpty() || !r.extraTurnCombos.isEmpty()) { + + BracketExplanationSection s; + s.title = "Extra turns"; + + if (!r.extraTurnCards.isEmpty()) { + s.bulletPoints << QString("The deck contains %1 extra-turn cards (%2).") + .arg(r.extraTurnCards.size()) + .arg(cardList(r.extraTurnCards)); + } + + if (!r.extraTurnTemplates.isEmpty()) { + s.bulletPoints << QString("%1 extra-turn templates were identified.").arg(comboCount(r.extraTurnTemplates)); + } + + if (!r.extraTurnCombos.isEmpty()) { + s.bulletPoints + << QString("%1 extra-turn combo variants were identified.").arg(comboCount(r.extraTurnCombos)); + } + + out.sections << s; + } + + if (!r.massLandDenialCards.isEmpty() || !r.massLandDenialTemplates.isEmpty() || !r.massLandDenialCombos.isEmpty()) { + + BracketExplanationSection s; + s.title = "Mass land denial"; + + if (!r.massLandDenialCards.isEmpty()) { + s.bulletPoints << QString("The deck contains %1 mass land denial cards (%2).") + .arg(r.massLandDenialCards.size()) + .arg(cardList(r.massLandDenialCards)); + } + + if (!r.massLandDenialTemplates.isEmpty()) { + s.bulletPoints + << QString("%1 mass land denial templates were identified.").arg(comboCount(r.massLandDenialTemplates)); + } + + if (!r.massLandDenialCombos.isEmpty()) { + s.bulletPoints << QString("%1 mass land denial combo variants were identified.") + .arg(comboCount(r.massLandDenialCombos)); + } + + out.sections << s; + } + + if (!r.lockCombos.isEmpty() || !r.skipTurnsCombos.isEmpty()) { + + BracketExplanationSection s; + s.title = "Lock pieces"; + + if (!r.lockCombos.isEmpty()) { + s.bulletPoints << QString("%1 lock combo variants were detected.").arg(comboCount(r.lockCombos)); + } + + if (!r.skipTurnsCombos.isEmpty()) { + s.bulletPoints << QString("%1 skip-turn combo variants were detected.").arg(comboCount(r.skipTurnsCombos)); + } + + out.sections << s; + } + + if (!r.definitelyTwoCardCombos.isEmpty() || !r.arguablyTwoCardCombos.isEmpty()) { + + BracketExplanationSection s; + s.title = "Two-card combos"; + + if (!r.definitelyTwoCardCombos.isEmpty()) { + s.bulletPoints << QString("%1 definite two-card combo variants were identified.") + .arg(comboCount(r.definitelyTwoCardCombos)); + } + + if (!r.arguablyTwoCardCombos.isEmpty()) { + s.bulletPoints << QString("%1 arguable two-card combo variants were identified.") + .arg(comboCount(r.arguablyTwoCardCombos)); + } + + out.sections << s; + } + + return out; +} diff --git a/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_spellbook_bracket_explainer.h b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_spellbook_bracket_explainer.h new file mode 100644 index 000000000..9ed499742 --- /dev/null +++ b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_spellbook_bracket_explainer.h @@ -0,0 +1,43 @@ +#ifndef COCKATRICE_COMMANDER_SPELLBOOK_BRACKET_EXPLAINER_H +#define COCKATRICE_COMMANDER_SPELLBOOK_BRACKET_EXPLAINER_H +#include "api_response/commander_spellbook_estimate_bracket_result.h" + +namespace CommanderBracketNames +{ +inline const char *CommanderSpellbookBracketNames = QT_TR_NOOP("CommanderSpellbook"); +inline const char *OfficialCommanderBracketNames = QT_TR_NOOP("Official (approximate)"); +inline const char *Explainer = QT_TR_NOOP( + "The bracket system combines both objective data, as well as subjective play experience to estimate a " + "bracket for a deck.\nCommanderSpellbook's estimation is algorithmical, which means that it can only operate " + "on the objective data, not the subjective intent. \nThey have chosen to represent this by defining their " + "own bracket system which matches their algorithm.\n" + "This custom bracket system maps loosely to the standard system. \nYou may choose to use these mapped " + "standardized names if these are more familiar to you, however, you should keep in mind that these are just " + "rough estimations.\n\nAlways consider the subjective factors of the bracket system when determing a deck's " + "final bracket!"); +} // namespace CommanderBracketNames + +struct BracketExplanationSection +{ + QString title; + QStringList bulletPoints; +}; + +struct BracketExplanation +{ + QString bracket; + QList sections; + + bool isEmpty() const + { + return sections.isEmpty(); + } +}; + +class BracketExplainer +{ +public: + static BracketExplanation explain(const EstimateBracketResult &result); +}; + +#endif // COCKATRICE_COMMANDER_SPELLBOOK_BRACKET_EXPLAINER_H diff --git a/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/handle_commander_brackets.cpp b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/handle_commander_brackets.cpp new file mode 100644 index 000000000..90b490efe --- /dev/null +++ b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/handle_commander_brackets.cpp @@ -0,0 +1,62 @@ +#include "handle_commander_brackets.h" + +#include "../../../../../client/settings/cache_settings.h" + +#include +#include +#include +#include + +static const QUrl COMMANDER_BRACKET_JSON_URL(QStringLiteral("https://cockatrice.github.io/commander-brackets.json")); + +HandleCommanderBrackets::HandleCommanderBrackets(QObject *parent) + : QObject(parent), nam(new QNetworkAccessManager(this)), reply(nullptr) +{ +} + +void HandleCommanderBrackets::downloadBracketDefinitions() +{ + if (reply) { + return; + } + + reply = nam->get(QNetworkRequest(COMMANDER_BRACKET_JSON_URL)); + + connect(reply, &QNetworkReply::finished, this, &HandleCommanderBrackets::actFinishParsingDownloadedData); +} + +void HandleCommanderBrackets::actFinishParsingDownloadedData() +{ + reply = qobject_cast(sender()); + + if (reply->error() != QNetworkReply::NoError) { + emit sigBracketDefinitionsDownloadFailed(reply->error()); + + reply->deleteLater(); + return; + } + + QJsonParseError parseError; + + auto document = QJsonDocument::fromJson(reply->readAll(), &parseError); + + if (parseError.error != QJsonParseError::NoError) { + emit sigBracketDefinitionsDownloadFailed(QNetworkReply::UnknownContentError); + + reply->deleteLater(); + return; + } + + updateBracketDefinitions(document.toVariant().toMap()); + + emit sigBracketDefinitionsDownloaded(); + + reply->deleteLater(); +} + +void HandleCommanderBrackets::updateBracketDefinitions(const QVariantMap &jsonMap) +{ + const auto bracketList = jsonMap.value("brackets").toList(); + SettingsCache::instance().commanderBrackets().saveDefinitions(bracketList); + SettingsCache::instance().commanderBrackets().reloadDefinitions(bracketList); +} diff --git a/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/handle_commander_brackets.h b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/handle_commander_brackets.h new file mode 100644 index 000000000..b8348b13c --- /dev/null +++ b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/handle_commander_brackets.h @@ -0,0 +1,31 @@ +#ifndef COCKATRICE_HANDLE_COMMANDER_BRACKETS_H +#define COCKATRICE_HANDLE_COMMANDER_BRACKETS_H + +#include +#include +#include + +class HandleCommanderBrackets : public QObject +{ + Q_OBJECT + +public: + explicit HandleCommanderBrackets(QObject *parent = nullptr); + + void downloadBracketDefinitions(); + +signals: + void sigBracketDefinitionsDownloaded(); + void sigBracketDefinitionsDownloadFailed(QNetworkReply::NetworkError error); + +private slots: + void actFinishParsingDownloadedData(); + +private: + void updateBracketDefinitions(const QVariantMap &jsonMap); + + QNetworkAccessManager *nam; + QNetworkReply *reply; +}; + +#endif // COCKATRICE_HANDLE_COMMANDER_BRACKETS_H diff --git a/cockatrice/src/interface/window_main.cpp b/cockatrice/src/interface/window_main.cpp index e21737d67..44e188760 100644 --- a/cockatrice/src/interface/window_main.cpp +++ b/cockatrice/src/interface/window_main.cpp @@ -38,6 +38,7 @@ #include "version_string.h" #include "widgets/dialogs/dlg_connect.h" #include "widgets/server/handle_public_servers.h" +#include "widgets/tabs/api/commander_spellbook/handle_commander_brackets.h" #include "widgets/utility/get_text_with_max.h" #include @@ -559,6 +560,8 @@ void MainWindow::startupConfigCheck() actCheckClientUpdates(); } + actCheckCommanderBracketDefinitionUpdates(); + if (SettingsCache::instance().network().getClientVersion() == CLIENT_INFO_NOT_SET) { // no config found, 99% new clean install qCInfo(WindowMainStartupVersionLog) @@ -661,6 +664,7 @@ void MainWindow::alertForcedOracleRun(const QString &version, bool isUpdate) actCheckCardUpdates(); actCheckServerUpdates(); + actCheckCommanderBracketDefinitionUpdates(); } MainWindow::~MainWindow() @@ -1005,6 +1009,16 @@ void MainWindow::checkClientUpdatesFinished(bool needToUpdate, bool /* isCompati } } +void MainWindow::actCheckCommanderBracketDefinitionUpdates() +{ + auto *handler = new HandleCommanderBrackets(this); + + connect(handler, &HandleCommanderBrackets::sigBracketDefinitionsDownloaded, this, + []() { qDebug() << "Bracket definitions loaded"; }); + + handler->downloadBracketDefinitions(); +} + void MainWindow::refreshShortcuts() { ShortcutsSettings &shortcuts = SettingsCache::instance().shortcuts(); diff --git a/cockatrice/src/interface/window_main.h b/cockatrice/src/interface/window_main.h index 610f11965..fa6c79915 100644 --- a/cockatrice/src/interface/window_main.h +++ b/cockatrice/src/interface/window_main.h @@ -92,6 +92,7 @@ private slots: void cardDatabaseAllNewSetsEnabled(); void checkClientUpdatesFinished(bool needToUpdate, bool isCompatible, Release *release); + void actCheckCommanderBracketDefinitionUpdates(); void actOpenCustomFolder(); void actOpenCustomsetsFolder(); diff --git a/libcockatrice_settings/CMakeLists.txt b/libcockatrice_settings/CMakeLists.txt index 9e2654a9a..80a465b6d 100644 --- a/libcockatrice_settings/CMakeLists.txt +++ b/libcockatrice_settings/CMakeLists.txt @@ -9,6 +9,7 @@ set(HEADERS libcockatrice/settings/card_override_settings.h libcockatrice/settings/cards_display_settings.h libcockatrice/settings/chat_settings.h + libcockatrice/settings/commander_bracket_settings.h libcockatrice/settings/debug_settings.h libcockatrice/settings/deck_editor_settings.h libcockatrice/settings/download_settings.h @@ -39,6 +40,7 @@ add_library( libcockatrice/settings/cache_storage_settings.cpp libcockatrice/settings/card_database_settings.cpp libcockatrice/settings/card_override_settings.cpp + libcockatrice/settings/commander_bracket_settings.cpp libcockatrice/settings/cards_display_settings.cpp libcockatrice/settings/chat_settings.cpp libcockatrice/settings/debug_settings.cpp diff --git a/libcockatrice_settings/libcockatrice/settings/commander_bracket_settings.cpp b/libcockatrice_settings/libcockatrice/settings/commander_bracket_settings.cpp new file mode 100644 index 000000000..0c5f5521e --- /dev/null +++ b/libcockatrice_settings/libcockatrice/settings/commander_bracket_settings.cpp @@ -0,0 +1,194 @@ +#include "commander_bracket_settings.h" + +#include + +QVariantList CommanderBracketSettings::defaultDefinitions() +{ + return { + QVariantMap{{"tag", "R"}, + {"officialName", "[5] cEDH"}, + {"displayName", "Ruthless"}, + {"explanation", + "Top-tier competitive decks with maximum optimization, fast combos, and minimal variance."}}, + QVariantMap{{"tag", "S"}, + {"officialName", "[4] Optimized"}, + {"displayName", "Spicy"}, + {"explanation", "Highly tuned decks with strong synergy and occasional combo finishes."}}, + QVariantMap{{"tag", "P"}, + {"officialName", "[3] Upgraded"}, + {"displayName", "Powerful"}, + {"explanation", "Focused decks with clear win conditions and solid consistency."}}, + QVariantMap{{"tag", "O"}, + {"officialName", "[2] Core"}, + {"displayName", "Oddball"}, + {"explanation", "Unconventional or thematic decks with some structure but non-standard choices."}}, + QVariantMap{{"tag", "C"}, + {"officialName", "[2] Core"}, + {"displayName", "Core"}, + {"explanation", "Preconstructed or precon-level decks with straightforward strategies."}}, + QVariantMap{{"tag", "E"}, + {"officialName", "[1] Exhibition"}, + {"displayName", "Exhibition"}, + {"explanation", "Ultra-casual, theme-focused decks with minimal optimization."}}, + QVariantMap{{"tag", "B"}, + {"officialName", "Banned"}, + {"displayName", "Banned"}, + {"explanation", "The deck contains one or more cards banned in Commander."}}, + }; +} + +CommanderBracketSettings::CommanderBracketSettings(const QString &settingPath, QObject *parent) + : SettingsManager(settingPath + "commander_brackets.ini", "commander_brackets", QString(), parent) +{ +} + +void CommanderBracketSettings::setSchemaVersion(int version) +{ + setValue(version, "schemaVersion"); +} +int CommanderBracketSettings::getSchemaVersion() const +{ + QVariant value = getValue("schemaVersion"); + return value.isValid() ? value.toInt() : 0; +} + +void CommanderBracketSettings::clearDefinitions() +{ + auto settings = getSettings(); + + settings.beginGroup("commander_brackets"); + settings.remove(""); + settings.endGroup(); + + settings.sync(); +} + +void CommanderBracketSettings::saveDefinitions(const QVariantList &definitions) +{ + auto settings = getSettings(); + + settings.beginGroup("commander_brackets"); + + settings.remove(""); + + settings.setValue("schemaVersion", CurrentSchemaVersion); + + for (const auto &entry : definitions) { + QVariantMap map = entry.toMap(); + + QString tag = map.value("tag").toString(); + + if (tag.isEmpty()) { + continue; + } + + settings.beginGroup(tag); + + settings.setValue("officialName", map.value("officialName")); + + settings.setValue("displayName", map.value("displayName")); + + settings.setValue("explanation", map.value("explanation")); + + settings.endGroup(); + } + + settings.endGroup(); + + settings.sync(); +} + +QVariantList CommanderBracketSettings::loadDefinitions() const +{ + QVariantList result; + + auto settings = getSettings(); + + settings.beginGroup("commander_brackets"); + + int version = settings.value("schemaVersion", 0).toInt(); + + if (version != CurrentSchemaVersion) { + settings.endGroup(); + return result; + } + + QStringList groups = settings.childGroups(); + + for (const QString &tag : groups) { + settings.beginGroup(tag); + + QVariantMap map; + + map["tag"] = tag; + map["officialName"] = settings.value("officialName"); + map["displayName"] = settings.value("displayName"); + map["explanation"] = settings.value("explanation"); + + result.append(map); + + settings.endGroup(); + } + + settings.endGroup(); + + return result; +} + +void CommanderBracketSettings::reloadDefinitions(const QVariantList &definitionsList) +{ + definitions.clear(); + + for (const auto &entry : definitionsList) { + const auto map = entry.toMap(); + + CommanderBracketDefinition definition; + + definition.tag = map.value("tag").toString(); + definition.officialName = map.value("officialName").toString(); + definition.displayName = map.value("displayName").toString(); + definition.explanation = map.value("explanation").toString(); + + if (!definition.tag.isEmpty()) { + definitions.insert(definition.tag, definition); + } + } +} + +QString CommanderBracketSettings::officialName(const QString &tag) const +{ + auto it = definitions.find(tag); + + if (it == definitions.end()) { + return tag; + } + + return it->officialName; +} + +QString CommanderBracketSettings::displayName(const QString &tag) const +{ + auto it = definitions.find(tag); + + if (it == definitions.end()) { + return tag; + } + + return it->displayName; +} + +QString CommanderBracketSettings::explanation(const QString &tag) const +{ + auto it = definitions.find(tag); + + if (it == definitions.end()) { + return {}; + } + + return it->explanation; +} + +bool CommanderBracketSettings::contains(const QString &tag) const +{ + return definitions.contains(tag); +} diff --git a/libcockatrice_settings/libcockatrice/settings/commander_bracket_settings.h b/libcockatrice_settings/libcockatrice/settings/commander_bracket_settings.h new file mode 100644 index 000000000..5a29f5dc7 --- /dev/null +++ b/libcockatrice_settings/libcockatrice/settings/commander_bracket_settings.h @@ -0,0 +1,56 @@ +#ifndef COMMANDER_BRACKET_SETTINGS_H +#define COMMANDER_BRACKET_SETTINGS_H + +#include "settings_manager.h" + +#include +#include +#include +#include + +struct CommanderBracketDefinition +{ + QString tag; + + QString officialName; + QString displayName; + + QString explanation; +}; + +class CommanderBracketSettings : public SettingsManager +{ + Q_OBJECT + friend class SettingsCache; + +public: + static constexpr int CurrentSchemaVersion = 1; + + static QVariantList defaultDefinitions(); + + void clearDefinitions(); + + void saveDefinitions(const QVariantList &definitions); + + QVariantList loadDefinitions() const; + + void reloadDefinitions(const QVariantList &definitions); + + QString officialName(const QString &tag) const; + QString displayName(const QString &tag) const; + QString explanation(const QString &tag) const; + bool contains(const QString &tag) const; + + void setSchemaVersion(int version); + int getSchemaVersion() const; + +private: + explicit CommanderBracketSettings(const QString &settingPath, QObject *parent = nullptr); + + CommanderBracketSettings(const CommanderBracketSettings &) = delete; + CommanderBracketSettings &operator=(const CommanderBracketSettings &) = delete; + + QHash definitions; +}; + +#endif // COMMANDER_BRACKET_SETTINGS_H diff --git a/libcockatrice_settings/libcockatrice/settings/deck_editor_settings.cpp b/libcockatrice_settings/libcockatrice/settings/deck_editor_settings.cpp index d6b9b389b..65296a450 100644 --- a/libcockatrice_settings/libcockatrice/settings/deck_editor_settings.cpp +++ b/libcockatrice_settings/libcockatrice/settings/deck_editor_settings.cpp @@ -46,3 +46,27 @@ void DeckEditorSettings::setDefaultDeckEditorType(int _defaultDeckEditorType) { setValue(_defaultDeckEditorType, "defaultDeckEditorType"); } + +int DeckEditorSettings::getCommanderSpellbookIntegrationEnabled() const +{ + return getValue("commanderspellbookintegrationenabled", QString(), QString(), + commanderSpellbookIntegrationEnabledIndexUnprompted) + .toInt(); +} + +bool DeckEditorSettings::getCommanderSpellbookIntegrationUseOfficialBracketNames() const +{ + return getValue("commanderspellbookintegrationuseofficialbracketnames", QString(), QString(), false).toBool(); +} + +void DeckEditorSettings::setCommanderSpellbookIntegrationEnabled(int _commanderSpellbookIntegrationEnabled) +{ + setValue(_commanderSpellbookIntegrationEnabled, "commanderspellbookintegrationenabled"); + emit commanderSpellbookIntegrationEnabledChanged(_commanderSpellbookIntegrationEnabled); +} + +void DeckEditorSettings::setCommanderSpellbookIntegrationUseOfficialBracketNames(bool _useOfficialBracketNames) +{ + setValue(_useOfficialBracketNames, "commanderspellbookintegrationuseofficialbracketnames"); + emit commanderSpellbookIntegrationUseOfficialBracketNamesChanged(_useOfficialBracketNames); +} diff --git a/libcockatrice_settings/libcockatrice/settings/deck_editor_settings.h b/libcockatrice_settings/libcockatrice/settings/deck_editor_settings.h index 5929cf968..70f91be9b 100644 --- a/libcockatrice_settings/libcockatrice/settings/deck_editor_settings.h +++ b/libcockatrice_settings/libcockatrice/settings/deck_editor_settings.h @@ -5,6 +5,14 @@ #include +enum commanderSpellbookIntegrationEnabledIndex +{ + commanderSpellbookIntegrationEnabledIndexDisabled, + commanderSpellbookIntegrationEnabledIndexEnabled, + commanderSpellbookIntegrationEnabledIndexAutomatic, + commanderSpellbookIntegrationEnabledIndexUnprompted, +}; + class DeckEditorSettings : public SettingsManager, public IDeckEditorSettingsProvider { Q_OBJECT @@ -15,15 +23,21 @@ public: [[nodiscard]] bool getBannerCardComboBoxVisible() const override; [[nodiscard]] bool getTagsWidgetVisible() const override; [[nodiscard]] int getDefaultDeckEditorType() const override; + [[nodiscard]] int getCommanderSpellbookIntegrationEnabled() const; + [[nodiscard]] bool getCommanderSpellbookIntegrationUseOfficialBracketNames() const; void setOpenDeckInNewTab(bool _openDeckInNewTab); void setBannerCardComboBoxVisible(bool _bannerCardComboBoxVisible); void setTagsWidgetVisible(bool _tagsWidgetVisible); void setDefaultDeckEditorType(int _defaultDeckEditorType); + void setCommanderSpellbookIntegrationEnabled(int _commanderSpellbookIntegrationEnabled); + void setCommanderSpellbookIntegrationUseOfficialBracketNames(bool _useOfficialBracketNames); signals: void bannerCardComboBoxVisibleChanged(bool visible); void tagsWidgetVisibleChanged(bool visible); + void commanderSpellbookIntegrationEnabledChanged(int enabled); + void commanderSpellbookIntegrationUseOfficialBracketNamesChanged(bool useOfficialBracketNames); public: explicit DeckEditorSettings(const QString &settingPath, QObject *parent = nullptr); From e65fcbfa7d34b326eb4eb7838c60ef2fb6055e27 Mon Sep 17 00:00:00 2001 From: BruebachL <44814898+BruebachL@users.noreply.github.com> Date: Sun, 9 Aug 2026 01:44:22 +0200 Subject: [PATCH 19/21] [TabServer] ensure "isOpen" settings value is always persisted/kepy in sync (#7085) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Took 23 minutes Co-authored-by: Lukas Brübach --- cockatrice/src/interface/widgets/tabs/tab_supervisor.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/cockatrice/src/interface/widgets/tabs/tab_supervisor.cpp b/cockatrice/src/interface/widgets/tabs/tab_supervisor.cpp index 07774770e..d8f2e7935 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_supervisor.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_supervisor.cpp @@ -583,6 +583,7 @@ void TabSupervisor::actTabServer(bool checked) void TabSupervisor::openTabServer() { + SettingsCache::instance().tabs().setTabServerOpen(true); if (tabServer) { return; } From 46bdb6df32a2110fed10bf38e4fedcd24d36f6cb Mon Sep 17 00:00:00 2001 From: Galaxy Date: Sat, 8 Aug 2026 20:20:47 -0500 Subject: [PATCH 20/21] Included an Exclude button (ironic) on the Visual Deck Storage. (#7086) --- ...k_preview_color_identity_filter_widget.cpp | 95 +++++++++++++------ ...eck_preview_color_identity_filter_widget.h | 17 +++- 2 files changed, 79 insertions(+), 33 deletions(-) diff --git a/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_color_identity_filter_widget.cpp b/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_color_identity_filter_widget.cpp index 575632724..f1dcf113f 100644 --- a/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_color_identity_filter_widget.cpp +++ b/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_color_identity_filter_widget.cpp @@ -28,11 +28,10 @@ DeckPreviewColorIdentityFilterWidget::DeckPreviewColorIdentityFilterWidget(Visua } toggleButton = new QPushButton(this); - toggleButton->setCheckable(true); layout->addWidget(toggleButton); - // Connect the button's toggled signal - connect(toggleButton, &QPushButton::toggled, this, &DeckPreviewColorIdentityFilterWidget::updateFilterMode); + // Connect the button's clicked signal + connect(toggleButton, &QPushButton::clicked, this, &DeckPreviewColorIdentityFilterWidget::updateFilterMode); connect(this, &DeckPreviewColorIdentityFilterWidget::activeColorsChanged, parent, &VisualDeckStorageWidget::updateColorFilter); connect(this, &DeckPreviewColorIdentityFilterWidget::filterModeChanged, parent, @@ -45,7 +44,17 @@ DeckPreviewColorIdentityFilterWidget::DeckPreviewColorIdentityFilterWidget(Visua void DeckPreviewColorIdentityFilterWidget::retranslateUi() { // Set the toggle button text based on the current mode - toggleButton->setText(exactMatchMode ? tr("Mode: Exact Match") : tr("Mode: Includes")); + switch (filterMode) { + case ExactMatch: + toggleButton->setText(tr("Mode: Exact Match")); + break; + case Includes: + toggleButton->setText(tr("Mode: Includes")); + break; + case Excludes: + toggleButton->setText(tr("Mode: Excludes")); + break; + } toggleButton->setToolTip(tr("Color identity filter mode (AND/OR/NOT conjunctions of filters)")); } @@ -55,11 +64,23 @@ void DeckPreviewColorIdentityFilterWidget::handleColorToggled(QChar color, bool emit activeColorsChanged(); } -void DeckPreviewColorIdentityFilterWidget::updateFilterMode(bool checked) +void DeckPreviewColorIdentityFilterWidget::updateFilterMode() { - exactMatchMode = checked; // Toggle between modes - retranslateUi(); // Update the button text - emit filterModeChanged(exactMatchMode); + // Cycle through the modes + switch (filterMode) { + case ExactMatch: + filterMode = Includes; + break; + case Includes: + filterMode = Excludes; + break; + case Excludes: + filterMode = ExactMatch; + break; + } + + retranslateUi(); // Update the button text + emit filterModeChanged(filterMode); } void DeckPreviewColorIdentityFilterWidget::filterWidgets(QList widgets) @@ -78,41 +99,55 @@ void DeckPreviewColorIdentityFilterWidget::filterWidgets(QListfilteredByColor = false; } + return; } for (const auto &widget : widgets) { QString colorIdentity = widget->getColorIdentity(); bool matchesFilter = true; - if (exactMatchMode) { - // Exact match mode: active colors must exactly match colorIdentity + switch (filterMode) { + case ExactMatch: { + // Exact match mode: active colors must exactly match colorIdentity - // Create a set of active colors - QSet activeColorSet; - for (auto it = activeColors.constBegin(); it != activeColors.constEnd(); ++it) { - if (it.value()) { - activeColorSet.insert(it.key().toUpper()); // Use uppercase for uniformity + // Create a set of active colors + QSet activeColorSet; + for (auto it = activeColors.constBegin(); it != activeColors.constEnd(); ++it) { + if (it.value()) { + activeColorSet.insert(it.key().toUpper()); // Use uppercase for uniformity + } } - } - // Create a set of colors from the color identity string - QSet colorIdentitySet; - for (const QChar &color : colorIdentity) { - colorIdentitySet.insert(color.toUpper()); // Ensure case uniformity - } + // Create a set of colors from the color identity string + QSet colorIdentitySet; + for (const QChar &color : colorIdentity) { + colorIdentitySet.insert(color.toUpper()); // Ensure case uniformity + } - // Compare the sets: the sets must match exactly - if (activeColorSet != colorIdentitySet) { - matchesFilter = false; - } - } else { - // Includes mode: colorIdentity must contain all active colors - for (auto it = activeColors.constBegin(); it != activeColors.constEnd(); ++it) { - if (it.value() && !colorIdentity.contains(it.key())) { + // Compare the sets: the sets must match exactly + if (activeColorSet != colorIdentitySet) { matchesFilter = false; - break; } + break; } + case Includes: + // Includes mode: colorIdentity must contain all active colors + for (auto it = activeColors.constBegin(); it != activeColors.constEnd(); ++it) { + if (it.value() && !colorIdentity.contains(it.key())) { + matchesFilter = false; + break; + } + } + break; + case Excludes: + // Excludes mode: colorIdentity must contain none of the active colors + for (auto it = activeColors.constBegin(); it != activeColors.constEnd(); ++it) { + if (it.value() && colorIdentity.contains(it.key())) { + matchesFilter = false; + break; + } + } + break; } widget->filteredByColor = !matchesFilter; diff --git a/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_color_identity_filter_widget.h b/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_color_identity_filter_widget.h index 0207e2ee2..8e60b16fb 100644 --- a/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_color_identity_filter_widget.h +++ b/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_color_identity_filter_widget.h @@ -21,23 +21,34 @@ class DeckPreviewColorIdentityFilterWidget : public QWidget Q_OBJECT public: + /** + * How the active colors are matched against a deck's color identity. + */ + enum FilterMode + { + ExactMatch, ///< The color identity consists of exactly the active colors. + Includes, ///< The color identity contains all of the active colors. + Excludes ///< The color identity contains none of the active colors. + }; + Q_ENUM(FilterMode) + explicit DeckPreviewColorIdentityFilterWidget(VisualDeckStorageWidget *parent); void retranslateUi(); void filterWidgets(QList widgets); signals: - void filterModeChanged(bool exactMatchMode); + void filterModeChanged(FilterMode mode); void activeColorsChanged(); private slots: void handleColorToggled(QChar color, bool active); - void updateFilterMode(bool checked); + void updateFilterMode(); private: QHBoxLayout *layout; QPushButton *toggleButton; QMap activeColors; - bool exactMatchMode = false; // Default to "includes" mode + FilterMode filterMode = Includes; // Default to "includes" mode }; #endif // DECK_PREVIEW_COLOR_IDENTITY_FILTER_WIDGET_H From c08dc780a72def7a796fe4c831342955512e44a8 Mon Sep 17 00:00:00 2001 From: RickyRister <42636155+RickyRister@users.noreply.github.com> Date: Sat, 8 Aug 2026 23:12:10 -0700 Subject: [PATCH 21/21] [Replay] Implement option to skip empty sections (#7069) * add settings * [Replay] Implement option to skip empty sections * correct starting offset --- .../widgets/replay/replay_manager.cpp | 56 +++++++++++++++++++ .../interface/widgets/replay/replay_manager.h | 3 + .../replay/replay_quick_settings_widget.cpp | 13 +++++ .../replay/replay_quick_settings_widget.h | 6 ++ .../widgets/replay/replay_widget.cpp | 4 ++ .../interface_interface_settings_provider.h | 1 + .../settings/interface_settings.cpp | 10 ++++ .../settings/interface_settings.h | 2 + 8 files changed, 95 insertions(+) diff --git a/cockatrice/src/interface/widgets/replay/replay_manager.cpp b/cockatrice/src/interface/widgets/replay/replay_manager.cpp index c6e7ff1bb..a2c1e0ff0 100644 --- a/cockatrice/src/interface/widgets/replay/replay_manager.cpp +++ b/cockatrice/src/interface/widgets/replay/replay_manager.cpp @@ -3,9 +3,11 @@ #include "../../../client/settings/cache_settings.h" #include +#include #include static constexpr int TIMER_INTERVAL_MS = 200; +static constexpr int EMPTY_SECTION_MARGIN_MS = 500; static QList createReplayTimeline(const GameReplay *replay) { @@ -119,6 +121,10 @@ void ReplayManager::replayTimerTimeout() processNewEvents(NORMAL_PLAYBACK); timeChanged(currentVisualTime); + + if (skipEmptySections) { + handleSkipEmptySection(); + } } /** @brief Processes all unprocessed events up to the current time. */ @@ -149,6 +155,51 @@ void ReplayManager::processNewEvents(PlaybackMode playbackMode) } } +static bool hasMeaningfulEvent(const GameEventContainer &cont) +{ + const int eventListSize = cont.event_list_size(); + for (int i = 0; i < eventListSize; ++i) { + const GameEvent &event = cont.event_list(i); + const auto eventType = static_cast(getPbExtension(event)); + + if (eventType != GameEvent::PLAYER_PROPERTIES_CHANGED) { + return true; + } + } + + return false; +} + +void ReplayManager::handleSkipEmptySection() +{ + if (currentEvent == replayTimeline.size()) { + return; + } + + // find most recent meaningful event + int prevEvent = std::max(0, currentEvent - 1); + for (; prevEvent > 0 && !hasMeaningfulEvent(replay->event_list(prevEvent)); --prevEvent) { + } + + int prevEventTime = replayTimeline.value(prevEvent); + if (currentVisualTime - prevEventTime <= EMPTY_SECTION_MARGIN_MS) { + return; + } + + // find next earliest meaningful event + int nextEvent = currentEvent; + for (; nextEvent < replayTimeline.size() - 1 && !hasMeaningfulEvent(replay->event_list(nextEvent)); ++nextEvent) { + } + + int nextEventTime = replayTimeline.value(nextEvent); + if (nextEventTime - currentVisualTime <= EMPTY_SECTION_MARGIN_MS) { + return; + } + + // skip forward if we're not within margin of either event + skipToTime(nextEventTime - EMPTY_SECTION_MARGIN_MS, false); +} + void ReplayManager::setTimeScaleFactor(qreal _timeScaleFactor) { timeScaleFactor = _timeScaleFactor; @@ -156,6 +207,11 @@ void ReplayManager::setTimeScaleFactor(qreal _timeScaleFactor) replayTimer->setInterval(interval); } +void ReplayManager::setSkipEmptySections(bool value) +{ + skipEmptySections = value; +} + void ReplayManager::startReplay() { replayTimer->start(); diff --git a/cockatrice/src/interface/widgets/replay/replay_manager.h b/cockatrice/src/interface/widgets/replay/replay_manager.h index 16d3591ba..81e66824d 100644 --- a/cockatrice/src/interface/widgets/replay/replay_manager.h +++ b/cockatrice/src/interface/widgets/replay/replay_manager.h @@ -31,6 +31,7 @@ class ReplayManager : public QObject QTimer *rewindBufferingTimer; qreal timeScaleFactor = 1.0; + bool skipEmptySections = false; int currentVisualTime = 0; ///< time currently displayed by the timeline int currentProcessedTime = 0; ///< time that events are currently processed up to. Could differ from visual time due @@ -41,6 +42,7 @@ class ReplayManager : public QObject void handleBackwardsSkip(bool doRewindBuffering); void processRewind(); void processNewEvents(PlaybackMode playbackMode); + void handleSkipEmptySection(); private slots: void replayTimerTimeout(); @@ -63,6 +65,7 @@ public: } void setTimeScaleFactor(qreal _timeScaleFactor); + void setSkipEmptySections(bool value); public slots: void startReplay(); diff --git a/cockatrice/src/interface/widgets/replay/replay_quick_settings_widget.cpp b/cockatrice/src/interface/widgets/replay/replay_quick_settings_widget.cpp index 08113d2cd..5d58705d2 100644 --- a/cockatrice/src/interface/widgets/replay/replay_quick_settings_widget.cpp +++ b/cockatrice/src/interface/widgets/replay/replay_quick_settings_widget.cpp @@ -18,12 +18,17 @@ ReplayQuickSettingsWidget::ReplayQuickSettingsWidget(QWidget *parent) : Settings connect(&fastForwardSpeedBox, qOverload(&QDoubleSpinBox::valueChanged), this, &ReplayQuickSettingsWidget::actUpdateFastForwardSpeed); + skipEmptyCheckBox.setChecked(SettingsCache::instance().userInterface().getSkipEmptySections()); + connect(&skipEmptyCheckBox, &QCheckBox::QT_STATE_CHANGED, this, + &ReplayQuickSettingsWidget::actUpdateSkipEmptySections); + // putting it all together auto *widget = new QWidget; auto *grid = new QGridLayout(widget); grid->setContentsMargins(0, 0, 0, 0); grid->addWidget(&fastForwardSpeedLabel, 0, 0, 1, 1); grid->addWidget(&fastForwardSpeedBox, 0, 1, 1, 1); + grid->addWidget(&skipEmptyCheckBox, 1, 0, 1, 2); this->addSettingsWidget(widget); @@ -36,6 +41,8 @@ void ReplayQuickSettingsWidget::retranslateUi() { fastForwardSpeedLabel.setText(tr("Fast forward speed:")); fastForwardSpeedBox.setSuffix("x"); + + skipEmptyCheckBox.setText(tr("Skip empty sections")); } void ReplayQuickSettingsWidget::actUpdateFastForwardSpeed(qreal value) @@ -43,3 +50,9 @@ void ReplayQuickSettingsWidget::actUpdateFastForwardSpeed(qreal value) SettingsCache::instance().userInterface().setFastForwardSpeed(value); emit fastForwardSpeedChanged(value); } + +void ReplayQuickSettingsWidget::actUpdateSkipEmptySections(QT_STATE_CHANGED_T value) +{ + SettingsCache::instance().userInterface().setSkipEmptySections(value); + emit skipEmptySectionsChanged(value); +} diff --git a/cockatrice/src/interface/widgets/replay/replay_quick_settings_widget.h b/cockatrice/src/interface/widgets/replay/replay_quick_settings_widget.h index b88a8b4e3..a337ea0a6 100644 --- a/cockatrice/src/interface/widgets/replay/replay_quick_settings_widget.h +++ b/cockatrice/src/interface/widgets/replay/replay_quick_settings_widget.h @@ -3,7 +3,9 @@ #include "../../interface/widgets/quick_settings/settings_button_widget.h" +#include #include +#include class ReplayQuickSettingsWidget : public SettingsButtonWidget { @@ -16,13 +18,17 @@ public: signals: void fastForwardSpeedChanged(qreal speed); + void skipEmptySectionsChanged(bool skip); private: QLabel fastForwardSpeedLabel; QDoubleSpinBox fastForwardSpeedBox; + QCheckBox skipEmptyCheckBox; + private slots: void actUpdateFastForwardSpeed(qreal value); + void actUpdateSkipEmptySections(QT_STATE_CHANGED_T value); }; #endif // COCKATRICE_REPLAY_QUICK_SETTINGS_WIDGET_H diff --git a/cockatrice/src/interface/widgets/replay/replay_widget.cpp b/cockatrice/src/interface/widgets/replay/replay_widget.cpp index fc0110ff1..6c85d950e 100644 --- a/cockatrice/src/interface/widgets/replay/replay_widget.cpp +++ b/cockatrice/src/interface/widgets/replay/replay_widget.cpp @@ -66,6 +66,10 @@ ReplayWidget::ReplayWidget(QWidget *parent, GameReplay *replay) settingsWidget->setFixedSize(QSize(32, 32)); connect(settingsWidget, &ReplayQuickSettingsWidget::fastForwardSpeedChanged, this, [this] { updateTimeScaleFactor(replayFastForwardButton->isChecked()); }); + connect(settingsWidget, &ReplayQuickSettingsWidget::skipEmptySectionsChanged, replayManager, + &ReplayManager::setSkipEmptySections); + + replayManager->setSkipEmptySections(SettingsCache::instance().userInterface().getSkipEmptySections()); // putting everything together auto replayControlLayout = new QHBoxLayout; diff --git a/libcockatrice_interfaces/libcockatrice/interfaces/interface_interface_settings_provider.h b/libcockatrice_interfaces/libcockatrice/interfaces/interface_interface_settings_provider.h index 07e60b28f..ab2caa0d7 100644 --- a/libcockatrice_interfaces/libcockatrice/interfaces/interface_interface_settings_provider.h +++ b/libcockatrice_interfaces/libcockatrice/interfaces/interface_interface_settings_provider.h @@ -31,6 +31,7 @@ public: [[nodiscard]] virtual int getMinPlayersForMultiColumnLayout() const = 0; [[nodiscard]] virtual int getRewindBufferingMs() const = 0; [[nodiscard]] virtual qreal getFastForwardSpeed() const = 0; + [[nodiscard]] virtual bool getSkipEmptySections() const = 0; [[nodiscard]] virtual bool getLeftJustified() const = 0; [[nodiscard]] virtual int getZoneViewGroupByIndex() const = 0; [[nodiscard]] virtual int getZoneViewSortByIndex() const = 0; diff --git a/libcockatrice_settings/libcockatrice/settings/interface_settings.cpp b/libcockatrice_settings/libcockatrice/settings/interface_settings.cpp index 0fa56ee33..29c57c57e 100644 --- a/libcockatrice_settings/libcockatrice/settings/interface_settings.cpp +++ b/libcockatrice_settings/libcockatrice/settings/interface_settings.cpp @@ -120,6 +120,11 @@ qreal InterfaceSettings::getFastForwardSpeed() const return getValue("fastForwardSpeed", "replay", QString(), 10).toReal(); } +bool InterfaceSettings::getSkipEmptySections() const +{ + return getValue("skipEmptySections", "replay", QString(), false).toBool(); +} + bool InterfaceSettings::getLeftJustified() const { return getValue("leftJustified", QString(), QString(), false).toBool(); @@ -279,6 +284,11 @@ void InterfaceSettings::setFastForwardSpeed(qreal _value) setValue(_value, "fastForwardSpeed", "replay"); } +void InterfaceSettings::setSkipEmptySections(bool _value) +{ + setValue(_value, "skipEmptySections", "replay"); +} + void InterfaceSettings::setLeftJustified(bool _leftJustified) { setValue(_leftJustified, "leftJustified"); diff --git a/libcockatrice_settings/libcockatrice/settings/interface_settings.h b/libcockatrice_settings/libcockatrice/settings/interface_settings.h index 7ef367cb9..982976310 100644 --- a/libcockatrice_settings/libcockatrice/settings/interface_settings.h +++ b/libcockatrice_settings/libcockatrice/settings/interface_settings.h @@ -34,6 +34,7 @@ public: [[nodiscard]] int getMinPlayersForMultiColumnLayout() const override; [[nodiscard]] int getRewindBufferingMs() const override; [[nodiscard]] qreal getFastForwardSpeed() const override; + [[nodiscard]] bool getSkipEmptySections() const override; [[nodiscard]] bool getLeftJustified() const override; [[nodiscard]] int getZoneViewGroupByIndex() const override; [[nodiscard]] int getZoneViewSortByIndex() const override; @@ -65,6 +66,7 @@ public: void setMinPlayersForMultiColumnLayout(int _minPlayersForMultiColumnLayout); void setRewindBufferingMs(int _rewindBufferingMs); void setFastForwardSpeed(qreal _value); + void setSkipEmptySections(bool _value); void setLeftJustified(bool _leftJustified); void setZoneViewGroupByIndex(int _zoneViewGroupByIndex); void setZoneViewSortByIndex(int _zoneViewSortByIndex);