Merge branch 'master' into tooomm-qt5

This commit is contained in:
tooomm 2026-08-09 09:12:56 +02:00
commit bbf2412157
219 changed files with 7310 additions and 1824 deletions

View file

@ -159,6 +159,7 @@ if [[ $PACKAGE_TYPE ]]; then
fi fi
if [[ $USE_VCPKG ]]; then if [[ $USE_VCPKG ]]; then
flags+=("-DUSE_VCPKG=1") flags+=("-DUSE_VCPKG=1")
flags+=("-DVCPKG_INSTALL_OPTIONS=--x-abi-tools-use-exact-versions")
fi fi
# Add CMake --build flags # Add CMake --build flags

View file

@ -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); QString message = tr("Everyone draws %n cards", "english hint for translators", amount);
``` ```
See [Qt's wiki on translations]( 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 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 string in the code, you don't need to take care of adding the new strings to

View file

@ -61,10 +61,11 @@ if(WIN32 OR USE_VCPKG) # Windows (including 64bit) or USE_VCPKG set
) )
set(QTDIR 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( 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()
endif() endif()
@ -99,8 +100,7 @@ include(createversionfile)
# Define a proper install path # Define a proper install path
if(UNIX) if(UNIX)
if(APPLE) if(APPLE) # macOS
# macOS
# Due to the special bundle structure ignore # Due to the special bundle structure ignore
# the prefix eventually set by the user. # the prefix eventually set by the user.
set(CMAKE_INSTALL_PREFIX ${CMAKE_BINARY_DIR}/release) 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_LD "${CMAKE_BINARY_DIR}/launch-c")
set(CMAKE_XCODE_ATTRIBUTE_LDPLUSPLUS "${CMAKE_BINARY_DIR}/launch-cxx") set(CMAKE_XCODE_ATTRIBUTE_LDPLUSPLUS "${CMAKE_BINARY_DIR}/launch-cxx")
endif() endif()
else() else() # Linux / BSD
# Linux / BSD
if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
#fix package build #fix package build
if(PREFIX) if(PREFIX)
@ -131,7 +130,7 @@ if(UNIX)
endif() endif()
endif() endif()
endif() endif()
elseif(WIN32) elseif(WIN32) # Windows, including 64 bit
set(CMAKE_INSTALL_PREFIX ${CMAKE_BINARY_DIR}/rundir/${CMAKE_BUILD_TYPE}) set(CMAKE_INSTALL_PREFIX ${CMAKE_BINARY_DIR}/rundir/${CMAKE_BUILD_TYPE})
endif() endif()
@ -254,7 +253,7 @@ set(CPACK_PACKAGE_VERSION_PATCH "${PROJECT_VERSION_PATCH}")
set(CPACK_PACKAGE_FILE_NAME "${PROJECT_VERSION_FILENAME}") set(CPACK_PACKAGE_FILE_NAME "${PROJECT_VERSION_FILENAME}")
if(UNIX) if(UNIX)
if(APPLE) if(APPLE) #macOS
set(CPACK_GENERATOR DragNDrop ${CPACK_GENERATOR}) set(CPACK_GENERATOR DragNDrop ${CPACK_GENERATOR})
set(CPACK_GENERATOR "DragNDrop") set(CPACK_GENERATOR "DragNDrop")
set(CPACK_DMG_FORMAT "UDBZ") 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_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_DMG_BACKGROUND_IMAGE "${CMAKE_CURRENT_SOURCE_DIR}/cmake/dmgBackground.tif")
set(CPACK_PRE_BUILD_SCRIPTS "${CMAKE_CURRENT_SOURCE_DIR}/cmake/SignMacApplications.cmake") set(CPACK_PRE_BUILD_SCRIPTS "${CMAKE_CURRENT_SOURCE_DIR}/cmake/SignMacApplications.cmake")
else() else() # Linux
# linux
if(CPACK_GENERATOR STREQUAL "RPM") if(CPACK_GENERATOR STREQUAL "RPM")
set(CPACK_RPM_PACKAGE_LICENSE "GPLv2") set(CPACK_RPM_PACKAGE_LICENSE "GPLv2")
set(CPACK_RPM_MAIN_COMPONENT "cockatrice") set(CPACK_RPM_MAIN_COMPONENT "cockatrice")
if(Qt6_FOUND) set(CPACK_RPM_PACKAGE_REQUIRES "protobuf, qt6-qttools, qt6-qtsvg, qt6-qtmultimedia, qt6-qtimageformats")
set(CPACK_RPM_PACKAGE_REQUIRES "protobuf, qt6-qttools, qt6-qtsvg, qt6-qtmultimedia, qt6-qtimageformats")
endif()
set(CPACK_RPM_PACKAGE_GROUP "Amusements/Games") set(CPACK_RPM_PACKAGE_GROUP "Amusements/Games")
set(CPACK_RPM_PACKAGE_URL "http://github.com/Cockatrice/Cockatrice") set(CPACK_RPM_PACKAGE_URL "http://github.com/Cockatrice/Cockatrice")
# stop directories from making package conflicts # stop directories from making package conflicts
@ -289,13 +285,11 @@ if(UNIX)
set(CPACK_DEBIAN_PACKAGE_SHLIBDEPS ON) set(CPACK_DEBIAN_PACKAGE_SHLIBDEPS ON)
set(CPACK_DEBIAN_PACKAGE_SECTION "games") set(CPACK_DEBIAN_PACKAGE_SECTION "games")
set(CPACK_DEBIAN_PACKAGE_HOMEPAGE "http://github.com/Cockatrice/Cockatrice") 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_DEPENDS "libqt6multimedia6, libqt6svg6, qt6-qpa-plugins, qt6-image-formats-plugins") set(CPACK_DEBIAN_PACKAGE_RECOMMENDS "libqt6sql6-mysql") # for connecting servatrice to a mysql db
set(CPACK_DEBIAN_PACKAGE_RECOMMENDS "libqt6sql6-mysql") # for connecting servatrice to a mysql db
endif()
endif() endif()
endif() endif()
elseif(WIN32) elseif(WIN32) # Windows, including 64 bit
set(CPACK_GENERATOR NSIS ${CPACK_GENERATOR}) set(CPACK_GENERATOR NSIS ${CPACK_GENERATOR})
if("${CMAKE_GENERATOR_PLATFORM}" MATCHES "(x64)") if("${CMAKE_GENERATOR_PLATFORM}" MATCHES "(x64)")
set(TRICE_IS_64_BIT 1) set(TRICE_IS_64_BIT 1)
@ -352,6 +346,4 @@ if(TEST)
add_subdirectory(tests) add_subdirectory(tests)
endif() endif()
if(Qt6_FOUND) qt6_finalize_project()
qt6_finalize_project()
endif()

View file

@ -38,44 +38,38 @@ set(REQUIRED_QT_COMPONENTS ${REQUIRED_QT_COMPONENTS} ${_SERVATRICE_NEEDED} ${_CO
) )
list(REMOVE_DUPLICATES REQUIRED_QT_COMPONENTS) 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( find_package(
Qt6 Qt6
COMPONENTS ${REQUIRED_QT_COMPONENTS} Linguist COMPONENTS ${REQUIRED_QT_COMPONENTS} Linguist
QUIET HINTS ${Qt6_DIR} QUIET HINTS ${Qt6_DIR}
) )
if(Qt6_FOUND) if(NOT Qt6_FOUND)
set(COCKATRICE_QT_VERSION_NAME Qt6) message(FATAL_ERROR "No suitable version of Qt was found")
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")
endif() 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 # Establish Qt Plugins directory & Library directories
get_target_property(QT_LIBRARY_DIR ${COCKATRICE_QT_VERSION_NAME}::Core LOCATION) get_target_property(QT_LIBRARY_DIR ${COCKATRICE_QT_VERSION_NAME}::Core LOCATION)
get_filename_component(QT_LIBRARY_DIR ${QT_LIBRARY_DIR} DIRECTORY) 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_PLUGINS_DIR "${Qt6Core_DIR}/../../../${QT6_INSTALL_PLUGINS}" ABSOLUTE) get_filename_component(QT_LIBRARY_DIR "${QT_LIBRARY_DIR}/../../.." ABSOLUTE)
get_filename_component(QT_LIBRARY_DIR "${QT_LIBRARY_DIR}/../../.." ABSOLUTE) if(UNIX AND APPLE)
if(UNIX AND APPLE) # Mac needs a bit more help finding all necessary components
# Mac needs a bit more help finding all necessary components list(APPEND QT_LIBRARY_DIR "/usr/local/lib")
list(APPEND QT_LIBRARY_DIR "/usr/local/lib")
endif()
endif() endif()
message(DEBUG "QT_PLUGINS_DIR = ${QT_PLUGINS_DIR}") message(DEBUG "QT_PLUGINS_DIR = ${QT_PLUGINS_DIR}")
message(DEBUG "QT_LIBRARY_DIR = ${QT_LIBRARY_DIR}") message(DEBUG "QT_LIBRARY_DIR = ${QT_LIBRARY_DIR}")

View file

