diff --git a/.ci/compile.sh b/.ci/compile.sh index 5c2eec894..aa1c71e65 100755 --- a/.ci/compile.sh +++ b/.ci/compile.sh @@ -159,6 +159,7 @@ if [[ $PACKAGE_TYPE ]]; then fi if [[ $USE_VCPKG ]]; then flags+=("-DUSE_VCPKG=1") + flags+=("-DVCPKG_INSTALL_OPTIONS=--x-abi-tools-use-exact-versions") fi # Add CMake --build flags diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 56ad64283..589cae1d8 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -334,7 +334,7 @@ the tr() call, also you can add an extra string as a hint for translators: QString message = tr("Everyone draws %n cards", "english hint for translators", amount); ``` See [Qt's wiki on translations]( -https://doc.qt.io/qt-5/i18n-source-translation.html#handling-plurals) +https://doc.qt.io/qt-6/i18n-source-translation.html#handling-plurals) If you're about to propose a change that adds or modifies any translatable string in the code, you don't need to take care of adding the new strings to diff --git a/CMakeLists.txt b/CMakeLists.txt index 66c03a2d3..108e94da7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -61,10 +61,11 @@ if(WIN32 OR USE_VCPKG) # Windows (including 64bit) or USE_VCPKG set ) set(QTDIR "" - CACHE PATH "Path to Qt (e.g. C:/Qt/5.7/msvc2015_64)" + CACHE PATH "Path to Qt (e.g. C:/Qt/6.4.2/msvc2019_64)" ) message( - WARNING "QTDIR variable is missing. Please set this variable to specify path to Qt (e.g. C:/Qt/5.7/msvc2015_64)" + WARNING + "QTDIR variable is missing. Please set this variable to specify path to Qt (e.g. C:/Qt/6.4.2/msvc2019_64)" ) endif() endif() @@ -99,8 +100,7 @@ include(createversionfile) # Define a proper install path if(UNIX) - if(APPLE) - # macOS + if(APPLE) # macOS # Due to the special bundle structure ignore # the prefix eventually set by the user. set(CMAKE_INSTALL_PREFIX ${CMAKE_BINARY_DIR}/release) @@ -120,8 +120,7 @@ if(UNIX) set(CMAKE_XCODE_ATTRIBUTE_LD "${CMAKE_BINARY_DIR}/launch-c") set(CMAKE_XCODE_ATTRIBUTE_LDPLUSPLUS "${CMAKE_BINARY_DIR}/launch-cxx") endif() - else() - # Linux / BSD + else() # Linux / BSD if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) #fix package build if(PREFIX) @@ -131,7 +130,7 @@ if(UNIX) endif() endif() endif() -elseif(WIN32) +elseif(WIN32) # Windows, including 64 bit set(CMAKE_INSTALL_PREFIX ${CMAKE_BINARY_DIR}/rundir/${CMAKE_BUILD_TYPE}) endif() @@ -254,7 +253,7 @@ set(CPACK_PACKAGE_VERSION_PATCH "${PROJECT_VERSION_PATCH}") set(CPACK_PACKAGE_FILE_NAME "${PROJECT_VERSION_FILENAME}") if(UNIX) - if(APPLE) + if(APPLE) #macOS set(CPACK_GENERATOR DragNDrop ${CPACK_GENERATOR}) set(CPACK_GENERATOR "DragNDrop") set(CPACK_DMG_FORMAT "UDBZ") @@ -264,14 +263,11 @@ if(UNIX) set(CPACK_DMG_DS_STORE_SETUP_SCRIPT "${CMAKE_CURRENT_SOURCE_DIR}/cmake/CMakeDMGSetup.script") set(CPACK_DMG_BACKGROUND_IMAGE "${CMAKE_CURRENT_SOURCE_DIR}/cmake/dmgBackground.tif") set(CPACK_PRE_BUILD_SCRIPTS "${CMAKE_CURRENT_SOURCE_DIR}/cmake/SignMacApplications.cmake") - else() - # linux + else() # Linux if(CPACK_GENERATOR STREQUAL "RPM") set(CPACK_RPM_PACKAGE_LICENSE "GPLv2") set(CPACK_RPM_MAIN_COMPONENT "cockatrice") - if(Qt6_FOUND) - set(CPACK_RPM_PACKAGE_REQUIRES "protobuf, qt6-qttools, qt6-qtsvg, qt6-qtmultimedia, qt6-qtimageformats") - endif() + set(CPACK_RPM_PACKAGE_REQUIRES "protobuf, qt6-qttools, qt6-qtsvg, qt6-qtmultimedia, qt6-qtimageformats") set(CPACK_RPM_PACKAGE_GROUP "Amusements/Games") set(CPACK_RPM_PACKAGE_URL "http://github.com/Cockatrice/Cockatrice") # stop directories from making package conflicts @@ -289,13 +285,11 @@ if(UNIX) set(CPACK_DEBIAN_PACKAGE_SHLIBDEPS ON) set(CPACK_DEBIAN_PACKAGE_SECTION "games") set(CPACK_DEBIAN_PACKAGE_HOMEPAGE "http://github.com/Cockatrice/Cockatrice") - if(Qt6_FOUND) - set(CPACK_DEBIAN_PACKAGE_DEPENDS "libqt6multimedia6, libqt6svg6, qt6-qpa-plugins, qt6-image-formats-plugins") - set(CPACK_DEBIAN_PACKAGE_RECOMMENDS "libqt6sql6-mysql") # for connecting servatrice to a mysql db - endif() + set(CPACK_DEBIAN_PACKAGE_DEPENDS "libqt6multimedia6, libqt6svg6, qt6-qpa-plugins, qt6-image-formats-plugins") + set(CPACK_DEBIAN_PACKAGE_RECOMMENDS "libqt6sql6-mysql") # for connecting servatrice to a mysql db endif() endif() -elseif(WIN32) +elseif(WIN32) # Windows, including 64 bit set(CPACK_GENERATOR NSIS ${CPACK_GENERATOR}) if("${CMAKE_GENERATOR_PLATFORM}" MATCHES "(x64)") set(TRICE_IS_64_BIT 1) @@ -352,6 +346,4 @@ if(TEST) add_subdirectory(tests) endif() -if(Qt6_FOUND) - qt6_finalize_project() -endif() +qt6_finalize_project() diff --git a/cmake/FindQtRuntime.cmake b/cmake/FindQtRuntime.cmake index eecb1fcc8..78c9aa1b0 100644 --- a/cmake/FindQtRuntime.cmake +++ b/cmake/FindQtRuntime.cmake @@ -38,44 +38,38 @@ set(REQUIRED_QT_COMPONENTS ${REQUIRED_QT_COMPONENTS} ${_SERVATRICE_NEEDED} ${_CO ) list(REMOVE_DUPLICATES REQUIRED_QT_COMPONENTS) -# Find Qt and all required components, as well as Linguist +# Find Qt and all required components including Linguist find_package( Qt6 COMPONENTS ${REQUIRED_QT_COMPONENTS} Linguist QUIET HINTS ${Qt6_DIR} ) -if(Qt6_FOUND) - set(COCKATRICE_QT_VERSION_NAME Qt6) - set(CMAKE_POSITION_INDEPENDENT_CODE ON) - - if(Qt6LinguistTools_FOUND) - list(FIND Qt6LinguistTools_TARGETS Qt6::lrelease QT6_LRELEASE_INDEX) - if(QT6_LRELEASE_INDEX EQUAL -1) - message(WARNING "Qt6 lrelease not found.") - endif() - - list(FIND Qt6LinguistTools_TARGETS Qt6::lupdate QT6_LUPDATE_INDEX) - if(QT6_LUPDATE_INDEX EQUAL -1) - message(WARNING "Qt6 lupdate not found.") - endif() - else() - message(WARNING "Linguist Tools not found, cannot handle translations") - endif() -else() - message(FATAL_ERROR "Qt6 not found") +if(NOT Qt6_FOUND) + message(FATAL_ERROR "No suitable version of Qt was found") endif() +set(COCKATRICE_QT_VERSION_NAME Qt6) + +list(FIND Qt6LinguistTools_TARGETS Qt6::lrelease QT6_LRELEASE_INDEX) +if(QT6_LRELEASE_INDEX EQUAL -1) + message(WARNING "Qt6 lrelease not found.") +endif() + +list(FIND Qt6LinguistTools_TARGETS Qt6::lupdate QT6_LUPDATE_INDEX) +if(QT6_LUPDATE_INDEX EQUAL -1) + message(WARNING "Qt6 lupdate not found.") +endif() + +set(CMAKE_POSITION_INDEPENDENT_CODE ON) # Establish Qt Plugins directory & Library directories get_target_property(QT_LIBRARY_DIR ${COCKATRICE_QT_VERSION_NAME}::Core LOCATION) get_filename_component(QT_LIBRARY_DIR ${QT_LIBRARY_DIR} DIRECTORY) -if(Qt6_FOUND) - get_filename_component(QT_PLUGINS_DIR "${Qt6Core_DIR}/../../../${QT6_INSTALL_PLUGINS}" ABSOLUTE) - get_filename_component(QT_LIBRARY_DIR "${QT_LIBRARY_DIR}/../../.." ABSOLUTE) - if(UNIX AND APPLE) - # Mac needs a bit more help finding all necessary components - list(APPEND QT_LIBRARY_DIR "/usr/local/lib") - endif() +get_filename_component(QT_PLUGINS_DIR "${Qt6Core_DIR}/../../../${QT6_INSTALL_PLUGINS}" ABSOLUTE) +get_filename_component(QT_LIBRARY_DIR "${QT_LIBRARY_DIR}/../../.." ABSOLUTE) +if(UNIX AND APPLE) + # Mac needs a bit more help finding all necessary components + list(APPEND QT_LIBRARY_DIR "/usr/local/lib") endif() message(DEBUG "QT_PLUGINS_DIR = ${QT_PLUGINS_DIR}") message(DEBUG "QT_LIBRARY_DIR = ${QT_LIBRARY_DIR}") diff --git a/cmake/Info.plist b/cmake/Info.plist index 614d82509..7f01befcb 100644 --- a/cmake/Info.plist +++ b/cmake/Info.plist @@ -1,38 +1,118 @@ - + + + + + + CFBundleDevelopmentRegion English + CFBundleExecutable ${MACOSX_BUNDLE_EXECUTABLE_NAME} + CFBundleGetInfoString ${MACOSX_BUNDLE_INFO_STRING} + CFBundleIconFile ${MACOSX_BUNDLE_ICON_FILE} + CFBundleIdentifier ${MACOSX_BUNDLE_GUI_IDENTIFIER} + CFBundleInfoDictionaryVersion 6.0 + CFBundleLongVersionString ${MACOSX_BUNDLE_LONG_VERSION_STRING} + CFBundleName ${MACOSX_BUNDLE_BUNDLE_NAME} + CFBundlePackageType APPL + CFBundleShortVersionString ${MACOSX_BUNDLE_SHORT_VERSION_STRING} + CFBundleSignature ???? + CFBundleVersion ${MACOSX_BUNDLE_BUNDLE_VERSION} - CSResourcesFileMapped - - LSRequiresCarbon - + NSHumanReadableCopyright ${MACOSX_BUNDLE_COPYRIGHT} + NSHighResolutionCapable + + + + + + UTExportedTypeDeclarations + + + UTTypeIdentifier + org.cockatrice.deck + + UTTypeDescription + Cockatrice Deck + + UTTypeConformsTo + + public.data + + + UTTypeTagSpecification + + public.filename-extension + + cod + + + + + + CFBundleDocumentTypes + + + CFBundleTypeName + Cockatrice Deck + + CFBundleTypeRole + Editor + + LSHandlerRank + Default + + LSItemContentTypes + + org.cockatrice.deck + + + + + + + + + CFBundleURLTypes + + + CFBundleURLName + Cockatrice URL Scheme + + CFBundleURLSchemes + + cockatrice + + + + diff --git a/cockatrice/CMakeLists.txt b/cockatrice/CMakeLists.txt index a7885d33f..a15fda582 100644 --- a/cockatrice/CMakeLists.txt +++ b/cockatrice/CMakeLists.txt @@ -99,6 +99,7 @@ set(cockatrice_SOURCES src/game_graphics/player/menu/sideboard_menu.cpp src/game_graphics/player/menu/tally_menu.cpp src/game_graphics/player/menu/utility_menu.cpp + src/game_graphics/tally/stats_tally.cpp src/game_graphics/tally/subtype_tally.cpp src/game_graphics/tally/tally.cpp src/game/player/player_actions.cpp @@ -134,6 +135,12 @@ set(cockatrice_SOURCES src/interface/card_picture_loader/card_picture_loader_worker.cpp src/interface/card_picture_loader/card_picture_loader_worker_work.cpp src/interface/card_picture_loader/card_picture_to_load.cpp + src/interface/intents/intent.cpp + src/interface/intents/intent.h + src/interface/intents/intent_open_local_deck.cpp + src/interface/intents/intent_open_local_deck.h + src/interface/intents/intent_wait_for_database_load.cpp + src/interface/intents/intent_wait_for_database_load.h src/interface/layouts/flow_layout.cpp src/interface/layouts/overlap_layout.cpp src/interface/widgets/utility/line_edit_completer.cpp @@ -229,6 +236,7 @@ set(cockatrice_SOURCES src/interface/widgets/printing_selector/set_name_and_collectors_number_display_widget.cpp src/interface/widgets/quick_settings/settings_button_widget.cpp src/interface/widgets/quick_settings/settings_popup_widget.cpp + src/interface/widgets/replay/replay_manager.cpp src/interface/widgets/replay/replay_quick_settings_widget.cpp src/interface/widgets/replay/replay_timeline_widget.cpp src/interface/widgets/replay/replay_widget.cpp @@ -249,10 +257,13 @@ set(cockatrice_SOURCES src/interface/widgets/server/user/user_list_manager.cpp src/interface/widgets/server/user/user_list_painter.cpp src/interface/widgets/server/user/user_list_widget.cpp + src/interface/widgets/settings_page/abstract_settings_page.cpp src/interface/widgets/settings_page/appearance_settings_page.cpp src/interface/widgets/settings_page/deck_editor_settings_page.cpp src/interface/widgets/settings_page/general_settings_page.cpp src/interface/widgets/settings_page/messages_settings_page.cpp + src/interface/widgets/settings_page/settings_search_delegate.cpp + src/interface/widgets/settings_page/settings_search_model.cpp src/interface/widgets/settings_page/shortcut_settings_page.cpp src/interface/widgets/settings_page/sound_settings_page.cpp src/interface/widgets/settings_page/storage_settings_page.cpp @@ -291,6 +302,7 @@ set(cockatrice_SOURCES src/interface/widgets/visual_deck_storage/visual_deck_storage_widget.cpp src/interface/window_main.cpp src/main.cpp + src/single_instance_manager.cpp src/interface/widgets/tabs/abstract_tab_deck_editor.cpp src/interface/widgets/tabs/api/archidekt/tab_archidekt.cpp src/interface/widgets/tabs/api/archidekt/api_response/archidekt_deck_listing_api_response.cpp @@ -306,6 +318,13 @@ set(cockatrice_SOURCES src/interface/widgets/tabs/api/archidekt/display/archidekt_api_response_deck_entry_display_widget.cpp src/interface/widgets/tabs/api/archidekt/display/archidekt_api_response_deck_listings_display_widget.cpp src/interface/widgets/tabs/api/archidekt/display/archidekt_deck_preview_image_display_widget.cpp + src/interface/widgets/tabs/api/commander_spellbook/api_response/card_in_deck_request.cpp + src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_deck_request.cpp + src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_card_result.cpp + src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_variant_result.cpp + src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_estimate_bracket_result.cpp + src/interface/widgets/tabs/api/commander_spellbook/commander_spellbook_bracket_explainer.cpp + src/interface/widgets/tabs/api/commander_spellbook/commander_spellbook_api_accessor.cpp src/interface/widgets/tabs/api/edhrec/api_response/archidekt_links/edhrec_api_response_archidekt_links.cpp src/interface/widgets/tabs/api/edhrec/api_response/average_deck/edhrec_average_deck_api_response.cpp src/interface/widgets/tabs/api/edhrec/api_response/average_deck/edhrec_deck_api_response.cpp @@ -352,12 +371,29 @@ set(cockatrice_SOURCES src/interface/widgets/tabs/visual_deck_storage/tab_deck_storage_visual.cpp src/interface/key_signals.cpp src/interface/logger.cpp + src/interface/widgets/tabs/api/commander_spellbook/commander_bracket_service.cpp + src/interface/widgets/tabs/api/commander_spellbook/commander_bracket_widget.cpp + src/interface/widgets/tabs/api/commander_spellbook/handle_commander_brackets.cpp src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_bracket_navigation_widget.cpp src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_bracket_navigation_widget.h src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_budget_navigation_widget.cpp src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_budget_navigation_widget.h src/interface/widgets/utility/compact_push_button.cpp src/interface/widgets/utility/compact_push_button.h + src/single_instance_manager.h + src/client/url_scheme_event_filter.h + src/interface/intents/intent_connect_to_server.cpp + src/interface/intents/intent_connect_to_server.h + src/interface/intents/intent_disconnect_from_server.cpp + src/interface/intents/intent_disconnect_from_server.h + src/interface/intents/intent_join_server_game.cpp + src/interface/intents/intent_join_server_game.h + src/interface/intents/intent_join_server_room.cpp + src/interface/intents/intent_join_server_room.h + src/interface/intents/intent_login.cpp + src/interface/intents/intent_login.h + src/interface/intents/url_parser.cpp + src/interface/intents/url_parser.h src/interface/widgets/server/user/user_info_popup.cpp src/interface/widgets/server/user/user_info_popup.h ) @@ -401,9 +437,7 @@ if(APPLE) set(cockatrice_SOURCES ${cockatrice_SOURCES} ${CMAKE_CURRENT_SOURCE_DIR}/resources/appicon.icns) endif(APPLE) -if(Qt6_FOUND) - qt6_add_resources(cockatrice_RESOURCES_RCC ${cockatrice_RESOURCES}) -endif() +qt6_add_resources(cockatrice_RESOURCES_RCC ${cockatrice_RESOURCES}) # Declare path variables set(ICONDIR @@ -415,34 +449,37 @@ set(DESKTOPDIR CACHE STRING "desktop file destination" ) +set(MIMEDIR + share/mime/packages + CACHE STRING "mime file destination" +) + set(COCKATRICE_MAC_QM_INSTALL_DIR "cockatrice.app/Contents/Resources/translations") set(COCKATRICE_UNIX_QM_INSTALL_DIR "share/cockatrice/translations") set(COCKATRICE_WIN32_QM_INSTALL_DIR "translations") -if(Qt6_FOUND) - qt6_add_executable( - cockatrice - WIN32 - MACOSX_BUNDLE - ${cockatrice_SOURCES} - ${cockatrice_RESOURCES_RCC} - ${cockatrice_MOC_SRCS} - MANUAL_FINALIZATION - ) +qt6_add_executable( + cockatrice + WIN32 + MACOSX_BUNDLE + ${cockatrice_SOURCES} + ${cockatrice_RESOURCES_RCC} + ${cockatrice_MOC_SRCS} + MANUAL_FINALIZATION +) - target_link_libraries( - cockatrice - PUBLIC libcockatrice_card - libcockatrice_deck_list - libcockatrice_filters - libcockatrice_utility - libcockatrice_network - libcockatrice_models - libcockatrice_rng - libcockatrice_settings - ${COCKATRICE_QT_MODULES} - ) -endif() +target_link_libraries( + cockatrice + PUBLIC libcockatrice_card + libcockatrice_deck_list + libcockatrice_filters + libcockatrice_utility + libcockatrice_network + libcockatrice_models + libcockatrice_rng + libcockatrice_settings + ${COCKATRICE_QT_MODULES} +) if(UNIX) if(APPLE) @@ -462,6 +499,23 @@ if(UNIX) install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/resources/cockatrice.png DESTINATION ${ICONDIR}/hicolor/48x48/apps) install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/resources/cockatrice.svg DESTINATION ${ICONDIR}/hicolor/scalable/apps) install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/cockatrice.desktop DESTINATION ${DESKTOPDIR}) + install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/cockatrice-cod.xml DESTINATION ${MIMEDIR}) + + # Refresh the freedesktop databases so the file associations and scheme + # handler register without requiring the user to run them manually. The + # tools may be missing on minimal systems; that is fine, packaging systems + # usually refresh these databases through their own triggers. + find_program(UPDATE_MIME_DATABASE update-mime-database) + if(UPDATE_MIME_DATABASE) + install(CODE "execute_process(COMMAND \"${UPDATE_MIME_DATABASE}\" \"${CMAKE_INSTALL_PREFIX}/share/mime\")") + endif() + + find_program(UPDATE_DESKTOP_DATABASE update-desktop-database) + if(UPDATE_DESKTOP_DATABASE) + install( + CODE "execute_process(COMMAND \"${UPDATE_DESKTOP_DATABASE}\" \"${CMAKE_INSTALL_PREFIX}/share/applications\")" + ) + endif() endif() elseif(WIN32) install(TARGETS cockatrice RUNTIME DESTINATION ./) @@ -590,7 +644,7 @@ Data = Resources\") endif() endif() -if(Qt6_FOUND AND Qt6LinguistTools_FOUND) +if(Qt6LinguistTools_FOUND) #Qt6 Translations happen after the executable is built up if(UPDATE_TRANSLATIONS) qt6_add_translations( @@ -617,6 +671,4 @@ if(Qt6_FOUND AND Qt6LinguistTools_FOUND) endif() endif() -if(Qt6_FOUND) - qt6_finalize_target(cockatrice) -endif() +qt6_finalize_target(cockatrice) diff --git a/cockatrice/cockatrice-cod.xml b/cockatrice/cockatrice-cod.xml new file mode 100644 index 000000000..1a0199433 --- /dev/null +++ b/cockatrice/cockatrice-cod.xml @@ -0,0 +1,7 @@ + + + + Cockatrice Deck File + + + diff --git a/cockatrice/cockatrice.desktop b/cockatrice/cockatrice.desktop index 092d84ef5..4b15fa9c1 100644 --- a/cockatrice/cockatrice.desktop +++ b/cockatrice/cockatrice.desktop @@ -3,6 +3,8 @@ Version=1.0 Type=Application Name=Cockatrice -Exec=cockatrice +Exec=cockatrice %U Icon=cockatrice Categories=Game;CardGame; +MimeType=application/x-cockatrice; +X-Scheme-Handler/cockatrice=true diff --git a/cockatrice/src/client/network/update/card_spoiler/spoiler_background_updater.cpp b/cockatrice/src/client/network/update/card_spoiler/spoiler_background_updater.cpp index 480ff701d..dae633717 100644 --- a/cockatrice/src/client/network/update/card_spoiler/spoiler_background_updater.cpp +++ b/cockatrice/src/client/network/update/card_spoiler/spoiler_background_updater.cpp @@ -14,8 +14,8 @@ #include #include #include +#include #include -#include #include #define SPOILERS_STATUS_URL "https://raw.githubusercontent.com/Cockatrice/Magic-Spoiler/files/SpoilerSeasonEnabled" @@ -23,7 +23,7 @@ SpoilerBackgroundUpdater::SpoilerBackgroundUpdater(QObject *apParent) : QObject(apParent), cardUpdateProcess(nullptr) { - isSpoilerDownloadEnabled = SettingsCache::instance().personal().getDownloadSpoilersStatus(); + isSpoilerDownloadEnabled = SettingsCache::instance().downloads().getDownloadSpoilersStatus(); if (isSpoilerDownloadEnabled) { // Start the process of checking if we're in spoiler season // File exists means we're in spoiler season diff --git a/cockatrice/src/client/settings/cache_settings.cpp b/cockatrice/src/client/settings/cache_settings.cpp index 4f36bbb3b..eaa5d96ac 100644 --- a/cockatrice/src/client/settings/cache_settings.cpp +++ b/cockatrice/src/client/settings/cache_settings.cpp @@ -11,18 +11,22 @@ #include #include #include +#include #include #include #include #include #include +#include #include +#include #include #include #include #include #include #include +#include #include #include #include @@ -135,8 +139,12 @@ SettingsCache::SettingsCache() personalSettings = new PersonalSettings(settingsPath, this); cardsDisplaySettings = new CardsDisplaySettings(settingsPath, this); interfaceSettings = new InterfaceSettings(settingsPath, this); + deckEditorSettings = new DeckEditorSettings(settingsPath, this); pathsSettings = new PathsSettings(settingsPath, this); visualDeckStorageSettings = new VisualDeckStorageSettings(settingsPath, this); + appearanceSettings = new AppearanceSettings(settingsPath, this); + networkSettings = new NetworkSettings(settingsPath, this); + commanderBracketSettings = new CommanderBracketSettings(settingsPath, this); // Forward ICardDatabasePathProvider signal from PathsSettings connect(pathsSettings, &PathsSettings::cardDatabasePathChanged, this, @@ -147,7 +155,13 @@ SettingsCache::SettingsCache() releaseChannels << new StableReleaseChannel(); releaseChannels << new BetaReleaseChannel(); - themeName = personalSettings->getThemeName(); + themeName = appearanceSettings->getThemeName(); + + auto definitions = commanderBracketSettings->loadDefinitions(); + if (definitions.isEmpty()) { + definitions = CommanderBracketSettings::defaultDefinitions(); + } + commanderBracketSettings->reloadDefinitions(definitions); loadPaths(); } @@ -155,7 +169,7 @@ SettingsCache::SettingsCache() void SettingsCache::setThemeName(const QString &_themeName) { themeName = _themeName; - personalSettings->setThemeName(themeName); + appearanceSettings->setThemeName(themeName); emit themeChanged(); } @@ -216,15 +230,15 @@ void SettingsCache::loadPaths() // customPicsPath derived from picsPath QString picsPath = pathsIni.value("paths/pics").toString(); if (picsPath.endsWith("/")) { - computePath("custompics", picsPath + "CUSTOM/"); + computePath("customPics", picsPath + "CUSTOM/"); } else { - computePath("custompics", picsPath + "/CUSTOM/"); + computePath("customPics", picsPath + "/CUSTOM/"); } - computePath("customsets", dataPath + "/customsets/"); - computeFilePath("carddatabase", dataPath + "/cards.xml"); - computeFilePath("tokendatabase", dataPath + "/tokens.xml"); - computeFilePath("spoilerdatabase", dataPath + "/spoiler.xml"); + computePath("customSets", dataPath + "/customsets/"); + computeFilePath("cardDatabase", dataPath + "/cards.xml"); + computeFilePath("tokenDatabase", dataPath + "/tokens.xml"); + computeFilePath("spoilerDatabase", dataPath + "/spoiler.xml"); } void SettingsCache::resetPaths() @@ -272,12 +286,12 @@ QString SettingsCache::getTokenDatabasePath() const // INetworkSettingsProvider - delegate to sub-objects int SettingsCache::getKeepAlive() const { - return personalSettings->getKeepAlive(); + return networkSettings->getKeepAlive(); } int SettingsCache::getTimeOut() const { - return personalSettings->getTimeOut(); + return networkSettings->getTimeOut(); } bool SettingsCache::getNotifyAboutUpdates() const @@ -287,17 +301,17 @@ bool SettingsCache::getNotifyAboutUpdates() const void SettingsCache::setKnownMissingFeatures(const QString &_knownMissingFeatures) { - interfaceSettings->setKnownMissingFeatures(_knownMissingFeatures); + networkSettings->setKnownMissingFeatures(_knownMissingFeatures); } QString SettingsCache::getKnownMissingFeatures() { - return interfaceSettings->getKnownMissingFeatures(); + return networkSettings->getKnownMissingFeatures(); } QString SettingsCache::getClientID() { - return personalSettings->getClientID(); + return networkSettings->getClientID(); } // Release channels @@ -412,7 +426,7 @@ CardsDisplaySettings &SettingsCache::cardsDisplay() const return *cardsDisplaySettings; } -InterfaceSettings &SettingsCache::interface() const +InterfaceSettings &SettingsCache::userInterface() const { return *interfaceSettings; } @@ -422,7 +436,22 @@ PathsSettings &SettingsCache::paths() const return *pathsSettings; } +DeckEditorSettings &SettingsCache::deckEditor() const +{ + return *deckEditorSettings; +} + VisualDeckStorageSettings &SettingsCache::visualDeckStorage() const { return *visualDeckStorageSettings; } + +AppearanceSettings &SettingsCache::appearance() const +{ + return *appearanceSettings; +} + +NetworkSettings &SettingsCache::network() const +{ + return *networkSettings; +} diff --git a/cockatrice/src/client/settings/cache_settings.h b/cockatrice/src/client/settings/cache_settings.h index f9cce4cfe..23cdb4dbf 100644 --- a/cockatrice/src/client/settings/cache_settings.h +++ b/cockatrice/src/client/settings/cache_settings.h @@ -28,7 +28,9 @@ class CardDatabaseSettings; class CardOverrideSettings; class CardsDisplaySettings; class ChatSettings; +class CommanderBracketSettings; class DebugSettings; +class DeckEditorSettings; class DownloadSettings; class GameFiltersSettings; class GameSettings; @@ -44,6 +46,8 @@ class SoundSettings; class TabsSettings; class UpdatesSettings; class VisualDeckStorageSettings; +class AppearanceSettings; +class NetworkSettings; class QSettings; class SettingsCache : public ICardDatabasePathProvider, public INetworkSettingsProvider @@ -75,8 +79,12 @@ private: PersonalSettings *personalSettings; CardsDisplaySettings *cardsDisplaySettings; InterfaceSettings *interfaceSettings; + DeckEditorSettings *deckEditorSettings; PathsSettings *pathsSettings; VisualDeckStorageSettings *visualDeckStorageSettings; + AppearanceSettings *appearanceSettings; + NetworkSettings *networkSettings; + CommanderBracketSettings *commanderBracketSettings; QString themeName; @@ -138,9 +146,16 @@ public: [[nodiscard]] UpdatesSettings &updates() const; [[nodiscard]] PersonalSettings &personal() const; [[nodiscard]] CardsDisplaySettings &cardsDisplay() const; - [[nodiscard]] InterfaceSettings &interface() const; + [[nodiscard]] InterfaceSettings &userInterface() const; + [[nodiscard]] DeckEditorSettings &deckEditor() const; [[nodiscard]] PathsSettings &paths() const; [[nodiscard]] VisualDeckStorageSettings &visualDeckStorage() const; + [[nodiscard]] AppearanceSettings &appearance() const; + [[nodiscard]] NetworkSettings &network() const; + [[nodiscard]] CommanderBracketSettings &commanderBrackets() const + { + return *commanderBracketSettings; + } [[nodiscard]] bool getIsPortableBuild() const { diff --git a/cockatrice/src/client/settings/card_counter_settings.cpp b/cockatrice/src/client/settings/card_counter_settings.cpp index 662ae0c7d..d4030c174 100644 --- a/cockatrice/src/client/settings/card_counter_settings.cpp +++ b/cockatrice/src/client/settings/card_counter_settings.cpp @@ -5,7 +5,7 @@ #include CardCounterSettings::CardCounterSettings(const QString &settingsPath, QObject *parent) - : SettingsManager(settingsPath + "global.ini", "cards", "counters", parent) + : SettingsManager(settingsPath + "card_counters.ini", "cards", "counters", parent) { } diff --git a/cockatrice/src/client/url_scheme_event_filter.h b/cockatrice/src/client/url_scheme_event_filter.h new file mode 100644 index 000000000..9e96502ca --- /dev/null +++ b/cockatrice/src/client/url_scheme_event_filter.h @@ -0,0 +1,69 @@ +#ifndef COCKATRICE_URL_SCHEME_EVENT_FILTER_H +#define COCKATRICE_URL_SCHEME_EVENT_FILTER_H + +#include +#include +#include +#include +#include + +/** + * @brief Event filter that catches QFileOpenEvent URLs matching a scheme and + * re-emits them as urlReceived(). + * + * On macOS, when the application is registered as a URL scheme handler, the + * OS delivers incoming URLs via QFileOpenEvent on the QApplication object. + * Install this filter on QApplication to intercept them: + * + * @code + * UrlSchemeEventFilter filter(QStringList{QStringLiteral("cockatrice")}); + * QObject::connect(&filter, &UrlSchemeEventFilter::urlReceived, + * &mainWindow, &MainWindow::handleUrl); + * app.installEventFilter(&filter); + * @endcode + * + * Note: the strings are compared against QUrl::scheme(), so they must be + * written without the "://" suffix (e.g. "cockatrice", not "cockatrice://"). + */ +class UrlSchemeEventFilter : public QObject +{ + Q_OBJECT + +public: + explicit UrlSchemeEventFilter(const QStringList &schemes, QObject *parent = nullptr) + : QObject(parent), prefixes(schemes) + { + } + +signals: + void urlReceived(const QString &url); + +public: + bool eventFilter(QObject *watched, QEvent *event) override + { + if (event->type() == QEvent::FileOpen) { + auto *fileEvent = static_cast(event); + + const QUrl url = fileEvent->url(); + + for (const auto &prefix : prefixes) { + if (url.scheme() == prefix) { + emit urlReceived(url.toString()); + return true; + } + } + + if (url.isLocalFile()) { + emit urlReceived(url.toLocalFile()); + return true; + } + } + + return QObject::eventFilter(watched, event); + } + +private: + QStringList prefixes; +}; + +#endif // COCKATRICE_URL_SCHEME_EVENT_FILTER_H diff --git a/cockatrice/src/game/abstract_game.cpp b/cockatrice/src/game/abstract_game.cpp index c20003ece..6aa2ab28f 100644 --- a/cockatrice/src/game/abstract_game.cpp +++ b/cockatrice/src/game/abstract_game.cpp @@ -31,7 +31,7 @@ AbstractClient *AbstractGame::getClientForPlayer(int playerId) const } } -void AbstractGame::loadReplay(GameReplay *replay) +void AbstractGame::loadReplay(const GameReplay *replay) { gameMetaInfo->setFromProto(replay->game_info()); gameMetaInfo->setSpectatorsOmniscient(true); diff --git a/cockatrice/src/game/abstract_game.h b/cockatrice/src/game/abstract_game.h index 5115ed5ca..fcf764492 100644 --- a/cockatrice/src/game/abstract_game.h +++ b/cockatrice/src/game/abstract_game.h @@ -53,7 +53,7 @@ public: AbstractClient *getClientForPlayer(int playerId) const; - void loadReplay(GameReplay *replay); + void loadReplay(const GameReplay *replay); CardItem *getCard(int playerId, const QString &zoneName, int cardId) const; diff --git a/cockatrice/src/game/player/player_actions.cpp b/cockatrice/src/game/player/player_actions.cpp index 12abb994f..67c3295d6 100644 --- a/cockatrice/src/game/player/player_actions.cpp +++ b/cockatrice/src/game/player/player_actions.cpp @@ -69,7 +69,7 @@ void PlayerActions::playCard(CardItem *card, bool faceDown) const CardInfo &info = exactCard.getInfo(); int tableRow = info.getUiAttributes().tableRow; - bool playToStack = SettingsCache::instance().interface().getPlayToStack(); + bool playToStack = SettingsCache::instance().userInterface().getPlayToStack(); QString currentZone = card->getZone()->getName(); if (!faceDown && currentZone == ZoneNames::STACK && tableRow == 3) { cmd.set_target_zone(ZoneNames::GRAVE); @@ -312,7 +312,7 @@ void PlayerActions::actDrawCard() void PlayerActions::actRequestMulliganDialog() { - int startSize = SettingsCache::instance().interface().getStartingHandSize(); + int startSize = SettingsCache::instance().userInterface().getStartingHandSize(); int handSize = player->getHandZone()->getCards().size(); int deckSize = player->getDeckZone()->getCards().size() + handSize; @@ -328,7 +328,7 @@ void PlayerActions::actMulligan(int number) } doMulligan(number); - SettingsCache::instance().interface().setStartingHandSize(number); + SettingsCache::instance().userInterface().setStartingHandSize(number); } void PlayerActions::actMulliganSameSize() @@ -932,13 +932,13 @@ void PlayerActions::setLastTokenInfo(CardInfoPtr cardInfo) return; } - lastTokenInfo = {.name = cardInfo->getName(), - .color = cardInfo->getColors().isEmpty() ? QString() : cardInfo->getColors().left(1).toLower(), - .pt = cardInfo->getPowTough(), - .annotation = SettingsCache::instance().interface().getAnnotateTokens() ? cardInfo->getText() : "", - .destroy = true, - .providerId = - SettingsCache::instance().cardOverrides().getCardPreferenceOverride(cardInfo->getName())}; + lastTokenInfo = { + .name = cardInfo->getName(), + .color = cardInfo->getColors().isEmpty() ? QString() : cardInfo->getColors().left(1).toLower(), + .pt = cardInfo->getPowTough(), + .annotation = SettingsCache::instance().userInterface().getAnnotateTokens() ? cardInfo->getText() : "", + .destroy = true, + .providerId = SettingsCache::instance().cardOverrides().getCardPreferenceOverride(cardInfo->getName())}; lastTokenTableRow = TableZone::tableRowToGridY(cardInfo->getUiAttributes().tableRow); @@ -1171,7 +1171,7 @@ void PlayerActions::createCard(const CardItem *sourceCard, } cmd.set_pt(cardInfo->getPowTough().toStdString()); - if (SettingsCache::instance().interface().getAnnotateTokens()) { + if (SettingsCache::instance().userInterface().getAnnotateTokens()) { cmd.set_annotation(cardInfo->getText().toStdString()); } else { cmd.set_annotation(""); diff --git a/cockatrice/src/game/replay.cpp b/cockatrice/src/game/replay.cpp index 69f9d8b20..dcf3e9b9b 100644 --- a/cockatrice/src/game/replay.cpp +++ b/cockatrice/src/game/replay.cpp @@ -2,7 +2,7 @@ #include "../interface/widgets/tabs/tab_game.h" -Replay::Replay(QObject *_parent, GameReplay *_replay, bool isLocalGame) : AbstractGame(_parent) +Replay::Replay(QObject *_parent, const GameReplay *_replay, bool isLocalGame) : AbstractGame(_parent) { gameState = new GameState(this, 0, -1, isLocalGame, {}, false, false, -1, false); connect(gameMetaInfo, &GameMetaInfo::startedChanged, gameState, &GameState::onStartedChanged); diff --git a/cockatrice/src/game/replay.h b/cockatrice/src/game/replay.h index ecb3a10d0..1c269b273 100644 --- a/cockatrice/src/game/replay.h +++ b/cockatrice/src/game/replay.h @@ -15,7 +15,7 @@ class Replay : public AbstractGame Q_OBJECT public: - explicit Replay(QObject *_parent, GameReplay *_replay, bool isLocalGame); + explicit Replay(QObject *_parent, const GameReplay *_replay, bool isLocalGame); }; #endif // COCKATRICE_REPLAY_H diff --git a/cockatrice/src/game/zones/view_zone_logic.cpp b/cockatrice/src/game/zones/view_zone_logic.cpp index 60fe39bb2..2ef04284a 100644 --- a/cockatrice/src/game/zones/view_zone_logic.cpp +++ b/cockatrice/src/game/zones/view_zone_logic.cpp @@ -58,7 +58,7 @@ bool ZoneViewZoneLogic::prepareAddCard(int x) // autoclose check is done both here and in removeCard - if (cards.isEmpty() && !doInsert && SettingsCache::instance().interface().getCloseEmptyCardView()) { + if (cards.isEmpty() && !doInsert && SettingsCache::instance().userInterface().getCloseEmptyCardView()) { emit closeView(); } @@ -145,7 +145,7 @@ void ZoneViewZoneLogic::removeCard(int position, bool toNewZone) // card gets dragged within the view. // Another autoclose check is done in prepareAddCard so that the view autocloses if the last card was moved to an // unrevealed portion of the same zone. - if (cards.isEmpty() && SettingsCache::instance().interface().getCloseEmptyCardView() && toNewZone) { + if (cards.isEmpty() && SettingsCache::instance().userInterface().getCloseEmptyCardView() && toNewZone) { emit closeView(); return; } diff --git a/cockatrice/src/game_graphics/board/abstract_card_item.cpp b/cockatrice/src/game_graphics/board/abstract_card_item.cpp index a9e0167d4..e0029ee2d 100644 --- a/cockatrice/src/game_graphics/board/abstract_card_item.cpp +++ b/cockatrice/src/game_graphics/board/abstract_card_item.cpp @@ -12,9 +12,9 @@ #include #include #include +#include #include #include -#include AbstractCardItem::AbstractCardItem(QGraphicsItem *parent, const CardRef &cardRef, PlayerLogic *_owner, int _id) : ArrowTarget(_owner, parent), id(_id), cardRef(cardRef), tapped(false), facedown(false), tapAngle(0), @@ -107,7 +107,7 @@ QSizeF AbstractCardItem::getTranslatedSize(QPainter *painter) const void AbstractCardItem::transformPainter(QPainter *painter, const QSizeF &translatedSize, int angle) { - const int MAX_FONT_SIZE = SettingsCache::instance().personal().getMaxFontSize(); + const int MAX_FONT_SIZE = SettingsCache::instance().appearance().getMaxFontSize(); const int fontSize = std::max(9, MAX_FONT_SIZE); QRectF totalBoundingRect = painter->combinedTransform().mapRect(boundingRect()); diff --git a/cockatrice/src/game_graphics/board/arrow_item.cpp b/cockatrice/src/game_graphics/board/arrow_item.cpp index c40827361..ce8967bb5 100644 --- a/cockatrice/src/game_graphics/board/arrow_item.cpp +++ b/cockatrice/src/game_graphics/board/arrow_item.cpp @@ -262,7 +262,7 @@ void ArrowDragItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) if (startZone->getName() == ZoneNames::HAND) { startCard->playCard(false); CardInfoPtr ci = startCard->getCard().getCardPtr(); - bool playToStack = SettingsCache::instance().interface().getPlayToStack(); + bool playToStack = SettingsCache::instance().userInterface().getPlayToStack(); if (ci && ((!playToStack && ci->getUiAttributes().tableRow == 3) || (playToStack && ci->getUiAttributes().tableRow != 0 && startCard->getZone()->getName() != ZoneNames::STACK))) { diff --git a/cockatrice/src/game_graphics/board/card_item.cpp b/cockatrice/src/game_graphics/board/card_item.cpp index 63e298886..c40c8c214 100644 --- a/cockatrice/src/game_graphics/board/card_item.cpp +++ b/cockatrice/src/game_graphics/board/card_item.cpp @@ -281,7 +281,7 @@ void CardItem::drawArrow(const QColor &arrowColor) auto *game = owner->getGame(); PlayerLogic *arrowOwner = game->getPlayerManager()->getActiveLocalPlayer(game->getGameState()->getActivePlayer()); int phase = 0; // 0 means to not set the phase - if (SettingsCache::instance().interface().getDoNotDeleteArrowsInSubPhases()) { + if (SettingsCache::instance().userInterface().getDoNotDeleteArrowsInSubPhases()) { int currentPhase = game->getGameState()->getCurrentPhase(); phase = Phases::getLastSubphase(currentPhase) + 1; } @@ -400,7 +400,7 @@ void CardItem::playCard(bool faceDown) if (tz) { emit tz->toggleTapped(); } else { - if (SettingsCache::instance().interface().getClickPlaysAllSelected()) { + if (SettingsCache::instance().userInterface().getClickPlaysAllSelected()) { if (faceDown) { emit playSelectedFaceDown(this); } else { @@ -464,7 +464,7 @@ static bool isUnwritableRevealZone(CardZoneLogic *zone) void CardItem::handleClickedToPlay(bool shiftHeld) { if (isUnwritableRevealZone(state->getZone())) { - if (SettingsCache::instance().interface().getClickPlaysAllSelected()) { + if (SettingsCache::instance().userInterface().getClickPlaysAllSelected()) { emit hideSelected(this); } else { state->getZone()->removeCard(this); @@ -481,7 +481,7 @@ void CardItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) return; } if ((event->modifiers() != Qt::AltModifier) && (event->button() == Qt::LeftButton) && - (!SettingsCache::instance().interface().getDoubleClickToPlay())) { + (!SettingsCache::instance().userInterface().getDoubleClickToPlay())) { handleClickedToPlay(event->modifiers().testFlag(Qt::ShiftModifier)); } if (owner != nullptr) { @@ -493,7 +493,7 @@ void CardItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) void CardItem::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) { if ((event->modifiers() != Qt::AltModifier) && (event->buttons() == Qt::LeftButton) && - (SettingsCache::instance().interface().getDoubleClickToPlay())) { + (SettingsCache::instance().userInterface().getDoubleClickToPlay())) { handleClickedToPlay(event->modifiers().testFlag(Qt::ShiftModifier)); } event->accept(); diff --git a/cockatrice/src/game_graphics/dialogs/dlg_create_token.cpp b/cockatrice/src/game_graphics/dialogs/dlg_create_token.cpp index e53069025..b311d2ebd 100644 --- a/cockatrice/src/game_graphics/dialogs/dlg_create_token.cpp +++ b/cockatrice/src/game_graphics/dialogs/dlg_create_token.cpp @@ -189,7 +189,7 @@ void DlgCreateToken::tokenSelectionChanged(const QModelIndex ¤t, const QMo const QChar cardColor = cardInfo->getColorChar(); colorEdit->setCurrentIndex(colorEdit->findData(cardColor, Qt::UserRole, Qt::MatchFixedString)); ptEdit->setText(cardInfo->getPowTough()); - if (SettingsCache::instance().interface().getAnnotateTokens()) { + if (SettingsCache::instance().userInterface().getAnnotateTokens()) { annotationEdit->setText(cardInfo->getText()); } } else { diff --git a/cockatrice/src/game_graphics/game_scene.cpp b/cockatrice/src/game_graphics/game_scene.cpp index 58e6888c6..db2088104 100644 --- a/cockatrice/src/game_graphics/game_scene.cpp +++ b/cockatrice/src/game_graphics/game_scene.cpp @@ -37,7 +37,7 @@ GameScene::GameScene(PhasesToolbar *_phasesToolbar, QObject *parent) { animationTimer = new QBasicTimer; addItem(phasesToolbar); - connect(&SettingsCache::instance().interface(), &InterfaceSettings::minPlayersForMultiColumnLayoutChanged, this, + connect(&SettingsCache::instance().userInterface(), &InterfaceSettings::minPlayersForMultiColumnLayoutChanged, this, &GameScene::rearrange); rearrange(); @@ -336,7 +336,7 @@ QList GameScene::rotatePlayers(const QList &active int GameScene::determineColumnCount(int playerCount) { - return playerCount < SettingsCache::instance().interface().getMinPlayersForMultiColumnLayout() ? 1 : 2; + return playerCount < SettingsCache::instance().userInterface().getMinPlayersForMultiColumnLayout() ? 1 : 2; } /** diff --git a/cockatrice/src/game_graphics/game_view.cpp b/cockatrice/src/game_graphics/game_view.cpp index bda5ea76d..b768c8317 100644 --- a/cockatrice/src/game_graphics/game_view.cpp +++ b/cockatrice/src/game_graphics/game_view.cpp @@ -47,11 +47,11 @@ GameView::GameView(GameScene *scene, QWidget *parent) : QGraphicsView(scene, par connect(scene, &GameScene::sigResizeRubberBand, this, &GameView::resizeRubberBand); connect(scene, &GameScene::sigStopRubberBand, this, &GameView::stopRubberBand); connect(scene, &QGraphicsScene::selectionChanged, this, [this]() { updateTotalSelectionCount(); }); - connect(&SettingsCache::instance().interface(), &InterfaceSettings::tallyTypeChanged, this, + connect(&SettingsCache::instance().userInterface(), &InterfaceSettings::tallyTypeChanged, this, [this] { updateTotalSelectionCount(); }); - setFocusDisabled(SettingsCache::instance().interface().getKeepGameChatFocus()); - connect(&SettingsCache::instance().interface(), &InterfaceSettings::keepGameChatFocusChanged, this, + setFocusDisabled(SettingsCache::instance().userInterface().getKeepGameChatFocus()); + connect(&SettingsCache::instance().userInterface(), &InterfaceSettings::keepGameChatFocusChanged, this, &GameView::setFocusDisabled); aCloseMostRecentZoneView = new QAction(this); @@ -130,7 +130,7 @@ void GameView::resizeRubberBand(const QPointF &cursorPoint, int selectedCount) QRect rect = QRect(mapFromScene(selectionOrigin), cursor).normalized(); rubberBand->setGeometry(rect); - if (!SettingsCache::instance().interface().getShowDragSelectionCount()) { + if (!SettingsCache::instance().userInterface().getShowDragSelectionCount()) { dragCountLabel->hide(); return; } @@ -239,7 +239,7 @@ void GameView::updateTotalSelectionCount(const QSize &viewSize) int count = scene()->selectedItems().count(); - if (!SettingsCache::instance().interface().getShowTotalSelectionCount() || count <= 1) { + if (!SettingsCache::instance().userInterface().getShowTotalSelectionCount() || count <= 1) { totalCountLabel->hide(); } else { totalCountLabel->setText(QString::number(count)); @@ -251,12 +251,12 @@ void GameView::updateTotalSelectionCount(const QSize &viewSize) totalCountLabel->show(); } - TallyType tallyType = Tally::intToType(SettingsCache::instance().interface().getTallyType()); + TallyType tallyType = Tally::intToType(SettingsCache::instance().userInterface().getTallyType()); GameScene *gameScene = static_cast(scene()); QList entries = Tally::compute(gameScene->selectedCards(), tallyType); - if (entries.isEmpty() || count <= 1) { + if (entries.isEmpty()) { tallyContainer->hide(); cachedTallyRows.clear(); return; diff --git a/cockatrice/src/game_graphics/player/menu/tally_menu.cpp b/cockatrice/src/game_graphics/player/menu/tally_menu.cpp index b8616c75a..7eb3945b3 100644 --- a/cockatrice/src/game_graphics/player/menu/tally_menu.cpp +++ b/cockatrice/src/game_graphics/player/menu/tally_menu.cpp @@ -11,24 +11,26 @@ TallyMenu::TallyMenu() aTallyNone = createTallyAction(TallyType::None); aTallySubtypes = createTallyAction(TallyType::Subtypes); + aTallyTotalPower = createTallyAction(TallyType::TotalPower); addAction(aTallyNone); addSeparator(); addAction(aTallySubtypes); + addAction(aTallyTotalPower); retranslateUi(); } QAction *TallyMenu::createTallyAction(TallyType tallyType) { - TallyType currentType = Tally::intToType(SettingsCache::instance().interface().getTallyType()); + TallyType currentType = Tally::intToType(SettingsCache::instance().userInterface().getTallyType()); QAction *action = new QAction(this); action->setCheckable(true); action->setChecked(tallyType == currentType); - connect(action, &QAction::triggered, &SettingsCache::instance().interface(), - [tallyType] { SettingsCache::instance().interface().setTallyType(static_cast(tallyType)); }); + connect(action, &QAction::triggered, &SettingsCache::instance().userInterface(), + [tallyType] { SettingsCache::instance().userInterface().setTallyType(static_cast(tallyType)); }); actionGroup->addAction(action); @@ -51,4 +53,5 @@ void TallyMenu::retranslateUi() aTallyNone->setText(tr("None")); aTallySubtypes->setText(tr("Subtypes")); + aTallyTotalPower->setText(tr("Total Power")); } diff --git a/cockatrice/src/game_graphics/player/menu/tally_menu.h b/cockatrice/src/game_graphics/player/menu/tally_menu.h index 28c056f44..acd1daf67 100644 --- a/cockatrice/src/game_graphics/player/menu/tally_menu.h +++ b/cockatrice/src/game_graphics/player/menu/tally_menu.h @@ -23,6 +23,7 @@ private: QAction *aTallyNone = nullptr; QAction *aTallySubtypes = nullptr; + QAction *aTallyTotalPower = nullptr; QAction *createTallyAction(TallyType tallyType); }; diff --git a/cockatrice/src/game_graphics/player/player_graphics_item.cpp b/cockatrice/src/game_graphics/player/player_graphics_item.cpp index 20f6128f4..d443853ce 100644 --- a/cockatrice/src/game_graphics/player/player_graphics_item.cpp +++ b/cockatrice/src/game_graphics/player/player_graphics_item.cpp @@ -17,9 +17,9 @@ PlayerGraphicsItem::PlayerGraphicsItem(PlayerLogic *_player) : player(_player) { - connect(&SettingsCache::instance().interface(), &InterfaceSettings::horizontalHandChanged, this, + connect(&SettingsCache::instance().userInterface(), &InterfaceSettings::horizontalHandChanged, this, &PlayerGraphicsItem::rearrangeZones); - connect(&SettingsCache::instance().interface(), &InterfaceSettings::handJustificationChanged, this, + connect(&SettingsCache::instance().userInterface(), &InterfaceSettings::handJustificationChanged, this, &PlayerGraphicsItem::rearrangeZones); connect(player, &PlayerLogic::rearrangeCounters, this, &PlayerGraphicsItem::rearrangeCounters); connect(player, &PlayerLogic::activeChanged, this, &PlayerGraphicsItem::onPlayerActiveChanged); @@ -149,7 +149,7 @@ qreal PlayerGraphicsItem::getMinimumWidth() const { qreal result = tableZoneGraphicsItem->getMinimumWidth() + CardDimensions::HEIGHT_F + 15 + counterAreaWidth + stackZoneGraphicsItem->boundingRect().width(); - if (!SettingsCache::instance().interface().getHorizontalHand()) { + if (!SettingsCache::instance().userInterface().getHorizontalHand()) { result += handZoneGraphicsItem->boundingRect().width(); } return result; @@ -166,7 +166,7 @@ void PlayerGraphicsItem::processSceneSizeChange(int newPlayerWidth) // Extend table (and hand, if horizontal) to accommodate the new player width. qreal tableWidth = newPlayerWidth - CardDimensions::HEIGHT_F - 15 - counterAreaWidth - stackZoneGraphicsItem->boundingRect().width(); - if (!SettingsCache::instance().interface().getHorizontalHand()) { + if (!SettingsCache::instance().userInterface().getHorizontalHand()) { tableWidth -= handZoneGraphicsItem->boundingRect().width(); } @@ -234,7 +234,7 @@ void PlayerGraphicsItem::rearrangeCounters() void PlayerGraphicsItem::rearrangeZones() { auto base = QPointF(CardDimensions::HEIGHT_F + counterAreaWidth + 15, 0); - if (SettingsCache::instance().interface().getHorizontalHand()) { + if (SettingsCache::instance().userInterface().getHorizontalHand()) { if (mirrored) { if (player->getHandZone()->contentsKnown()) { handVisible = true; @@ -285,7 +285,7 @@ void PlayerGraphicsItem::updateBoundingRect() { prepareGeometryChange(); qreal width = CardDimensions::HEIGHT_F + 15 + counterAreaWidth + stackZoneGraphicsItem->boundingRect().width(); - if (SettingsCache::instance().interface().getHorizontalHand()) { + if (SettingsCache::instance().userInterface().getHorizontalHand()) { qreal handHeight = handVisible ? handZoneGraphicsItem->boundingRect().height() : 0; bRect = QRectF(0, 0, width + tableZoneGraphicsItem->boundingRect().width(), tableZoneGraphicsItem->boundingRect().height() + handHeight); diff --git a/cockatrice/src/game_graphics/tally/stats_tally.cpp b/cockatrice/src/game_graphics/tally/stats_tally.cpp new file mode 100644 index 000000000..e7a6621fa --- /dev/null +++ b/cockatrice/src/game_graphics/tally/stats_tally.cpp @@ -0,0 +1,36 @@ +#include "stats_tally.h" + +#include "../board/card_item.h" + +#include +#include +#include + +static int sumPowers(const QList &cards) +{ + // calculate total power; + int total = 0; + for (auto card : cards) { + QVariantList parsed = CardItem::parsePT(card->getPT()); + if (!parsed.isEmpty()) { + int power = parsed.first().toInt(); // toInt will default to 0 if it's not an int + total += qMax(power, 0); + } + } + return total; +} + +QList StatsTally::computeTotalPower(const QList &cards) +{ + // don't bother if none of the cards have pt + bool hasPT = + std::any_of(cards.cbegin(), cards.cend(), [](const CardItem *card) { return !card->getPT().isEmpty(); }); + if (!hasPT) { + return {}; + } + + int total = sumPowers(cards); + + QString name = QCoreApplication::translate("StatsTally", "Total Power"); + return {TallyRow{name, QString::number(total)}}; +} diff --git a/cockatrice/src/game_graphics/tally/stats_tally.h b/cockatrice/src/game_graphics/tally/stats_tally.h new file mode 100644 index 000000000..4c3d93b56 --- /dev/null +++ b/cockatrice/src/game_graphics/tally/stats_tally.h @@ -0,0 +1,21 @@ +#ifndef COCKATRICE_STATS_TALLY_H +#define COCKATRICE_STATS_TALLY_H +#include "tally.h" + +/** + * @brief Extracts and tallies stats from selected cards. + */ +namespace StatsTally +{ + +/** + * @brief Sums the power of all selected cards + * + * @param cards The list of selected card items to analyze. + * @return A single row containing the total, or an empty list if none of the cards have pt + */ +QList computeTotalPower(const QList &cards); + +} // namespace StatsTally + +#endif // COCKATRICE_STATS_TALLY_H diff --git a/cockatrice/src/game_graphics/tally/subtype_tally.cpp b/cockatrice/src/game_graphics/tally/subtype_tally.cpp index 5d34f6874..2241eb3b2 100644 --- a/cockatrice/src/game_graphics/tally/subtype_tally.cpp +++ b/cockatrice/src/game_graphics/tally/subtype_tally.cpp @@ -66,8 +66,9 @@ QList countSubtypes(const QList &cards) }); // convert entries into TallyRows - QList rows = QList(entries.size()); - std::transform(entries.begin(), entries.end(), rows.begin(), + QList rows; + rows.reserve(entries.size()); + std::transform(entries.begin(), entries.end(), std::back_inserter(rows), [](const SubtypeEntry &e) { return TallyRow{e.name, QString::number(e.count)}; }); return rows; diff --git a/cockatrice/src/game_graphics/tally/tally.cpp b/cockatrice/src/game_graphics/tally/tally.cpp index f9389d0d6..aa2cae024 100644 --- a/cockatrice/src/game_graphics/tally/tally.cpp +++ b/cockatrice/src/game_graphics/tally/tally.cpp @@ -1,5 +1,6 @@ #include "tally.h" +#include "stats_tally.h" #include "subtype_tally.h" TallyType Tally::intToType(int value) @@ -18,6 +19,8 @@ QList Tally::compute(const QList &cards, const TallyType t return {}; case TallyType::Subtypes: return SubtypeTally::countSubtypes(cards); + case TallyType::TotalPower: + return StatsTally::computeTotalPower(cards); } return {}; } diff --git a/cockatrice/src/game_graphics/tally/tally.h b/cockatrice/src/game_graphics/tally/tally.h index d0fd77127..97406cddb 100644 --- a/cockatrice/src/game_graphics/tally/tally.h +++ b/cockatrice/src/game_graphics/tally/tally.h @@ -20,7 +20,8 @@ enum class TallyType { None, Subtypes, - MaxValue = Subtypes // sentinel value + TotalPower, + MaxValue = TotalPower // sentinel value }; namespace Tally diff --git a/cockatrice/src/game_graphics/zones/hand_zone.cpp b/cockatrice/src/game_graphics/zones/hand_zone.cpp index 8d0a28fc6..b52a4955a 100644 --- a/cockatrice/src/game_graphics/zones/hand_zone.cpp +++ b/cockatrice/src/game_graphics/zones/hand_zone.cpp @@ -34,7 +34,7 @@ void HandZone::handleDropEvent(const QList &dragItems, QPoint point = dropPoint + scenePos().toPoint(); int x = -1; - if (SettingsCache::instance().interface().getHorizontalHand()) { + if (SettingsCache::instance().userInterface().getHorizontalHand()) { for (x = 0; x < getLogic()->getCards().size(); x++) { if (point.x() < static_cast(getLogic()->getCards().at(x))->scenePos().x()) { break; @@ -61,7 +61,7 @@ void HandZone::handleDropEvent(const QList &dragItems, QRectF HandZone::boundingRect() const { - if (SettingsCache::instance().interface().getHorizontalHand()) { + if (SettingsCache::instance().userInterface().getHorizontalHand()) { return QRectF(0, 0, width, CardDimensions::HEIGHT_F + 10); } else { return QRectF(0, 0, CardDimensions::WIDTH_F * 1.5, zoneHeight); @@ -78,8 +78,8 @@ void HandZone::reorganizeCards() { if (!getLogic()->getCards().isEmpty()) { const int cardCount = getLogic()->getCards().size(); - if (SettingsCache::instance().interface().getHorizontalHand()) { - bool leftJustified = SettingsCache::instance().interface().getLeftJustified(); + if (SettingsCache::instance().userInterface().getHorizontalHand()) { + bool leftJustified = SettingsCache::instance().userInterface().getLeftJustified(); qreal cardWidth = getLogic()->getCards().at(0)->boundingRect().width(); const int xPadding = leftJustified ? cardWidth * 1.4 : 5; qreal totalWidth = @@ -127,7 +127,7 @@ void HandZone::sortHand(const QList &options) void HandZone::setWidth(qreal _width) { - if (SettingsCache::instance().interface().getHorizontalHand()) { + if (SettingsCache::instance().userInterface().getHorizontalHand()) { prepareGeometryChange(); width = _width; reorganizeCards(); diff --git a/cockatrice/src/game_graphics/zones/table_zone.cpp b/cockatrice/src/game_graphics/zones/table_zone.cpp index 21138854e..4ef01853f 100644 --- a/cockatrice/src/game_graphics/zones/table_zone.cpp +++ b/cockatrice/src/game_graphics/zones/table_zone.cpp @@ -29,7 +29,7 @@ TableZone::TableZone(TableZoneLogic *_logic, bool _mirrored, QGraphicsItem *pare connect(_logic, &TableZoneLogic::contentSizeChanged, this, &TableZone::resizeToContents); connect(_logic, &TableZoneLogic::toggleTapped, this, &TableZone::toggleTapped); connect(themeManager, &ThemeManager::themeChanged, this, &TableZone::updateBg); - connect(&SettingsCache::instance().interface(), &InterfaceSettings::invertVerticalCoordinateChanged, this, + connect(&SettingsCache::instance().userInterface(), &InterfaceSettings::invertVerticalCoordinateChanged, this, &TableZone::reorganizeCards); updateBg(); @@ -60,8 +60,8 @@ void TableZone::setMirrored(bool isMirrored) bool TableZone::isInverted() const { - return ((mirrored && !SettingsCache::instance().interface().getInvertVerticalCoordinate()) || - (!mirrored && SettingsCache::instance().interface().getInvertVerticalCoordinate())); + return ((mirrored && !SettingsCache::instance().userInterface().getInvertVerticalCoordinate()) || + (!mirrored && SettingsCache::instance().userInterface().getInvertVerticalCoordinate())); } void TableZone::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*option*/, QWidget * /*widget*/) diff --git a/cockatrice/src/game_graphics/zones/view_zone_widget.cpp b/cockatrice/src/game_graphics/zones/view_zone_widget.cpp index 90f7dbca5..17118e80d 100644 --- a/cockatrice/src/game_graphics/zones/view_zone_widget.cpp +++ b/cockatrice/src/game_graphics/zones/view_zone_widget.cpp @@ -66,7 +66,7 @@ ZoneViewWidget::ZoneViewWidget(PlayerLogic *_player, connect(help, &QAction::triggered, this, [this] { createSearchSyntaxHelpWindow(&searchEdit); }); - if (SettingsCache::instance().interface().getFocusCardViewSearchBar()) { + if (SettingsCache::instance().userInterface().getFocusCardViewSearchBar()) { this->setActive(true); searchEdit.setFocus(); } @@ -77,9 +77,9 @@ ZoneViewWidget::ZoneViewWidget(PlayerLogic *_player, vbox->addItem(searchEditProxy); // hide search bar if chat autofocus setting is enabled, since typing into it will no longer work anyway - searchEditProxy->setVisible(!SettingsCache::instance().interface().getKeepGameChatFocus()); - connect(&SettingsCache::instance().interface(), &InterfaceSettings::keepGameChatFocusChanged, searchEditProxy, - [searchEditProxy](bool keepFocus) { searchEditProxy->setVisible(!keepFocus); }); + searchEditProxy->setVisible(!SettingsCache::instance().userInterface().getKeepGameChatFocus()); + connect(&SettingsCache::instance().userInterface(), &InterfaceSettings::keepGameChatFocusChanged, + searchEditProxy, [searchEditProxy](bool keepFocus) { searchEditProxy->setVisible(!keepFocus); }); // top row QGraphicsLinearLayout *hTopRow = new QGraphicsLinearLayout(Qt::Horizontal); @@ -159,9 +159,9 @@ ZoneViewWidget::ZoneViewWidget(PlayerLogic *_player, connect(&sortBySelector, static_cast(&QComboBox::currentIndexChanged), this, &ZoneViewWidget::processSortBy); connect(&pileViewCheckBox, &QCheckBox::QT_STATE_CHANGED, this, &ZoneViewWidget::processSetPileView); - groupBySelector.setCurrentIndex(SettingsCache::instance().interface().getZoneViewGroupByIndex()); - sortBySelector.setCurrentIndex(SettingsCache::instance().interface().getZoneViewSortByIndex()); - pileViewCheckBox.setChecked(SettingsCache::instance().interface().getZoneViewPileView()); + groupBySelector.setCurrentIndex(SettingsCache::instance().userInterface().getZoneViewGroupByIndex()); + sortBySelector.setCurrentIndex(SettingsCache::instance().userInterface().getZoneViewSortByIndex()); + pileViewCheckBox.setChecked(SettingsCache::instance().userInterface().getZoneViewPileView()); if (CardList::NoSort == static_cast(groupBySelector.currentData().toInt())) { pileViewCheckBox.setEnabled(false); @@ -191,7 +191,7 @@ ZoneViewWidget::ZoneViewWidget(PlayerLogic *_player, void ZoneViewWidget::processGroupBy(int index) { auto option = static_cast(groupBySelector.itemData(index).toInt()); - SettingsCache::instance().interface().setZoneViewGroupByIndex(index); + SettingsCache::instance().userInterface().setZoneViewGroupByIndex(index); zone->setGroupBy(option); // disable pile view checkbox if we're not grouping by anything @@ -215,13 +215,13 @@ void ZoneViewWidget::processSortBy(int index) return; } - SettingsCache::instance().interface().setZoneViewSortByIndex(index); + SettingsCache::instance().userInterface().setZoneViewSortByIndex(index); zone->setSortBy(option); } void ZoneViewWidget::processSetPileView(QT_STATE_CHANGED_T value) { - SettingsCache::instance().interface().setZoneViewPileView(value); + SettingsCache::instance().userInterface().setZoneViewPileView(value); zone->setPileView(value); } @@ -478,7 +478,7 @@ static qreal rowsToHeight(int rows) **/ static qreal calcMaxInitialHeight() { - return rowsToHeight(SettingsCache::instance().interface().getCardViewInitialRowsMax()); + return rowsToHeight(SettingsCache::instance().userInterface().getCardViewInitialRowsMax()); } /** @@ -560,7 +560,7 @@ void ZoneViewWidget::initStyleOption(QStyleOption *option) const void ZoneViewWidget::expandWindow() { qreal maxInitialHeight = calcMaxInitialHeight(); - qreal maxExpandedHeight = rowsToHeight(SettingsCache::instance().interface().getCardViewExpandedRowsMax()); + qreal maxExpandedHeight = rowsToHeight(SettingsCache::instance().userInterface().getCardViewExpandedRowsMax()); qreal height = rect().height() - extraHeight - 10; qreal maxHeight = maximumHeight() - extraHeight - 10; diff --git a/cockatrice/src/interface/card_picture_loader/card_picture_loader.cpp b/cockatrice/src/interface/card_picture_loader/card_picture_loader.cpp index 47f308698..0a1bb8be1 100644 --- a/cockatrice/src/interface/card_picture_loader/card_picture_loader.cpp +++ b/cockatrice/src/interface/card_picture_loader/card_picture_loader.cpp @@ -19,19 +19,22 @@ #include #include #include +#include #include -#include #include // never cache more than 300 cards at once for a single deck #define CACHED_CARD_PER_DECK_MAX 300 +// wait at least this long before retrying a card whose picture failed to load +static constexpr int RETRY_FAILED_CARDS_SECS = 300; + CardPictureLoader::CardPictureLoader() : QObject(nullptr) { worker = new CardPictureLoaderWorker; connect(&SettingsCache::instance().paths(), &PathsSettings::picsPathChanged, this, &CardPictureLoader::picsPathChanged); - connect(&SettingsCache::instance().personal(), &PersonalSettings::picDownloadChanged, this, + connect(&SettingsCache::instance().downloads(), &DownloadSettings::picDownloadChanged, this, &CardPictureLoader::picDownloadChanged); qRegisterMetaType(); @@ -135,7 +138,14 @@ void CardPictureLoader::getPixmap(QPixmap &pixmap, const ExactCard &card, QSize QPixmap bigPixmap; if (QPixmapCache::find(key, &bigPixmap)) { if (bigPixmap.isNull()) { - qCDebug(CardPictureLoaderLog) << "Cached pixmap for key" << key << "is NULL!"; + getCardBackLoadingFailedPixmap(pixmap, size); + QDateTime failedAtTime = getInstance().failedAt.value(key); + if (!failedAtTime.isValid() || + failedAtTime.addSecs(RETRY_FAILED_CARDS_SECS) < QDateTime::currentDateTime()) { + getInstance().failedAt.remove(key); + QPixmapCache::remove(key); + getInstance().worker->enqueueImageLoad(card); + } return; } @@ -159,8 +169,10 @@ void CardPictureLoader::imageLoaded(const ExactCard &card, const QImage &image) QPixmap finalPixmap; if (image.isNull()) { + getInstance().failedAt.insert(card.getPixmapCacheKey(), QDateTime::currentDateTime()); qCDebug(CardPictureLoaderLog) << "Caching NULL pixmap for" << card.getName(); } else { + getInstance().failedAt.remove(card.getPixmapCacheKey()); if (card.getInfo().getUiAttributes().upsideDownArt) { #if (QT_VERSION >= QT_VERSION_CHECK(6, 9, 0)) QImage mirrorImage = image.flipped(Qt::Horizontal | Qt::Vertical); @@ -184,8 +196,10 @@ void CardPictureLoader::imageLoaded(const ExactCard &card, const QImage &image) // imageLoaded should only be reached if the exactCard isn't already in cache. // (plus there's a deduplication mechanism in CardPictureLoaderWorker) // It should be safe to connect the CardInfo here without worrying about redundant connections. - connect(card.getCardPtr().data(), &QObject::destroyed, this, - [cacheKey = card.getPixmapCacheKey()] { QPixmapCache::remove(cacheKey); }); + connect(card.getCardPtr().data(), &QObject::destroyed, this, [cacheKey = card.getPixmapCacheKey()] { + QPixmapCache::remove(cacheKey); + getInstance().failedAt.remove(cacheKey); + }); card.emitPixmapUpdated(); } diff --git a/cockatrice/src/interface/card_picture_loader/card_picture_loader.h b/cockatrice/src/interface/card_picture_loader/card_picture_loader.h index 0c114ae92..5c3ac84a3 100644 --- a/cockatrice/src/interface/card_picture_loader/card_picture_loader.h +++ b/cockatrice/src/interface/card_picture_loader/card_picture_loader.h @@ -4,6 +4,8 @@ #include "card_picture_loader_status_bar.h" #include "card_picture_loader_worker.h" +#include +#include #include inline Q_LOGGING_CATEGORY(CardPictureLoaderLog, "card_picture_loader"); @@ -56,6 +58,7 @@ private: CardPictureLoaderWorker *worker; ///< Worker thread for async image loading CardPictureLoaderStatusBar *statusBar; ///< Status bar widget showing load progress + QHash failedAt; ///< Timestamp of the last failed load attempt per pixmap cache key public: /** diff --git a/cockatrice/src/interface/card_picture_loader/card_picture_loader_worker.cpp b/cockatrice/src/interface/card_picture_loader/card_picture_loader_worker.cpp index 3724c184d..d288236d2 100644 --- a/cockatrice/src/interface/card_picture_loader/card_picture_loader_worker.cpp +++ b/cockatrice/src/interface/card_picture_loader/card_picture_loader_worker.cpp @@ -11,15 +11,17 @@ #include #include #include +#include #include -#include #include #include static constexpr int MAX_REQUESTS_PER_SEC = 10; +static constexpr int MIN_HOST_QUOTA = 1; ///< Floor for the per-host request allowance +static constexpr qint64 QUOTA_RECOVER_MS = 60000; ///< Idle time before a reduced quota starts recovering CardPictureLoaderWorker::CardPictureLoaderWorker() - : QObject(nullptr), picDownload(SettingsCache::instance().personal().getPicDownload()), + : QObject(nullptr), picDownload(SettingsCache::instance().downloads().getPicDownload()), requestQuota(MAX_REQUESTS_PER_SEC) { networkManager = new QNetworkAccessManager(this); @@ -124,6 +126,19 @@ QNetworkReply *CardPictureLoaderWorker::makeRequest(const QUrl &url, CardPicture void CardPictureLoaderWorker::resetRequestQuota() { requestQuota = MAX_REQUESTS_PER_SEC; + + QDateTime now = QDateTime::currentDateTime(); + for (auto it = hostRequestQuota.begin(); it != hostRequestQuota.end(); ++it) { + if (!hostLast429.contains(it.key()) || now.msecsTo(hostLast429.value(it.key())) < -QUOTA_RECOVER_MS) { + it.value() = qMin(MAX_REQUESTS_PER_SEC, it.value() + 1); + } + } + + for (const auto &request : requestLoadQueue) { + const QString host = request.first.host(); + hostQuotaRemaining.insert(host, hostRequestQuota.value(host, MAX_REQUESTS_PER_SEC)); + } + processQueuedRequests(); } @@ -136,14 +151,26 @@ void CardPictureLoaderWorker::processQueuedRequests() bool CardPictureLoaderWorker::processSingleRequest() { - if (!requestLoadQueue.isEmpty()) { - auto request = requestLoadQueue.takeFirst(); - makeRequest(request.first, request.second); - return true; + for (int i = 0; i < requestLoadQueue.size(); ++i) { + const auto &request = requestLoadQueue.at(i); + QString host = request.first.host(); + int allowance = hostQuotaRemaining.value(host, MAX_REQUESTS_PER_SEC); + if (allowance > 0) { + hostQuotaRemaining.insert(host, allowance - 1); + makeRequest(request.first, request.second); + requestLoadQueue.removeAt(i); + return true; + } } return false; } +void CardPictureLoaderWorker::onHostRateLimited(const QString &host) +{ + hostRequestQuota.insert(host, qMax(MIN_HOST_QUOTA, hostRequestQuota.value(host, MAX_REQUESTS_PER_SEC) / 2)); + hostLast429.insert(host, QDateTime::currentDateTime()); +} + void CardPictureLoaderWorker::enqueueImageLoad(const ExactCard &card) { // Send call through a connection to ensure the handling is run on the pictureLoader thread diff --git a/cockatrice/src/interface/card_picture_loader/card_picture_loader_worker.h b/cockatrice/src/interface/card_picture_loader/card_picture_loader_worker.h index f927abde6..d1c519b7a 100644 --- a/cockatrice/src/interface/card_picture_loader/card_picture_loader_worker.h +++ b/cockatrice/src/interface/card_picture_loader/card_picture_loader_worker.h @@ -5,6 +5,8 @@ #include "card_picture_loader_worker_work.h" #include "card_picture_to_load.h" +#include +#include #include #include #include @@ -66,6 +68,12 @@ public: */ void queueRequest(const QUrl &url, CardPictureLoaderWorkerWork *worker); + /** + * @brief Handles a server returning HTTP 429 by reducing that host's request quota. + * @param host The host that returned 429 + */ + void onHostRateLimited(const QString &host); + /** @brief Clears the network cache and redirect cache. */ void clearNetworkCache(); @@ -110,8 +118,11 @@ private: bool picDownload; ///< Whether downloading images from network is enabled QQueue> requestLoadQueue; ///< Queue of pending network requests - int requestQuota; ///< Remaining requests allowed per second - QTimer requestTimer; ///< Timer to reset the request quota + int requestQuota; ///< Remaining requests allowed per second + QTimer requestTimer; ///< Timer to reset the request quota + QHash hostRequestQuota; ///< Sustained per-host request allowance + QHash hostQuotaRemaining; ///< Per-host allowance left in the current second + QHash hostLast429; ///< When each host was last rate limited CardPictureLoaderLocal *localLoader; ///< Loader for local images QSet currentlyLoading; ///< Deduplication: contains pixmapCacheKey currently being loaded diff --git a/cockatrice/src/interface/card_picture_loader/card_picture_loader_worker_work.cpp b/cockatrice/src/interface/card_picture_loader/card_picture_loader_worker_work.cpp index ebaf11fff..66c56337c 100644 --- a/cockatrice/src/interface/card_picture_loader/card_picture_loader_worker_work.cpp +++ b/cockatrice/src/interface/card_picture_loader/card_picture_loader_worker_work.cpp @@ -8,9 +8,13 @@ #include #include #include +#include #include #include -#include +#include + +ServerRateLimiter CardPictureLoaderWorkerWork::s_rateLimiter; +#include // Card back returned by gatherer when card is not found static const QStringList MD5_BLACKLIST = { @@ -20,7 +24,7 @@ static const QStringList MD5_BLACKLIST = { CardPictureLoaderWorkerWork::CardPictureLoaderWorkerWork(const CardPictureLoaderWorker *worker, const ExactCard &toLoad) : QObject(nullptr), cardToDownload(CardPictureToLoad(toLoad)), - picDownload(SettingsCache::instance().personal().getPicDownload()) + picDownload(SettingsCache::instance().downloads().getPicDownload()) { // Hook up signals to the orchestrator connect(this, &CardPictureLoaderWorkerWork::requestImageDownload, worker, &CardPictureLoaderWorker::queueRequest); @@ -30,19 +34,48 @@ CardPictureLoaderWorkerWork::CardPictureLoaderWorkerWork(const CardPictureLoader connect(this, &CardPictureLoaderWorkerWork::imageLoaded, worker, &CardPictureLoaderWorker::handleImageLoaded); connect(this, &CardPictureLoaderWorkerWork::requestSucceeded, worker, &CardPictureLoaderWorker::imageRequestSucceeded); + connect(this, &CardPictureLoaderWorkerWork::rateLimited, worker, &CardPictureLoaderWorker::onHostRateLimited); // Hook up signals to settings - connect(&SettingsCache::instance().personal(), SIGNAL(picDownloadChanged()), this, SLOT(picDownloadChanged())); + connect(&SettingsCache::instance().downloads(), SIGNAL(picDownloadChanged()), this, SLOT(picDownloadChanged())); startNextPicDownload(); } void CardPictureLoaderWorkerWork::startNextPicDownload() { + QDateTime now = QDateTime::currentDateTime(); + while (!cardToDownload.getCurrentUrl().isEmpty() && + s_rateLimiter.isRateLimited(QUrl(cardToDownload.getCurrentUrl()).host(), now)) { + QString host = QUrl(cardToDownload.getCurrentUrl()).host(); + if (s_rateLimiter.rounds(host) == 1) { + // First 429 round for this server: wait out the backoff and give it + // one more chance instead of immediately falling through to a worse + // source. A second 429 makes us fall through instead. + qCDebug(CardPictureLoaderWorkerWorkLog).nospace() + << "PictureLoader: [card: " << cardToDownload.getCard().getInfo().getCorrectedName() + << " set: " << cardToDownload.getSetName() << "]: Waiting out backoff for " << host << " to retry " + << cardToDownload.getCurrentUrl(); + scheduleDeferredRetry(); + return; + } + + // The server has already 429'd us at least twice, so further retries are + // unlikely to succeed: move on to the other configured sources. + qCDebug(CardPictureLoaderWorkerWorkLog).nospace() + << "PictureLoader: [card: " << cardToDownload.getCard().getInfo().getCorrectedName() + << " set: " << cardToDownload.getSetName() << "]: Skipping rate-limited URL " + << cardToDownload.getCurrentUrl() << " (server " << host << " still rate limiting)"; + if (!cardToDownload.nextUrl() && !cardToDownload.nextSet()) { + scheduleDeferredRetry(); + return; + } + } + QString picUrl = cardToDownload.getCurrentUrl(); if (picUrl.isEmpty()) { - picDownloadFailed(); + scheduleDeferredRetry(); } else { QUrl url(picUrl); qCDebug(CardPictureLoaderWorkerWorkLog).nospace() @@ -108,7 +141,41 @@ static bool imageIsBlackListed(const QByteArray &picData) void CardPictureLoaderWorkerWork::handleFailedReply(const QNetworkReply *reply) { if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 429) { - qCWarning(CardPictureLoaderWorkerWorkLog) << "Too many requests."; + QString host = reply->url().host(); + QDateTime now = QDateTime::currentDateTime(); + + qint64 retryAfterMs = 0; + const QByteArray retryAfterHeader = reply->rawHeader("Retry-After"); + if (!retryAfterHeader.isEmpty()) { + bool ok = false; + int seconds = retryAfterHeader.toInt(&ok); + if (ok && seconds > 0) { + retryAfterMs = static_cast(seconds) * 1000; + } else { + QDateTime retryAfterDate = + QDateTime::fromString(QString::fromLatin1(retryAfterHeader), Qt::RFC2822Date); + if (retryAfterDate.isValid()) { + retryAfterMs = qMax(0, now.msecsTo(retryAfterDate)); + } + } + } + + QDateTime backoffUntil = s_rateLimiter.on429(host, now, retryAfterMs); + emit rateLimited(host); + + if (s_rateLimiter.rounds(host) == 1) { + qCWarning(CardPictureLoaderWorkerWorkLog).nospace() + << "PictureLoader: [card: " << cardToDownload.getCard().getName() + << " set: " << cardToDownload.getSetName() << "]: Too many requests from " << host + << ", backing off until " << backoffUntil.toString(Qt::ISODate) << ", retrying the same url"; + scheduleDeferredRetry(); + } else { + qCWarning(CardPictureLoaderWorkerWorkLog).nospace() + << "PictureLoader: [card: " << cardToDownload.getCard().getName() + << " set: " << cardToDownload.getSetName() << "]: Too many requests from " << host + << ", retry already attempted, falling through to other sources"; + picDownloadFailed(); + } } else { bool isFromCache = reply->attribute(QNetworkRequest::SourceIsFromCacheAttribute).toBool(); @@ -149,6 +216,9 @@ void CardPictureLoaderWorkerWork::handleSuccessfulReply(QNetworkReply *reply) return; } + // A non-redirect successful response means the server is not rate limiting us anymore. + s_rateLimiter.onSuccess(reply->url().host()); + // peek is used to keep the data in the buffer for use by QImageReader const QByteArray &picData = reply->peek(reply->size()); @@ -203,6 +273,42 @@ QImage CardPictureLoaderWorkerWork::tryLoadImageFromReply(QNetworkReply *reply) return imgReader.read(); } +void CardPictureLoaderWorkerWork::scheduleDeferredRetry() +{ + QDateTime now = QDateTime::currentDateTime(); + + // Prefer waiting on the current URL's server so we retry the same source. + QString currentHost = QUrl(cardToDownload.getCurrentUrl()).host(); + QDateTime backoffUntil = s_rateLimiter.deadline(currentHost); + if (!s_rateLimiter.isRateLimited(currentHost, now)) { + backoffUntil = s_rateLimiter.earliestDeadline(now); + } + + if (!backoffUntil.isValid()) { + qCWarning(CardPictureLoaderWorkerWorkLog).nospace() + << "PictureLoader: [card: " << cardToDownload.getCard().getInfo().getCorrectedName() + << " set: " << cardToDownload.getSetName() << "]: All URLs exhausted, no servers in backoff: BAILING OUT"; + concludeImageLoad(QImage()); + return; + } + + qint64 waitMs = qMax(0, now.msecsTo(backoffUntil)); + // Add some jitter to desynchronize concurrent retries and avoid a thundering herd. + waitMs += QRandomGenerator::global()->bounded(5000); + + qCDebug(CardPictureLoaderWorkerWorkLog).nospace() + << "PictureLoader: [card: " << cardToDownload.getCard().getInfo().getCorrectedName() + << " set: " << cardToDownload.getSetName() << "]: All URLs exhausted, scheduling deferred retry in " << waitMs + << "ms"; + + QTimer::singleShot(waitMs, this, [this] { + s_rateLimiter.clearExpired(QDateTime::currentDateTime()); + + cardToDownload.resetIndices(); + startNextPicDownload(); + }); +} + void CardPictureLoaderWorkerWork::concludeImageLoad(const QImage &image) { emit imageLoaded(cardToDownload.getCard(), image); @@ -211,5 +317,5 @@ void CardPictureLoaderWorkerWork::concludeImageLoad(const QImage &image) void CardPictureLoaderWorkerWork::picDownloadChanged() { - picDownload = SettingsCache::instance().personal().getPicDownload(); + picDownload = SettingsCache::instance().downloads().getPicDownload(); } diff --git a/cockatrice/src/interface/card_picture_loader/card_picture_loader_worker_work.h b/cockatrice/src/interface/card_picture_loader/card_picture_loader_worker_work.h index cdffc1dff..1e56a4373 100644 --- a/cockatrice/src/interface/card_picture_loader/card_picture_loader_worker_work.h +++ b/cockatrice/src/interface/card_picture_loader/card_picture_loader_worker_work.h @@ -4,12 +4,15 @@ #include "card_picture_loader_worker.h" #include "card_picture_to_load.h" +#include #include #include #include #include +#include #include #include +#include inline Q_LOGGING_CATEGORY(CardPictureLoaderWorkerWorkLog, "card_picture_loader.worker"); @@ -50,6 +53,8 @@ public slots: private: bool picDownload; ///< Whether network downloading is enabled + static ServerRateLimiter s_rateLimiter; ///< Shared per-server 429 backoff state + /** @brief Starts downloading the next URL for this card. */ void startNextPicDownload(); @@ -77,6 +82,16 @@ private: */ void concludeImageLoad(const QImage &image); + /** + * @brief Schedules a deferred retry after the relevant server backoff expires. + * + * Waits on the current URL's server when it is the reason we are blocked, + * otherwise on the earliest active backoff. If no servers are in backoff, + * concludes with failure. Otherwise resets the CardPictureToLoad indices and + * retries after the backoff period. + */ + void scheduleDeferredRetry(); + private slots: /** @brief Updates the picDownload setting when it changes. */ void picDownloadChanged(); @@ -100,6 +115,9 @@ signals: /** @brief Emitted when a URL has been redirected. */ void urlRedirected(const QUrl &originalUrl, const QUrl &redirectUrl); + /** @brief Emitted when a server returned HTTP 429. */ + void rateLimited(const QString &host); + /** @brief Emitted when a cached URL is invalid and must be removed. */ void cachedUrlInvalidated(const QUrl &url); }; diff --git a/cockatrice/src/interface/card_picture_loader/card_picture_to_load.cpp b/cockatrice/src/interface/card_picture_loader/card_picture_to_load.cpp index 33e4aabdb..5f4ff0bbd 100644 --- a/cockatrice/src/interface/card_picture_loader/card_picture_to_load.cpp +++ b/cockatrice/src/interface/card_picture_loader/card_picture_to_load.cpp @@ -17,8 +17,9 @@ CardPictureToLoad::CardPictureToLoad(const ExactCard &_card) { if (card) { sortedSets = extractSetsSorted(card); - // The first time called, nextSet will also populate the Urls for the first set. - nextSet(); + currentSetIndex = 0; + currentSet = sortedSets.first(); + populateSetUrls(); } } @@ -101,15 +102,19 @@ void CardPictureToLoad::populateSetUrls() } } - /* Call nextUrl to make sure currentUrl is up-to-date - but we don't need the result here. */ - (void)nextUrl(); + currentUrlIndex = 0; + if (!currentSetUrls.isEmpty()) { + currentUrl = currentSetUrls.first(); + } else { + currentUrl = QString(); + } } bool CardPictureToLoad::nextSet() { - if (!sortedSets.isEmpty()) { - currentSet = sortedSets.takeFirst(); + currentSetIndex++; + if (currentSetIndex < sortedSets.size()) { + currentSet = sortedSets.at(currentSetIndex); populateSetUrls(); return true; } @@ -119,8 +124,9 @@ bool CardPictureToLoad::nextSet() bool CardPictureToLoad::nextUrl() { - if (!currentSetUrls.isEmpty()) { - currentUrl = currentSetUrls.takeFirst(); + currentUrlIndex++; + if (currentUrlIndex < currentSetUrls.size()) { + currentUrl = currentSetUrls.at(currentUrlIndex); return true; } currentUrl = QString(); @@ -136,6 +142,28 @@ QString CardPictureToLoad::getSetName() const } } +QString CardPictureToLoad::peekNextUrl() const +{ + int nextIndex = currentUrlIndex + 1; + if (nextIndex < currentSetUrls.size()) { + return currentSetUrls.at(nextIndex); + } + return QString(); +} + +void CardPictureToLoad::resetIndices() +{ + currentSetIndex = 0; + if (!sortedSets.isEmpty()) { + currentSet = sortedSets.first(); + populateSetUrls(); + } else { + currentSet = {}; + currentSetUrls.clear(); + currentUrl = QString(); + } +} + static int parse(const QString &urlTemplate, const QString &propType, const QString &cardName, diff --git a/cockatrice/src/interface/card_picture_loader/card_picture_to_load.h b/cockatrice/src/interface/card_picture_loader/card_picture_to_load.h index b57e57644..9e0e7449c 100644 --- a/cockatrice/src/interface/card_picture_loader/card_picture_to_load.h +++ b/cockatrice/src/interface/card_picture_loader/card_picture_to_load.h @@ -25,6 +25,8 @@ private: QList currentSetUrls; ///< URLs for the current set being attempted QString currentUrl; ///< Currently active URL to download CardSetPtr currentSet; ///< Currently active set + int currentSetIndex = 0; ///< Current position in sortedSets + int currentUrlIndex = 0; ///< Current position in currentSetUrls public: /** @@ -56,6 +58,9 @@ public: /** @return The short name of the current set, or empty string if no set. */ [[nodiscard]] QString getSetName() const; + /** @return The next URL in the current set's list without advancing, or empty if at end. */ + [[nodiscard]] QString peekNextUrl() const; + /** * @brief Transforms a URL template into a concrete URL for this card/set. * @param urlTemplate The URL template to transform @@ -88,6 +93,14 @@ public: */ void populateSetUrls(); + /** + * @brief Resets iteration indices to the beginning. + * + * Restarts URL/set iteration from the first set and first URL. + * Used for deferred retry after server backoff expires. + */ + void resetIndices(); + /** * @brief Extract all sets from the card and sort them by priority. * @param card The card to extract sets from diff --git a/cockatrice/src/interface/intents/contexts/context_connect_to_server.h b/cockatrice/src/interface/intents/contexts/context_connect_to_server.h new file mode 100644 index 000000000..c7c40b261 --- /dev/null +++ b/cockatrice/src/interface/intents/contexts/context_connect_to_server.h @@ -0,0 +1,14 @@ +#ifndef COCKATRICE_CONTEXT_CONNECT_TO_SERVER_H +#define COCKATRICE_CONTEXT_CONNECT_TO_SERVER_H + +#include + +struct ContextConnectToServer +{ + QString hostname; + QString port; + QString username; + QString password; +}; + +#endif // COCKATRICE_CONTEXT_CONNECT_TO_SERVER_H diff --git a/cockatrice/src/interface/intents/contexts/context_join_game.h b/cockatrice/src/interface/intents/contexts/context_join_game.h new file mode 100644 index 000000000..102e2a520 --- /dev/null +++ b/cockatrice/src/interface/intents/contexts/context_join_game.h @@ -0,0 +1,11 @@ +#ifndef COCKATRICE_CONTEXT_JOIN_GAME_H +#define COCKATRICE_CONTEXT_JOIN_GAME_H +#include "context_join_room.h" + +struct ContextJoinGame +{ + ContextJoinRoom roomContext; + int gameId; +}; + +#endif // COCKATRICE_CONTEXT_JOIN_GAME_H diff --git a/cockatrice/src/interface/intents/contexts/context_join_room.h b/cockatrice/src/interface/intents/contexts/context_join_room.h new file mode 100644 index 000000000..23ae05e81 --- /dev/null +++ b/cockatrice/src/interface/intents/contexts/context_join_room.h @@ -0,0 +1,14 @@ +#ifndef COCKATRICE_CONTEXT_JOIN_ROOM_H +#define COCKATRICE_CONTEXT_JOIN_ROOM_H + +#include "context_connect_to_server.h" + +#include + +struct ContextJoinRoom +{ + ContextConnectToServer serverContext; + int roomId; +}; + +#endif // COCKATRICE_CONTEXT_JOIN_ROOM_H diff --git a/cockatrice/src/interface/intents/intent.cpp b/cockatrice/src/interface/intents/intent.cpp new file mode 100644 index 000000000..c02a89f35 --- /dev/null +++ b/cockatrice/src/interface/intents/intent.cpp @@ -0,0 +1,48 @@ +#include "intent.h" + +Intent::Intent(QObject *parent) : QObject(parent) +{ + // An intent is done as soon as it reports success or failure. Deleting it + // also tears down its dependency chain and disconnects any signal wiring. + connect(this, &Intent::finished, this, &QObject::deleteLater); + connect(this, &Intent::failed, this, &QObject::deleteLater); +} + +Intent::~Intent() = default; + +void Intent::execute() +{ + if (checkPrecondition()) { + onPreconditionSatisfied(); + } else { + onPreconditionNotSatisfied(); + } +} + +void Intent::runDependency(Intent *dependency) +{ + dependency->setParent(this); + connect(dependency, &Intent::finished, this, [this]() { + // Re-check after dependency finishes + this->execute(); + }); + connect(dependency, &Intent::failed, this, &Intent::failed); + + dependency->execute(); +} + +void Intent::emitFinished() +{ + if (!completed) { + completed = true; + emit finished(); + } +} + +void Intent::emitFailed(const QString &reason) +{ + if (!completed) { + completed = true; + emit failed(reason); + } +} diff --git a/cockatrice/src/interface/intents/intent.h b/cockatrice/src/interface/intents/intent.h new file mode 100644 index 000000000..125900ecd --- /dev/null +++ b/cockatrice/src/interface/intents/intent.h @@ -0,0 +1,37 @@ +#ifndef COCKATRICE_INTENT_H +#define COCKATRICE_INTENT_H + +#include + +class Intent : public QObject +{ + Q_OBJECT + +public: + explicit Intent(QObject *parent = nullptr); + ~Intent() override; + + void execute(); + +signals: + void finished(); + void failed(QString reason); + +protected: + // --- Subclasses must implement these --- + virtual bool checkPrecondition() const = 0; + virtual void onPreconditionSatisfied() = 0; + virtual void onPreconditionNotSatisfied() = 0; + + // Helper to chain another intent + void runDependency(Intent *dependency); + + // Emit the outcome exactly once; ignore late signals after the intent is done. + void emitFinished(); + void emitFailed(const QString &reason); + +private: + bool completed = false; +}; + +#endif // COCKATRICE_INTENT_H diff --git a/cockatrice/src/interface/intents/intent_connect_to_server.cpp b/cockatrice/src/interface/intents/intent_connect_to_server.cpp new file mode 100644 index 000000000..1cccc5a23 --- /dev/null +++ b/cockatrice/src/interface/intents/intent_connect_to_server.cpp @@ -0,0 +1,46 @@ +#include "intent_connect_to_server.h" + +#include "intent_disconnect_from_server.h" + +#include + +IntentConnectToServer::IntentConnectToServer(RemoteClient *_remoteClient, ContextConnectToServer *_context) + : Intent(), remoteClient(_remoteClient), context(_context) +{ +} + +bool IntentConnectToServer::checkPrecondition() const +{ + return remoteClient->getStatus() == ClientStatus::StatusDisconnected; +} + +void IntentConnectToServer::onPreconditionSatisfied() +{ + remoteClient->connectToServer(context->hostname, context->port.toUInt(), context->username, context->password); + connect(remoteClient, &RemoteClient::statusChanged, this, &IntentConnectToServer::onStatusChanged); + connect(remoteClient, &RemoteClient::socketError, this, &IntentConnectToServer::onSocketError); + connect( + remoteClient, &RemoteClient::loginError, this, + [this](Response::ResponseCode, const QString &reason, quint32, const QList &) { emitFailed(reason); }); + + QTimer::singleShot(15000, this, [this]() { + emitFailed(tr("Timed out while connecting to %1:%2").arg(context->hostname, context->port)); + }); +} + +void IntentConnectToServer::onPreconditionNotSatisfied() +{ + runDependency(new IntentDisconnectFromServer(remoteClient)); +} + +void IntentConnectToServer::onStatusChanged(ClientStatus status) +{ + if (status == ClientStatus::StatusLoggedIn) { + emitFinished(); + } +} + +void IntentConnectToServer::onSocketError(const QString &errorString) +{ + emitFailed(tr("Failed to connect to %1:%2: %3").arg(context->hostname, context->port, errorString)); +} diff --git a/cockatrice/src/interface/intents/intent_connect_to_server.h b/cockatrice/src/interface/intents/intent_connect_to_server.h new file mode 100644 index 000000000..eab4d1a21 --- /dev/null +++ b/cockatrice/src/interface/intents/intent_connect_to_server.h @@ -0,0 +1,29 @@ +#ifndef COCKATRICE_INTENT_CONNECT_TO_SERVER_H +#define COCKATRICE_INTENT_CONNECT_TO_SERVER_H + +#include "contexts/context_connect_to_server.h" +#include "intent.h" +#include "remote_client.h" + +class IntentConnectToServer : public Intent +{ + Q_OBJECT + +public: + IntentConnectToServer(RemoteClient *_remoteClient, ContextConnectToServer *_context); + +protected: + bool checkPrecondition() const override; + void onPreconditionSatisfied() override; + void onPreconditionNotSatisfied() override; + +private: + RemoteClient *remoteClient; + ContextConnectToServer *context; + +private slots: + void onStatusChanged(ClientStatus status); + void onSocketError(const QString &errorString); +}; + +#endif // COCKATRICE_INTENT_CONNECT_TO_SERVER_H diff --git a/cockatrice/src/interface/intents/intent_disconnect_from_server.cpp b/cockatrice/src/interface/intents/intent_disconnect_from_server.cpp new file mode 100644 index 000000000..cb39d7bab --- /dev/null +++ b/cockatrice/src/interface/intents/intent_disconnect_from_server.cpp @@ -0,0 +1,29 @@ +#include "intent_disconnect_from_server.h" + +IntentDisconnectFromServer::IntentDisconnectFromServer(RemoteClient *_remoteClient) + : Intent(), remoteClient(_remoteClient) +{ +} + +bool IntentDisconnectFromServer::checkPrecondition() const +{ + return remoteClient->getStatus() == ClientStatus::StatusDisconnected; +} + +void IntentDisconnectFromServer::onPreconditionSatisfied() +{ + emitFinished(); +} + +void IntentDisconnectFromServer::onPreconditionNotSatisfied() +{ + connect(remoteClient, &RemoteClient::statusChanged, this, &IntentDisconnectFromServer::onStatusChanged); + remoteClient->disconnectFromServer(); +} + +void IntentDisconnectFromServer::onStatusChanged(ClientStatus status) +{ + if (status == ClientStatus::StatusDisconnected) { + emitFinished(); + } +} diff --git a/cockatrice/src/interface/intents/intent_disconnect_from_server.h b/cockatrice/src/interface/intents/intent_disconnect_from_server.h new file mode 100644 index 000000000..6e1dfd0c1 --- /dev/null +++ b/cockatrice/src/interface/intents/intent_disconnect_from_server.h @@ -0,0 +1,26 @@ +#ifndef COCKATRICE_INTENT_DISCONNECT_FROM_SERVER_H +#define COCKATRICE_INTENT_DISCONNECT_FROM_SERVER_H + +#include "intent.h" +#include "remote_client.h" + +class IntentDisconnectFromServer : public Intent +{ + Q_OBJECT + +public: + IntentDisconnectFromServer(RemoteClient *_remoteClient); + +protected: + bool checkPrecondition() const override; + void onPreconditionSatisfied() override; + void onPreconditionNotSatisfied() override; + +private: + RemoteClient *remoteClient; + +private slots: + void onStatusChanged(ClientStatus status); +}; + +#endif // COCKATRICE_INTENT_DISCONNECT_FROM_SERVER_H diff --git a/cockatrice/src/interface/intents/intent_join_server_game.cpp b/cockatrice/src/interface/intents/intent_join_server_game.cpp new file mode 100644 index 000000000..fb9c4d5ce --- /dev/null +++ b/cockatrice/src/interface/intents/intent_join_server_game.cpp @@ -0,0 +1,76 @@ +#include "intent_join_server_game.h" + +#include "../widgets/server/game_selector.h" +#include "../widgets/tabs/tab_room.h" +#include "../widgets/tabs/tab_supervisor.h" +#include "intent_join_server_room.h" + +#include + +IntentJoinServerGame::IntentJoinServerGame(TabSupervisor *_tabSupervisor, + RemoteClient *_remoteClient, + std::unique_ptr _context) + : Intent(), tabSupervisor(_tabSupervisor), remoteClient(_remoteClient), context(_context.release()) +{ +} + +bool IntentJoinServerGame::checkPrecondition() const +{ + if (remoteClient->getStatus() != ClientStatus::StatusLoggedIn) { + return false; + } + // peerPort() reflects the actual TCP peer, which may differ from the + // configured server port (e.g. when connecting through a proxy), so only + // the hostname is compared here. + if (remoteClient->peerName() != context->roomContext.serverContext.hostname) { + return false; + } + if (QString::number(remoteClient->peerPort()) != context->roomContext.serverContext.port) { + return false; + } + + if (!tabSupervisor->getRoomTabs().contains(context->roomContext.roomId)) { + return false; + } + + return true; +} + +void IntentJoinServerGame::onPreconditionSatisfied() +{ + TabRoom *room = tabSupervisor->getRoomTabs().value(context->roomContext.roomId); + if (!tryJoinGame(room)) { + waitForGame(room); + } +} + +void IntentJoinServerGame::onPreconditionNotSatisfied() +{ + runDependency(new IntentJoinServerRoom(tabSupervisor, remoteClient, &context->roomContext)); +} + +bool IntentJoinServerGame::tryJoinGame(TabRoom *room) +{ + if (!room) { + return false; + } + + if (room->getGameSelector()->joinGameById(context->gameId)) { + emitFinished(); + return true; + } + + return false; +} + +void IntentJoinServerGame::waitForGame(TabRoom *room) +{ + connect(room, &TabRoom::gameListUpdated, this, [this]() { + TabRoom *updatedRoom = tabSupervisor->getRoomTabs().value(context->roomContext.roomId); + if (updatedRoom) { + tryJoinGame(updatedRoom); + } + }); + + QTimer::singleShot(15000, this, [this]() { emitFailed(tr("Game %1 not found in the room").arg(context->gameId)); }); +} diff --git a/cockatrice/src/interface/intents/intent_join_server_game.h b/cockatrice/src/interface/intents/intent_join_server_game.h new file mode 100644 index 000000000..5e196df38 --- /dev/null +++ b/cockatrice/src/interface/intents/intent_join_server_game.h @@ -0,0 +1,37 @@ +#ifndef COCKATRICE_INTENT_JOIN_SERVER_GAME_H +#define COCKATRICE_INTENT_JOIN_SERVER_GAME_H + +#include "contexts/context_join_game.h" +#include "intent.h" +#include "remote_client.h" + +#include +#include + +class TabRoom; +class TabSupervisor; + +class IntentJoinServerGame : public Intent +{ + Q_OBJECT + +public: + IntentJoinServerGame(TabSupervisor *_tabSupervisor, + RemoteClient *_remoteClient, + std::unique_ptr _context); + +protected: + bool checkPrecondition() const override; + void onPreconditionSatisfied() override; + void onPreconditionNotSatisfied() override; + +private: + bool tryJoinGame(TabRoom *room); + void waitForGame(TabRoom *room); + + TabSupervisor *tabSupervisor; + RemoteClient *remoteClient; + QScopedPointer context; +}; + +#endif // COCKATRICE_INTENT_JOIN_SERVER_GAME_H diff --git a/cockatrice/src/interface/intents/intent_join_server_room.cpp b/cockatrice/src/interface/intents/intent_join_server_room.cpp new file mode 100644 index 000000000..d25bc8d17 --- /dev/null +++ b/cockatrice/src/interface/intents/intent_join_server_room.cpp @@ -0,0 +1,74 @@ +#include "intent_join_server_room.h" + +#include "../widgets/tabs/tab_room.h" +#include "../widgets/tabs/tab_server.h" +#include "../widgets/tabs/tab_supervisor.h" +#include "intent_connect_to_server.h" + +#include +#include + +IntentJoinServerRoom::IntentJoinServerRoom(TabSupervisor *_tabSupervisor, + RemoteClient *_remoteClient, + ContextJoinRoom *_context) + : Intent(), tabSupervisor(_tabSupervisor), remoteClient(_remoteClient), context(_context) +{ +} + +bool IntentJoinServerRoom::checkPrecondition() const +{ + if (remoteClient->getStatus() != ClientStatus::StatusLoggedIn) { + return false; + } + // peerPort() reflects the actual TCP peer, which may differ from the + // configured server port (e.g. when connecting through a proxy), so only + // the hostname is compared here. + if (remoteClient->peerName() != context->serverContext.hostname) { + return false; + } + if (QString::number(remoteClient->peerPort()) != context->serverContext.port) { + return false; + } + + return true; +} + +void IntentJoinServerRoom::onPreconditionSatisfied() +{ + if (tabSupervisor->getRoomTabs().contains(context->roomId)) { + tabSupervisor->setCurrentWidget(tabSupervisor->getRoomTabs().value(context->roomId)); + emitFinished(); + return; + } + + TabServer *tabServer = tabSupervisor->getTabServer(); + if (!tabServer) { + tabSupervisor->openTabServer(); + tabServer = tabSupervisor->getTabServer(); + } + if (!tabServer) { + emitFailed(tr("No server tab available")); + return; + } + + const int roomId = context->roomId; + tabServer->joinRoom(roomId, true); + connect(tabServer, &TabServer::roomJoined, this, [this, roomId](const ServerInfo_Room &info, bool) { + if (info.room_id() == roomId) { + emitFinished(); + } + }); + connect(tabServer, &TabServer::roomJoinFailed, this, [this, roomId](int failedRoomId) { + if (failedRoomId == roomId) { + emitFailed(tr("Failed to join the server room %1").arg(roomId)); + } + }); + + QTimer::singleShot(15000, this, + [this, roomId]() { emitFailed(tr("Timed out while joining the server room %1").arg(roomId)); }); +} + +void IntentJoinServerRoom::onPreconditionNotSatisfied() +{ + runDependency(new IntentConnectToServer(remoteClient, &context->serverContext)); +} diff --git a/cockatrice/src/interface/intents/intent_join_server_room.h b/cockatrice/src/interface/intents/intent_join_server_room.h new file mode 100644 index 000000000..4a5599896 --- /dev/null +++ b/cockatrice/src/interface/intents/intent_join_server_room.h @@ -0,0 +1,28 @@ +#ifndef COCKATRICE_INTENT_JOIN_SERVER_ROOM_H +#define COCKATRICE_INTENT_JOIN_SERVER_ROOM_H + +#include "contexts/context_join_room.h" +#include "intent.h" +#include "remote_client.h" + +class TabSupervisor; + +class IntentJoinServerRoom : public Intent +{ + Q_OBJECT + +public: + IntentJoinServerRoom(TabSupervisor *_tabSupervisor, RemoteClient *_remoteClient, ContextJoinRoom *_context); + +protected: + bool checkPrecondition() const override; + void onPreconditionSatisfied() override; + void onPreconditionNotSatisfied() override; + +private: + TabSupervisor *tabSupervisor; + RemoteClient *remoteClient; + ContextJoinRoom *context; +}; + +#endif // COCKATRICE_INTENT_JOIN_SERVER_ROOM_H diff --git a/cockatrice/src/interface/intents/intent_login.cpp b/cockatrice/src/interface/intents/intent_login.cpp new file mode 100644 index 000000000..ff871fd03 --- /dev/null +++ b/cockatrice/src/interface/intents/intent_login.cpp @@ -0,0 +1,33 @@ +#include "intent_login.h" + +#include "../../client/settings/cache_settings.h" +#include "libcockatrice/settings/servers_settings.h" + +IntentGetLoginCredentials::IntentGetLoginCredentials(ContextConnectToServer *_context) : Intent(), context(_context) +{ +} + +bool IntentGetLoginCredentials::checkPrecondition() const +{ + ServersSettings &servers = SettingsCache::instance().servers(); + return servers.hasLoginData(context->hostname, context->port); +} + +void IntentGetLoginCredentials::onPreconditionSatisfied() +{ + ServersSettings &servers = SettingsCache::instance().servers(); + const int index = servers.findServerIndex(context->hostname, context->port); + + if (index >= 0) { + context->username = servers.getValue(QString("username%1").arg(index), "server", "server_details").toString(); + context->password = servers.getValue(QString("password%1").arg(index), "server", "server_details").toString(); + emitFinished(); + } else { + emitFailed(tr("No saved credentials for this server")); + } +} + +void IntentGetLoginCredentials::onPreconditionNotSatisfied() +{ + emitFailed(tr("No saved credentials for this server")); +} diff --git a/cockatrice/src/interface/intents/intent_login.h b/cockatrice/src/interface/intents/intent_login.h new file mode 100644 index 000000000..c7fec92b7 --- /dev/null +++ b/cockatrice/src/interface/intents/intent_login.h @@ -0,0 +1,23 @@ +#ifndef COCKATRICE_INTENT_LOGIN_H +#define COCKATRICE_INTENT_LOGIN_H + +#include "contexts/context_connect_to_server.h" +#include "intent.h" + +class IntentGetLoginCredentials : public Intent +{ + Q_OBJECT + +public: + IntentGetLoginCredentials(ContextConnectToServer *_context); + +protected: + bool checkPrecondition() const override; + void onPreconditionSatisfied() override; + void onPreconditionNotSatisfied() override; + +private: + ContextConnectToServer *context; +}; + +#endif // COCKATRICE_INTENT_LOGIN_H diff --git a/cockatrice/src/interface/intents/intent_open_local_deck.cpp b/cockatrice/src/interface/intents/intent_open_local_deck.cpp new file mode 100644 index 000000000..2457bec72 --- /dev/null +++ b/cockatrice/src/interface/intents/intent_open_local_deck.cpp @@ -0,0 +1,34 @@ +#include "intent_open_local_deck.h" + +#include "../deck_loader/deck_file_format.h" +#include "../deck_loader/deck_loader.h" +#include "../widgets/tabs/tab_supervisor.h" +#include "intent_wait_for_database_load.h" + +#include + +IntentOpenLocalDeck::IntentOpenLocalDeck(TabSupervisor *_tabSupervisor, const QString &_file) + : Intent(), tabSupervisor(_tabSupervisor), file(_file) +{ +} + +bool IntentOpenLocalDeck::checkPrecondition() const +{ + return CardDatabaseManager::getInstance()->getLoadStatus() == LoadStatus::Ok; +} + +void IntentOpenLocalDeck::onPreconditionSatisfied() +{ + std::optional deckOpt = DeckLoader::loadFromFile(file, DeckFileFormat::getFormatFromName(file), true); + if (deckOpt) { + tabSupervisor->openDeckInNewTab(deckOpt.value()); + emitFinished(); + } else { + emitFailed(tr("Unable to load deck file %1").arg(file)); + } +} + +void IntentOpenLocalDeck::onPreconditionNotSatisfied() +{ + runDependency(new IntentWaitForDatabaseLoad); +} diff --git a/cockatrice/src/interface/intents/intent_open_local_deck.h b/cockatrice/src/interface/intents/intent_open_local_deck.h new file mode 100644 index 000000000..97f875e39 --- /dev/null +++ b/cockatrice/src/interface/intents/intent_open_local_deck.h @@ -0,0 +1,27 @@ +#ifndef COCKATRICE_INTENT_OPEN_LOCAL_DECK_H +#define COCKATRICE_INTENT_OPEN_LOCAL_DECK_H + +#include "intent.h" + +#include + +class TabSupervisor; + +class IntentOpenLocalDeck : public Intent +{ + Q_OBJECT + +public: + IntentOpenLocalDeck(TabSupervisor *_tabSupervisor, const QString &_file); + +protected: + bool checkPrecondition() const override; + void onPreconditionSatisfied() override; + void onPreconditionNotSatisfied() override; + +private: + TabSupervisor *tabSupervisor; + QString file; +}; + +#endif // COCKATRICE_INTENT_OPEN_LOCAL_DECK_H diff --git a/cockatrice/src/interface/intents/intent_wait_for_database_load.cpp b/cockatrice/src/interface/intents/intent_wait_for_database_load.cpp new file mode 100644 index 000000000..c36378818 --- /dev/null +++ b/cockatrice/src/interface/intents/intent_wait_for_database_load.cpp @@ -0,0 +1,19 @@ +#include "intent_wait_for_database_load.h" + +#include + +bool IntentWaitForDatabaseLoad::checkPrecondition() const +{ + return CardDatabaseManager::getInstance()->getLoadStatus() == LoadStatus::Ok; +} + +void IntentWaitForDatabaseLoad::onPreconditionSatisfied() +{ + emitFinished(); +} + +void IntentWaitForDatabaseLoad::onPreconditionNotSatisfied() +{ + connect(CardDatabaseManager::getInstance(), &CardDatabase::cardDatabaseLoadingFinished, this, + [this]() { emitFinished(); }); +} diff --git a/cockatrice/src/interface/intents/intent_wait_for_database_load.h b/cockatrice/src/interface/intents/intent_wait_for_database_load.h new file mode 100644 index 000000000..72f4a1ffc --- /dev/null +++ b/cockatrice/src/interface/intents/intent_wait_for_database_load.h @@ -0,0 +1,16 @@ +#ifndef COCKATRICE_INTENT_WAIT_FOR_DATABASE_LOAD_H +#define COCKATRICE_INTENT_WAIT_FOR_DATABASE_LOAD_H + +#include "intent.h" + +class IntentWaitForDatabaseLoad : public Intent +{ + Q_OBJECT + +protected: + bool checkPrecondition() const override; + void onPreconditionSatisfied() override; + void onPreconditionNotSatisfied() override; +}; + +#endif // COCKATRICE_INTENT_WAIT_FOR_DATABASE_LOAD_H diff --git a/cockatrice/src/interface/intents/url_parser.cpp b/cockatrice/src/interface/intents/url_parser.cpp new file mode 100644 index 000000000..8b5309603 --- /dev/null +++ b/cockatrice/src/interface/intents/url_parser.cpp @@ -0,0 +1,89 @@ +#include "url_parser.h" + +#include "../window_main.h" +#include "contexts/context_join_game.h" +#include "intent_join_server_game.h" +#include "intent_login.h" + +#include +#include +#include +#include +#include + +IntentUrlParser::IntentUrlParser(QObject *parent, MainWindow *_mainWindow) : QObject(parent), mainWindow(_mainWindow) +{ +} + +void IntentUrlParser::handle(const QString &urlStr) +{ + QUrl url(urlStr); + + if (url.scheme() != "cockatrice") { + return; + } + + const QString action = url.host(); + QUrlQuery query(url); + + if (action == "joingame") { + handleJoinGame(query); + } else if (action == "opendeck") { + // handleOpenDeck(query); + } else { + qWarning() << "Unknown intent:" << action; + } +} + +void IntentUrlParser::handleJoinGame(const QUrlQuery &query) +{ + auto showError = [this](const QString &message) { QMessageBox::warning(mainWindow, tr("Open game"), message); }; + + auto ctx = std::make_unique(); + + ctx->roomContext.serverContext.hostname = query.queryItemValue("hostname"); + ctx->roomContext.serverContext.port = query.queryItemValue("port"); + + if (ctx->roomContext.serverContext.hostname.isEmpty()) { + showError(tr("Missing or empty hostname in the game link")); + return; + } + + bool ok = false; + ctx->roomContext.serverContext.port.toUShort(&ok); + if (!ok) { + showError(tr("Invalid or missing port in the game link")); + return; + } + + ctx->roomContext.roomId = query.queryItemValue("roomid").toInt(&ok); + + if (!ok) { + showError(tr("Invalid or missing room id in the game link")); + return; + } + + ok = false; + ctx->gameId = query.queryItemValue("gameid").toInt(&ok); + + if (!ok) { + showError(tr("Invalid or missing game id in the game link")); + return; + } + + // The join game intent owns the context and the credential lookup; once the + // chain finishes (or fails) it deletes the whole tree. + ContextConnectToServer *serverContext = &ctx->roomContext.serverContext; + auto joinGameIntent = + new IntentJoinServerGame(mainWindow->getTabSupervisor(), mainWindow->getRemoteClient(), std::move(ctx)); + joinGameIntent->setParent(this); + + auto getLoginCredentialsIntent = new IntentGetLoginCredentials(serverContext); + getLoginCredentialsIntent->setParent(joinGameIntent); + + connect(getLoginCredentialsIntent, &Intent::finished, joinGameIntent, &Intent::execute); + connect(getLoginCredentialsIntent, &Intent::failed, joinGameIntent, &Intent::failed); + connect(joinGameIntent, &Intent::failed, this, [showError](const QString &reason) { showError(reason); }); + + getLoginCredentialsIntent->execute(); +} diff --git a/cockatrice/src/interface/intents/url_parser.h b/cockatrice/src/interface/intents/url_parser.h new file mode 100644 index 000000000..bac0e3d25 --- /dev/null +++ b/cockatrice/src/interface/intents/url_parser.h @@ -0,0 +1,20 @@ +#ifndef COCKATRICE_URL_PARSER_H +#define COCKATRICE_URL_PARSER_H +#include +#include + +class MainWindow; +class IntentUrlParser : public QObject +{ + Q_OBJECT + +public: + IntentUrlParser(QObject *parent, MainWindow *mainWindow); + void handle(const QString &urlStr); + void handleJoinGame(const QUrlQuery &query); + +private: + MainWindow *mainWindow; +}; + +#endif // COCKATRICE_URL_PARSER_H diff --git a/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.cpp b/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.cpp index 9c52f535f..fc53b296f 100644 --- a/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.cpp +++ b/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.cpp @@ -2,6 +2,8 @@ #include "../../../client/settings/cache_settings.h" #include "../../../client/settings/shortcuts_settings.h" +#include "../settings_page/user_interface_settings_page.h" +#include "../tabs/api/commander_spellbook/commander_bracket_widget.h" #include "deck_list_style_proxy.h" #include "deck_state_manager.h" @@ -12,7 +14,7 @@ #include #include #include -#include +#include #include #include @@ -111,20 +113,18 @@ void DeckEditorDeckDockWidget::createDeckDock() showBannerCardCheckBox = new QCheckBox(); showBannerCardCheckBox->setObjectName("showBannerCardCheckBox"); - showBannerCardCheckBox->setChecked( - SettingsCache::instance().cardsDisplay().getDeckEditorBannerCardComboBoxVisible()); - connect(showBannerCardCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().cardsDisplay(), - &CardsDisplaySettings::setDeckEditorBannerCardComboBoxVisible); - connect(&SettingsCache::instance().cardsDisplay(), - &CardsDisplaySettings::deckEditorBannerCardComboBoxVisibleChanged, this, + showBannerCardCheckBox->setChecked(SettingsCache::instance().deckEditor().getBannerCardComboBoxVisible()); + connect(showBannerCardCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().deckEditor(), + &DeckEditorSettings::setBannerCardComboBoxVisible); + connect(&SettingsCache::instance().deckEditor(), &DeckEditorSettings::bannerCardComboBoxVisibleChanged, this, &DeckEditorDeckDockWidget::updateShowBannerCardComboBox); showTagsWidgetCheckBox = new QCheckBox(); showTagsWidgetCheckBox->setObjectName("showTagsWidgetCheckBox"); - showTagsWidgetCheckBox->setChecked(SettingsCache::instance().cardsDisplay().getDeckEditorTagsWidgetVisible()); - connect(showTagsWidgetCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().cardsDisplay(), - &CardsDisplaySettings::setDeckEditorTagsWidgetVisible); - connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::deckEditorTagsWidgetVisibleChanged, this, + showTagsWidgetCheckBox->setChecked(SettingsCache::instance().deckEditor().getTagsWidgetVisible()); + connect(showTagsWidgetCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().deckEditor(), + &DeckEditorSettings::setTagsWidgetVisible); + connect(&SettingsCache::instance().deckEditor(), &DeckEditorSettings::tagsWidgetVisibleChanged, this, &DeckEditorDeckDockWidget::updateShowTagsWidget); quickSettingsWidget->addSettingsWidget(showBannerCardCheckBox); @@ -136,6 +136,8 @@ void DeckEditorDeckDockWidget::createDeckDock() formatComboBox->addItem(tr("Loading Database...")); formatComboBox->setEnabled(false); // Disable until loaded + commanderBracketWidget = new CommanderBracketWidget(this); + commentsLabel = new QLabel(); commentsLabel->setObjectName("commentsLabel"); commentsEdit = new QTextEdit; @@ -156,7 +158,7 @@ void DeckEditorDeckDockWidget::createDeckDock() bannerCardLabel = new QLabel(); bannerCardLabel->setObjectName("bannerCardLabel"); bannerCardLabel->setText(tr("Banner Card")); - bannerCardLabel->setHidden(!SettingsCache::instance().cardsDisplay().getDeckEditorBannerCardComboBoxVisible()); + bannerCardLabel->setHidden(!SettingsCache::instance().deckEditor().getBannerCardComboBoxVisible()); bannerCardComboBox = new QComboBox(this); connect(getModel(), &DeckListModel::cardNodesChanged, this, [this]() { // Delay the update to avoid race conditions @@ -167,10 +169,10 @@ void DeckEditorDeckDockWidget::createDeckDock() connect(bannerCardComboBox, QOverload::of(&QComboBox::currentIndexChanged), this, &DeckEditorDeckDockWidget::writeBannerCard); - bannerCardComboBox->setHidden(!SettingsCache::instance().cardsDisplay().getDeckEditorBannerCardComboBoxVisible()); + bannerCardComboBox->setHidden(!SettingsCache::instance().deckEditor().getBannerCardComboBoxVisible()); deckTagsDisplayWidget = new DeckPreviewDeckTagsDisplayWidget(this, {}); - deckTagsDisplayWidget->setHidden(!SettingsCache::instance().cardsDisplay().getDeckEditorTagsWidgetVisible()); + deckTagsDisplayWidget->setHidden(!SettingsCache::instance().deckEditor().getTagsWidgetVisible()); connect(deckTagsDisplayWidget, &DeckPreviewDeckTagsDisplayWidget::tagsChanged, deckStateManager, &DeckStateManager::setTags); @@ -221,13 +223,15 @@ void DeckEditorDeckDockWidget::createDeckDock() upperLayout->addWidget(formatLabel, 2, 0); upperLayout->addWidget(formatComboBox, 2, 1); - upperLayout->addWidget(bannerCardLabel, 3, 0); - upperLayout->addWidget(bannerCardComboBox, 3, 1); + upperLayout->addWidget(commanderBracketWidget, 3, 0, 1, 2); - upperLayout->addWidget(deckTagsDisplayWidget, 4, 1); + upperLayout->addWidget(bannerCardLabel, 4, 0); + upperLayout->addWidget(bannerCardComboBox, 4, 1); - upperLayout->addWidget(activeGroupCriteriaLabel, 5, 0); - upperLayout->addWidget(activeGroupCriteriaComboBox, 5, 1); + upperLayout->addWidget(deckTagsDisplayWidget, 5, 1); + + upperLayout->addWidget(activeGroupCriteriaLabel, 6, 0); + upperLayout->addWidget(activeGroupCriteriaComboBox, 6, 1); hashLabel1 = new QLabel(); hashLabel1->setObjectName("hashLabel1"); @@ -305,15 +309,19 @@ void DeckEditorDeckDockWidget::initializeFormats() // Ensure no selection is visible initially formatComboBox->setCurrentIndex(-1); } - connect(formatComboBox, QOverload::of(&QComboBox::currentIndexChanged), this, [this](int index) { + QString formatKey; if (index >= 0) { - QString formatKey = formatComboBox->itemData(index).toString(); + formatKey = formatComboBox->itemData(index).toString(); deckStateManager->setFormat(formatKey); } else { deckStateManager->setFormat(""); // clear format if deselected } + + commanderBracketWidget->setDeck(deckStateManager->getDeckListShared()); }); + + commanderBracketWidget->setDeck(deckStateManager->getDeckListShared()); } ExactCard DeckEditorDeckDockWidget::getCurrentCard() @@ -495,6 +503,8 @@ void DeckEditorDeckDockWidget::syncDisplayWidgetsToModel() formatComboBox->setCurrentIndex(formatComboBox->findData(deckStateManager->getMetadata().gameFormat)); formatComboBox->blockSignals(false); + commanderBracketWidget->setDeck(deckStateManager->getDeckListShared()); + deckTagsDisplayWidget->blockSignals(true); deckTagsDisplayWidget->setTags(deckStateManager->getMetadata().tags); deckTagsDisplayWidget->blockSignals(false); @@ -748,6 +758,7 @@ void DeckEditorDeckDockWidget::retranslateUi() commentsLabel->setText(tr("&Comments:")); activeGroupCriteriaLabel->setText(tr("Group by:")); formatLabel->setText(tr("Format:")); + commanderBracketWidget->retranslateUi(); hashLabel1->setText(tr("Hash:")); @@ -755,4 +766,4 @@ void DeckEditorDeckDockWidget::retranslateUi() aDecrement->setText(tr("&Decrement number")); aRemoveCard->setText(tr("&Remove row")); aSwapCard->setText(tr("Swap card to/from sideboard")); -} \ No newline at end of file +} diff --git a/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.h b/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.h index 8dddf5882..540199f0d 100644 --- a/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.h +++ b/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.h @@ -21,6 +21,7 @@ #include #include +class CommanderBracketWidget; class DeckListModel; class AbstractTabDeckEditor; class DeckEditorDeckDockWidget : public QDockWidget @@ -89,6 +90,8 @@ private: QAction *aRemoveCard, *aIncrement, *aDecrement, *aSwapCard; + CommanderBracketWidget *commanderBracketWidget; + DeckListModel *getModel() const; [[nodiscard]] QModelIndexList getSelectedCardNodeSourceIndices() const; void offsetCountAtIndex(const QModelIndex &idx, bool isIncrement); diff --git a/cockatrice/src/interface/widgets/deck_editor/deck_state_manager.h b/cockatrice/src/interface/widgets/deck_editor/deck_state_manager.h index 10312d0a0..6fce6be57 100644 --- a/cockatrice/src/interface/widgets/deck_editor/deck_state_manager.h +++ b/cockatrice/src/interface/widgets/deck_editor/deck_state_manager.h @@ -57,6 +57,14 @@ public: */ const DeckList &getDeckList() const; + /** + * @brief Gets the underlying DeckList. + */ + QSharedPointer getDeckListShared() const + { + return deckList; + } + /** * @brief Creates a LoadedDeck containing the contents of the current deck and the current LoadInfo. * diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_connect.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_connect.cpp index f1e7a8ba1..aa8a916f8 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_connect.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_connect.cpp @@ -271,6 +271,7 @@ void DlgConnect::updateDisplayInfo(const QString &saveName) hostEdit->setText(_data.at(1)); portEdit->setText(_data.at(2)); playernameEdit->setText(_data.at(3)); + playernameEdit->setFocus(); savePasswordCheckBox->setChecked(savePasswordStatus); if (savePasswordStatus) { diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_load_deck_from_clipboard.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_load_deck_from_clipboard.cpp index 267b80a2c..a3242c797 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_load_deck_from_clipboard.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_load_deck_from_clipboard.cpp @@ -4,7 +4,6 @@ #include "../../../client/settings/shortcuts_settings.h" #include "../../deck_loader/card_node_function.h" #include "../../deck_loader/deck_loader.h" -#include "dlg_settings.h" #include #include diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_settings.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_settings.cpp index 279e10c28..883cfcd03 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_settings.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_settings.cpp @@ -1,3 +1,8 @@ +/** + * @file dlg_settings.cpp + * @brief Implementation of the main settings dialog + * @ingroup Dialogs + */ #include "dlg_settings.h" #include "../../../client/settings/cache_settings.h" @@ -6,6 +11,8 @@ #include "../settings_page/deck_editor_settings_page.h" #include "../settings_page/general_settings_page.h" #include "../settings_page/messages_settings_page.h" +#include "../settings_page/settings_search_delegate.h" +#include "../settings_page/settings_search_model.h" #include "../settings_page/shortcut_settings_page.h" #include "../settings_page/sound_settings_page.h" #include "../settings_page/storage_settings_page.h" @@ -13,20 +20,36 @@ #include "libcockatrice/card/database/card_database_loader.h" #include "libcockatrice/card/database/card_database_manager.h" +#include #include -#include #include +#include +#include #include -#include +#include +#include +#include +#include #include +#include +#include #include #include #include +#include +#include +#include #include +#include #include #include #include +/** + * @brief Wraps a widget in a scroll area for long settings pages + * @param widget The widget to wrap + * @return The scroll area containing the widget + */ static QScrollArea *makeScrollable(QWidget *widget) { widget->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Maximum); @@ -40,112 +63,355 @@ static QScrollArea *makeScrollable(QWidget *widget) return scrollArea; } -DlgSettings::DlgSettings(QWidget *parent) : QDialog(parent) +/** + * @brief Returns the theme icon resources for each settings page, indexed by SettingsPage order + */ +static QStringList pageIconResources() +{ + return {QStringLiteral("theme:config/general"), QStringLiteral("theme:config/appearance"), + QStringLiteral("theme:config/interface"), QStringLiteral("theme:config/deckeditor"), + QStringLiteral("theme:config/storage"), QStringLiteral("theme:config/messages"), + QStringLiteral("theme:config/sound"), QStringLiteral("theme:config/shorcuts")}; +} + +DlgSettings::DlgSettings(QWidget *parent) : QDialog(parent), currentTabIndex(0), searchActive(false) { auto rec = QGuiApplication::primaryScreen()->availableGeometry(); - this->setMinimumSize(qMin(700, rec.width()), qMin(700, rec.height())); + setMinimumSize(qMin(750, rec.width()), qMin(700, rec.height())); connect(&SettingsCache::instance().personal(), &PersonalSettings::langChanged, this, &DlgSettings::updateLanguage); - contentsWidget = new QListWidget; - contentsWidget->setViewMode(QListView::IconMode); - contentsWidget->setIconSize(QSize(58, 50)); - contentsWidget->setMovement(QListView::Static); - contentsWidget->setMinimumHeight(85); - contentsWidget->setMaximumHeight(85); - contentsWidget->setSpacing(5); - - pagesWidget = new QStackedWidget; - pagesWidget->addWidget(makeScrollable(new GeneralSettingsPage)); - pagesWidget->addWidget(makeScrollable(new AppearanceSettingsPage)); - pagesWidget->addWidget(makeScrollable(new UserInterfaceSettingsPage)); - pagesWidget->addWidget(new DeckEditorSettingsPage); - pagesWidget->addWidget(makeScrollable(new StorageSettingsPage)); - pagesWidget->addWidget(new MessagesSettingsPage); - pagesWidget->addWidget(new SoundSettingsPage); - pagesWidget->addWidget(new ShortcutSettingsPage); - - createIcons(); - contentsWidget->setCurrentRow(0); - - auto *vboxLayout = new QVBoxLayout; - vboxLayout->addWidget(contentsWidget); - vboxLayout->addWidget(pagesWidget); - - auto *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok); - connect(buttonBox, &QDialogButtonBox::accepted, this, &DlgSettings::close); - - auto *mainLayout = new QVBoxLayout; - mainLayout->addLayout(vboxLayout); - mainLayout->addSpacing(2); - mainLayout->addWidget(buttonBox); - setLayout(mainLayout); + setupUi(); connect(&SettingsCache::instance().personal(), &PersonalSettings::langChanged, this, &DlgSettings::retranslateUi); retranslateUi(); + searchEdit->setFocus(); + adjustSize(); } -void DlgSettings::createIcons() +void DlgSettings::setupUi() { - generalButton = new QListWidgetItem(contentsWidget); - generalButton->setTextAlignment(Qt::AlignHCenter); - generalButton->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); - generalButton->setIcon(QPixmap("theme:config/general")); + // Search bar + searchEdit = new QLineEdit; + searchEdit->setClearButtonEnabled(true); + searchEdit->addAction(QPixmap("theme:icons/search"), QLineEdit::LeadingPosition); + searchEdit->installEventFilter(this); + connect(searchEdit, &QLineEdit::textChanged, this, &DlgSettings::onSearchTextChanged); - appearanceButton = new QListWidgetItem(contentsWidget); - appearanceButton->setTextAlignment(Qt::AlignHCenter); - appearanceButton->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); - appearanceButton->setIcon(QPixmap("theme:config/appearance")); + auto *searchLayout = new QHBoxLayout; + searchLayout->addWidget(searchEdit); - userInterfaceButton = new QListWidgetItem(contentsWidget); - userInterfaceButton->setTextAlignment(Qt::AlignHCenter); - userInterfaceButton->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); - userInterfaceButton->setIcon(QPixmap("theme:config/interface")); + // Tab bar (built in setupTabBar) + setupTabBar(); - deckEditorButton = new QListWidgetItem(contentsWidget); - deckEditorButton->setTextAlignment(Qt::AlignHCenter); - deckEditorButton->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); - deckEditorButton->setIcon(QPixmap("theme:config/deckeditor")); + // Pages stacked widget + pagesWidget = new QStackedWidget; - storageButton = new QListWidgetItem(contentsWidget); - storageButton->setTextAlignment(Qt::AlignHCenter); - storageButton->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); - storageButton->setIcon(QPixmap("theme:config/storage")); + auto *generalPage = new GeneralSettingsPage; + auto *appearancePage = new AppearanceSettingsPage; + auto *userInterfacePage = new UserInterfaceSettingsPage; + auto *deckEditorPage = new DeckEditorSettingsPage; + auto *storagePage = new StorageSettingsPage; + auto *messagesPage = new MessagesSettingsPage; + auto *soundPage = new SoundSettingsPage; + auto *shortcutsPage = new ShortcutSettingsPage; - messagesButton = new QListWidgetItem(contentsWidget); - messagesButton->setTextAlignment(Qt::AlignHCenter); - messagesButton->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); - messagesButton->setIcon(QPixmap("theme:config/messages")); + pages.append(generalPage); + pages.append(appearancePage); + pages.append(userInterfacePage); + pages.append(deckEditorPage); + pages.append(storagePage); + pages.append(messagesPage); + pages.append(soundPage); + pages.append(shortcutsPage); - soundButton = new QListWidgetItem(contentsWidget); - soundButton->setTextAlignment(Qt::AlignHCenter); - soundButton->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); - soundButton->setIcon(QPixmap("theme:config/sound")); + pagesWidget->addWidget(makeScrollable(generalPage)); + pagesWidget->addWidget(makeScrollable(appearancePage)); + pagesWidget->addWidget(makeScrollable(userInterfacePage)); + pagesWidget->addWidget(makeScrollable(deckEditorPage)); + pagesWidget->addWidget(makeScrollable(storagePage)); + pagesWidget->addWidget(messagesPage); + pagesWidget->addWidget(soundPage); + pagesWidget->addWidget(shortcutsPage); - shortcutsButton = new QListWidgetItem(contentsWidget); - shortcutsButton->setTextAlignment(Qt::AlignHCenter); - shortcutsButton->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); - shortcutsButton->setIcon(QPixmap("theme:config/shorcuts")); + Q_ASSERT(pages.size() == NumPages); - connect(contentsWidget, &QListWidget::currentItemChanged, this, &DlgSettings::changePage); + // Search results view (hidden by default) + searchResultsView = new QListView; + searchResultsView->setUniformItemSizes(false); + searchResultsView->setSelectionMode(QAbstractItemView::SingleSelection); + searchResultsView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + searchResultsView->setVisible(false); + searchResultsView->setStyleSheet( + "QListView::item:selected { background: palette(highlight); color: palette(highlighted-text); }"); + + searchModel = new SettingsSearchModel(this); + searchDelegate = new SettingsSearchDelegate(this); + searchResultsView->setModel(searchModel); + searchResultsView->setItemDelegate(searchDelegate); + connect(searchResultsView, &QListView::clicked, this, &DlgSettings::onSearchResultClicked); + + connect(&SettingsCache::instance(), &SettingsCache::themeChanged, this, [this] { + const QStringList icons = pageIconResources(); + for (int i = 0; i < tabButtons.size() && i < icons.size(); ++i) { + tabButtons[i]->setIcon(QPixmap(icons[i])); + } + searchDelegate->setPageIcons(icons); + searchResultsView->viewport()->update(); + }); + + // Build search index after pages are created + buildSearchIndex(); + + // Pages container (stacked widget + search results overlay) + pagesContainer = new QWidget; + auto *containerLayout = new QStackedLayout; + containerLayout->setStackingMode(QStackedLayout::StackAll); + containerLayout->addWidget(pagesWidget); + containerLayout->addWidget(searchResultsView); + pagesContainer->setLayout(containerLayout); + + // Bottom buttons + auto *buttonBox = new QHBoxLayout; + buttonBox->addStretch(); + okButton = new QPushButton; + okButton->setDefault(true); + connect(okButton, &QPushButton::clicked, this, &DlgSettings::close); + buttonBox->addWidget(okButton); + + // Main layout + auto *mainLayout = new QVBoxLayout; + mainLayout->addLayout(searchLayout); + mainLayout->addWidget(tabBarWidget); + auto *separator = new QFrame; + separator->setFrameShape(QFrame::HLine); + separator->setFrameShadow(QFrame::Sunken); + mainLayout->addWidget(separator); + mainLayout->addWidget(pagesContainer); + mainLayout->addSpacing(4); + mainLayout->addLayout(buttonBox); + setLayout(mainLayout); + + // Keyboard shortcuts + auto *searchShortcut = new QShortcut(QKeySequence(Qt::CTRL | Qt::Key_F), this); + connect(searchShortcut, &QShortcut::activated, searchEdit, qOverload<>(&QLineEdit::setFocus)); + + auto *nextTabShortcut = new QShortcut(QKeySequence(Qt::CTRL | Qt::Key_Tab), this); + connect(nextTabShortcut, &QShortcut::activated, this, [this] { + int next = (currentTabIndex + 1) % tabButtons.size(); + setActiveTab(next); + }); + + auto *prevTabShortcut = new QShortcut(QKeySequence(Qt::CTRL | Qt::SHIFT | Qt::Key_Tab), this); + connect(prevTabShortcut, &QShortcut::activated, this, [this] { + int prev = (currentTabIndex - 1 + tabButtons.size()) % tabButtons.size(); + setActiveTab(prev); + }); + + // Initialize to first tab + setActiveTab(0); } -void DlgSettings::changePage(QListWidgetItem *current, QListWidgetItem *previous) +void DlgSettings::setupTabBar() { - if (!current) { - current = previous; + tabBarWidget = new QWidget; + auto *tabLayout = new QHBoxLayout; + tabLayout->setContentsMargins(0, 0, 0, 0); + tabLayout->setSpacing(2); + + const QStringList iconResources = pageIconResources(); + + for (int i = 0; i < iconResources.size(); ++i) { + auto *tabButton = new QToolButton; + tabButton->setCheckable(true); + tabButton->setIcon(QPixmap(iconResources[i])); + tabButton->setIconSize(QSize(48, 48)); + tabButton->setToolButtonStyle(Qt::ToolButtonTextUnderIcon); + tabButton->setAutoExclusive(true); + tabButton->setMinimumHeight(85); + tabButton->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); + + connect(tabButton, &QToolButton::clicked, this, [this, idx = i] { onTabClicked(idx); }); + + tabButtons.append(tabButton); + tabLayout->addWidget(tabButton); } - pagesWidget->setCurrentIndex(contentsWidget->row(current)); + tabBarWidget->setLayout(tabLayout); +} + +void DlgSettings::buildSearchIndex() +{ + QList allEntries; + + const QStringList pageNames = translatedPageNames(); + searchDelegate->setPageNames(pageNames); + searchDelegate->setPageIcons(pageIconResources()); + + for (int i = 0; i < pages.size(); ++i) { + QList pageEntries = pages[i]->getSearchEntries(); + for (auto &entry : pageEntries) { + if (entry.pageIndex == -1) { + entry.pageIndex = i; + } + } + allEntries.append(pageEntries); + } + + searchModel->setSourceEntries(allEntries); +} + +void DlgSettings::onTabClicked(int index) +{ + if (searchActive) { + switchToTabMode(); + } + setActiveTab(index); +} + +void DlgSettings::setActiveTab(int index) +{ + if (index < 0 || index >= tabButtons.size()) { + return; + } + + currentTabIndex = index; + pagesWidget->setCurrentIndex(index); + + for (int i = 0; i < tabButtons.size(); ++i) { + tabButtons[i]->setChecked(i == index); + } + + // Style active tab with a thick accent border + subtle background tint + for (int i = 0; i < tabButtons.size(); ++i) { + if (i == index) { + tabButtons[i]->setStyleSheet("QToolButton { border: none; border-bottom: 3px solid palette(highlight); " + "border-top-left-radius: 4px; border-top-right-radius: 4px; " + "background: palette(window); padding-bottom: 1px; }"); + } else { + tabButtons[i]->setStyleSheet("QToolButton { border: none; border-bottom: 1px solid transparent; " + "border-top-left-radius: 4px; border-top-right-radius: 4px; " + "background: transparent; }"); + } + } +} + +void DlgSettings::flashWidget(QWidget *widget) +{ + auto *overlay = new QWidget(widget); + overlay->setGeometry(widget->rect()); + overlay->setAttribute(Qt::WA_TransparentForMouseEvents, true); + + QPalette pal = overlay->palette(); + QColor flashColor = pal.color(QPalette::Highlight); + flashColor.setAlpha(100); + pal.setBrush(QPalette::Window, flashColor); + overlay->setPalette(pal); + overlay->setAutoFillBackground(true); + + auto *effect = new QGraphicsOpacityEffect(overlay); + effect->setOpacity(0.0); + overlay->setGraphicsEffect(effect); + overlay->show(); + overlay->raise(); + + auto *flashIn = new QPropertyAnimation(effect, "opacity"); + flashIn->setDuration(120); + flashIn->setStartValue(0.0); + flashIn->setEndValue(0.6); + flashIn->setEasingCurve(QEasingCurve::OutCubic); + + auto *fadeOut = new QPropertyAnimation(effect, "opacity"); + fadeOut->setDuration(900); + fadeOut->setStartValue(0.6); + fadeOut->setEndValue(0.0); + fadeOut->setEasingCurve(QEasingCurve::InCubic); + + auto *group = new QSequentialAnimationGroup(overlay); + group->addAnimation(flashIn); + group->addAnimation(fadeOut); + + connect(group, &QSequentialAnimationGroup::finished, overlay, &QWidget::deleteLater); + + group->start(QAbstractAnimation::DeleteWhenStopped); +} + +void DlgSettings::onSearchTextChanged(const QString &text) +{ + searchModel->setFilterString(text); + + if (searchModel->isFilterActive() && !text.trimmed().isEmpty()) { + if (!searchActive) { + switchToSearchMode(); + } + if (searchModel->rowCount(QModelIndex()) > 0) { + searchResultsView->setCurrentIndex(searchModel->index(0)); + } + } else if (searchActive) { + switchToTabMode(); + } +} + +void DlgSettings::switchToSearchMode() +{ + searchActive = true; + tabBarWidget->setVisible(false); + pagesWidget->setVisible(false); + searchResultsView->setVisible(true); + if (searchModel->rowCount(QModelIndex()) > 0) { + searchResultsView->setCurrentIndex(searchModel->index(0)); + } +} + +void DlgSettings::switchToTabMode() +{ + searchActive = false; + tabBarWidget->setVisible(true); + pagesWidget->setVisible(true); + searchResultsView->setVisible(false); + searchEdit->blockSignals(true); + searchEdit->clear(); + searchEdit->blockSignals(false); + setActiveTab(currentTabIndex); +} + +void DlgSettings::onSearchResultClicked(const QModelIndex &index) +{ + navigateToSearchResult(index); +} + +void DlgSettings::navigateToSearchResult(const QModelIndex &index) +{ + SettingsSearchEntry entry = searchModel->entryForIndex(index); + if (entry.pageIndex < 0 || entry.pageIndex >= pages.size()) { + return; + } + + // Switch to the page + switchToTabMode(); + setActiveTab(entry.pageIndex); + + // Scroll to the widget, focus it, and flash to highlight it + if (entry.widget) { + QWidget *widget = entry.widget; + while (widget) { + if (auto *scrollArea = qobject_cast(widget)) { + scrollArea->ensureWidgetVisible(entry.widget); + break; + } + widget = widget->parentWidget(); + } + entry.widget->setFocus(); + flashWidget(entry.widget); + } } void DlgSettings::setTab(int index) { - if (index <= contentsWidget->count() - 1 && index >= 0) { - changePage(contentsWidget->item(index), contentsWidget->currentItem()); - contentsWidget->setCurrentRow(index); + if (index >= 0 && index < tabButtons.size()) { + setActiveTab(index); } } @@ -155,6 +421,49 @@ void DlgSettings::updateLanguage() installNewTranslator(); } +bool DlgSettings::eventFilter(QObject *watched, QEvent *event) +{ + if (watched == searchEdit && event->type() == QEvent::KeyPress) { + auto *keyEvent = static_cast(event); + if (keyEvent->key() == Qt::Key_Escape) { + if (searchActive) { + switchToTabMode(); + return true; + } + } else if (keyEvent->key() == Qt::Key_Return || keyEvent->key() == Qt::Key_Enter) { + if (searchActive) { + if (searchResultsView->currentIndex().isValid()) { + navigateToSearchResult(searchResultsView->currentIndex()); + } + return true; + } + } else if (keyEvent->key() == Qt::Key_Down) { + if (searchActive) { + int nextRow = searchResultsView->currentIndex().row() + 1; + if (nextRow >= searchModel->rowCount()) { + nextRow = 0; + } + searchResultsView->setCurrentIndex(searchModel->index(nextRow)); + searchResultsView->scrollTo(searchModel->index(nextRow)); + return true; + } + } else if (keyEvent->key() == Qt::Key_Up) { + if (searchActive) { + int prevRow = searchResultsView->currentIndex().row() - 1; + if (prevRow < 0) { + prevRow = searchModel->rowCount() - 1; + } + if (prevRow >= 0) { + searchResultsView->setCurrentIndex(searchModel->index(prevRow)); + searchResultsView->scrollTo(searchModel->index(prevRow)); + } + return true; + } + } + } + return QDialog::eventFilter(watched, event); +} + void DlgSettings::closeEvent(QCloseEvent *event) { bool showLoadError = true; @@ -209,7 +518,6 @@ void DlgSettings::closeEvent(QCloseEvent *event) if (!QDir(SettingsCache::instance().paths().getDeckPath()).exists() || SettingsCache::instance().paths().getDeckPath().isEmpty()) { - //! \todo Prompt to create the deck directory. if (QMessageBox::critical( this, tr("Error"), tr("The path to your deck directory is invalid. Would you like to go back and set the correct path?"), @@ -221,7 +529,6 @@ void DlgSettings::closeEvent(QCloseEvent *event) if (!QDir(SettingsCache::instance().paths().getPicsPath()).exists() || SettingsCache::instance().paths().getPicsPath().isEmpty()) { - //! \todo Prompt to create the pictures directory. if (QMessageBox::critical(this, tr("Error"), tr("The path to your card pictures directory is invalid. Would you like to go back " "and set the correct path?"), @@ -236,15 +543,26 @@ void DlgSettings::closeEvent(QCloseEvent *event) void DlgSettings::retranslateUi() { setWindowTitle(tr("Settings")); + retranslateTabNames(); - generalButton->setText(tr("General")); - appearanceButton->setText(tr("Appearance")); - userInterfaceButton->setText(tr("User Interface")); - storageButton->setText(tr("Storage")); - deckEditorButton->setText(tr("Card Sources")); - messagesButton->setText(tr("Chat")); - soundButton->setText(tr("Sound")); - shortcutsButton->setText(tr("Shortcuts")); + searchEdit->setPlaceholderText(tr("Search settings...")); + okButton->setText(tr("OK")); - contentsWidget->reset(); + // Rebuild search index for translated text + buildSearchIndex(); +} + +QStringList DlgSettings::translatedPageNames() +{ + return {tr("General"), tr("Appearance"), tr("User Interface"), tr("Card Sources"), + tr("Storage"), tr("Chat"), tr("Sound"), tr("Shortcuts")}; +} + +void DlgSettings::retranslateTabNames() +{ + const QStringList tabLabels = translatedPageNames(); + + for (int i = 0; i < tabButtons.size() && i < tabLabels.size(); ++i) { + tabButtons[i]->setText(tabLabels[i]); + } } diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_settings.h b/cockatrice/src/interface/widgets/dialogs/dlg_settings.h index 3ffee6388..b700f7af9 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_settings.h +++ b/cockatrice/src/interface/widgets/dialogs/dlg_settings.h @@ -1,43 +1,99 @@ /** * @file dlg_settings.h + * @brief Main settings dialog for the Cockatrice client * @ingroup Dialogs */ -//! \todo Document this file. - #ifndef DLG_SETTINGS_H #define DLG_SETTINGS_H -#include #include #include +class QPushButton; + inline Q_LOGGING_CATEGORY(DlgSettingsLog, "dlg_settings"); -class QListWidget; class QStackedWidget; -class QListWidgetItem; +class QToolButton; +class QListView; +class QLineEdit; +class AbstractSettingsPage; +class SettingsSearchModel; +class SettingsSearchDelegate; + +/** + * @brief Main application settings dialog with tabbed navigation and search + * + * Provides a modern settings interface organized into tabbed pages. Users can + * either navigate by clicking tabs or search for specific settings using the + * built-in search bar. Search results are filtered and ranked by relevance. + */ class DlgSettings : public QDialog { Q_OBJECT public: + /** + * @brief Page order in the tab bar, matching the order pages are added in setupUi() + * + * Use these values instead of raw indices so reordering pages never silently + * breaks external callers like tab_room.cpp. + */ + enum SettingsPage + { + GeneralPage = 0, + AppearancePage, + UserInterfacePage, + DeckEditorPage, + StoragePage, + MessagesPage, + SoundPage, + ShortcutsPage, + NumPages + }; + explicit DlgSettings(QWidget *parent = nullptr); void setTab(int index); private slots: - void changePage(QListWidgetItem *current, QListWidgetItem *previous); + void onTabClicked(int index); + void onSearchTextChanged(const QString &text); + void onSearchResultClicked(const QModelIndex &index); void updateLanguage(); private: - QListWidget *contentsWidget; - QStackedWidget *pagesWidget; - QListWidgetItem *generalButton, *appearanceButton, *userInterfaceButton, *deckEditorButton, *storageButton, - *messagesButton, *soundButton, *shortcutsButton; - void createIcons(); + // UI elements + QLineEdit *searchEdit; ///< Search bar for filtering settings + QWidget *tabBarWidget; ///< Container widget for the tab buttons + QList tabButtons; ///< Navigation tab buttons + QStackedWidget *pagesWidget; ///< Stacked widget containing settings pages + QListView *searchResultsView; ///< Search results list view + QWidget *pagesContainer; ///< Container stacking pages and search results + QPushButton *okButton; ///< Button to close the dialog + + // Data + QList pages; ///< All settings page instances + SettingsSearchModel *searchModel; ///< Model for search results + SettingsSearchDelegate *searchDelegate; ///< Delegate for search result rendering + int currentTabIndex; ///< Currently active tab index + bool searchActive; ///< Whether search mode is active + + void setupUi(); + void setupTabBar(); + void buildSearchIndex(); + void switchToTabMode(); + void switchToSearchMode(); + void navigateToSearchResult(const QModelIndex &index); + void setActiveTab(int index); + static void flashWidget(QWidget *widget); + static QStringList translatedPageNames(); + void retranslateUi(); + void retranslateTabNames(); protected: void closeEvent(QCloseEvent *event) override; + bool eventFilter(QObject *watched, QEvent *event) override; }; -#endif +#endif // DLG_SETTINGS_H diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_update.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_update.cpp index ee2149309..7cf58d3e0 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_update.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_update.cpp @@ -134,7 +134,7 @@ void DlgUpdate::finishedUpdateCheck(bool needToUpdate, bool isCompatible, Releas // If there's no need to update, tell them that. However we still allow them to run the // downloader themselves if there's a compatible build QMessageBox::information( - this, tr("No Update Available"), + window(), tr("No Update Available"), tr("Cockatrice is up to date!") + "

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

" + "" + tr("New version") + QString(": %1
").arg(release->getName()) + "" + tr("Released") + QString(": %1 (").arg(publishDate, release->getDescriptionUrl()) + tr("Changelog") + @@ -156,10 +156,12 @@ void DlgUpdate::finishedUpdateCheck(bool needToUpdate, bool isCompatible, Releas if (reply == QMessageBox::Yes) { downloadUpdate(release->getName()); + } else { + closeDialog(); } } else { QMessageBox::information( - this, tr("Update Available"), + window(), tr("Update Available"), tr("A new version of Cockatrice is available!") + "

" + "" + tr("New version") + QString(": %1
").arg(release->getName()) + "" + tr("Released") + QString(": %1 (
").arg(publishDate, release->getDescriptionUrl()) + tr("Changelog") + diff --git a/cockatrice/src/interface/widgets/general/display/charts/pies/color_pie.cpp b/cockatrice/src/interface/widgets/general/display/charts/pies/color_pie.cpp index b71b24a40..bd4641981 100644 --- a/cockatrice/src/interface/widgets/general/display/charts/pies/color_pie.cpp +++ b/cockatrice/src/interface/widgets/general/display/charts/pies/color_pie.cpp @@ -82,9 +82,7 @@ void ColorPie::paintEvent(QPaintEvent *) QString label = QString("%1%").arg(int(ratio * 100 + 0.5)); QFontMetrics fm(p.font()); - int labelWidth = fm.horizontalAdvance(label); - QRectF textRect(labelPos.x() - labelWidth / 2.0, labelPos.y() - fm.height() / 2.0, labelWidth, fm.height()); p.setPen(Qt::black); diff --git a/cockatrice/src/interface/widgets/general/home_widget.cpp b/cockatrice/src/interface/widgets/general/home_widget.cpp index e873f5f3e..8589e3517 100644 --- a/cockatrice/src/interface/widgets/general/home_widget.cpp +++ b/cockatrice/src/interface/widgets/general/home_widget.cpp @@ -14,8 +14,8 @@ #include #include #include +#include #include -#include HomeWidget::HomeWidget(QWidget *parent, TabSupervisor *_tabSupervisor) : QWidget(parent), tabSupervisor(_tabSupervisor), background("theme:backgrounds/home"), overlay("theme:cockatrice") @@ -43,12 +43,12 @@ HomeWidget::HomeWidget(QWidget *parent, TabSupervisor *_tabSupervisor) updateConnectButton(tabSupervisor->getClient()->getStatus()); connect(tabSupervisor->getClient(), &RemoteClient::statusChanged, this, &HomeWidget::updateConnectButton); - connect(&SettingsCache::instance().personal(), &PersonalSettings::homeTabBackgroundSourceChanged, this, + connect(&SettingsCache::instance().appearance(), &AppearanceSettings::homeTabBackgroundSourceChanged, this, &HomeWidget::initializeBackgroundFromSource); - connect(&SettingsCache::instance().personal(), &PersonalSettings::homeTabBackgroundShuffleFrequencyChanged, this, - &HomeWidget::onBackgroundShuffleFrequencyChanged); + connect(&SettingsCache::instance().appearance(), &AppearanceSettings::homeTabBackgroundShuffleFrequencyChanged, + this, &HomeWidget::onBackgroundShuffleFrequencyChanged); // Lambda is cleaner to read than overloading this - connect(&SettingsCache::instance().personal(), &PersonalSettings::homeTabDisplayCardNameChanged, this, + connect(&SettingsCache::instance().appearance(), &AppearanceSettings::homeTabDisplayCardNameChanged, this, [this] { repaint(); }); connect(&SettingsCache::instance(), &SettingsCache::themeChanged, this, &HomeWidget::initializeBackgroundFromSource); @@ -65,7 +65,7 @@ void HomeWidget::initializeBackgroundFromSource() } auto backgroundSourceType = - BackgroundSources::fromId(SettingsCache::instance().personal().getHomeTabBackgroundSource()); + BackgroundSources::fromId(SettingsCache::instance().appearance().getHomeTabBackgroundSource()); switch (backgroundSourceType) { case BackgroundSources::Theme: @@ -113,7 +113,7 @@ void HomeWidget::setRandomCard(ExactCard &newCard) void HomeWidget::updateRandomCard() { auto backgroundSourceType = - BackgroundSources::fromId(SettingsCache::instance().personal().getHomeTabBackgroundSource()); + BackgroundSources::fromId(SettingsCache::instance().appearance().getHomeTabBackgroundSource()); ExactCard newCard; @@ -156,8 +156,8 @@ void HomeWidget::updateRandomCard() void HomeWidget::onBackgroundShuffleFrequencyChanged() { cardChangeTimer->stop(); - if (SettingsCache::instance().personal().getHomeTabBackgroundShuffleFrequency() > 0) { - cardChangeTimer->start(SettingsCache::instance().personal().getHomeTabBackgroundShuffleFrequency() * 1000); + if (SettingsCache::instance().appearance().getHomeTabBackgroundShuffleFrequency() > 0) { + cardChangeTimer->start(SettingsCache::instance().appearance().getHomeTabBackgroundShuffleFrequency() * 1000); } } @@ -265,7 +265,7 @@ void HomeWidget::updateConnectButton(const ClientStatus status) QPair HomeWidget::extractDominantColors(const QPixmap &pixmap) { - if (themeManager->isBuiltInTheme() && SettingsCache::instance().personal().getHomeTabBackgroundSource() == + if (themeManager->isBuiltInTheme() && SettingsCache::instance().appearance().getHomeTabBackgroundSource() == BackgroundSources::toId(BackgroundSources::Theme)) { return QPair(QColor::fromRgb(20, 140, 60), QColor::fromRgb(120, 200, 80)); } @@ -352,7 +352,7 @@ void HomeWidget::paintEvent(QPaintEvent *event) } } - if (!cardName.isEmpty() && SettingsCache::instance().personal().getHomeTabDisplayCardName()) { + if (!cardName.isEmpty() && SettingsCache::instance().appearance().getHomeTabDisplayCardName()) { QFont font = painter.font(); font.setPointSize(14); font.setBold(true); diff --git a/cockatrice/src/interface/widgets/menus/tearoff_menu.h b/cockatrice/src/interface/widgets/menus/tearoff_menu.h index 26dcd1f6c..9a9c4ff01 100644 --- a/cockatrice/src/interface/widgets/menus/tearoff_menu.h +++ b/cockatrice/src/interface/widgets/menus/tearoff_menu.h @@ -16,16 +16,16 @@ class TearOffMenu : public QMenu public: explicit TearOffMenu(const QString &title, QWidget *parent = nullptr) : QMenu(title, parent) { - connect(&SettingsCache::instance().interface(), &InterfaceSettings::useTearOffMenusChanged, this, + connect(&SettingsCache::instance().userInterface(), &InterfaceSettings::useTearOffMenusChanged, this, [this](const bool state) { setTearOffEnabled(state); }); - setTearOffEnabled(SettingsCache::instance().interface().getUseTearOffMenus()); + setTearOffEnabled(SettingsCache::instance().userInterface().getUseTearOffMenus()); } explicit TearOffMenu(QWidget *parent = nullptr) : QMenu(parent) { - connect(&SettingsCache::instance().interface(), &InterfaceSettings::useTearOffMenusChanged, this, + connect(&SettingsCache::instance().userInterface(), &InterfaceSettings::useTearOffMenusChanged, this, [this](const bool state) { setTearOffEnabled(state); }); - setTearOffEnabled(SettingsCache::instance().interface().getUseTearOffMenus()); + setTearOffEnabled(SettingsCache::instance().userInterface().getUseTearOffMenus()); } TearOffMenu *addTearOffMenu(const QString &title) diff --git a/cockatrice/src/interface/widgets/replay/replay_manager.cpp b/cockatrice/src/interface/widgets/replay/replay_manager.cpp new file mode 100644 index 000000000..a2c1e0ff0 --- /dev/null +++ b/cockatrice/src/interface/widgets/replay/replay_manager.cpp @@ -0,0 +1,234 @@ +#include "replay_manager.h" + +#include "../../../client/settings/cache_settings.h" + +#include +#include +#include + +static constexpr int TIMER_INTERVAL_MS = 200; +static constexpr int EMPTY_SECTION_MARGIN_MS = 500; + +static QList createReplayTimeline(const GameReplay *replay) +{ + // Create list: event number -> time [ms] + unsigned int lastEventTimestamp = 0; + const int eventCount = replay->event_list_size(); + + QList replayTimeline; + for (int i = 0; i < eventCount; ++i) { + int nextSecondIndex = i + 1; + while (nextSecondIndex < eventCount && + replay->event_list(nextSecondIndex).seconds_elapsed() == lastEventTimestamp) { + ++nextSecondIndex; + } + + // Distribute simultaneous events evenly across 1 second. + const int numberEventsThisSecond = nextSecondIndex - i; + for (int k = 0; k < numberEventsThisSecond; ++k) { + int eventMs = replay->event_list(i + k).seconds_elapsed() * 1000; + int distributionMs = static_cast(static_cast(k) / numberEventsThisSecond * 1000); + replayTimeline.append(eventMs + distributionMs); + } + + if (nextSecondIndex < eventCount) { + lastEventTimestamp = replay->event_list(nextSecondIndex).seconds_elapsed(); + } + i += numberEventsThisSecond - 1; + } + + return replayTimeline; +} + +ReplayManager::ReplayManager(QObject *parent, GameReplay *replay) + : QObject(parent), replay(replay), replayTimeline(createReplayTimeline(replay)) +{ + maxTime = replayTimeline.isEmpty() ? 0 : replayTimeline.last(); + + replayTimer = new QTimer(this); + replayTimer->setInterval(TIMER_INTERVAL_MS); + connect(replayTimer, &QTimer::timeout, this, &ReplayManager::replayTimerTimeout); + + rewindBufferingTimer = new QTimer(this); + rewindBufferingTimer->setSingleShot(true); + connect(rewindBufferingTimer, &QTimer::timeout, this, &ReplayManager::processRewind); +} + +ReplayManager::~ReplayManager() +{ + delete replay; +} + +void ReplayManager::skipToTime(int newTime, bool doRewindBuffering) +{ + // check boundary conditions + if (newTime < 0) { + newTime = 0; + } + if (newTime > maxTime) { + newTime = maxTime; + } + + newTime -= newTime % TIMER_INTERVAL_MS; // Time should always be a multiple of the interval + + const bool isBackwardsSkip = newTime < currentProcessedTime; + currentVisualTime = newTime; + + if (isBackwardsSkip) { + handleBackwardsSkip(doRewindBuffering); + } else { + processNewEvents(FORWARD_SKIP); + } + + timeChanged(currentVisualTime); +} + +/** + * @brief Handles a backwards skip in the replay timeline. + * + * @param doRewindBuffering When true, if multiple backward skips are made in quick succession, only a single rewind + * is processed at the end. When false, the backwards skip will always cause an immediate rewind. + */ +void ReplayManager::handleBackwardsSkip(bool doRewindBuffering) +{ + if (doRewindBuffering) { + // We use a one-shot timer to implement the rewind buffering. + // The rewind only happens once the timer runs out. + // If another backwards skip happens, the timer will just get reset instead of rewinding. + rewindBufferingTimer->stop(); + rewindBufferingTimer->start(SettingsCache::instance().userInterface().getRewindBufferingMs()); + } else { + // otherwise, process the rewind immediately + processRewind(); + } +} + +void ReplayManager::processRewind() +{ + // stop any queued-up rewinds + rewindBufferingTimer->stop(); + + // process the rewind + currentEvent = 0; + emit rewound(); + processNewEvents(BACKWARD_SKIP); +} + +void ReplayManager::replayTimerTimeout() +{ + currentVisualTime += TIMER_INTERVAL_MS; + + processNewEvents(NORMAL_PLAYBACK); + + timeChanged(currentVisualTime); + + if (skipEmptySections) { + handleSkipEmptySection(); + } +} + +/** @brief Processes all unprocessed events up to the current time. */ +void ReplayManager::processNewEvents(PlaybackMode playbackMode) +{ + currentProcessedTime = currentVisualTime; + + while (currentEvent < replayTimeline.size() && replayTimeline[currentEvent] < currentProcessedTime) { + EventProcessingOptions options; + + // backwards skip => always skip reveal windows + // forwards skip => skip reveal windows that don't happen within a big skip of the target + if (playbackMode == BACKWARD_SKIP || currentProcessedTime - replayTimeline[currentEvent] > BIG_SKIP_MS) { + options |= SKIP_REVEAL_WINDOW; + } + + // backwards skip => always skip tap animation + if (playbackMode == BACKWARD_SKIP) { + options |= SKIP_TAP_ANIMATION; + } + + emit eventReplayed(replay->event_list(currentEvent), options); + ++currentEvent; + } + if (currentEvent == replayTimeline.size()) { + emit replayFinished(); + replayTimer->stop(); + } +} + +static bool hasMeaningfulEvent(const GameEventContainer &cont) +{ + const int eventListSize = cont.event_list_size(); + for (int i = 0; i < eventListSize; ++i) { + const GameEvent &event = cont.event_list(i); + const auto eventType = static_cast(getPbExtension(event)); + + if (eventType != GameEvent::PLAYER_PROPERTIES_CHANGED) { + return true; + } + } + + return false; +} + +void ReplayManager::handleSkipEmptySection() +{ + if (currentEvent == replayTimeline.size()) { + return; + } + + // find most recent meaningful event + int prevEvent = std::max(0, currentEvent - 1); + for (; prevEvent > 0 && !hasMeaningfulEvent(replay->event_list(prevEvent)); --prevEvent) { + } + + int prevEventTime = replayTimeline.value(prevEvent); + if (currentVisualTime - prevEventTime <= EMPTY_SECTION_MARGIN_MS) { + return; + } + + // find next earliest meaningful event + int nextEvent = currentEvent; + for (; nextEvent < replayTimeline.size() - 1 && !hasMeaningfulEvent(replay->event_list(nextEvent)); ++nextEvent) { + } + + int nextEventTime = replayTimeline.value(nextEvent); + if (nextEventTime - currentVisualTime <= EMPTY_SECTION_MARGIN_MS) { + return; + } + + // skip forward if we're not within margin of either event + skipToTime(nextEventTime - EMPTY_SECTION_MARGIN_MS, false); +} + +void ReplayManager::setTimeScaleFactor(qreal _timeScaleFactor) +{ + timeScaleFactor = _timeScaleFactor; + int interval = std::max(1, qRound(TIMER_INTERVAL_MS / timeScaleFactor)); + replayTimer->setInterval(interval); +} + +void ReplayManager::setSkipEmptySections(bool value) +{ + skipEmptySections = value; +} + +void ReplayManager::startReplay() +{ + replayTimer->start(); +} + +void ReplayManager::stopReplay() +{ + replayTimer->stop(); +} + +void ReplayManager::setTime(int time) +{ + // don't buffer rewinds from clicks, since clicks usually don't happen fast enough to require buffering + skipToTime(time, false); +} + +void ReplayManager::skipByAmount(int amount) +{ + skipToTime(currentVisualTime + amount, amount < 0); +} \ No newline at end of file diff --git a/cockatrice/src/interface/widgets/replay/replay_manager.h b/cockatrice/src/interface/widgets/replay/replay_manager.h new file mode 100644 index 000000000..81e66824d --- /dev/null +++ b/cockatrice/src/interface/widgets/replay/replay_manager.h @@ -0,0 +1,83 @@ +#ifndef COCKATRICE_REPLAY_MANAGER_H +#define COCKATRICE_REPLAY_MANAGER_H + +#include "../../../game/player/event_processing_options.h" + +#include +#include + +class GameReplay; +class QTimer; + +/** + * @brief This class handles all logic to do with playing back replays + */ +class ReplayManager : public QObject +{ + Q_OBJECT + + enum PlaybackMode + { + NORMAL_PLAYBACK, + FORWARD_SKIP, + BACKWARD_SKIP + }; + + GameReplay *replay; + QList replayTimeline; ///< timestamp of each event, with the indexes corresponding + int maxTime; + + QTimer *replayTimer; + QTimer *rewindBufferingTimer; + + qreal timeScaleFactor = 1.0; + bool skipEmptySections = false; + + int currentVisualTime = 0; ///< time currently displayed by the timeline + int currentProcessedTime = 0; ///< time that events are currently processed up to. Could differ from visual time due + ///< to rewind buffering + int currentEvent = 0; ///< current event's index + + void skipToTime(int newTime, bool doRewindBuffering); + void handleBackwardsSkip(bool doRewindBuffering); + void processRewind(); + void processNewEvents(PlaybackMode playbackMode); + void handleSkipEmptySection(); + +private slots: + void replayTimerTimeout(); + +public: + static constexpr int SMALL_SKIP_MS = 1000; + static constexpr int BIG_SKIP_MS = 10000; + + /** + * @param parent The parent QObject + * @param replay Cannot be null. Takes ownership of the object. + */ + explicit ReplayManager(QObject *parent, GameReplay *replay); + + ~ReplayManager() override; + + const QList &getReplayTimeline() const + { + return replayTimeline; + } + + void setTimeScaleFactor(qreal _timeScaleFactor); + void setSkipEmptySections(bool value); + +public slots: + void startReplay(); + void stopReplay(); + void setTime(int time); + void skipByAmount(int amount); // use a negative amount to skip backwards + +signals: + void timeChanged(int time); + void eventReplayed(const GameEventContainer &cont, EventProcessingOptions options); + void replayFinished(); + void rewound(); +}; + +#endif // COCKATRICE_REPLAY_MANAGER_H diff --git a/cockatrice/src/interface/widgets/replay/replay_quick_settings_widget.cpp b/cockatrice/src/interface/widgets/replay/replay_quick_settings_widget.cpp index 446427e26..5d58705d2 100644 --- a/cockatrice/src/interface/widgets/replay/replay_quick_settings_widget.cpp +++ b/cockatrice/src/interface/widgets/replay/replay_quick_settings_widget.cpp @@ -14,16 +14,21 @@ ReplayQuickSettingsWidget::ReplayQuickSettingsWidget(QWidget *parent) : Settings fastForwardSpeedBox.setMinimum(1); fastForwardSpeedBox.setMaximum(99.9); fastForwardSpeedBox.setDecimals(1); - fastForwardSpeedBox.setValue(SettingsCache::instance().interface().getFastForwardSpeed()); + fastForwardSpeedBox.setValue(SettingsCache::instance().userInterface().getFastForwardSpeed()); connect(&fastForwardSpeedBox, qOverload(&QDoubleSpinBox::valueChanged), this, &ReplayQuickSettingsWidget::actUpdateFastForwardSpeed); + skipEmptyCheckBox.setChecked(SettingsCache::instance().userInterface().getSkipEmptySections()); + connect(&skipEmptyCheckBox, &QCheckBox::QT_STATE_CHANGED, this, + &ReplayQuickSettingsWidget::actUpdateSkipEmptySections); + // putting it all together auto *widget = new QWidget; auto *grid = new QGridLayout(widget); grid->setContentsMargins(0, 0, 0, 0); grid->addWidget(&fastForwardSpeedLabel, 0, 0, 1, 1); grid->addWidget(&fastForwardSpeedBox, 0, 1, 1, 1); + grid->addWidget(&skipEmptyCheckBox, 1, 0, 1, 2); this->addSettingsWidget(widget); @@ -36,10 +41,18 @@ void ReplayQuickSettingsWidget::retranslateUi() { fastForwardSpeedLabel.setText(tr("Fast forward speed:")); fastForwardSpeedBox.setSuffix("x"); + + skipEmptyCheckBox.setText(tr("Skip empty sections")); } void ReplayQuickSettingsWidget::actUpdateFastForwardSpeed(qreal value) { - SettingsCache::instance().interface().setFastForwardSpeed(value); + SettingsCache::instance().userInterface().setFastForwardSpeed(value); emit fastForwardSpeedChanged(value); } + +void ReplayQuickSettingsWidget::actUpdateSkipEmptySections(QT_STATE_CHANGED_T value) +{ + SettingsCache::instance().userInterface().setSkipEmptySections(value); + emit skipEmptySectionsChanged(value); +} diff --git a/cockatrice/src/interface/widgets/replay/replay_quick_settings_widget.h b/cockatrice/src/interface/widgets/replay/replay_quick_settings_widget.h index b88a8b4e3..a337ea0a6 100644 --- a/cockatrice/src/interface/widgets/replay/replay_quick_settings_widget.h +++ b/cockatrice/src/interface/widgets/replay/replay_quick_settings_widget.h @@ -3,7 +3,9 @@ #include "../../interface/widgets/quick_settings/settings_button_widget.h" +#include #include +#include class ReplayQuickSettingsWidget : public SettingsButtonWidget { @@ -16,13 +18,17 @@ public: signals: void fastForwardSpeedChanged(qreal speed); + void skipEmptySectionsChanged(bool skip); private: QLabel fastForwardSpeedLabel; QDoubleSpinBox fastForwardSpeedBox; + QCheckBox skipEmptyCheckBox; + private slots: void actUpdateFastForwardSpeed(qreal value); + void actUpdateSkipEmptySections(QT_STATE_CHANGED_T value); }; #endif // COCKATRICE_REPLAY_QUICK_SETTINGS_WIDGET_H diff --git a/cockatrice/src/interface/widgets/replay/replay_timeline_widget.cpp b/cockatrice/src/interface/widgets/replay/replay_timeline_widget.cpp index 8ec20b267..fe4ac330b 100644 --- a/cockatrice/src/interface/widgets/replay/replay_timeline_widget.cpp +++ b/cockatrice/src/interface/widgets/replay/replay_timeline_widget.cpp @@ -4,26 +4,19 @@ #include #include -#include -#include -ReplayTimelineWidget::ReplayTimelineWidget(QWidget *parent) - : QWidget(parent), maxBinValue(1), maxTime(1), timeScaleFactor(1.0), currentVisualTime(0), currentProcessedTime(0), - currentEvent(0) +static constexpr int BIN_LENGTH = 5000; +static constexpr int MIN_RESOLUTION_MS = 1000; + +ReplayTimelineWidget::ReplayTimelineWidget(QWidget *parent) : QWidget(parent) { - replayTimer = new QTimer(this); - replayTimer->setInterval(TIMER_INTERVAL_MS); - connect(replayTimer, &QTimer::timeout, this, &ReplayTimelineWidget::replayTimerTimeout); - - rewindBufferingTimer = new QTimer(this); - rewindBufferingTimer->setSingleShot(true); - connect(rewindBufferingTimer, &QTimer::timeout, this, &ReplayTimelineWidget::processRewind); } -void ReplayTimelineWidget::setTimeline(const QList &_replayTimeline) +void ReplayTimelineWidget::setTimeline(const QList &replayTimeline) { - replayTimeline = _replayTimeline; histogram.clear(); + currentTime = 0; + int binEndTime = BIN_LENGTH - 1; int binValue = 0; for (int i : replayTimeline) { @@ -66,70 +59,31 @@ void ReplayTimelineWidget::paintEvent(QPaintEvent * /* event */) painter.fillPath(path, Qt::black); const QColor barColor = QColor::fromHsv(120, 255, 255, 100); - quint64 w = (quint64)(width() - 1) * (quint64)currentVisualTime / maxTime; + quint64 w = (quint64)(width() - 1) * (quint64)currentTime / maxTime; painter.fillRect(0, 0, static_cast(w), height() - 1, barColor); } void ReplayTimelineWidget::mousePressEvent(QMouseEvent *event) { int newTime = static_cast((qint64)maxTime * (qint64)event->position().x() / width()); - // don't buffer rewinds from clicks, since clicks usually don't happen fast enough to require buffering - skipToTime(newTime, false); + emit timeClicked(newTime); } -void ReplayTimelineWidget::skipToTime(int newTime, bool doRewindBuffering) +void ReplayTimelineWidget::setCurrentTime(int time) { - // check boundary conditions - if (newTime < 0) { - newTime = 0; - } - if (newTime > maxTime) { - newTime = maxTime; + int newTime = qBound(0, time, maxTime); + + if (currentTime == newTime) { + return; } - newTime -= newTime % TIMER_INTERVAL_MS; // Time should always be a multiple of the interval + bool doUpdate = currentTime / MIN_RESOLUTION_MS != newTime / MIN_RESOLUTION_MS; - const bool isBackwardsSkip = newTime < currentProcessedTime; - currentVisualTime = newTime; + currentTime = newTime; - if (isBackwardsSkip) { - handleBackwardsSkip(doRewindBuffering); - } else { - processNewEvents(FORWARD_SKIP); + if (doUpdate) { + update(); } - - update(); -} - -/** - * @brief Handles a backwards skip in the replay timeline. - * - * @param doRewindBuffering When true, if multiple backward skips are made in quick succession, only a single rewind - * is processed at the end. When false, the backwards skip will always cause an immediate rewind. - */ -void ReplayTimelineWidget::handleBackwardsSkip(bool doRewindBuffering) -{ - if (doRewindBuffering) { - // We use a one-shot timer to implement the rewind buffering. - // The rewind only happens once the timer runs out. - // If another backwards skip happens, the timer will just get reset instead of rewinding. - rewindBufferingTimer->stop(); - rewindBufferingTimer->start(SettingsCache::instance().interface().getRewindBufferingMs()); - } else { - // otherwise, process the rewind immediately - processRewind(); - } -} - -void ReplayTimelineWidget::processRewind() -{ - // stop any queued-up rewinds - rewindBufferingTimer->stop(); - - // process the rewind - currentEvent = 0; - emit rewound(); - processNewEvents(BACKWARD_SKIP); } QSize ReplayTimelineWidget::sizeHint() const @@ -141,64 +95,3 @@ QSize ReplayTimelineWidget::minimumSizeHint() const { return {400, 50}; } - -void ReplayTimelineWidget::replayTimerTimeout() -{ - currentVisualTime += TIMER_INTERVAL_MS; - - processNewEvents(NORMAL_PLAYBACK); - - if (!(currentVisualTime % 1000)) { - update(); - } -} - -/** @brief Processes all unprocessed events up to the current time. */ -void ReplayTimelineWidget::processNewEvents(PlaybackMode playbackMode) -{ - currentProcessedTime = currentVisualTime; - - while ((currentEvent < replayTimeline.size()) && (replayTimeline[currentEvent] < currentProcessedTime)) { - EventProcessingOptions options; - - // backwards skip => always skip reveal windows - // forwards skip => skip reveal windows that don't happen within a big skip of the target - if (playbackMode == BACKWARD_SKIP || currentProcessedTime - replayTimeline[currentEvent] > BIG_SKIP_MS) { - options |= SKIP_REVEAL_WINDOW; - } - - // backwards skip => always skip tap animation - if (playbackMode == BACKWARD_SKIP) { - options |= SKIP_TAP_ANIMATION; - } - - emit processNextEvent(options); - ++currentEvent; - } - if (currentEvent == replayTimeline.size()) { - emit replayFinished(); - replayTimer->stop(); - } -} - -void ReplayTimelineWidget::setTimeScaleFactor(qreal _timeScaleFactor) -{ - timeScaleFactor = _timeScaleFactor; - int interval = std::max(1, qRound(TIMER_INTERVAL_MS / timeScaleFactor)); - replayTimer->setInterval(interval); -} - -void ReplayTimelineWidget::startReplay() -{ - replayTimer->start(); -} - -void ReplayTimelineWidget::stopReplay() -{ - replayTimer->stop(); -} - -void ReplayTimelineWidget::skipByAmount(int amount) -{ - skipToTime(currentVisualTime + amount, amount < 0); -} \ No newline at end of file diff --git a/cockatrice/src/interface/widgets/replay/replay_timeline_widget.h b/cockatrice/src/interface/widgets/replay/replay_timeline_widget.h index 6cdb8bcb2..47d19a741 100644 --- a/cockatrice/src/interface/widgets/replay/replay_timeline_widget.h +++ b/cockatrice/src/interface/widgets/replay/replay_timeline_widget.h @@ -18,57 +18,25 @@ class QTimer; class ReplayTimelineWidget : public QWidget { Q_OBJECT + signals: - void processNextEvent(EventProcessingOptions options); - void replayFinished(); - void rewound(); + void timeClicked(int newTime); private: - enum PlaybackMode - { - NORMAL_PLAYBACK, - FORWARD_SKIP, - BACKWARD_SKIP - }; - - static constexpr int TIMER_INTERVAL_MS = 200; - static constexpr int BIN_LENGTH = 5000; - - QTimer *replayTimer; - QTimer *rewindBufferingTimer; - QList replayTimeline; QList histogram; - int maxBinValue, maxTime; - qreal timeScaleFactor; - int currentVisualTime; // time currently displayed by the timeline - int currentProcessedTime; // time that events are currently processed up to. Could differ from visual time due to - // rewind buffering - int currentEvent; + int maxBinValue = 1; + int maxTime = 1; - void skipToTime(int newTime, bool doRewindBuffering); - void handleBackwardsSkip(bool doRewindBuffering); - void processRewind(); - void processNewEvents(PlaybackMode playbackMode); -private slots: - void replayTimerTimeout(); + int currentTime = 0; public: - static constexpr int SMALL_SKIP_MS = 1000; - static constexpr int BIG_SKIP_MS = 10000; - explicit ReplayTimelineWidget(QWidget *parent = nullptr); - void setTimeline(const QList &_replayTimeline); + void setTimeline(const QList &replayTimeline); [[nodiscard]] QSize sizeHint() const override; [[nodiscard]] QSize minimumSizeHint() const override; - void setTimeScaleFactor(qreal _timeScaleFactor); - [[nodiscard]] int getCurrentEvent() const - { - return currentEvent; - } + public slots: - void startReplay(); - void stopReplay(); - void skipByAmount(int amount); // use a negative amount to skip backwards + void setCurrentTime(int time); protected: void paintEvent(QPaintEvent *event) override; diff --git a/cockatrice/src/interface/widgets/replay/replay_widget.cpp b/cockatrice/src/interface/widgets/replay/replay_widget.cpp index 4dc7b1380..6c85d950e 100644 --- a/cockatrice/src/interface/widgets/replay/replay_widget.cpp +++ b/cockatrice/src/interface/widgets/replay/replay_widget.cpp @@ -1,70 +1,50 @@ #include "replay_widget.h" +#include "../../../client/settings/cache_settings.h" #include "../../../client/settings/shortcuts_settings.h" #include "../interface/widgets/tabs/tab_game.h" +#include "replay_manager.h" #include "replay_quick_settings_widget.h" #include #include -ReplayWidget::ReplayWidget(TabGame *parent, GameReplay *_replay) - : QWidget(parent), game(parent), replay(_replay), replayPlayButton(nullptr), replayFastForwardButton(nullptr), - aReplaySkipForward(nullptr), aReplaySkipBackward(nullptr), aReplaySkipForwardBig(nullptr), - aReplaySkipBackwardBig(nullptr) +ReplayWidget::ReplayWidget(QWidget *parent, GameReplay *replay) + : QWidget(parent), replayPlayButton(nullptr), replayFastForwardButton(nullptr), aReplaySkipForward(nullptr), + aReplaySkipBackward(nullptr), aReplaySkipForwardBig(nullptr), aReplaySkipBackwardBig(nullptr) { - if (replay) { - game->getGame()->loadReplay(replay); - - // Create list: event number -> time [ms] - // Distribute simultaneous events evenly across 1 second. - unsigned int lastEventTimestamp = 0; - const int eventCount = replay->event_list_size(); - for (int i = 0; i < eventCount; ++i) { - int j = i + 1; - while ((j < eventCount) && (replay->event_list(j).seconds_elapsed() == lastEventTimestamp)) { - ++j; - } - - const int numberEventsThisSecond = j - i; - for (int k = 0; k < numberEventsThisSecond; ++k) { - replayTimeline.append(replay->event_list(i + k).seconds_elapsed() * 1000 + - (int)((qreal)k / (qreal)numberEventsThisSecond * 1000)); - } - - if (j < eventCount) { - lastEventTimestamp = replay->event_list(j).seconds_elapsed(); - } - i += numberEventsThisSecond - 1; - } - } + // replay manager + replayManager = new ReplayManager(this, replay); + connect(replayManager, &ReplayManager::eventReplayed, this, &ReplayWidget::eventReplayed); + connect(replayManager, &ReplayManager::replayFinished, this, &ReplayWidget::replayFinished); + connect(replayManager, &ReplayManager::rewound, this, &ReplayWidget::rewound); // timeline widget timelineWidget = new ReplayTimelineWidget; - timelineWidget->setTimeline(replayTimeline); - connect(timelineWidget, &ReplayTimelineWidget::processNextEvent, this, &ReplayWidget::replayNextEvent); - connect(timelineWidget, &ReplayTimelineWidget::replayFinished, this, &ReplayWidget::replayFinished); - connect(timelineWidget, &ReplayTimelineWidget::rewound, this, &ReplayWidget::replayRewind); + timelineWidget->setTimeline(replayManager->getReplayTimeline()); + connect(replayManager, &ReplayManager::timeChanged, timelineWidget, &ReplayTimelineWidget::setCurrentTime); + connect(timelineWidget, &ReplayTimelineWidget::timeClicked, replayManager, &ReplayManager::setTime); // timeline skip shortcuts aReplaySkipForward = new QAction(timelineWidget); timelineWidget->addAction(aReplaySkipForward); connect(aReplaySkipForward, &QAction::triggered, this, - [this] { timelineWidget->skipByAmount(ReplayTimelineWidget::SMALL_SKIP_MS); }); + [this] { replayManager->skipByAmount(ReplayManager::SMALL_SKIP_MS); }); aReplaySkipBackward = new QAction(timelineWidget); timelineWidget->addAction(aReplaySkipBackward); connect(aReplaySkipBackward, &QAction::triggered, this, - [this] { timelineWidget->skipByAmount(-ReplayTimelineWidget::SMALL_SKIP_MS); }); + [this] { replayManager->skipByAmount(-ReplayManager::SMALL_SKIP_MS); }); aReplaySkipForwardBig = new QAction(timelineWidget); timelineWidget->addAction(aReplaySkipForwardBig); connect(aReplaySkipForwardBig, &QAction::triggered, this, - [this] { timelineWidget->skipByAmount(ReplayTimelineWidget::BIG_SKIP_MS); }); + [this] { replayManager->skipByAmount(ReplayManager::BIG_SKIP_MS); }); aReplaySkipBackwardBig = new QAction(timelineWidget); timelineWidget->addAction(aReplaySkipBackwardBig); connect(aReplaySkipBackwardBig, &QAction::triggered, this, - [this] { timelineWidget->skipByAmount(-ReplayTimelineWidget::BIG_SKIP_MS); }); + [this] { replayManager->skipByAmount(-ReplayManager::BIG_SKIP_MS); }); // buttons replayPlayButton = new QToolButton; @@ -86,6 +66,10 @@ ReplayWidget::ReplayWidget(TabGame *parent, GameReplay *_replay) settingsWidget->setFixedSize(QSize(32, 32)); connect(settingsWidget, &ReplayQuickSettingsWidget::fastForwardSpeedChanged, this, [this] { updateTimeScaleFactor(replayFastForwardButton->isChecked()); }); + connect(settingsWidget, &ReplayQuickSettingsWidget::skipEmptySectionsChanged, replayManager, + &ReplayManager::setSkipEmptySections); + + replayManager->setSkipEmptySections(SettingsCache::instance().userInterface().getSkipEmptySections()); // putting everything together auto replayControlLayout = new QHBoxLayout; @@ -97,18 +81,11 @@ ReplayWidget::ReplayWidget(TabGame *parent, GameReplay *_replay) setObjectName("replayControlWidget"); setLayout(replayControlLayout); - connect(this, &ReplayWidget::requestChatAndPhaseReset, game, &TabGame::resetChatAndPhase); - connect(&SettingsCache::instance().shortcuts(), &ShortcutsSettings::shortCutChanged, this, &ReplayWidget::refreshShortcuts); refreshShortcuts(); } -void ReplayWidget::replayNextEvent(EventProcessingOptions options) -{ - emit eventReplayed(replay->event_list(timelineWidget->getCurrentEvent()), options); -} - void ReplayWidget::replayFinished() { replayPlayButton->setChecked(false); @@ -117,24 +94,16 @@ void ReplayWidget::replayFinished() void ReplayWidget::replayPlayButtonToggled(bool checked) { if (checked) { // start replay - timelineWidget->startReplay(); + replayManager->startReplay(); } else { // pause replay - timelineWidget->stopReplay(); + replayManager->stopReplay(); } } void ReplayWidget::updateTimeScaleFactor(bool isFastForward) { - qreal factor = isFastForward ? SettingsCache::instance().interface().getFastForwardSpeed() : 1.0; - timelineWidget->setTimeScaleFactor(factor); -} - -/** - * @brief Handles everything that needs to be reset when doing a replay rewind. - */ -void ReplayWidget::replayRewind() -{ - emit requestChatAndPhaseReset(); + qreal factor = isFastForward ? SettingsCache::instance().userInterface().getFastForwardSpeed() : 1.0; + replayManager->setTimeScaleFactor(factor); } void ReplayWidget::refreshShortcuts() diff --git a/cockatrice/src/interface/widgets/replay/replay_widget.h b/cockatrice/src/interface/widgets/replay/replay_widget.h index eca356e9e..6d2b7a043 100644 --- a/cockatrice/src/interface/widgets/replay/replay_widget.h +++ b/cockatrice/src/interface/widgets/replay/replay_widget.h @@ -14,11 +14,12 @@ #include #include +class ReplayManager; class ReplayQuickSettingsWidget; class TabGame; /** - * @brief The top-level that is put in the replay dock widget. + * @brief The top-level widget that is put in the replay dock widget. * Contains the replay timeline as well as the buttons. */ class ReplayWidget : public QWidget @@ -26,29 +27,28 @@ class ReplayWidget : public QWidget Q_OBJECT public: - ReplayWidget(TabGame *parent, GameReplay *replay); - TabGame *game; - GameReplay *replay; + /** + * @param parent The parent widget + * @param replay Cannot be null. Takes ownership of the replay. + */ + ReplayWidget(QWidget *parent, GameReplay *replay); signals: - void requestChatAndPhaseReset(); + void rewound(); void eventReplayed(const GameEventContainer &cont, EventProcessingOptions options); private: - // Replay related members - int currentReplayStep = 0; - QList replayTimeline; + ReplayManager *replayManager; + ReplayTimelineWidget *timelineWidget; QToolButton *replayPlayButton, *replayFastForwardButton; ReplayQuickSettingsWidget *settingsWidget; QAction *aReplaySkipForward, *aReplaySkipBackward, *aReplaySkipForwardBig, *aReplaySkipBackwardBig; private slots: - void replayNextEvent(EventProcessingOptions options); void replayFinished(); void replayPlayButtonToggled(bool checked); void updateTimeScaleFactor(bool checked); - void replayRewind(); void refreshShortcuts(); }; diff --git a/cockatrice/src/interface/widgets/server/chat_view/chat_view.cpp b/cockatrice/src/interface/widgets/server/chat_view/chat_view.cpp index 2415c3bb4..064064380 100644 --- a/cockatrice/src/interface/widgets/server/chat_view/chat_view.cpp +++ b/cockatrice/src/interface/widgets/server/chat_view/chat_view.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include @@ -62,12 +63,14 @@ void ChatView::adjustColorsToPalette() serverMessageColor = QColor(0xFF, 0x73, 0x83); otherUserColor = otherUserColor.lighter(150); linkColor = QColor(71, 158, 252); + unresolvedCardTagColor = QColor(0xFF, 0xA5, 0x00); } else { document()->setDefaultStyleSheet(R"( a { text-decoration: none; color: blue; } .blue { color: blue } )"); linkColor = palette().link().color(); + unresolvedCardTagColor = QColor(0xA0, 0x52, 0x2D); } QTimer::singleShot(0, this, &ChatView::refreshBlockColors); @@ -173,13 +176,22 @@ void ChatView::appendHtmlServerMessage(const QString &html, bool optionalIsBold, void ChatView::appendCardTag(QTextCursor &cursor, const QString &cardName) { QTextCharFormat oldFormat = cursor.charFormat(); - QTextCharFormat anchorFormat = oldFormat; - anchorFormat.setForeground(linkColor); - anchorFormat.setAnchor(true); - anchorFormat.setAnchorHref("card://" + cardName); - anchorFormat.setFontItalic(true); + QTextCharFormat cardFormat = oldFormat; + cardFormat.setFontItalic(true); - cursor.setCharFormat(anchorFormat); + if (!CardDatabaseManager::query()->lookupCardByName(cardName)) { + cardFormat.setForeground(unresolvedCardTagColor); + cursor.setCharFormat(cardFormat); + cursor.insertText(cardName); + cursor.setCharFormat(oldFormat); + return; + } + + cardFormat.setForeground(linkColor); + cardFormat.setAnchor(true); + cardFormat.setAnchorHref("card://" + cardName); + + cursor.setCharFormat(cardFormat); cursor.insertText(cardName); cursor.setCharFormat(oldFormat); } diff --git a/cockatrice/src/interface/widgets/server/chat_view/chat_view.h b/cockatrice/src/interface/widgets/server/chat_view/chat_view.h index 506605d24..8d5894613 100644 --- a/cockatrice/src/interface/widgets/server/chat_view/chat_view.h +++ b/cockatrice/src/interface/widgets/server/chat_view/chat_view.h @@ -81,6 +81,7 @@ private: QColor otherUserColor = QColor(0, 65, 255); // dark blue QColor serverMessageColor = QColor(0x85, 0x15, 0x15); QColor linkColor; + QColor unresolvedCardTagColor; private slots: void openLink(const QUrl &link); diff --git a/cockatrice/src/interface/widgets/server/game_selector.cpp b/cockatrice/src/interface/widgets/server/game_selector.cpp index e9fa3c3cf..6580f0262 100644 --- a/cockatrice/src/interface/widgets/server/game_selector.cpp +++ b/cockatrice/src/interface/widgets/server/game_selector.cpp @@ -10,18 +10,23 @@ #include "games_model.h" #include "user/user_list_manager.h" +#include #include +#include #include #include #include #include #include +#include +#include #include #include #include #include #include #include +#include GameSelector::GameSelector(AbstractClient *_client, TabSupervisor *_tabSupervisor, @@ -79,12 +84,12 @@ GameSelector::GameSelector(AbstractClient *_client, if (showFilters && restoresettings) { quickFilterToolBar = new GameSelectorQuickFilterToolBar(this, tabSupervisor, gameListProxyModel, gameTypeMap); quickFilterToolBar->setVisible(showFilters && restoresettings && - SettingsCache::instance().cardsDisplay().getShowGameSelectorFilterToolbar()); + SettingsCache::instance().userInterface().getShowGameSelectorFilterToolbar()); - connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::showGameSelectorFilterToolbarChanged, + connect(&SettingsCache::instance().userInterface(), &InterfaceSettings::showGameSelectorFilterToolbarChanged, this, [this] { quickFilterToolBar->setVisible( - SettingsCache::instance().cardsDisplay().getShowGameSelectorFilterToolbar()); + SettingsCache::instance().userInterface().getShowGameSelectorFilterToolbar()); }); } else { quickFilterToolBar = nullptr; @@ -315,6 +320,21 @@ void GameSelector::customContextMenu(const QPoint &point) dlg.exec(); }); + QAction copyLink(tr("Copy Game Link")); + connect(©Link, &QAction::triggered, this, [=, this]() { + const ServerInfo_Game &gameInfo = gameListModel->getGame(index.data(Qt::UserRole).toInt()); + QUrl url; + url.setScheme("cockatrice"); + url.setHost("joingame"); + QUrlQuery query; + query.addQueryItem("hostname", client->serverName()); + query.addQueryItem("port", QString::number(client->serverPort())); + query.addQueryItem("roomid", QString::number(gameInfo.room_id())); + query.addQueryItem("gameid", QString::number(gameInfo.game_id())); + url.setQuery(query); + QGuiApplication::clipboard()->setText(url.toString(QUrl::FullyEncoded)); + }); + QMenu menu; menu.addAction(&joinGame); @@ -332,6 +352,11 @@ void GameSelector::customContextMenu(const QPoint &point) menu.addAction(&spectateGame); menu.addAction(&getGameInfo); + + if (!client->serverName().isEmpty()) { + menu.addAction(©Link); + } + menu.exec(gameListView->mapToGlobal(point)); } @@ -379,6 +404,24 @@ void GameSelector::joinGame(const bool asSpectator, const bool asJudge) disableButtons(); } +bool GameSelector::joinGameById(int gameId) +{ + auto *model = gameListView->model(); + + for (int row = 0; row < model->rowCount(); ++row) { + QModelIndex idx = model->index(row, 0); + const ServerInfo_Game &game = gameListModel->getGame(idx.data(Qt::UserRole).toInt()); + if (game.game_id() == gameId) { + gameListView->setCurrentIndex(idx); + joinGame(); + return true; + } + } + + qWarning() << "Game" << gameId << "not found"; + return false; +} + void GameSelector::disableButtons() { if (createButton) { diff --git a/cockatrice/src/interface/widgets/server/game_selector.h b/cockatrice/src/interface/widgets/server/game_selector.h index fa91e5f96..da34d5322 100644 --- a/cockatrice/src/interface/widgets/server/game_selector.h +++ b/cockatrice/src/interface/widgets/server/game_selector.h @@ -202,6 +202,7 @@ public: * @param info The ServerInfo_Game object containing information about the game to update. */ void processGameInfo(const ServerInfo_Game &info); + bool joinGameById(int gameId); }; #endif diff --git a/cockatrice/src/interface/widgets/server/user/user_list_widget.cpp b/cockatrice/src/interface/widgets/server/user/user_list_widget.cpp index 9c29c62bc..64cbb7b7d 100644 --- a/cockatrice/src/interface/widgets/server/user/user_list_widget.cpp +++ b/cockatrice/src/interface/widgets/server/user/user_list_widget.cpp @@ -30,7 +30,7 @@ #include #include #include -#include +#include #include BanDialog::BanDialog(const ServerInfo_User &info, QWidget *parent) : QDialog(parent) @@ -349,7 +349,7 @@ bool UserListItemDelegate::editorEvent(QEvent *event, QSize UserListItemDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const { - if (!SettingsCache::instance().interface().getStyleUserList()) { + if (!SettingsCache::instance().appearance().getStyleUserList()) { return QStyledItemDelegate::sizeHint(option, index); } return UserListPainter::sizeHint(); @@ -357,7 +357,7 @@ QSize UserListItemDelegate::sizeHint(const QStyleOptionViewItem &option, const Q void UserListItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const { - if (!SettingsCache::instance().interface().getStyleUserList()) { + if (!SettingsCache::instance().appearance().getStyleUserList()) { QStyledItemDelegate::paint(painter, option, index); return; } @@ -521,7 +521,7 @@ UserListWidget::UserListWidget(TabSupervisor *_tabSupervisor, // Pin on item click connect(userTree, &QTreeWidget::itemClicked, this, [this](QTreeWidgetItem *item, int) { - if (!SettingsCache::instance().interface().getStyleUserList()) { + if (!SettingsCache::instance().appearance().getStyleUserList()) { return; } const QString name = static_cast(item)->getUserInfo().name().c_str(); @@ -553,7 +553,7 @@ UserListWidget::UserListWidget(TabSupervisor *_tabSupervisor, connect(cardArtProvider, &UserCardArtProvider::cardArtUpdated, this, [this](const QString &) { userTree->viewport()->update(); }); - connect(&SettingsCache::instance().interface(), &InterfaceSettings::styleUserListChanged, this, + connect(&SettingsCache::instance().appearance(), &AppearanceSettings::styleUserListChanged, this, &UserListWidget::applyDisplayMode); applyDisplayMode(); @@ -659,7 +659,7 @@ void UserListWidget::hideEvent(QHideEvent *e) void UserListWidget::applyDisplayMode() { - const bool styled = SettingsCache::instance().interface().getStyleUserList(); + const bool styled = SettingsCache::instance().appearance().getStyleUserList(); if (styled) { userTree->header()->setSectionResizeMode(0, QHeaderView::Stretch); @@ -718,7 +718,7 @@ bool UserListWidget::eventFilter(QObject *obj, QEvent *event) { if (obj == userTree->viewport()) { if (event->type() == QEvent::MouseMove) { - if (!SettingsCache::instance().interface().getStyleUserList()) { + if (!SettingsCache::instance().appearance().getStyleUserList()) { return QGroupBox::eventFilter(obj, event); } auto *me = static_cast(event); diff --git a/cockatrice/src/interface/widgets/settings_page/abstract_settings_page.cpp b/cockatrice/src/interface/widgets/settings_page/abstract_settings_page.cpp new file mode 100644 index 000000000..9a70e94c5 --- /dev/null +++ b/cockatrice/src/interface/widgets/settings_page/abstract_settings_page.cpp @@ -0,0 +1,196 @@ +#include "abstract_settings_page.h" + +#include "settings_search_model.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +/** + * @brief Recursively collects all widgets within a layout + * @param layout The layout to walk + * @param widgets Output list of (widget, containing layout) pairs + */ +static void collectWidgets(QLayout *layout, QList> &widgets) +{ + for (int i = 0; i < layout->count(); ++i) { + QLayoutItem *item = layout->itemAt(i); + if (!item) { + continue; + } + if (QWidget *widget = item->widget()) { + widgets.append({widget, layout}); + } else if (QLayout *subLayout = item->layout()) { + collectWidgets(subLayout, widgets); + } + } +} + +/** + * @brief Rejects QLabels that are not setting names + * + * HTML link labels, path values, and excessively long labels are filtered out. + */ +static bool isValidSettingLabel(const QLabel *label) +{ + const QString &text = label->text(); + if (Qt::mightBeRichText(text)) { + return false; + } + if (text.contains(QLatin1Char('/')) || text.contains(QLatin1Char('\\'))) { + return false; + } + if (text.size() > 60) { + return false; + } + return true; +} + +/** + * @brief Finds the control associated with a setting label + * + * Uses the explicit buddy if set, otherwise the widget in the cell (or slot) + * immediately following the label within the same layout. Returns nullptr when + * no obvious control is found. + */ +static QWidget *controlForLabel(QLabel *label, QLayout *containingLayout) +{ + if (QWidget *buddy = label->buddy()) { + return buddy; + } + + if (auto *grid = qobject_cast(containingLayout)) { + int index = grid->indexOf(label); + if (index != -1) { + int row = 0; + int column = 0; + int rowSpan = 1; + int columnSpan = 1; + grid->getItemPosition(index, &row, &column, &rowSpan, &columnSpan); + if (QLayoutItem *next = grid->itemAtPosition(row, column + columnSpan)) { + if (QWidget *nextWidget = next->widget()) { + return nextWidget; + } + } + } + } else { + int index = containingLayout->indexOf(label); + if (index != -1) { + for (int i = index + 1; i < containingLayout->count(); ++i) { + if (QLayoutItem *next = containingLayout->itemAt(i)) { + if (QWidget *nextWidget = next->widget()) { + return nextWidget; + } + } + } + } + } + + return nullptr; +} + +/** + * @brief Builds the extended search text for an entry + * + * Combines the group title, label, and any extra searchable text derived from + * the associated control (placeholder, prefix/suffix, tooltip). Combo values + * and numeric tooltips are excluded since they change at runtime and would + * make the search index stale. + */ +static QString buildFullSearchText(const QString &groupTitle, const QString &cleanLabel, QWidget *control) +{ + QStringList parts = {groupTitle, cleanLabel}; + if (control) { + if (auto *lineEdit = qobject_cast(control)) { + parts.append(lineEdit->placeholderText()); + } else if (auto *spinBox = qobject_cast(control)) { + parts.append(spinBox->prefix()); + parts.append(spinBox->suffix()); + } + if (!control->toolTip().isEmpty()) { + bool isNumeric = false; + control->toolTip().toInt(&isNumeric); + if (!isNumeric) { + parts.append(control->toolTip()); + } + } + } + parts.removeAll(QString()); + return parts.join(QLatin1Char(' ')); +} + +QList AbstractSettingsPage::getSearchEntries() +{ + return autoDetectSearchEntries(this, -1); +} + +QList AbstractSettingsPage::autoDetectSearchEntries(QWidget *page, int pageIndex) +{ + QList entries; + + const auto children = page->children(); + for (QObject *child : children) { + auto *groupBox = qobject_cast(child); + if (!groupBox) { + continue; + } + + QString groupTitle = groupBox->title(); + if (groupTitle.isEmpty()) { + continue; + } + + QLayout *groupLayout = groupBox->layout(); + if (!groupLayout) { + continue; + } + + QList> widgets; + collectWidgets(groupLayout, widgets); + + for (const auto &pair : widgets) { + QWidget *widget = pair.first; + QString label; + + auto *checkBox = qobject_cast(widget); + if (checkBox) { + label = checkBox->text(); + } else { + auto *labelWidget = qobject_cast(widget); + if (!labelWidget || labelWidget->text().isEmpty() || !isValidSettingLabel(labelWidget)) { + continue; + } + label = labelWidget->text(); + } + + if (label.isEmpty()) { + continue; + } + + // Strip accelerator markers (&) for search + QString cleanLabel = label; + cleanLabel.remove(QLatin1Char('&')); + + QWidget *control = widget; + if (auto *labelWidget = qobject_cast(widget)) { + control = controlForLabel(labelWidget, pair.second); + if (!control) { + continue; + } + } + + entries.append(SettingsSearchEntry{.pageIndex = pageIndex, + .groupTitle = groupTitle, + .widgetLabel = cleanLabel, + .fullSearchText = buildFullSearchText(groupTitle, cleanLabel, control), + .widget = control}); + } + } + + return entries; +} diff --git a/cockatrice/src/interface/widgets/settings_page/abstract_settings_page.h b/cockatrice/src/interface/widgets/settings_page/abstract_settings_page.h index 4cbf2d71a..3140f94f3 100644 --- a/cockatrice/src/interface/widgets/settings_page/abstract_settings_page.h +++ b/cockatrice/src/interface/widgets/settings_page/abstract_settings_page.h @@ -1,16 +1,24 @@ #ifndef COCKATRICE_ABSTRACT_SETTINGS_PAGE_H #define COCKATRICE_ABSTRACT_SETTINGS_PAGE_H +#include #include #define WIKI_CUSTOM_PIC_URL "https://github.com/Cockatrice/Cockatrice/wiki/Custom-Picture-Download-URLs" #define WIKI_CUSTOM_SHORTCUTS "https://github.com/Cockatrice/Cockatrice/wiki/Custom-Keyboard-Shortcuts" #define WIKI_TRANSLATION_FAQ "https://github.com/Cockatrice/Cockatrice/wiki/Translation-FAQ" +struct SettingsSearchEntry; + class AbstractSettingsPage : public QWidget { + Q_OBJECT public: virtual void retranslateUi() = 0; + virtual QList getSearchEntries(); + +protected: + static QList autoDetectSearchEntries(QWidget *page, int pageIndex); }; #endif // COCKATRICE_ABSTRACT_SETTINGS_PAGE_H diff --git a/cockatrice/src/interface/widgets/settings_page/appearance_settings_page.cpp b/cockatrice/src/interface/widgets/settings_page/appearance_settings_page.cpp index 0441c0d03..c470e54fe 100644 --- a/cockatrice/src/interface/widgets/settings_page/appearance_settings_page.cpp +++ b/cockatrice/src/interface/widgets/settings_page/appearance_settings_page.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #include #include @@ -104,7 +105,7 @@ AppearanceSettingsPage::AppearanceSettingsPage() homeTabBackgroundSourceBox.addItem(QObject::tr(entry.trKey), QVariant::fromValue(entry.type)); } - QString homeTabBackgroundSource = SettingsCache::instance().personal().getHomeTabBackgroundSource(); + QString homeTabBackgroundSource = settings.appearance().getHomeTabBackgroundSource(); int homeTabBackgroundSourceId = homeTabBackgroundSourceBox.findData(BackgroundSources::fromId(homeTabBackgroundSource)); if (homeTabBackgroundSourceId != -1) { @@ -113,20 +114,19 @@ AppearanceSettingsPage::AppearanceSettingsPage() connect(&homeTabBackgroundSourceBox, QOverload::of(&QComboBox::currentIndexChanged), this, [this]() { auto type = homeTabBackgroundSourceBox.currentData().value(); - SettingsCache::instance().personal().setHomeTabBackgroundSource(BackgroundSources::toId(type)); + SettingsCache::instance().appearance().setHomeTabBackgroundSource(BackgroundSources::toId(type)); updateHomeTabSettingsVisibility(); }); homeTabBackgroundShuffleFrequencySpinBox.setRange(0, 3600); homeTabBackgroundShuffleFrequencySpinBox.setSuffix(tr(" seconds")); - homeTabBackgroundShuffleFrequencySpinBox.setValue( - SettingsCache::instance().personal().getHomeTabBackgroundShuffleFrequency()); - connect(&homeTabBackgroundShuffleFrequencySpinBox, qOverload(&QSpinBox::valueChanged), &settings.personal(), - &PersonalSettings::setHomeTabBackgroundShuffleFrequency); + homeTabBackgroundShuffleFrequencySpinBox.setValue(settings.appearance().getHomeTabBackgroundShuffleFrequency()); + connect(&homeTabBackgroundShuffleFrequencySpinBox, qOverload(&QSpinBox::valueChanged), &settings.appearance(), + &AppearanceSettings::setHomeTabBackgroundShuffleFrequency); - homeTabDisplayCardNameCheckBox.setChecked(settings.personal().getHomeTabDisplayCardName()); - connect(&homeTabDisplayCardNameCheckBox, &QCheckBox::QT_STATE_CHANGED, &settings.personal(), - &PersonalSettings::setHomeTabDisplayCardName); + homeTabDisplayCardNameCheckBox.setChecked(settings.appearance().getHomeTabDisplayCardName()); + connect(&homeTabDisplayCardNameCheckBox, &QCheckBox::QT_STATE_CHANGED, &settings.appearance(), + &AppearanceSettings::setHomeTabDisplayCardName); updateHomeTabSettingsVisibility(); @@ -140,9 +140,9 @@ AppearanceSettingsPage::AppearanceSettingsPage() homeTabGroupBox = new QGroupBox; homeTabGroupBox->setLayout(homeTabGrid); - styleUserListCheckBox.setChecked(settings.interface().getStyleUserList()); - connect(&styleUserListCheckBox, &QCheckBox::QT_STATE_CHANGED, &settings.interface(), - &InterfaceSettings::setStyleUserList); + styleUserListCheckBox.setChecked(settings.appearance().getStyleUserList()); + connect(&styleUserListCheckBox, &QCheckBox::QT_STATE_CHANGED, &settings.appearance(), + &AppearanceSettings::setStyleUserList); auto stylingTabGrid = new QGridLayout; stylingTabGrid->addWidget(&styleUserListCheckBox, 0, 0, 1, 2); @@ -151,12 +151,12 @@ AppearanceSettingsPage::AppearanceSettingsPage() stylingGroupBox->setLayout(stylingTabGrid); // Menu settings - showShortcutsCheckBox.setChecked(settings.cardsDisplay().getShowShortcuts()); + showShortcutsCheckBox.setChecked(settings.userInterface().getShowShortcuts()); connect(&showShortcutsCheckBox, &QCheckBox::QT_STATE_CHANGED, this, &AppearanceSettingsPage::showShortcutsChanged); - showGameSelectorFilterToolbarCheckBox.setChecked(settings.cardsDisplay().getShowGameSelectorFilterToolbar()); - connect(&showGameSelectorFilterToolbarCheckBox, &QCheckBox::QT_STATE_CHANGED, &settings.cardsDisplay(), - &CardsDisplaySettings::setShowGameSelectorFilterToolbar); + showGameSelectorFilterToolbarCheckBox.setChecked(settings.userInterface().getShowGameSelectorFilterToolbar()); + connect(&showGameSelectorFilterToolbarCheckBox, &QCheckBox::QT_STATE_CHANGED, &settings.userInterface(), + &InterfaceSettings::setShowGameSelectorFilterToolbar); auto *menuGrid = new QGridLayout; menuGrid->addWidget(&showShortcutsCheckBox, 0, 0); @@ -199,9 +199,9 @@ AppearanceSettingsPage::AppearanceSettingsPage() connect(&roundCardCornersCheckBox, &QAbstractButton::toggled, &settings.cardsDisplay(), &CardsDisplaySettings::setRoundCardCorners); - connect(&maxFontSizeForCardsEdit, qOverload(&QSpinBox::valueChanged), &settings.personal(), - &PersonalSettings::setMaxFontSize); - maxFontSizeForCardsEdit.setValue(settings.personal().getMaxFontSize()); + connect(&maxFontSizeForCardsEdit, qOverload(&QSpinBox::valueChanged), &settings.appearance(), + &AppearanceSettings::setMaxFontSize); + maxFontSizeForCardsEdit.setValue(settings.appearance().getMaxFontSize()); maxFontSizeForCardsLabel.setBuddy(&maxFontSizeForCardsEdit); maxFontSizeForCardsEdit.setMinimum(9); maxFontSizeForCardsEdit.setMaximum(100); @@ -224,12 +224,12 @@ AppearanceSettingsPage::AppearanceSettingsPage() &CardsDisplaySettings::setStackCardOverlapPercent); cardViewInitialRowsMaxBox.setRange(1, 999); - cardViewInitialRowsMaxBox.setValue(SettingsCache::instance().interface().getCardViewInitialRowsMax()); + cardViewInitialRowsMaxBox.setValue(SettingsCache::instance().userInterface().getCardViewInitialRowsMax()); connect(&cardViewInitialRowsMaxBox, qOverload(&QSpinBox::valueChanged), this, &AppearanceSettingsPage::cardViewInitialRowsMaxChanged); cardViewExpandedRowsMaxBox.setRange(1, 999); - cardViewExpandedRowsMaxBox.setValue(SettingsCache::instance().interface().getCardViewExpandedRowsMax()); + cardViewExpandedRowsMaxBox.setValue(SettingsCache::instance().userInterface().getCardViewExpandedRowsMax()); connect(&cardViewExpandedRowsMaxBox, qOverload(&QSpinBox::valueChanged), this, &AppearanceSettingsPage::cardViewExpandedRowsMaxChanged); @@ -291,12 +291,12 @@ AppearanceSettingsPage::AppearanceSettingsPage() cardCountersGroupBox->setLayout(cardCountersLayout); // Hand layout - horizontalHandCheckBox.setChecked(settings.interface().getHorizontalHand()); - connect(&horizontalHandCheckBox, &QCheckBox::QT_STATE_CHANGED, &settings.interface(), + horizontalHandCheckBox.setChecked(settings.userInterface().getHorizontalHand()); + connect(&horizontalHandCheckBox, &QCheckBox::QT_STATE_CHANGED, &settings.userInterface(), &InterfaceSettings::setHorizontalHand); - leftJustifiedHandCheckBox.setChecked(settings.interface().getLeftJustified()); - connect(&leftJustifiedHandCheckBox, &QCheckBox::QT_STATE_CHANGED, &settings.interface(), + leftJustifiedHandCheckBox.setChecked(settings.userInterface().getLeftJustified()); + connect(&leftJustifiedHandCheckBox, &QCheckBox::QT_STATE_CHANGED, &settings.userInterface(), &InterfaceSettings::setLeftJustified); auto *handGrid = new QGridLayout; @@ -307,13 +307,13 @@ AppearanceSettingsPage::AppearanceSettingsPage() handGroupBox->setLayout(handGrid); // table grid layout - invertVerticalCoordinateCheckBox.setChecked(settings.interface().getInvertVerticalCoordinate()); - connect(&invertVerticalCoordinateCheckBox, &QCheckBox::QT_STATE_CHANGED, &settings.interface(), + invertVerticalCoordinateCheckBox.setChecked(settings.userInterface().getInvertVerticalCoordinate()); + connect(&invertVerticalCoordinateCheckBox, &QCheckBox::QT_STATE_CHANGED, &settings.userInterface(), &InterfaceSettings::setInvertVerticalCoordinate); minPlayersForMultiColumnLayoutEdit.setMinimum(2); - minPlayersForMultiColumnLayoutEdit.setValue(settings.interface().getMinPlayersForMultiColumnLayout()); - connect(&minPlayersForMultiColumnLayoutEdit, qOverload(&QSpinBox::valueChanged), &settings.interface(), + minPlayersForMultiColumnLayoutEdit.setValue(settings.userInterface().getMinPlayersForMultiColumnLayout()); + connect(&minPlayersForMultiColumnLayoutEdit, qOverload(&QSpinBox::valueChanged), &settings.userInterface(), &InterfaceSettings::setMinPlayersForMultiColumnLayout); minPlayersForMultiColumnLayoutLabel.setBuddy(&minPlayersForMultiColumnLayoutEdit); @@ -375,7 +375,7 @@ void AppearanceSettingsPage::editPalette() void AppearanceSettingsPage::updateHomeTabSettingsVisibility() { - bool visible = SettingsCache::instance().personal().getHomeTabBackgroundSource() != + bool visible = SettingsCache::instance().appearance().getHomeTabBackgroundSource() != BackgroundSources::toId(BackgroundSources::Theme); homeTabBackgroundShuffleFrequencyLabel.setVisible(visible); @@ -385,7 +385,7 @@ void AppearanceSettingsPage::updateHomeTabSettingsVisibility() void AppearanceSettingsPage::showShortcutsChanged(QT_STATE_CHANGED_T value) { - SettingsCache::instance().cardsDisplay().setShowShortcuts(value); + SettingsCache::instance().userInterface().setShowShortcuts(value); qApp->setAttribute(Qt::AA_DontShowShortcutsInContextMenus, value == 0); // 0 = unchecked } @@ -412,7 +412,7 @@ void AppearanceSettingsPage::overrideAllCardArtWithPersonalPreferenceToggled(QT_ */ void AppearanceSettingsPage::cardViewInitialRowsMaxChanged(int value) { - SettingsCache::instance().interface().setCardViewInitialRowsMax(value); + SettingsCache::instance().userInterface().setCardViewInitialRowsMax(value); if (cardViewExpandedRowsMaxBox.value() < value) { cardViewExpandedRowsMaxBox.setValue(value); } @@ -425,7 +425,7 @@ void AppearanceSettingsPage::cardViewInitialRowsMaxChanged(int value) */ void AppearanceSettingsPage::cardViewExpandedRowsMaxChanged(int value) { - SettingsCache::instance().interface().setCardViewExpandedRowsMax(value); + SettingsCache::instance().userInterface().setCardViewExpandedRowsMax(value); if (cardViewInitialRowsMaxBox.value() > value) { cardViewInitialRowsMaxBox.setValue(value); } diff --git a/cockatrice/src/interface/widgets/settings_page/appearance_settings_page.h b/cockatrice/src/interface/widgets/settings_page/appearance_settings_page.h index 09b73ad04..0b6b6832c 100644 --- a/cockatrice/src/interface/widgets/settings_page/appearance_settings_page.h +++ b/cockatrice/src/interface/widgets/settings_page/appearance_settings_page.h @@ -40,10 +40,10 @@ private: QSpinBox homeTabBackgroundShuffleFrequencySpinBox; QCheckBox homeTabDisplayCardNameCheckBox; QCheckBox styleUserListCheckBox; - QLabel minPlayersForMultiColumnLayoutLabel; - QLabel maxFontSizeForCardsLabel; QCheckBox showShortcutsCheckBox; QCheckBox showGameSelectorFilterToolbarCheckBox; + QLabel minPlayersForMultiColumnLayoutLabel; + QLabel maxFontSizeForCardsLabel; QCheckBox overrideAllCardArtWithPersonalPreferenceCheckBox; QCheckBox bumpSetsWithCardsInDeckToTopCheckBox; QCheckBox displayCardNamesCheckBox; diff --git a/cockatrice/src/interface/widgets/settings_page/deck_editor_settings_page.cpp b/cockatrice/src/interface/widgets/settings_page/deck_editor_settings_page.cpp index bb0a1096c..f425afe60 100644 --- a/cockatrice/src/interface/widgets/settings_page/deck_editor_settings_page.cpp +++ b/cockatrice/src/interface/widgets/settings_page/deck_editor_settings_page.cpp @@ -17,9 +17,9 @@ DeckEditorSettingsPage::DeckEditorSettingsPage() { - picDownloadCheckBox.setChecked(SettingsCache::instance().personal().getPicDownload()); - connect(&picDownloadCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().personal(), - &PersonalSettings::setPicDownload); + picDownloadCheckBox.setChecked(SettingsCache::instance().downloads().getPicDownload()); + connect(&picDownloadCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().downloads(), + &DownloadSettings::setPicDownload); urlLinkLabel.setTextInteractionFlags(Qt::LinksAccessibleByMouse); urlLinkLabel.setOpenExternalLinks(true); @@ -29,7 +29,7 @@ DeckEditorSettingsPage::DeckEditorSettingsPage() auto *lpGeneralGrid = new QGridLayout; auto *lpSpoilerGrid = new QGridLayout; - mcDownloadSpoilersCheckBox.setChecked(SettingsCache::instance().personal().getDownloadSpoilersStatus()); + mcDownloadSpoilersCheckBox.setChecked(SettingsCache::instance().downloads().getDownloadSpoilersStatus()); mpSpoilerSavePathLineEdit = new QLineEdit(SettingsCache::instance().getSpoilerCardDatabasePath()); mpSpoilerSavePathLineEdit->setReadOnly(true); @@ -91,8 +91,8 @@ DeckEditorSettingsPage::DeckEditorSettingsPage() lpSpoilerGrid->addWidget(&infoOnSpoilersLabel, 3, 0, 1, 3, Qt::AlignTop); // On a change to the checkbox, hide/un-hide the other fields - connect(&mcDownloadSpoilersCheckBox, &QCheckBox::toggled, &SettingsCache::instance().personal(), - &PersonalSettings::setDownloadSpoilerStatus); + connect(&mcDownloadSpoilersCheckBox, &QCheckBox::toggled, &SettingsCache::instance().downloads(), + &DownloadSettings::setDownloadSpoilerStatus); connect(&mcDownloadSpoilersCheckBox, &QCheckBox::toggled, this, &DeckEditorSettingsPage::setSpoilersEnabled); mpGeneralGroupBox = new QGroupBox; diff --git a/cockatrice/src/interface/widgets/settings_page/messages_settings_page.cpp b/cockatrice/src/interface/widgets/settings_page/messages_settings_page.cpp index a2437727c..b37668db6 100644 --- a/cockatrice/src/interface/widgets/settings_page/messages_settings_page.cpp +++ b/cockatrice/src/interface/widgets/settings_page/messages_settings_page.cpp @@ -90,6 +90,7 @@ MessagesSettingsPage::MessagesSettingsPage() highlightNotice->addWidget(&hexHighlightLabel, 1, 2); highlightNotice->addWidget(customAlertString, 0, 0); highlightNotice->addWidget(&customAlertStringLabel, 1, 0); + customAlertStringLabel.setBuddy(customAlertString); highlightGroupBox = new QGroupBox; highlightGroupBox->setLayout(highlightNotice); diff --git a/cockatrice/src/interface/widgets/settings_page/settings_search_delegate.cpp b/cockatrice/src/interface/widgets/settings_page/settings_search_delegate.cpp new file mode 100644 index 000000000..2fdff3bce --- /dev/null +++ b/cockatrice/src/interface/widgets/settings_page/settings_search_delegate.cpp @@ -0,0 +1,125 @@ +/** + * @file settings_search_delegate.cpp + * @brief Implementation of the custom settings search result delegate + * @ingroup Dialogs + */ +#include "settings_search_delegate.h" + +#include "settings_search_model.h" + +#include + +SettingsSearchDelegate::SettingsSearchDelegate(QObject *parent) : QStyledItemDelegate(parent) +{ +} + +void SettingsSearchDelegate::setPageNames(const QStringList &names) +{ + pageNames = names; +} + +void SettingsSearchDelegate::setPageIcons(const QStringList &iconResources) +{ + pageIcons.clear(); + for (const QString &resource : iconResources) { + pageIcons.append(QPixmap(resource)); + } +} + +void SettingsSearchDelegate::paint(QPainter *painter, + const QStyleOptionViewItem &option, + const QModelIndex &index) const +{ + painter->save(); + + SettingsSearchEntry entry = index.data(SettingsSearchModel::EntryRole).value(); + + bool isSelected = option.state & QStyle::State_Selected; + bool isHovered = option.state & QStyle::State_MouseOver; + + // Background + QColor bgColor = isSelected ? option.palette.color(QPalette::Highlight) + : isHovered ? option.palette.color(QPalette::Midlight) + : option.palette.color(QPalette::Base); + painter->fillRect(option.rect, bgColor); + + if (isSelected) { + // Accent bar on the left to make the selection unmistakable + painter->fillRect(QRect(option.rect.left(), option.rect.top(), 4, option.rect.height()), + option.palette.color(QPalette::Highlight).darker(150)); + } + + int leftMargin = 12; + int topMargin = 8; + int rightMargin = 12; + int bottomMargin = 4; + + QRect contentRect = option.rect.adjusted(leftMargin, topMargin, -rightMargin, -bottomMargin); + int yPos = contentRect.top(); + + // Icon of the related settings page + const int iconSize = 24; + QPixmap pageIcon = + (entry.pageIndex >= 0 && entry.pageIndex < pageIcons.size()) ? pageIcons.at(entry.pageIndex) : QPixmap(); + int iconOffset = pageIcon.isNull() ? 0 : iconSize + 8; + if (!pageIcon.isNull()) { + QRect iconRect(contentRect.left(), contentRect.top() + (contentRect.height() - iconSize) / 2, iconSize, + iconSize); + painter->drawPixmap(iconRect, pageIcon); + } + + QRect textRect = contentRect.adjusted(iconOffset, 0, 0, 0); + + // Breadcrumb: "Page > Group" + QFont breadcrumbFont = option.font; + breadcrumbFont.setPointSize(breadcrumbFont.pointSize() - 1); + breadcrumbFont.setBold(true); + + QColor breadcrumbColor = + isSelected ? option.palette.color(QPalette::HighlightedText) : option.palette.color(QPalette::Text); + if (!isSelected) { + breadcrumbColor.setAlpha(180); + } + + QString pageName; + if (entry.pageIndex >= 0 && entry.pageIndex < pageNames.size()) { + pageName = pageNames[entry.pageIndex]; + } else { + pageName = QString::number(entry.pageIndex); + } + + QString breadcrumbText = QStringLiteral("%1 > %2").arg(pageName, entry.groupTitle); + painter->setFont(breadcrumbFont); + painter->setPen(breadcrumbColor); + painter->drawText(QRect(textRect.left(), yPos, textRect.width(), 20), Qt::AlignLeft | Qt::AlignVCenter, + breadcrumbText); + yPos += 20; + + // Setting label + QFont labelFont = option.font; + labelFont.setPointSize(labelFont.pointSize() + 1); + labelFont.setBold(isSelected); + + QColor labelColor = + isSelected ? option.palette.color(QPalette::HighlightedText) : option.palette.color(QPalette::Text); + + painter->setFont(labelFont); + painter->setPen(labelColor); + painter->drawText(QRect(textRect.left(), yPos, textRect.width(), 24), Qt::AlignLeft | Qt::AlignVCenter, + entry.widgetLabel); + yPos += 24; + + // Bottom separator + QPen separatorPen(option.palette.color(QPalette::Mid), 1); + painter->setPen(separatorPen); + painter->drawLine(option.rect.left() + leftMargin, option.rect.bottom(), option.rect.right() - rightMargin, + option.rect.bottom()); + + painter->restore(); +} + +QSize SettingsSearchDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const +{ + Q_UNUSED(index); + return QSize(option.rect.width(), 56); +} diff --git a/cockatrice/src/interface/widgets/settings_page/settings_search_delegate.h b/cockatrice/src/interface/widgets/settings_page/settings_search_delegate.h new file mode 100644 index 000000000..aa296049a --- /dev/null +++ b/cockatrice/src/interface/widgets/settings_page/settings_search_delegate.h @@ -0,0 +1,38 @@ +/** + * @file settings_search_delegate.h + * @brief Custom delegate for rendering settings search results + * @ingroup Dialogs + */ +#ifndef COCKATRICE_SETTINGS_SEARCH_DELEGATE_H +#define COCKATRICE_SETTINGS_SEARCH_DELEGATE_H + +#include +#include + +/** + * @brief Custom paint delegate for settings search result items + * + * Renders each search result with a breadcrumb line ("Page > Group"), + * the setting label, and a subtle separator. Supports selected/hovered states. + */ +class SettingsSearchDelegate : public QStyledItemDelegate +{ + Q_OBJECT +public: + explicit SettingsSearchDelegate(QObject *parent = nullptr); + + void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override; + QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override; + + /** @brief Sets the translated page names for breadcrumb display */ + void setPageNames(const QStringList &names); + + /** @brief Sets the icons shown in front of results, indexed by page position */ + void setPageIcons(const QStringList &iconResources); + +private: + QStringList pageNames; ///< Translated page names indexed by page position + QList pageIcons; ///< Icons of the related settings pages +}; + +#endif // COCKATRICE_SETTINGS_SEARCH_DELEGATE_H diff --git a/cockatrice/src/interface/widgets/settings_page/settings_search_model.cpp b/cockatrice/src/interface/widgets/settings_page/settings_search_model.cpp new file mode 100644 index 000000000..71f859d4b --- /dev/null +++ b/cockatrice/src/interface/widgets/settings_page/settings_search_model.cpp @@ -0,0 +1,145 @@ +/** + * @file settings_search_model.cpp + * @brief Implementation of the settings search list model + * @ingroup Dialogs + */ +#include "settings_search_model.h" + +#include +#include + +SettingsSearchModel::SettingsSearchModel(QObject *parent) : QAbstractListModel(parent) +{ +} + +void SettingsSearchModel::setSourceEntries(const QList &entries) +{ + beginResetModel(); + sourceEntries = entries; + endResetModel(); + rebuildFilter(); +} + +void SettingsSearchModel::setFilterString(const QString &text) +{ + filterActive = !text.trimmed().isEmpty(); + if (filterActive) { + filterQuery = text.trimmed(); + filterRegex = + QRegularExpression(QRegularExpression::escape(filterQuery), QRegularExpression::CaseInsensitiveOption); + } + rebuildFilter(); +} + +bool SettingsSearchModel::isFilterActive() const +{ + return filterActive; +} + +/** + * @brief Calculates a relevance score for a single entry against the query + * + * Scoring priorities (highest to lowest): + * 1. Label starts with query -> 100 + * 2. Label contains query -> 80 + * 3. Group title starts with query -> 60 + * 4. Group title contains query -> 40 + * 5. Full text regex match -> 20 + * 6. No match -> 0 (excluded from results) + */ +static int relevanceScore(const SettingsSearchEntry &entry, const QString &query, const QRegularExpression ®ex) +{ + QString lowerQuery = query.toLower(); + + // Label matches are most relevant + QString label = entry.widgetLabel.toLower(); + if (label.startsWith(lowerQuery)) { + return 100; + } + if (label.contains(lowerQuery)) { + return 80; + } + + // Group title matches are next + QString group = entry.groupTitle.toLower(); + if (group.startsWith(lowerQuery)) { + return 60; + } + if (group.contains(lowerQuery)) { + return 40; + } + + // Full text match is least relevant + if (entry.fullSearchText.contains(regex)) { + return 20; + } + + return 0; +} + +void SettingsSearchModel::rebuildFilter() +{ + beginResetModel(); + filteredIndices.clear(); + + if (!filterActive) { + for (int i = 0; i < sourceEntries.size(); ++i) { + filteredIndices.append(i); + } + } else { + QList> scored; // + for (int i = 0; i < sourceEntries.size(); ++i) { + const SettingsSearchEntry &entry = sourceEntries[i]; + // Skip conditional settings that are currently disabled or hidden + if (entry.widget && (!entry.widget->isEnabled() || entry.widget->isHidden())) { + continue; + } + int score = relevanceScore(entry, filterQuery, filterRegex); + if (score > 0) { + scored.append({-score, i}); // negative for descending sort + } + } + std::sort(scored.begin(), scored.end()); + for (const auto &pair : scored) { + filteredIndices.append(pair.second); + } + } + endResetModel(); +} + +int SettingsSearchModel::rowCount(const QModelIndex &parent) const +{ + if (parent.isValid()) { + return 0; + } + return filteredIndices.size(); +} + +QVariant SettingsSearchModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid() || index.row() >= filteredIndices.size()) { + return {}; + } + + const SettingsSearchEntry &entry = sourceEntries[filteredIndices[index.row()]]; + + switch (role) { + case EntryRole: + return QVariant::fromValue(entry); + case Qt::DisplayRole: + return entry.widgetLabel; + case Qt::ToolTipRole: + return QStringLiteral("%1 > %2 > %3") + .arg(QString::number(entry.pageIndex), entry.groupTitle, entry.widgetLabel); + default: + return {}; + } +} + +SettingsSearchEntry SettingsSearchModel::entryForIndex(const QModelIndex &index) const +{ + if (!index.isValid() || index.row() >= filteredIndices.size()) { + return {}; + } + return sourceEntries[filteredIndices[index.row()]]; +} diff --git a/cockatrice/src/interface/widgets/settings_page/settings_search_model.h b/cockatrice/src/interface/widgets/settings_page/settings_search_model.h new file mode 100644 index 000000000..0b590f180 --- /dev/null +++ b/cockatrice/src/interface/widgets/settings_page/settings_search_model.h @@ -0,0 +1,76 @@ +/** + * @file settings_search_model.h + * @brief Data model for the settings search feature + * @ingroup Dialogs + */ +#ifndef COCKATRICE_SETTINGS_SEARCH_MODEL_H +#define COCKATRICE_SETTINGS_SEARCH_MODEL_H + +#include +#include +#include +#include + +/** + * @brief Represents a single searchable setting entry + * + * Each settings page provides a list of these entries via getSearchEntries(). + * The model uses them for filtering, relevance scoring, and display. + */ +struct SettingsSearchEntry +{ + int pageIndex; ///< Index of the settings page this entry belongs to + QString groupTitle; ///< Title of the group/section within the page + QString widgetLabel; ///< Display label for the setting widget + QString fullSearchText; ///< Extended search text (label, control text, tooltip) for full-text matching + QWidget *widget; ///< Pointer to the setting widget for focus/scrolling +}; + +/** + * @brief List model providing filtered, ranked search results + * + * Manages a list of SettingsSearchEntry items. When a filter string is set, + * entries are scored by relevance and sorted so the best matches appear first. + * Supports custom roles for accessing entry fields from views and delegates. + */ +class SettingsSearchModel : public QAbstractListModel +{ + Q_OBJECT +public: + /** + * @brief Custom data role for accessing the full entry + */ + enum Roles + { + EntryRole = Qt::UserRole + 1, ///< Full SettingsSearchEntry object + }; + + explicit SettingsSearchModel(QObject *parent = nullptr); + + /** @brief Replaces the source entries and rebuilds the filter */ + void setSourceEntries(const QList &entries); + /** @brief Sets the filter string and recalculates the results */ + void setFilterString(const QString &text); + /** @brief Whether a non-empty filter is currently active */ + bool isFilterActive() const; + + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + + /** @brief Returns the full entry for a given model index */ + SettingsSearchEntry entryForIndex(const QModelIndex &index) const; + +private: + QList sourceEntries; ///< Complete unfiltered entry list + QList filteredIndices; ///< Indices into sourceEntries matching the filter + QRegularExpression filterRegex; ///< Compiled regex for the current filter + QString filterQuery; ///< Current filter query string + bool filterActive = false; ///< Whether filtering is active + + /** @brief Recalculates the filtered index list and ranking */ + void rebuildFilter(); +}; + +Q_DECLARE_METATYPE(SettingsSearchEntry) + +#endif // COCKATRICE_SETTINGS_SEARCH_MODEL_H diff --git a/cockatrice/src/interface/widgets/settings_page/shortcut_settings_page.cpp b/cockatrice/src/interface/widgets/settings_page/shortcut_settings_page.cpp index e3b9994ea..1f1867f7c 100644 --- a/cockatrice/src/interface/widgets/settings_page/shortcut_settings_page.cpp +++ b/cockatrice/src/interface/widgets/settings_page/shortcut_settings_page.cpp @@ -123,6 +123,7 @@ void ShortcutSettingsPage::retranslateUi() currentActionGroupLabel->setText(tr("Section:")); currentActionLabel->setText(tr("Action:")); currentShortcutLabel->setText(tr("Shortcut:")); + editShortcutGroupBox->setTitle(tr("Shortcut editor")); editTextBox->retranslateUi(); faqLabel->setText(QString("%2").arg(WIKI_CUSTOM_SHORTCUTS).arg(tr("How to set custom shortcuts"))); btnResetAll->setText(tr("Restore all default shortcuts")); diff --git a/cockatrice/src/interface/widgets/settings_page/user_interface_settings_page.cpp b/cockatrice/src/interface/widgets/settings_page/user_interface_settings_page.cpp index fa27ec198..634df0b15 100644 --- a/cockatrice/src/interface/widgets/settings_page/user_interface_settings_page.cpp +++ b/cockatrice/src/interface/widgets/settings_page/user_interface_settings_page.cpp @@ -2,9 +2,11 @@ #include "../../../client/settings/cache_settings.h" #include "../interface/widgets/tabs/tab_supervisor.h" +#include "../tabs/api/commander_spellbook/commander_spellbook_bracket_explainer.h" #include #include +#include #include #include #include @@ -19,70 +21,70 @@ enum visualDeckStoragePromptForConversionIndex UserInterfaceSettingsPage::UserInterfaceSettingsPage() { // general settings and notification settings - notificationsEnabledCheckBox.setChecked(SettingsCache::instance().interface().getNotificationsEnabled()); - connect(¬ificationsEnabledCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().interface(), + notificationsEnabledCheckBox.setChecked(SettingsCache::instance().userInterface().getNotificationsEnabled()); + connect(¬ificationsEnabledCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().userInterface(), &InterfaceSettings::setNotificationsEnabled); connect(¬ificationsEnabledCheckBox, &QCheckBox::QT_STATE_CHANGED, this, &UserInterfaceSettingsPage::setNotificationEnabled); specNotificationsEnabledCheckBox.setChecked( - SettingsCache::instance().interface().getSpectatorNotificationsEnabled()); - specNotificationsEnabledCheckBox.setEnabled(SettingsCache::instance().interface().getNotificationsEnabled()); - connect(&specNotificationsEnabledCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().interface(), + SettingsCache::instance().userInterface().getSpectatorNotificationsEnabled()); + specNotificationsEnabledCheckBox.setEnabled(SettingsCache::instance().userInterface().getNotificationsEnabled()); + connect(&specNotificationsEnabledCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().userInterface(), &InterfaceSettings::setSpectatorNotificationsEnabled); buddyConnectNotificationsEnabledCheckBox.setChecked( - SettingsCache::instance().interface().getBuddyConnectNotificationsEnabled()); + SettingsCache::instance().userInterface().getBuddyConnectNotificationsEnabled()); buddyConnectNotificationsEnabledCheckBox.setEnabled( - SettingsCache::instance().interface().getNotificationsEnabled()); + SettingsCache::instance().userInterface().getNotificationsEnabled()); connect(&buddyConnectNotificationsEnabledCheckBox, &QCheckBox::QT_STATE_CHANGED, - &SettingsCache::instance().interface(), &InterfaceSettings::setBuddyConnectNotificationsEnabled); + &SettingsCache::instance().userInterface(), &InterfaceSettings::setBuddyConnectNotificationsEnabled); - doubleClickToPlayCheckBox.setChecked(SettingsCache::instance().interface().getDoubleClickToPlay()); - connect(&doubleClickToPlayCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().interface(), + doubleClickToPlayCheckBox.setChecked(SettingsCache::instance().userInterface().getDoubleClickToPlay()); + connect(&doubleClickToPlayCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().userInterface(), &InterfaceSettings::setDoubleClickToPlay); - clickPlaysAllSelectedCheckBox.setChecked(SettingsCache::instance().interface().getClickPlaysAllSelected()); - connect(&clickPlaysAllSelectedCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().interface(), + clickPlaysAllSelectedCheckBox.setChecked(SettingsCache::instance().userInterface().getClickPlaysAllSelected()); + connect(&clickPlaysAllSelectedCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().userInterface(), &InterfaceSettings::setClickPlaysAllSelected); - playToStackCheckBox.setChecked(SettingsCache::instance().interface().getPlayToStack()); - connect(&playToStackCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().interface(), + playToStackCheckBox.setChecked(SettingsCache::instance().userInterface().getPlayToStack()); + connect(&playToStackCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().userInterface(), &InterfaceSettings::setPlayToStack); doNotDeleteArrowsInSubPhasesCheckBox.setChecked( - SettingsCache::instance().interface().getDoNotDeleteArrowsInSubPhases()); - connect(&doNotDeleteArrowsInSubPhasesCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().interface(), - &InterfaceSettings::setDoNotDeleteArrowsInSubPhases); + SettingsCache::instance().userInterface().getDoNotDeleteArrowsInSubPhases()); + connect(&doNotDeleteArrowsInSubPhasesCheckBox, &QCheckBox::QT_STATE_CHANGED, + &SettingsCache::instance().userInterface(), &InterfaceSettings::setDoNotDeleteArrowsInSubPhases); - closeEmptyCardViewCheckBox.setChecked(SettingsCache::instance().interface().getCloseEmptyCardView()); - connect(&closeEmptyCardViewCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().interface(), + closeEmptyCardViewCheckBox.setChecked(SettingsCache::instance().userInterface().getCloseEmptyCardView()); + connect(&closeEmptyCardViewCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().userInterface(), &InterfaceSettings::setCloseEmptyCardView); - focusCardViewSearchBarCheckBox.setChecked(SettingsCache::instance().interface().getFocusCardViewSearchBar()); - connect(&focusCardViewSearchBarCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().interface(), + focusCardViewSearchBarCheckBox.setChecked(SettingsCache::instance().userInterface().getFocusCardViewSearchBar()); + connect(&focusCardViewSearchBarCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().userInterface(), &InterfaceSettings::setFocusCardViewSearchBar); - annotateTokensCheckBox.setChecked(SettingsCache::instance().interface().getAnnotateTokens()); - connect(&annotateTokensCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().interface(), + annotateTokensCheckBox.setChecked(SettingsCache::instance().userInterface().getAnnotateTokens()); + connect(&annotateTokensCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().userInterface(), &InterfaceSettings::setAnnotateTokens); - showDragSelectionCountCheckBox.setChecked(SettingsCache::instance().interface().getShowDragSelectionCount()); - connect(&showDragSelectionCountCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().interface(), + showDragSelectionCountCheckBox.setChecked(SettingsCache::instance().userInterface().getShowDragSelectionCount()); + connect(&showDragSelectionCountCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().userInterface(), &InterfaceSettings::setShowDragSelectionCount); - showTotalSelectionCountCheckBox.setChecked(SettingsCache::instance().interface().getShowTotalSelectionCount()); - connect(&showTotalSelectionCountCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().interface(), + showTotalSelectionCountCheckBox.setChecked(SettingsCache::instance().userInterface().getShowTotalSelectionCount()); + connect(&showTotalSelectionCountCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().userInterface(), &InterfaceSettings::setShowTotalSelectionCount); - useTearOffMenusCheckBox.setChecked(SettingsCache::instance().interface().getUseTearOffMenus()); - connect(&useTearOffMenusCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().interface(), + useTearOffMenusCheckBox.setChecked(SettingsCache::instance().userInterface().getUseTearOffMenus()); + connect(&useTearOffMenusCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().userInterface(), [](const QT_STATE_CHANGED_T state) { - SettingsCache::instance().interface().setUseTearOffMenus(state == Qt::Checked); + SettingsCache::instance().userInterface().setUseTearOffMenus(state == Qt::Checked); }); - keepGameChatFocusCheckBox.setChecked(SettingsCache::instance().interface().getKeepGameChatFocus()); - connect(&keepGameChatFocusCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().interface(), + keepGameChatFocusCheckBox.setChecked(SettingsCache::instance().userInterface().getKeepGameChatFocus()); + connect(&keepGameChatFocusCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().userInterface(), &InterfaceSettings::setKeepGameChatFocus); auto *generalGrid = new QGridLayout; @@ -121,9 +123,9 @@ UserInterfaceSettingsPage::UserInterfaceSettingsPage() animationGroupBox->setLayout(animationGrid); // deck editor settings - openDeckInNewTabCheckBox.setChecked(SettingsCache::instance().interface().getOpenDeckInNewTab()); - connect(&openDeckInNewTabCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().interface(), - &InterfaceSettings::setOpenDeckInNewTab); + openDeckInNewTabCheckBox.setChecked(SettingsCache::instance().deckEditor().getOpenDeckInNewTab()); + connect(&openDeckInNewTabCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().deckEditor(), + &DeckEditorSettings::setOpenDeckInNewTab); visualDeckStorageInGameCheckBox.setChecked( SettingsCache::instance().visualDeckStorage().getVisualDeckStorageInGame()); @@ -156,10 +158,60 @@ UserInterfaceSettingsPage::UserInterfaceSettingsPage() defaultDeckEditorTypeSelector.addItem(""); // these will be set in retranslateUI defaultDeckEditorTypeSelector.addItem(""); - defaultDeckEditorTypeSelector.setCurrentIndex( - SettingsCache::instance().visualDeckStorage().getDefaultDeckEditorType()); + defaultDeckEditorTypeSelector.setCurrentIndex(SettingsCache::instance().deckEditor().getDefaultDeckEditorType()); connect(&defaultDeckEditorTypeSelector, QOverload::of(&QComboBox::currentIndexChanged), - &SettingsCache::instance().visualDeckStorage(), &VisualDeckStorageSettings::setDefaultDeckEditorType); + &SettingsCache::instance().deckEditor(), &DeckEditorSettings::setDefaultDeckEditorType); + + commanderSpellbookIntegrationUseOfficialBracketNamesExplainer.setText("?"); + commanderSpellbookIntegrationUseOfficialBracketNamesExplainer.setAutoRaise(true); + commanderSpellbookIntegrationUseOfficialBracketNamesExplainer.setEnabled(false); + + // Add items with userData = internal enum + commanderSpellbookIntegrationEnabledSelector.addItem(tr("Disabled"), + commanderSpellbookIntegrationEnabledIndexDisabled); + commanderSpellbookIntegrationEnabledSelector.addItem(tr("Enabled"), + commanderSpellbookIntegrationEnabledIndexEnabled); + commanderSpellbookIntegrationEnabledSelector.addItem(tr("Automatic"), + commanderSpellbookIntegrationEnabledIndexAutomatic); + + int storedMode = SettingsCache::instance().deckEditor().getCommanderSpellbookIntegrationEnabled(); + for (int i = 0; i < commanderSpellbookIntegrationEnabledSelector.count(); ++i) { + if (commanderSpellbookIntegrationEnabledSelector.itemData(i).toInt() == storedMode) { + commanderSpellbookIntegrationEnabledSelector.setCurrentIndex(i); + break; + } + } + + connect(&commanderSpellbookIntegrationEnabledSelector, QOverload::of(&QComboBox::currentIndexChanged), this, + [this](int index) { + int mode = commanderSpellbookIntegrationEnabledSelector.itemData(index).toInt(); + SettingsCache::instance().deckEditor().setCommanderSpellbookIntegrationEnabled(mode); + updateCommanderSpellbookUiState(); + }); + + commanderSpellbookIntegrationBracketNamingSelector.addItem( + tr("CommanderSpellbook bracket names")); // index 0 = false + commanderSpellbookIntegrationBracketNamingSelector.addItem( + tr("Official Commander bracket names (approximate)")); // index 1 = true + + commanderSpellbookIntegrationBracketNamingSelector.setCurrentIndex( + SettingsCache::instance().deckEditor().getCommanderSpellbookIntegrationUseOfficialBracketNames() ? 1 : 0); + + connect(&commanderSpellbookIntegrationBracketNamingSelector, QOverload::of(&QComboBox::currentIndexChanged), + &SettingsCache::instance(), [](int index) { + SettingsCache::instance().deckEditor().setCommanderSpellbookIntegrationUseOfficialBracketNames(index == + 1); + }); + + updateCommanderSpellbookUiState(); + + auto *labelLayout = new QHBoxLayout; + labelLayout->setContentsMargins(0, 0, 0, 0); + labelLayout->addWidget(&commanderSpellbookIntegrationUseOfficialBracketNamesLabel); + labelLayout->addWidget(&commanderSpellbookIntegrationUseOfficialBracketNamesExplainer); + + auto *labelWidget = new QWidget; + labelWidget->setLayout(labelLayout); auto *deckEditorGrid = new QGridLayout; deckEditorGrid->addWidget(&openDeckInNewTabCheckBox, 0, 0); @@ -169,14 +221,18 @@ UserInterfaceSettingsPage::UserInterfaceSettingsPage() deckEditorGrid->addWidget(&visualDeckStoragePromptForConversionSelector, 3, 1); deckEditorGrid->addWidget(&defaultDeckEditorTypeLabel, 4, 0); deckEditorGrid->addWidget(&defaultDeckEditorTypeSelector, 4, 1); + deckEditorGrid->addWidget(&commanderSpellbookIntegrationEnabledLabel, 5, 0); + deckEditorGrid->addWidget(&commanderSpellbookIntegrationEnabledSelector, 5, 1); + deckEditorGrid->addWidget(labelWidget, 6, 0); + deckEditorGrid->addWidget(&commanderSpellbookIntegrationBracketNamingSelector, 6, 1); deckEditorGroupBox = new QGroupBox; deckEditorGroupBox->setLayout(deckEditorGrid); // replay settings rewindBufferingMsBox.setRange(0, 9999); - rewindBufferingMsBox.setValue(SettingsCache::instance().interface().getRewindBufferingMs()); - connect(&rewindBufferingMsBox, qOverload(&QSpinBox::valueChanged), &SettingsCache::instance().interface(), + rewindBufferingMsBox.setValue(SettingsCache::instance().userInterface().getRewindBufferingMs()); + connect(&rewindBufferingMsBox, qOverload(&QSpinBox::valueChanged), &SettingsCache::instance().userInterface(), &InterfaceSettings::setRewindBufferingMs); auto *replayGrid = new QGridLayout; @@ -212,6 +268,27 @@ void UserInterfaceSettingsPage::setNotificationEnabled(QT_STATE_CHANGED_T i) } } +void UserInterfaceSettingsPage::updateCommanderSpellbookUiState() +{ + const int mode = SettingsCache::instance().deckEditor().getCommanderSpellbookIntegrationEnabled(); + + const bool enabled = mode != commanderSpellbookIntegrationEnabledIndexDisabled && + mode != commanderSpellbookIntegrationEnabledIndexUnprompted; + + commanderSpellbookIntegrationBracketNamingSelector.setEnabled(enabled); + commanderSpellbookIntegrationUseOfficialBracketNamesExplainer.setEnabled(enabled); + commanderSpellbookIntegrationUseOfficialBracketNamesLabel.setVisible(enabled); + commanderSpellbookIntegrationUseOfficialBracketNamesExplainer.setVisible(enabled); + commanderSpellbookIntegrationBracketNamingSelector.setVisible(enabled); + + if (enabled) { + // Sync selector with the current stored bool + const bool useOfficial = + SettingsCache::instance().deckEditor().getCommanderSpellbookIntegrationUseOfficialBracketNames(); + commanderSpellbookIntegrationBracketNamingSelector.setCurrentIndex(useOfficial ? 1 : 0); + } +} + void UserInterfaceSettingsPage::retranslateUi() { generalGroupBox->setTitle(tr("General interface settings")); @@ -241,15 +318,31 @@ void UserInterfaceSettingsPage::retranslateUi() visualDeckStoragePromptForConversionLabel.setText( tr("When adding a tag in the visual deck storage to a .txt deck:")); visualDeckStoragePromptForConversionSelector.setItemText(visualDeckStoragePromptForConversionIndexNone, - tr("do nothing")); + tr("Do nothing")); visualDeckStoragePromptForConversionSelector.setItemText(visualDeckStoragePromptForConversionIndexPrompt, - tr("ask to convert to .cod")); + tr("Ask to convert to .cod")); visualDeckStoragePromptForConversionSelector.setItemText(visualDeckStoragePromptForConversionIndexAlways, - tr("always convert to .cod")); + tr("Always convert to .cod")); defaultDeckEditorTypeLabel.setText(tr("Default deck editor type")); defaultDeckEditorTypeSelector.setItemText(TabSupervisor::ClassicDeckEditor, tr("Classic Deck Editor")); defaultDeckEditorTypeSelector.setItemText(TabSupervisor::VisualDeckEditor, tr("Visual Deck Editor")); + + commanderSpellbookIntegrationEnabledLabel.setText( + tr("CommanderSpellbook integration to estimate commander bracket")); + commanderSpellbookIntegrationEnabledSelector.setItemText(commanderSpellbookIntegrationEnabledIndexDisabled, + tr("Disabled")); + commanderSpellbookIntegrationEnabledSelector.setItemText(commanderSpellbookIntegrationEnabledIndexEnabled, + tr("Enabled")); + commanderSpellbookIntegrationEnabledSelector.setItemText(commanderSpellbookIntegrationEnabledIndexAutomatic, + tr("Automatic")); + commanderSpellbookIntegrationUseOfficialBracketNamesLabel.setText(tr("Bracket naming")); + commanderSpellbookIntegrationBracketNamingSelector.setItemText( + 0, CommanderBracketNames::CommanderSpellbookBracketNames); + commanderSpellbookIntegrationBracketNamingSelector.setItemText( + 1, CommanderBracketNames::OfficialCommanderBracketNames); + + commanderSpellbookIntegrationUseOfficialBracketNamesExplainer.setToolTip(CommanderBracketNames::Explainer); replayGroupBox->setTitle(tr("Replay settings")); rewindBufferingMsLabel.setText(tr("Buffer time for backwards skip via shortcut:")); rewindBufferingMsBox.setSuffix(" ms"); -} \ No newline at end of file +} diff --git a/cockatrice/src/interface/widgets/settings_page/user_interface_settings_page.h b/cockatrice/src/interface/widgets/settings_page/user_interface_settings_page.h index e10ed2a06..9e6fada69 100644 --- a/cockatrice/src/interface/widgets/settings_page/user_interface_settings_page.h +++ b/cockatrice/src/interface/widgets/settings_page/user_interface_settings_page.h @@ -8,6 +8,8 @@ #include #include #include +#include +#include #include class UserInterfaceSettingsPage : public AbstractSettingsPage @@ -15,6 +17,7 @@ class UserInterfaceSettingsPage : public AbstractSettingsPage Q_OBJECT private slots: void setNotificationEnabled(QT_STATE_CHANGED_T); + void updateCommanderSpellbookUiState(); private: QCheckBox notificationsEnabledCheckBox; @@ -39,6 +42,11 @@ private: QCheckBox visualDeckStorageSelectionAnimationCheckBox; QLabel defaultDeckEditorTypeLabel; QComboBox defaultDeckEditorTypeSelector; + QLabel commanderSpellbookIntegrationEnabledLabel; + QComboBox commanderSpellbookIntegrationEnabledSelector; + QLabel commanderSpellbookIntegrationUseOfficialBracketNamesLabel; + QToolButton commanderSpellbookIntegrationUseOfficialBracketNamesExplainer; + QComboBox commanderSpellbookIntegrationBracketNamingSelector; QLabel rewindBufferingMsLabel; QSpinBox rewindBufferingMsBox; QGroupBox *generalGroupBox; diff --git a/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.cpp b/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.cpp index 565cc3341..f80649eba 100644 --- a/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.cpp +++ b/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.cpp @@ -44,7 +44,7 @@ #include #include #include -#include +#include #include #include #include @@ -203,7 +203,7 @@ void AbstractTabDeckEditor::cleanDeckAndResetModified() */ AbstractTabDeckEditor::DeckOpenLocation AbstractTabDeckEditor::confirmOpen(const bool openInSameTabIfBlank) { - if (SettingsCache::instance().interface().getOpenDeckInNewTab()) { + if (SettingsCache::instance().deckEditor().getOpenDeckInNewTab()) { if (openInSameTabIfBlank && deckStateManager->isBlankNewDeck()) { return SAME_TAB; } else { diff --git a/cockatrice/src/interface/widgets/tabs/api/archidekt/display/archidekt_api_response_deck_entry_display_widget.h b/cockatrice/src/interface/widgets/tabs/api/archidekt/display/archidekt_api_response_deck_entry_display_widget.h index 7d0526fd4..fa5bc1712 100644 --- a/cockatrice/src/interface/widgets/tabs/api/archidekt/display/archidekt_api_response_deck_entry_display_widget.h +++ b/cockatrice/src/interface/widgets/tabs/api/archidekt/display/archidekt_api_response_deck_entry_display_widget.h @@ -97,7 +97,7 @@ public slots: protected: void mousePressEvent(QMouseEvent *event) override; - void enterEvent(QEnterEvent *event) override; + void enterEvent(QEnterEvent *event) override; ///< Hover enter void leaveEvent(QEvent *event) override; private: diff --git a/cockatrice/src/interface/widgets/tabs/api/archidekt/tab_archidekt.cpp b/cockatrice/src/interface/widgets/tabs/api/archidekt/tab_archidekt.cpp index b9b946a25..374d35cdf 100644 --- a/cockatrice/src/interface/widgets/tabs/api/archidekt/tab_archidekt.cpp +++ b/cockatrice/src/interface/widgets/tabs/api/archidekt/tab_archidekt.cpp @@ -27,7 +27,7 @@ #include #include #include -#include +#include #include TabArchidekt::TabArchidekt(TabSupervisor *_tabSupervisor) @@ -132,8 +132,8 @@ void TabArchidekt::initializeUi() // Settings settingsButton = new SettingsButtonWidget(primaryToolbar); - cardSizeSlider = new CardSizeWidget(primaryToolbar, nullptr, - SettingsCache::instance().visualDeckStorage().getArchidektPreviewSize()); + cardSizeSlider = + new CardSizeWidget(primaryToolbar, nullptr, SettingsCache::instance().cardsDisplay().getArchidektPreviewSize()); settingsButton->addSettingsWidget(cardSizeSlider); // Assemble primary toolbar @@ -339,8 +339,8 @@ void TabArchidekt::connectSignals() doSearch(); }); - connect(cardSizeSlider, &CardSizeWidget::cardSizeSettingUpdated, &SettingsCache::instance().visualDeckStorage(), - &VisualDeckStorageSettings::setArchidektPreviewCardSize); + connect(cardSizeSlider, &CardSizeWidget::cardSizeSettingUpdated, &SettingsCache::instance().cardsDisplay(), + &CardsDisplaySettings::setArchidektPreviewCardSize); // Search button triggers immediate search connect(searchButton, &QPushButton::clicked, this, &TabArchidekt::doSearchImmediate); diff --git a/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/card_in_deck_request.cpp b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/card_in_deck_request.cpp new file mode 100644 index 000000000..3c95a4553 --- /dev/null +++ b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/card_in_deck_request.cpp @@ -0,0 +1,17 @@ +#include "card_in_deck_request.h" + +CardInDeckRequest CardInDeckRequest::fromJson(const QJsonObject &json) +{ + CardInDeckRequest request; + request.card = json.value("card").toString(); + request.quantity = json.value("quantity").toInt(); + return request; +} + +QJsonObject CardInDeckRequest::toJson() const +{ + QJsonObject json; + json.insert("card", card); + json.insert("quantity", quantity); + return json; +} diff --git a/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/card_in_deck_request.h b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/card_in_deck_request.h new file mode 100644 index 000000000..b7eb96211 --- /dev/null +++ b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/card_in_deck_request.h @@ -0,0 +1,14 @@ +#ifndef COCKATRICE_CARD_IN_DECK_REQUEST_H +#define COCKATRICE_CARD_IN_DECK_REQUEST_H +#include + +struct CardInDeckRequest +{ + static CardInDeckRequest fromJson(const QJsonObject &json); + QJsonObject toJson() const; + + QString card; + int quantity; +}; + +#endif // COCKATRICE_CARD_IN_DECK_REQUEST_H diff --git a/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_card_result.cpp b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_card_result.cpp new file mode 100644 index 000000000..14558d234 --- /dev/null +++ b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_card_result.cpp @@ -0,0 +1,26 @@ +#include "commander_spellbook_card_result.h" + +CommanderSpellbookCardResult CommanderSpellbookCardResult::fromJson(const QJsonObject &json) +{ + CommanderSpellbookCardResult result; + + result.id = json.value("id").toString(); + result.name = json.value("name").toString(); + result.oracleId = json.value("oracleId").toString(); + result.spoiler = json.value("spoiler").toBool(); + result.typeLine = json.value("typeLine").toString(); + + result.imageUriFrontPng = json.value("imageUriFrontPng").toString(); + result.imageUriFrontLarge = json.value("imageUriFrontLarge").toString(); + result.imageUriFrontNormal = json.value("imageUriFrontNormal").toString(); + result.imageUriFrontSmall = json.value("imageUriFrontSmall").toString(); + result.imageUriFrontArtCrop = json.value("imageUriFrontArtCrop").toString(); + + result.imageUriBackPng = json.value("imageUriBackPng").toString(); + result.imageUriBackLarge = json.value("imageUriBackLarge").toString(); + result.imageUriBackNormal = json.value("imageUriBackNormal").toString(); + result.imageUriBackSmall = json.value("imageUriBackSmall").toString(); + result.imageUriBackArtCrop = json.value("imageUriBackArtCrop").toString(); + + return result; +} diff --git a/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_card_result.h b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_card_result.h new file mode 100644 index 000000000..eaa474097 --- /dev/null +++ b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_card_result.h @@ -0,0 +1,29 @@ +#ifndef COCKATRICE_COMMANDER_SPELLBOOK_CARD_RESULT_H +#define COCKATRICE_COMMANDER_SPELLBOOK_CARD_RESULT_H +#include +#include + +struct CommanderSpellbookCardResult +{ + static CommanderSpellbookCardResult fromJson(const QJsonObject &json); + + QString id; + QString name; + QString oracleId; + bool spoiler = false; + QString typeLine; + + QString imageUriFrontPng; + QString imageUriFrontLarge; + QString imageUriFrontNormal; + QString imageUriFrontSmall; + QString imageUriFrontArtCrop; + + QString imageUriBackPng; + QString imageUriBackLarge; + QString imageUriBackNormal; + QString imageUriBackSmall; + QString imageUriBackArtCrop; +}; + +#endif // COCKATRICE_COMMANDER_SPELLBOOK_CARD_RESULT_H diff --git a/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_deck_request.cpp b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_deck_request.cpp new file mode 100644 index 000000000..166469353 --- /dev/null +++ b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_deck_request.cpp @@ -0,0 +1,95 @@ +#include "commander_spellbook_deck_request.h" + +#include +#include + +CommanderSpellbookDeckRequest CommanderSpellbookDeckRequest::fromJson(const QJsonObject &json) +{ + CommanderSpellbookDeckRequest request; + + // Main deck + const QJsonArray mainArray = json.value("main").toArray(); + for (const QJsonValue &value : mainArray) { + if (!value.isObject()) { + continue; + } + + request.mainDeck.append(CardInDeckRequest::fromJson(value.toObject())); + + // Max size allowed by commanderspellbook + if (request.mainDeck.size() >= 600) { + break; + } + } + + // Commanders + const QJsonArray commanderArray = json.value("commanders").toArray(); + for (const QJsonValue &value : commanderArray) { + if (!value.isObject()) { + continue; + } + + request.commanderDeck.append(CardInDeckRequest::fromJson(value.toObject())); + + // Max size allowed by commanderspellbook + if (request.commanderDeck.size() >= 12) { + break; + } + } + + return request; +} + +QJsonObject CommanderSpellbookDeckRequest::toJson() const +{ + QJsonObject json; + + QJsonArray mainArray; + for (const CardInDeckRequest &card : mainDeck) { + mainArray.append(card.toJson()); + } + + QJsonArray commanderArray; + for (const CardInDeckRequest &card : commanderDeck) { + commanderArray.append(card.toJson()); + } + + json.insert("main", mainArray); + json.insert("commanders", commanderArray); + + return json; +} + +CommanderSpellbookDeckRequest CommanderSpellbookDeckRequest::fromDeckList(const DeckList &deck) +{ + CommanderSpellbookDeckRequest request; + + // --- Mainboard --- + const auto mainCards = deck.getCardNodes({DECK_ZONE_MAIN}); + for (const DecklistCardNode *node : mainCards) { + if (!node) { + continue; + } + + QJsonObject json; + json.insert("card", node->getName()); + json.insert("quantity", node->getNumber()); + request.mainDeck.append(CardInDeckRequest::fromJson(json)); + + // Max size allowed by commanderspellbook + if (request.mainDeck.size() >= 600) { + break; + } + } + + // --- Commander (bannerCard) --- + const auto &metadata = deck.getMetadata(); + if (!metadata.bannerCard.name.isEmpty()) { + QJsonObject json; + json.insert("card", metadata.bannerCard.name); + json.insert("quantity", 1); + request.commanderDeck.append(CardInDeckRequest::fromJson(json)); + } + + return request; +} diff --git a/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_deck_request.h b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_deck_request.h new file mode 100644 index 000000000..46d8e053b --- /dev/null +++ b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_deck_request.h @@ -0,0 +1,19 @@ +#ifndef COCKATRICE_COMMANDER_SPELLBOOK_DECK_REQUEST_H +#define COCKATRICE_COMMANDER_SPELLBOOK_DECK_REQUEST_H +#include "card_in_deck_request.h" +#include "libcockatrice/deck_list/deck_list.h" + +#include +#include + +struct CommanderSpellbookDeckRequest +{ + static CommanderSpellbookDeckRequest fromJson(const QJsonObject &json); + static CommanderSpellbookDeckRequest fromDeckList(const DeckList &deck); + QJsonObject toJson() const; + + QList mainDeck; // maxItems: 600 + QList commanderDeck; // maxItems: 12 +}; + +#endif // COCKATRICE_COMMANDER_SPELLBOOK_DECK_REQUEST_H diff --git a/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_estimate_bracket_result.cpp b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_estimate_bracket_result.cpp new file mode 100644 index 000000000..c3640ce79 --- /dev/null +++ b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_estimate_bracket_result.cpp @@ -0,0 +1,93 @@ +#include "commander_spellbook_estimate_bracket_result.h" + +EstimateBracketResult EstimateBracketResult::fromJson(const QJsonObject &json) +{ + EstimateBracketResult result; + + result.bracketTag = json.value("bracketTag").toString(); + + // + // Cards + // + for (const auto &value : json.value("cards").toArray()) { + if (!value.isObject()) { + continue; + } + + const QJsonObject obj = value.toObject(); + + CommanderSpellbookCardResult card = CommanderSpellbookCardResult::fromJson(obj.value("card").toObject()); + + if (obj.value("gameChanger").toBool()) { + result.gameChangerCards.append(card); + } + + if (obj.value("massLandDenial").toBool()) { + result.massLandDenialCards.append(card); + } + + if (obj.value("extraTurn").toBool()) { + result.extraTurnCards.append(card); + } + } + + // + // Templates + // + for (const auto &value : json.value("templates").toArray()) { + if (!value.isObject()) { + continue; + } + + const QJsonObject obj = value.toObject(); + + CommanderSpellbookVariantResult variant = CommanderSpellbookVariantResult::fromJson(obj); + + if (obj.value("massLandDenial").toBool()) { + result.massLandDenialTemplates.append(variant); + } + + if (obj.value("extraTurn").toBool()) { + result.extraTurnTemplates.append(variant); + } + } + + // + // Combos + // + for (const auto &value : json.value("combos").toArray()) { + if (!value.isObject()) { + continue; + } + + const QJsonObject obj = value.toObject(); + + CommanderSpellbookVariantResult combo = CommanderSpellbookVariantResult::fromJson(obj); + + if (obj.value("massLandDenial").toBool()) { + result.massLandDenialCombos.append(combo); + } + + if (obj.value("extraTurn").toBool()) { + result.extraTurnCombos.append(combo); + } + + if (obj.value("lock").toBool()) { + result.lockCombos.append(combo); + } + + if (obj.value("skipTurns").toBool()) { + result.skipTurnsCombos.append(combo); + } + + if (obj.value("definitelyTwoCard").toBool()) { + result.definitelyTwoCardCombos.append(combo); + } + + if (obj.value("arguablyTwoCard").toBool()) { + result.arguablyTwoCardCombos.append(combo); + } + } + + return result; +} diff --git a/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_estimate_bracket_result.h b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_estimate_bracket_result.h new file mode 100644 index 000000000..bb589a901 --- /dev/null +++ b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_estimate_bracket_result.h @@ -0,0 +1,32 @@ +#ifndef COCKATRICE_COMMANDER_SPELLBOOK_ESTIMATE_BRACKET_RESULT_H +#define COCKATRICE_COMMANDER_SPELLBOOK_ESTIMATE_BRACKET_RESULT_H + +#include "commander_spellbook_card_result.h" +#include "commander_spellbook_variant_result.h" + +#include +#include + +struct EstimateBracketResult +{ + static EstimateBracketResult fromJson(const QJsonObject &json); + + QString bracketTag; + + QList gameChangerCards; + QList massLandDenialCards; + QList extraTurnCards; + + QList massLandDenialTemplates; + QList extraTurnTemplates; + + QList massLandDenialCombos; + QList extraTurnCombos; + QList lockCombos; + QList skipTurnsCombos; + + QList definitelyTwoCardCombos; + QList arguablyTwoCardCombos; +}; + +#endif // COCKATRICE_COMMANDER_SPELLBOOK_ESTIMATE_BRACKET_RESULT_H diff --git a/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_variant_result.cpp b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_variant_result.cpp new file mode 100644 index 000000000..bbc23e15e --- /dev/null +++ b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_variant_result.cpp @@ -0,0 +1,35 @@ +#include "commander_spellbook_variant_result.h" + +CommanderSpellbookVariantResult CommanderSpellbookVariantResult::fromJson(const QJsonObject &json) +{ + CommanderSpellbookVariantResult result; + + result.id = json.value("id").toString(); + result.status = json.value("status").toString(); + + result.uses = json.value("uses").toArray(); + result.cardRequires = json.value("requires").toArray(); + result.produces = json.value("produces").toArray(); + result.of = json.value("of").toArray(); + result.includes = json.value("includes").toArray(); + + result.manaNeeded = json.value("manaNeeded").toArray(); + result.manaValueNeeded = json.value("manaValueNeeded").toArray(); + + result.easyPrerequisites = json.value("easyPrerequisites").toArray(); + result.notablePrerequisites = json.value("notablePrerequisites").toArray(); + + result.description = json.value("description").toString(); + result.notes = json.value("notes").toString(); + result.popularity = json.value("popularity").toDouble(); + + result.spoiler = json.value("spoiler").toBool(); + result.bracketTag = json.value("bracketTag").toString(); + + result.legalities = json.value("legalities").toObject(); + result.prices = json.value("prices").toObject(); + + result.variantCount = json.value("variantCount").toInt(); + + return result; +} diff --git a/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_variant_result.h b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_variant_result.h new file mode 100644 index 000000000..d908f1299 --- /dev/null +++ b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_variant_result.h @@ -0,0 +1,39 @@ +#ifndef COCKATRICE_COMMANDER_SPELLBOOK_VARIANT_RESULT_H +#define COCKATRICE_COMMANDER_SPELLBOOK_VARIANT_RESULT_H + +#include +#include + +struct CommanderSpellbookVariantResult +{ + static CommanderSpellbookVariantResult fromJson(const QJsonObject &json); + + QString id; + QString status; + + QJsonArray uses; + QJsonArray cardRequires; + QJsonArray produces; + QJsonArray of; + QJsonArray includes; + + QJsonArray manaNeeded; + QJsonArray manaValueNeeded; + + QJsonArray easyPrerequisites; + QJsonArray notablePrerequisites; + + QString description; + QString notes; + double popularity = 0.0; + + bool spoiler = false; + QString bracketTag; + + QJsonObject legalities; + QJsonObject prices; + + int variantCount = 0; +}; + +#endif // COCKATRICE_COMMANDER_SPELLBOOK_VARIANT_RESULT_H diff --git a/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_bracket_service.cpp b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_bracket_service.cpp new file mode 100644 index 000000000..b85129a3f --- /dev/null +++ b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_bracket_service.cpp @@ -0,0 +1,53 @@ +#include "commander_bracket_service.h" + +#include "../../../../../client/settings/cache_settings.h" + +#include + +CommanderBracketService &CommanderBracketService::instance() +{ + static CommanderBracketService service; + return service; +} + +CommanderBracketService::CommanderBracketService(QObject *parent) : QObject(parent) +{ + connect(&CommanderSpellbookApiAccessor::instance(), &CommanderSpellbookApiAccessor::estimateBracketFinished, this, + &CommanderBracketService::onEstimateBracketFinished); + + connect(&CommanderSpellbookApiAccessor::instance(), &CommanderSpellbookApiAccessor::estimateBracketError, this, + &CommanderBracketService::onEstimateBracketError); +} + +quint64 CommanderBracketService::estimateBracket(const DeckList &deck, QObject *requester) +{ + return CommanderSpellbookApiAccessor::instance().estimateBracket(deck, requester); +} + +void CommanderBracketService::onEstimateBracketFinished(CommanderSpellbookApiAccessor::RequestId id, + QObject *requester, + const EstimateBracketResult &result) +{ + CommanderBracketEstimate estimate; + + estimate.bracketTag = result.bracketTag; + + estimate.rawResult = result; + + auto &brackets = SettingsCache::instance().commanderBrackets(); + + estimate.officialName = brackets.officialName(result.bracketTag); + + estimate.displayName = brackets.displayName(result.bracketTag); + + estimate.explanation = brackets.explanation(result.bracketTag); + + emit estimateFinished(id, requester, estimate); +} + +void CommanderBracketService::onEstimateBracketError(CommanderSpellbookApiAccessor::RequestId id, + QObject *requester, + const QString &error) +{ + emit estimateError(id, requester, error); +} diff --git a/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_bracket_service.h b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_bracket_service.h new file mode 100644 index 000000000..e0c2a9d55 --- /dev/null +++ b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_bracket_service.h @@ -0,0 +1,45 @@ +#ifndef COCKATRICE_COMMANDER_BRACKET_SERVICE_H +#define COCKATRICE_COMMANDER_BRACKET_SERVICE_H + +#include "commander_spellbook_api_accessor.h" +#include "libcockatrice/deck_list/deck_list.h" + +#include + +struct CommanderBracketEstimate +{ + QString bracketTag; + + QString officialName; + QString displayName; + QString explanation; + + EstimateBracketResult rawResult; +}; + +class CommanderBracketService : public QObject +{ + Q_OBJECT + +public: + static CommanderBracketService &instance(); + + quint64 estimateBracket(const DeckList &deck, QObject *requester); + +signals: + void estimateFinished(quint64 requestId, QObject *requester, const CommanderBracketEstimate &estimate); + + void estimateError(quint64 requestId, QObject *requester, const QString &error); + +private slots: + void onEstimateBracketFinished(CommanderSpellbookApiAccessor::RequestId id, + QObject *requester, + const EstimateBracketResult &result); + + void onEstimateBracketError(CommanderSpellbookApiAccessor::RequestId id, QObject *requester, const QString &error); + +private: + explicit CommanderBracketService(QObject *parent = nullptr); +}; + +#endif diff --git a/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_bracket_widget.cpp b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_bracket_widget.cpp new file mode 100644 index 000000000..4f50e38a6 --- /dev/null +++ b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_bracket_widget.cpp @@ -0,0 +1,269 @@ +#include "commander_bracket_widget.h" + +#include "../../../../../client/settings/cache_settings.h" +#include "commander_bracket_service.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +CommanderBracketWidget::CommanderBracketWidget(QWidget *parent) : QWidget(parent) +{ + bracketLabel = new QLabel(tr("Bracket:"), this); + + bracketValueLabel = new QLabel(this); + bracketValueLabel->setText("-"); + bracketValueLabel->setObjectName("bracketValueLabel"); + + bracketInfoButton = new QToolButton(this); + bracketInfoButton->setText("?"); + bracketInfoButton->setAutoRaise(true); + bracketInfoButton->setEnabled(false); + + bracketRefreshButton = new QToolButton(this); + bracketRefreshButton->setIcon(QPixmap("theme:icons/reload")); + bracketRefreshButton->setAutoRaise(true); + + connect(bracketRefreshButton, &QToolButton::clicked, this, &CommanderBracketWidget::requestBracketEstimate); + + auto *layout = new QGridLayout(this); + layout->setContentsMargins(0, 0, 0, 0); + layout->addWidget(bracketLabel, 0, 0); + + auto *bracketRow = new QHBoxLayout; + bracketRow->addWidget(bracketValueLabel); + bracketRow->addWidget(bracketInfoButton); + bracketRow->addWidget(bracketRefreshButton); + bracketRow->addStretch(); + + layout->addLayout(bracketRow, 0, 1); + + connect(&CommanderBracketService::instance(), &CommanderBracketService::estimateFinished, this, + &CommanderBracketWidget::onEstimateBracketFinished); + connect(&CommanderBracketService::instance(), &CommanderBracketService::estimateError, this, + &CommanderBracketWidget::onEstimateBracketError); + + connect(&SettingsCache::instance().deckEditor(), &DeckEditorSettings::commanderSpellbookIntegrationEnabledChanged, + this, &CommanderBracketWidget::maybeAutoEstimateBracket); + connect(&SettingsCache::instance().deckEditor(), + &DeckEditorSettings::commanderSpellbookIntegrationUseOfficialBracketNamesChanged, this, + &CommanderBracketWidget::maybeAutoEstimateBracket); + + setVisible(false); +} + +void CommanderBracketWidget::setDeck(const QSharedPointer &_deck) +{ + deck = _deck; + requestId = 0; // invalidate any in-flight estimate for the previous deck + + // Reset the displayed bracket + bracketValueLabel->setText("-"); + bracketInfoButton->setToolTip({}); + bracketInfoButton->setEnabled(false); + bracketRefreshButton->setEnabled(true); + + maybeAutoEstimateBracket(); +} + +bool CommanderBracketWidget::promptCommanderSpellbookIntegration() +{ + QDialog dialog(this); + dialog.setWindowTitle(tr("CommanderSpellbook integration")); + + auto *mainLayout = new QVBoxLayout(&dialog); + + // Main text + auto *label = new QLabel(tr("CommanderSpellbook can analyze your deck and estimate its Commander bracket.\n\n" + "This sends your deck list to an external service.\n\n" + "CommanderSpellbook uses its own bracket naming system based on their own algorithm. " + "These names can be mapped to the official Commander brackets, but the mapping " + "is only an approximation.")); + label->setWordWrap(true); + mainLayout->addWidget(label); + + // Naming selector + auto *formLayout = new QFormLayout; + auto *namingCombo = new QComboBox(&dialog); + namingCombo->addItem(tr("CommanderSpellbook bracket names")); + namingCombo->addItem(tr("Official Commander bracket names (approximate)")); + namingCombo->setCurrentIndex( + SettingsCache::instance().deckEditor().getCommanderSpellbookIntegrationUseOfficialBracketNames() ? 1 : 0); + + // Create label + explainer button + auto *labelWidget = new QWidget(&dialog); + auto *labelLayout = new QHBoxLayout(labelWidget); + labelLayout->setContentsMargins(0, 0, 0, 0); + + auto *namingLabel = new QLabel(tr("Bracket naming:"), labelWidget); + auto *explainerButton = new QToolButton(labelWidget); + explainerButton->setText("?"); + explainerButton->setAutoRaise(true); + explainerButton->setEnabled(false); + explainerButton->setToolTip(CommanderBracketNames::Explainer); + + labelLayout->addWidget(namingLabel); + labelLayout->addWidget(explainerButton); + labelLayout->addStretch(); // push the button next to label, combo stays aligned + + // Add row with the custom label widget + formLayout->addRow(labelWidget, namingCombo); + mainLayout->addLayout(formLayout); + + // Buttons + auto *buttonBox = new QDialogButtonBox(&dialog); + auto *enableBtn = buttonBox->addButton(tr("Enable"), QDialogButtonBox::AcceptRole); + auto *automaticBtn = buttonBox->addButton(tr("Automatic"), QDialogButtonBox::ApplyRole); + auto *disableBtn = buttonBox->addButton(tr("Disable"), QDialogButtonBox::RejectRole); + mainLayout->addWidget(buttonBox); + + // Track which button was clicked + QAbstractButton *clickedButton = nullptr; + QObject::connect(buttonBox, &QDialogButtonBox::clicked, &dialog, [&](QAbstractButton *btn) { + clickedButton = btn; + dialog.accept(); + }); + + dialog.exec(); + + // Persist naming choice (if not disabled) + if (clickedButton != disableBtn) { + bool useOfficial = namingCombo->currentIndex() == 1; + SettingsCache::instance().deckEditor().setCommanderSpellbookIntegrationUseOfficialBracketNames(useOfficial); + } + + // Persist integration mode + if (clickedButton == disableBtn) { + SettingsCache::instance().deckEditor().setCommanderSpellbookIntegrationEnabled( + commanderSpellbookIntegrationEnabledIndexDisabled); + return false; + } + if (clickedButton == enableBtn) { + SettingsCache::instance().deckEditor().setCommanderSpellbookIntegrationEnabled( + commanderSpellbookIntegrationEnabledIndexEnabled); + return true; + } + if (clickedButton == automaticBtn) { + SettingsCache::instance().deckEditor().setCommanderSpellbookIntegrationEnabled( + commanderSpellbookIntegrationEnabledIndexAutomatic); + return true; + } + + return false; +} + +void CommanderBracketWidget::updateBracketVisibility(bool visible) +{ + setVisible(visible); +} + +void CommanderBracketWidget::requestBracketEstimate() +{ + bracketRefreshButton->setEnabled(false); + bracketInfoButton->setEnabled(false); + bracketValueLabel->setText(tr("Calculating…")); + + requestId = CommanderBracketService::instance().estimateBracket(*deck, this); +} + +void CommanderBracketWidget::onEstimateBracketFinished(quint64 id, + QObject *requester, + const CommanderBracketEstimate &result) +{ + if (requester != this || id != requestId) { + return; + } + + BracketExplainer explainer; + lastBracketExplanation = explainer.explain(result.rawResult); + + // Display bracket + bracketValueLabel->setText( + SettingsCache::instance().deckEditor().getCommanderSpellbookIntegrationUseOfficialBracketNames() + ? result.officialName + : result.displayName); + bracketRefreshButton->setEnabled(true); + + // Build tooltip + QString tooltip; + for (const auto §ion : lastBracketExplanation.sections) { + tooltip += "" + section.title + "
"; + for (const auto &line : section.bulletPoints) { + tooltip += "• " + line + "
"; + } + tooltip += "
"; + } + + bracketInfoButton->setToolTip(tooltip); + bracketInfoButton->setEnabled(!tooltip.isEmpty()); +} + +void CommanderBracketWidget::onEstimateBracketError(quint64 id, QObject *requester, const QString & /*error*/) +{ + if (requester != this || id != requestId) { + return; + } + + bracketValueLabel->setText("-"); + bracketRefreshButton->setEnabled(true); + bracketInfoButton->setToolTip({}); + bracketInfoButton->setEnabled(false); +} + +void CommanderBracketWidget::maybeAutoEstimateBracket() +{ + const QString formatKey = deck->getGameFormat(); + + const bool isCommander = (formatKey.compare("commander", Qt::CaseInsensitive) == 0); + + int mode = SettingsCache::instance().deckEditor().getCommanderSpellbookIntegrationEnabled(); + + if (!isCommander || mode == commanderSpellbookIntegrationEnabledIndexDisabled) { + updateBracketVisibility(false); + return; + } + + if (mode == commanderSpellbookIntegrationEnabledIndexUnprompted) { + if (prompting) { + return; + } + prompting = true; + const bool accepted = promptCommanderSpellbookIntegration(); + prompting = false; + if (!accepted) { + updateBracketVisibility(false); + return; + } + } + + updateBracketVisibility(true); + + mode = SettingsCache::instance().deckEditor().getCommanderSpellbookIntegrationEnabled(); + if (mode != commanderSpellbookIntegrationEnabledIndexAutomatic) { + return; + } + + // Avoid firing if we already have a result or a request in flight + if (!bracketRefreshButton->isEnabled()) { + return; + } + + // Defer to avoid races during init / model rebuild + QTimer::singleShot(0, this, &CommanderBracketWidget::requestBracketEstimate); +} + +void CommanderBracketWidget::retranslateUi() +{ + bracketLabel->setText(tr("Bracket:")); + bracketInfoButton->setToolTip(tr("Why this bracket?")); + bracketRefreshButton->setToolTip(tr("Recalculate bracket")); +} diff --git a/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_bracket_widget.h b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_bracket_widget.h new file mode 100644 index 000000000..a45610fcf --- /dev/null +++ b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_bracket_widget.h @@ -0,0 +1,46 @@ +#ifndef COCKATRICE_COMMANDER_BRACKET_WIDGET_H +#define COCKATRICE_COMMANDER_BRACKET_WIDGET_H + +#include "commander_spellbook_bracket_explainer.h" + +#include +#include +#include + +class QLabel; +class QToolButton; +struct CommanderBracketEstimate; + +class CommanderBracketWidget : public QWidget +{ + Q_OBJECT + +public: + explicit CommanderBracketWidget(QWidget *parent = nullptr); + + void setDeck(const QSharedPointer &_deck); + void retranslateUi(); + +private slots: + void requestBracketEstimate(); + void onEstimateBracketFinished(quint64 id, QObject *requester, const CommanderBracketEstimate &result); + void onEstimateBracketError(quint64 id, QObject *requester, const QString &error); + void maybeAutoEstimateBracket(); + +private: + bool promptCommanderSpellbookIntegration(); + void updateBracketVisibility(bool visible); + + QSharedPointer deck; + bool prompting = false; + quint64 requestId = 0; + + QLabel *bracketLabel; + QLabel *bracketValueLabel; + QToolButton *bracketInfoButton; + QToolButton *bracketRefreshButton; + + BracketExplanation lastBracketExplanation; +}; + +#endif // COCKATRICE_COMMANDER_BRACKET_WIDGET_H diff --git a/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_spellbook_api_accessor.cpp b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_spellbook_api_accessor.cpp new file mode 100644 index 000000000..590692d98 --- /dev/null +++ b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_spellbook_api_accessor.cpp @@ -0,0 +1,75 @@ +#include "commander_spellbook_api_accessor.h" + +#include "api_response/commander_spellbook_deck_request.h" + +#include +#include +#include +#include +#include + +static const QUrl ESTIMATE_BRACKET_URL(QStringLiteral("https://backend.commanderspellbook.com/estimate-bracket")); + +CommanderSpellbookApiAccessor &CommanderSpellbookApiAccessor::instance() +{ + static CommanderSpellbookApiAccessor instance; + return instance; +} + +CommanderSpellbookApiAccessor::CommanderSpellbookApiAccessor(QObject *parent) : QObject(parent) +{ +} + +CommanderSpellbookApiAccessor::RequestId CommanderSpellbookApiAccessor::estimateBracket(const DeckList &deck, + QObject *requester) +{ + CommanderSpellbookDeckRequest deckRequest = CommanderSpellbookDeckRequest::fromDeckList(deck); + + QJsonDocument doc(deckRequest.toJson()); + QByteArray body = doc.toJson(QJsonDocument::Compact); + + QNetworkRequest req(ESTIMATE_BRACKET_URL); + req.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + req.setHeader(QNetworkRequest::UserAgentHeader, QString("Cockatrice %1").arg(VERSION_STRING)); + + QNetworkReply *reply = network.post(req, body); + + const RequestId id = nextRequestId++; + + reply->setProperty("requestId", QVariant::fromValue(id)); + reply->setProperty("requester", QVariant::fromValue(requester)); + + connect(reply, &QNetworkReply::finished, this, [this, reply]() { onEstimateReplyFinished(reply); }); + + return id; +} + +void CommanderSpellbookApiAccessor::onEstimateReplyFinished(QNetworkReply *reply) +{ + reply->deleteLater(); + + const RequestId id = reply->property("requestId").toULongLong(); + QObject *requester = reply->property("requester").value(); + + if (!requester) { + // Requester died — silently drop + return; + } + + if (reply->error() != QNetworkReply::NoError) { + emit estimateBracketError(id, requester, reply->errorString()); + return; + } + + QJsonParseError err; + QJsonDocument doc = QJsonDocument::fromJson(reply->readAll(), &err); + + if (err.error != QJsonParseError::NoError || !doc.isObject()) { + emit estimateBracketError(id, requester, QStringLiteral("Invalid JSON response")); + return; + } + + EstimateBracketResult result = EstimateBracketResult::fromJson(doc.object()); + + emit estimateBracketFinished(id, requester, result); +} diff --git a/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_spellbook_api_accessor.h b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_spellbook_api_accessor.h new file mode 100644 index 000000000..682a48524 --- /dev/null +++ b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_spellbook_api_accessor.h @@ -0,0 +1,37 @@ +#ifndef COCKATRICE_COMMANDER_SPELLBOOK_API_ACCESSOR_H +#define COCKATRICE_COMMANDER_SPELLBOOK_API_ACCESSOR_H + +#include "api_response/commander_spellbook_estimate_bracket_result.h" + +#include +#include +#include +#include + +class CommanderSpellbookApiAccessor final : public QObject +{ + Q_OBJECT + +public: + static CommanderSpellbookApiAccessor &instance(); + + using RequestId = quint64; + + RequestId estimateBracket(const DeckList &deck, QObject *requester); + +signals: + void estimateBracketFinished(RequestId id, QObject *requester, const EstimateBracketResult &result); + + void estimateBracketError(RequestId id, QObject *requester, const QString &errorMessage); + +private: + explicit CommanderSpellbookApiAccessor(QObject *parent = nullptr); + Q_DISABLE_COPY_MOVE(CommanderSpellbookApiAccessor) + + void onEstimateReplyFinished(QNetworkReply *reply); + + QNetworkAccessManager network; + RequestId nextRequestId = 1; +}; + +#endif // COCKATRICE_COMMANDER_SPELLBOOK_API_ACCESSOR_H diff --git a/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_spellbook_bracket_explainer.cpp b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_spellbook_bracket_explainer.cpp new file mode 100644 index 000000000..fc308ed60 --- /dev/null +++ b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_spellbook_bracket_explainer.cpp @@ -0,0 +1,120 @@ +#include "commander_spellbook_bracket_explainer.h" + +static QString cardList(const QList &cards, int max = 5) +{ + QStringList names; + for (int i = 0; i < cards.size() && i < max; ++i) { + names << cards[i].name; + } + + if (cards.size() > max) { + names << QString("and %1 more").arg(cards.size() - max); + } + + return names.join(", "); +} + +static QString comboCount(const QList &variants) +{ + return QString::number(variants.size()); +} + +BracketExplanation BracketExplainer::explain(const EstimateBracketResult &r) +{ + BracketExplanation out; + out.bracket = r.bracketTag; + + if (!r.gameChangerCards.isEmpty()) { + BracketExplanationSection s; + s.title = "Game-changing cards"; + + s.bulletPoints << QString("Your deck contains %1 game-changing cards, such as %2.") + .arg(r.gameChangerCards.size()) + .arg(cardList(r.gameChangerCards)); + + out.sections << s; + } + + if (!r.extraTurnCards.isEmpty() || !r.extraTurnTemplates.isEmpty() || !r.extraTurnCombos.isEmpty()) { + + BracketExplanationSection s; + s.title = "Extra turns"; + + if (!r.extraTurnCards.isEmpty()) { + s.bulletPoints << QString("The deck contains %1 extra-turn cards (%2).") + .arg(r.extraTurnCards.size()) + .arg(cardList(r.extraTurnCards)); + } + + if (!r.extraTurnTemplates.isEmpty()) { + s.bulletPoints << QString("%1 extra-turn templates were identified.").arg(comboCount(r.extraTurnTemplates)); + } + + if (!r.extraTurnCombos.isEmpty()) { + s.bulletPoints + << QString("%1 extra-turn combo variants were identified.").arg(comboCount(r.extraTurnCombos)); + } + + out.sections << s; + } + + if (!r.massLandDenialCards.isEmpty() || !r.massLandDenialTemplates.isEmpty() || !r.massLandDenialCombos.isEmpty()) { + + BracketExplanationSection s; + s.title = "Mass land denial"; + + if (!r.massLandDenialCards.isEmpty()) { + s.bulletPoints << QString("The deck contains %1 mass land denial cards (%2).") + .arg(r.massLandDenialCards.size()) + .arg(cardList(r.massLandDenialCards)); + } + + if (!r.massLandDenialTemplates.isEmpty()) { + s.bulletPoints + << QString("%1 mass land denial templates were identified.").arg(comboCount(r.massLandDenialTemplates)); + } + + if (!r.massLandDenialCombos.isEmpty()) { + s.bulletPoints << QString("%1 mass land denial combo variants were identified.") + .arg(comboCount(r.massLandDenialCombos)); + } + + out.sections << s; + } + + if (!r.lockCombos.isEmpty() || !r.skipTurnsCombos.isEmpty()) { + + BracketExplanationSection s; + s.title = "Lock pieces"; + + if (!r.lockCombos.isEmpty()) { + s.bulletPoints << QString("%1 lock combo variants were detected.").arg(comboCount(r.lockCombos)); + } + + if (!r.skipTurnsCombos.isEmpty()) { + s.bulletPoints << QString("%1 skip-turn combo variants were detected.").arg(comboCount(r.skipTurnsCombos)); + } + + out.sections << s; + } + + if (!r.definitelyTwoCardCombos.isEmpty() || !r.arguablyTwoCardCombos.isEmpty()) { + + BracketExplanationSection s; + s.title = "Two-card combos"; + + if (!r.definitelyTwoCardCombos.isEmpty()) { + s.bulletPoints << QString("%1 definite two-card combo variants were identified.") + .arg(comboCount(r.definitelyTwoCardCombos)); + } + + if (!r.arguablyTwoCardCombos.isEmpty()) { + s.bulletPoints << QString("%1 arguable two-card combo variants were identified.") + .arg(comboCount(r.arguablyTwoCardCombos)); + } + + out.sections << s; + } + + return out; +} diff --git a/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_spellbook_bracket_explainer.h b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_spellbook_bracket_explainer.h new file mode 100644 index 000000000..9ed499742 --- /dev/null +++ b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_spellbook_bracket_explainer.h @@ -0,0 +1,43 @@ +#ifndef COCKATRICE_COMMANDER_SPELLBOOK_BRACKET_EXPLAINER_H +#define COCKATRICE_COMMANDER_SPELLBOOK_BRACKET_EXPLAINER_H +#include "api_response/commander_spellbook_estimate_bracket_result.h" + +namespace CommanderBracketNames +{ +inline const char *CommanderSpellbookBracketNames = QT_TR_NOOP("CommanderSpellbook"); +inline const char *OfficialCommanderBracketNames = QT_TR_NOOP("Official (approximate)"); +inline const char *Explainer = QT_TR_NOOP( + "The bracket system combines both objective data, as well as subjective play experience to estimate a " + "bracket for a deck.\nCommanderSpellbook's estimation is algorithmical, which means that it can only operate " + "on the objective data, not the subjective intent. \nThey have chosen to represent this by defining their " + "own bracket system which matches their algorithm.\n" + "This custom bracket system maps loosely to the standard system. \nYou may choose to use these mapped " + "standardized names if these are more familiar to you, however, you should keep in mind that these are just " + "rough estimations.\n\nAlways consider the subjective factors of the bracket system when determing a deck's " + "final bracket!"); +} // namespace CommanderBracketNames + +struct BracketExplanationSection +{ + QString title; + QStringList bulletPoints; +}; + +struct BracketExplanation +{ + QString bracket; + QList sections; + + bool isEmpty() const + { + return sections.isEmpty(); + } +}; + +class BracketExplainer +{ +public: + static BracketExplanation explain(const EstimateBracketResult &result); +}; + +#endif // COCKATRICE_COMMANDER_SPELLBOOK_BRACKET_EXPLAINER_H diff --git a/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/handle_commander_brackets.cpp b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/handle_commander_brackets.cpp new file mode 100644 index 000000000..90b490efe --- /dev/null +++ b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/handle_commander_brackets.cpp @@ -0,0 +1,62 @@ +#include "handle_commander_brackets.h" + +#include "../../../../../client/settings/cache_settings.h" + +#include +#include +#include +#include + +static const QUrl COMMANDER_BRACKET_JSON_URL(QStringLiteral("https://cockatrice.github.io/commander-brackets.json")); + +HandleCommanderBrackets::HandleCommanderBrackets(QObject *parent) + : QObject(parent), nam(new QNetworkAccessManager(this)), reply(nullptr) +{ +} + +void HandleCommanderBrackets::downloadBracketDefinitions() +{ + if (reply) { + return; + } + + reply = nam->get(QNetworkRequest(COMMANDER_BRACKET_JSON_URL)); + + connect(reply, &QNetworkReply::finished, this, &HandleCommanderBrackets::actFinishParsingDownloadedData); +} + +void HandleCommanderBrackets::actFinishParsingDownloadedData() +{ + reply = qobject_cast(sender()); + + if (reply->error() != QNetworkReply::NoError) { + emit sigBracketDefinitionsDownloadFailed(reply->error()); + + reply->deleteLater(); + return; + } + + QJsonParseError parseError; + + auto document = QJsonDocument::fromJson(reply->readAll(), &parseError); + + if (parseError.error != QJsonParseError::NoError) { + emit sigBracketDefinitionsDownloadFailed(QNetworkReply::UnknownContentError); + + reply->deleteLater(); + return; + } + + updateBracketDefinitions(document.toVariant().toMap()); + + emit sigBracketDefinitionsDownloaded(); + + reply->deleteLater(); +} + +void HandleCommanderBrackets::updateBracketDefinitions(const QVariantMap &jsonMap) +{ + const auto bracketList = jsonMap.value("brackets").toList(); + SettingsCache::instance().commanderBrackets().saveDefinitions(bracketList); + SettingsCache::instance().commanderBrackets().reloadDefinitions(bracketList); +} diff --git a/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/handle_commander_brackets.h b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/handle_commander_brackets.h new file mode 100644 index 000000000..b8348b13c --- /dev/null +++ b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/handle_commander_brackets.h @@ -0,0 +1,31 @@ +#ifndef COCKATRICE_HANDLE_COMMANDER_BRACKETS_H +#define COCKATRICE_HANDLE_COMMANDER_BRACKETS_H + +#include +#include +#include + +class HandleCommanderBrackets : public QObject +{ + Q_OBJECT + +public: + explicit HandleCommanderBrackets(QObject *parent = nullptr); + + void downloadBracketDefinitions(); + +signals: + void sigBracketDefinitionsDownloaded(); + void sigBracketDefinitionsDownloadFailed(QNetworkReply::NetworkError error); + +private slots: + void actFinishParsingDownloadedData(); + +private: + void updateBracketDefinitions(const QVariantMap &jsonMap); + + QNetworkAccessManager *nam; + QNetworkReply *reply; +}; + +#endif // COCKATRICE_HANDLE_COMMANDER_BRACKETS_H diff --git a/cockatrice/src/interface/widgets/tabs/api/edhrec/display/cards/edhrec_api_response_card_details_display_widget.h b/cockatrice/src/interface/widgets/tabs/api/edhrec/display/cards/edhrec_api_response_card_details_display_widget.h index 2c81e155a..b78f56ec8 100644 --- a/cockatrice/src/interface/widgets/tabs/api/edhrec/display/cards/edhrec_api_response_card_details_display_widget.h +++ b/cockatrice/src/interface/widgets/tabs/api/edhrec/display/cards/edhrec_api_response_card_details_display_widget.h @@ -38,8 +38,7 @@ private: protected slots: void mousePressEvent(QMouseEvent *event) override; - - void enterEvent(QEnterEvent *event) override; + void enterEvent(QEnterEvent *event) override; ///< Hover enter void leaveEvent(QEvent *event) override; }; diff --git a/cockatrice/src/interface/widgets/tabs/api/edhrec/tab_edhrec_main.cpp b/cockatrice/src/interface/widgets/tabs/api/edhrec/tab_edhrec_main.cpp index b833f3369..42a689898 100644 --- a/cockatrice/src/interface/widgets/tabs/api/edhrec/tab_edhrec_main.cpp +++ b/cockatrice/src/interface/widgets/tabs/api/edhrec/tab_edhrec_main.cpp @@ -25,7 +25,7 @@ #include #include #include -#include +#include #include static bool canBeCommander(const CardInfoPtr &cardInfo) @@ -95,10 +95,9 @@ TabEdhRecMain::TabEdhRecMain(TabSupervisor *_tabSupervisor) : Tab(_tabSupervisor settingsButton = new SettingsButtonWidget(this); - cardSizeSlider = - new CardSizeWidget(this, nullptr, SettingsCache::instance().visualDeckStorage().getEDHRecCardSize()); - connect(cardSizeSlider, &CardSizeWidget::cardSizeSettingUpdated, &SettingsCache::instance().visualDeckStorage(), - &VisualDeckStorageSettings::setEDHRecCardSize); + cardSizeSlider = new CardSizeWidget(this, nullptr, SettingsCache::instance().cardsDisplay().getEDHRecCardSize()); + connect(cardSizeSlider, &CardSizeWidget::cardSizeSettingUpdated, &SettingsCache::instance().cardsDisplay(), + &CardsDisplaySettings::setEDHRecCardSize); settingsButton->addSettingsWidget(cardSizeSlider); diff --git a/cockatrice/src/interface/widgets/tabs/tab_game.cpp b/cockatrice/src/interface/widgets/tabs/tab_game.cpp index c9ebf06c8..7ffcd8a9b 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_game.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_game.cpp @@ -258,16 +258,13 @@ void TabGame::resetChatAndPhase() void TabGame::emitUserEvent() { bool globalEvent = !game->getPlayerManager()->isSpectator() || - SettingsCache::instance().interface().getSpectatorNotificationsEnabled(); + SettingsCache::instance().userInterface().getSpectatorNotificationsEnabled(); emit userEvent(globalEvent); updatePlayerListDockTitle(); } TabGame::~TabGame() { - if (replayWidget) { - delete replayWidget->replay; - } for (auto &player : game->getPlayerManager()->getPlayers()) { player->clear(); } @@ -1183,6 +1180,7 @@ void TabGame::createReplayDock(GameReplay *replay) replayDock->setWidget(replayWidget); replayDock->setFloating(false); + connect(replayWidget, &ReplayWidget::rewound, this, &TabGame::resetChatAndPhase); connect(replayWidget, &ReplayWidget::eventReplayed, game->getGameEventHandler(), [this](const auto &event, auto options) { game->getGameEventHandler()->processGameEventContainer(event, nullptr, options); diff --git a/cockatrice/src/interface/widgets/tabs/tab_room.cpp b/cockatrice/src/interface/widgets/tabs/tab_room.cpp index 5cf400099..5705c184e 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_room.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_room.cpp @@ -242,7 +242,7 @@ void TabRoom::actClearChat() void TabRoom::actOpenChatSettings() { DlgSettings settings(this); - settings.setTab(4); + settings.setTab(DlgSettings::MessagesPage); settings.exec(); } @@ -280,6 +280,7 @@ void TabRoom::processListGamesEvent(const Event_ListGames &event) for (int i = 0; i < gameListSize; ++i) { gameSelector->processGameInfo(event.game_list(i)); } + emit gameListUpdated(); } void TabRoom::processJoinRoomEvent(const Event_JoinRoom &event) diff --git a/cockatrice/src/interface/widgets/tabs/tab_room.h b/cockatrice/src/interface/widgets/tabs/tab_room.h index d669b6107..2881c25f4 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_room.h +++ b/cockatrice/src/interface/widgets/tabs/tab_room.h @@ -78,6 +78,7 @@ signals: void openMessageDialog(const QString &userName, bool focus); void maximizeClient(); void notIdle(); + void gameListUpdated(); private slots: void sendMessage(); void sayFinished(const Response &response); @@ -127,6 +128,10 @@ public: { return ownUser; } + [[nodiscard]] GameSelector *getGameSelector() const + { + return gameSelector; + } PendingCommand *prepareRoomCommand(const ::google::protobuf::Message &cmd); void sendRoomCommand(PendingCommand *pend); diff --git a/cockatrice/src/interface/widgets/tabs/tab_server.cpp b/cockatrice/src/interface/widgets/tabs/tab_server.cpp index 2fce5c1fa..13a77e957 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_server.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_server.cpp @@ -191,7 +191,10 @@ void TabServer::joinRoom(int id, bool setCurrent) PendingCommand *pend = client->prepareSessionCommand(cmd); pend->setExtraData(setCurrent); - connect(pend, &PendingCommand::finished, this, &TabServer::joinRoomFinished); + connect(pend, &PendingCommand::finished, this, + [this, id](const Response &r, const CommandContainer &c, const QVariant &v) { + joinRoomFinished(r, c, v, id); + }); client->sendCommand(pend); @@ -205,7 +208,8 @@ void TabServer::joinRoom(int id, bool setCurrent) void TabServer::joinRoomFinished(const Response &r, const CommandContainer & /*commandContainer*/, - const QVariant &extraData) + const QVariant &extraData, + int roomId) { switch (r.response_code()) { case Response::RespOk: @@ -213,21 +217,25 @@ void TabServer::joinRoomFinished(const Response &r, case Response::RespNameNotFound: QMessageBox::critical(this, tr("Error"), tr("Failed to join the server room: it doesn't exist on the server.")); + emit roomJoinFailed(roomId); return; case Response::RespContextError: QMessageBox::critical( this, tr("Error"), tr("The server thinks you are in the server room but your client is unable to display it. " "Try restarting your client.")); + emit roomJoinFailed(roomId); return; case Response::RespUserLevelTooLow: QMessageBox::critical(this, tr("Error"), tr("You do not have the required permission to join this server room.")); + emit roomJoinFailed(roomId); return; default: QMessageBox::critical( this, tr("Error"), tr("Failed to join the server room due to an unknown error: %1.").arg(r.response_code())); + emit roomJoinFailed(roomId); return; } diff --git a/cockatrice/src/interface/widgets/tabs/tab_server.h b/cockatrice/src/interface/widgets/tabs/tab_server.h index 137823592..c10b7945b 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_server.h +++ b/cockatrice/src/interface/widgets/tabs/tab_server.h @@ -49,10 +49,13 @@ class TabServer : public Tab Q_OBJECT signals: void roomJoined(const ServerInfo_Room &info, bool setCurrent); + void roomJoinFailed(int roomId); private slots: void processServerMessageEvent(const Event_ServerMessage &event); - void joinRoom(int id, bool setCurrent); - void joinRoomFinished(const Response &resp, const CommandContainer &commandContainer, const QVariant &extraData); + void joinRoomFinished(const Response &resp, + const CommandContainer &commandContainer, + const QVariant &extraData, + int roomId); private: AbstractClient *client; @@ -62,6 +65,7 @@ private: public: TabServer(TabSupervisor *_tabSupervisor, AbstractClient *_client); + void joinRoom(int id, bool setCurrent); void retranslateUi() override; [[nodiscard]] QString getTabText() const override { diff --git a/cockatrice/src/interface/widgets/tabs/tab_supervisor.cpp b/cockatrice/src/interface/widgets/tabs/tab_supervisor.cpp index 460495efe..d8f2e7935 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_supervisor.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_supervisor.cpp @@ -39,9 +39,21 @@ #include #include #include +#include #include #include -#include + +QRect MacOSTabFixStyle::subElementRect(SubElement element, const QStyleOption *option, const QWidget *widget) const +{ + if (element != SE_TabBarTabText) { + return QProxyStyle::subElementRect(element, option, widget); + } + + // Skip over QProxyStyle handling subElementRect, + // This fixes an issue on OSX where the labels for tabs with a button and an icon + // get cut-off too early + return QCommonStyle::subElementRect(element, option, widget); +} CloseButton::CloseButton(QWidget *parent) : QAbstractButton(parent) { @@ -106,6 +118,12 @@ TabSupervisor::TabSupervisor(AbstractClient *_client, QMenu *tabsMenu, QWidget * setMovable(true); setIconSize(QSize(15, 15)); +#if defined(Q_OS_MAC) + // This is necessary to fix an issue on macOS, + // where tabs with icons and buttons get drawn incorrectly + tabBar()->setStyle(new MacOSTabFixStyle); +#endif + userListManager = new UserListManager(client, this); // connect tab changes @@ -565,6 +583,11 @@ void TabSupervisor::actTabServer(bool checked) void TabSupervisor::openTabServer() { + SettingsCache::instance().tabs().setTabServerOpen(true); + if (tabServer) { + return; + } + tabServer = new TabServer(this, client); connect(tabServer, &TabServer::roomJoined, this, &TabSupervisor::addRoomTab); myAddTab(tabServer, aTabServer); @@ -888,7 +911,7 @@ void TabSupervisor::talkLeft(TabMessage *tab) */ void TabSupervisor::openDeckInNewTab(const LoadedDeck &deckToOpen) { - int type = SettingsCache::instance().visualDeckStorage().getDefaultDeckEditorType(); + int type = SettingsCache::instance().deckEditor().getDefaultDeckEditorType(); switch (type) { case ClassicDeckEditor: addDeckEditorTab(deckToOpen); @@ -987,7 +1010,7 @@ void TabSupervisor::tabUserEvent(bool globalEvent) tab->setContentsChanged(true); setTabIcon(indexOf(tab), QPixmap("theme:icons/tab_changed")); } - if (globalEvent && SettingsCache::instance().interface().getNotificationsEnabled()) { + if (globalEvent && SettingsCache::instance().userInterface().getNotificationsEnabled()) { QApplication::alert(this); } } @@ -1082,7 +1105,7 @@ void TabSupervisor::processUserJoined(const ServerInfo_User &userInfoJoined) } } - if (SettingsCache::instance().interface().getBuddyConnectNotificationsEnabled()) { + if (SettingsCache::instance().userInterface().getBuddyConnectNotificationsEnabled()) { QApplication::alert(this); this->actShowPopup(tr("Your buddy %1 has signed on!").arg(userName)); } diff --git a/cockatrice/src/interface/widgets/tabs/tab_supervisor.h b/cockatrice/src/interface/widgets/tabs/tab_supervisor.h index d645bc7d7..5d349e18c 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_supervisor.h +++ b/cockatrice/src/interface/widgets/tabs/tab_supervisor.h @@ -140,6 +140,10 @@ public: { return userListManager; } + [[nodiscard]] TabServer *getTabServer() const + { + return tabServer; + } [[nodiscard]] const QMap &getRoomTabs() const { return roomTabs; @@ -171,6 +175,7 @@ public slots: void maximizeMainWindow(); void actTabVisualDeckStorage(bool checked); void actTabReplays(bool checked); + void openTabServer(); private slots: void refreshShortcuts(); @@ -183,7 +188,6 @@ private slots: void openTabVisualDeckStorage(); void openTabHome(); - void openTabServer(); void openTabAccount(); void openTabDeckStorage(); void openTabReplays(); diff --git a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_widget.cpp b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_widget.cpp index 21c44246e..dc98e6940 100644 --- a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_widget.cpp +++ b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_widget.cpp @@ -20,7 +20,7 @@ #include #include #include -#include +#include #include VisualDatabaseDisplayWidget::VisualDatabaseDisplayWidget(QWidget *parent, @@ -52,10 +52,10 @@ VisualDatabaseDisplayWidget::VisualDatabaseDisplayWidget(QWidget *parent, mainLayout->setContentsMargins(0, 0, 0, 0); flowWidget = new FlowWidget(this, Qt::Horizontal, Qt::ScrollBarAlwaysOff, Qt::ScrollBarPolicy::ScrollBarAsNeeded); - cardSizeWidget = new CardSizeWidget( - this, flowWidget, SettingsCache::instance().visualDeckStorage().getVisualDatabaseDisplayCardSize()); - connect(cardSizeWidget, &CardSizeWidget::cardSizeSettingUpdated, &SettingsCache::instance().visualDeckStorage(), - &VisualDeckStorageSettings::setVisualDatabaseDisplayCardSize); + cardSizeWidget = new CardSizeWidget(this, flowWidget, + SettingsCache::instance().cardsDisplay().getVisualDatabaseDisplayCardSize()); + connect(cardSizeWidget, &CardSizeWidget::cardSizeSettingUpdated, &SettingsCache::instance().cardsDisplay(), + &CardsDisplaySettings::setVisualDatabaseDisplayCardSize); searchContainer = new FlowWidget(this, Qt::Horizontal, Qt::ScrollBarAlwaysOff, Qt::ScrollBarAlwaysOff); diff --git a/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_editor_sample_hand_widget.cpp b/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_editor_sample_hand_widget.cpp index 20ad3b65c..a4268563a 100644 --- a/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_editor_sample_hand_widget.cpp +++ b/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_editor_sample_hand_widget.cpp @@ -8,7 +8,7 @@ #include #include -#include +#include #include VisualDeckEditorSampleHandWidget::VisualDeckEditorSampleHandWidget(QWidget *parent, @@ -34,10 +34,10 @@ VisualDeckEditorSampleHandWidget::VisualDeckEditorSampleHandWidget(QWidget *pare resetAndHandSizeLayout->addWidget(resetButton); handSizeSpinBox = new QSpinBox(this); - handSizeSpinBox->setValue(SettingsCache::instance().visualDeckStorage().getVisualDeckEditorSampleHandSize()); + handSizeSpinBox->setValue(SettingsCache::instance().cardsDisplay().getSampleHandSize()); handSizeSpinBox->setMinimum(1); - connect(handSizeSpinBox, qOverload(&QSpinBox::valueChanged), &SettingsCache::instance().visualDeckStorage(), - &VisualDeckStorageSettings::setVisualDeckEditorSampleHandSize); + connect(handSizeSpinBox, qOverload(&QSpinBox::valueChanged), &SettingsCache::instance().cardsDisplay(), + &CardsDisplaySettings::setSampleHandSize); connect(handSizeSpinBox, qOverload(&QSpinBox::valueChanged), this, &VisualDeckEditorSampleHandWidget::updateDisplay); resetAndHandSizeLayout->addWidget(handSizeSpinBox); diff --git a/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_editor_widget.cpp b/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_editor_widget.cpp index befd57804..e5bcb2fd3 100644 --- a/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_editor_widget.cpp +++ b/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_editor_widget.cpp @@ -25,7 +25,7 @@ #include #include #include -#include +#include #include VisualDeckEditorWidget::VisualDeckEditorWidget(QWidget *parent, @@ -45,9 +45,9 @@ VisualDeckEditorWidget::VisualDeckEditorWidget(QWidget *parent, initializeScrollAreaAndZoneContainer(); cardSizeWidget = - new CardSizeWidget(this, nullptr, SettingsCache::instance().visualDeckStorage().getVisualDeckEditorCardSize()); - connect(cardSizeWidget, &CardSizeWidget::cardSizeSettingUpdated, &SettingsCache::instance().visualDeckStorage(), - &VisualDeckStorageSettings::setVisualDeckEditorCardSize); + new CardSizeWidget(this, nullptr, SettingsCache::instance().cardsDisplay().getVisualDeckEditorCardSize()); + connect(cardSizeWidget, &CardSizeWidget::cardSizeSettingUpdated, &SettingsCache::instance().cardsDisplay(), + &CardsDisplaySettings::setVisualDeckEditorCardSize); mainLayout->addWidget(displayOptionsAndSearch); mainLayout->addWidget(scrollArea); diff --git a/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_color_identity_filter_widget.cpp b/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_color_identity_filter_widget.cpp index 575632724..f1dcf113f 100644 --- a/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_color_identity_filter_widget.cpp +++ b/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_color_identity_filter_widget.cpp @@ -28,11 +28,10 @@ DeckPreviewColorIdentityFilterWidget::DeckPreviewColorIdentityFilterWidget(Visua } toggleButton = new QPushButton(this); - toggleButton->setCheckable(true); layout->addWidget(toggleButton); - // Connect the button's toggled signal - connect(toggleButton, &QPushButton::toggled, this, &DeckPreviewColorIdentityFilterWidget::updateFilterMode); + // Connect the button's clicked signal + connect(toggleButton, &QPushButton::clicked, this, &DeckPreviewColorIdentityFilterWidget::updateFilterMode); connect(this, &DeckPreviewColorIdentityFilterWidget::activeColorsChanged, parent, &VisualDeckStorageWidget::updateColorFilter); connect(this, &DeckPreviewColorIdentityFilterWidget::filterModeChanged, parent, @@ -45,7 +44,17 @@ DeckPreviewColorIdentityFilterWidget::DeckPreviewColorIdentityFilterWidget(Visua void DeckPreviewColorIdentityFilterWidget::retranslateUi() { // Set the toggle button text based on the current mode - toggleButton->setText(exactMatchMode ? tr("Mode: Exact Match") : tr("Mode: Includes")); + switch (filterMode) { + case ExactMatch: + toggleButton->setText(tr("Mode: Exact Match")); + break; + case Includes: + toggleButton->setText(tr("Mode: Includes")); + break; + case Excludes: + toggleButton->setText(tr("Mode: Excludes")); + break; + } toggleButton->setToolTip(tr("Color identity filter mode (AND/OR/NOT conjunctions of filters)")); } @@ -55,11 +64,23 @@ void DeckPreviewColorIdentityFilterWidget::handleColorToggled(QChar color, bool emit activeColorsChanged(); } -void DeckPreviewColorIdentityFilterWidget::updateFilterMode(bool checked) +void DeckPreviewColorIdentityFilterWidget::updateFilterMode() { - exactMatchMode = checked; // Toggle between modes - retranslateUi(); // Update the button text - emit filterModeChanged(exactMatchMode); + // Cycle through the modes + switch (filterMode) { + case ExactMatch: + filterMode = Includes; + break; + case Includes: + filterMode = Excludes; + break; + case Excludes: + filterMode = ExactMatch; + break; + } + + retranslateUi(); // Update the button text + emit filterModeChanged(filterMode); } void DeckPreviewColorIdentityFilterWidget::filterWidgets(QList widgets) @@ -78,41 +99,55 @@ void DeckPreviewColorIdentityFilterWidget::filterWidgets(QListfilteredByColor = false; } + return; } for (const auto &widget : widgets) { QString colorIdentity = widget->getColorIdentity(); bool matchesFilter = true; - if (exactMatchMode) { - // Exact match mode: active colors must exactly match colorIdentity + switch (filterMode) { + case ExactMatch: { + // Exact match mode: active colors must exactly match colorIdentity - // Create a set of active colors - QSet activeColorSet; - for (auto it = activeColors.constBegin(); it != activeColors.constEnd(); ++it) { - if (it.value()) { - activeColorSet.insert(it.key().toUpper()); // Use uppercase for uniformity + // Create a set of active colors + QSet activeColorSet; + for (auto it = activeColors.constBegin(); it != activeColors.constEnd(); ++it) { + if (it.value()) { + activeColorSet.insert(it.key().toUpper()); // Use uppercase for uniformity + } } - } - // Create a set of colors from the color identity string - QSet colorIdentitySet; - for (const QChar &color : colorIdentity) { - colorIdentitySet.insert(color.toUpper()); // Ensure case uniformity - } + // Create a set of colors from the color identity string + QSet colorIdentitySet; + for (const QChar &color : colorIdentity) { + colorIdentitySet.insert(color.toUpper()); // Ensure case uniformity + } - // Compare the sets: the sets must match exactly - if (activeColorSet != colorIdentitySet) { - matchesFilter = false; - } - } else { - // Includes mode: colorIdentity must contain all active colors - for (auto it = activeColors.constBegin(); it != activeColors.constEnd(); ++it) { - if (it.value() && !colorIdentity.contains(it.key())) { + // Compare the sets: the sets must match exactly + if (activeColorSet != colorIdentitySet) { matchesFilter = false; - break; } + break; } + case Includes: + // Includes mode: colorIdentity must contain all active colors + for (auto it = activeColors.constBegin(); it != activeColors.constEnd(); ++it) { + if (it.value() && !colorIdentity.contains(it.key())) { + matchesFilter = false; + break; + } + } + break; + case Excludes: + // Excludes mode: colorIdentity must contain none of the active colors + for (auto it = activeColors.constBegin(); it != activeColors.constEnd(); ++it) { + if (it.value() && colorIdentity.contains(it.key())) { + matchesFilter = false; + break; + } + } + break; } widget->filteredByColor = !matchesFilter; diff --git a/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_color_identity_filter_widget.h b/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_color_identity_filter_widget.h index 0207e2ee2..8e60b16fb 100644 --- a/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_color_identity_filter_widget.h +++ b/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_color_identity_filter_widget.h @@ -21,23 +21,34 @@ class DeckPreviewColorIdentityFilterWidget : public QWidget Q_OBJECT public: + /** + * How the active colors are matched against a deck's color identity. + */ + enum FilterMode + { + ExactMatch, ///< The color identity consists of exactly the active colors. + Includes, ///< The color identity contains all of the active colors. + Excludes ///< The color identity contains none of the active colors. + }; + Q_ENUM(FilterMode) + explicit DeckPreviewColorIdentityFilterWidget(VisualDeckStorageWidget *parent); void retranslateUi(); void filterWidgets(QList widgets); signals: - void filterModeChanged(bool exactMatchMode); + void filterModeChanged(FilterMode mode); void activeColorsChanged(); private slots: void handleColorToggled(QChar color, bool active); - void updateFilterMode(bool checked); + void updateFilterMode(); private: QHBoxLayout *layout; QPushButton *toggleButton; QMap activeColors; - bool exactMatchMode = false; // Default to "includes" mode + FilterMode filterMode = Includes; // Default to "includes" mode }; #endif // DECK_PREVIEW_COLOR_IDENTITY_FILTER_WIDGET_H diff --git a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_quick_settings_widget.cpp b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_quick_settings_widget.cpp index b89c62f9c..cce3ff6ce 100644 --- a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_quick_settings_widget.cpp +++ b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_quick_settings_widget.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include @@ -115,11 +116,11 @@ VisualDeckStorageQuickSettingsWidget::VisualDeckStorageQuickSettingsWidget(QWidg // card size slider cardSizeWidget = - new CardSizeWidget(this, nullptr, SettingsCache::instance().visualDeckStorage().getVisualDeckStorageCardSize()); + new CardSizeWidget(this, nullptr, SettingsCache::instance().cardsDisplay().getVisualDeckStorageCardSize()); connect(cardSizeWidget->getSlider(), &QSlider::valueChanged, this, &VisualDeckStorageQuickSettingsWidget::cardSizeChanged); - connect(cardSizeWidget, &CardSizeWidget::cardSizeSettingUpdated, &SettingsCache::instance().visualDeckStorage(), - &VisualDeckStorageSettings::setVisualDeckStorageCardSize); + connect(cardSizeWidget, &CardSizeWidget::cardSizeSettingUpdated, &SettingsCache::instance().cardsDisplay(), + &CardsDisplaySettings::setVisualDeckStorageCardSize); // putting everything together this->addSettingsWidget(showFoldersCheckBox); diff --git a/cockatrice/src/interface/window_main.cpp b/cockatrice/src/interface/window_main.cpp index 21d847e63..44e188760 100644 --- a/cockatrice/src/interface/window_main.cpp +++ b/cockatrice/src/interface/window_main.cpp @@ -38,6 +38,7 @@ #include "version_string.h" #include "widgets/dialogs/dlg_connect.h" #include "widgets/server/handle_public_servers.h" +#include "widgets/tabs/api/commander_spellbook/handle_commander_brackets.h" #include "widgets/utility/get_text_with_max.h" #include @@ -70,7 +71,9 @@ #include #include #include +#include #include +#include #include #include #include @@ -391,9 +394,9 @@ void MainWindow::createActions() connect(aCheckCardUpdatesBackground, &QAction::triggered, this, &MainWindow::actCheckCardUpdatesBackground); aStatusBar = new QAction(this); aStatusBar->setCheckable(true); - aStatusBar->setChecked(SettingsCache::instance().personal().getShowStatusBar()); - connect(aStatusBar, &QAction::triggered, &SettingsCache::instance().personal(), - &PersonalSettings::setShowStatusBar); + aStatusBar->setChecked(SettingsCache::instance().userInterface().getShowStatusBar()); + connect(aStatusBar, &QAction::triggered, &SettingsCache::instance().userInterface(), + &InterfaceSettings::setShowStatusBar); aViewLog = new QAction(this); connect(aViewLog, &QAction::triggered, this, &MainWindow::actViewLog); aOpenSettingsFolder = new QAction(this); @@ -518,9 +521,9 @@ MainWindow::MainWindow(QWidget *parent) } // status bar - connect(&SettingsCache::instance().personal(), &PersonalSettings::showStatusBarChanged, this, + connect(&SettingsCache::instance().userInterface(), &InterfaceSettings::showStatusBarChanged, this, [this](bool show) { statusBar()->setVisible(show); }); - statusBar()->setVisible(SettingsCache::instance().personal().getShowStatusBar()); + statusBar()->setVisible(SettingsCache::instance().userInterface().getShowStatusBar()); connect(&SettingsCache::instance().shortcuts(), &ShortcutsSettings::shortCutChanged, this, &MainWindow::refreshShortcuts); @@ -557,23 +560,25 @@ void MainWindow::startupConfigCheck() actCheckClientUpdates(); } - if (SettingsCache::instance().personal().getClientVersion() == CLIENT_INFO_NOT_SET) { + actCheckCommanderBracketDefinitionUpdates(); + + if (SettingsCache::instance().network().getClientVersion() == CLIENT_INFO_NOT_SET) { // no config found, 99% new clean install qCInfo(WindowMainStartupVersionLog) << "Startup: old client version empty, assuming first start after clean install"; alertForcedOracleRun(VERSION_STRING, false); SettingsCache::instance().downloads().resetToDefaultURLs(); // populate the download urls - SettingsCache::instance().personal().setClientVersion(VERSION_STRING); + SettingsCache::instance().network().setClientVersion(VERSION_STRING); if (QString(VERSION_STRING).contains("custom", Qt::CaseInsensitive)) { SettingsCache::instance().updates().setCheckUpdatesOnStartup(false); } else if (QString(VERSION_STRING).contains("beta", Qt::CaseInsensitive)) { SettingsCache::instance().updates().setUpdateReleaseChannelIndex(1); } - } else if (SettingsCache::instance().personal().getClientVersion() != VERSION_STRING) { + } else if (SettingsCache::instance().network().getClientVersion() != VERSION_STRING) { // config found, from another (presumably older) version qCInfo(WindowMainStartupVersionLog) - << "Startup: old client version" << SettingsCache::instance().personal().getClientVersion() + << "Startup: old client version" << SettingsCache::instance().network().getClientVersion() << "differs, assuming first start after update"; if (SettingsCache::instance().updates().getNotifyAboutNewVersion()) { alertForcedOracleRun(VERSION_STRING, true); @@ -598,7 +603,7 @@ void MainWindow::startupConfigCheck() } } - SettingsCache::instance().personal().setClientVersion(VERSION_STRING); + SettingsCache::instance().network().setClientVersion(VERSION_STRING); } else { // previous config from this version found qCInfo(WindowMainStartupVersionLog) << "Startup: found config with current version"; @@ -659,6 +664,7 @@ void MainWindow::alertForcedOracleRun(const QString &version, bool isUpdate) actCheckCardUpdates(); actCheckServerUpdates(); + actCheckCommanderBracketDefinitionUpdates(); } MainWindow::~MainWindow() @@ -1003,6 +1009,16 @@ void MainWindow::checkClientUpdatesFinished(bool needToUpdate, bool /* isCompati } } +void MainWindow::actCheckCommanderBracketDefinitionUpdates() +{ + auto *handler = new HandleCommanderBrackets(this); + + connect(handler, &HandleCommanderBrackets::sigBracketDefinitionsDownloaded, this, + []() { qDebug() << "Bracket definitions loaded"; }); + + handler->downloadBracketDefinitions(); +} + void MainWindow::refreshShortcuts() { ShortcutsSettings &shortcuts = SettingsCache::instance().shortcuts(); diff --git a/cockatrice/src/interface/window_main.h b/cockatrice/src/interface/window_main.h index 5f631ddc3..fa6c79915 100644 --- a/cockatrice/src/interface/window_main.h +++ b/cockatrice/src/interface/window_main.h @@ -92,6 +92,7 @@ private slots: void cardDatabaseAllNewSetsEnabled(); void checkClientUpdatesFinished(bool needToUpdate, bool isCompatible, Release *release); + void actCheckCommanderBracketDefinitionUpdates(); void actOpenCustomFolder(); void actOpenCustomsetsFolder(); @@ -150,6 +151,11 @@ public: } ~MainWindow() override; + RemoteClient *getRemoteClient() const + { + return connectionController->client(); + } + TabSupervisor *getTabSupervisor() const { return tabSupervisor; diff --git a/cockatrice/src/main.cpp b/cockatrice/src/main.cpp index b14292581..814da9808 100644 --- a/cockatrice/src/main.cpp +++ b/cockatrice/src/main.cpp @@ -23,12 +23,17 @@ #include "client/network/update/card_spoiler/spoiler_background_updater.h" #include "client/settings/cache_settings.h" #include "client/sound_engine.h" +#include "client/url_scheme_event_filter.h" #include "database/interface/settings_card_preference_provider.h" +#include "interface/intents/intent_open_local_deck.h" +#include "interface/intents/url_parser.h" #include "interface/logger.h" #include "interface/pixel_map_generator.h" #include "interface/theme_manager.h" #include "interface/widgets/dialogs/dlg_settings.h" +#include "interface/widgets/tabs/tab_supervisor.h" #include "interface/window_main.h" +#include "single_instance_manager.h" #include "version_string.h" #include @@ -37,12 +42,16 @@ #include #include #include +#include #include #include #include #include +#include #include #include +#include +#include #include QTranslator *translator, *qtTranslator; @@ -173,6 +182,7 @@ int main(int argc, char *argv[]) SetUnhandledExceptionFilter(CockatriceUnhandledExceptionFilter); #endif + // Logging setup #ifdef Q_OS_APPLE // /cockatrice/cockatrice.app/Contents/MacOS/cockatrice const QByteArray configPath = "../../../qtlogging.ini"; @@ -190,15 +200,29 @@ int main(int argc, char *argv[]) // Set the QT_LOGGING_CONF environment variable qputenv("QT_LOGGING_CONF", configPath); } + qSetMessagePattern( "\033[0m[%{time yyyy-MM-dd h:mm:ss.zzz} " "%{if-debug}\033[36mD%{endif}%{if-info}\033[32mI%{endif}%{if-warning}\033[33mW%{endif}%{if-critical}\033[31mC%{" "endif}%{if-fatal}\033[1;31mF%{endif}\033[0m] [%{function}] - %{message} [%{file}:%{line}]"); QApplication app(argc, argv); +#ifdef Q_OS_MAC + UrlSchemeEventFilter cockatriceFilter(QStringList{QStringLiteral("cockatrice")}); + + QStringList pendingMacUrls; + + const auto cocoaBufferConn = + QObject::connect(&cockatriceFilter, &UrlSchemeEventFilter::urlReceived, + [&pendingMacUrls](const QString &url) { pendingMacUrls.append(url); }); + + app.installEventFilter(&cockatriceFilter); +#endif + QObject::connect(&app, &QApplication::lastWindowClosed, &app, &QApplication::quit); qInstallMessageHandler(CockatriceLogger); + #ifdef Q_OS_WIN app.addLibraryPath(app.applicationDirPath() + "/plugins"); #endif @@ -214,6 +238,7 @@ int main(int argc, char *argv[]) qApp->setAttribute(Qt::AA_DontShowIconsInMenus, true); #endif + // Translations #ifdef Q_OS_MAC translationPath = qApp->applicationDirPath() + "/../Resources/translations"; #elif defined(Q_OS_WIN) @@ -222,6 +247,7 @@ int main(int argc, char *argv[]) translationPath = qApp->applicationDirPath() + "/../share/cockatrice/translations"; #endif + // Command-line parser QCommandLineParser parser; parser.setApplicationDescription("Cockatrice"); parser.addHelpOption(); @@ -237,6 +263,35 @@ int main(int argc, char *argv[]) Logger::getInstance().logToFile(true); } + // --- Handle files or URLs passed at startup --- + // Only positional arguments are treated as files/URLs, so options like + // --connect are never handed off to another instance. + const QStringList startupFiles = parser.positionalArguments(); + const bool hasActivationFiles = !startupFiles.isEmpty(); + + SingleInstanceManager instance; + + if (hasActivationFiles) { + // Activation launch: hand off to the primary instance if one is + // running, otherwise become the primary ourselves. Do this before + // constructing the main window so a hand-off exits cheaply. + if (!instance.tryRun(startupFiles)) { + // Sent successfully → exit + return 0; + } + // No primary instance → become server + qInfo() << "No existing instance found, becoming primary instance"; + } else { + // Plain launch: if another instance is running, run independently + // instead of handing off and exiting. + if (!instance.tryRun(QStringList())) { + // Another instance is already running → just run independently + qInfo() << "Another instance exists, running independently"; + } else { + qInfo() << "No existing instance found, starting server"; + } + } + rng = new RNG_SFMT; themeManager = new ThemeManager; soundEngine = new SoundEngine; @@ -268,6 +323,26 @@ int main(int argc, char *argv[]) CardDatabaseManager::getInstance()->loadCardDatabases(); MainWindow ui; + + auto handleActivation = [&ui](const QString &file) { + if (file.startsWith("cockatrice://")) { + auto urlParser = new IntentUrlParser(&ui, &ui); + urlParser->handle(file); + } else if (QFileInfo(file).exists()) { + auto openDeckIntent = new IntentOpenLocalDeck(ui.getTabSupervisor(), file); + QObject::connect(openDeckIntent, &Intent::failed, &ui, [&ui](const QString &reason) { + QMessageBox::warning(&ui, QObject::tr("Open deck"), reason); + }); + openDeckIntent->execute(); + } + }; + +#ifdef Q_OS_MAC + QObject::disconnect(cocoaBufferConn); + + QObject::connect(&cockatriceFilter, &UrlSchemeEventFilter::urlReceived, + [&handleActivation](const QString &url) { handleActivation(url); }); +#endif if (parser.isSet("connect")) { ui.setConnectTo(parser.value("connect")); } @@ -277,7 +352,7 @@ int main(int argc, char *argv[]) // set name of the app desktop file; used by wayland to load the window icon QGuiApplication::setDesktopFileName("cockatrice"); - SettingsCache::instance().personal().setClientID(generateClientID()); + SettingsCache::instance().network().setClientID(generateClientID()); // If spoiler mode is enabled, we will download the spoilers // then reload the DB. otherwise just reload the DB @@ -288,9 +363,27 @@ int main(int argc, char *argv[]) // force shortcuts to be shown/hidden in right-click menus, regardless of system defaults qApp->setAttribute(Qt::AA_DontShowShortcutsInContextMenus, - !SettingsCache::instance().cardsDisplay().getShowShortcuts()); + !SettingsCache::instance().userInterface().getShowShortcuts()); - app.exec(); +#ifdef Q_OS_MAC + for (const QString &url : pendingMacUrls) { + handleActivation(url); + } + pendingMacUrls.clear(); +#endif + + for (const QString &file : startupFiles) { + handleActivation(file); + } + + // Connect to future file/URL events from other instances + QObject::connect(&instance, &SingleInstanceManager::filesReceived, [&handleActivation](const QStringList &files) { + for (const QString &file : files) { + handleActivation(file); + } + }); + + int ret = app.exec(); qCInfo(MainLog) << "Event loop finished, terminating..."; delete rng; @@ -298,5 +391,5 @@ int main(int argc, char *argv[]) CountryPixmapGenerator::clear(); UserLevelPixmapGenerator::clear(); - return 0; + return ret; } diff --git a/cockatrice/src/single_instance_manager.cpp b/cockatrice/src/single_instance_manager.cpp new file mode 100644 index 000000000..aca23160c --- /dev/null +++ b/cockatrice/src/single_instance_manager.cpp @@ -0,0 +1,126 @@ +#include "single_instance_manager.h" + +#include + +SingleInstanceManager::SingleInstanceManager(QObject *parent) : QObject(parent) +{ +} + +bool SingleInstanceManager::tryRun(const QStringList &filesToSend) +{ + // Scope the socket name to the current user. On Linux the default abstract + // namespace is system-wide, so a plain name would let one user's instance + // hijack another user's session. + QString userName = qEnvironmentVariable("USER"); + if (userName.isEmpty()) { + userName = qEnvironmentVariable("USERNAME"); + } + if (userName.isEmpty()) { + userName = QDir::home().dirName(); + } + serverName = QStringLiteral("CockatriceSingleInstance-%1").arg(userName); + + // Hand off to an already-running primary instance if one exists. + if (forwardToPrimary(filesToSend)) { + return false; + } + + // No primary instance is currently reachable, so become the primary. + server = new QLocalServer(this); + connect(server, &QLocalServer::newConnection, this, &SingleInstanceManager::handleNewConnection); + + if (server->listen(serverName)) { + return true; + } + + // Another instance may have started while we were probing; hand off to it + // instead of stealing its socket. + if (forwardToPrimary(filesToSend)) { + return false; + } + + // The socket is stale (left over by a crashed instance): remove it and + // retry. If that still fails, another instance just took the name. + QLocalServer::removeServer(serverName); + if (server->listen(serverName)) { + return true; + } + + forwardToPrimary(filesToSend); + return false; +} + +bool SingleInstanceManager::forwardToPrimary(const QStringList &filesToSend) +{ + QLocalSocket socket; + socket.connectToServer(serverName); + if (!socket.waitForConnected(200)) { + return false; + } + + // Serialize payload with length prefix + QByteArray payload; + QDataStream out(&payload, QIODevice::WriteOnly); + out << filesToSend; + + QByteArray message; + QDataStream msgStream(&message, QIODevice::WriteOnly); + msgStream << quint32(payload.size()); + message.append(payload); + + socket.write(message); + socket.flush(); + socket.waitForBytesWritten(1000); + + return true; +} + +void SingleInstanceManager::handleNewConnection() +{ + QLocalSocket *socket = server->nextPendingConnection(); + + // Per-connection state. QSharedPointer keeps the buffers alive for as long + // as the connection handler is attached to the socket. + auto buffer = QSharedPointer::create(); + auto expectedSize = QSharedPointer::create(0); + + connect(socket, &QLocalSocket::readyRead, this, [this, socket, buffer, expectedSize]() { + buffer->append(socket->readAll()); + + QDataStream stream(buffer.data(), QIODevice::ReadOnly); + + while (true) { + // Step 1: read size + if (*expectedSize == 0) { + if (buffer->size() < static_cast(sizeof(quint32))) { + return; + } + + stream >> *expectedSize; + } + + // Step 2: wait for full payload + if (buffer->size() < static_cast(sizeof(quint32) + *expectedSize)) { + return; + } + + // Step 3: extract payload + QByteArray payload = buffer->mid(sizeof(quint32), *expectedSize); + + QDataStream payloadStream(&payload, QIODevice::ReadOnly); + QStringList files; + payloadStream >> files; + + emit filesReceived(files); + + // Reset buffer (single message use-case) + buffer->clear(); + *expectedSize = 0; + + socket->disconnectFromServer(); + return; + } + }); + + connect(socket, &QLocalSocket::disconnected, socket, &QLocalSocket::deleteLater); +} diff --git a/cockatrice/src/single_instance_manager.h b/cockatrice/src/single_instance_manager.h new file mode 100644 index 000000000..55bff0e80 --- /dev/null +++ b/cockatrice/src/single_instance_manager.h @@ -0,0 +1,32 @@ +#ifndef COCKATRICE_SINGLE_INSTANCE_MANAGER_H +#define COCKATRICE_SINGLE_INSTANCE_MANAGER_H + +#include +#include +#include +#include + +class SingleInstanceManager : public QObject +{ + Q_OBJECT +public: + explicit SingleInstanceManager(QObject *parent = nullptr); + + // Returns true if this process became the primary instance, false if + // another instance is already running (and received our files). + bool tryRun(const QStringList &initialFiles); + +signals: + void filesReceived(const QStringList &files); + +private slots: + void handleNewConnection(); + +private: + bool forwardToPrimary(const QStringList &filesToSend); + + QString serverName; + QLocalServer *server = nullptr; +}; + +#endif // COCKATRICE_SINGLE_INSTANCE_MANAGER_H diff --git a/libcockatrice_card/CMakeLists.txt b/libcockatrice_card/CMakeLists.txt index b756add5b..7d3d47eea 100644 --- a/libcockatrice_card/CMakeLists.txt +++ b/libcockatrice_card/CMakeLists.txt @@ -19,9 +19,7 @@ set(HEADERS libcockatrice/card/relation/card_relation.h ) -if(Qt6_FOUND) - qt6_wrap_cpp(MOC_SOURCES ${HEADERS}) -endif() +qt6_wrap_cpp(MOC_SOURCES ${HEADERS}) add_library( libcockatrice_card STATIC diff --git a/libcockatrice_card/libcockatrice/card/card_info.cpp b/libcockatrice_card/libcockatrice/card/card_info.cpp index d37408a58..f03503550 100644 --- a/libcockatrice_card/libcockatrice/card/card_info.cpp +++ b/libcockatrice_card/libcockatrice/card/card_info.cpp @@ -12,7 +12,6 @@ #include #include #include -#include #include #include @@ -24,7 +23,7 @@ using CardInfoPtr = QSharedPointer; namespace { -QByteArray serializeProperties(const QVariantHash &props) +QByteArray serializeProperties(const QHash &props) { QByteArray blob; QDataStream out(&blob, QIODevice::WriteOnly); @@ -48,7 +47,7 @@ void CardInfo::ensurePropertiesLoaded() const propertiesLoaded = true; } -const QVariantHash &CardInfo::getPropertiesHash() const +const QHash &CardInfo::getPropertiesHash() const { ensurePropertiesLoaded(); return propertiesCache; @@ -57,7 +56,7 @@ const QVariantHash &CardInfo::getPropertiesHash() const void CardInfo::setProperty(const QString &_name, const QString &_value) { ensurePropertiesLoaded(); - if (propertiesCache.value(_name).toString() == _value) { + if (propertiesCache.value(_name) == _value) { return; } propertiesCache.insert(_name, _value); @@ -65,18 +64,10 @@ void CardInfo::setProperty(const QString &_name, const QString &_value) emit cardInfoChanged(smartThis); } -void CardInfo::setProperties(const QVariantHash &_props) -{ - ensurePropertiesLoaded(); - propertiesCache = _props; - propertiesBlob = serializeProperties(propertiesCache); - emit cardInfoChanged(smartThis); -} - CardInfo::CardInfo(const QString &_name, const QString &_text, bool _isToken, - QVariantHash _properties, + QHash _properties, const QList &_relatedCards, const QList &_reverseRelatedCards, SetToPrintingsMap _sets, @@ -122,7 +113,7 @@ CardInfoPtr CardInfo::newInstance(const QString &_name) CardInfoPtr CardInfo::newInstance(const QString &_name, const QString &_text, bool _isToken, - QVariantHash _properties, + QHash _properties, const QList &_relatedCards, const QList &_reverseRelatedCards, SetToPrintingsMap _sets, @@ -210,10 +201,10 @@ void CardInfo::addToSet(const CardSetPtr &_set, const PrintingInfo &_info) refreshCachedSets(); } -void CardInfo::combineLegalities(const QVariantHash &props) +void CardInfo::combineLegalities(const QHash &props) { ensurePropertiesLoaded(); - QHashIterator it(props); + QHashIterator it(props); while (it.hasNext()) { it.next(); if (it.key().startsWith("format-")) { diff --git a/libcockatrice_card/libcockatrice/card/card_info.h b/libcockatrice_card/libcockatrice/card/card_info.h index ad99864c8..a5c208893 100644 --- a/libcockatrice_card/libcockatrice/card/card_info.h +++ b/libcockatrice_card/libcockatrice/card/card_info.h @@ -76,12 +76,12 @@ private: QString text; ///< Text description or rules text of the card. bool isToken; ///< Whether this card is a token or not. // Properties are stored as a pre-serialized blob (cheap to load) and the - // QVariantHash is materialized on first query, so database load avoids - // constructing thousands of QVariants per card. - mutable QByteArray propertiesBlob; ///< Serialized properties (load form). - mutable QVariantHash propertiesCache; ///< Materialized properties (query form). - mutable bool propertiesLoaded = false; ///< Whether propertiesCache is valid. - mutable QMutex propertiesMutex; ///< Guards lazy materialization. + // QHash is materialized on first query, so database load avoids + // constructing thousands of QStrings per card. + mutable QByteArray propertiesBlob; ///< Serialized properties (load form). + mutable QHash propertiesCache; ///< Materialized properties (query form). + mutable bool propertiesLoaded = false; ///< Whether propertiesCache is valid. + mutable QMutex propertiesMutex; ///< Guards lazy materialization. /** * @brief Materializes propertiesCache from propertiesBlob if not already done. @@ -114,7 +114,7 @@ public: explicit CardInfo(const QString &_name, const QString &_text, bool _isToken, - QVariantHash _properties, + QHash _properties, const QList &_relatedCards, const QList &_reverseRelatedCards, SetToPrintingsMap _sets, @@ -127,7 +127,7 @@ public: * Used by the binary cache reader to skip recomputing @p _simpleName and * @p _altNames (which otherwise require a Unicode normalization and a full * printing scan). Properties are supplied as a pre-serialized @p _propertiesBlob - * so the QVariantHash is not built at load time (it is materialized on first + * so the QHash is not built at load time (it is materialized on first * query). * * @param _name The card name. @@ -194,7 +194,7 @@ public: static CardInfoPtr newInstance(const QString &_name, const QString &_text, bool _isToken, - QVariantHash _properties, + QHash _properties, const QList &_relatedCards, const QList &_reverseRelatedCards, SetToPrintingsMap _sets, @@ -288,27 +288,13 @@ public: { return getPropertiesHash().keys(); } - [[nodiscard]] const QVariantHash &getPropertiesHash() const; + [[nodiscard]] const QHash &getPropertiesHash() const; - /** - * @brief Stores the pre-serialized properties blob and invalidates the - * materialized cache. Used by the binary cache reader so the - * QVariantHash is not built at load time. - * @param _blob The serialized properties (as written by the cache writer). - */ - void setPropertiesBlob(QByteArray _blob) const - { - QMutexLocker lock(&propertiesMutex); - propertiesBlob = std::move(_blob); - propertiesLoaded = false; - propertiesCache.clear(); - } [[nodiscard]] QString getProperty(const QString &propertyName) const { - return getPropertiesHash().value(propertyName).toString(); + return getPropertiesHash().value(propertyName); } void setProperty(const QString &_name, const QString &_value); - void setProperties(const QVariantHash &_props); [[nodiscard]] bool hasProperty(const QString &propertyName) const { return getPropertiesHash().contains(propertyName); @@ -415,7 +401,7 @@ public: * * @param props Key-value mapping of format legalities. */ - void combineLegalities(const QVariantHash &props); + void combineLegalities(const QHash &props); /** * @brief Refreshes all cached fields that are calculated from the contained sets and printings. diff --git a/libcockatrice_card/libcockatrice/card/card_info_comparator.cpp b/libcockatrice_card/libcockatrice/card/card_info_comparator.cpp index 4133b7de0..cc0314e21 100644 --- a/libcockatrice_card/libcockatrice/card/card_info_comparator.cpp +++ b/libcockatrice_card/libcockatrice/card/card_info_comparator.cpp @@ -57,7 +57,7 @@ QVariant CardInfoComparator::getProperty(const CardInfoPtr &card, const QString return card->getIsToken(); } - // Otherwise, check if it's a custom property in the QVariantHash + // Otherwise, check if it's a custom property in the properties hash if (card->hasProperty(property)) { return card->getProperty(property); } diff --git a/libcockatrice_card/libcockatrice/card/database/card_database_cache.cpp b/libcockatrice_card/libcockatrice/card/database/card_database_cache.cpp index 0b68a389a..3985889b7 100644 --- a/libcockatrice_card/libcockatrice/card/database/card_database_cache.cpp +++ b/libcockatrice_card/libcockatrice/card/database/card_database_cache.cpp @@ -13,12 +13,11 @@ #include #include #include -#include namespace { constexpr quint32 CACHE_MAGIC = 0x43445243; // "CDRC" -constexpr quint32 CACHE_VERSION = 1; +constexpr quint32 CACHE_VERSION = 2; // ---- Primitives ----------------------------------------------------------- @@ -34,11 +33,11 @@ QString readString(QDataStream &in) return s; } -// Stores a QVariantHash as a single pre-serialized blob. The reader keeps the -// blob as-is and materializes the QVariantHash lazily on first query, which is +// Stores a QHash as a single pre-serialized blob. The reader keeps the +// blob as-is and materializes the QHash lazily on first query, which is // what removes the allocation storm from database load (see card_info.cpp / // printing_info.cpp). -void writeHashBlob(QDataStream &out, const QVariantHash &h) +void writeHashBlob(QDataStream &out, const QHash &h) { QByteArray blob; QDataStream blobOut(&blob, QIODevice::WriteOnly); @@ -118,12 +117,9 @@ PrintingInfo readPrinting(QDataStream &in, const SetNameMap &sets) { QString setName = readString(in); QByteArray propsBlob = readHashBlob(in); - PrintingInfo p; - if (auto set = sets.value(setName)) { - p = PrintingInfo(set); - } - p.setPropertiesBlob(propsBlob); - return p; + auto set = sets.value(setName); + + return PrintingInfo(set, propsBlob); } // ---- CardSet --------------------------------------------------------------- diff --git a/libcockatrice_card/libcockatrice/card/database/parser/cockatrice_xml_3.cpp b/libcockatrice_card/libcockatrice/card/database/parser/cockatrice_xml_3.cpp index 2dd5e91e9..f3aac7809 100644 --- a/libcockatrice_card/libcockatrice/card/database/parser/cockatrice_xml_3.cpp +++ b/libcockatrice_card/libcockatrice/card/database/parser/cockatrice_xml_3.cpp @@ -172,7 +172,7 @@ void CockatriceXml3Parser::loadCardsFromXml(QXmlStreamReader &xml) if (xmlName == "card") { QString name = QString(""); QString text = QString(""); - QVariantHash properties = QVariantHash(); + QHash properties; QString colors = QString(""); QList relatedCards, reverseRelatedCards; auto _sets = SetToPrintingsMap(); @@ -228,8 +228,7 @@ void CockatriceXml3Parser::loadCardsFromXml(QXmlStreamReader &xml) // Only load printings from sets the user has enabled, matching the v4 loader's // behaviour. Without this check, disabling a set has no effect on v3 databases. if (set->getEnabled()) { - PrintingInfo setInfo(set); - QVariantHash printingProps; + QHash printingProps; if (attrs.hasAttribute("muId")) { printingProps.insert("muid", attrs.value("muId").toString()); } @@ -249,7 +248,7 @@ void CockatriceXml3Parser::loadCardsFromXml(QXmlStreamReader &xml) if (attrs.hasAttribute("rarity")) { printingProps.insert("rarity", attrs.value("rarity").toString()); } - setInfo.setProperties(printingProps); + PrintingInfo setInfo(set, printingProps); _sets[setName].append(setInfo); } // related cards diff --git a/libcockatrice_card/libcockatrice/card/database/parser/cockatrice_xml_4.cpp b/libcockatrice_card/libcockatrice/card/database/parser/cockatrice_xml_4.cpp index 129bae9bc..8649bbfaf 100644 --- a/libcockatrice_card/libcockatrice/card/database/parser/cockatrice_xml_4.cpp +++ b/libcockatrice_card/libcockatrice/card/database/parser/cockatrice_xml_4.cpp @@ -243,9 +243,9 @@ void CockatriceXml4Parser::loadSetsFromXml(QXmlStreamReader &xml) } } -QVariantHash CockatriceXml4Parser::loadCardPropertiesFromXml(QXmlStreamReader &xml) +QHash CockatriceXml4Parser::loadCardPropertiesFromXml(QXmlStreamReader &xml) { - QVariantHash properties = QVariantHash(); + QHash properties; while (!xml.atEnd()) { if (xml.readNext() == QXmlStreamReader::EndElement) { break; @@ -272,7 +272,7 @@ void CockatriceXml4Parser::loadCardsFromXml(QXmlStreamReader &xml) if (xmlName == "card") { QString name = QString(""); QString text = QString(""); - QVariantHash properties = QVariantHash(); + QHash properties; QList relatedCards, reverseRelatedCards; auto _sets = SetToPrintingsMap(); int tableRow = 0; @@ -314,8 +314,7 @@ void CockatriceXml4Parser::loadCardsFromXml(QXmlStreamReader &xml) QString setName = xml.readElementText(QXmlStreamReader::IncludeChildElements); auto set = internalAddSet(setName); if (set->getEnabled()) { - PrintingInfo printingInfo(set); - QVariantHash printingProps; + QHash printingProps; for (QXmlStreamAttribute attr : attrs) { QString attrName = attr.name().toString(); if (attrName == "picURL") { @@ -323,7 +322,7 @@ void CockatriceXml4Parser::loadCardsFromXml(QXmlStreamReader &xml) } printingProps.insert(attrName, attr.value().toString()); } - printingInfo.setProperties(printingProps); + PrintingInfo printingInfo(set, printingProps); // This is very much a hack and not the right place to // put this check, as it requires a reload of Cockatrice diff --git a/libcockatrice_card/libcockatrice/card/database/parser/cockatrice_xml_4.h b/libcockatrice_card/libcockatrice/card/database/parser/cockatrice_xml_4.h index 92c967a0c..55f7c5a3b 100644 --- a/libcockatrice_card/libcockatrice/card/database/parser/cockatrice_xml_4.h +++ b/libcockatrice_card/libcockatrice/card/database/parser/cockatrice_xml_4.h @@ -19,7 +19,7 @@ inline Q_LOGGING_CATEGORY(CockatriceXml4Log, "cockatrice_xml.xml_4_parser"); * making the parser more extensible and schema-compliant. * * @note Differences from v3: - * - Card properties are stored in blocks as a QVariantHash. + * - Card properties are stored in blocks as a QHash. * - Sets can include a element. * - Supports user preferences via ICardPreferenceProvider (e.g., skipping rebalanced cards). * - Related cards support persistent relations and multiple attach types (e.g., transform). @@ -70,9 +70,9 @@ private: /** * @brief Loads a generic block from a element. * @param xml The open QXmlStreamReader positioned at a element. - * @return A QVariantHash mapping property names to values. + * @return A QHash mapping property names to values. */ - QVariantHash loadCardPropertiesFromXml(QXmlStreamReader &xml); + QHash loadCardPropertiesFromXml(QXmlStreamReader &xml); /** * @brief Load all elements from the XML stream. diff --git a/libcockatrice_card/libcockatrice/card/printing/printing_info.cpp b/libcockatrice_card/libcockatrice/card/printing/printing_info.cpp index 5df3680a0..49086f8b7 100644 --- a/libcockatrice_card/libcockatrice/card/printing/printing_info.cpp +++ b/libcockatrice_card/libcockatrice/card/printing/printing_info.cpp @@ -5,7 +5,12 @@ #include #include -PrintingInfo::PrintingInfo(const CardSetPtr &_set) : set(_set) +PrintingInfo::PrintingInfo(const CardSetPtr &_set, const QHash &_properties) + : set(_set), propertiesCache(_properties), propertiesLoaded(true) +{ +} + +PrintingInfo::PrintingInfo(const CardSetPtr &_set, const QByteArray &_blob) : set(_set), propertiesBlob(_blob) { } @@ -27,20 +32,10 @@ void PrintingInfo::ensurePropertiesLoaded() const void PrintingInfo::setProperty(const QString &_name, const QString &_value) { ensurePropertiesLoaded(); - if (propertiesCache.value(_name).toString() == _value) { + if (propertiesCache.value(_name) == _value) { return; } propertiesCache.insert(_name, _value); - setProperties(propertiesCache); -} - -void PrintingInfo::setProperties(const QVariantHash &_props) -{ - ensurePropertiesLoaded(); - propertiesCache = _props; - QDataStream out(&propertiesBlob, QIODevice::WriteOnly); - out.setVersion(QDataStream::Qt_6_4); - out << propertiesCache; } /** @@ -48,10 +43,10 @@ void PrintingInfo::setProperties(const QVariantHash &_props) */ QString PrintingInfo::getUuid() const { - return getPropertiesHash().value("uuid").toString(); + return getPropertiesHash().value("uuid"); } QString PrintingInfo::getFlavorName() const { - return getPropertiesHash().value("flavorName").toString(); -} \ No newline at end of file + return getPropertiesHash().value("flavorName"); +} diff --git a/libcockatrice_card/libcockatrice/card/printing/printing_info.h b/libcockatrice_card/libcockatrice/card/printing/printing_info.h index adec2010c..70093b686 100644 --- a/libcockatrice_card/libcockatrice/card/printing/printing_info.h +++ b/libcockatrice_card/libcockatrice/card/printing/printing_info.h @@ -32,8 +32,17 @@ public: * @brief Constructs a PrintingInfo associated with a specific set. * * @param _set The set this printing belongs to (defaults to null). + * @param _properties The printing properties (defaults to empty) */ - explicit PrintingInfo(const CardSetPtr &_set = nullptr); + explicit PrintingInfo(const CardSetPtr &_set = nullptr, const QHash &_properties = {}); + + /** + * @brief Constructs a PrintingInfo associated with a specific set. + * + * @param _set The set this printing belongs to (defaults to null). + * @param _blob The serialized properties (as written by the cache writer). + */ + explicit PrintingInfo(const CardSetPtr &_set, const QByteArray &_blob); /** * @brief Destroys the PrintingInfo. @@ -70,11 +79,11 @@ private: CardSetPtr set; ///< The set this variation belongs to. // Properties are stored as a pre-serialized blob (cheap to load) and the - // QVariantHash is materialized on first query. This avoids constructing - // thousands of QVariants per card at database-load time. - mutable QByteArray propertiesBlob; ///< Serialized properties (load form). - mutable QVariantHash propertiesCache; ///< Materialized properties (query form). - mutable bool propertiesLoaded = false; ///< Whether propertiesCache is valid. + // QHash is materialized on first query. This avoids constructing + // thousands of QStrings per card at database-load time. + mutable QByteArray propertiesBlob; ///< Serialized properties (load form). + mutable QHash propertiesCache; ///< Materialized properties (query form). + mutable bool propertiesLoaded = false; ///< Whether propertiesCache is valid. mutable QSharedPointer propertiesMutex = QSharedPointer::create(); ///< Guards lazy materialization. @@ -101,7 +110,7 @@ public: return getPropertiesHash().keys(); } - [[nodiscard]] const QVariantHash &getPropertiesHash() const + [[nodiscard]] const QHash &getPropertiesHash() const { ensurePropertiesLoaded(); return propertiesCache; @@ -115,7 +124,7 @@ public: */ [[nodiscard]] QString getProperty(const QString &propertyName) const { - return getPropertiesHash().value(propertyName).toString(); + return getPropertiesHash().value(propertyName); } /** @@ -127,21 +136,6 @@ public: * @param _value The string value to assign. */ void setProperty(const QString &_name, const QString &_value); - void setProperties(const QVariantHash &_props); - - /** - * @brief Stores the pre-serialized properties blob and marks the materialized - * cache as invalid. Used by the binary cache reader to avoid building a - * QVariantHash at load time. - * @param _blob The serialized properties (as written by the cache writer). - */ - void setPropertiesBlob(QByteArray _blob) const - { - QMutexLocker lock(propertiesMutex.data()); - propertiesBlob = std::move(_blob); - propertiesLoaded = false; - propertiesCache.clear(); - } /** * @brief Returns the providerID for this printing. diff --git a/libcockatrice_deck_list/CMakeLists.txt b/libcockatrice_deck_list/CMakeLists.txt index 20a5185df..33494b9a9 100644 --- a/libcockatrice_deck_list/CMakeLists.txt +++ b/libcockatrice_deck_list/CMakeLists.txt @@ -14,9 +14,7 @@ set(HEADERS libcockatrice/deck_list/sideboard_plan.h ) -if(Qt6_FOUND) - qt6_wrap_cpp(MOC_SOURCES ${HEADERS}) -endif() +qt6_wrap_cpp(MOC_SOURCES ${HEADERS}) add_library( libcockatrice_deck_list STATIC diff --git a/libcockatrice_filters/CMakeLists.txt b/libcockatrice_filters/CMakeLists.txt index 55809d5fd..905533df8 100644 --- a/libcockatrice_filters/CMakeLists.txt +++ b/libcockatrice_filters/CMakeLists.txt @@ -6,9 +6,7 @@ set(HEADERS libcockatrice/filters/filter_card.h libcockatrice/filters/filter_str libcockatrice/filters/filter_tree.h ) -if(Qt6_FOUND) - qt6_wrap_cpp(MOC_SOURCES ${HEADERS}) -endif() +qt6_wrap_cpp(MOC_SOURCES ${HEADERS}) add_library( libcockatrice_filters STATIC ${MOC_SOURCES} libcockatrice/filters/filter_card.cpp diff --git a/libcockatrice_interfaces/CMakeLists.txt b/libcockatrice_interfaces/CMakeLists.txt index d2f81fb66..f606f6207 100644 --- a/libcockatrice_interfaces/CMakeLists.txt +++ b/libcockatrice_interfaces/CMakeLists.txt @@ -9,6 +9,7 @@ set(HEADERS libcockatrice/interfaces/interface_card_set_priority_controller.h libcockatrice/interfaces/interface_cards_display_settings_provider.h libcockatrice/interfaces/interface_chat_settings_provider.h + libcockatrice/interfaces/interface_deck_editor_settings_provider.h libcockatrice/interfaces/interface_game_settings_provider.h libcockatrice/interfaces/interface_interface_settings_provider.h libcockatrice/interfaces/interface_network_settings_provider.h @@ -23,9 +24,7 @@ set(HEADERS libcockatrice/interfaces/noop_card_set_priority_controller.h ) -if(Qt6_FOUND) - qt6_wrap_cpp(MOC_SOURCES ${HEADERS}) -endif() +qt6_wrap_cpp(MOC_SOURCES ${HEADERS}) add_library(libcockatrice_interfaces STATIC ${MOC_SOURCES}) diff --git a/libcockatrice_interfaces/libcockatrice/interfaces/interface_cards_display_settings_provider.h b/libcockatrice_interfaces/libcockatrice/interfaces/interface_cards_display_settings_provider.h index 2ef9dc87d..3ee3d2aef 100644 --- a/libcockatrice_interfaces/libcockatrice/interfaces/interface_cards_display_settings_provider.h +++ b/libcockatrice_interfaces/libcockatrice/interfaces/interface_cards_display_settings_provider.h @@ -14,15 +14,17 @@ public: [[nodiscard]] virtual int getPrintingSelectorCardSize() const = 0; [[nodiscard]] virtual bool getIncludeRebalancedCards() const = 0; [[nodiscard]] virtual bool getPrintingSelectorNavigationButtonsVisible() const = 0; - [[nodiscard]] virtual bool getDeckEditorBannerCardComboBoxVisible() const = 0; - [[nodiscard]] virtual bool getDeckEditorTagsWidgetVisible() const = 0; [[nodiscard]] virtual bool getTapAnimation() const = 0; [[nodiscard]] virtual bool getAutoRotateSidewaysLayoutCards() const = 0; [[nodiscard]] virtual bool getScaleCards() const = 0; [[nodiscard]] virtual int getStackCardOverlapPercent() const = 0; [[nodiscard]] virtual int getCardInfoViewMode() const = 0; - [[nodiscard]] virtual bool getShowShortcuts() const = 0; - [[nodiscard]] virtual bool getShowGameSelectorFilterToolbar() const = 0; + [[nodiscard]] virtual int getVisualDeckStorageCardSize() const = 0; + [[nodiscard]] virtual int getVisualDatabaseDisplayCardSize() const = 0; + [[nodiscard]] virtual int getVisualDeckEditorCardSize() const = 0; + [[nodiscard]] virtual int getEDHRecCardSize() const = 0; + [[nodiscard]] virtual int getArchidektPreviewSize() const = 0; + [[nodiscard]] virtual int getSampleHandSize() const = 0; }; #endif // COCKATRICE_INTERFACE_CARDS_DISPLAY_SETTINGS_PROVIDER_H diff --git a/libcockatrice_interfaces/libcockatrice/interfaces/interface_deck_editor_settings_provider.h b/libcockatrice_interfaces/libcockatrice/interfaces/interface_deck_editor_settings_provider.h new file mode 100644 index 000000000..0ef39d044 --- /dev/null +++ b/libcockatrice_interfaces/libcockatrice/interfaces/interface_deck_editor_settings_provider.h @@ -0,0 +1,15 @@ +#ifndef COCKATRICE_INTERFACE_DECK_EDITOR_SETTINGS_PROVIDER_H +#define COCKATRICE_INTERFACE_DECK_EDITOR_SETTINGS_PROVIDER_H + +class IDeckEditorSettingsProvider +{ +public: + virtual ~IDeckEditorSettingsProvider() = default; + + [[nodiscard]] virtual bool getOpenDeckInNewTab() const = 0; + [[nodiscard]] virtual bool getBannerCardComboBoxVisible() const = 0; + [[nodiscard]] virtual bool getTagsWidgetVisible() const = 0; + [[nodiscard]] virtual int getDefaultDeckEditorType() const = 0; +}; + +#endif // COCKATRICE_INTERFACE_DECK_EDITOR_SETTINGS_PROVIDER_H diff --git a/libcockatrice_interfaces/libcockatrice/interfaces/interface_interface_settings_provider.h b/libcockatrice_interfaces/libcockatrice/interfaces/interface_interface_settings_provider.h index e4c8677c8..ab2caa0d7 100644 --- a/libcockatrice_interfaces/libcockatrice/interfaces/interface_interface_settings_provider.h +++ b/libcockatrice_interfaces/libcockatrice/interfaces/interface_interface_settings_provider.h @@ -29,15 +29,16 @@ public: [[nodiscard]] virtual bool getHorizontalHand() const = 0; [[nodiscard]] virtual bool getInvertVerticalCoordinate() const = 0; [[nodiscard]] virtual int getMinPlayersForMultiColumnLayout() const = 0; - [[nodiscard]] virtual bool getOpenDeckInNewTab() const = 0; [[nodiscard]] virtual int getRewindBufferingMs() const = 0; [[nodiscard]] virtual qreal getFastForwardSpeed() const = 0; - [[nodiscard]] virtual bool getStyleUserList() const = 0; + [[nodiscard]] virtual bool getSkipEmptySections() const = 0; [[nodiscard]] virtual bool getLeftJustified() const = 0; [[nodiscard]] virtual int getZoneViewGroupByIndex() const = 0; [[nodiscard]] virtual int getZoneViewSortByIndex() const = 0; [[nodiscard]] virtual bool getZoneViewPileView() const = 0; - [[nodiscard]] virtual QString getKnownMissingFeatures() = 0; + [[nodiscard]] virtual bool getShowStatusBar() const = 0; + [[nodiscard]] virtual bool getShowShortcuts() const = 0; + [[nodiscard]] virtual bool getShowGameSelectorFilterToolbar() const = 0; }; #endif // COCKATRICE_INTERFACE_INTERFACE_SETTINGS_PROVIDER_H diff --git a/libcockatrice_interfaces/libcockatrice/interfaces/interface_personal_settings_provider.h b/libcockatrice_interfaces/libcockatrice/interfaces/interface_personal_settings_provider.h index 76c8a8367..f3d3b1220 100644 --- a/libcockatrice_interfaces/libcockatrice/interfaces/interface_personal_settings_provider.h +++ b/libcockatrice_interfaces/libcockatrice/interfaces/interface_personal_settings_provider.h @@ -11,20 +11,8 @@ public: virtual ~IPersonalSettingsProvider() = default; [[nodiscard]] virtual QString getLang() const = 0; - [[nodiscard]] virtual QString getClientID() = 0; - [[nodiscard]] virtual QString getClientVersion() = 0; - [[nodiscard]] virtual int getKeepAlive() const = 0; - [[nodiscard]] virtual int getTimeOut() const = 0; - [[nodiscard]] virtual bool getPicDownload() const = 0; - [[nodiscard]] virtual bool getShowStatusBar() const = 0; - [[nodiscard]] virtual int getMaxFontSize() const = 0; - [[nodiscard]] virtual QString getHighlightWords() const = 0; - [[nodiscard]] virtual QString getHomeTabBackgroundSource() const = 0; - [[nodiscard]] virtual int getHomeTabBackgroundShuffleFrequency() const = 0; - [[nodiscard]] virtual bool getHomeTabDisplayCardName() const = 0; [[nodiscard]] virtual bool getShowTipsOnStartup() const = 0; [[nodiscard]] virtual QList getSeenTips() const = 0; - [[nodiscard]] virtual bool getDownloadSpoilersStatus() const = 0; }; #endif // COCKATRICE_INTERFACE_PERSONAL_SETTINGS_PROVIDER_H diff --git a/libcockatrice_interfaces/libcockatrice/interfaces/interface_visual_deck_storage_settings_provider.h b/libcockatrice_interfaces/libcockatrice/interfaces/interface_visual_deck_storage_settings_provider.h index e5e7fe0d3..114b053c6 100644 --- a/libcockatrice_interfaces/libcockatrice/interfaces/interface_visual_deck_storage_settings_provider.h +++ b/libcockatrice_interfaces/libcockatrice/interfaces/interface_visual_deck_storage_settings_provider.h @@ -16,7 +16,6 @@ public: [[nodiscard]] virtual bool getVisualDeckStorageShowColorIdentity() const = 0; [[nodiscard]] virtual bool getVisualDeckStorageShowBannerCardComboBox() const = 0; [[nodiscard]] virtual bool getVisualDeckStorageShowTagsOnDeckPreviews() const = 0; - [[nodiscard]] virtual int getVisualDeckStorageCardSize() const = 0; [[nodiscard]] virtual bool getVisualDeckStorageDrawUnusedColorIdentities() const = 0; [[nodiscard]] virtual int getVisualDeckStorageUnusedColorIdentitiesOpacity() const = 0; [[nodiscard]] virtual int getVisualDeckStorageTooltipType() const = 0; @@ -24,14 +23,8 @@ public: [[nodiscard]] virtual bool getVisualDeckStorageAlwaysConvert() const = 0; [[nodiscard]] virtual bool getVisualDeckStorageInGame() const = 0; [[nodiscard]] virtual bool getVisualDeckStorageSelectionAnimation() const = 0; - [[nodiscard]] virtual int getVisualDeckEditorCardSize() const = 0; - [[nodiscard]] virtual int getVisualDeckEditorSampleHandSize() const = 0; - [[nodiscard]] virtual int getVisualDatabaseDisplayCardSize() const = 0; [[nodiscard]] virtual bool getVisualDatabaseDisplayFilterToMostRecentSetsEnabled() const = 0; [[nodiscard]] virtual int getVisualDatabaseDisplayFilterToMostRecentSetsAmount() const = 0; - [[nodiscard]] virtual int getEDHRecCardSize() const = 0; - [[nodiscard]] virtual int getArchidektPreviewSize() const = 0; - [[nodiscard]] virtual int getDefaultDeckEditorType() const = 0; }; #endif // COCKATRICE_INTERFACE_VISUAL_DECK_STORAGE_SETTINGS_PROVIDER_H diff --git a/libcockatrice_models/libcockatrice/models/database/CMakeLists.txt b/libcockatrice_models/libcockatrice/models/database/CMakeLists.txt index 1f0331690..840f14b08 100644 --- a/libcockatrice_models/libcockatrice/models/database/CMakeLists.txt +++ b/libcockatrice_models/libcockatrice/models/database/CMakeLists.txt @@ -12,9 +12,7 @@ set(HEADERS token/token_edit_model.h ) -if(Qt6_FOUND) - qt6_wrap_cpp(MOC_SOURCES ${HEADERS}) -endif() +qt6_wrap_cpp(MOC_SOURCES ${HEADERS}) add_library( libcockatrice_models_database STATIC diff --git a/libcockatrice_models/libcockatrice/models/deck_list/CMakeLists.txt b/libcockatrice_models/libcockatrice/models/deck_list/CMakeLists.txt index 0c90b8004..d4aee3686 100644 --- a/libcockatrice_models/libcockatrice/models/deck_list/CMakeLists.txt +++ b/libcockatrice_models/libcockatrice/models/deck_list/CMakeLists.txt @@ -4,9 +4,7 @@ set(CMAKE_AUTORCC ON) set(HEADERS deck_list_model.h deck_list_sort_filter_proxy_model.h) -if(Qt6_FOUND) - qt6_wrap_cpp(MOC_SOURCES ${HEADERS}) -endif() +qt6_wrap_cpp(MOC_SOURCES ${HEADERS}) add_library( libcockatrice_models_deck_list STATIC ${MOC_SOURCES} deck_list_model.cpp deck_list_sort_filter_proxy_model.cpp diff --git a/libcockatrice_network/libcockatrice/network/client/abstract/CMakeLists.txt b/libcockatrice_network/libcockatrice/network/client/abstract/CMakeLists.txt index b9f4c8c9c..c4a8e4648 100644 --- a/libcockatrice_network/libcockatrice/network/client/abstract/CMakeLists.txt +++ b/libcockatrice_network/libcockatrice/network/client/abstract/CMakeLists.txt @@ -6,9 +6,7 @@ set(HEADERS abstract_client.h) set(SOURCES abstract_client.cpp) -if(Qt6_FOUND) - qt6_wrap_cpp(MOC_SOURCES ${HEADERS}) -endif() +qt6_wrap_cpp(MOC_SOURCES ${HEADERS}) add_library(libcockatrice_network_client_abstract STATIC ${MOC_SOURCES} ${SOURCES}) diff --git a/libcockatrice_network/libcockatrice/network/client/abstract/abstract_client.h b/libcockatrice_network/libcockatrice/network/client/abstract/abstract_client.h index 2eb7e3356..982aa6bf3 100644 --- a/libcockatrice_network/libcockatrice/network/client/abstract/abstract_client.h +++ b/libcockatrice_network/libcockatrice/network/client/abstract/abstract_client.h @@ -122,6 +122,20 @@ public: return userName; } + /** + * @brief Returns the server address configured for the current connection. + * + * May be empty for clients that have no server counterpart (e.g. local test clients). + */ + virtual QString serverName() const + { + return {}; + } + virtual quint16 serverPort() const + { + return 0; + } + static PendingCommand *prepareSessionCommand(const ::google::protobuf::Message &cmd); static PendingCommand *prepareRoomCommand(const ::google::protobuf::Message &cmd, int roomId); static PendingCommand *prepareModeratorCommand(const ::google::protobuf::Message &cmd); diff --git a/libcockatrice_network/libcockatrice/network/client/local/CMakeLists.txt b/libcockatrice_network/libcockatrice/network/client/local/CMakeLists.txt index 210a09ac3..2ac12e1fe 100644 --- a/libcockatrice_network/libcockatrice/network/client/local/CMakeLists.txt +++ b/libcockatrice_network/libcockatrice/network/client/local/CMakeLists.txt @@ -6,9 +6,7 @@ set(HEADERS local_client.h) set(SOURCES local_client.cpp) -if(Qt6_FOUND) - qt6_wrap_cpp(MOC_SOURCES ${HEADERS}) -endif() +qt6_wrap_cpp(MOC_SOURCES ${HEADERS}) add_library(libcockatrice_network_client_local STATIC ${MOC_SOURCES} ${SOURCES}) diff --git a/libcockatrice_network/libcockatrice/network/client/remote/CMakeLists.txt b/libcockatrice_network/libcockatrice/network/client/remote/CMakeLists.txt index fb164763c..cb68d0c37 100644 --- a/libcockatrice_network/libcockatrice/network/client/remote/CMakeLists.txt +++ b/libcockatrice_network/libcockatrice/network/client/remote/CMakeLists.txt @@ -6,9 +6,7 @@ set(HEADERS remote_client.h) set(SOURCES remote_client.cpp) -if(Qt6_FOUND) - qt6_wrap_cpp(MOC_SOURCES ${HEADERS}) -endif() +qt6_wrap_cpp(MOC_SOURCES ${HEADERS}) add_library(libcockatrice_network_client_remote STATIC ${MOC_SOURCES} ${SOURCES}) diff --git a/libcockatrice_network/libcockatrice/network/client/remote/remote_client.h b/libcockatrice_network/libcockatrice/network/client/remote/remote_client.h index 289fdc5d0..862dac06e 100644 --- a/libcockatrice_network/libcockatrice/network/client/remote/remote_client.h +++ b/libcockatrice_network/libcockatrice/network/client/remote/remote_client.h @@ -131,6 +131,22 @@ public: return socket->peerName(); } } + quint16 peerPort() const + { + if (usingWebSocket) { + return websocket->peerPort(); + } else { + return socket->peerPort(); + } + } + QString serverName() const override + { + return lastHostname; + } + quint16 serverPort() const override + { + return static_cast(lastPort); + } void connectToServer(const QString &hostname, unsigned int port, const QString &_userName, const QString &_password); void registerToServer(const QString &hostname, diff --git a/libcockatrice_network/libcockatrice/network/server/local/CMakeLists.txt b/libcockatrice_network/libcockatrice/network/server/local/CMakeLists.txt index 6e9dfc77b..494ae1294 100644 --- a/libcockatrice_network/libcockatrice/network/server/local/CMakeLists.txt +++ b/libcockatrice_network/libcockatrice/network/server/local/CMakeLists.txt @@ -6,9 +6,7 @@ set(HEADERS local_server.h local_server_interface.h) set(SOURCES local_server.cpp local_server_interface.cpp) -if(Qt6_FOUND) - qt6_wrap_cpp(MOC_SOURCES ${HEADERS}) -endif() +qt6_wrap_cpp(MOC_SOURCES ${HEADERS}) add_library(libcockatrice_network_server_local STATIC ${MOC_SOURCES} ${SOURCES}) diff --git a/libcockatrice_network/libcockatrice/network/server/remote/CMakeLists.txt b/libcockatrice_network/libcockatrice/network/server/remote/CMakeLists.txt index 48a882e9b..9fb63c221 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/CMakeLists.txt +++ b/libcockatrice_network/libcockatrice/network/server/remote/CMakeLists.txt @@ -23,9 +23,7 @@ set(HEADERS serverinfo_user_container.h ) -if(Qt6_FOUND) - qt6_wrap_cpp(MOC_SOURCES ${HEADERS}) -endif() +qt6_wrap_cpp(MOC_SOURCES ${HEADERS}) add_library( libcockatrice_network_server_remote STATIC diff --git a/libcockatrice_network/libcockatrice/network/server/remote/server_protocolhandler.cpp b/libcockatrice_network/libcockatrice/network/server/remote/server_protocolhandler.cpp index c441da781..c3686ddfa 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/server_protocolhandler.cpp +++ b/libcockatrice_network/libcockatrice/network/server/remote/server_protocolhandler.cpp @@ -685,6 +685,15 @@ Response::ResponseCode Server_ProtocolHandler::cmdGetUserInfo(const Command_GetU ServerInfo_User_Container *infoSource = server->findUser(userName); if (!infoSource) { re->mutable_user_info()->CopyFrom(databaseInterface->getUserData(userName, true)); + // The user is not currently online. Mirror the redaction that + // copyUserInfo() applies to online users: the id and email address + // are only ever visible to the account owner, and the client id + // only to moderators. + re->mutable_user_info()->clear_id(); + re->mutable_user_info()->clear_email(); + if (!(userInfo->user_level() & ServerInfo_User::IsModerator)) { + re->mutable_user_info()->clear_clientid(); + } } else { re->mutable_user_info()->CopyFrom( infoSource->copyUserInfo(true, false, userInfo->user_level() & ServerInfo_User::IsModerator)); diff --git a/libcockatrice_rng/CMakeLists.txt b/libcockatrice_rng/CMakeLists.txt index 7b091686a..b988c9d2f 100644 --- a/libcockatrice_rng/CMakeLists.txt +++ b/libcockatrice_rng/CMakeLists.txt @@ -4,9 +4,7 @@ set(CMAKE_AUTORCC ON) set(HEADERS libcockatrice/rng/rng_abstract.h libcockatrice/rng/rng_sfmt.h libcockatrice/rng/sfmt/SFMT.h) -if(Qt6_FOUND) - qt6_wrap_cpp(MOC_SOURCES ${HEADERS}) -endif() +qt6_wrap_cpp(MOC_SOURCES ${HEADERS}) add_library( libcockatrice_rng STATIC ${MOC_SOURCES} libcockatrice/rng/rng_abstract.cpp libcockatrice/rng/rng_sfmt.cpp diff --git a/libcockatrice_settings/CMakeLists.txt b/libcockatrice_settings/CMakeLists.txt index 98c3c80f0..80a465b6d 100644 --- a/libcockatrice_settings/CMakeLists.txt +++ b/libcockatrice_settings/CMakeLists.txt @@ -3,18 +3,22 @@ set(CMAKE_AUTOUIC ON) set(CMAKE_AUTORCC ON) set(HEADERS + libcockatrice/settings/appearance_settings.h libcockatrice/settings/cache_storage_settings.h libcockatrice/settings/card_database_settings.h libcockatrice/settings/card_override_settings.h libcockatrice/settings/cards_display_settings.h libcockatrice/settings/chat_settings.h + libcockatrice/settings/commander_bracket_settings.h libcockatrice/settings/debug_settings.h + libcockatrice/settings/deck_editor_settings.h libcockatrice/settings/download_settings.h libcockatrice/settings/game_filters_settings.h libcockatrice/settings/game_settings.h libcockatrice/settings/interface_settings.h libcockatrice/settings/layouts_settings.h libcockatrice/settings/message_settings.h + libcockatrice/settings/network_settings.h libcockatrice/settings/paths_settings.h libcockatrice/settings/personal_settings.h libcockatrice/settings/recents_settings.h @@ -27,25 +31,27 @@ set(HEADERS libcockatrice/settings/visual_deck_storage_settings.h ) -if(Qt6_FOUND) - qt6_wrap_cpp(MOC_SOURCES ${HEADERS}) -endif() +qt6_wrap_cpp(MOC_SOURCES ${HEADERS}) add_library( libcockatrice_settings STATIC ${MOC_SOURCES} + libcockatrice/settings/appearance_settings.cpp libcockatrice/settings/cache_storage_settings.cpp libcockatrice/settings/card_database_settings.cpp libcockatrice/settings/card_override_settings.cpp + libcockatrice/settings/commander_bracket_settings.cpp libcockatrice/settings/cards_display_settings.cpp libcockatrice/settings/chat_settings.cpp libcockatrice/settings/debug_settings.cpp + libcockatrice/settings/deck_editor_settings.cpp libcockatrice/settings/download_settings.cpp libcockatrice/settings/game_filters_settings.cpp libcockatrice/settings/game_settings.cpp libcockatrice/settings/interface_settings.cpp libcockatrice/settings/layouts_settings.cpp libcockatrice/settings/message_settings.cpp + libcockatrice/settings/network_settings.cpp libcockatrice/settings/paths_settings.cpp libcockatrice/settings/personal_settings.cpp libcockatrice/settings/recents_settings.cpp diff --git a/libcockatrice_settings/libcockatrice/settings/appearance_settings.cpp b/libcockatrice_settings/libcockatrice/settings/appearance_settings.cpp new file mode 100644 index 000000000..45a02299e --- /dev/null +++ b/libcockatrice_settings/libcockatrice/settings/appearance_settings.cpp @@ -0,0 +1,71 @@ +#include "appearance_settings.h" + +AppearanceSettings::AppearanceSettings(const QString &settingPath, QObject *parent) + : SettingsManager(settingPath + "appearance.ini", "appearance", QString(), parent) +{ +} + +QString AppearanceSettings::getThemeName() const +{ + return getValue("themeName", QString(), QString()).toString(); +} + +void AppearanceSettings::setThemeName(const QString &_themeName) +{ + setValue(_themeName, "themeName"); + emit themeNameChanged(); +} + +bool AppearanceSettings::getStyleUserList() const +{ + return getValue("styleUserList", QString(), QString(), true).toBool(); +} + +void AppearanceSettings::setStyleUserList(bool _styleUserList) +{ + setValue(_styleUserList, "styleUserList"); + emit styleUserListChanged(); +} + +int AppearanceSettings::getMaxFontSize() const +{ + return getValue("maxFontSize", QString(), QString(), 12).toInt(); +} + +void AppearanceSettings::setMaxFontSize(int _max) +{ + setValue(_max, "maxFontSize"); +} + +QString AppearanceSettings::getHomeTabBackgroundSource() const +{ + return getValue("homeTabBackgroundSource", QString(), QString(), "themed").toString(); +} + +void AppearanceSettings::setHomeTabBackgroundSource(const QString &_backgroundSource) +{ + setValue(_backgroundSource, "homeTabBackgroundSource"); + emit homeTabBackgroundSourceChanged(); +} + +int AppearanceSettings::getHomeTabBackgroundShuffleFrequency() const +{ + return getValue("homeTabBackgroundShuffleFrequency", QString(), QString(), 0).toInt(); +} + +void AppearanceSettings::setHomeTabBackgroundShuffleFrequency(int _frequency) +{ + setValue(_frequency, "homeTabBackgroundShuffleFrequency"); + emit homeTabBackgroundShuffleFrequencyChanged(); +} + +bool AppearanceSettings::getHomeTabDisplayCardName() const +{ + return getValue("homeTabDisplayCardName", QString(), QString(), true).toBool(); +} + +void AppearanceSettings::setHomeTabDisplayCardName(bool _displayCardName) +{ + setValue(_displayCardName, "homeTabDisplayCardName"); + emit homeTabDisplayCardNameChanged(); +} diff --git a/libcockatrice_settings/libcockatrice/settings/appearance_settings.h b/libcockatrice_settings/libcockatrice/settings/appearance_settings.h new file mode 100644 index 000000000..d9b326bee --- /dev/null +++ b/libcockatrice_settings/libcockatrice/settings/appearance_settings.h @@ -0,0 +1,45 @@ +/** + * @file appearance_settings.h + * @ingroup CoreSettings + */ +//! \todo Document this file. + +#ifndef APPEARANCE_SETTINGS_H +#define APPEARANCE_SETTINGS_H + +#include "settings_manager.h" + +class AppearanceSettings : public SettingsManager +{ + Q_OBJECT + friend class SettingsCache; + +public: + [[nodiscard]] QString getThemeName() const; + void setThemeName(const QString &_themeName); + [[nodiscard]] bool getStyleUserList() const; + void setStyleUserList(bool _styleUserList); + [[nodiscard]] int getMaxFontSize() const; + void setMaxFontSize(int _max); + [[nodiscard]] QString getHomeTabBackgroundSource() const; + void setHomeTabBackgroundSource(const QString &_backgroundSource); + [[nodiscard]] int getHomeTabBackgroundShuffleFrequency() const; + void setHomeTabBackgroundShuffleFrequency(int _frequency); + [[nodiscard]] bool getHomeTabDisplayCardName() const; + void setHomeTabDisplayCardName(bool _displayCardName); + +signals: + void themeNameChanged(); + void styleUserListChanged(); + void homeTabBackgroundSourceChanged(); + void homeTabBackgroundShuffleFrequencyChanged(); + void homeTabDisplayCardNameChanged(); + +public: + explicit AppearanceSettings(const QString &settingPath, QObject *parent = nullptr); + +private: + AppearanceSettings(const AppearanceSettings & /*other*/); +}; + +#endif // APPEARANCE_SETTINGS_H diff --git a/libcockatrice_settings/libcockatrice/settings/cache_storage_settings.cpp b/libcockatrice_settings/libcockatrice/settings/cache_storage_settings.cpp index c1fd87ceb..c800cc024 100644 --- a/libcockatrice_settings/libcockatrice/settings/cache_storage_settings.cpp +++ b/libcockatrice_settings/libcockatrice/settings/cache_storage_settings.cpp @@ -1,7 +1,7 @@ #include "cache_storage_settings.h" CacheStorageSettings::CacheStorageSettings(const QString &settingPath, QObject *parent) - : SettingsManager(settingPath + "cache_storage.ini", "personal", QString(), parent) + : SettingsManager(settingPath + "cache_storage.ini", "cache_storage", QString(), parent) { } diff --git a/libcockatrice_settings/libcockatrice/settings/card_database_settings.cpp b/libcockatrice_settings/libcockatrice/settings/card_database_settings.cpp index 219c79c34..b55c97909 100644 --- a/libcockatrice_settings/libcockatrice/settings/card_database_settings.cpp +++ b/libcockatrice_settings/libcockatrice/settings/card_database_settings.cpp @@ -9,7 +9,7 @@ CardDatabaseSettings::CardDatabaseSettings(const QString &settingPath, QObject * void CardDatabaseSettings::setSortKey(QString shortName, unsigned int sortKey) { - setValue(sortKey, "sortkey", "sets", shortName); + setValue(sortKey, "sortKey", "sets", shortName); QMutexLocker lock(&setOptionsMutex); ensureSetOptionsLoaded(); setOptionsCache[shortName].sortKey = sortKey; @@ -25,7 +25,7 @@ void CardDatabaseSettings::setEnabled(QString shortName, bool enabled) void CardDatabaseSettings::setIsKnown(QString shortName, bool isknown) { - setValue(isknown, "isknown", "sets", shortName); + setValue(isknown, "isKnown", "sets", shortName); QMutexLocker lock(&setOptionsMutex); ensureSetOptionsLoaded(); setOptionsCache[shortName].isKnown = isknown; @@ -42,9 +42,9 @@ void CardDatabaseSettings::ensureSetOptionsLoaded() const for (const QString &group : groups) { settings.beginGroup(group); SetOptions &o = setOptionsCache[group]; - o.sortKey = settings.value("sortkey", 0).toUInt(); + o.sortKey = settings.value("sortKey", 0).toUInt(); o.enabled = settings.value("enabled", true).toBool(); - o.isKnown = settings.value("isknown", true).toBool(); + o.isKnown = settings.value("isKnown", true).toBool(); settings.endGroup(); } setOptionsLoaded = true; @@ -84,7 +84,7 @@ void CardDatabaseSettings::saveSets(const QVector + +QVariantList CommanderBracketSettings::defaultDefinitions() +{ + return { + QVariantMap{{"tag", "R"}, + {"officialName", "[5] cEDH"}, + {"displayName", "Ruthless"}, + {"explanation", + "Top-tier competitive decks with maximum optimization, fast combos, and minimal variance."}}, + QVariantMap{{"tag", "S"}, + {"officialName", "[4] Optimized"}, + {"displayName", "Spicy"}, + {"explanation", "Highly tuned decks with strong synergy and occasional combo finishes."}}, + QVariantMap{{"tag", "P"}, + {"officialName", "[3] Upgraded"}, + {"displayName", "Powerful"}, + {"explanation", "Focused decks with clear win conditions and solid consistency."}}, + QVariantMap{{"tag", "O"}, + {"officialName", "[2] Core"}, + {"displayName", "Oddball"}, + {"explanation", "Unconventional or thematic decks with some structure but non-standard choices."}}, + QVariantMap{{"tag", "C"}, + {"officialName", "[2] Core"}, + {"displayName", "Core"}, + {"explanation", "Preconstructed or precon-level decks with straightforward strategies."}}, + QVariantMap{{"tag", "E"}, + {"officialName", "[1] Exhibition"}, + {"displayName", "Exhibition"}, + {"explanation", "Ultra-casual, theme-focused decks with minimal optimization."}}, + QVariantMap{{"tag", "B"}, + {"officialName", "Banned"}, + {"displayName", "Banned"}, + {"explanation", "The deck contains one or more cards banned in Commander."}}, + }; +} + +CommanderBracketSettings::CommanderBracketSettings(const QString &settingPath, QObject *parent) + : SettingsManager(settingPath + "commander_brackets.ini", "commander_brackets", QString(), parent) +{ +} + +void CommanderBracketSettings::setSchemaVersion(int version) +{ + setValue(version, "schemaVersion"); +} +int CommanderBracketSettings::getSchemaVersion() const +{ + QVariant value = getValue("schemaVersion"); + return value.isValid() ? value.toInt() : 0; +} + +void CommanderBracketSettings::clearDefinitions() +{ + auto settings = getSettings(); + + settings.beginGroup("commander_brackets"); + settings.remove(""); + settings.endGroup(); + + settings.sync(); +} + +void CommanderBracketSettings::saveDefinitions(const QVariantList &definitions) +{ + auto settings = getSettings(); + + settings.beginGroup("commander_brackets"); + + settings.remove(""); + + settings.setValue("schemaVersion", CurrentSchemaVersion); + + for (const auto &entry : definitions) { + QVariantMap map = entry.toMap(); + + QString tag = map.value("tag").toString(); + + if (tag.isEmpty()) { + continue; + } + + settings.beginGroup(tag); + + settings.setValue("officialName", map.value("officialName")); + + settings.setValue("displayName", map.value("displayName")); + + settings.setValue("explanation", map.value("explanation")); + + settings.endGroup(); + } + + settings.endGroup(); + + settings.sync(); +} + +QVariantList CommanderBracketSettings::loadDefinitions() const +{ + QVariantList result; + + auto settings = getSettings(); + + settings.beginGroup("commander_brackets"); + + int version = settings.value("schemaVersion", 0).toInt(); + + if (version != CurrentSchemaVersion) { + settings.endGroup(); + return result; + } + + QStringList groups = settings.childGroups(); + + for (const QString &tag : groups) { + settings.beginGroup(tag); + + QVariantMap map; + + map["tag"] = tag; + map["officialName"] = settings.value("officialName"); + map["displayName"] = settings.value("displayName"); + map["explanation"] = settings.value("explanation"); + + result.append(map); + + settings.endGroup(); + } + + settings.endGroup(); + + return result; +} + +void CommanderBracketSettings::reloadDefinitions(const QVariantList &definitionsList) +{ + definitions.clear(); + + for (const auto &entry : definitionsList) { + const auto map = entry.toMap(); + + CommanderBracketDefinition definition; + + definition.tag = map.value("tag").toString(); + definition.officialName = map.value("officialName").toString(); + definition.displayName = map.value("displayName").toString(); + definition.explanation = map.value("explanation").toString(); + + if (!definition.tag.isEmpty()) { + definitions.insert(definition.tag, definition); + } + } +} + +QString CommanderBracketSettings::officialName(const QString &tag) const +{ + auto it = definitions.find(tag); + + if (it == definitions.end()) { + return tag; + } + + return it->officialName; +} + +QString CommanderBracketSettings::displayName(const QString &tag) const +{ + auto it = definitions.find(tag); + + if (it == definitions.end()) { + return tag; + } + + return it->displayName; +} + +QString CommanderBracketSettings::explanation(const QString &tag) const +{ + auto it = definitions.find(tag); + + if (it == definitions.end()) { + return {}; + } + + return it->explanation; +} + +bool CommanderBracketSettings::contains(const QString &tag) const +{ + return definitions.contains(tag); +} diff --git a/libcockatrice_settings/libcockatrice/settings/commander_bracket_settings.h b/libcockatrice_settings/libcockatrice/settings/commander_bracket_settings.h new file mode 100644 index 000000000..5a29f5dc7 --- /dev/null +++ b/libcockatrice_settings/libcockatrice/settings/commander_bracket_settings.h @@ -0,0 +1,56 @@ +#ifndef COMMANDER_BRACKET_SETTINGS_H +#define COMMANDER_BRACKET_SETTINGS_H + +#include "settings_manager.h" + +#include +#include +#include +#include + +struct CommanderBracketDefinition +{ + QString tag; + + QString officialName; + QString displayName; + + QString explanation; +}; + +class CommanderBracketSettings : public SettingsManager +{ + Q_OBJECT + friend class SettingsCache; + +public: + static constexpr int CurrentSchemaVersion = 1; + + static QVariantList defaultDefinitions(); + + void clearDefinitions(); + + void saveDefinitions(const QVariantList &definitions); + + QVariantList loadDefinitions() const; + + void reloadDefinitions(const QVariantList &definitions); + + QString officialName(const QString &tag) const; + QString displayName(const QString &tag) const; + QString explanation(const QString &tag) const; + bool contains(const QString &tag) const; + + void setSchemaVersion(int version); + int getSchemaVersion() const; + +private: + explicit CommanderBracketSettings(const QString &settingPath, QObject *parent = nullptr); + + CommanderBracketSettings(const CommanderBracketSettings &) = delete; + CommanderBracketSettings &operator=(const CommanderBracketSettings &) = delete; + + QHash definitions; +}; + +#endif // COMMANDER_BRACKET_SETTINGS_H diff --git a/libcockatrice_settings/libcockatrice/settings/deck_editor_settings.cpp b/libcockatrice_settings/libcockatrice/settings/deck_editor_settings.cpp new file mode 100644 index 000000000..65296a450 --- /dev/null +++ b/libcockatrice_settings/libcockatrice/settings/deck_editor_settings.cpp @@ -0,0 +1,72 @@ +#include "deck_editor_settings.h" + +DeckEditorSettings::DeckEditorSettings(const QString &settingPath, QObject *parent) + : SettingsManager(settingPath + "deck_editor.ini", "deckeditor", QString(), parent) +{ +} + +bool DeckEditorSettings::getOpenDeckInNewTab() const +{ + return getValue("openDeckInNewTab", QString(), QString(), false).toBool(); +} + +bool DeckEditorSettings::getBannerCardComboBoxVisible() const +{ + return getValue("bannerCardComboBoxVisible", QString(), QString(), true).toBool(); +} + +bool DeckEditorSettings::getTagsWidgetVisible() const +{ + return getValue("tagsWidgetVisible", QString(), QString(), true).toBool(); +} + +int DeckEditorSettings::getDefaultDeckEditorType() const +{ + return getValue("defaultDeckEditorType", QString(), QString(), 1).toInt(); +} + +void DeckEditorSettings::setOpenDeckInNewTab(bool _openDeckInNewTab) +{ + setValue(_openDeckInNewTab, "openDeckInNewTab"); +} + +void DeckEditorSettings::setBannerCardComboBoxVisible(bool _bannerCardComboBoxVisible) +{ + setValue(_bannerCardComboBoxVisible, "bannerCardComboBoxVisible"); + emit bannerCardComboBoxVisibleChanged(_bannerCardComboBoxVisible); +} + +void DeckEditorSettings::setTagsWidgetVisible(bool _tagsWidgetVisible) +{ + setValue(_tagsWidgetVisible, "tagsWidgetVisible"); + emit tagsWidgetVisibleChanged(_tagsWidgetVisible); +} + +void DeckEditorSettings::setDefaultDeckEditorType(int _defaultDeckEditorType) +{ + setValue(_defaultDeckEditorType, "defaultDeckEditorType"); +} + +int DeckEditorSettings::getCommanderSpellbookIntegrationEnabled() const +{ + return getValue("commanderspellbookintegrationenabled", QString(), QString(), + commanderSpellbookIntegrationEnabledIndexUnprompted) + .toInt(); +} + +bool DeckEditorSettings::getCommanderSpellbookIntegrationUseOfficialBracketNames() const +{ + return getValue("commanderspellbookintegrationuseofficialbracketnames", QString(), QString(), false).toBool(); +} + +void DeckEditorSettings::setCommanderSpellbookIntegrationEnabled(int _commanderSpellbookIntegrationEnabled) +{ + setValue(_commanderSpellbookIntegrationEnabled, "commanderspellbookintegrationenabled"); + emit commanderSpellbookIntegrationEnabledChanged(_commanderSpellbookIntegrationEnabled); +} + +void DeckEditorSettings::setCommanderSpellbookIntegrationUseOfficialBracketNames(bool _useOfficialBracketNames) +{ + setValue(_useOfficialBracketNames, "commanderspellbookintegrationuseofficialbracketnames"); + emit commanderSpellbookIntegrationUseOfficialBracketNamesChanged(_useOfficialBracketNames); +} diff --git a/libcockatrice_settings/libcockatrice/settings/deck_editor_settings.h b/libcockatrice_settings/libcockatrice/settings/deck_editor_settings.h new file mode 100644 index 000000000..70f91be9b --- /dev/null +++ b/libcockatrice_settings/libcockatrice/settings/deck_editor_settings.h @@ -0,0 +1,49 @@ +#ifndef DECK_EDITOR_SETTINGS_H +#define DECK_EDITOR_SETTINGS_H + +#include "settings_manager.h" + +#include + +enum commanderSpellbookIntegrationEnabledIndex +{ + commanderSpellbookIntegrationEnabledIndexDisabled, + commanderSpellbookIntegrationEnabledIndexEnabled, + commanderSpellbookIntegrationEnabledIndexAutomatic, + commanderSpellbookIntegrationEnabledIndexUnprompted, +}; + +class DeckEditorSettings : public SettingsManager, public IDeckEditorSettingsProvider +{ + Q_OBJECT + friend class SettingsCache; + +public: + [[nodiscard]] bool getOpenDeckInNewTab() const override; + [[nodiscard]] bool getBannerCardComboBoxVisible() const override; + [[nodiscard]] bool getTagsWidgetVisible() const override; + [[nodiscard]] int getDefaultDeckEditorType() const override; + [[nodiscard]] int getCommanderSpellbookIntegrationEnabled() const; + [[nodiscard]] bool getCommanderSpellbookIntegrationUseOfficialBracketNames() const; + + void setOpenDeckInNewTab(bool _openDeckInNewTab); + void setBannerCardComboBoxVisible(bool _bannerCardComboBoxVisible); + void setTagsWidgetVisible(bool _tagsWidgetVisible); + void setDefaultDeckEditorType(int _defaultDeckEditorType); + void setCommanderSpellbookIntegrationEnabled(int _commanderSpellbookIntegrationEnabled); + void setCommanderSpellbookIntegrationUseOfficialBracketNames(bool _useOfficialBracketNames); + +signals: + void bannerCardComboBoxVisibleChanged(bool visible); + void tagsWidgetVisibleChanged(bool visible); + void commanderSpellbookIntegrationEnabledChanged(int enabled); + void commanderSpellbookIntegrationUseOfficialBracketNamesChanged(bool useOfficialBracketNames); + +public: + explicit DeckEditorSettings(const QString &settingPath, QObject *parent = nullptr); + +private: + DeckEditorSettings(const DeckEditorSettings & /*other*/); +}; + +#endif // DECK_EDITOR_SETTINGS_H diff --git a/libcockatrice_settings/libcockatrice/settings/download_settings.cpp b/libcockatrice_settings/libcockatrice/settings/download_settings.cpp index 66525a598..cfa1c054e 100644 --- a/libcockatrice_settings/libcockatrice/settings/download_settings.cpp +++ b/libcockatrice_settings/libcockatrice/settings/download_settings.cpp @@ -3,6 +3,7 @@ #include "settings_manager.h" const QStringList DownloadSettings::DEFAULT_DOWNLOAD_URLS = { + "https://cards.scryfall.io/large/!prop:side!/!set:uuid_substr_0_1!/!set:uuid_substr_1_1!/!set:uuid!.jpg", "https://api.scryfall.com/cards/!set:uuid!?format=image&face=!prop:side!", "https://api.scryfall.com/cards/multiverse/!set:muid!?format=image", "https://gatherer.wizards.com/Handlers/Image.ashx?multiverseid=!set:muid!&type=card", @@ -27,3 +28,25 @@ void DownloadSettings::resetToDefaultURLs() { setValue(QVariant::fromValue(DEFAULT_DOWNLOAD_URLS), "urls"); } + +bool DownloadSettings::getPicDownload() const +{ + return getValue("pictureDownload", QString(), QString(), true).toBool(); +} + +void DownloadSettings::setPicDownload(bool _picDownload) +{ + setValue(_picDownload, "pictureDownload"); + emit picDownloadChanged(); +} + +bool DownloadSettings::getDownloadSpoilersStatus() const +{ + return getValue("downloadSpoilers", QString(), QString(), false).toBool(); +} + +void DownloadSettings::setDownloadSpoilerStatus(bool _spoilerStatus) +{ + setValue(_spoilerStatus, "downloadSpoilers"); + emit downloadSpoilerStatusChanged(); +} diff --git a/libcockatrice_settings/libcockatrice/settings/download_settings.h b/libcockatrice_settings/libcockatrice/settings/download_settings.h index 60e59220b..a3a6f4ca9 100644 --- a/libcockatrice_settings/libcockatrice/settings/download_settings.h +++ b/libcockatrice_settings/libcockatrice/settings/download_settings.h @@ -22,6 +22,14 @@ public: QStringList getAllURLs() const; void setDownloadUrls(const QStringList &downloadURLs); void resetToDefaultURLs(); + [[nodiscard]] bool getPicDownload() const; + void setPicDownload(bool _picDownload); + [[nodiscard]] bool getDownloadSpoilersStatus() const; + void setDownloadSpoilerStatus(bool _spoilerStatus); + +signals: + void picDownloadChanged(); + void downloadSpoilerStatusChanged(); }; #endif // COCKATRICE_DOWNLOADSETTINGS_H diff --git a/libcockatrice_settings/libcockatrice/settings/game_filters_settings.cpp b/libcockatrice_settings/libcockatrice/settings/game_filters_settings.cpp index 4f5bf52ee..ad972b433 100644 --- a/libcockatrice_settings/libcockatrice/settings/game_filters_settings.cpp +++ b/libcockatrice_settings/libcockatrice/settings/game_filters_settings.cpp @@ -19,137 +19,137 @@ static QString hashGameType(const QString &gameType) void GameFiltersSettings::setHideBuddiesOnlyGames(bool hide) { - setValue(hide, "hide_buddies_only_games"); + setValue(hide, "hideBuddiesOnlyGames"); } bool GameFiltersSettings::isHideBuddiesOnlyGames() const { - QVariant previous = getValue("hide_buddies_only_games"); + QVariant previous = getValue("hideBuddiesOnlyGames"); return previous == QVariant() ? false : previous.toBool(); } void GameFiltersSettings::setHideFullGames(bool hide) { - setValue(hide, "hide_full_games"); + setValue(hide, "hideFullGames"); } bool GameFiltersSettings::isHideFullGames() const { - QVariant previous = getValue("hide_full_games"); + QVariant previous = getValue("hideFullGames"); return previous == QVariant() ? false : previous.toBool(); } void GameFiltersSettings::setHideGamesThatStarted(bool hide) { - setValue(hide, "hide_games_that_started"); + setValue(hide, "hideGamesThatStarted"); } bool GameFiltersSettings::isHideGamesThatStarted() const { - QVariant previous = getValue("hide_games_that_started"); + QVariant previous = getValue("hideGamesThatStarted"); return previous == QVariant() ? false : previous.toBool(); } void GameFiltersSettings::setHidePasswordProtectedGames(bool hide) { - setValue(hide, "hide_password_protected_games"); + setValue(hide, "hidePasswordProtectedGames"); } bool GameFiltersSettings::isHidePasswordProtectedGames() const { - QVariant previous = getValue("hide_password_protected_games"); + QVariant previous = getValue("hidePasswordProtectedGames"); return previous == QVariant() ? false : previous.toBool(); } void GameFiltersSettings::setHideIgnoredUserGames(bool hide) { - setValue(hide, "hide_ignored_user_games"); + setValue(hide, "hideIgnoredUserGames"); } bool GameFiltersSettings::isHideIgnoredUserGames() const { - QVariant previous = getValue("hide_ignored_user_games"); + QVariant previous = getValue("hideIgnoredUserGames"); return previous == QVariant() ? true : previous.toBool(); } void GameFiltersSettings::setHideNotBuddyCreatedGames(bool hide) { - setValue(hide, "hide_not_buddy_created_games"); + setValue(hide, "hideNotBuddyCreatedGames"); } bool GameFiltersSettings::isHideNotBuddyCreatedGames() const { - QVariant previous = getValue("hide_not_buddy_created_games"); + QVariant previous = getValue("hideNotBuddyCreatedGames"); return previous == QVariant() ? false : previous.toBool(); } void GameFiltersSettings::setHideOpenDecklistGames(bool hide) { - setValue(hide, "hide_open_decklist_games"); + setValue(hide, "hideOpenDecklistGames"); } bool GameFiltersSettings::isHideOpenDecklistGames() const { - QVariant previous = getValue("hide_open_decklist_games"); + QVariant previous = getValue("hideOpenDecklistGames"); return previous == QVariant() ? false : previous.toBool(); } void GameFiltersSettings::setGameNameFilter(QString gameName) { - setValue(gameName, "game_name_filter"); + setValue(gameName, "gameNameFilter"); } QString GameFiltersSettings::getGameNameFilter() const { - return getValue("game_name_filter").toString(); + return getValue("gameNameFilter").toString(); } void GameFiltersSettings::setCreatorNameFilters(QStringList creatorName) { - setValue(creatorName, "creator_name_filter"); + setValue(creatorName, "creatorNameFilter"); } QStringList GameFiltersSettings::getCreatorNameFilters() const { - return getValue("creator_name_filter").toStringList(); + return getValue("creatorNameFilter").toStringList(); } void GameFiltersSettings::setMinPlayers(int min) { - setValue(min, "min_players"); + setValue(min, "minPlayers"); } int GameFiltersSettings::getMinPlayers() const { - QVariant previous = getValue("min_players"); + QVariant previous = getValue("minPlayers"); return previous == QVariant() ? 1 : previous.toInt(); } void GameFiltersSettings::setMaxPlayers(int max) { - setValue(max, "max_players"); + setValue(max, "maxPlayers"); } int GameFiltersSettings::getMaxPlayers() const { - QVariant previous = getValue("max_players"); + QVariant previous = getValue("maxPlayers"); return previous == QVariant() ? 99 : previous.toInt(); } void GameFiltersSettings::setMaxGameAge(const QTime &maxGameAge) { - setValue(maxGameAge, "max_game_age_time"); + setValue(maxGameAge, "maxGameAgeTime"); } QTime GameFiltersSettings::getMaxGameAge() const { - QVariant previous = getValue("max_game_age_time"); + QVariant previous = getValue("maxGameAgeTime"); return previous.toTime(); } void GameFiltersSettings::setGameTypeEnabled(QString gametype, bool enabled) { - setValue(enabled, "game_type/" + hashGameType(gametype)); + setValue(enabled, "gameType/" + hashGameType(gametype)); } void GameFiltersSettings::setGameHashedTypeEnabled(QString gametypeHASHED, bool enabled) @@ -159,50 +159,50 @@ void GameFiltersSettings::setGameHashedTypeEnabled(QString gametypeHASHED, bool bool GameFiltersSettings::isGameTypeEnabled(QString gametype) const { - QVariant previous = getValue("game_type/" + hashGameType(gametype)); + QVariant previous = getValue("gameType/" + hashGameType(gametype)); return previous == QVariant() ? false : previous.toBool(); } void GameFiltersSettings::setShowOnlyIfSpectatorsCanWatch(bool show) { - setValue(show, "show_only_if_spectators_can_watch"); + setValue(show, "showOnlyIfSpectatorsCanWatch"); } bool GameFiltersSettings::isShowOnlyIfSpectatorsCanWatch() const { - QVariant previous = getValue("show_only_if_spectators_can_watch"); + QVariant previous = getValue("showOnlyIfSpectatorsCanWatch"); return previous == QVariant() ? false : previous.toBool(); } void GameFiltersSettings::setShowSpectatorPasswordProtected(bool show) { - setValue(show, "show_spectator_password_protected"); + setValue(show, "showSpectatorPasswordProtected"); } bool GameFiltersSettings::isShowSpectatorPasswordProtected() const { - QVariant previous = getValue("show_spectator_password_protected"); + QVariant previous = getValue("showSpectatorPasswordProtected"); return previous == QVariant() ? false : previous.toBool(); } void GameFiltersSettings::setShowOnlyIfSpectatorsCanChat(bool show) { - setValue(show, "show_only_if_spectators_can_chat"); + setValue(show, "showOnlyIfSpectatorsCanChat"); } bool GameFiltersSettings::isShowOnlyIfSpectatorsCanChat() const { - QVariant previous = getValue("show_only_if_spectators_can_chat"); + QVariant previous = getValue("showOnlyIfSpectatorsCanChat"); return previous == QVariant() ? false : previous.toBool(); } void GameFiltersSettings::setShowOnlyIfSpectatorsCanSeeHands(bool show) { - setValue(show, "show_only_if_spectators_can_see_hands"); + setValue(show, "showOnlyIfSpectatorsCanSeeHands"); } bool GameFiltersSettings::isShowOnlyIfSpectatorsCanSeeHands() const { - QVariant previous = getValue("show_only_if_spectators_can_see_hands"); + QVariant previous = getValue("showOnlyIfSpectatorsCanSeeHands"); return previous == QVariant() ? false : previous.toBool(); } \ No newline at end of file diff --git a/libcockatrice_settings/libcockatrice/settings/game_settings.cpp b/libcockatrice_settings/libcockatrice/settings/game_settings.cpp index c8d19f801..dfd67c98a 100644 --- a/libcockatrice_settings/libcockatrice/settings/game_settings.cpp +++ b/libcockatrice_settings/libcockatrice/settings/game_settings.cpp @@ -7,160 +7,160 @@ GameSettings::GameSettings(const QString &settingPath, QObject *parent) QString GameSettings::getGameDescription() const { - return getValue("gamedescription", "game").toString(); + return getValue("gameDescription", "game").toString(); } int GameSettings::getMaxPlayers() const { - return getValue("maxplayers", "game", QString(), 2).toInt(); + return getValue("maxPlayers", "game", QString(), 2).toInt(); } QString GameSettings::getGameTypes() const { - return getValue("gametypes", "game").toString(); + return getValue("gameTypes", "game").toString(); } bool GameSettings::getOnlyBuddies() const { - return getValue("onlybuddies", "game").toBool(); + return getValue("onlyBuddies", "game").toBool(); } bool GameSettings::getOnlyRegistered() const { - return getValue("onlyregistered", "game").toBool(); + return getValue("onlyRegistered", "game").toBool(); } bool GameSettings::getSpectatorsAllowed() const { - return getValue("spectatorsallowed", "game").toBool(); + return getValue("spectatorsAllowed", "game").toBool(); } bool GameSettings::getSpectatorsNeedPassword() const { - return getValue("spectatorsneedpassword", "game").toBool(); + return getValue("spectatorsNeedPassword", "game").toBool(); } bool GameSettings::getSpectatorsCanTalk() const { - return getValue("spectatorscantalk", "game").toBool(); + return getValue("spectatorsCanTalk", "game").toBool(); } bool GameSettings::getSpectatorsCanSeeEverything() const { - return getValue("spectatorscanseeeverything", "game").toBool(); + return getValue("spectatorsCanSeeEverything", "game").toBool(); } bool GameSettings::getCreateGameAsSpectator() const { - return getValue("creategameasspectator", "game").toBool(); + return getValue("createGameAsSpectator", "game").toBool(); } int GameSettings::getDefaultStartingLifeTotal() const { - return getValue("defaultstartinglifetotal", "game", QString(), 20).toInt(); + return getValue("defaultStartingLifeTotal", "game", QString(), 20).toInt(); } bool GameSettings::getShareDecklistsOnLoad() const { - return getValue("sharedecklistsonload", "game").toBool(); + return getValue("shareDecklistsOnLoad", "game").toBool(); } bool GameSettings::getRememberGameSettings() const { - return getValue("remembergamesettings", "game", QString(), true).toBool(); + return getValue("rememberGameSettings", "game", QString(), true).toBool(); } bool GameSettings::getLocalGameRememberSettings() const { - return getValue("remembersettings", "localgameoptions").toBool(); + return getValue("rememberSettings", "localgameoptions").toBool(); } int GameSettings::getLocalGameMaxPlayers() const { - return getValue("maxplayers", "localgameoptions", QString(), 1).toInt(); + return getValue("maxPlayers", "localgameoptions", QString(), 1).toInt(); } int GameSettings::getLocalGameStartingLifeTotal() const { - return getValue("startinglifetotal", "localgameoptions", QString(), 20).toInt(); + return getValue("startingLifeTotal", "localgameoptions", QString(), 20).toInt(); } void GameSettings::setGameDescription(const QString &_gameDescription) { - setValue(_gameDescription, "gamedescription", "game"); + setValue(_gameDescription, "gameDescription", "game"); } void GameSettings::setMaxPlayers(int _maxPlayers) { - setValue(_maxPlayers, "maxplayers", "game"); + setValue(_maxPlayers, "maxPlayers", "game"); } void GameSettings::setGameTypes(const QString &_gameTypes) { - setValue(_gameTypes, "gametypes", "game"); + setValue(_gameTypes, "gameTypes", "game"); } void GameSettings::setOnlyBuddies(bool _onlyBuddies) { - setValue(_onlyBuddies, "onlybuddies", "game"); + setValue(_onlyBuddies, "onlyBuddies", "game"); } void GameSettings::setOnlyRegistered(bool _onlyRegistered) { - setValue(_onlyRegistered, "onlyregistered", "game"); + setValue(_onlyRegistered, "onlyRegistered", "game"); } void GameSettings::setSpectatorsAllowed(bool _spectatorsAllowed) { - setValue(_spectatorsAllowed, "spectatorsallowed", "game"); + setValue(_spectatorsAllowed, "spectatorsAllowed", "game"); } void GameSettings::setSpectatorsNeedPassword(bool _spectatorsNeedPassword) { - setValue(_spectatorsNeedPassword, "spectatorsneedpassword", "game"); + setValue(_spectatorsNeedPassword, "spectatorsNeedPassword", "game"); } void GameSettings::setSpectatorsCanTalk(bool _spectatorsCanTalk) { - setValue(_spectatorsCanTalk, "spectatorscantalk", "game"); + setValue(_spectatorsCanTalk, "spectatorsCanTalk", "game"); } void GameSettings::setSpectatorsCanSeeEverything(bool _spectatorsCanSeeEverything) { - setValue(_spectatorsCanSeeEverything, "spectatorscanseeeverything", "game"); + setValue(_spectatorsCanSeeEverything, "spectatorsCanSeeEverything", "game"); } void GameSettings::setCreateGameAsSpectator(bool _createGameAsSpectator) { - setValue(_createGameAsSpectator, "creategameasspectator", "game"); + setValue(_createGameAsSpectator, "createGameAsSpectator", "game"); } void GameSettings::setDefaultStartingLifeTotal(int _defaultStartingLifeTotal) { - setValue(_defaultStartingLifeTotal, "defaultstartinglifetotal", "game"); + setValue(_defaultStartingLifeTotal, "defaultStartingLifeTotal", "game"); } void GameSettings::setShareDecklistsOnLoad(bool _shareDecklistsOnLoad) { - setValue(_shareDecklistsOnLoad, "sharedecklistsonload", "game"); + setValue(_shareDecklistsOnLoad, "shareDecklistsOnLoad", "game"); } void GameSettings::setRememberGameSettings(bool _rememberGameSettings) { - setValue(_rememberGameSettings, "remembergamesettings", "game"); + setValue(_rememberGameSettings, "rememberGameSettings", "game"); } void GameSettings::setLocalGameRememberSettings(bool value) { - setValue(value, "remembersettings", "localgameoptions"); + setValue(value, "rememberSettings", "localgameoptions"); } void GameSettings::setLocalGameMaxPlayers(int value) { - setValue(value, "maxplayers", "localgameoptions"); + setValue(value, "maxPlayers", "localgameoptions"); } void GameSettings::setLocalGameStartingLifeTotal(int value) { - setValue(value, "startinglifetotal", "localgameoptions"); + setValue(value, "startingLifeTotal", "localgameoptions"); } diff --git a/libcockatrice_settings/libcockatrice/settings/interface_settings.cpp b/libcockatrice_settings/libcockatrice/settings/interface_settings.cpp index b0d1d523d..29c57c57e 100644 --- a/libcockatrice_settings/libcockatrice/settings/interface_settings.cpp +++ b/libcockatrice_settings/libcockatrice/settings/interface_settings.cpp @@ -7,7 +7,7 @@ InterfaceSettings::InterfaceSettings(const QString &settingPath, QObject *parent bool InterfaceSettings::getUseTearOffMenus() const { - return getValue("usetearoffmenus", QString(), QString(), true).toBool(); + return getValue("useTearOffMenus", QString(), QString(), true).toBool(); } int InterfaceSettings::getCardViewInitialRowsMax() const @@ -37,22 +37,22 @@ bool InterfaceSettings::getKeepGameChatFocus() const bool InterfaceSettings::getNotificationsEnabled() const { - return getValue("notificationsenabled", QString(), QString(), true).toBool(); + return getValue("enabled", "interface", "notifications", true).toBool(); } bool InterfaceSettings::getSpectatorNotificationsEnabled() const { - return getValue("specnotificationsenabled", QString(), QString(), false).toBool(); + return getValue("spectatorsEnabled", "interface", "notifications", false).toBool(); } bool InterfaceSettings::getBuddyConnectNotificationsEnabled() const { - return getValue("buddyconnectnotificationsenabled", QString(), QString(), true).toBool(); + return getValue("buddyConnectEnabled", "interface", "notifications", true).toBool(); } bool InterfaceSettings::getDoubleClickToPlay() const { - return getValue("doubleclicktoplay", QString(), QString(), true).toBool(); + return getValue("doubleClickToPlay", QString(), QString(), true).toBool(); } bool InterfaceSettings::getClickPlaysAllSelected() const @@ -62,7 +62,7 @@ bool InterfaceSettings::getClickPlaysAllSelected() const bool InterfaceSettings::getPlayToStack() const { - return getValue("playtostack", QString(), QString(), true).toBool(); + return getValue("playToStack", QString(), QString(), true).toBool(); } bool InterfaceSettings::getDoNotDeleteArrowsInSubPhases() const @@ -72,22 +72,22 @@ bool InterfaceSettings::getDoNotDeleteArrowsInSubPhases() const int InterfaceSettings::getStartingHandSize() const { - return getValue("startinghandsize", QString(), QString(), 7).toInt(); + return getValue("startingHandSize", QString(), QString(), 7).toInt(); } bool InterfaceSettings::getAnnotateTokens() const { - return getValue("annotatetokens", QString(), QString(), false).toBool(); + return getValue("annotateTokens", QString(), QString(), false).toBool(); } bool InterfaceSettings::getShowDragSelectionCount() const { - return getValue("showlassoselectioncount", QString(), QString(), true).toBool(); + return getValue("showLassoSelectionCount", QString(), QString(), true).toBool(); } bool InterfaceSettings::getShowTotalSelectionCount() const { - return getValue("showpersistentselectioncount", QString(), QString(), true).toBool(); + return getValue("showPersistentSelectionCount", QString(), QString(), true).toBool(); } int InterfaceSettings::getTallyType() const @@ -102,17 +102,12 @@ bool InterfaceSettings::getHorizontalHand() const bool InterfaceSettings::getInvertVerticalCoordinate() const { - return getValue("invert_vertical", "table", QString(), false).toBool(); + return getValue("invertVertical", "table", QString(), false).toBool(); } int InterfaceSettings::getMinPlayersForMultiColumnLayout() const { - return getValue("min_players_multicolumn", QString(), QString(), 4).toInt(); -} - -bool InterfaceSettings::getOpenDeckInNewTab() const -{ - return getValue("openDeckInNewTab", "editor", QString(), false).toBool(); + return getValue("minPlayersMulticolumn", QString(), QString(), 4).toInt(); } int InterfaceSettings::getRewindBufferingMs() const @@ -125,39 +120,49 @@ qreal InterfaceSettings::getFastForwardSpeed() const return getValue("fastForwardSpeed", "replay", QString(), 10).toReal(); } -bool InterfaceSettings::getStyleUserList() const +bool InterfaceSettings::getSkipEmptySections() const { - return getValue("styleUserList", "appearance", QString(), true).toBool(); + return getValue("skipEmptySections", "replay", QString(), false).toBool(); } bool InterfaceSettings::getLeftJustified() const { - return getValue("leftjustified", QString(), QString(), false).toBool(); + return getValue("leftJustified", QString(), QString(), false).toBool(); } int InterfaceSettings::getZoneViewGroupByIndex() const { - return getValue("groupby", "zoneview", QString(), 1).toInt(); + return getValue("groupBy", "zoneview", QString(), 1).toInt(); } int InterfaceSettings::getZoneViewSortByIndex() const { - return getValue("sortby", "zoneview", QString(), 1).toInt(); + return getValue("sortBy", "zoneview", QString(), 1).toInt(); } bool InterfaceSettings::getZoneViewPileView() const { - return getValue("pileview", "zoneview", QString(), true).toBool(); + return getValue("pileView", "zoneview", QString(), true).toBool(); } -QString InterfaceSettings::getKnownMissingFeatures() +bool InterfaceSettings::getShowStatusBar() const { - return getValue("knownmissingfeatures", QString(), QString(), "").toString(); + return getValue("showStatusBar", QString(), QString(), false).toBool(); +} + +bool InterfaceSettings::getShowShortcuts() const +{ + return getValue("showShortcuts", QString(), QString(), true).toBool(); +} + +bool InterfaceSettings::getShowGameSelectorFilterToolbar() const +{ + return getValue("showGameSelectorFilterToolbar", QString(), QString(), true).toBool(); } void InterfaceSettings::setUseTearOffMenus(bool _useTearOffMenus) { - setValue(_useTearOffMenus, "usetearoffmenus"); + setValue(_useTearOffMenus, "useTearOffMenus"); emit useTearOffMenusChanged(_useTearOffMenus); } @@ -189,22 +194,22 @@ void InterfaceSettings::setKeepGameChatFocus(bool value) void InterfaceSettings::setNotificationsEnabled(bool _notificationsEnabled) { - setValue(_notificationsEnabled, "notificationsenabled"); + setValue(_notificationsEnabled, "enabled", "interface", "notifications"); } void InterfaceSettings::setSpectatorNotificationsEnabled(bool _spectatorNotificationsEnabled) { - setValue(_spectatorNotificationsEnabled, "specnotificationsenabled"); + setValue(_spectatorNotificationsEnabled, "spectatorsEnabled", "interface", "notifications"); } void InterfaceSettings::setBuddyConnectNotificationsEnabled(bool _buddyConnectNotificationsEnabled) { - setValue(_buddyConnectNotificationsEnabled, "buddyconnectnotificationsenabled"); + setValue(_buddyConnectNotificationsEnabled, "buddyConnectEnabled", "interface", "notifications"); } void InterfaceSettings::setDoubleClickToPlay(bool _doubleClickToPlay) { - setValue(_doubleClickToPlay, "doubleclicktoplay"); + setValue(_doubleClickToPlay, "doubleClickToPlay"); } void InterfaceSettings::setClickPlaysAllSelected(bool _clickPlaysAllSelected) @@ -214,7 +219,7 @@ void InterfaceSettings::setClickPlaysAllSelected(bool _clickPlaysAllSelected) void InterfaceSettings::setPlayToStack(bool _playToStack) { - setValue(_playToStack, "playtostack"); + setValue(_playToStack, "playToStack"); } void InterfaceSettings::setDoNotDeleteArrowsInSubPhases(bool _doNotDeleteArrowsInSubPhases) @@ -224,22 +229,22 @@ void InterfaceSettings::setDoNotDeleteArrowsInSubPhases(bool _doNotDeleteArrowsI void InterfaceSettings::setStartingHandSize(int _startingHandSize) { - setValue(_startingHandSize, "startinghandsize"); + setValue(_startingHandSize, "startingHandSize"); } void InterfaceSettings::setAnnotateTokens(bool _annotateTokens) { - setValue(_annotateTokens, "annotatetokens"); + setValue(_annotateTokens, "annotateTokens"); } void InterfaceSettings::setShowDragSelectionCount(bool _showDragSelectionCount) { - setValue(_showDragSelectionCount, "showlassoselectioncount"); + setValue(_showDragSelectionCount, "showLassoSelectionCount"); } void InterfaceSettings::setShowTotalSelectionCount(bool _showTotalSelectionCount) { - setValue(_showTotalSelectionCount, "showpersistentselectioncount"); + setValue(_showTotalSelectionCount, "showPersistentSelectionCount"); } void InterfaceSettings::setTallyType(int value) @@ -259,21 +264,16 @@ void InterfaceSettings::setHorizontalHand(bool _horizontalHand) void InterfaceSettings::setInvertVerticalCoordinate(bool _invertVerticalCoordinate) { - setValue(_invertVerticalCoordinate, "invert_vertical", "table"); + setValue(_invertVerticalCoordinate, "invertVertical", "table"); emit invertVerticalCoordinateChanged(); } void InterfaceSettings::setMinPlayersForMultiColumnLayout(int _minPlayersForMultiColumnLayout) { - setValue(_minPlayersForMultiColumnLayout, "min_players_multicolumn"); + setValue(_minPlayersForMultiColumnLayout, "minPlayersMulticolumn"); emit minPlayersForMultiColumnLayoutChanged(); } -void InterfaceSettings::setOpenDeckInNewTab(bool _openDeckInNewTab) -{ - setValue(_openDeckInNewTab, "openDeckInNewTab", "editor"); -} - void InterfaceSettings::setRewindBufferingMs(int _rewindBufferingMs) { setValue(_rewindBufferingMs, "rewindBufferingMs", "replay"); @@ -284,34 +284,45 @@ void InterfaceSettings::setFastForwardSpeed(qreal _value) setValue(_value, "fastForwardSpeed", "replay"); } -void InterfaceSettings::setStyleUserList(bool _styleUserList) +void InterfaceSettings::setSkipEmptySections(bool _value) { - setValue(_styleUserList, "styleUserList", "appearance"); - emit styleUserListChanged(); + setValue(_value, "skipEmptySections", "replay"); } void InterfaceSettings::setLeftJustified(bool _leftJustified) { - setValue(_leftJustified, "leftjustified"); + setValue(_leftJustified, "leftJustified"); emit handJustificationChanged(); } void InterfaceSettings::setZoneViewGroupByIndex(int _zoneViewGroupByIndex) { - setValue(_zoneViewGroupByIndex, "groupby", "zoneview"); + setValue(_zoneViewGroupByIndex, "groupBy", "zoneview"); } void InterfaceSettings::setZoneViewSortByIndex(int _zoneViewSortByIndex) { - setValue(_zoneViewSortByIndex, "sortby", "zoneview"); + setValue(_zoneViewSortByIndex, "sortBy", "zoneview"); } void InterfaceSettings::setZoneViewPileView(bool _zoneViewPileView) { - setValue(_zoneViewPileView, "pileview", "zoneview"); + setValue(_zoneViewPileView, "pileView", "zoneview"); } -void InterfaceSettings::setKnownMissingFeatures(const QString &_knownMissingFeatures) +void InterfaceSettings::setShowStatusBar(bool _showStatusBar) { - setValue(_knownMissingFeatures, "knownmissingfeatures"); + setValue(_showStatusBar, "showStatusBar"); + emit showStatusBarChanged(_showStatusBar); +} + +void InterfaceSettings::setShowShortcuts(bool _showShortcuts) +{ + setValue(_showShortcuts, "showShortcuts"); +} + +void InterfaceSettings::setShowGameSelectorFilterToolbar(bool _showGameSelectorFilterToolbar) +{ + setValue(_showGameSelectorFilterToolbar, "showGameSelectorFilterToolbar"); + emit showGameSelectorFilterToolbarChanged(_showGameSelectorFilterToolbar); } diff --git a/libcockatrice_settings/libcockatrice/settings/interface_settings.h b/libcockatrice_settings/libcockatrice/settings/interface_settings.h index 9d3fec496..982976310 100644 --- a/libcockatrice_settings/libcockatrice/settings/interface_settings.h +++ b/libcockatrice_settings/libcockatrice/settings/interface_settings.h @@ -32,15 +32,16 @@ public: [[nodiscard]] bool getHorizontalHand() const override; [[nodiscard]] bool getInvertVerticalCoordinate() const override; [[nodiscard]] int getMinPlayersForMultiColumnLayout() const override; - [[nodiscard]] bool getOpenDeckInNewTab() const override; [[nodiscard]] int getRewindBufferingMs() const override; [[nodiscard]] qreal getFastForwardSpeed() const override; - [[nodiscard]] bool getStyleUserList() const override; + [[nodiscard]] bool getSkipEmptySections() const override; [[nodiscard]] bool getLeftJustified() const override; [[nodiscard]] int getZoneViewGroupByIndex() const override; [[nodiscard]] int getZoneViewSortByIndex() const override; [[nodiscard]] bool getZoneViewPileView() const override; - [[nodiscard]] QString getKnownMissingFeatures() override; + [[nodiscard]] bool getShowStatusBar() const override; + [[nodiscard]] bool getShowShortcuts() const override; + [[nodiscard]] bool getShowGameSelectorFilterToolbar() const override; void setUseTearOffMenus(bool _useTearOffMenus); void setCardViewInitialRowsMax(int _cardViewInitialRowsMax); @@ -63,15 +64,16 @@ public: void setHorizontalHand(bool _horizontalHand); void setInvertVerticalCoordinate(bool _invertVerticalCoordinate); void setMinPlayersForMultiColumnLayout(int _minPlayersForMultiColumnLayout); - void setOpenDeckInNewTab(bool _openDeckInNewTab); void setRewindBufferingMs(int _rewindBufferingMs); void setFastForwardSpeed(qreal _value); - void setStyleUserList(bool _styleUserList); + void setSkipEmptySections(bool _value); void setLeftJustified(bool _leftJustified); void setZoneViewGroupByIndex(int _zoneViewGroupByIndex); void setZoneViewSortByIndex(int _zoneViewSortByIndex); void setZoneViewPileView(bool _zoneViewPileView); - void setKnownMissingFeatures(const QString &_knownMissingFeatures); + void setShowStatusBar(bool _showStatusBar); + void setShowShortcuts(bool _showShortcuts); + void setShowGameSelectorFilterToolbar(bool _showGameSelectorFilterToolbar); signals: void useTearOffMenusChanged(bool state); @@ -79,12 +81,15 @@ signals: void horizontalHandChanged(); void invertVerticalCoordinateChanged(); void minPlayersForMultiColumnLayoutChanged(); - void styleUserListChanged(); void handJustificationChanged(); void tallyTypeChanged(int type); + void showStatusBarChanged(bool state); + void showGameSelectorFilterToolbarChanged(bool state); + +public: + explicit InterfaceSettings(const QString &settingPath, QObject *parent = nullptr); private: - explicit InterfaceSettings(const QString &settingPath, QObject *parent = nullptr); InterfaceSettings(const InterfaceSettings & /*other*/); }; diff --git a/libcockatrice_settings/libcockatrice/settings/network_settings.cpp b/libcockatrice_settings/libcockatrice/settings/network_settings.cpp new file mode 100644 index 000000000..2d34d47ad --- /dev/null +++ b/libcockatrice_settings/libcockatrice/settings/network_settings.cpp @@ -0,0 +1,56 @@ +#include "network_settings.h" + +NetworkSettings::NetworkSettings(const QString &settingPath, QObject *parent) + : SettingsManager(settingPath + "network.ini", "network", QString(), parent) +{ +} + +QString NetworkSettings::getClientID() const +{ + return getValue("clientId", QString(), QString(), "notset").toString(); +} + +void NetworkSettings::setClientID(const QString &_clientID) +{ + setValue(_clientID, "clientId"); +} + +QString NetworkSettings::getClientVersion() const +{ + return getValue("clientVersion", QString(), QString(), "notset").toString(); +} + +void NetworkSettings::setClientVersion(const QString &_clientVersion) +{ + setValue(_clientVersion, "clientVersion"); +} + +int NetworkSettings::getKeepAlive() const +{ + return getValue("keepAlive", QString(), QString(), 3).toInt(); +} + +void NetworkSettings::setKeepAlive(int _keepAlive) +{ + setValue(_keepAlive, "keepAlive"); +} + +int NetworkSettings::getTimeOut() const +{ + return getValue("timeout", QString(), QString(), 5).toInt(); +} + +void NetworkSettings::setTimeOut(int _timeOut) +{ + setValue(_timeOut, "timeout"); +} + +QString NetworkSettings::getKnownMissingFeatures() const +{ + return getValue("knownMissingFeatures", QString(), QString(), "").toString(); +} + +void NetworkSettings::setKnownMissingFeatures(const QString &_knownMissingFeatures) +{ + setValue(_knownMissingFeatures, "knownMissingFeatures"); +} diff --git a/libcockatrice_settings/libcockatrice/settings/network_settings.h b/libcockatrice_settings/libcockatrice/settings/network_settings.h new file mode 100644 index 000000000..446bb51d3 --- /dev/null +++ b/libcockatrice_settings/libcockatrice/settings/network_settings.h @@ -0,0 +1,36 @@ +/** + * @file network_settings.h + * @ingroup NetworkSettings + */ +//! \todo Document this file. + +#ifndef NETWORK_SETTINGS_H +#define NETWORK_SETTINGS_H + +#include "settings_manager.h" + +class NetworkSettings : public SettingsManager +{ + Q_OBJECT + friend class SettingsCache; + +public: + [[nodiscard]] QString getClientID() const; + void setClientID(const QString &_clientID); + [[nodiscard]] QString getClientVersion() const; + void setClientVersion(const QString &_clientVersion); + [[nodiscard]] int getKeepAlive() const; + void setKeepAlive(int _keepAlive); + [[nodiscard]] int getTimeOut() const; + void setTimeOut(int _timeOut); + [[nodiscard]] QString getKnownMissingFeatures() const; + void setKnownMissingFeatures(const QString &_knownMissingFeatures); + +public: + explicit NetworkSettings(const QString &settingPath, QObject *parent = nullptr); + +private: + NetworkSettings(const NetworkSettings & /*other*/); +}; + +#endif // NETWORK_SETTINGS_H diff --git a/libcockatrice_settings/libcockatrice/settings/paths_settings.cpp b/libcockatrice_settings/libcockatrice/settings/paths_settings.cpp index bf155414d..67032e38f 100644 --- a/libcockatrice_settings/libcockatrice/settings/paths_settings.cpp +++ b/libcockatrice_settings/libcockatrice/settings/paths_settings.cpp @@ -30,7 +30,7 @@ QString PathsSettings::getPicsPath() const QString PathsSettings::getCustomPicsPath() const { - return getValue("custompics").toString(); + return getValue("customPics").toString(); } QString PathsSettings::getThemesPath() const @@ -40,22 +40,22 @@ QString PathsSettings::getThemesPath() const QString PathsSettings::getCardDatabasePath() const { - return getValue("carddatabase").toString(); + return getValue("cardDatabase").toString(); } QString PathsSettings::getCustomCardDatabasePath() const { - return getValue("customsets").toString(); + return getValue("customSets").toString(); } QString PathsSettings::getTokenDatabasePath() const { - return getValue("tokendatabase").toString(); + return getValue("tokenDatabase").toString(); } QString PathsSettings::getSpoilerCardDatabasePath() const { - return getValue("spoilerdatabase").toString(); + return getValue("spoilerDatabase").toString(); } QString PathsSettings::getRedirectCachePath() const @@ -86,7 +86,7 @@ void PathsSettings::setPicsPath(const QString &_picsPath) void PathsSettings::setCustomPicsPath(const QString &_customPicsPath) { - setValue(_customPicsPath, "custompics"); + setValue(_customPicsPath, "customPics"); } void PathsSettings::setThemesPath(const QString &_themesPath) @@ -97,24 +97,24 @@ void PathsSettings::setThemesPath(const QString &_themesPath) void PathsSettings::setCardDatabasePath(const QString &_cardDatabasePath) { - setValue(_cardDatabasePath, "carddatabase"); + setValue(_cardDatabasePath, "cardDatabase"); emit cardDatabasePathChanged(); } void PathsSettings::setCustomCardDatabasePath(const QString &_customCardDatabasePath) { - setValue(_customCardDatabasePath, "customsets"); + setValue(_customCardDatabasePath, "customSets"); emit cardDatabasePathChanged(); } void PathsSettings::setTokenDatabasePath(const QString &_tokenDatabasePath) { - setValue(_tokenDatabasePath, "tokendatabase"); + setValue(_tokenDatabasePath, "tokenDatabase"); emit cardDatabasePathChanged(); } void PathsSettings::setSpoilerDatabasePath(const QString &_spoilerDatabasePath) { - setValue(_spoilerDatabasePath, "spoilerdatabase"); + setValue(_spoilerDatabasePath, "spoilerDatabase"); emit cardDatabasePathChanged(); } diff --git a/libcockatrice_settings/libcockatrice/settings/personal_settings.cpp b/libcockatrice_settings/libcockatrice/settings/personal_settings.cpp index aec8d4df6..d1cb74d09 100644 --- a/libcockatrice_settings/libcockatrice/settings/personal_settings.cpp +++ b/libcockatrice_settings/libcockatrice/settings/personal_settings.cpp @@ -10,61 +10,6 @@ QString PersonalSettings::getLang() const return getValue("lang", QString(), QString(), QString()).toString(); } -QString PersonalSettings::getClientID() -{ - return getValue("clientid", QString(), QString(), "notset").toString(); -} - -QString PersonalSettings::getClientVersion() -{ - return getValue("clientversion", QString(), QString(), "notset").toString(); -} - -int PersonalSettings::getKeepAlive() const -{ - return getValue("keepalive", QString(), QString(), 3).toInt(); -} - -int PersonalSettings::getTimeOut() const -{ - return getValue("timeout", QString(), QString(), 5).toInt(); -} - -bool PersonalSettings::getPicDownload() const -{ - return getValue("picturedownload", QString(), QString(), true).toBool(); -} - -bool PersonalSettings::getShowStatusBar() const -{ - return getValue("showStatusBar", QString(), QString(), false).toBool(); -} - -int PersonalSettings::getMaxFontSize() const -{ - return getValue("maxfontsize", "game", QString(), 12).toInt(); -} - -QString PersonalSettings::getHighlightWords() const -{ - return getValue("highlightWords", QString(), QString(), "").toString(); -} - -QString PersonalSettings::getHomeTabBackgroundSource() const -{ - return getValue("background", "home", QString(), "themed").toString(); -} - -int PersonalSettings::getHomeTabBackgroundShuffleFrequency() const -{ - return getValue("shuffleTimer", "home/background", QString(), 0).toInt(); -} - -bool PersonalSettings::getHomeTabDisplayCardName() const -{ - return getValue("displayCardName", "home/background", QString(), true).toBool(); -} - bool PersonalSettings::getShowTipsOnStartup() const { return getValue("showTips", "tipOfDay", QString(), true).toBool(); @@ -80,78 +25,12 @@ QList PersonalSettings::getSeenTips() const return tips; } -bool PersonalSettings::getDownloadSpoilersStatus() const -{ - return getValue("downloadspoilers", QString(), QString(), false).toBool(); -} - void PersonalSettings::setLang(const QString &_lang) { setValue(_lang, "lang"); emit langChanged(); } -void PersonalSettings::setClientID(const QString &_clientID) -{ - setValue(_clientID, "clientid"); -} - -void PersonalSettings::setClientVersion(const QString &_clientVersion) -{ - setValue(_clientVersion, "clientversion"); -} - -void PersonalSettings::setPicDownload(bool _picDownload) -{ - setValue(_picDownload, "picturedownload"); - emit picDownloadChanged(); -} - -void PersonalSettings::setShowStatusBar(bool value) -{ - setValue(value, "showStatusBar"); - emit showStatusBarChanged(value); -} - -void PersonalSettings::setMaxFontSize(int _max) -{ - setValue(_max, "maxfontsize", "game"); -} - -QString PersonalSettings::getThemeName() const -{ - return getValue("themeName", QString(), QString()).toString(); -} - -void PersonalSettings::setThemeName(const QString &_themeName) -{ - setValue(_themeName, "themeName"); - emit themeNameChanged(); -} - -void PersonalSettings::setHighlightWords(const QString &_highlightWords) -{ - setValue(_highlightWords, "highlightWords"); -} - -void PersonalSettings::setHomeTabBackgroundSource(const QString &_backgroundSource) -{ - setValue(_backgroundSource, "background", "home"); - emit homeTabBackgroundSourceChanged(); -} - -void PersonalSettings::setHomeTabBackgroundShuffleFrequency(int _frequency) -{ - setValue(_frequency, "shuffleTimer", "home/background"); - emit homeTabBackgroundShuffleFrequencyChanged(); -} - -void PersonalSettings::setHomeTabDisplayCardName(bool _displayCardName) -{ - setValue(_displayCardName, "displayCardName", "home/background"); - emit homeTabDisplayCardNameChanged(); -} - void PersonalSettings::setShowTipsOnStartup(bool _showTipsOnStartup) { setValue(_showTipsOnStartup, "showTips", "tipOfDay"); @@ -165,9 +44,3 @@ void PersonalSettings::setSeenTips(const QList &_seenTips) } setValue(QVariant::fromValue(storedTipList), "seenTips", "tipOfDay"); } - -void PersonalSettings::setDownloadSpoilerStatus(bool _spoilerStatus) -{ - setValue(_spoilerStatus, "downloadspoilers"); - emit downloadSpoilerStatusChanged(); -} diff --git a/libcockatrice_settings/libcockatrice/settings/personal_settings.h b/libcockatrice_settings/libcockatrice/settings/personal_settings.h index 2c79eec8f..04eab2a8a 100644 --- a/libcockatrice_settings/libcockatrice/settings/personal_settings.h +++ b/libcockatrice_settings/libcockatrice/settings/personal_settings.h @@ -14,46 +14,15 @@ class PersonalSettings : public SettingsManager, public IPersonalSettingsProvide public: [[nodiscard]] QString getLang() const override; - [[nodiscard]] QString getClientID() override; - [[nodiscard]] QString getClientVersion() override; - [[nodiscard]] int getKeepAlive() const override; - [[nodiscard]] int getTimeOut() const override; - [[nodiscard]] bool getPicDownload() const override; - [[nodiscard]] bool getShowStatusBar() const override; - [[nodiscard]] int getMaxFontSize() const override; - [[nodiscard]] QString getHighlightWords() const override; - [[nodiscard]] QString getHomeTabBackgroundSource() const override; - [[nodiscard]] int getHomeTabBackgroundShuffleFrequency() const override; - [[nodiscard]] QString getThemeName() const; - void setThemeName(const QString &_themeName); - [[nodiscard]] bool getHomeTabDisplayCardName() const override; [[nodiscard]] bool getShowTipsOnStartup() const override; [[nodiscard]] QList getSeenTips() const override; - [[nodiscard]] bool getDownloadSpoilersStatus() const override; void setLang(const QString &_lang); - void setClientID(const QString &_clientID); - void setClientVersion(const QString &_clientVersion); - void setPicDownload(bool _picDownload); - void setShowStatusBar(bool value); - void setMaxFontSize(int _max); - void setHighlightWords(const QString &_highlightWords); - void setHomeTabBackgroundSource(const QString &_backgroundSource); - void setHomeTabBackgroundShuffleFrequency(int _frequency); - void setHomeTabDisplayCardName(bool _displayCardName); void setShowTipsOnStartup(bool _showTipsOnStartup); void setSeenTips(const QList &_seenTips); - void setDownloadSpoilerStatus(bool _spoilerStatus); signals: void langChanged(); - void themeNameChanged(); - void picDownloadChanged(); - void showStatusBarChanged(bool state); - void homeTabBackgroundSourceChanged(); - void homeTabBackgroundShuffleFrequencyChanged(); - void homeTabDisplayCardNameChanged(); - void downloadSpoilerStatusChanged(); public: explicit PersonalSettings(const QString &settingPath, QObject *parent = nullptr); diff --git a/libcockatrice_settings/libcockatrice/settings/recents_settings.cpp b/libcockatrice_settings/libcockatrice/settings/recents_settings.cpp index 76bc4069e..5f16af179 100644 --- a/libcockatrice_settings/libcockatrice/settings/recents_settings.cpp +++ b/libcockatrice_settings/libcockatrice/settings/recents_settings.cpp @@ -9,16 +9,16 @@ RecentsSettings::RecentsSettings(const QString &settingPath, QObject *parent) QStringList RecentsSettings::getRecentlyOpenedDeckPaths() const { - return getValue("deckpaths").toStringList(); + return getValue("deckPaths").toStringList(); } void RecentsSettings::clearRecentlyOpenedDeckPaths() { - deleteValue("deckpaths"); + deleteValue("deckPaths"); emit recentlyOpenedDeckPathsChanged(); } void RecentsSettings::updateRecentlyOpenedDeckPaths(const QString &deckPath) { - auto deckPaths = getValue("deckpaths").toStringList(); + auto deckPaths = getValue("deckPaths").toStringList(); deckPaths.removeAll(deckPath); deckPaths.prepend(deckPath); @@ -27,7 +27,7 @@ void RecentsSettings::updateRecentlyOpenedDeckPaths(const QString &deckPath) deckPaths.removeLast(); } - setValue(deckPaths, "deckpaths"); + setValue(deckPaths, "deckPaths"); emit recentlyOpenedDeckPathsChanged(); } diff --git a/libcockatrice_settings/libcockatrice/settings/servers_settings.cpp b/libcockatrice_settings/libcockatrice/settings/servers_settings.cpp index d9b98e036..811b0c842 100644 --- a/libcockatrice_settings/libcockatrice/settings/servers_settings.cpp +++ b/libcockatrice_settings/libcockatrice/settings/servers_settings.cpp @@ -10,28 +10,28 @@ ServersSettings::ServersSettings(const QString &settingPath, QObject *parent) void ServersSettings::setPreviousHostLogin(int previous) { - setValue(previous, "previoushostlogin"); + setValue(previous, "previousHostLogin"); } int ServersSettings::getPreviousHostLogin() const { - QVariant previous = getValue("previoushostlogin"); + QVariant previous = getValue("previousHostLogin"); return previous == QVariant() ? 1 : previous.toInt(); } void ServersSettings::setPreviousHostList(QStringList list) { - setValue(list, "previoushosts"); + setValue(list, "previousHosts"); } QStringList ServersSettings::getPreviousHostList() const { - return getValue("previoushosts").toStringList(); + return getValue("previousHosts").toStringList(); } void ServersSettings::setPrevioushostName(const QString &name) { - setValue(name, "previoushostName"); + setValue(name, "previousHostName"); } QString ServersSettings::getSaveName(QString defaultname) @@ -50,7 +50,7 @@ QString ServersSettings::getSite(QString defaultSite) QString ServersSettings::getPrevioushostName() const { - QVariant value = getValue("previoushostName"); + QVariant value = getValue("previousHostName"); return value == QVariant() ? "Rooster Ranges" : value.toString(); } @@ -110,56 +110,56 @@ bool ServersSettings::getSavePassword() const void ServersSettings::setAutoConnect(int autoconnect) { - setValue(autoconnect, "auto_connect"); + setValue(autoconnect, "autoConnect"); } int ServersSettings::getAutoConnect() const { - QVariant autoconnect = getValue("auto_connect"); + QVariant autoconnect = getValue("autoConnect"); return autoconnect == QVariant() ? 0 : autoconnect.toInt(); } void ServersSettings::setFPHostName(QString hostname) { - setValue(hostname, "fphostname"); + setValue(hostname, "fpHostName"); } QString ServersSettings::getFPHostname(QString defaultHost) const { - QVariant hostname = getValue("fphostname"); + QVariant hostname = getValue("fpHostName"); return hostname == QVariant() ? std::move(defaultHost) : hostname.toString(); } void ServersSettings::setFPPort(QString port) { - setValue(port, "fpport"); + setValue(port, "fpPort"); } QString ServersSettings::getFPPort(QString defaultPort) const { - QVariant port = getValue("fpport"); + QVariant port = getValue("fpPort"); return port == QVariant() ? std::move(defaultPort) : port.toString(); } void ServersSettings::setFPPlayerName(QString playerName) { - setValue(playerName, "fpplayername"); + setValue(playerName, "fpPlayerName"); } QString ServersSettings::getFPPlayerName(QString defaultName) const { - QVariant name = getValue("fpplayername"); + QVariant name = getValue("fpPlayerName"); return name == QVariant() ? std::move(defaultName) : name.toString(); } void ServersSettings::setClearDebugLogStatus(bool abIsChecked) { - setValue(abIsChecked, "save_debug_log"); + setValue(abIsChecked, "saveDebugLog"); } bool ServersSettings::getClearDebugLogStatus(bool abDefaultValue) const { - QVariant cbFlushLog = getValue("save_debug_log"); + QVariant cbFlushLog = getValue("saveDebugLog"); return cbFlushLog == QVariant() ? abDefaultValue : cbFlushLog.toBool(); } @@ -293,3 +293,48 @@ bool ServersSettings::updateExistingServer(QString saveName, } return false; } + +int ServersSettings::findServerIndex(const QString &host, const QString &port) const +{ + int size = getValue("totalServers", "server", "server_details").toInt(); + + for (int i = 0; i <= size; ++i) { + QString storedHost = getValue(QString("server%1").arg(i), "server", "server_details").toString(); + QString storedPort = getValue(QString("port%1").arg(i), "server", "server_details").toString(); + + if (storedHost == host && storedPort == port) { + return i; + } + } + + return -1; +} + +bool ServersSettings::hasUsername(const QString &host, const QString &port) const +{ + int index = findServerIndex(host, port); + if (index < 0) { + return false; + } + + QString user = getValue(QString("username%1").arg(index), "server", "server_details").toString(); + return !user.isEmpty(); +} + +bool ServersSettings::hasCredentials(const QString &host, const QString &port) const +{ + int index = findServerIndex(host, port); + if (index < 0) { + return false; + } + + bool save = getValue(QString("savePassword%1").arg(index), "server", "server_details").toBool(); + QString password = getValue(QString("password%1").arg(index), "server", "server_details").toString(); + + return save && !password.isEmpty(); +} + +bool ServersSettings::hasLoginData(const QString &host, const QString &port) const +{ + return hasUsername(host, port) && hasCredentials(host, port); +} diff --git a/libcockatrice_settings/libcockatrice/settings/servers_settings.h b/libcockatrice_settings/libcockatrice/settings/servers_settings.h index 40fa996fb..f9803a158 100644 --- a/libcockatrice_settings/libcockatrice/settings/servers_settings.h +++ b/libcockatrice_settings/libcockatrice/settings/servers_settings.h @@ -61,6 +61,10 @@ public: QString password, bool savePassword, QString site = QString()); + int findServerIndex(const QString &host, const QString &port) const; + bool hasUsername(const QString &host, const QString &port) const; + bool hasCredentials(const QString &host, const QString &port) const; + bool hasLoginData(const QString &host, const QString &port) const; bool updateExistingServerWithoutLoss(QString saveName, QString serv = QString(), diff --git a/libcockatrice_settings/libcockatrice/settings/settings_migration.cpp b/libcockatrice_settings/libcockatrice/settings/settings_migration.cpp index 078e5ac16..37dc9a0a0 100644 --- a/libcockatrice_settings/libcockatrice/settings/settings_migration.cpp +++ b/libcockatrice_settings/libcockatrice/settings/settings_migration.cpp @@ -35,66 +35,100 @@ static void migrateSoundSettings(const QString &settingsPath, QSettings &globalI QSettings soundIni(settingsPath + "sound.ini", QSettings::IniFormat); soundIni.setValue("sound/enabled", globalIni.value("sound/enabled", false)); soundIni.setValue("sound/theme", globalIni.value("sound/theme")); - soundIni.setValue("sound/mastervolume", globalIni.value("sound/mastervolume", 100)); + soundIni.setValue("sound/masterVolume", globalIni.value("sound/mastervolume", 100)); } static void migrateGameSettings(const QString &settingsPath, QSettings &globalIni) { - bool hasGameKeys = false; - globalIni.beginGroup("game"); - if (!globalIni.childKeys().isEmpty()) { - hasGameKeys = true; - } - QStringList gameKeys = globalIni.childKeys(); - globalIni.endGroup(); + const QMap gameKeyMap = { + {"game/maxplayers", "game/maxPlayers"}, + {"game/gamedescription", "game/gameDescription"}, + {"game/gametypes", "game/gameTypes"}, + {"game/onlybuddies", "game/onlyBuddies"}, + {"game/onlyregistered", "game/onlyRegistered"}, + {"game/spectatorsallowed", "game/spectatorsAllowed"}, + {"game/spectatorsneedpassword", "game/spectatorsNeedPassword"}, + {"game/spectatorscantalk", "game/spectatorsCanTalk"}, + {"game/spectatorscanseeeverything", "game/spectatorsCanSeeEverything"}, + {"game/creategameasspectator", "game/createGameAsSpectator"}, + {"game/defaultstartinglifetotal", "game/defaultStartingLifeTotal"}, + {"game/sharedecklistsonload", "game/shareDecklistsOnLoad"}, + {"game/remembergamesettings", "game/rememberGameSettings"}, + {"localgameoptions/maxplayers", "localgameoptions/maxPlayers"}, + {"localgameoptions/startinglifetotal", "localgameoptions/startingLifeTotal"}, + {"localgameoptions/remembersettings", "localgameoptions/rememberSettings"}, + }; - globalIni.beginGroup("localgameoptions"); - if (!globalIni.childKeys().isEmpty()) { - hasGameKeys = true; + bool hasAny = false; + for (auto it = gameKeyMap.constBegin(); it != gameKeyMap.constEnd(); ++it) { + if (globalIni.contains(it.key())) { + hasAny = true; + break; + } } - QStringList localGameKeys = globalIni.childKeys(); - globalIni.endGroup(); - - if (!hasGameKeys) { + if (!hasAny) { return; } QSettings gameIni(settingsPath + "game.ini", QSettings::IniFormat); - for (const auto &key : gameKeys) { - if (key == "maxfontsize") { - continue; + for (auto it = gameKeyMap.constBegin(); it != gameKeyMap.constEnd(); ++it) { + if (globalIni.contains(it.key())) { + gameIni.setValue(it.value(), globalIni.value(it.key())); } - gameIni.setValue("game/" + key, globalIni.value("game/" + key)); - } - for (const auto &key : localGameKeys) { - gameIni.setValue("localgameoptions/" + key, globalIni.value("localgameoptions/" + key)); } } static void migrateChatSettings(const QString &settingsPath, QSettings &globalIni) { - globalIni.beginGroup("chat"); - QStringList chatKeys = globalIni.childKeys(); - globalIni.endGroup(); - - if (chatKeys.isEmpty()) { + const QMap chatKeyMap = { + {"chat/mention", "chat/mention"}, + {"chat/mentioncompleter", "chat/mentionCompleter"}, + {"chat/mentioncolor", "chat/mentionColor"}, + {"chat/highlightcolor", "chat/highlightColor"}, + {"chat/mentionforeground", "chat/mentionForeground"}, + {"chat/highlightforeground", "chat/highlightForeground"}, + {"chat/ignore_unregistered", "chat/ignoreUnregistered"}, + {"chat/ignore_unregistered_messages", "chat/ignoreUnregisteredMessages"}, + {"chat/ignore_nonbuddy_messages", "chat/ignoreNonBuddyMessages"}, + {"chat/showmessagepopups", "chat/showMessagePopups"}, + {"chat/showmentionpopups", "chat/showMentionPopups"}, + {"chat/roomhistory", "chat/roomHistory"}, + {"chat/highlightwords", "chat/highlightWords"}, + // Legacy highlight words lived under [personal], but the chat settings + // class reads them from [chat] + {"personal/highlightWords", "chat/highlightWords"}, + }; + bool hasAny = false; + for (auto it = chatKeyMap.constBegin(); it != chatKeyMap.constEnd(); ++it) { + if (globalIni.contains(it.key())) { + hasAny = true; + break; + } + } + if (!hasAny) { return; } QSettings chatIni(settingsPath + "chat.ini", QSettings::IniFormat); - for (const auto &key : chatKeys) { - chatIni.setValue("chat/" + key, globalIni.value("chat/" + key)); + for (auto it = chatKeyMap.constBegin(); it != chatKeyMap.constEnd(); ++it) { + if (globalIni.contains(it.key())) { + chatIni.setValue(it.value(), globalIni.value(it.key())); + } } } static void migrateCacheStorageSettings(const QString &settingsPath, QSettings &globalIni) { - const QStringList cacheKeys = {"personal/pixmapCacheSize", "personal/networkCacheSize", "personal/redirectCacheTtl", - "personal/cardPictureLoaderCacheMethod", - "personal/localCardImageStorageNamingScheme"}; + const QMap cacheStorageKeyMap = { + {"personal/pixmapCacheSize", "cache_storage/pixmapCacheSize"}, + {"personal/networkCacheSize", "cache_storage/networkCacheSize"}, + {"personal/redirectCacheTtl", "cache_storage/redirectCacheTtl"}, + {"personal/cardPictureLoaderCacheMethod", "cache_storage/cardPictureLoaderCacheMethod"}, + {"personal/localCardImageStorageNamingScheme", "cache_storage/localCardImageStorageNamingScheme"}, + }; bool hasAny = false; - for (const auto &key : cacheKeys) { - if (globalIni.contains(key)) { + for (auto it = cacheStorageKeyMap.constBegin(); it != cacheStorageKeyMap.constEnd(); ++it) { + if (globalIni.contains(it.key())) { hasAny = true; break; } @@ -104,9 +138,9 @@ static void migrateCacheStorageSettings(const QString &settingsPath, QSettings & } QSettings cacheStorageIni(settingsPath + "cache_storage.ini", QSettings::IniFormat); - for (const auto &key : cacheKeys) { - if (globalIni.contains(key)) { - cacheStorageIni.setValue(key, globalIni.value(key)); + for (auto it = cacheStorageKeyMap.constBegin(); it != cacheStorageKeyMap.constEnd(); ++it) { + if (globalIni.contains(it.key())) { + cacheStorageIni.setValue(it.value(), globalIni.value(it.key())); } } } @@ -120,9 +154,9 @@ static void migrateUpdatesSettings(const QString &settingsPath, QSettings &globa {"personal/cardUpdateCheckInterval", "updates/cardUpdateCheckInterval"}, {"personal/lastCardUpdateCheck", "updates/lastCardUpdateCheck"}, {"personal/alwaysEnableNewSets", "updates/alwaysEnableNewSets"}, - {"personal/updatenotification", "updates/updatenotification"}, - {"personal/newversionnotification", "updates/newversionnotification"}, - {"personal/updatereleasechannel", "updates/updatereleasechannel"}, + {"personal/updatenotification", "updates/updateNotification"}, + {"personal/newversionnotification", "updates/newVersionNotification"}, + {"personal/updatereleasechannel", "updates/updateReleaseChannel"}, }; bool hasAny = false; for (auto it = updateKeyMap.constBegin(); it != updateKeyMap.constEnd(); ++it) { @@ -145,19 +179,7 @@ static void migrateUpdatesSettings(const QString &settingsPath, QSettings &globa static void migratePersonalSettings(const QString &settingsPath, QSettings &globalIni) { - const QStringList personalRootKeys = {"personal/lang", "personal/highlightWords"}; - const QMap personalKeyMap = { - {"theme/name", "personal/themeName"}, - {"personal/clientid", "personal/clientid"}, - {"personal/clientversion", "personal/clientversion"}, - {"personal/keepalive", "personal/keepalive"}, - {"personal/timeout", "personal/timeout"}, - {"personal/picturedownload", "personal/picturedownload"}, - {"personal/showStatusBar", "personal/showStatusBar"}, - {"game/maxfontsize", "game/maxfontsize"}, - {"personal/downloadspoilers", "personal/downloadspoilers"}, - }; - const QStringList homeKeys = {"home/background", "home/background/shuffleTimer", "home/background/displayCardName"}; + const QStringList personalRootKeys = {"personal/lang"}; const QStringList tipKeys = {"tipOfDay/showTips", "tipOfDay/seenTips"}; bool hasAny = false; @@ -166,16 +188,6 @@ static void migratePersonalSettings(const QString &settingsPath, QSettings &glob hasAny = true; } } - for (auto it = personalKeyMap.constBegin(); it != personalKeyMap.constEnd(); ++it) { - if (globalIni.contains(it.key())) { - hasAny = true; - } - } - for (const auto &key : homeKeys) { - if (globalIni.contains(key)) { - hasAny = true; - } - } for (const auto &key : tipKeys) { if (globalIni.contains(key)) { hasAny = true; @@ -191,16 +203,6 @@ static void migratePersonalSettings(const QString &settingsPath, QSettings &glob personalIni.setValue(key, globalIni.value(key)); } } - for (auto it = personalKeyMap.constBegin(); it != personalKeyMap.constEnd(); ++it) { - if (globalIni.contains(it.key())) { - personalIni.setValue(it.value(), globalIni.value(it.key())); - } - } - for (const auto &key : homeKeys) { - if (globalIni.contains(key)) { - personalIni.setValue(key, globalIni.value(key)); - } - } for (const auto &key : tipKeys) { if (globalIni.contains(key)) { personalIni.setValue(key, globalIni.value(key)); @@ -210,39 +212,33 @@ static void migratePersonalSettings(const QString &settingsPath, QSettings &glob static void migrateCardsDisplaySettings(const QString &settingsPath, QSettings &globalIni) { - const QStringList cardsRootKeys = { - "cards/displaycardnames", - "cards/roundcardcorners", - "cards/overrideallcardartwithpersonalpreference", - "cards/bumpsetswithcardsindecktotop", - "cards/printingselectorsortorder", - "cards/printingselectorcardsize", - "cards/includerebalancedcards", - "cards/printingselectornavigationbuttonsvisible", - "cards/tapanimation", - "cards/autorotatesidewayslayoutcards", - "cards/scaleCards", - "cards/verticalCardOverlapPercent", - "cards/cardinfoviewmode", + const QMap cardsKeyMap = { + {"cards/displaycardnames", "cards/displayCardNames"}, + {"cards/roundcardcorners", "cards/roundCardCorners"}, + {"cards/overrideallcardartwithpersonalpreference", "cards/overrideAllCardArtWithPersonalPreference"}, + {"cards/bumpsetswithcardsindecktotop", "cards/bumpSetsWithCardsInDeckToTop"}, + {"cards/includerebalancedcards", "cards/includerebalancedcards"}, + {"cards/tapanimation", "cards/tapAnimation"}, + {"cards/autorotatesidewayslayoutcards", "cards/autoRotateSidewaysLayoutCards"}, + {"cards/scaleCards", "cards/scaleCards"}, + {"cards/verticalCardOverlapPercent", "cards/verticalCardOverlapPercent"}, + {"cards/cardinfoviewmode", "cards/cardInfoViewMode"}, + {"cards/printingselectorsortorder", "cards/printingSelector/sortOrder"}, + {"cards/printingselectornavigationbuttonsvisible", "cards/printingSelector/navigationButtonsVisible"}, + {"cards/printingselectorcardsize", "cards/cardSize/printingSelector"}, + {"interface/visualdeckstoragecardsize", "cards/cardSize/visualDeckStorage"}, + {"interface/visualdatabasedisplaycardsize", "cards/cardSize/visualDatabaseDisplay"}, + {"interface/visualdeckeditorcardsize", "cards/cardSize/visualDeckEditor"}, + {"interface/edhreccardsize", "cards/cardSize/edhrec"}, + {"interface/archidektpreviewsize", "cards/cardSize/archidektPreview"}, + {"interface/visualdeckeditorsamplehandsize", "cards/cardSize/sampleHandSize"}, }; - const QStringList cardsInterfaceKeys = {"interface/deckeditorbannercardcomboboxvisible", - "interface/deckeditortagswidgetvisible"}; - const QStringList menuKeys = {"menu/showshortcuts", "menu/showgameselectorfiltertoolbar"}; bool hasAny = false; - for (const auto &key : cardsRootKeys) { - if (globalIni.contains(key)) { - hasAny = true; - } - } - for (const auto &key : cardsInterfaceKeys) { - if (globalIni.contains(key)) { - hasAny = true; - } - } - for (const auto &key : menuKeys) { - if (globalIni.contains(key)) { + for (auto it = cardsKeyMap.constBegin(); it != cardsKeyMap.constEnd(); ++it) { + if (globalIni.contains(it.key())) { hasAny = true; + break; } } if (!hasAny) { @@ -250,60 +246,71 @@ static void migrateCardsDisplaySettings(const QString &settingsPath, QSettings & } QSettings cardsIni(settingsPath + "cards_display.ini", QSettings::IniFormat); - for (const auto &key : cardsRootKeys) { - if (globalIni.contains(key)) { - cardsIni.setValue(key, globalIni.value(key)); + for (auto it = cardsKeyMap.constBegin(); it != cardsKeyMap.constEnd(); ++it) { + if (globalIni.contains(it.key())) { + cardsIni.setValue(it.value(), globalIni.value(it.key())); } } - for (const auto &key : cardsInterfaceKeys) { - if (globalIni.contains(key)) { - cardsIni.setValue(key, globalIni.value(key)); +} + +static void migrateCardCounterSettings(const QString &settingsPath, QSettings &globalIni) +{ + QStringList counterKeys; + const QStringList allKeys = globalIni.allKeys(); + for (const auto &key : allKeys) { + if (key.startsWith("cards/counters/")) { + counterKeys.append(key); } } - for (const auto &key : menuKeys) { - if (globalIni.contains(key)) { - cardsIni.setValue(key, globalIni.value(key)); - } + if (counterKeys.isEmpty()) { + return; + } + + QSettings countersIni(settingsPath + "card_counters.ini", QSettings::IniFormat); + for (const auto &key : counterKeys) { + countersIni.setValue(key, globalIni.value(key)); } } static void migrateInterfaceSettings(const QString &settingsPath, QSettings &globalIni) { - const QStringList interfaceRootKeys = { - "interface/usetearoffmenus", - "interface/cardViewInitialRowsMax", - "interface/cardViewExpandedRowsMax", - "interface/closeEmptyCardView", - "interface/focusCardViewSearchBar", - "interface/keepGameChatFocus", - "interface/notificationsenabled", - "interface/specnotificationsenabled", - "interface/buddyconnectnotificationsenabled", - "interface/doubleclicktoplay", - "interface/clickPlaysAllSelected", - "interface/playtostack", - "interface/doNotDeleteArrowsInSubPhases", - "interface/startinghandsize", - "interface/annotatetokens", - "interface/showlassoselectioncount", - "interface/showpersistentselectioncount", - "interface/showsubtypeselectiontally", - "interface/leftjustified", - "interface/min_players_multicolumn", - "interface/knownmissingfeatures", + const QMap interfaceKeyMap = { + {"interface/usetearoffmenus", "interface/useTearOffMenus"}, + {"interface/cardViewInitialRowsMax", "interface/cardViewInitialRowsMax"}, + {"interface/cardViewExpandedRowsMax", "interface/cardViewExpandedRowsMax"}, + {"interface/closeEmptyCardView", "interface/closeEmptyCardView"}, + {"interface/focusCardViewSearchBar", "interface/focusCardViewSearchBar"}, + {"interface/keepGameChatFocus", "interface/keepGameChatFocus"}, + {"interface/doubleclicktoplay", "interface/doubleClickToPlay"}, + {"interface/clickPlaysAllSelected", "interface/clickPlaysAllSelected"}, + {"interface/playtostack", "interface/playToStack"}, + {"interface/doNotDeleteArrowsInSubPhases", "interface/doNotDeleteArrowsInSubPhases"}, + {"interface/startinghandsize", "interface/startingHandSize"}, + {"interface/annotatetokens", "interface/annotateTokens"}, + {"interface/showlassoselectioncount", "interface/showLassoSelectionCount"}, + {"interface/showpersistentselectioncount", "interface/showPersistentSelectionCount"}, + {"interface/tallyType", "interface/tallyType"}, + {"interface/leftjustified", "interface/leftJustified"}, + {"interface/min_players_multicolumn", "interface/minPlayersMulticolumn"}, + {"hand/horizontal", "hand/horizontal"}, + {"table/invert_vertical", "table/invertVertical"}, + {"replay/rewindBufferingMs", "replay/rewindBufferingMs"}, + {"replay/fastForwardSpeed", "replay/fastForwardSpeed"}, + {"zoneview/groupby", "zoneview/groupBy"}, + {"zoneview/sortby", "zoneview/sortBy"}, + {"zoneview/pileview", "zoneview/pileView"}, + {"personal/showStatusBar", "interface/showStatusBar"}, + {"menu/showshortcuts", "interface/showShortcuts"}, + {"menu/showgameselectorfiltertoolbar", "interface/showGameSelectorFilterToolbar"}, + {"interface/notificationsenabled", "interface/notifications/enabled"}, + {"interface/specnotificationsenabled", "interface/notifications/spectatorsEnabled"}, + {"interface/buddyconnectnotificationsenabled", "interface/notifications/buddyConnectEnabled"}, }; - const QStringList interfaceSubKeys = { - "hand/horizontal", "table/invert_vertical", "editor/openDeckInNewTab", "replay/rewindBufferingMs", - "appearance/styleUserList", "zoneview/groupby", "zoneview/sortby", "zoneview/pileview"}; bool hasAny = false; - for (const auto &key : interfaceRootKeys) { - if (globalIni.contains(key)) { - hasAny = true; - } - } - for (const auto &key : interfaceSubKeys) { - if (globalIni.contains(key)) { + for (auto it = interfaceKeyMap.constBegin(); it != interfaceKeyMap.constEnd(); ++it) { + if (globalIni.contains(it.key())) { hasAny = true; + break; } } if (!hasAny) { @@ -311,63 +318,159 @@ static void migrateInterfaceSettings(const QString &settingsPath, QSettings &glo } QSettings interfaceIni(settingsPath + "interface.ini", QSettings::IniFormat); - for (const auto &key : interfaceRootKeys) { - if (globalIni.contains(key)) { - interfaceIni.setValue(key, globalIni.value(key)); + for (auto it = interfaceKeyMap.constBegin(); it != interfaceKeyMap.constEnd(); ++it) { + if (globalIni.contains(it.key())) { + interfaceIni.setValue(it.value(), globalIni.value(it.key())); } } - for (const auto &key : interfaceSubKeys) { - if (globalIni.contains(key)) { - interfaceIni.setValue(key, globalIni.value(key)); +} + +static void migrateDownloadSettings(const QString &settingsPath, QSettings &globalIni) +{ + const QMap downloadKeyMap = { + {"personal/picturedownload", "downloads/pictureDownload"}, + {"personal/downloadspoilers", "downloads/downloadSpoilers"}, + }; + bool hasAny = false; + for (auto it = downloadKeyMap.constBegin(); it != downloadKeyMap.constEnd(); ++it) { + if (globalIni.contains(it.key())) { + hasAny = true; + break; + } + } + if (!hasAny) { + return; + } + + QSettings downloadsIni(settingsPath + "downloads.ini", QSettings::IniFormat); + for (auto it = downloadKeyMap.constBegin(); it != downloadKeyMap.constEnd(); ++it) { + if (globalIni.contains(it.key())) { + downloadsIni.setValue(it.value(), globalIni.value(it.key())); + } + } +} + +static void migrateAppearanceSettings(const QString &settingsPath, QSettings &globalIni) +{ + const QMap appearanceKeyMap = { + {"theme/name", "appearance/themeName"}, + {"game/maxfontsize", "appearance/maxFontSize"}, + {"home/background", "appearance/homeTabBackgroundSource"}, + {"home/background/shuffleTimer", "appearance/homeTabBackgroundShuffleFrequency"}, + {"home/background/displayCardName", "appearance/homeTabDisplayCardName"}, + {"appearance/styleUserList", "appearance/styleUserList"}, + }; + bool hasAny = false; + for (auto it = appearanceKeyMap.constBegin(); it != appearanceKeyMap.constEnd(); ++it) { + if (globalIni.contains(it.key())) { + hasAny = true; + break; + } + } + if (!hasAny) { + return; + } + + QSettings appearanceIni(settingsPath + "appearance.ini", QSettings::IniFormat); + for (auto it = appearanceKeyMap.constBegin(); it != appearanceKeyMap.constEnd(); ++it) { + if (globalIni.contains(it.key())) { + appearanceIni.setValue(it.value(), globalIni.value(it.key())); + } + } +} + +static void migrateNetworkSettings(const QString &settingsPath, QSettings &globalIni) +{ + const QMap networkKeyMap = { + {"personal/clientid", "network/clientId"}, + {"personal/clientversion", "network/clientVersion"}, + {"personal/keepalive", "network/keepAlive"}, + {"personal/timeout", "network/timeout"}, + {"interface/knownmissingfeatures", "network/knownMissingFeatures"}, + }; + bool hasAny = false; + for (auto it = networkKeyMap.constBegin(); it != networkKeyMap.constEnd(); ++it) { + if (globalIni.contains(it.key())) { + hasAny = true; + break; + } + } + if (!hasAny) { + return; + } + + QSettings networkIni(settingsPath + "network.ini", QSettings::IniFormat); + for (auto it = networkKeyMap.constBegin(); it != networkKeyMap.constEnd(); ++it) { + if (globalIni.contains(it.key())) { + networkIni.setValue(it.value(), globalIni.value(it.key())); } } } static void migratePathsSettings(const QString &settingsPath, QSettings &globalIni) { - globalIni.beginGroup("paths"); - QStringList pathsKeys = globalIni.childKeys(); - globalIni.endGroup(); - if (pathsKeys.isEmpty()) { + const QMap pathsKeyMap = { + {"paths/decks", "paths/decks"}, + {"paths/filters", "paths/filters"}, + {"paths/replays", "paths/replays"}, + {"paths/pics", "paths/pics"}, + {"paths/custompics", "paths/customPics"}, + {"paths/themes", "paths/themes"}, + {"paths/carddatabase", "paths/cardDatabase"}, + {"paths/customsets", "paths/customSets"}, + {"paths/tokendatabase", "paths/tokenDatabase"}, + {"paths/spoilerdatabase", "paths/spoilerDatabase"}, + {"paths/redirects", "paths/redirects"}, + }; + bool hasAny = false; + for (auto it = pathsKeyMap.constBegin(); it != pathsKeyMap.constEnd(); ++it) { + if (globalIni.contains(it.key())) { + hasAny = true; + break; + } + } + if (!hasAny) { return; } QSettings pathsIni(settingsPath + "paths.ini", QSettings::IniFormat); - for (const auto &key : pathsKeys) { - pathsIni.setValue("paths/" + key, globalIni.value("paths/" + key)); + for (auto it = pathsKeyMap.constBegin(); it != pathsKeyMap.constEnd(); ++it) { + if (globalIni.contains(it.key())) { + pathsIni.setValue(it.value(), globalIni.value(it.key())); + } } } static void migrateVisualDeckStorageSettings(const QString &settingsPath, QSettings &globalIni) { - const QStringList vdsKeys = {"interface/visualdeckstoragecardsize", - "interface/visualdeckstoragesortingorder", - "interface/visualdeckstorageshowfolders", - "interface/visualdeckstorageshowtagfilter", - "interface/visualdeckstoragedefaulttagslist", - "interface/visualdeckstoragesearchfoldernames", - "interface/visualdeckstorageshowcoloridentity", - "interface/visualdeckstorageshowbannercardcombobox", - "interface/visualdeckstorageshowtagsondeckpreviews", - "interface/visualdeckstoragedrawunusedcoloridentities", - "interface/visualdeckstorageunusedcoloridentitiesopacity", - "interface/visualdeckstoragetooltiptype", - "interface/visualdeckstoragepromptforconversion", - "interface/visualdeckstoragealwaysconvert", - "interface/visualdeckstorageingame", - "interface/visualdeckstorageselectionanimation", - "interface/defaultDeckEditorType", - "interface/visualdatabasedisplayfiltertomostrecentsetsenabled", - "interface/visualdatabasedisplayfiltertomostrecentsetsamount", - "interface/visualdeckeditorsamplehandsize", - "interface/visualdeckeditorcardsize", - "interface/visualdatabasedisplaycardsize", - "interface/edhreccardsize", - "interface/archidektpreviewsize"}; + const QMap vdsKeyMap = { + {"interface/visualdeckstoragesortingorder", "interface/visualDeckStorage/sortingOrder"}, + {"interface/visualdeckstorageshowfolders", "interface/visualDeckStorage/showFolders"}, + {"interface/visualdeckstorageshowtagfilter", "interface/visualDeckStorage/showTagFilter"}, + {"interface/visualdeckstoragedefaulttagslist", "interface/visualDeckStorage/defaultTagsList"}, + {"interface/visualdeckstoragesearchfoldernames", "interface/visualDeckStorage/searchFolderNames"}, + {"interface/visualdeckstorageshowcoloridentity", "interface/visualDeckStorage/showColorIdentity"}, + {"interface/visualdeckstorageshowbannercardcombobox", "interface/visualDeckStorage/showBannerCardComboBox"}, + {"interface/visualdeckstorageshowtagsondeckpreviews", "interface/visualDeckStorage/showTagsOnDeckPreviews"}, + {"interface/visualdeckstoragedrawunusedcoloridentities", + "interface/visualDeckStorage/drawUnusedColorIdentities"}, + {"interface/visualdeckstorageunusedcoloridentitiesopacity", + "interface/visualDeckStorage/unusedColorIdentitiesOpacity"}, + {"interface/visualdeckstoragetooltiptype", "interface/visualDeckStorage/tooltipType"}, + {"interface/visualdeckstoragepromptforconversion", "interface/visualDeckStorage/promptForConversion"}, + {"interface/visualdeckstoragealwaysconvert", "interface/visualDeckStorage/alwaysConvert"}, + {"interface/visualdeckstorageingame", "interface/visualDeckStorage/inGame"}, + {"interface/visualdeckstorageselectionanimation", "interface/visualDeckStorage/selectionAnimation"}, + {"interface/visualdatabasedisplayfiltertomostrecentsetsenabled", + "interface/visualDatabaseDisplay/filterToMostRecentSetsEnabled"}, + {"interface/visualdatabasedisplayfiltertomostrecentsetsamount", + "interface/visualDatabaseDisplay/filterToMostRecentSetsAmount"}, + }; bool hasAny = false; - for (const auto &key : vdsKeys) { - if (globalIni.contains(key)) { + for (auto it = vdsKeyMap.constBegin(); it != vdsKeyMap.constEnd(); ++it) { + if (globalIni.contains(it.key())) { hasAny = true; + break; } } if (!hasAny) { @@ -375,9 +478,36 @@ static void migrateVisualDeckStorageSettings(const QString &settingsPath, QSetti } QSettings vdsIni(settingsPath + "visual_deck_storage.ini", QSettings::IniFormat); - for (const auto &key : vdsKeys) { - if (globalIni.contains(key)) { - vdsIni.setValue(key, globalIni.value(key)); + for (auto it = vdsKeyMap.constBegin(); it != vdsKeyMap.constEnd(); ++it) { + if (globalIni.contains(it.key())) { + vdsIni.setValue(it.value(), globalIni.value(it.key())); + } + } +} + +static void migrateDeckEditorSettings(const QString &settingsPath, QSettings &globalIni) +{ + const QMap deckEditorKeyMap = { + {"editor/openDeckInNewTab", "deckeditor/openDeckInNewTab"}, + {"interface/deckeditorbannercardcomboboxvisible", "deckeditor/bannerCardComboBoxVisible"}, + {"interface/deckeditortagswidgetvisible", "deckeditor/tagsWidgetVisible"}, + {"interface/defaultDeckEditorType", "deckeditor/defaultDeckEditorType"}, + }; + bool hasAny = false; + for (auto it = deckEditorKeyMap.constBegin(); it != deckEditorKeyMap.constEnd(); ++it) { + if (globalIni.contains(it.key())) { + hasAny = true; + break; + } + } + if (!hasAny) { + return; + } + + QSettings deckEditorIni(settingsPath + "deck_editor.ini", QSettings::IniFormat); + for (auto it = deckEditorKeyMap.constBegin(); it != deckEditorKeyMap.constEnd(); ++it) { + if (globalIni.contains(it.key())) { + deckEditorIni.setValue(it.value(), globalIni.value(it.key())); } } } @@ -395,9 +525,9 @@ static void migrateLegacySets(const QString &settingsPath) QSettings cardDbIni(settingsPath + "cardDatabase.ini", QSettings::IniFormat); for (const auto &shortName : groups) { legacySetting.beginGroup(shortName); - cardDbIni.setValue("sets/" + shortName + "/sortkey", legacySetting.value("sortkey")); + cardDbIni.setValue("sets/" + shortName + "/sortKey", legacySetting.value("sortkey")); cardDbIni.setValue("sets/" + shortName + "/enabled", legacySetting.value("enabled")); - cardDbIni.setValue("sets/" + shortName + "/isknown", legacySetting.value("isknown")); + cardDbIni.setValue("sets/" + shortName + "/isKnown", legacySetting.value("isknown")); legacySetting.endGroup(); } legacySetting.endGroup(); @@ -413,13 +543,30 @@ static void migrateLegacyServers(const QString &settingsPath) return; } + const QMap serverKeyMap = { + {"previoushostlogin", "previousHostLogin"}, + {"previoushosts", "previousHosts"}, + {"previoushostName", "previousHostName"}, + {"auto_connect", "autoConnect"}, + {"fphostname", "fpHostName"}, + {"fpport", "fpPort"}, + {"fpplayername", "fpPlayerName"}, + {"save_debug_log", "saveDebugLog"}, + }; + QSettings serversIni(settingsPath + "servers.ini", QSettings::IniFormat); - serversIni.setValue("server/previoushostlogin", legacySetting.value("previoushostlogin")); - serversIni.setValue("server/previoushosts", legacySetting.value("previoushosts")); - serversIni.setValue("server/auto_connect", legacySetting.value("auto_connect")); - serversIni.setValue("server/fphostname", legacySetting.value("fphostname")); - serversIni.setValue("server/fpport", legacySetting.value("fpport")); - serversIni.setValue("server/fpplayername", legacySetting.value("fpplayername")); + for (auto it = serverKeyMap.constBegin(); it != serverKeyMap.constEnd(); ++it) { + if (legacySetting.contains(it.key())) { + serversIni.setValue("server/" + it.value(), legacySetting.value(it.key())); + } + } + + legacySetting.beginGroup("server_details"); + const QStringList detailsKeys = legacySetting.allKeys(); + for (const auto &key : detailsKeys) { + serversIni.setValue("server/server_details/" + key, legacySetting.value(key)); + } + legacySetting.endGroup(); legacySetting.endGroup(); } @@ -454,9 +601,36 @@ static void migrateLegacyGameFilters(const QString &settingsPath) return; } + const QMap filterKeyMap = { + {"hide_buddies_only_games", "hideBuddiesOnlyGames"}, + {"hide_full_games", "hideFullGames"}, + {"hide_games_that_started", "hideGamesThatStarted"}, + {"hide_password_protected_games", "hidePasswordProtectedGames"}, + {"hide_ignored_user_games", "hideIgnoredUserGames"}, + {"hide_not_buddy_created_games", "hideNotBuddyCreatedGames"}, + {"hide_open_decklist_games", "hideOpenDecklistGames"}, + {"game_name_filter", "gameNameFilter"}, + {"creator_name_filter", "creatorNameFilter"}, + {"min_players", "minPlayers"}, + {"max_players", "maxPlayers"}, + {"max_game_age_time", "maxGameAgeTime"}, + {"show_only_if_spectators_can_watch", "showOnlyIfSpectatorsCanWatch"}, + {"show_spectator_password_protected", "showSpectatorPasswordProtected"}, + {"show_only_if_spectators_can_chat", "showOnlyIfSpectatorsCanChat"}, + {"show_only_if_spectators_can_see_hands", "showOnlyIfSpectatorsCanSeeHands"}, + }; + QSettings filtersIni(settingsPath + "gamefilters.ini", QSettings::IniFormat); + for (auto it = filterKeyMap.constBegin(); it != filterKeyMap.constEnd(); ++it) { + if (legacySetting.contains(it.key())) { + filtersIni.setValue("filter_games/" + it.value(), legacySetting.value(it.key())); + } + } + const QString gameTypePrefix = "game_type/"; for (const auto &key : keys) { - filtersIni.setValue("filter_games/" + key, legacySetting.value(key)); + if (key.startsWith(gameTypePrefix)) { + filtersIni.setValue("filter_games/gameType/" + key.mid(gameTypePrefix.size()), legacySetting.value(key)); + } } legacySetting.endGroup(); } @@ -503,10 +677,15 @@ bool SettingsMigration::migrateSettingsFromGlobalIni(const QString &settingsPath migrateCacheStorageSettings(settingsPath, globalIni); migrateUpdatesSettings(settingsPath, globalIni); migratePersonalSettings(settingsPath, globalIni); + migrateDownloadSettings(settingsPath, globalIni); migrateCardsDisplaySettings(settingsPath, globalIni); + migrateCardCounterSettings(settingsPath, globalIni); migrateInterfaceSettings(settingsPath, globalIni); + migrateAppearanceSettings(settingsPath, globalIni); + migrateNetworkSettings(settingsPath, globalIni); migratePathsSettings(settingsPath, globalIni); migrateVisualDeckStorageSettings(settingsPath, globalIni); + migrateDeckEditorSettings(settingsPath, globalIni); QFile::remove(settingsPath + "global.ini.old"); QFile::rename(settingsPath + "global.ini", settingsPath + "global.ini.old"); diff --git a/libcockatrice_settings/libcockatrice/settings/sound_settings.cpp b/libcockatrice_settings/libcockatrice/settings/sound_settings.cpp index 7fd61d263..82b724a07 100644 --- a/libcockatrice_settings/libcockatrice/settings/sound_settings.cpp +++ b/libcockatrice_settings/libcockatrice/settings/sound_settings.cpp @@ -17,7 +17,7 @@ QString SoundSettings::getSoundThemeName() const int SoundSettings::getMasterVolume() const { - return getValue("mastervolume", QString(), QString(), 100).toInt(); + return getValue("masterVolume", QString(), QString(), 100).toInt(); } void SoundSettings::setSoundEnabled(bool _soundEnabled) @@ -34,6 +34,6 @@ void SoundSettings::setSoundThemeName(const QString &_soundThemeName) void SoundSettings::setMasterVolume(int _masterVolume) { - setValue(_masterVolume, "mastervolume"); + setValue(_masterVolume, "masterVolume"); emit masterVolumeChanged(_masterVolume); } diff --git a/libcockatrice_settings/libcockatrice/settings/updates_settings.cpp b/libcockatrice_settings/libcockatrice/settings/updates_settings.cpp index 3d6d77e53..166bc0aa8 100644 --- a/libcockatrice_settings/libcockatrice/settings/updates_settings.cpp +++ b/libcockatrice_settings/libcockatrice/settings/updates_settings.cpp @@ -45,17 +45,17 @@ bool UpdatesSettings::getAlwaysEnableNewSets() const bool UpdatesSettings::getNotifyAboutUpdates() const { - return getValue("updatenotification", QString(), QString(), true).toBool(); + return getValue("updateNotification", QString(), QString(), true).toBool(); } bool UpdatesSettings::getNotifyAboutNewVersion() const { - return getValue("newversionnotification", QString(), QString(), true).toBool(); + return getValue("newVersionNotification", QString(), QString(), true).toBool(); } int UpdatesSettings::getUpdateReleaseChannelIndex() const { - return getValue("updatereleasechannel", QString(), QString(), 0).toInt(); + return getValue("updateReleaseChannel", QString(), QString(), 0).toInt(); } void UpdatesSettings::setCheckUpdatesOnStartup(bool value) @@ -90,15 +90,15 @@ void UpdatesSettings::setAlwaysEnableNewSets(bool value) void UpdatesSettings::setNotifyAboutUpdates(bool _notifyaboutupdate) { - setValue(_notifyaboutupdate, "updatenotification"); + setValue(_notifyaboutupdate, "updateNotification"); } void UpdatesSettings::setNotifyAboutNewVersion(bool _notifyaboutnewversion) { - setValue(_notifyaboutnewversion, "newversionnotification"); + setValue(_notifyaboutnewversion, "newVersionNotification"); } void UpdatesSettings::setUpdateReleaseChannelIndex(int value) { - setValue(value, "updatereleasechannel"); + setValue(value, "updateReleaseChannel"); } diff --git a/libcockatrice_settings/libcockatrice/settings/visual_deck_storage_settings.cpp b/libcockatrice_settings/libcockatrice/settings/visual_deck_storage_settings.cpp index c0ccc37aa..1b21af58e 100644 --- a/libcockatrice_settings/libcockatrice/settings/visual_deck_storage_settings.cpp +++ b/libcockatrice_settings/libcockatrice/settings/visual_deck_storage_settings.cpp @@ -91,258 +91,182 @@ VisualDeckStorageSettings::VisualDeckStorageSettings(const QString &settingPath, int VisualDeckStorageSettings::getVisualDeckStorageSortingOrder() const { - return getValue("visualdeckstoragesortingorder", QString(), QString(), 0).toInt(); + return getValue("sortingOrder", "interface", "visualDeckStorage", 0).toInt(); } bool VisualDeckStorageSettings::getVisualDeckStorageShowFolders() const { - return getValue("visualdeckstorageshowfolders", QString(), QString(), true).toBool(); + return getValue("showFolders", "interface", "visualDeckStorage", true).toBool(); } bool VisualDeckStorageSettings::getVisualDeckStorageShowTagFilter() const { - return getValue("visualdeckstorageshowtagfilter", QString(), QString(), true).toBool(); + return getValue("showTagFilter", "interface", "visualDeckStorage", true).toBool(); } QStringList VisualDeckStorageSettings::getVisualDeckStorageDefaultTagsList() const { - return getValue("visualdeckstoragedefaulttagslist", QString(), QString(), QVariant::fromValue(defaultTags)) + return getValue("defaultTagsList", "interface", "visualDeckStorage", QVariant::fromValue(defaultTags)) .toStringList(); } bool VisualDeckStorageSettings::getVisualDeckStorageSearchFolderNames() const { - return getValue("visualdeckstoragesearchfoldernames", QString(), QString(), true).toBool(); + return getValue("searchFolderNames", "interface", "visualDeckStorage", true).toBool(); } bool VisualDeckStorageSettings::getVisualDeckStorageShowColorIdentity() const { - return getValue("visualdeckstorageshowcoloridentity", QString(), QString(), true).toBool(); + return getValue("showColorIdentity", "interface", "visualDeckStorage", true).toBool(); } bool VisualDeckStorageSettings::getVisualDeckStorageShowBannerCardComboBox() const { - return getValue("visualdeckstorageshowbannercardcombobox", QString(), QString(), true).toBool(); + return getValue("showBannerCardComboBox", "interface", "visualDeckStorage", true).toBool(); } bool VisualDeckStorageSettings::getVisualDeckStorageShowTagsOnDeckPreviews() const { - return getValue("visualdeckstorageshowtagsondeckpreviews", QString(), QString(), true).toBool(); -} - -int VisualDeckStorageSettings::getVisualDeckStorageCardSize() const -{ - return getValue("visualdeckstoragecardsize", QString(), QString(), 100).toInt(); + return getValue("showTagsOnDeckPreviews", "interface", "visualDeckStorage", true).toBool(); } bool VisualDeckStorageSettings::getVisualDeckStorageDrawUnusedColorIdentities() const { - return getValue("visualdeckstoragedrawunusedcoloridentities", QString(), QString(), true).toBool(); + return getValue("drawUnusedColorIdentities", "interface", "visualDeckStorage", true).toBool(); } int VisualDeckStorageSettings::getVisualDeckStorageUnusedColorIdentitiesOpacity() const { - return getValue("visualdeckstorageunusedcoloridentitiesopacity", QString(), QString(), 15).toInt(); + return getValue("unusedColorIdentitiesOpacity", "interface", "visualDeckStorage", 15).toInt(); } int VisualDeckStorageSettings::getVisualDeckStorageTooltipType() const { - return getValue("visualdeckstoragetooltiptype", QString(), QString(), 0).toInt(); + return getValue("tooltipType", "interface", "visualDeckStorage", 0).toInt(); } bool VisualDeckStorageSettings::getVisualDeckStoragePromptForConversion() const { - return getValue("visualdeckstoragepromptforconversion", QString(), QString(), true).toBool(); + return getValue("promptForConversion", "interface", "visualDeckStorage", true).toBool(); } bool VisualDeckStorageSettings::getVisualDeckStorageAlwaysConvert() const { - return getValue("visualdeckstoragealwaysconvert", QString(), QString(), false).toBool(); + return getValue("alwaysConvert", "interface", "visualDeckStorage", false).toBool(); } bool VisualDeckStorageSettings::getVisualDeckStorageInGame() const { - return getValue("visualdeckstorageingame", QString(), QString(), true).toBool(); + return getValue("inGame", "interface", "visualDeckStorage", true).toBool(); } bool VisualDeckStorageSettings::getVisualDeckStorageSelectionAnimation() const { - return getValue("visualdeckstorageselectionanimation", QString(), QString(), true).toBool(); -} - -int VisualDeckStorageSettings::getVisualDeckEditorCardSize() const -{ - return getValue("visualdeckeditorcardsize", QString(), QString(), 100).toInt(); -} - -int VisualDeckStorageSettings::getVisualDeckEditorSampleHandSize() const -{ - return getValue("visualdeckeditorsamplehandsize", QString(), QString(), 7).toInt(); -} - -int VisualDeckStorageSettings::getVisualDatabaseDisplayCardSize() const -{ - return getValue("visualdatabasedisplaycardsize", QString(), QString(), 100).toInt(); + return getValue("selectionAnimation", "interface", "visualDeckStorage", true).toBool(); } bool VisualDeckStorageSettings::getVisualDatabaseDisplayFilterToMostRecentSetsEnabled() const { - return getValue("visualdatabasedisplayfiltertomostrecentsetsenabled", QString(), QString(), false).toBool(); + return getValue("filterToMostRecentSetsEnabled", "interface", "visualDatabaseDisplay", false).toBool(); } int VisualDeckStorageSettings::getVisualDatabaseDisplayFilterToMostRecentSetsAmount() const { - return getValue("visualdatabasedisplayfiltertomostrecentsetsamount", QString(), QString(), 10).toInt(); -} - -int VisualDeckStorageSettings::getEDHRecCardSize() const -{ - return getValue("edhreccardsize", QString(), QString(), 100).toInt(); -} - -int VisualDeckStorageSettings::getArchidektPreviewSize() const -{ - return getValue("archidektpreviewsize", QString(), QString(), 100).toInt(); -} - -int VisualDeckStorageSettings::getDefaultDeckEditorType() const -{ - return getValue("defaultDeckEditorType", QString(), QString(), 1).toInt(); + return getValue("filterToMostRecentSetsAmount", "interface", "visualDatabaseDisplay", 10).toInt(); } void VisualDeckStorageSettings::setVisualDeckStorageSortingOrder(int _sortingOrder) { - setValue(_sortingOrder, "visualdeckstoragesortingorder"); + setValue(_sortingOrder, "sortingOrder", "interface", "visualDeckStorage"); } void VisualDeckStorageSettings::setVisualDeckStorageShowFolders(bool value) { - setValue(value, "visualdeckstorageshowfolders"); + setValue(value, "showFolders", "interface", "visualDeckStorage"); } void VisualDeckStorageSettings::setVisualDeckStorageShowTagFilter(bool _showTags) { - setValue(_showTags, "visualdeckstorageshowtagfilter"); + setValue(_showTags, "showTagFilter", "interface", "visualDeckStorage"); emit visualDeckStorageShowTagFilterChanged(_showTags); } void VisualDeckStorageSettings::setVisualDeckStorageDefaultTagsList(QStringList _defaultTagsList) { - setValue(QVariant::fromValue(_defaultTagsList), "visualdeckstoragedefaulttagslist"); + setValue(QVariant::fromValue(_defaultTagsList), "defaultTagsList", "interface", "visualDeckStorage"); emit visualDeckStorageDefaultTagsListChanged(); } void VisualDeckStorageSettings::setVisualDeckStorageSearchFolderNames(bool value) { - setValue(value, "visualdeckstoragesearchfoldernames"); + setValue(value, "searchFolderNames", "interface", "visualDeckStorage"); } void VisualDeckStorageSettings::setVisualDeckStorageShowColorIdentity(bool value) { - setValue(value, "visualdeckstorageshowcoloridentity"); + setValue(value, "showColorIdentity", "interface", "visualDeckStorage"); emit visualDeckStorageShowColorIdentityChanged(value); } void VisualDeckStorageSettings::setVisualDeckStorageShowBannerCardComboBox(bool _showBannerCardComboBox) { - setValue(_showBannerCardComboBox, "visualdeckstorageshowbannercardcombobox"); + setValue(_showBannerCardComboBox, "showBannerCardComboBox", "interface", "visualDeckStorage"); emit visualDeckStorageShowBannerCardComboBoxChanged(_showBannerCardComboBox); } void VisualDeckStorageSettings::setVisualDeckStorageShowTagsOnDeckPreviews(bool _showTags) { - setValue(_showTags, "visualdeckstorageshowtagsondeckpreviews"); + setValue(_showTags, "showTagsOnDeckPreviews", "interface", "visualDeckStorage"); emit visualDeckStorageShowTagsOnDeckPreviewsChanged(_showTags); } -void VisualDeckStorageSettings::setVisualDeckStorageCardSize(int _cardSize) -{ - setValue(_cardSize, "visualdeckstoragecardsize"); - emit visualDeckStorageCardSizeChanged(); -} - void VisualDeckStorageSettings::setVisualDeckStorageDrawUnusedColorIdentities(bool _draw) { - setValue(_draw, "visualdeckstoragedrawunusedcoloridentities"); + setValue(_draw, "drawUnusedColorIdentities", "interface", "visualDeckStorage"); emit visualDeckStorageDrawUnusedColorIdentitiesChanged(_draw); } void VisualDeckStorageSettings::setVisualDeckStorageUnusedColorIdentitiesOpacity(int _opacity) { - setValue(_opacity, "visualdeckstorageunusedcoloridentitiesopacity"); + setValue(_opacity, "unusedColorIdentitiesOpacity", "interface", "visualDeckStorage"); emit visualDeckStorageUnusedColorIdentitiesOpacityChanged(_opacity); } void VisualDeckStorageSettings::setVisualDeckStorageTooltipType(int value) { - setValue(value, "visualdeckstoragetooltiptype"); + setValue(value, "tooltipType", "interface", "visualDeckStorage"); } void VisualDeckStorageSettings::setVisualDeckStoragePromptForConversion(bool _prompt) { - setValue(_prompt, "visualdeckstoragepromptforconversion"); + setValue(_prompt, "promptForConversion", "interface", "visualDeckStorage"); } void VisualDeckStorageSettings::setVisualDeckStorageAlwaysConvert(bool _always) { - setValue(_always, "visualdeckstoragealwaysconvert"); + setValue(_always, "alwaysConvert", "interface", "visualDeckStorage"); } void VisualDeckStorageSettings::setVisualDeckStorageInGame(bool enabled) { - setValue(enabled, "visualdeckstorageingame"); + setValue(enabled, "inGame", "interface", "visualDeckStorage"); emit visualDeckStorageInGameChanged(enabled); } void VisualDeckStorageSettings::setVisualDeckStorageSelectionAnimation(bool enabled) { - setValue(enabled, "visualdeckstorageselectionanimation"); + setValue(enabled, "selectionAnimation", "interface", "visualDeckStorage"); emit visualDeckStorageSelectionAnimationChanged(enabled); } -void VisualDeckStorageSettings::setVisualDeckEditorCardSize(int _cardSize) -{ - setValue(_cardSize, "visualdeckeditorcardsize"); - emit visualDeckEditorCardSizeChanged(); -} - -void VisualDeckStorageSettings::setVisualDeckEditorSampleHandSize(int _amount) -{ - setValue(_amount, "visualdeckeditorsamplehandsize"); - emit visualDeckEditorSampleHandSizeAmountChanged(_amount); -} - -void VisualDeckStorageSettings::setVisualDatabaseDisplayCardSize(int _cardSize) -{ - setValue(_cardSize, "visualdatabasedisplaycardsize"); - emit visualDatabaseDisplayCardSizeChanged(); -} - void VisualDeckStorageSettings::setVisualDatabaseDisplayFilterToMostRecentSetsEnabled(bool _enabled) { - setValue(_enabled, "visualdatabasedisplayfiltertomostrecentsetsenabled"); + setValue(_enabled, "filterToMostRecentSetsEnabled", "interface", "visualDatabaseDisplay"); emit visualDatabaseDisplayFilterToMostRecentSetsEnabledChanged(_enabled); } void VisualDeckStorageSettings::setVisualDatabaseDisplayFilterToMostRecentSetsAmount(int _amount) { - setValue(_amount, "visualdatabasedisplayfiltertomostrecentsetsamount"); + setValue(_amount, "filterToMostRecentSetsAmount", "interface", "visualDatabaseDisplay"); emit visualDatabaseDisplayFilterToMostRecentSetsAmountChanged(_amount); } - -void VisualDeckStorageSettings::setEDHRecCardSize(int _edhrecCardSize) -{ - setValue(_edhrecCardSize, "edhreccardsize"); - emit edhRecCardSizeChanged(); -} - -void VisualDeckStorageSettings::setArchidektPreviewCardSize(int _archidektPreviewCardSize) -{ - setValue(_archidektPreviewCardSize, "archidektpreviewsize"); - emit archidektPreviewSizeChanged(); -} - -void VisualDeckStorageSettings::setDefaultDeckEditorType(int value) -{ - setValue(value, "defaultDeckEditorType"); -} diff --git a/libcockatrice_settings/libcockatrice/settings/visual_deck_storage_settings.h b/libcockatrice_settings/libcockatrice/settings/visual_deck_storage_settings.h index 06337ce79..fd2a76663 100644 --- a/libcockatrice_settings/libcockatrice/settings/visual_deck_storage_settings.h +++ b/libcockatrice_settings/libcockatrice/settings/visual_deck_storage_settings.h @@ -20,7 +20,6 @@ public: [[nodiscard]] bool getVisualDeckStorageShowColorIdentity() const override; [[nodiscard]] bool getVisualDeckStorageShowBannerCardComboBox() const override; [[nodiscard]] bool getVisualDeckStorageShowTagsOnDeckPreviews() const override; - [[nodiscard]] int getVisualDeckStorageCardSize() const override; [[nodiscard]] bool getVisualDeckStorageDrawUnusedColorIdentities() const override; [[nodiscard]] int getVisualDeckStorageUnusedColorIdentitiesOpacity() const override; [[nodiscard]] int getVisualDeckStorageTooltipType() const override; @@ -28,14 +27,8 @@ public: [[nodiscard]] bool getVisualDeckStorageAlwaysConvert() const override; [[nodiscard]] bool getVisualDeckStorageInGame() const override; [[nodiscard]] bool getVisualDeckStorageSelectionAnimation() const override; - [[nodiscard]] int getVisualDeckEditorCardSize() const override; - [[nodiscard]] int getVisualDeckEditorSampleHandSize() const override; - [[nodiscard]] int getVisualDatabaseDisplayCardSize() const override; [[nodiscard]] bool getVisualDatabaseDisplayFilterToMostRecentSetsEnabled() const override; [[nodiscard]] int getVisualDatabaseDisplayFilterToMostRecentSetsAmount() const override; - [[nodiscard]] int getEDHRecCardSize() const override; - [[nodiscard]] int getArchidektPreviewSize() const override; - [[nodiscard]] int getDefaultDeckEditorType() const override; void setVisualDeckStorageSortingOrder(int _sortingOrder); void setVisualDeckStorageShowFolders(bool value); @@ -45,7 +38,6 @@ public: void setVisualDeckStorageShowColorIdentity(bool value); void setVisualDeckStorageShowBannerCardComboBox(bool _showBannerCardComboBox); void setVisualDeckStorageShowTagsOnDeckPreviews(bool _showTags); - void setVisualDeckStorageCardSize(int _cardSize); void setVisualDeckStorageDrawUnusedColorIdentities(bool _draw); void setVisualDeckStorageUnusedColorIdentitiesOpacity(int _opacity); void setVisualDeckStorageTooltipType(int value); @@ -53,14 +45,8 @@ public: void setVisualDeckStorageAlwaysConvert(bool _always); void setVisualDeckStorageInGame(bool enabled); void setVisualDeckStorageSelectionAnimation(bool enabled); - void setVisualDeckEditorCardSize(int _cardSize); - void setVisualDeckEditorSampleHandSize(int _amount); - void setVisualDatabaseDisplayCardSize(int _cardSize); void setVisualDatabaseDisplayFilterToMostRecentSetsEnabled(bool _enabled); void setVisualDatabaseDisplayFilterToMostRecentSetsAmount(int _amount); - void setEDHRecCardSize(int _edhrecCardSize); - void setArchidektPreviewCardSize(int _archidektPreviewCardSize); - void setDefaultDeckEditorType(int value); signals: void visualDeckStorageShowTagFilterChanged(bool _visible); @@ -68,18 +54,12 @@ signals: void visualDeckStorageShowColorIdentityChanged(bool _visible); void visualDeckStorageShowBannerCardComboBoxChanged(bool _visible); void visualDeckStorageShowTagsOnDeckPreviewsChanged(bool _visible); - void visualDeckStorageCardSizeChanged(); void visualDeckStorageDrawUnusedColorIdentitiesChanged(bool _visible); void visualDeckStorageUnusedColorIdentitiesOpacityChanged(bool value); void visualDeckStorageInGameChanged(bool enabled); void visualDeckStorageSelectionAnimationChanged(bool enabled); void visualDatabaseDisplayFilterToMostRecentSetsEnabledChanged(bool enabled); void visualDatabaseDisplayFilterToMostRecentSetsAmountChanged(int amount); - void visualDeckEditorSampleHandSizeAmountChanged(int amount); - void visualDeckEditorCardSizeChanged(); - void visualDatabaseDisplayCardSizeChanged(); - void edhRecCardSizeChanged(); - void archidektPreviewSizeChanged(); public: explicit VisualDeckStorageSettings(const QString &settingPath, QObject *parent = nullptr); diff --git a/libcockatrice_utility/CMakeLists.txt b/libcockatrice_utility/CMakeLists.txt index 2d34cad31..79f5a11e4 100644 --- a/libcockatrice_utility/CMakeLists.txt +++ b/libcockatrice_utility/CMakeLists.txt @@ -6,7 +6,7 @@ set(CMAKE_AUTOUIC ON) set(CMAKE_AUTORCC ON) set(UTILITY_SOURCES libcockatrice/utility/expression.cpp libcockatrice/utility/levenshtein.cpp - libcockatrice/utility/passwordhasher.cpp + libcockatrice/utility/passwordhasher.cpp libcockatrice/utility/server_rate_limiter.cpp ) set(UTILITY_HEADERS @@ -21,6 +21,7 @@ set(UTILITY_HEADERS libcockatrice/utility/clamped_arithmetic.h libcockatrice/utility/zone_names.h libcockatrice/utility/days_years_between.h + libcockatrice/utility/server_rate_limiter.h ) add_library(libcockatrice_utility STATIC ${UTILITY_SOURCES} ${UTILITY_HEADERS}) diff --git a/libcockatrice_utility/libcockatrice/utility/server_rate_limiter.cpp b/libcockatrice_utility/libcockatrice/utility/server_rate_limiter.cpp new file mode 100644 index 000000000..2a1e3b283 --- /dev/null +++ b/libcockatrice_utility/libcockatrice/utility/server_rate_limiter.cpp @@ -0,0 +1,85 @@ +#include "server_rate_limiter.h" + +bool ServerRateLimiter::isRateLimited(const QString &host, const QDateTime &now) const +{ + auto it = backoffUntil.constFind(host); + return it != backoffUntil.constEnd() && now < it.value(); +} + +int ServerRateLimiter::rounds(const QString &host) const +{ + return retryRounds.value(host, 0); +} + +QDateTime ServerRateLimiter::deadline(const QString &host) const +{ + return backoffUntil.value(host); +} + +QDateTime ServerRateLimiter::earliestDeadline(const QDateTime &now) const +{ + QDateTime earliest; + for (auto it = backoffUntil.cbegin(); it != backoffUntil.cend(); ++it) { + if (now < it.value() && (!earliest.isValid() || it.value() < earliest)) { + earliest = it.value(); + } + } + return earliest; +} + +QDateTime ServerRateLimiter::on429(const QString &host, const QDateTime &now, qint64 retryAfterMs) +{ + QDateTime existing = backoffUntil.value(host); + int round = retryRounds.value(host, 0); + + if (!existing.isValid() || now >= existing) { + if (last429.value(host).isValid() && now >= last429.value(host).addMSecs(RESET_GRACE_MS)) { + round = 0; + } + round = qMin(round + 1, MAX_RETRIES); + retryRounds.insert(host, round); + } + last429.insert(host, now); + + qint64 delay; + if (round >= MAX_RETRIES) { + delay = EXHAUSTED_BACKOFF_MS; + } else { + delay = qMax(MIN_429_BACKOFF_MS, retryAfterMs); + delay = qMin(delay, MAX_BACKOFF_MS); + } + + QDateTime deadline = now.addMSecs(delay); + if (existing.isValid() && existing > deadline) { + deadline = existing; // never shorten an active backoff + } + backoffUntil.insert(host, deadline); + + return deadline; +} + +void ServerRateLimiter::onSuccess(const QString &host) +{ + backoffUntil.remove(host); + retryRounds.remove(host); + last429.remove(host); +} + +void ServerRateLimiter::clearExpired(const QDateTime &now) +{ + auto it = backoffUntil.begin(); + while (it != backoffUntil.end()) { + if (now < it.value()) { + ++it; + continue; + } + + QString host = it.key(); + bool budgetStillFresh = last429.value(host).isValid() && now < last429.value(host).addMSecs(RESET_GRACE_MS); + it = backoffUntil.erase(it); + if (!budgetStillFresh) { + retryRounds.remove(host); + last429.remove(host); + } + } +} diff --git a/libcockatrice_utility/libcockatrice/utility/server_rate_limiter.h b/libcockatrice_utility/libcockatrice/utility/server_rate_limiter.h new file mode 100644 index 000000000..e071b9189 --- /dev/null +++ b/libcockatrice_utility/libcockatrice/utility/server_rate_limiter.h @@ -0,0 +1,73 @@ +#ifndef SERVER_RATE_LIMITER_H +#define SERVER_RATE_LIMITER_H + +#include +#include +#include + +/** + * @class ServerRateLimiter + * @ingroup Utility + * @brief Tracks per-server backoff state triggered by HTTP 429 responses. + * + * Keeps a monotonic backoff deadline per host and a retry-round budget that + * escalates once per backoff window (a burst of concurrent 429s counts as a + * single round). The budget is refreshed after a long period without 429s so a + * server is not permanently blacklisted after a past overload. + */ +class ServerRateLimiter +{ +public: + static constexpr int MAX_RETRIES = 5; ///< Max 429 rounds per host before the budget is exhausted + static constexpr int MIN_429_BACKOFF_MS = 30000; ///< Minimum wait after a 429 (scryfall documented cool-down) + static constexpr int MAX_BACKOFF_MS = 60000; ///< Cap for a single backoff period + static constexpr int EXHAUSTED_BACKOFF_MS = 60000; ///< Cool-down once the retry budget is exhausted + static constexpr qint64 RESET_GRACE_MS = 300000; ///< Idle time after which the retry budget refreshes + + /** + * @brief Checks whether a host is currently in backoff. + * @param host The host to check + * @param now The current time + * @return True if requests to the host should be paused + */ + [[nodiscard]] bool isRateLimited(const QString &host, const QDateTime &now) const; + + /** @return The number of consecutive 429 rounds recorded for the host. */ + [[nodiscard]] int rounds(const QString &host) const; + + /** @return The current backoff deadline for the host, or an invalid QDateTime. */ + [[nodiscard]] QDateTime deadline(const QString &host) const; + + /** @return The earliest active backoff deadline across all hosts, or an invalid QDateTime. */ + [[nodiscard]] QDateTime earliestDeadline(const QDateTime &now) const; + + /** + * @brief Registers a 429 response for the given host. + * @param host The host that returned 429 + * @param now The time the response was received + * @param retryAfterMs A Retry-After hint in milliseconds, or 0 if absent + * @return The new backoff deadline for the host + * + * The round counter only escalates once the previous backoff window has + * fully passed, so concurrent 429s from a burst do not consume the whole + * budget. The deadline is extended monotonically and never shortened. + */ + QDateTime on429(const QString &host, const QDateTime &now, qint64 retryAfterMs = 0); + + /** @brief Clears all penalty state for a host after a successful request. */ + void onSuccess(const QString &host); + + /** + * @brief Removes expired backoffs, refreshing the round budget of hosts + * that have been idle past the reset grace period. + * @param now The current time + */ + void clearExpired(const QDateTime &now); + +private: + QMap backoffUntil; ///< When each host may be contacted again + QMap retryRounds; ///< Consecutive 429 rounds per host + QMap last429; ///< When each host was last rate limited +}; + +#endif // SERVER_RATE_LIMITER_H diff --git a/oracle/CMakeLists.txt b/oracle/CMakeLists.txt index 8ffa7cdaf..c250d55da 100644 --- a/oracle/CMakeLists.txt +++ b/oracle/CMakeLists.txt @@ -64,9 +64,7 @@ set(oracle_RESOURCES oracle.qrc) # ------------------------ # Qt resources # ------------------------ -if(Qt6_FOUND) - qt6_add_resources(oracle_RESOURCES_RCC ${oracle_RESOURCES}) -endif() +qt6_add_resources(oracle_RESOURCES_RCC ${oracle_RESOURCES}) # ------------------------ # Include directories @@ -99,23 +97,20 @@ endif() # ------------------------ # Build executable # ------------------------ - set(ORACLE_MAC_QM_INSTALL_DIR "oracle.app/Contents/Resources/translations") set(ORACLE_UNIX_QM_INSTALL_DIR "share/oracle/translations") set(ORACLE_WIN32_QM_INSTALL_DIR "translations") -if(Qt6_FOUND) - # Qt6 Translations are linked after the executable is created in manual mode - qt6_add_executable( - oracle - WIN32 - MACOSX_BUNDLE - ${oracle_SOURCES} - ${oracle_RESOURCES_RCC} - ${oracle_MOC_SRCS} - MANUAL_FINALIZATION - ) -endif() +# Qt6 Translations are linked after the executable is created in manual mode +qt6_add_executable( + oracle + WIN32 + MACOSX_BUNDLE + ${oracle_SOURCES} + ${oracle_RESOURCES_RCC} + ${oracle_MOC_SRCS} + MANUAL_FINALIZATION +) # ------------------------ # Link libraries @@ -264,8 +259,8 @@ endif() # ------------------------ # Qt translations # ------------------------ -if(Qt6_FOUND AND Qt6LinguistTools_FOUND) - # Qt6 Translations happen after the executable is built up +if(Qt6LinguistTools_FOUND) + #Qt6 Translations happen after the executable is built up if(UPDATE_TRANSLATIONS) qt6_add_translations( oracle @@ -291,6 +286,4 @@ if(Qt6_FOUND AND Qt6LinguistTools_FOUND) endif() endif() -if(Qt6_FOUND) - qt6_finalize_target(oracle) -endif() +qt6_finalize_target(oracle) diff --git a/oracle/src/oracleimporter.cpp b/oracle/src/oracleimporter.cpp index 4eeb6ecc4..a9008c4da 100644 --- a/oracle/src/oracleimporter.cpp +++ b/oracle/src/oracleimporter.cpp @@ -18,7 +18,7 @@ static const QList kSingletonCounts = {{1, "legal"}, {0, "banned"} SplitCardPart::SplitCardPart(const QString &_name, const QString &_text, - const QVariantHash &_properties, + const QHash &_properties, const PrintingInfo &_printingInfo) : name(_name), text(_text), properties(_properties), printingInfo(_printingInfo) { @@ -135,7 +135,7 @@ static void sortAndReduceColors(QString &colors) CardInfoPtr OracleImporter::addCard(QString name, const QString &text, bool isToken, - QVariantHash properties, + QHash properties, const QList &relatedCards, const PrintingInfo &printingInfo) { @@ -152,7 +152,7 @@ CardInfoPtr OracleImporter::addCard(QString name, } // Remove {} around mana costs, except if it's split cost - QString manacost = properties.value("manacost").toString(); + QString manacost = properties.value("manacost"); if (!manacost.isEmpty()) { QStringList symbols = manacost.split("}"); QString formattedCardCost; @@ -169,12 +169,12 @@ CardInfoPtr OracleImporter::addCard(QString name, } // fix colors - QString allColors = properties.value("colors").toString(); + QString allColors = properties.value("colors"); if (allColors.size() > 1) { sortAndReduceColors(allColors); properties.insert("colors", allColors); } - QString allColorIdent = properties.value("coloridentity").toString(); + QString allColorIdent = properties.value("coloridentity"); if (allColorIdent.size() > 1) { sortAndReduceColors(allColorIdent); properties.insert("coloridentity", allColorIdent); @@ -182,16 +182,15 @@ CardInfoPtr OracleImporter::addCard(QString name, // DETECT CARD POSITIONING INFO - bool landscapeOrientation = properties.value("maintype").toString() == "Battle" || - properties.value("layout").toString() == "split" || - properties.value("layout").toString() == "planar"; + bool landscapeOrientation = properties.value("maintype") == "Battle" || properties.value("layout") == "split" || + properties.value("layout") == "planar"; // cards that enter the field tapped bool cipt = parseCipt(name, text) || landscapeOrientation; // table row int tableRow = 1; - QString mainCardType = properties.value("maintype").toString(); + QString mainCardType = properties.value("maintype"); if (mainCardType == "Land") { tableRow = 0; } else if (mainCardType == "Sorcery" || mainCardType == "Instant") { @@ -201,11 +200,11 @@ CardInfoPtr OracleImporter::addCard(QString name, } // card side - QString side = properties.value("side").toString() == "b" ? "back" : "front"; + QString side = properties.value("side") == "b" ? "back" : "front"; properties.insert("side", side); // upsideDown (flip cards) - QString layout = properties.value("layout").toString(); + QString layout = properties.value("layout"); bool upsideDown = layout == "flip" && side == "back"; // insert the card and its properties @@ -279,7 +278,7 @@ int OracleImporter::importCardsFromSet(const CardSetPtr ¤tSet, const QList } // card properties - QVariantHash properties; + QHash properties; for (auto i = cardProperties.cbegin(), end = cardProperties.cend(); i != end; ++i) { QString mtgjsonProperty = i.key(); QString xmlPropertyName = i.value(); @@ -290,8 +289,7 @@ int OracleImporter::importCardsFromSet(const CardSetPtr ¤tSet, const QList } // per-set properties - PrintingInfo printingInfo = PrintingInfo(currentSet); - QVariantHash printingProps; + QHash printingProps; for (auto i = setInfoProperties.cbegin(), end = setInfoProperties.cend(); i != end; ++i) { QString mtgjsonProperty = i.key(); QString xmlPropertyName = i.value(); @@ -318,7 +316,7 @@ int OracleImporter::importCardsFromSet(const CardSetPtr ¤tSet, const QList } } - printingInfo.setProperties(printingProps); + PrintingInfo printingInfo(currentSet, printingProps); QString numComponent; const QString numProperty = printingInfo.getProperty("num"); @@ -431,7 +429,7 @@ int OracleImporter::importCardsFromSet(const CardSetPtr ¤tSet, const QList QList, QString>> partsAndNames = splitCards.values(); for (auto [splitCardParts, name] : partsAndNames) { QString text; - QVariantHash properties; + QHash properties; PrintingInfo printingInfo; for (const SplitCardPart &tmp : splitCardParts) { @@ -444,11 +442,11 @@ int OracleImporter::importCardsFromSet(const CardSetPtr ¤tSet, const QList properties = tmp.getProperties(); printingInfo = tmp.getPrintingInfo(); } else { - const QVariantHash &tmpProps = tmp.getProperties(); + const QHash &tmpProps = tmp.getProperties(); for (auto i = tmpProps.cbegin(), end = tmpProps.cend(); i != end; ++i) { QString prop = i.key(); - QString originalPropertyValue = properties.value(prop).toString(); - QString thisCardPropertyValue = i.value().toString(); + QString originalPropertyValue = properties.value(prop); + QString thisCardPropertyValue = i.value(); if (!thisCardPropertyValue.isEmpty() && originalPropertyValue != thisCardPropertyValue) { if (originalPropertyValue.isEmpty()) { // don't create //es if one field is empty properties.insert(prop, thisCardPropertyValue); diff --git a/oracle/src/oracleimporter.h b/oracle/src/oracleimporter.h index 5bf352594..99644f9ce 100644 --- a/oracle/src/oracleimporter.h +++ b/oracle/src/oracleimporter.h @@ -95,7 +95,7 @@ class SplitCardPart public: SplitCardPart(const QString &_name, const QString &_text, - const QVariantHash &_properties, + const QHash &_properties, const PrintingInfo &_printingInfo); inline const QString &getName() const { @@ -105,7 +105,7 @@ public: { return text; } - inline const QVariantHash &getProperties() const + inline const QHash &getProperties() const { return properties; } @@ -117,7 +117,7 @@ public: private: QString name; QString text; - QVariantHash properties; + QHash properties; PrintingInfo printingInfo; }; @@ -142,7 +142,7 @@ private: CardInfoPtr addCard(QString name, const QString &text, bool isToken, - QVariantHash properties, + QHash properties, const QList &relatedCards, const PrintingInfo &printingInfo); signals: diff --git a/servatrice/CMakeLists.txt b/servatrice/CMakeLists.txt index 62a52d347..246a6a836 100644 --- a/servatrice/CMakeLists.txt +++ b/servatrice/CMakeLists.txt @@ -43,9 +43,7 @@ if(APPLE) set(servatrice_SOURCES ${servatrice_SOURCES} ${CMAKE_CURRENT_SOURCE_DIR}/resources/appicon.icns) endif(APPLE) -if(Qt6_FOUND) - qt6_add_resources(servatrice_RESOURCES_RCC ${servatrice_RESOURCES}) -endif() +qt6_add_resources(servatrice_RESOURCES_RCC ${servatrice_RESOURCES}) set(QT_DONT_USE_QTGUI TRUE) diff --git a/servatrice/src/serversocketinterface.cpp b/servatrice/src/serversocketinterface.cpp index 6ceebfca9..842ddb4c8 100644 --- a/servatrice/src/serversocketinterface.cpp +++ b/servatrice/src/serversocketinterface.cpp @@ -325,6 +325,10 @@ Response::ResponseCode AbstractServerSocketInterface::cmdAddToList(const Command Event_AddToList event; event.set_list_name(cmd.list()); event.mutable_user_info()->CopyFrom(databaseInterface->getUserData(user)); + // The buddy/ignore list entry is only used to display the user's basic + // profile: never leak the target's email address or client id. + event.mutable_user_info()->clear_email(); + event.mutable_user_info()->clear_clientid(); rc.enqueuePreResponseItem(ServerMessage::SESSION_EVENT, prepareSessionEvent(event)); return Response::RespOk; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 857e0b041..1c3f4c2c6 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -9,6 +9,7 @@ add_test(NAME test_age_formatting COMMAND test_age_formatting) add_test(NAME password_hash_test COMMAND password_hash_test) add_test(NAME server_card_counter_test COMMAND server_card_counter_test) add_test(NAME server_counter_test COMMAND server_counter_test) +add_test(NAME server_rate_limiter_test COMMAND server_rate_limiter_test) add_test(NAME deck_hash_performance_test COMMAND deck_hash_performance_test) set_tests_properties(deck_hash_performance_test PROPERTIES TIMEOUT 5) @@ -23,6 +24,7 @@ add_executable(password_hash_test password_hash_test.cpp) add_executable(deck_hash_performance_test deck_hash_performance_test.cpp) add_executable(server_card_counter_test server_card_counter_test.cpp) add_executable(server_counter_test server_counter_test.cpp) +add_executable(server_rate_limiter_test server_rate_limiter_test.cpp) find_package(GTest) @@ -57,6 +59,7 @@ if(NOT GTEST_FOUND) add_dependencies(deck_hash_performance_test gtest) add_dependencies(server_card_counter_test gtest) add_dependencies(server_counter_test gtest) + add_dependencies(server_rate_limiter_test gtest) endif() include_directories(${GTEST_INCLUDE_DIRS}) @@ -81,6 +84,9 @@ target_link_libraries( target_link_libraries( server_counter_test libcockatrice_network Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES} ) +target_link_libraries( + server_rate_limiter_test libcockatrice_utility Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES} +) add_subdirectory(card_zone_algorithms) add_subdirectory(carddatabase) diff --git a/tests/server_rate_limiter_test.cpp b/tests/server_rate_limiter_test.cpp new file mode 100644 index 000000000..bc15ca421 --- /dev/null +++ b/tests/server_rate_limiter_test.cpp @@ -0,0 +1,151 @@ +#include "gtest/gtest.h" +#include +#include +#include + +namespace +{ + +const QString HOST = "api.scryfall.com"; + +QDateTime timeAt(qint64 secsFromEpoch) +{ + return QDateTime::fromMSecsSinceEpoch(secsFromEpoch * 1000, QTimeZone::UTC); +} + +TEST(ServerRateLimiterTest, First429SetsThirtySecondBackoff) +{ + ServerRateLimiter limiter; + QDateTime now = timeAt(1000); + + QDateTime deadline = limiter.on429(HOST, now); + + EXPECT_TRUE(limiter.isRateLimited(HOST, now)); + EXPECT_EQ(now.addSecs(30), deadline); + EXPECT_EQ(1, limiter.rounds(HOST)); + EXPECT_FALSE(limiter.isRateLimited(HOST, deadline)); +} + +TEST(ServerRateLimiterTest, RetryAfterHeaderIsHonoredWhenLarger) +{ + ServerRateLimiter limiter; + QDateTime now = timeAt(1000); + + QDateTime deadline = limiter.on429(HOST, now, 45 * 1000); + + EXPECT_EQ(now.addSecs(45), deadline); +} + +TEST(ServerRateLimiterTest, RetryAfterSmallerThanFloorIsIgnored) +{ + ServerRateLimiter limiter; + QDateTime now = timeAt(1000); + + QDateTime deadline = limiter.on429(HOST, now, 5 * 1000); + + EXPECT_EQ(now.addSecs(30), deadline); +} + +TEST(ServerRateLimiterTest, Concurrent429sDoNotEscalateOrShorten) +{ + ServerRateLimiter limiter; + QDateTime now = timeAt(1000); + + QDateTime first = limiter.on429(HOST, now); + limiter.on429(HOST, now.addMSecs(100)); + limiter.on429(HOST, now.addMSecs(200)); + + // The burst counts as a single round, but each 429 slides the deadline forward. + EXPECT_EQ(1, limiter.rounds(HOST)); + EXPECT_EQ(now.addMSecs(30200), limiter.deadline(HOST)); + EXPECT_TRUE(limiter.deadline(HOST) >= first); +} + +TEST(ServerRateLimiterTest, EscalatesAfterWindowPasses) +{ + ServerRateLimiter limiter; + QDateTime now = timeAt(1000); + + limiter.on429(HOST, now); + limiter.on429(HOST, now.addSecs(31)); + + EXPECT_EQ(2, limiter.rounds(HOST)); +} + +TEST(ServerRateLimiterTest, BudgetExhaustionAppliesCooldown) +{ + ServerRateLimiter limiter; + QDateTime now = timeAt(1000); + + QDateTime deadline; + for (int i = 0; i < ServerRateLimiter::MAX_RETRIES; ++i) { + deadline = limiter.on429(HOST, now.addSecs(31 * i)); + } + + EXPECT_EQ(ServerRateLimiter::MAX_RETRIES, limiter.rounds(HOST)); + EXPECT_EQ(now.addSecs(31 * (ServerRateLimiter::MAX_RETRIES - 1) + 60), deadline); + + // A 429 inside the exhausted cooldown does not escalate, but keeps sliding the cooldown forward. + limiter.on429(HOST, now.addSecs(31 * (ServerRateLimiter::MAX_RETRIES - 1) + 30)); + EXPECT_EQ(ServerRateLimiter::MAX_RETRIES, limiter.rounds(HOST)); + EXPECT_EQ(now.addSecs(31 * (ServerRateLimiter::MAX_RETRIES - 1) + 30 + 60), limiter.deadline(HOST)); +} + +TEST(ServerRateLimiterTest, SuccessClearsPenalty) +{ + ServerRateLimiter limiter; + QDateTime now = timeAt(1000); + + limiter.on429(HOST, now); + limiter.onSuccess(HOST); + + EXPECT_EQ(0, limiter.rounds(HOST)); + EXPECT_FALSE(limiter.isRateLimited(HOST, now)); +} + +TEST(ServerRateLimiterTest, ClearExpiredRefreshesStaleBudget) +{ + ServerRateLimiter limiter; + QDateTime now = timeAt(1000); + + limiter.on429(HOST, now); + limiter.clearExpired(now.addSecs(400)); + + EXPECT_EQ(0, limiter.rounds(HOST)); + EXPECT_FALSE(limiter.isRateLimited(HOST, now.addSecs(400))); +} + +TEST(ServerRateLimiterTest, ClearExpiredKeepsRecentBudget) +{ + ServerRateLimiter limiter; + QDateTime now = timeAt(1000); + + for (int i = 0; i < ServerRateLimiter::MAX_RETRIES; ++i) { + limiter.on429(HOST, now.addSecs(31 * i)); + } + limiter.clearExpired(now.addSecs(200)); + + EXPECT_EQ(ServerRateLimiter::MAX_RETRIES, limiter.rounds(HOST)); + EXPECT_FALSE(limiter.isRateLimited(HOST, now.addSecs(200))); +} + +TEST(ServerRateLimiterTest, EarliestDeadlineAcrossHosts) +{ + ServerRateLimiter limiter; + QDateTime now = timeAt(1000); + + limiter.on429("api.scryfall.com", now, 40 * 1000); + limiter.on429("gatherer.wizards.com", now.addSecs(5)); + + EXPECT_EQ(now.addSecs(35), limiter.earliestDeadline(now)); + EXPECT_EQ(now.addSecs(40), limiter.deadline("api.scryfall.com")); + EXPECT_EQ(now.addSecs(40), limiter.earliestDeadline(now.addSecs(36))); +} + +} // namespace + +int main(int argc, char **argv) +{ + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/tests/settings/settings_defaults_test.cpp b/tests/settings/settings_defaults_test.cpp index 0cd68d9d4..dfdad4780 100644 --- a/tests/settings/settings_defaults_test.cpp +++ b/tests/settings/settings_defaults_test.cpp @@ -1,9 +1,15 @@ #include "gtest/gtest.h" #include #include +#include #include +#include #include +#include +#include #include +#include +#include #include #include #include @@ -258,66 +264,218 @@ TEST_F(SettingsDefaultsTest, Personal_Lang_Default) ASSERT_EQ(s.getLang(), QString("")); } -TEST_F(SettingsDefaultsTest, Personal_ClientID_Default) -{ - PersonalSettings s(settingsPath, nullptr); - ASSERT_EQ(s.getClientID(), QString("notset")); -} - -TEST_F(SettingsDefaultsTest, Personal_KeepAlive_Default) -{ - PersonalSettings s(settingsPath, nullptr); - ASSERT_EQ(s.getKeepAlive(), 3); -} - -TEST_F(SettingsDefaultsTest, Personal_TimeOut_Default) -{ - PersonalSettings s(settingsPath, nullptr); - ASSERT_EQ(s.getTimeOut(), 5); -} - -TEST_F(SettingsDefaultsTest, Personal_PicDownload_Default) -{ - PersonalSettings s(settingsPath, nullptr); - ASSERT_EQ(s.getPicDownload(), true); -} - -TEST_F(SettingsDefaultsTest, Personal_ShowStatusBar_Default) -{ - PersonalSettings s(settingsPath, nullptr); - ASSERT_EQ(s.getShowStatusBar(), false); -} - -TEST_F(SettingsDefaultsTest, Personal_MaxFontSize_Default) -{ - PersonalSettings s(settingsPath, nullptr); - ASSERT_EQ(s.getMaxFontSize(), 12); -} - -TEST_F(SettingsDefaultsTest, Personal_HomeTabBackgroundSource_Default) -{ - PersonalSettings s(settingsPath, nullptr); - ASSERT_EQ(s.getHomeTabBackgroundSource(), QString("themed")); -} - -TEST_F(SettingsDefaultsTest, Personal_HomeTabDisplayCardName_Default) -{ - PersonalSettings s(settingsPath, nullptr); - ASSERT_EQ(s.getHomeTabDisplayCardName(), true); -} - TEST_F(SettingsDefaultsTest, Personal_ShowTipsOnStartup_Default) { PersonalSettings s(settingsPath, nullptr); ASSERT_EQ(s.getShowTipsOnStartup(), true); } -TEST_F(SettingsDefaultsTest, Personal_DownloadSpoilersStatus_Default) +// --- DownloadSettings --- + +TEST_F(SettingsDefaultsTest, Download_PicDownload_Default) { - PersonalSettings s(settingsPath, nullptr); + DownloadSettings s(settingsPath, nullptr); + ASSERT_EQ(s.getPicDownload(), true); +} + +TEST_F(SettingsDefaultsTest, Download_DownloadSpoilersStatus_Default) +{ + DownloadSettings s(settingsPath, nullptr); ASSERT_EQ(s.getDownloadSpoilersStatus(), false); } +// --- AppearanceSettings --- + +TEST_F(SettingsDefaultsTest, Appearance_ThemeName_Default) +{ + AppearanceSettings s(settingsPath, nullptr); + ASSERT_EQ(s.getThemeName(), QString("")); +} + +TEST_F(SettingsDefaultsTest, Appearance_ThemeName_SetAndGet) +{ + AppearanceSettings s(settingsPath, nullptr); + s.setThemeName("my_theme"); + ASSERT_EQ(s.getThemeName(), QString("my_theme")); +} + +TEST_F(SettingsDefaultsTest, Appearance_StyleUserList_Default) +{ + AppearanceSettings s(settingsPath, nullptr); + ASSERT_EQ(s.getStyleUserList(), true); +} + +TEST_F(SettingsDefaultsTest, Appearance_MaxFontSize_Default) +{ + AppearanceSettings s(settingsPath, nullptr); + ASSERT_EQ(s.getMaxFontSize(), 12); +} + +TEST_F(SettingsDefaultsTest, Appearance_MaxFontSize_SetAndGet) +{ + AppearanceSettings s(settingsPath, nullptr); + s.setMaxFontSize(14); + ASSERT_EQ(s.getMaxFontSize(), 14); +} + +TEST_F(SettingsDefaultsTest, Appearance_HomeTabBackgroundSource_Default) +{ + AppearanceSettings s(settingsPath, nullptr); + ASSERT_EQ(s.getHomeTabBackgroundSource(), QString("themed")); +} + +TEST_F(SettingsDefaultsTest, Appearance_HomeTabBackgroundShuffleFrequency_Default) +{ + AppearanceSettings s(settingsPath, nullptr); + ASSERT_EQ(s.getHomeTabBackgroundShuffleFrequency(), 0); +} + +TEST_F(SettingsDefaultsTest, Appearance_HomeTabDisplayCardName_Default) +{ + AppearanceSettings s(settingsPath, nullptr); + ASSERT_EQ(s.getHomeTabDisplayCardName(), true); +} + +// --- InterfaceSettings --- + +TEST_F(SettingsDefaultsTest, Interface_ShowStatusBar_Default) +{ + InterfaceSettings s(settingsPath, nullptr); + ASSERT_EQ(s.getShowStatusBar(), false); +} + +TEST_F(SettingsDefaultsTest, Interface_ShowShortcuts_Default) +{ + InterfaceSettings s(settingsPath, nullptr); + ASSERT_EQ(s.getShowShortcuts(), true); +} + +TEST_F(SettingsDefaultsTest, Interface_ShowGameSelectorFilterToolbar_Default) +{ + InterfaceSettings s(settingsPath, nullptr); + ASSERT_EQ(s.getShowGameSelectorFilterToolbar(), true); +} + +TEST_F(SettingsDefaultsTest, Interface_NotificationsEnabled_Default) +{ + InterfaceSettings s(settingsPath, nullptr); + ASSERT_EQ(s.getNotificationsEnabled(), true); +} + +TEST_F(SettingsDefaultsTest, Interface_SpectatorNotificationsEnabled_Default) +{ + InterfaceSettings s(settingsPath, nullptr); + ASSERT_EQ(s.getSpectatorNotificationsEnabled(), false); +} + +TEST_F(SettingsDefaultsTest, Interface_BuddyConnectNotificationsEnabled_Default) +{ + InterfaceSettings s(settingsPath, nullptr); + ASSERT_EQ(s.getBuddyConnectNotificationsEnabled(), true); +} + +// --- DeckEditorSettings --- + +TEST_F(SettingsDefaultsTest, DeckEditor_OpenDeckInNewTab_Default) +{ + DeckEditorSettings s(settingsPath, nullptr); + ASSERT_EQ(s.getOpenDeckInNewTab(), false); +} + +TEST_F(SettingsDefaultsTest, DeckEditor_BannerCardComboBoxVisible_Default) +{ + DeckEditorSettings s(settingsPath, nullptr); + ASSERT_EQ(s.getBannerCardComboBoxVisible(), true); +} + +TEST_F(SettingsDefaultsTest, DeckEditor_TagsWidgetVisible_Default) +{ + DeckEditorSettings s(settingsPath, nullptr); + ASSERT_EQ(s.getTagsWidgetVisible(), true); +} + +TEST_F(SettingsDefaultsTest, DeckEditor_DefaultDeckEditorType_Default) +{ + DeckEditorSettings s(settingsPath, nullptr); + ASSERT_EQ(s.getDefaultDeckEditorType(), 1); +} + +// --- NetworkSettings --- + +TEST_F(SettingsDefaultsTest, Network_ClientID_Default) +{ + NetworkSettings s(settingsPath, nullptr); + ASSERT_EQ(s.getClientID(), QString("notset")); +} + +TEST_F(SettingsDefaultsTest, Network_ClientVersion_Default) +{ + NetworkSettings s(settingsPath, nullptr); + ASSERT_EQ(s.getClientVersion(), QString("notset")); +} + +TEST_F(SettingsDefaultsTest, Network_KeepAlive_Default) +{ + NetworkSettings s(settingsPath, nullptr); + ASSERT_EQ(s.getKeepAlive(), 3); +} + +TEST_F(SettingsDefaultsTest, Network_TimeOut_Default) +{ + NetworkSettings s(settingsPath, nullptr); + ASSERT_EQ(s.getTimeOut(), 5); +} + +TEST_F(SettingsDefaultsTest, Network_KnownMissingFeatures_Default) +{ + NetworkSettings s(settingsPath, nullptr); + ASSERT_EQ(s.getKnownMissingFeatures(), QString("")); +} + +// --- CardsDisplaySettings --- + +TEST_F(SettingsDefaultsTest, CardsDisplay_PrintingSelectorCardSize_Default) +{ + CardsDisplaySettings s(settingsPath, nullptr); + ASSERT_EQ(s.getPrintingSelectorCardSize(), 100); +} + +TEST_F(SettingsDefaultsTest, CardsDisplay_VisualDeckStorageCardSize_Default) +{ + CardsDisplaySettings s(settingsPath, nullptr); + ASSERT_EQ(s.getVisualDeckStorageCardSize(), 100); +} + +TEST_F(SettingsDefaultsTest, CardsDisplay_VisualDatabaseDisplayCardSize_Default) +{ + CardsDisplaySettings s(settingsPath, nullptr); + ASSERT_EQ(s.getVisualDatabaseDisplayCardSize(), 100); +} + +TEST_F(SettingsDefaultsTest, CardsDisplay_VisualDeckEditorCardSize_Default) +{ + CardsDisplaySettings s(settingsPath, nullptr); + ASSERT_EQ(s.getVisualDeckEditorCardSize(), 100); +} + +TEST_F(SettingsDefaultsTest, CardsDisplay_EDHRecCardSize_Default) +{ + CardsDisplaySettings s(settingsPath, nullptr); + ASSERT_EQ(s.getEDHRecCardSize(), 100); +} + +TEST_F(SettingsDefaultsTest, CardsDisplay_ArchidektPreviewSize_Default) +{ + CardsDisplaySettings s(settingsPath, nullptr); + ASSERT_EQ(s.getArchidektPreviewSize(), 100); +} + +TEST_F(SettingsDefaultsTest, CardsDisplay_SampleHandSize_Default) +{ + CardsDisplaySettings s(settingsPath, nullptr); + ASSERT_EQ(s.getSampleHandSize(), 7); +} + // --- VisualDeckStorageSettings --- TEST_F(SettingsDefaultsTest, VisualDeckStorage_SortingOrder_Default) diff --git a/tests/settings/settings_migration_test.cpp b/tests/settings/settings_migration_test.cpp index 00e5c65c1..8a3d756a0 100644 --- a/tests/settings/settings_migration_test.cpp +++ b/tests/settings/settings_migration_test.cpp @@ -1,5 +1,7 @@ #include "gtest/gtest.h" +#include #include +#include #include #include #include @@ -10,18 +12,6 @@ namespace { -static bool nativeSettingsAvailable() -{ - QSettings probe; - probe.setValue("_migration_native_probe", "ok"); - probe.sync(); - QSettings read; - bool ok = read.value("_migration_native_probe").toString() == "ok"; - QSettings().clear(); - QSettings().sync(); - return ok; -} - class SettingsMigrationTest : public ::testing::Test { protected: @@ -31,6 +21,17 @@ protected: void SetUp() override { settingsPath = tempDir.path() + "/"; + + // Isolate the settings used by the legacy migration tests inside the temporary + // directory so the tests never read or write the real user config (registry on + // Windows, plist on macOS, .conf on Linux), which would otherwise be shared + // across CI jobs and flaky. Default-format QSettings is forced to IniFormat and + // its UserScope path is redirected here; setPath wins over the XDG_CONFIG_HOME + // default on Unix, so every platform resolves to /config/... + const QString configDir = tempDir.path() + "/config"; + QDir().mkpath(configDir); + qputenv("XDG_CONFIG_HOME", configDir.toUtf8()); + QSettings::setPath(QSettings::IniFormat, QSettings::UserScope, configDir); } bool fileExists(const QString &name) const @@ -43,6 +44,26 @@ protected: QSettings ini(settingsPath + fileName, QSettings::IniFormat); return ini.value(key); } + + // Checks for the exact legacy key in the raw INI content. On Windows, INI keys are + // case-insensitive, so reading back a lower-case legacy key would match its migrated + // camelCase counterpart and hide regressions. Checking the file bytes directly keeps + // the comparison case-sensitive on every platform. + bool iniFileHasKeyCaseSensitive(const QString &fileName, const QString &key) const + { + QFile f(settingsPath + fileName); + if (!f.open(QIODevice::ReadOnly)) { + return false; + } + const QString valueName = key.section('/', -1); + const QStringList lines = QString::fromUtf8(f.readAll()).split('\n'); + for (const auto &line : lines) { + if (line.startsWith(valueName + '=')) { + return true; + } + } + return false; + } }; TEST_F(SettingsMigrationTest, NoGlobalIniDoesNothing) @@ -95,6 +116,8 @@ TEST_F(SettingsMigrationTest, MigratesAllSettingsGroups) g.setValue("maxplayers", 4); g.setValue("gamedescription", "test game"); g.setValue("remembergamesettings", false); + g.setValue("gametypes", "commander"); + g.setValue("onlybuddies", true); g.endGroup(); // localgameoptions @@ -108,44 +131,119 @@ TEST_F(SettingsMigrationTest, MigratesAllSettingsGroups) g.setValue("mention", false); g.setValue("mentioncolor", "FF0000"); g.setValue("showmessagepopups", false); + g.setValue("mentioncompleter", false); + g.setValue("roomhistory", false); + g.setValue("highlightcolor", "00FF00"); g.endGroup(); + // legacy highlight words (under [personal]) + g.setValue("personal/highlightWords", "alpha beta"); + // cache storage (under [personal] group) g.setValue("personal/pixmapCacheSize", 1024); g.setValue("personal/networkCacheSize", 2048); + g.setValue("personal/redirectCacheTtl", 5); + g.setValue("personal/cardPictureLoaderCacheMethod", 1); + g.setValue("personal/localCardImageStorageNamingScheme", 2); // updates (under [personal] group) g.setValue("personal/startupUpdateCheck", false); + g.setValue("personal/startupCardUpdateCheckPromptForUpdate", false); + g.setValue("personal/startupCardUpdateCheckAlwaysUpdate", true); g.setValue("personal/cardUpdateCheckInterval", 14); + g.setValue("personal/lastCardUpdateCheck", QDate(2024, 1, 1)); + g.setValue("personal/alwaysEnableNewSets", true); + g.setValue("personal/updatenotification", false); + g.setValue("personal/newversionnotification", false); // personal g.setValue("personal/lang", "de"); - g.setValue("personal/keepalive", 10); - g.setValue("personal/timeout", 30); - g.setValue("personal/clientid", "test-client-id"); + + // downloads (previously under [personal]) g.setValue("personal/picturedownload", true); + g.setValue("personal/downloadspoilers", true); + + // interface (previously under [personal]) g.setValue("personal/showStatusBar", true); + // theme + g.setValue("theme/name", "custom_theme"); + g.setValue("game/maxfontsize", 14); + + // appearance + g.setValue("appearance/styleUserList", false); + g.setValue("home/background/displayCardName", false); + g.setValue("menu/showshortcuts", false); + g.setValue("menu/showgameselectorfiltertoolbar", false); + + // deck editor + g.setValue("editor/openDeckInNewTab", false); + g.setValue("interface/deckeditortagswidgetvisible", false); + g.setValue("interface/defaultDeckEditorType", 0); + g.setValue("interface/visualdeckeditorsamplehandsize", 5); + // personal home g.setValue("home/background", "custom_bg"); g.setValue("home/background/shuffleTimer", 30); // personal tipOfDay g.setValue("tipOfDay/showTips", false); + g.setValue("tipOfDay/seenTips", QStringList{"1", "2", "3"}); + + // network + g.setValue("personal/keepalive", 10); + g.setValue("personal/timeout", 30); + g.setValue("personal/clientid", "test-client-id"); + g.setValue("personal/clientversion", "test-client-version"); + g.setValue("interface/knownmissingfeatures", "feature1,feature2"); // cards g.setValue("cards/displaycardnames", false); + g.setValue("cards/roundcardcorners", false); + g.setValue("cards/overrideallcardartwithpersonalpreference", true); + g.setValue("cards/bumpsetswithcardsindecktotop", false); + g.setValue("cards/includerebalancedcards", false); + g.setValue("cards/autorotatesidewayslayoutcards", false); g.setValue("cards/tapanimation", true); g.setValue("cards/scaleCards", false); + g.setValue("cards/verticalCardOverlapPercent", 42); + g.setValue("cards/cardinfoviewmode", 1); + g.setValue("cards/printingselectorcardsize", 90); + g.setValue("cards/printingselectorsortorder", 3); + g.setValue("cards/printingselectornavigationbuttonsvisible", false); + // card counters (migrate into card_counters.ini) + g.setValue("cards/counters/0/color", QColor(Qt::red)); // interface g.setValue("interface/usetearoffmenus", true); + g.setValue("interface/cardViewInitialRowsMax", 8); + g.setValue("interface/cardViewExpandedRowsMax", 12); + g.setValue("interface/closeEmptyCardView", false); + g.setValue("interface/focusCardViewSearchBar", false); + g.setValue("interface/keepGameChatFocus", true); g.setValue("interface/notificationsenabled", false); + g.setValue("interface/specnotificationsenabled", true); + g.setValue("interface/buddyconnectnotificationsenabled", false); + g.setValue("interface/doubleclicktoplay", false); + g.setValue("interface/clickPlaysAllSelected", false); + g.setValue("interface/playtostack", false); + g.setValue("interface/doNotDeleteArrowsInSubPhases", false); g.setValue("interface/startinghandsize", 5); - - // hand/table + g.setValue("interface/annotatetokens", true); + g.setValue("interface/showlassoselectioncount", false); + g.setValue("interface/showpersistentselectioncount", false); + g.setValue("interface/tallyType", 2); + g.setValue("interface/leftjustified", true); + g.setValue("interface/min_players_multicolumn", 6); + g.setValue("interface/deckeditorbannercardcomboboxvisible", false); + // hand/table/replay/zoneview g.setValue("hand/horizontal", true); g.setValue("table/invert_vertical", true); + g.setValue("replay/rewindBufferingMs", 6000); + g.setValue("replay/fastForwardSpeed", 5); + g.setValue("zoneview/groupby", 2); + g.setValue("zoneview/sortby", 1); + g.setValue("zoneview/pileview", false); // paths g.beginGroup("paths"); @@ -155,8 +253,29 @@ TEST_F(SettingsMigrationTest, MigratesAllSettingsGroups) // visual deck storage (under [interface] group) g.setValue("interface/visualdeckstoragecardsize", 150); + g.setValue("interface/visualdeckstoragesortingorder", 2); g.setValue("interface/visualdeckstorageshowfolders", false); g.setValue("interface/visualdeckstorageshowtagfilter", false); + g.setValue("interface/visualdeckstoragedefaulttagslist", QStringList{"Alpha", "Beta"}); + g.setValue("interface/visualdeckstoragesearchfoldernames", false); + g.setValue("interface/visualdeckstorageshowcoloridentity", false); + g.setValue("interface/visualdeckstorageshowbannercardcombobox", false); + g.setValue("interface/visualdeckstorageshowtagsondeckpreviews", false); + g.setValue("interface/visualdeckstoragedrawunusedcoloridentities", false); + g.setValue("interface/visualdeckstorageunusedcoloridentitiesopacity", 35); + g.setValue("interface/visualdeckstoragetooltiptype", 1); + g.setValue("interface/visualdeckstoragepromptforconversion", false); + g.setValue("interface/visualdeckstoragealwaysconvert", true); + g.setValue("interface/visualdeckstorageingame", false); + g.setValue("interface/visualdeckstorageselectionanimation", false); + g.setValue("interface/visualdatabasedisplayfiltertomostrecentsetsenabled", true); + g.setValue("interface/visualdatabasedisplayfiltertomostrecentsetsamount", 25); + + // card sizes (migrate into cards_display.ini) + g.setValue("interface/visualdatabasedisplaycardsize", 80); + g.setValue("interface/visualdeckeditorcardsize", 70); + g.setValue("interface/edhreccardsize", 60); + g.setValue("interface/archidektpreviewsize", 50); g.sync(); } @@ -176,54 +295,189 @@ TEST_F(SettingsMigrationTest, MigratesAllSettingsGroups) ASSERT_TRUE(fileExists("sound.ini")); ASSERT_EQ(readFromIni("sound.ini", "sound/enabled"), QVariant(true)); ASSERT_EQ(readFromIni("sound.ini", "sound/theme"), QVariant("custom_theme")); - ASSERT_EQ(readFromIni("sound.ini", "sound/mastervolume"), QVariant(75)); + ASSERT_EQ(readFromIni("sound.ini", "sound/masterVolume"), QVariant(75)); ASSERT_TRUE(fileExists("game.ini")); - ASSERT_EQ(readFromIni("game.ini", "game/maxplayers"), QVariant(4)); - ASSERT_EQ(readFromIni("game.ini", "game/gamedescription"), QVariant("test game")); - ASSERT_EQ(readFromIni("game.ini", "game/remembergamesettings"), QVariant(false)); - ASSERT_EQ(readFromIni("game.ini", "localgameoptions/maxplayers"), QVariant(2)); - ASSERT_EQ(readFromIni("game.ini", "localgameoptions/startinglifetotal"), QVariant(40)); + ASSERT_EQ(readFromIni("game.ini", "game/maxPlayers"), QVariant(4)); + ASSERT_EQ(readFromIni("game.ini", "game/gameDescription"), QVariant("test game")); + ASSERT_EQ(readFromIni("game.ini", "game/rememberGameSettings"), QVariant(false)); + ASSERT_EQ(readFromIni("game.ini", "game/gameTypes"), QVariant("commander")); + ASSERT_EQ(readFromIni("game.ini", "game/onlyBuddies"), QVariant(true)); + ASSERT_EQ(readFromIni("game.ini", "localgameoptions/maxPlayers"), QVariant(2)); + ASSERT_EQ(readFromIni("game.ini", "localgameoptions/startingLifeTotal"), QVariant(40)); ASSERT_TRUE(fileExists("chat.ini")); ASSERT_EQ(readFromIni("chat.ini", "chat/mention"), QVariant(false)); - ASSERT_EQ(readFromIni("chat.ini", "chat/mentioncolor"), QVariant("FF0000")); - ASSERT_EQ(readFromIni("chat.ini", "chat/showmessagepopups"), QVariant(false)); + ASSERT_EQ(readFromIni("chat.ini", "chat/mentionColor"), QVariant("FF0000")); + ASSERT_EQ(readFromIni("chat.ini", "chat/showMessagePopups"), QVariant(false)); + ASSERT_EQ(readFromIni("chat.ini", "chat/mentionCompleter"), QVariant(false)); + ASSERT_EQ(readFromIni("chat.ini", "chat/roomHistory"), QVariant(false)); + ASSERT_EQ(readFromIni("chat.ini", "chat/highlightColor"), QVariant("00FF00")); + ASSERT_EQ(readFromIni("chat.ini", "chat/highlightWords"), QVariant("alpha beta")); ASSERT_TRUE(fileExists("cache_storage.ini")); - ASSERT_EQ(readFromIni("cache_storage.ini", "personal/pixmapCacheSize"), QVariant(1024)); - ASSERT_EQ(readFromIni("cache_storage.ini", "personal/networkCacheSize"), QVariant(2048)); + ASSERT_EQ(readFromIni("cache_storage.ini", "cache_storage/pixmapCacheSize"), QVariant(1024)); + ASSERT_EQ(readFromIni("cache_storage.ini", "cache_storage/networkCacheSize"), QVariant(2048)); + ASSERT_EQ(readFromIni("cache_storage.ini", "cache_storage/redirectCacheTtl"), QVariant(5)); + ASSERT_EQ(readFromIni("cache_storage.ini", "cache_storage/cardPictureLoaderCacheMethod"), QVariant(1)); + ASSERT_EQ(readFromIni("cache_storage.ini", "cache_storage/localCardImageStorageNamingScheme"), QVariant(2)); ASSERT_TRUE(fileExists("updates.ini")); ASSERT_EQ(readFromIni("updates.ini", "updates/startupUpdateCheck"), QVariant(false)); + ASSERT_EQ(readFromIni("updates.ini", "updates/startupCardUpdateCheckPromptForUpdate"), QVariant(false)); + ASSERT_EQ(readFromIni("updates.ini", "updates/startupCardUpdateCheckAlwaysUpdate"), QVariant(true)); ASSERT_EQ(readFromIni("updates.ini", "updates/cardUpdateCheckInterval"), QVariant(14)); + ASSERT_EQ(readFromIni("updates.ini", "updates/lastCardUpdateCheck"), QVariant(QDate(2024, 1, 1))); + ASSERT_EQ(readFromIni("updates.ini", "updates/alwaysEnableNewSets"), QVariant(true)); + ASSERT_EQ(readFromIni("updates.ini", "updates/updateNotification"), QVariant(false)); + ASSERT_EQ(readFromIni("updates.ini", "updates/newVersionNotification"), QVariant(false)); ASSERT_TRUE(fileExists("personal.ini")); ASSERT_EQ(readFromIni("personal.ini", "personal/lang"), QVariant("de")); - ASSERT_EQ(readFromIni("personal.ini", "personal/keepalive"), QVariant(10)); - ASSERT_EQ(readFromIni("personal.ini", "personal/clientid"), QVariant("test-client-id")); - ASSERT_EQ(readFromIni("personal.ini", "personal/showStatusBar"), QVariant(true)); - ASSERT_EQ(readFromIni("personal.ini", "home/background"), QVariant("custom_bg")); ASSERT_EQ(readFromIni("personal.ini", "tipOfDay/showTips"), QVariant(false)); + ASSERT_EQ(readFromIni("personal.ini", "tipOfDay/seenTips"), QVariant(QStringList{"1", "2", "3"})); + + ASSERT_TRUE(fileExists("downloads.ini")); + ASSERT_EQ(readFromIni("downloads.ini", "downloads/pictureDownload"), QVariant(true)); + ASSERT_EQ(readFromIni("downloads.ini", "downloads/downloadSpoilers"), QVariant(true)); + + ASSERT_TRUE(fileExists("appearance.ini")); + ASSERT_EQ(readFromIni("appearance.ini", "appearance/themeName"), QVariant("custom_theme")); + ASSERT_EQ(readFromIni("appearance.ini", "appearance/maxFontSize"), QVariant(14)); + ASSERT_EQ(readFromIni("appearance.ini", "appearance/styleUserList"), QVariant(false)); + ASSERT_EQ(readFromIni("appearance.ini", "appearance/homeTabBackgroundSource"), QVariant("custom_bg")); + ASSERT_EQ(readFromIni("appearance.ini", "appearance/homeTabBackgroundShuffleFrequency"), QVariant(30)); + ASSERT_EQ(readFromIni("appearance.ini", "appearance/homeTabDisplayCardName"), QVariant(false)); + + ASSERT_TRUE(fileExists("network.ini")); + ASSERT_EQ(readFromIni("network.ini", "network/keepAlive"), QVariant(10)); + ASSERT_EQ(readFromIni("network.ini", "network/timeout"), QVariant(30)); + ASSERT_EQ(readFromIni("network.ini", "network/clientId"), QVariant("test-client-id")); + ASSERT_EQ(readFromIni("network.ini", "network/clientVersion"), QVariant("test-client-version")); + ASSERT_EQ(readFromIni("network.ini", "network/knownMissingFeatures"), QVariant("feature1,feature2")); ASSERT_TRUE(fileExists("cards_display.ini")); - ASSERT_EQ(readFromIni("cards_display.ini", "cards/displaycardnames"), QVariant(false)); - ASSERT_EQ(readFromIni("cards_display.ini", "cards/tapanimation"), QVariant(true)); + ASSERT_EQ(readFromIni("cards_display.ini", "cards/displayCardNames"), QVariant(false)); + ASSERT_EQ(readFromIni("cards_display.ini", "cards/roundCardCorners"), QVariant(false)); + ASSERT_EQ(readFromIni("cards_display.ini", "cards/overrideAllCardArtWithPersonalPreference"), QVariant(true)); + ASSERT_EQ(readFromIni("cards_display.ini", "cards/bumpSetsWithCardsInDeckToTop"), QVariant(false)); + ASSERT_EQ(readFromIni("cards_display.ini", "cards/includerebalancedcards"), QVariant(false)); + ASSERT_EQ(readFromIni("cards_display.ini", "cards/autoRotateSidewaysLayoutCards"), QVariant(false)); + ASSERT_EQ(readFromIni("cards_display.ini", "cards/tapAnimation"), QVariant(true)); + ASSERT_EQ(readFromIni("cards_display.ini", "cards/scaleCards"), QVariant(false)); + ASSERT_EQ(readFromIni("cards_display.ini", "cards/verticalCardOverlapPercent"), QVariant(42)); + ASSERT_EQ(readFromIni("cards_display.ini", "cards/cardInfoViewMode"), QVariant(1)); + ASSERT_EQ(readFromIni("cards_display.ini", "cards/cardSize/printingSelector"), QVariant(90)); + ASSERT_EQ(readFromIni("cards_display.ini", "cards/printingSelector/sortOrder"), QVariant(3)); + ASSERT_EQ(readFromIni("cards_display.ini", "cards/printingSelector/navigationButtonsVisible"), QVariant(false)); + ASSERT_EQ(readFromIni("cards_display.ini", "cards/cardSize/visualDeckStorage"), QVariant(150)); + ASSERT_EQ(readFromIni("cards_display.ini", "cards/cardSize/visualDatabaseDisplay"), QVariant(80)); + ASSERT_EQ(readFromIni("cards_display.ini", "cards/cardSize/visualDeckEditor"), QVariant(70)); + ASSERT_EQ(readFromIni("cards_display.ini", "cards/cardSize/edhrec"), QVariant(60)); + ASSERT_EQ(readFromIni("cards_display.ini", "cards/cardSize/archidektPreview"), QVariant(50)); + ASSERT_EQ(readFromIni("cards_display.ini", "cards/cardSize/sampleHandSize"), QVariant(5)); + + ASSERT_TRUE(fileExists("card_counters.ini")); + ASSERT_EQ(readFromIni("card_counters.ini", "cards/counters/0/color").toString(), QColor(Qt::red).name()); + ASSERT_FALSE(readFromIni("global.ini", "cards/counters/0/color").isValid()); ASSERT_TRUE(fileExists("interface.ini")); - ASSERT_EQ(readFromIni("interface.ini", "interface/usetearoffmenus"), QVariant(true)); - ASSERT_EQ(readFromIni("interface.ini", "interface/notificationsenabled"), QVariant(false)); - ASSERT_EQ(readFromIni("interface.ini", "interface/startinghandsize"), QVariant(5)); + ASSERT_EQ(readFromIni("interface.ini", "interface/useTearOffMenus"), QVariant(true)); + ASSERT_EQ(readFromIni("interface.ini", "interface/cardViewInitialRowsMax"), QVariant(8)); + ASSERT_EQ(readFromIni("interface.ini", "interface/cardViewExpandedRowsMax"), QVariant(12)); + ASSERT_EQ(readFromIni("interface.ini", "interface/closeEmptyCardView"), QVariant(false)); + ASSERT_EQ(readFromIni("interface.ini", "interface/focusCardViewSearchBar"), QVariant(false)); + ASSERT_EQ(readFromIni("interface.ini", "interface/keepGameChatFocus"), QVariant(true)); + ASSERT_EQ(readFromIni("interface.ini", "interface/notifications/enabled"), QVariant(false)); + ASSERT_EQ(readFromIni("interface.ini", "interface/notifications/spectatorsEnabled"), QVariant(true)); + ASSERT_EQ(readFromIni("interface.ini", "interface/notifications/buddyConnectEnabled"), QVariant(false)); + ASSERT_EQ(readFromIni("interface.ini", "interface/doubleClickToPlay"), QVariant(false)); + ASSERT_EQ(readFromIni("interface.ini", "interface/clickPlaysAllSelected"), QVariant(false)); + ASSERT_EQ(readFromIni("interface.ini", "interface/playToStack"), QVariant(false)); + ASSERT_EQ(readFromIni("interface.ini", "interface/doNotDeleteArrowsInSubPhases"), QVariant(false)); + ASSERT_EQ(readFromIni("interface.ini", "interface/startingHandSize"), QVariant(5)); + ASSERT_EQ(readFromIni("interface.ini", "interface/annotateTokens"), QVariant(true)); + ASSERT_EQ(readFromIni("interface.ini", "interface/showLassoSelectionCount"), QVariant(false)); + ASSERT_EQ(readFromIni("interface.ini", "interface/showPersistentSelectionCount"), QVariant(false)); + ASSERT_EQ(readFromIni("interface.ini", "interface/tallyType"), QVariant(2)); + ASSERT_EQ(readFromIni("interface.ini", "interface/leftJustified"), QVariant(true)); + ASSERT_EQ(readFromIni("interface.ini", "interface/minPlayersMulticolumn"), QVariant(6)); + ASSERT_EQ(readFromIni("interface.ini", "interface/showStatusBar"), QVariant(true)); + ASSERT_EQ(readFromIni("interface.ini", "interface/showShortcuts"), QVariant(false)); + ASSERT_EQ(readFromIni("interface.ini", "interface/showGameSelectorFilterToolbar"), QVariant(false)); ASSERT_EQ(readFromIni("interface.ini", "hand/horizontal"), QVariant(true)); - ASSERT_EQ(readFromIni("interface.ini", "table/invert_vertical"), QVariant(true)); + ASSERT_EQ(readFromIni("interface.ini", "table/invertVertical"), QVariant(true)); + ASSERT_EQ(readFromIni("interface.ini", "replay/rewindBufferingMs"), QVariant(6000)); + ASSERT_EQ(readFromIni("interface.ini", "replay/fastForwardSpeed"), QVariant(5)); + ASSERT_EQ(readFromIni("interface.ini", "zoneview/groupBy"), QVariant(2)); + ASSERT_EQ(readFromIni("interface.ini", "zoneview/sortBy"), QVariant(1)); + ASSERT_EQ(readFromIni("interface.ini", "zoneview/pileView"), QVariant(false)); + + ASSERT_TRUE(fileExists("deck_editor.ini")); + ASSERT_EQ(readFromIni("deck_editor.ini", "deckeditor/openDeckInNewTab"), QVariant(false)); + ASSERT_EQ(readFromIni("deck_editor.ini", "deckeditor/bannerCardComboBoxVisible"), QVariant(false)); + ASSERT_EQ(readFromIni("deck_editor.ini", "deckeditor/tagsWidgetVisible"), QVariant(false)); + ASSERT_EQ(readFromIni("deck_editor.ini", "deckeditor/defaultDeckEditorType"), QVariant(0)); ASSERT_TRUE(fileExists("paths.ini")); ASSERT_EQ(readFromIni("paths.ini", "paths/decks"), QVariant("/custom/decks")); ASSERT_EQ(readFromIni("paths.ini", "paths/pics"), QVariant("/custom/pics")); ASSERT_TRUE(fileExists("visual_deck_storage.ini")); - ASSERT_EQ(readFromIni("visual_deck_storage.ini", "interface/visualdeckstoragecardsize"), QVariant(150)); - ASSERT_EQ(readFromIni("visual_deck_storage.ini", "interface/visualdeckstorageshowfolders"), QVariant(false)); + ASSERT_EQ(readFromIni("visual_deck_storage.ini", "interface/visualDeckStorage/sortingOrder"), QVariant(2)); + ASSERT_EQ(readFromIni("visual_deck_storage.ini", "interface/visualDeckStorage/showFolders"), QVariant(false)); + ASSERT_EQ(readFromIni("visual_deck_storage.ini", "interface/visualDeckStorage/showTagFilter"), QVariant(false)); + ASSERT_EQ(readFromIni("visual_deck_storage.ini", "interface/visualDeckStorage/defaultTagsList"), + QVariant(QStringList{"Alpha", "Beta"})); + ASSERT_EQ(readFromIni("visual_deck_storage.ini", "interface/visualDeckStorage/searchFolderNames"), QVariant(false)); + ASSERT_EQ(readFromIni("visual_deck_storage.ini", "interface/visualDeckStorage/showColorIdentity"), QVariant(false)); + ASSERT_EQ(readFromIni("visual_deck_storage.ini", "interface/visualDeckStorage/showBannerCardComboBox"), + QVariant(false)); + ASSERT_EQ(readFromIni("visual_deck_storage.ini", "interface/visualDeckStorage/showTagsOnDeckPreviews"), + QVariant(false)); + ASSERT_EQ(readFromIni("visual_deck_storage.ini", "interface/visualDeckStorage/drawUnusedColorIdentities"), + QVariant(false)); + ASSERT_EQ(readFromIni("visual_deck_storage.ini", "interface/visualDeckStorage/unusedColorIdentitiesOpacity"), + QVariant(35)); + ASSERT_EQ(readFromIni("visual_deck_storage.ini", "interface/visualDeckStorage/tooltipType"), QVariant(1)); + ASSERT_EQ(readFromIni("visual_deck_storage.ini", "interface/visualDeckStorage/promptForConversion"), + QVariant(false)); + ASSERT_EQ(readFromIni("visual_deck_storage.ini", "interface/visualDeckStorage/alwaysConvert"), QVariant(true)); + ASSERT_EQ(readFromIni("visual_deck_storage.ini", "interface/visualDeckStorage/inGame"), QVariant(false)); + ASSERT_EQ(readFromIni("visual_deck_storage.ini", "interface/visualDeckStorage/selectionAnimation"), + QVariant(false)); + ASSERT_EQ(readFromIni("visual_deck_storage.ini", "interface/visualDatabaseDisplay/filterToMostRecentSetsEnabled"), + QVariant(true)); + ASSERT_EQ(readFromIni("visual_deck_storage.ini", "interface/visualDatabaseDisplay/filterToMostRecentSetsAmount"), + QVariant(25)); + + // No legacy flat keys should remain in the per-file INIs + ASSERT_FALSE(iniFileHasKeyCaseSensitive("visual_deck_storage.ini", "interface/visualdeckstorageshowfolders")); + ASSERT_FALSE(iniFileHasKeyCaseSensitive("visual_deck_storage.ini", "interface/visualdeckstoragecardsize")); + ASSERT_FALSE(iniFileHasKeyCaseSensitive("deck_editor.ini", "deckeditor/sampleHandSize")); + ASSERT_FALSE(iniFileHasKeyCaseSensitive("deck_editor.ini", "deckeditor/cardSize")); + ASSERT_FALSE(iniFileHasKeyCaseSensitive("interface.ini", "interface/notificationsenabled")); + ASSERT_FALSE(iniFileHasKeyCaseSensitive("cards_display.ini", "cards/printingselectorsortorder")); + ASSERT_FALSE(iniFileHasKeyCaseSensitive("cards_display.ini", "cards/visualDeckStorage/cardSize")); + + // No legacy non-camelCase keys should remain in the per-file INIs + ASSERT_FALSE(iniFileHasKeyCaseSensitive("game.ini", "game/gamedescription")); + ASSERT_FALSE(iniFileHasKeyCaseSensitive("game.ini", "localgameoptions/maxplayers")); + ASSERT_FALSE(iniFileHasKeyCaseSensitive("chat.ini", "chat/roomhistory")); + ASSERT_FALSE(iniFileHasKeyCaseSensitive("chat.ini", "chat/highlightwords")); + ASSERT_FALSE(iniFileHasKeyCaseSensitive("sound.ini", "sound/mastervolume")); + ASSERT_FALSE(iniFileHasKeyCaseSensitive("downloads.ini", "downloads/picturedownload")); + ASSERT_FALSE(iniFileHasKeyCaseSensitive("network.ini", "network/keepalive")); + ASSERT_FALSE(iniFileHasKeyCaseSensitive("network.ini", "network/knownmissingfeatures")); + ASSERT_FALSE(iniFileHasKeyCaseSensitive("updates.ini", "updates/updatenotification")); + ASSERT_FALSE(iniFileHasKeyCaseSensitive("cards_display.ini", "cards/displaycardnames")); + ASSERT_FALSE(iniFileHasKeyCaseSensitive("cards_display.ini", "cards/cardinfoviewmode")); + ASSERT_FALSE(iniFileHasKeyCaseSensitive("interface.ini", "interface/usetearoffmenus")); + ASSERT_FALSE(iniFileHasKeyCaseSensitive("interface.ini", "interface/doubleclicktoplay")); + ASSERT_FALSE(iniFileHasKeyCaseSensitive("interface.ini", "interface/min_players_multicolumn")); + ASSERT_FALSE(iniFileHasKeyCaseSensitive("interface.ini", "table/invert_vertical")); + ASSERT_FALSE(iniFileHasKeyCaseSensitive("interface.ini", "zoneview/groupby")); + ASSERT_FALSE(iniFileHasKeyCaseSensitive("interface.ini", "zoneview/pileview")); // Verify sentinel was written ASSERT_EQ(readFromIni("global.ini", "migration/perfile_complete"), QVariant(true)); @@ -303,7 +557,7 @@ TEST_F(SettingsMigrationTest, KeyMapTranslationIsCorrect) ASSERT_TRUE(fileExists("updates.ini")); // The key should be translated from "personal/cardUpdateCheckInterval" to "updates/cardUpdateCheckInterval" ASSERT_EQ(readFromIni("updates.ini", "updates/cardUpdateCheckInterval"), QVariant(30)); - ASSERT_EQ(readFromIni("updates.ini", "updates/updatereleasechannel"), QVariant(1)); + ASSERT_EQ(readFromIni("updates.ini", "updates/updateReleaseChannel"), QVariant(1)); } TEST_F(SettingsMigrationTest, CardsKeysKeepGroupPrefix) @@ -319,18 +573,15 @@ TEST_F(SettingsMigrationTest, CardsKeysKeepGroupPrefix) ASSERT_TRUE(fileExists("global.ini.old")); ASSERT_TRUE(fileExists("cards_display.ini")); + ASSERT_TRUE(fileExists("deck_editor.ini")); // "cards/displaycardnames" should be stored with its group prefix - ASSERT_EQ(readFromIni("cards_display.ini", "cards/displaycardnames"), QVariant(false)); - // "interface/..." keys should keep their full path - ASSERT_EQ(readFromIni("cards_display.ini", "interface/deckeditorbannercardcomboboxvisible"), QVariant(true)); + ASSERT_EQ(readFromIni("cards_display.ini", "cards/displayCardNames"), QVariant(false)); + // deck editor keys belong to the deck editor settings now + ASSERT_EQ(readFromIni("deck_editor.ini", "deckeditor/bannerCardComboBoxVisible"), QVariant(true)); } TEST_F(SettingsMigrationTest, LegacyMigrationIsIdempotent) { - if (!nativeSettingsAvailable()) { - GTEST_SKIP() << "NativeFormat QSettings not available in this environment"; - } - { QSettings nativeSettings; nativeSettings.setValue("server/previoushostlogin", "test_user"); @@ -343,17 +594,62 @@ TEST_F(SettingsMigrationTest, LegacyMigrationIsIdempotent) // Change the migrated value { QSettings serversIni(settingsPath + "servers.ini", QSettings::IniFormat); - serversIni.setValue("server/previoushostlogin", "modified_user"); + serversIni.setValue("server/previousHostLogin", "modified_user"); serversIni.sync(); } // Second migration should NOT overwrite the change ASSERT_FALSE(SettingsMigration::migrateLegacySettings(settingsPath)); - ASSERT_EQ(readFromIni("servers.ini", "server/previoushostlogin"), QVariant("modified_user")); + ASSERT_EQ(readFromIni("servers.ini", "server/previousHostLogin"), QVariant("modified_user")); +} + +TEST_F(SettingsMigrationTest, LegacyMigrationCamelCasesKeys) +{ + { + QSettings nativeSettings; + nativeSettings.setValue("sets/AAA/sortkey", 2); + nativeSettings.setValue("sets/AAA/enabled", false); + nativeSettings.setValue("sets/AAA/isknown", false); + nativeSettings.setValue("server/previoushostlogin", "legacy_user"); + nativeSettings.setValue("server/auto_connect", 1); + nativeSettings.setValue("server/fpport", "5080"); + nativeSettings.setValue("messages/count", 1); + nativeSettings.setValue("messages/msg0", "hello"); + nativeSettings.setValue("filter_games/hide_full_games", true); + nativeSettings.setValue("filter_games/min_players", 3); + nativeSettings.setValue("filter_games/max_players", 5); + nativeSettings.setValue("filter_games/game_type/deadbeef", true); + nativeSettings.sync(); + } + + ASSERT_TRUE(SettingsMigration::migrateLegacySettings(settingsPath)); + + ASSERT_TRUE(fileExists("cardDatabase.ini")); + ASSERT_EQ(readFromIni("cardDatabase.ini", "sets/AAA/sortKey"), QVariant(2)); + ASSERT_EQ(readFromIni("cardDatabase.ini", "sets/AAA/enabled"), QVariant(false)); + ASSERT_EQ(readFromIni("cardDatabase.ini", "sets/AAA/isKnown"), QVariant(false)); + + ASSERT_TRUE(fileExists("servers.ini")); + ASSERT_EQ(readFromIni("servers.ini", "server/previousHostLogin"), QVariant("legacy_user")); + ASSERT_EQ(readFromIni("servers.ini", "server/autoConnect"), QVariant(1)); + ASSERT_EQ(readFromIni("servers.ini", "server/fpPort"), QVariant("5080")); + + ASSERT_TRUE(fileExists("messages.ini")); + ASSERT_EQ(readFromIni("messages.ini", "messages/count"), QVariant(1)); + ASSERT_EQ(readFromIni("messages.ini", "messages/msg0"), QVariant("hello")); + + ASSERT_TRUE(fileExists("gamefilters.ini")); + ASSERT_EQ(readFromIni("gamefilters.ini", "filter_games/hideFullGames"), QVariant(true)); + ASSERT_EQ(readFromIni("gamefilters.ini", "filter_games/minPlayers"), QVariant(3)); + ASSERT_EQ(readFromIni("gamefilters.ini", "filter_games/maxPlayers"), QVariant(5)); + ASSERT_EQ(readFromIni("gamefilters.ini", "filter_games/gameType/deadbeef"), QVariant(true)); } TEST_F(SettingsMigrationTest, LegacyMigrationEmptyNativeFormatWritesSentinel) { + QSettings().clear(); + QSettings().sync(); + ASSERT_TRUE(SettingsMigration::migrateLegacySettings(settingsPath)); ASSERT_TRUE(fileExists("personal.ini")); ASSERT_EQ(readFromIni("personal.ini", "migration/legacy_complete"), QVariant(true)); @@ -409,5 +705,6 @@ int main(int argc, char **argv) QCoreApplication app(argc, argv); app.setOrganizationName("CockatriceTest"); app.setApplicationName("SettingsMigrationTest"); + QSettings::setDefaultFormat(QSettings::IniFormat); return RUN_ALL_TESTS(); } diff --git a/vcpkg b/vcpkg index 56bb24116..9e593bb18 160000 --- a/vcpkg +++ b/vcpkg @@ -1 +1 @@ -Subproject commit 56bb2411609227288b70117ead2c47585ba07713 +Subproject commit 9e593bb18ea69cc5095e012465dcd675a822ed0d diff --git a/vcpkg.json b/vcpkg.json index ac7b75d07..f12227024 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -1,5 +1,6 @@ { "$schema": "https://raw.githubusercontent.com/microsoft/vcpkg-tool/main/docs/vcpkg.schema.json", + "builtin-baseline": "9e593bb18ea69cc5095e012465dcd675a822ed0d", "dependencies": [ "gtest", "liblzma",