Merge branch 'master' into small-fixes/0-2725-chat-scroll-bottom

This commit is contained in:
BruebachL 2026-08-12 13:24:01 +02:00 • committed by GitHub
commit f1888c6d97
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
176 changed files with 7230 additions and 1728 deletions

View file

@ -143,7 +143,11 @@ set(cockatrice_SOURCES
src/interface/intents/intent_wait_for_database_load.h src/interface/intents/intent_wait_for_database_load.h
src/interface/layouts/flow_layout.cpp src/interface/layouts/flow_layout.cpp
src/interface/layouts/overlap_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/line_edit_completer.cpp
src/interface/widgets/utility/reversed_completer_model.cpp
src/interface/pixel_map_generator.cpp src/interface/pixel_map_generator.cpp
src/interface/theme_config.cpp src/interface/theme_config.cpp
src/interface/theme_manager.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_manager.cpp
src/interface/widgets/server/user/user_list_painter.cpp src/interface/widgets/server/user/user_list_painter.cpp
src/interface/widgets/server/user/user_list_widget.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/appearance_settings_page.cpp
src/interface/widgets/settings_page/deck_editor_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/general_settings_page.cpp
src/interface/widgets/settings_page/messages_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/shortcut_settings_page.cpp
src/interface/widgets/settings_page/sound_settings_page.cpp src/interface/widgets/settings_page/sound_settings_page.cpp
src/interface/widgets/settings_page/storage_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_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_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/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/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_average_deck_api_response.cpp
src/interface/widgets/tabs/api/edhrec/api_response/average_deck/edhrec_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/widgets/tabs/visual_deck_storage/tab_deck_storage_visual.cpp
src/interface/key_signals.cpp src/interface/key_signals.cpp
src/interface/logger.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.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_bracket_navigation_widget.h
src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_budget_navigation_widget.cpp src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_budget_navigation_widget.cpp

View file

@ -14,8 +14,8 @@
#include <QtConcurrent> #include <QtConcurrent>
#include <libcockatrice/card/database/card_database.h> #include <libcockatrice/card/database/card_database.h>
#include <libcockatrice/card/database/card_database_manager.h> #include <libcockatrice/card/database/card_database_manager.h>
#include <libcockatrice/settings/download_settings.h>
#include <libcockatrice/settings/paths_settings.h> #include <libcockatrice/settings/paths_settings.h>
#include <libcockatrice/settings/personal_settings.h>
#include <version_string.h> #include <version_string.h>
#define SPOILERS_STATUS_URL "https://raw.githubusercontent.com/Cockatrice/Magic-Spoiler/files/SpoilerSeasonEnabled" #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) SpoilerBackgroundUpdater::SpoilerBackgroundUpdater(QObject *apParent) : QObject(apParent), cardUpdateProcess(nullptr)
{ {
isSpoilerDownloadEnabled = SettingsCache::instance().personal().getDownloadSpoilersStatus(); isSpoilerDownloadEnabled = SettingsCache::instance().downloads().getDownloadSpoilersStatus();
if (isSpoilerDownloadEnabled) { if (isSpoilerDownloadEnabled) {
// Start the process of checking if we're in spoiler season // Start the process of checking if we're in spoiler season
// File exists means we're in spoiler season // File exists means we're in spoiler season

View file

@ -11,18 +11,22 @@
#include <QGlobalStatic> #include <QGlobalStatic>
#include <QSettings> #include <QSettings>
#include <QStandardPaths> #include <QStandardPaths>
#include <libcockatrice/settings/appearance_settings.h>
#include <libcockatrice/settings/cache_storage_settings.h> #include <libcockatrice/settings/cache_storage_settings.h>
#include <libcockatrice/settings/card_database_settings.h> #include <libcockatrice/settings/card_database_settings.h>
#include <libcockatrice/settings/card_override_settings.h> #include <libcockatrice/settings/card_override_settings.h>
#include <libcockatrice/settings/cards_display_settings.h> #include <libcockatrice/settings/cards_display_settings.h>
#include <libcockatrice/settings/chat_settings.h> #include <libcockatrice/settings/chat_settings.h>
#include <libcockatrice/settings/commander_bracket_settings.h>
#include <libcockatrice/settings/debug_settings.h> #include <libcockatrice/settings/debug_settings.h>
#include <libcockatrice/settings/deck_editor_settings.h>
#include <libcockatrice/settings/download_settings.h> #include <libcockatrice/settings/download_settings.h>
#include <libcockatrice/settings/game_filters_settings.h> #include <libcockatrice/settings/game_filters_settings.h>
#include <libcockatrice/settings/game_settings.h> #include <libcockatrice/settings/game_settings.h>
#include <libcockatrice/settings/interface_settings.h> #include <libcockatrice/settings/interface_settings.h>
#include <libcockatrice/settings/layouts_settings.h> #include <libcockatrice/settings/layouts_settings.h>
#include <libcockatrice/settings/message_settings.h> #include <libcockatrice/settings/message_settings.h>
#include <libcockatrice/settings/network_settings.h>
#include <libcockatrice/settings/paths_settings.h> #include <libcockatrice/settings/paths_settings.h>
#include <libcockatrice/settings/personal_settings.h> #include <libcockatrice/settings/personal_settings.h>
#include <libcockatrice/settings/recents_settings.h> #include <libcockatrice/settings/recents_settings.h>
@ -135,8 +139,12 @@ SettingsCache::SettingsCache()
personalSettings = new PersonalSettings(settingsPath, this); personalSettings = new PersonalSettings(settingsPath, this);
cardsDisplaySettings = new CardsDisplaySettings(settingsPath, this); cardsDisplaySettings = new CardsDisplaySettings(settingsPath, this);
interfaceSettings = new InterfaceSettings(settingsPath, this); interfaceSettings = new InterfaceSettings(settingsPath, this);
deckEditorSettings = new DeckEditorSettings(settingsPath, this);
pathsSettings = new PathsSettings(settingsPath, this); pathsSettings = new PathsSettings(settingsPath, this);
visualDeckStorageSettings = new VisualDeckStorageSettings(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 // Forward ICardDatabasePathProvider signal from PathsSettings
connect(pathsSettings, &PathsSettings::cardDatabasePathChanged, this, connect(pathsSettings, &PathsSettings::cardDatabasePathChanged, this,
@ -147,7 +155,13 @@ SettingsCache::SettingsCache()
releaseChannels << new StableReleaseChannel(); releaseChannels << new StableReleaseChannel();
releaseChannels << new BetaReleaseChannel(); releaseChannels << new BetaReleaseChannel();
themeName = personalSettings->getThemeName(); themeName = appearanceSettings->getThemeName();
auto definitions = commanderBracketSettings->loadDefinitions();
if (definitions.isEmpty()) {
definitions = CommanderBracketSettings::defaultDefinitions();
}
commanderBracketSettings->reloadDefinitions(definitions);
loadPaths(); loadPaths();
} }
@ -155,7 +169,7 @@ SettingsCache::SettingsCache()
void SettingsCache::setThemeName(const QString &_themeName) void SettingsCache::setThemeName(const QString &_themeName)
{ {
themeName = _themeName; themeName = _themeName;
personalSettings->setThemeName(themeName); appearanceSettings->setThemeName(themeName);
emit themeChanged(); emit themeChanged();
} }
@ -216,15 +230,15 @@ void SettingsCache::loadPaths()
// customPicsPath derived from picsPath // customPicsPath derived from picsPath
QString picsPath = pathsIni.value("paths/pics").toString(); QString picsPath = pathsIni.value("paths/pics").toString();
if (picsPath.endsWith("/")) { if (picsPath.endsWith("/")) {
computePath("custompics", picsPath + "CUSTOM/"); computePath("customPics", picsPath + "CUSTOM/");
} else { } else {
computePath("custompics", picsPath + "/CUSTOM/"); computePath("customPics", picsPath + "/CUSTOM/");
} }
computePath("customsets", dataPath + "/customsets/"); computePath("customSets", dataPath + "/customsets/");
computeFilePath("carddatabase", dataPath + "/cards.xml"); computeFilePath("cardDatabase", dataPath + "/cards.xml");
computeFilePath("tokendatabase", dataPath + "/tokens.xml"); computeFilePath("tokenDatabase", dataPath + "/tokens.xml");
computeFilePath("spoilerdatabase", dataPath + "/spoiler.xml"); computeFilePath("spoilerDatabase", dataPath + "/spoiler.xml");
} }
void SettingsCache::resetPaths() void SettingsCache::resetPaths()
@ -272,12 +286,12 @@ QString SettingsCache::getTokenDatabasePath() const
// INetworkSettingsProvider - delegate to sub-objects // INetworkSettingsProvider - delegate to sub-objects
int SettingsCache::getKeepAlive() const int SettingsCache::getKeepAlive() const
{ {
return personalSettings->getKeepAlive(); return networkSettings->getKeepAlive();
} }
int SettingsCache::getTimeOut() const int SettingsCache::getTimeOut() const
{ {
return personalSettings->getTimeOut(); return networkSettings->getTimeOut();
} }
bool SettingsCache::getNotifyAboutUpdates() const bool SettingsCache::getNotifyAboutUpdates() const
@ -287,17 +301,17 @@ bool SettingsCache::getNotifyAboutUpdates() const
void SettingsCache::setKnownMissingFeatures(const QString &_knownMissingFeatures) void SettingsCache::setKnownMissingFeatures(const QString &_knownMissingFeatures)
{ {
interfaceSettings->setKnownMissingFeatures(_knownMissingFeatures); networkSettings->setKnownMissingFeatures(_knownMissingFeatures);
} }
QString SettingsCache::getKnownMissingFeatures() QString SettingsCache::getKnownMissingFeatures()
{ {
return interfaceSettings->getKnownMissingFeatures(); return networkSettings->getKnownMissingFeatures();
} }
QString SettingsCache::getClientID() QString SettingsCache::getClientID()
{ {
return personalSettings->getClientID(); return networkSettings->getClientID();
} }
// Release channels // Release channels
@ -412,7 +426,7 @@ CardsDisplaySettings &SettingsCache::cardsDisplay() const
return *cardsDisplaySettings; return *cardsDisplaySettings;
} }
InterfaceSettings &SettingsCache::interface() const InterfaceSettings &SettingsCache::userInterface() const
{ {
return *interfaceSettings; return *interfaceSettings;
} }
@ -422,7 +436,22 @@ PathsSettings &SettingsCache::paths() const
return *pathsSettings; return *pathsSettings;
} }
DeckEditorSettings &SettingsCache::deckEditor() const
{
return *deckEditorSettings;
}
VisualDeckStorageSettings &SettingsCache::visualDeckStorage() const VisualDeckStorageSettings &SettingsCache::visualDeckStorage() const
{ {
return *visualDeckStorageSettings; return *visualDeckStorageSettings;
} }
AppearanceSettings &SettingsCache::appearance() const
{
return *appearanceSettings;
}
NetworkSettings &SettingsCache::network() const
{
return *networkSettings;
}

View file

@ -28,7 +28,9 @@ class CardDatabaseSettings;
class CardOverrideSettings; class CardOverrideSettings;
class CardsDisplaySettings; class CardsDisplaySettings;
class ChatSettings; class ChatSettings;
class CommanderBracketSettings;
class DebugSettings; class DebugSettings;
class DeckEditorSettings;
class DownloadSettings; class DownloadSettings;
class GameFiltersSettings; class GameFiltersSettings;
class GameSettings; class GameSettings;
@ -44,6 +46,8 @@ class SoundSettings;
class TabsSettings; class TabsSettings;
class UpdatesSettings; class UpdatesSettings;
class VisualDeckStorageSettings; class VisualDeckStorageSettings;
class AppearanceSettings;
class NetworkSettings;
class QSettings; class QSettings;
class SettingsCache : public ICardDatabasePathProvider, public INetworkSettingsProvider class SettingsCache : public ICardDatabasePathProvider, public INetworkSettingsProvider
@ -75,8 +79,12 @@ private:
PersonalSettings *personalSettings; PersonalSettings *personalSettings;
CardsDisplaySettings *cardsDisplaySettings; CardsDisplaySettings *cardsDisplaySettings;
InterfaceSettings *interfaceSettings; InterfaceSettings *interfaceSettings;
DeckEditorSettings *deckEditorSettings;
PathsSettings *pathsSettings; PathsSettings *pathsSettings;
VisualDeckStorageSettings *visualDeckStorageSettings; VisualDeckStorageSettings *visualDeckStorageSettings;
AppearanceSettings *appearanceSettings;
NetworkSettings *networkSettings;
CommanderBracketSettings *commanderBracketSettings;
QString themeName; QString themeName;
@ -138,9 +146,16 @@ public:
[[nodiscard]] UpdatesSettings &updates() const; [[nodiscard]] UpdatesSettings &updates() const;
[[nodiscard]] PersonalSettings &personal() const; [[nodiscard]] PersonalSettings &personal() const;
[[nodiscard]] CardsDisplaySettings &cardsDisplay() const; [[nodiscard]] CardsDisplaySettings &cardsDisplay() const;
[[nodiscard]] InterfaceSettings &interface() const; [[nodiscard]] InterfaceSettings &userInterface() const;
[[nodiscard]] DeckEditorSettings &deckEditor() const;
[[nodiscard]] PathsSettings &paths() const; [[nodiscard]] PathsSettings &paths() const;
[[nodiscard]] VisualDeckStorageSettings &visualDeckStorage() const; [[nodiscard]] VisualDeckStorageSettings &visualDeckStorage() const;
[[nodiscard]] AppearanceSettings &appearance() const;
[[nodiscard]] NetworkSettings &network() const;
[[nodiscard]] CommanderBracketSettings &commanderBrackets() const
{
return *commanderBracketSettings;
}
[[nodiscard]] bool getIsPortableBuild() const [[nodiscard]] bool getIsPortableBuild() const
{ {

View file

@ -5,7 +5,7 @@
#include <QtMath> #include <QtMath>
CardCounterSettings::CardCounterSettings(const QString &settingsPath, QObject *parent) CardCounterSettings::CardCounterSettings(const QString &settingsPath, QObject *parent)
: SettingsManager(settingsPath + "global.ini", "cards", "counters", parent) : SettingsManager(settingsPath + "card_counters.ini", "cards", "counters", parent)
{ {
} }

View file

@ -287,6 +287,9 @@ void GameEventHandler::eventGameStateChanged(const Event_GameStateChanged &event
if (!game->getGameMetaInfo()->proto().share_decklists_on_load()) { if (!game->getGameMetaInfo()->proto().share_decklists_on_load()) {
continue; continue;
} }
if (!playerInfo.has_deck_list()) {
continue;
}
opponentDecksToDisplay.append( opponentDecksToDisplay.append(
qMakePair(playerId, qMakePair(playerName, QString::fromStdString(playerInfo.deck_list())))); qMakePair(playerId, qMakePair(playerName, QString::fromStdString(playerInfo.deck_list()))));

View file

@ -69,7 +69,7 @@ void PlayerActions::playCard(CardItem *card, bool faceDown)
const CardInfo &info = exactCard.getInfo(); const CardInfo &info = exactCard.getInfo();
int tableRow = info.getUiAttributes().tableRow; int tableRow = info.getUiAttributes().tableRow;
bool playToStack = SettingsCache::instance().interface().getPlayToStack(); bool playToStack = SettingsCache::instance().userInterface().getPlayToStack();
QString currentZone = card->getZone()->getName(); QString currentZone = card->getZone()->getName();
if (!faceDown && currentZone == ZoneNames::STACK && tableRow == 3) { if (!faceDown && currentZone == ZoneNames::STACK && tableRow == 3) {
cmd.set_target_zone(ZoneNames::GRAVE); cmd.set_target_zone(ZoneNames::GRAVE);
@ -312,7 +312,7 @@ void PlayerActions::actDrawCard()
void PlayerActions::actRequestMulliganDialog() void PlayerActions::actRequestMulliganDialog()
{ {
int startSize = SettingsCache::instance().interface().getStartingHandSize(); int startSize = SettingsCache::instance().userInterface().getStartingHandSize();
int handSize = player->getHandZone()->getCards().size(); int handSize = player->getHandZone()->getCards().size();
int deckSize = player->getDeckZone()->getCards().size() + handSize; int deckSize = player->getDeckZone()->getCards().size() + handSize;
@ -328,7 +328,7 @@ void PlayerActions::actMulligan(int number)
} }
doMulligan(number); doMulligan(number);
SettingsCache::instance().interface().setStartingHandSize(number); SettingsCache::instance().userInterface().setStartingHandSize(number);
} }
void PlayerActions::actMulliganSameSize() void PlayerActions::actMulliganSameSize()
@ -932,13 +932,13 @@ void PlayerActions::setLastTokenInfo(CardInfoPtr cardInfo)
return; return;
} }
lastTokenInfo = {.name = cardInfo->getName(), lastTokenInfo = {
.color = cardInfo->getColors().isEmpty() ? QString() : cardInfo->getColors().left(1).toLower(), .name = cardInfo->getName(),
.pt = cardInfo->getPowTough(), .color = cardInfo->getColors().isEmpty() ? QString() : cardInfo->getColors().left(1).toLower(),
.annotation = SettingsCache::instance().interface().getAnnotateTokens() ? cardInfo->getText() : "", .pt = cardInfo->getPowTough(),
.destroy = true, .annotation = SettingsCache::instance().userInterface().getAnnotateTokens() ? cardInfo->getText() : "",
.providerId = .destroy = true,
SettingsCache::instance().cardOverrides().getCardPreferenceOverride(cardInfo->getName())}; .providerId = SettingsCache::instance().cardOverrides().getCardPreferenceOverride(cardInfo->getName())};
lastTokenTableRow = TableZone::tableRowToGridY(cardInfo->getUiAttributes().tableRow); lastTokenTableRow = TableZone::tableRowToGridY(cardInfo->getUiAttributes().tableRow);
@ -1171,7 +1171,7 @@ void PlayerActions::createCard(const CardItem *sourceCard,
} }
cmd.set_pt(cardInfo->getPowTough().toStdString()); cmd.set_pt(cardInfo->getPowTough().toStdString());
if (SettingsCache::instance().interface().getAnnotateTokens()) { if (SettingsCache::instance().userInterface().getAnnotateTokens()) {
cmd.set_annotation(cardInfo->getText().toStdString()); cmd.set_annotation(cardInfo->getText().toStdString());
} else { } else {
cmd.set_annotation(""); cmd.set_annotation("");

View file

@ -58,7 +58,7 @@ bool ZoneViewZoneLogic::prepareAddCard(int x)
// autoclose check is done both here and in removeCard // 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(); emit closeView();
} }
@ -145,7 +145,7 @@ void ZoneViewZoneLogic::removeCard(int position, bool toNewZone)
// card gets dragged within the view. // 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 // 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. // unrevealed portion of the same zone.
if (cards.isEmpty() && SettingsCache::instance().interface().getCloseEmptyCardView() && toNewZone) { if (cards.isEmpty() && SettingsCache::instance().userInterface().getCloseEmptyCardView() && toNewZone) {
emit closeView(); emit closeView();
return; return;
} }

View file

@ -12,9 +12,9 @@
#include <algorithm> #include <algorithm>
#include <libcockatrice/card/database/card_database.h> #include <libcockatrice/card/database/card_database.h>
#include <libcockatrice/card/database/card_database_manager.h> #include <libcockatrice/card/database/card_database_manager.h>
#include <libcockatrice/settings/appearance_settings.h>
#include <libcockatrice/settings/cards_display_settings.h> #include <libcockatrice/settings/cards_display_settings.h>
#include <libcockatrice/settings/debug_settings.h> #include <libcockatrice/settings/debug_settings.h>
#include <libcockatrice/settings/personal_settings.h>
AbstractCardItem::AbstractCardItem(QGraphicsItem *parent, const CardRef &cardRef, PlayerLogic *_owner, int _id) AbstractCardItem::AbstractCardItem(QGraphicsItem *parent, const CardRef &cardRef, PlayerLogic *_owner, int _id)
: ArrowTarget(_owner, parent), id(_id), cardRef(cardRef), tapped(false), facedown(false), tapAngle(0), : 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) 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); const int fontSize = std::max(9, MAX_FONT_SIZE);
QRectF totalBoundingRect = painter->combinedTransform().mapRect(boundingRect()); QRectF totalBoundingRect = painter->combinedTransform().mapRect(boundingRect());

View file

