diff --git a/cockatrice/CMakeLists.txt b/cockatrice/CMakeLists.txt index eac9bc83d..9fd05ae01 100644 --- a/cockatrice/CMakeLists.txt +++ b/cockatrice/CMakeLists.txt @@ -143,7 +143,11 @@ set(cockatrice_SOURCES 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/card_completer_delegate.cpp + src/interface/widgets/utility/card_completer_styler.cpp + src/interface/widgets/utility/completer_utils.cpp src/interface/widgets/utility/line_edit_completer.cpp + src/interface/widgets/utility/reversed_completer_model.cpp src/interface/pixel_map_generator.cpp src/interface/theme_config.cpp src/interface/theme_manager.cpp @@ -257,10 +261,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 @@ -315,6 +322,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 @@ -361,6 +375,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/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..eaa5d96ac 100644 --- a/cockatrice/src/client/settings/cache_settings.cpp +++ b/cockatrice/src/client/settings/cache_settings.cpp @@ -11,18 +11,22 @@ #include #include #include +#include #include #include #include #include #include +#include #include +#include #include #include #include #include #include #include +#include #include #include #include @@ -135,8 +139,12 @@ 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); + commanderBracketSettings = new CommanderBracketSettings(settingsPath, this); // Forward ICardDatabasePathProvider signal from PathsSettings connect(pathsSettings, &PathsSettings::cardDatabasePathChanged, this, @@ -147,7 +155,13 @@ SettingsCache::SettingsCache() releaseChannels << new StableReleaseChannel(); releaseChannels << new BetaReleaseChannel(); - themeName = personalSettings->getThemeName(); + themeName = appearanceSettings->getThemeName(); + + auto definitions = commanderBracketSettings->loadDefinitions(); + if (definitions.isEmpty()) { + definitions = CommanderBracketSettings::defaultDefinitions(); + } + commanderBracketSettings->reloadDefinitions(definitions); loadPaths(); } @@ -155,7 +169,7 @@ SettingsCache::SettingsCache() void SettingsCache::setThemeName(const QString &_themeName) { themeName = _themeName; - personalSettings->setThemeName(themeName); + appearanceSettings->setThemeName(themeName); emit themeChanged(); } @@ -216,15 +230,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 +286,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 +301,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 +426,7 @@ CardsDisplaySettings &SettingsCache::cardsDisplay() const return *cardsDisplaySettings; } -InterfaceSettings &SettingsCache::interface() const +InterfaceSettings &SettingsCache::userInterface() const { return *interfaceSettings; } @@ -422,7 +436,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..23cdb4dbf 100644 --- a/cockatrice/src/client/settings/cache_settings.h +++ b/cockatrice/src/client/settings/cache_settings.h @@ -28,7 +28,9 @@ class CardDatabaseSettings; class CardOverrideSettings; class CardsDisplaySettings; class ChatSettings; +class CommanderBracketSettings; class DebugSettings; +class DeckEditorSettings; class DownloadSettings; class GameFiltersSettings; class GameSettings; @@ -44,6 +46,8 @@ class SoundSettings; class TabsSettings; class UpdatesSettings; class VisualDeckStorageSettings; +class AppearanceSettings; +class NetworkSettings; class QSettings; class SettingsCache : public ICardDatabasePathProvider, public INetworkSettingsProvider @@ -75,8 +79,12 @@ private: PersonalSettings *personalSettings; CardsDisplaySettings *cardsDisplaySettings; InterfaceSettings *interfaceSettings; + DeckEditorSettings *deckEditorSettings; PathsSettings *pathsSettings; VisualDeckStorageSettings *visualDeckStorageSettings; + AppearanceSettings *appearanceSettings; + NetworkSettings *networkSettings; + CommanderBracketSettings *commanderBracketSettings; QString themeName; @@ -138,9 +146,16 @@ 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]] CommanderBracketSettings &commanderBrackets() const + { + return *commanderBracketSettings; + } [[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/game_event_handler.cpp b/cockatrice/src/game/game_event_handler.cpp index 4a96eebdb..c91d08385 100644 --- a/cockatrice/src/game/game_event_handler.cpp +++ b/cockatrice/src/game/game_event_handler.cpp @@ -287,6 +287,9 @@ void GameEventHandler::eventGameStateChanged(const Event_GameStateChanged &event if (!game->getGameMetaInfo()->proto().share_decklists_on_load()) { continue; } + if (!playerInfo.has_deck_list()) { + continue; + } opponentDecksToDisplay.append( qMakePair(playerId, qMakePair(playerName, QString::fromStdString(playerInfo.deck_list())))); 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..2f46e7941 100644 --- a/cockatrice/src/interface/card_picture_loader/card_picture_loader.cpp +++ b/cockatrice/src/interface/card_picture_loader/card_picture_loader.cpp @@ -19,19 +19,22 @@ #include #include #include +#include #include -#include #include // 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; 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(); @@ -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 3724c184d..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 @@ -11,15 +11,17 @@ #include #include #include +#include #include -#include #include #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().personal().getPicDownload()), + : QObject(nullptr), picDownload(SettingsCache::instance().downloads().getPicDownload()), requestQuota(MAX_REQUESTS_PER_SEC) { networkManager = new QNetworkAccessManager(this); @@ -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 ebaf11fff..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,9 +8,13 @@ #include #include #include +#include #include #include -#include +#include + +ServerRateLimiter CardPictureLoaderWorkerWork::s_rateLimiter; +#include // Card back returned by gatherer when card is not found static const QStringList MD5_BLACKLIST = { @@ -20,7 +24,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); @@ -30,19 +34,48 @@ 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().personal(), SIGNAL(picDownloadChanged()), this, SLOT(picDownloadChanged())); + connect(&SettingsCache::instance().downloads(), SIGNAL(picDownloadChanged()), this, SLOT(picDownloadChanged())); startNextPicDownload(); } 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); @@ -211,5 +317,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/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/cockatrice/src/interface/widgets/cards/additional_info/mana_cost_widget.h b/cockatrice/src/interface/widgets/cards/additional_info/mana_cost_widget.h index b2f6b62c0..8bd2e9bc0 100644 --- a/cockatrice/src/interface/widgets/cards/additional_info/mana_cost_widget.h +++ b/cockatrice/src/interface/widgets/cards/additional_info/mana_cost_widget.h @@ -17,7 +17,7 @@ class ManaCostWidget : public QWidget public: explicit ManaCostWidget(QWidget *parent, CardInfoPtr card); - QStringList parseManaCost(const QString &manaString); + static QStringList parseManaCost(const QString &manaString); public slots: void resizeEvent(QResizeEvent *event) override; diff --git a/cockatrice/src/interface/widgets/cards/card_info_picture_enlarged_widget.cpp b/cockatrice/src/interface/widgets/cards/card_info_picture_enlarged_widget.cpp index f442fe425..40bef3df3 100644 --- a/cockatrice/src/interface/widgets/cards/card_info_picture_enlarged_widget.cpp +++ b/cockatrice/src/interface/widgets/cards/card_info_picture_enlarged_widget.cpp @@ -52,12 +52,29 @@ void CardInfoPictureEnlargedWidget::loadPixmap(const QSize &size) * @param size The desired size for the pixmap. * * Sets the widget's pixmap to the card image and resizes the widget to match the specified size. Triggers a repaint. + * + * When the image is not yet cached, the pixmap is cleared (instead of showing a stale previous card) and the widget + * refreshes automatically once the card image finishes loading. */ void CardInfoPictureEnlargedWidget::setCardPixmap(const ExactCard &_card, const QSize size) { + if (card.getCardPtr()) { + disconnect(card.getCardPtr().data(), nullptr, this, nullptr); + } + card = _card; + + // Clear any previous card's art so we never paint a stale pixmap while the new image loads + enlargedPixmap = QPixmap(); loadPixmap(size); + if (card.getCardPtr()) { + connect(card.getCardPtr().data(), &CardInfo::pixmapUpdated, this, [this]() { + loadPixmap(this->size()); + update(); + }); + } + setFixedSize(size); // Set the widget size to the enlarged size update(); // Trigger a repaint 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..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" @@ -12,7 +14,7 @@ #include #include #include -#include +#include #include #include @@ -111,20 +113,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); @@ -136,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; @@ -156,7 +158,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 +169,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); @@ -221,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"); @@ -305,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() @@ -495,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); @@ -748,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:")); @@ -755,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/dialogs/dlg_create_game.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_create_game.cpp index 2b1e9e8c2..59f3e033d 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_create_game.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_create_game.cpp @@ -214,6 +214,7 @@ DlgCreateGame::DlgCreateGame(const ServerInfo_Game &gameInfo, const QMapsetChecked(gameInfo.spectators_need_password()); spectatorsCanTalkCheckBox->setChecked(gameInfo.spectators_can_chat()); spectatorsSeeEverythingCheckBox->setChecked(gameInfo.spectators_omniscient()); + shareDecklistsOnLoadCheckBox->setChecked(gameInfo.share_decklists_on_load()); QSet types; for (int i = 0; i < gameInfo.game_types_size(); ++i) { diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_filter_games.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_filter_games.cpp index 8f498cc2c..4e45f2c25 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_filter_games.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_filter_games.cpp @@ -30,7 +30,7 @@ DlgFilterGames::DlgFilterGames(const QMap &_allGameTypes, hideFullGames = new QCheckBox(tr("Hide full games")); hideFullGames->setChecked(filters.hideFullGames); - hideGamesThatStarted = new QCheckBox(tr("Hide games that have started")); + hideGamesThatStarted = new QCheckBox(tr("Hide started games")); hideGamesThatStarted->setChecked(filters.hideGamesThatStarted); hidePasswordProtectedGames = new QCheckBox(tr("Hide password protected games")); 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/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..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) { @@ -94,7 +96,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(); @@ -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 446427e26..5d58705d2 100644 --- a/cockatrice/src/interface/widgets/replay/replay_quick_settings_widget.cpp +++ b/cockatrice/src/interface/widgets/replay/replay_quick_settings_widget.cpp @@ -14,16 +14,21 @@ 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); + 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,10 +41,18 @@ void ReplayQuickSettingsWidget::retranslateUi() { fastForwardSpeedLabel.setText(tr("Fast forward speed:")); fastForwardSpeedBox.setSuffix("x"); + + skipEmptyCheckBox.setText(tr("Skip empty sections")); } void ReplayQuickSettingsWidget::actUpdateFastForwardSpeed(qreal value) { - SettingsCache::instance().interface().setFastForwardSpeed(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 f5768a7aa..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; @@ -98,7 +102,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/chat_view/chat_view.cpp b/cockatrice/src/interface/widgets/server/chat_view/chat_view.cpp index 5b567e77e..869df4cf3 100644 --- a/cockatrice/src/interface/widgets/server/chat_view/chat_view.cpp +++ b/cockatrice/src/interface/widgets/server/chat_view/chat_view.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include #include @@ -66,12 +67,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); @@ -177,13 +180,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 e722ab1c1..646aa6a80 100644 --- a/cockatrice/src/interface/widgets/server/chat_view/chat_view.h +++ b/cockatrice/src/interface/widgets/server/chat_view/chat_view.h @@ -83,6 +83,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); 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_card_settings_dialog.cpp b/cockatrice/src/interface/widgets/server/user/user_card_settings_dialog.cpp index 56c9600a0..108338332 100644 --- a/cockatrice/src/interface/widgets/server/user/user_card_settings_dialog.cpp +++ b/cockatrice/src/interface/widgets/server/user/user_card_settings_dialog.cpp @@ -1,8 +1,7 @@ #include "user_card_settings_dialog.h" #include "../../../card_picture_loader/card_picture_loader.h" -#include "card/card_completer_proxy_model.h" -#include "card/card_search_model.h" +#include "../../utility/completer_utils.h" #include "card_database_display_model.h" #include "card_database_model.h" #include "user_card_art_provider.h" @@ -19,7 +18,6 @@ #include #include #include -#include #include #include @@ -133,29 +131,14 @@ void UserCardArtSettingsDialog::initializeSearchBar() cardDatabaseModel = new CardDatabaseModel(CardDatabaseManager::getInstance(), false, this); cardDatabaseDisplayModel = new CardDatabaseDisplayModel(this); cardDatabaseDisplayModel->setSourceModel(cardDatabaseModel); - searchModel = new CardSearchModel(cardDatabaseDisplayModel, this); - proxyModel = new CardCompleterProxyModel(this); - proxyModel->setSourceModel(searchModel); - proxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive); - proxyModel->setFilterRole(Qt::DisplayRole); - - completer = new QCompleter(proxyModel, this); - completer->setCompletionRole(Qt::DisplayRole); - completer->setCompletionMode(QCompleter::PopupCompletion); - completer->setCaseSensitivity(Qt::CaseInsensitive); - completer->setFilterMode(Qt::MatchContains); - completer->setMaxVisibleItems(15); + const CardCompleterSetup cardSetup = createCardCompleter(cardDatabaseDisplayModel, this, 15); + searchModel = cardSetup.searchModel; + proxyModel = cardSetup.proxyModel; + completer = cardSetup.completer; searchBar->setCompleter(completer); - connect(searchBar, &QLineEdit::textEdited, searchModel, &CardSearchModel::updateSearchResults); - connect(searchBar, &QLineEdit::textEdited, this, [this](const QString &text) { - const QString pattern = ".*" + QRegularExpression::escape(text) + ".*"; - proxyModel->setFilterRegularExpression(QRegularExpression(pattern, QRegularExpression::CaseInsensitiveOption)); - if (!text.isEmpty()) { - completer->complete(); - } - }); + connectCardCompleterSearch(searchBar, cardSetup); connect(completer, static_cast(&QCompleter::activated), this, [this](const QString &completion) { 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 edb95f2df..112f107d4 100644 --- a/cockatrice/src/interface/widgets/server/user/user_info_popup.cpp +++ b/cockatrice/src/interface/widgets/server/user/user_info_popup.cpp @@ -527,6 +527,21 @@ void UserInfoPopup::onGamesContextMenu(const QPoint &pos) // ── showForUser ─────────────────────────────────────────────────────────────── +void UserInfoPopup::refreshHeader() +{ + if (m_currentUser.isEmpty()) { + return; + } + + const QPixmap avatar = m_avatarCache ? m_avatarCache->value(m_currentUser) : QPixmap{}; + const CardArtParams params = (m_cardArtParamsMap && m_cardArtParamsMap->contains(m_currentUser)) + ? m_cardArtParamsMap->value(m_currentUser) + : CardArtParams{}; + const QString artKey = m_currentUser + u'|' + params.cardName + u'|' + params.cardProviderId; + const QPixmap cardArt = (m_cardArtCache && !params.cardName.isEmpty()) ? m_cardArtCache->value(artKey) : QPixmap{}; + m_header->setUserData(m_currentUserInfo, m_currentOnline, avatar, cardArt, params); +} + void UserInfoPopup::showForUser(const QString &userName, const ServerInfo_User &userInfo, bool online, @@ -538,13 +553,7 @@ void UserInfoPopup::showForUser(const QString &userName, m_currentOnline = online; // Header - const QPixmap avatar = m_avatarCache ? m_avatarCache->value(userName) : QPixmap{}; - const CardArtParams params = (m_cardArtParamsMap && m_cardArtParamsMap->contains(userName)) - ? m_cardArtParamsMap->value(userName) - : CardArtParams{}; - const QString artKey = userName + u'|' + params.cardName + u'|' + params.cardProviderId; - const QPixmap cardArt = (m_cardArtCache && !params.cardName.isEmpty()) ? m_cardArtCache->value(artKey) : QPixmap{}; - m_header->setUserData(userInfo, online, avatar, cardArt, params); + refreshHeader(); // Actions rebuildActionButtons(userInfo, online, isBuddy, isIgnored); 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 69517093f..c634511e1 100644 --- a/cockatrice/src/interface/widgets/server/user/user_info_popup.h +++ b/cockatrice/src/interface/widgets/server/user/user_info_popup.h @@ -116,6 +116,9 @@ public: /** Called when buddy/ignore status changes externally while popup is open. */ void updateActionButtons(const ServerInfo_User &userInfo, bool online, bool isBuddy, bool isIgnored); + /** Re-pulls the avatar/card art for the currently shown user (e.g. after it loads). */ + void refreshHeader(); + signals: void mouseEnteredPopup(); void mouseLeftPopup(); 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..3ad357dd7 100644 --- a/cockatrice/src/interface/widgets/server/user/user_list_widget.cpp +++ b/cockatrice/src/interface/widgets/server/user/user_list_widget.cpp @@ -23,6 +23,7 @@ #include #include #include +#include #include #include #include @@ -30,7 +31,7 @@ #include #include #include -#include +#include #include BanDialog::BanDialog(const ServerInfo_User &info, QWidget *parent) : QDialog(parent) @@ -349,7 +350,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 +358,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; } @@ -495,8 +496,6 @@ UserListWidget::UserListWidget(TabSupervisor *_tabSupervisor, m_userInfoPopup->setWindowOpacity(0.0); m_userInfoPopup->installEventFilter(this); - connectPopupSignals(); - m_showPopupTimer = new QTimer(this); m_showPopupTimer->setSingleShot(true); m_showPopupTimer->setInterval(280); @@ -515,13 +514,15 @@ UserListWidget::UserListWidget(TabSupervisor *_tabSupervisor, } }); + connectPopupSignals(); + userTree->setMouseTracking(true); userTree->viewport()->setMouseTracking(true); userTree->viewport()->installEventFilter(this); // 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(); @@ -543,17 +544,16 @@ UserListWidget::UserListWidget(TabSupervisor *_tabSupervisor, connect(userTree->verticalScrollBar(), &QScrollBar::valueChanged, this, [this] { m_showPopupTimer->stop(); hidePopup(true); + requestAvatarsForVisibleItems(); }); // Forward join requests from popup upward connect(m_userInfoPopup, &UserInfoPopup::joinGameRequested, this, &UserListWidget::joinGameRequested); - connect(avatarProvider, &UserAvatarProvider::avatarUpdated, this, - [this](const QString &) { userTree->viewport()->update(); }); - connect(cardArtProvider, &UserCardArtProvider::cardArtUpdated, this, - [this](const QString &) { userTree->viewport()->update(); }); + connect(avatarProvider, &UserAvatarProvider::avatarUpdated, this, &UserListWidget::refreshVisibleUserHeader); + connect(cardArtProvider, &UserCardArtProvider::cardArtUpdated, this, &UserListWidget::refreshVisibleUserHeader); - connect(&SettingsCache::instance().interface(), &InterfaceSettings::styleUserListChanged, this, + connect(&SettingsCache::instance().appearance(), &AppearanceSettings::styleUserListChanged, this, &UserListWidget::applyDisplayMode); applyDisplayMode(); @@ -633,6 +633,14 @@ void UserListWidget::bind(UserListManager *mgr) rebuild(); } +void UserListWidget::refreshVisibleUserHeader(const QString &name) +{ + userTree->viewport()->update(); + if (m_userInfoPopup->isVisible() && m_userInfoPopup->currentUser() == name) { + m_userInfoPopup->refreshHeader(); + } +} + void UserListWidget::refreshPopupButtons(const QString &userName) { UserListTWI *item = users.value(userName); @@ -657,9 +665,15 @@ void UserListWidget::hideEvent(QHideEvent *e) hidePopup(true); } +void UserListWidget::showEvent(QShowEvent *e) +{ + QGroupBox::showEvent(e); + requestAvatarsForVisibleItems(); +} + 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 +732,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); @@ -758,6 +772,8 @@ void UserListWidget::showPopupForUser(const QString &userName) return; } + avatarProvider->requestAvatar(userName); // ensure the hovered user's avatar is fetched promptly + const ServerInfo_User &info = item->getUserInfo(); const bool online = item->data(0, UserListRoles::Online).toBool(); const bool isBuddy = userContextMenu->getUserListProxy()->isUserBuddy(userName); @@ -801,7 +817,12 @@ void UserListWidget::positionPopup(const QString &userName) const int popH = m_userInfoPopup->height(); const int margin = 12; - const QRect screen = QGuiApplication::primaryScreen()->availableGeometry(); + QScreen *activeScreen = QGuiApplication::screenAt(itemTL); + if (!activeScreen) { + activeScreen = window()->screen(); + } + const QRect screen = + activeScreen ? activeScreen->availableGeometry() : QGuiApplication::primaryScreen()->availableGeometry(); // ── X: prefer the side with more space ─────────────────────────────────── const int spaceLeft = vpTL.x() - screen.left() - margin; @@ -875,6 +896,38 @@ void UserListWidget::retranslateUi() updateCount(); } +void UserListWidget::beginBulkLoad() +{ + m_bulkLoading = true; +} + +void UserListWidget::endBulkLoad() +{ + m_bulkLoading = false; + sortItems(); + requestAvatarsForVisibleItems(); + userTree->viewport()->update(); +} + +bool UserListWidget::isItemNearViewport(const UserListTWI *item) const +{ + // Prefetch a full viewport of rows above and below so scrolling never shows + // an unloaded row. + const QRect nearView = + userTree->viewport()->rect().adjusted(0, -userTree->viewport()->height(), 0, userTree->viewport()->height()); + return userTree->visualItemRect(item).intersects(nearView); +} + +void UserListWidget::requestAvatarsForVisibleItems() +{ + for (int i = 0; i < userTree->topLevelItemCount(); ++i) { + auto *twi = static_cast(userTree->topLevelItem(i)); + if (isItemNearViewport(twi)) { + avatarProvider->requestAvatar(QString::fromStdString(twi->getUserInfo().name())); + } + } +} + void UserListWidget::rebuild() { userTree->clear(); @@ -901,11 +954,11 @@ void UserListWidget::rebuild() break; } + beginBulkLoad(); for (auto it = source->cbegin(); it != source->cend(); ++it) { processUserInfo(it.value(), manager->getOnlineUser(it.key()) != nullptr); } - - sortItems(); + endBulkLoad(); } void UserListWidget::processUserInfo(const ServerInfo_User &user, bool online) @@ -940,11 +993,15 @@ void UserListWidget::processUserInfo(const ServerInfo_User &user, bool online) ++onlineCount; } updateCount(); - avatarProvider->requestAvatar(userName); + if (!m_bulkLoading && isItemNearViewport(item)) { + avatarProvider->requestAvatar(userName); + } } item->setOnline(online); - sortItems(); - userTree->viewport()->update(); + if (!m_bulkLoading) { + sortItems(); + userTree->viewport()->update(); + } } bool UserListWidget::deleteUser(const QString &userName) diff --git a/cockatrice/src/interface/widgets/server/user/user_list_widget.h b/cockatrice/src/interface/widgets/server/user/user_list_widget.h index d70cdfbbd..c98ebebdf 100644 --- a/cockatrice/src/interface/widgets/server/user/user_list_widget.h +++ b/cockatrice/src/interface/widgets/server/user/user_list_widget.h @@ -36,6 +36,7 @@ class QPlainTextEdit; class Response; class CommandContainer; class UserContextMenu; +class QShowEvent; class BanDialog : public QDialog { @@ -158,11 +159,14 @@ private: QTimer *m_hidePopupTimer = nullptr; QString m_hoveredUser; bool m_popupPinned = false; + bool m_bulkLoading = false; void showPopupForUser(const QString &userName); void hidePopup(bool immediate = false); void positionPopup(const QString &userName); void connectPopupSignals(); + bool isItemNearViewport(const UserListTWI *item) const; + void requestAvatarsForVisibleItems(); QMap users; TabSupervisor *tabSupervisor; @@ -177,6 +181,7 @@ private: void refreshPopupButtons(const QString &userName); private slots: void userClicked(QTreeWidgetItem *item, int column); + void refreshVisibleUserHeader(const QString &name); signals: void openMessageDialog(const QString &userName, bool focus); void addBuddy(const QString &userName); @@ -192,6 +197,8 @@ public: QWidget *parent = nullptr); void bind(UserListManager *mgr); void applyDisplayMode(); + void beginBulkLoad(); + void endBulkLoad(); bool eventFilter(QObject *obj, QEvent *event) override; void retranslateUi(); void rebuild(); @@ -207,6 +214,7 @@ public: protected: void hideEvent(QHideEvent *e) override; + void showEvent(QShowEvent *e) override; }; #endif 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/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/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/settings_page/user_interface_settings_page.cpp b/cockatrice/src/interface/widgets/settings_page/user_interface_settings_page.cpp index 8203dee58..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,9 +2,11 @@ #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 +#include #include #include #include @@ -19,70 +21,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 +123,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 +158,60 @@ 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); + + 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); @@ -169,14 +221,18 @@ 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); // 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; @@ -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/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..98b21d0f1 100644 --- a/cockatrice/src/interface/widgets/tabs/api/archidekt/tab_archidekt.cpp +++ b/cockatrice/src/interface/widgets/tabs/api/archidekt/tab_archidekt.cpp @@ -2,12 +2,12 @@ #include "../../../../../client/settings/cache_settings.h" #include "../../../cards/additional_info/mana_symbol_widget.h" +#include "../../../utility/completer_utils.h" #include "../../tab_supervisor.h" #include "api_response/archidekt_deck_listing_api_response.h" #include "display/archidekt_api_response_deck_display_widget.h" #include "display/archidekt_api_response_deck_listings_display_widget.h" -#include #include #include #include @@ -19,7 +19,6 @@ #include #include #include -#include #include #include #include @@ -27,7 +26,7 @@ #include #include #include -#include +#include #include TabArchidekt::TabArchidekt(TabSupervisor *_tabSupervisor) @@ -132,8 +131,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 @@ -279,41 +278,14 @@ void TabArchidekt::setupFilterWidgets() auto cardDatabaseModel = new CardDatabaseModel(CardDatabaseManager::getInstance(), false, this); auto displayModel = new CardDatabaseDisplayModel(this); displayModel->setSourceModel(cardDatabaseModel); - auto *searchModel = new CardSearchModel(displayModel, this); - auto *proxyModel = new CardCompleterProxyModel(this); - proxyModel->setSourceModel(searchModel); - proxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive); - proxyModel->setFilterRole(Qt::DisplayRole); - - auto *completer = new QCompleter(proxyModel, this); - completer->setCompletionRole(Qt::DisplayRole); - completer->setCompletionMode(QCompleter::PopupCompletion); - completer->setCaseSensitivity(Qt::CaseInsensitive); - completer->setFilterMode(Qt::MatchContains); - completer->setMaxVisibleItems(10); - - cardsField->setCompleter(completer); - commandersField->setCompleter(completer); + const CardCompleterSetup cardSetup = createCardCompleter(displayModel, this); + cardsField->setCompleter(cardSetup.completer); + commandersField->setCompleter(cardSetup.completer); // Keep autocomplete working for both fields - connect(cardsField, &QLineEdit::textChanged, this, [=](const QString &text) { - searchModel->updateSearchResults(text); - QString pattern = ".*" + QRegularExpression::escape(text) + ".*"; - proxyModel->setFilterRegularExpression(QRegularExpression(pattern, QRegularExpression::CaseInsensitiveOption)); - if (!text.isEmpty()) { - completer->complete(); - } - }); - - connect(commandersField, &QLineEdit::textChanged, this, [=](const QString &text) { - searchModel->updateSearchResults(text); - QString pattern = ".*" + QRegularExpression::escape(text) + ".*"; - proxyModel->setFilterRegularExpression(QRegularExpression(pattern, QRegularExpression::CaseInsensitiveOption)); - if (!text.isEmpty()) { - completer->complete(); - } - }); + connectCardCompleterSearch(cardsField, cardSetup); + connectCardCompleterSearch(commandersField, cardSetup); // Assemble secondary toolbar secondaryToolbarLayout->addWidget(bracketLabel); @@ -339,8 +311,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/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/widgets/tabs/api/edhrec/tab_edhrec_main.cpp b/cockatrice/src/interface/widgets/tabs/api/edhrec/tab_edhrec_main.cpp index b833f3369..500eab13a 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 @@ -1,6 +1,7 @@ #include "tab_edhrec_main.h" #include "../../../../../client/settings/cache_settings.h" +#include "../../../utility/completer_utils.h" #include "../../tab_supervisor.h" #include "api_response/average_deck/edhrec_average_deck_api_response.h" #include "api_response/commander/edhrec_commander_api_response.h" @@ -12,7 +13,6 @@ #include "display/top_commander/edhrec_top_commanders_api_response_display_widget.h" #include "display/top_tags/edhrec_top_tags_api_response_display_widget.h" -#include #include #include #include @@ -25,7 +25,7 @@ #include #include #include -#include +#include #include static bool canBeCommander(const CardInfoPtr &cardInfo) @@ -63,42 +63,21 @@ TabEdhRecMain::TabEdhRecMain(TabSupervisor *_tabSupervisor) : Tab(_tabSupervisor auto cardDatabaseModel = new CardDatabaseModel(CardDatabaseManager::getInstance(), false, this); auto displayModel = new CardDatabaseDisplayModel(this); displayModel->setSourceModel(cardDatabaseModel); - auto *searchModel = new CardSearchModel(displayModel, this); - auto *proxyModel = new CardCompleterProxyModel(this); - proxyModel->setSourceModel(searchModel); - proxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive); - proxyModel->setFilterRole(Qt::DisplayRole); - - auto *completer = new QCompleter(proxyModel, this); - completer->setCompletionRole(Qt::DisplayRole); - completer->setCompletionMode(QCompleter::PopupCompletion); - completer->setCaseSensitivity(Qt::CaseInsensitive); - completer->setFilterMode(Qt::MatchContains); - completer->setMaxVisibleItems(10); - searchBar->setCompleter(completer); + const CardCompleterSetup cardSetup = createCardCompleter(displayModel, this); + searchBar->setCompleter(cardSetup.completer); // Update suggestions dynamically - connect(searchBar, &QLineEdit::textChanged, searchModel, &CardSearchModel::updateSearchResults); - connect(searchBar, &QLineEdit::textChanged, this, [=](const QString &text) { - // Ensure substring matching - QString pattern = ".*" + QRegularExpression::escape(text) + ".*"; - proxyModel->setFilterRegularExpression(QRegularExpression(pattern, QRegularExpression::CaseInsensitiveOption)); - - if (!text.isEmpty()) { - completer->complete(); // Force the dropdown to appear - } - }); + connectCardCompleterSearch(searchBar, cardSetup); searchPushButton = new QPushButton(navigationContainer); connect(searchPushButton, &QPushButton::clicked, this, [=, this]() { doSearch(); }); 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_account.cpp b/cockatrice/src/interface/widgets/tabs/tab_account.cpp index 2cc8165e8..2c30178f3 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_account.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_account.cpp @@ -135,6 +135,7 @@ void TabAccount::retranslateUi() void TabAccount::processListUsersResponse(const Response &response) { const Response_ListUsers &resp = response.GetExtension(Response_ListUsers::ext); + allUsersList->beginBulkLoad(); for (int i = 0; i < resp.user_list_size(); ++i) { const ServerInfo_User &info = resp.user_list(i); const QString &userName = QString::fromStdString(info.name()); @@ -142,8 +143,8 @@ void TabAccount::processListUsersResponse(const Response &response) ignoreList->setUserOnline(userName, true); buddyList->setUserOnline(userName, true); } + allUsersList->endBulkLoad(); - allUsersList->sortItems(); ignoreList->sortItems(); buddyList->sortItems(); } @@ -188,18 +189,20 @@ void TabAccount::processUserLeftEvent(const Event_UserLeft &event) void TabAccount::buddyListReceived(const QList &_buddyList) { + buddyList->beginBulkLoad(); for (const auto &user : _buddyList) { buddyList->processUserInfo(user, false); } - buddyList->sortItems(); + buddyList->endBulkLoad(); } void TabAccount::ignoreListReceived(const QList &_ignoreList) { + ignoreList->beginBulkLoad(); for (const auto &user : _ignoreList) { ignoreList->processUserInfo(user, false); } - ignoreList->sortItems(); + ignoreList->endBulkLoad(); } void TabAccount::processAddToListEvent(const Event_AddToList &event) diff --git a/cockatrice/src/interface/widgets/tabs/tab_card_art_rules.cpp b/cockatrice/src/interface/widgets/tabs/tab_card_art_rules.cpp index 3dc4de15f..999bc3db0 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_card_art_rules.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_card_art_rules.cpp @@ -1,5 +1,6 @@ #include "tab_card_art_rules.h" +#include "../utility/completer_utils.h" #include "libcockatrice/card/database/card_database_manager.h" #include @@ -194,29 +195,14 @@ void TabCardArtRules::initSearchBar() cardDbModel = new CardDatabaseModel(CardDatabaseManager::getInstance(), false, this); cardDbDisplayModel = new CardDatabaseDisplayModel(this); cardDbDisplayModel->setSourceModel(cardDbModel); - cardSearchModel = new CardSearchModel(cardDbDisplayModel, this); - cardProxyModel = new CardCompleterProxyModel(this); - cardProxyModel->setSourceModel(cardSearchModel); - cardProxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive); - - searchCompleter = new QCompleter(cardProxyModel, this); - searchCompleter->setCompletionRole(Qt::DisplayRole); - searchCompleter->setCompletionMode(QCompleter::PopupCompletion); - searchCompleter->setCaseSensitivity(Qt::CaseInsensitive); - searchCompleter->setFilterMode(Qt::MatchContains); - searchCompleter->setMaxVisibleItems(15); + const CardCompleterSetup cardSetup = createCardCompleter(cardDbDisplayModel, this, 15); + cardSearchModel = cardSetup.searchModel; + cardProxyModel = cardSetup.proxyModel; + searchCompleter = cardSetup.completer; searchEdit->setCompleter(searchCompleter); - connect(searchEdit, &QLineEdit::textEdited, cardSearchModel, &CardSearchModel::updateSearchResults); - connect(searchEdit, &QLineEdit::textEdited, this, [this](const QString &text) { - const QString pattern = ".*" + QRegularExpression::escape(text) + ".*"; - cardProxyModel->setFilterRegularExpression( - QRegularExpression(pattern, QRegularExpression::CaseInsensitiveOption)); - if (!text.isEmpty()) { - searchCompleter->complete(); - } - }); + connectCardCompleterSearch(searchEdit, cardSetup); connect(searchCompleter, static_cast(&QCompleter::activated), this, [this](const QString &name) { searchEdit->setText(name); }); connect(searchEdit, &QLineEdit::editingFinished, this, diff --git a/cockatrice/src/interface/widgets/tabs/tab_game.cpp b/cockatrice/src/interface/widgets/tabs/tab_game.cpp index d3f3a1735..82d99b605 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_game.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_game.cpp @@ -21,10 +21,15 @@ #include "../interface/widgets/cards/card_info_frame_widget.h" #include "../interface/widgets/dialogs/dlg_create_game.h" #include "../interface/widgets/server/user/user_list_manager.h" +#include "../interface/widgets/utility/completer_utils.h" #include "../interface/widgets/utility/line_edit_completer.h" #include "../interface/window_main.h" #include "../main.h" #include "../utility/visibility_change_listener.h" +#include "card/card_completer_proxy_model.h" +#include "card/card_search_model.h" +#include "card_database_display_model.h" +#include "card_database_model.h" #include "tab_supervisor.h" #include @@ -35,7 +40,9 @@ #include #include #include +#include #include +#include #include #include #include @@ -258,7 +265,7 @@ void TabGame::resetChatAndPhase() void TabGame::emitUserEvent() { bool globalEvent = !game->getPlayerManager()->isSpectator() || - SettingsCache::instance().interface().getSpectatorNotificationsEnabled(); + SettingsCache::instance().userInterface().getSpectatorNotificationsEnabled(); emit userEvent(globalEvent); updatePlayerListDockTitle(); } @@ -538,7 +545,8 @@ bool TabGame::leaveGame() void TabGame::actSay() { - if (completer->popup()->isVisible()) { + if (sayEdit->hasVisibleCompleterPopup()) { + sayEdit->hideCompleterPopups(); return; } @@ -558,14 +566,14 @@ void TabGame::addPlayerToAutoCompleteList(QString playerName) { if (sayEdit && !autocompleteUserList.contains(playerName)) { autocompleteUserList << playerName; - sayEdit->setCompletionList(autocompleteUserList); + mentionModel->setStringList(autocompleteUserList); } } void TabGame::removePlayerFromAutoCompleteList(QString playerName) { if (sayEdit && autocompleteUserList.removeOne(playerName)) { - sayEdit->setCompletionList(autocompleteUserList); + mentionModel->setStringList(autocompleteUserList); } } @@ -628,8 +636,8 @@ void TabGame::actRotateViewCCW() void TabGame::actCompleterChanged() { - SettingsCache::instance().chat().getChatMentionCompleter() ? completer->setCompletionRole(2) - : completer->setCompletionRole(1); + SettingsCache::instance().chat().getChatMentionCompleter() ? mentionCompleter->setCompletionRole(2) + : mentionCompleter->setCompletionRole(1); } void TabGame::notifyPlayerJoin(QString playerName) @@ -1281,12 +1289,25 @@ void TabGame::createMessageDock(bool bReplay) sayEdit->setMaxLength(MAX_TEXT_LENGTH); sayLabel->setBuddy(sayEdit); connect(this, &TabGame::chatMessageSent, game->getGameEventHandler(), &GameEventHandler::handleChatMessageSent); - completer = new QCompleter(autocompleteUserList, sayEdit); - completer->setCaseSensitivity(Qt::CaseInsensitive); - completer->setMaxVisibleItems(5); - completer->setFilterMode(Qt::MatchStartsWith); + mentionModel = new QStringListModel(autocompleteUserList, sayEdit); + mentionCompleter = createMentionCompleter(mentionModel, sayEdit); + sayEdit->addCompleter(mentionCompleter, CompleterTrigger::Mention); + + auto *cardDatabaseModel = new CardDatabaseModel(CardDatabaseManager::getInstance(), false, sayEdit); + auto *displayModel = new CardDatabaseDisplayModel(sayEdit); + displayModel->setSourceModel(cardDatabaseModel); + const CardCompleterSetup cardSetup = createCardCompleter(displayModel, sayEdit); + sayEdit->addCompleter(cardSetup.completer, CompleterTrigger::Card); + + connect(sayEdit, &LineEditCompleter::cardPartialChanged, this, [this, cardSetup](const QString &text) { + cardSetup.searchModel->updateSearchResults(text); + cardSetup.proxyModel->setFilterRegularExpression( + QRegularExpression(QRegularExpression::escape(text), QRegularExpression::CaseInsensitiveOption)); + if (sayEdit->hasFocus()) { + cardSetup.completer->complete(); + } + }); - sayEdit->setCompleter(completer); actCompleterChanged(); if (game->getPlayerManager()->isSpectator()) { diff --git a/cockatrice/src/interface/widgets/tabs/tab_game.h b/cockatrice/src/interface/widgets/tabs/tab_game.h index 9c1406b56..fc51817c6 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_game.h +++ b/cockatrice/src/interface/widgets/tabs/tab_game.h @@ -19,6 +19,7 @@ #include #include #include +#include class CardMenu; class ServerInfo_PlayerProperties; @@ -61,7 +62,8 @@ private: const UserListProxy *userListProxy; ReplayWidget *replayWidget = nullptr; QStringList gameTypes; - QCompleter *completer; + QCompleter *mentionCompleter; + QStringListModel *mentionModel; QStringList autocompleteUserList; QStackedWidget *mainWidget; diff --git a/cockatrice/src/interface/widgets/tabs/tab_room.cpp b/cockatrice/src/interface/widgets/tabs/tab_room.cpp index 899f38ec2..705266b1d 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_room.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_room.cpp @@ -8,6 +8,11 @@ #include "../interface/widgets/server/user/user_list_manager.h" #include "../interface/widgets/server/user/user_list_widget.h" #include "../main.h" +#include "../utility/completer_utils.h" +#include "card/card_completer_proxy_model.h" +#include "card/card_search_model.h" +#include "card_database_display_model.h" +#include "card_database_model.h" #include "tab_account.h" #include "tab_supervisor.h" @@ -17,11 +22,14 @@ #include #include #include +#include #include +#include #include #include #include #include +#include #include #include #include @@ -137,13 +145,27 @@ TabRoom::TabRoom(TabSupervisor *_tabSupervisor, gameSelector->processGameInfo(info.game_list(i)); } - completer = new QCompleter(autocompleteUserList, sayEdit); - completer->setCaseSensitivity(Qt::CaseInsensitive); - completer->setMaxVisibleItems(5); - completer->setFilterMode(Qt::MatchStartsWith); + mentionModel = new QStringListModel(autocompleteUserList, sayEdit); + mentionCompleter = createMentionCompleter(mentionModel, sayEdit); + sayEdit->addCompleter(mentionCompleter, CompleterTrigger::Mention); + + auto *cardDatabaseModel = new CardDatabaseModel(CardDatabaseManager::getInstance(), false, sayEdit); + auto *displayModel = new CardDatabaseDisplayModel(sayEdit); + displayModel->setSourceModel(cardDatabaseModel); + const CardCompleterSetup cardSetup = createCardCompleter(displayModel, sayEdit); + sayEdit->addCompleter(cardSetup.completer, CompleterTrigger::Card); + + connect(sayEdit, &LineEditCompleter::cardPartialChanged, this, [this, cardSetup](const QString &text) { + cardSetup.searchModel->updateSearchResults(text); + cardSetup.proxyModel->setFilterRegularExpression( + QRegularExpression(QRegularExpression::escape(text), QRegularExpression::CaseInsensitiveOption)); + if (sayEdit->hasFocus()) { + cardSetup.completer->complete(); + } + }); - sayEdit->setCompleter(completer); actCompleterChanged(); + connect(&SettingsCache::instance().shortcuts(), &ShortcutsSettings::shortCutChanged, this, &TabRoom::refreshShortcuts); refreshShortcuts(); @@ -213,8 +235,8 @@ void TabRoom::sendMessage() { if (sayEdit->text().isEmpty()) { return; - } else if (completer->popup()->isVisible()) { - completer->popup()->hide(); + } else if (sayEdit->hasVisibleCompleterPopup()) { + sayEdit->hideCompleterPopups(); return; } else { Command_RoomSay cmd; @@ -242,14 +264,14 @@ void TabRoom::actClearChat() void TabRoom::actOpenChatSettings() { DlgSettings settings(this); - settings.setTab(4); + settings.setTab(DlgSettings::MessagesPage); settings.exec(); } void TabRoom::actCompleterChanged() { - SettingsCache::instance().chat().getChatMentionCompleter() ? completer->setCompletionRole(2) - : completer->setCompletionRole(1); + SettingsCache::instance().chat().getChatMentionCompleter() ? mentionCompleter->setCompletionRole(2) + : mentionCompleter->setCompletionRole(1); } void TabRoom::processRoomEvent(const RoomEvent &event) @@ -285,16 +307,18 @@ void TabRoom::processListGamesEvent(const Event_ListGames &event) void TabRoom::processJoinRoomEvent(const Event_JoinRoom &event) { - if (!autocompleteUserList.contains("@" + QString::fromStdString(event.user_info().name()))) { - autocompleteUserList << "@" + QString::fromStdString(event.user_info().name()); - sayEdit->setCompletionList(autocompleteUserList); + QString mention = "@" + QString::fromStdString(event.user_info().name()); + if (!autocompleteUserList.contains(mention)) { + autocompleteUserList << mention; + mentionModel->setStringList(autocompleteUserList); } } void TabRoom::processLeaveRoomEvent(const Event_LeaveRoom &event) { - autocompleteUserList.removeOne("@" + QString::fromStdString(event.name())); - sayEdit->setCompletionList(autocompleteUserList); + QString mention = "@" + QString::fromStdString(event.name()); + autocompleteUserList.removeOne(mention); + mentionModel->setStringList(autocompleteUserList); } void TabRoom::processRoomSayEvent(const Event_RoomSay &event) diff --git a/cockatrice/src/interface/widgets/tabs/tab_room.h b/cockatrice/src/interface/widgets/tabs/tab_room.h index 2881c25f4..dc58b8bf6 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_room.h +++ b/cockatrice/src/interface/widgets/tabs/tab_room.h @@ -14,6 +14,7 @@ #include #include #include +#include class UserListProxy; class UserListManager; @@ -63,6 +64,7 @@ private: ChatView *chatView; QLabel *sayLabel; LineEditCompleter *sayEdit; + QStringListModel *mentionModel; QGroupBox *chatGroupBox; QMenu *roomMenu; @@ -72,7 +74,7 @@ private: [[nodiscard]] QString sanitizeHtml(QString dirty) const; QStringList autocompleteUserList; - QCompleter *completer; + QCompleter *mentionCompleter; signals: void roomClosing(TabRoom *tab); void openMessageDialog(const QString &userName, bool focus); diff --git a/cockatrice/src/interface/widgets/tabs/tab_supervisor.cpp b/cockatrice/src/interface/widgets/tabs/tab_supervisor.cpp index 3d03a1863..d8f2e7935 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 { @@ -583,6 +583,7 @@ void TabSupervisor::actTabServer(bool checked) void TabSupervisor::openTabServer() { + SettingsCache::instance().tabs().setTabServerOpen(true); if (tabServer) { return; } @@ -910,7 +911,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 +1010,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 +1105,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/utility/card_completer_delegate.cpp b/cockatrice/src/interface/widgets/utility/card_completer_delegate.cpp new file mode 100644 index 000000000..6cf096cd4 --- /dev/null +++ b/cockatrice/src/interface/widgets/utility/card_completer_delegate.cpp @@ -0,0 +1,439 @@ +#include "card_completer_delegate.h" + +#include "../cards/additional_info/mana_cost_widget.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// --------------------------------------------------------------------------- +// Internal colour helpers +// --------------------------------------------------------------------------- + +namespace +{ + +struct ManaColor +{ + QColor fill; + QColor rim; + QColor text; +}; + +ManaColor manaColor(QChar symbol) +{ + switch (symbol.unicode()) { + case 'W': + return {QColor(248, 248, 246), QColor(190, 180, 160), QColor(80, 70, 50)}; + case 'U': + return {QColor(55, 130, 210), QColor(30, 90, 160), QColor(255, 255, 255)}; + case 'B': + return {QColor(90, 65, 160), QColor(55, 38, 110), QColor(220, 200, 255)}; + case 'R': + return {QColor(210, 55, 55), QColor(150, 30, 30), QColor(255, 255, 255)}; + case 'G': + return {QColor(45, 148, 90), QColor(28, 95, 58), QColor(255, 255, 255)}; + default: + return {QColor(100, 115, 135), QColor(65, 78, 95), QColor(230, 235, 240)}; + } +} + +QColor blend(QColor a, QColor b, qreal t) +{ + return QColor::fromRgbF(a.redF() + (b.redF() - a.redF()) * t, a.greenF() + (b.greenF() - a.greenF()) * t, + a.blueF() + (b.blueF() - a.blueF()) * t, a.alphaF() + (b.alphaF() - a.alphaF()) * t); +} + +} // namespace + +// --------------------------------------------------------------------------- + +QColor CardCompleterDelegate::accentForColors(const QString &colors) +{ + if (colors.isEmpty()) { + return QColor(100, 115, 135); + } + + QSet seen; + for (const QChar c : colors) { + if (QString("WUBRG").contains(c)) { + seen.insert(c); + } + } + + if (seen.size() > 1) { + return QColor(205, 145, 25); + } + + if (seen.isEmpty()) { + return QColor(100, 115, 135); + } + + return manaColor(*seen.begin()).fill; +} + +// --------------------------------------------------------------------------- + +CardCompleterDelegate::CardCompleterDelegate(QObject *parent) : QStyledItemDelegate(parent) +{ + symbolCache.setMaxCost(64); + setCodeCache.setMaxCost(64); +} + +// --------------------------------------------------------------------------- +// sizeHint +// --------------------------------------------------------------------------- + +QSize CardCompleterDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const +{ + Q_UNUSED(option) + + if (!index.isValid()) { + return QStyledItemDelegate::sizeHint(option, index); + } + + // Fixed wide rows so the popup has room for name, type line, set and mana + return {480, CardRowHeight}; +} + +// --------------------------------------------------------------------------- +// Mana symbol painting +// --------------------------------------------------------------------------- + +const QPixmap *CardCompleterDelegate::cachedSymbolPixmap(const QString &symbol, int size) const +{ + const QString key = symbol + QString::number(size); + + if (symbolCache.contains(key)) { + return symbolCache[key]; + } + + QPixmap src(QString("theme:icons/mana/%1").arg(symbol)); + + if (!src.isNull()) { + auto *pm = new QPixmap(src.scaled(size, size, Qt::KeepAspectRatio, Qt::SmoothTransformation)); + + symbolCache.insert(key, pm); + return pm; + } + + return nullptr; +} + +// --------------------------------------------------------------------------- + +void CardCompleterDelegate::drawManaSymbol(QPainter *p, QPoint centre, const QString &symbol, int radius) const +{ + const QRect pip(centre.x() - radius, centre.y() - radius, radius * 2, radius * 2); + + const QPixmap *px = cachedSymbolPixmap(symbol, radius * 2); + + if (px && !px->isNull()) { + p->drawPixmap(pip, *px); + return; + } + + bool isNumeric = false; + const int numVal = symbol.toInt(&isNumeric); + + const QString label = isNumeric ? QString::number(numVal) : symbol; + + const ManaColor mc = + (symbol.length() == 1 && QString("WUBRG").contains(symbol)) ? manaColor(symbol[0]) : manaColor(QChar('X')); + + QPainterPath circle; + circle.addEllipse(pip); + + p->save(); + p->setClipPath(circle); + p->fillPath(circle, mc.fill); + p->restore(); + + p->setPen(QPen(mc.rim, 1.2)); + p->setBrush(Qt::NoBrush); + p->drawEllipse(pip.adjusted(1, 1, -1, -1)); + + QFont f = p->font(); + f.setPixelSize(qMax(radius - 1, 7)); + f.setBold(true); + + p->setFont(f); + p->setPen(mc.text); + p->drawText(pip, Qt::AlignCenter, label); +} + +// --------------------------------------------------------------------------- + +int CardCompleterDelegate::drawManaCost(QPainter *p, const QRect &row, const QString &manaCost, int radius) const +{ + if (manaCost.isEmpty()) { + return row.right(); + } + + const int diam = radius * 2; + + // Split, adventure, aftermath and prepare cards store both halves of the + // cost joined by "//" (e.g. "1W // W"); draw each half as its own group. + static const QRegularExpression splitRegex("\\s*//\\s*"); + + QList parts; + + for (const QString &part : manaCost.split(splitRegex, Qt::SkipEmptyParts)) { + const QStringList symbols = ManaCostWidget::parseManaCost(part); + + if (!symbols.isEmpty()) { + parts.append(symbols); + } + } + + int totalW = 0; + + for (int i = 0; i < parts.size(); ++i) { + if (i > 0) { + totalW += PartGap; + } + + totalW += parts.at(i).size() * diam + qMax(0, parts.at(i).size() - 1) * SymbolSpacing; + } + + const int rightPad = 14; + + int x = row.right() - rightPad - totalW + radius; + + const int cy = row.center().y(); + + for (int i = 0; i < parts.size(); ++i) { + const QStringList &symbols = parts.at(i); + + for (const QString &sym : symbols) { + drawManaSymbol(p, {x, cy}, sym, radius); + x += diam + SymbolSpacing; + } + + if (i < parts.size() - 1) { + x += PartGap - SymbolSpacing; + } + } + + return row.right() - rightPad - totalW - 10; +} + +// --------------------------------------------------------------------------- + +QString CardCompleterDelegate::setCodeForCard(const QSharedPointer &card) const +{ + if (!card) { + return QString(); + } + + const QString name = card->getName(); + + if (setCodeCache.contains(name)) { + return *setCodeCache[name]; + } + + QString code; + + const PrintingInfo printing = CardDatabaseManager::query()->getPreferredPrinting(card); + + if (auto set = printing.getSet()) { + code = set->getShortName(); + } + + auto *cached = new QString(code); + setCodeCache.insert(name, cached); + return code; +} + +// --------------------------------------------------------------------------- +// paint +// --------------------------------------------------------------------------- + +void CardCompleterDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const +{ + if (!index.isValid()) { + QStyledItemDelegate::paint(painter, option, index); + return; + } + + const QRect rect = option.rect; + const QPalette &pal = option.palette; + + const bool selected = option.state & QStyle::State_Selected; + const bool hovered = option.state & QStyle::State_MouseOver; + + painter->save(); + painter->setRenderHints(QPainter::Antialiasing | QPainter::TextAntialiasing | QPainter::SmoothPixmapTransform); + + auto card = index.data(CardSearchModel::CardInfoRole).value>(); + + if (!card) { + painter->fillRect(rect, pal.color(QPalette::Base)); + painter->restore(); + return; + } + + const QString cardName = card->getName(); + const QString manaCost = card->getManaCost(); + const QString typeLine = card->getCardType(); + const QString setCode = setCodeForCard(card); + + const QColor accent = accentForColors(card->getColors()); + + const QColor base = pal.color(QPalette::Base); + const QColor textColor = pal.color(QPalette::Text); + const QColor secondaryColor = pal.color(QPalette::PlaceholderText); + + QColor tinted = blend(base, accent, 0.40); + + if (hovered) { + tinted = blend(tinted, Qt::white, 0.05); + } + + const QRectF cardRect = rect.adjusted(3, 2, -3, -2); + + // ----------------------------------------------------------------------- + // Main card body + // ----------------------------------------------------------------------- + + QPainterPath path; + path.addRoundedRect(cardRect, 7, 7); + + painter->save(); + painter->setClipPath(path); + + QLinearGradient bodyGrad(cardRect.topLeft(), cardRect.bottomLeft()); + + bodyGrad.setColorAt(0.0, blend(tinted, Qt::white, 0.10)); + bodyGrad.setColorAt(1.0, blend(tinted, Qt::black, 0.18)); + + painter->fillPath(path, bodyGrad); + + // ----------------------------------------------------------------------- + // Accent strip + // ----------------------------------------------------------------------- + + QRectF accentRect(cardRect.left(), cardRect.top(), AccentBarWidth, cardRect.height()); + + QLinearGradient accentGrad(accentRect.topLeft(), accentRect.bottomLeft()); + + accentGrad.setColorAt(0.0, blend(accent, Qt::white, 0.20)); + accentGrad.setColorAt(1.0, blend(accent, Qt::black, 0.25)); + + painter->fillRect(accentRect, accentGrad); + + // ----------------------------------------------------------------------- + // Right mana zone + // ----------------------------------------------------------------------- + + const QRectF manaZone(cardRect.right() - 110, cardRect.top(), 110, cardRect.height()); + + QLinearGradient manaGrad(manaZone.topLeft(), manaZone.bottomLeft()); + + manaGrad.setColorAt(0, QColor(0, 0, 0, 18)); + manaGrad.setColorAt(1, QColor(0, 0, 0, 42)); + + painter->fillRect(manaZone, manaGrad); + + painter->restore(); + + // ----------------------------------------------------------------------- + // Border + // ----------------------------------------------------------------------- + + QColor border = blend(accent, Qt::black, 0.45); + + if (hovered) { + border = blend(border, Qt::white, 0.18); + } + + painter->setPen(QPen(border, 1.2)); + painter->drawPath(path); + + // ----------------------------------------------------------------------- + // Selection glow + // ----------------------------------------------------------------------- + + if (selected) { + QColor glow = pal.color(QPalette::Highlight); + glow.setAlpha(30); + + painter->fillPath(path, glow); + + painter->setPen(QPen(pal.color(QPalette::Highlight), 2)); + painter->drawPath(path); + } + + // ----------------------------------------------------------------------- + // Mana cost + // ----------------------------------------------------------------------- + + const int costLeft = drawManaCost(painter, cardRect.toRect(), manaCost, SymbolRadius); + + // ----------------------------------------------------------------------- + // Card name + type line + set code + // ----------------------------------------------------------------------- + + const int textLeft = cardRect.left() + AccentBarWidth + 12; + const int textRight = costLeft - 8; + const int textWidth = qMax(0, textRight - textLeft); + + // ----------------------------------------------------------------------- + // Card name (top band) + // ----------------------------------------------------------------------- + + { + const QRect nameRect(textLeft, rect.top() + 2, textWidth, 20); + + QFont f = option.font; + f.setPixelSize(13); + f.setBold(true); + + painter->setFont(f); + + const QString nameText = QFontMetrics(f).elidedText(cardName, Qt::ElideRight, nameRect.width()); + + painter->setPen(QColor(0, 0, 0, 140)); + painter->drawText(nameRect.translated(0, 1), Qt::AlignLeft | Qt::AlignVCenter, nameText); + + painter->setPen(textColor); + painter->drawText(nameRect, Qt::AlignLeft | Qt::AlignVCenter, nameText); + } + + // ----------------------------------------------------------------------- + // Type line + set code (bottom band) + // ----------------------------------------------------------------------- + + { + const QRect infoRect(textLeft, rect.top() + 22, textWidth, rect.height() - 24); + + QFont f = option.font; + f.setPixelSize(10); + + painter->setFont(f); + + QString infoLine = typeLine; + + if (!setCode.isEmpty()) { + infoLine += " \u00b7 " + setCode; + } + + const QString infoText = QFontMetrics(f).elidedText(infoLine, Qt::ElideRight, infoRect.width()); + + painter->setPen(QColor(0, 0, 0, 120)); + painter->drawText(infoRect.translated(0, 1), Qt::AlignLeft | Qt::AlignVCenter, infoText); + + painter->setPen(secondaryColor); + painter->drawText(infoRect, Qt::AlignLeft | Qt::AlignVCenter, infoText); + } + + painter->restore(); +} diff --git a/cockatrice/src/interface/widgets/utility/card_completer_delegate.h b/cockatrice/src/interface/widgets/utility/card_completer_delegate.h new file mode 100644 index 000000000..18661a21b --- /dev/null +++ b/cockatrice/src/interface/widgets/utility/card_completer_delegate.h @@ -0,0 +1,63 @@ +/** + * @file card_completer_delegate.h + * @ingroup UtilityWidgets + */ +//! \todo Document this file. + +#ifndef CARD_COMPLETER_DELEGATE_H +#define CARD_COMPLETER_DELEGATE_H + +#include +#include +#include +#include + +class CardInfo; + +/** + * @brief Paints styled card completer popup rows. + * + * Each row shows the card name, type line, set code and mana cost pips, + * color-coded by the card's color identity. Card data is read directly from + * the CardSearchModel::CardInfoRole so no extra database lookups are needed. + */ +class CardCompleterDelegate : public QStyledItemDelegate +{ + Q_OBJECT +public: + explicit CardCompleterDelegate(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; + +private: + // Mana symbol pixmaps, loaded once and cached + mutable QCache symbolCache; + + // Set short codes, resolved once per card name and cached + mutable QCache setCodeCache; + + // Resolve the card's color string ("RG", "W", "", ...) → accent QColor + static QColor accentForColors(const QString &colors); + + // Draw a single mana symbol pip at centre point + void drawManaSymbol(QPainter *p, QPoint centre, const QString &symbol, int radius) const; + + // Draw all mana pips for a cost string like "2RG" or "{2}{R}{G}"; split and + // adventure costs ("1W // W") are drawn as separate groups. Returns the left-most x used + int drawManaCost(QPainter *p, const QRect &row, const QString &manaCost, int radius) const; + + // Load (or return cached) a mana icon pixmap; falls back to painted circle + const QPixmap *cachedSymbolPixmap(const QString &symbol, int size) const; + + // Resolve the preferred printing's set short code for a card + QString setCodeForCard(const QSharedPointer &card) const; + + static constexpr int CardRowHeight = 40; + static constexpr int AccentBarWidth = 5; + static constexpr int SymbolRadius = 9; + static constexpr int SymbolSpacing = 2; + static constexpr int PartGap = 14; +}; + +#endif // CARD_COMPLETER_DELEGATE_H diff --git a/cockatrice/src/interface/widgets/utility/card_completer_styler.cpp b/cockatrice/src/interface/widgets/utility/card_completer_styler.cpp new file mode 100644 index 000000000..6794ccbb4 --- /dev/null +++ b/cockatrice/src/interface/widgets/utility/card_completer_styler.cpp @@ -0,0 +1,340 @@ +#include "card_completer_styler.h" + +#include "../cards/card_info_picture_enlarged_widget.h" +#include "card_completer_delegate.h" +#include "reversed_completer_model.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace +{ +const QSize PreviewSize(300, 419); +const int PreviewMargin = 16; +const int FadeDuration = 120; +} // namespace + +// --------------------------------------------------------------------------- + +void CardCompleterStyler::apply(QCompleter *completer) +{ + if (!completer) { + return; + } + + // The styler lives as long as the completer + new CardCompleterStyler(completer, completer); +} + +// --------------------------------------------------------------------------- + +CardCompleterStyler::CardCompleterStyler(QCompleter *completer, QObject *parent) + : QObject(parent), completer(completer), reversedModel(nullptr), preview(nullptr), above(false) +{ + auto *popup = completer->popup(); + + // Wrap the completer's model so its row order can be reversed when the + // popup is shown above the text edit + QAbstractItemModel *sourceModel = completer->model(); + reversedModel = new ReversedCompleterModel(completer); + reversedModel->setSourceModel(sourceModel); + completer->setModel(reversedModel); + + popup->setItemDelegate(new CardCompleterDelegate(popup)); + + popup->viewport()->setMouseTracking(true); + + popup->installEventFilter(this); + popup->viewport()->installEventFilter(this); + + connect(popup->selectionModel(), &QItemSelectionModel::currentRowChanged, this, + &CardCompleterStyler::updatePreview); + connect(completer, qOverload(&QCompleter::activated), this, &CardCompleterStyler::hidePreview); + connect(completer->completionModel(), &QAbstractItemModel::modelReset, this, + &CardCompleterStyler::onCompletionReset); +} + +CardCompleterStyler::~CardCompleterStyler() +{ + if (preview) { + preview->deleteLater(); + } +} + +// --------------------------------------------------------------------------- + +bool CardCompleterStyler::eventFilter(QObject *obj, QEvent *ev) +{ + auto *popup = completer->popup(); + + if (obj == popup->viewport()) { + if (ev->type() == QEvent::MouseMove) { + updatePreviewFromHover(static_cast(ev)->pos()); + } + } else if (obj == popup) { + switch (ev->type()) { + case QEvent::Show: + case QEvent::Move: + case QEvent::Resize: + updateOrientation(); + reposition(); + break; + case QEvent::Hide: + hidePreview(); + break; + case QEvent::KeyPress: + if (handlePopupKeyPress(static_cast(ev))) { + return true; + } + break; + default: + break; + } + } + + return QObject::eventFilter(obj, ev); +} + +// --------------------------------------------------------------------------- + +bool CardCompleterStyler::handlePopupKeyPress(QKeyEvent *event) +{ + const int key = event->key(); + if (key != Qt::Key_Up && key != Qt::Key_Down) { + return false; + } + + if (!above) { + return false; + } + + auto *popup = completer->popup(); + const int rowCount = completer->completionModel()->rowCount(); + if (rowCount == 0) { + return true; + } + + // With the popup above the text edit the rows are shown in reverse order and + // the closest match is in the bottom-most row. The up arrow then advances + // through the list (towards its end at the top of the popup). + const int currentRow = popup->currentIndex().row(); + const int step = (key == Qt::Key_Up) ? -1 : 1; + const int newRow = qBound(0, currentRow + step, rowCount - 1); + + if (newRow != currentRow) { + popup->setCurrentIndex(completer->completionModel()->index(newRow, completer->completionColumn())); + } + + return true; +} + +// --------------------------------------------------------------------------- + +void CardCompleterStyler::updateOrientation() +{ + above = isPopupAboveWidget(); + reversedModel->setEnabled(above); +} + +// --------------------------------------------------------------------------- + +void CardCompleterStyler::ensureClosestSelected() +{ + auto *popup = completer->popup(); + auto *completionModel = completer->completionModel(); + const int rowCount = completionModel->rowCount(); + if (rowCount == 0) { + return; + } + + const int currentRow = popup->currentIndex().row(); + const int closestRow = rowCount - 1; + + if (currentRow != closestRow) { + popup->setCurrentIndex(completionModel->index(closestRow, completer->completionColumn())); + } +} + +// --------------------------------------------------------------------------- + +void CardCompleterStyler::onCompletionReset() +{ + if (above) { + ensureClosestSelected(); + } +} + +// --------------------------------------------------------------------------- + +bool CardCompleterStyler::isPopupAboveWidget() const +{ + auto *popup = completer->popup(); + QWidget *widget = completer->widget(); + + if (!widget || !popup->isVisible()) { + return false; + } + + const QPoint popupBottom = popup->mapToGlobal(QPoint(0, popup->height())); + const QPoint fieldTop = widget->mapToGlobal(QPoint(0, 0)); + + return popupBottom.y() <= fieldTop.y(); +} + +// --------------------------------------------------------------------------- + +void CardCompleterStyler::updatePreviewFromHover(const QPoint &pos) +{ + const QModelIndex index = completer->popup()->indexAt(pos); + + // Hovering updates the preview but must not change the current completion + if (index.isValid() && index != previewedIndex) { + updatePreview(index); + } +} + +// --------------------------------------------------------------------------- + +void CardCompleterStyler::updatePreview(const QModelIndex &index) +{ + previewedIndex = index; + + if (!index.isValid()) { + hidePreview(); + return; + } + + auto card = index.data(CardSearchModel::CardInfoRole).value>(); + + if (!card) { + hidePreview(); + return; + } + + if (!preview) { + QWidget *contextWindow = completer->widget() ? completer->widget()->window() : nullptr; + + preview = new CardInfoPictureEnlargedWidget(contextWindow); + preview->hide(); + preview->setWindowOpacity(0.0); + } + + const ExactCard exact = CardDatabaseManager::query()->getCard({card->getName()}); + + if (!exact) { + hidePreview(); + return; + } + + preview->setCardPixmap(exact, PreviewSize); + + reposition(); + showPreview(); +} + +// --------------------------------------------------------------------------- + +void CardCompleterStyler::showPreview() +{ + if (!preview) { + return; + } + + stopPreviewFade(); + + preview->show(); + preview->raise(); + + auto *fade = new QPropertyAnimation(preview, "windowOpacity", preview); + + fade->setDuration(FadeDuration); + fade->setStartValue(preview->windowOpacity()); + fade->setEndValue(1.0); + fade->start(QAbstractAnimation::DeleteWhenStopped); +} + +// --------------------------------------------------------------------------- + +void CardCompleterStyler::hidePreview() +{ + if (!preview) { + return; + } + + // Stop any in-flight fade first so a stale one cannot keep the preview visible + stopPreviewFade(); + preview->hide(); +} + +// --------------------------------------------------------------------------- + +void CardCompleterStyler::stopPreviewFade() +{ + if (!preview) { + return; + } + + const auto animations = preview->findChildren(); + for (auto *animation : animations) { + animation->stop(); + } +} + +// --------------------------------------------------------------------------- + +void CardCompleterStyler::reposition() +{ + if (!preview) { + return; + } + + auto *popup = completer->popup(); + + const QRect popupGlobalRect(popup->mapToGlobal(popup->rect().topLeft()), popup->rect().size()); + + int anchorY; + + if (previewedIndex.isValid()) { + const QRect itemRect = popup->visualRect(previewedIndex); + const QPoint itemCenter = popup->viewport()->mapToGlobal(itemRect.center()); + anchorY = itemCenter.y(); + } else { + anchorY = popupGlobalRect.center().y(); + } + + const QScreen *screen = popup->screen(); + const QRect screenGeom = screen ? screen->availableGeometry() : QRect(); + + const int rightX = popupGlobalRect.right() + PreviewMargin; + const int leftX = popupGlobalRect.left() - PreviewMargin - preview->width(); + + int x; + int y = anchorY - preview->height() / 2; + + if (screenGeom.isEmpty()) { + x = rightX; + } else if (rightX + preview->width() <= screenGeom.right()) { + x = rightX; + } else if (leftX >= screenGeom.left()) { + x = leftX; + } else { + x = rightX; + } + + if (!screenGeom.isEmpty()) { + x = qBound(screenGeom.left(), x, qMax(screenGeom.left(), screenGeom.right() - preview->width())); + y = qBound(screenGeom.top(), y, qMax(screenGeom.top(), screenGeom.bottom() - preview->height())); + } + + preview->move(x, y); +} diff --git a/cockatrice/src/interface/widgets/utility/card_completer_styler.h b/cockatrice/src/interface/widgets/utility/card_completer_styler.h new file mode 100644 index 000000000..616e3fe61 --- /dev/null +++ b/cockatrice/src/interface/widgets/utility/card_completer_styler.h @@ -0,0 +1,67 @@ +/** + * @file card_completer_styler.h + * @ingroup UtilityWidgets + */ +//! \todo Document this file. + +#ifndef CARD_COMPLETER_STYLER_H +#define CARD_COMPLETER_STYLER_H + +#include +#include + +class CardInfoPictureEnlargedWidget; +class QCompleter; +class QKeyEvent; +class QPoint; +class ReversedCompleterModel; + +/** + * @brief Applies styled row painting and a card-image preview to a card completer. + * + * The completer popup rows are painted by CardCompleterDelegate and the image of + * the currently selected (or hovered) row is shown beside the popup. + */ +class CardCompleterStyler : public QObject +{ + Q_OBJECT +public: + /** + * @brief Styles @p completer and follows its current selection. + * + * The styler is parented to the completer so it lives exactly as long as the + * completer itself. + */ + static void apply(QCompleter *completer); + +protected: + bool eventFilter(QObject *obj, QEvent *ev) override; + +private slots: + void updatePreview(const QModelIndex &index); + void updatePreviewFromHover(const QPoint &pos); + void onCompletionReset(); + +private: + explicit CardCompleterStyler(QCompleter *completer, QObject *parent = nullptr); + ~CardCompleterStyler() override; + + void showPreview(); + void hidePreview(); + void stopPreviewFade(); + void reposition(); + + void updateOrientation(); + void ensureClosestSelected(); + + bool handlePopupKeyPress(QKeyEvent *event); + bool isPopupAboveWidget() const; + + QCompleter *completer; + ReversedCompleterModel *reversedModel; + CardInfoPictureEnlargedWidget *preview; + QModelIndex previewedIndex; + bool above; +}; + +#endif // CARD_COMPLETER_STYLER_H diff --git a/cockatrice/src/interface/widgets/utility/completer_utils.cpp b/cockatrice/src/interface/widgets/utility/completer_utils.cpp new file mode 100644 index 000000000..16d5cfd13 --- /dev/null +++ b/cockatrice/src/interface/widgets/utility/completer_utils.cpp @@ -0,0 +1,52 @@ +#include "completer_utils.h" + +#include "card_completer_styler.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +CardCompleterSetup createCardCompleter(CardDatabaseDisplayModel *displayModel, QObject *parent, int maxVisibleItems) +{ + auto *searchModel = new CardSearchModel(displayModel, parent); + + auto *proxyModel = new CardCompleterProxyModel(parent); + proxyModel->setSourceModel(searchModel); + proxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive); + + auto *completer = new QCompleter(proxyModel, parent); + completer->setCompletionRole(Qt::DisplayRole); + completer->setCompletionMode(QCompleter::PopupCompletion); + completer->setCaseSensitivity(Qt::CaseInsensitive); + completer->setFilterMode(Qt::MatchContains); + completer->setMaxVisibleItems(maxVisibleItems); + CardCompleterStyler::apply(completer); + + return {searchModel, proxyModel, completer}; +} + +void connectCardCompleterSearch(QLineEdit *edit, const CardCompleterSetup &setup) +{ + QObject::connect(edit, &QLineEdit::textEdited, setup.searchModel, &CardSearchModel::updateSearchResults); + QObject::connect(edit, &QLineEdit::textEdited, setup.completer, [setup](const QString &text) { + setup.proxyModel->setFilterRegularExpression( + QRegularExpression(QRegularExpression::escape(text), QRegularExpression::CaseInsensitiveOption)); + if (!text.isEmpty()) { + setup.completer->complete(); + } + }); +} + +QCompleter *createMentionCompleter(QStringListModel *model, QObject *parent) +{ + auto *completer = new QCompleter(model, parent); + completer->setCaseSensitivity(Qt::CaseInsensitive); + completer->setMaxVisibleItems(5); + completer->setFilterMode(Qt::MatchStartsWith); + return completer; +} diff --git a/cockatrice/src/interface/widgets/utility/completer_utils.h b/cockatrice/src/interface/widgets/utility/completer_utils.h new file mode 100644 index 000000000..5d2ddfc30 --- /dev/null +++ b/cockatrice/src/interface/widgets/utility/completer_utils.h @@ -0,0 +1,32 @@ +/** + * @file completer_utils.h + * @ingroup UtilityWidgets + */ +//! \todo Document this file. + +#ifndef COMPLETER_UTILS_H +#define COMPLETER_UTILS_H + +class CardCompleterProxyModel; +class CardDatabaseDisplayModel; +class CardSearchModel; +class QCompleter; +class QLineEdit; +class QObject; +class QStringListModel; + +struct CardCompleterSetup +{ + CardSearchModel *searchModel; + CardCompleterProxyModel *proxyModel; + QCompleter *completer; +}; + +CardCompleterSetup +createCardCompleter(CardDatabaseDisplayModel *displayModel, QObject *parent, int maxVisibleItems = 10); + +void connectCardCompleterSearch(QLineEdit *edit, const CardCompleterSetup &setup); + +QCompleter *createMentionCompleter(QStringListModel *model, QObject *parent); + +#endif // CARD_COMPLETER_UTILS_H diff --git a/cockatrice/src/interface/widgets/utility/line_edit_completer.cpp b/cockatrice/src/interface/widgets/utility/line_edit_completer.cpp index 13f475a61..3a678029d 100644 --- a/cockatrice/src/interface/widgets/utility/line_edit_completer.cpp +++ b/cockatrice/src/interface/widgets/utility/line_edit_completer.cpp @@ -1,135 +1,199 @@ #include "line_edit_completer.h" #include -#include #include -#include -#include -#include -#include +#include -LineEditCompleter::LineEditCompleter(QWidget *parent) : LineEditUnfocusable(parent), c(nullptr) +LineEditCompleter::LineEditCompleter(QWidget *parent) : LineEditUnfocusable(parent) { } +void LineEditCompleter::addCompleter(QCompleter *c, CompleterTrigger trigger) +{ + c->setWidget(this); + c->setCompletionMode(QCompleter::PopupCompletion); + c->setCaseSensitivity(Qt::CaseInsensitive); + connect(c, qOverload(&QCompleter::activated), this, + qOverload(&LineEditCompleter::insertCompletion)); + + completers.append({c, trigger}); +} + +bool LineEditCompleter::hasVisibleCompleterPopup() const +{ + for (const auto &info : completers) { + if (info.completer->popup()->isVisible()) { + return true; + } + } + return false; +} + +void LineEditCompleter::hideCompleterPopups() +{ + for (const auto &info : completers) { + info.completer->popup()->hide(); + } +} + void LineEditCompleter::focusOutEvent(QFocusEvent *e) { LineEditUnfocusable::focusOutEvent(e); - if (c->popup()->isVisible()) { - // Remove Popup - c->popup()->hide(); - // Truncate the line to last space or whole string - QString textValue = text(); - int lastIndex = textValue.length(); - int lastWordStartIndex = textValue.lastIndexOf(" ") + 1; - int leftShift = qMin(lastIndex, lastWordStartIndex); - setText(textValue.left(leftShift)); - // Insert highlighted line from popup - insert(c->completionModel()->index(c->popup()->currentIndex().row(), 0).data().toString() + " "); - // Set focus back to the textbox since tab was pressed - setFocus(); + + // Only commit the highlighted completion when focus moves away via Tab. + // Other focus losses (e.g. the unfocus shortcut / Escape) must simply close + // the popup without inserting anything. + if (e->reason() != Qt::TabFocusReason) { + hideCompleterPopups(); + return; } + + for (auto &info : completers) { + if (!info.completer->popup()->isVisible()) { + continue; + } + + const QModelIndex currentIndex = info.completer->popup()->currentIndex(); + if (currentIndex.isValid()) { + insertCompletion(info.completer, currentIndex.data().toString()); + } + } + + hideCompleterPopups(); } void LineEditCompleter::keyPressEvent(QKeyEvent *event) { - switch (event->key()) { - case Qt::Key_Return: - case Qt::Key_Enter: - case Qt::Key_Escape: - if (c->popup()->isVisible()) { - event->ignore(); - // Remove Popup - c->popup()->hide(); - // Truncate the line to last space or whole string - QString textValue = text(); - int lastIndexof = qMax(0, textValue.lastIndexOf(" ")); - QString finalString = textValue.left(lastIndexof); - // Add a space if there's a word - if (finalString != "") { - finalString += " "; - } - setText(finalString); - return; - } - break; - case Qt::Key_Space: - if (c->popup()->isVisible()) { - event->ignore(); - // Remove Popup - c->popup()->hide(); - // Truncate the line to last space or whole string - QString textValue = text(); - int lastIndex = textValue.length(); - int lastWordStartIndex = textValue.lastIndexOf(" ") + 1; - int leftShift = qMin(lastIndex, lastWordStartIndex); - setText(textValue.left(leftShift)); - // Insert highlighted line from popup - insert(c->completionModel()->index(c->popup()->currentIndex().row(), 0).data().toString() + " "); - return; - } - break; - default: - break; - } - LineEditUnfocusable::keyPressEvent(event); - // return if the completer is null or if the most recently typed char was '@'. - // Only want the popup AFTER typing the first char of the mention. - if (!c || text().right(1).contains("@")) { - c->popup()->hide(); + + if (event->key() == Qt::Key_Escape) { + hideCompleterPopups(); return; } - // Set new completion prefix - c->setCompletionPrefix(cursorWord(text())); - if (c->completionPrefix().length() < 1) { - c->popup()->hide(); + QString textValue = text(); + int cursorPos = cursorPosition(); + + CompleterInfo *active = nullptr; + QString prefix; + + for (auto &info : completers) { + bool triggered = false; + switch (info.trigger) { + case CompleterTrigger::Mention: { + int triggerPos = textValue.lastIndexOf("@", cursorPos - 1); + if (triggerPos != -1 && (triggerPos == 0 || textValue[triggerPos - 1].isSpace())) { + triggered = true; + // Keep the "@" so the prefix matches the "@"-prefixed mention model entries. + prefix = textValue.mid(triggerPos, cursorPos - triggerPos); + } + break; + } + case CompleterTrigger::Card: { + int triggerPos = textValue.lastIndexOf("[[", cursorPos - 1); + int closePos = textValue.indexOf("]]", triggerPos + 2); + if (triggerPos != -1 && (closePos == -1 || closePos >= cursorPos)) { + triggered = true; + prefix = textValue.mid(triggerPos + 2, cursorPos - (triggerPos + 2)); + } + break; + } + } + + if (triggered) { + active = &info; + break; + } + } + + if (!active) { + for (auto &info : completers) { + info.completer->popup()->hide(); + } return; } - // Draw completion box - QRect cr = cursorRect(); - cr.setWidth(c->popup()->sizeHintForColumn(0) + c->popup()->verticalScrollBar()->sizeHint().width()); - c->complete(cr); + active->completer->setCompletionPrefix(prefix); - // Select first item in the completion popup - QItemSelectionModel *sm = new QItemSelectionModel(c->completionModel()); - c->popup()->setSelectionModel(sm); - sm->select(c->completionModel()->index(0, 0), QItemSelectionModel::ClearAndSelect); - sm->setCurrentIndex(c->completionModel()->index(0, 0), QItemSelectionModel::NoUpdate); -} - -QString LineEditCompleter::cursorWord(const QString &line) const -{ - return line.mid(line.left(cursorPosition()).lastIndexOf(" ") + 1, - cursorPosition() - line.left(cursorPosition()).lastIndexOf(" ") - 1); -} - -void LineEditCompleter::insertCompletion(QString arg) -{ - QString s_arg = arg + " "; - setText(text().replace(text().left(cursorPosition()).lastIndexOf(" ") + 1, - cursorPosition() - text().left(cursorPosition()).lastIndexOf(" ") - 1, s_arg)); -} - -void LineEditCompleter::setCompleter(QCompleter *completer) -{ - c = completer; - c->setWidget(this); - connect(c, qOverload(&QCompleter::activated), this, &LineEditCompleter::insertCompletion); -} - -void LineEditCompleter::setCompletionList(QStringList completionList) -{ - if (!c || c->popup()->isVisible()) { - return; + switch (active->trigger) { + case CompleterTrigger::Card: + emit cardPartialChanged(prefix); + return; + case CompleterTrigger::Mention: + break; } - QStringListModel *model; - model = (QStringListModel *)(c->model()); - if (model == NULL) { - model = new QStringListModel(); - } - model->setStringList(completionList); + active->completer->complete(); +} + +void LineEditCompleter::insertCompletion(const QString &completion) +{ + for (auto &info : completers) { + if (info.completer == sender()) { + insertCompletion(info.completer, completion); + return; + } + } +} + +void LineEditCompleter::insertCompletion(QCompleter *completer, const QString &completion) +{ + QString t = text(); + int pos = cursorPosition(); + + for (const auto &info : completers) { + if (info.completer != completer) { + continue; + } + + switch (info.trigger) { + case CompleterTrigger::Card: { + int triggerPos = t.lastIndexOf("[[", pos - 1); + if (triggerPos == -1) { + return; + } + + // If an earlier "[[" is still open it also encloses the cursor, so + // replace from its start. Otherwise completing in text such as + // "[[Opt[[Amok" would leave a stray "[[" behind. + int startPos = triggerPos; + for (int searchFrom = triggerPos; searchFrom > 0;) { + const int earlier = t.lastIndexOf("[[", searchFrom - 1); + if (earlier == -1) { + break; + } + const int earlierClose = t.indexOf("]]", earlier + 2); + if (earlierClose != -1 && earlierClose < pos) { + break; + } + startPos = earlier; + searchFrom = earlier; + } + + // If the cursor sits inside an already-closed [[...]] pair, replace + // the whole construct instead of leaving a duplicate closing bracket + // behind. + int insertEnd = pos; + const int closePos = t.indexOf("]]", startPos + 2); + if (closePos != -1 && closePos >= pos) { + insertEnd = closePos + 2; + } + + QString after = t.mid(insertEnd); + QString replaced = t.left(startPos + 2) + completion + "]] "; + setText(replaced + after); + setCursorPosition(replaced.length()); + return; + } + case CompleterTrigger::Mention: { + int triggerPos = t.lastIndexOf("@", pos - 1); + if (triggerPos == -1) { + return; + } + setText(t.replace(triggerPos, pos - triggerPos, completion + " ")); + setCursorPosition(triggerPos + completion.length() + 1); + return; + } + } + } } diff --git a/cockatrice/src/interface/widgets/utility/line_edit_completer.h b/cockatrice/src/interface/widgets/utility/line_edit_completer.h index 65fa382ac..369b7b553 100644 --- a/cockatrice/src/interface/widgets/utility/line_edit_completer.h +++ b/cockatrice/src/interface/widgets/utility/line_edit_completer.h @@ -1,6 +1,7 @@ /** * @file line_edit_completer.h * @ingroup UI + * @brief Line edit with support for multiple trigger-based completers, e.g., @mention and [[card]]. */ //! \todo Document this file. @@ -9,25 +10,49 @@ #include "custom_line_edit.h" +#include +#include #include +#include +#include +#include + +enum class CompleterTrigger +{ + Mention, //< "@" prefix, replaced in place with a trailing space. + Card //< "[[" prefix, replaced with the completion plus a closing "]] ". +}; + +struct CompleterInfo +{ + QCompleter *completer; + CompleterTrigger trigger; +}; class LineEditCompleter : public LineEditUnfocusable { Q_OBJECT +signals: + void cardPartialChanged(const QString &partial); + private: - QString cursorWord(const QString &line) const; - QCompleter *c; + QList completers; + + void insertCompletion(QCompleter *completer, const QString &completion); + private slots: - void insertCompletion(QString); + void insertCompletion(const QString &text); protected: - void keyPressEvent(QKeyEvent *event); - void focusOutEvent(QFocusEvent *e); + void keyPressEvent(QKeyEvent *event) override; + void focusOutEvent(QFocusEvent *e) override; public: explicit LineEditCompleter(QWidget *parent = nullptr); - void setCompleter(QCompleter *); - void setCompletionList(QStringList); + void addCompleter(QCompleter *c, CompleterTrigger trigger); + + bool hasVisibleCompleterPopup() const; + void hideCompleterPopups(); }; #endif diff --git a/cockatrice/src/interface/widgets/utility/reversed_completer_model.cpp b/cockatrice/src/interface/widgets/utility/reversed_completer_model.cpp new file mode 100644 index 000000000..1bc9ba14b --- /dev/null +++ b/cockatrice/src/interface/widgets/utility/reversed_completer_model.cpp @@ -0,0 +1,102 @@ +#include "reversed_completer_model.h" + +#include +#include +#include + +void ReversedCompleterModel::setSourceModel(QAbstractItemModel *sourceModel) +{ + if (sourceModel == this->sourceModel()) { + return; + } + + if (QAbstractItemModel *old = this->sourceModel()) { + disconnect(old, nullptr, this, nullptr); + } + + QAbstractProxyModel::setSourceModel(sourceModel); + + if (sourceModel) { + connect(sourceModel, &QAbstractItemModel::modelReset, this, &ReversedCompleterModel::invalidate); + connect(sourceModel, &QAbstractItemModel::rowsInserted, this, &ReversedCompleterModel::invalidate); + connect(sourceModel, &QAbstractItemModel::rowsRemoved, this, &ReversedCompleterModel::invalidate); + connect(sourceModel, &QAbstractItemModel::rowsMoved, this, &ReversedCompleterModel::invalidate); + connect(sourceModel, &QAbstractItemModel::columnsInserted, this, &ReversedCompleterModel::invalidate); + connect(sourceModel, &QAbstractItemModel::columnsRemoved, this, &ReversedCompleterModel::invalidate); + connect(sourceModel, &QAbstractItemModel::dataChanged, this, &ReversedCompleterModel::invalidate); + connect(sourceModel, &QAbstractItemModel::layoutChanged, this, &ReversedCompleterModel::invalidate); + } + + invalidate(); +} + +void ReversedCompleterModel::setEnabled(bool enabled) +{ + if (enabled == isEnabled) { + return; + } + isEnabled = enabled; + invalidate(); +} + +QModelIndex ReversedCompleterModel::mapToSource(const QModelIndex &proxyIndex) const +{ + if (!proxyIndex.isValid() || !sourceModel()) { + return {}; + } + const int sourceRow = isEnabled ? sourceRowCount() - 1 - proxyIndex.row() : proxyIndex.row(); + return sourceModel()->index(sourceRow, proxyIndex.column()); +} + +QModelIndex ReversedCompleterModel::mapFromSource(const QModelIndex &sourceIndex) const +{ + if (!sourceIndex.isValid() || !sourceModel()) { + return {}; + } + const int proxyRow = isEnabled ? sourceRowCount() - 1 - sourceIndex.row() : sourceIndex.row(); + return index(proxyRow, sourceIndex.column()); +} + +QModelIndex ReversedCompleterModel::index(int row, int column, const QModelIndex &parent) const +{ + if (parent.isValid() || !sourceModel() || row < 0 || row >= rowCount() || column < 0 || column >= columnCount()) { + return {}; + } + return createIndex(row, column); +} + +QModelIndex ReversedCompleterModel::parent(const QModelIndex &) const +{ + return {}; +} + +int ReversedCompleterModel::rowCount(const QModelIndex &parent) const +{ + return parent.isValid() || !sourceModel() ? 0 : sourceModel()->rowCount(); +} + +int ReversedCompleterModel::columnCount(const QModelIndex &parent) const +{ + return parent.isValid() || !sourceModel() ? 0 : sourceModel()->columnCount(); +} + +QVariant ReversedCompleterModel::data(const QModelIndex &proxyIndex, int role) const +{ + return sourceModel() ? sourceModel()->data(mapToSource(proxyIndex), role) : QVariant(); +} + +QVariant ReversedCompleterModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + return sourceModel() ? sourceModel()->headerData(section, orientation, role) : QVariant(); +} + +int ReversedCompleterModel::sourceRowCount() const +{ + return sourceModel() ? sourceModel()->rowCount() : 0; +} + +void ReversedCompleterModel::invalidate() +{ + beginResetModel(); + endResetModel(); +} diff --git a/cockatrice/src/interface/widgets/utility/reversed_completer_model.h b/cockatrice/src/interface/widgets/utility/reversed_completer_model.h new file mode 100644 index 000000000..64233a1b0 --- /dev/null +++ b/cockatrice/src/interface/widgets/utility/reversed_completer_model.h @@ -0,0 +1,47 @@ +/** + * @file reversed_completer_model.h + * @ingroup UtilityWidgets + */ +//! \todo Document this file. + +#ifndef REVERSED_COMPLETER_MODEL_H +#define REVERSED_COMPLETER_MODEL_H + +#include + +class QAbstractItemModel; + +/** + * @brief A completer model that can present its rows bottom-to-top. + * + * The original row order is kept intact in the source model (row 0 is always the + * closest match). When enabled, the proxy maps the source rows in reverse so the + * popup shows the closest match in the row nearest to the text edit. Any change + * in the source model is forwarded as a full reset, which is all QCompleter + * needs to rebuild its completion list. + */ +class ReversedCompleterModel : public QAbstractProxyModel +{ +public: + using QAbstractProxyModel::QAbstractProxyModel; + + void setSourceModel(QAbstractItemModel *sourceModel) override; + void setEnabled(bool enabled); + + QModelIndex mapToSource(const QModelIndex &proxyIndex) const override; + QModelIndex mapFromSource(const QModelIndex &sourceIndex) const override; + QModelIndex index(int row, int column, const QModelIndex &parent = {}) const override; + QModelIndex parent(const QModelIndex &) const override; + int rowCount(const QModelIndex &parent = {}) const override; + int columnCount(const QModelIndex &parent = {}) const override; + QVariant data(const QModelIndex &proxyIndex, int role = Qt::DisplayRole) const override; + QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; + +private: + int sourceRowCount() const; + void invalidate(); + + bool isEnabled = false; +}; + +#endif // REVERSED_COMPLETER_MODEL_H diff --git a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_widget.cpp b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_widget.cpp index 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..6a4eaa382 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 @@ -10,6 +10,7 @@ #include "../tabs/visual_deck_editor/tab_deck_editor_visual.h" #include "../tabs/visual_deck_editor/tab_deck_editor_visual_tab_widget.h" #include "../utility/compact_push_button.h" +#include "../utility/completer_utils.h" #include "visual_deck_display_options_widget.h" #include @@ -21,11 +22,9 @@ #include #include #include -#include -#include #include #include -#include +#include #include VisualDeckEditorWidget::VisualDeckEditorWidget(QWidget *parent, @@ -45,9 +44,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); @@ -98,32 +97,14 @@ void VisualDeckEditorWidget::initializeSearchBarAndCompleter() cardDatabaseModel = new CardDatabaseModel(CardDatabaseManager::getInstance(), false, this); cardDatabaseDisplayModel = new CardDatabaseDisplayModel(this); cardDatabaseDisplayModel->setSourceModel(cardDatabaseModel); - CardSearchModel *searchModel = new CardSearchModel(cardDatabaseDisplayModel, this); - proxyModel = new CardCompleterProxyModel(this); - proxyModel->setSourceModel(searchModel); - proxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive); - proxyModel->setFilterRole(Qt::DisplayRole); - - completer = new QCompleter(proxyModel, this); - completer->setCompletionRole(Qt::DisplayRole); - completer->setCompletionMode(QCompleter::PopupCompletion); - completer->setCaseSensitivity(Qt::CaseInsensitive); - completer->setFilterMode(Qt::MatchContains); - completer->setMaxVisibleItems(15); + const CardCompleterSetup cardSetup = createCardCompleter(cardDatabaseDisplayModel, this, 15); + proxyModel = cardSetup.proxyModel; + completer = cardSetup.completer; searchBar->setCompleter(completer); // Update suggestions dynamically - connect(searchBar, &QLineEdit::textEdited, searchModel, &CardSearchModel::updateSearchResults); - connect(searchBar, &QLineEdit::textEdited, this, [=, this](const QString &text) { - // Ensure substring matching - QString pattern = ".*" + QRegularExpression::escape(text) + ".*"; - proxyModel->setFilterRegularExpression(QRegularExpression(pattern, QRegularExpression::CaseInsensitiveOption)); - - if (!text.isEmpty()) { - completer->complete(); // Force the dropdown to appear - } - }); + connectCardCompleterSearch(searchBar, cardSetup); connect(completer, static_cast(&QCompleter::activated), this, [=, this](const QString &completion) { 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 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..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 @@ -70,7 +71,9 @@ #include #include #include +#include #include +#include #include #include #include @@ -391,9 +394,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 +521,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 +560,25 @@ void MainWindow::startupConfigCheck() actCheckClientUpdates(); } - if (SettingsCache::instance().personal().getClientVersion() == CLIENT_INFO_NOT_SET) { + actCheckCommanderBracketDefinitionUpdates(); + + 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 +603,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"; @@ -659,6 +664,7 @@ void MainWindow::alertForcedOracleRun(const QString &version, bool isUpdate) actCheckCardUpdates(); actCheckServerUpdates(); + actCheckCommanderBracketDefinitionUpdates(); } MainWindow::~MainWindow() @@ -1003,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/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_card/CMakeLists.txt b/libcockatrice_card/CMakeLists.txt index 7d3d47eea..081e6fd05 100644 --- a/libcockatrice_card/CMakeLists.txt +++ b/libcockatrice_card/CMakeLists.txt @@ -5,6 +5,7 @@ set(CMAKE_AUTORCC ON) set(HEADERS libcockatrice/card/card_info.h libcockatrice/card/card_info_comparator.h + libcockatrice/card/lazy_properties_hash.h libcockatrice/card/database/card_database.h libcockatrice/card/database/card_database_loader.h libcockatrice/card/database/card_database_manager.h @@ -26,6 +27,7 @@ add_library( ${MOC_SOURCES} libcockatrice/card/card_info.cpp libcockatrice/card/card_info_comparator.cpp + libcockatrice/card/lazy_properties_hash.cpp libcockatrice/card/database/card_database.cpp libcockatrice/card/database/card_database_cache.cpp libcockatrice/card/database/card_database_loader.cpp diff --git a/libcockatrice_card/libcockatrice/card/card_info.cpp b/libcockatrice_card/libcockatrice/card/card_info.cpp index f03503550..786e17950 100644 --- a/libcockatrice_card/libcockatrice/card/card_info.cpp +++ b/libcockatrice_card/libcockatrice/card/card_info.cpp @@ -5,10 +5,7 @@ #include "relation/card_relation.h" #include "set/card_set.h" -#include #include -#include -#include #include #include #include @@ -21,64 +18,33 @@ class CardInfo; using CardInfoPtr = QSharedPointer; -namespace -{ -QByteArray serializeProperties(const QHash &props) -{ - QByteArray blob; - QDataStream out(&blob, QIODevice::WriteOnly); - out.setVersion(QDataStream::Qt_6_4); - out << props; - return blob; -} -} // namespace - -void CardInfo::ensurePropertiesLoaded() const -{ - QMutexLocker lock(&propertiesMutex); - if (propertiesLoaded) { - return; - } - if (!propertiesBlob.isEmpty()) { - QDataStream in(propertiesBlob); - in.setVersion(QDataStream::Qt_6_4); - in >> propertiesCache; - } - propertiesLoaded = true; -} - const QHash &CardInfo::getPropertiesHash() const { - ensurePropertiesLoaded(); - return propertiesCache; + return properties.getProperties(); } void CardInfo::setProperty(const QString &_name, const QString &_value) { - ensurePropertiesLoaded(); - if (propertiesCache.value(_name) == _value) { + bool changed = properties.insert(_name, _value); + if (!changed) { return; } - propertiesCache.insert(_name, _value); - propertiesBlob = serializeProperties(propertiesCache); + emit cardInfoChanged(smartThis); } CardInfo::CardInfo(const QString &_name, const QString &_text, bool _isToken, - QHash _properties, + const QHash &_properties, const QList &_relatedCards, const QList &_reverseRelatedCards, SetToPrintingsMap _sets, const UiAttributes _uiAttributes) - : name(_name), text(_text), isToken(_isToken), relatedCards(_relatedCards), - reverseRelatedCards(_reverseRelatedCards), setsToPrintings(std::move(_sets)), uiAttributes(_uiAttributes) + : name(_name), text(_text), isToken(_isToken), properties(LazyPropertiesHash(_properties)), + relatedCards(_relatedCards), reverseRelatedCards(_reverseRelatedCards), setsToPrintings(std::move(_sets)), + uiAttributes(_uiAttributes) { - propertiesCache = std::move(_properties); - propertiesBlob = serializeProperties(propertiesCache); - propertiesLoaded = true; - simpleName = CardInfo::simplifyName(name); refreshCachedSets(); @@ -87,7 +53,7 @@ CardInfo::CardInfo(const QString &_name, CardInfo::CardInfo(const QString &_name, const QString &_text, bool _isToken, - QByteArray _propertiesBlob, + const QByteArray &_propertiesBlob, const QList &_relatedCards, const QList &_reverseRelatedCards, SetToPrintingsMap _sets, @@ -95,7 +61,7 @@ CardInfo::CardInfo(const QString &_name, QString _simpleName, QSet _altNames) : name(_name), simpleName(std::move(_simpleName)), text(_text), isToken(_isToken), - propertiesBlob(std::move(_propertiesBlob)), relatedCards(_relatedCards), + properties(LazyPropertiesHash(_propertiesBlob)), relatedCards(_relatedCards), reverseRelatedCards(_reverseRelatedCards), setsToPrintings(std::move(_sets)), uiAttributes(_uiAttributes), altNames(std::move(_altNames)) { @@ -113,14 +79,14 @@ CardInfoPtr CardInfo::newInstance(const QString &_name) CardInfoPtr CardInfo::newInstance(const QString &_name, const QString &_text, bool _isToken, - QHash _properties, + const QHash &_properties, const QList &_relatedCards, const QList &_reverseRelatedCards, SetToPrintingsMap _sets, const UiAttributes _uiAttributes) { - CardInfoPtr ptr(new CardInfo(_name, _text, _isToken, std::move(_properties), _relatedCards, _reverseRelatedCards, - _sets, _uiAttributes)); + CardInfoPtr ptr( + new CardInfo(_name, _text, _isToken, _properties, _relatedCards, _reverseRelatedCards, _sets, _uiAttributes)); ptr->setSmartPointer(ptr); for (const auto &printings : _sets) { @@ -203,15 +169,13 @@ void CardInfo::addToSet(const CardSetPtr &_set, const PrintingInfo &_info) void CardInfo::combineLegalities(const QHash &props) { - ensurePropertiesLoaded(); QHashIterator it(props); while (it.hasNext()) { it.next(); if (it.key().startsWith("format-")) { - propertiesCache.insert(it.key(), it.value()); + properties.insert(it.key(), it.value()); } } - propertiesBlob = serializeProperties(propertiesCache); emit cardInfoChanged(smartThis); } diff --git a/libcockatrice_card/libcockatrice/card/card_info.h b/libcockatrice_card/libcockatrice/card/card_info.h index a5c208893..392dc3849 100644 --- a/libcockatrice_card/libcockatrice/card/card_info.h +++ b/libcockatrice_card/libcockatrice/card/card_info.h @@ -2,6 +2,7 @@ #define CARD_INFO_H #include "format/format_legality_rules.h" +#include "lazy_properties_hash.h" #include "printing/printing_info.h" #include @@ -75,19 +76,8 @@ private: QString simpleName; ///< Simplified name for fuzzy matching. 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 - // 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. - * Safe to call from const getters (members are mutable). - */ - void ensurePropertiesLoaded() const; + LazyPropertiesHash properties; ///< Key-value store of dynamic card properties. QList relatedCards; ///< Forward references to related cards. QList reverseRelatedCards; ///< Cards that refer back to this card. @@ -114,7 +104,7 @@ public: explicit CardInfo(const QString &_name, const QString &_text, bool _isToken, - QHash _properties, + const QHash &_properties, const QList &_relatedCards, const QList &_reverseRelatedCards, SetToPrintingsMap _sets, @@ -144,7 +134,7 @@ public: explicit CardInfo(const QString &_name, const QString &_text, bool _isToken, - QByteArray _propertiesBlob, + const QByteArray &_propertiesBlob, const QList &_relatedCards, const QList &_reverseRelatedCards, SetToPrintingsMap _sets, @@ -161,7 +151,7 @@ public: */ CardInfo(const CardInfo &other) : QObject(other.parent()), name(other.name), simpleName(other.simpleName), text(other.text), - isToken(other.isToken), propertiesBlob(other.propertiesBlob), relatedCards(other.relatedCards), + isToken(other.isToken), properties(other.properties), relatedCards(other.relatedCards), reverseRelatedCards(other.reverseRelatedCards), reverseRelatedCardsToMe(other.reverseRelatedCardsToMe), setsToPrintings(other.setsToPrintings), uiAttributes(other.uiAttributes), setsNames(other.setsNames), altNames(other.altNames) @@ -194,7 +184,7 @@ public: static CardInfoPtr newInstance(const QString &_name, const QString &_text, bool _isToken, - QHash _properties, + const QHash &_properties, const QList &_relatedCards, const QList &_reverseRelatedCards, SetToPrintingsMap _sets, diff --git a/libcockatrice_card/libcockatrice/card/database/card_database_cache.cpp b/libcockatrice_card/libcockatrice/card/database/card_database_cache.cpp index 3985889b7..2b27f50f8 100644 --- a/libcockatrice_card/libcockatrice/card/database/card_database_cache.cpp +++ b/libcockatrice_card/libcockatrice/card/database/card_database_cache.cpp @@ -119,7 +119,7 @@ PrintingInfo readPrinting(QDataStream &in, const SetNameMap &sets) QByteArray propsBlob = readHashBlob(in); auto set = sets.value(setName); - return PrintingInfo(set, propsBlob); + return PrintingInfo(set, LazyPropertiesHash(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 f3aac7809..64202ab21 100644 --- a/libcockatrice_card/libcockatrice/card/database/parser/cockatrice_xml_3.cpp +++ b/libcockatrice_card/libcockatrice/card/database/parser/cockatrice_xml_3.cpp @@ -248,7 +248,7 @@ void CockatriceXml3Parser::loadCardsFromXml(QXmlStreamReader &xml) if (attrs.hasAttribute("rarity")) { printingProps.insert("rarity", attrs.value("rarity").toString()); } - PrintingInfo setInfo(set, printingProps); + PrintingInfo setInfo(set, LazyPropertiesHash(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 8649bbfaf..ec460d685 100644 --- a/libcockatrice_card/libcockatrice/card/database/parser/cockatrice_xml_4.cpp +++ b/libcockatrice_card/libcockatrice/card/database/parser/cockatrice_xml_4.cpp @@ -322,7 +322,7 @@ void CockatriceXml4Parser::loadCardsFromXml(QXmlStreamReader &xml) } printingProps.insert(attrName, attr.value().toString()); } - PrintingInfo printingInfo(set, printingProps); + PrintingInfo printingInfo(set, LazyPropertiesHash(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/lazy_properties_hash.cpp b/libcockatrice_card/libcockatrice/card/lazy_properties_hash.cpp new file mode 100644 index 000000000..4d903a3bd --- /dev/null +++ b/libcockatrice_card/libcockatrice/card/lazy_properties_hash.cpp @@ -0,0 +1,95 @@ +#include "lazy_properties_hash.h" + +#include + +LazyPropertiesHash::LazyPropertiesHash() : isMaterialized(true) +{ +} + +LazyPropertiesHash::LazyPropertiesHash(const QByteArray &blob) : blob(blob) +{ +} + +LazyPropertiesHash::LazyPropertiesHash(const QHash &properties) + : properties(properties), isMaterialized(true) +{ +} + +LazyPropertiesHash::LazyPropertiesHash(const LazyPropertiesHash &other) +{ + // since we do not allow dematerialization, we only need to lock if not materialized yet + if (other.isMaterialized) { + blob = other.blob; + properties = other.properties; + isMaterialized = true; + } else { + QMutexLocker lock(&other.propertiesMutex); + blob = other.blob; + properties = other.properties; + isMaterialized = false; + } +} + +LazyPropertiesHash &LazyPropertiesHash::operator=(const LazyPropertiesHash &other) +{ + if (this == &other) { + return *this; + } + + // since we do not allow dematerialization, we only need to lock if not materialized yet + if (other.isMaterialized) { + blob = other.blob; + properties = other.properties; + isMaterialized = true; + } else { + QMutexLocker lock(&other.propertiesMutex); + blob = other.blob; + properties = other.properties; + isMaterialized = false; + } + + return *this; +} + +void LazyPropertiesHash::ensureMaterialized() const +{ + QMutexLocker lock(&propertiesMutex); + + if (isMaterialized) { + return; + } + + if (!blob.isEmpty()) { + QDataStream in(blob); + in.setVersion(QDataStream::Qt_6_4); + in >> properties; + } + + blob.clear(); + + isMaterialized = true; +} + +QString LazyPropertiesHash::value(const QString &key) const +{ + ensureMaterialized(); + return properties.value(key); +} + +bool LazyPropertiesHash::insert(const QString &key, const QString &value) +{ + ensureMaterialized(); + + if (value == properties.value(key)) { + return false; + } + + properties.insert(key, value); + return true; +} + +const QHash &LazyPropertiesHash::getProperties() const +{ + ensureMaterialized(); + return properties; +} diff --git a/libcockatrice_card/libcockatrice/card/lazy_properties_hash.h b/libcockatrice_card/libcockatrice/card/lazy_properties_hash.h new file mode 100644 index 000000000..0ed8c215b --- /dev/null +++ b/libcockatrice_card/libcockatrice/card/lazy_properties_hash.h @@ -0,0 +1,74 @@ +#ifndef COCKATRICE_LAZY_PROPERTIES_HASH_H +#define COCKATRICE_LAZY_PROPERTIES_HASH_H + +#include +#include + +/** + * @brief A property map that can lazily deserialize blobs to avoid loading overhead. + * + * Properties are stored as a pre-serialized blob (cheap to load), and the QString is materialized on the first query, + * so the database load avoids constructing thousands of QString per card. + * + * Once the properties are materialized, it cannot be unmaterialized. + * If you want to reset the properties to an unmaterialized state, you should create a new LazyPropertiesHash. + */ +class LazyPropertiesHash +{ + + mutable QByteArray blob; ///< Serialized properties (load form). + mutable QHash properties; ///< Materialized properties (query form). + mutable QMutex propertiesMutex; ///< Guards lazy materialization. + mutable bool isMaterialized = false; ///< Whether propertiesCache is valid. + + /** + * @brief Materializes properties from blob if not already done. Clears blob afterward. + * Safe to call from const getters (members are mutable). + */ + void ensureMaterialized() const; + +public: + /** + * @brief Default constructor. + */ + LazyPropertiesHash(); + + /** + * @brief Creates an unmaterialized LazyPropertiesHash + * @param blob The pre-serialized blob + */ + explicit LazyPropertiesHash(const QByteArray &blob); + + /** + * @brief Creates an already-materialized LazyPropertiesHash + * @param properties The properties + */ + explicit LazyPropertiesHash(const QHash &properties); + + // Override copy constructor and copy-assignment because mutex isn't copiable + LazyPropertiesHash(const LazyPropertiesHash &other); + LazyPropertiesHash &operator=(const LazyPropertiesHash &other); + + /** + * @brief Gets the value from the materialized properties hash + * @param key The key + * @return The value, or an empty string if the key is not present + */ + QString value(const QString &key) const; + + /** + * @brief Inserts a value into the materialized properties hash + * @param key The key + * @param value The value to insert + * @return True if a new value was inserted; false if the new value is the same as the existing value + */ + bool insert(const QString &key, const QString &value); + + /** + * @brief Gets a view of the materialized properties hash. + * @return The properties hash + */ + const QHash &getProperties() const; +}; + +#endif // COCKATRICE_LAZY_PROPERTIES_HASH_H diff --git a/libcockatrice_card/libcockatrice/card/printing/printing_info.cpp b/libcockatrice_card/libcockatrice/card/printing/printing_info.cpp index 49086f8b7..3d185583a 100644 --- a/libcockatrice_card/libcockatrice/card/printing/printing_info.cpp +++ b/libcockatrice_card/libcockatrice/card/printing/printing_info.cpp @@ -5,37 +5,14 @@ #include #include -PrintingInfo::PrintingInfo(const CardSetPtr &_set, const QHash &_properties) - : set(_set), propertiesCache(_properties), propertiesLoaded(true) +PrintingInfo::PrintingInfo(const CardSetPtr &_set, const LazyPropertiesHash &_properties) + : set(_set), properties(_properties) { } -PrintingInfo::PrintingInfo(const CardSetPtr &_set, const QByteArray &_blob) : set(_set), propertiesBlob(_blob) -{ -} - -void PrintingInfo::ensurePropertiesLoaded() const -{ - QMutexLocker lock(propertiesMutex.data()); - if (propertiesLoaded) { - return; - } - propertiesCache.clear(); - if (!propertiesBlob.isEmpty()) { - QDataStream in(propertiesBlob); - in.setVersion(QDataStream::Qt_6_4); - in >> propertiesCache; - } - propertiesLoaded = true; -} - void PrintingInfo::setProperty(const QString &_name, const QString &_value) { - ensurePropertiesLoaded(); - if (propertiesCache.value(_name) == _value) { - return; - } - propertiesCache.insert(_name, _value); + properties.insert(_name, _value); } /** diff --git a/libcockatrice_card/libcockatrice/card/printing/printing_info.h b/libcockatrice_card/libcockatrice/card/printing/printing_info.h index 70093b686..4d174dc41 100644 --- a/libcockatrice_card/libcockatrice/card/printing/printing_info.h +++ b/libcockatrice_card/libcockatrice/card/printing/printing_info.h @@ -2,12 +2,11 @@ #define COCKATRICE_PRINTING_INFO_H #include "../set/card_set.h" +#include "libcockatrice/card/lazy_properties_hash.h" #include #include -#include #include -#include class PrintingInfo; @@ -34,15 +33,7 @@ public: * @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, 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); + explicit PrintingInfo(const CardSetPtr &_set = nullptr, const LazyPropertiesHash &_properties = {}); /** * @brief Destroys the PrintingInfo. @@ -76,18 +67,8 @@ public: } private: - CardSetPtr set; ///< The set this variation belongs to. - - // Properties are stored as a pre-serialized blob (cheap to load) and the - // 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. - - void ensurePropertiesLoaded() const; + CardSetPtr set; ///< The set this variation belongs to. + LazyPropertiesHash properties; ///< Key-value store for variation-specific attributes. public: /** @@ -112,8 +93,7 @@ public: [[nodiscard]] const QHash &getPropertiesHash() const { - ensurePropertiesLoaded(); - return propertiesCache; + return properties.getProperties(); } /** @@ -124,7 +104,7 @@ public: */ [[nodiscard]] QString getProperty(const QString &propertyName) const { - return getPropertiesHash().value(propertyName); + return properties.value(propertyName); } /** 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..ab2caa0d7 100644 --- a/libcockatrice_interfaces/libcockatrice/interfaces/interface_interface_settings_provider.h +++ b/libcockatrice_interfaces/libcockatrice/interfaces/interface_interface_settings_provider.h @@ -29,15 +29,16 @@ 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 getSkipEmptySections() 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_models/libcockatrice/models/database/card/card_completer_proxy_model.cpp b/libcockatrice_models/libcockatrice/models/database/card/card_completer_proxy_model.cpp index 387eb454f..f31463b50 100644 --- a/libcockatrice_models/libcockatrice/models/database/card/card_completer_proxy_model.cpp +++ b/libcockatrice_models/libcockatrice/models/database/card/card_completer_proxy_model.cpp @@ -6,6 +6,10 @@ CardCompleterProxyModel::CardCompleterProxyModel(QObject *parent) : QSortFilterP bool CardCompleterProxyModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const { + if (!sourceModel()) { + return false; + } + if (filterRegularExpression().pattern().isEmpty()) { return true; } @@ -13,6 +17,5 @@ bool CardCompleterProxyModel::filterAcceptsRow(int sourceRow, const QModelIndex QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent); QString data = index.data(Qt::DisplayRole).toString(); - // Ensure substring matching return data.contains(filterRegularExpression()); } diff --git a/libcockatrice_models/libcockatrice/models/database/card/card_search_model.cpp b/libcockatrice_models/libcockatrice/models/database/card/card_search_model.cpp index d1fbbac2f..621f28983 100644 --- a/libcockatrice_models/libcockatrice/models/database/card/card_search_model.cpp +++ b/libcockatrice_models/libcockatrice/models/database/card/card_search_model.cpp @@ -23,8 +23,14 @@ QVariant CardSearchModel::data(const QModelIndex &index, int role) const return QVariant(); } + const SearchResult &result = searchResults.at(index.row()); + if (role == Qt::DisplayRole) { - return searchResults.at(index.row()).card->getName(); + return result.card->getName(); + } + + if (role == CardInfoRole) { + return QVariant::fromValue(result.card); } return QVariant(); @@ -36,43 +42,62 @@ void CardSearchModel::updateSearchResults(const QString &query) searchResults.clear(); if (query.isEmpty() || !sourceModel) { + endResetModel(); return; } - // Set the filter for the display model - sourceModel->setCardName(query); + CardDatabaseModel *sourceDbModel = qobject_cast(sourceModel->sourceModel()); + if (!sourceDbModel) { + endResetModel(); + return; + } - // Collect matching cards and compute Levenshtein distance - for (int i = 0; i < sourceModel->rowCount(); ++i) { - QModelIndex modelIndex = sourceModel->index(i, 0); - QModelIndex sourceIndex = sourceModel->mapToSource(modelIndex); - CardDatabaseModel *sourceDbModel = qobject_cast(sourceModel->sourceModel()); + const QString lowerQuery = query.toLower(); - if (!sourceDbModel || !sourceIndex.isValid()) { - return; - } - - CardInfoPtr card = sourceDbModel->getCard(sourceIndex.row()); + QList prefixMatches; + QList containsMatches; + // Iterate the raw database model directly so results are always complete and fresh + const int rowCount = sourceDbModel->rowCount(); + for (int i = 0; i < rowCount; ++i) { + CardInfoPtr card = sourceDbModel->getCard(i); if (!card) { continue; } - int distance = levenshteinDistance(query.toLower(), card->getName().toLower()); - searchResults.append({card, distance}); + const QString lowerName = card->getName().toLower(); + if (!lowerName.contains(lowerQuery)) { + continue; + } + + const int distance = levenshteinDistance(lowerQuery, lowerName); + + if (lowerName.startsWith(lowerQuery)) { + prefixMatches.append({card, distance}); + } else { + containsMatches.append({card, distance}); + } } - // Sort by Levenshtein distance (lower distance = better match) - std::sort(searchResults.begin(), searchResults.end(), - [](const SearchResult &a, const SearchResult &b) { return a.distance < b.distance; }); + auto sortByDistanceThenLength = [](const SearchResult &a, const SearchResult &b) { + if (a.distance != b.distance) { + return a.distance < b.distance; + } + return a.card->getName().size() < b.card->getName().size(); + }; - // Keep only the top 5 results + std::sort(prefixMatches.begin(), prefixMatches.end(), sortByDistanceThenLength); + std::sort(containsMatches.begin(), containsMatches.end(), sortByDistanceThenLength); + + // Prefix matches always come first, then contains-only matches + searchResults.reserve(prefixMatches.size() + containsMatches.size()); + searchResults.append(prefixMatches); + searchResults.append(containsMatches); + + // Keep only the top 10 results if (searchResults.size() > 10) { searchResults = searchResults.mid(0, 10); } - emit dataChanged(index(0, 0), index(rowCount() - 1, 0)); - emit layoutChanged(); - endResetModel(); } diff --git a/libcockatrice_models/libcockatrice/models/database/card/card_search_model.h b/libcockatrice_models/libcockatrice/models/database/card/card_search_model.h index bc4be7a0e..646bf7e61 100644 --- a/libcockatrice_models/libcockatrice/models/database/card/card_search_model.h +++ b/libcockatrice_models/libcockatrice/models/database/card/card_search_model.h @@ -15,6 +15,11 @@ class CardSearchModel : public QAbstractListModel { Q_OBJECT public: + enum CardSearchRoles + { + CardInfoRole = Qt::UserRole + 1, + }; + explicit CardSearchModel(CardDatabaseDisplayModel *sourceModel, QObject *parent = nullptr); [[nodiscard]] int rowCount(const QModelIndex &parent = QModelIndex()) const override; diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_player.cpp b/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_player.cpp index 5bf27eebb..4128c6c90 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_player.cpp +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_player.cpp @@ -1631,7 +1631,9 @@ void Server_AbstractPlayer::getInfo(ServerInfo_Player *info, { getProperties(*info->mutable_properties(), withUserInfo); - if (deck) { + // Deck lists are only shared with other players when the game is in Open Decklists mode, + // so a player joining an open lobby can see every deck that was loaded before they joined. + if (deck && (recipient == this || game->getShareDecklistsOnLoad())) { info->set_deck_list(deck->writeToString_Native().toStdString()); } diff --git a/libcockatrice_network/libcockatrice/network/server/remote/server.h b/libcockatrice_network/libcockatrice/network/server/remote/server.h index 2fca46593..12e71ebff 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/server.h +++ b/libcockatrice_network/libcockatrice/network/server/remote/server.h @@ -91,6 +91,10 @@ public: { return QString(); } + virtual SessionEvent *getLoginSessionEvent() const + { + return nullptr; + } virtual QString getRequiredFeatures() const { return QString(); diff --git a/libcockatrice_network/libcockatrice/network/server/remote/server_protocolhandler.cpp b/libcockatrice_network/libcockatrice/network/server/remote/server_protocolhandler.cpp index c3686ddfa..5b893799f 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/server_protocolhandler.cpp +++ b/libcockatrice_network/libcockatrice/network/server/remote/server_protocolhandler.cpp @@ -562,6 +562,11 @@ Response::ResponseCode Server_ProtocolHandler::cmdLogin(const Command_Login &cmd event.set_message(server->getLoginMessage().toStdString()); rc.enqueuePostResponseItem(ServerMessage::SESSION_EVENT, prepareSessionEvent(event)); + SessionEvent *loginEvent = server->getLoginSessionEvent(); + if (loginEvent) { + rc.enqueuePostResponseItem(ServerMessage::SESSION_EVENT, loginEvent); + } + auto *re = new Response_Login; re->mutable_user_info()->CopyFrom(copyUserInfo(true)); diff --git a/libcockatrice_settings/CMakeLists.txt b/libcockatrice_settings/CMakeLists.txt index f8e9c2bce..80a465b6d 100644 --- a/libcockatrice_settings/CMakeLists.txt +++ b/libcockatrice_settings/CMakeLists.txt @@ -3,18 +3,22 @@ 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/commander_bracket_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 +36,22 @@ 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/commander_bracket_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 + +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 new file mode 100644 index 000000000..65296a450 --- /dev/null +++ b/libcockatrice_settings/libcockatrice/settings/deck_editor_settings.cpp @@ -0,0 +1,72 @@ +#include "deck_editor_settings.h" + +DeckEditorSettings::DeckEditorSettings(const QString &settingPath, QObject *parent) + : SettingsManager(settingPath + "deck_editor.ini", "deckeditor", QString(), parent) +{ +} + +bool DeckEditorSettings::getOpenDeckInNewTab() const +{ + return getValue("openDeckInNewTab", QString(), QString(), false).toBool(); +} + +bool DeckEditorSettings::getBannerCardComboBoxVisible() const +{ + return getValue("bannerCardComboBoxVisible", QString(), QString(), true).toBool(); +} + +bool DeckEditorSettings::getTagsWidgetVisible() const +{ + return getValue("tagsWidgetVisible", QString(), QString(), true).toBool(); +} + +int DeckEditorSettings::getDefaultDeckEditorType() const +{ + return getValue("defaultDeckEditorType", QString(), QString(), 1).toInt(); +} + +void DeckEditorSettings::setOpenDeckInNewTab(bool _openDeckInNewTab) +{ + setValue(_openDeckInNewTab, "openDeckInNewTab"); +} + +void DeckEditorSettings::setBannerCardComboBoxVisible(bool _bannerCardComboBoxVisible) +{ + setValue(_bannerCardComboBoxVisible, "bannerCardComboBoxVisible"); + emit bannerCardComboBoxVisibleChanged(_bannerCardComboBoxVisible); +} + +void DeckEditorSettings::setTagsWidgetVisible(bool _tagsWidgetVisible) +{ + setValue(_tagsWidgetVisible, "tagsWidgetVisible"); + emit tagsWidgetVisibleChanged(_tagsWidgetVisible); +} + +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 new file mode 100644 index 000000000..70f91be9b --- /dev/null +++ b/libcockatrice_settings/libcockatrice/settings/deck_editor_settings.h @@ -0,0 +1,49 @@ +#ifndef DECK_EDITOR_SETTINGS_H +#define DECK_EDITOR_SETTINGS_H + +#include "settings_manager.h" + +#include + +enum commanderSpellbookIntegrationEnabledIndex +{ + commanderSpellbookIntegrationEnabledIndexDisabled, + commanderSpellbookIntegrationEnabledIndexEnabled, + commanderSpellbookIntegrationEnabledIndexAutomatic, + commanderSpellbookIntegrationEnabledIndexUnprompted, +}; + +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; + [[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); + +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..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", @@ -27,3 +28,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..29c57c57e 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,49 @@ qreal InterfaceSettings::getFastForwardSpeed() const return getValue("fastForwardSpeed", "replay", QString(), 10).toReal(); } -bool InterfaceSettings::getStyleUserList() const +bool InterfaceSettings::getSkipEmptySections() const { - return getValue("styleUserList", "appearance", QString(), true).toBool(); + return getValue("skipEmptySections", "replay", QString(), false).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 +194,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 +219,7 @@ void InterfaceSettings::setClickPlaysAllSelected(bool _clickPlaysAllSelected) void InterfaceSettings::setPlayToStack(bool _playToStack) { - setValue(_playToStack, "playtostack"); + setValue(_playToStack, "playToStack"); } void InterfaceSettings::setDoNotDeleteArrowsInSubPhases(bool _doNotDeleteArrowsInSubPhases) @@ -224,22 +229,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 +264,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 +284,45 @@ void InterfaceSettings::setFastForwardSpeed(qreal _value) setValue(_value, "fastForwardSpeed", "replay"); } -void InterfaceSettings::setStyleUserList(bool _styleUserList) +void InterfaceSettings::setSkipEmptySections(bool _value) { - setValue(_styleUserList, "styleUserList", "appearance"); - emit styleUserListChanged(); + setValue(_value, "skipEmptySections", "replay"); } 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..982976310 100644 --- a/libcockatrice_settings/libcockatrice/settings/interface_settings.h +++ b/libcockatrice_settings/libcockatrice/settings/interface_settings.h @@ -32,15 +32,16 @@ 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 getSkipEmptySections() 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 +64,16 @@ 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 setSkipEmptySections(bool _value); 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 +81,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/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/oracle/src/oracleimporter.cpp b/oracle/src/oracleimporter.cpp index a9008c4da..85859e7a2 100644 --- a/oracle/src/oracleimporter.cpp +++ b/oracle/src/oracleimporter.cpp @@ -316,7 +316,7 @@ int OracleImporter::importCardsFromSet(const CardSetPtr ¤tSet, const QList } } - PrintingInfo printingInfo(currentSet, printingProps); + PrintingInfo printingInfo(currentSet, LazyPropertiesHash(printingProps)); QString numComponent; const QString numProperty = printingInfo.getProperty("num"); diff --git a/servatrice/src/servatrice.cpp b/servatrice/src/servatrice.cpp index aa50e068a..4305f6882 100644 --- a/servatrice/src/servatrice.cpp +++ b/servatrice/src/servatrice.cpp @@ -673,8 +673,27 @@ void Servatrice::statusUpdate() } } +SessionEvent *Servatrice::makeShutdownEvent() const +{ + Event_ServerShutdown event; + event.set_reason(shutdownReason.toStdString()); + event.set_minutes(static_cast(shutdownMinutes)); + return Server_ProtocolHandler::prepareSessionEvent(event); +} + +SessionEvent *Servatrice::getLoginSessionEvent() const +{ + // Notify newly logged-in users of a pending server shutdown + QMutexLocker locker(&shutdownStateMutex); + if (shutdownTimer && shutdownMinutes > 0) { + return makeShutdownEvent(); + } + return nullptr; +} + void Servatrice::scheduleShutdown(const QString &reason, int minutes) { + shutdownStateMutex.lock(); shutdownReason = reason; shutdownMinutes = minutes; nextShutdownMessageMinutes = shutdownMinutes; @@ -683,6 +702,7 @@ void Servatrice::scheduleShutdown(const QString &reason, int minutes) connect(shutdownTimer, SIGNAL(timeout()), this, SLOT(shutdownTimeout())); shutdownTimer->start(60000); } + shutdownStateMutex.unlock(); shutdownTimeout(); } @@ -702,6 +722,7 @@ void Servatrice::incRxBytes(quint64 num) void Servatrice::shutdownTimeout() { + QMutexLocker locker(&shutdownStateMutex); // Show every time counter cut in half & every minute for last 5 minutes if (shutdownMinutes <= 5 || shutdownMinutes == nextShutdownMessageMinutes) { if (shutdownMinutes == nextShutdownMessageMinutes) { @@ -710,10 +731,7 @@ void Servatrice::shutdownTimeout() SessionEvent *se; if (shutdownMinutes) { - Event_ServerShutdown event; - event.set_reason(shutdownReason.toStdString()); - event.set_minutes(static_cast(shutdownMinutes)); - se = Server_ProtocolHandler::prepareSessionEvent(event); + se = makeShutdownEvent(); } else { Event_ConnectionClosed event; event.set_reason(Event_ConnectionClosed::SERVER_SHUTDOWN); diff --git a/servatrice/src/servatrice.h b/servatrice/src/servatrice.h index 62fb382cb..6eb00c165 100644 --- a/servatrice/src/servatrice.h +++ b/servatrice/src/servatrice.h @@ -158,6 +158,7 @@ private: Servatrice_IslServer *islServer; mutable QMutex loginMessageMutex; QString loginMessage; + mutable QMutex shutdownStateMutex; QString dbPrefix; QMap serverRequiredFeatureList; QString officialWarnings; @@ -216,6 +217,8 @@ public: QMutexLocker locker(&loginMessageMutex); return loginMessage; } + SessionEvent *getLoginSessionEvent() const override; + SessionEvent *makeShutdownEvent() const; QString getRequiredFeatures() const override; QString getAuthenticationMethodString() const; QString getDBTypeString() const; 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(); +} 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(); }