@ -1,38 +1,118 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<!-- ========================= -->
<!-- CORE BUNDLE METADATA -->
<!-- ========================= -->
<key>CFBundleDevelopmentRegion</key> <key>CFBundleDevelopmentRegion</key>
<string>English</string> <string>English</string>
<key>CFBundleExecutable</key> <key>CFBundleExecutable</key>
<string>${MACOSX_BUNDLE_EXECUTABLE_NAME}</string> <string>${MACOSX_BUNDLE_EXECUTABLE_NAME}</string>
<key>CFBundleGetInfoString</key> <key>CFBundleGetInfoString</key>
<string>${MACOSX_BUNDLE_INFO_STRING}</string> <string>${MACOSX_BUNDLE_INFO_STRING}</string>
<key>CFBundleIconFile</key> <key>CFBundleIconFile</key>
<string>${MACOSX_BUNDLE_ICON_FILE}</string> <string>${MACOSX_BUNDLE_ICON_FILE}</string>
<key>CFBundleIdentifier</key> <key>CFBundleIdentifier</key>
<string>${MACOSX_BUNDLE_GUI_IDENTIFIER}</string> <string>${MACOSX_BUNDLE_GUI_IDENTIFIER}</string>
<key>CFBundleInfoDictionaryVersion</key> <key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string> <string>6.0</string>
<key>CFBundleLongVersionString</key> <key>CFBundleLongVersionString</key>
<string>${MACOSX_BUNDLE_LONG_VERSION_STRING}</string> <string>${MACOSX_BUNDLE_LONG_VERSION_STRING}</string>
<key>CFBundleName</key> <key>CFBundleName</key>
<string>${MACOSX_BUNDLE_BUNDLE_NAME}</string> <string>${MACOSX_BUNDLE_BUNDLE_NAME}</string>
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>APPL</string> <string>APPL</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>${MACOSX_BUNDLE_SHORT_VERSION_STRING}</string> <string>${MACOSX_BUNDLE_SHORT_VERSION_STRING}</string>
<key>CFBundleSignature</key> <key>CFBundleSignature</key>
<string>????</string> <string>????</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>${MACOSX_BUNDLE_BUNDLE_VERSION}</string> <string>${MACOSX_BUNDLE_BUNDLE_VERSION}</string>
<key>CSResourcesFileMapped</key>
<true/>
<key>LSRequiresCarbon</key>
<true/>
<key>NSHumanReadableCopyright</key> <key>NSHumanReadableCopyright</key>
<string>${MACOSX_BUNDLE_COPYRIGHT}</string> <string>${MACOSX_BUNDLE_COPYRIGHT}</string>
<key>NSHighResolutionCapable</key> <key>NSHighResolutionCapable</key>
<true/> <true/>
<!-- ========================= -->
<!-- FILE TYPE (.cod) SUPPORT -->
<!-- ========================= -->
<key>UTExportedTypeDeclarations</key>
<array>
<dict>
<key>UTTypeIdentifier</key>
<string>org.cockatrice.deck</string>
<key>UTTypeDescription</key>
<string>Cockatrice Deck</string>
<key>UTTypeConformsTo</key>
<array>
<string>public.data</string>
</array>
<key>UTTypeTagSpecification</key>
<dict>
<key>public.filename-extension</key>
<array>
<string>cod</string>
</array>
</dict>
</dict>
</array>
<key>CFBundleDocumentTypes</key>
<array>
<dict>
<key>CFBundleTypeName</key>
<string>Cockatrice Deck</string>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>LSHandlerRank</key>
<string>Default</string>
<key>LSItemContentTypes</key>
<array>
<string>org.cockatrice.deck</string>
</array>
</dict>
</array>
<!-- ========================= -->
<!-- URL SCHEME (cockatrice://) -->
<!-- ========================= -->
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLName</key>
<string>Cockatrice URL Scheme</string>
<key>CFBundleURLSchemes</key>
<array>
<string>cockatrice</string>
</array>
</dict>
</array>
</dict> </dict>
</plist> </plist>

View file

@ -99,6 +99,7 @@ set(cockatrice_SOURCES
src/game_graphics/player/menu/sideboard_menu.cpp src/game_graphics/player/menu/sideboard_menu.cpp
src/game_graphics/player/menu/tally_menu.cpp src/game_graphics/player/menu/tally_menu.cpp
src/game_graphics/player/menu/utility_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/subtype_tally.cpp
src/game_graphics/tally/tally.cpp src/game_graphics/tally/tally.cpp
src/game/player/player_actions.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.cpp
src/interface/card_picture_loader/card_picture_loader_worker_work.cpp src/interface/card_picture_loader/card_picture_loader_worker_work.cpp
src/interface/card_picture_loader/card_picture_to_load.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/flow_layout.cpp
src/interface/layouts/overlap_layout.cpp src/interface/layouts/overlap_layout.cpp
src/interface/widgets/utility/line_edit_completer.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/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_button_widget.cpp
src/interface/widgets/quick_settings/settings_popup_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_quick_settings_widget.cpp
src/interface/widgets/replay/replay_timeline_widget.cpp src/interface/widgets/replay/replay_timeline_widget.cpp
src/interface/widgets/replay/replay_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_manager.cpp
src/interface/widgets/server/user/user_list_painter.cpp src/interface/widgets/server/user/user_list_painter.cpp
src/interface/widgets/server/user/user_list_widget.cpp src/interface/widgets/server/user/user_list_widget.cpp
src/interface/widgets/settings_page/abstract_settings_page.cpp
src/interface/widgets/settings_page/appearance_settings_page.cpp src/interface/widgets/settings_page/appearance_settings_page.cpp
src/interface/widgets/settings_page/deck_editor_settings_page.cpp src/interface/widgets/settings_page/deck_editor_settings_page.cpp
src/interface/widgets/settings_page/general_settings_page.cpp src/interface/widgets/settings_page/general_settings_page.cpp
src/interface/widgets/settings_page/messages_settings_page.cpp src/interface/widgets/settings_page/messages_settings_page.cpp
src/interface/widgets/settings_page/settings_search_delegate.cpp
src/interface/widgets/settings_page/settings_search_model.cpp
src/interface/widgets/settings_page/shortcut_settings_page.cpp src/interface/widgets/settings_page/shortcut_settings_page.cpp
src/interface/widgets/settings_page/sound_settings_page.cpp src/interface/widgets/settings_page/sound_settings_page.cpp
src/interface/widgets/settings_page/storage_settings_page.cpp src/interface/widgets/settings_page/storage_settings_page.cpp
@ -291,6 +302,7 @@ set(cockatrice_SOURCES
src/interface/widgets/visual_deck_storage/visual_deck_storage_widget.cpp src/interface/widgets/visual_deck_storage/visual_deck_storage_widget.cpp
src/interface/window_main.cpp src/interface/window_main.cpp
src/main.cpp src/main.cpp
src/single_instance_manager.cpp
src/interface/widgets/tabs/abstract_tab_deck_editor.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/tab_archidekt.cpp
src/interface/widgets/tabs/api/archidekt/api_response/archidekt_deck_listing_api_response.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_entry_display_widget.cpp
src/interface/widgets/tabs/api/archidekt/display/archidekt_api_response_deck_listings_display_widget.cpp src/interface/widgets/tabs/api/archidekt/display/archidekt_api_response_deck_listings_display_widget.cpp
src/interface/widgets/tabs/api/archidekt/display/archidekt_deck_preview_image_display_widget.cpp src/interface/widgets/tabs/api/archidekt/display/archidekt_deck_preview_image_display_widget.cpp
src/interface/widgets/tabs/api/commander_spellbook/api_response/card_in_deck_request.cpp
src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_deck_request.cpp
src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_card_result.cpp
src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_variant_result.cpp
src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_estimate_bracket_result.cpp
src/interface/widgets/tabs/api/commander_spellbook/commander_spellbook_bracket_explainer.cpp
src/interface/widgets/tabs/api/commander_spellbook/commander_spellbook_api_accessor.cpp
src/interface/widgets/tabs/api/edhrec/api_response/archidekt_links/edhrec_api_response_archidekt_links.cpp src/interface/widgets/tabs/api/edhrec/api_response/archidekt_links/edhrec_api_response_archidekt_links.cpp
src/interface/widgets/tabs/api/edhrec/api_response/average_deck/edhrec_average_deck_api_response.cpp src/interface/widgets/tabs/api/edhrec/api_response/average_deck/edhrec_average_deck_api_response.cpp
src/interface/widgets/tabs/api/edhrec/api_response/average_deck/edhrec_deck_api_response.cpp src/interface/widgets/tabs/api/edhrec/api_response/average_deck/edhrec_deck_api_response.cpp
@ -352,12 +371,29 @@ set(cockatrice_SOURCES
src/interface/widgets/tabs/visual_deck_storage/tab_deck_storage_visual.cpp src/interface/widgets/tabs/visual_deck_storage/tab_deck_storage_visual.cpp
src/interface/key_signals.cpp src/interface/key_signals.cpp
src/interface/logger.cpp src/interface/logger.cpp
src/interface/widgets/tabs/api/commander_spellbook/commander_bracket_service.cpp
src/interface/widgets/tabs/api/commander_spellbook/commander_bracket_widget.cpp
src/interface/widgets/tabs/api/commander_spellbook/handle_commander_brackets.cpp
src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_bracket_navigation_widget.cpp src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_bracket_navigation_widget.cpp
src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_bracket_navigation_widget.h src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_bracket_navigation_widget.h
src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_budget_navigation_widget.cpp src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_budget_navigation_widget.cpp
src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_budget_navigation_widget.h 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.cpp
src/interface/widgets/utility/compact_push_button.h 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.cpp
src/interface/widgets/server/user/user_info_popup.h 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) set(cockatrice_SOURCES ${cockatrice_SOURCES} ${CMAKE_CURRENT_SOURCE_DIR}/resources/appicon.icns)
endif(APPLE) endif(APPLE)
if(Qt6_FOUND) qt6_add_resources(cockatrice_RESOURCES_RCC ${cockatrice_RESOURCES})
qt6_add_resources(cockatrice_RESOURCES_RCC ${cockatrice_RESOURCES})
endif()
# Declare path variables # Declare path variables
set(ICONDIR set(ICONDIR
@ -415,34 +449,37 @@ set(DESKTOPDIR
CACHE STRING "desktop file destination" 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_MAC_QM_INSTALL_DIR "cockatrice.app/Contents/Resources/translations")
set(COCKATRICE_UNIX_QM_INSTALL_DIR "share/cockatrice/translations") set(COCKATRICE_UNIX_QM_INSTALL_DIR "share/cockatrice/translations")
set(COCKATRICE_WIN32_QM_INSTALL_DIR "translations") set(COCKATRICE_WIN32_QM_INSTALL_DIR "translations")
if(Qt6_FOUND) qt6_add_executable(
qt6_add_executable( cockatrice
cockatrice WIN32
WIN32 MACOSX_BUNDLE
MACOSX_BUNDLE ${cockatrice_SOURCES}
${cockatrice_SOURCES} ${cockatrice_RESOURCES_RCC}
${cockatrice_RESOURCES_RCC} ${cockatrice_MOC_SRCS}
${cockatrice_MOC_SRCS} MANUAL_FINALIZATION
MANUAL_FINALIZATION )
)
target_link_libraries( target_link_libraries(
cockatrice cockatrice
PUBLIC libcockatrice_card PUBLIC libcockatrice_card
libcockatrice_deck_list libcockatrice_deck_list
libcockatrice_filters libcockatrice_filters
libcockatrice_utility libcockatrice_utility
libcockatrice_network libcockatrice_network
libcockatrice_models libcockatrice_models
libcockatrice_rng libcockatrice_rng
libcockatrice_settings libcockatrice_settings
${COCKATRICE_QT_MODULES} ${COCKATRICE_QT_MODULES}
) )
endif()
if(UNIX) if(UNIX)
if(APPLE) 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.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}/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.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() endif()
elseif(WIN32) elseif(WIN32)
install(TARGETS cockatrice RUNTIME DESTINATION ./) install(TARGETS cockatrice RUNTIME DESTINATION ./)
@ -590,7 +644,7 @@ Data = Resources\")
endif() endif()
endif() endif()
if(Qt6_FOUND AND Qt6LinguistTools_FOUND) if(Qt6LinguistTools_FOUND)
#Qt6 Translations happen after the executable is built up #Qt6 Translations happen after the executable is built up
if(UPDATE_TRANSLATIONS) if(UPDATE_TRANSLATIONS)
qt6_add_translations( qt6_add_translations(
@ -617,6 +671,4 @@ if(Qt6_FOUND AND Qt6LinguistTools_FOUND)
endif() endif()
endif() endif()
if(Qt6_FOUND) qt6_finalize_target(cockatrice)
qt6_finalize_target(cockatrice)
endif()

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<mime-info xmlns="http://www.freedesktop.org/standards/shared-mime-info">
<mime-type type="application/x-cockatrice">
<comment>Cockatrice Deck File</comment>
<glob pattern="*.cod"/>
</mime-type>
</mime-info>

View file

@ -3,6 +3,8 @@
Version=1.0 Version=1.0
Type=Application Type=Application
Name=Cockatrice Name=Cockatrice
Exec=cockatrice Exec=cockatrice %U
Icon=cockatrice Icon=cockatrice
Categories=Game;CardGame; Categories=Game;CardGame;
MimeType=application/x-cockatrice;
X-Scheme-Handler/cockatrice=true

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,69 @@
#ifndef COCKATRICE_URL_SCHEME_EVENT_FILTER_H
#define COCKATRICE_URL_SCHEME_EVENT_FILTER_H
#include <QEvent>
#include <QFileOpenEvent>
#include <QObject>
#include <QString>
#include <QUrl>
/**
* @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<QFileOpenEvent *>(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

View file

@ -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->setFromProto(replay->game_info());
gameMetaInfo->setSpectatorsOmniscient(true); gameMetaInfo->setSpectatorsOmniscient(true);

View file

@ -53,7 +53,7 @@ public:
AbstractClient *getClientForPlayer(int playerId) const; AbstractClient *getClientForPlayer(int playerId) const;
void loadReplay(GameReplay *replay); void loadReplay(const GameReplay *replay);
CardItem *getCard(int playerId, const QString &zoneName, int cardId) const; CardItem *getCard(int playerId, const QString &zoneName, int cardId) const;

View file

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

View file

@ -2,7 +2,7 @@
#include "../interface/widgets/tabs/tab_game.h" #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); gameState = new GameState(this, 0, -1, isLocalGame, {}, false, false, -1, false);
connect(gameMetaInfo, &GameMetaInfo::startedChanged, gameState, &GameState::onStartedChanged); connect(gameMetaInfo, &GameMetaInfo::startedChanged, gameState, &GameState::onStartedChanged);

View file

@ -15,7 +15,7 @@ class Replay : public AbstractGame
Q_OBJECT Q_OBJECT
public: public:
explicit Replay(QObject *_parent, GameReplay *_replay, bool isLocalGame); explicit Replay(QObject *_parent, const GameReplay *_replay, bool isLocalGame);
}; };
#endif // COCKATRICE_REPLAY_H #endif // COCKATRICE_REPLAY_H

View file

@ -58,7 +58,7 @@ bool ZoneViewZoneLogic::prepareAddCard(int x)
// autoclose check is done both here and in removeCard // autoclose check is done both here and in removeCard
if (cards.isEmpty() && !doInsert && SettingsCache::instance().interface().getCloseEmptyCardView()) { if (cards.isEmpty() && !doInsert && SettingsCache::instance().userInterface().getCloseEmptyCardView()) {
emit closeView(); emit closeView();
} }
@ -145,7 +145,7 @@ void ZoneViewZoneLogic::removeCard(int position, bool toNewZone)
// card gets dragged within the view. // card gets dragged within the view.
// Another autoclose check is done in prepareAddCard so that the view autocloses if the last card was moved to an // Another autoclose check is done in prepareAddCard so that the view autocloses if the last card was moved to an
// unrevealed portion of the same zone. // unrevealed portion of the same zone.
if (cards.isEmpty() && SettingsCache::instance().interface().getCloseEmptyCardView() && toNewZone) { if (cards.isEmpty() && SettingsCache::instance().userInterface().getCloseEmptyCardView() && toNewZone) {
emit closeView(); emit closeView();
return; return;
} }

View file

@ -12,9 +12,9 @@
#include <algorithm> #include <algorithm>
#include <libcockatrice/card/database/card_database.h> #include <libcockatrice/card/database/card_database.h>
#include <libcockatrice/card/database/card_database_manager.h> #include <libcockatrice/card/database/card_database_manager.h>
#include <libcockatrice/settings/appearance_settings.h>
#include <libcockatrice/settings/cards_display_settings.h> #include <libcockatrice/settings/cards_display_settings.h>
#include <libcockatrice/settings/debug_settings.h> #include <libcockatrice/settings/debug_settings.h>
#include <libcockatrice/settings/personal_settings.h>
AbstractCardItem::AbstractCardItem(QGraphicsItem *parent, const CardRef &cardRef, PlayerLogic *_owner, int _id) AbstractCardItem::AbstractCardItem(QGraphicsItem *parent, const CardRef &cardRef, PlayerLogic *_owner, int _id)
: ArrowTarget(_owner, parent), id(_id), cardRef(cardRef), tapped(false), facedown(false), tapAngle(0), : ArrowTarget(_owner, parent), id(_id), cardRef(cardRef), tapped(false), facedown(false), tapAngle(0),
@ -107,7 +107,7 @@ QSizeF AbstractCardItem::getTranslatedSize(QPainter *painter) const
void AbstractCardItem::transformPainter(QPainter *painter, const QSizeF &translatedSize, int angle) void AbstractCardItem::transformPainter(QPainter *painter, const QSizeF &translatedSize, int angle)
{ {
const int MAX_FONT_SIZE = SettingsCache::instance().personal().getMaxFontSize(); const int MAX_FONT_SIZE = SettingsCache::instance().appearance().getMaxFontSize();
const int fontSize = std::max(9, MAX_FONT_SIZE); const int fontSize = std::max(9, MAX_FONT_SIZE);
QRectF totalBoundingRect = painter->combinedTransform().mapRect(boundingRect()); QRectF totalBoundingRect = painter->combinedTransform().mapRect(boundingRect());

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -11,24 +11,26 @@ TallyMenu::TallyMenu()
aTallyNone = createTallyAction(TallyType::None); aTallyNone = createTallyAction(TallyType::None);
aTallySubtypes = createTallyAction(TallyType::Subtypes); aTallySubtypes = createTallyAction(TallyType::Subtypes);
aTallyTotalPower = createTallyAction(TallyType::TotalPower);
addAction(aTallyNone); addAction(aTallyNone);
addSeparator(); addSeparator();
addAction(aTallySubtypes); addAction(aTallySubtypes);
addAction(aTallyTotalPower);
retranslateUi(); retranslateUi();
} }
QAction *TallyMenu::createTallyAction(TallyType tallyType) QAction *TallyMenu::createTallyAction(TallyType tallyType)
{ {
TallyType currentType = Tally::intToType(SettingsCache::instance().interface().getTallyType()); TallyType currentType = Tally::intToType(SettingsCache::instance().userInterface().getTallyType());
QAction *action = new QAction(this); QAction *action = new QAction(this);
action->setCheckable(true); action->setCheckable(true);
action->setChecked(tallyType == currentType); action->setChecked(tallyType == currentType);
connect(action, &QAction::triggered, &SettingsCache::instance().interface(), connect(action, &QAction::triggered, &SettingsCache::instance().userInterface(),
[tallyType] { SettingsCache::instance().interface().setTallyType(static_cast<int>(tallyType)); }); [tallyType] { SettingsCache::instance().userInterface().setTallyType(static_cast<int>(tallyType)); });
actionGroup->addAction(action); actionGroup->addAction(action);
@ -51,4 +53,5 @@ void TallyMenu::retranslateUi()
aTallyNone->setText(tr("None")); aTallyNone->setText(tr("None"));
aTallySubtypes->setText(tr("Subtypes")); aTallySubtypes->setText(tr("Subtypes"));
aTallyTotalPower->setText(tr("Total Power"));
} }

View file

@ -23,6 +23,7 @@ private:
QAction *aTallyNone = nullptr; QAction *aTallyNone = nullptr;
QAction *aTallySubtypes = nullptr; QAction *aTallySubtypes = nullptr;
QAction *aTallyTotalPower = nullptr;
QAction *createTallyAction(TallyType tallyType); QAction *createTallyAction(TallyType tallyType);
}; };

View file

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

View file

@ -0,0 +1,36 @@
#include "stats_tally.h"
#include "../board/card_item.h"
#include <QCoreApplication>
#include <QList>
#include <algorithm>
static int sumPowers(const QList<CardItem *> &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<TallyRow> StatsTally::computeTotalPower(const QList<CardItem *> &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)}};
}

View file

@ -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<TallyRow> computeTotalPower(const QList<CardItem *> &cards);
} // namespace StatsTally
#endif // COCKATRICE_STATS_TALLY_H

View file

@ -66,8 +66,9 @@ QList<TallyRow> countSubtypes(const QList<CardItem *> &cards)
}); });
// convert entries into TallyRows // convert entries into TallyRows
QList<TallyRow> rows = QList<TallyRow>(entries.size()); QList<TallyRow> rows;
std::transform(entries.begin(), entries.end(), rows.begin(), 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)}; }); [](const SubtypeEntry &e) { return TallyRow{e.name, QString::number(e.count)}; });
return rows; return rows;

View file

@ -1,5 +1,6 @@
#include "tally.h" #include "tally.h"
#include "stats_tally.h"
#include "subtype_tally.h" #include "subtype_tally.h"
TallyType Tally::intToType(int value) TallyType Tally::intToType(int value)
@ -18,6 +19,8 @@ QList<TallyRow> Tally::compute(const QList<CardItem *> &cards, const TallyType t
return {}; return {};
case TallyType::Subtypes: case TallyType::Subtypes:
return SubtypeTally::countSubtypes(cards); return SubtypeTally::countSubtypes(cards);
case TallyType::TotalPower:
return StatsTally::computeTotalPower(cards);
} }
return {}; return {};
} }

View file

@ -20,7 +20,8 @@ enum class TallyType
{ {
None, None,
Subtypes, Subtypes,
MaxValue = Subtypes // sentinel value TotalPower,
MaxValue = TotalPower // sentinel value
}; };
namespace Tally namespace Tally

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -11,15 +11,17 @@
#include <QNetworkReply> #include <QNetworkReply>
#include <QThread> #include <QThread>
#include <libcockatrice/settings/cache_storage_settings.h> #include <libcockatrice/settings/cache_storage_settings.h>
#include <libcockatrice/settings/download_settings.h>
#include <libcockatrice/settings/paths_settings.h> #include <libcockatrice/settings/paths_settings.h>
#include <libcockatrice/settings/personal_settings.h>
#include <utility> #include <utility>
#include <version_string.h> #include <version_string.h>
static constexpr int MAX_REQUESTS_PER_SEC = 10; static constexpr int MAX_REQUESTS_PER_SEC = 10;
static constexpr int MIN_HOST_QUOTA = 1; ///< Floor for the per-host request allowance
static constexpr qint64 QUOTA_RECOVER_MS = 60000; ///< Idle time before a reduced quota starts recovering
CardPictureLoaderWorker::CardPictureLoaderWorker() CardPictureLoaderWorker::CardPictureLoaderWorker()
: QObject(nullptr), picDownload(SettingsCache::instance().personal().getPicDownload()), : QObject(nullptr), picDownload(SettingsCache::instance().downloads().getPicDownload()),
requestQuota(MAX_REQUESTS_PER_SEC) requestQuota(MAX_REQUESTS_PER_SEC)
{ {
networkManager = new QNetworkAccessManager(this); networkManager = new QNetworkAccessManager(this);
@ -124,6 +126,19 @@ QNetworkReply *CardPictureLoaderWorker::makeRequest(const QUrl &url, CardPicture
void CardPictureLoaderWorker::resetRequestQuota() void CardPictureLoaderWorker::resetRequestQuota()
{ {
requestQuota = MAX_REQUESTS_PER_SEC; requestQuota = MAX_REQUESTS_PER_SEC;
QDateTime now = QDateTime::currentDateTime();
for (auto it = hostRequestQuota.begin(); it != hostRequestQuota.end(); ++it) {
if (!hostLast429.contains(it.key()) || now.msecsTo(hostLast429.value(it.key())) < -QUOTA_RECOVER_MS) {
it.value() = qMin(MAX_REQUESTS_PER_SEC, it.value() + 1);
}
}
for (const auto &request : requestLoadQueue) {
const QString host = request.first.host();
hostQuotaRemaining.insert(host, hostRequestQuota.value(host, MAX_REQUESTS_PER_SEC));
}
processQueuedRequests(); processQueuedRequests();
} }
@ -136,14 +151,26 @@ void CardPictureLoaderWorker::processQueuedRequests()
bool CardPictureLoaderWorker::processSingleRequest() bool CardPictureLoaderWorker::processSingleRequest()
{ {
if (!requestLoadQueue.isEmpty()) { for (int i = 0; i < requestLoadQueue.size(); ++i) {
auto request = requestLoadQueue.takeFirst(); const auto &request = requestLoadQueue.at(i);
makeRequest(request.first, request.second); QString host = request.first.host();
return true; int allowance = hostQuotaRemaining.value(host, MAX_REQUESTS_PER_SEC);
if (allowance > 0) {
hostQuotaRemaining.insert(host, allowance - 1);
makeRequest(request.first, request.second);
requestLoadQueue.removeAt(i);
return true;
}
} }
return false; return false;
} }
void CardPictureLoaderWorker::onHostRateLimited(const QString &host)
{
hostRequestQuota.insert(host, qMax(MIN_HOST_QUOTA, hostRequestQuota.value(host, MAX_REQUESTS_PER_SEC) / 2));
hostLast429.insert(host, QDateTime::currentDateTime());
}
void CardPictureLoaderWorker::enqueueImageLoad(const ExactCard &card) void CardPictureLoaderWorker::enqueueImageLoad(const ExactCard &card)
{ {
// Send call through a connection to ensure the handling is run on the pictureLoader thread // Send call through a connection to ensure the handling is run on the pictureLoader thread

View file

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

View file

@ -8,9 +8,13 @@
#include <QLoggingCategory> #include <QLoggingCategory>
#include <QMovie> #include <QMovie>
#include <QNetworkReply> #include <QNetworkReply>
#include <QRandomGenerator>
#include <QThread> #include <QThread>
#include <QThreadPool> #include <QThreadPool>
#include <libcockatrice/settings/personal_settings.h> #include <QTimer>
ServerRateLimiter CardPictureLoaderWorkerWork::s_rateLimiter;
#include <libcockatrice/settings/download_settings.h>
// Card back returned by gatherer when card is not found // Card back returned by gatherer when card is not found
static const QStringList MD5_BLACKLIST = { static const QStringList MD5_BLACKLIST = {
@ -20,7 +24,7 @@ static const QStringList MD5_BLACKLIST = {
CardPictureLoaderWorkerWork::CardPictureLoaderWorkerWork(const CardPictureLoaderWorker *worker, const ExactCard &toLoad) CardPictureLoaderWorkerWork::CardPictureLoaderWorkerWork(const CardPictureLoaderWorker *worker, const ExactCard &toLoad)
: QObject(nullptr), cardToDownload(CardPictureToLoad(toLoad)), : QObject(nullptr), cardToDownload(CardPictureToLoad(toLoad)),
picDownload(SettingsCache::instance().personal().getPicDownload()) picDownload(SettingsCache::instance().downloads().getPicDownload())
{ {
// Hook up signals to the orchestrator // Hook up signals to the orchestrator
connect(this, &CardPictureLoaderWorkerWork::requestImageDownload, worker, &CardPictureLoaderWorker::queueRequest); connect(this, &CardPictureLoaderWorkerWork::requestImageDownload, worker, &CardPictureLoaderWorker::queueRequest);
@ -30,19 +34,48 @@ CardPictureLoaderWorkerWork::CardPictureLoaderWorkerWork(const CardPictureLoader
connect(this, &CardPictureLoaderWorkerWork::imageLoaded, worker, &CardPictureLoaderWorker::handleImageLoaded); connect(this, &CardPictureLoaderWorkerWork::imageLoaded, worker, &CardPictureLoaderWorker::handleImageLoaded);
connect(this, &CardPictureLoaderWorkerWork::requestSucceeded, worker, connect(this, &CardPictureLoaderWorkerWork::requestSucceeded, worker,
&CardPictureLoaderWorker::imageRequestSucceeded); &CardPictureLoaderWorker::imageRequestSucceeded);
connect(this, &CardPictureLoaderWorkerWork::rateLimited, worker, &CardPictureLoaderWorker::onHostRateLimited);
// Hook up signals to settings // Hook up signals to settings
connect(&SettingsCache::instance().personal(), SIGNAL(picDownloadChanged()), this, SLOT(picDownloadChanged())); connect(&SettingsCache::instance().downloads(), SIGNAL(picDownloadChanged()), this, SLOT(picDownloadChanged()));
startNextPicDownload(); startNextPicDownload();
} }
void CardPictureLoaderWorkerWork::startNextPicDownload() void CardPictureLoaderWorkerWork::startNextPicDownload()
{ {
QDateTime now = QDateTime::currentDateTime();
while (!cardToDownload.getCurrentUrl().isEmpty() &&
s_rateLimiter.isRateLimited(QUrl(cardToDownload.getCurrentUrl()).host(), now)) {
QString host = QUrl(cardToDownload.getCurrentUrl()).host();
if (s_rateLimiter.rounds(host) == 1) {
// First 429 round for this server: wait out the backoff and give it
// one more chance instead of immediately falling through to a worse
// source. A second 429 makes us fall through instead.
qCDebug(CardPictureLoaderWorkerWorkLog).nospace()
<< "PictureLoader: [card: " << cardToDownload.getCard().getInfo().getCorrectedName()
<< " set: " << cardToDownload.getSetName() << "]: Waiting out backoff for " << host << " to retry "
<< cardToDownload.getCurrentUrl();
scheduleDeferredRetry();
return;
}
// The server has already 429'd us at least twice, so further retries are
// unlikely to succeed: move on to the other configured sources.
qCDebug(CardPictureLoaderWorkerWorkLog).nospace()
<< "PictureLoader: [card: " << cardToDownload.getCard().getInfo().getCorrectedName()
<< " set: " << cardToDownload.getSetName() << "]: Skipping rate-limited URL "
<< cardToDownload.getCurrentUrl() << " (server " << host << " still rate limiting)";
if (!cardToDownload.nextUrl() && !cardToDownload.nextSet()) {
scheduleDeferredRetry();
return;
}
}
QString picUrl = cardToDownload.getCurrentUrl(); QString picUrl = cardToDownload.getCurrentUrl();
if (picUrl.isEmpty()) { if (picUrl.isEmpty()) {
picDownloadFailed(); scheduleDeferredRetry();
} else { } else {
QUrl url(picUrl); QUrl url(picUrl);
qCDebug(CardPictureLoaderWorkerWorkLog).nospace() qCDebug(CardPictureLoaderWorkerWorkLog).nospace()
@ -108,7 +141,41 @@ static bool imageIsBlackListed(const QByteArray &picData)
void CardPictureLoaderWorkerWork::handleFailedReply(const QNetworkReply *reply) void CardPictureLoaderWorkerWork::handleFailedReply(const QNetworkReply *reply)
{ {
if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 429) { if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 429) {
qCWarning(CardPictureLoaderWorkerWorkLog) << "Too many requests."; QString host = reply->url().host();
QDateTime now = QDateTime::currentDateTime();
qint64 retryAfterMs = 0;
const QByteArray retryAfterHeader = reply->rawHeader("Retry-After");
if (!retryAfterHeader.isEmpty()) {
bool ok = false;
int seconds = retryAfterHeader.toInt(&ok);
if (ok && seconds > 0) {
retryAfterMs = static_cast<qint64>(seconds) * 1000;
} else {
QDateTime retryAfterDate =
QDateTime::fromString(QString::fromLatin1(retryAfterHeader), Qt::RFC2822Date);
if (retryAfterDate.isValid()) {
retryAfterMs = qMax<qint64>(0, now.msecsTo(retryAfterDate));
}
}
}
QDateTime backoffUntil = s_rateLimiter.on429(host, now, retryAfterMs);
emit rateLimited(host);
if (s_rateLimiter.rounds(host) == 1) {
qCWarning(CardPictureLoaderWorkerWorkLog).nospace()
<< "PictureLoader: [card: " << cardToDownload.getCard().getName()
<< " set: " << cardToDownload.getSetName() << "]: Too many requests from " << host
<< ", backing off until " << backoffUntil.toString(Qt::ISODate) << ", retrying the same url";
scheduleDeferredRetry();
} else {
qCWarning(CardPictureLoaderWorkerWorkLog).nospace()
<< "PictureLoader: [card: " << cardToDownload.getCard().getName()
<< " set: " << cardToDownload.getSetName() << "]: Too many requests from " << host
<< ", retry already attempted, falling through to other sources";
picDownloadFailed();
}
} else { } else {
bool isFromCache = reply->attribute(QNetworkRequest::SourceIsFromCacheAttribute).toBool(); bool isFromCache = reply->attribute(QNetworkRequest::SourceIsFromCacheAttribute).toBool();
@ -149,6 +216,9 @@ void CardPictureLoaderWorkerWork::handleSuccessfulReply(QNetworkReply *reply)
return; return;
} }
// A non-redirect successful response means the server is not rate limiting us anymore.
s_rateLimiter.onSuccess(reply->url().host());
// peek is used to keep the data in the buffer for use by QImageReader // peek is used to keep the data in the buffer for use by QImageReader
const QByteArray &picData = reply->peek(reply->size()); const QByteArray &picData = reply->peek(reply->size());
@ -203,6 +273,42 @@ QImage CardPictureLoaderWorkerWork::tryLoadImageFromReply(QNetworkReply *reply)
return imgReader.read(); return imgReader.read();
} }
void CardPictureLoaderWorkerWork::scheduleDeferredRetry()
{
QDateTime now = QDateTime::currentDateTime();
// Prefer waiting on the current URL's server so we retry the same source.
QString currentHost = QUrl(cardToDownload.getCurrentUrl()).host();
QDateTime backoffUntil = s_rateLimiter.deadline(currentHost);
if (!s_rateLimiter.isRateLimited(currentHost, now)) {
backoffUntil = s_rateLimiter.earliestDeadline(now);
}
if (!backoffUntil.isValid()) {
qCWarning(CardPictureLoaderWorkerWorkLog).nospace()
<< "PictureLoader: [card: " << cardToDownload.getCard().getInfo().getCorrectedName()
<< " set: " << cardToDownload.getSetName() << "]: All URLs exhausted, no servers in backoff: BAILING OUT";
concludeImageLoad(QImage());
return;
}
qint64 waitMs = qMax<qint64>(0, now.msecsTo(backoffUntil));
// Add some jitter to desynchronize concurrent retries and avoid a thundering herd.
waitMs += QRandomGenerator::global()->bounded(5000);
qCDebug(CardPictureLoaderWorkerWorkLog).nospace()
<< "PictureLoader: [card: " << cardToDownload.getCard().getInfo().getCorrectedName()
<< " set: " << cardToDownload.getSetName() << "]: All URLs exhausted, scheduling deferred retry in " << waitMs
<< "ms";
QTimer::singleShot(waitMs, this, [this] {
s_rateLimiter.clearExpired(QDateTime::currentDateTime());
cardToDownload.resetIndices();
startNextPicDownload();
});
}
void CardPictureLoaderWorkerWork::concludeImageLoad(const QImage &image) void CardPictureLoaderWorkerWork::concludeImageLoad(const QImage &image)
{ {
emit imageLoaded(cardToDownload.getCard(), image); emit imageLoaded(cardToDownload.getCard(), image);
@ -211,5 +317,5 @@ void CardPictureLoaderWorkerWork::concludeImageLoad(const QImage &image)
void CardPictureLoaderWorkerWork::picDownloadChanged() void CardPictureLoaderWorkerWork::picDownloadChanged()
{ {
picDownload = SettingsCache::instance().personal().getPicDownload(); picDownload = SettingsCache::instance().downloads().getPicDownload();
} }

View file

@ -4,12 +4,15 @@
#include "card_picture_loader_worker.h" #include "card_picture_loader_worker.h"
#include "card_picture_to_load.h" #include "card_picture_to_load.h"
#include <QDateTime>
#include <QLoggingCategory> #include <QLoggingCategory>
#include <QMutex> #include <QMutex>
#include <QNetworkAccessManager> #include <QNetworkAccessManager>
#include <QObject> #include <QObject>
#include <QRandomGenerator>
#include <QThread> #include <QThread>
#include <libcockatrice/card/database/card_database.h> #include <libcockatrice/card/database/card_database.h>
#include <libcockatrice/utility/server_rate_limiter.h>
inline Q_LOGGING_CATEGORY(CardPictureLoaderWorkerWorkLog, "card_picture_loader.worker"); inline Q_LOGGING_CATEGORY(CardPictureLoaderWorkerWorkLog, "card_picture_loader.worker");
@ -50,6 +53,8 @@ public slots:
private: private:
bool picDownload; ///< Whether network downloading is enabled bool picDownload; ///< Whether network downloading is enabled
static ServerRateLimiter s_rateLimiter; ///< Shared per-server 429 backoff state
/** @brief Starts downloading the next URL for this card. */ /** @brief Starts downloading the next URL for this card. */
void startNextPicDownload(); void startNextPicDownload();
@ -77,6 +82,16 @@ private:
*/ */
void concludeImageLoad(const QImage &image); void concludeImageLoad(const QImage &image);
/**
* @brief Schedules a deferred retry after the relevant server backoff expires.
*
* Waits on the current URL's server when it is the reason we are blocked,
* otherwise on the earliest active backoff. If no servers are in backoff,
* concludes with failure. Otherwise resets the CardPictureToLoad indices and
* retries after the backoff period.
*/
void scheduleDeferredRetry();
private slots: private slots:
/** @brief Updates the picDownload setting when it changes. */ /** @brief Updates the picDownload setting when it changes. */
void picDownloadChanged(); void picDownloadChanged();
@ -100,6 +115,9 @@ signals:
/** @brief Emitted when a URL has been redirected. */ /** @brief Emitted when a URL has been redirected. */
void urlRedirected(const QUrl &originalUrl, const QUrl &redirectUrl); void urlRedirected(const QUrl &originalUrl, const QUrl &redirectUrl);
/** @brief Emitted when a server returned HTTP 429. */
void rateLimited(const QString &host);
/** @brief Emitted when a cached URL is invalid and must be removed. */ /** @brief Emitted when a cached URL is invalid and must be removed. */
void cachedUrlInvalidated(const QUrl &url); void cachedUrlInvalidated(const QUrl &url);
}; };

View file

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

View file

@ -25,6 +25,8 @@ private:
QList<QString> currentSetUrls; ///< URLs for the current set being attempted QList<QString> currentSetUrls; ///< URLs for the current set being attempted
QString currentUrl; ///< Currently active URL to download QString currentUrl; ///< Currently active URL to download
CardSetPtr currentSet; ///< Currently active set CardSetPtr currentSet; ///< Currently active set
int currentSetIndex = 0; ///< Current position in sortedSets
int currentUrlIndex = 0; ///< Current position in currentSetUrls
public: public:
/** /**
@ -56,6 +58,9 @@ public:
/** @return The short name of the current set, or empty string if no set. */ /** @return The short name of the current set, or empty string if no set. */
[[nodiscard]] QString getSetName() const; [[nodiscard]] QString getSetName() const;
/** @return The next URL in the current set's list without advancing, or empty if at end. */
[[nodiscard]] QString peekNextUrl() const;
/** /**
* @brief Transforms a URL template into a concrete URL for this card/set. * @brief Transforms a URL template into a concrete URL for this card/set.
* @param urlTemplate The URL template to transform * @param urlTemplate The URL template to transform
@ -88,6 +93,14 @@ public:
*/ */
void populateSetUrls(); void populateSetUrls();
/**
* @brief Resets iteration indices to the beginning.
*
* Restarts URL/set iteration from the first set and first URL.
* Used for deferred retry after server backoff expires.
*/
void resetIndices();
/** /**
* @brief Extract all sets from the card and sort them by priority. * @brief Extract all sets from the card and sort them by priority.
* @param card The card to extract sets from * @param card The card to extract sets from

View file

@ -0,0 +1,14 @@
#ifndef COCKATRICE_CONTEXT_CONNECT_TO_SERVER_H
#define COCKATRICE_CONTEXT_CONNECT_TO_SERVER_H
#include <QString>
struct ContextConnectToServer
{
QString hostname;
QString port;
QString username;
QString password;
};
#endif // COCKATRICE_CONTEXT_CONNECT_TO_SERVER_H

View file

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

View file

@ -0,0 +1,14 @@
#ifndef COCKATRICE_CONTEXT_JOIN_ROOM_H
#define COCKATRICE_CONTEXT_JOIN_ROOM_H
#include "context_connect_to_server.h"
#include <QString>
struct ContextJoinRoom
{
ContextConnectToServer serverContext;
int roomId;
};
#endif // COCKATRICE_CONTEXT_JOIN_ROOM_H

View file

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

View file

@ -0,0 +1,37 @@
#ifndef COCKATRICE_INTENT_H
#define COCKATRICE_INTENT_H
#include <QObject>
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

View file

@ -0,0 +1,46 @@
#include "intent_connect_to_server.h"
#include "intent_disconnect_from_server.h"
#include <QTimer>
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<QString> &) { 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));
}

View file

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

View file

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

View file

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

View file

@ -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 <QTimer>
IntentJoinServerGame::IntentJoinServerGame(TabSupervisor *_tabSupervisor,
RemoteClient *_remoteClient,
std::unique_ptr<ContextJoinGame> _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)); });
}

View file

@ -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 <QScopedPointer>
#include <memory>
class TabRoom;
class TabSupervisor;
class IntentJoinServerGame : public Intent
{
Q_OBJECT
public:
IntentJoinServerGame(TabSupervisor *_tabSupervisor,
RemoteClient *_remoteClient,
std::unique_ptr<ContextJoinGame> _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<ContextJoinGame> context;
};
#endif // COCKATRICE_INTENT_JOIN_SERVER_GAME_H

View file

@ -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 <QTimer>
#include <libcockatrice/protocol/pb/serverinfo_room.pb.h>
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));
}

View file

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

View file

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

View file

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

View file

@ -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 <libcockatrice/card/database/card_database_manager.h>
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<LoadedDeck> 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);
}

View file

@ -0,0 +1,27 @@
#ifndef COCKATRICE_INTENT_OPEN_LOCAL_DECK_H
#define COCKATRICE_INTENT_OPEN_LOCAL_DECK_H
#include "intent.h"
#include <QString>
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

View file

@ -0,0 +1,19 @@
#include "intent_wait_for_database_load.h"
#include <libcockatrice/card/database/card_database_manager.h>
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(); });
}

View file

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

View file

@ -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 <QDebug>
#include <QMessageBox>
#include <QUrl>
#include <QUrlQuery>
#include <memory>
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<ContextJoinGame>();
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();
}

View file

@ -0,0 +1,20 @@
#ifndef COCKATRICE_URL_PARSER_H
#define COCKATRICE_URL_PARSER_H
#include <QObject>
#include <QUrlQuery>
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

View file

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

View file

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

View file

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

View file

@ -271,6 +271,7 @@ void DlgConnect::updateDisplayInfo(const QString &saveName)
hostEdit->setText(_data.at(1)); hostEdit->setText(_data.at(1));
portEdit->setText(_data.at(2)); portEdit->setText(_data.at(2));
playernameEdit->setText(_data.at(3)); playernameEdit->setText(_data.at(3));
playernameEdit->setFocus();
savePasswordCheckBox->setChecked(savePasswordStatus); savePasswordCheckBox->setChecked(savePasswordStatus);
if (savePasswordStatus) { if (savePasswordStatus) {

View file

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

View file

@ -1,3 +1,8 @@
/**
* @file dlg_settings.cpp
* @brief Implementation of the main settings dialog
* @ingroup Dialogs
*/
#include "dlg_settings.h" #include "dlg_settings.h"
#include "../../../client/settings/cache_settings.h" #include "../../../client/settings/cache_settings.h"
@ -6,6 +11,8 @@
#include "../settings_page/deck_editor_settings_page.h" #include "../settings_page/deck_editor_settings_page.h"
#include "../settings_page/general_settings_page.h" #include "../settings_page/general_settings_page.h"
#include "../settings_page/messages_settings_page.h" #include "../settings_page/messages_settings_page.h"
#include "../settings_page/settings_search_delegate.h"
#include "../settings_page/settings_search_model.h"
#include "../settings_page/shortcut_settings_page.h" #include "../settings_page/shortcut_settings_page.h"
#include "../settings_page/sound_settings_page.h" #include "../settings_page/sound_settings_page.h"
#include "../settings_page/storage_settings_page.h" #include "../settings_page/storage_settings_page.h"
@ -13,20 +20,36 @@
#include "libcockatrice/card/database/card_database_loader.h" #include "libcockatrice/card/database/card_database_loader.h"
#include "libcockatrice/card/database/card_database_manager.h" #include "libcockatrice/card/database/card_database_manager.h"
#include <QAbstractItemView>
#include <QCloseEvent> #include <QCloseEvent>
#include <QDialogButtonBox>
#include <QDir> #include <QDir>
#include <QFrame>
#include <QGraphicsOpacityEffect>
#include <QGuiApplication> #include <QGuiApplication>
#include <QListWidget> #include <QHBoxLayout>
#include <QKeyEvent>
#include <QLineEdit>
#include <QListView>
#include <QMessageBox> #include <QMessageBox>
#include <QPropertyAnimation>
#include <QPushButton>
#include <QScreen> #include <QScreen>
#include <QScrollArea> #include <QScrollArea>
#include <QScrollBar> #include <QScrollBar>
#include <QSequentialAnimationGroup>
#include <QShortcut>
#include <QStackedLayout>
#include <QStackedWidget> #include <QStackedWidget>
#include <QToolButton>
#include <QVBoxLayout> #include <QVBoxLayout>
#include <libcockatrice/settings/paths_settings.h> #include <libcockatrice/settings/paths_settings.h>
#include <libcockatrice/settings/personal_settings.h> #include <libcockatrice/settings/personal_settings.h>
/**
* @brief Wraps a widget in a scroll area for long settings pages
* @param widget The widget to wrap
* @return The scroll area containing the widget
*/
static QScrollArea *makeScrollable(QWidget *widget) static QScrollArea *makeScrollable(QWidget *widget)
{ {
widget->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Maximum); widget->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Maximum);
@ -40,112 +63,355 @@ static QScrollArea *makeScrollable(QWidget *widget)
return scrollArea; return scrollArea;
} }
DlgSettings::DlgSettings(QWidget *parent) : QDialog(parent) /**
* @brief Returns the theme icon resources for each settings page, indexed by SettingsPage order
*/
static QStringList pageIconResources()
{
return {QStringLiteral("theme:config/general"), QStringLiteral("theme:config/appearance"),
QStringLiteral("theme:config/interface"), QStringLiteral("theme:config/deckeditor"),
QStringLiteral("theme:config/storage"), QStringLiteral("theme:config/messages"),
QStringLiteral("theme:config/sound"), QStringLiteral("theme:config/shorcuts")};
}
DlgSettings::DlgSettings(QWidget *parent) : QDialog(parent), currentTabIndex(0), searchActive(false)
{ {
auto rec = QGuiApplication::primaryScreen()->availableGeometry(); auto rec = QGuiApplication::primaryScreen()->availableGeometry();
this->setMinimumSize(qMin(700, rec.width()), qMin(700, rec.height())); setMinimumSize(qMin(750, rec.width()), qMin(700, rec.height()));
connect(&SettingsCache::instance().personal(), &PersonalSettings::langChanged, this, &DlgSettings::updateLanguage); connect(&SettingsCache::instance().personal(), &PersonalSettings::langChanged, this, &DlgSettings::updateLanguage);
contentsWidget = new QListWidget; setupUi();
contentsWidget->setViewMode(QListView::IconMode);
contentsWidget->setIconSize(QSize(58, 50));
contentsWidget->setMovement(QListView::Static);
contentsWidget->setMinimumHeight(85);
contentsWidget->setMaximumHeight(85);
contentsWidget->setSpacing(5);
pagesWidget = new QStackedWidget;
pagesWidget->addWidget(makeScrollable(new GeneralSettingsPage));
pagesWidget->addWidget(makeScrollable(new AppearanceSettingsPage));
pagesWidget->addWidget(makeScrollable(new UserInterfaceSettingsPage));
pagesWidget->addWidget(new DeckEditorSettingsPage);
pagesWidget->addWidget(makeScrollable(new StorageSettingsPage));
pagesWidget->addWidget(new MessagesSettingsPage);
pagesWidget->addWidget(new SoundSettingsPage);
pagesWidget->addWidget(new ShortcutSettingsPage);
createIcons();
contentsWidget->setCurrentRow(0);
auto *vboxLayout = new QVBoxLayout;
vboxLayout->addWidget(contentsWidget);
vboxLayout->addWidget(pagesWidget);
auto *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok);
connect(buttonBox, &QDialogButtonBox::accepted, this, &DlgSettings::close);
auto *mainLayout = new QVBoxLayout;
mainLayout->addLayout(vboxLayout);
mainLayout->addSpacing(2);
mainLayout->addWidget(buttonBox);
setLayout(mainLayout);
connect(&SettingsCache::instance().personal(), &PersonalSettings::langChanged, this, &DlgSettings::retranslateUi); connect(&SettingsCache::instance().personal(), &PersonalSettings::langChanged, this, &DlgSettings::retranslateUi);
retranslateUi(); retranslateUi();
searchEdit->setFocus();
adjustSize(); adjustSize();
} }
void DlgSettings::createIcons() void DlgSettings::setupUi()
{ {
generalButton = new QListWidgetItem(contentsWidget); // Search bar
generalButton->setTextAlignment(Qt::AlignHCenter); searchEdit = new QLineEdit;
generalButton->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); searchEdit->setClearButtonEnabled(true);
generalButton->setIcon(QPixmap("theme:config/general")); searchEdit->addAction(QPixmap("theme:icons/search"), QLineEdit::LeadingPosition);
searchEdit->installEventFilter(this);
connect(searchEdit, &QLineEdit::textChanged, this, &DlgSettings::onSearchTextChanged);
appearanceButton = new QListWidgetItem(contentsWidget); auto *searchLayout = new QHBoxLayout;
appearanceButton->setTextAlignment(Qt::AlignHCenter); searchLayout->addWidget(searchEdit);
appearanceButton->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled);
appearanceButton->setIcon(QPixmap("theme:config/appearance"));
userInterfaceButton = new QListWidgetItem(contentsWidget); // Tab bar (built in setupTabBar)
userInterfaceButton->setTextAlignment(Qt::AlignHCenter); setupTabBar();
userInterfaceButton->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled);
userInterfaceButton->setIcon(QPixmap("theme:config/interface"));
deckEditorButton = new QListWidgetItem(contentsWidget); // Pages stacked widget
deckEditorButton->setTextAlignment(Qt::AlignHCenter); pagesWidget = new QStackedWidget;
deckEditorButton->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled);
deckEditorButton->setIcon(QPixmap("theme:config/deckeditor"));
storageButton = new QListWidgetItem(contentsWidget); auto *generalPage = new GeneralSettingsPage;
storageButton->setTextAlignment(Qt::AlignHCenter); auto *appearancePage = new AppearanceSettingsPage;
storageButton->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); auto *userInterfacePage = new UserInterfaceSettingsPage;
storageButton->setIcon(QPixmap("theme:config/storage")); auto *deckEditorPage = new DeckEditorSettingsPage;
auto *storagePage = new StorageSettingsPage;
auto *messagesPage = new MessagesSettingsPage;
auto *soundPage = new SoundSettingsPage;
auto *shortcutsPage = new ShortcutSettingsPage;
messagesButton = new QListWidgetItem(contentsWidget); pages.append(generalPage);
messagesButton->setTextAlignment(Qt::AlignHCenter); pages.append(appearancePage);
messagesButton->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); pages.append(userInterfacePage);
messagesButton->setIcon(QPixmap("theme:config/messages")); pages.append(deckEditorPage);
pages.append(storagePage);
pages.append(messagesPage);
pages.append(soundPage);
pages.append(shortcutsPage);
soundButton = new QListWidgetItem(contentsWidget); pagesWidget->addWidget(makeScrollable(generalPage));
soundButton->setTextAlignment(Qt::AlignHCenter); pagesWidget->addWidget(makeScrollable(appearancePage));
soundButton->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); pagesWidget->addWidget(makeScrollable(userInterfacePage));
soundButton->setIcon(QPixmap("theme:config/sound")); pagesWidget->addWidget(makeScrollable(deckEditorPage));
pagesWidget->addWidget(makeScrollable(storagePage));
pagesWidget->addWidget(messagesPage);
pagesWidget->addWidget(soundPage);
pagesWidget->addWidget(shortcutsPage);
shortcutsButton = new QListWidgetItem(contentsWidget); Q_ASSERT(pages.size() == NumPages);
shortcutsButton->setTextAlignment(Qt::AlignHCenter);
shortcutsButton->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled);
shortcutsButton->setIcon(QPixmap("theme:config/shorcuts"));
connect(contentsWidget, &QListWidget::currentItemChanged, this, &DlgSettings::changePage); // Search results view (hidden by default)
searchResultsView = new QListView;
searchResultsView->setUniformItemSizes(false);
searchResultsView->setSelectionMode(QAbstractItemView::SingleSelection);
searchResultsView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
searchResultsView->setVisible(false);
searchResultsView->setStyleSheet(
"QListView::item:selected { background: palette(highlight); color: palette(highlighted-text); }");
searchModel = new SettingsSearchModel(this);
searchDelegate = new SettingsSearchDelegate(this);
searchResultsView->setModel(searchModel);
searchResultsView->setItemDelegate(searchDelegate);
connect(searchResultsView, &QListView::clicked, this, &DlgSettings::onSearchResultClicked);
connect(&SettingsCache::instance(), &SettingsCache::themeChanged, this, [this] {
const QStringList icons = pageIconResources();
for (int i = 0; i < tabButtons.size() && i < icons.size(); ++i) {
tabButtons[i]->setIcon(QPixmap(icons[i]));
}
searchDelegate->setPageIcons(icons);
searchResultsView->viewport()->update();
});
// Build search index after pages are created
buildSearchIndex();
// Pages container (stacked widget + search results overlay)
pagesContainer = new QWidget;
auto *containerLayout = new QStackedLayout;
containerLayout->setStackingMode(QStackedLayout::StackAll);
containerLayout->addWidget(pagesWidget);
containerLayout->addWidget(searchResultsView);
pagesContainer->setLayout(containerLayout);
// Bottom buttons
auto *buttonBox = new QHBoxLayout;
buttonBox->addStretch();
okButton = new QPushButton;
okButton->setDefault(true);
connect(okButton, &QPushButton::clicked, this, &DlgSettings::close);
buttonBox->addWidget(okButton);
// Main layout
auto *mainLayout = new QVBoxLayout;
mainLayout->addLayout(searchLayout);
mainLayout->addWidget(tabBarWidget);
auto *separator = new QFrame;
separator->setFrameShape(QFrame::HLine);
separator->setFrameShadow(QFrame::Sunken);
mainLayout->addWidget(separator);
mainLayout->addWidget(pagesContainer);
mainLayout->addSpacing(4);
mainLayout->addLayout(buttonBox);
setLayout(mainLayout);
// Keyboard shortcuts
auto *searchShortcut = new QShortcut(QKeySequence(Qt::CTRL | Qt::Key_F), this);
connect(searchShortcut, &QShortcut::activated, searchEdit, qOverload<>(&QLineEdit::setFocus));
auto *nextTabShortcut = new QShortcut(QKeySequence(Qt::CTRL | Qt::Key_Tab), this);
connect(nextTabShortcut, &QShortcut::activated, this, [this] {
int next = (currentTabIndex + 1) % tabButtons.size();
setActiveTab(next);
});
auto *prevTabShortcut = new QShortcut(QKeySequence(Qt::CTRL | Qt::SHIFT | Qt::Key_Tab), this);
connect(prevTabShortcut, &QShortcut::activated, this, [this] {
int prev = (currentTabIndex - 1 + tabButtons.size()) % tabButtons.size();
setActiveTab(prev);
});
// Initialize to first tab
setActiveTab(0);
} }
void DlgSettings::changePage(QListWidgetItem *current, QListWidgetItem *previous) void DlgSettings::setupTabBar()
{ {
if (!current) { tabBarWidget = new QWidget;
current = previous; auto *tabLayout = new QHBoxLayout;
tabLayout->setContentsMargins(0, 0, 0, 0);
tabLayout->setSpacing(2);
const QStringList iconResources = pageIconResources();
for (int i = 0; i < iconResources.size(); ++i) {
auto *tabButton = new QToolButton;
tabButton->setCheckable(true);
tabButton->setIcon(QPixmap(iconResources[i]));
tabButton->setIconSize(QSize(48, 48));
tabButton->setToolButtonStyle(Qt::ToolButtonTextUnderIcon);
tabButton->setAutoExclusive(true);
tabButton->setMinimumHeight(85);
tabButton->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
connect(tabButton, &QToolButton::clicked, this, [this, idx = i] { onTabClicked(idx); });
tabButtons.append(tabButton);
tabLayout->addWidget(tabButton);
} }
pagesWidget->setCurrentIndex(contentsWidget->row(current)); tabBarWidget->setLayout(tabLayout);
}
void DlgSettings::buildSearchIndex()
{
QList<SettingsSearchEntry> allEntries;
const QStringList pageNames = translatedPageNames();
searchDelegate->setPageNames(pageNames);
searchDelegate->setPageIcons(pageIconResources());
for (int i = 0; i < pages.size(); ++i) {
QList<SettingsSearchEntry> pageEntries = pages[i]->getSearchEntries();
for (auto &entry : pageEntries) {
if (entry.pageIndex == -1) {
entry.pageIndex = i;
}
}
allEntries.append(pageEntries);
}
searchModel->setSourceEntries(allEntries);
}
void DlgSettings::onTabClicked(int index)
{
if (searchActive) {
switchToTabMode();
}
setActiveTab(index);
}
void DlgSettings::setActiveTab(int index)
{
if (index < 0 || index >= tabButtons.size()) {
return;
}
currentTabIndex = index;
pagesWidget->setCurrentIndex(index);
for (int i = 0; i < tabButtons.size(); ++i) {
tabButtons[i]->setChecked(i == index);
}
// Style active tab with a thick accent border + subtle background tint
for (int i = 0; i < tabButtons.size(); ++i) {
if (i == index) {
tabButtons[i]->setStyleSheet("QToolButton { border: none; border-bottom: 3px solid palette(highlight); "
"border-top-left-radius: 4px; border-top-right-radius: 4px; "
"background: palette(window); padding-bottom: 1px; }");
} else {
tabButtons[i]->setStyleSheet("QToolButton { border: none; border-bottom: 1px solid transparent; "
"border-top-left-radius: 4px; border-top-right-radius: 4px; "
"background: transparent; }");
}
}
}
void DlgSettings::flashWidget(QWidget *widget)
{
auto *overlay = new QWidget(widget);
overlay->setGeometry(widget->rect());
overlay->setAttribute(Qt::WA_TransparentForMouseEvents, true);
QPalette pal = overlay->palette();
QColor flashColor = pal.color(QPalette::Highlight);
flashColor.setAlpha(100);
pal.setBrush(QPalette::Window, flashColor);
overlay->setPalette(pal);
overlay->setAutoFillBackground(true);
auto *effect = new QGraphicsOpacityEffect(overlay);
effect->setOpacity(0.0);
overlay->setGraphicsEffect(effect);
overlay->show();
overlay->raise();
auto *flashIn = new QPropertyAnimation(effect, "opacity");
flashIn->setDuration(120);
flashIn->setStartValue(0.0);
flashIn->setEndValue(0.6);
flashIn->setEasingCurve(QEasingCurve::OutCubic);
auto *fadeOut = new QPropertyAnimation(effect, "opacity");
fadeOut->setDuration(900);
fadeOut->setStartValue(0.6);
fadeOut->setEndValue(0.0);
fadeOut->setEasingCurve(QEasingCurve::InCubic);
auto *group = new QSequentialAnimationGroup(overlay);
group->addAnimation(flashIn);
group->addAnimation(fadeOut);
connect(group, &QSequentialAnimationGroup::finished, overlay, &QWidget::deleteLater);
group->start(QAbstractAnimation::DeleteWhenStopped);
}
void DlgSettings::onSearchTextChanged(const QString &text)
{
searchModel->setFilterString(text);
if (searchModel->isFilterActive() && !text.trimmed().isEmpty()) {
if (!searchActive) {
switchToSearchMode();
}
if (searchModel->rowCount(QModelIndex()) > 0) {
searchResultsView->setCurrentIndex(searchModel->index(0));
}
} else if (searchActive) {
switchToTabMode();
}
}
void DlgSettings::switchToSearchMode()
{
searchActive = true;
tabBarWidget->setVisible(false);
pagesWidget->setVisible(false);
searchResultsView->setVisible(true);
if (searchModel->rowCount(QModelIndex()) > 0) {
searchResultsView->setCurrentIndex(searchModel->index(0));
}
}
void DlgSettings::switchToTabMode()
{
searchActive = false;
tabBarWidget->setVisible(true);
pagesWidget->setVisible(true);
searchResultsView->setVisible(false);
searchEdit->blockSignals(true);
searchEdit->clear();
searchEdit->blockSignals(false);
setActiveTab(currentTabIndex);
}
void DlgSettings::onSearchResultClicked(const QModelIndex &index)
{
navigateToSearchResult(index);
}
void DlgSettings::navigateToSearchResult(const QModelIndex &index)
{
SettingsSearchEntry entry = searchModel->entryForIndex(index);
if (entry.pageIndex < 0 || entry.pageIndex >= pages.size()) {
return;
}
// Switch to the page
switchToTabMode();
setActiveTab(entry.pageIndex);
// Scroll to the widget, focus it, and flash to highlight it
if (entry.widget) {
QWidget *widget = entry.widget;
while (widget) {
if (auto *scrollArea = qobject_cast<QScrollArea *>(widget)) {
scrollArea->ensureWidgetVisible(entry.widget);
break;
}
widget = widget->parentWidget();
}
entry.widget->setFocus();
flashWidget(entry.widget);
}
} }
void DlgSettings::setTab(int index) void DlgSettings::setTab(int index)
{ {
if (index <= contentsWidget->count() - 1 && index >= 0) { if (index >= 0 && index < tabButtons.size()) {
changePage(contentsWidget->item(index), contentsWidget->currentItem()); setActiveTab(index);
contentsWidget->setCurrentRow(index);
} }
} }
@ -155,6 +421,49 @@ void DlgSettings::updateLanguage()
installNewTranslator(); installNewTranslator();
} }
bool DlgSettings::eventFilter(QObject *watched, QEvent *event)
{
if (watched == searchEdit && event->type() == QEvent::KeyPress) {
auto *keyEvent = static_cast<QKeyEvent *>(event);
if (keyEvent->key() == Qt::Key_Escape) {
if (searchActive) {
switchToTabMode();
return true;
}
} else if (keyEvent->key() == Qt::Key_Return || keyEvent->key() == Qt::Key_Enter) {
if (searchActive) {
if (searchResultsView->currentIndex().isValid()) {
navigateToSearchResult(searchResultsView->currentIndex());
}
return true;
}
} else if (keyEvent->key() == Qt::Key_Down) {
if (searchActive) {
int nextRow = searchResultsView->currentIndex().row() + 1;
if (nextRow >= searchModel->rowCount()) {
nextRow = 0;
}
searchResultsView->setCurrentIndex(searchModel->index(nextRow));
searchResultsView->scrollTo(searchModel->index(nextRow));
return true;
}
} else if (keyEvent->key() == Qt::Key_Up) {
if (searchActive) {
int prevRow = searchResultsView->currentIndex().row() - 1;
if (prevRow < 0) {
prevRow = searchModel->rowCount() - 1;
}
if (prevRow >= 0) {
searchResultsView->setCurrentIndex(searchModel->index(prevRow));
searchResultsView->scrollTo(searchModel->index(prevRow));
}
return true;
}
}
}
return QDialog::eventFilter(watched, event);
}
void DlgSettings::closeEvent(QCloseEvent *event) void DlgSettings::closeEvent(QCloseEvent *event)
{ {
bool showLoadError = true; bool showLoadError = true;
@ -209,7 +518,6 @@ void DlgSettings::closeEvent(QCloseEvent *event)
if (!QDir(SettingsCache::instance().paths().getDeckPath()).exists() || if (!QDir(SettingsCache::instance().paths().getDeckPath()).exists() ||
SettingsCache::instance().paths().getDeckPath().isEmpty()) { SettingsCache::instance().paths().getDeckPath().isEmpty()) {
//! \todo Prompt to create the deck directory.
if (QMessageBox::critical( if (QMessageBox::critical(
this, tr("Error"), this, tr("Error"),
tr("The path to your deck directory is invalid. Would you like to go back and set the correct path?"), tr("The path to your deck directory is invalid. Would you like to go back and set the correct path?"),
@ -221,7 +529,6 @@ void DlgSettings::closeEvent(QCloseEvent *event)
if (!QDir(SettingsCache::instance().paths().getPicsPath()).exists() || if (!QDir(SettingsCache::instance().paths().getPicsPath()).exists() ||
SettingsCache::instance().paths().getPicsPath().isEmpty()) { SettingsCache::instance().paths().getPicsPath().isEmpty()) {
//! \todo Prompt to create the pictures directory.
if (QMessageBox::critical(this, tr("Error"), if (QMessageBox::critical(this, tr("Error"),
tr("The path to your card pictures directory is invalid. Would you like to go back " tr("The path to your card pictures directory is invalid. Would you like to go back "
"and set the correct path?"), "and set the correct path?"),
@ -236,15 +543,26 @@ void DlgSettings::closeEvent(QCloseEvent *event)
void DlgSettings::retranslateUi() void DlgSettings::retranslateUi()
{ {
setWindowTitle(tr("Settings")); setWindowTitle(tr("Settings"));
retranslateTabNames();
generalButton->setText(tr("General")); searchEdit->setPlaceholderText(tr("Search settings..."));
appearanceButton->setText(tr("Appearance")); okButton->setText(tr("OK"));
userInterfaceButton->setText(tr("User Interface"));
storageButton->setText(tr("Storage"));
deckEditorButton->setText(tr("Card Sources"));
messagesButton->setText(tr("Chat"));
soundButton->setText(tr("Sound"));
shortcutsButton->setText(tr("Shortcuts"));
contentsWidget->reset(); // Rebuild search index for translated text
buildSearchIndex();
}
QStringList DlgSettings::translatedPageNames()
{
return {tr("General"), tr("Appearance"), tr("User Interface"), tr("Card Sources"),
tr("Storage"), tr("Chat"), tr("Sound"), tr("Shortcuts")};
}
void DlgSettings::retranslateTabNames()
{
const QStringList tabLabels = translatedPageNames();
for (int i = 0; i < tabButtons.size() && i < tabLabels.size(); ++i) {
tabButtons[i]->setText(tabLabels[i]);
}
} }

View file

@ -1,43 +1,99 @@
/** /**
* @file dlg_settings.h * @file dlg_settings.h
* @brief Main settings dialog for the Cockatrice client
* @ingroup Dialogs * @ingroup Dialogs
*/ */
//! \todo Document this file.
#ifndef DLG_SETTINGS_H #ifndef DLG_SETTINGS_H
#define DLG_SETTINGS_H #define DLG_SETTINGS_H
#include <QComboBox>
#include <QDialog> #include <QDialog>
#include <QLoggingCategory> #include <QLoggingCategory>
class QPushButton;
inline Q_LOGGING_CATEGORY(DlgSettingsLog, "dlg_settings"); inline Q_LOGGING_CATEGORY(DlgSettingsLog, "dlg_settings");
class QListWidget;
class QStackedWidget; class QStackedWidget;
class QListWidgetItem; class QToolButton;
class QListView;
class QLineEdit;
class AbstractSettingsPage;
class SettingsSearchModel;
class SettingsSearchDelegate;
/**
* @brief Main application settings dialog with tabbed navigation and search
*
* Provides a modern settings interface organized into tabbed pages. Users can
* either navigate by clicking tabs or search for specific settings using the
* built-in search bar. Search results are filtered and ranked by relevance.
*/
class DlgSettings : public QDialog class DlgSettings : public QDialog
{ {
Q_OBJECT Q_OBJECT
public: public:
/**
* @brief Page order in the tab bar, matching the order pages are added in setupUi()
*
* Use these values instead of raw indices so reordering pages never silently
* breaks external callers like tab_room.cpp.
*/
enum SettingsPage
{
GeneralPage = 0,
AppearancePage,
UserInterfacePage,
DeckEditorPage,
StoragePage,
MessagesPage,
SoundPage,
ShortcutsPage,
NumPages
};
explicit DlgSettings(QWidget *parent = nullptr); explicit DlgSettings(QWidget *parent = nullptr);
void setTab(int index); void setTab(int index);
private slots: private slots:
void changePage(QListWidgetItem *current, QListWidgetItem *previous); void onTabClicked(int index);
void onSearchTextChanged(const QString &text);
void onSearchResultClicked(const QModelIndex &index);
void updateLanguage(); void updateLanguage();
private: private:
QListWidget *contentsWidget; // UI elements
QStackedWidget *pagesWidget; QLineEdit *searchEdit; ///< Search bar for filtering settings
QListWidgetItem *generalButton, *appearanceButton, *userInterfaceButton, *deckEditorButton, *storageButton, QWidget *tabBarWidget; ///< Container widget for the tab buttons
*messagesButton, *soundButton, *shortcutsButton; QList<QToolButton *> tabButtons; ///< Navigation tab buttons
void createIcons(); QStackedWidget *pagesWidget; ///< Stacked widget containing settings pages
QListView *searchResultsView; ///< Search results list view
QWidget *pagesContainer; ///< Container stacking pages and search results
QPushButton *okButton; ///< Button to close the dialog
// Data
QList<AbstractSettingsPage *> pages; ///< All settings page instances
SettingsSearchModel *searchModel; ///< Model for search results
SettingsSearchDelegate *searchDelegate; ///< Delegate for search result rendering
int currentTabIndex; ///< Currently active tab index
bool searchActive; ///< Whether search mode is active
void setupUi();
void setupTabBar();
void buildSearchIndex();
void switchToTabMode();
void switchToSearchMode();
void navigateToSearchResult(const QModelIndex &index);
void setActiveTab(int index);
static void flashWidget(QWidget *widget);
static QStringList translatedPageNames();
void retranslateUi(); void retranslateUi();
void retranslateTabNames();
protected: protected:
void closeEvent(QCloseEvent *event) override; void closeEvent(QCloseEvent *event) override;
bool eventFilter(QObject *watched, QEvent *event) override;
}; };
#endif #endif // DLG_SETTINGS_H

View file

@ -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 // 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 // downloader themselves if there's a compatible build
QMessageBox::information( QMessageBox::information(
this, tr("No Update Available"), window(), tr("No Update Available"),
tr("Cockatrice is up to date!") + "<br><br>" + tr("Cockatrice is up to date!") + "<br><br>" +
tr("You are already running the latest version available in the chosen release channel.") + "<br>" + tr("You are already running the latest version available in the chosen release channel.") + "<br>" +
"<b>" + tr("Current version") + QString(":</b> %1<br>").arg(VERSION_STRING) + "<b>" + "<b>" + tr("Current version") + QString(":</b> %1<br>").arg(VERSION_STRING) + "<b>" +
@ -147,7 +147,7 @@ void DlgUpdate::finishedUpdateCheck(bool needToUpdate, bool isCompatible, Releas
if (isCompatible) { if (isCompatible) {
int reply; int reply;
reply = QMessageBox::question( reply = QMessageBox::question(
this, tr("Update Available"), window(), tr("Update Available"),
tr("A new version of Cockatrice is available!") + "<br><br>" + "<b>" + tr("New version") + tr("A new version of Cockatrice is available!") + "<br><br>" + "<b>" + tr("New version") +
QString(":</b> %1<br>").arg(release->getName()) + "<b>" + tr("Released") + QString(":</b> %1<br>").arg(release->getName()) + "<b>" + tr("Released") +
QString(":</b> %1 (<a href=\"%2\">").arg(publishDate, release->getDescriptionUrl()) + tr("Changelog") + QString(":</b> %1 (<a href=\"%2\">").arg(publishDate, release->getDescriptionUrl()) + tr("Changelog") +
@ -156,10 +156,12 @@ void DlgUpdate::finishedUpdateCheck(bool needToUpdate, bool isCompatible, Releas
if (reply == QMessageBox::Yes) { if (reply == QMessageBox::Yes) {
downloadUpdate(release->getName()); downloadUpdate(release->getName());
} else {
closeDialog();
} }
} else { } else {
QMessageBox::information( QMessageBox::information(
this, tr("Update Available"), window(), tr("Update Available"),
tr("A new version of Cockatrice is available!") + "<br><br>" + "<b>" + tr("New version") + tr("A new version of Cockatrice is available!") + "<br><br>" + "<b>" + tr("New version") +
QString(":</b> %1<br>").arg(release->getName()) + "<b>" + tr("Released") + QString(":</b> %1<br>").arg(release->getName()) + "<b>" + tr("Released") +
QString(":</b> %1 (<a href=\"%2\">").arg(publishDate, release->getDescriptionUrl()) + tr("Changelog") + QString(":</b> %1 (<a href=\"%2\">").arg(publishDate, release->getDescriptionUrl()) + tr("Changelog") +

View file

@ -82,9 +82,7 @@ void ColorPie::paintEvent(QPaintEvent *)
QString label = QString("%1%").arg(int(ratio * 100 + 0.5)); QString label = QString("%1%").arg(int(ratio * 100 + 0.5));
QFontMetrics fm(p.font()); QFontMetrics fm(p.font());
int labelWidth = fm.horizontalAdvance(label); int labelWidth = fm.horizontalAdvance(label);
QRectF textRect(labelPos.x() - labelWidth / 2.0, labelPos.y() - fm.height() / 2.0, labelWidth, fm.height()); QRectF textRect(labelPos.x() - labelWidth / 2.0, labelPos.y() - fm.height() / 2.0, labelWidth, fm.height());
p.setPen(Qt::black); p.setPen(Qt::black);

View file

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

View file

@ -16,16 +16,16 @@ class TearOffMenu : public QMenu
public: public:
explicit TearOffMenu(const QString &title, QWidget *parent = nullptr) : QMenu(title, parent) explicit TearOffMenu(const QString &title, QWidget *parent = nullptr) : QMenu(title, parent)
{ {
connect(&SettingsCache::instance().interface(), &InterfaceSettings::useTearOffMenusChanged, this, connect(&SettingsCache::instance().userInterface(), &InterfaceSettings::useTearOffMenusChanged, this,
[this](const bool state) { setTearOffEnabled(state); }); [this](const bool state) { setTearOffEnabled(state); });
setTearOffEnabled(SettingsCache::instance().interface().getUseTearOffMenus()); setTearOffEnabled(SettingsCache::instance().userInterface().getUseTearOffMenus());
} }
explicit TearOffMenu(QWidget *parent = nullptr) : QMenu(parent) explicit TearOffMenu(QWidget *parent = nullptr) : QMenu(parent)
{ {
connect(&SettingsCache::instance().interface(), &InterfaceSettings::useTearOffMenusChanged, this, connect(&SettingsCache::instance().userInterface(), &InterfaceSettings::useTearOffMenusChanged, this,
[this](const bool state) { setTearOffEnabled(state); }); [this](const bool state) { setTearOffEnabled(state); });
setTearOffEnabled(SettingsCache::instance().interface().getUseTearOffMenus()); setTearOffEnabled(SettingsCache::instance().userInterface().getUseTearOffMenus());
} }
TearOffMenu *addTearOffMenu(const QString &title) TearOffMenu *addTearOffMenu(const QString &title)

View file

@ -0,0 +1,234 @@
#include "replay_manager.h"
#include "../../../client/settings/cache_settings.h"
#include <QTimer>
#include <libcockatrice/protocol/get_pb_extension.h>
#include <libcockatrice/settings/interface_settings.h>
static constexpr int TIMER_INTERVAL_MS = 200;
static constexpr int EMPTY_SECTION_MARGIN_MS = 500;
static QList<int> createReplayTimeline(const GameReplay *replay)
{
// Create list: event number -> time [ms]
unsigned int lastEventTimestamp = 0;
const int eventCount = replay->event_list_size();
QList<int> 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<int>(static_cast<qreal>(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<GameEvent::GameEventType>(getPbExtension(event));
if (eventType != GameEvent::PLAYER_PROPERTIES_CHANGED) {
return true;
}
}
return false;
}
void ReplayManager::handleSkipEmptySection()
{
if (currentEvent == replayTimeline.size()) {
return;
}
// find most recent meaningful event
int prevEvent = std::max(0, currentEvent - 1);
for (; prevEvent > 0 && !hasMeaningfulEvent(replay->event_list(prevEvent)); --prevEvent) {
}
int prevEventTime = replayTimeline.value(prevEvent);
if (currentVisualTime - prevEventTime <= EMPTY_SECTION_MARGIN_MS) {
return;
}
// find next earliest meaningful event
int nextEvent = currentEvent;
for (; nextEvent < replayTimeline.size() - 1 && !hasMeaningfulEvent(replay->event_list(nextEvent)); ++nextEvent) {
}
int nextEventTime = replayTimeline.value(nextEvent);
if (nextEventTime - currentVisualTime <= EMPTY_SECTION_MARGIN_MS) {
return;
}
// skip forward if we're not within margin of either event
skipToTime(nextEventTime - EMPTY_SECTION_MARGIN_MS, false);
}
void ReplayManager::setTimeScaleFactor(qreal _timeScaleFactor)
{
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);
}

View file

@ -0,0 +1,83 @@
#ifndef COCKATRICE_REPLAY_MANAGER_H
#define COCKATRICE_REPLAY_MANAGER_H
#include "../../../game/player/event_processing_options.h"
#include <QObject>
#include <libcockatrice/protocol/pb/game_replay.pb.h>
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<int> 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<int> &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

View file

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

View file

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

View file

@ -4,26 +4,19 @@
#include <QPainter> #include <QPainter>
#include <QPainterPath> #include <QPainterPath>
#include <QTimer>
#include <libcockatrice/settings/interface_settings.h>
ReplayTimelineWidget::ReplayTimelineWidget(QWidget *parent) static constexpr int BIN_LENGTH = 5000;
: QWidget(parent), maxBinValue(1), maxTime(1), timeScaleFactor(1.0), currentVisualTime(0), currentProcessedTime(0), static constexpr int MIN_RESOLUTION_MS = 1000;
currentEvent(0)
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<int> &_replayTimeline) void ReplayTimelineWidget::setTimeline(const QList<int> &replayTimeline)
{ {
replayTimeline = _replayTimeline;
histogram.clear(); histogram.clear();
currentTime = 0;
int binEndTime = BIN_LENGTH - 1; int binEndTime = BIN_LENGTH - 1;
int binValue = 0; int binValue = 0;
for (int i : replayTimeline) { for (int i : replayTimeline) {
@ -66,70 +59,31 @@ void ReplayTimelineWidget::paintEvent(QPaintEvent * /* event */)
painter.fillPath(path, Qt::black); painter.fillPath(path, Qt::black);
const QColor barColor = QColor::fromHsv(120, 255, 255, 100); 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<int>(w), height() - 1, barColor); painter.fillRect(0, 0, static_cast<int>(w), height() - 1, barColor);
} }
void ReplayTimelineWidget::mousePressEvent(QMouseEvent *event) void ReplayTimelineWidget::mousePressEvent(QMouseEvent *event)
{ {
int newTime = static_cast<int>((qint64)maxTime * (qint64)event->position().x() / width()); int newTime = static_cast<int>((qint64)maxTime * (qint64)event->position().x() / width());
// don't buffer rewinds from clicks, since clicks usually don't happen fast enough to require buffering emit timeClicked(newTime);
skipToTime(newTime, false);
} }
void ReplayTimelineWidget::skipToTime(int newTime, bool doRewindBuffering) void ReplayTimelineWidget::setCurrentTime(int time)
{ {
// check boundary conditions int newTime = qBound(0, time, maxTime);
if (newTime < 0) {
newTime = 0; if (currentTime == newTime) {
} return;
if (newTime > maxTime) {
newTime = maxTime;
} }
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; currentTime = newTime;
currentVisualTime = newTime;
if (isBackwardsSkip) { if (doUpdate) {
handleBackwardsSkip(doRewindBuffering); update();
} else {
processNewEvents(FORWARD_SKIP);
} }
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 QSize ReplayTimelineWidget::sizeHint() const
@ -141,64 +95,3 @@ QSize ReplayTimelineWidget::minimumSizeHint() const
{ {
return {400, 50}; 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);
}

View file

@ -18,57 +18,25 @@ class QTimer;
class ReplayTimelineWidget : public QWidget class ReplayTimelineWidget : public QWidget
{ {
Q_OBJECT Q_OBJECT
signals: signals:
void processNextEvent(EventProcessingOptions options); void timeClicked(int newTime);
void replayFinished();
void rewound();
private: 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<int> replayTimeline;
QList<int> histogram; QList<int> histogram;
int maxBinValue, maxTime; int maxBinValue = 1;
qreal timeScaleFactor; int maxTime = 1;
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;
void skipToTime(int newTime, bool doRewindBuffering); int currentTime = 0;
void handleBackwardsSkip(bool doRewindBuffering);
void processRewind();
void processNewEvents(PlaybackMode playbackMode);
private slots:
void replayTimerTimeout();
public: public:
static constexpr int SMALL_SKIP_MS = 1000;
static constexpr int BIG_SKIP_MS = 10000;
explicit ReplayTimelineWidget(QWidget *parent = nullptr); explicit ReplayTimelineWidget(QWidget *parent = nullptr);
void setTimeline(const QList<int> &_replayTimeline); void setTimeline(const QList<int> &replayTimeline);
[[nodiscard]] QSize sizeHint() const override; [[nodiscard]] QSize sizeHint() const override;
[[nodiscard]] QSize minimumSizeHint() const override; [[nodiscard]] QSize minimumSizeHint() const override;
void setTimeScaleFactor(qreal _timeScaleFactor);
[[nodiscard]] int getCurrentEvent() const
{
return currentEvent;
}
public slots: public slots:
void startReplay(); void setCurrentTime(int time);
void stopReplay();
void skipByAmount(int amount); // use a negative amount to skip backwards
protected: protected:
void paintEvent(QPaintEvent *event) override; void paintEvent(QPaintEvent *event) override;

View file

@ -1,70 +1,50 @@
#include "replay_widget.h" #include "replay_widget.h"
#include "../../../client/settings/cache_settings.h"
#include "../../../client/settings/shortcuts_settings.h" #include "../../../client/settings/shortcuts_settings.h"
#include "../interface/widgets/tabs/tab_game.h" #include "../interface/widgets/tabs/tab_game.h"
#include "replay_manager.h"
#include "replay_quick_settings_widget.h" #include "replay_quick_settings_widget.h"
#include <QHBoxLayout> #include <QHBoxLayout>
#include <QToolButton> #include <QToolButton>
ReplayWidget::ReplayWidget(TabGame *parent, GameReplay *_replay) ReplayWidget::ReplayWidget(QWidget *parent, GameReplay *replay)
: QWidget(parent), game(parent), replay(_replay), replayPlayButton(nullptr), replayFastForwardButton(nullptr), : QWidget(parent), replayPlayButton(nullptr), replayFastForwardButton(nullptr), aReplaySkipForward(nullptr),
aReplaySkipForward(nullptr), aReplaySkipBackward(nullptr), aReplaySkipForwardBig(nullptr), aReplaySkipBackward(nullptr), aReplaySkipForwardBig(nullptr), aReplaySkipBackwardBig(nullptr)
aReplaySkipBackwardBig(nullptr)
{ {
if (replay) { // replay manager
game->getGame()->loadReplay(replay); replayManager = new ReplayManager(this, replay);
connect(replayManager, &ReplayManager::eventReplayed, this, &ReplayWidget::eventReplayed);
// Create list: event number -> time [ms] connect(replayManager, &ReplayManager::replayFinished, this, &ReplayWidget::replayFinished);
// Distribute simultaneous events evenly across 1 second. connect(replayManager, &ReplayManager::rewound, this, &ReplayWidget::rewound);
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;
}
}
// timeline widget // timeline widget
timelineWidget = new ReplayTimelineWidget; timelineWidget = new ReplayTimelineWidget;
timelineWidget->setTimeline(replayTimeline); timelineWidget->setTimeline(replayManager->getReplayTimeline());
connect(timelineWidget, &ReplayTimelineWidget::processNextEvent, this, &ReplayWidget::replayNextEvent); connect(replayManager, &ReplayManager::timeChanged, timelineWidget, &ReplayTimelineWidget::setCurrentTime);
connect(timelineWidget, &ReplayTimelineWidget::replayFinished, this, &ReplayWidget::replayFinished); connect(timelineWidget, &ReplayTimelineWidget::timeClicked, replayManager, &ReplayManager::setTime);
connect(timelineWidget, &ReplayTimelineWidget::rewound, this, &ReplayWidget::replayRewind);
// timeline skip shortcuts // timeline skip shortcuts
aReplaySkipForward = new QAction(timelineWidget); aReplaySkipForward = new QAction(timelineWidget);
timelineWidget->addAction(aReplaySkipForward); timelineWidget->addAction(aReplaySkipForward);
connect(aReplaySkipForward, &QAction::triggered, this, connect(aReplaySkipForward, &QAction::triggered, this,
[this] { timelineWidget->skipByAmount(ReplayTimelineWidget::SMALL_SKIP_MS); }); [this] { replayManager->skipByAmount(ReplayManager::SMALL_SKIP_MS); });
aReplaySkipBackward = new QAction(timelineWidget); aReplaySkipBackward = new QAction(timelineWidget);
timelineWidget->addAction(aReplaySkipBackward); timelineWidget->addAction(aReplaySkipBackward);
connect(aReplaySkipBackward, &QAction::triggered, this, connect(aReplaySkipBackward, &QAction::triggered, this,
[this] { timelineWidget->skipByAmount(-ReplayTimelineWidget::SMALL_SKIP_MS); }); [this] { replayManager->skipByAmount(-ReplayManager::SMALL_SKIP_MS); });
aReplaySkipForwardBig = new QAction(timelineWidget); aReplaySkipForwardBig = new QAction(timelineWidget);
timelineWidget->addAction(aReplaySkipForwardBig); timelineWidget->addAction(aReplaySkipForwardBig);
connect(aReplaySkipForwardBig, &QAction::triggered, this, connect(aReplaySkipForwardBig, &QAction::triggered, this,
[this] { timelineWidget->skipByAmount(ReplayTimelineWidget::BIG_SKIP_MS); }); [this] { replayManager->skipByAmount(ReplayManager::BIG_SKIP_MS); });
aReplaySkipBackwardBig = new QAction(timelineWidget); aReplaySkipBackwardBig = new QAction(timelineWidget);
timelineWidget->addAction(aReplaySkipBackwardBig); timelineWidget->addAction(aReplaySkipBackwardBig);
connect(aReplaySkipBackwardBig, &QAction::triggered, this, connect(aReplaySkipBackwardBig, &QAction::triggered, this,
[this] { timelineWidget->skipByAmount(-ReplayTimelineWidget::BIG_SKIP_MS); }); [this] { replayManager->skipByAmount(-ReplayManager::BIG_SKIP_MS); });
// buttons // buttons
replayPlayButton = new QToolButton; replayPlayButton = new QToolButton;
@ -86,6 +66,10 @@ ReplayWidget::ReplayWidget(TabGame *parent, GameReplay *_replay)
settingsWidget->setFixedSize(QSize(32, 32)); settingsWidget->setFixedSize(QSize(32, 32));
connect(settingsWidget, &ReplayQuickSettingsWidget::fastForwardSpeedChanged, this, connect(settingsWidget, &ReplayQuickSettingsWidget::fastForwardSpeedChanged, this,
[this] { updateTimeScaleFactor(replayFastForwardButton->isChecked()); }); [this] { updateTimeScaleFactor(replayFastForwardButton->isChecked()); });
connect(settingsWidget, &ReplayQuickSettingsWidget::skipEmptySectionsChanged, replayManager,
&ReplayManager::setSkipEmptySections);
replayManager->setSkipEmptySections(SettingsCache::instance().userInterface().getSkipEmptySections());
// putting everything together // putting everything together
auto replayControlLayout = new QHBoxLayout; auto replayControlLayout = new QHBoxLayout;
@ -97,18 +81,11 @@ ReplayWidget::ReplayWidget(TabGame *parent, GameReplay *_replay)
setObjectName("replayControlWidget"); setObjectName("replayControlWidget");
setLayout(replayControlLayout); setLayout(replayControlLayout);
connect(this, &ReplayWidget::requestChatAndPhaseReset, game, &TabGame::resetChatAndPhase);
connect(&SettingsCache::instance().shortcuts(), &ShortcutsSettings::shortCutChanged, this, connect(&SettingsCache::instance().shortcuts(), &ShortcutsSettings::shortCutChanged, this,
&ReplayWidget::refreshShortcuts); &ReplayWidget::refreshShortcuts);
refreshShortcuts(); refreshShortcuts();
} }
void ReplayWidget::replayNextEvent(EventProcessingOptions options)
{
emit eventReplayed(replay->event_list(timelineWidget->getCurrentEvent()), options);
}
void ReplayWidget::replayFinished() void ReplayWidget::replayFinished()
{ {
replayPlayButton->setChecked(false); replayPlayButton->setChecked(false);
@ -117,24 +94,16 @@ void ReplayWidget::replayFinished()
void ReplayWidget::replayPlayButtonToggled(bool checked) void ReplayWidget::replayPlayButtonToggled(bool checked)
{ {
if (checked) { // start replay if (checked) { // start replay
timelineWidget->startReplay(); replayManager->startReplay();
} else { // pause replay } else { // pause replay
timelineWidget->stopReplay(); replayManager->stopReplay();
} }
} }
void ReplayWidget::updateTimeScaleFactor(bool isFastForward) void ReplayWidget::updateTimeScaleFactor(bool isFastForward)
{ {
qreal factor = isFastForward ? SettingsCache::instance().interface().getFastForwardSpeed() : 1.0; qreal factor = isFastForward ? SettingsCache::instance().userInterface().getFastForwardSpeed() : 1.0;
timelineWidget->setTimeScaleFactor(factor); replayManager->setTimeScaleFactor(factor);
}
/**
* @brief Handles everything that needs to be reset when doing a replay rewind.
*/
void ReplayWidget::replayRewind()
{
emit requestChatAndPhaseReset();
} }
void ReplayWidget::refreshShortcuts() void ReplayWidget::refreshShortcuts()

View file

@ -14,11 +14,12 @@
#include <QWidget> #include <QWidget>
#include <libcockatrice/protocol/pb/game_replay.pb.h> #include <libcockatrice/protocol/pb/game_replay.pb.h>
class ReplayManager;
class ReplayQuickSettingsWidget; class ReplayQuickSettingsWidget;
class TabGame; 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. * Contains the replay timeline as well as the buttons.
*/ */
class ReplayWidget : public QWidget class ReplayWidget : public QWidget
@ -26,29 +27,28 @@ class ReplayWidget : public QWidget
Q_OBJECT Q_OBJECT
public: public:
ReplayWidget(TabGame *parent, GameReplay *replay); /**
TabGame *game; * @param parent The parent widget
GameReplay *replay; * @param replay Cannot be null. Takes ownership of the replay.
*/
ReplayWidget(QWidget *parent, GameReplay *replay);
signals: signals:
void requestChatAndPhaseReset(); void rewound();
void eventReplayed(const GameEventContainer &cont, EventProcessingOptions options); void eventReplayed(const GameEventContainer &cont, EventProcessingOptions options);
private: private:
// Replay related members ReplayManager *replayManager;
int currentReplayStep = 0;
QList<int> replayTimeline;
ReplayTimelineWidget *timelineWidget; ReplayTimelineWidget *timelineWidget;
QToolButton *replayPlayButton, *replayFastForwardButton; QToolButton *replayPlayButton, *replayFastForwardButton;
ReplayQuickSettingsWidget *settingsWidget; ReplayQuickSettingsWidget *settingsWidget;
QAction *aReplaySkipForward, *aReplaySkipBackward, *aReplaySkipForwardBig, *aReplaySkipBackwardBig; QAction *aReplaySkipForward, *aReplaySkipBackward, *aReplaySkipForwardBig, *aReplaySkipBackwardBig;
private slots: private slots:
void replayNextEvent(EventProcessingOptions options);
void replayFinished(); void replayFinished();
void replayPlayButtonToggled(bool checked); void replayPlayButtonToggled(bool checked);
void updateTimeScaleFactor(bool checked); void updateTimeScaleFactor(bool checked);
void replayRewind();
void refreshShortcuts(); void refreshShortcuts();
}; };

View file

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

View file

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

View file

@ -10,18 +10,23 @@
#include "games_model.h" #include "games_model.h"
#include "user/user_list_manager.h" #include "user/user_list_manager.h"
#include <QClipboard>
#include <QDebug> #include <QDebug>
#include <QGuiApplication>
#include <QHBoxLayout> #include <QHBoxLayout>
#include <QHeaderView> #include <QHeaderView>
#include <QMessageBox> #include <QMessageBox>
#include <QPushButton> #include <QPushButton>
#include <QTreeView> #include <QTreeView>
#include <QUrl>
#include <QUrlQuery>
#include <libcockatrice/network/client/abstract/abstract_client.h> #include <libcockatrice/network/client/abstract/abstract_client.h>
#include <libcockatrice/protocol/pb/response.pb.h> #include <libcockatrice/protocol/pb/response.pb.h>
#include <libcockatrice/protocol/pb/room_commands.pb.h> #include <libcockatrice/protocol/pb/room_commands.pb.h>
#include <libcockatrice/protocol/pb/serverinfo_game.pb.h> #include <libcockatrice/protocol/pb/serverinfo_game.pb.h>
#include <libcockatrice/protocol/pending_command.h> #include <libcockatrice/protocol/pending_command.h>
#include <libcockatrice/settings/cards_display_settings.h> #include <libcockatrice/settings/cards_display_settings.h>
#include <libcockatrice/settings/interface_settings.h>
GameSelector::GameSelector(AbstractClient *_client, GameSelector::GameSelector(AbstractClient *_client,
TabSupervisor *_tabSupervisor, TabSupervisor *_tabSupervisor,
@ -79,12 +84,12 @@ GameSelector::GameSelector(AbstractClient *_client,
if (showFilters && restoresettings) { if (showFilters && restoresettings) {
quickFilterToolBar = new GameSelectorQuickFilterToolBar(this, tabSupervisor, gameListProxyModel, gameTypeMap); quickFilterToolBar = new GameSelectorQuickFilterToolBar(this, tabSupervisor, gameListProxyModel, gameTypeMap);
quickFilterToolBar->setVisible(showFilters && restoresettings && quickFilterToolBar->setVisible(showFilters && restoresettings &&
SettingsCache::instance().cardsDisplay().getShowGameSelectorFilterToolbar()); SettingsCache::instance().userInterface().getShowGameSelectorFilterToolbar());
connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::showGameSelectorFilterToolbarChanged, connect(&SettingsCache::instance().userInterface(), &InterfaceSettings::showGameSelectorFilterToolbarChanged,
this, [this] { this, [this] {
quickFilterToolBar->setVisible( quickFilterToolBar->setVisible(
SettingsCache::instance().cardsDisplay().getShowGameSelectorFilterToolbar()); SettingsCache::instance().userInterface().getShowGameSelectorFilterToolbar());
}); });
} else { } else {
quickFilterToolBar = nullptr; quickFilterToolBar = nullptr;
@ -315,6 +320,21 @@ void GameSelector::customContextMenu(const QPoint &point)
dlg.exec(); dlg.exec();
}); });
QAction copyLink(tr("Copy Game Link"));
connect(&copyLink, &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; QMenu menu;
menu.addAction(&joinGame); menu.addAction(&joinGame);
@ -332,6 +352,11 @@ void GameSelector::customContextMenu(const QPoint &point)
menu.addAction(&spectateGame); menu.addAction(&spectateGame);
menu.addAction(&getGameInfo); menu.addAction(&getGameInfo);
if (!client->serverName().isEmpty()) {
menu.addAction(&copyLink);
}
menu.exec(gameListView->mapToGlobal(point)); menu.exec(gameListView->mapToGlobal(point));
} }
@ -379,6 +404,24 @@ void GameSelector::joinGame(const bool asSpectator, const bool asJudge)
disableButtons(); 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() void GameSelector::disableButtons()
{ {
if (createButton) { if (createButton) {

View file

@ -202,6 +202,7 @@ public:
* @param info The ServerInfo_Game object containing information about the game to update. * @param info The ServerInfo_Game object containing information about the game to update.
*/ */
void processGameInfo(const ServerInfo_Game &info); void processGameInfo(const ServerInfo_Game &info);
bool joinGameById(int gameId);
}; };
#endif #endif

View file

@ -30,7 +30,7 @@
#include <libcockatrice/protocol/pb/response_get_games_of_user.pb.h> #include <libcockatrice/protocol/pb/response_get_games_of_user.pb.h>
#include <libcockatrice/protocol/pb/response_get_user_info.pb.h> #include <libcockatrice/protocol/pb/response_get_user_info.pb.h>
#include <libcockatrice/protocol/pending_command.h> #include <libcockatrice/protocol/pending_command.h>
#include <libcockatrice/settings/interface_settings.h> #include <libcockatrice/settings/appearance_settings.h>
#include <libcockatrice/utility/string_limits.h> #include <libcockatrice/utility/string_limits.h>
BanDialog::BanDialog(const ServerInfo_User &info, QWidget *parent) : QDialog(parent) BanDialog::BanDialog(const ServerInfo_User &info, QWidget *parent) : QDialog(parent)
@ -349,7 +349,7 @@ bool UserListItemDelegate::editorEvent(QEvent *event,
QSize UserListItemDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const QSize UserListItemDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const
{ {
if (!SettingsCache::instance().interface().getStyleUserList()) { if (!SettingsCache::instance().appearance().getStyleUserList()) {
return QStyledItemDelegate::sizeHint(option, index); return QStyledItemDelegate::sizeHint(option, index);
} }
return UserListPainter::sizeHint(); return UserListPainter::sizeHint();
@ -357,7 +357,7 @@ QSize UserListItemDelegate::sizeHint(const QStyleOptionViewItem &option, const Q
void UserListItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const void UserListItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
{ {
if (!SettingsCache::instance().interface().getStyleUserList()) { if (!SettingsCache::instance().appearance().getStyleUserList()) {
QStyledItemDelegate::paint(painter, option, index); QStyledItemDelegate::paint(painter, option, index);
return; return;
} }
@ -521,7 +521,7 @@ UserListWidget::UserListWidget(TabSupervisor *_tabSupervisor,
// Pin on item click // Pin on item click
connect(userTree, &QTreeWidget::itemClicked, this, [this](QTreeWidgetItem *item, int) { connect(userTree, &QTreeWidget::itemClicked, this, [this](QTreeWidgetItem *item, int) {
if (!SettingsCache::instance().interface().getStyleUserList()) { if (!SettingsCache::instance().appearance().getStyleUserList()) {
return; return;
} }
const QString name = static_cast<UserListTWI *>(item)->getUserInfo().name().c_str(); const QString name = static_cast<UserListTWI *>(item)->getUserInfo().name().c_str();
@ -553,7 +553,7 @@ UserListWidget::UserListWidget(TabSupervisor *_tabSupervisor,
connect(cardArtProvider, &UserCardArtProvider::cardArtUpdated, this, connect(cardArtProvider, &UserCardArtProvider::cardArtUpdated, this,
[this](const QString &) { userTree->viewport()->update(); }); [this](const QString &) { userTree->viewport()->update(); });
connect(&SettingsCache::instance().interface(), &InterfaceSettings::styleUserListChanged, this, connect(&SettingsCache::instance().appearance(), &AppearanceSettings::styleUserListChanged, this,
&UserListWidget::applyDisplayMode); &UserListWidget::applyDisplayMode);
applyDisplayMode(); applyDisplayMode();
@ -659,7 +659,7 @@ void UserListWidget::hideEvent(QHideEvent *e)
void UserListWidget::applyDisplayMode() void UserListWidget::applyDisplayMode()
{ {
const bool styled = SettingsCache::instance().interface().getStyleUserList(); const bool styled = SettingsCache::instance().appearance().getStyleUserList();
if (styled) { if (styled) {
userTree->header()->setSectionResizeMode(0, QHeaderView::Stretch); userTree->header()->setSectionResizeMode(0, QHeaderView::Stretch);
@ -718,7 +718,7 @@ bool UserListWidget::eventFilter(QObject *obj, QEvent *event)
{ {
if (obj == userTree->viewport()) { if (obj == userTree->viewport()) {
if (event->type() == QEvent::MouseMove) { if (event->type() == QEvent::MouseMove) {
if (!SettingsCache::instance().interface().getStyleUserList()) { if (!SettingsCache::instance().appearance().getStyleUserList()) {
return QGroupBox::eventFilter(obj, event); return QGroupBox::eventFilter(obj, event);
} }
auto *me = static_cast<QMouseEvent *>(event); auto *me = static_cast<QMouseEvent *>(event);

View file

@ -0,0 +1,196 @@
#include "abstract_settings_page.h"
#include "settings_search_model.h"
#include <QCheckBox>
#include <QGridLayout>
#include <QGroupBox>
#include <QLabel>
#include <QLayout>
#include <QLineEdit>
#include <QPair>
#include <QSpinBox>
/**
* @brief Recursively collects all widgets within a layout
* @param layout The layout to walk
* @param widgets Output list of (widget, containing layout) pairs
*/
static void collectWidgets(QLayout *layout, QList<QPair<QWidget *, QLayout *>> &widgets)
{
for (int i = 0; i < layout->count(); ++i) {
QLayoutItem *item = layout->itemAt(i);
if (!item) {
continue;
}
if (QWidget *widget = item->widget()) {
widgets.append({widget, layout});
} else if (QLayout *subLayout = item->layout()) {
collectWidgets(subLayout, widgets);
}
}
}
/**
* @brief Rejects QLabels that are not setting names
*
* HTML link labels, path values, and excessively long labels are filtered out.
*/
static bool isValidSettingLabel(const QLabel *label)
{
const QString &text = label->text();
if (Qt::mightBeRichText(text)) {
return false;
}
if (text.contains(QLatin1Char('/')) || text.contains(QLatin1Char('\\'))) {
return false;
}
if (text.size() > 60) {
return false;
}
return true;
}
/**
* @brief Finds the control associated with a setting label
*
* Uses the explicit buddy if set, otherwise the widget in the cell (or slot)
* immediately following the label within the same layout. Returns nullptr when
* no obvious control is found.
*/
static QWidget *controlForLabel(QLabel *label, QLayout *containingLayout)
{
if (QWidget *buddy = label->buddy()) {
return buddy;
}
if (auto *grid = qobject_cast<QGridLayout *>(containingLayout)) {
int index = grid->indexOf(label);
if (index != -1) {
int row = 0;
int column = 0;
int rowSpan = 1;
int columnSpan = 1;
grid->getItemPosition(index, &row, &column, &rowSpan, &columnSpan);
if (QLayoutItem *next = grid->itemAtPosition(row, column + columnSpan)) {
if (QWidget *nextWidget = next->widget()) {
return nextWidget;
}
}
}
} else {
int index = containingLayout->indexOf(label);
if (index != -1) {
for (int i = index + 1; i < containingLayout->count(); ++i) {
if (QLayoutItem *next = containingLayout->itemAt(i)) {
if (QWidget *nextWidget = next->widget()) {
return nextWidget;
}
}
}
}
}
return nullptr;
}
/**
* @brief Builds the extended search text for an entry
*
* Combines the group title, label, and any extra searchable text derived from
* the associated control (placeholder, prefix/suffix, tooltip). Combo values
* and numeric tooltips are excluded since they change at runtime and would
* make the search index stale.
*/
static QString buildFullSearchText(const QString &groupTitle, const QString &cleanLabel, QWidget *control)
{
QStringList parts = {groupTitle, cleanLabel};
if (control) {
if (auto *lineEdit = qobject_cast<QLineEdit *>(control)) {
parts.append(lineEdit->placeholderText());
} else if (auto *spinBox = qobject_cast<QSpinBox *>(control)) {
parts.append(spinBox->prefix());
parts.append(spinBox->suffix());
}
if (!control->toolTip().isEmpty()) {
bool isNumeric = false;
control->toolTip().toInt(&isNumeric);
if (!isNumeric) {
parts.append(control->toolTip());
}
}
}
parts.removeAll(QString());
return parts.join(QLatin1Char(' '));
}
QList<SettingsSearchEntry> AbstractSettingsPage::getSearchEntries()
{
return autoDetectSearchEntries(this, -1);
}
QList<SettingsSearchEntry> AbstractSettingsPage::autoDetectSearchEntries(QWidget *page, int pageIndex)
{
QList<SettingsSearchEntry> entries;
const auto children = page->children();
for (QObject *child : children) {
auto *groupBox = qobject_cast<QGroupBox *>(child);
if (!groupBox) {
continue;
}
QString groupTitle = groupBox->title();
if (groupTitle.isEmpty()) {
continue;
}
QLayout *groupLayout = groupBox->layout();
if (!groupLayout) {
continue;
}
QList<QPair<QWidget *, QLayout *>> widgets;
collectWidgets(groupLayout, widgets);
for (const auto &pair : widgets) {
QWidget *widget = pair.first;
QString label;
auto *checkBox = qobject_cast<QCheckBox *>(widget);
if (checkBox) {
label = checkBox->text();
} else {
auto *labelWidget = qobject_cast<QLabel *>(widget);
if (!labelWidget || labelWidget->text().isEmpty() || !isValidSettingLabel(labelWidget)) {
continue;
}
label = labelWidget->text();
}
if (label.isEmpty()) {
continue;
}
// Strip accelerator markers (&) for search
QString cleanLabel = label;
cleanLabel.remove(QLatin1Char('&'));
QWidget *control = widget;
if (auto *labelWidget = qobject_cast<QLabel *>(widget)) {
control = controlForLabel(labelWidget, pair.second);
if (!control) {
continue;
}
}
entries.append(SettingsSearchEntry{.pageIndex = pageIndex,
.groupTitle = groupTitle,
.widgetLabel = cleanLabel,
.fullSearchText = buildFullSearchText(groupTitle, cleanLabel, control),
.widget = control});
}
}
return entries;
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,125 @@
/**
* @file settings_search_delegate.cpp
* @brief Implementation of the custom settings search result delegate
* @ingroup Dialogs
*/
#include "settings_search_delegate.h"
#include "settings_search_model.h"
#include <QPainter>
SettingsSearchDelegate::SettingsSearchDelegate(QObject *parent) : QStyledItemDelegate(parent)
{
}
void SettingsSearchDelegate::setPageNames(const QStringList &names)
{
pageNames = names;
}
void SettingsSearchDelegate::setPageIcons(const QStringList &iconResources)
{
pageIcons.clear();
for (const QString &resource : iconResources) {
pageIcons.append(QPixmap(resource));
}
}
void SettingsSearchDelegate::paint(QPainter *painter,
const QStyleOptionViewItem &option,
const QModelIndex &index) const
{
painter->save();
SettingsSearchEntry entry = index.data(SettingsSearchModel::EntryRole).value<SettingsSearchEntry>();
bool isSelected = option.state & QStyle::State_Selected;
bool isHovered = option.state & QStyle::State_MouseOver;
// Background
QColor bgColor = isSelected ? option.palette.color(QPalette::Highlight)
: isHovered ? option.palette.color(QPalette::Midlight)
: option.palette.color(QPalette::Base);
painter->fillRect(option.rect, bgColor);
if (isSelected) {
// Accent bar on the left to make the selection unmistakable
painter->fillRect(QRect(option.rect.left(), option.rect.top(), 4, option.rect.height()),
option.palette.color(QPalette::Highlight).darker(150));
}
int leftMargin = 12;
int topMargin = 8;
int rightMargin = 12;
int bottomMargin = 4;
QRect contentRect = option.rect.adjusted(leftMargin, topMargin, -rightMargin, -bottomMargin);
int yPos = contentRect.top();
// Icon of the related settings page
const int iconSize = 24;
QPixmap pageIcon =
(entry.pageIndex >= 0 && entry.pageIndex < pageIcons.size()) ? pageIcons.at(entry.pageIndex) : QPixmap();
int iconOffset = pageIcon.isNull() ? 0 : iconSize + 8;
if (!pageIcon.isNull()) {
QRect iconRect(contentRect.left(), contentRect.top() + (contentRect.height() - iconSize) / 2, iconSize,
iconSize);
painter->drawPixmap(iconRect, pageIcon);
}
QRect textRect = contentRect.adjusted(iconOffset, 0, 0, 0);
// Breadcrumb: "Page > Group"
QFont breadcrumbFont = option.font;
breadcrumbFont.setPointSize(breadcrumbFont.pointSize() - 1);
breadcrumbFont.setBold(true);
QColor breadcrumbColor =
isSelected ? option.palette.color(QPalette::HighlightedText) : option.palette.color(QPalette::Text);
if (!isSelected) {
breadcrumbColor.setAlpha(180);
}
QString pageName;
if (entry.pageIndex >= 0 && entry.pageIndex < pageNames.size()) {
pageName = pageNames[entry.pageIndex];
} else {
pageName = QString::number(entry.pageIndex);
}
QString breadcrumbText = QStringLiteral("%1 > %2").arg(pageName, entry.groupTitle);
painter->setFont(breadcrumbFont);
painter->setPen(breadcrumbColor);
painter->drawText(QRect(textRect.left(), yPos, textRect.width(), 20), Qt::AlignLeft | Qt::AlignVCenter,
breadcrumbText);
yPos += 20;
// Setting label
QFont labelFont = option.font;
labelFont.setPointSize(labelFont.pointSize() + 1);
labelFont.setBold(isSelected);
QColor labelColor =
isSelected ? option.palette.color(QPalette::HighlightedText) : option.palette.color(QPalette::Text);
painter->setFont(labelFont);
painter->setPen(labelColor);
painter->drawText(QRect(textRect.left(), yPos, textRect.width(), 24), Qt::AlignLeft | Qt::AlignVCenter,
entry.widgetLabel);
yPos += 24;
// Bottom separator
QPen separatorPen(option.palette.color(QPalette::Mid), 1);
painter->setPen(separatorPen);
painter->drawLine(option.rect.left() + leftMargin, option.rect.bottom(), option.rect.right() - rightMargin,
option.rect.bottom());
painter->restore();
}
QSize SettingsSearchDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const
{
Q_UNUSED(index);
return QSize(option.rect.width(), 56);
}

View file

@ -0,0 +1,38 @@
/**
* @file settings_search_delegate.h
* @brief Custom delegate for rendering settings search results
* @ingroup Dialogs
*/
#ifndef COCKATRICE_SETTINGS_SEARCH_DELEGATE_H
#define COCKATRICE_SETTINGS_SEARCH_DELEGATE_H
#include <QPixmap>
#include <QStyledItemDelegate>
/**
* @brief Custom paint delegate for settings search result items
*
* Renders each search result with a breadcrumb line ("Page > Group"),
* the setting label, and a subtle separator. Supports selected/hovered states.
*/
class SettingsSearchDelegate : public QStyledItemDelegate
{
Q_OBJECT
public:
explicit SettingsSearchDelegate(QObject *parent = nullptr);
void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override;
QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override;
/** @brief Sets the translated page names for breadcrumb display */
void setPageNames(const QStringList &names);
/** @brief Sets the icons shown in front of results, indexed by page position */
void setPageIcons(const QStringList &iconResources);
private:
QStringList pageNames; ///< Translated page names indexed by page position
QList<QPixmap> pageIcons; ///< Icons of the related settings pages
};
#endif // COCKATRICE_SETTINGS_SEARCH_DELEGATE_H

View file

@ -0,0 +1,145 @@
/**
* @file settings_search_model.cpp
* @brief Implementation of the settings search list model
* @ingroup Dialogs
*/
#include "settings_search_model.h"
#include <QPair>
#include <algorithm>
SettingsSearchModel::SettingsSearchModel(QObject *parent) : QAbstractListModel(parent)
{
}
void SettingsSearchModel::setSourceEntries(const QList<SettingsSearchEntry> &entries)
{
beginResetModel();
sourceEntries = entries;
endResetModel();
rebuildFilter();
}
void SettingsSearchModel::setFilterString(const QString &text)
{
filterActive = !text.trimmed().isEmpty();
if (filterActive) {
filterQuery = text.trimmed();
filterRegex =
QRegularExpression(QRegularExpression::escape(filterQuery), QRegularExpression::CaseInsensitiveOption);
}
rebuildFilter();
}
bool SettingsSearchModel::isFilterActive() const
{
return filterActive;
}
/**
* @brief Calculates a relevance score for a single entry against the query
*
* Scoring priorities (highest to lowest):
* 1. Label starts with query -> 100
* 2. Label contains query -> 80
* 3. Group title starts with query -> 60
* 4. Group title contains query -> 40
* 5. Full text regex match -> 20
* 6. No match -> 0 (excluded from results)
*/
static int relevanceScore(const SettingsSearchEntry &entry, const QString &query, const QRegularExpression &regex)
{
QString lowerQuery = query.toLower();
// Label matches are most relevant
QString label = entry.widgetLabel.toLower();
if (label.startsWith(lowerQuery)) {
return 100;
}
if (label.contains(lowerQuery)) {
return 80;
}
// Group title matches are next
QString group = entry.groupTitle.toLower();
if (group.startsWith(lowerQuery)) {
return 60;
}
if (group.contains(lowerQuery)) {
return 40;
}
// Full text match is least relevant
if (entry.fullSearchText.contains(regex)) {
return 20;
}
return 0;
}
void SettingsSearchModel::rebuildFilter()
{
beginResetModel();
filteredIndices.clear();
if (!filterActive) {
for (int i = 0; i < sourceEntries.size(); ++i) {
filteredIndices.append(i);
}
} else {
QList<QPair<int, int>> scored; // <score, index>
for (int i = 0; i < sourceEntries.size(); ++i) {
const SettingsSearchEntry &entry = sourceEntries[i];
// Skip conditional settings that are currently disabled or hidden
if (entry.widget && (!entry.widget->isEnabled() || entry.widget->isHidden())) {
continue;
}
int score = relevanceScore(entry, filterQuery, filterRegex);
if (score > 0) {
scored.append({-score, i}); // negative for descending sort
}
}
std::sort(scored.begin(), scored.end());
for (const auto &pair : scored) {
filteredIndices.append(pair.second);
}
}
endResetModel();
}
int SettingsSearchModel::rowCount(const QModelIndex &parent) const
{
if (parent.isValid()) {
return 0;
}
return filteredIndices.size();
}
QVariant SettingsSearchModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid() || index.row() >= filteredIndices.size()) {
return {};
}
const SettingsSearchEntry &entry = sourceEntries[filteredIndices[index.row()]];
switch (role) {
case EntryRole:
return QVariant::fromValue(entry);
case Qt::DisplayRole:
return entry.widgetLabel;
case Qt::ToolTipRole:
return QStringLiteral("%1 > %2 > %3")
.arg(QString::number(entry.pageIndex), entry.groupTitle, entry.widgetLabel);
default:
return {};
}
}
SettingsSearchEntry SettingsSearchModel::entryForIndex(const QModelIndex &index) const
{
if (!index.isValid() || index.row() >= filteredIndices.size()) {
return {};
}
return sourceEntries[filteredIndices[index.row()]];
}

View file

@ -0,0 +1,76 @@
/**
* @file settings_search_model.h
* @brief Data model for the settings search feature
* @ingroup Dialogs
*/
#ifndef COCKATRICE_SETTINGS_SEARCH_MODEL_H
#define COCKATRICE_SETTINGS_SEARCH_MODEL_H
#include <QAbstractListModel>
#include <QList>
#include <QRegularExpression>
#include <QWidget>
/**
* @brief Represents a single searchable setting entry
*
* Each settings page provides a list of these entries via getSearchEntries().
* The model uses them for filtering, relevance scoring, and display.
*/
struct SettingsSearchEntry
{
int pageIndex; ///< Index of the settings page this entry belongs to
QString groupTitle; ///< Title of the group/section within the page
QString widgetLabel; ///< Display label for the setting widget
QString fullSearchText; ///< Extended search text (label, control text, tooltip) for full-text matching
QWidget *widget; ///< Pointer to the setting widget for focus/scrolling
};
/**
* @brief List model providing filtered, ranked search results
*
* Manages a list of SettingsSearchEntry items. When a filter string is set,
* entries are scored by relevance and sorted so the best matches appear first.
* Supports custom roles for accessing entry fields from views and delegates.
*/
class SettingsSearchModel : public QAbstractListModel
{
Q_OBJECT
public:
/**
* @brief Custom data role for accessing the full entry
*/
enum Roles
{
EntryRole = Qt::UserRole + 1, ///< Full SettingsSearchEntry object
};
explicit SettingsSearchModel(QObject *parent = nullptr);
/** @brief Replaces the source entries and rebuilds the filter */
void setSourceEntries(const QList<SettingsSearchEntry> &entries);
/** @brief Sets the filter string and recalculates the results */
void setFilterString(const QString &text);
/** @brief Whether a non-empty filter is currently active */
bool isFilterActive() const;
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
/** @brief Returns the full entry for a given model index */
SettingsSearchEntry entryForIndex(const QModelIndex &index) const;
private:
QList<SettingsSearchEntry> sourceEntries; ///< Complete unfiltered entry list
QList<int> filteredIndices; ///< Indices into sourceEntries matching the filter
QRegularExpression filterRegex; ///< Compiled regex for the current filter
QString filterQuery; ///< Current filter query string
bool filterActive = false; ///< Whether filtering is active
/** @brief Recalculates the filtered index list and ranking */
void rebuildFilter();
};
Q_DECLARE_METATYPE(SettingsSearchEntry)
#endif // COCKATRICE_SETTINGS_SEARCH_MODEL_H

View file

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

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