mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-09-28 08:52:19 -07:00
Merge branch 'master' into small-fixes/0-2725-chat-scroll-bottom
This commit is contained in:
commit
f1888c6d97
176 changed files with 7230 additions and 1728 deletions
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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()))));
|
||||||
|
|
|
||||||
|
|
@ -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 = {
|
||||||
|
.name = cardInfo->getName(),
|
||||||
.color = cardInfo->getColors().isEmpty() ? QString() : cardInfo->getColors().left(1).toLower(),
|
.color = cardInfo->getColors().isEmpty() ? QString() : cardInfo->getColors().left(1).toLower(),
|
||||||
.pt = cardInfo->getPowTough(),
|
.pt = cardInfo->getPowTough(),
|
||||||
.annotation = SettingsCache::instance().interface().getAnnotateTokens() ? cardInfo->getText() : "",
|
.annotation = SettingsCache::instance().userInterface().getAnnotateTokens() ? cardInfo->getText() : "",
|
||||||
.destroy = true,
|
.destroy = true,
|
||||||
.providerId =
|
.providerId = SettingsCache::instance().cardOverrides().getCardPreferenceOverride(cardInfo->getName())};
|
||||||
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("");
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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());
|
||||||
|
|
|
||||||
|
|
@ -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))) {
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -189,7 +189,7 @@ void DlgCreateToken::tokenSelectionChanged(const QModelIndex ¤t, 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 {
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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*/)
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
QString host = request.first.host();
|
||||||
|
int allowance = hostQuotaRemaining.value(host, MAX_REQUESTS_PER_SEC);
|
||||||
|
if (allowance > 0) {
|
||||||
|
hostQuotaRemaining.insert(host, allowance - 1);
|
||||||
makeRequest(request.first, request.second);
|
makeRequest(request.first, request.second);
|
||||||
|
requestLoadQueue.removeAt(i);
|
||||||
return true;
|
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
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
||||||
|
|
@ -112,6 +120,9 @@ private:
|
||||||
|
|
||||||
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
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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:"));
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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"));
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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,12 +993,16 @@ void UserListWidget::processUserInfo(const ServerInfo_User &user, bool online)
|
||||||
++onlineCount;
|
++onlineCount;
|
||||||
}
|
}
|
||||||
updateCount();
|
updateCount();
|
||||||
|
if (!m_bulkLoading && isItemNearViewport(item)) {
|
||||||
avatarProvider->requestAvatar(userName);
|
avatarProvider->requestAvatar(userName);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
item->setOnline(online);
|
item->setOnline(online);
|
||||||
|
if (!m_bulkLoading) {
|
||||||
sortItems();
|
sortItems();
|
||||||
userTree->viewport()->update();
|
userTree->viewport()->update();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
bool UserListWidget::deleteUser(const QString &userName)
|
bool UserListWidget::deleteUser(const QString &userName)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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 ®ex)
|
||||||
|
{
|
||||||
|
QString lowerQuery = query.toLower();
|
||||||
|
|
||||||
|
// Label matches are most relevant
|
||||||
|
QString label = entry.widgetLabel.toLower();
|
||||||
|
if (label.startsWith(lowerQuery)) {
|
||||||
|
return 100;
|
||||||
|
}
|
||||||
|
if (label.contains(lowerQuery)) {
|
||||||
|
return 80;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Group title matches are next
|
||||||
|
QString group = entry.groupTitle.toLower();
|
||||||
|
if (group.startsWith(lowerQuery)) {
|
||||||
|
return 60;
|
||||||
|
}
|
||||||
|
if (group.contains(lowerQuery)) {
|
||||||
|
return 40;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Full text match is least relevant
|
||||||
|
if (entry.fullSearchText.contains(regex)) {
|
||||||
|
return 20;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SettingsSearchModel::rebuildFilter()
|
||||||
|
{
|
||||||
|
beginResetModel();
|
||||||
|
filteredIndices.clear();
|
||||||
|
|
||||||
|
if (!filterActive) {
|
||||||
|
for (int i = 0; i < sourceEntries.size(); ++i) {
|
||||||
|
filteredIndices.append(i);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
QList<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()]];
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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"));
|
||||||
|
|
|
||||||
|
|
@ -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(¬ificationsEnabledCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().interface(),
|
connect(¬ificationsEnabledCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().userInterface(),
|
||||||
&InterfaceSettings::setNotificationsEnabled);
|
&InterfaceSettings::setNotificationsEnabled);
|
||||||
connect(¬ificationsEnabledCheckBox, &QCheckBox::QT_STATE_CHANGED, this,
|
connect(¬ificationsEnabledCheckBox, &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");
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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 §ion : 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"));
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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()) {
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
52
cockatrice/src/interface/widgets/utility/completer_utils.cpp
Normal file
52
cockatrice/src/interface/widgets/utility/completer_utils.cpp
Normal 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
Loading…
Add table
Add a link
Reference in a new issue