@ -262,7 +262,7 @@ void ArrowDragItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
if (startZone->getName() == ZoneNames::HAND) { if (startZone->getName() == ZoneNames::HAND) {
startCard->playCard(false); startCard->playCard(false);
CardInfoPtr ci = startCard->getCard().getCardPtr(); CardInfoPtr ci = startCard->getCard().getCardPtr();
bool playToStack = SettingsCache::instance().interface().getPlayToStack(); bool playToStack = SettingsCache::instance().userInterface().getPlayToStack();
if (ci && ((!playToStack && ci->getUiAttributes().tableRow == 3) || if (ci && ((!playToStack && ci->getUiAttributes().tableRow == 3) ||
(playToStack && ci->getUiAttributes().tableRow != 0 && (playToStack && ci->getUiAttributes().tableRow != 0 &&
startCard->getZone()->getName() != ZoneNames::STACK))) { startCard->getZone()->getName() != ZoneNames::STACK))) {

View file

@ -281,7 +281,7 @@ void CardItem::drawArrow(const QColor &arrowColor)
auto *game = owner->getGame(); auto *game = owner->getGame();
PlayerLogic *arrowOwner = game->getPlayerManager()->getActiveLocalPlayer(game->getGameState()->getActivePlayer()); PlayerLogic *arrowOwner = game->getPlayerManager()->getActiveLocalPlayer(game->getGameState()->getActivePlayer());
int phase = 0; // 0 means to not set the phase 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(); int currentPhase = game->getGameState()->getCurrentPhase();
phase = Phases::getLastSubphase(currentPhase) + 1; phase = Phases::getLastSubphase(currentPhase) + 1;
} }
@ -400,7 +400,7 @@ void CardItem::playCard(bool faceDown)
if (tz) { if (tz) {
emit tz->toggleTapped(); emit tz->toggleTapped();
} else { } else {
if (SettingsCache::instance().interface().getClickPlaysAllSelected()) { if (SettingsCache::instance().userInterface().getClickPlaysAllSelected()) {
if (faceDown) { if (faceDown) {
emit playSelectedFaceDown(this); emit playSelectedFaceDown(this);
} else { } else {
@ -464,7 +464,7 @@ static bool isUnwritableRevealZone(CardZoneLogic *zone)
void CardItem::handleClickedToPlay(bool shiftHeld) void CardItem::handleClickedToPlay(bool shiftHeld)
{ {
if (isUnwritableRevealZone(state->getZone())) { if (isUnwritableRevealZone(state->getZone())) {
if (SettingsCache::instance().interface().getClickPlaysAllSelected()) { if (SettingsCache::instance().userInterface().getClickPlaysAllSelected()) {
emit hideSelected(this); emit hideSelected(this);
} else { } else {
state->getZone()->removeCard(this); state->getZone()->removeCard(this);
@ -481,7 +481,7 @@ void CardItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
return; return;
} }
if ((event->modifiers() != Qt::AltModifier) && (event->button() == Qt::LeftButton) && if ((event->modifiers() != Qt::AltModifier) && (event->button() == Qt::LeftButton) &&
(!SettingsCache::instance().interface().getDoubleClickToPlay())) { (!SettingsCache::instance().userInterface().getDoubleClickToPlay())) {
handleClickedToPlay(event->modifiers().testFlag(Qt::ShiftModifier)); handleClickedToPlay(event->modifiers().testFlag(Qt::ShiftModifier));
} }
if (owner != nullptr) { if (owner != nullptr) {
@ -493,7 +493,7 @@ void CardItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
void CardItem::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) void CardItem::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event)
{ {
if ((event->modifiers() != Qt::AltModifier) && (event->buttons() == Qt::LeftButton) && if ((event->modifiers() != Qt::AltModifier) && (event->buttons() == Qt::LeftButton) &&
(SettingsCache::instance().interface().getDoubleClickToPlay())) { (SettingsCache::instance().userInterface().getDoubleClickToPlay())) {
handleClickedToPlay(event->modifiers().testFlag(Qt::ShiftModifier)); handleClickedToPlay(event->modifiers().testFlag(Qt::ShiftModifier));
} }
event->accept(); event->accept();

View file

@ -189,7 +189,7 @@ void DlgCreateToken::tokenSelectionChanged(const QModelIndex &current, const QMo
const QChar cardColor = cardInfo->getColorChar(); const QChar cardColor = cardInfo->getColorChar();
colorEdit->setCurrentIndex(colorEdit->findData(cardColor, Qt::UserRole, Qt::MatchFixedString)); colorEdit->setCurrentIndex(colorEdit->findData(cardColor, Qt::UserRole, Qt::MatchFixedString));
ptEdit->setText(cardInfo->getPowTough()); ptEdit->setText(cardInfo->getPowTough());
if (SettingsCache::instance().interface().getAnnotateTokens()) { if (SettingsCache::instance().userInterface().getAnnotateTokens()) {
annotationEdit->setText(cardInfo->getText()); annotationEdit->setText(cardInfo->getText());
} }
} else { } else {

View file

@ -37,7 +37,7 @@ GameScene::GameScene(PhasesToolbar *_phasesToolbar, QObject *parent)
{ {
animationTimer = new QBasicTimer; animationTimer = new QBasicTimer;
addItem(phasesToolbar); addItem(phasesToolbar);
connect(&SettingsCache::instance().interface(), &InterfaceSettings::minPlayersForMultiColumnLayoutChanged, this, connect(&SettingsCache::instance().userInterface(), &InterfaceSettings::minPlayersForMultiColumnLayoutChanged, this,
&GameScene::rearrange); &GameScene::rearrange);
rearrange(); rearrange();
@ -336,7 +336,7 @@ QList<PlayerLogic *> GameScene::rotatePlayers(const QList<PlayerLogic *> &active
int GameScene::determineColumnCount(int playerCount) int GameScene::determineColumnCount(int playerCount)
{ {
return playerCount < SettingsCache::instance().interface().getMinPlayersForMultiColumnLayout() ? 1 : 2; return playerCount < SettingsCache::instance().userInterface().getMinPlayersForMultiColumnLayout() ? 1 : 2;
} }
/** /**

View file

@ -47,11 +47,11 @@ GameView::GameView(GameScene *scene, QWidget *parent) : QGraphicsView(scene, par
connect(scene, &GameScene::sigResizeRubberBand, this, &GameView::resizeRubberBand); connect(scene, &GameScene::sigResizeRubberBand, this, &GameView::resizeRubberBand);
connect(scene, &GameScene::sigStopRubberBand, this, &GameView::stopRubberBand); connect(scene, &GameScene::sigStopRubberBand, this, &GameView::stopRubberBand);
connect(scene, &QGraphicsScene::selectionChanged, this, [this]() { updateTotalSelectionCount(); }); connect(scene, &QGraphicsScene::selectionChanged, this, [this]() { updateTotalSelectionCount(); });
connect(&SettingsCache::instance().interface(), &InterfaceSettings::tallyTypeChanged, this, connect(&SettingsCache::instance().userInterface(), &InterfaceSettings::tallyTypeChanged, this,
[this] { updateTotalSelectionCount(); }); [this] { updateTotalSelectionCount(); });
setFocusDisabled(SettingsCache::instance().interface().getKeepGameChatFocus()); setFocusDisabled(SettingsCache::instance().userInterface().getKeepGameChatFocus());
connect(&SettingsCache::instance().interface(), &InterfaceSettings::keepGameChatFocusChanged, this, connect(&SettingsCache::instance().userInterface(), &InterfaceSettings::keepGameChatFocusChanged, this,
&GameView::setFocusDisabled); &GameView::setFocusDisabled);
aCloseMostRecentZoneView = new QAction(this); aCloseMostRecentZoneView = new QAction(this);
@ -130,7 +130,7 @@ void GameView::resizeRubberBand(const QPointF &cursorPoint, int selectedCount)
QRect rect = QRect(mapFromScene(selectionOrigin), cursor).normalized(); QRect rect = QRect(mapFromScene(selectionOrigin), cursor).normalized();
rubberBand->setGeometry(rect); rubberBand->setGeometry(rect);
if (!SettingsCache::instance().interface().getShowDragSelectionCount()) { if (!SettingsCache::instance().userInterface().getShowDragSelectionCount()) {
dragCountLabel->hide(); dragCountLabel->hide();
return; return;
} }
@ -239,7 +239,7 @@ void GameView::updateTotalSelectionCount(const QSize &viewSize)
int count = scene()->selectedItems().count(); int count = scene()->selectedItems().count();
if (!SettingsCache::instance().interface().getShowTotalSelectionCount() || count <= 1) { if (!SettingsCache::instance().userInterface().getShowTotalSelectionCount() || count <= 1) {
totalCountLabel->hide(); totalCountLabel->hide();
} else { } else {
totalCountLabel->setText(QString::number(count)); totalCountLabel->setText(QString::number(count));
@ -251,7 +251,7 @@ void GameView::updateTotalSelectionCount(const QSize &viewSize)
totalCountLabel->show(); totalCountLabel->show();
} }
TallyType tallyType = Tally::intToType(SettingsCache::instance().interface().getTallyType()); TallyType tallyType = Tally::intToType(SettingsCache::instance().userInterface().getTallyType());
GameScene *gameScene = static_cast<GameScene *>(scene()); GameScene *gameScene = static_cast<GameScene *>(scene());
QList<TallyRow> entries = Tally::compute(gameScene->selectedCards(), tallyType); QList<TallyRow> entries = Tally::compute(gameScene->selectedCards(), tallyType);

View file

@ -23,14 +23,14 @@ TallyMenu::TallyMenu()
QAction *TallyMenu::createTallyAction(TallyType tallyType) 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); QAction *action = new QAction(this);
action->setCheckable(true); action->setCheckable(true);
action->setChecked(tallyType == currentType); action->setChecked(tallyType == currentType);
connect(action, &QAction::triggered, &SettingsCache::instance().interface(), connect(action, &QAction::triggered, &SettingsCache::instance().userInterface(),
[tallyType] { SettingsCache::instance().interface().setTallyType(static_cast<int>(tallyType)); }); [tallyType] { SettingsCache::instance().userInterface().setTallyType(static_cast<int>(tallyType)); });
actionGroup->addAction(action); actionGroup->addAction(action);

View file

@ -17,9 +17,9 @@
PlayerGraphicsItem::PlayerGraphicsItem(PlayerLogic *_player) : player(_player) PlayerGraphicsItem::PlayerGraphicsItem(PlayerLogic *_player) : player(_player)
{ {
connect(&SettingsCache::instance().interface(), &InterfaceSettings::horizontalHandChanged, this, connect(&SettingsCache::instance().userInterface(), &InterfaceSettings::horizontalHandChanged, this,
&PlayerGraphicsItem::rearrangeZones); &PlayerGraphicsItem::rearrangeZones);
connect(&SettingsCache::instance().interface(), &InterfaceSettings::handJustificationChanged, this, connect(&SettingsCache::instance().userInterface(), &InterfaceSettings::handJustificationChanged, this,
&PlayerGraphicsItem::rearrangeZones); &PlayerGraphicsItem::rearrangeZones);
connect(player, &PlayerLogic::rearrangeCounters, this, &PlayerGraphicsItem::rearrangeCounters); connect(player, &PlayerLogic::rearrangeCounters, this, &PlayerGraphicsItem::rearrangeCounters);
connect(player, &PlayerLogic::activeChanged, this, &PlayerGraphicsItem::onPlayerActiveChanged); connect(player, &PlayerLogic::activeChanged, this, &PlayerGraphicsItem::onPlayerActiveChanged);
@ -149,7 +149,7 @@ qreal PlayerGraphicsItem::getMinimumWidth() const
{ {
qreal result = tableZoneGraphicsItem->getMinimumWidth() + CardDimensions::HEIGHT_F + 15 + counterAreaWidth + qreal result = tableZoneGraphicsItem->getMinimumWidth() + CardDimensions::HEIGHT_F + 15 + counterAreaWidth +
stackZoneGraphicsItem->boundingRect().width(); stackZoneGraphicsItem->boundingRect().width();
if (!SettingsCache::instance().interface().getHorizontalHand()) { if (!SettingsCache::instance().userInterface().getHorizontalHand()) {
result += handZoneGraphicsItem->boundingRect().width(); result += handZoneGraphicsItem->boundingRect().width();
} }
return result; return result;
@ -166,7 +166,7 @@ void PlayerGraphicsItem::processSceneSizeChange(int newPlayerWidth)
// Extend table (and hand, if horizontal) to accommodate the new player width. // Extend table (and hand, if horizontal) to accommodate the new player width.
qreal tableWidth = newPlayerWidth - CardDimensions::HEIGHT_F - 15 - counterAreaWidth - qreal tableWidth = newPlayerWidth - CardDimensions::HEIGHT_F - 15 - counterAreaWidth -
stackZoneGraphicsItem->boundingRect().width(); stackZoneGraphicsItem->boundingRect().width();
if (!SettingsCache::instance().interface().getHorizontalHand()) { if (!SettingsCache::instance().userInterface().getHorizontalHand()) {
tableWidth -= handZoneGraphicsItem->boundingRect().width(); tableWidth -= handZoneGraphicsItem->boundingRect().width();
} }
@ -234,7 +234,7 @@ void PlayerGraphicsItem::rearrangeCounters()
void PlayerGraphicsItem::rearrangeZones() void PlayerGraphicsItem::rearrangeZones()
{ {
auto base = QPointF(CardDimensions::HEIGHT_F + counterAreaWidth + 15, 0); auto base = QPointF(CardDimensions::HEIGHT_F + counterAreaWidth + 15, 0);
if (SettingsCache::instance().interface().getHorizontalHand()) { if (SettingsCache::instance().userInterface().getHorizontalHand()) {
if (mirrored) { if (mirrored) {
if (player->getHandZone()->contentsKnown()) { if (player->getHandZone()->contentsKnown()) {
handVisible = true; handVisible = true;
@ -285,7 +285,7 @@ void PlayerGraphicsItem::updateBoundingRect()
{ {
prepareGeometryChange(); prepareGeometryChange();
qreal width = CardDimensions::HEIGHT_F + 15 + counterAreaWidth + stackZoneGraphicsItem->boundingRect().width(); 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; qreal handHeight = handVisible ? handZoneGraphicsItem->boundingRect().height() : 0;
bRect = QRectF(0, 0, width + tableZoneGraphicsItem->boundingRect().width(), bRect = QRectF(0, 0, width + tableZoneGraphicsItem->boundingRect().width(),
tableZoneGraphicsItem->boundingRect().height() + handHeight); tableZoneGraphicsItem->boundingRect().height() + handHeight);

View file

@ -34,7 +34,7 @@ void HandZone::handleDropEvent(const QList<CardDragItem *> &dragItems,
QPoint point = dropPoint + scenePos().toPoint(); QPoint point = dropPoint + scenePos().toPoint();
int x = -1; int x = -1;
if (SettingsCache::instance().interface().getHorizontalHand()) { if (SettingsCache::instance().userInterface().getHorizontalHand()) {
for (x = 0; x < getLogic()->getCards().size(); x++) { for (x = 0; x < getLogic()->getCards().size(); x++) {
if (point.x() < static_cast<CardItem *>(getLogic()->getCards().at(x))->scenePos().x()) { if (point.x() < static_cast<CardItem *>(getLogic()->getCards().at(x))->scenePos().x()) {
break; break;
@ -61,7 +61,7 @@ void HandZone::handleDropEvent(const QList<CardDragItem *> &dragItems,
QRectF HandZone::boundingRect() const QRectF HandZone::boundingRect() const
{ {
if (SettingsCache::instance().interface().getHorizontalHand()) { if (SettingsCache::instance().userInterface().getHorizontalHand()) {
return QRectF(0, 0, width, CardDimensions::HEIGHT_F + 10); return QRectF(0, 0, width, CardDimensions::HEIGHT_F + 10);
} else { } else {
return QRectF(0, 0, CardDimensions::WIDTH_F * 1.5, zoneHeight); return QRectF(0, 0, CardDimensions::WIDTH_F * 1.5, zoneHeight);
@ -78,8 +78,8 @@ void HandZone::reorganizeCards()
{ {
if (!getLogic()->getCards().isEmpty()) { if (!getLogic()->getCards().isEmpty()) {
const int cardCount = getLogic()->getCards().size(); const int cardCount = getLogic()->getCards().size();
if (SettingsCache::instance().interface().getHorizontalHand()) { if (SettingsCache::instance().userInterface().getHorizontalHand()) {
bool leftJustified = SettingsCache::instance().interface().getLeftJustified(); bool leftJustified = SettingsCache::instance().userInterface().getLeftJustified();
qreal cardWidth = getLogic()->getCards().at(0)->boundingRect().width(); qreal cardWidth = getLogic()->getCards().at(0)->boundingRect().width();
const int xPadding = leftJustified ? cardWidth * 1.4 : 5; const int xPadding = leftJustified ? cardWidth * 1.4 : 5;
qreal totalWidth = qreal totalWidth =
@ -127,7 +127,7 @@ void HandZone::sortHand(const QList<CardList::SortOption> &options)
void HandZone::setWidth(qreal _width) void HandZone::setWidth(qreal _width)
{ {
if (SettingsCache::instance().interface().getHorizontalHand()) { if (SettingsCache::instance().userInterface().getHorizontalHand()) {
prepareGeometryChange(); prepareGeometryChange();
width = _width; width = _width;
reorganizeCards(); reorganizeCards();

View file

@ -29,7 +29,7 @@ TableZone::TableZone(TableZoneLogic *_logic, bool _mirrored, QGraphicsItem *pare
connect(_logic, &TableZoneLogic::contentSizeChanged, this, &TableZone::resizeToContents); connect(_logic, &TableZoneLogic::contentSizeChanged, this, &TableZone::resizeToContents);
connect(_logic, &TableZoneLogic::toggleTapped, this, &TableZone::toggleTapped); connect(_logic, &TableZoneLogic::toggleTapped, this, &TableZone::toggleTapped);
connect(themeManager, &ThemeManager::themeChanged, this, &TableZone::updateBg); connect(themeManager, &ThemeManager::themeChanged, this, &TableZone::updateBg);
connect(&SettingsCache::instance().interface(), &InterfaceSettings::invertVerticalCoordinateChanged, this, connect(&SettingsCache::instance().userInterface(), &InterfaceSettings::invertVerticalCoordinateChanged, this,
&TableZone::reorganizeCards); &TableZone::reorganizeCards);
updateBg(); updateBg();
@ -60,8 +60,8 @@ void TableZone::setMirrored(bool isMirrored)
bool TableZone::isInverted() const bool TableZone::isInverted() const
{ {
return ((mirrored && !SettingsCache::instance().interface().getInvertVerticalCoordinate()) || return ((mirrored && !SettingsCache::instance().userInterface().getInvertVerticalCoordinate()) ||
(!mirrored && SettingsCache::instance().interface().getInvertVerticalCoordinate())); (!mirrored && SettingsCache::instance().userInterface().getInvertVerticalCoordinate()));
} }
void TableZone::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*option*/, QWidget * /*widget*/) void TableZone::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*option*/, QWidget * /*widget*/)

View file

@ -66,7 +66,7 @@ ZoneViewWidget::ZoneViewWidget(PlayerLogic *_player,
connect(help, &QAction::triggered, this, [this] { createSearchSyntaxHelpWindow(&searchEdit); }); connect(help, &QAction::triggered, this, [this] { createSearchSyntaxHelpWindow(&searchEdit); });
if (SettingsCache::instance().interface().getFocusCardViewSearchBar()) { if (SettingsCache::instance().userInterface().getFocusCardViewSearchBar()) {
this->setActive(true); this->setActive(true);
searchEdit.setFocus(); searchEdit.setFocus();
} }
@ -77,9 +77,9 @@ ZoneViewWidget::ZoneViewWidget(PlayerLogic *_player,
vbox->addItem(searchEditProxy); vbox->addItem(searchEditProxy);
// hide search bar if chat autofocus setting is enabled, since typing into it will no longer work anyway // hide search bar if chat autofocus setting is enabled, since typing into it will no longer work anyway
searchEditProxy->setVisible(!SettingsCache::instance().interface().getKeepGameChatFocus()); searchEditProxy->setVisible(!SettingsCache::instance().userInterface().getKeepGameChatFocus());
connect(&SettingsCache::instance().interface(), &InterfaceSettings::keepGameChatFocusChanged, searchEditProxy, connect(&SettingsCache::instance().userInterface(), &InterfaceSettings::keepGameChatFocusChanged,
[searchEditProxy](bool keepFocus) { searchEditProxy->setVisible(!keepFocus); }); searchEditProxy, [searchEditProxy](bool keepFocus) { searchEditProxy->setVisible(!keepFocus); });
// top row // top row
QGraphicsLinearLayout *hTopRow = new QGraphicsLinearLayout(Qt::Horizontal); QGraphicsLinearLayout *hTopRow = new QGraphicsLinearLayout(Qt::Horizontal);
@ -159,9 +159,9 @@ ZoneViewWidget::ZoneViewWidget(PlayerLogic *_player,
connect(&sortBySelector, static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged), this, connect(&sortBySelector, static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged), this,
&ZoneViewWidget::processSortBy); &ZoneViewWidget::processSortBy);
connect(&pileViewCheckBox, &QCheckBox::QT_STATE_CHANGED, this, &ZoneViewWidget::processSetPileView); connect(&pileViewCheckBox, &QCheckBox::QT_STATE_CHANGED, this, &ZoneViewWidget::processSetPileView);
groupBySelector.setCurrentIndex(SettingsCache::instance().interface().getZoneViewGroupByIndex()); groupBySelector.setCurrentIndex(SettingsCache::instance().userInterface().getZoneViewGroupByIndex());
sortBySelector.setCurrentIndex(SettingsCache::instance().interface().getZoneViewSortByIndex()); sortBySelector.setCurrentIndex(SettingsCache::instance().userInterface().getZoneViewSortByIndex());
pileViewCheckBox.setChecked(SettingsCache::instance().interface().getZoneViewPileView()); pileViewCheckBox.setChecked(SettingsCache::instance().userInterface().getZoneViewPileView());
if (CardList::NoSort == static_cast<CardList::SortOption>(groupBySelector.currentData().toInt())) { if (CardList::NoSort == static_cast<CardList::SortOption>(groupBySelector.currentData().toInt())) {
pileViewCheckBox.setEnabled(false); pileViewCheckBox.setEnabled(false);
@ -191,7 +191,7 @@ ZoneViewWidget::ZoneViewWidget(PlayerLogic *_player,
void ZoneViewWidget::processGroupBy(int index) void ZoneViewWidget::processGroupBy(int index)
{ {
auto option = static_cast<CardList::SortOption>(groupBySelector.itemData(index).toInt()); auto option = static_cast<CardList::SortOption>(groupBySelector.itemData(index).toInt());
SettingsCache::instance().interface().setZoneViewGroupByIndex(index); SettingsCache::instance().userInterface().setZoneViewGroupByIndex(index);
zone->setGroupBy(option); zone->setGroupBy(option);
// disable pile view checkbox if we're not grouping by anything // disable pile view checkbox if we're not grouping by anything
@ -215,13 +215,13 @@ void ZoneViewWidget::processSortBy(int index)
return; return;
} }
SettingsCache::instance().interface().setZoneViewSortByIndex(index); SettingsCache::instance().userInterface().setZoneViewSortByIndex(index);
zone->setSortBy(option); zone->setSortBy(option);
} }
void ZoneViewWidget::processSetPileView(QT_STATE_CHANGED_T value) void ZoneViewWidget::processSetPileView(QT_STATE_CHANGED_T value)
{ {
SettingsCache::instance().interface().setZoneViewPileView(value); SettingsCache::instance().userInterface().setZoneViewPileView(value);
zone->setPileView(value); zone->setPileView(value);
} }
@ -478,7 +478,7 @@ static qreal rowsToHeight(int rows)
**/ **/
static qreal calcMaxInitialHeight() 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() void ZoneViewWidget::expandWindow()
{ {
qreal maxInitialHeight = calcMaxInitialHeight(); qreal maxInitialHeight = calcMaxInitialHeight();
qreal maxExpandedHeight = rowsToHeight(SettingsCache::instance().interface().getCardViewExpandedRowsMax()); qreal maxExpandedHeight = rowsToHeight(SettingsCache::instance().userInterface().getCardViewExpandedRowsMax());
qreal height = rect().height() - extraHeight - 10; qreal height = rect().height() - extraHeight - 10;
qreal maxHeight = maximumHeight() - extraHeight - 10; qreal maxHeight = maximumHeight() - extraHeight - 10;

View file

@ -19,19 +19,22 @@
#include <QThread> #include <QThread>
#include <algorithm> #include <algorithm>
#include <libcockatrice/settings/cache_storage_settings.h> #include <libcockatrice/settings/cache_storage_settings.h>
#include <libcockatrice/settings/download_settings.h>
#include <libcockatrice/settings/paths_settings.h> #include <libcockatrice/settings/paths_settings.h>
#include <libcockatrice/settings/personal_settings.h>
#include <utility> #include <utility>
// never cache more than 300 cards at once for a single deck // never cache more than 300 cards at once for a single deck
#define CACHED_CARD_PER_DECK_MAX 300 #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) CardPictureLoader::CardPictureLoader() : QObject(nullptr)
{ {
worker = new CardPictureLoaderWorker; worker = new CardPictureLoaderWorker;
connect(&SettingsCache::instance().paths(), &PathsSettings::picsPathChanged, this, connect(&SettingsCache::instance().paths(), &PathsSettings::picsPathChanged, this,
&CardPictureLoader::picsPathChanged); &CardPictureLoader::picsPathChanged);
connect(&SettingsCache::instance().personal(), &PersonalSettings::picDownloadChanged, this, connect(&SettingsCache::instance().downloads(), &DownloadSettings::picDownloadChanged, this,
&CardPictureLoader::picDownloadChanged); &CardPictureLoader::picDownloadChanged);
qRegisterMetaType<ExactCard>(); qRegisterMetaType<ExactCard>();
@ -135,7 +138,14 @@ void CardPictureLoader::getPixmap(QPixmap &pixmap, const ExactCard &card, QSize
QPixmap bigPixmap; QPixmap bigPixmap;
if (QPixmapCache::find(key, &bigPixmap)) { if (QPixmapCache::find(key, &bigPixmap)) {
if (bigPixmap.isNull()) { if (bigPixmap.isNull()) {
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; return;
} }
@ -159,8 +169,10 @@ void CardPictureLoader::imageLoaded(const ExactCard &card, const QImage &image)
QPixmap finalPixmap; QPixmap finalPixmap;
if (image.isNull()) { if (image.isNull()) {
getInstance().failedAt.insert(card.getPixmapCacheKey(), QDateTime::currentDateTime());
qCDebug(CardPictureLoaderLog) << "Caching NULL pixmap for" << card.getName(); qCDebug(CardPictureLoaderLog) << "Caching NULL pixmap for" << card.getName();
} else { } else {
getInstance().failedAt.remove(card.getPixmapCacheKey());
if (card.getInfo().getUiAttributes().upsideDownArt) { if (card.getInfo().getUiAttributes().upsideDownArt) {
#if (QT_VERSION >= QT_VERSION_CHECK(6, 9, 0)) #if (QT_VERSION >= QT_VERSION_CHECK(6, 9, 0))
QImage mirrorImage = image.flipped(Qt::Horizontal | Qt::Vertical); 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. // imageLoaded should only be reached if the exactCard isn't already in cache.
// (plus there's a deduplication mechanism in CardPictureLoaderWorker) // (plus there's a deduplication mechanism in CardPictureLoaderWorker)
// It should be safe to connect the CardInfo here without worrying about redundant connections. // It should be safe to connect the CardInfo here without worrying about redundant connections.
connect(card.getCardPtr().data(), &QObject::destroyed, this, connect(card.getCardPtr().data(), &QObject::destroyed, this, [cacheKey = card.getPixmapCacheKey()] {
[cacheKey = card.getPixmapCacheKey()] { QPixmapCache::remove(cacheKey); }); QPixmapCache::remove(cacheKey);
getInstance().failedAt.remove(cacheKey);
});
card.emitPixmapUpdated(); card.emitPixmapUpdated();
} }

View file

@ -4,6 +4,8 @@
#include "card_picture_loader_status_bar.h" #include "card_picture_loader_status_bar.h"
#include "card_picture_loader_worker.h" #include "card_picture_loader_worker.h"
#include <QDateTime>
#include <QHash>
#include <QLoggingCategory> #include <QLoggingCategory>
inline Q_LOGGING_CATEGORY(CardPictureLoaderLog, "card_picture_loader"); inline Q_LOGGING_CATEGORY(CardPictureLoaderLog, "card_picture_loader");
@ -56,6 +58,7 @@ private:
CardPictureLoaderWorker *worker; ///< Worker thread for async image loading CardPictureLoaderWorker *worker; ///< Worker thread for async image loading
CardPictureLoaderStatusBar *statusBar; ///< Status bar widget showing load progress CardPictureLoaderStatusBar *statusBar; ///< Status bar widget showing load progress
QHash<QString, QDateTime> failedAt; ///< Timestamp of the last failed load attempt per pixmap cache key
public: public:
/** /**

View file

@ -11,15 +11,17 @@
#include <QNetworkReply> #include <QNetworkReply>
#include <QThread> #include <QThread>
#include <libcockatrice/settings/cache_storage_settings.h> #include <libcockatrice/settings/cache_storage_settings.h>
#include <libcockatrice/settings/download_settings.h>
#include <libcockatrice/settings/paths_settings.h> #include <libcockatrice/settings/paths_settings.h>
#include <libcockatrice/settings/personal_settings.h>
#include <utility> #include <utility>
#include <version_string.h> #include <version_string.h>
static constexpr int MAX_REQUESTS_PER_SEC = 10; 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() CardPictureLoaderWorker::CardPictureLoaderWorker()
: QObject(nullptr), picDownload(SettingsCache::instance().personal().getPicDownload()), : QObject(nullptr), picDownload(SettingsCache::instance().downloads().getPicDownload()),
requestQuota(MAX_REQUESTS_PER_SEC) requestQuota(MAX_REQUESTS_PER_SEC)
{ {
networkManager = new QNetworkAccessManager(this); networkManager = new QNetworkAccessManager(this);
@ -124,6 +126,19 @@ QNetworkReply *CardPictureLoaderWorker::makeRequest(const QUrl &url, CardPicture
void CardPictureLoaderWorker::resetRequestQuota() void CardPictureLoaderWorker::resetRequestQuota()
{ {
requestQuota = MAX_REQUESTS_PER_SEC; 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(); processQueuedRequests();
} }
@ -136,14 +151,26 @@ void CardPictureLoaderWorker::processQueuedRequests()
bool CardPictureLoaderWorker::processSingleRequest() bool CardPictureLoaderWorker::processSingleRequest()
{ {
if (!requestLoadQueue.isEmpty()) { for (int i = 0; i < requestLoadQueue.size(); ++i) {
auto request = requestLoadQueue.takeFirst(); const auto &request = requestLoadQueue.at(i);
makeRequest(request.first, request.second); QString host = request.first.host();
return true; 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; 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) void CardPictureLoaderWorker::enqueueImageLoad(const ExactCard &card)
{ {
// Send call through a connection to ensure the handling is run on the pictureLoader thread // Send call through a connection to ensure the handling is run on the pictureLoader thread

View file

@ -5,6 +5,8 @@
#include "card_picture_loader_worker_work.h" #include "card_picture_loader_worker_work.h"
#include "card_picture_to_load.h" #include "card_picture_to_load.h"
#include <QDateTime>
#include <QHash>
#include <QLoggingCategory> #include <QLoggingCategory>
#include <QMutex> #include <QMutex>
#include <QNetworkAccessManager> #include <QNetworkAccessManager>
@ -66,6 +68,12 @@ public:
*/ */
void queueRequest(const QUrl &url, CardPictureLoaderWorkerWork *worker); 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. */ /** @brief Clears the network cache and redirect cache. */
void clearNetworkCache(); void clearNetworkCache();
@ -110,8 +118,11 @@ private:
bool picDownload; ///< Whether downloading images from network is enabled bool picDownload; ///< Whether downloading images from network is enabled
QQueue<QPair<QUrl, CardPictureLoaderWorkerWork *>> requestLoadQueue; ///< Queue of pending network requests QQueue<QPair<QUrl, CardPictureLoaderWorkerWork *>> requestLoadQueue; ///< Queue of pending network requests
int requestQuota; ///< Remaining requests allowed per second int requestQuota; ///< Remaining requests allowed per second
QTimer requestTimer; ///< Timer to reset the request quota QTimer requestTimer; ///< Timer to reset the request quota
QHash<QString, int> hostRequestQuota; ///< Sustained per-host request allowance
QHash<QString, int> hostQuotaRemaining; ///< Per-host allowance left in the current second
QHash<QString, QDateTime> hostLast429; ///< When each host was last rate limited
CardPictureLoaderLocal *localLoader; ///< Loader for local images CardPictureLoaderLocal *localLoader; ///< Loader for local images
QSet<QString> currentlyLoading; ///< Deduplication: contains pixmapCacheKey currently being loaded QSet<QString> currentlyLoading; ///< Deduplication: contains pixmapCacheKey currently being loaded

View file

@ -8,9 +8,13 @@
#include <QLoggingCategory> #include <QLoggingCategory>
#include <QMovie> #include <QMovie>
#include <QNetworkReply> #include <QNetworkReply>
#include <QRandomGenerator>
#include <QThread> #include <QThread>
#include <QThreadPool> #include <QThreadPool>
#include <libcockatrice/settings/personal_settings.h> #include <QTimer>
ServerRateLimiter CardPictureLoaderWorkerWork::s_rateLimiter;
#include <libcockatrice/settings/download_settings.h>
// Card back returned by gatherer when card is not found // Card back returned by gatherer when card is not found
static const QStringList MD5_BLACKLIST = { static const QStringList MD5_BLACKLIST = {
@ -20,7 +24,7 @@ static const QStringList MD5_BLACKLIST = {
CardPictureLoaderWorkerWork::CardPictureLoaderWorkerWork(const CardPictureLoaderWorker *worker, const ExactCard &toLoad) CardPictureLoaderWorkerWork::CardPictureLoaderWorkerWork(const CardPictureLoaderWorker *worker, const ExactCard &toLoad)
: QObject(nullptr), cardToDownload(CardPictureToLoad(toLoad)), : QObject(nullptr), cardToDownload(CardPictureToLoad(toLoad)),
picDownload(SettingsCache::instance().personal().getPicDownload()) picDownload(SettingsCache::instance().downloads().getPicDownload())
{ {
// Hook up signals to the orchestrator // Hook up signals to the orchestrator
connect(this, &CardPictureLoaderWorkerWork::requestImageDownload, worker, &CardPictureLoaderWorker::queueRequest); 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::imageLoaded, worker, &CardPictureLoaderWorker::handleImageLoaded);
connect(this, &CardPictureLoaderWorkerWork::requestSucceeded, worker, connect(this, &CardPictureLoaderWorkerWork::requestSucceeded, worker,
&CardPictureLoaderWorker::imageRequestSucceeded); &CardPictureLoaderWorker::imageRequestSucceeded);
connect(this, &CardPictureLoaderWorkerWork::rateLimited, worker, &CardPictureLoaderWorker::onHostRateLimited);
// Hook up signals to settings // Hook up signals to settings
connect(&SettingsCache::instance().personal(), SIGNAL(picDownloadChanged()), this, SLOT(picDownloadChanged())); connect(&SettingsCache::instance().downloads(), SIGNAL(picDownloadChanged()), this, SLOT(picDownloadChanged()));
startNextPicDownload(); startNextPicDownload();
} }
void CardPictureLoaderWorkerWork::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(); QString picUrl = cardToDownload.getCurrentUrl();
if (picUrl.isEmpty()) { if (picUrl.isEmpty()) {
picDownloadFailed(); scheduleDeferredRetry();
} else { } else {
QUrl url(picUrl); QUrl url(picUrl);
qCDebug(CardPictureLoaderWorkerWorkLog).nospace() qCDebug(CardPictureLoaderWorkerWorkLog).nospace()
@ -108,7 +141,41 @@ static bool imageIsBlackListed(const QByteArray &picData)
void CardPictureLoaderWorkerWork::handleFailedReply(const QNetworkReply *reply) void CardPictureLoaderWorkerWork::handleFailedReply(const QNetworkReply *reply)
{ {
if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 429) { 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<qint64>(seconds) * 1000;
} else {
QDateTime retryAfterDate =
QDateTime::fromString(QString::fromLatin1(retryAfterHeader), Qt::RFC2822Date);
if (retryAfterDate.isValid()) {
retryAfterMs = qMax<qint64>(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 { } else {
bool isFromCache = reply->attribute(QNetworkRequest::SourceIsFromCacheAttribute).toBool(); bool isFromCache = reply->attribute(QNetworkRequest::SourceIsFromCacheAttribute).toBool();
@ -149,6 +216,9 @@ void CardPictureLoaderWorkerWork::handleSuccessfulReply(QNetworkReply *reply)
return; 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 // peek is used to keep the data in the buffer for use by QImageReader
const QByteArray &picData = reply->peek(reply->size()); const QByteArray &picData = reply->peek(reply->size());
@ -203,6 +273,42 @@ QImage CardPictureLoaderWorkerWork::tryLoadImageFromReply(QNetworkReply *reply)
return imgReader.read(); 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<qint64>(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) void CardPictureLoaderWorkerWork::concludeImageLoad(const QImage &image)
{ {
emit imageLoaded(cardToDownload.getCard(), image); emit imageLoaded(cardToDownload.getCard(), image);
@ -211,5 +317,5 @@ void CardPictureLoaderWorkerWork::concludeImageLoad(const QImage &image)
void CardPictureLoaderWorkerWork::picDownloadChanged() void CardPictureLoaderWorkerWork::picDownloadChanged()
{ {
picDownload = SettingsCache::instance().personal().getPicDownload(); picDownload = SettingsCache::instance().downloads().getPicDownload();
} }

View file

@ -4,12 +4,15 @@
#include "card_picture_loader_worker.h" #include "card_picture_loader_worker.h"
#include "card_picture_to_load.h" #include "card_picture_to_load.h"
#include <QDateTime>
#include <QLoggingCategory> #include <QLoggingCategory>
#include <QMutex> #include <QMutex>
#include <QNetworkAccessManager> #include <QNetworkAccessManager>
#include <QObject> #include <QObject>
#include <QRandomGenerator>
#include <QThread> #include <QThread>
#include <libcockatrice/card/database/card_database.h> #include <libcockatrice/card/database/card_database.h>
#include <libcockatrice/utility/server_rate_limiter.h>
inline Q_LOGGING_CATEGORY(CardPictureLoaderWorkerWorkLog, "card_picture_loader.worker"); inline Q_LOGGING_CATEGORY(CardPictureLoaderWorkerWorkLog, "card_picture_loader.worker");
@ -50,6 +53,8 @@ public slots:
private: private:
bool picDownload; ///< Whether network downloading is enabled 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. */ /** @brief Starts downloading the next URL for this card. */
void startNextPicDownload(); void startNextPicDownload();
@ -77,6 +82,16 @@ private:
*/ */
void concludeImageLoad(const QImage &image); 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: private slots:
/** @brief Updates the picDownload setting when it changes. */ /** @brief Updates the picDownload setting when it changes. */
void picDownloadChanged(); void picDownloadChanged();
@ -100,6 +115,9 @@ signals:
/** @brief Emitted when a URL has been redirected. */ /** @brief Emitted when a URL has been redirected. */
void urlRedirected(const QUrl &originalUrl, const QUrl &redirectUrl); 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. */ /** @brief Emitted when a cached URL is invalid and must be removed. */
void cachedUrlInvalidated(const QUrl &url); void cachedUrlInvalidated(const QUrl &url);
}; };

View file

@ -17,8 +17,9 @@ CardPictureToLoad::CardPictureToLoad(const ExactCard &_card)
{ {
if (card) { if (card) {
sortedSets = extractSetsSorted(card); sortedSets = extractSetsSorted(card);
// The first time called, nextSet will also populate the Urls for the first set. currentSetIndex = 0;
nextSet(); currentSet = sortedSets.first();
populateSetUrls();
} }
} }
@ -101,15 +102,19 @@ void CardPictureToLoad::populateSetUrls()
} }
} }
/* Call nextUrl to make sure currentUrl is up-to-date currentUrlIndex = 0;
but we don't need the result here. */ if (!currentSetUrls.isEmpty()) {
(void)nextUrl(); currentUrl = currentSetUrls.first();
} else {
currentUrl = QString();
}
} }
bool CardPictureToLoad::nextSet() bool CardPictureToLoad::nextSet()
{ {
if (!sortedSets.isEmpty()) { currentSetIndex++;
currentSet = sortedSets.takeFirst(); if (currentSetIndex < sortedSets.size()) {
currentSet = sortedSets.at(currentSetIndex);
populateSetUrls(); populateSetUrls();
return true; return true;
} }
@ -119,8 +124,9 @@ bool CardPictureToLoad::nextSet()
bool CardPictureToLoad::nextUrl() bool CardPictureToLoad::nextUrl()
{ {
if (!currentSetUrls.isEmpty()) { currentUrlIndex++;
currentUrl = currentSetUrls.takeFirst(); if (currentUrlIndex < currentSetUrls.size()) {
currentUrl = currentSetUrls.at(currentUrlIndex);
return true; return true;
} }
currentUrl = QString(); 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, static int parse(const QString &urlTemplate,
const QString &propType, const QString &propType,
const QString &cardName, const QString &cardName,

View file

@ -25,6 +25,8 @@ private:
QList<QString> currentSetUrls; ///< URLs for the current set being attempted QList<QString> currentSetUrls; ///< URLs for the current set being attempted
QString currentUrl; ///< Currently active URL to download QString currentUrl; ///< Currently active URL to download
CardSetPtr currentSet; ///< Currently active set CardSetPtr currentSet; ///< Currently active set
int currentSetIndex = 0; ///< Current position in sortedSets
int currentUrlIndex = 0; ///< Current position in currentSetUrls
public: public:
/** /**
@ -56,6 +58,9 @@ public:
/** @return The short name of the current set, or empty string if no set. */ /** @return The short name of the current set, or empty string if no set. */
[[nodiscard]] QString getSetName() const; [[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. * @brief Transforms a URL template into a concrete URL for this card/set.
* @param urlTemplate The URL template to transform * @param urlTemplate The URL template to transform
@ -88,6 +93,14 @@ public:
*/ */
void populateSetUrls(); 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. * @brief Extract all sets from the card and sort them by priority.
* @param card The card to extract sets from * @param card The card to extract sets from

View file

@ -17,7 +17,7 @@ class ManaCostWidget : public QWidget
public: public:
explicit ManaCostWidget(QWidget *parent, CardInfoPtr card); explicit ManaCostWidget(QWidget *parent, CardInfoPtr card);
QStringList parseManaCost(const QString &manaString); static QStringList parseManaCost(const QString &manaString);
public slots: public slots:
void resizeEvent(QResizeEvent *event) override; void resizeEvent(QResizeEvent *event) override;

View file

@ -52,12 +52,29 @@ void CardInfoPictureEnlargedWidget::loadPixmap(const QSize &size)
* @param size The desired size for the pixmap. * @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. * 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) void CardInfoPictureEnlargedWidget::setCardPixmap(const ExactCard &_card, const QSize size)
{ {
if (card.getCardPtr()) {
disconnect(card.getCardPtr().data(), nullptr, this, nullptr);
}
card = _card; card = _card;
// Clear any previous card's art so we never paint a stale pixmap while the new image loads
enlargedPixmap = QPixmap();
loadPixmap(size); 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 setFixedSize(size); // Set the widget size to the enlarged size
update(); // Trigger a repaint update(); // Trigger a repaint

View file

@ -2,6 +2,8 @@
#include "../../../client/settings/cache_settings.h" #include "../../../client/settings/cache_settings.h"
#include "../../../client/settings/shortcuts_settings.h" #include "../../../client/settings/shortcuts_settings.h"
#include "../settings_page/user_interface_settings_page.h"
#include "../tabs/api/commander_spellbook/commander_bracket_widget.h"
#include "deck_list_style_proxy.h" #include "deck_list_style_proxy.h"
#include "deck_state_manager.h" #include "deck_state_manager.h"
@ -12,7 +14,7 @@
#include <QSplitter> #include <QSplitter>
#include <QTextEdit> #include <QTextEdit>
#include <libcockatrice/card/database/card_database_manager.h> #include <libcockatrice/card/database/card_database_manager.h>
#include <libcockatrice/settings/cards_display_settings.h> #include <libcockatrice/settings/deck_editor_settings.h>
#include <libcockatrice/utility/macros.h> #include <libcockatrice/utility/macros.h>
#include <libcockatrice/utility/string_limits.h> #include <libcockatrice/utility/string_limits.h>
@ -111,20 +113,18 @@ void DeckEditorDeckDockWidget::createDeckDock()
showBannerCardCheckBox = new QCheckBox(); showBannerCardCheckBox = new QCheckBox();
showBannerCardCheckBox->setObjectName("showBannerCardCheckBox"); showBannerCardCheckBox->setObjectName("showBannerCardCheckBox");
showBannerCardCheckBox->setChecked( showBannerCardCheckBox->setChecked(SettingsCache::instance().deckEditor().getBannerCardComboBoxVisible());
SettingsCache::instance().cardsDisplay().getDeckEditorBannerCardComboBoxVisible()); connect(showBannerCardCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().deckEditor(),
connect(showBannerCardCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().cardsDisplay(), &DeckEditorSettings::setBannerCardComboBoxVisible);
&CardsDisplaySettings::setDeckEditorBannerCardComboBoxVisible); connect(&SettingsCache::instance().deckEditor(), &DeckEditorSettings::bannerCardComboBoxVisibleChanged, this,
connect(&SettingsCache::instance().cardsDisplay(),
&CardsDisplaySettings::deckEditorBannerCardComboBoxVisibleChanged, this,
&DeckEditorDeckDockWidget::updateShowBannerCardComboBox); &DeckEditorDeckDockWidget::updateShowBannerCardComboBox);
showTagsWidgetCheckBox = new QCheckBox(); showTagsWidgetCheckBox = new QCheckBox();
showTagsWidgetCheckBox->setObjectName("showTagsWidgetCheckBox"); showTagsWidgetCheckBox->setObjectName("showTagsWidgetCheckBox");
showTagsWidgetCheckBox->setChecked(SettingsCache::instance().cardsDisplay().getDeckEditorTagsWidgetVisible()); showTagsWidgetCheckBox->setChecked(SettingsCache::instance().deckEditor().getTagsWidgetVisible());
connect(showTagsWidgetCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().cardsDisplay(), connect(showTagsWidgetCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().deckEditor(),
&CardsDisplaySettings::setDeckEditorTagsWidgetVisible); &DeckEditorSettings::setTagsWidgetVisible);
connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::deckEditorTagsWidgetVisibleChanged, this, connect(&SettingsCache::instance().deckEditor(), &DeckEditorSettings::tagsWidgetVisibleChanged, this,
&DeckEditorDeckDockWidget::updateShowTagsWidget); &DeckEditorDeckDockWidget::updateShowTagsWidget);
quickSettingsWidget->addSettingsWidget(showBannerCardCheckBox); quickSettingsWidget->addSettingsWidget(showBannerCardCheckBox);
@ -136,6 +136,8 @@ void DeckEditorDeckDockWidget::createDeckDock()
formatComboBox->addItem(tr("Loading Database...")); formatComboBox->addItem(tr("Loading Database..."));
formatComboBox->setEnabled(false); // Disable until loaded formatComboBox->setEnabled(false); // Disable until loaded
commanderBracketWidget = new CommanderBracketWidget(this);
commentsLabel = new QLabel(); commentsLabel = new QLabel();
commentsLabel->setObjectName("commentsLabel"); commentsLabel->setObjectName("commentsLabel");
commentsEdit = new QTextEdit; commentsEdit = new QTextEdit;
@ -156,7 +158,7 @@ void DeckEditorDeckDockWidget::createDeckDock()
bannerCardLabel = new QLabel(); bannerCardLabel = new QLabel();
bannerCardLabel->setObjectName("bannerCardLabel"); bannerCardLabel->setObjectName("bannerCardLabel");
bannerCardLabel->setText(tr("Banner Card")); bannerCardLabel->setText(tr("Banner Card"));
bannerCardLabel->setHidden(!SettingsCache::instance().cardsDisplay().getDeckEditorBannerCardComboBoxVisible()); bannerCardLabel->setHidden(!SettingsCache::instance().deckEditor().getBannerCardComboBoxVisible());
bannerCardComboBox = new QComboBox(this); bannerCardComboBox = new QComboBox(this);
connect(getModel(), &DeckListModel::cardNodesChanged, this, [this]() { connect(getModel(), &DeckListModel::cardNodesChanged, this, [this]() {
// Delay the update to avoid race conditions // Delay the update to avoid race conditions
@ -167,10 +169,10 @@ void DeckEditorDeckDockWidget::createDeckDock()
connect(bannerCardComboBox, QOverload<int>::of(&QComboBox::currentIndexChanged), this, connect(bannerCardComboBox, QOverload<int>::of(&QComboBox::currentIndexChanged), this,
&DeckEditorDeckDockWidget::writeBannerCard); &DeckEditorDeckDockWidget::writeBannerCard);
bannerCardComboBox->setHidden(!SettingsCache::instance().cardsDisplay().getDeckEditorBannerCardComboBoxVisible()); bannerCardComboBox->setHidden(!SettingsCache::instance().deckEditor().getBannerCardComboBoxVisible());
deckTagsDisplayWidget = new DeckPreviewDeckTagsDisplayWidget(this, {}); deckTagsDisplayWidget = new DeckPreviewDeckTagsDisplayWidget(this, {});
deckTagsDisplayWidget->setHidden(!SettingsCache::instance().cardsDisplay().getDeckEditorTagsWidgetVisible()); deckTagsDisplayWidget->setHidden(!SettingsCache::instance().deckEditor().getTagsWidgetVisible());
connect(deckTagsDisplayWidget, &DeckPreviewDeckTagsDisplayWidget::tagsChanged, deckStateManager, connect(deckTagsDisplayWidget, &DeckPreviewDeckTagsDisplayWidget::tagsChanged, deckStateManager,
&DeckStateManager::setTags); &DeckStateManager::setTags);
@ -221,13 +223,15 @@ void DeckEditorDeckDockWidget::createDeckDock()
upperLayout->addWidget(formatLabel, 2, 0); upperLayout->addWidget(formatLabel, 2, 0);
upperLayout->addWidget(formatComboBox, 2, 1); upperLayout->addWidget(formatComboBox, 2, 1);
upperLayout->addWidget(bannerCardLabel, 3, 0); upperLayout->addWidget(commanderBracketWidget, 3, 0, 1, 2);
upperLayout->addWidget(bannerCardComboBox, 3, 1);
upperLayout->addWidget(deckTagsDisplayWidget, 4, 1); upperLayout->addWidget(bannerCardLabel, 4, 0);
upperLayout->addWidget(bannerCardComboBox, 4, 1);
upperLayout->addWidget(activeGroupCriteriaLabel, 5, 0); upperLayout->addWidget(deckTagsDisplayWidget, 5, 1);
upperLayout->addWidget(activeGroupCriteriaComboBox, 5, 1);
upperLayout->addWidget(activeGroupCriteriaLabel, 6, 0);
upperLayout->addWidget(activeGroupCriteriaComboBox, 6, 1);
hashLabel1 = new QLabel(); hashLabel1 = new QLabel();
hashLabel1->setObjectName("hashLabel1"); hashLabel1->setObjectName("hashLabel1");
@ -305,15 +309,19 @@ void DeckEditorDeckDockWidget::initializeFormats()
// Ensure no selection is visible initially // Ensure no selection is visible initially
formatComboBox->setCurrentIndex(-1); formatComboBox->setCurrentIndex(-1);
} }
connect(formatComboBox, QOverload<int>::of(&QComboBox::currentIndexChanged), this, [this](int index) { connect(formatComboBox, QOverload<int>::of(&QComboBox::currentIndexChanged), this, [this](int index) {
QString formatKey;
if (index >= 0) { if (index >= 0) {
QString formatKey = formatComboBox->itemData(index).toString(); formatKey = formatComboBox->itemData(index).toString();
deckStateManager->setFormat(formatKey); deckStateManager->setFormat(formatKey);
} else { } else {
deckStateManager->setFormat(""); // clear format if deselected deckStateManager->setFormat(""); // clear format if deselected
} }
commanderBracketWidget->setDeck(deckStateManager->getDeckListShared());
}); });
commanderBracketWidget->setDeck(deckStateManager->getDeckListShared());
} }
ExactCard DeckEditorDeckDockWidget::getCurrentCard() ExactCard DeckEditorDeckDockWidget::getCurrentCard()
@ -495,6 +503,8 @@ void DeckEditorDeckDockWidget::syncDisplayWidgetsToModel()
formatComboBox->setCurrentIndex(formatComboBox->findData(deckStateManager->getMetadata().gameFormat)); formatComboBox->setCurrentIndex(formatComboBox->findData(deckStateManager->getMetadata().gameFormat));
formatComboBox->blockSignals(false); formatComboBox->blockSignals(false);
commanderBracketWidget->setDeck(deckStateManager->getDeckListShared());
deckTagsDisplayWidget->blockSignals(true); deckTagsDisplayWidget->blockSignals(true);
deckTagsDisplayWidget->setTags(deckStateManager->getMetadata().tags); deckTagsDisplayWidget->setTags(deckStateManager->getMetadata().tags);
deckTagsDisplayWidget->blockSignals(false); deckTagsDisplayWidget->blockSignals(false);
@ -748,6 +758,7 @@ void DeckEditorDeckDockWidget::retranslateUi()
commentsLabel->setText(tr("&Comments:")); commentsLabel->setText(tr("&Comments:"));
activeGroupCriteriaLabel->setText(tr("Group by:")); activeGroupCriteriaLabel->setText(tr("Group by:"));
formatLabel->setText(tr("Format:")); formatLabel->setText(tr("Format:"));
commanderBracketWidget->retranslateUi();
hashLabel1->setText(tr("Hash:")); hashLabel1->setText(tr("Hash:"));

View file

@ -21,6 +21,7 @@
#include <QTreeView> #include <QTreeView>
#include <libcockatrice/card/card_info.h> #include <libcockatrice/card/card_info.h>
class CommanderBracketWidget;
class DeckListModel; class DeckListModel;
class AbstractTabDeckEditor; class AbstractTabDeckEditor;
class DeckEditorDeckDockWidget : public QDockWidget class DeckEditorDeckDockWidget : public QDockWidget
@ -89,6 +90,8 @@ private:
QAction *aRemoveCard, *aIncrement, *aDecrement, *aSwapCard; QAction *aRemoveCard, *aIncrement, *aDecrement, *aSwapCard;
CommanderBracketWidget *commanderBracketWidget;
DeckListModel *getModel() const; DeckListModel *getModel() const;
[[nodiscard]] QModelIndexList getSelectedCardNodeSourceIndices() const; [[nodiscard]] QModelIndexList getSelectedCardNodeSourceIndices() const;
void offsetCountAtIndex(const QModelIndex &idx, bool isIncrement); void offsetCountAtIndex(const QModelIndex &idx, bool isIncrement);

View file

@ -57,6 +57,14 @@ public:
*/ */
const DeckList &getDeckList() const; const DeckList &getDeckList() const;
/**
* @brief Gets the underlying DeckList.
*/
QSharedPointer<DeckList> getDeckListShared() const
{
return deckList;
}
/** /**
* @brief Creates a LoadedDeck containing the contents of the current deck and the current LoadInfo. * @brief Creates a LoadedDeck containing the contents of the current deck and the current LoadInfo.
* *

View file

@ -214,6 +214,7 @@ DlgCreateGame::DlgCreateGame(const ServerInfo_Game &gameInfo, const QMap<int, QS
spectatorsNeedPasswordCheckBox->setChecked(gameInfo.spectators_need_password()); spectatorsNeedPasswordCheckBox->setChecked(gameInfo.spectators_need_password());
spectatorsCanTalkCheckBox->setChecked(gameInfo.spectators_can_chat()); spectatorsCanTalkCheckBox->setChecked(gameInfo.spectators_can_chat());
spectatorsSeeEverythingCheckBox->setChecked(gameInfo.spectators_omniscient()); spectatorsSeeEverythingCheckBox->setChecked(gameInfo.spectators_omniscient());
shareDecklistsOnLoadCheckBox->setChecked(gameInfo.share_decklists_on_load());
QSet<int> types; QSet<int> types;
for (int i = 0; i < gameInfo.game_types_size(); ++i) { for (int i = 0; i < gameInfo.game_types_size(); ++i) {

View file

@ -30,7 +30,7 @@ DlgFilterGames::DlgFilterGames(const QMap<int, QString> &_allGameTypes,
hideFullGames = new QCheckBox(tr("Hide full games")); hideFullGames = new QCheckBox(tr("Hide full games"));
hideFullGames->setChecked(filters.hideFullGames); hideFullGames->setChecked(filters.hideFullGames);
hideGamesThatStarted = new QCheckBox(tr("Hide games that have started")); hideGamesThatStarted = new QCheckBox(tr("Hide started games"));
hideGamesThatStarted->setChecked(filters.hideGamesThatStarted); hideGamesThatStarted->setChecked(filters.hideGamesThatStarted);
hidePasswordProtectedGames = new QCheckBox(tr("Hide password protected games")); hidePasswordProtectedGames = new QCheckBox(tr("Hide password protected games"));

View file

@ -4,7 +4,6 @@
#include "../../../client/settings/shortcuts_settings.h" #include "../../../client/settings/shortcuts_settings.h"
#include "../../deck_loader/card_node_function.h" #include "../../deck_loader/card_node_function.h"
#include "../../deck_loader/deck_loader.h" #include "../../deck_loader/deck_loader.h"
#include "dlg_settings.h"
#include <QApplication> #include <QApplication>
#include <QCheckBox> #include <QCheckBox>

View file

@ -1,3 +1,8 @@
/**
* @file dlg_settings.cpp
* @brief Implementation of the main settings dialog
* @ingroup Dialogs
*/
#include "dlg_settings.h" #include "dlg_settings.h"
#include "../../../client/settings/cache_settings.h" #include "../../../client/settings/cache_settings.h"
@ -6,6 +11,8 @@
#include "../settings_page/deck_editor_settings_page.h" #include "../settings_page/deck_editor_settings_page.h"
#include "../settings_page/general_settings_page.h" #include "../settings_page/general_settings_page.h"
#include "../settings_page/messages_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/shortcut_settings_page.h"
#include "../settings_page/sound_settings_page.h" #include "../settings_page/sound_settings_page.h"
#include "../settings_page/storage_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_loader.h"
#include "libcockatrice/card/database/card_database_manager.h" #include "libcockatrice/card/database/card_database_manager.h"
#include <QAbstractItemView>
#include <QCloseEvent> #include <QCloseEvent>
#include <QDialogButtonBox>
#include <QDir> #include <QDir>
#include <QFrame>
#include <QGraphicsOpacityEffect>
#include <QGuiApplication> #include <QGuiApplication>
#include <QListWidget> #include <QHBoxLayout>
#include <QKeyEvent>
#include <QLineEdit>
#include <QListView>
#include <QMessageBox> #include <QMessageBox>
#include <QPropertyAnimation>
#include <QPushButton>
#include <QScreen> #include <QScreen>
#include <QScrollArea> #include <QScrollArea>
#include <QScrollBar> #include <QScrollBar>
#include <QSequentialAnimationGroup>
#include <QShortcut>
#include <QStackedLayout>
#include <QStackedWidget> #include <QStackedWidget>
#include <QToolButton>
#include <QVBoxLayout> #include <QVBoxLayout>
#include <libcockatrice/settings/paths_settings.h> #include <libcockatrice/settings/paths_settings.h>
#include <libcockatrice/settings/personal_settings.h> #include <libcockatrice/settings/personal_settings.h>
/**
* @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) static QScrollArea *makeScrollable(QWidget *widget)
{ {
widget->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Maximum); widget->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Maximum);
@ -40,112 +63,355 @@ static QScrollArea *makeScrollable(QWidget *widget)
return scrollArea; 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(); 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); connect(&SettingsCache::instance().personal(), &PersonalSettings::langChanged, this, &DlgSettings::updateLanguage);
contentsWidget = new QListWidget; setupUi();
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);
connect(&SettingsCache::instance().personal(), &PersonalSettings::langChanged, this, &DlgSettings::retranslateUi); connect(&SettingsCache::instance().personal(), &PersonalSettings::langChanged, this, &DlgSettings::retranslateUi);
retranslateUi(); retranslateUi();
searchEdit->setFocus();
adjustSize(); adjustSize();
} }
void DlgSettings::createIcons() void DlgSettings::setupUi()
{ {
generalButton = new QListWidgetItem(contentsWidget); // Search bar
generalButton->setTextAlignment(Qt::AlignHCenter); searchEdit = new QLineEdit;
generalButton->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); searchEdit->setClearButtonEnabled(true);
generalButton->setIcon(QPixmap("theme:config/general")); searchEdit->addAction(QPixmap("theme:icons/search"), QLineEdit::LeadingPosition);
searchEdit->installEventFilter(this);
connect(searchEdit, &QLineEdit::textChanged, this, &DlgSettings::onSearchTextChanged);
appearanceButton = new QListWidgetItem(contentsWidget); auto *searchLayout = new QHBoxLayout;
appearanceButton->setTextAlignment(Qt::AlignHCenter); searchLayout->addWidget(searchEdit);
appearanceButton->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled);
appearanceButton->setIcon(QPixmap("theme:config/appearance"));
userInterfaceButton = new QListWidgetItem(contentsWidget); // Tab bar (built in setupTabBar)
userInterfaceButton->setTextAlignment(Qt::AlignHCenter); setupTabBar();
userInterfaceButton->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled);
userInterfaceButton->setIcon(QPixmap("theme:config/interface"));
deckEditorButton = new QListWidgetItem(contentsWidget); // Pages stacked widget
deckEditorButton->setTextAlignment(Qt::AlignHCenter); pagesWidget = new QStackedWidget;
deckEditorButton->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled);
deckEditorButton->setIcon(QPixmap("theme:config/deckeditor"));
storageButton = new QListWidgetItem(contentsWidget); auto *generalPage = new GeneralSettingsPage;
storageButton->setTextAlignment(Qt::AlignHCenter); auto *appearancePage = new AppearanceSettingsPage;
storageButton->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); auto *userInterfacePage = new UserInterfaceSettingsPage;
storageButton->setIcon(QPixmap("theme:config/storage")); auto *deckEditorPage = new DeckEditorSettingsPage;
auto *storagePage = new StorageSettingsPage;
auto *messagesPage = new MessagesSettingsPage;
auto *soundPage = new SoundSettingsPage;
auto *shortcutsPage = new ShortcutSettingsPage;
messagesButton = new QListWidgetItem(contentsWidget); pages.append(generalPage);
messagesButton->setTextAlignment(Qt::AlignHCenter); pages.append(appearancePage);
messagesButton->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); pages.append(userInterfacePage);
messagesButton->setIcon(QPixmap("theme:config/messages")); pages.append(deckEditorPage);
pages.append(storagePage);
pages.append(messagesPage);
pages.append(soundPage);
pages.append(shortcutsPage);
soundButton = new QListWidgetItem(contentsWidget); pagesWidget->addWidget(makeScrollable(generalPage));
soundButton->setTextAlignment(Qt::AlignHCenter); pagesWidget->addWidget(makeScrollable(appearancePage));
soundButton->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); pagesWidget->addWidget(makeScrollable(userInterfacePage));
soundButton->setIcon(QPixmap("theme:config/sound")); pagesWidget->addWidget(makeScrollable(deckEditorPage));
pagesWidget->addWidget(makeScrollable(storagePage));
pagesWidget->addWidget(messagesPage);
pagesWidget->addWidget(soundPage);
pagesWidget->addWidget(shortcutsPage);
shortcutsButton = new QListWidgetItem(contentsWidget); Q_ASSERT(pages.size() == NumPages);
shortcutsButton->setTextAlignment(Qt::AlignHCenter);
shortcutsButton->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled);
shortcutsButton->setIcon(QPixmap("theme:config/shorcuts"));
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) { tabBarWidget = new QWidget;
current = previous; 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<SettingsSearchEntry> allEntries;
const QStringList pageNames = translatedPageNames();
searchDelegate->setPageNames(pageNames);
searchDelegate->setPageIcons(pageIconResources());
for (int i = 0; i < pages.size(); ++i) {
QList<SettingsSearchEntry> 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<QScrollArea *>(widget)) {
scrollArea->ensureWidgetVisible(entry.widget);
break;
}
widget = widget->parentWidget();
}
entry.widget->setFocus();
flashWidget(entry.widget);
}
} }
void DlgSettings::setTab(int index) void DlgSettings::setTab(int index)
{ {
if (index <= contentsWidget->count() - 1 && index >= 0) { if (index >= 0 && index < tabButtons.size()) {
changePage(contentsWidget->item(index), contentsWidget->currentItem()); setActiveTab(index);
contentsWidget->setCurrentRow(index);
} }
} }
@ -155,6 +421,49 @@ void DlgSettings::updateLanguage()
installNewTranslator(); installNewTranslator();
} }
bool DlgSettings::eventFilter(QObject *watched, QEvent *event)
{
if (watched == searchEdit && event->type() == QEvent::KeyPress) {
auto *keyEvent = static_cast<QKeyEvent *>(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) void DlgSettings::closeEvent(QCloseEvent *event)
{ {
bool showLoadError = true; bool showLoadError = true;
@ -209,7 +518,6 @@ void DlgSettings::closeEvent(QCloseEvent *event)
if (!QDir(SettingsCache::instance().paths().getDeckPath()).exists() || if (!QDir(SettingsCache::instance().paths().getDeckPath()).exists() ||
SettingsCache::instance().paths().getDeckPath().isEmpty()) { SettingsCache::instance().paths().getDeckPath().isEmpty()) {
//! \todo Prompt to create the deck directory.
if (QMessageBox::critical( if (QMessageBox::critical(
this, tr("Error"), this, tr("Error"),
tr("The path to your deck directory is invalid. Would you like to go back and set the correct path?"), 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() || if (!QDir(SettingsCache::instance().paths().getPicsPath()).exists() ||
SettingsCache::instance().paths().getPicsPath().isEmpty()) { SettingsCache::instance().paths().getPicsPath().isEmpty()) {
//! \todo Prompt to create the pictures directory.
if (QMessageBox::critical(this, tr("Error"), if (QMessageBox::critical(this, tr("Error"),
tr("The path to your card pictures directory is invalid. Would you like to go back " tr("The path to your card pictures directory is invalid. Would you like to go back "
"and set the correct path?"), "and set the correct path?"),
@ -236,15 +543,26 @@ void DlgSettings::closeEvent(QCloseEvent *event)
void DlgSettings::retranslateUi() void DlgSettings::retranslateUi()
{ {
setWindowTitle(tr("Settings")); setWindowTitle(tr("Settings"));
retranslateTabNames();
generalButton->setText(tr("General")); searchEdit->setPlaceholderText(tr("Search settings..."));
appearanceButton->setText(tr("Appearance")); okButton->setText(tr("OK"));
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"));
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]);
}
} }

View file

@ -1,43 +1,99 @@
/** /**
* @file dlg_settings.h * @file dlg_settings.h
* @brief Main settings dialog for the Cockatrice client
* @ingroup Dialogs * @ingroup Dialogs
*/ */
//! \todo Document this file.
#ifndef DLG_SETTINGS_H #ifndef DLG_SETTINGS_H
#define DLG_SETTINGS_H #define DLG_SETTINGS_H
#include <QComboBox>
#include <QDialog> #include <QDialog>
#include <QLoggingCategory> #include <QLoggingCategory>
class QPushButton;
inline Q_LOGGING_CATEGORY(DlgSettingsLog, "dlg_settings"); inline Q_LOGGING_CATEGORY(DlgSettingsLog, "dlg_settings");
class QListWidget;
class QStackedWidget; 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 class DlgSettings : public QDialog
{ {
Q_OBJECT Q_OBJECT
public: 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); explicit DlgSettings(QWidget *parent = nullptr);
void setTab(int index); void setTab(int index);
private slots: private slots:
void changePage(QListWidgetItem *current, QListWidgetItem *previous); void onTabClicked(int index);
void onSearchTextChanged(const QString &text);
void onSearchResultClicked(const QModelIndex &index);
void updateLanguage(); void updateLanguage();
private: private:
QListWidget *contentsWidget; // UI elements
QStackedWidget *pagesWidget; QLineEdit *searchEdit; ///< Search bar for filtering settings
QListWidgetItem *generalButton, *appearanceButton, *userInterfaceButton, *deckEditorButton, *storageButton, QWidget *tabBarWidget; ///< Container widget for the tab buttons
*messagesButton, *soundButton, *shortcutsButton; QList<QToolButton *> tabButtons; ///< Navigation tab buttons
void createIcons(); 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<AbstractSettingsPage *> 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 retranslateUi();
void retranslateTabNames();
protected: protected:
void closeEvent(QCloseEvent *event) override; void closeEvent(QCloseEvent *event) override;
bool eventFilter(QObject *watched, QEvent *event) override;
}; };
#endif #endif // DLG_SETTINGS_H

View file

@ -14,8 +14,8 @@
#include <QVBoxLayout> #include <QVBoxLayout>
#include <libcockatrice/card/database/card_database_manager.h> #include <libcockatrice/card/database/card_database_manager.h>
#include <libcockatrice/network/client/remote/remote_client.h> #include <libcockatrice/network/client/remote/remote_client.h>
#include <libcockatrice/settings/appearance_settings.h>
#include <libcockatrice/settings/paths_settings.h> #include <libcockatrice/settings/paths_settings.h>
#include <libcockatrice/settings/personal_settings.h>
HomeWidget::HomeWidget(QWidget *parent, TabSupervisor *_tabSupervisor) HomeWidget::HomeWidget(QWidget *parent, TabSupervisor *_tabSupervisor)
: QWidget(parent), tabSupervisor(_tabSupervisor), background("theme:backgrounds/home"), overlay("theme:cockatrice") : QWidget(parent), tabSupervisor(_tabSupervisor), background("theme:backgrounds/home"), overlay("theme:cockatrice")
@ -43,12 +43,12 @@ HomeWidget::HomeWidget(QWidget *parent, TabSupervisor *_tabSupervisor)
updateConnectButton(tabSupervisor->getClient()->getStatus()); updateConnectButton(tabSupervisor->getClient()->getStatus());
connect(tabSupervisor->getClient(), &RemoteClient::statusChanged, this, &HomeWidget::updateConnectButton); connect(tabSupervisor->getClient(), &RemoteClient::statusChanged, this, &HomeWidget::updateConnectButton);
connect(&SettingsCache::instance().personal(), &PersonalSettings::homeTabBackgroundSourceChanged, this, connect(&SettingsCache::instance().appearance(), &AppearanceSettings::homeTabBackgroundSourceChanged, this,
&HomeWidget::initializeBackgroundFromSource); &HomeWidget::initializeBackgroundFromSource);
connect(&SettingsCache::instance().personal(), &PersonalSettings::homeTabBackgroundShuffleFrequencyChanged, this, connect(&SettingsCache::instance().appearance(), &AppearanceSettings::homeTabBackgroundShuffleFrequencyChanged,
&HomeWidget::onBackgroundShuffleFrequencyChanged); this, &HomeWidget::onBackgroundShuffleFrequencyChanged);
// Lambda is cleaner to read than overloading this // Lambda is cleaner to read than overloading this
connect(&SettingsCache::instance().personal(), &PersonalSettings::homeTabDisplayCardNameChanged, this, connect(&SettingsCache::instance().appearance(), &AppearanceSettings::homeTabDisplayCardNameChanged, this,
[this] { repaint(); }); [this] { repaint(); });
connect(&SettingsCache::instance(), &SettingsCache::themeChanged, this, connect(&SettingsCache::instance(), &SettingsCache::themeChanged, this,
&HomeWidget::initializeBackgroundFromSource); &HomeWidget::initializeBackgroundFromSource);
@ -65,7 +65,7 @@ void HomeWidget::initializeBackgroundFromSource()
} }
auto backgroundSourceType = auto backgroundSourceType =
BackgroundSources::fromId(SettingsCache::instance().personal().getHomeTabBackgroundSource()); BackgroundSources::fromId(SettingsCache::instance().appearance().getHomeTabBackgroundSource());
switch (backgroundSourceType) { switch (backgroundSourceType) {
case BackgroundSources::Theme: case BackgroundSources::Theme:
@ -113,7 +113,7 @@ void HomeWidget::setRandomCard(ExactCard &newCard)
void HomeWidget::updateRandomCard() void HomeWidget::updateRandomCard()
{ {
auto backgroundSourceType = auto backgroundSourceType =
BackgroundSources::fromId(SettingsCache::instance().personal().getHomeTabBackgroundSource()); BackgroundSources::fromId(SettingsCache::instance().appearance().getHomeTabBackgroundSource());
ExactCard newCard; ExactCard newCard;
@ -156,8 +156,8 @@ void HomeWidget::updateRandomCard()
void HomeWidget::onBackgroundShuffleFrequencyChanged() void HomeWidget::onBackgroundShuffleFrequencyChanged()
{ {
cardChangeTimer->stop(); cardChangeTimer->stop();
if (SettingsCache::instance().personal().getHomeTabBackgroundShuffleFrequency() > 0) { if (SettingsCache::instance().appearance().getHomeTabBackgroundShuffleFrequency() > 0) {
cardChangeTimer->start(SettingsCache::instance().personal().getHomeTabBackgroundShuffleFrequency() * 1000); cardChangeTimer->start(SettingsCache::instance().appearance().getHomeTabBackgroundShuffleFrequency() * 1000);
} }
} }
@ -265,7 +265,7 @@ void HomeWidget::updateConnectButton(const ClientStatus status)
QPair<QColor, QColor> HomeWidget::extractDominantColors(const QPixmap &pixmap) QPair<QColor, QColor> HomeWidget::extractDominantColors(const QPixmap &pixmap)
{ {
if (themeManager->isBuiltInTheme() && SettingsCache::instance().personal().getHomeTabBackgroundSource() == if (themeManager->isBuiltInTheme() && SettingsCache::instance().appearance().getHomeTabBackgroundSource() ==
BackgroundSources::toId(BackgroundSources::Theme)) { BackgroundSources::toId(BackgroundSources::Theme)) {
return QPair<QColor, QColor>(QColor::fromRgb(20, 140, 60), QColor::fromRgb(120, 200, 80)); return QPair<QColor, QColor>(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(); QFont font = painter.font();
font.setPointSize(14); font.setPointSize(14);
font.setBold(true); font.setBold(true);

View file

@ -16,16 +16,16 @@ class TearOffMenu : public QMenu
public: public:
explicit TearOffMenu(const QString &title, QWidget *parent = nullptr) : QMenu(title, parent) 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); }); [this](const bool state) { setTearOffEnabled(state); });
setTearOffEnabled(SettingsCache::instance().interface().getUseTearOffMenus()); setTearOffEnabled(SettingsCache::instance().userInterface().getUseTearOffMenus());
} }
explicit TearOffMenu(QWidget *parent = nullptr) : QMenu(parent) 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); }); [this](const bool state) { setTearOffEnabled(state); });
setTearOffEnabled(SettingsCache::instance().interface().getUseTearOffMenus()); setTearOffEnabled(SettingsCache::instance().userInterface().getUseTearOffMenus());
} }
TearOffMenu *addTearOffMenu(const QString &title) TearOffMenu *addTearOffMenu(const QString &title)

View file

@ -3,9 +3,11 @@
#include "../../../client/settings/cache_settings.h" #include "../../../client/settings/cache_settings.h"
#include <QTimer> #include <QTimer>
#include <libcockatrice/protocol/get_pb_extension.h>
#include <libcockatrice/settings/interface_settings.h> #include <libcockatrice/settings/interface_settings.h>
static constexpr int TIMER_INTERVAL_MS = 200; static constexpr int TIMER_INTERVAL_MS = 200;
static constexpr int EMPTY_SECTION_MARGIN_MS = 500;
static QList<int> createReplayTimeline(const GameReplay *replay) static QList<int> createReplayTimeline(const GameReplay *replay)
{ {
@ -94,7 +96,7 @@ void ReplayManager::handleBackwardsSkip(bool doRewindBuffering)
// The rewind only happens once the timer runs out. // The rewind only happens once the timer runs out.
// If another backwards skip happens, the timer will just get reset instead of rewinding. // If another backwards skip happens, the timer will just get reset instead of rewinding.
rewindBufferingTimer->stop(); rewindBufferingTimer->stop();
rewindBufferingTimer->start(SettingsCache::instance().interface().getRewindBufferingMs()); rewindBufferingTimer->start(SettingsCache::instance().userInterface().getRewindBufferingMs());
} else { } else {
// otherwise, process the rewind immediately // otherwise, process the rewind immediately
processRewind(); processRewind();
@ -119,6 +121,10 @@ void ReplayManager::replayTimerTimeout()
processNewEvents(NORMAL_PLAYBACK); processNewEvents(NORMAL_PLAYBACK);
timeChanged(currentVisualTime); timeChanged(currentVisualTime);
if (skipEmptySections) {
handleSkipEmptySection();
}
} }
/** @brief Processes all unprocessed events up to the current time. */ /** @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<GameEvent::GameEventType>(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) void ReplayManager::setTimeScaleFactor(qreal _timeScaleFactor)
{ {
timeScaleFactor = _timeScaleFactor; timeScaleFactor = _timeScaleFactor;
@ -156,6 +207,11 @@ void ReplayManager::setTimeScaleFactor(qreal _timeScaleFactor)
replayTimer->setInterval(interval); replayTimer->setInterval(interval);
} }
void ReplayManager::setSkipEmptySections(bool value)
{
skipEmptySections = value;
}
void ReplayManager::startReplay() void ReplayManager::startReplay()
{ {
replayTimer->start(); replayTimer->start();

View file

@ -31,6 +31,7 @@ class ReplayManager : public QObject
QTimer *rewindBufferingTimer; QTimer *rewindBufferingTimer;
qreal timeScaleFactor = 1.0; qreal timeScaleFactor = 1.0;
bool skipEmptySections = false;
int currentVisualTime = 0; ///< time currently displayed by the timeline 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 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 handleBackwardsSkip(bool doRewindBuffering);
void processRewind(); void processRewind();
void processNewEvents(PlaybackMode playbackMode); void processNewEvents(PlaybackMode playbackMode);
void handleSkipEmptySection();
private slots: private slots:
void replayTimerTimeout(); void replayTimerTimeout();
@ -63,6 +65,7 @@ public:
} }
void setTimeScaleFactor(qreal _timeScaleFactor); void setTimeScaleFactor(qreal _timeScaleFactor);
void setSkipEmptySections(bool value);
public slots: public slots:
void startReplay(); void startReplay();

View file

@ -14,16 +14,21 @@ ReplayQuickSettingsWidget::ReplayQuickSettingsWidget(QWidget *parent) : Settings
fastForwardSpeedBox.setMinimum(1); fastForwardSpeedBox.setMinimum(1);
fastForwardSpeedBox.setMaximum(99.9); fastForwardSpeedBox.setMaximum(99.9);
fastForwardSpeedBox.setDecimals(1); fastForwardSpeedBox.setDecimals(1);
fastForwardSpeedBox.setValue(SettingsCache::instance().interface().getFastForwardSpeed()); fastForwardSpeedBox.setValue(SettingsCache::instance().userInterface().getFastForwardSpeed());
connect(&fastForwardSpeedBox, qOverload<double>(&QDoubleSpinBox::valueChanged), this, connect(&fastForwardSpeedBox, qOverload<double>(&QDoubleSpinBox::valueChanged), this,
&ReplayQuickSettingsWidget::actUpdateFastForwardSpeed); &ReplayQuickSettingsWidget::actUpdateFastForwardSpeed);
skipEmptyCheckBox.setChecked(SettingsCache::instance().userInterface().getSkipEmptySections());
connect(&skipEmptyCheckBox, &QCheckBox::QT_STATE_CHANGED, this,
&ReplayQuickSettingsWidget::actUpdateSkipEmptySections);
// putting it all together // putting it all together
auto *widget = new QWidget; auto *widget = new QWidget;
auto *grid = new QGridLayout(widget); auto *grid = new QGridLayout(widget);
grid->setContentsMargins(0, 0, 0, 0); grid->setContentsMargins(0, 0, 0, 0);
grid->addWidget(&fastForwardSpeedLabel, 0, 0, 1, 1); grid->addWidget(&fastForwardSpeedLabel, 0, 0, 1, 1);
grid->addWidget(&fastForwardSpeedBox, 0, 1, 1, 1); grid->addWidget(&fastForwardSpeedBox, 0, 1, 1, 1);
grid->addWidget(&skipEmptyCheckBox, 1, 0, 1, 2);
this->addSettingsWidget(widget); this->addSettingsWidget(widget);
@ -36,10 +41,18 @@ void ReplayQuickSettingsWidget::retranslateUi()
{ {
fastForwardSpeedLabel.setText(tr("Fast forward speed:")); fastForwardSpeedLabel.setText(tr("Fast forward speed:"));
fastForwardSpeedBox.setSuffix("x"); fastForwardSpeedBox.setSuffix("x");
skipEmptyCheckBox.setText(tr("Skip empty sections"));
} }
void ReplayQuickSettingsWidget::actUpdateFastForwardSpeed(qreal value) void ReplayQuickSettingsWidget::actUpdateFastForwardSpeed(qreal value)
{ {
SettingsCache::instance().interface().setFastForwardSpeed(value); SettingsCache::instance().userInterface().setFastForwardSpeed(value);
emit fastForwardSpeedChanged(value); emit fastForwardSpeedChanged(value);
} }
void ReplayQuickSettingsWidget::actUpdateSkipEmptySections(QT_STATE_CHANGED_T value)
{
SettingsCache::instance().userInterface().setSkipEmptySections(value);
emit skipEmptySectionsChanged(value);
}

View file

@ -3,7 +3,9 @@
#include "../../interface/widgets/quick_settings/settings_button_widget.h" #include "../../interface/widgets/quick_settings/settings_button_widget.h"
#include <QCheckBox>
#include <QDoubleSpinBox> #include <QDoubleSpinBox>
#include <libcockatrice/utility/macros.h>
class ReplayQuickSettingsWidget : public SettingsButtonWidget class ReplayQuickSettingsWidget : public SettingsButtonWidget
{ {
@ -16,13 +18,17 @@ public:
signals: signals:
void fastForwardSpeedChanged(qreal speed); void fastForwardSpeedChanged(qreal speed);
void skipEmptySectionsChanged(bool skip);
private: private:
QLabel fastForwardSpeedLabel; QLabel fastForwardSpeedLabel;
QDoubleSpinBox fastForwardSpeedBox; QDoubleSpinBox fastForwardSpeedBox;
QCheckBox skipEmptyCheckBox;
private slots: private slots:
void actUpdateFastForwardSpeed(qreal value); void actUpdateFastForwardSpeed(qreal value);
void actUpdateSkipEmptySections(QT_STATE_CHANGED_T value);
}; };
#endif // COCKATRICE_REPLAY_QUICK_SETTINGS_WIDGET_H #endif // COCKATRICE_REPLAY_QUICK_SETTINGS_WIDGET_H

View file

@ -66,6 +66,10 @@ ReplayWidget::ReplayWidget(QWidget *parent, GameReplay *replay)
settingsWidget->setFixedSize(QSize(32, 32)); settingsWidget->setFixedSize(QSize(32, 32));
connect(settingsWidget, &ReplayQuickSettingsWidget::fastForwardSpeedChanged, this, connect(settingsWidget, &ReplayQuickSettingsWidget::fastForwardSpeedChanged, this,
[this] { updateTimeScaleFactor(replayFastForwardButton->isChecked()); }); [this] { updateTimeScaleFactor(replayFastForwardButton->isChecked()); });
connect(settingsWidget, &ReplayQuickSettingsWidget::skipEmptySectionsChanged, replayManager,
&ReplayManager::setSkipEmptySections);
replayManager->setSkipEmptySections(SettingsCache::instance().userInterface().getSkipEmptySections());
// putting everything together // putting everything together
auto replayControlLayout = new QHBoxLayout; auto replayControlLayout = new QHBoxLayout;
@ -98,7 +102,7 @@ void ReplayWidget::replayPlayButtonToggled(bool checked)
void ReplayWidget::updateTimeScaleFactor(bool isFastForward) 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); replayManager->setTimeScaleFactor(factor);
} }

View file

@ -14,6 +14,7 @@
#include <QMouseEvent> #include <QMouseEvent>
#include <QScrollBar> #include <QScrollBar>
#include <QTimer> #include <QTimer>
#include <libcockatrice/card/database/card_database_manager.h>
#include <libcockatrice/network/server/remote/user_level.h> #include <libcockatrice/network/server/remote/user_level.h>
#include <libcockatrice/settings/chat_settings.h> #include <libcockatrice/settings/chat_settings.h>
@ -66,12 +67,14 @@ void ChatView::adjustColorsToPalette()
serverMessageColor = QColor(0xFF, 0x73, 0x83); serverMessageColor = QColor(0xFF, 0x73, 0x83);
otherUserColor = otherUserColor.lighter(150); otherUserColor = otherUserColor.lighter(150);
linkColor = QColor(71, 158, 252); linkColor = QColor(71, 158, 252);
unresolvedCardTagColor = QColor(0xFF, 0xA5, 0x00);
} else { } else {
document()->setDefaultStyleSheet(R"( document()->setDefaultStyleSheet(R"(
a { text-decoration: none; color: blue; } a { text-decoration: none; color: blue; }
.blue { color: blue } .blue { color: blue }
)"); )");
linkColor = palette().link().color(); linkColor = palette().link().color();
unresolvedCardTagColor = QColor(0xA0, 0x52, 0x2D);
} }
QTimer::singleShot(0, this, &ChatView::refreshBlockColors); 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) void ChatView::appendCardTag(QTextCursor &cursor, const QString &cardName)
{ {
QTextCharFormat oldFormat = cursor.charFormat(); QTextCharFormat oldFormat = cursor.charFormat();
QTextCharFormat anchorFormat = oldFormat; QTextCharFormat cardFormat = oldFormat;
anchorFormat.setForeground(linkColor); cardFormat.setFontItalic(true);
anchorFormat.setAnchor(true);
anchorFormat.setAnchorHref("card://" + cardName);
anchorFormat.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.insertText(cardName);
cursor.setCharFormat(oldFormat); cursor.setCharFormat(oldFormat);
} }

View file

@ -83,6 +83,7 @@ private:
QColor otherUserColor = QColor(0, 65, 255); // dark blue QColor otherUserColor = QColor(0, 65, 255); // dark blue
QColor serverMessageColor = QColor(0x85, 0x15, 0x15); QColor serverMessageColor = QColor(0x85, 0x15, 0x15);
QColor linkColor; QColor linkColor;
QColor unresolvedCardTagColor;
private slots: private slots:
void openLink(const QUrl &link); void openLink(const QUrl &link);

View file

@ -26,6 +26,7 @@
#include <libcockatrice/protocol/pb/serverinfo_game.pb.h> #include <libcockatrice/protocol/pb/serverinfo_game.pb.h>
#include <libcockatrice/protocol/pending_command.h> #include <libcockatrice/protocol/pending_command.h>
#include <libcockatrice/settings/cards_display_settings.h> #include <libcockatrice/settings/cards_display_settings.h>
#include <libcockatrice/settings/interface_settings.h>
GameSelector::GameSelector(AbstractClient *_client, GameSelector::GameSelector(AbstractClient *_client,
TabSupervisor *_tabSupervisor, TabSupervisor *_tabSupervisor,
@ -83,12 +84,12 @@ GameSelector::GameSelector(AbstractClient *_client,
if (showFilters && restoresettings) { if (showFilters && restoresettings) {
quickFilterToolBar = new GameSelectorQuickFilterToolBar(this, tabSupervisor, gameListProxyModel, gameTypeMap); quickFilterToolBar = new GameSelectorQuickFilterToolBar(this, tabSupervisor, gameListProxyModel, gameTypeMap);
quickFilterToolBar->setVisible(showFilters && restoresettings && 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] { this, [this] {
quickFilterToolBar->setVisible( quickFilterToolBar->setVisible(
SettingsCache::instance().cardsDisplay().getShowGameSelectorFilterToolbar()); SettingsCache::instance().userInterface().getShowGameSelectorFilterToolbar());
}); });
} else { } else {
quickFilterToolBar = nullptr; quickFilterToolBar = nullptr;

View file

@ -1,8 +1,7 @@
#include "user_card_settings_dialog.h" #include "user_card_settings_dialog.h"
#include "../../../card_picture_loader/card_picture_loader.h" #include "../../../card_picture_loader/card_picture_loader.h"
#include "card/card_completer_proxy_model.h" #include "../../utility/completer_utils.h"
#include "card/card_search_model.h"
#include "card_database_display_model.h" #include "card_database_display_model.h"
#include "card_database_model.h" #include "card_database_model.h"
#include "user_card_art_provider.h" #include "user_card_art_provider.h"
@ -19,7 +18,6 @@
#include <QPainter> #include <QPainter>
#include <QPainterPath> #include <QPainterPath>
#include <QPushButton> #include <QPushButton>
#include <QRegularExpression>
#include <QVBoxLayout> #include <QVBoxLayout>
#include <libcockatrice/card/database/card_database_manager.h> #include <libcockatrice/card/database/card_database_manager.h>
@ -133,29 +131,14 @@ void UserCardArtSettingsDialog::initializeSearchBar()
cardDatabaseModel = new CardDatabaseModel(CardDatabaseManager::getInstance(), false, this); cardDatabaseModel = new CardDatabaseModel(CardDatabaseManager::getInstance(), false, this);
cardDatabaseDisplayModel = new CardDatabaseDisplayModel(this); cardDatabaseDisplayModel = new CardDatabaseDisplayModel(this);
cardDatabaseDisplayModel->setSourceModel(cardDatabaseModel); cardDatabaseDisplayModel->setSourceModel(cardDatabaseModel);
searchModel = new CardSearchModel(cardDatabaseDisplayModel, this);
proxyModel = new CardCompleterProxyModel(this); const CardCompleterSetup cardSetup = createCardCompleter(cardDatabaseDisplayModel, this, 15);
proxyModel->setSourceModel(searchModel); searchModel = cardSetup.searchModel;
proxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive); proxyModel = cardSetup.proxyModel;
proxyModel->setFilterRole(Qt::DisplayRole); completer = cardSetup.completer;
completer = new QCompleter(proxyModel, this);
completer->setCompletionRole(Qt::DisplayRole);
completer->setCompletionMode(QCompleter::PopupCompletion);
completer->setCaseSensitivity(Qt::CaseInsensitive);
completer->setFilterMode(Qt::MatchContains);
completer->setMaxVisibleItems(15);
searchBar->setCompleter(completer); searchBar->setCompleter(completer);
connect(searchBar, &QLineEdit::textEdited, searchModel, &CardSearchModel::updateSearchResults); connectCardCompleterSearch(searchBar, cardSetup);
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();
}
});
connect(completer, static_cast<void (QCompleter::*)(const QString &)>(&QCompleter::activated), this, connect(completer, static_cast<void (QCompleter::*)(const QString &)>(&QCompleter::activated), this,
[this](const QString &completion) { [this](const QString &completion) {

View file

@ -527,6 +527,21 @@ void UserInfoPopup::onGamesContextMenu(const QPoint &pos)
// ── showForUser ─────────────────────────────────────────────────────────────── // ── 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, void UserInfoPopup::showForUser(const QString &userName,
const ServerInfo_User &userInfo, const ServerInfo_User &userInfo,
bool online, bool online,
@ -538,13 +553,7 @@ void UserInfoPopup::showForUser(const QString &userName,
m_currentOnline = online; m_currentOnline = online;
// Header // Header
const QPixmap avatar = m_avatarCache ? m_avatarCache->value(userName) : QPixmap{}; refreshHeader();
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);
// Actions // Actions
rebuildActionButtons(userInfo, online, isBuddy, isIgnored); rebuildActionButtons(userInfo, online, isBuddy, isIgnored);

View file

@ -116,6 +116,9 @@ public:
/** Called when buddy/ignore status changes externally while popup is open. */ /** Called when buddy/ignore status changes externally while popup is open. */
void updateActionButtons(const ServerInfo_User &userInfo, bool online, bool isBuddy, bool isIgnored); 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: signals:
void mouseEnteredPopup(); void mouseEnteredPopup();
void mouseLeftPopup(); void mouseLeftPopup();

View file

@ -23,6 +23,7 @@
#include <QPlainTextEdit> #include <QPlainTextEdit>
#include <QPushButton> #include <QPushButton>
#include <QRadioButton> #include <QRadioButton>
#include <QScreen>
#include <QSpinBox> #include <QSpinBox>
#include <QWidget> #include <QWidget>
#include <libcockatrice/card/database/card_database_manager.h> #include <libcockatrice/card/database/card_database_manager.h>
@ -30,7 +31,7 @@
#include <libcockatrice/protocol/pb/response_get_games_of_user.pb.h> #include <libcockatrice/protocol/pb/response_get_games_of_user.pb.h>
#include <libcockatrice/protocol/pb/response_get_user_info.pb.h> #include <libcockatrice/protocol/pb/response_get_user_info.pb.h>
#include <libcockatrice/protocol/pending_command.h> #include <libcockatrice/protocol/pending_command.h>
#include <libcockatrice/settings/interface_settings.h> #include <libcockatrice/settings/appearance_settings.h>
#include <libcockatrice/utility/string_limits.h> #include <libcockatrice/utility/string_limits.h>
BanDialog::BanDialog(const ServerInfo_User &info, QWidget *parent) : QDialog(parent) 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 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 QStyledItemDelegate::sizeHint(option, index);
} }
return UserListPainter::sizeHint(); 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 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); QStyledItemDelegate::paint(painter, option, index);
return; return;
} }
@ -495,8 +496,6 @@ UserListWidget::UserListWidget(TabSupervisor *_tabSupervisor,
m_userInfoPopup->setWindowOpacity(0.0); m_userInfoPopup->setWindowOpacity(0.0);
m_userInfoPopup->installEventFilter(this); m_userInfoPopup->installEventFilter(this);
connectPopupSignals();
m_showPopupTimer = new QTimer(this); m_showPopupTimer = new QTimer(this);
m_showPopupTimer->setSingleShot(true); m_showPopupTimer->setSingleShot(true);
m_showPopupTimer->setInterval(280); m_showPopupTimer->setInterval(280);
@ -515,13 +514,15 @@ UserListWidget::UserListWidget(TabSupervisor *_tabSupervisor,
} }
}); });
connectPopupSignals();
userTree->setMouseTracking(true); userTree->setMouseTracking(true);
userTree->viewport()->setMouseTracking(true); userTree->viewport()->setMouseTracking(true);
userTree->viewport()->installEventFilter(this); userTree->viewport()->installEventFilter(this);
// Pin on item click // Pin on item click
connect(userTree, &QTreeWidget::itemClicked, this, [this](QTreeWidgetItem *item, int) { connect(userTree, &QTreeWidget::itemClicked, this, [this](QTreeWidgetItem *item, int) {
if (!SettingsCache::instance().interface().getStyleUserList()) { if (!SettingsCache::instance().appearance().getStyleUserList()) {
return; return;
} }
const QString name = static_cast<UserListTWI *>(item)->getUserInfo().name().c_str(); const QString name = static_cast<UserListTWI *>(item)->getUserInfo().name().c_str();
@ -543,17 +544,16 @@ UserListWidget::UserListWidget(TabSupervisor *_tabSupervisor,
connect(userTree->verticalScrollBar(), &QScrollBar::valueChanged, this, [this] { connect(userTree->verticalScrollBar(), &QScrollBar::valueChanged, this, [this] {
m_showPopupTimer->stop(); m_showPopupTimer->stop();
hidePopup(true); hidePopup(true);
requestAvatarsForVisibleItems();
}); });
// Forward join requests from popup upward // Forward join requests from popup upward
connect(m_userInfoPopup, &UserInfoPopup::joinGameRequested, this, &UserListWidget::joinGameRequested); connect(m_userInfoPopup, &UserInfoPopup::joinGameRequested, this, &UserListWidget::joinGameRequested);
connect(avatarProvider, &UserAvatarProvider::avatarUpdated, this, connect(avatarProvider, &UserAvatarProvider::avatarUpdated, this, &UserListWidget::refreshVisibleUserHeader);
[this](const QString &) { userTree->viewport()->update(); }); connect(cardArtProvider, &UserCardArtProvider::cardArtUpdated, this, &UserListWidget::refreshVisibleUserHeader);
connect(cardArtProvider, &UserCardArtProvider::cardArtUpdated, this,
[this](const QString &) { userTree->viewport()->update(); });
connect(&SettingsCache::instance().interface(), &InterfaceSettings::styleUserListChanged, this, connect(&SettingsCache::instance().appearance(), &AppearanceSettings::styleUserListChanged, this,
&UserListWidget::applyDisplayMode); &UserListWidget::applyDisplayMode);
applyDisplayMode(); applyDisplayMode();
@ -633,6 +633,14 @@ void UserListWidget::bind(UserListManager *mgr)
rebuild(); 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) void UserListWidget::refreshPopupButtons(const QString &userName)
{ {
UserListTWI *item = users.value(userName); UserListTWI *item = users.value(userName);
@ -657,9 +665,15 @@ void UserListWidget::hideEvent(QHideEvent *e)
hidePopup(true); hidePopup(true);
} }
void UserListWidget::showEvent(QShowEvent *e)
{
QGroupBox::showEvent(e);
requestAvatarsForVisibleItems();
}
void UserListWidget::applyDisplayMode() void UserListWidget::applyDisplayMode()
{ {
const bool styled = SettingsCache::instance().interface().getStyleUserList(); const bool styled = SettingsCache::instance().appearance().getStyleUserList();
if (styled) { if (styled) {
userTree->header()->setSectionResizeMode(0, QHeaderView::Stretch); userTree->header()->setSectionResizeMode(0, QHeaderView::Stretch);
@ -718,7 +732,7 @@ bool UserListWidget::eventFilter(QObject *obj, QEvent *event)
{ {
if (obj == userTree->viewport()) { if (obj == userTree->viewport()) {
if (event->type() == QEvent::MouseMove) { if (event->type() == QEvent::MouseMove) {
if (!SettingsCache::instance().interface().getStyleUserList()) { if (!SettingsCache::instance().appearance().getStyleUserList()) {
return QGroupBox::eventFilter(obj, event); return QGroupBox::eventFilter(obj, event);
} }
auto *me = static_cast<QMouseEvent *>(event); auto *me = static_cast<QMouseEvent *>(event);
@ -758,6 +772,8 @@ void UserListWidget::showPopupForUser(const QString &userName)
return; return;
} }
avatarProvider->requestAvatar(userName); // ensure the hovered user's avatar is fetched promptly
const ServerInfo_User &info = item->getUserInfo(); const ServerInfo_User &info = item->getUserInfo();
const bool online = item->data(0, UserListRoles::Online).toBool(); const bool online = item->data(0, UserListRoles::Online).toBool();
const bool isBuddy = userContextMenu->getUserListProxy()->isUserBuddy(userName); 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 popH = m_userInfoPopup->height();
const int margin = 12; 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 ─────────────────────────────────── // ── X: prefer the side with more space ───────────────────────────────────
const int spaceLeft = vpTL.x() - screen.left() - margin; const int spaceLeft = vpTL.x() - screen.left() - margin;
@ -875,6 +896,38 @@ void UserListWidget::retranslateUi()
updateCount(); 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<UserListTWI *>(userTree->topLevelItem(i));
if (isItemNearViewport(twi)) {
avatarProvider->requestAvatar(QString::fromStdString(twi->getUserInfo().name()));
}
}
}
void UserListWidget::rebuild() void UserListWidget::rebuild()
{ {
userTree->clear(); userTree->clear();
@ -901,11 +954,11 @@ void UserListWidget::rebuild()
break; break;
} }
beginBulkLoad();
for (auto it = source->cbegin(); it != source->cend(); ++it) { for (auto it = source->cbegin(); it != source->cend(); ++it) {
processUserInfo(it.value(), manager->getOnlineUser(it.key()) != nullptr); processUserInfo(it.value(), manager->getOnlineUser(it.key()) != nullptr);
} }
endBulkLoad();
sortItems();
} }
void UserListWidget::processUserInfo(const ServerInfo_User &user, bool online) void UserListWidget::processUserInfo(const ServerInfo_User &user, bool online)
@ -940,11 +993,15 @@ void UserListWidget::processUserInfo(const ServerInfo_User &user, bool online)
++onlineCount; ++onlineCount;
} }
updateCount(); updateCount();
avatarProvider->requestAvatar(userName); if (!m_bulkLoading && isItemNearViewport(item)) {
avatarProvider->requestAvatar(userName);
}
} }
item->setOnline(online); item->setOnline(online);
sortItems(); if (!m_bulkLoading) {
userTree->viewport()->update(); sortItems();
userTree->viewport()->update();
}
} }
bool UserListWidget::deleteUser(const QString &userName) bool UserListWidget::deleteUser(const QString &userName)

View file

@ -36,6 +36,7 @@ class QPlainTextEdit;
class Response; class Response;
class CommandContainer; class CommandContainer;
class UserContextMenu; class UserContextMenu;
class QShowEvent;
class BanDialog : public QDialog class BanDialog : public QDialog
{ {
@ -158,11 +159,14 @@ private:
QTimer *m_hidePopupTimer = nullptr; QTimer *m_hidePopupTimer = nullptr;
QString m_hoveredUser; QString m_hoveredUser;
bool m_popupPinned = false; bool m_popupPinned = false;
bool m_bulkLoading = false;
void showPopupForUser(const QString &userName); void showPopupForUser(const QString &userName);
void hidePopup(bool immediate = false); void hidePopup(bool immediate = false);
void positionPopup(const QString &userName); void positionPopup(const QString &userName);
void connectPopupSignals(); void connectPopupSignals();
bool isItemNearViewport(const UserListTWI *item) const;
void requestAvatarsForVisibleItems();
QMap<QString, UserListTWI *> users; QMap<QString, UserListTWI *> users;
TabSupervisor *tabSupervisor; TabSupervisor *tabSupervisor;
@ -177,6 +181,7 @@ private:
void refreshPopupButtons(const QString &userName); void refreshPopupButtons(const QString &userName);
private slots: private slots:
void userClicked(QTreeWidgetItem *item, int column); void userClicked(QTreeWidgetItem *item, int column);
void refreshVisibleUserHeader(const QString &name);
signals: signals:
void openMessageDialog(const QString &userName, bool focus); void openMessageDialog(const QString &userName, bool focus);
void addBuddy(const QString &userName); void addBuddy(const QString &userName);
@ -192,6 +197,8 @@ public:
QWidget *parent = nullptr); QWidget *parent = nullptr);
void bind(UserListManager *mgr); void bind(UserListManager *mgr);
void applyDisplayMode(); void applyDisplayMode();
void beginBulkLoad();
void endBulkLoad();
bool eventFilter(QObject *obj, QEvent *event) override; bool eventFilter(QObject *obj, QEvent *event) override;
void retranslateUi(); void retranslateUi();
void rebuild(); void rebuild();
@ -207,6 +214,7 @@ public:
protected: protected:
void hideEvent(QHideEvent *e) override; void hideEvent(QHideEvent *e) override;
void showEvent(QShowEvent *e) override;
}; };
#endif #endif

View file

@ -0,0 +1,196 @@
#include "abstract_settings_page.h"
#include "settings_search_model.h"
#include <QCheckBox>
#include <QGridLayout>
#include <QGroupBox>
#include <QLabel>
#include <QLayout>
#include <QLineEdit>
#include <QPair>
#include <QSpinBox>
/**
* @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<QPair<QWidget *, QLayout *>> &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<QGridLayout *>(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<QLineEdit *>(control)) {
parts.append(lineEdit->placeholderText());
} else if (auto *spinBox = qobject_cast<QSpinBox *>(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<SettingsSearchEntry> AbstractSettingsPage::getSearchEntries()
{
return autoDetectSearchEntries(this, -1);
}
QList<SettingsSearchEntry> AbstractSettingsPage::autoDetectSearchEntries(QWidget *page, int pageIndex)
{
QList<SettingsSearchEntry> entries;
const auto children = page->children();
for (QObject *child : children) {
auto *groupBox = qobject_cast<QGroupBox *>(child);
if (!groupBox) {
continue;
}
QString groupTitle = groupBox->title();
if (groupTitle.isEmpty()) {
continue;
}
QLayout *groupLayout = groupBox->layout();
if (!groupLayout) {
continue;
}
QList<QPair<QWidget *, QLayout *>> widgets;
collectWidgets(groupLayout, widgets);
for (const auto &pair : widgets) {
QWidget *widget = pair.first;
QString label;
auto *checkBox = qobject_cast<QCheckBox *>(widget);
if (checkBox) {
label = checkBox->text();
} else {
auto *labelWidget = qobject_cast<QLabel *>(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<QLabel *>(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;
}

View file

@ -1,16 +1,24 @@
#ifndef COCKATRICE_ABSTRACT_SETTINGS_PAGE_H #ifndef COCKATRICE_ABSTRACT_SETTINGS_PAGE_H
#define COCKATRICE_ABSTRACT_SETTINGS_PAGE_H #define COCKATRICE_ABSTRACT_SETTINGS_PAGE_H
#include <QList>
#include <QWidget> #include <QWidget>
#define WIKI_CUSTOM_PIC_URL "https://github.com/Cockatrice/Cockatrice/wiki/Custom-Picture-Download-URLs" #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_CUSTOM_SHORTCUTS "https://github.com/Cockatrice/Cockatrice/wiki/Custom-Keyboard-Shortcuts"
#define WIKI_TRANSLATION_FAQ "https://github.com/Cockatrice/Cockatrice/wiki/Translation-FAQ" #define WIKI_TRANSLATION_FAQ "https://github.com/Cockatrice/Cockatrice/wiki/Translation-FAQ"
struct SettingsSearchEntry;
class AbstractSettingsPage : public QWidget class AbstractSettingsPage : public QWidget
{ {
Q_OBJECT
public: public:
virtual void retranslateUi() = 0; virtual void retranslateUi() = 0;
virtual QList<SettingsSearchEntry> getSearchEntries();
protected:
static QList<SettingsSearchEntry> autoDetectSearchEntries(QWidget *page, int pageIndex);
}; };
#endif // COCKATRICE_ABSTRACT_SETTINGS_PAGE_H #endif // COCKATRICE_ABSTRACT_SETTINGS_PAGE_H

View file

@ -15,6 +15,7 @@
#include <QMessageBox> #include <QMessageBox>
#include <QStyleFactory> #include <QStyleFactory>
#include <QTimer> #include <QTimer>
#include <libcockatrice/settings/appearance_settings.h>
#include <libcockatrice/settings/cards_display_settings.h> #include <libcockatrice/settings/cards_display_settings.h>
#include <libcockatrice/settings/interface_settings.h> #include <libcockatrice/settings/interface_settings.h>
#include <libcockatrice/settings/paths_settings.h> #include <libcockatrice/settings/paths_settings.h>
@ -104,7 +105,7 @@ AppearanceSettingsPage::AppearanceSettingsPage()
homeTabBackgroundSourceBox.addItem(QObject::tr(entry.trKey), QVariant::fromValue(entry.type)); homeTabBackgroundSourceBox.addItem(QObject::tr(entry.trKey), QVariant::fromValue(entry.type));
} }
QString homeTabBackgroundSource = SettingsCache::instance().personal().getHomeTabBackgroundSource(); QString homeTabBackgroundSource = settings.appearance().getHomeTabBackgroundSource();
int homeTabBackgroundSourceId = int homeTabBackgroundSourceId =
homeTabBackgroundSourceBox.findData(BackgroundSources::fromId(homeTabBackgroundSource)); homeTabBackgroundSourceBox.findData(BackgroundSources::fromId(homeTabBackgroundSource));
if (homeTabBackgroundSourceId != -1) { if (homeTabBackgroundSourceId != -1) {
@ -113,20 +114,19 @@ AppearanceSettingsPage::AppearanceSettingsPage()
connect(&homeTabBackgroundSourceBox, QOverload<int>::of(&QComboBox::currentIndexChanged), this, [this]() { connect(&homeTabBackgroundSourceBox, QOverload<int>::of(&QComboBox::currentIndexChanged), this, [this]() {
auto type = homeTabBackgroundSourceBox.currentData().value<BackgroundSources::Type>(); auto type = homeTabBackgroundSourceBox.currentData().value<BackgroundSources::Type>();
SettingsCache::instance().personal().setHomeTabBackgroundSource(BackgroundSources::toId(type)); SettingsCache::instance().appearance().setHomeTabBackgroundSource(BackgroundSources::toId(type));
updateHomeTabSettingsVisibility(); updateHomeTabSettingsVisibility();
}); });
homeTabBackgroundShuffleFrequencySpinBox.setRange(0, 3600); homeTabBackgroundShuffleFrequencySpinBox.setRange(0, 3600);
homeTabBackgroundShuffleFrequencySpinBox.setSuffix(tr(" seconds")); homeTabBackgroundShuffleFrequencySpinBox.setSuffix(tr(" seconds"));
homeTabBackgroundShuffleFrequencySpinBox.setValue( homeTabBackgroundShuffleFrequencySpinBox.setValue(settings.appearance().getHomeTabBackgroundShuffleFrequency());
SettingsCache::instance().personal().getHomeTabBackgroundShuffleFrequency()); connect(&homeTabBackgroundShuffleFrequencySpinBox, qOverload<int>(&QSpinBox::valueChanged), &settings.appearance(),
connect(&homeTabBackgroundShuffleFrequencySpinBox, qOverload<int>(&QSpinBox::valueChanged), &settings.personal(), &AppearanceSettings::setHomeTabBackgroundShuffleFrequency);
&PersonalSettings::setHomeTabBackgroundShuffleFrequency);
homeTabDisplayCardNameCheckBox.setChecked(settings.personal().getHomeTabDisplayCardName()); homeTabDisplayCardNameCheckBox.setChecked(settings.appearance().getHomeTabDisplayCardName());
connect(&homeTabDisplayCardNameCheckBox, &QCheckBox::QT_STATE_CHANGED, &settings.personal(), connect(&homeTabDisplayCardNameCheckBox, &QCheckBox::QT_STATE_CHANGED, &settings.appearance(),
&PersonalSettings::setHomeTabDisplayCardName); &AppearanceSettings::setHomeTabDisplayCardName);
updateHomeTabSettingsVisibility(); updateHomeTabSettingsVisibility();
@ -140,9 +140,9 @@ AppearanceSettingsPage::AppearanceSettingsPage()
homeTabGroupBox = new QGroupBox; homeTabGroupBox = new QGroupBox;
homeTabGroupBox->setLayout(homeTabGrid); homeTabGroupBox->setLayout(homeTabGrid);
styleUserListCheckBox.setChecked(settings.interface().getStyleUserList()); styleUserListCheckBox.setChecked(settings.appearance().getStyleUserList());
connect(&styleUserListCheckBox, &QCheckBox::QT_STATE_CHANGED, &settings.interface(), connect(&styleUserListCheckBox, &QCheckBox::QT_STATE_CHANGED, &settings.appearance(),
&InterfaceSettings::setStyleUserList); &AppearanceSettings::setStyleUserList);
auto stylingTabGrid = new QGridLayout; auto stylingTabGrid = new QGridLayout;
stylingTabGrid->addWidget(&styleUserListCheckBox, 0, 0, 1, 2); stylingTabGrid->addWidget(&styleUserListCheckBox, 0, 0, 1, 2);
@ -151,12 +151,12 @@ AppearanceSettingsPage::AppearanceSettingsPage()
stylingGroupBox->setLayout(stylingTabGrid); stylingGroupBox->setLayout(stylingTabGrid);
// Menu settings // Menu settings
showShortcutsCheckBox.setChecked(settings.cardsDisplay().getShowShortcuts()); showShortcutsCheckBox.setChecked(settings.userInterface().getShowShortcuts());
connect(&showShortcutsCheckBox, &QCheckBox::QT_STATE_CHANGED, this, &AppearanceSettingsPage::showShortcutsChanged); connect(&showShortcutsCheckBox, &QCheckBox::QT_STATE_CHANGED, this, &AppearanceSettingsPage::showShortcutsChanged);
showGameSelectorFilterToolbarCheckBox.setChecked(settings.cardsDisplay().getShowGameSelectorFilterToolbar()); showGameSelectorFilterToolbarCheckBox.setChecked(settings.userInterface().getShowGameSelectorFilterToolbar());
connect(&showGameSelectorFilterToolbarCheckBox, &QCheckBox::QT_STATE_CHANGED, &settings.cardsDisplay(), connect(&showGameSelectorFilterToolbarCheckBox, &QCheckBox::QT_STATE_CHANGED, &settings.userInterface(),
&CardsDisplaySettings::setShowGameSelectorFilterToolbar); &InterfaceSettings::setShowGameSelectorFilterToolbar);
auto *menuGrid = new QGridLayout; auto *menuGrid = new QGridLayout;
menuGrid->addWidget(&showShortcutsCheckBox, 0, 0); menuGrid->addWidget(&showShortcutsCheckBox, 0, 0);
@ -199,9 +199,9 @@ AppearanceSettingsPage::AppearanceSettingsPage()
connect(&roundCardCornersCheckBox, &QAbstractButton::toggled, &settings.cardsDisplay(), connect(&roundCardCornersCheckBox, &QAbstractButton::toggled, &settings.cardsDisplay(),
&CardsDisplaySettings::setRoundCardCorners); &CardsDisplaySettings::setRoundCardCorners);
connect(&maxFontSizeForCardsEdit, qOverload<int>(&QSpinBox::valueChanged), &settings.personal(), connect(&maxFontSizeForCardsEdit, qOverload<int>(&QSpinBox::valueChanged), &settings.appearance(),
&PersonalSettings::setMaxFontSize); &AppearanceSettings::setMaxFontSize);
maxFontSizeForCardsEdit.setValue(settings.personal().getMaxFontSize()); maxFontSizeForCardsEdit.setValue(settings.appearance().getMaxFontSize());
maxFontSizeForCardsLabel.setBuddy(&maxFontSizeForCardsEdit); maxFontSizeForCardsLabel.setBuddy(&maxFontSizeForCardsEdit);
maxFontSizeForCardsEdit.setMinimum(9); maxFontSizeForCardsEdit.setMinimum(9);
maxFontSizeForCardsEdit.setMaximum(100); maxFontSizeForCardsEdit.setMaximum(100);
@ -224,12 +224,12 @@ AppearanceSettingsPage::AppearanceSettingsPage()
&CardsDisplaySettings::setStackCardOverlapPercent); &CardsDisplaySettings::setStackCardOverlapPercent);
cardViewInitialRowsMaxBox.setRange(1, 999); cardViewInitialRowsMaxBox.setRange(1, 999);
cardViewInitialRowsMaxBox.setValue(SettingsCache::instance().interface().getCardViewInitialRowsMax()); cardViewInitialRowsMaxBox.setValue(SettingsCache::instance().userInterface().getCardViewInitialRowsMax());
connect(&cardViewInitialRowsMaxBox, qOverload<int>(&QSpinBox::valueChanged), this, connect(&cardViewInitialRowsMaxBox, qOverload<int>(&QSpinBox::valueChanged), this,
&AppearanceSettingsPage::cardViewInitialRowsMaxChanged); &AppearanceSettingsPage::cardViewInitialRowsMaxChanged);
cardViewExpandedRowsMaxBox.setRange(1, 999); cardViewExpandedRowsMaxBox.setRange(1, 999);
cardViewExpandedRowsMaxBox.setValue(SettingsCache::instance().interface().getCardViewExpandedRowsMax()); cardViewExpandedRowsMaxBox.setValue(SettingsCache::instance().userInterface().getCardViewExpandedRowsMax());
connect(&cardViewExpandedRowsMaxBox, qOverload<int>(&QSpinBox::valueChanged), this, connect(&cardViewExpandedRowsMaxBox, qOverload<int>(&QSpinBox::valueChanged), this,
&AppearanceSettingsPage::cardViewExpandedRowsMaxChanged); &AppearanceSettingsPage::cardViewExpandedRowsMaxChanged);
@ -291,12 +291,12 @@ AppearanceSettingsPage::AppearanceSettingsPage()
cardCountersGroupBox->setLayout(cardCountersLayout); cardCountersGroupBox->setLayout(cardCountersLayout);
// Hand layout // Hand layout
horizontalHandCheckBox.setChecked(settings.interface().getHorizontalHand()); horizontalHandCheckBox.setChecked(settings.userInterface().getHorizontalHand());
connect(&horizontalHandCheckBox, &QCheckBox::QT_STATE_CHANGED, &settings.interface(), connect(&horizontalHandCheckBox, &QCheckBox::QT_STATE_CHANGED, &settings.userInterface(),
&InterfaceSettings::setHorizontalHand); &InterfaceSettings::setHorizontalHand);
leftJustifiedHandCheckBox.setChecked(settings.interface().getLeftJustified()); leftJustifiedHandCheckBox.setChecked(settings.userInterface().getLeftJustified());
connect(&leftJustifiedHandCheckBox, &QCheckBox::QT_STATE_CHANGED, &settings.interface(), connect(&leftJustifiedHandCheckBox, &QCheckBox::QT_STATE_CHANGED, &settings.userInterface(),
&InterfaceSettings::setLeftJustified); &InterfaceSettings::setLeftJustified);
auto *handGrid = new QGridLayout; auto *handGrid = new QGridLayout;
@ -307,13 +307,13 @@ AppearanceSettingsPage::AppearanceSettingsPage()
handGroupBox->setLayout(handGrid); handGroupBox->setLayout(handGrid);
// table grid layout // table grid layout
invertVerticalCoordinateCheckBox.setChecked(settings.interface().getInvertVerticalCoordinate()); invertVerticalCoordinateCheckBox.setChecked(settings.userInterface().getInvertVerticalCoordinate());
connect(&invertVerticalCoordinateCheckBox, &QCheckBox::QT_STATE_CHANGED, &settings.interface(), connect(&invertVerticalCoordinateCheckBox, &QCheckBox::QT_STATE_CHANGED, &settings.userInterface(),
&InterfaceSettings::setInvertVerticalCoordinate); &InterfaceSettings::setInvertVerticalCoordinate);
minPlayersForMultiColumnLayoutEdit.setMinimum(2); minPlayersForMultiColumnLayoutEdit.setMinimum(2);
minPlayersForMultiColumnLayoutEdit.setValue(settings.interface().getMinPlayersForMultiColumnLayout()); minPlayersForMultiColumnLayoutEdit.setValue(settings.userInterface().getMinPlayersForMultiColumnLayout());
connect(&minPlayersForMultiColumnLayoutEdit, qOverload<int>(&QSpinBox::valueChanged), &settings.interface(), connect(&minPlayersForMultiColumnLayoutEdit, qOverload<int>(&QSpinBox::valueChanged), &settings.userInterface(),
&InterfaceSettings::setMinPlayersForMultiColumnLayout); &InterfaceSettings::setMinPlayersForMultiColumnLayout);
minPlayersForMultiColumnLayoutLabel.setBuddy(&minPlayersForMultiColumnLayoutEdit); minPlayersForMultiColumnLayoutLabel.setBuddy(&minPlayersForMultiColumnLayoutEdit);
@ -375,7 +375,7 @@ void AppearanceSettingsPage::editPalette()
void AppearanceSettingsPage::updateHomeTabSettingsVisibility() void AppearanceSettingsPage::updateHomeTabSettingsVisibility()
{ {
bool visible = SettingsCache::instance().personal().getHomeTabBackgroundSource() != bool visible = SettingsCache::instance().appearance().getHomeTabBackgroundSource() !=
BackgroundSources::toId(BackgroundSources::Theme); BackgroundSources::toId(BackgroundSources::Theme);
homeTabBackgroundShuffleFrequencyLabel.setVisible(visible); homeTabBackgroundShuffleFrequencyLabel.setVisible(visible);
@ -385,7 +385,7 @@ void AppearanceSettingsPage::updateHomeTabSettingsVisibility()
void AppearanceSettingsPage::showShortcutsChanged(QT_STATE_CHANGED_T value) 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 qApp->setAttribute(Qt::AA_DontShowShortcutsInContextMenus, value == 0); // 0 = unchecked
} }
@ -412,7 +412,7 @@ void AppearanceSettingsPage::overrideAllCardArtWithPersonalPreferenceToggled(QT_
*/ */
void AppearanceSettingsPage::cardViewInitialRowsMaxChanged(int value) void AppearanceSettingsPage::cardViewInitialRowsMaxChanged(int value)
{ {
SettingsCache::instance().interface().setCardViewInitialRowsMax(value); SettingsCache::instance().userInterface().setCardViewInitialRowsMax(value);
if (cardViewExpandedRowsMaxBox.value() < value) { if (cardViewExpandedRowsMaxBox.value() < value) {
cardViewExpandedRowsMaxBox.setValue(value); cardViewExpandedRowsMaxBox.setValue(value);
} }
@ -425,7 +425,7 @@ void AppearanceSettingsPage::cardViewInitialRowsMaxChanged(int value)
*/ */
void AppearanceSettingsPage::cardViewExpandedRowsMaxChanged(int value) void AppearanceSettingsPage::cardViewExpandedRowsMaxChanged(int value)
{ {
SettingsCache::instance().interface().setCardViewExpandedRowsMax(value); SettingsCache::instance().userInterface().setCardViewExpandedRowsMax(value);
if (cardViewInitialRowsMaxBox.value() > value) { if (cardViewInitialRowsMaxBox.value() > value) {
cardViewInitialRowsMaxBox.setValue(value); cardViewInitialRowsMaxBox.setValue(value);
} }

View file

@ -40,10 +40,10 @@ private:
QSpinBox homeTabBackgroundShuffleFrequencySpinBox; QSpinBox homeTabBackgroundShuffleFrequencySpinBox;
QCheckBox homeTabDisplayCardNameCheckBox; QCheckBox homeTabDisplayCardNameCheckBox;
QCheckBox styleUserListCheckBox; QCheckBox styleUserListCheckBox;
QLabel minPlayersForMultiColumnLayoutLabel;
QLabel maxFontSizeForCardsLabel;
QCheckBox showShortcutsCheckBox; QCheckBox showShortcutsCheckBox;
QCheckBox showGameSelectorFilterToolbarCheckBox; QCheckBox showGameSelectorFilterToolbarCheckBox;
QLabel minPlayersForMultiColumnLayoutLabel;
QLabel maxFontSizeForCardsLabel;
QCheckBox overrideAllCardArtWithPersonalPreferenceCheckBox; QCheckBox overrideAllCardArtWithPersonalPreferenceCheckBox;
QCheckBox bumpSetsWithCardsInDeckToTopCheckBox; QCheckBox bumpSetsWithCardsInDeckToTopCheckBox;
QCheckBox displayCardNamesCheckBox; QCheckBox displayCardNamesCheckBox;

View file

@ -17,9 +17,9 @@
DeckEditorSettingsPage::DeckEditorSettingsPage() DeckEditorSettingsPage::DeckEditorSettingsPage()
{ {
picDownloadCheckBox.setChecked(SettingsCache::instance().personal().getPicDownload()); picDownloadCheckBox.setChecked(SettingsCache::instance().downloads().getPicDownload());
connect(&picDownloadCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().personal(), connect(&picDownloadCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().downloads(),
&PersonalSettings::setPicDownload); &DownloadSettings::setPicDownload);
urlLinkLabel.setTextInteractionFlags(Qt::LinksAccessibleByMouse); urlLinkLabel.setTextInteractionFlags(Qt::LinksAccessibleByMouse);
urlLinkLabel.setOpenExternalLinks(true); urlLinkLabel.setOpenExternalLinks(true);
@ -29,7 +29,7 @@ DeckEditorSettingsPage::DeckEditorSettingsPage()
auto *lpGeneralGrid = new QGridLayout; auto *lpGeneralGrid = new QGridLayout;
auto *lpSpoilerGrid = 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 = new QLineEdit(SettingsCache::instance().getSpoilerCardDatabasePath());
mpSpoilerSavePathLineEdit->setReadOnly(true); mpSpoilerSavePathLineEdit->setReadOnly(true);
@ -91,8 +91,8 @@ DeckEditorSettingsPage::DeckEditorSettingsPage()
lpSpoilerGrid->addWidget(&infoOnSpoilersLabel, 3, 0, 1, 3, Qt::AlignTop); lpSpoilerGrid->addWidget(&infoOnSpoilersLabel, 3, 0, 1, 3, Qt::AlignTop);
// On a change to the checkbox, hide/un-hide the other fields // On a change to the checkbox, hide/un-hide the other fields
connect(&mcDownloadSpoilersCheckBox, &QCheckBox::toggled, &SettingsCache::instance().personal(), connect(&mcDownloadSpoilersCheckBox, &QCheckBox::toggled, &SettingsCache::instance().downloads(),
&PersonalSettings::setDownloadSpoilerStatus); &DownloadSettings::setDownloadSpoilerStatus);
connect(&mcDownloadSpoilersCheckBox, &QCheckBox::toggled, this, &DeckEditorSettingsPage::setSpoilersEnabled); connect(&mcDownloadSpoilersCheckBox, &QCheckBox::toggled, this, &DeckEditorSettingsPage::setSpoilersEnabled);
mpGeneralGroupBox = new QGroupBox; mpGeneralGroupBox = new QGroupBox;

View file

@ -90,6 +90,7 @@ MessagesSettingsPage::MessagesSettingsPage()
highlightNotice->addWidget(&hexHighlightLabel, 1, 2); highlightNotice->addWidget(&hexHighlightLabel, 1, 2);
highlightNotice->addWidget(customAlertString, 0, 0); highlightNotice->addWidget(customAlertString, 0, 0);
highlightNotice->addWidget(&customAlertStringLabel, 1, 0); highlightNotice->addWidget(&customAlertStringLabel, 1, 0);
customAlertStringLabel.setBuddy(customAlertString);
highlightGroupBox = new QGroupBox; highlightGroupBox = new QGroupBox;
highlightGroupBox->setLayout(highlightNotice); highlightGroupBox->setLayout(highlightNotice);

View file

@ -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 <QPainter>
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<SettingsSearchEntry>();
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);
}

View file

@ -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 <QPixmap>
#include <QStyledItemDelegate>
/**
* @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<QPixmap> pageIcons; ///< Icons of the related settings pages
};
#endif // COCKATRICE_SETTINGS_SEARCH_DELEGATE_H

View file

@ -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 <QPair>
#include <algorithm>
SettingsSearchModel::SettingsSearchModel(QObject *parent) : QAbstractListModel(parent)
{
}
void SettingsSearchModel::setSourceEntries(const QList<SettingsSearchEntry> &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 &regex)
{
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<QPair<int, int>> scored; // <score, index>
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()]];
}

View file

@ -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 <QAbstractListModel>
#include <QList>
#include <QRegularExpression>
#include <QWidget>
/**
* @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<SettingsSearchEntry> &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<SettingsSearchEntry> sourceEntries; ///< Complete unfiltered entry list
QList<int> 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

View file

@ -123,6 +123,7 @@ void ShortcutSettingsPage::retranslateUi()
currentActionGroupLabel->setText(tr("Section:")); currentActionGroupLabel->setText(tr("Section:"));
currentActionLabel->setText(tr("Action:")); currentActionLabel->setText(tr("Action:"));
currentShortcutLabel->setText(tr("Shortcut:")); currentShortcutLabel->setText(tr("Shortcut:"));
editShortcutGroupBox->setTitle(tr("Shortcut editor"));
editTextBox->retranslateUi(); editTextBox->retranslateUi();
faqLabel->setText(QString("<a href='%1'>%2</a>").arg(WIKI_CUSTOM_SHORTCUTS).arg(tr("How to set custom shortcuts"))); faqLabel->setText(QString("<a href='%1'>%2</a>").arg(WIKI_CUSTOM_SHORTCUTS).arg(tr("How to set custom shortcuts")));
btnResetAll->setText(tr("Restore all default shortcuts")); btnResetAll->setText(tr("Restore all default shortcuts"));

View file

@ -2,9 +2,11 @@
#include "../../../client/settings/cache_settings.h" #include "../../../client/settings/cache_settings.h"
#include "../interface/widgets/tabs/tab_supervisor.h" #include "../interface/widgets/tabs/tab_supervisor.h"
#include "../tabs/api/commander_spellbook/commander_spellbook_bracket_explainer.h"
#include <QGridLayout> #include <QGridLayout>
#include <libcockatrice/settings/cards_display_settings.h> #include <libcockatrice/settings/cards_display_settings.h>
#include <libcockatrice/settings/deck_editor_settings.h>
#include <libcockatrice/settings/interface_settings.h> #include <libcockatrice/settings/interface_settings.h>
#include <libcockatrice/settings/personal_settings.h> #include <libcockatrice/settings/personal_settings.h>
#include <libcockatrice/settings/visual_deck_storage_settings.h> #include <libcockatrice/settings/visual_deck_storage_settings.h>
@ -19,70 +21,70 @@ enum visualDeckStoragePromptForConversionIndex
UserInterfaceSettingsPage::UserInterfaceSettingsPage() UserInterfaceSettingsPage::UserInterfaceSettingsPage()
{ {
// general settings and notification settings // general settings and notification settings
notificationsEnabledCheckBox.setChecked(SettingsCache::instance().interface().getNotificationsEnabled()); notificationsEnabledCheckBox.setChecked(SettingsCache::instance().userInterface().getNotificationsEnabled());
connect(&notificationsEnabledCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().interface(), connect(&notificationsEnabledCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().userInterface(),
&InterfaceSettings::setNotificationsEnabled); &InterfaceSettings::setNotificationsEnabled);
connect(&notificationsEnabledCheckBox, &QCheckBox::QT_STATE_CHANGED, this, connect(&notificationsEnabledCheckBox, &QCheckBox::QT_STATE_CHANGED, this,
&UserInterfaceSettingsPage::setNotificationEnabled); &UserInterfaceSettingsPage::setNotificationEnabled);
specNotificationsEnabledCheckBox.setChecked( specNotificationsEnabledCheckBox.setChecked(
SettingsCache::instance().interface().getSpectatorNotificationsEnabled()); SettingsCache::instance().userInterface().getSpectatorNotificationsEnabled());
specNotificationsEnabledCheckBox.setEnabled(SettingsCache::instance().interface().getNotificationsEnabled()); specNotificationsEnabledCheckBox.setEnabled(SettingsCache::instance().userInterface().getNotificationsEnabled());
connect(&specNotificationsEnabledCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().interface(), connect(&specNotificationsEnabledCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().userInterface(),
&InterfaceSettings::setSpectatorNotificationsEnabled); &InterfaceSettings::setSpectatorNotificationsEnabled);
buddyConnectNotificationsEnabledCheckBox.setChecked( buddyConnectNotificationsEnabledCheckBox.setChecked(
SettingsCache::instance().interface().getBuddyConnectNotificationsEnabled()); SettingsCache::instance().userInterface().getBuddyConnectNotificationsEnabled());
buddyConnectNotificationsEnabledCheckBox.setEnabled( buddyConnectNotificationsEnabledCheckBox.setEnabled(
SettingsCache::instance().interface().getNotificationsEnabled()); SettingsCache::instance().userInterface().getNotificationsEnabled());
connect(&buddyConnectNotificationsEnabledCheckBox, &QCheckBox::QT_STATE_CHANGED, connect(&buddyConnectNotificationsEnabledCheckBox, &QCheckBox::QT_STATE_CHANGED,
&SettingsCache::instance().interface(), &InterfaceSettings::setBuddyConnectNotificationsEnabled); &SettingsCache::instance().userInterface(), &InterfaceSettings::setBuddyConnectNotificationsEnabled);
doubleClickToPlayCheckBox.setChecked(SettingsCache::instance().interface().getDoubleClickToPlay()); doubleClickToPlayCheckBox.setChecked(SettingsCache::instance().userInterface().getDoubleClickToPlay());
connect(&doubleClickToPlayCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().interface(), connect(&doubleClickToPlayCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().userInterface(),
&InterfaceSettings::setDoubleClickToPlay); &InterfaceSettings::setDoubleClickToPlay);
clickPlaysAllSelectedCheckBox.setChecked(SettingsCache::instance().interface().getClickPlaysAllSelected()); clickPlaysAllSelectedCheckBox.setChecked(SettingsCache::instance().userInterface().getClickPlaysAllSelected());
connect(&clickPlaysAllSelectedCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().interface(), connect(&clickPlaysAllSelectedCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().userInterface(),
&InterfaceSettings::setClickPlaysAllSelected); &InterfaceSettings::setClickPlaysAllSelected);
playToStackCheckBox.setChecked(SettingsCache::instance().interface().getPlayToStack()); playToStackCheckBox.setChecked(SettingsCache::instance().userInterface().getPlayToStack());
connect(&playToStackCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().interface(), connect(&playToStackCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().userInterface(),
&InterfaceSettings::setPlayToStack); &InterfaceSettings::setPlayToStack);
doNotDeleteArrowsInSubPhasesCheckBox.setChecked( doNotDeleteArrowsInSubPhasesCheckBox.setChecked(
SettingsCache::instance().interface().getDoNotDeleteArrowsInSubPhases()); SettingsCache::instance().userInterface().getDoNotDeleteArrowsInSubPhases());
connect(&doNotDeleteArrowsInSubPhasesCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().interface(), connect(&doNotDeleteArrowsInSubPhasesCheckBox, &QCheckBox::QT_STATE_CHANGED,
&InterfaceSettings::setDoNotDeleteArrowsInSubPhases); &SettingsCache::instance().userInterface(), &InterfaceSettings::setDoNotDeleteArrowsInSubPhases);
closeEmptyCardViewCheckBox.setChecked(SettingsCache::instance().interface().getCloseEmptyCardView()); closeEmptyCardViewCheckBox.setChecked(SettingsCache::instance().userInterface().getCloseEmptyCardView());
connect(&closeEmptyCardViewCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().interface(), connect(&closeEmptyCardViewCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().userInterface(),
&InterfaceSettings::setCloseEmptyCardView); &InterfaceSettings::setCloseEmptyCardView);
focusCardViewSearchBarCheckBox.setChecked(SettingsCache::instance().interface().getFocusCardViewSearchBar()); focusCardViewSearchBarCheckBox.setChecked(SettingsCache::instance().userInterface().getFocusCardViewSearchBar());
connect(&focusCardViewSearchBarCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().interface(), connect(&focusCardViewSearchBarCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().userInterface(),
&InterfaceSettings::setFocusCardViewSearchBar); &InterfaceSettings::setFocusCardViewSearchBar);
annotateTokensCheckBox.setChecked(SettingsCache::instance().interface().getAnnotateTokens()); annotateTokensCheckBox.setChecked(SettingsCache::instance().userInterface().getAnnotateTokens());
connect(&annotateTokensCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().interface(), connect(&annotateTokensCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().userInterface(),
&InterfaceSettings::setAnnotateTokens); &InterfaceSettings::setAnnotateTokens);
showDragSelectionCountCheckBox.setChecked(SettingsCache::instance().interface().getShowDragSelectionCount()); showDragSelectionCountCheckBox.setChecked(SettingsCache::instance().userInterface().getShowDragSelectionCount());
connect(&showDragSelectionCountCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().interface(), connect(&showDragSelectionCountCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().userInterface(),
&InterfaceSettings::setShowDragSelectionCount); &InterfaceSettings::setShowDragSelectionCount);
showTotalSelectionCountCheckBox.setChecked(SettingsCache::instance().interface().getShowTotalSelectionCount()); showTotalSelectionCountCheckBox.setChecked(SettingsCache::instance().userInterface().getShowTotalSelectionCount());
connect(&showTotalSelectionCountCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().interface(), connect(&showTotalSelectionCountCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().userInterface(),
&InterfaceSettings::setShowTotalSelectionCount); &InterfaceSettings::setShowTotalSelectionCount);
useTearOffMenusCheckBox.setChecked(SettingsCache::instance().interface().getUseTearOffMenus()); useTearOffMenusCheckBox.setChecked(SettingsCache::instance().userInterface().getUseTearOffMenus());
connect(&useTearOffMenusCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().interface(), connect(&useTearOffMenusCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().userInterface(),
[](const QT_STATE_CHANGED_T state) { [](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()); keepGameChatFocusCheckBox.setChecked(SettingsCache::instance().userInterface().getKeepGameChatFocus());
connect(&keepGameChatFocusCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().interface(), connect(&keepGameChatFocusCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().userInterface(),
&InterfaceSettings::setKeepGameChatFocus); &InterfaceSettings::setKeepGameChatFocus);
auto *generalGrid = new QGridLayout; auto *generalGrid = new QGridLayout;
@ -121,9 +123,9 @@ UserInterfaceSettingsPage::UserInterfaceSettingsPage()
animationGroupBox->setLayout(animationGrid); animationGroupBox->setLayout(animationGrid);
// deck editor settings // deck editor settings
openDeckInNewTabCheckBox.setChecked(SettingsCache::instance().interface().getOpenDeckInNewTab()); openDeckInNewTabCheckBox.setChecked(SettingsCache::instance().deckEditor().getOpenDeckInNewTab());
connect(&openDeckInNewTabCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().interface(), connect(&openDeckInNewTabCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().deckEditor(),
&InterfaceSettings::setOpenDeckInNewTab); &DeckEditorSettings::setOpenDeckInNewTab);
visualDeckStorageInGameCheckBox.setChecked( visualDeckStorageInGameCheckBox.setChecked(
SettingsCache::instance().visualDeckStorage().getVisualDeckStorageInGame()); SettingsCache::instance().visualDeckStorage().getVisualDeckStorageInGame());
@ -156,10 +158,60 @@ UserInterfaceSettingsPage::UserInterfaceSettingsPage()
defaultDeckEditorTypeSelector.addItem(""); // these will be set in retranslateUI defaultDeckEditorTypeSelector.addItem(""); // these will be set in retranslateUI
defaultDeckEditorTypeSelector.addItem(""); defaultDeckEditorTypeSelector.addItem("");
defaultDeckEditorTypeSelector.setCurrentIndex( defaultDeckEditorTypeSelector.setCurrentIndex(SettingsCache::instance().deckEditor().getDefaultDeckEditorType());
SettingsCache::instance().visualDeckStorage().getDefaultDeckEditorType());
connect(&defaultDeckEditorTypeSelector, QOverload<int>::of(&QComboBox::currentIndexChanged), connect(&defaultDeckEditorTypeSelector, QOverload<int>::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<int>::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<int>::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; auto *deckEditorGrid = new QGridLayout;
deckEditorGrid->addWidget(&openDeckInNewTabCheckBox, 0, 0); deckEditorGrid->addWidget(&openDeckInNewTabCheckBox, 0, 0);
@ -169,14 +221,18 @@ UserInterfaceSettingsPage::UserInterfaceSettingsPage()
deckEditorGrid->addWidget(&visualDeckStoragePromptForConversionSelector, 3, 1); deckEditorGrid->addWidget(&visualDeckStoragePromptForConversionSelector, 3, 1);
deckEditorGrid->addWidget(&defaultDeckEditorTypeLabel, 4, 0); deckEditorGrid->addWidget(&defaultDeckEditorTypeLabel, 4, 0);
deckEditorGrid->addWidget(&defaultDeckEditorTypeSelector, 4, 1); 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 = new QGroupBox;
deckEditorGroupBox->setLayout(deckEditorGrid); deckEditorGroupBox->setLayout(deckEditorGrid);
// replay settings // replay settings
rewindBufferingMsBox.setRange(0, 9999); rewindBufferingMsBox.setRange(0, 9999);
rewindBufferingMsBox.setValue(SettingsCache::instance().interface().getRewindBufferingMs()); rewindBufferingMsBox.setValue(SettingsCache::instance().userInterface().getRewindBufferingMs());
connect(&rewindBufferingMsBox, qOverload<int>(&QSpinBox::valueChanged), &SettingsCache::instance().interface(), connect(&rewindBufferingMsBox, qOverload<int>(&QSpinBox::valueChanged), &SettingsCache::instance().userInterface(),
&InterfaceSettings::setRewindBufferingMs); &InterfaceSettings::setRewindBufferingMs);
auto *replayGrid = new QGridLayout; 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() void UserInterfaceSettingsPage::retranslateUi()
{ {
generalGroupBox->setTitle(tr("General interface settings")); generalGroupBox->setTitle(tr("General interface settings"));
@ -249,6 +326,22 @@ void UserInterfaceSettingsPage::retranslateUi()
defaultDeckEditorTypeLabel.setText(tr("Default deck editor type")); defaultDeckEditorTypeLabel.setText(tr("Default deck editor type"));
defaultDeckEditorTypeSelector.setItemText(TabSupervisor::ClassicDeckEditor, tr("Classic Deck Editor")); defaultDeckEditorTypeSelector.setItemText(TabSupervisor::ClassicDeckEditor, tr("Classic Deck Editor"));
defaultDeckEditorTypeSelector.setItemText(TabSupervisor::VisualDeckEditor, tr("Visual 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")); replayGroupBox->setTitle(tr("Replay settings"));
rewindBufferingMsLabel.setText(tr("Buffer time for backwards skip via shortcut:")); rewindBufferingMsLabel.setText(tr("Buffer time for backwards skip via shortcut:"));
rewindBufferingMsBox.setSuffix(" ms"); rewindBufferingMsBox.setSuffix(" ms");

View file

@ -8,6 +8,8 @@
#include <QGroupBox> #include <QGroupBox>
#include <QLabel> #include <QLabel>
#include <QSpinBox> #include <QSpinBox>
#include <QToolButton>
#include <libcockatrice/settings/cards_display_settings.h>
#include <libcockatrice/utility/macros.h> #include <libcockatrice/utility/macros.h>
class UserInterfaceSettingsPage : public AbstractSettingsPage class UserInterfaceSettingsPage : public AbstractSettingsPage
@ -15,6 +17,7 @@ class UserInterfaceSettingsPage : public AbstractSettingsPage
Q_OBJECT Q_OBJECT
private slots: private slots:
void setNotificationEnabled(QT_STATE_CHANGED_T); void setNotificationEnabled(QT_STATE_CHANGED_T);
void updateCommanderSpellbookUiState();
private: private:
QCheckBox notificationsEnabledCheckBox; QCheckBox notificationsEnabledCheckBox;
@ -39,6 +42,11 @@ private:
QCheckBox visualDeckStorageSelectionAnimationCheckBox; QCheckBox visualDeckStorageSelectionAnimationCheckBox;
QLabel defaultDeckEditorTypeLabel; QLabel defaultDeckEditorTypeLabel;
QComboBox defaultDeckEditorTypeSelector; QComboBox defaultDeckEditorTypeSelector;
QLabel commanderSpellbookIntegrationEnabledLabel;
QComboBox commanderSpellbookIntegrationEnabledSelector;
QLabel commanderSpellbookIntegrationUseOfficialBracketNamesLabel;
QToolButton commanderSpellbookIntegrationUseOfficialBracketNamesExplainer;
QComboBox commanderSpellbookIntegrationBracketNamingSelector;
QLabel rewindBufferingMsLabel; QLabel rewindBufferingMsLabel;
QSpinBox rewindBufferingMsBox; QSpinBox rewindBufferingMsBox;
QGroupBox *generalGroupBox; QGroupBox *generalGroupBox;

View file

@ -44,7 +44,7 @@
#include <libcockatrice/protocol/pb/command_deck_upload.pb.h> #include <libcockatrice/protocol/pb/command_deck_upload.pb.h>
#include <libcockatrice/protocol/pb/response.pb.h> #include <libcockatrice/protocol/pb/response.pb.h>
#include <libcockatrice/protocol/pending_command.h> #include <libcockatrice/protocol/pending_command.h>
#include <libcockatrice/settings/interface_settings.h> #include <libcockatrice/settings/deck_editor_settings.h>
#include <libcockatrice/settings/paths_settings.h> #include <libcockatrice/settings/paths_settings.h>
#include <libcockatrice/settings/recents_settings.h> #include <libcockatrice/settings/recents_settings.h>
#include <libcockatrice/utility/string_limits.h> #include <libcockatrice/utility/string_limits.h>
@ -203,7 +203,7 @@ void AbstractTabDeckEditor::cleanDeckAndResetModified()
*/ */
AbstractTabDeckEditor::DeckOpenLocation AbstractTabDeckEditor::confirmOpen(const bool openInSameTabIfBlank) AbstractTabDeckEditor::DeckOpenLocation AbstractTabDeckEditor::confirmOpen(const bool openInSameTabIfBlank)
{ {
if (SettingsCache::instance().interface().getOpenDeckInNewTab()) { if (SettingsCache::instance().deckEditor().getOpenDeckInNewTab()) {
if (openInSameTabIfBlank && deckStateManager->isBlankNewDeck()) { if (openInSameTabIfBlank && deckStateManager->isBlankNewDeck()) {
return SAME_TAB; return SAME_TAB;
} else { } else {

View file

@ -2,12 +2,12 @@
#include "../../../../../client/settings/cache_settings.h" #include "../../../../../client/settings/cache_settings.h"
#include "../../../cards/additional_info/mana_symbol_widget.h" #include "../../../cards/additional_info/mana_symbol_widget.h"
#include "../../../utility/completer_utils.h"
#include "../../tab_supervisor.h" #include "../../tab_supervisor.h"
#include "api_response/archidekt_deck_listing_api_response.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_display_widget.h"
#include "display/archidekt_api_response_deck_listings_display_widget.h" #include "display/archidekt_api_response_deck_listings_display_widget.h"
#include <QCompleter>
#include <QDebug> #include <QDebug>
#include <QFormLayout> #include <QFormLayout>
#include <QGridLayout> #include <QGridLayout>
@ -19,7 +19,6 @@
#include <QNetworkAccessManager> #include <QNetworkAccessManager>
#include <QNetworkReply> #include <QNetworkReply>
#include <QPushButton> #include <QPushButton>
#include <QRegularExpression>
#include <QResizeEvent> #include <QResizeEvent>
#include <QScrollArea> #include <QScrollArea>
#include <QScrollBar> #include <QScrollBar>
@ -27,7 +26,7 @@
#include <libcockatrice/card/database/card_database_manager.h> #include <libcockatrice/card/database/card_database_manager.h>
#include <libcockatrice/models/database/card/card_completer_proxy_model.h> #include <libcockatrice/models/database/card/card_completer_proxy_model.h>
#include <libcockatrice/models/database/card/card_search_model.h> #include <libcockatrice/models/database/card/card_search_model.h>
#include <libcockatrice/settings/visual_deck_storage_settings.h> #include <libcockatrice/settings/cards_display_settings.h>
#include <version_string.h> #include <version_string.h>
TabArchidekt::TabArchidekt(TabSupervisor *_tabSupervisor) TabArchidekt::TabArchidekt(TabSupervisor *_tabSupervisor)
@ -132,8 +131,8 @@ void TabArchidekt::initializeUi()
// Settings // Settings
settingsButton = new SettingsButtonWidget(primaryToolbar); settingsButton = new SettingsButtonWidget(primaryToolbar);
cardSizeSlider = new CardSizeWidget(primaryToolbar, nullptr, cardSizeSlider =
SettingsCache::instance().visualDeckStorage().getArchidektPreviewSize()); new CardSizeWidget(primaryToolbar, nullptr, SettingsCache::instance().cardsDisplay().getArchidektPreviewSize());
settingsButton->addSettingsWidget(cardSizeSlider); settingsButton->addSettingsWidget(cardSizeSlider);
// Assemble primary toolbar // Assemble primary toolbar
@ -279,41 +278,14 @@ void TabArchidekt::setupFilterWidgets()
auto cardDatabaseModel = new CardDatabaseModel(CardDatabaseManager::getInstance(), false, this); auto cardDatabaseModel = new CardDatabaseModel(CardDatabaseManager::getInstance(), false, this);
auto displayModel = new CardDatabaseDisplayModel(this); auto displayModel = new CardDatabaseDisplayModel(this);
displayModel->setSourceModel(cardDatabaseModel); displayModel->setSourceModel(cardDatabaseModel);
auto *searchModel = new CardSearchModel(displayModel, this);
auto *proxyModel = new CardCompleterProxyModel(this); const CardCompleterSetup cardSetup = createCardCompleter(displayModel, this);
proxyModel->setSourceModel(searchModel); cardsField->setCompleter(cardSetup.completer);
proxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive); commandersField->setCompleter(cardSetup.completer);
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);
// Keep autocomplete working for both fields // Keep autocomplete working for both fields
connect(cardsField, &QLineEdit::textChanged, this, [=](const QString &text) { connectCardCompleterSearch(cardsField, cardSetup);
searchModel->updateSearchResults(text); connectCardCompleterSearch(commandersField, cardSetup);
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();
}
});
// Assemble secondary toolbar // Assemble secondary toolbar
secondaryToolbarLayout->addWidget(bracketLabel); secondaryToolbarLayout->addWidget(bracketLabel);
@ -339,8 +311,8 @@ void TabArchidekt::connectSignals()
doSearch(); doSearch();
}); });
connect(cardSizeSlider, &CardSizeWidget::cardSizeSettingUpdated, &SettingsCache::instance().visualDeckStorage(), connect(cardSizeSlider, &CardSizeWidget::cardSizeSettingUpdated, &SettingsCache::instance().cardsDisplay(),
&VisualDeckStorageSettings::setArchidektPreviewCardSize); &CardsDisplaySettings::setArchidektPreviewCardSize);
// Search button triggers immediate search // Search button triggers immediate search
connect(searchButton, &QPushButton::clicked, this, &TabArchidekt::doSearchImmediate); connect(searchButton, &QPushButton::clicked, this, &TabArchidekt::doSearchImmediate);

View file

@ -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;
}

View file

@ -0,0 +1,14 @@
#ifndef COCKATRICE_CARD_IN_DECK_REQUEST_H
#define COCKATRICE_CARD_IN_DECK_REQUEST_H
#include <QJsonObject>
struct CardInDeckRequest
{
static CardInDeckRequest fromJson(const QJsonObject &json);
QJsonObject toJson() const;
QString card;
int quantity;
};
#endif // COCKATRICE_CARD_IN_DECK_REQUEST_H

View file

@ -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;
}

View file

@ -0,0 +1,29 @@
#ifndef COCKATRICE_COMMANDER_SPELLBOOK_CARD_RESULT_H
#define COCKATRICE_COMMANDER_SPELLBOOK_CARD_RESULT_H
#include <QJsonObject>
#include <QString>
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

View file

@ -0,0 +1,95 @@
#include "commander_spellbook_deck_request.h"
#include <QJsonArray>
#include <libcockatrice/deck_list/tree/deck_list_card_node.h>
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;
}

View file

@ -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 <QJsonObject>
#include <QList>
struct CommanderSpellbookDeckRequest
{
static CommanderSpellbookDeckRequest fromJson(const QJsonObject &json);
static CommanderSpellbookDeckRequest fromDeckList(const DeckList &deck);
QJsonObject toJson() const;
QList<CardInDeckRequest> mainDeck; // maxItems: 600
QList<CardInDeckRequest> commanderDeck; // maxItems: 12
};
#endif // COCKATRICE_COMMANDER_SPELLBOOK_DECK_REQUEST_H

View file

@ -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;
}

View file

@ -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 <QJsonObject>
#include <QList>
struct EstimateBracketResult
{
static EstimateBracketResult fromJson(const QJsonObject &json);
QString bracketTag;
QList<CommanderSpellbookCardResult> gameChangerCards;
QList<CommanderSpellbookCardResult> massLandDenialCards;
QList<CommanderSpellbookCardResult> extraTurnCards;
QList<CommanderSpellbookVariantResult> massLandDenialTemplates;
QList<CommanderSpellbookVariantResult> extraTurnTemplates;
QList<CommanderSpellbookVariantResult> massLandDenialCombos;
QList<CommanderSpellbookVariantResult> extraTurnCombos;
QList<CommanderSpellbookVariantResult> lockCombos;
QList<CommanderSpellbookVariantResult> skipTurnsCombos;
QList<CommanderSpellbookVariantResult> definitelyTwoCardCombos;
QList<CommanderSpellbookVariantResult> arguablyTwoCardCombos;
};
#endif // COCKATRICE_COMMANDER_SPELLBOOK_ESTIMATE_BRACKET_RESULT_H

View file

@ -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;
}

View file

@ -0,0 +1,39 @@
#ifndef COCKATRICE_COMMANDER_SPELLBOOK_VARIANT_RESULT_H
#define COCKATRICE_COMMANDER_SPELLBOOK_VARIANT_RESULT_H
#include <QJsonArray>
#include <QJsonObject>
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

View file

@ -0,0 +1,53 @@
#include "commander_bracket_service.h"
#include "../../../../../client/settings/cache_settings.h"
#include <libcockatrice/settings/commander_bracket_settings.h>
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);
}

View file

@ -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 <QObject>
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

View file

@ -0,0 +1,269 @@
#include "commander_bracket_widget.h"
#include "../../../../../client/settings/cache_settings.h"
#include "commander_bracket_service.h"
#include <QComboBox>
#include <QDialog>
#include <QDialogButtonBox>
#include <QFormLayout>
#include <QGridLayout>
#include <QHBoxLayout>
#include <QLabel>
#include <QPushButton>
#include <QTimer>
#include <QToolButton>
#include <QVBoxLayout>
#include <libcockatrice/settings/deck_editor_settings.h>
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<DeckList> &_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 &section : lastBracketExplanation.sections) {
tooltip += "<b>" + section.title + "</b><br>";
for (const auto &line : section.bulletPoints) {
tooltip += "• " + line + "<br>";
}
tooltip += "<br>";
}
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"));
}

View file

@ -0,0 +1,46 @@
#ifndef COCKATRICE_COMMANDER_BRACKET_WIDGET_H
#define COCKATRICE_COMMANDER_BRACKET_WIDGET_H
#include "commander_spellbook_bracket_explainer.h"
#include <QSharedPointer>
#include <QWidget>
#include <libcockatrice/deck_list/deck_list.h>
class QLabel;
class QToolButton;
struct CommanderBracketEstimate;
class CommanderBracketWidget : public QWidget
{
Q_OBJECT
public:
explicit CommanderBracketWidget(QWidget *parent = nullptr);
void setDeck(const QSharedPointer<DeckList> &_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<DeckList> deck;
bool prompting = false;
quint64 requestId = 0;
QLabel *bracketLabel;
QLabel *bracketValueLabel;
QToolButton *bracketInfoButton;
QToolButton *bracketRefreshButton;
BracketExplanation lastBracketExplanation;
};
#endif // COCKATRICE_COMMANDER_BRACKET_WIDGET_H

View file

@ -0,0 +1,75 @@
#include "commander_spellbook_api_accessor.h"
#include "api_response/commander_spellbook_deck_request.h"
#include <QJsonDocument>
#include <QJsonObject>
#include <QNetworkRequest>
#include <QUrl>
#include <version_string.h>
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<QObject *>();
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);
}

View file

@ -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 <QNetworkAccessManager>
#include <QNetworkReply>
#include <QObject>
#include <libcockatrice/deck_list/deck_list.h>
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

View file

@ -0,0 +1,120 @@
#include "commander_spellbook_bracket_explainer.h"
static QString cardList(const QList<CommanderSpellbookCardResult> &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<CommanderSpellbookVariantResult> &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;
}

View file

@ -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<BracketExplanationSection> sections;
bool isEmpty() const
{
return sections.isEmpty();
}
};
class BracketExplainer
{
public:
static BracketExplanation explain(const EstimateBracketResult &result);
};
#endif // COCKATRICE_COMMANDER_SPELLBOOK_BRACKET_EXPLAINER_H

View file

@ -0,0 +1,62 @@
#include "handle_commander_brackets.h"
#include "../../../../../client/settings/cache_settings.h"
#include <QJsonDocument>
#include <QJsonObject>
#include <QUrl>
#include <libcockatrice/settings/commander_bracket_settings.h>
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<QNetworkReply *>(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);
}

View file

@ -0,0 +1,31 @@
#ifndef COCKATRICE_HANDLE_COMMANDER_BRACKETS_H
#define COCKATRICE_HANDLE_COMMANDER_BRACKETS_H
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QObject>
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

View file

@ -1,6 +1,7 @@
#include "tab_edhrec_main.h" #include "tab_edhrec_main.h"
#include "../../../../../client/settings/cache_settings.h" #include "../../../../../client/settings/cache_settings.h"
#include "../../../utility/completer_utils.h"
#include "../../tab_supervisor.h" #include "../../tab_supervisor.h"
#include "api_response/average_deck/edhrec_average_deck_api_response.h" #include "api_response/average_deck/edhrec_average_deck_api_response.h"
#include "api_response/commander/edhrec_commander_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_commander/edhrec_top_commanders_api_response_display_widget.h"
#include "display/top_tags/edhrec_top_tags_api_response_display_widget.h" #include "display/top_tags/edhrec_top_tags_api_response_display_widget.h"
#include <QCompleter>
#include <QDebug> #include <QDebug>
#include <QHBoxLayout> #include <QHBoxLayout>
#include <QJsonArray> #include <QJsonArray>
@ -25,7 +25,7 @@
#include <libcockatrice/card/database/card_database_manager.h> #include <libcockatrice/card/database/card_database_manager.h>
#include <libcockatrice/models/database/card/card_completer_proxy_model.h> #include <libcockatrice/models/database/card/card_completer_proxy_model.h>
#include <libcockatrice/models/database/card/card_search_model.h> #include <libcockatrice/models/database/card/card_search_model.h>
#include <libcockatrice/settings/visual_deck_storage_settings.h> #include <libcockatrice/settings/cards_display_settings.h>
#include <version_string.h> #include <version_string.h>
static bool canBeCommander(const CardInfoPtr &cardInfo) 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 cardDatabaseModel = new CardDatabaseModel(CardDatabaseManager::getInstance(), false, this);
auto displayModel = new CardDatabaseDisplayModel(this); auto displayModel = new CardDatabaseDisplayModel(this);
displayModel->setSourceModel(cardDatabaseModel); displayModel->setSourceModel(cardDatabaseModel);
auto *searchModel = new CardSearchModel(displayModel, this);
auto *proxyModel = new CardCompleterProxyModel(this); const CardCompleterSetup cardSetup = createCardCompleter(displayModel, this);
proxyModel->setSourceModel(searchModel); searchBar->setCompleter(cardSetup.completer);
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);
// Update suggestions dynamically // Update suggestions dynamically
connect(searchBar, &QLineEdit::textChanged, searchModel, &CardSearchModel::updateSearchResults); connectCardCompleterSearch(searchBar, cardSetup);
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
}
});
searchPushButton = new QPushButton(navigationContainer); searchPushButton = new QPushButton(navigationContainer);
connect(searchPushButton, &QPushButton::clicked, this, [=, this]() { doSearch(); }); connect(searchPushButton, &QPushButton::clicked, this, [=, this]() { doSearch(); });
settingsButton = new SettingsButtonWidget(this); settingsButton = new SettingsButtonWidget(this);
cardSizeSlider = cardSizeSlider = new CardSizeWidget(this, nullptr, SettingsCache::instance().cardsDisplay().getEDHRecCardSize());
new CardSizeWidget(this, nullptr, SettingsCache::instance().visualDeckStorage().getEDHRecCardSize()); connect(cardSizeSlider, &CardSizeWidget::cardSizeSettingUpdated, &SettingsCache::instance().cardsDisplay(),
connect(cardSizeSlider, &CardSizeWidget::cardSizeSettingUpdated, &SettingsCache::instance().visualDeckStorage(), &CardsDisplaySettings::setEDHRecCardSize);
&VisualDeckStorageSettings::setEDHRecCardSize);
settingsButton->addSettingsWidget(cardSizeSlider); settingsButton->addSettingsWidget(cardSizeSlider);

View file

@ -135,6 +135,7 @@ void TabAccount::retranslateUi()
void TabAccount::processListUsersResponse(const Response &response) void TabAccount::processListUsersResponse(const Response &response)
{ {
const Response_ListUsers &resp = response.GetExtension(Response_ListUsers::ext); const Response_ListUsers &resp = response.GetExtension(Response_ListUsers::ext);
allUsersList->beginBulkLoad();
for (int i = 0; i < resp.user_list_size(); ++i) { for (int i = 0; i < resp.user_list_size(); ++i) {
const ServerInfo_User &info = resp.user_list(i); const ServerInfo_User &info = resp.user_list(i);
const QString &userName = QString::fromStdString(info.name()); const QString &userName = QString::fromStdString(info.name());
@ -142,8 +143,8 @@ void TabAccount::processListUsersResponse(const Response &response)
ignoreList->setUserOnline(userName, true); ignoreList->setUserOnline(userName, true);
buddyList->setUserOnline(userName, true); buddyList->setUserOnline(userName, true);
} }
allUsersList->endBulkLoad();
allUsersList->sortItems();
ignoreList->sortItems(); ignoreList->sortItems();
buddyList->sortItems(); buddyList->sortItems();
} }
@ -188,18 +189,20 @@ void TabAccount::processUserLeftEvent(const Event_UserLeft &event)
void TabAccount::buddyListReceived(const QList<ServerInfo_User> &_buddyList) void TabAccount::buddyListReceived(const QList<ServerInfo_User> &_buddyList)
{ {
buddyList->beginBulkLoad();
for (const auto &user : _buddyList) { for (const auto &user : _buddyList) {
buddyList->processUserInfo(user, false); buddyList->processUserInfo(user, false);
} }
buddyList->sortItems(); buddyList->endBulkLoad();
} }
void TabAccount::ignoreListReceived(const QList<ServerInfo_User> &_ignoreList) void TabAccount::ignoreListReceived(const QList<ServerInfo_User> &_ignoreList)
{ {
ignoreList->beginBulkLoad();
for (const auto &user : _ignoreList) { for (const auto &user : _ignoreList) {
ignoreList->processUserInfo(user, false); ignoreList->processUserInfo(user, false);
} }
ignoreList->sortItems(); ignoreList->endBulkLoad();
} }
void TabAccount::processAddToListEvent(const Event_AddToList &event) void TabAccount::processAddToListEvent(const Event_AddToList &event)

View file

@ -1,5 +1,6 @@
#include "tab_card_art_rules.h" #include "tab_card_art_rules.h"
#include "../utility/completer_utils.h"
#include "libcockatrice/card/database/card_database_manager.h" #include "libcockatrice/card/database/card_database_manager.h"
#include <QCompleter> #include <QCompleter>
@ -194,29 +195,14 @@ void TabCardArtRules::initSearchBar()
cardDbModel = new CardDatabaseModel(CardDatabaseManager::getInstance(), false, this); cardDbModel = new CardDatabaseModel(CardDatabaseManager::getInstance(), false, this);
cardDbDisplayModel = new CardDatabaseDisplayModel(this); cardDbDisplayModel = new CardDatabaseDisplayModel(this);
cardDbDisplayModel->setSourceModel(cardDbModel); cardDbDisplayModel->setSourceModel(cardDbModel);
cardSearchModel = new CardSearchModel(cardDbDisplayModel, this);
cardProxyModel = new CardCompleterProxyModel(this); const CardCompleterSetup cardSetup = createCardCompleter(cardDbDisplayModel, this, 15);
cardProxyModel->setSourceModel(cardSearchModel); cardSearchModel = cardSetup.searchModel;
cardProxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive); cardProxyModel = cardSetup.proxyModel;
searchCompleter = cardSetup.completer;
searchCompleter = new QCompleter(cardProxyModel, this);
searchCompleter->setCompletionRole(Qt::DisplayRole);
searchCompleter->setCompletionMode(QCompleter::PopupCompletion);
searchCompleter->setCaseSensitivity(Qt::CaseInsensitive);
searchCompleter->setFilterMode(Qt::MatchContains);
searchCompleter->setMaxVisibleItems(15);
searchEdit->setCompleter(searchCompleter); searchEdit->setCompleter(searchCompleter);
connect(searchEdit, &QLineEdit::textEdited, cardSearchModel, &CardSearchModel::updateSearchResults); connectCardCompleterSearch(searchEdit, cardSetup);
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();
}
});
connect(searchCompleter, static_cast<void (QCompleter::*)(const QString &)>(&QCompleter::activated), this, connect(searchCompleter, static_cast<void (QCompleter::*)(const QString &)>(&QCompleter::activated), this,
[this](const QString &name) { searchEdit->setText(name); }); [this](const QString &name) { searchEdit->setText(name); });
connect(searchEdit, &QLineEdit::editingFinished, this, connect(searchEdit, &QLineEdit::editingFinished, this,

View file

@ -21,10 +21,15 @@
#include "../interface/widgets/cards/card_info_frame_widget.h" #include "../interface/widgets/cards/card_info_frame_widget.h"
#include "../interface/widgets/dialogs/dlg_create_game.h" #include "../interface/widgets/dialogs/dlg_create_game.h"
#include "../interface/widgets/server/user/user_list_manager.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/widgets/utility/line_edit_completer.h"
#include "../interface/window_main.h" #include "../interface/window_main.h"
#include "../main.h" #include "../main.h"
#include "../utility/visibility_change_listener.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 "tab_supervisor.h"
#include <QAction> #include <QAction>
@ -35,7 +40,9 @@
#include <QLabel> #include <QLabel>
#include <QMenu> #include <QMenu>
#include <QMessageBox> #include <QMessageBox>
#include <QRegularExpression>
#include <QStackedWidget> #include <QStackedWidget>
#include <QStringListModel>
#include <QTimer> #include <QTimer>
#include <QWidget> #include <QWidget>
#include <libcockatrice/card/database/card_database.h> #include <libcockatrice/card/database/card_database.h>
@ -258,7 +265,7 @@ void TabGame::resetChatAndPhase()
void TabGame::emitUserEvent() void TabGame::emitUserEvent()
{ {
bool globalEvent = !game->getPlayerManager()->isSpectator() || bool globalEvent = !game->getPlayerManager()->isSpectator() ||
SettingsCache::instance().interface().getSpectatorNotificationsEnabled(); SettingsCache::instance().userInterface().getSpectatorNotificationsEnabled();
emit userEvent(globalEvent); emit userEvent(globalEvent);
updatePlayerListDockTitle(); updatePlayerListDockTitle();
} }
@ -538,7 +545,8 @@ bool TabGame::leaveGame()
void TabGame::actSay() void TabGame::actSay()
{ {
if (completer->popup()->isVisible()) { if (sayEdit->hasVisibleCompleterPopup()) {
sayEdit->hideCompleterPopups();
return; return;
} }
@ -558,14 +566,14 @@ void TabGame::addPlayerToAutoCompleteList(QString playerName)
{ {
if (sayEdit && !autocompleteUserList.contains(playerName)) { if (sayEdit && !autocompleteUserList.contains(playerName)) {
autocompleteUserList << playerName; autocompleteUserList << playerName;
sayEdit->setCompletionList(autocompleteUserList); mentionModel->setStringList(autocompleteUserList);
} }
} }
void TabGame::removePlayerFromAutoCompleteList(QString playerName) void TabGame::removePlayerFromAutoCompleteList(QString playerName)
{ {
if (sayEdit && autocompleteUserList.removeOne(playerName)) { if (sayEdit && autocompleteUserList.removeOne(playerName)) {
sayEdit->setCompletionList(autocompleteUserList); mentionModel->setStringList(autocompleteUserList);
} }
} }
@ -628,8 +636,8 @@ void TabGame::actRotateViewCCW()
void TabGame::actCompleterChanged() void TabGame::actCompleterChanged()
{ {
SettingsCache::instance().chat().getChatMentionCompleter() ? completer->setCompletionRole(2) SettingsCache::instance().chat().getChatMentionCompleter() ? mentionCompleter->setCompletionRole(2)
: completer->setCompletionRole(1); : mentionCompleter->setCompletionRole(1);
} }
void TabGame::notifyPlayerJoin(QString playerName) void TabGame::notifyPlayerJoin(QString playerName)
@ -1281,12 +1289,25 @@ void TabGame::createMessageDock(bool bReplay)
sayEdit->setMaxLength(MAX_TEXT_LENGTH); sayEdit->setMaxLength(MAX_TEXT_LENGTH);
sayLabel->setBuddy(sayEdit); sayLabel->setBuddy(sayEdit);
connect(this, &TabGame::chatMessageSent, game->getGameEventHandler(), &GameEventHandler::handleChatMessageSent); connect(this, &TabGame::chatMessageSent, game->getGameEventHandler(), &GameEventHandler::handleChatMessageSent);
completer = new QCompleter(autocompleteUserList, sayEdit); mentionModel = new QStringListModel(autocompleteUserList, sayEdit);
completer->setCaseSensitivity(Qt::CaseInsensitive); mentionCompleter = createMentionCompleter(mentionModel, sayEdit);
completer->setMaxVisibleItems(5); sayEdit->addCompleter(mentionCompleter, CompleterTrigger::Mention);
completer->setFilterMode(Qt::MatchStartsWith);
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(); actCompleterChanged();
if (game->getPlayerManager()->isSpectator()) { if (game->getPlayerManager()->isSpectator()) {

View file

@ -19,6 +19,7 @@
#include <QCompleter> #include <QCompleter>
#include <QLoggingCategory> #include <QLoggingCategory>
#include <QMap> #include <QMap>
#include <QStringListModel>
class CardMenu; class CardMenu;
class ServerInfo_PlayerProperties; class ServerInfo_PlayerProperties;
@ -61,7 +62,8 @@ private:
const UserListProxy *userListProxy; const UserListProxy *userListProxy;
ReplayWidget *replayWidget = nullptr; ReplayWidget *replayWidget = nullptr;
QStringList gameTypes; QStringList gameTypes;
QCompleter *completer; QCompleter *mentionCompleter;
QStringListModel *mentionModel;
QStringList autocompleteUserList; QStringList autocompleteUserList;
QStackedWidget *mainWidget; QStackedWidget *mainWidget;

View file

@ -8,6 +8,11 @@
#include "../interface/widgets/server/user/user_list_manager.h" #include "../interface/widgets/server/user/user_list_manager.h"
#include "../interface/widgets/server/user/user_list_widget.h" #include "../interface/widgets/server/user/user_list_widget.h"
#include "../main.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_account.h"
#include "tab_supervisor.h" #include "tab_supervisor.h"
@ -17,11 +22,14 @@
#include <QMenu> #include <QMenu>
#include <QMessageBox> #include <QMessageBox>
#include <QPushButton> #include <QPushButton>
#include <QRegularExpression>
#include <QSplitter> #include <QSplitter>
#include <QStringListModel>
#include <QSystemTrayIcon> #include <QSystemTrayIcon>
#include <QToolButton> #include <QToolButton>
#include <QVBoxLayout> #include <QVBoxLayout>
#include <QtCore/qdatetime.h> #include <QtCore/qdatetime.h>
#include <libcockatrice/card/database/card_database_manager.h>
#include <libcockatrice/network/client/abstract/abstract_client.h> #include <libcockatrice/network/client/abstract/abstract_client.h>
#include <libcockatrice/protocol/get_pb_extension.h> #include <libcockatrice/protocol/get_pb_extension.h>
#include <libcockatrice/protocol/pb/event_join_room.pb.h> #include <libcockatrice/protocol/pb/event_join_room.pb.h>
@ -137,13 +145,27 @@ TabRoom::TabRoom(TabSupervisor *_tabSupervisor,
gameSelector->processGameInfo(info.game_list(i)); gameSelector->processGameInfo(info.game_list(i));
} }
completer = new QCompleter(autocompleteUserList, sayEdit); mentionModel = new QStringListModel(autocompleteUserList, sayEdit);
completer->setCaseSensitivity(Qt::CaseInsensitive); mentionCompleter = createMentionCompleter(mentionModel, sayEdit);
completer->setMaxVisibleItems(5); sayEdit->addCompleter(mentionCompleter, CompleterTrigger::Mention);
completer->setFilterMode(Qt::MatchStartsWith);
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(); actCompleterChanged();
connect(&SettingsCache::instance().shortcuts(), &ShortcutsSettings::shortCutChanged, this, connect(&SettingsCache::instance().shortcuts(), &ShortcutsSettings::shortCutChanged, this,
&TabRoom::refreshShortcuts); &TabRoom::refreshShortcuts);
refreshShortcuts(); refreshShortcuts();
@ -213,8 +235,8 @@ void TabRoom::sendMessage()
{ {
if (sayEdit->text().isEmpty()) { if (sayEdit->text().isEmpty()) {
return; return;
} else if (completer->popup()->isVisible()) { } else if (sayEdit->hasVisibleCompleterPopup()) {
completer->popup()->hide(); sayEdit->hideCompleterPopups();
return; return;
} else { } else {
Command_RoomSay cmd; Command_RoomSay cmd;
@ -242,14 +264,14 @@ void TabRoom::actClearChat()
void TabRoom::actOpenChatSettings() void TabRoom::actOpenChatSettings()
{ {
DlgSettings settings(this); DlgSettings settings(this);
settings.setTab(4); settings.setTab(DlgSettings::MessagesPage);
settings.exec(); settings.exec();
} }
void TabRoom::actCompleterChanged() void TabRoom::actCompleterChanged()
{ {
SettingsCache::instance().chat().getChatMentionCompleter() ? completer->setCompletionRole(2) SettingsCache::instance().chat().getChatMentionCompleter() ? mentionCompleter->setCompletionRole(2)
: completer->setCompletionRole(1); : mentionCompleter->setCompletionRole(1);
} }
void TabRoom::processRoomEvent(const RoomEvent &event) void TabRoom::processRoomEvent(const RoomEvent &event)
@ -285,16 +307,18 @@ void TabRoom::processListGamesEvent(const Event_ListGames &event)
void TabRoom::processJoinRoomEvent(const Event_JoinRoom &event) void TabRoom::processJoinRoomEvent(const Event_JoinRoom &event)
{ {
if (!autocompleteUserList.contains("@" + QString::fromStdString(event.user_info().name()))) { QString mention = "@" + QString::fromStdString(event.user_info().name());
autocompleteUserList << "@" + QString::fromStdString(event.user_info().name()); if (!autocompleteUserList.contains(mention)) {
sayEdit->setCompletionList(autocompleteUserList); autocompleteUserList << mention;
mentionModel->setStringList(autocompleteUserList);
} }
} }
void TabRoom::processLeaveRoomEvent(const Event_LeaveRoom &event) void TabRoom::processLeaveRoomEvent(const Event_LeaveRoom &event)
{ {
autocompleteUserList.removeOne("@" + QString::fromStdString(event.name())); QString mention = "@" + QString::fromStdString(event.name());
sayEdit->setCompletionList(autocompleteUserList); autocompleteUserList.removeOne(mention);
mentionModel->setStringList(autocompleteUserList);
} }
void TabRoom::processRoomSayEvent(const Event_RoomSay &event) void TabRoom::processRoomSayEvent(const Event_RoomSay &event)

View file

@ -14,6 +14,7 @@
#include <QFocusEvent> #include <QFocusEvent>
#include <QGroupBox> #include <QGroupBox>
#include <QMap> #include <QMap>
#include <QStringListModel>
class UserListProxy; class UserListProxy;
class UserListManager; class UserListManager;
@ -63,6 +64,7 @@ private:
ChatView *chatView; ChatView *chatView;
QLabel *sayLabel; QLabel *sayLabel;
LineEditCompleter *sayEdit; LineEditCompleter *sayEdit;
QStringListModel *mentionModel;
QGroupBox *chatGroupBox; QGroupBox *chatGroupBox;
QMenu *roomMenu; QMenu *roomMenu;
@ -72,7 +74,7 @@ private:
[[nodiscard]] QString sanitizeHtml(QString dirty) const; [[nodiscard]] QString sanitizeHtml(QString dirty) const;
QStringList autocompleteUserList; QStringList autocompleteUserList;
QCompleter *completer; QCompleter *mentionCompleter;
signals: signals:
void roomClosing(TabRoom *tab); void roomClosing(TabRoom *tab);
void openMessageDialog(const QString &userName, bool focus); void openMessageDialog(const QString &userName, bool focus);

View file

@ -39,9 +39,9 @@
#include <libcockatrice/protocol/pb/serverinfo_room.pb.h> #include <libcockatrice/protocol/pb/serverinfo_room.pb.h>
#include <libcockatrice/protocol/pb/serverinfo_user.pb.h> #include <libcockatrice/protocol/pb/serverinfo_user.pb.h>
#include <libcockatrice/settings/chat_settings.h> #include <libcockatrice/settings/chat_settings.h>
#include <libcockatrice/settings/deck_editor_settings.h>
#include <libcockatrice/settings/interface_settings.h> #include <libcockatrice/settings/interface_settings.h>
#include <libcockatrice/settings/tabs_settings.h> #include <libcockatrice/settings/tabs_settings.h>
#include <libcockatrice/settings/visual_deck_storage_settings.h>
QRect MacOSTabFixStyle::subElementRect(SubElement element, const QStyleOption *option, const QWidget *widget) const QRect MacOSTabFixStyle::subElementRect(SubElement element, const QStyleOption *option, const QWidget *widget) const
{ {
@ -583,6 +583,7 @@ void TabSupervisor::actTabServer(bool checked)
void TabSupervisor::openTabServer() void TabSupervisor::openTabServer()
{ {
SettingsCache::instance().tabs().setTabServerOpen(true);
if (tabServer) { if (tabServer) {
return; return;
} }
@ -910,7 +911,7 @@ void TabSupervisor::talkLeft(TabMessage *tab)
*/ */
void TabSupervisor::openDeckInNewTab(const LoadedDeck &deckToOpen) void TabSupervisor::openDeckInNewTab(const LoadedDeck &deckToOpen)
{ {
int type = SettingsCache::instance().visualDeckStorage().getDefaultDeckEditorType(); int type = SettingsCache::instance().deckEditor().getDefaultDeckEditorType();
switch (type) { switch (type) {
case ClassicDeckEditor: case ClassicDeckEditor:
addDeckEditorTab(deckToOpen); addDeckEditorTab(deckToOpen);
@ -1009,7 +1010,7 @@ void TabSupervisor::tabUserEvent(bool globalEvent)
tab->setContentsChanged(true); tab->setContentsChanged(true);
setTabIcon(indexOf(tab), QPixmap("theme:icons/tab_changed")); setTabIcon(indexOf(tab), QPixmap("theme:icons/tab_changed"));
} }
if (globalEvent && SettingsCache::instance().interface().getNotificationsEnabled()) { if (globalEvent && SettingsCache::instance().userInterface().getNotificationsEnabled()) {
QApplication::alert(this); 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); QApplication::alert(this);
this->actShowPopup(tr("Your buddy %1 has signed on!").arg(userName)); this->actShowPopup(tr("Your buddy %1 has signed on!").arg(userName));
} }

View file

@ -0,0 +1,439 @@
#include "card_completer_delegate.h"
#include "../cards/additional_info/mana_cost_widget.h"
#include <QFontMetrics>
#include <QLinearGradient>
#include <QPainter>
#include <QPainterPath>
#include <QRegularExpression>
#include <QSet>
#include <QStyleOptionViewItem>
#include <libcockatrice/card/card_info.h>
#include <libcockatrice/card/database/card_database_manager.h>
#include <libcockatrice/card/printing/printing_info.h>
#include <libcockatrice/models/database/card/card_search_model.h>
// ---------------------------------------------------------------------------
// 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<QChar> 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<QStringList> 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<CardInfo> &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<QSharedPointer<CardInfo>>();
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();
}

View file

@ -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 <QCache>
#include <QColor>
#include <QPixmap>
#include <QStyledItemDelegate>
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<QString, QPixmap> symbolCache;
// Set short codes, resolved once per card name and cached
mutable QCache<QString, QString> 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<CardInfo> &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

View file

@ -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 <QAbstractItemModel>
#include <QAbstractItemView>
#include <QCompleter>
#include <QEvent>
#include <QKeyEvent>
#include <QMouseEvent>
#include <QPropertyAnimation>
#include <QScreen>
#include <QSize>
#include <libcockatrice/card/card_info.h>
#include <libcockatrice/card/database/card_database_manager.h>
#include <libcockatrice/models/database/card/card_search_model.h>
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<const QString &>(&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<QMouseEvent *>(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<QKeyEvent *>(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<QSharedPointer<CardInfo>>();
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<QPropertyAnimation *>();
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);
}

View file

@ -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 <QModelIndex>
#include <QObject>
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

View file

@ -0,0 +1,52 @@
#include "completer_utils.h"
#include "card_completer_styler.h"
#include <QCompleter>
#include <QLineEdit>
#include <QObject>
#include <QRegularExpression>
#include <QStringListModel>
#include <libcockatrice/models/database/card/card_completer_proxy_model.h>
#include <libcockatrice/models/database/card/card_search_model.h>
#include <libcockatrice/models/database/card_database_display_model.h>
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;
}

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